Spring项目中优雅的异常处理

前言

现在的Java Web项目可能是以 MVC 模式构建的,一般咱们都是将 Service 层的异常统一的抛出,包括自定义异常和一些意外出现的异常,以便进行事务回滚,而 Service 的调用者 Controller 则承担着异常处理的责任,由于他是与 Web 前端交互的最后一道防线,若是此时还不进行处理则用户会在网页上看到一脸懵逼的前端

Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 4
    at cn.keats.TestAdd.main(TestAdd.java:20)

这样作有如下几点坏处:java

  1. 用户体验很不友好,可能用户会吐槽一句:这是什么XX网站。而后再也不访问了
  2. 若是这个用户是同行,他不只看到了项目代码的结构,并且看到抛出的是这么低级的索引越界异常,会被人家看不起
  3. 用户看到网站有问题,打电话给客服,客服找到产品,产品叫醒正在熟睡/打游戏的你。你不只睡很差游戏打不了还得挨批评完事改代码

哎,真惨。所以通常咱们采用的方法会是像这样:程序员

异常处理

通常的Controller处理

Service代码以下:web

@Service
public class DemoService {
    public String respException(String param){
        if(StringUtils.isEmpty(param)){
            throw new MyException(ExceptionEnum.PARAM_EXCEPTION);
        }
        int i = 1/0;
        return "你看不见我!";
    }
}

Controller代码以下:spring

@RestController
public class DemoController {
    @Autowired
    private DemoService demoService;
    
    @PostMapping("respException")
    public Result respException(){
        try {
            return Result.OK(demoService.respException(null)); 
        } catch (MyException e){
            return Result.Exception(e, null);
        }
        catch (Exception e) {
            return Result.Error();
        }
    } 
}

若是此时发送以下的请求:编程

http://localhost/respException

服务器捕捉到自定义的异常 MyException,而返回参数异常的Json串:json

{
    "code": 1,
    "msg": "参数异常",
    "data": null
}

而当咱们补上参数:服务器

http://localhost/respException?param=zhangsan

则服务器捕捉到 by zero 异常,会返回未知错误到前端页面app

{
    "code": -1,
    "msg": "未知错误",
    "data": null
}

这样就会在必定程度上规避一些问题,例如参数错误就可让用户去修改其参数,固然这通常须要前端同窗配合作页面的参数校验,必传参数都有的时候再向服务器发送请求,一方面减轻服务器压力,一方面将问题前置节省双方的时间。可是这样写有一个坏处就是全部的Controller方法中关于异常的部分都是同样的,代码很是冗余。且不利于维护,并且一些不太熟悉异常机制的同窗可能会像踢皮球同样将异常抓了抛,抛完又抓回来,闹着玩呢。。。(笔者就曾经接手过一个跑路同窗的代码这样处理异常,那简直是跟异常捉迷藏呢!可恨)咱们在Service有全局事务处理,在系统中能够有全局的日志处理,这些都是基于Spring 的一大杀器:AOP(面向切面编程) 实现的,AOP是什么呢?框架

AOP

AOP是Spring框架面向切面的编程思想,AOP采用一种称为“横切”的技术,将涉及多业务流程的通用功能抽取并单独封装,造成独立的切面,在合适的时机将这些切面横向切入到业务流程指定的位置中。若是说咱们经常使用的OOP思想是从上到下执行业务流程的话,AOP就至关于在咱们执行业务的时候横切一刀,以下图所示:

image-20191201205830708

而Advice(通知)是AOP思想中重要的一个术语,分为前置通知(Before)、后置通知(AfterReturning)、异常通知(AfterThrowing)、最终通知(After)和环绕通知(Around)五种。具体通知所表示的意义我这里很少赘述,网上关于Spring核心原理的讲解都会说起。而咱们熟知的 Service 事务处理其实就是基于AOP AfterThrowing 通知实现的事务回滚。咱们自定义的日志处理也能够根据不一样的需求定制不一样的通知入口。那既然如此,咱们为什么不自定义一个全局异常处理的切面去简化咱们的代码呢?别急,且继续向下看。

优雅的处理异常

Spring 在 3.2 版本已经为咱们提供了该功能: @ControllerAdvice 注解。此注解会捕捉Controller层抛出的异常,并根据 @ExceptionHandler 注解配置的方法进行异常处理。下面是一个示例工程,主要代码以下:

Result类:

此 Result 采用泛型的方式,便于在 Swagger 中配置方法的出参。使用静态工厂方法是的对象的初始化更加见名只意。对于不存在共享变量问题的 Error 对象,采用双重校验锁懒汉单例模式来节省服务器资源(固然最好仍是整个项目运行中一直没有初始化它让人更加舒服。)

package cn.keats.util;

import cn.keats.exception.MyException;
import lombok.Data;

/**
 * 功能:统一返回结果,直接调用对应的工厂方法
 *
 * @author Keats
 * @date 2019/11/29 18:20
 */
@Data
public class Result<T>  {
    private Integer code;
    private String msg;
    private T data;

    /**
     * 功能:响应成功
     *
     * @param data 响应的数据
     * @return woke.cloud.property.transformat.Result
     * @author Keats
     * @date 2019/11/30 8:54
     */
    public static <T> Result<T> OK(T data){
        return new Result<>(0, "响应成功", data);
    }

    private static Result errorResult;
    /**
     * 功能:返回错误,此错误不可定制,全局惟一。通常是代码出了问题,须要修改代码
     *
     * @param
     * @return Result
     * @author Keats
     * @date 2019/11/30 8:55
     */
    public static Result Error(){
        if(errorResult == null){
            synchronized (Result.class){
                if(errorResult == null){
                    synchronized (Result.class){
                        errorResult = new Result<>(-1, "未知错误", null);
                    }
                }
            }
        }
        return errorResult;
    }

    /**
     * 功能:返回异常,直接甩自定义异常类进来
     *
     * @param e 自定义异常类
     * @param data 数据,若是没有填入 null 便可
     * @return woke.cloud.property.transformat.Result<T>
     * @author Keats
     * @date 2019/11/30 8:55
     */
    public static <T> Result<T> Exception(MyException e, T data){
        return new Result<>(e.getCode(), e.getMsg(), data);
    }

    /**
     * 功能:为了方便使用,使用静态工厂方法建立对象。如需新的构造方式,请添加对应的静态工厂方法
     *
     * @author Keats
     * @date 2019/11/30 8:56
     */
    private Result(Integer code, String msg, T data) {
        this.code = code;
        this.msg = msg;
        this.data = data;
    }
}

自定义异常类:

package cn.keats.exception;

import lombok.Getter;

/**
 * 功能:系统自定义异常类。继承自RuntimeException,方便Spring进行事务回滚
 *
 * @author Keats
 * @date 2019/11/29 18:50
 */
@Getter
public class MyException extends RuntimeException{
    private Integer code;
    private String msg;

    public MyException(ExceptionEnum eEnum) {
        this.code = eEnum.getCode();
        this.msg = eEnum.getMsg();
    }
}

异常代码枚举类:

package cn.keats.exception;

import lombok.AllArgsConstructor;
import lombok.Getter;

/**
 * 功能:异常枚举
 *
 * @author Keats
 * @date 2019/11/29 18:49
 */
@Getter
@AllArgsConstructor
public enum ExceptionEnum {
    PARAM_EXCEPTION(1,"参数异常"),
    USER_NOT_LOGIN(2,"用户未登陆"),
    FILE_NOT_FOUND(3,"文件不存在,请从新选择");


    private Integer code;
    private String msg;
}

异常切面:

其中 @RestControllerAdvice 是spring 4.3 添加的新注解,是 @ControllerAdvice 和 @ResponseBody 的简写方式,相似与 @RestController 与 @Controller 的关系

package cn.keats.advice;

import cn.keats.exception.MyException;
import cn.keats.util.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestControllerAdvice;

/**
 * 功能:全局异常处理器,Controller异常直接抛出
 *
 * @return
 * @author Keats
 * @date 2019/11/30 10:28
 */
@Slf4j
@RestControllerAdvice
public class ExceptionAdvice {
    /**
     * 功能:其他非预先规避的异常返回错误
     *
     * @param e
     * @return woke.cloud.property.transformat.Result
     * @author Keats
     * @date 2019/11/30 10:08
     */
    @ExceptionHandler(value = Exception.class)
    @ResponseBody
    public Result ResponseException(Exception e) {
        log.error("未知错误,错误信息:", e);
        return Result.Error();
    }

    /**
     * 功能:捕捉到 MyException 返回对应的消息
     *
     * @param e
     * @return woke.cloud.property.transformat.Result
     * @author Keats
     * @date 2019/11/30 10:07
     */
    @ExceptionHandler(value = MyException.class)
    @ResponseBody
    public Result myException(MyException e) {
        log.info("返回自定义异常:异常代码:" + e.getCode() + "异常信息:" + e.getMsg());
        return Result.Exception(e, null);
    }
}

此时的 Controller 方法能够这样写:

package cn.keats.controller;

import cn.keats.service.DemoService;
import cn.keats.util.Result;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class DemoController {
    @Autowired
    private DemoService demoService;

    @PostMapping("respException")
    public Result respException(String param) throws Exception {
        return Result.OK(demoService.respException(param));
    }
    
    @PostMapping("respError")
    public Result respError() throws Exception {
        return Result.OK(demoService.respException(null));
    }
}

省略的大部分的异常处理代码,使得咱们只须要关注业务,一方面提升了代码质量,可阅读性,另外一方面也提升了咱们的开发速度。美哉!

启动项目,进行测试没有问题。

image-20191201213847490

image-20191201213912251

我是 Keats,一个热爱技术的程序员,鉴于技术有限,若是本文有什么纰漏或者兄台还有其余更好的建议/实现方式,欢迎留言评论,谢谢您!

相关文章
相关标签/搜索