Spring Cloud 与 响应式微服务
响应式微服务
什么是响应式(reactive)?现在响应式(reactive)是一个重载的术语。reactive软件基于它收到的激发作出响应以及适应它的行为。
- 响应式编程——一种以数据流监视、变更响应以及传播为中心的开发模型。
- 响应式系统——一种架构类型,用于构建响应式的健壮的分布式系统,基于异步消息传递。
响应式微服务是响应式微服务系统的构建块。尽管如此,由于它们的异步因素,这些微服务的实现是具有挑战性的。响应式编程降低了这种复杂性。怎么使用?让我们现在就回答这个问题。
响应式编程
响应式编程是一种开发模型,它面向数据流和数据传播。在响应式编程(reactive programming)中,激发(stimuli)是在流中传输的数据,称为streams。
响应式系统
响应式编程(reactive programming)是一种开发模型,而响应式系统(reactive systems)是一种架构类型,用于构建分布式系统(http://www.reactivemanifesto.org/);它是一组原则,用于实现响应式(responsiveness)并构建系统,即使在故障或负载下也能及时响应请求。
要构建这样的系统,响应式系统(reactive systems)采用了消息驱动的方法。所有组件使用异步的消息接收和发送进行交互。为了解耦发送者和接收者,组件向虚拟地址(virtual addresses)发送消息。它们也向虚拟地址(address)注册以接收消息。一个地址(address)是一个目的地标识,如隐性字符串或者URL。相同的地址可以注册数个接收者——传递语义取决于底层技术。发送者不阻塞并等待一个响应。发送者可以稍后接收响应,但是同时,它可以接收和发送其它消息。这种异步特性非常重要并且影响了你的应用程序如何开发。
使用异步消息传递交互为响应式系统提供了两个极重要的特性:
- 弹性(Elasticity)——水平伸缩的能力
- 还原能力(Resilience)——处理故障和恢复的能力
弹性源自消息交互所提供的解耦。发送到一个地址的消息可以使用一种负载均衡策略由一组消费者消费。当响应式系统在负载中面临尖峰时,它可以产生新的消费者实例并随后处理它们。
这种快速恢复的特性是由无阻塞的故障处理能力和组件复制能力所提供的。首先,消息交互允许组件本地处理故障。多亏异步特性,组件不主动等待响应,因此在一个组件中发生故障不影响其它组件。复制也是处理快速恢复的关键能力。当一个处理节点消息失败时,消息可以由注册在相同地址上的其它节点处理。
由于这两个特性,系统变为响应式。它可以适应更高或者更低的负载,并且在高负载或者故障时持续服务请求。在构建高度分布式的微服务系统时以及处理超出调用方控制的服务时,这一原则是基本的。有必要运行服务的数个实例,以平衡负载和处理故障而不破坏可用性。
响应式微服务
构建一个微服务(从而分布式)系统,每个服务可能在任何时候变更、演变、故障、呈现缓慢或者撤回。这样的问题不能影响整个系统的行为。你的系统必须接收变更并且能够处理故障。你可以在降级模式下运行,但你的系统应该仍然能够处理请求。
为了确保这种行为,响应式微服务系统由响应式微服务构成。这些微服务有四个特性:
- 自治
- 异步
- 快速恢复能力
- 弹性
响应式微服务是自治的。它们可以应付它们周围的服务的可用或者不可用。然而,自治与隔离是成对的。响应式微服务可以本地处理故障,独立工作,并且必要时与其它协作。响应式微服务使用异步消息传递与同伴交互。它还接收消息并且拥有对这些消息产生响应的能力。
由于异步消息传递,响应式微服务可以面对故障并相应的调整自己的行为。故障不应被传播,而是接近根源处理。当一个微服务故障,消费者微服务必须处理故障而不要传播它。这种隔离原则是防止故障冒泡并且破坏整个系统的关键特性。快速恢复能力不仅仅是管理故障,还关于自愈。响应式微服务应该实现故障发生时的恢复或补偿策略。
最后,一个响应式微服务必须是弹性的,因此系统可以改变实例的数量来管理负载。这意味着一组约束,如避免内存状态,实例间共享状态如果需要,或者能够将消息路由到有状态服务的相同实例。
Spring Cloud 实现 响应式微服务
服务注册中心
服务注册中心沿用之前的consul
网关
网关沿用之前的gateway-woqu
业务系统 - 价格服务
创建一个标准的 Spring Boot 工程,命名为:business-price-woqu,包含client-business-price-woqu和server-business-price-woqu
最终 business-price-woqu 的 pom.xml 文件如下:
<?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">
<parent>
<artifactId>modules-woqu</artifactId>
<groupId>com.orrin</groupId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>business-price-woqu</artifactId>
<packaging>pom</packaging>
<modules>
<module>client-business-price-woqu</module>
<module>server-business-price-woqu</module>
</modules>
</project>
client-business-price-woqu 的 pom.xml 文件如下:
<?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">
<parent>
<artifactId>business-price-woqu</artifactId>
<groupId>com.orrin</groupId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>client-business-price-woqu</artifactId>
</project>
server-business-price-woqu 的 pom.xml 文件如下:
<?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">
<parent>
<artifactId>business-price-woqu</artifactId>
<groupId>com.orrin</groupId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<packaging>jar</packaging>
<artifactId>server-business-price-woqu</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-config</artifactId>
</dependency>
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-starter-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>
<dependency>
<groupId>org.jolokia</groupId>
<artifactId>jolokia-core</artifactId>
</dependency>
<dependency>
<groupId>com.orrin</groupId>
<artifactId>woqu-cloud-core</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.orrin</groupId>
<artifactId>client-business-price-woqu</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>
</project>
server-business-price-woqu 的配置文件 application.yml
spring:
application:
name: business-price-woqu
boot:
admin:
client:
url: http://woqu.admin:7000
cloud:
consul:
host: woqu.consul
port: 8500
discovery:
#instance-id: ${spring.application.name}:${server.port}
instance-group: ${spring.application.name}
register: true
config:
enabled: true #默认是true
format: YAML # 表示consul上面文件的格式 有四种 YAML PROPERTIES KEY-VALUE FILES
fail-fast: true
watch:
enabled: true
default-context: ${spring.application.name} #指定consul配置的配置文件父路径
# 指定consul配置的文件夹前缀为config
prefix: woqu_configuration/master/common-server-config-respo
data-key: application.yml
zipkin:
base-url: http://zipkin.server:9411/ # 指定了 Zipkin 服务器的地址
sleuth:
sampler:
probability: 1.0
web:
client:
enabled: true
#consul config 路径:prefix defaultContext data-key
server:
port: 9004
feign:
hystrix:
enabled: true
management:
endpoints:
web:
exposure:
include: "*"
exclude: dev
logging:
level:
root: info
com.woqu: debug
hystrix:
command:
default:
execution:
isolation:
strategy: THREAD
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds: 60000
ribbon:
ConnectTimeout: 10000
ReadTimeout: 60000
client-business-price-woqu中创建价格的实体类
package com.woqu.business.price.client;
import java.io.Serializable;
/**
* @author orrin
*/
public class Price implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 原价
*/
private Double originalPrice;
/**
* 最终价格
*/
private Double finalPrice;
//省略getter setter方法
}
创建价格查询Service
import java.math.BigDecimal;
/**
* @author orrin
*/
@Service("priceService")
public class PriceService {
private static final Logger LOGGER = LoggerFactory.getLogger(PriceService.class);
public Mono<Price> getByUserId(String merchandiseId, String userId) {
Price price = new Price();
double finalPrice = RandomUtils.nextDouble() * RandomUtils.nextInt(100);
double originalPrice = RandomUtils.nextDouble() * RandomUtils.nextInt(50) + finalPrice;
BigDecimal finalDecimal = new BigDecimal(finalPrice);
price.setFinalPrice(finalDecimal.setScale(2,BigDecimal.ROUND_HALF_UP).doubleValue());
BigDecimal originalPriceDecimal = new BigDecimal(originalPrice);
price.setOriginalPrice(originalPriceDecimal.setScale(2,BigDecimal.ROUND_HALF_UP).doubleValue());
return Mono.justOrEmpty(price);
}
public Mono<Price> get(String merchandiseId) {
return this.getByUserId(merchandiseId, null);
}
}
创建价格查询restful接口
import com.woqu.business.price.client.Price;
import com.woqu.business.price.server.service.PriceService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Mono;
/**
* @author orrin
*/
@Component
public class PriceWebHandler {
@Autowired
private PriceService priceService;
public Mono<ServerResponse> ready(ServerRequest request) {
return ServerResponse.ok().contentType(MediaType.APPLICATION_JSON_UTF8)
.body(BodyInserters.fromObject(true));
}
public Mono<ServerResponse> getByUserId(ServerRequest request) {
String merchandiseId = request.pathVariable("merchandiseId");
String userId = request.pathVariable("userId");
Mono<Price> result = priceService.getByUserId(merchandiseId, userId);
return ServerResponse.ok().contentType(MediaType.APPLICATION_JSON_UTF8).body(result, Price.class);
}
public Mono<ServerResponse> get(ServerRequest request) {
String merchandiseId = request.pathVariable("merchandiseId");
Mono<Price> result = priceService.get(merchandiseId);
return ServerResponse.ok().contentType(MediaType.APPLICATION_JSON_UTF8).body(result, Price.class);
}
}
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.server.RequestPredicates;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.reactive.function.server.ServerResponse;
/**
* @author orrin
*/
@Configuration
public class PriceWebRouter {
@Bean
public RouterFunction<ServerResponse> route(PriceWebHandler priceWebHandler) {
return RouterFunctions
.route(RequestPredicates.GET("/ready")
.and(RequestPredicates.accept(MediaType.APPLICATION_JSON_UTF8)),
priceWebHandler::ready)
.andRoute(RequestPredicates.GET("/price/{merchandiseId}")
.and(RequestPredicates.accept(MediaType.APPLICATION_JSON_UTF8)),
priceWebHandler::get)
.andRoute(RequestPredicates.GET("/price/{merchandiseId}/{userId}")
.and(RequestPredicates.accept(MediaType.APPLICATION_JSON_UTF8)),
priceWebHandler::getByUserId);
}
}
测试一下运行结果
GET http://127.0.0.1:9004/price/abc
HTTP/1.1 200 OK
transfer-encoding: chunked
Content-Type: application/json;charset=UTF-8
{
"originalPrice": 75.78,
"finalPrice": 33.63
}
Response code: 200 (OK); Time: 46ms; Content length: 42 bytes
业务系统 - 商品服务
创建一个标准的 Spring Boot 工程,命名为:business-merchandise-woqu,包含client-business-merchandise-woqu和server-business-merchandise-woqu
最终 business-merchandise-woqu 的 pom.xml 文件如下:
<?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">
<parent>
<artifactId>modules-woqu</artifactId>
<groupId>com.orrin</groupId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>business-merchandise-woqu</artifactId>
<packaging>pom</packaging>
<modules>
<module>client-business-merchandise-woqu</module>
<module>server-business-merchandise-woqu</module>
</modules>
</project>
client-business-merchandise-woqu 的 pom.xml 文件如下:
<?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">
<parent>
<artifactId>business-merchandise-woqu</artifactId>
<groupId>com.orrin</groupId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>client-business-merchandise-woqu</artifactId>
<dependencies>
<dependency>
<groupId>com.orrin</groupId>
<artifactId>client-business-price-woqu</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>
</project>
server-business-merchandise-woqu 的 pom.xml 文件如下:
<?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">
<parent>
<artifactId>business-merchandise-woqu</artifactId>
<groupId>com.orrin</groupId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<packaging>jar</packaging>
<artifactId>server-business-merchandise-woqu</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-config</artifactId>
</dependency>
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-starter-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>
<dependency>
<groupId>org.jolokia</groupId>
<artifactId>jolokia-core</artifactId>
</dependency>
<dependency>
<groupId>com.orrin</groupId>
<artifactId>woqu-cloud-core</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.orrin</groupId>
<artifactId>client-business-merchandise-woqu</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>
</project>
server-business-merchandise-woqu 的配置文件 application.yml
spring:
application:
name: business-merchandise-woqu
boot:
admin:
client:
url: http://woqu.admin:7000
cloud:
consul:
host: woqu.consul
port: 8500
discovery:
#instance-id: ${spring.application.name}:${server.port}
instance-group: ${spring.application.name}
register: true
config:
enabled: true #默认是true
format: YAML # 表示consul上面文件的格式 有四种 YAML PROPERTIES KEY-VALUE FILES
fail-fast: true
watch:
enabled: true
default-context: ${spring.application.name} #指定consul配置的配置文件父路径
# 指定consul配置的文件夹前缀为config
prefix: woqu_configuration/master/common-server-config-respo
data-key: application.yml
zipkin:
base-url: http://zipkin.server:9411/ # 指定了 Zipkin 服务器的地址
sleuth:
sampler:
probability: 1.0
web:
client:
enabled: true
#consul config 路径:prefix defaultContext data-key
server:
port: 9003
feign:
hystrix:
enabled: true
management:
endpoints:
web:
exposure:
include: "*"
exclude: dev
logging:
level:
root: info
com.woqu: debug
hystrix:
command:
default:
execution:
isolation:
strategy: THREAD
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds: 60000
ribbon:
ConnectTimeout: 10000
ReadTimeout: 60000
server-business-merchandise-woqu 的启动类 BusinessMerchandiseApp.java
package com.woqu.business.merchandise.server;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.hystrix.EnableHystrix;
import org.springframework.context.annotation.ComponentScan;
/**
* @author orrin
*/
@SpringBootApplication
@EnableDiscoveryClient
@EnableHystrix
@ComponentScan(value = "com.woqu")
public class BusinessMerchandiseApp {
public static void main(String[] args) {
SpringApplication.run(BusinessMerchandiseApp.class, args);
}
}
client-business-merchandise-woqu中创建商品的实体类
package com.woqu.business.merchandise.client;
import java.io.Serializable;
/**
* @author orrin
*/
public class Merchandise implements Serializable {
private static final long serialVersionUID = 1L;
/**
* id
*/
private String merchandiseId;
/**
* 名称
*/
private String name;
/**
* 货号
*/
private String itemNum;
/**
* 品牌
*/
private String brand;
private Price price;
//省略getter setter方法
}
首先创建一个 Java Config,这里我们不再使用RestTemplate 来调用服务,而是WebClient。这个配置看起来和注册RestTemplate时差不多,但是要注意这里注册的 Bean 是WebClient.Builder。
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.client.WebClient;
/**
* @author orrin
*/
@Configuration
public class WebClientConfig {
@Bean
@LoadBalanced
public WebClient.Builder loadBalancedWebClientBuilder() {
return WebClient.builder();
}
}
或者
@Configuration
public class WebClientConfig {
@Autowired
private LoadBalancerExchangeFilterFunction lbFunction;
@Bean
public WebClient.Builder doOtherStuff() {
return WebClient.builder().filter(lbFunction);
}
}
查询商品及价格的Service
import com.woqu.business.merchandise.client.Merchandise;
import com.woqu.business.price.client.Price;
import com.woqu.cloud.core.constant.AppWebClientBaseURL;
import org.apache.commons.lang.RandomStringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;
/**
* @author orrin
*/
@Service("merchandiseService")
public class MerchandiseService {
private static final Logger LOGGER = LoggerFactory.getLogger(MerchandiseService.class);
@Autowired
private WebClient.Builder webClientBuilder;
public Mono<Merchandise> get(String merchandiseId) {
Merchandise merchandise = new Merchandise();
merchandise.setMerchandiseId(merchandiseId);
merchandise.setName(RandomStringUtils.randomAscii(10));
merchandise.setBrand(RandomStringUtils.randomAscii(10));
merchandise.setItemNum(RandomStringUtils.randomNumeric(10));
Mono<Price> monoPrice = this.price(merchandiseId);
Mono<Merchandise> merchandiseMono = monoPrice.flatMap(t -> {
merchandise.setPrice(t);
return Mono.justOrEmpty(merchandise);
});
return merchandiseMono;
}
public Mono<Price> price(String merchandiseId) {
Mono<Price> price = webClientBuilder.baseUrl(AppWebClientBaseURL.BUSINESS_PRICE_WOQU).build()
.get().uri("/price/" + merchandiseId)
.retrieve()
.bodyToMono(Price.class);
return price;
}
}
查询的restful endpoint
import com.woqu.business.merchandise.client.Merchandise;
import com.woqu.business.merchandise.server.service.MerchandiseService;
import com.woqu.business.price.client.Price;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Mono;
/**
* @author orrin
*/
@Component
public class MerchandiseWebHandler {
@Autowired
private MerchandiseService merchandiseService;
public Mono<ServerResponse> ready(ServerRequest request) {
return ServerResponse.ok().contentType(MediaType.APPLICATION_JSON_UTF8)
.body(BodyInserters.fromObject(true));
}
public Mono<ServerResponse> get(ServerRequest request) {
String merchandiseId = request.pathVariable("merchandiseId");
Mono<Merchandise> result = merchandiseService.get(merchandiseId);
return ServerResponse.ok().contentType(MediaType.APPLICATION_JSON_UTF8).body(result, Merchandise.class);
}
public Mono<ServerResponse> price(ServerRequest request) {
String merchandiseId = request.pathVariable("merchandiseId");
Mono<Price> result = merchandiseService.price(merchandiseId);
return ServerResponse.ok().contentType(MediaType.APPLICATION_JSON_UTF8).body(result, Price.class);
}
}
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.server.RequestPredicates;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.reactive.function.server.ServerResponse;
/**
* @author orrin
*/
@Configuration
public class MerchandiseWebRouter {
@Bean
public RouterFunction<ServerResponse> route(MerchandiseWebHandler merchandiseWebHandler) {
return RouterFunctions
.route(RequestPredicates.GET("/ready")
.and(RequestPredicates.accept(MediaType.APPLICATION_JSON_UTF8)),
merchandiseWebHandler::ready)
.andRoute(RequestPredicates.GET("/merchandise/{merchandiseId}")
.and(RequestPredicates.accept(MediaType.APPLICATION_JSON_UTF8)),
merchandiseWebHandler::get)
.andRoute(RequestPredicates.GET("/merchandise/{merchandiseId}/price")
.and(RequestPredicates.accept(MediaType.APPLICATION_JSON_UTF8)),
merchandiseWebHandler::price);
}
}
网关配置
在 gateway-woqu 的配置文件 application.yml 中增加以下配置:
spring:
cloud:
gateway:
routes:
- id: business-merchandise-woqu
uri: lb://business-merchandise-woqu
order: 10002
predicates:
- Path=/api/merchandise/**
filters:
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 1 # 令牌桶的容积
redis-rate-limiter.burstCapacity: 5 # 流速 每秒
key-resolver: "#{@remoteAddrKeyResolver}" #SPEL表达式去的对应的bean
- StripPrefix=2
- id: business-price-woqu
uri: lb://business-merchandise-woqu
order: 10002
predicates:
- Path=/api/price/**
filters:
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 1 # 令牌桶的容积
redis-rate-limiter.burstCapacity: 5 # 流速 每秒
key-resolver: "#{@remoteAddrKeyResolver}" #SPEL表达式去的对应的bean
- StripPrefix=2
测试验证
启动各应用,请求endpoint验证
GET http://127.0.0.1:7001/api/merchandise/merchandise/abc
HTTP/1.1 200 OK
transfer-encoding: chunked
X-RateLimit-Remaining: 4
X-RateLimit-Burst-Capacity: 5
X-RateLimit-Replenish-Rate: 1
Content-Type: application/json;charset=UTF-8
{
"merchandiseId": "abc",
"name": "'z7X&j5?+i",
"itemNum": "7897859440",
"brand": "#J!:KIN:FD",
"price": {
"originalPrice": 12.13,
"finalPrice": 0.35
}
}
Response code: 200 (OK); Time: 369ms; Content length: 137 bytes