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

Java技术栈

共 28953字,需浏览 58分钟

 ·

2022-06-20 23:33

点击关注公众号,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 实战笔记!
浏览 53
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报