Spring Cloud 如何统一异常处理?写得太好了!

Posted Java技术栈

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring Cloud 如何统一异常处理?写得太好了!相关的知识,希望对你有一定的参考价值。

点击关注公众号,Java干货及时送达

作者:BNDong
链接:www.cnblogs.com/bndong/p/10135370.html

前言

在启动应用时会发现在控制台打印的日志中出现了两个路径为 [/error] 的访问地址,当系统中发送异常错误时,Spring Boot 会根据请求方式分别跳转到以 JSON 格式或以界面显示的 /error 地址中显示错误信息。

2018-12-18 09:36:24.627  INFO 19040 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "[/error]" ...
2018-12-18 09:36:24.632  INFO 19040 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "[/error],produces=[text/html]" ...

Spring Boot 基础就不介绍了,推荐下这个实战教程:

https://github.com/javastacks/spring-boot-best-practice

默认异常处理

使用 AJAX 方式请求时返回的 JSON 格式错误信息。


    "timestamp": "2018-12-18T01:50:51.196+0000",
    "status": 404,
    "error": "Not Found",
    "message": "No handler found for GET /err404",
    "path": "/err404"

使用浏览器请求时返回的错误信息界面。

自定义异常处理

引入依赖

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.54</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>

fastjson 是 JSON 序列化依赖, spring-boot-starter-freemarker 是一个模板引擎,用于我们设置错误输出模板。最新 Spring Boot 面试题整理好了,大家可以在Java面试库小程序在线刷题。

增加配置

# 出现错误时, 直接抛出异常(便于异常统一处理,否则捕获不到404)
spring.mvc.throw-exception-if-no-handler-found=true
# 不要为工程中的资源文件建立映射
spring.resources.add-mappings=false
spring:
  # 出现错误时, 直接抛出异常(便于异常统一处理,否则捕获不到404)
  mvc:
    throw-exception-if-no-handler-found: true
  # 不要为工程中的资源文件建立映射
  resources:
    add-mappings: false

Spring Boot 基础就不介绍了,推荐下这个实战教程:

https://github.com/javastacks/spring-boot-best-practice

新建错误信息实体

/**
 * 信息实体
 */
public class ExceptionEntity implements Serializable 

    private static final long serialVersionUID = 1L;

    private String message;

    private int    code;

    private String error;

    private String path;

    @JSONField(format = "yyyy-MM-dd hh:mm:ss")
    private Date timestamp = new Date();

    public static long getSerialVersionUID() 
        return serialVersionUID;
    

    public String getMessage() 
        return message;
    

    public void setMessage(String message) 
        this.message = message;
    

    public int getCode() 
        return code;
    

    public void setCode(int code) 
        this.code = code;
    

    public String getError() 
        return error;
    

    public void setError(String error) 
        this.error = error;
    

    public String getPath() 
        return path;
    

    public void setPath(String path) 
        this.path = path;
    

    public Date getTimestamp() 
        return timestamp;
    

    public void setTimestamp(Date timestamp) 
        this.timestamp = timestamp;
    

新建自定义异常

/**
 * 自定义异常
 */
public class BasicException extends RuntimeException 

    private static final long serialVersionUID = 1L;

    private int code = 0;

    public BasicException(int code, String message) 
        super(message);
        this.code = code;
    

    public int getCode() 
        return this.code;
    
/**
 * 业务异常
 */
public class BusinessException extends BasicException 

    private static final long serialVersionUID = 1L;

    public BusinessException(int code, String message) 
        super(code, message);
    

BasicException 继承了 RuntimeException ,并在原有的 Message 基础上增加了错误码 code 的内容。而 BusinessException 则是在业务中具体使用的自定义异常类,起到了对不同的异常信息进行分类的作用。分享资料:Spring Boot 学习笔记

新建 error.ftl 模板文件

位置:/src/main/resources/templates/ 用于显示错误信息

<!DOCTYPE html>
<html>
<head>
    <meta name="robots" content="noindex,nofollow" />
    <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
    <style>
        h2
            color: #4288ce;
            font-weight: 400;
            padding: 6px 0;
            margin: 6px 0 0;
            font-size: 18px;
            border-bottom: 1px solid #eee;
        

        /* Exception Variables */
        .exception-var table
            width: 100%;
            max-width: 500px;
            margin: 12px 0;
            box-sizing: border-box;
            table-layout:fixed;
            word-wrap:break-word;
        
        .exception-var table caption
            text-align: left;
            font-size: 16px;
            font-weight: bold;
            padding: 6px 0;
        
        .exception-var table caption small
            font-weight: 300;
            display: inline-block;
            margin-left: 10px;
            color: #ccc;
        
        .exception-var table tbody
            font-size: 13px;
            font-family: Consolas,"Liberation Mono",Courier,"微软雅黑";
        
        .exception-var table td
            padding: 0 6px;
            vertical-align: top;
            word-break: break-all;
        
        .exception-var table td:first-child
            width: 28%;
            font-weight: bold;
            white-space: nowrap;
        
        .exception-var table td pre
            margin: 0;
        
    </style>
</head>
<body>

<div class="exception-var">
    <h2>Exception Datas</h2>
    <table>
        <tbody>
        <tr>
            <td>Code</td>
            <td>
                $(exception.code)!
            </td>
        </tr>
        <tr>
            <td>Time</td>
            <td>
                $(exception.timestamp?datetime)!
            </td>
        </tr>
        <tr>
            <td>Path</td>
            <td>
                $(exception.path)!
            </td>
        </tr>
        <tr>
            <td>Exception</td>
            <td>
                $(exception.error)!
            </td>
        </tr>
        <tr>
            <td>Message</td>
            <td>
                $(exception.message)!
            </td>
        </tr>
        </tbody>
    </table>
</div>
</body>
</html>

编写全局异常控制类

/**
 * 全局异常控制类
 */
@ControllerAdvice
public class GlobalExceptionHandler 

    /**
     * 404异常处理
     */
    @ExceptionHandler(value = NoHandlerFoundException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public ModelAndView errorHandler(HttpServletRequest request, NoHandlerFoundException exception, HttpServletResponse response) 
        return commonHandler(request, response,
                exception.getClass().getSimpleName(),
                HttpStatus.NOT_FOUND.value(),
                exception.getMessage());
    

    /**
     * 405异常处理
     */
    @ExceptionHandler(HttpRequestMethodNotSupportedException.class)
    public ModelAndView errorHandler(HttpServletRequest request, HttpRequestMethodNotSupportedException exception, HttpServletResponse response) 
        return commonHandler(request, response,
                exception.getClass().getSimpleName(),
                HttpStatus.METHOD_NOT_ALLOWED.value(),
                exception.getMessage());
    

    /**
     * 415异常处理
     */
    @ExceptionHandler(HttpMediaTypeNotSupportedException.class)
    public ModelAndView errorHandler(HttpServletRequest request, HttpMediaTypeNotSupportedException exception, HttpServletResponse response) 
        return commonHandler(request, response,
                exception.getClass().getSimpleName(),
                HttpStatus.UNSUPPORTED_MEDIA_TYPE.value(),
                exception.getMessage());
    

    /**
     * 500异常处理
     */
    @ExceptionHandler(value = Exception.class)
    public ModelAndView errorHandler (HttpServletRequest request, Exception exception, HttpServletResponse response) 
        return commonHandler(request, response,
                exception.getClass().getSimpleName(),
                HttpStatus.INTERNAL_SERVER_ERROR.value(),
                exception.getMessage());
    

    /**
     * 业务异常处理
     */
    @ExceptionHandler(value = BasicException.class)
    private ModelAndView errorHandler (HttpServletRequest request, BasicException exception, HttpServletResponse response) 
        return commonHandler(request, response,
                exception.getClass().getSimpleName(),
                exception.getCode(),
                exception.getMessage());
    

    /**
     * 表单验证异常处理
     */
    @ExceptionHandler(value = BindException.class)
    @ResponseBody
    public ExceptionEntity validExceptionHandler(BindException exception, HttpServletRequest request, HttpServletResponse response) 
        List<FieldError> fieldErrors = exception.getBindingResult().getFieldErrors();
        Map<String,String> errors = new HashMap<>();
        for (FieldError error:fieldErrors) 
            errors.put(error.getField(), error.getDefaultMessage());
        
        ExceptionEntity entity = new ExceptionEntity();
        entity.setMessage(JSON.toJSONString(errors));
        entity.setPath(request.getRequestURI());
        entity.setCode(HttpStatus.INTERNAL_SERVER_ERROR.value());
        entity.setError(exception.getClass().getSimpleName());
        response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
        return entity;
    

    /**
     * 异常处理数据处理
     */
    private ModelAndView commonHandler (HttpServletRequest request, HttpServletResponse response,
                                            String error, int httpCode, String message) 
        ExceptionEntity entity = new ExceptionEntity();
        entity.setPath(request.getRequestURI());
        entity.setError(error);
        entity.setCode(httpCode);
        entity.setMessage(message);
        return determineOutput(request, response, entity);
    

    /**
     * 异常输出处理
     */
    private ModelAndView determineOutput(HttpServletRequest request, HttpServletResponse response, ExceptionEntity entity) 
        if (!(
                request.getHeader("accept").contains("application/json")
                || (request.getHeader("X-Requested-With") != null && request.getHeader("X-Requested-With").contains("XMLHttpRequest"))
        )) 
            ModelAndView modelAndView = new ModelAndView("error");
            modelAndView.addObject("exception", entity);
            return modelAndView;
         else 
            response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
            response.setCharacterEncoding("UTF8");
            response.setHeader("Content-Type", "application/json");
            try 
                response.getWriter().write(ResultJsonTools.build(
                        ResponseCodeConstant.SYSTEM_ERROR,
                        ResponseMessageConstant.APP_EXCEPTION,
                        JSONObject.parseObject(JSON.toJSONString(entity))
                ));
             catch (IOException e) 
                e.printStackTrace();
            
            return null;
        
    

@ControllerAdvice

作用于类上,用于标识该类用于处理全局异常。

点击关注公众号,Java干货及时送达

@ExceptionHandler

作用于方法上,用于对拦截的异常类型进行处理。value 属性用于指定具体的拦截异常类型,如果有多个 ExceptionHandler 存在,则需要指定不同的 value 类型,由于异常类拥有继承关系,所以 ExceptionHandler 会首先执行在继承树中靠前的异常类型。

BindException

该异常来自于表单验证框架 Hibernate validation,当字段验证未通过时会抛出此异常。

编写测试 Controller

@RestController
public class TestController 

    @RequestMapping(value = "err")
    public void error()
        throw new BusinessException(400, "业务异常错误信息");
    

    @RequestMapping(value = "err2")
    public void error2()
        throw new NullPointerException("手动抛出异常信息");
    

    @RequestMapping(value = "err3")
    public int error3()
        int a = 10 / 0;
        return a;
    

使用 AJAX 方式请求时返回的 JSON 格式错误信息。最新 Spring Boot 面试题整理好了,大家可以在Java面试库小程序在线刷题。

# /err

    "msg": "应用程序异常",
    "code": -1,
    "status_code": 0,
    "data": 
        "path": "/err",
        "code": 400,
        "error": "BusinessException",
        "message": "业务异常错误信息",
        "timestamp": "2018-12-18 11:09:00"
    


# /err2

    "msg": "应用程序异常",
    "code": -1,
    "status_code": 0,
    "data": 
        "path": "/err2",
        "code": 500,
        "error": "NullPointerException",
        "message": "手动抛出异常信息",
        "timestamp": "2018-12-18 11:15:15"
    


# /err3

    "msg": "应用程序异常",
    "code": -1,
    "status_code": 0,
    "data": 
        "path": "/err3",
        "code": 500,
        "error": "ArithmeticException",
        "message": "/ by zero",
        "timestamp": "2018-12-18 11:15:46"
    


# /err404

    "msg": "应用程序异常",
    "code": -1,
    "status_code": 0,
    "data": 
        "path": "/err404",
        "code": 404,
        "error": "NoHandlerFoundException",
        "message": "No handler found for GET /err404",
        "timestamp": "2018-12-18 11:16:11"
    

使用浏览器请求时返回的错误信息界面。

示例代码:https://github.com/BNDong/spring-cloud-examples/tree/master/spring-cloud-zuul/cloud-zuul

参考资料:

  • 《微服务 分布式架构开发实战》 龚鹏 著

  • https://www.jianshu.com/p/1a49fa436623


Spring Boot 定时任务开启后,怎么自动停止?

工作 3 年的同事不知道如何回滚代码

23 种设计模式实战(很全)

Spring Boot 保护敏感配置的 4 种方法!

面了个 5 年 Java,两个线程数据交换都不会

阿里为什么推荐使用 LongAdder?

新来一个技术总监:禁止戴耳机写代码。。

重磅!Spring Boot 2.7 正式发布

Java 18 正式发布,finalize 被弃用。

Spring Boot Admin 横空出世!

Spring Boot 学习笔记,这个太全了!

关注Java技术栈看更多干货

获取 Spring Boot 实战笔记!

以上是关于Spring Cloud 如何统一异常处理?写得太好了!的主要内容,如果未能解决你的问题,请参考以下文章

Spring Cloud 如何统一异常处理?写得太好了!

Spring Cloud 微服务优雅下线 + 灰度发布的正确姿势,写得太好了!

Spring Boot + Web Socket 打造实时监控异常,写得太好了!

Spring Boot + Web Socket 打造实时监控异常,写得太好了!

Spring Boot + Web Socket 打造实时监控异常,写得太好了!

Spring Cloud微服务统一处理异常