最近我们的项目在考虑使用Gateway,考虑使用Spring Cloud Gateway,发现网关的异常处理和spring boot 单体应用异常处理还是有很大区别的。让我们来回顾一下异常。

关于异常是拿来干什么的,很多人老程序员认为就是拿来我们Debug的时候排错的,当然这一点确实是异常机制非常大的一个好处,但异常机制包含着更多的意义。

  • 关注业务实现。异常机制使得业务代码与异常处理代码可以分开,你可以将一些你调用数据库操作的代码写在一个方法里而只需要在方法上加上throw DB相关的异常。至于如何处理它,你可以在调用该方法的时候处理或者甚至选择不处理,而不是直接在该方法内部添加上if判断如果数据库操作错误该如何办,这样业务代码会非常混乱。
  • 统一异常处理。与上一点有所联系。我当前所在项目的实践是,自定义业务类异常,在Controller或Service中抛出,让后使用Spring提供的异常接口统一处理我们自己在内部抛出的异常。这样一个异常处理架构就非常明了。
  • 程序的健壮性。如果没有异常机制,那么来了个对空对象的某方法调用怎么办呢?直接让程序挂掉?这令人无法接受,当然,我们自己平时写的一些小的东西确实是这样,没有处理它,让后程序挂了。但在web框架中,可以利用异常处理机制捕获该异常并将错误信息传递给我们然后继续处理下个请求。所以异常对于健壮性是非常有帮助的。

异常处理(又称为错误处理)功能提供了处理程序运行时出现的任何意外或异常情况的方法。异常处理使用 try、catch 和 finally 关键字来尝试可能未成功的操作,处理失败,以及在事后清理资源。异常根据意义成三种:业务、系统、代码异常,不同的异常采用不同的处理方式。具体的什么样的异常怎么处理就不说了。

Spring Cloud Gateway中异常处理-LMLPHP

红线和绿线代表两条异常路径

1,红线代表:请求到Gateway发生异常,可能由于后端app在启动或者是没启动

2,绿线代表:请求到Gateway转发到后端app,后端app发生异常,然后Gateway转发后端异常到前端

红线肯定是走Gateway自定义异常:

Spring Cloud Gateway中异常处理-LMLPHP

两个类的代码如下(参考:http://cxytiandi.com/blog/detail/20548):

 1 @Configuration
 2 @EnableConfigurationProperties({ServerProperties.class, ResourceProperties.class})
 3 public class ExceptionHandlerConfiguration {
 4
 5     private final ServerProperties serverProperties;
 6
 7     private final ApplicationContext applicationContext;
 8
 9     private final ResourceProperties resourceProperties;
10
11     private final List<ViewResolver> viewResolvers;
12
13     private final ServerCodecConfigurer serverCodecConfigurer;
14
15     public ExceptionHandlerConfiguration(ServerProperties serverProperties,
16                                          ResourceProperties resourceProperties,
17                                          ObjectProvider<List<ViewResolver>> viewResolversProvider,
18                                          ServerCodecConfigurer serverCodecConfigurer,
19                                          ApplicationContext applicationContext) {
20         this.serverProperties = serverProperties;
21         this.applicationContext = applicationContext;
22         this.resourceProperties = resourceProperties;
23         this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList);
24         this.serverCodecConfigurer = serverCodecConfigurer;
25     }
26
27     @Bean
28     @Order(Ordered.HIGHEST_PRECEDENCE)
29     public ErrorWebExceptionHandler errorWebExceptionHandler(ErrorAttributes errorAttributes) {
30         JsonExceptionHandler exceptionHandler = new JsonExceptionHandler(
31                 errorAttributes,
32                 this.resourceProperties,
33                 this.serverProperties.getError(),
34                 this.applicationContext);
35         exceptionHandler.setViewResolvers(this.viewResolvers);
36         exceptionHandler.setMessageWriters(this.serverCodecConfigurer.getWriters());
37         exceptionHandler.setMessageReaders(this.serverCodecConfigurer.getReaders());
38         return exceptionHandler;
39     }
 1 public class JsonExceptionHandler extends DefaultErrorWebExceptionHandler {
 2
 3     private static Logger logger = LoggerFactory.getLogger(JsonExceptionHandler.class);
 4
 5     public JsonExceptionHandler(ErrorAttributes errorAttributes, ResourceProperties resourceProperties,
 6                                 ErrorProperties errorProperties, ApplicationContext applicationContext) {
 7         super(errorAttributes, resourceProperties, errorProperties, applicationContext);
 8     }
 9
10         /**
11          * 获取异常属性
12          */
13         @Override
14         protected Map<String, Object> getErrorAttributes(ServerRequest request, boolean includeStackTrace) {
15             int code = HttpStatus.INTERNAL_SERVER_ERROR.value();
16             Throwable error = super.getError(request);
17             if (error instanceof org.springframework.cloud.gateway.support.NotFoundException) {
18                 code = HttpStatus.NOT_FOUND.value();
19             }
20             return response(code, this.buildMessage(request, error));
21         }
22
23         /**
24          * 指定响应处理方法为JSON处理的方法
25          * @param errorAttributes
26          */
27         @Override
28         protected RouterFunction<ServerResponse> getRoutingFunction(ErrorAttributes errorAttributes) {
29             return RouterFunctions.route(RequestPredicates.all(), this::renderErrorResponse);
30         }
31
32
33         /**
34          * 根据code获取对应的HttpStatus
35          * @param errorAttributes
36          */
37         @Override
38         protected HttpStatus getHttpStatus(Map<String, Object> errorAttributes) {
39             int statusCode = (int) errorAttributes.get("code");
40             return HttpStatus.valueOf(statusCode);
41         }
42
43         /**
44          * 构建异常信息
45          * @param request
46          * @param ex
47          * @return
48          */
49         private String buildMessage(ServerRequest request, Throwable ex) {
50             StringBuilder message = new StringBuilder("Failed to handle request [");
51             message.append(request.methodName());
52             message.append(" ");
53             message.append(request.uri());
54             message.append("]");
55             if (ex != null) {
56                 message.append(": ");
57                 message.append(ex.getMessage());
58             }
59             return message.toString();
60         }
61
62         /**
63          * 构建返回的JSON数据格式
64          * @param status        状态码
65          * @param errorMessage  异常信息
66          * @return
67          */
68         public static Map<String, Object> response(int status, String errorMessage) {
69             Map<String, Object> map = new HashMap<>();
70             map.put("code", status);
71             map.put("message", errorMessage);
72             map.put("data", null);
73             logger.error(map.toString());
74             return map;
75         }
76     }

绿线代表Gateway转发异常

转发的异常,肯定是springboot单体中处理的,至于spring单体中的异常是怎么处理的呢?肯定是用@ControllerAdvice去做。

1     @ExceptionHandler(value = Exception.class)
2     @ResponseBody
3     public AppResponse exceptionHandler(HttpServletRequest request, Exception e) {
4         String ip = RequestUtil.getIpAddress(request);
5         logger.info("调用者IP:" + ip);
6         String errorMessage = String.format("Url:[%s]%n{%s}", request.getRequestURL().toString(), e.getMessage());
7         logger.error(errorMessage, e);
8         return AppResponse.error(HttpStatus.INTERNAL_SERVER_ERROR.value(), e.getMessage());
9     }

到这里基本上可以了,大家不要试着去用Gateway去捕获后端异常,回到最初的起点,API 网关(API Gateway)主要负责服务请求路由、组合及协议转换,异常同样也是一样,Gateway只负责转发单体应用的异常,不要试图Gateway捕获后端服务异常,然后再输出给前端。感谢猿天地的一句惊醒梦中人!

Spring Cloud Gateway中异常处理-LMLPHP

02-20 05:02