Zuul 介绍
什么是Zuul?
Zuul 也是Netflix公司做出的系统,主要是Netflix API流量的数量和多样性有时会导致生产问题迅速出现而没有警告。因此Netflix团队做了这么一个统一的入口——网关,可以将所有的API组织起来。
Zuul是所有从设备和web站点到Netflix流媒体应用程序后端请求的前门。作为一个边缘服务应用程序,Zuul被构建来支持动态路由、监视、弹性和安全性。它还可以根据需要将请求路由到多个Amazon自动伸缩组。
Zuul 是一个基于JVM路由和服务端的负载均衡器,Spring Cloud 对Zuul 进行了一系列的封装和时间,并且可以和Eureka、Ribbon、Hystrix、Feign等组件配合使用,对于Spring Cloud 微服务来说不必暴露过多的接口,提升了整个分布式集群的安全性。
为什么构建Zuul?
Zuul使用了一系列不同类型的过滤器,使我们能够快速灵活地将功能应用到服务中。这些过滤器帮助我们执行以下功能:
- 身份验证和安全性: 识别每个资源的身份验证需求,并拒绝不满足它们的请求
- 监控: 在边缘跟踪有意义的数据和统计数据,以便给我们一个准确的生产视图
- 动态路由: 动态路由请求到不同的后端集群
- 压力测试: 逐渐增加集群的流量,以评估性能
- 限流: 为每种请求类型分配容量,并丢弃超过限制的请求
静态响应处理: 直接在边缘构建一些响应,而不是将它们转发到内部集群
Zuul 2.0架构
https://github.com/Netflix/zuul/wiki/How-It-Works-2.0
从较高的角度来看,Zuul 2.0是一台Netty服务器,它运行前置过滤器(入站过滤器),然后使用Netty客户端代理请求,然后在运行后置过滤器(出站过滤器)后返回响应。
过滤器是Zuul业务逻辑的核心所在。它们具有执行大量动作的能力,并且可以在请求-响应生命周期的不同部分运行,如上图所示。入站筛选器在路由到源之前执行,并且可以用于身份验证,路由和修饰请求。
- 端点过滤器可用于返回静态响应,否则内置
ProxyEndpoint
过滤器会将请求路由到源。 - 出站筛选器在从源获取响应后执行,可用于度量标准,修饰用户的响应或添加自定义标头。
过滤器也有两种类型:同步和异步。由于我们在事件循环上运行,因此永远不要阻塞过滤器至关重要。如果要阻塞,请在单独的线程池上的异步过滤器中进行阻塞,否则可以使用同步过滤器。
和Zuul 1.*对比
- 前端用Netty Server代替Servlet,目的是支持前端异步。后端用Netty Client代替Http Client,目的是支持后端异步。
- 过滤器换了一下名字,用Inbound Filters代替Pre-routing Filters,用Endpoint Filter代替Routing Filter,用Outbound Filters代替Post-routing Filters。
- 性能提升约20%
本书讲解的
Spring Cloud Hoxton.RELEASE
支持的Zuul 版本仍然是1.* 系列,用的1.3.1,因为对于Zuul 的使用和介绍仍然以1.3.1 版本为主。
<dependency>
<groupId>com.netflix.zuul</groupId>
<artifactId>zuul-core</artifactId>
<version>1.3.1</version>
<scope>compile</scope>
<exclusions>
<exclusion>
<artifactId>groovy-all</artifactId>
<groupId>org.codehaus.groovy</groupId>
</exclusion>
<exclusion>
<artifactId>mockito-all</artifactId>
<groupId>org.mockito</groupId>
</exclusion>
</exclusions>
</dependency>
Zuul 简单应用
本节代码地址
GitHub: https://github.com/xuyisu/fw-sping-cloud/tree/master/fw-cloud-gateways/fw-cloud-gateways-zuul-simple
下面通过一个简单项目带领大家入门,我们直接使用Spring Cloud 封装好的包,本节我们要实现当输入Zuul的地址时可以跳转发哦另一个服务上。如下图
1.新建项目
1.1 添加maven 配置
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
</dependencies>
1.2 添加启动类
启动类中需要添加@EnableZuulProxy
注解
@EnableZuulProxy
@SpringBootApplication
public class FwZuulSimpleApplication {
public static void main(String[] args) {
SpringApplication.run(FwZuulSimpleApplication.class, args);
}
}
1.3 添加应用配置
我们给zuul添加转发配置,当输入ip:port/simple
会转发到http://localhost:8764
server:
port: 8678
spring:
application:
name: fw-gateways-zuul-simple
zuul:
routes:
simple:
url: http://localhost:8764 #转发的地址
1.4 启动项目
这里需要fw-cloud-client-eureka
服务的支持(8764端口),然后启动当前项目
在Postman 中输入localhost:8678/simple/hello实际上调用的是localhost:8764/hello
2. 运行原理
因为我们开启了@EnableZuulProxy
注解,在Spring
容器初始化的时候,会将Zuul
的相关配置也初始化,Spring Boot
提供了ServletRegistrationBean
用于注册Servlet
,Zuul
中有一个类ZuulServlet
,在Servlet
的service
方法中执行各种ZuulFilter
。下图是ZuulServlet
的生命周期。
zuul把过滤器分为四个阶段,分别是
- pre:主要是在请求路由之前调用,很多验证可以在这里做
- route:在路由请求时候被调用,主要用来转发请求
- post:主要用来处理响应请求
- error:当错误发生时,会经由这个类型的过滤器处理
ZuulServlet
的service
方法在接收到请求后,会先执行pre
阶段的过滤器,再执行routing
阶段的过滤器,最后执行post
阶段的过滤器,其中routing
阶段的过滤器会将请求转发到“源服务”,源服务一般都是第三方服务,也可以是当前集群的其它服务,在实行pre
、routing
、post
阶段发生异常时,会执行error
过滤器,整个HTTP
请求、响应等数据会被封装到RequestContext
对象中。
Zuul路由配置
1. 简单路由
上面的配置方式对运维人员很不友好,因此Spring Cloud
团队实现Spring Cloud Zuul
与Spring Cloud Eureka
的整合然后进行路由操作了,并且默认的转发规则就是”Zuul网关地址+访问的服务名称+API URL”,所以给定服务名称的时候尽量简短一点,服务名称直接可以从Eureka
中获取。
比如:
- 网关地址:
localhost:8678
- 服务名称:
fw-auth
- 接口名称:
/hello
通过Zuul网关访问的默认地址就是localhost:8678/fw-auth/hello本节代码地址
GitHub: https://github.com/xuyisu/fw-sping-cloud/tree/master/fw-cloud-gateways/fw-cloud-gateways-zuul-simple
GitHub: https://github.com/xuyisu/fw-sping-cloud/tree/master/fw-cloud-client/fw-cloud-client-eureka
1.1 应用配置
server:
port: 8678
spring:
application:
name: fw-gateways-zuul-simple
zuul:
routes:
simple:
url: http://localhost:8764 #转发的地址
上文的简单实用里面,我们说的配置方式合计就是简单路由。url 的配置会匹配http://
或https://
,如果直接配置localhost:8764
将转发不成功。
这种配置方式是一种简单路由,由过滤器SimpleHostRoutingFilter
使用HttpClient
进行转发,该过滤器会将HttpServletRequest
的请求数据转化为HttpClient
的请求实例HttpRequest
,然后把使用CloseableHttpClient
转发。
当然Zuul
也支持修改HttpClient
的配置属性,
zuul:
host:
max-total-connections: 300 #设置目标主机的最大连接数,默认200
max-per-route-connections: 30 #设置每个主机的初始化连接数,默认20
1.2 应用启动
浏览器或者Postman 输入localhost:8678/simple/hello
2.路由前缀
本节代码地址
GitHub: https://github.com/xuyisu/fw-sping-cloud/tree/master/fw-cloud-gateways/fw-cloud-gateways-zuul-simple
比如我们想给将通过网关的接口统一设置一个url前缀,可以使用如下配置
zuul:
prefix: /api
那之前的接口在想请求simple 的数据,就必须带上/api,localhost:8678/simple/hello
要变成localhost:8678/api/simple/hello
3. 指定路由
本节代码地址
GitHub: https://github.com/xuyisu/fw-sping-cloud/tree/master/fw-cloud-gateways/fw-cloud-gateways-zuul-simple
GitHub: https://github.com/xuyisu/fw-sping-cloud/tree/master/fw-cloud-ribbon/fw-cloud-ribbon-server
3.1 新建项目
3.2 新建启动类
记得开启@EnableZuulProxy
注解
@EnableZuulProxy
@EnableDiscoveryClient
@SpringBootApplication
public class FwZuulApplication {
public static void main(String[] args) {
SpringApplication.run(FwZuulApplication.class, args);
}
}
3.3 应用配置
server:
port: 8679
spring:
application:
name: fw-gateways-zuul
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
zuul:
routes:
fw-cloud-ribbon-server:
path: /ribbon/**
当然还可以简写
zuul:
routes:
fw-cloud-ribbon-server:
path: /ribbon/**
也可以写成,path 可以省略
zuul:
routes:
fw-cloud-ribbon-server: /ribbon/**
3.4 启动项目
浏览器或Postman 输入localhost:8678/ribbon/user/1
4. 路由跳转
在简单路由
里已经介绍了跳转到具体第三方地址的配置,下面我们来演示一下本地跳转的功能,Zuul 里面的本地跳转只要通过forward就可以了。
本节代码地址
GitHub: https://github.com/xuyisu/fw-sping-cloud/tree/master/fw-cloud-gateways/fw-cloud-gateways-zuul-simple
GitHub: https://github.com/xuyisu/fw-sping-cloud/tree/master/fw-cloud-ribbon/fw-cloud-ribbon-server
4.1 修改应用配置
修改Zuul 部分的配置,添加一个forward的API前缀
zuul:
routes:
fw-cloud-ribbon-server:
path: /ribbon/**
url: forward:/local
4.2 添加控制层
fw-cloud-gateways-zuul
添加/local控制层
/**
* @author xuyisu
* @description forward转发
* @date 2020/1/11
*/
@RestController
public class LocalController {
@GetMapping("/local/user/{id:\\d+}")
public String getId(@PathVariable Long id){
return id.toString()+",我是forward转发来的";
}
}
4.3启动项目
浏览器或者Postman 输入http://localhost:8679/ribbon/user/1
5. Zuul 过滤器
本节代码地址
GitHub: https://github.com/xuyisu/fw-sping-cloud/tree/master/fw-cloud-gateways/fw-cloud-gateways-zuul-simple
GitHub: https://github.com/xuyisu/fw-sping-cloud/tree/master/fw-cloud-ribbon/fw-cloud-ribbon-server
Zuul
作为网关来说,会做很多转发前的校验,而这些校验我们都可以基于Zuul
过滤器来实现,比如Token
校验、IP黑名单等。
5.1 新建过滤器
通过模拟用户必须携带token,来拦截用户的请求。前面我们说过Zuul 的生命周期,我们这里模拟在Header里面需要携带token,并且token 的值必须为123456,否则就会执行失败。
/**
* @author xuyisu
* @description Token Filter
* @date 2019/12/12
*/
public class TokenFilter extends ZuulFilter {
@Override
public String filterType() {
return "pre"; // 在请求被路由之前调用
}
@Override
public int filterOrder() {
return 0; // filter执行顺序,通过数字指定 ,优先级为0,数字越大,优先级越低
}
@Override
public boolean shouldFilter() {
return true; // 是否执行该过滤器,此处为true,说明需要过滤
}
@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
String token = request.getHeader("token");// 获取请求的参数
// 如果有token参数并且token值为123456,才进行路由
if (StringUtils.isNotBlank(token) && token.equals("123456")) {
ctx.setSendZuulResponse(true); //对请求进行路由
ctx.setResponseStatusCode(200);
ctx.set("code", 1);
} else {
ctx.setSendZuulResponse(false); //不对其进行路由
ctx.setResponseStatusCode(401);
HttpServletResponse response = ctx.getResponse();
response.setHeader("content-type", "text/html;charset=utf8");
ctx.setResponseBody("认证失败");
ctx.set("code", 0);
}
return null;
}
}
自定义过滤器需要继承ZuulFilter , 并且需要实现下面几个方法:
• shou ldFilter : 是否执行该过滤器, true 为执行, false 为不执行,可以结合配置中心及结合业务逻辑设置
• filterType :过滤器类型,pre 、route 、post 、error 。
• filterOrder :过滤器的执行顺序,数值越小,优先级越高。
• run :执行自己的业务逻辑。设置的ctx.setSendZuulResponse(false);
代表不对其进行路由,ctx.setSendZuulResponse(true);
表示对请求进行路由。
5.3 配置过滤器
@Configuration
public class ZuulConfig {
/**
* Zuul 过滤器配置,如果不想启动,注释掉即可
*/
@Bean
public TokenFilter tokenFilter(){
return new TokenFilter();
}
}
5.4 重启应用
浏览器或Postman 输入localhost:8679/ribbon/user/1
首先未设置token
设置token
6. Zuul 过滤器拦截顺序
本节代码地址
GitHub: https://github.com/xuyisu/fw-sping-cloud/tree/master/fw-cloud-gateways/fw-cloud-gateways-zuul-simple
GitHub: https://github.com/xuyisu/fw-sping-cloud/tree/master/fw-cloud-ribbon/fw-cloud-ribbon-server
6.1 新建过滤器
新建一个过滤器,设置执行顺序为5,在TokenFilter
之后,是不是这样,待会可以验证一下,这个过滤器是验证header
中是否有name
这个key
,并且key的值要是zuul
才能通过,否则失败。
/**
* @author xuyisu
* @description Zuul Filter
* @date 2019/12/28
*/
@Slf4j
public class ZuulFilter extends com.netflix.zuul.ZuulFilter {
@Override
public String filterType() {
return "pre"; // 在请求被路由之前调用
}
@Override
public int filterOrder() {
return 5; // filter执行顺序,通过数字指定 ,优先级为0,数字越大,优先级越低
}
@Override
public boolean shouldFilter() {
return true; // 是否执行该过滤器,此处为true,说明需要过滤
}
@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
log.info("我是ZuulFilter");
String token = request.getHeader("name");// 获取请求的参数
// 如果有name参数并且token值为zuul,才进行路由
if (StringUtils.isNotBlank(token) && token.equals("zuul")) {
ctx.setSendZuulResponse(true); //对请求进行路由
ctx.setResponseStatusCode(200);
ctx.set("code", 1);
} else {
ctx.setSendZuulResponse(false); //不对其进行路由
ctx.setResponseStatusCode(401);
HttpServletResponse response = ctx.getResponse();
response.setHeader("content-type", "text/html;charset=utf8");
ctx.setResponseBody("请携带网关必须参数");
ctx.set("code", 0);
}
return null;
}
}
6.2 添加过滤器配置
@Bean
public ZuulFilter zuulFilter(){
return new ZuulFilter();
}
6.3 重启项目
浏览器或Postman
输入localhost:8679/ribbon/user/1
首先全部设置正确的header
并且我们看到控制台输出的日志,确实是filterOrder
越小越先执行
2020-01-04 18:14:11.379 INFO 10940 --- [nio-8679-exec-2] c.yisu.gateways.zuul.filter.TokenFilter : 我是TokenFilter
2020-01-04 18:14:11.385 INFO 10940 --- [nio-8679-exec-2] c.yisu.gateways.zuul.filter.ZuulFilter : 我是ZuulFilter
去掉token 验证
发现虽然验证失败,按理说应该不会执行第二个过滤器,但是执行了,看控制台日志
2020-01-04 18:23:49.714 INFO 10940 --- [nio-8679-exec-5] c.yisu.gateways.zuul.filter.TokenFilter : 我是TokenFilter
2020-01-04 18:23:49.715 INFO 10940 --- [nio-8679-exec-5] c.yisu.gateways.zuul.filter.ZuulFilter : 我是ZuulFilter
原因是什么呢?
Zuul
中Filter
的执行逻辑如下:在ZuuLServlet
中的service
方法
中执行对应的Filter
,比如preRoute()
。preRoute()
中会通过zuulRunner
来执行
首先请求经过ZuulServlet
执行
@Override
public void service(javax.servlet.ServletRequest servletRequest, javax.servlet.ServletResponse servletResponse) throws ServletException, IOException {
try {
init((HttpServletRequest) servletRequest, (HttpServletResponse) servletResponse);
// Marks this request as having passed through the "Zuul engine", as opposed to servlets
// explicitly bound in web.xml, for which requests will not have the same data attached
RequestContext context = RequestContext.getCurrentContext();
context.setZuulEngineRan();
try {
preRoute();
} catch (ZuulException e) {
error(e);
postRoute();
return;
}
try {
route();
} catch (ZuulException e) {
error(e);
postRoute();
return;
}
try {
postRoute();
} catch (ZuulException e) {
error(e);
return;
}
} catch (Throwable e) {
error(new ZuulException(e, 500, "UNHANDLED_EXCEPTION_" + e.getClass().getName()));
} finally {
RequestContext.getCurrentContext().unset();
}
}
执行pre
类型的过滤器
/**
* executes "pre" filters
*
* @throws ZuulException
*/
void preRoute() throws ZuulException {
zuulRunner.preRoute();
}
调用FilterProcessor
的preRoute()
public void preRoute() throws ZuulException {
FilterProcessor.getInstance().preRoute();
}
然后preRoute()
调用runFilters()
获取所有过滤器并执行
public void preRoute() throws ZuulException {
try {
this.runFilters("pre");
} catch (ZuulException var2) {
throw var2;
} catch (Throwable var3) {
throw new ZuulException(var3, 500, "UNCAUGHT_EXCEPTION_IN_PRE_FILTER_" + var3.getClass().getName());
}
}
public Object runFilters(String sType) throws Throwable {
if (RequestContext.getCurrentContext().debugRouting()) {
Debug.addRoutingDebug("Invoking {" + sType + "} type filters");
}
boolean bResult = false;
List<ZuulFilter> list = FilterLoader.getInstance().getFiltersByType(sType);
if (list != null) {
for(int i = 0; i < list.size(); ++i) {
ZuulFilter zuulFilter = (ZuulFilter)list.get(i);
Object result = this.processZuulFilter(zuulFilter);
if (result != null && result instanceof Boolean) {
bResult |= (Boolean)result;
}
}
}
return bResult;
}
由此,可以知道为什么第一个报错了,为什么第二个过滤器也执行。
那么如何让第一个停了之后不执行第二个过滤器了呢?请看下一节数据传递
7. 数据传递
本节代码地址
GitHub: https://github.com/xuyisu/fw-sping-cloud/tree/master/fw-cloud-gateways/fw-cloud-gateways-zuul-simple
GitHub: https://github.com/xuyisu/fw-sping-cloud/tree/master/fw-cloud-ribbon/fw-cloud-ribbon-server
上节我们说过过滤器的拦截顺序,现在继续分析拦截器如何在第一个没通过,就不会执行第二个。
7.1 分析源码
通过分析Zuul 的这段源码,可以看出,先判断这个过滤器是不是已经禁用了,禁用的话不执行,然后判断shouldFilter()
,我们可以通过设置shouldFilter()
的true、false
来控制过滤器的执行。
public ZuulFilterResult runFilter() {
ZuulFilterResult zr = new ZuulFilterResult();
if (!this.isFilterDisabled()) {
if (this.shouldFilter()) {
Tracer t = TracerFactory.instance().startMicroTracer("ZUUL::" + this.getClass().getSimpleName());
try {
Object res = this.run();
zr = new ZuulFilterResult(res, ExecutionStatus.SUCCESS);
} catch (Throwable var7) {
t.setName("ZUUL::" + this.getClass().getSimpleName() + " failed");
zr = new ZuulFilterResult(ExecutionStatus.FAILED);
zr.setException(var7);
} finally {
t.stopAndLog();
}
} else {
zr = new ZuulFilterResult(ExecutionStatus.SKIPPED);
}
}
return zr;
}
7.2 数据传递
因此我们可以在第一个失败的时候,通过RequestContext传递变量,为什么用RequestContext,主要还是因为RequestContext的实现原理是基于ThreadLocal实现的,源码如下
private static RequestContext testContext = null;
protected static final ThreadLocal<? extends RequestContext> threadLocal = new ThreadLocal<RequestContext>() {
protected RequestContext initialValue() {
try {
return (RequestContext)RequestContext.contextClass.newInstance();
} catch (Throwable var2) {
throw new RuntimeException(var2);
}
}
};
在TokenFilter中设置如下值。
//失败之后通知后续不应该执行了
ctx.set("isShould",false);
将ZuulFilter 中的shouldFilter()修改成如下。
@Override
public boolean shouldFilter() {
RequestContext requestContext=RequestContext.getCurrentContext();
Boolean isShould = (Boolean) requestContext.get("isShould");
return null==isShould?true:isShould; // 是否执行该过滤器,此处为true,说明需要过滤
}
7.3 项目重启
浏览器或Postman 输入localhost:8679/ribbon/user/1
控制台中我可以看到只输出一条日志了
2020-01-03 21:44:02.236 INFO 19344 --- [nio-8679-exec-1] c.yisu.gateways.zuul.filter.TokenFilter : 我是TokenF
8.Zuul 禁用过滤器
本节代码地址
GitHub: https://github.com/xuyisu/fw-sping-cloud/tree/master/fw-cloud-gateways/fw-cloud-gateways-zuul-simple
GitHub: https://github.com/xuyisu/fw-sping-cloud/tree/master/fw-cloud-ribbon/fw-cloud-ribbon-server
如果想禁用一个Zuul过滤器,我们通过如下方式
配置中将相应的Bean 初始掉
/**
* Zuul 过滤器配置,如果不想启动,注释掉即可
*/
// @Bean
// public TokenFilter tokenFilter(){
// return new TokenFilter();
// }
在配置打开禁用的开关
TokenFilter 就是过滤器的类名zuul:
TokenFilter:
route:
disabled: true
8.1 重启项目
浏览器或Postman 输入localhost:8679/ribbon/user/1,Header 里面什么都没放
可以看到,由于我们把TokenFilter 禁用了,也就是不起作用了,仅仅ZuulFilter打印出了日志,控制台日志如下2020-01-03 21:57:24.112 INFO 18400 --- [nio-8679-exec-1] c.yisu.gateways.zuul.filter.ZuulFilter : 我是ZuulFilte
9. Zuul 异常处理
本节代码地址
GitHub: https://github.com/xuyisu/fw-sping-cloud/tree/master/fw-cloud-gateways/fw-cloud-gateways-zuul-simple
GitHub: https://github.com/xuyisu/fw-sping-cloud/tree/master/fw-cloud-ribbon/fw-cloud-ribbon-server
对于过滤器的各阶段在执行时,异常主要发生在run()方法中,Zuul 为我们提供了异常处理的过滤器,方法执行时抛出的异常会被捕获到并且调用RequestContext.setThrowable 方法设置异常,error 阶段的SendErrorFilter会判断RequestContext中是否存在异常,如果存在会执行SendErrorFilter过滤器。
SendErrorFilter过滤器在执行的时候,会将异常信息写到HttpServletRequest中,再调用RequestDispatcher 的forward方法,默认会跳转到/error页面,我们这里会实现一个/error 的接口,让错误信息可以被我们自定义成JSON输出。
9.1 新建异常过滤器
主要是通过Throwable throwable = ctx.getThrowable();
获取异常信息
/**
* @author xuyisu
* @description 异常处理过滤器
* @date 2020/1/3
*/
@Slf4j
public class ErrorFilter extends ZuulFilter {
@Override
public String filterType() {
return "error";
}
@Override
public int filterOrder() {
return 10;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() {
RequestContext ctx=RequestContext.getCurrentContext();
Throwable throwable = ctx.getThrowable();
ctx.setSendZuulResponse(false); //不对其进行路由
ctx.setResponseStatusCode(401);
HttpServletResponse response = ctx.getResponse();
response.setHeader("content-type", "text/html;charset=utf8");
ctx.setResponseBody("认证失败"+throwable.getCause().getMessage());
ctx.set("code", 500);
log.error("异常信息,{}",throwable.getCause().getMessage());
return null;
}
}
9.2 添加配置
@Bean
public ErrorFilter errorFilter(){
return new ErrorFilter();
}
9.3 在TokenFilter添加一个异常
int i=10/0;
9.4 新建异常处理处理类
FwResult是我统一封装的返回类,代码就不贴了,可以到源码里面看看。
/**
* @author xuyisu
* @description 异常处理类
* @date 2020/1/3
*/
@RestController
public class ErrorHandlerController implements ErrorController {
@Autowired
private ErrorAttributes errorAttributes;
@Override
public String getErrorPath() {
return "/error";
}
@RequestMapping("/error")
public FwResult error(HttpServletRequest request){
Map<String, Object> errorAttributes = this.errorAttributes.getErrorAttributes(new ServletWebRequest(request), true);
String message = (String) errorAttributes.get("message");
String trace = (String) errorAttributes.get("trace");
if(StrUtil.isNotBlank(trace)){
message=message+",trace is "+trace;
}
return FwResult.failed(message);
}
}
9.5 重启项目
浏览器或Postman 输入localhost:8679/ribbon/user/1
返回的异常信息正好使我们自定义的。
10. 重试机制
本节代码地址
GitHub: https://github.com/xuyisu/fw-sping-cloud/tree/master/fw-cloud-gateways/fw-cloud-gateways-zuul-simple
GitHub: https://github.com/xuyisu/fw-sping-cloud/tree/master/fw-cloud-ribbon/fw-cloud-ribbon-server
之前我们在介绍Feign,也讲到的重试机制,现在我们来说一下Zuul的重试机制,希望Zuul 在转发服务的时候,如果失败可以重试几次,可能第一次是网络抖动,或者转发到相同服务名是的其它地址上面。
spring-retry是spring提供的一个基于spring的重试框架,我们直接使用这个框架。
10.1 maven 配置添加
<!--重试包-->
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
10.2 修改应用配置
zuul:
retryable: true
这个开关默认是关闭的,开启后的配置和Ribbon 配置一样,如下
#重试
ribbon:
#配置首台服务器重试1次
MaxAutoRetries: 1
#配置其他服务器重试两次
MaxAutoRetriesNextServer: 2
#链接超时时间
ConnectTimeout: 500
#请求处理时间
ReadTimeout: 500
#每个操作都开启重试机制
OkToRetryOnAllOperations: true
这里可以直接使用Ribbon 的配置,是因为zuul 里面已经集成了Ribbon的包
10.3 在Ribbon Server添加随机延时
超过500秒就会重试
int millis = new Random().nextInt(3000);
System.out.println("client线程休眠时间:"+millis);
Thread.sleep(millis);
10.4 重启项目
浏览器或Postman 输入localhost:8679/ribbon/user/1
可以看到控制台重试的日志
这里和Feign 章节说的重试是一样的,可以回顾一下
11.Zuul FallBack回调
本节代码地址
GitHub: https://github.com/xuyisu/fw-sping-cloud/tree/master/fw-cloud-gateways/fw-cloud-gateways-zuul-simple
GitHub: https://github.com/xuyisu/fw-sping-cloud/tree/master/fw-cloud-ribbon/fw-cloud-ribbon-server
虽然上一节我们配置了重试机制,但是重试次数结束了没成功任然会失败,比如如下错误,因为Spring Cloud Zuul 里面已经集成了Hystrix,我们可以自己定义回退机制。
Caused by: java.net.SocketTimeoutException: Read timed out
11.1 新建FallBack 类
实现回退需要实现FallbackProvider
接口
/**
* @author xuyisu
* @description fallback
* @date 2020/1/4
*/
@Slf4j
@Component
public class ZuulFallBack implements FallbackProvider {
@Override
public String getRoute() {
return "*";
}
@Override
public ClientHttpResponse fallbackResponse(String route, Throwable cause) {
return new ClientHttpResponse() {
@Override
public HttpStatus getStatusCode() throws IOException {
return HttpStatus.OK;
}
@Override
public int getRawStatusCode() throws IOException {
return this.getStatusCode().value();
}
@Override
public String getStatusText() throws IOException {
return this.getStatusCode().getReasonPhrase();
}
@Override
public void close() {
}
@Override
public InputStream getBody() throws IOException {
RequestContext ctx=RequestContext.getCurrentContext();
Throwable throwable = ctx.getThrowable();
if(null!=throwable){
log.error("Zuul发生错误,{}",throwable.getCause().getMessage());
FwResult<String> byteMsg = FwResult.failed(throwable.getCause().getMessage(), "网络或服务异常");
return new ByteArrayInputStream(JSONUtil.toJsonStr(byteMsg).getBytes());
}
FwResult<String> byteMsg = FwResult.failedMsg("网络或服务异常");
return new ByteArrayInputStream(JSONUtil.toJsonStr(byteMsg).getBytes());
}
@Override
public HttpHeaders getHeaders() {
HttpHeaders headers=new HttpHeaders();
MediaType mediaType=new MediaType("application", "json", StandardCharsets.UTF_8);
headers.setContentType(mediaType);
return headers;
}
};
}
}
- getRoute 方法中返回*表示对所有服务进行回退操作,如果只想对某个服务进行回退,
那么就返回需要回退的服务名称,这个名称是注册到Eureka中的名称 - ClientHttpResponse 构造回退的内容
- getStatusCode 返回响应的状态码
- getStatusText 返回响应状态码对应的文本
- getBody 返回回退的内容
- getHeaders 返回响应的请求头信息
11.2 设置hystrix 超时时间
假设设置1秒#回退超时时间
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 1000
11.3 重启服务
浏览器或Postman 输入地址localhost:8679/ribbon/user/1,如果想快速看到,可以将fw-cloud-ribbon-server
关掉