Zuul

Zuul

服务重启zuul不能及时发现

Spring CloudLevin 回复了问题 • 4 人关注 • 4 个回复 • 238 次浏览 • 2017-09-16 12:43 • 来自相关话题

部署了一个zuul的api-gateway,自定义一个filter,但是无效

Spring Cloud程序猿DD 回复了问题 • 4 人关注 • 3 个回复 • 184 次浏览 • 2017-09-07 22:20 • 来自相关话题

使用zuul进行代理,后端应用获取静态资源时的问题

Spring Cloudkaicheng90 回复了问题 • 4 人关注 • 4 个回复 • 1195 次浏览 • 2017-09-06 16:14 • 来自相关话题

使用zuul内存泄露的问题

回复

Spring Cloudyanghuijava 发起了问题 • 1 人关注 • 0 个回复 • 242 次浏览 • 2017-08-07 10:33 • 来自相关话题

上传文件通过ZUUL网关路由有问题,小几十KB的文件OK,大文件不OK

Spring Cloudbruceouyang 回复了问题 • 5 人关注 • 5 个回复 • 1183 次浏览 • 2017-07-21 13:21 • 来自相关话题

Zuul 路由前缀

Spring Cloud程序猿DD 回复了问题 • 5 人关注 • 3 个回复 • 368 次浏览 • 2017-07-19 17:23 • 来自相关话题

zuul如何通过swagger-ui访问到其他微服务的接口文档

Spring Cloudconanli 回复了问题 • 5 人关注 • 2 个回复 • 690 次浏览 • 2017-07-19 10:11 • 来自相关话题

zuul静态资源问题

Spring Cloudxiaobaxi 回复了问题 • 4 人关注 • 2 个回复 • 339 次浏览 • 2017-07-14 10:14 • 来自相关话题

gateway层能否做业务逻辑处理?

Spring Cloudroger 回复了问题 • 4 人关注 • 3 个回复 • 586 次浏览 • 2017-06-28 10:11 • 来自相关话题

zuul反向代理异常

Spring Cloudxiaobaxi 回复了问题 • 3 人关注 • 3 个回复 • 359 次浏览 • 2017-06-28 00:00 • 来自相关话题

条新动态, 点击查看
## 自己再来回复一下
 
前两天买了两本书:didi老大的 《SpringCloud微服务实战》 和周立老大的 《SpringCloud和Docker微服务架构实战》 ,刚到。
 
在周老大的书里边有介绍zuul上传文件的具体说明

8.7 使用Zuul上... 显示全部 »
## 自己再来回复一下
 
前两天买了两本书:didi老大的 《SpringCloud微服务实战》 和周立老大的 《SpringCloud和Docker微服务架构实战》 ,刚到。
 
在周老大的书里边有介绍zuul上传文件的具体说明

8.7 使用Zuul上传文件

 
这里简述一下规则:
1.通过网关上传小于1MB的文件,无须任何处理,即可正常上传;
 
2.大文件,需要在上传路径之前添加/zuul,或者使用zuul.servlet-path自定义前缀。
例如: //zuul-config
zuul.routes.ms-fileupload = /ms-fileupload/**

//fileupload request path

//for small-file-upload
http://gateway-server/ms-fileupload/upload

//for large-file-upload
http://gateway-server:port/zuul/ms-fileupload/upload
 
3.如果zuul使用了Ribbon做负载均衡,对于大文件上传,需要提升超时设置
例如: hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds: 60000
ribbon:
ConnectionTimeout: 3000
ReadTimeout: 6000
4.在上传文件的微服务的配置application.yml里边也有文件大小的配置要修改
例如: spring:
http:
multipart:
max-file-size: 2000Mb # Max file size, default 1MB
max-request-size: 2500Mb # Max request size, default 10MB
 

服务重启zuul不能及时发现

回复

Spring CloudLevin 回复了问题 • 4 人关注 • 4 个回复 • 238 次浏览 • 2017-09-16 12:43 • 来自相关话题

部署了一个zuul的api-gateway,自定义一个filter,但是无效

回复

Spring Cloud程序猿DD 回复了问题 • 4 人关注 • 3 个回复 • 184 次浏览 • 2017-09-07 22:20 • 来自相关话题

使用zuul进行代理,后端应用获取静态资源时的问题

回复

Spring Cloudkaicheng90 回复了问题 • 4 人关注 • 4 个回复 • 1195 次浏览 • 2017-09-06 16:14 • 来自相关话题

使用zuul内存泄露的问题

回复

Spring Cloudyanghuijava 发起了问题 • 1 人关注 • 0 个回复 • 242 次浏览 • 2017-08-07 10:33 • 来自相关话题

上传文件通过ZUUL网关路由有问题,小几十KB的文件OK,大文件不OK

回复

Spring Cloudbruceouyang 回复了问题 • 5 人关注 • 5 个回复 • 1183 次浏览 • 2017-07-21 13:21 • 来自相关话题

Zuul 路由前缀

回复

Spring Cloud程序猿DD 回复了问题 • 5 人关注 • 3 个回复 • 368 次浏览 • 2017-07-19 17:23 • 来自相关话题

zuul如何通过swagger-ui访问到其他微服务的接口文档

回复

Spring Cloudconanli 回复了问题 • 5 人关注 • 2 个回复 • 690 次浏览 • 2017-07-19 10:11 • 来自相关话题

zuul静态资源问题

回复

Spring Cloudxiaobaxi 回复了问题 • 4 人关注 • 2 个回复 • 339 次浏览 • 2017-07-14 10:14 • 来自相关话题

gateway层能否做业务逻辑处理?

回复

Spring Cloudroger 回复了问题 • 4 人关注 • 3 个回复 • 586 次浏览 • 2017-06-28 10:11 • 来自相关话题

zuul反向代理异常

回复

Spring Cloudxiaobaxi 回复了问题 • 3 人关注 • 3 个回复 • 359 次浏览 • 2017-06-28 00:00 • 来自相关话题

Zuul的高可用

Spring Clouditmuch.com 发表了文章 • 3 个评论 • 454 次浏览 • 2017-06-06 20:49 • 来自相关话题

近期挺多朋友问到Zuul如何高可用,这里详细探讨一下。

Zuul的高可用非常关键,因为外部请求到后端微服务的流量都会经过Zuul。故而在生产环境中,我们一般都需要部署高可用的Zuul以避免单点故障。

笔者分两种场景讨论Zuul的高可用。

Zuul客户端也注册到了Eureka Server上

这种情况下,Zuul的高可用非常简单,只需将多个Zuul节点注册到Eureka Server上,就可实现Zuul的高可用。此时,Zuul的高可用与其他微服务的高可用没什么区别。

图8-7 Zuul高可用架构图

如图8-7,当Zuul客户端也注册到Eureka Server上时,只需部署多个Zuul节点即可实现其高可用。Zuul客户端会自动从Eureka Server中查询Zuul Server的列表,并使用Ribbon负载均衡地请求Zuul集群。

这种场景一般用于Sidecar。

Zuul客户端未注册到Eureka Server上

现实中,这种场景往往更常见,例如,Zuul客户端是一个手机APP——我们不可能让所有的手机终端都注册到Eureka Server上。这种情况下,我们可借助一个额外的负载均衡器来实现Zuul的高可用,例如Nginx、HAProxy、F5等。

图8-8 Zuul高可用架构图

如图8-8,Zuul客户端将请求发送到负载均衡器,负载均衡器将请求转发到其代理的其中一个Zuul节点。这样,就可以实现Zuul的高可用。

节选自《Spring Cloud与Docker微服务架构实战》8.10节

版权说明

本文采用 CC BY 3.0 CN协议 进行许可。 可自由转载、引用,但需署名作者且注明文章出处。如转载至微信公众号,请在文末添加作者公众号二维码。

本文作者:周立
博客:http://www.itmuch.com 
版权归作者所有,转载请注明出处 查看全部
近期挺多朋友问到Zuul如何高可用,这里详细探讨一下。

Zuul的高可用非常关键,因为外部请求到后端微服务的流量都会经过Zuul。故而在生产环境中,我们一般都需要部署高可用的Zuul以避免单点故障。

笔者分两种场景讨论Zuul的高可用。

Zuul客户端也注册到了Eureka Server上


这种情况下,Zuul的高可用非常简单,只需将多个Zuul节点注册到Eureka Server上,就可实现Zuul的高可用。此时,Zuul的高可用与其他微服务的高可用没什么区别。

图8-7 Zuul高可用架构图

如图8-7,当Zuul客户端也注册到Eureka Server上时,只需部署多个Zuul节点即可实现其高可用。Zuul客户端会自动从Eureka Server中查询Zuul Server的列表,并使用Ribbon负载均衡地请求Zuul集群。

这种场景一般用于Sidecar。

Zuul客户端未注册到Eureka Server上

现实中,这种场景往往更常见,例如,Zuul客户端是一个手机APP——我们不可能让所有的手机终端都注册到Eureka Server上。这种情况下,我们可借助一个额外的负载均衡器来实现Zuul的高可用,例如Nginx、HAProxy、F5等。

图8-8 Zuul高可用架构图

如图8-8,Zuul客户端将请求发送到负载均衡器,负载均衡器将请求转发到其代理的其中一个Zuul节点。这样,就可以实现Zuul的高可用。

节选自《Spring Cloud与Docker微服务架构实战》8.10节

版权说明

本文采用 CC BY 3.0 CN协议 进行许可。 可自由转载、引用,但需署名作者且注明文章出处。如转载至微信公众号,请在文末添加作者公众号二维码。


本文作者:周立
博客:http://www.itmuch.com 
版权归作者所有,转载请注明出处


Spring Cloud实战小贴士:Zuul处理Cookie和重定向

Spring Cloud程序猿DD 发表了文章 • 5 个评论 • 456 次浏览 • 2017-05-29 20:57 • 来自相关话题

由于我们在之前所有的入门教程中,对于HTTP请求都采用了简单的接口实现。而实际使用过程中,我们的HTTP请求要复杂的多,比如当我们将Spring Cloud Zuul作为API网关接入网站类应用时,往往都会碰到下面这两个非常常见的问题:

- 会话无法保持
- 重定向后的HOST错误

本文将帮助大家分析问题原因并给出解决这两个常见问题的方法。

会话保持问题

通过跟踪一个HTTP请求经过Zuul到具体服务,再到返回结果的全过程。我们很容易就能发现,在传递的过程中,HTTP请求头信息中的Cookie和Authorization都没有被正确地传递给具体服务,所以最终导致会话状态没有得到保持的现象。

那么这些信息是在哪里丢失的呢?我们从Zuul进行路由转发的过滤器作为起点,来一探究竟。下面是`RibbonRoutingFilter`过滤器的实现片段:$(document).ready(function() {$('pre code').each(function(i, block) { hljs.highlightBlock( block); }); });public class RibbonRoutingFilter extends ZuulFilter {
...
protected ProxyRequestHelper helper;

@Override
public Object run() {
RequestContext context = RequestContext.getCurrentContext();
this.helper.addIgnoredHeaders();
try {
RibbonCommandContext commandContext = buildCommandContext(context);
ClientHttpResponse response = forward(commandContext);
setResponse(response);
return response;
}
...
return null;
}

protected RibbonCommandContext buildCommandContext(RequestContext context) {
HttpServletRequest request = context.getRequest();

MultiValueMap<String, String> headers = this.helper
.buildZuulRequestHeaders(request);
MultiValueMap<String, String> params = this.helper
.buildZuulRequestQueryParams(request);
...
}
}
这里有三个重要元素:

- 过滤器的核心逻辑`run`函数实现,其中调用了内部函数`buildCommandContext`来构建上下文内容
- 而`buildCommandContext`中调用了`helper`对象的`buildZuulRequestHeaders`方法来处理请求头信息
- `helper`对象是`ProxyRequestHelper`类的实例

接下来我们再看看`ProxyRequestHelper`的实现:public class ProxyRequestHelper {

public MultiValueMap<String, String> buildZuulRequestHeaders(
HttpServletRequest request) {
RequestContext context = RequestContext.getCurrentContext();
MultiValueMap<String, String> headers = new HttpHeaders();
Enumeration<String> headerNames = request.getHeaderNames();
if (headerNames != null) {
while (headerNames.hasMoreElements()) {
String name = headerNames.nextElement();
if (isIncludedHeader(name)) {
Enumeration<String> values = request.getHeaders(name);
while (values.hasMoreElements()) {
String value = values.nextElement();
headers.add(name, value);
}
}
}
}
Map<String, String> zuulRequestHeaders = context.getZuulRequestHeaders();
for (String header : zuulRequestHeaders.keySet()) {
headers.set(header, zuulRequestHeaders.get(header));
}
headers.set(HttpHeaders.ACCEPT_ENCODING, "gzip");
return headers;
}

public boolean isIncludedHeader(String headerName) {
String name = headerName.toLowerCase();
RequestContext ctx = RequestContext.getCurrentContext();
if (ctx.containsKey(IGNORED_HEADERS)) {
Object object = ctx.get(IGNORED_HEADERS);
if (object instanceof Collection && ((Collection<?>) object).contains(name)) {
return false;
}
}
...
}
}
从上述源码中,我们可以看到构建头信息的方法`buildZuulRequestHeaders`通过`isIncludedHeader`函数来判断当前请求的各个头信息是否在忽略的头信息清单中,如果是的话就不组织到此次转发的请求中去。那么这些需要忽略的头信息是在哪里初始化的呢?在PRE阶段的PreDecorationFilter过滤器中,我们可以找到答案:public class PreDecorationFilter extends ZuulFilter {
...
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
final String requestURI = this.urlPathHelper.getPathWithinApplication(ctx.getRequest());
Route route = this.routeLocator.getMatchingRoute(requestURI);
if (route != null) {
String location = route.getLocation();
if (location != null) {
ctx.put("requestURI", route.getPath());
ctx.put("proxy", route.getId());
// 处理忽略头信息的部分
if (!route.isCustomSensitiveHeaders()) {
this.proxyRequestHelper.addIgnoredHeaders(
this.properties.getSensitiveHeaders()
.toArray(new String[0]));
} else {
this.proxyRequestHelper.addIgnoredHeaders(
route.getSensitiveHeaders()
.toArray(new String[0]));
}
...
}
从上述源码中,我们可以看到有一段if/else块,通过调用`ProxyRequestHelper`的`addIgnoredHeaders`方法来添加需要忽略的信息到请求上下文中,供后续ROUTE阶段的过滤器使用。这里的if/else块分别用来处理全局设置的敏感头信息和指定路由设置的敏感头信息。而全局的敏感头信息定义于`ZuulProperties`中:@Data
@ConfigurationProperties("zuul")
public class ZuulProperties {
private Set<String> sensitiveHeaders = new LinkedHashSet<>(
Arrays.asList("Cookie", "Set-Cookie", "Authorization"));
...
}
所以解决该问题的思路也很简单,我们只需要通过设置sensitiveHeaders即可,设置方法分为两种:
- 全局设置:
  - zuul.sensitive-headers=
- 指定路由设置:
  - zuul.routes.<routeName>.sensitive-headers=
  - zuul.routes.<routeName>.custom-sensitive-headers=true

重定向问题

在使用Spring Cloud Zuul对接Web网站的时候,处理完了会话控制问题之后。往往我们还会碰到如下图所示的问题,我们在浏览器中通过Zuul发起了登录请求,该请求会被路由到某WebSite服务,该服务在完成了登录处理之后,会进行重定向到某个主页或欢迎页面。此时,仔细的开发者会发现,在登录完成之后,我们浏览器中URL的HOST部分发生的改变,该地址变成了具体WebSite服务的地址了。这就是在这一节,我们将分析和解决的重定向问题!



出现该问题的根源是Spring Cloud Zuul没有正确的处理HTTP请求头信息中的Host导致。在Brixton版本中,Spring Cloud Zuul的`PreDecorationFilter`过滤器实现时完全没有考虑这一问题,它更多的定位于REST API的网关。所以如果要在Brixton版本中增加这一特性就相对较为复杂,不过好在Camden版本之后,Spring Cloud Netflix 1.2.x版本的Zuul增强了该功能,我们只需要通过配置属性`zuul.add-host-header=true`就能让原本有问题的重定向操作得到正确的处理。关于更多Host头信息的处理,读者可以参考本文之前的分析思路,可以通过查看`PreDecorationFilter`过滤器的源码来详细更多实现细节。
 

本文作者:程序猿DD-翟永超
原文链接:Spring Cloud实战小贴士:Zuul处理Cookie和重定向
版权归作者所有,转载请注明出处
  查看全部
由于我们在之前所有的入门教程中,对于HTTP请求都采用了简单的接口实现。而实际使用过程中,我们的HTTP请求要复杂的多,比如当我们将Spring Cloud Zuul作为API网关接入网站类应用时,往往都会碰到下面这两个非常常见的问题:

- 会话无法保持
- 重定向后的HOST错误

本文将帮助大家分析问题原因并给出解决这两个常见问题的方法。

会话保持问题

通过跟踪一个HTTP请求经过Zuul到具体服务,再到返回结果的全过程。我们很容易就能发现,在传递的过程中,HTTP请求头信息中的Cookie和Authorization都没有被正确地传递给具体服务,所以最终导致会话状态没有得到保持的现象。

那么这些信息是在哪里丢失的呢?我们从Zuul进行路由转发的过滤器作为起点,来一探究竟。下面是`RibbonRoutingFilter`过滤器的实现片段:
public class RibbonRoutingFilter extends ZuulFilter {
...
protected ProxyRequestHelper helper;

@Override
public Object run() {
RequestContext context = RequestContext.getCurrentContext();
this.helper.addIgnoredHeaders();
try {
RibbonCommandContext commandContext = buildCommandContext(context);
ClientHttpResponse response = forward(commandContext);
setResponse(response);
return response;
}
...
return null;
}

protected RibbonCommandContext buildCommandContext(RequestContext context) {
HttpServletRequest request = context.getRequest();

MultiValueMap<String, String> headers = this.helper
.buildZuulRequestHeaders(request);
MultiValueMap<String, String> params = this.helper
.buildZuulRequestQueryParams(request);
...
}
}

这里有三个重要元素:

- 过滤器的核心逻辑`run`函数实现,其中调用了内部函数`buildCommandContext`来构建上下文内容
- 而`buildCommandContext`中调用了`helper`对象的`buildZuulRequestHeaders`方法来处理请求头信息
- `helper`对象是`ProxyRequestHelper`类的实例

接下来我们再看看`ProxyRequestHelper`的实现:
public class ProxyRequestHelper {

public MultiValueMap<String, String> buildZuulRequestHeaders(
HttpServletRequest request) {
RequestContext context = RequestContext.getCurrentContext();
MultiValueMap<String, String> headers = new HttpHeaders();
Enumeration<String> headerNames = request.getHeaderNames();
if (headerNames != null) {
while (headerNames.hasMoreElements()) {
String name = headerNames.nextElement();
if (isIncludedHeader(name)) {
Enumeration<String> values = request.getHeaders(name);
while (values.hasMoreElements()) {
String value = values.nextElement();
headers.add(name, value);
}
}
}
}
Map<String, String> zuulRequestHeaders = context.getZuulRequestHeaders();
for (String header : zuulRequestHeaders.keySet()) {
headers.set(header, zuulRequestHeaders.get(header));
}
headers.set(HttpHeaders.ACCEPT_ENCODING, "gzip");
return headers;
}

public boolean isIncludedHeader(String headerName) {
String name = headerName.toLowerCase();
RequestContext ctx = RequestContext.getCurrentContext();
if (ctx.containsKey(IGNORED_HEADERS)) {
Object object = ctx.get(IGNORED_HEADERS);
if (object instanceof Collection && ((Collection<?>) object).contains(name)) {
return false;
}
}
...
}
}

从上述源码中,我们可以看到构建头信息的方法`buildZuulRequestHeaders`通过`isIncludedHeader`函数来判断当前请求的各个头信息是否在忽略的头信息清单中,如果是的话就不组织到此次转发的请求中去。那么这些需要忽略的头信息是在哪里初始化的呢?在PRE阶段的PreDecorationFilter过滤器中,我们可以找到答案:
public class PreDecorationFilter extends ZuulFilter {
...
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
final String requestURI = this.urlPathHelper.getPathWithinApplication(ctx.getRequest());
Route route = this.routeLocator.getMatchingRoute(requestURI);
if (route != null) {
String location = route.getLocation();
if (location != null) {
ctx.put("requestURI", route.getPath());
ctx.put("proxy", route.getId());
// 处理忽略头信息的部分
if (!route.isCustomSensitiveHeaders()) {
this.proxyRequestHelper.addIgnoredHeaders(
this.properties.getSensitiveHeaders()
.toArray(new String[0]));
} else {
this.proxyRequestHelper.addIgnoredHeaders(
route.getSensitiveHeaders()
.toArray(new String[0]));
}
...
}

从上述源码中,我们可以看到有一段if/else块,通过调用`ProxyRequestHelper`的`addIgnoredHeaders`方法来添加需要忽略的信息到请求上下文中,供后续ROUTE阶段的过滤器使用。这里的if/else块分别用来处理全局设置的敏感头信息和指定路由设置的敏感头信息。而全局的敏感头信息定义于`ZuulProperties`中:
@Data
@ConfigurationProperties("zuul")
public class ZuulProperties {
private Set<String> sensitiveHeaders = new LinkedHashSet<>(
Arrays.asList("Cookie", "Set-Cookie", "Authorization"));
...
}

所以解决该问题的思路也很简单,我们只需要通过设置sensitiveHeaders即可,设置方法分为两种:
- 全局设置:
  - zuul.sensitive-headers=
- 指定路由设置:
  - zuul.routes.<routeName>.sensitive-headers=
  - zuul.routes.<routeName>.custom-sensitive-headers=true

重定向问题

在使用Spring Cloud Zuul对接Web网站的时候,处理完了会话控制问题之后。往往我们还会碰到如下图所示的问题,我们在浏览器中通过Zuul发起了登录请求,该请求会被路由到某WebSite服务,该服务在完成了登录处理之后,会进行重定向到某个主页或欢迎页面。此时,仔细的开发者会发现,在登录完成之后,我们浏览器中URL的HOST部分发生的改变,该地址变成了具体WebSite服务的地址了。这就是在这一节,我们将分析和解决的重定向问题!



出现该问题的根源是Spring Cloud Zuul没有正确的处理HTTP请求头信息中的Host导致。在Brixton版本中,Spring Cloud Zuul的`PreDecorationFilter`过滤器实现时完全没有考虑这一问题,它更多的定位于REST API的网关。所以如果要在Brixton版本中增加这一特性就相对较为复杂,不过好在Camden版本之后,Spring Cloud Netflix 1.2.x版本的Zuul增强了该功能,我们只需要通过配置属性`zuul.add-host-header=true`就能让原本有问题的重定向操作得到正确的处理。关于更多Host头信息的处理,读者可以参考本文之前的分析思路,可以通过查看`PreDecorationFilter`过滤器的源码来详细更多实现细节。
 


本文作者:程序猿DD-翟永超
原文链接:Spring Cloud实战小贴士:Zuul处理Cookie和重定向
版权归作者所有,转载请注明出处
 


Spring Cloud实战小贴士:Zuul统一异常处理(二)

Spring Cloud程序猿DD 发表了文章 • 2 个评论 • 356 次浏览 • 2017-05-28 15:38 • 来自相关话题

在前几天发布的《Spring Cloud实战小贴士:Zuul统一异常处理(一)》一文中,我们详细说明了当Zuul的过滤器中抛出异常时会发生客户端没有返回任何内容的问题以及针对这个问题的两种解决方案:一种是通过在各个阶段的过滤器中增加`try-catch`块,实现过滤器内部的异常处理;另一种是利用`error`类型过滤器的生命周期特性,集中地处理`pre`、`route`、`post`阶段抛出的异常信息。通常情况下,我们可以将这两种手段同时使用,其中第一种是对开发人员的基本要求;而第二种是对第一种处理方式的补充,以防止一些意外情况的发生。这样的异常处理机制看似已经完美,但是如果在多一些应用实践或源码分析之后,我们会发现依然存在一些不足。

不足之处

下面,我们不妨跟着源码来看看,到底上面的方案还有哪些不足之处需要我们注意和进一步优化的。先来看看外部请求到达API网关服务之后,各个阶段的过滤器是如何进行调度的: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;
}

上面代码源自`com.netflix.zuul.http.ZuulServlet`的`service`方法实现,它定义了Zuul处理外部请求过程时,各个类型过滤器的执行逻辑。从代码中我们可以看到三个`try-catch`块,它们依次分别代表了`pre`、`route`、`post`三个阶段的过滤器调用,在`catch`的异常处理中我们可以看到它们都会被`error`类型的过滤器进行处理(之前使用`error`过滤器来定义统一的异常处理也正是利用了这个特性);`error`类型的过滤器处理完毕之后,除了来自`post`阶段的异常之外,都会再被`post`过滤器进行处理。而对于从`post`过滤器中抛出异常的情况,在经过了`error`过滤器处理之后,就没有其他类型的过滤器来接手了,这就是使用之前所述方案存在不足之处的根源。

问题分析与进一步优化

回想一下之前实现的两种异常处理方法,其中非常核心的一点,这两种处理方法都在异常处理时候往请求上下文中添加了一系列的`error.*`参数,而这些参数真正起作用的地方是在`post`阶段的`SendErrorFilter`,在该过滤器中会使用这些参数来组织内容返回给客户端。而对于`post`阶段抛出异常的情况下,由`error`过滤器处理之后并不会在调用`post`阶段的请求,自然这些`error.*`参数也就不会被`SendErrorFilter`消费输出。所以,如果我们在自定义`post`过滤器的时候,没有正确的处理异常,就依然有可能出现日志中没有异常并且请求响应内容为空的问题。我们可以通过修改之前`ThrowExceptionFilter`的`filterType`修改为`post`来验证这个问题的存在,注意去掉`try-catch`块的处理,让它能够抛出异常。

解决上述问题的方法有很多种,比如最直接的我们可以在实现`error`过滤器的时候,直接来组织结果返回就能实现效果,但是这样的缺点也很明显,对于错误信息组织和返回的代码实现就会存在多份,这样非常不易于我们日后的代码维护工作。所以为了保持对异常返回处理逻辑的一致,我们还是希望将`post`过滤器抛出的异常能够交给`SendErrorFilter`来处理。

在前文中,我们已经实现了一个`ErrorFilter`来捕获`pre`、`route`、`post`过滤器抛出的异常,并组织`error.*`参数保存到请求的上下文中。由于我们的目标是沿用`SendErrorFilter`,这些`error.*`参数依然对我们有用,所以我们可以继续沿用该过滤器,让它在`post`过滤器抛出异常的时候,继续组织`error.*`参数,只是这里我们已经无法将这些`error.*`参数再传递给`SendErrorFitler`过滤器来处理了。所以,我们需要在`ErrorFilter`过滤器之后再定义一个`error`类型的过滤器,让它来实现`SendErrorFilter`的功能,但是这个`error`过滤器并不需要处理所有出现异常的情况,它仅对`post`过滤器抛出的异常才有效。根据上面的思路,我们完全可以创建一个继承自`SendErrorFilter`的过滤器,就能复用它的`run`方法,然后重写它的类型、顺序以及执行条件,实现对原有逻辑的复用,具体实现如下:@Component
public class ErrorExtFilter extends SendErrorFilter {

@Override
public String filterType() {
return "error";
}

@Override
public int filterOrder() {
return 30; // 大于ErrorFilter的值
}

@Override
public boolean shouldFilter() {
// TODO 判断:仅处理来自post过滤器引起的异常
return true;
}

}
到这里,我们在过滤器调度上的实现思路已经很清晰了,但是又有一个问题出现在我们面前:怎么判断引起异常的过滤器是来自什么阶段呢?(`shouldFilter`方法该如何实现)对于这个问题,我们第一反应会寄希望于请求上下文`RequestContext`对象,可是在查阅文档和源码后发现其中并没有存储异常来源的内容,所以我们不得不扩展原来的过滤器处理逻辑,当有异常抛出的时候,记录下抛出异常的过滤器,这样我们就可以在`ErrorExtFilter`过滤器的`shouldFilter`方法中获取并以此判断异常是否来自`post`阶段的过滤器了。

为了扩展过滤器的处理逻辑,为请求上下文增加一些自定义属性,我们需要深入了解一下Zuul过滤器的核心处理器:`com.netflix.zuul.FilterProcessor`。该类中定义了下面过滤器调用和处理相关的核心方法:

- `getInstance()`:该方法用来获取当前处理器的实例
- `setProcessor(FilterProcessor processor)`:该方法用来设置处理器实例,可以使用此方法来设置自定义的处理器
- `processZuulFilter(ZuulFilter filter)`:该方法定义了用来执行`filter`的具体逻辑,包括对请求上下文的设置,判断是否应该执行,执行时一些异常处理等
- `getFiltersByType(String filterType)`:该方法用来根据传入的`filterType`获取API网关中对应类型的过滤器,并根据这些过滤器的`filterOrder`从小到大排序,组织成一个列表返回
- `runFilters(String sType)`:该方法会根据传入的`filterType`来调用`getFiltersByType(String filterType)`获取排序后的过滤器列表,然后轮询这些过滤器,并调用`processZuulFilter(ZuulFilter filter)`来依次执行它们
- `preRoute()`:调用`runFilters("pre")`来执行所有`pre`类型的过滤器
- `route()`:调用`runFilters("route")`来执行所有`route`类型的过滤器
- `postRoute()`:调用`runFilters("post")`来执行所有`post`类型的过滤器
- `error()`:调用`runFilters("error")`来执行所有`error`类型的过滤器

根据我们之前的设计,我们可以直接通过扩展`processZuulFilter(ZuulFilter filter)`方法,当过滤器执行抛出异常的时候,我们捕获它,并往请求上下中记录一些信息。比如下面的具体实现:public class DidiFilterProcessor extends FilterProcessor {

@Override
public Object processZuulFilter(ZuulFilter filter) throws ZuulException {
try {
return super.processZuulFilter(filter);
} catch (ZuulException e) {
RequestContext ctx = RequestContext.getCurrentContext();
ctx.set("failed.filter", filter);
throw e;
}
}

}
在上面代码的实现中,我们创建了一个`FilterProcessor`的子类,并重写了`processZuulFilter(ZuulFilter filter)`,虽然主逻辑依然使用了父类的实现,但是在最外层,我们为其增加了异常捕获,并在异常处理中为请求上下文添加了`failed.filter`属性,以存储抛出异常的过滤器实例。在实现了这个扩展之后,我们也就可以完善之前`ErrorExtFilter`中的`shouldFilter()`方法,通过从请求上下文中获取该信息作出正确的判断,具体实现如下:@Component
public class ErrorExtFilter extends SendErrorFilter {

@Override
public String filterType() {
return "error";
}

@Override
public int filterOrder() {
return 30; // 大于ErrorFilter的值
}

@Override
public boolean shouldFilter() {
// 判断:仅处理来自post过滤器引起的异常
RequestContext ctx = RequestContext.getCurrentContext();
ZuulFilter failedFilter = (ZuulFilter) ctx.get("failed.filter");
if(failedFilter != null && failedFilter.filterType().equals("post")) {
return true;
}
return false;
}

}
到这里,我们的优化任务还没有完成,因为扩展的过滤器处理类并还没有生效。最后,我们需要在应用主类中,通过调用`FilterProcessor.setProcessor(new DidiFilterProcessor());`方法来启用自定义的核心处理器以完成我们的优化目标。
 

本文作者:程序猿DD-翟永超 
原文链接:Spring Cloud实战小贴士:Zuul统一异常处理(二) 
版权归作者所有,转载请注明出处
 

 
  查看全部
在前几天发布的《Spring Cloud实战小贴士:Zuul统一异常处理(一)》一文中,我们详细说明了当Zuul的过滤器中抛出异常时会发生客户端没有返回任何内容的问题以及针对这个问题的两种解决方案:一种是通过在各个阶段的过滤器中增加`try-catch`块,实现过滤器内部的异常处理;另一种是利用`error`类型过滤器的生命周期特性,集中地处理`pre`、`route`、`post`阶段抛出的异常信息。通常情况下,我们可以将这两种手段同时使用,其中第一种是对开发人员的基本要求;而第二种是对第一种处理方式的补充,以防止一些意外情况的发生。这样的异常处理机制看似已经完美,但是如果在多一些应用实践或源码分析之后,我们会发现依然存在一些不足。

不足之处

下面,我们不妨跟着源码来看看,到底上面的方案还有哪些不足之处需要我们注意和进一步优化的。先来看看外部请求到达API网关服务之后,各个阶段的过滤器是如何进行调度的:
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;
}


上面代码源自`com.netflix.zuul.http.ZuulServlet`的`service`方法实现,它定义了Zuul处理外部请求过程时,各个类型过滤器的执行逻辑。从代码中我们可以看到三个`try-catch`块,它们依次分别代表了`pre`、`route`、`post`三个阶段的过滤器调用,在`catch`的异常处理中我们可以看到它们都会被`error`类型的过滤器进行处理(之前使用`error`过滤器来定义统一的异常处理也正是利用了这个特性);`error`类型的过滤器处理完毕之后,除了来自`post`阶段的异常之外,都会再被`post`过滤器进行处理。而对于从`post`过滤器中抛出异常的情况,在经过了`error`过滤器处理之后,就没有其他类型的过滤器来接手了,这就是使用之前所述方案存在不足之处的根源。

问题分析与进一步优化

回想一下之前实现的两种异常处理方法,其中非常核心的一点,这两种处理方法都在异常处理时候往请求上下文中添加了一系列的`error.*`参数,而这些参数真正起作用的地方是在`post`阶段的`SendErrorFilter`,在该过滤器中会使用这些参数来组织内容返回给客户端。而对于`post`阶段抛出异常的情况下,由`error`过滤器处理之后并不会在调用`post`阶段的请求,自然这些`error.*`参数也就不会被`SendErrorFilter`消费输出。所以,如果我们在自定义`post`过滤器的时候,没有正确的处理异常,就依然有可能出现日志中没有异常并且请求响应内容为空的问题。我们可以通过修改之前`ThrowExceptionFilter`的`filterType`修改为`post`来验证这个问题的存在,注意去掉`try-catch`块的处理,让它能够抛出异常。

解决上述问题的方法有很多种,比如最直接的我们可以在实现`error`过滤器的时候,直接来组织结果返回就能实现效果,但是这样的缺点也很明显,对于错误信息组织和返回的代码实现就会存在多份,这样非常不易于我们日后的代码维护工作。所以为了保持对异常返回处理逻辑的一致,我们还是希望将`post`过滤器抛出的异常能够交给`SendErrorFilter`来处理。

在前文中,我们已经实现了一个`ErrorFilter`来捕获`pre`、`route`、`post`过滤器抛出的异常,并组织`error.*`参数保存到请求的上下文中。由于我们的目标是沿用`SendErrorFilter`,这些`error.*`参数依然对我们有用,所以我们可以继续沿用该过滤器,让它在`post`过滤器抛出异常的时候,继续组织`error.*`参数,只是这里我们已经无法将这些`error.*`参数再传递给`SendErrorFitler`过滤器来处理了。所以,我们需要在`ErrorFilter`过滤器之后再定义一个`error`类型的过滤器,让它来实现`SendErrorFilter`的功能,但是这个`error`过滤器并不需要处理所有出现异常的情况,它仅对`post`过滤器抛出的异常才有效。根据上面的思路,我们完全可以创建一个继承自`SendErrorFilter`的过滤器,就能复用它的`run`方法,然后重写它的类型、顺序以及执行条件,实现对原有逻辑的复用,具体实现如下:
@Component
public class ErrorExtFilter extends SendErrorFilter {

@Override
public String filterType() {
return "error";
}

@Override
public int filterOrder() {
return 30; // 大于ErrorFilter的值
}

@Override
public boolean shouldFilter() {
// TODO 判断:仅处理来自post过滤器引起的异常
return true;
}

}

到这里,我们在过滤器调度上的实现思路已经很清晰了,但是又有一个问题出现在我们面前:怎么判断引起异常的过滤器是来自什么阶段呢?(`shouldFilter`方法该如何实现)对于这个问题,我们第一反应会寄希望于请求上下文`RequestContext`对象,可是在查阅文档和源码后发现其中并没有存储异常来源的内容,所以我们不得不扩展原来的过滤器处理逻辑,当有异常抛出的时候,记录下抛出异常的过滤器,这样我们就可以在`ErrorExtFilter`过滤器的`shouldFilter`方法中获取并以此判断异常是否来自`post`阶段的过滤器了。

为了扩展过滤器的处理逻辑,为请求上下文增加一些自定义属性,我们需要深入了解一下Zuul过滤器的核心处理器:`com.netflix.zuul.FilterProcessor`。该类中定义了下面过滤器调用和处理相关的核心方法:

- `getInstance()`:该方法用来获取当前处理器的实例
- `setProcessor(FilterProcessor processor)`:该方法用来设置处理器实例,可以使用此方法来设置自定义的处理器
- `processZuulFilter(ZuulFilter filter)`:该方法定义了用来执行`filter`的具体逻辑,包括对请求上下文的设置,判断是否应该执行,执行时一些异常处理等
- `getFiltersByType(String filterType)`:该方法用来根据传入的`filterType`获取API网关中对应类型的过滤器,并根据这些过滤器的`filterOrder`从小到大排序,组织成一个列表返回
- `runFilters(String sType)`:该方法会根据传入的`filterType`来调用`getFiltersByType(String filterType)`获取排序后的过滤器列表,然后轮询这些过滤器,并调用`processZuulFilter(ZuulFilter filter)`来依次执行它们
- `preRoute()`:调用`runFilters("pre")`来执行所有`pre`类型的过滤器
- `route()`:调用`runFilters("route")`来执行所有`route`类型的过滤器
- `postRoute()`:调用`runFilters("post")`来执行所有`post`类型的过滤器
- `error()`:调用`runFilters("error")`来执行所有`error`类型的过滤器

根据我们之前的设计,我们可以直接通过扩展`processZuulFilter(ZuulFilter filter)`方法,当过滤器执行抛出异常的时候,我们捕获它,并往请求上下中记录一些信息。比如下面的具体实现:
public class DidiFilterProcessor extends FilterProcessor {

@Override
public Object processZuulFilter(ZuulFilter filter) throws ZuulException {
try {
return super.processZuulFilter(filter);
} catch (ZuulException e) {
RequestContext ctx = RequestContext.getCurrentContext();
ctx.set("failed.filter", filter);
throw e;
}
}

}

在上面代码的实现中,我们创建了一个`FilterProcessor`的子类,并重写了`processZuulFilter(ZuulFilter filter)`,虽然主逻辑依然使用了父类的实现,但是在最外层,我们为其增加了异常捕获,并在异常处理中为请求上下文添加了`failed.filter`属性,以存储抛出异常的过滤器实例。在实现了这个扩展之后,我们也就可以完善之前`ErrorExtFilter`中的`shouldFilter()`方法,通过从请求上下文中获取该信息作出正确的判断,具体实现如下:
@Component
public class ErrorExtFilter extends SendErrorFilter {

@Override
public String filterType() {
return "error";
}

@Override
public int filterOrder() {
return 30; // 大于ErrorFilter的值
}

@Override
public boolean shouldFilter() {
// 判断:仅处理来自post过滤器引起的异常
RequestContext ctx = RequestContext.getCurrentContext();
ZuulFilter failedFilter = (ZuulFilter) ctx.get("failed.filter");
if(failedFilter != null && failedFilter.filterType().equals("post")) {
return true;
}
return false;
}

}

到这里,我们的优化任务还没有完成,因为扩展的过滤器处理类并还没有生效。最后,我们需要在应用主类中,通过调用`FilterProcessor.setProcessor(new DidiFilterProcessor());`方法来启用自定义的核心处理器以完成我们的优化目标。
 


本文作者:程序猿DD-翟永超 
原文链接:Spring Cloud实战小贴士:Zuul统一异常处理(二) 
版权归作者所有,转载请注明出处
 


 
 

Spring Cloud实战小贴士:Zuul统一异常处理(一)

Spring Cloud程序猿DD 发表了文章 • 6 个评论 • 574 次浏览 • 2017-05-28 15:30 • 来自相关话题

在上一篇《Spring Cloud源码分析(四)Zuul:核心过滤器》一文中,我们详细介绍了Spring Cloud Zuul中自己实现的一些核心过滤器,以及这些过滤器在请求生命周期中的不同作用。我们会发现在这些核心过滤器中并没有实现error阶段的过滤器。那么这些过滤器可以用来做什么呢?接下来,本文将介绍如何利用error过滤器来实现统一的异常处理。

过滤器中抛出异常的问题

首先,我们可以来看看默认情况下,过滤器中抛出异常Spring Cloud Zuul会发生什么现象。我们创建一个pre类型的过滤器,并在该过滤器的run方法实现中抛出一个异常。比如下面的实现,在run方法中调用的`doSomething`方法将抛出`RuntimeException`异常。public class ThrowExceptionFilter extends ZuulFilter {

private static Logger log = LoggerFactory.getLogger(ThrowExceptionFilter.class);

@Override
public String filterType() {
return "pre";
}

@Override
public int filterOrder() {
return 0;
}

@Override
public boolean shouldFilter() {
return true;
}

@Override
public Object run() {
log.info("This is a pre filter, it will throw a RuntimeException");
doSomething();
return null;
}

private void doSomething() {
throw new RuntimeException("Exist some errors...");
}

}

运行网关程序并访问某个路由请求,此时我们会发现:在API网关服务的控制台中输出了`ThrowExceptionFilter`的过滤逻辑中的日志信息,但是并没有输出任何异常信息,同时发起的请求也没有获得任何响应结果。为什么会出现这样的情况呢?我们又该如何在过滤器中处理异常呢?

解决方案一:严格的try-catch处理

回想一下,我们在上一节中介绍的所有核心过滤器,是否还记得有一个`post`过滤器`SendErrorFilter`是用来处理异常信息的?根据正常的处理流程,该过滤器会处理异常信息,那么这里没有出现任何异常信息说明很有可能就是这个过滤器没有被执行。所以,我们不妨来详细看看`SendErrorFilter`的`shouldFilter`函数:public boolean shouldFilter() {
RequestContext ctx = RequestContext.getCurrentContext();
return ctx.containsKey("error.status_code") && !ctx.getBoolean(SEND_ERROR_FILTER_RAN, false);
}
可以看到该方法的返回值中有一个重要的判断依据`ctx.containsKey("error.status_code")`,也就是说请求上下文中必须有`error.status_code`参数,我们实现的`ThrowExceptionFilter`中并没有设置这个参数,所以自然不会进入`SendErrorFilter`过滤器的处理逻辑。那么我们要如何用这个参数呢?我们可以看一下`route`类型的几个过滤器,由于这些过滤器会对外发起请求,所以肯定会有异常需要处理,比如`RibbonRoutingFilter`的`run`方法实现如下:public Object run() {
RequestContext context = RequestContext.getCurrentContext();
this.helper.addIgnoredHeaders();
try {
RibbonCommandContext commandContext = buildCommandContext(context);
ClientHttpResponse response = forward(commandContext);
setResponse(response);
return response;
}
catch (ZuulException ex) {
context.set(ERROR_STATUS_CODE, ex.nStatusCode);
context.set("error.message", ex.errorCause);
context.set("error.exception", ex);
}
catch (Exception ex) {
context.set("error.status_code", HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
context.set("error.exception", ex);
}
return null;
}

可以看到,整个发起请求的逻辑都采用了`try-catch`块处理。在`catch`异常的处理逻辑中并没有做任何输出操作,而是往请求上下文中添加一些`error`相关的参数,主要有下面三个参数:
error.status_code:错误编码error.exception:Exception异常对象error.message:错误信息

其中,`error.status_code`参数就是`SendErrorFilter`过滤器用来判断是否需要执行的重要参数。分析到这里,实现异常处理的大致思路就开始明朗了,我们可以参考`RibbonRoutingFilter`的实现对`ThrowExceptionFilter`的`run`方法做一些异常处理的改造,具体如下:public Object run() {
log.info("This is a pre filter, it will throw a RuntimeException");
RequestContext ctx = RequestContext.getCurrentContext();
try {
doSomething();
} catch (Exception e) {
ctx.set("error.status_code", HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
ctx.set("error.exception", e);
}
return null;
}
通过上面的改造之后,我们再尝试访问之前的接口,这个时候我们可以得到如下响应内容:{
"timestamp": 1481674980376,
"status": 500,
"error": "Internal Server Error",
"exception": "java.lang.RuntimeException",
"message": "Exist some errors..."
}
此时,我们的异常信息已经被`SendErrorFilter`过滤器正常处理并返回给客户端了,同时在网关的控制台中也输出了异常信息。从返回的响应信息中,我们可以看到几个我们之前设置在请求上下文中的内容,它们的对应关系如下:

- `status`:对应`error.status_code`参数的值
- `exception`:对应`error.exception`参数中`Exception`的类型
- `message`:对应`error.exception`参数中`Exception`的`message`信息。对于`message`的信息,我们在过滤器中还可以通过`ctx.set("error.message", "自定义异常消息");`来定义更友好的错误信息。`SendErrorFilter`会优先取`error.message`来作为返回的`message`内容,如果没有的话才会使用`Exception`中的`message`信息

解决方案二:ErrorFilter处理

通过上面的分析与实验,我们已经知道如何在过滤器中正确的处理异常,让错误信息能够顺利地流转到后续的`SendErrorFilter`过滤器来组织和输出。但是,即使我们不断强调要在过滤器中使用`try-catch`来处理业务逻辑并往请求上下文添加异常信息,但是不可控的人为因素、意料之外的程序因素等,依然会使得一些异常从过滤器中抛出,对于意外抛出的异常又会导致没有控制台输出也没有任何响应信息的情况出现,那么是否有什么好的方法来为这些异常做一个统一的处理呢?

这个时候,我们就可以用到`error`类型的过滤器了。由于在请求生命周期的`pre`、`route`、`post`三个阶段中有异常抛出的时候都会进入`error`阶段的处理,所以我们可以通过创建一个`error`类型的过滤器来捕获这些异常信息,并根据这些异常信息在请求上下文中注入需要返回给客户端的错误描述,这里我们可以直接沿用在`try-catch`处理异常信息时用的那些error参数,这样就可以让这些信息被`SendErrorFilter`捕获并组织成消息响应返回给客户端。比如,下面的代码就实现了这里所描述的一个过滤器:public class ErrorFilter extends ZuulFilter {

Logger log = LoggerFactory.getLogger(ErrorFilter.class);

@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();
log.error("this is a ErrorFilter : {}", throwable.getCause().getMessage());
ctx.set("error.status_code", HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
ctx.set("error.exception", throwable.getCause());
return null;
}

}
在将该过滤器加入到我们的API网关服务之后,我们可以尝试使用之前介绍`try-catch`处理时实现的`ThrowExceptionFilter`(不包含异常处理机制的代码),让该过滤器能够抛出异常。这个时候我们再通过API网关来访问服务接口。此时,我们就可以在控制台中看到`ThrowExceptionFilter`过滤器抛出的异常信息,并且请求响应中也能获得如下的错误信息内容,而不是什么信息都没有的情况了。{
"timestamp": 1481674993561,
"status": 500,
"error": "Internal Server Error",
"exception": "java.lang.RuntimeException",
"message": "Exist some errors..."
}

 

本文作者:程序猿DD-翟永超 
原文链接:Spring Cloud实战小贴士:Zuul统一异常处理(一)
版权归作者所有,转载请注明出处
  查看全部
在上一篇《Spring Cloud源码分析(四)Zuul:核心过滤器》一文中,我们详细介绍了Spring Cloud Zuul中自己实现的一些核心过滤器,以及这些过滤器在请求生命周期中的不同作用。我们会发现在这些核心过滤器中并没有实现error阶段的过滤器。那么这些过滤器可以用来做什么呢?接下来,本文将介绍如何利用error过滤器来实现统一的异常处理。

过滤器中抛出异常的问题

首先,我们可以来看看默认情况下,过滤器中抛出异常Spring Cloud Zuul会发生什么现象。我们创建一个pre类型的过滤器,并在该过滤器的run方法实现中抛出一个异常。比如下面的实现,在run方法中调用的`doSomething`方法将抛出`RuntimeException`异常。
public class ThrowExceptionFilter extends ZuulFilter  {

private static Logger log = LoggerFactory.getLogger(ThrowExceptionFilter.class);

@Override
public String filterType() {
return "pre";
}

@Override
public int filterOrder() {
return 0;
}

@Override
public boolean shouldFilter() {
return true;
}

@Override
public Object run() {
log.info("This is a pre filter, it will throw a RuntimeException");
doSomething();
return null;
}

private void doSomething() {
throw new RuntimeException("Exist some errors...");
}

}


运行网关程序并访问某个路由请求,此时我们会发现:在API网关服务的控制台中输出了`ThrowExceptionFilter`的过滤逻辑中的日志信息,但是并没有输出任何异常信息,同时发起的请求也没有获得任何响应结果。为什么会出现这样的情况呢?我们又该如何在过滤器中处理异常呢?

解决方案一:严格的try-catch处理

回想一下,我们在上一节中介绍的所有核心过滤器,是否还记得有一个`post`过滤器`SendErrorFilter`是用来处理异常信息的?根据正常的处理流程,该过滤器会处理异常信息,那么这里没有出现任何异常信息说明很有可能就是这个过滤器没有被执行。所以,我们不妨来详细看看`SendErrorFilter`的`shouldFilter`函数:
public boolean shouldFilter() {
RequestContext ctx = RequestContext.getCurrentContext();
return ctx.containsKey("error.status_code") && !ctx.getBoolean(SEND_ERROR_FILTER_RAN, false);
}

可以看到该方法的返回值中有一个重要的判断依据`ctx.containsKey("error.status_code")`,也就是说请求上下文中必须有`error.status_code`参数,我们实现的`ThrowExceptionFilter`中并没有设置这个参数,所以自然不会进入`SendErrorFilter`过滤器的处理逻辑。那么我们要如何用这个参数呢?我们可以看一下`route`类型的几个过滤器,由于这些过滤器会对外发起请求,所以肯定会有异常需要处理,比如`RibbonRoutingFilter`的`run`方法实现如下:
public Object run() {
RequestContext context = RequestContext.getCurrentContext();
this.helper.addIgnoredHeaders();
try {
RibbonCommandContext commandContext = buildCommandContext(context);
ClientHttpResponse response = forward(commandContext);
setResponse(response);
return response;
}
catch (ZuulException ex) {
context.set(ERROR_STATUS_CODE, ex.nStatusCode);
context.set("error.message", ex.errorCause);
context.set("error.exception", ex);
}
catch (Exception ex) {
context.set("error.status_code", HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
context.set("error.exception", ex);
}
return null;
}


可以看到,整个发起请求的逻辑都采用了`try-catch`块处理。在`catch`异常的处理逻辑中并没有做任何输出操作,而是往请求上下文中添加一些`error`相关的参数,主要有下面三个参数:
  • error.status_code:错误编码
  • error.exception:Exception异常对象
  • error.message:错误信息


其中,`error.status_code`参数就是`SendErrorFilter`过滤器用来判断是否需要执行的重要参数。分析到这里,实现异常处理的大致思路就开始明朗了,我们可以参考`RibbonRoutingFilter`的实现对`ThrowExceptionFilter`的`run`方法做一些异常处理的改造,具体如下:
public Object run() {
log.info("This is a pre filter, it will throw a RuntimeException");
RequestContext ctx = RequestContext.getCurrentContext();
try {
doSomething();
} catch (Exception e) {
ctx.set("error.status_code", HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
ctx.set("error.exception", e);
}
return null;
}

通过上面的改造之后,我们再尝试访问之前的接口,这个时候我们可以得到如下响应内容:
{
"timestamp": 1481674980376,
"status": 500,
"error": "Internal Server Error",
"exception": "java.lang.RuntimeException",
"message": "Exist some errors..."
}

此时,我们的异常信息已经被`SendErrorFilter`过滤器正常处理并返回给客户端了,同时在网关的控制台中也输出了异常信息。从返回的响应信息中,我们可以看到几个我们之前设置在请求上下文中的内容,它们的对应关系如下:

- `status`:对应`error.status_code`参数的值
- `exception`:对应`error.exception`参数中`Exception`的类型
- `message`:对应`error.exception`参数中`Exception`的`message`信息。对于`message`的信息,我们在过滤器中还可以通过`ctx.set("error.message", "自定义异常消息");`来定义更友好的错误信息。`SendErrorFilter`会优先取`error.message`来作为返回的`message`内容,如果没有的话才会使用`Exception`中的`message`信息

解决方案二:ErrorFilter处理

通过上面的分析与实验,我们已经知道如何在过滤器中正确的处理异常,让错误信息能够顺利地流转到后续的`SendErrorFilter`过滤器来组织和输出。但是,即使我们不断强调要在过滤器中使用`try-catch`来处理业务逻辑并往请求上下文添加异常信息,但是不可控的人为因素、意料之外的程序因素等,依然会使得一些异常从过滤器中抛出,对于意外抛出的异常又会导致没有控制台输出也没有任何响应信息的情况出现,那么是否有什么好的方法来为这些异常做一个统一的处理呢?

这个时候,我们就可以用到`error`类型的过滤器了。由于在请求生命周期的`pre`、`route`、`post`三个阶段中有异常抛出的时候都会进入`error`阶段的处理,所以我们可以通过创建一个`error`类型的过滤器来捕获这些异常信息,并根据这些异常信息在请求上下文中注入需要返回给客户端的错误描述,这里我们可以直接沿用在`try-catch`处理异常信息时用的那些error参数,这样就可以让这些信息被`SendErrorFilter`捕获并组织成消息响应返回给客户端。比如,下面的代码就实现了这里所描述的一个过滤器:
public class ErrorFilter extends ZuulFilter {

Logger log = LoggerFactory.getLogger(ErrorFilter.class);

@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();
log.error("this is a ErrorFilter : {}", throwable.getCause().getMessage());
ctx.set("error.status_code", HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
ctx.set("error.exception", throwable.getCause());
return null;
}

}

在将该过滤器加入到我们的API网关服务之后,我们可以尝试使用之前介绍`try-catch`处理时实现的`ThrowExceptionFilter`(不包含异常处理机制的代码),让该过滤器能够抛出异常。这个时候我们再通过API网关来访问服务接口。此时,我们就可以在控制台中看到`ThrowExceptionFilter`过滤器抛出的异常信息,并且请求响应中也能获得如下的错误信息内容,而不是什么信息都没有的情况了。
{
"timestamp": 1481674993561,
"status": 500,
"error": "Internal Server Error",
"exception": "java.lang.RuntimeException",
"message": "Exist some errors..."
}

 


本文作者:程序猿DD-翟永超 
原文链接:Spring Cloud实战小贴士:Zuul统一异常处理(一)
版权归作者所有,转载请注明出处