Osheep

时光不回头,当下最重要。

SpringBoot 统一全局异常处理(控制层“减肥”)

序言:
此前,我们主要通过在控制层(Controller)中手动捕捉异常(TryCatch)和处理错误,在SpringBoot中我们只需借助通知注解(ControllerAdvice)和异常注解(ExceptionHandler)就能捕获全局异常并作统一处理,这样做的好处是使用控制器层代码成功“瘦身”,让业务逻辑看起来更加清晰明朗! 本工程传送门:SpringBoot-Global-Exception

1.默认错误处理(Default Handling)

SpringBoot 默认为我们提供了BasicErrorController类并通过 /error 映射来处理全局错误,并在Servlet容器中注册为全局错误页。所以在浏览器端访问,发生错误时,我们能及时看到错误/异常信息和HTTP状态等反馈。例如下面这两个错误,对于日常开发而言,再熟悉不过了。

《SpringBoot 统一全局异常处理(控制层“减肥”)》

404 Not Found
《SpringBoot 统一全局异常处理(控制层“减肥”)》

500 服务器错误

2.全局异常处理(Global Exception Handler)

默认的英文空白页,显然不能够满足我们复杂多变的需求,因此我们可以通过专门的类来收集和管理这些异常信息,这样做不仅可以减少控制层的代码量,还有利于线上故障排查和紧急短信通知等。

这个工具实现起来也非常简单,只需借助Spring的注解@ControllerAdvice 限定范围 和@ExceptionHandler 指定异常,就可以轻松将特定的异常信息页渲染到浏览器端。如果想跟其它客户端进行REST交互,只需要加上@ResponseBody 注解就可以自动返回JSON数据。具体代码如下:

package com.hehe.exception;
/**
 * 主要用途:用于捕捉全局异常(针对控制层),并返回指定的异常信息页(View)或异常信息(Json)。
 * <p>
 * 使用说明:
 * {@link ControllerAdvice 默认扫描路径:例如com.hehe.controller}
 * {@link ExceptionHandler 指定异常:例如RuntimeException及其子类}
 * {@link ResponseBody 返回JSON}
 *
 * @author yizhiwazi
 */

@ControllerAdvice
public class GlobalExceptionHandler {

    private final static String EX_FALLBACK_VIEW = "exception";//指定异常信息页

    /**
     * 方式1:返回指定的异常信息页(View)
     */
    @ExceptionHandler(RuntimeException.class)
    public String runtimeExHandler(HttpServletRequest req, Exception ex, Model model) {
        model.addAttribute("exUrl", req.getRequestURL().toString());
        model.addAttribute("exMsg", ex.toString());
        return EX_FALLBACK_VIEW;
    }

    /**
     * 方式2:返回指定的异常信息(Json)
     */
    @ExceptionHandler(Exception.class)
    @ResponseBody
    public R defaultExHandler(HttpServletRequest req, Exception e) {
        return R.isFail(e).data("全局异常JSON:服务异常,请稍后再试!!:" + req.getRequestURL());
    }
}

好了,全局异常处理完成,我们编写控制层代码来测试相关效果。

package com.hehe.controller;

@RestController
public class HelloController {

    private void randomException() throws Exception {
        Exception[] exceptions = { //异常集合
                new NullPointerException(),
                new ArithmeticException(),
                new ArrayIndexOutOfBoundsException(),
                new NumberFormatException(),
                new NoSuchMethodException(),
                new SQLException()};

        if ((Math.random()) < 0.6) {
            //情况1:要么抛出异常
            throw exceptions[(int) (Math.random() * exceptions.length)];
        } else {
            //情况2:要么继续运行
        }

    }

    @GetMapping("/")
    public R index() throws Exception {
        randomException();//模拟测试环境,根据概率来抛出异常。
        return R.isOk().data(Arrays.asList("用户数据展示!", "你在刷新试试!"));
    }

}

这里为了方便展示JSON数据,沿用了之前的统一页面Bean。

package com.hehe.entity;

public class R<T> implements Serializable {
    private static final int OK = 0;
    private static final int FAIL = 1;
    private static final int UNAUTHORIZED = 2;

    private T data; //服务端数据
    private int status = OK; //状态码
    private String msg = ""; //描述信息

    //APIS
    public static R isOk(){
        return new R();
    }
    public static R isFail(){
        return new R().status(FAIL);
    }
    public static R isFail(Throwable e){
        return isFail().msg(e);
    }
    public R msg(Throwable e){
        this.setMsg(e.toString());
        return this;
    }
    public R data(T data){
        this.setData(data);
        return this;
    }
    public R status(int status){
       this.setStatus(status);
       return this;
    }

    //Getter&Setters
 
}

SpringBoot默认支持很多种模板引擎(如Thymelaf、FreeMarker),并提供了相应的自动配置,做到开箱即用。默认的页面加载路径是 src/main/resources/templates ,如果放到其它目录需在配置文件指定。(举例:spring.thymeleaf.prefix=classpath:/views/ )

代码完成之后,我们需要编写一个异常信息页面。
为了方便演示,我们在resources目录下创建templates目录,并新建文件exception.html。页面代码如下:

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>GlobalException</title>
</head>
    <h1>统一祖国 振兴中华</h1>
    <h2> 服务异常,请稍后再试。</h2>
    <h3 th:text="${'问题地址:'+exUrl}"></h3>
    <h3 th:text="${'问题类型:'+exMsg}"></h3>
</body>
</html>

然后在pom.xml 引入相关依赖:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <!--基本信息 -->
    <groupId>com.hehe</groupId>
    <artifactId>springboot-global-exception</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>
    <name>spring-boot-global-exception</name>
    <description>SpringBoot处理全局异常</description>

    <!--继承信息 -->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.0.M4</version>
        <relativePath/>
    </parent>

    <!--依赖管理 -->
    <dependencies>
        <dependency> <!--添加Web依赖 -->
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency> <!--添加Thymeleaf依赖 -->
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency> <!--添加热部署依赖 -->
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
        </dependency>
        <dependency><!--添加Test依赖 -->
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <!--指定远程仓库(含插件)-->
    <repositories>
        <repository>
            <id>spring-snapshots</id>
            <url>http://repo.spring.io/snapshot</url>
            <snapshots><enabled>true</enabled></snapshots>
        </repository>
        <repository>
            <id>spring-milestones</id>
            <url>http://repo.spring.io/milestone</url>
        </repository>
    </repositories>
    <pluginRepositories>
        <pluginRepository>
            <id>spring-snapshots</id>
            <url>http://repo.spring.io/snapshot</url>
        </pluginRepository>
        <pluginRepository>
            <id>spring-milestones</id>
            <url>http://repo.spring.io/milestone</url>
        </pluginRepository>
    </pluginRepositories>
</project>

上述步骤完成之后,打开启动类GlobalExceptionApplication,启动项目然后进行测试。本案例-项目结构图如下:

《SpringBoot 统一全局异常处理(控制层“减肥”)》

本案例-项目结构图

测试效果:在浏览器输入 http://localhost:8080 多次按F5刷新,然后查看页面效果。截图如下:

《SpringBoot 统一全局异常处理(控制层“减肥”)》

正常数据
《SpringBoot 统一全局异常处理(控制层“减肥”)》

情况1:返回异常信息(页面)
《SpringBoot 统一全局异常处理(控制层“减肥”)》

情况2:返回异常信息(JSON数据)

3. 本章小结

使用@ControllerAdvice+@ExceptionHandler 做全局异常处理的好处是方便快捷,但是灵活性方面则比较欠缺。例如现在有一个需求,若在浏览器端发生异常时返回特定的异常信息页(View),而在App端进行请求,发生异常时要求返回详细的异常信息。(JSON)。上述组合自然没法做到,要么指定返回页面,要么指定返回JSON,无法根据请求头信息来判断作处理,也无法根据状态码(如404,500)来分类处理。如果想实现更灵活的错误/异常处理机制,可以实现ErrorController来完成控制。来看看SpringBoot是如何借助BasicErrorController来实现自动识别不同客户端的,很简单,具体代码如下:

@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {

    @RequestMapping(produces = "text/html")
    public ModelAndView errorHtml(HttpServletRequest request,
            HttpServletResponse response) {
        HttpStatus status = getStatus(request);
        Map<String, Object> model = Collections.unmodifiableMap(getErrorAttributes(
                request, isIncludeStackTrace(request, MediaType.TEXT_HTML)));
        response.setStatus(status.value());
        ModelAndView modelAndView = resolveErrorView(request, response, status, model);
        return (modelAndView == null ? new ModelAndView("error", model) : modelAndView);
    }

    @RequestMapping
    @ResponseBody
    public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
        Map<String, Object> body = getErrorAttributes(request,
                isIncludeStackTrace(request, MediaType.ALL));
        HttpStatus status = getStatus(request);
        return new ResponseEntity<>(body, status);
    }
点赞