前言
声明:此文Demo摘自《重新定义》已得作者许可。
这篇博客不会去介绍关于zuul的基础知识以及配置详解,我会以代码的形式一步一步带领大家实现,利用多种或一种不同类型的过滤器进行处理相应的业务,以便让大家了解各个过滤器什么时候用,用来干什么,解决大家实际工作中可能碰到的问题。
正文
本篇博客会涉及到三个工程依次是一个客户端client-a、一个eureka服务(和前文的案例没有区别)、一个zuul-server。
这三个工程都依赖一个父级pom,和前文介绍的一样。为了让大家看到我使用的版本,我再一次把pom依赖贴出来。
<modules>
<module>ch8-1-eureka-server</module>
<module>ch8-1-zuul-server</module>
<module>ch8-1-client-a</module>
</modules>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.3.RELEASE</version>
</parent>
<!-- 利用传递依赖,公共部分 -->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- springboot web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
<!-- 管理依赖 -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Finchley.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<!--注意: 这里必须要添加,否则各种依赖有问题 -->
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/libs-milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
第一步
还是需要一个eureka服务端,在以后的案例里我不会再贴出来eureka服务端的配置以及启动类了。由于这次笔者加入了日志配置文件,所以在这三个工程的resources下都会有一个logback.log文件,由于springboot默认使用的日志就是logback,所以我们可以直接使用,如果你想了解springboot使用log4j,那么请自行百度,本文不介绍。下面就是日志配置文件,由于三个工程都有共同的文件,所以我只贴一次,唯一不同的就是生成日志文件的前缀需要大家更改
<property name="LOG_PREFIX" value="eureka-server" /> ,这里的value可以随意写,只要能区分出不同的工程就可以。
<?xml version="1.0" encoding="UTF-8"?>
<!-- Logback configuration. See http://logback.qos.ch/manual/index.html -->
<configuration scan="true" scanPeriod="10 seconds">
<!--<include resource="org/springframework/boot/logging/logback/base.xml"
/> -->
<!--定义日志文件的存储地址和前缀名 -->
<property name="LOG_HOME" value="logs" />
<property name="LOG_PREFIX" value="eureka-server" />
<!-- 一般信息按照每天生成日志文件 -->
<appender name="INFO_FILE"
class="ch.qos.logback.core.rolling.RollingFileAppender">
<File>${LOG_HOME}/${LOG_PREFIX}-info.log</File>
<rollingPolicy
class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!-- 每天一归档 -->
<fileNamePattern>${LOG_HOME}/${LOG_PREFIX}-info-%d{yyyyMMdd}.log.%i
</fileNamePattern>
<!-- 单个日志文件最多500MB, 30天的日志周期,最大不能超过20GB -->
<maxFileSize>100MB</maxFileSize>
<maxHistory>30</maxHistory>
<totalSizeCap>20GB</totalSizeCap>
</rollingPolicy>
<encoder>
<!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符 -->
<Pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36}
-%msg%n</Pattern>
</encoder>
</appender>
<!--错误信息按照每天生成日志文件 -->
<appender name="ERROR_FILE"
class="ch.qos.logback.core.rolling.RollingFileAppender">
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>ERROR</level>
</filter>
<File>${LOG_HOME}/${LOG_PREFIX}-error.log</File>
<rollingPolicy
class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!-- 每天一归档 -->
<fileNamePattern>${LOG_HOME}/${LOG_PREFIX}-error-%d{yyyyMMdd}.log.%i
</fileNamePattern>
<!-- 单个日志文件最多500MB, 30天的日志周期,最大不能超过20GB -->
<maxFileSize>100MB</maxFileSize>
<maxHistory>30</maxHistory>
<totalSizeCap>20GB</totalSizeCap>
</rollingPolicy>
<encoder>
<!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符 -->
<Pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36}
-%msg%n</Pattern>
</encoder>
</appender>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符 -->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} -
%msg%n</pattern>
</encoder>
</appender>
<!-- 日志输出级别 这样设置不打印日志 -->
<root level="INFO">
<appender-ref ref="STDOUT" />
<appender-ref ref="INFO_FILE" />
<appender-ref ref="ERROR_FILE" />
</root>
</configuration>
第二步
创建client-a工程,一个eureka客户端,代码很简单,下面依次贴出启动类、controller、yml、pom文件
@SpringBootApplication
@EnableDiscoveryClient
public class ClientAApplication {
public static void main(String[] args) {
SpringApplication.run(ClientAApplication.class, args);
}
}
@RestController
public class TestController {
@GetMapping("/add")
public Integer add(Integer a, Integer b){
return a + b;
}
@GetMapping("/a/add")
public Integer aadd(Integer a, Integer b){
return a + b;
}
@GetMapping("/sub")
public Integer sub(Integer a, Integer b){
return a - b;
}
@GetMapping("/mul")
public String mul(Integer a, Integer b){
System.out.println("进入client-a!");
return "client-a-" + a * b;
}
@GetMapping("/div")
public Integer div(Integer a, Integer b){
return a / b;
}
}
server:
port: 7070
spring:
application:
name: client-a
eureka:
client:
serviceUrl:
defaultZone: http://${eureka.host:127.0.0.1}:${eureka.port:8888}/eureka/
instance:
prefer-ip-address: true
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
第三步
大家对eureka应用过的话,我上面所有代码都不用看。
下面才是重点,创建一个zuul-server服务。我们都知道,在zuul过滤器里PRE_TYPE类型是在路由前执行的,所以我要给大家演示配置三个PRE_TYPE类型的过滤器,按照顺序依次处理不同的业务。以及,三个PRE_TYPE类型过滤器中任意一个出现异常时他的下游业务应该怎么处理。
首先给大家一个项目的目录,
我会分别贴出各个类的代码,这里作者已经把每行注释写的足够详细,我也没什么好介绍的了。
@SpringBootApplication
@EnableDiscoveryClient
@EnableZuulProxy
public class ZuulServerApplication {
public static void main(String[] args) {
SpringApplication.run(ZuulServerApplication.class, args);
}
@Bean
public FirstPreFilter firstPreFilter(){
return new FirstPreFilter();
}
@Bean
public SecondPreFilter secondPreFilter(){
return new SecondPreFilter();
}
@Bean
public ThirdPreFilter thirdPreFilter(){
return new ThirdPreFilter();
}
@Bean
public ErrorFilter errorFilter(){
return new ErrorFilter();
}
@Bean
public PostFilter postFilter(){
return new PostFilter();
}
}
public class ErrorFilter extends ZuulFilter {
@Override
public String filterType() {
return ERROR_TYPE;
}
@Override
public int filterOrder() {
return -1;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() throws ZuulException {
RequestContext ctx = RequestContext.getCurrentContext();
System.out.println("这是ErrorFilter");
return null;
}
}
public class FirstPreFilter extends ZuulFilter {
@Override
public String filterType() {
return PRE_TYPE;
}
@Override
public int filterOrder() {
return 0;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() throws ZuulException {
System.out.println("这是第一个自定义Zuul Filter!");
return null;
}
}
public class SecondPreFilter extends ZuulFilter {
@Override
public String filterType() {
return PRE_TYPE;
}
@Override
public int filterOrder() {
return 2;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() throws ZuulException {
System.out.println("这是SecondPreFilter!");
//从RequestContext获取上下文
RequestContext ctx = RequestContext.getCurrentContext();
//从上下文获取HttpServletRequest
HttpServletRequest request = ctx.getRequest();
//从request尝试获取a参数值
String a = request.getParameter("a");
//如果a参数值为空则进入此逻辑
if (null == a) {
//对该请求禁止路由,也就是禁止访问下游服务
ctx.setSendZuulResponse(false);
//设定responseBody供PostFilter使用
ctx.setResponseBody("{\"status\":500,\"message\":\"a参数为空!\"}");
//logic-is-success保存于上下文,作为同类型下游Filter的执行开关
ctx.set("logic-is-success", false);
//到这里此Filter逻辑结束
return null;
}
//设置避免报空
ctx.set("logic-is-success", true);
return null;
}
}
public class ThirdPreFilter extends ZuulFilter {
@Override
public String filterType() {
return PRE_TYPE;
}
@Override
public int filterOrder() {
return 3;
}
@Override
public boolean shouldFilter() {
RequestContext ctx = RequestContext.getCurrentContext();
//从上下文获取logic-is-success值,用于判断此Filter是否执行
return (boolean)ctx.get("logic-is-success");
}
@Override
public Object run() throws ZuulException {
System.out.println("这是ThirdPreFilter!");
//从RequestContext获取上下文
RequestContext ctx = RequestContext.getCurrentContext();
//从上下文获取HttpServletRequest
HttpServletRequest request = ctx.getRequest();
//从request尝试获取b参数值
String b = request.getParameter("b");
//如果b参数值为空则进入此逻辑
if (null == b) {
//对该请求禁止路由,也就是禁止访问下游服务
ctx.setSendZuulResponse(false);
//设定responseBody供PostFilter使用
ctx.setResponseBody("{\"status\":500,\"message\":\"b参数为空!\"}");
//logic-is-success保存于上下文,作为同类型下游Filter的执行开关,假定后续还有自定义Filter当设置此值
ctx.set("logic-is-success", false);
//到这里此Filter逻辑结束
return null;
}
return null;
}
}
public class PostFilter extends ZuulFilter {
@Override
public String filterType() {
return POST_TYPE;
}
@Override
public int filterOrder() {
return 0;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() throws ZuulException {
System.out.println("这是PostFilter!");
//从RequestContext获取上下文
RequestContext ctx = RequestContext.getCurrentContext();
//处理返回中文乱码
ctx.getResponse().setCharacterEncoding("GBK");
//获取上下文中保存的responseBody
String responseBody = ctx.getResponseBody();
//如果responseBody不为空,则说明流程有异常发生
if (null != responseBody) {
//设定返回状态码
ctx.setResponseStatusCode(500);
//替换响应报文
ctx.setResponseBody(responseBody);
}
return null;
}
}
spring:
application:
name: zuul-server
server:
port: 5555
eureka:
client:
serviceUrl:
defaultZone: http://${eureka.host:127.0.0.1}:${eureka.port:8888}/eureka/
instance:
prefer-ip-address: true
management:
security:
enabled: false
zuul:
routes:
client-a:
path: /client/**
serviceId: client-a
management.endpoints.web.exposure.include=*
#management.endpoint.shutdown.enabled=true
management.endpoint.health.show-details=always
<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-security</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
这里我简单介绍一下上面的代码
1.配置每种过滤器的类都要继承ZuulFilter。
2.每种过滤器实现类必须将他注入spring bean容器。
3.重写ZuulFilter的方法中,
filterType方法:使用返回值设定Filter类型,可以设置pre、route、post、error。
filterOrder方法:使用返回值确定当前类型的过滤器执行顺序,同一类型的过滤器,数值越小越先执行,如果每种类型都有一个过滤器的话,返回值写0就好了。
shouldFilter方法:使用返回值确定该Filter是否执行/生效,可以当作这个过滤器的开关,true:开,false:关。
run方法:这里就写你这个过滤器的业务逻辑,想让该过滤器做什么都可以在这里写。
4.上文的FirstPreFilter代表第一级pre过滤器,所以执行顺序我写了0。
5.SecondPreFilter类为第二级pre过滤器,执行顺序是2,用来从request尝试获取a参数值,如果获取到了a参数,就给RequestContext对象加入一个自定义的属性:logic-is-success,值:true。如果没有获取到就在相应体加入状态:500,描述:a参数为空!,自定义属性:logic-is-success, 值为:false,自定义logic-is-success属性的目的是为了当前过滤器的下游使用。
6.ThirdPreFilter类为第三级pre过滤器,执行顺序是3,用来从request尝试获取b参数值。如果没有获取到就在相应体加入状态:500,描述:b参数为空!,自定义属性:logic-is-success, 值为:false,自定义logic-is-success属性的目的是为了当前过滤器的下游使用,在这个类里shouldFilter方法获取了a参数如果a参数有值,ThirdPreFilter方法才会生效,否则不执行ThirdPreFilter方法,
7.ErrorFilter类是在pre、route、post过滤器中发生类异常才会触发error过滤器执行,并且触发类error后再执行post过滤器。所以说这里可以再pre过滤器中设置权限,不符合可以抛出异常,这样再error接收到可以做相应的处理。
8.
zuul:
routes:
client-a:
path: /client/**
serviceId: client-a
配置的含义是把所有请求为http://ip:5555/client/……所有的都映射到client-a服务中,……代表的是client-a服务中的url。
实际开发中类似这样映射服务的配置会有很多。
测试
1.这里为了给大家演示,我故意再第一个pre过滤器里出现异常,然后观察控制台打印效果。
public class FirstPreFilter extends ZuulFilter {
@Override
public String filterType() {
return PRE_TYPE;
}
@Override
public int filterOrder() {
return 0;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() throws ZuulException {
System.out.println("这是第一个自定义Zuul Filter!");
int a = 1/0;
return null;
}
}
中间异常信息省略
可以看到如果pre出现了异常,过滤器的执行顺序先是error,然后是post
2.接下来把手动触发异常的代码删掉,测试上面给出客户端的请求,这里我就简单使用一个请求给大家演示。
访问http://localhost:5555/client/add?a=1