网关可以做这些功能:参数校验、权限校验、流量监控、日志输出、协议转换、响应内容、响应头的修改,处理请求时发生错误时的逻辑等
一. 创建两种网关
1. 创建zuul网关
右键点击父项目,点击new,选择module),再选择maven,然后点击next
出现:
填写artifactid,然后点击next,最后点击finish
2. 引入依赖及配置
2.1 引入pom依赖
<?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>study-springcloudframework</artifactId>
<groupId>com.lzr</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>study-zuul-gateway</artifactId>
<name>zuul网关</name>
<description>zuul网关</description>
<properties>
<!-- 其实应该放入父pom中,统一版本 -->
<swagger-api-version>1.9.0.RELEASE</swagger-api-version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.spring4all</groupId>
<artifactId>swagger-spring-boot-starter</artifactId>
<version>${swagger-api-version}</version>
<exclusions>
<exclusion>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>20.0</version>
</dependency>
<!-- 限流 -->
<dependency>
<groupId>com.marcosbarbero.cloud</groupId>
<artifactId>spring-cloud-zuul-ratelimit</artifactId>
<version>2.2.2.RELEASE</version>
</dependency>
<!-- redis配合限流 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
</dependencies>
</project>
2.2 application.yml文件配置
## 网关服务器端口
server:
port: 81
## eureka
eureka:
instance:
##eureka主机名,会在控制页面中显示
hostname: localhost
prefer-ip-address: true
## 心跳检测,检测与持续时间
## 发送心跳间隔时间 单位秒 (测试的时候设置小一点,让eureka服务端及时踢出该服务)
lease-renewal-interval-in-seconds: 10
## eureka服务端收到最后一次心跳后等待时间上限 单位秒 (告诉eureka服务端按照规则执行)
lease-expiration-duration-in-seconds: 20
##eureka服务器页面中status的请求路径(查看swagger页面的信息)
status-page-url: http://${eureka.instance.hostname}:${server.port}/swagger-ui.html
client:
serviceUrl:
defaultZone: http://lzr:123456@localhost:1111/eureka/
## 服务名
spring:
application:
name: gateway
redis:
host: 127.0.0.1
password: 123456789
port: 6379
timeout: 20000
database: 9
## zuul网关配置
zuul:
routes:
api-a:
## 以这个/consumer/ 开头,则转发到 消费者 服务器上
path: /consumer/**
serviceId: study-service-consumer
## 限流配置
ratelimit:
## 对应用来标识请求的key的前缀
key-prefix: gateway
## 开启
enabled: true
## 存储方式(存进redis中)
repository: redis
## 与X-Forwarded-For(简称XFF头,它代表客户端,也就是HTTP的请求端真实的IP)合用,
## 如果不设置为true 或者 XFF请求头不存在(客户端不发送),当使用origin规则的时候,redis中存储的条件统计使用的是本服务器ip地址
behind-proxy: true
policy-list:
## 注意这里的名称要与routes下的名称对应
api-a:
## 每(refresh-interval)秒允许多少个请求
- limit: 20
## 每个用户(也可以是路径,用户+路径+ip等,根据type来判断)对应的请求总时间的限制(秒)
## (就如:quota=10 时,当前用户一次请求花费1秒,那么该用户还能请求的总时间是9(10-1)秒)
quota: 1000
## 刷新时间
refresh-interval: 60
## 根据条件统计(路径)
type:
- user
- origin
- url
# ## 默认配置(这个配置在policy-list中不存在时处理)
# default-policy-list:
# - limit: 2
# quota: 1000
# refresh-interval: 60
# type:
# - url
## 日志配置文件
logging:
config: classpath:logback.xml
2.3 logback日志文件配置
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="60 seconds" debug="false">
<contextName>logback</contextName>
<property name="log.path" value="logs/study-zuul-gateway/logback/" />
<property name="log.file" value="logs/study-zuul-gateway/logback.log" />
<!-- 彩色日志 -->
<!-- 彩色日志依赖的渲染类 -->
<conversionRule conversionWord="clr" converterClass="org.springframework.boot.logging.logback.ColorConverter" />
<!-- 彩色日志格式 -->
<property name="CONSOLE_LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} %clr(%contextName) [%thread] %clr(%-5level) %logger{36} - %msg%n" />
<!--输出到控制台 -->
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<!-- <filter class="ch.qos.logback.classic.filter.ThresholdFilter"> <level>ERROR</level>
</filter> -->
<encoder>
<pattern>${CONSOLE_LOG_PATTERN}</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<!--输出到文件 -->
<appender name="file" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.file}</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${log.path}%d{yyyy-MM-dd_HH}.log</fileNamePattern>
<maxHistory>30</maxHistory>
<!-- 指定日志文件的上限大小,例如设置为1GB的话,那么到了这个值,就会删除旧的日志 -->
<totalSizeCap>1GB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %contextName [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="info">
<appender-ref ref="console" />
<appender-ref ref="file" />
</root>
</configuration>
2.3 启动类
package com.lzr;
import com.spring4all.swagger.EnableSwagger2Doc;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
/**
* 网关(可以集群哟,用nginx或node就行,看项目需求)
* @author lzr
* @date 2019/11/18 0018 16:51
*/
@SpringBootApplication
@EnableEurekaClient// 注入到注册中心
@EnableZuulProxy// 开启zuul网关(默认集成了ribbon,启用负载均衡机制)
@EnableSwagger2Doc// swagger开启注解
public class ZuulGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(ZuulGatewayApplication.class,args);
}
}
2.4 过滤器配置(示例)
2.4.1 请求前置过滤器
package com.lzr.filter;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import lombok.extern.log4j.Log4j2;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* @author lzr
* @date 2019/11/18 0018 18:00
*/
@Log4j2
@Component
public class RequestFilter extends ZuulFilter {
// 过滤逻辑
@Override
public Object run() throws ZuulException {
RequestContext currentContext = RequestContext.getCurrentContext();
HttpServletRequest request = currentContext.getRequest();
log.info("网关[请求时]过滤器,请求方法:[{}],请求路径[{}]",request.getMethod(),request.getRequestURI());
// 可以获取一般可以获取token数据进行验证
/*String token = request.getHeader("token");
if (token == null) {
log.warn("token未传");
//令zuul过滤该请求 不进行转发
currentContext.setSendZuulResponse(false);
//返回状态码 401
currentContext.setResponseStatusCode(HttpServletResponse.SC_UNAUTHORIZED);
//防止乱码
currentContext.getResponse().setContentType("application/json;charset=UTF-8");
//设置body
currentContext.setResponseBody("token未传");
}*/
// 下面这个可以不需要,默认执行完所有过滤器
/*currentContext.setSendZuulResponse(true);
currentContext.set("isSuccess",true);
currentContext.setResponseStatusCode(200);*/
return null;
}
// 是否开启拦截
@Override
public boolean shouldFilter() {
return true;
}
// 过滤类型
// pre:表示在请求之前执行,在“pre”类型的过滤器可以做参数校验、权限校验、流量监控、日志输出、协议转换等。
// post:表示在请求之后执行,在“post”类型的过滤器中可以做响应内容、响应头的修改,日志的输出,流量监控等。
// route: 在请求时被调用。
// error: 处理请求时发生错误时被调用。
@Override
public String filterType() {
return "pre";
}
// 过滤器执行顺序(当多个过滤器,配置过滤顺序(按照 小-》大 执行))
@Override
public int filterOrder() {
return 0;
}
}
2.4.2 请求响应过滤器
package com.lzr.filter;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import lombok.extern.log4j.Log4j2;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletResponse;
/**
* 在请求之后执行
* @author lzr
* @date 2019/11/18 0018 18:00
*/
@Log4j2
@Component
public class ResponseFilter extends ZuulFilter {
// 过滤逻辑
@Override
public Object run() throws ZuulException {
RequestContext currentContext = RequestContext.getCurrentContext();
HttpServletResponse response = currentContext.getResponse();
log.info("网关[响应返回]过滤器,contentType为:[{}],返回的状态为:[{}]",response.getContentType(),response.getStatus());
// 返回的数据是以流的形式存在,也可以在这里处理多请求拦截(请求频繁)
return null;
}
// 是否开启拦截
@Override
public boolean shouldFilter() {
return true;
}
// 过滤类型
// pre:表示在请求之前执行,在“pre”类型的过滤器可以做参数校验、权限校验、流量监控、日志输出、协议转换等。
// post:表示在请求之后执行,在“post”类型的过滤器中可以做响应内容、响应头的修改,日志的输出,流量监控等。
// route: 在请求时被调用。
// error: 处理请求时发生错误时被调用。
@Override
public String filterType() {
return "post";
}
// 过滤器执行顺序(当多个过滤器,配置过滤顺序(按照 小-》大 执行))
@Override
public int filterOrder() {
return 0;
}
}
2.4.3 请求时发生错误调用执行过滤器
package com.lzr.filter;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import lombok.extern.log4j.Log4j2;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletResponse;
/**
* 在请求时发生错误时被调用执行
* @author lzr
* @date 2019/11/20 0020 15:25
*/
@Log4j2
@Component
public class ErrorFilter extends ZuulFilter {
// 执行逻辑
@Override
public Object run() throws ZuulException {// 可以将错误写入日志保存,方便查看
log.info("发生错误调用!!");
return null;
}
// 是否开启拦截
@Override
public boolean shouldFilter() {
return true;
}
// 过滤类型
// pre:表示在请求之前执行,在“pre”类型的过滤器可以做参数校验、权限校验、流量监控、日志输出、协议转换等。
// post:表示在请求之后执行,在“post”类型的过滤器中可以做响应内容、响应头的修改,日志的输出,流量监控等。
// route: 在请求时被调用。
// error: 处理请求时发生错误时被调用。
@Override
public String filterType() {
return "error";
}
// 过滤器执行顺序(当多个过滤器,配置过滤顺序(按照 小-》大 执行))
@Override
public int filterOrder() {
return 0;
}
}
2.5 代码配置类
2.5.1 网关swagger配置
package com.lzr.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;
import springfox.documentation.swagger.web.SwaggerResource;
import springfox.documentation.swagger.web.SwaggerResourcesProvider;
import java.util.ArrayList;
import java.util.List;
/**
* 网关swagger配置
* @author lzr
* @date 2019/11/18 0018 15:30
*/
@Component
@Primary // SwaggerResourcesProvider自带一个实现类,防止多个实现实例引起报错,加上这个注解,代表,这才是唯一实现类
public class SwaggerResourceConfig implements SwaggerResourcesProvider {
@Autowired
private DiscoveryClient discoveryClient;// 获取注册中心所有实例(注意:这里获取的是缓存)
@Override
public List<SwaggerResource> get() {
List<SwaggerResource> resources = new ArrayList<>();
List<String> services = discoveryClient.getServices();
// 动态设置
for (String service : services) {// 注意:没有配置swagger的服务,在页面上会报“Failed to load API definition”,不需要就不管就行
List<ServiceInstance> instances = discoveryClient.getInstances(service);
for (ServiceInstance instance : instances) {
resources.add(createSwaggerResource(instance.getInstanceId(),"/"+service+"/v2/api-docs","2.0"));
}
}
// 静态设置(注意静态设置时,集群不会全部显现出来,只会出现一个,也是负载均衡产生的效果)
//resources.add(createSwaggerResource("服务消费者","/study-service-consumer/v2/api-docs","2.0"));
return resources;
}
/**
* @param name 名称
* @param location 路径
* @param version 版本
* @return
*/
private SwaggerResource createSwaggerResource(String name,String location,String version){
SwaggerResource swaggerResource = new SwaggerResource();
swaggerResource.setName(name);
swaggerResource.setLocation(location);
swaggerResource.setSwaggerVersion(version);
return swaggerResource;
}
}
2.5.2 自定义被限流数据返回配置
package com.lzr.config;
import org.springframework.boot.web.servlet.error.DefaultErrorAttributes;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.WebRequest;
import java.util.HashMap;
import java.util.Map;
/**
* 自定义被限流数据返回
* @author lzr
* @date 2019/11/19 0019 17:55
*/
@Component
public class CustomizeRateLimit extends DefaultErrorAttributes {
@Override
public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
Map<String, Object> errorAttributes = super.getErrorAttributes(webRequest, includeStackTrace);
if(errorAttributes.get("status").equals(429)){// 如果状态是429,则代表是多请求拦截(请求频繁),注意:这里的429必须是数字
// 修改返回状态码为200
webRequest.setAttribute("javax.servlet.error.status_code",200, RequestAttributes.SCOPE_REQUEST);
Map<String, Object> result = new HashMap<>();
result.put("code","-1");
result.put("msg","请求频繁,请休息一下");
result.put("data",null);
return result;
}
return errorAttributes;
}
}
2.5.3 自定义用户限流配置
package com.lzr.config;
import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.RateLimitUtils;
import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.properties.RateLimitProperties;
import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.support.DefaultRateLimitKeyGenerator;
import lombok.extern.log4j.Log4j2;
import org.springframework.cloud.netflix.zuul.filters.Route;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
/**
* 自定义 用户限流(配置了这里,其实可以不用设置type为user了)
* @author lzr
* @date 2019/11/20 0020 11:26
*/
@Log4j2
@Component
public class CustomizeRateLimitKeyGenerator extends DefaultRateLimitKeyGenerator {
public CustomizeRateLimitKeyGenerator(RateLimitProperties properties, RateLimitUtils rateLimitUtils) {
super(properties, rateLimitUtils);
}
/**
* 数据是json格式不能得到参数,数据是以流的方式传递,(表单提交方式可以得到参数)
* 最好是得到token,然后获取用户id,
* 在网关集群模式中,可以选择单独开一个redis服务器处理逻辑
* 然后网关获取redis服务器中的数据,当然也需要根据项目大小
*/
@Override
public String key(HttpServletRequest request, Route route, RateLimitProperties.Policy policy) {
return super.key(request, route, policy)+":"+request.getHeader("token");
}
}
3. zuul网关项目结构图
二. 创建gateway网关
1. 创建springcloudgateway网关
右键点击父项目,点击new,选择module),再选择maven,然后点击next
出现:
填写artifactid,然后点击next,最后点击finish
2. 引入依赖及配置
2.1 引入pom依赖
<?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>study-springcloudframework</artifactId>
<groupId>com.lzr</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>study-springcloud-gateway</artifactId>
<name>gateway网关</name>
<description>SpringCloudGateway网关</description>
<properties>
<!-- 其实应该放入父pom中,统一版本 -->
<swagger-api-version>1.9.0.RELEASE</swagger-api-version>
</properties>
<dependencies>
<!-- 这个依赖包含了webflux,所以不需要引入web包了,注意:如果引入会报错的哟(加载冲突) -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!-- 这个限流jar包已经包含了redis以及连接池,不要要引入redis包了 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
<!-- swagger依赖 -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
</dependencies>
</project>
2.2 application.yml文件配置
## 网关服务器端口
server:
port: 82
## eureka
eureka:
instance:
##eureka主机名,会在控制页面中显示
hostname: localhost
prefer-ip-address: true
## 心跳检测,检测与持续时间
## 发送心跳间隔时间 单位秒 (测试的时候设置小一点,让eureka服务端及时踢出该服务)
lease-renewal-interval-in-seconds: 10
## eureka服务端收到最后一次心跳后等待时间上限 单位秒 (告诉eureka服务端按照规则执行)
lease-expiration-duration-in-seconds: 20
##eureka服务器页面中status的请求路径(查看swagger页面的信息)
status-page-url: http://${eureka.instance.hostname}:${server.port}/swagger-ui.html
client:
serviceUrl:
defaultZone: http://lzr:123456@localhost:1111/eureka/
## 服务名
spring:
application:
name: gateway
redis:
host: 127.0.0.1
password: 123456789
port: 6379
timeout: 20000
database: 9
cloud:
gateway:
discovery:
locator:
## 是否可以通过其他服务的serviceId来转发到具体的服务实例。默认为false
## 为true,自动创建路由,路由访问方式:http://Gateway_HOST:Gateway_PORT/大写的serviceId/**,其中微服务应用名默认大写访问
enabled: true
## 是否小写
lower-case-service-id: true
routes:
## 控制路由跳转到其他服务
- id: consumer
uri: lb://study-service-consumer
predicates:
## 请求路径匹配规则
- Path=/consumer/**
## 顺序执行过滤,注意过滤顺序!!
filters:
## 如果使用RequestRateLimiter则是RequestRateLimiterGatewayFilterFactory
## 这里我使用自定义处理,其实大部分代码还是原代码,使用RateLimiter(该名称+GatewayFilterFactory就是bean名称)则是RateLimiterGatewayFilterFactory
## 重写 429 too many request 返回数据
- name: RateLimiter
args:
## 允许用户每秒处理多少个请求
redis-rate-limiter.replenishRate: 1
## 允许在一秒钟内完成的最大请求数
redis-rate-limiter.burstCapacity: 1
## 使用SpEL按名称引用bean(自己设置的bean)
key-resolver: "#{@userKeyResolver}"
## 代表截取路径的个数
- StripPrefix=1
## 日志配置文件
logging:
config: classpath:logback.xml
2.3 logback日志文件配置
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="60 seconds" debug="false">
<contextName>logback</contextName>
<property name="log.path" value="logs/study-springcloud-gateway/logback/" />
<property name="log.file" value="logs/study-springcloud-gateway/logback.log" />
<!-- 彩色日志 -->
<!-- 彩色日志依赖的渲染类 -->
<conversionRule conversionWord="clr" converterClass="org.springframework.boot.logging.logback.ColorConverter" />
<!-- 彩色日志格式 -->
<property name="CONSOLE_LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} %clr(%contextName) [%thread] %clr(%-5level) %logger{36} - %msg%n" />
<!--输出到控制台 -->
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<!-- <filter class="ch.qos.logback.classic.filter.ThresholdFilter"> <level>ERROR</level>
</filter> -->
<encoder>
<pattern>${CONSOLE_LOG_PATTERN}</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<!--输出到文件 -->
<appender name="file" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.file}</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${log.path}%d{yyyy-MM-dd_HH}.log</fileNamePattern>
<maxHistory>30</maxHistory>
<!-- 指定日志文件的上限大小,例如设置为1GB的话,那么到了这个值,就会删除旧的日志 -->
<totalSizeCap>1GB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %contextName [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="info">
<appender-ref ref="console" />
<appender-ref ref="file" />
</root>
</configuration>
2.3 启动类
package com.lzr;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
/**
* springcloud-gateway网关
* @author lzr
* @date 2019/11/20 0020 16:41
*/
@EnableEurekaClient
@SpringBootApplication
public class SpringCloudGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(SpringCloudGatewayApplication.class,args);
}
}
2.4 全局异常拦截
package com.lzr.exception;
import lombok.extern.log4j.Log4j2;
import org.springframework.boot.web.reactive.error.ErrorWebExceptionHandler;
import org.springframework.cloud.gateway.support.NotFoundException;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
/**
* 全局exception拦截
* @author lzr
* @date 2019/12/02 0002 16:00
*/
@Log4j2
@Component
public class GlobalExceptionHandler implements ErrorWebExceptionHandler {
@Override
public Mono<Void> handle(ServerWebExchange serverWebExchange, Throwable throwable) {
log.info("GLOBAL EXCEPTION URL:{}", serverWebExchange.getRequest().getPath());
if(serverWebExchange.getResponse().isCommitted()){// 如果已经提交,则不处理response
return Mono.error(throwable);
}
// 未提交,则处理返回参数
String msg = "";
if (throwable instanceof NotFoundException) {
msg = "{\"msg\":\"not found exception\"}";
} else if (throwable instanceof ResponseStatusException) {
msg = "{\"msg\":\"response status exception\"}";
} else {
msg = "{\"msg\":\"other exception\"}";
}
DataBufferFactory bufferFactory = serverWebExchange.getResponse().bufferFactory();
serverWebExchange.getResponse().getHeaders().setContentType(MediaType.APPLICATION_JSON_UTF8);
return serverWebExchange.getResponse().writeWith(Flux.just(bufferFactory.wrap(msg.getBytes())));
}
}
2.5 提供者响应过滤器
package com.lzr.filter;
import lombok.extern.log4j.Log4j2;
import org.reactivestreams.Publisher;
import org.springframework.boot.jdbc.metadata.DataSourcePoolMetadata;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.cloud.gateway.filter.NettyWriteResponseFilter;
import org.springframework.cloud.gateway.filter.factory.rewrite.HttpMessageWriterResponse;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.http.server.reactive.ServerHttpResponseDecorator;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.util.context.Context;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
/**
* 提供者响应过滤器
* 请求到数据的时候,重写数据
* @author lzr
* @date 2019/12/3 0003 16:41
*/
@Log4j2
@Component
public class ProviderResponseFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpResponse originalResponse = exchange.getResponse();
DataBufferFactory bufferFactory = originalResponse.bufferFactory();
// 请求到数据的时候,重写数据
ServerHttpResponseDecorator decoratedResponse = new ServerHttpResponseDecorator(originalResponse) {
@Override
public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
if (body instanceof Flux) {// 代表是请求过来的数据
Flux<? extends DataBuffer> fluxBody = (Flux<? extends DataBuffer>) body;
return super.writeWith(fluxBody.map(dataBuffer -> {
byte[] content = new byte[dataBuffer.readableByteCount()];
dataBuffer.read(content);
//释放掉内存
DataBufferUtils.release(dataBuffer);
// 这里就是处理成自己定义的数据,可以根据需求修改
String s = new String(content, Charset.forName("UTF-8"));
log.info("请求数据状态:[{}],请求得到数据:[{}]",originalResponse.getStatusCode().value(),s);
byte[] uppedContent = s.getBytes();
// 重新写入
return bufferFactory.wrap(uppedContent);
}));
}
// 不修改
return super.writeWith(body);
}
};
return chain.filter(exchange.mutate().response(decoratedResponse).build()).then();
}
@Override
public int getOrder() {
return NettyWriteResponseFilter.WRITE_RESPONSE_FILTER_ORDER - 1;
}
}
2.6 网关限流过滤工厂
重写 RequestRateLimiterGatewayFilterFactory,主要处理 429 too many request 返回数据为空的情况,代码基本和RequestRateLimiterGatewayFilterFactory一样,只是处理了响应为429的情况,注意:要使用这个,则需要将application配置文件中的spring->cloud->gateway->routes->filters->-name 改为你自定义的过滤工厂名称前缀(该名称+GatewayFilterFactory就是bean名称),如:自定义限流过滤工厂Bean名称为rateLimiterGatewayFilterFactory ,前缀就为:rateLimiter
package com.lzr.gatewayfilterfactory;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.cloud.gateway.filter.ratelimit.RateLimiter;
import org.springframework.cloud.gateway.route.Route;
import org.springframework.cloud.gateway.support.HttpStatusHolder;
import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import java.util.Iterator;
import java.util.Map;
/**
* 重写 RequestRateLimiterGatewayFilterFactory
* 主要处理 429 too many request 返回数据为空的情况
* @author lzr
* @date 2019/12/04 0004 18:00
*/
@Primary
@Component
public class RateLimiterGatewayFilterFactory extends AbstractGatewayFilterFactory<RateLimiterGatewayFilterFactory.Config> {
public static final String KEY_RESOLVER_KEY = "keyResolver";
private static final String EMPTY_KEY = "____EMPTY_KEY__";
private final RateLimiter defaultRateLimiter;
private final KeyResolver defaultKeyResolver;
private boolean denyEmptyKey = true;
private String emptyKeyStatusCode;
public RateLimiterGatewayFilterFactory(RateLimiter defaultRateLimiter, KeyResolver defaultKeyResolver) {
super(RateLimiterGatewayFilterFactory.Config.class);
this.emptyKeyStatusCode = HttpStatus.FORBIDDEN.name();
this.defaultRateLimiter = defaultRateLimiter;
this.defaultKeyResolver = defaultKeyResolver;
}
public KeyResolver getDefaultKeyResolver() {
return this.defaultKeyResolver;
}
public RateLimiter getDefaultRateLimiter() {
return this.defaultRateLimiter;
}
public boolean isDenyEmptyKey() {
return this.denyEmptyKey;
}
public void setDenyEmptyKey(boolean denyEmptyKey) {
this.denyEmptyKey = denyEmptyKey;
}
public String getEmptyKeyStatusCode() {
return this.emptyKeyStatusCode;
}
public void setEmptyKeyStatusCode(String emptyKeyStatusCode) {
this.emptyKeyStatusCode = emptyKeyStatusCode;
}
public GatewayFilter apply(RateLimiterGatewayFilterFactory.Config config) {
// 这里获得你自定义的 限流方式(用户限流,ip限流等)
KeyResolver resolver = (KeyResolver)this.getOrDefault(config.keyResolver, this.defaultKeyResolver);
RateLimiter<Object> limiter = (RateLimiter)this.getOrDefault(config.rateLimiter, this.defaultRateLimiter);
boolean denyEmpty = (Boolean)this.getOrDefault(config.denyEmptyKey, this.denyEmptyKey);
HttpStatusHolder emptyKeyStatus = HttpStatusHolder.parse((String)this.getOrDefault(config.emptyKeyStatus, this.emptyKeyStatusCode));
return (exchange, chain) -> {
Route route = (Route)exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR);
return resolver.resolve(exchange).defaultIfEmpty(EMPTY_KEY).flatMap((key) -> {
if (EMPTY_KEY.equals(key)) {
if (denyEmpty) {
ServerWebExchangeUtils.setResponseStatus(exchange, emptyKeyStatus);
return exchange.getResponse().setComplete();
} else {
return chain.filter(exchange);
}
} else {
return limiter.isAllowed(route.getId(), key).flatMap((response) -> {
Iterator var4 = response.getHeaders().entrySet().iterator();
while(var4.hasNext()) {
Map.Entry<String, String> header = (Map.Entry)var4.next();
exchange.getResponse().getHeaders().add((String)header.getKey(), (String)header.getValue());
}
if (response.isAllowed()) {
return chain.filter(exchange);
} else {
// 重写 429 too many request 返回数据
ServerHttpResponse serverHttpResponse = exchange.getResponse();
String data = "{\"code\":\"429\",\"msg\":\"请求过快,请休息一下\",\"data\":\"\"}";
DataBuffer buffer = serverHttpResponse.bufferFactory().wrap(data.getBytes());
serverHttpResponse.setStatusCode(HttpStatus.OK);// 还是设置为ok
serverHttpResponse.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
return serverHttpResponse.writeWith(Mono.just(buffer));
}
});
}
});
};
}
private <T> T getOrDefault(T configValue, T defaultValue) {
return configValue != null ? configValue : defaultValue;
}
public static class Config {
private KeyResolver keyResolver;
private RateLimiter rateLimiter;
private HttpStatus statusCode;
private Boolean denyEmptyKey;
private String emptyKeyStatus;
public Config() {
this.statusCode = HttpStatus.TOO_MANY_REQUESTS;
}
public KeyResolver getKeyResolver() {
return this.keyResolver;
}
public RateLimiterGatewayFilterFactory.Config setKeyResolver(KeyResolver keyResolver) {
this.keyResolver = keyResolver;
return this;
}
public RateLimiter getRateLimiter() {
return this.rateLimiter;
}
public RateLimiterGatewayFilterFactory.Config setRateLimiter(RateLimiter rateLimiter) {
this.rateLimiter = rateLimiter;
return this;
}
public HttpStatus getStatusCode() {
return this.statusCode;
}
public RateLimiterGatewayFilterFactory.Config setStatusCode(HttpStatus statusCode) {
this.statusCode = statusCode;
return this;
}
public Boolean getDenyEmptyKey() {
return this.denyEmptyKey;
}
public RateLimiterGatewayFilterFactory.Config setDenyEmptyKey(Boolean denyEmptyKey) {
this.denyEmptyKey = denyEmptyKey;
return this;
}
public String getEmptyKeyStatus() {
return this.emptyKeyStatus;
}
public RateLimiterGatewayFilterFactory.Config setEmptyKeyStatus(String emptyKeyStatus) {
this.emptyKeyStatus = emptyKeyStatus;
return this;
}
}
}
2.6 自定义限流配置
注意:要使用这个,则需要在application配置文件配置key-resolver: “#{@userKeyResolver}”
这里面的名称是自己定义的文件的bean名称
package com.lzr.config;
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.context.annotation.Primary;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.nio.charset.Charset;
import java.util.List;
/**
* 自定义 用户限流
* @author lzr
* @date 2019/11/21 0021 10:59
*/
@Primary
@Component
public class UserKeyResolver implements KeyResolver {
@Override
public Mono<String> resolve(ServerWebExchange exchange) {
ServerHttpRequest request = exchange.getRequest();
String token = request.getHeaders().getFirst("token");
return Mono.just(token);
}
}
2.7 swagger配置
2.7.1 swagger处理请求
package com.lzr.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;
import springfox.documentation.swagger.web.*;
import java.util.Optional;
/**
* 网关swagger处理请求
* @author lzr
* @date 2019/11/21 0021 10:00
*/
@RestController
@RequestMapping("/swagger-resources")
public class SwaggerHandler {
@Autowired(required = false)
private SecurityConfiguration securityConfiguration;
@Autowired(required = false)
private UiConfiguration uiConfiguration;
private final SwaggerResourcesProvider swaggerResources;
@Autowired
public SwaggerHandler(SwaggerResourcesProvider swaggerResources) {
this.swaggerResources = swaggerResources;
}
@GetMapping("/configuration/security")
public Mono<ResponseEntity<SecurityConfiguration>> securityConfiguration() {
return Mono.just(new ResponseEntity<>(
Optional.ofNullable(securityConfiguration).orElse(SecurityConfigurationBuilder.builder().build()), HttpStatus.OK));
}
@GetMapping("/configuration/ui")
public Mono<ResponseEntity<UiConfiguration>> uiConfiguration() {
return Mono.just(new ResponseEntity<>(
Optional.ofNullable(uiConfiguration).orElse(UiConfigurationBuilder.builder().build()), HttpStatus.OK));
}
@GetMapping("")
public Mono<ResponseEntity> swaggerResources() {
return Mono.just((new ResponseEntity<>(swaggerResources.get(), HttpStatus.OK)));
}
}
2.7.2 swagger实例获取配置类
package com.lzr.config;
import lombok.AllArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.cloud.gateway.config.GatewayProperties;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.support.NameUtils;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;
import springfox.documentation.swagger.web.SwaggerResource;
import springfox.documentation.swagger.web.SwaggerResourcesProvider;
import java.util.ArrayList;
import java.util.List;
/**
* 网关swagger配置
* @author lzr
* @date 2019/11/21 0021 10:00
*/
@Component
@Primary
public class SwaggerProvider implements SwaggerResourcesProvider {
@Autowired
private DiscoveryClient discoveryClient;// 获取注册中心所有实例(注意:这里获取的是缓存)
@Override
public List<SwaggerResource> get() {
List<SwaggerResource> resources = new ArrayList<>();
List<String> services = discoveryClient.getServices();
// 动态设置(静态设置好一些)(集群方式,swagger的请求也会"负载均衡")
// 注意:没有配置swagger的服务,在页面上会报“Failed to load API definition”,不需要就不管就行
services.stream().forEach(service -> discoveryClient.getInstances(service).stream().
forEach(instance -> resources.add(createSwaggerResource(instance.getInstanceId(),"/"+service+"/v2/api-docs","2.0"))));
return resources;
}
/**
* @param name 名称
* @param location 路径
* @param version 版本
* @return
*/
private SwaggerResource createSwaggerResource(String name,String location,String version){
SwaggerResource swaggerResource = new SwaggerResource();
swaggerResource.setName(name);
swaggerResource.setLocation(location);
swaggerResource.setSwaggerVersion(version);
return swaggerResource;
}
}
3. gateway网关项目结构图
五. 项目地址
项目 git地址 将在最后一章附上。
若有疑问,请在评论区留言,谢谢