一 概述
想讓post請求中相同參數走特定的服務器(例如age=25只能走服務a).post\get請求走自定義策略,get\delete請求走輪訓策略.
二 自定義負載均衡實例
主要步驟:
a 自定義全局過濾器CacheBodyGlobalFilter,把body中的數據緩存起來,此過濾器優先級較高,負責自定義負載均衡策略時拿不到post請求中body的數據
b 路由規則接口 ICustomRule
c 路由規則實現類CustomChooseRule
d 自定義負載均衡CustomLoadBalancerFilter此優先級要比CacheBodyGlobalFilter低,負責先經過它,body數據沒緩存
e 解析body的工具類 FilterRequestResponseUtil
以People中的age內容路由(對psot put請求相同age只能通過相同服務,get\delete請求走自定義輪訓策略和框架一樣)
1 cloud-parent cloud-providea cloud-provideb的創建
(1)父工程pom文件(可能包含無用依賴) \及服務A B的創建

<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.dp</groupId> <artifactId>cloud-parent</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>pom</packaging> <properties> <java.version>1.8</java.version> <spring-boot.version>2.1.6.RELEASE</spring-boot.version> <spring-cloud.version>Greenwich.SR2</spring-cloud.version> <nacos.version>0.2.2.RELEASE</nacos.version> </properties> <dependencies> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> <version>2.9.2</version> </dependency> <dependency> <groupId>cn.miludeer</groupId> <artifactId>jsoncode</artifactId> <version>1.2.4</version> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger-ui</artifactId> <version>2.9.2</version> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.3.1.tmp</version> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus</artifactId> <version>3.3.1.tmp</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.0.20</version> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring-cloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>${spring-boot.version}</version> <type>pom</type> <scope>import</scope> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId> <version>${nacos.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <modules> <module>cloud-providea</module> <module>cloud-getway</module> <module>cloud-provideb</module> </modules> </project>
(2)服務A服務B目錄結構(共用實體類沒提出來,都單獨創建了,懶得再創建common服務)
以創建服務A為列(服務B和A一樣,只不過端口不一樣)
Controller層代碼如下:
import org.cloud.provide.entity.People; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/hello") public class HelloController { @PostMapping public String helloPost(@RequestBody People people) { System.out.println("a=="+people.getName()); return "服務A hello"; } @PutMapping @RequestMapping(method=RequestMethod.PUT,value = "{age}") public String helloPut(@PathVariable int age) { return "服務A put"+age; } @GetMapping public String helloGet(String age) { return "服務A get"+age; } }
實體類Peopler如下:
@Data @AllArgsConstructor @NoArgsConstructor public class People { private String name; private Integer age; }
yml配置文件(數據庫連接用不到沒刪除掉)
server: port: 7200 #服務B是7300 spring: application: name: nacos-provide cloud: nacos: discovery: server-addr: localhost:8848 datasource: username: root password: 123 url: jdbc:mysql://localhost:3306/cloud driver-class-name: com.mysql.jdbc.Driver type: com.alibaba.druid.pool.DruidDataSource mybatis-plus: configuration : map-underscore-to-camel-case : true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl mapper-locations: classpath:mapper/*.xml
服務B同上,只不過yml配置文件端口是7300
2 網關服務cloud-gateway的創建
(1) pom yml文件如下
pom.xml

<?xml version="1.0"?> <project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>com.dp</groupId> <artifactId>cloud-parent</artifactId> <version>0.0.1-SNAPSHOT</version> </parent> <groupId>com.dp</groupId> <artifactId>cloud-getway</artifactId> <version>0.0.1-SNAPSHOT</version> <name>cloud-getway</name> <url>http://maven.apache.org</url> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>3.8.1</version> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> <exclusions> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </exclusion> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webflux</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> <source>1.8</source> <target>1.8</target> </configuration> </plugin> </plugins> </build> </project>
yml

spring: application: name: getway cloud: #配置SpringCloudGateway的路由 gateway: discovery: locator: enabled: true lower-case-service-id: true # 服務名小寫 routes: - id: nacos-provide #路由的ID,沒有固定規則但求唯一,建議配合服務名 uri: lb://nacos-provide # 匹配后提供服務的路由地址,lb代表從注冊中心獲取服務,且以負載均衡方式轉發 predicates: - Path=/nacos-provide/** #斷言,路徑相匹配的進行路由 nacos: discovery: server-addr: localhost:8848 datasource: username: root password: 123 url: jdbc:mysql://localhost:3306/cloud driver-class-name: com.mysql.jdbc.Driver type: com.alibaba.druid.pool.DruidDataSource mybatis-plus: configuration : map-underscore-to-camel-case : true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl mapper-locations: classpath:mapper/*.xml server: port: 9000 #原來是http的8080
(2)路由規則接口ICustomRule
/* * 路由規則 */ public interface ICustomRule { ServiceInstance choose(ServerWebExchange exchange, DiscoveryClient discoveryClient); }
(3)路由規則實現類
import java.net.URI; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.discovery.DiscoveryClient; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.web.server.ServerWebExchange; import com.alibaba.fastjson.JSONObject; import reactor.core.publisher.Flux; import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR; public class CustomChooseRule implements ICustomRule { private AtomicInteger atomicInteger = new AtomicInteger(0); @Override public ServiceInstance choose(ServerWebExchange exchange, DiscoveryClient discoveryClient) { URI originalUrl = exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR); String instancesId = originalUrl.getHost(); Flux<DataBuffer> dataBufferFlux = exchange.getRequest().getBody(); //獲取body中的數據 String body = FilterRequestResponseUtil.resolveBodyFromRequest(dataBufferFlux); //所有服務數據 List<ServiceInstance> instances = discoveryClient.getInstances(instancesId); int index=0; //攔截nacos-provide服務也可攔截服務中特定url if(instancesId.equals("nacos-provide")){ /* * 根據age中的數據訪問特定的服務,例如age=25 只能訪問服務a */ if("POST"==exchange.getRequest().getMethodValue()) { JSONObject json = JSONObject.parseObject(new String(body)); index =json.getString("age").hashCode() % instances.size(); }else if("PUT"==exchange.getRequest().getMethodValue()) { String str=originalUrl.toString(); String json=str.substring(str.lastIndexOf("/")+1, str.length()); index=json.hashCode()% instances.size(); }else { /* * GET Delete請求采用輪訓算法 */ index = this.getAndIncrement() % instances.size(); } }else { /* * 別的服務采用輪訓 */ index = this.getAndIncrement() % instances.size(); } return instances.get(index); } /** * 計算得到當前調用次數 * @return */ public final int getAndIncrement(){ int current; int next; do { current = atomicInteger.get(); next = current >= Integer.MAX_VALUE ? 0 : current+1; }while (!atomicInteger.compareAndSet(current,next)); return next; } }
(3)CacheBodyGlobalFilter自定義全局過濾器把body中的數據緩存起來,負責拿不動body中的數據
order設置的是Ordered.HIGHEST_PRECEDENCE,即最高優先級的過濾器。優先級設置這么高的原因是某些系統內置的過濾器可能也會去讀body,這樣就會導致我們自定義過濾器中獲取body的時候報body只能讀取一次這樣的錯誤
import lombok.extern.slf4j.Slf4j; import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.GlobalFilter; import org.springframework.core.Ordered; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferUtils; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpRequestDecorator; import org.springframework.stereotype.Component; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; /** * @Description 把body中的數據緩存起來 */ @Slf4j @Component public class CacheBodyGlobalFilter implements Ordered, GlobalFilter { @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { if (exchange.getRequest().getHeaders().getContentType() == null) { return chain.filter(exchange); } else { return DataBufferUtils.join(exchange.getRequest().getBody()) .flatMap(dataBuffer -> { DataBufferUtils.retain(dataBuffer); Flux<DataBuffer> cachedFlux = Flux .defer(() -> Flux.just(dataBuffer.slice(0, dataBuffer.readableByteCount()))); ServerHttpRequest mutatedRequest = new ServerHttpRequestDecorator( exchange.getRequest()) { @Override public Flux<DataBuffer> getBody() { return cachedFlux; } }; //exchange.getAttributes().put(CACHE_REQUEST_BODY_OBJECT_KEY, cachedFlux); return chain.filter(exchange.mutate().request(mutatedRequest).build()); }); } } @Override public int getOrder() { return Ordered.HIGHEST_PRECEDENCE; } }
(4)自定義負責均衡策略
import java.util.ArrayList; import java.util.Iterator; import java.util.List; import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.discovery.DiscoveryClient; import org.springframework.cloud.client.loadbalancer.LoadBalancerClient; import org.springframework.cloud.gateway.config.LoadBalancerProperties; import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.LoadBalancerClientFilter; import org.springframework.core.io.buffer.DataBufferUtils; import org.springframework.util.CollectionUtils; import org.springframework.web.server.ServerWebExchange; import org.springframework.core.Ordered; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferUtils; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpRequestDecorator; import org.springframework.stereotype.Component; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR; import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_SCHEME_PREFIX_ATTR; /** * @Description 自定義負載均衡 **/ public class CustomLoadBalancerFilter extends LoadBalancerClientFilter { private final DiscoveryClient discoveryClient; private final List<ICustomRule> chooseRules; public CustomLoadBalancerFilter(LoadBalancerClient loadBalancer, LoadBalancerProperties properties, DiscoveryClient discoveryClient) { super(loadBalancer, properties); this.discoveryClient = discoveryClient; this.chooseRules = new ArrayList<>(); chooseRules.add(new CustomChooseRule()); } protected ServiceInstance choose(ServerWebExchange exchange) { if(!CollectionUtils.isEmpty(chooseRules)){ Iterator<ICustomRule> iChooseRuleIterator = chooseRules.iterator(); while (iChooseRuleIterator.hasNext()){ ICustomRule chooseRule = iChooseRuleIterator.next(); ServiceInstance choose = chooseRule.choose(exchange,discoveryClient); if(choose != null){ return choose; } } } return loadBalancer.choose( ((ServiceInstance) exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR)).getHost()); } /* * 降低優先級/要比CacheBodyGlobalFilter優先級低,要不然緩存不了body數據 */ public int getOrder() { return Ordered.LOWEST_PRECEDENCE; } }
(5)解析body數據的工具類
import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferUtils; import reactor.core.publisher.Flux; import java.nio.CharBuffer; import java.nio.charset.StandardCharsets; import java.util.concurrent.atomic.AtomicReference; import java.util.regex.Matcher; import java.util.regex.Pattern; public final class FilterRequestResponseUtil { /** * spring cloud gateway 獲取post請求的body體 * @param body * @return */ public static String resolveBodyFromRequest( Flux<DataBuffer> body){ AtomicReference<String> bodyRef = new AtomicReference<>(); // 緩存讀取的request body信息 body.subscribe(dataBuffer -> { CharBuffer charBuffer = StandardCharsets.UTF_8.decode(dataBuffer.asByteBuffer()); DataBufferUtils.release(dataBuffer); bodyRef.set(charBuffer.toString()); }); //獲取request body return bodyRef.get(); } /** * 讀取body內容 * @param body * @return */ public static String resolveBodyFromRequest2( Flux<DataBuffer> body){ StringBuilder sb = new StringBuilder(); body.subscribe(buffer -> { byte[] bytes = new byte[buffer.readableByteCount()]; buffer.read(bytes); DataBufferUtils.release(buffer); String bodyString = new String(bytes, StandardCharsets.UTF_8); sb.append(bodyString); }); return formatStr(sb.toString()); } /** * 去掉空格,換行和制表符 * @param str * @return */ private static String formatStr(String str){ if (str != null && str.length() > 0) { Pattern p = Pattern.compile("\\s*|\t|\r|\n"); Matcher m = p.matcher(str); return m.replaceAll(""); } return str; } }
(6)把自定義類啟動時加載到spring容器
import org.springframework.cloud.client.discovery.DiscoveryClient; import org.springframework.cloud.client.loadbalancer.LoadBalancerClient; import org.springframework.cloud.gateway.config.LoadBalancerProperties; import org.springframework.cloud.gateway.filter.GlobalFilter; import org.springframework.cloud.gateway.filter.LoadBalancerClientFilter; import org.springframework.cloud.gateway.route.RouteLocator; import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; import org.springframework.http.HttpRequest; import org.springframework.http.HttpStatus; import reactor.core.publisher.Mono; @Configuration public class GetawayConfig { @Bean public LoadBalancerClientFilter loadBalancerClientFilter(LoadBalancerClient client, LoadBalancerProperties properties, DiscoveryClient discoveryClient) { return new CustomLoadBalancerFilter(client, properties,discoveryClient); } }
yml文件:

spring: application: name: getway cloud: #配置SpringCloudGateway的路由 gateway: discovery: locator: enabled: true lower-case-service-id: true # 服務名小寫 routes: - id: nacos-provide #路由的ID,沒有固定規則但求唯一,建議配合服務名 uri: lb://nacos-provide # 匹配后提供服務的路由地址,lb代表從注冊中心獲取服務,且以負載均衡方式轉發 predicates: - Path=/nacos-provide/** #斷言,路徑相匹配的進行路由 nacos: discovery: server-addr: localhost:8848 datasource: username: root password: 123 url: jdbc:mysql://localhost:3306/cloud driver-class-name: com.mysql.jdbc.Driver type: com.alibaba.druid.pool.DruidDataSource mybatis-plus: configuration : map-underscore-to-camel-case : true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl mapper-locations: classpath:mapper/*.xml server: port: 9000 #原來是http的8080
3 簡單測試
(1)post請求如下(age相同請求多少次都是走服務A put請求同post請求):
(2) get請求測試如下(走輪訓策略,每次請求服務不一樣)
1第一次請求
2 第二次請求
代碼路徑: https://github.com/kangchangchang/gateway.git