全局异常 @ControllerAdvice 该怎么写

本文首发于稀土掘金:全局异常 @ControllerAdvice 该怎么写,该账号即为本人账号,非搬运。

问题由来

很多小伙伴刚进公司做项目的时候,会看到项目里面有一个@ControllerAdvice标记的类,整个类的编码结构大概是这样子:

@Slf4j
@ControllerAdvice
@ResponseBody
public class GlobalExceptionHandler {

    @ExceptionHandler(MyException.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public MyExceptionResponse myException(MyException e) {
        //记录日志
        log.error("自定义的异常:" + e.getMessage());
        
        //其他处理逻辑。。。。

        //返回结果
        MyExceptionResponse response = new MyExceptionResponse(1001, "自定义的异常:" + e.getMessage());
        return response;
    }

    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public MyExceptionResponse exception(Exception e) {
        //记录日志
        log.error("exception异常:" + e.getMessage());
        
        //其他处理逻辑。。。。

        //返回结果
        MyExceptionResponse response = new MyExceptionResponse(1002, "exception异常:" + e.getMessage());
        return response;
    }
}

其实你随便在网上搜一下@ControllerAdvice或者@ExceptionHandler,就知道这个是统一的全局异常处理,不过你有没有想过,我们为什么需要统一的全局异常处理,以及他的编码结构为啥发展成了上面的样子。

为什么要处理异常

为什么需要处理异常,因为异常是不可避免的,程序总是可能发生异常,所以我们需要处理异常,如果你就是不编写处理异常的代码,程序里面发生的异常最终会抛给spring,而spring其实是提供了异常处理的,也就是你不处理异常,异常就由spring来处理,那么spring处理异常的结果就是,你会在浏览器看到如下界面:

如果你点开F12去看,会看到返回的响应是这样的:

其中Content-Type是text/html,说明spring确实给你返回了一个html界面。

如果你是使用postman、idea的http client这种测试工具,会得到类似这样的结果:

这两种返回结果都是存在的,spring会根据客户端类型来判断是返回html界面还是返回一个json对象。

然而,这两种结果给到前端都是不合适的,一方面是用户体验不好,用户看不懂。另一方面即使用户给你反馈这样的结果,作为程序员的你也看不出来哪里出错了。这里说明一下,系统抛出的异常(包括RuntimeException甚至Exception),其实是带有具体信息的,只是这些异常抛给spring后,spring没有返回具体内容,而是返回上面这些东西,也就是spring把具体信息给隐藏了,只返回了一些静态的默认内容。

因为spring默认返回的结果不够好,所以我们要自定义处理异常的逻辑, 而代码的调用顺序是@Controller->@Service->@Mapper,如果想要捕获所有的异常,就得在最外层进行捕获,也就是在@Controller进行捕获,于是你会写出类似这样的代码:

@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {

    @Autowired
    private UserService userService;

    @GetMapping("/{id}")
    public ResponseEntity queryById(@PathVariable("id") Long id) {
        try {
            //执行业务逻辑
            User user = userService.queryById(id);

            //返回结果给前端
            HttpStatus httpStatus = HttpStatus.OK;
            ResponseEntity<User> responseEntity = new ResponseEntity<>(user, httpStatus);
            return responseEntity;
        } catch (Exception e) {
            String msg = "查询用户信息异常,id:" + id;
            
            //记录日志
            log.error(msg);

            //返回异常信息给前端
            HttpStatus httpStatus = HttpStatus.INTERNAL_SERVER_ERROR;
            ResponseEntity<String> responseEntity = new ResponseEntity<>(msg, httpStatus);
            return responseEntity;
        }
    }

    @PostMapping("/add")
    public ResponseEntity addUser(@RequestBody User user) {
        try {
            //执行业务逻辑
            User user = userService.addUser(user);

            //返回结果给前端
            HttpStatus httpStatus = HttpStatus.OK;
            ResponseEntity<User> responseEntity = new ResponseEntity<>(user, httpStatus);
            return responseEntity;
        } catch (Exception e) {
            String msg = "新增用户信息异常,用户信息:" + JSON.toJSONString(user);
            
            //记录日志
            log.error(msg);

            //返回异常信息给前端
            HttpStatus httpStatus = HttpStatus.INTERNAL_SERVER_ERROR;
            ResponseEntity<String> responseEntity = new ResponseEntity<>(msg, httpStatus);
            return responseEntity;
        }
    }
}

仔细琢磨一下你会发现这种写法非常难受,首先你需要在每一个@Controller类的每一个方法里面都写上try{}catch(){}块,这种重复性就不是我们能接受的。其次执行正常和异常的返回数据结构是不一样的,执行正常的时候一般返回当前业务对应的JavaBean,比如上面例子的User,执行异常的时候不可能返回User,取而代之的是要想办法把异常信息返回给前端,这就导致方法的返回值得定义成ResponseEntity,然后你每次都要去装填ResponseEntity对象。

为了避免如此麻烦的写法,自spring3.2开始,给我们提供了@ControllerAdvice注解来进行统一的异常处理,他的执行效果是这样的:

如果你使用@ControllerAdvice标记了一个类,那么在程序里发生的所有异常,spring都会将他传给@ControllerAdvice标记的那个类里面,让你自己处理异常。该注解基本用法如下:

@Slf4j
@ControllerAdvice
@ResponseBody
public class GlobalExceptionHandler {

    @ExceptionHandler(RuntimeException.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public String runtimeException(RuntimeException e) {
        //记录日志
        log.error("runtime异常:" + e.getMessage());

        //返回结果
        return "runtime异常:" + e.getMessage();
    }

    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public String exception(Exception e) {
        //记录日志
        log.error("exception异常:" + e.getMessage());

        //返回结果
        return "exception异常:" + e.getMessage();
    }
}

说明:

  1. 首先你不需要在每一个@Controller的每一个@RequestMapping方法里都写try{}catch(){}代码块了,spring会将发生的异常传递到@ControllerAdvice类里面的@ExceptionHandler方法,具体传递到哪个方法,是由异常的类型来进行划分的,也就是同一类型的异常执行相同的处理逻辑,像上面的例子就是将Exception下面的RutimeException做单独处理,其余的都归到Exception来处理。
  2. 其次你的@Controller里的@RequestMapping方法的返回值类型不再需要写ResponseEntity了,可以写具体业务对应的JavaBean,因为发生异常时的返回值类型由@ExceptionHandler所在方法的返回值定义了。
  3. @ControllerAdvice的用法和@Controller有点像,比如都可以在类上面添加@ResponseBody,让类里面的方法可以返回字符串或直接返回JavaBean。
  4. 最后说下@ResponseStatus,这个注解是用来设置http响应码的,这里我设置的“HttpStatus.INTERNAL_SERVER_ERROR”会返回500给前端,如果你不设置的话,会返回默认值200。

以上是spring提供的统一异常处理@ControllerAdvice的基本用法,不过他和文章开头的代码还是有些区别,我们来看看这段代码还有什么可以优化一下。

自定义业务异常类

就异常的来源来说,除了系统抛出的异常,其实我们自己也可以抛出异常,比如在业务执行过程中发现不对的时候,会通过抛异常的方式让程序终止:

@GetMapping("/{id}")
public User queryById(@PathVariable("id") Long id) {
    User user = userService.queryById(id);
    if(user == null){
        throw new RuntimeException("该用户不存在");
    }

    //可能还有其他的业务逻辑。。。。

    //最终返回结果
    return user;
}

其实这样子抛出的异常,也可以被上面“基本用法”当中的@ControllerAdvice接收和处理,但是这种异常情况,其实是在我们的预期范围内的,是在我们的预期下主动抛出的,是一种“业务异常”,他和系统抛出的RuntimeException不一样,像空指针、数组越界之类的(他们都是RuntimeException的子类)他们表示你代码写的有问题,代码不够完善,而我们自己抛出的异常,是一种在预期范围内的业务逻辑上的错误,我们希望把他们给区分开来,于是会为这种业务异常单独定义一个类:

public class MyException extends RuntimeException {

    private String errorMsg;

    public MyException() {
    }

    //省略getter、setter等方法
}

然后在抛异常的时候抛出自定义类的异常:

@GetMapping("/{id}")
public User queryById(@PathVariable("id") Long id) {
    User user = userService.queryById(id);
    if(user == null){
        throw new MyException("该用户不存在");
    }

    //可能还有其他的业务逻辑。。。。
    
    //最终返回结果
    return user;
}

于是@ControllerAdvice里面的写法会变成这样:

@Slf4j
@ControllerAdvice
@ResponseBody
public class GlobalExceptionHandler {

    //主要改动点在这里,把RuntimeException.class改成了MyException.class
    @ExceptionHandler(MyException.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public String myException(MyException e) {
        //记录日志
        log.error("自定义的异常:" + e.getMessage());

        //返回结果
        return "自定义的异常:" + e.getMessage();
    }

    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public String exception(Exception e) {
        //记录日志
        log.error("exception异常:" + e.getMessage());

        //返回结果
        return "exception异常:" + e.getMessage();
    }
}

其实就是把原来的RuntimeException改成了自定义的异常,另外仍然由Exception处理自定义异常之外的所有异常。

统一返回给前端的数据结构

接下来的一个优化就是,统一返回给前端的数据结构,上面代码当中返回给前端的都是一个字符串,用来描述异常信息,但其实更好的做法是像@Controller里的接口那样,返回一个JavaBean,甚至是所有类型的异常都返回一个格式统一的JavaBean,于是我们会定义一个用于表示返回值数据结构的类:

public class MyExceptionResponse implements Serializable {

    //其实这个code不是必须的,
    //不过下面的errorMsg就比较有必要,你总得有一个字符串来描述你的异常信息吧
    private int code;
    private String errorMsg;

    public MyExceptionResponse() {
    }
    
    //其他的什么构造方法、getter、setter方法就不写了,大家能看明白就行
}

最终得到的@ControllerAdvice就变成下面这样:

@Slf4j
@ControllerAdvice
@ResponseBody
public class GlobalExceptionHandler {

    @ExceptionHandler(MyException.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public MyExceptionResponse runtimeException(MyException e) {
        //记录日志
        log.error("自定义的异常:" + e.getMessage());
        
        //其他处理逻辑。。。。

        //返回结果
        MyExceptionResponse response = new MyExceptionResponse(1001, "自定义的异常:" + e.getMessage());
        return response;
    }

    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public MyExceptionResponse exception(Exception e) {
        //记录日志
        log.error("exception异常:" + e.getMessage());
        
        //其他处理逻辑。。。。

        //返回结果
        MyExceptionResponse response = new MyExceptionResponse(1002, "exception异常:" + e.getMessage());
        return response;
    }
}

其他补充

注意一下,@ControllerAdvice是对@Controller进行功能增强,搭配@ExceptionHandler进行全局异常处理只是他其中的一个用法(可以认为异常处理也是一种功能增强),他还可以搭配@InitBinder或者@ModelAttribute实现其他功能,基本都是对@Controller进行增强。

  • 19
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,您想了解关于使用 @ExceptionHandler + @ControllerAdvice +枚举实现全局异常统一处理的内容,这是一个常用的方案,可以实现全局异常的捕获和处理,让程序更加健壮和稳定。 @ExceptionHandler注解用于标记一个方法,该方法用于处理指定的异常类型。@ControllerAdvice注解用于标记一个类,该类用于处理全局异常。枚举则可以用于定义异常类型,方便异常处理时的统一管理。 在实现全局异常处理时,我们可以通过 @ExceptionHandler 注解来捕获指定的异常类型,并在方法中进行相应的处理。@ControllerAdvice注解可以让我们在一个类中定义多个 @ExceptionHandler方法,用于处理不同类型的异常。同时,我们也可以使用枚举来定义不同的异常类型,方便管理和调用。 下面是一个简单的示例,演示如何使用 @ExceptionHandler + @ControllerAdvice +枚举实现全局异常统一处理: ```java @ControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(value = BusinessException.class) @ResponseBody public ResultVO handleBusinessException(BusinessException e) { return ResultVO.error(e.getCode(), e.getMessage()); } } public enum ExceptionEnum { PARAMETER_ERROR(1001, "参数错误"), DATA_NOT_FOUND(1002, "数据不存在"), SYSTEM_ERROR(5000, "系统错误"); private final int code; private final String message; ExceptionEnum(int code, String message) { this.code = code; this.message = message; } public int getCode() { return code; } public String getMessage() { return message; } } public class BusinessException extends RuntimeException { private final int code; public BusinessException(int code, String message) { super(message); this.code = code; } public BusinessException(ExceptionEnum exceptionEnum) { super(exceptionEnum.getMessage()); this.code = exceptionEnum.getCode(); } public int getCode() { return code; } } ``` 在上面的示例中,GlobalExceptionHandler类标记了@ControllerAdvice注解,用于全局异常处理。其中,handleBusinessException方法用于处理BusinessException异常,返回一个ResultVO对象,其中包含错误码和错误信息。 BusinessException则是一个自定义的异常类,它包含一个code属性和一个message属性,用于表示异常的错误码和错误信息。同时,它还提供了一个构造方法,可以根据ExceptionEnum来构造一个BusinessException对象。 ExceptionEnum则是一个枚举类,包含了不同的异常类型,每个异常类型都有一个对应的错误码和错误信息。 在实际开发中,我们可以根据实际需求来定义不同的异常类型和错误码,以便更好地管理和调用。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值