Spring Cloud Gateway是Spring官方推出的第二代网关框架。在微服务系统中有着非常重要的作用,网关常见的功能有路由转发、权限校验、限流控制等作用。Spring Cloud Gateway作为Spring Cloud生态系统中的网关,目标是替代Netflix Zuul,其不仅提供统一的路由方式,并且还基于Filer链的方式提供了网关基本的功能,例如:安全、监控、限流等。
在微服务中网关通常提供API全托管服务,丰富的API管理功能,辅助企业管理大规模的API,以降低管理成本和安全风险,包括协议适配、协议转发、安全策略、防刷、流量、监控日志等功能。一般来说网关对外暴露的URL或者接口信息,我们统称为路由信息。下面介绍一下Spring Cloud Gateway中几个重要的概念。
路由是网关最基础的部分,路由信息有一个ID、一个目的URL、一组断言和一组Filter组成。如果断言路由为真,则说明请求的URL和配置匹配
Java8中的断言函数。Spring Cloud Gateway中的断言函数输入类型是Spring5.0框架中的ServerWebExchange。Spring Cloud Gateway中的断言函数允许开发者去定义匹配来自于http request中的任何信息,比如请求头和参数等。
一个标准的Spring webFilter。Spring cloud gateway中的filter分为两种类型的Filter,分别是Gateway Filter和Global Filter。过滤器Filter将会对请求和响应进行修改处理
我们通过IDEA开发工具内置的SpringInitializr来创建测试工程步骤如下
在SpringCloud Routing选择项中选择Gateway点击下一步完成创建。
Idea开发工具强大就强大在它可以根据我们的选择自动生成我们需要的代码框架、配置文件、核心代码等,非常方便快捷。
com.github.vladimir-bukhtoyarov
bucket4j-core
4.0.0
org.springframework.cloud
spring-cloud-starter-gateway
org.springframework.cloud
spring-cloud-starter-netflix-hystrix
#访问端口
server:
port: 8767
#服务名称
spring:
application:
name: routing.getway.web
#安全认证信息
security:
user:
name: admin
password: 123456
cloud:
gateway:
routes:
- id: api_a_route
#uri: http://localhost:8761
uri: lb://EUREKA.CLIENT.FIRST.WEBAPI
predicates:
- Path=/api/a/**
filters:
- StripPrefix=2
- name: Hystrix
args:
name: myerror
fallbackUri: forward:/error
#服务注册
eureka:
server:
register: localhost:8760
client:
serviceUrl:
defaultZone: http://${spring.security.user.name}:${spring.security.user.password}@${eureka.server.register}/eureka/
本教程所有实体代码都是基于系列教程之前的代码,所以运行本例需要同时运行如下工程。
SpringCloud-Getway 中Filters 的作用是很大的,我们可以用Filter实现的功能很多,比如熔断器、各类验证、日志记录、接口封装和网络限流等。
SpringCloud-Getway网关在应用中集成了众多的接口,在日常应用中接口与接口互相调用完成各自的业务单元,每个接口代表的业务逻辑都保持相对独立,所有的接口集成统一管理形成网关。其中任何接口都存在故障的风险,当一个重要接口发生故障必将影响依赖他的其它接口的正常使用,为了最大限度降低故障接口引起的危害,在网关层面统一提供熔断器策略,在遇到接口故障的时候网关会启用熔断器,对故障接口进行隔离,并为其它依赖接口继续提供默认接返回值。
如下配置
routes:
- id: api_a_route
#uri: http://localhost:8761
uri: lb://EUREKA.CLIENT.FIRST.WEBAPI
predicates:
- Path=/api/a/**
filters:
- StripPrefix=2
- name: Hystrix
args:
name: myerror
fallbackUri: forward:/error
创建接口熔断器响应接口
@RestController
public class HystrixApi {
private SimpleDateFormat dateFormat=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
@RequestMapping("/error")
public Mono<String> fallback() {
StringBuilder builder=new StringBuilder();
String strTime=dateFormat.format(new Date());
builder.append(strTime);
builder.append(" > ");
builder.append("Please try again later for a program exception");
return Mono.just(builder.toString());
}
}
启动如下工程
@Component
public class TokenFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
System.out.println();
System.out.println("自定义全局网关过滤器TokenFilter之token验证");
// String token = exchange.getRequest().getHeaders().getFirst("token");
String token = exchange.getRequest().getQueryParams().getFirst("token");
if (token == null || token.isEmpty()) {
System.out.println( "token is empty..." );
ServerHttpResponse response = exchange.getResponse();
Object message =JSON.toJSON(BaseData.result(401,"鉴权失败","鉴权失败"));
byte[] bits = message.toString().getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = response.bufferFactory().wrap(bits);
response.setStatusCode(HttpStatus.UNAUTHORIZED);
//指定编码,否则在浏览器中会中文乱码
// response.getHeaders().add("Content-Type", "text/plain;charset=UTF-8");
response.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
return response.writeWith(Mono.just(buffer));
}
return chain.filter(exchange);
}
/**
* @过滤器设定优先级别的,值越大则优先级越低
*/
@Override
public int getOrder() {
return 1;
}
}
/**
* 登录验证
*/
@Component
public class AuthFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String user = exchange.getRequest().getQueryParams().getFirst("user");
System.out.println();
System.out.println("登录验证");
//重定向(redirect)到登录页面
if (StringUtils.isBlank(user)) {
String url = "http://localhost:8760";
ServerHttpResponse response = exchange.getResponse();
//303状态码表示由于请求对应的资源存在着另一个URI,应使用GET方法定向获取请求的资源
response.setStatusCode(HttpStatus.SEE_OTHER);
response.getHeaders().set(HttpHeaders.LOCATION, url);
/*
Object message = JSON.toJSON(BaseData.result(401,"用户未登录","用户未登录"));
byte[] bits = message.toString().getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = response.bufferFactory().wrap(bits);
response.writeWith(Mono.just(buffer));
*/
return response.setComplete();
}
return chain.filter(exchange);
}
@Override
public int getOrder() {
return 2;
}
}
/**
* 全局网关路由过滤器
* 系统日志
*/
@Component
public class LogsFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange e, GatewayFilterChain c) {
System.out.println();
System.out.println("自定义全局网关过滤器LogsFilter记录系统日志");
System.out.println("请求地址:"+e.getRequest().getPath());
return c.filter(e);
}
/**
* @过滤器设定优先级别的,值越大则优先级越低
*/
@Override
public int getOrder() {
return 0;
}
}
/**
* 接口返回值统一封装
*/
@Component
public class DataFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
System.out.println("自定义全局网关过滤器DataFilter");
ResDataDecorator resDataDecorator=new ResDataDecorator(exchange.getResponse());
ServerWebExchange webex=exchange.mutate().response(resDataDecorator).build();
return chain.filter(webex);
}
@Override
public int getOrder() {
return -100;
}
}
在互联网应用中,特别是电商,高并发的场景非常多,比如:秒杀、抢购、双11等,在开始时间点会使流量爆发式地涌入,如果对网络流量不加控制很有可能造成后台实例资源耗尽。限流是指通过指定的策略削减流量,使到达后台实例的请求在合理范围内。本章将介绍spring cloud gateway如何实现限流。
限流算法
主流的限流算法有两种:漏桶(leaky bucket)和令牌桶(token bucket)。漏桶算法 有一个固定容量的桶,对于流入的水无法预计速率,流出的水以固定速率,当水满之后会溢出。
令牌桶算法,有一个固定容量的桶,桶里存放着令牌(token)。桶最开始是空的,token以一个固定速率向桶中填充,直到达到桶的容量,多余的token会被丢弃。每当一个请求过来时,都先去桶里取一个token,如果没有token的话请求无法通过。
两种算法的最主要区别是令牌桶算法允许一定流量的突发,因为令牌桶算法中取走token是不需要时间的,即桶内有多少个token都可以瞬时拿走。基于这个特点令牌桶算法在互联网企业中应用比较广泛,我们在实现限流的时候也会基于这个算法。
/**
* @全局限流
*/
@Component
public class GloLimitFilter implements GlobalFilter, Ordered {
/**
* @令牌桶总数
*/
private int capacity = 2;
/**
* @生产令牌桶数量
*/
private int refillTokens = 2;
/**
* @生产令牌桶的时间间隔
*/
private Duration refillDuration = Duration.ofSeconds(1);
public GloLimitFilter() {
}
/**
* @param capacity 令牌桶总数
* @param refillTokens 生产令牌桶数量
* @param refillDuration 生产令牌桶的时间间隔
*/
public GloLimitFilter(int capacity, int refillTokens, Duration refillDuration) {
this.capacity = capacity;
this.refillTokens = refillTokens;
this.refillDuration = refillDuration;
}
private static final Map<String, Bucket> CACHE = new ConcurrentHashMap<>();
private Bucket createNewBucket() {
Refill refill = Refill.of(refillTokens, refillDuration);
Bandwidth limit = Bandwidth.classic(capacity, refill);
return Bucket4j.builder().addLimit(limit).build();
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpResponse response = exchange.getResponse();
String ip = exchange.getRequest().getRemoteAddress().getAddress().getHostAddress();
Bucket bucket = CACHE.computeIfAbsent(ip, k -> createNewBucket());
System.out.println("IP: " + ip + ", available tokens :" + bucket.getAvailableTokens());
if (bucket.tryConsume(1L)) {
return chain.filter(exchange);
}
BaseData bdata = BaseData.result(401, "全局限流", "访问太频繁了,请稍后。。。");
System.out.println("IP: " + ip + ", available tokens :" + bucket.getAvailableTokens() + " too many requests");
Object message = JSON.toJSON(bdata);
byte[] bits = message.toString().getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = response.bufferFactory().wrap(bits);
response.setStatusCode(HttpStatus.TOO_MANY_REQUESTS);
//指定编码,否则在浏览器中会中文乱码
response.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
return response.writeWith(Mono.just(buffer));
}
@Override
public int getOrder() {
return 0;
}
}
/**
* @统一异常处理
*/
public class JsonExceptionHandler extends DefaultErrorWebExceptionHandler {
public JsonExceptionHandler(ErrorAttributes errorAttributes, ResourceProperties resourceProperties,
ErrorProperties errorProperties, ApplicationContext applicationContext) {
super(errorAttributes, resourceProperties, errorProperties, applicationContext);
}
/**
* 获取异常属性
*/
@Override
protected Map<String, Object> getErrorAttributes(ServerRequest request, boolean includeStackTrace) {
int code = 500;
Throwable error = super.getError(request);
if (error instanceof org.springframework.cloud.gateway.support.NotFoundException) {
code = 404;
}
return response(code, this.buildMessage(request, error));
}
/**
* 指定响应处理方法为JSON处理的方法
*
* @param errorAttributes
*/
@Override
protected RouterFunction<ServerResponse> getRoutingFunction(ErrorAttributes errorAttributes) {
//Html返回值
// return RouterFunctions.route(acceptsTextHtml(), this::renderErrorView).andRoute(RequestPredicates.all(), this::renderErrorResponse);
//Json返回值
return RouterFunctions.route(RequestPredicates.all(), this::renderErrorResponse);
}
/**
* 根据code获取对应的HttpStatus
*/
@Override
protected int getHttpStatus(Map<String, Object> errorAttributes) {
return (int) errorAttributes.get("status");
}
/**
* 构建异常信息
*
* @param request
* @param ex
* @return
*/
private String buildMessage(ServerRequest request, Throwable ex) {
StringBuilder message = new StringBuilder("Failed to handle request [");
message.append(request.methodName());
message.append(" ");
message.append(request.uri());
message.append("]");
if (ex != null) {
message.append(": ");
message.append(ex.getMessage());
}
return message.toString();
}
/**
* 构建返回的JSON数据格式
*
* @param status 状态码
* @param errorMessage 异常信息
* @return
*/
public static Map<String, Object> response(int status, String errorMessage) {
Map<String, Object> map = null;
try {
map = BaseData.result(status, errorMessage, null).toMap();
} catch (Exception ex) {
ex.printStackTrace();
}
return map;
}
}
@Configuration
@EnableConfigurationProperties({ ServerProperties.class, ResourceProperties.class })
public class ErrorHandlerConfiguration {
private final ServerProperties serverProperties;
private final ApplicationContext applicationContext;
private final ResourceProperties resourceProperties;
private final List<ViewResolver> viewResolvers;
private final ServerCodecConfigurer serverCodecConfigurer;
public ErrorHandlerConfiguration(ServerProperties serverProperties, ResourceProperties resourceProperties,
ObjectProvider<List<ViewResolver>> viewResolversProvider, ServerCodecConfigurer serverCodecConfigurer,
ApplicationContext applicationContext) {
this.serverProperties = serverProperties;
this.applicationContext = applicationContext;
this.resourceProperties = resourceProperties;
this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList);
this.serverCodecConfigurer = serverCodecConfigurer;
}
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public ErrorWebExceptionHandler errorWebExceptionHandler(ErrorAttributes errorAttributes) {
JsonExceptionHandler exceptionHandler = new JsonExceptionHandler(errorAttributes,
this.resourceProperties,this.serverProperties.getError(), this.applicationContext);
exceptionHandler.setViewResolvers(this.viewResolvers);
exceptionHandler.setMessageWriters(this.serverCodecConfigurer.getWriters());
exceptionHandler.setMessageReaders(this.serverCodecConfigurer.getReaders());
return exceptionHandler;
}
}
至此,SpringCloud-Gatway网关在实际开过程中可能用的关键知识点及用法就全部介绍了,本教程的代码都是点到为止,大家可以根据实际项目中的应用自行深化研发。
至此关于SpringCloud-Zuul常用路由网关就介绍完了。
上一篇 SpringCloud-Zuul常用路由网关
SpringCloud终极教程之核心讲解
源代码地址:https://github.com/crexlb/cre.springcloud.examples