@ControllerAdvice全局异常处理详解

引言

  最近在前后端分离的项目中,后端人员需要将异常信息返回给前端,并且提供不同的响应码,根据不同的响应,前端再进行友好的提示。于是乎,就需要在项目中做异常的处理,由于处理的地方太多太多,所以采用@ControllerAdvice注解进行全局异常统一处理。工作之余,便进一步的对此注解进行了学习。

@ControllerAdvice介绍

  @ControllerAdvice是Spring框架提供的一个注解,它可以用来集中处理应用程序中的异常情况,并且在一个地方进行全局的数据绑定和预处理。通常结合@ExceptionHandler@InitBinder@ModelAttribute注解来实现全局的异常处理全局数据绑定全局数据预处理,我们姑且可以把这三个注解称之为"全局三剑客"。

这里简单说下"全局三剑客"

  1. @ExceptionHandler:在@ControllerAdvice类中定义一些方法,使用@ExceptionHandler注解来捕获和处理应用程序中出现的异常。

  2. @InitBinder:在@ControllerAdvice类中定义方法,使用@InitBinder注解,可以进行全局的数据绑定操作。例如,将请求参数转换成指定类型的对象,比如格式化日期。

  3. @ModelAttribute:使用@ModelAttribute注解定义的方法,在每个请求处理之前都会被调用,可以为所有请求添加一些全局的模型数据。避免了在每个Controller中都进行重复设置。

通过查看@ControllerAdvice注解的源码,不难发现,其实它就是就是@Component的一个封装

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface ControllerAdvice {

	@AliasFor("basePackages")
	String[] value() default {};

	@AliasFor("value")
	String[] basePackages() default {};

	Class<?>[] basePackageClasses() default {};

	Class<?>[] assignableTypes() default {};

	Class<? extends Annotation>[] annotations() default {};

}

@ControllerAdvice注解的类会被Spring扫描到,并实例化一个bean, 注册到Spring容器中。

全局异常处理@ExceptionHandler

异常测试:

@RestController
@RequestMapping("/api/v1/")
public class ExceptionTestController {

    /**
     * 测试异常
     *
     * @return
     */
    @GetMapping("/error")
    public ResponseEntity<String> testException() {
        int temp = 1 / 0;
        return new ResponseEntity<>("请求成功",HttpStatus.OK);
    }
}

此时请求这个方法肯定会出错,且页面呈现下面的样子
在这里插入图片描述

控制台抛出错误
在这里插入图片描述
请添加图片描述

加入全局异常处理

@ControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    /**
     * 处理算术异常,例如除数为0的情况。
     *
     * @param exception 算术异常对象
     * @return ResponseEntity<String> 响应实体对象,包含异常信息和HTTP状态码
     */
    @ExceptionHandler(ArithmeticException.class)
    public ResponseEntity<String> bizException(ArithmeticException exception) {
        log.error("异常信息:{}", exception.getMessage());
        return new ResponseEntity<>("除数不能为0", HttpStatus.NOT_FOUND);
    }
}

上述方法注解@ExceptionHandler中指定了处理ArithmeticException类型的异常。

我们再看下该注解的源码

/**
 * 用于标注处理异常的方法的注解。
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ExceptionHandler {

	/**
	 * 指定被注解方法处理的异常类型。如果为空,则默认为方法参数列表中列出的任何异常。
	 */
	Class<? extends Throwable>[] value() default {};

}

  因为Throwable是所有异常的父类,所以,如果不写,则会处理任何异常,因此,我们可以写不同的方法来处理不同的异常信息。

自定义异常

我们再自定义一个异常看看

/**
 * 自定义异常
 */
public class MyException extends RuntimeException{
}
/**
  * 处理自定义异常
  * @param exception
  * @return
  */
@ExceptionHandler(MyException.class)
public ResponseEntity<String> myException(MyException exception) {
    log.error("自定义异常信息:{}", exception.getMessage());
    return new ResponseEntity<>("自定义异常信息", HttpStatus.BAD_REQUEST);
}

/**
    * 测试自定义的异常
    *
    * @return
    */
@GetMapping("/myerror")
public ResponseEntity<String> testMyException() {
    throw new MyException();
}

请求此方法就会进入自定义异常。

  我们知道,通过在一个类上添加@ControllerAdvice注解,就可以使这个类成为全局的异常处理器。那么如果我有多个类都加了这个注解,并且两个异常处理类中都有对同一异常的处理方法,到底进入哪一个呢?

我们先增加一个异常处理类,并且处理我们的ArithmeticException异常

@ControllerAdvice
@Slf4j
public class DivGlobalExceptionHandler {

    /**
     * 处理算术异常,例如除数为0的情况。
     *
     * @param exception 算术异常对象
     * @return ResponseEntity<String> 响应实体对象,包含异常信息和HTTP状态码
     */
    @ExceptionHandler(ArithmeticException.class)
    public ResponseEntity<String> bizException(ArithmeticException exception) {
        log.error("进入DivGlobalExceptionHandler异常处理器,异常信息:{}", exception.getMessage());
        return new ResponseEntity<>("除数不能为0", HttpStatus.NOT_FOUND);
    }
}


@ControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    /**
     * 处理算术异常,例如除数为0的情况。
     *
     * @param exception 算术异常对象
     * @return ResponseEntity<String> 响应实体对象,包含异常信息和HTTP状态码
     */
    @ExceptionHandler(ArithmeticException.class)
    public ResponseEntity<String> bizException(ArithmeticException exception) {
        log.error("进入GlobalExceptionHandler异常处理器,异常信息:{}", exception.getMessage());
        return new ResponseEntity<>("除数不能为0", HttpStatus.NOT_FOUND);
    }
}

我们访问下测试方法,看看进入哪一个异常处理器,通过测试,我们发现它只会进入一个异常处理器
在这里插入图片描述

那么疑问就来了,另外一个没生效吗,我们访问下之前自定义异常的方法
在这里插入图片描述

  不难发现其实是生效的,对于上面的ArithmeticException异常不进入GlobalExceptionHandler异常处理类,难道和加载顺序还有关系?我们通过@Order改变下加载顺序,让GlobalExceptionHandler类先加载

@Order注解可以用来控制这些组件的加载顺序,数值越小,优先级越高。

@ControllerAdvice
@Slf4j
@Order(1) // 先加载
public class GlobalExceptionHandler {
    
}

@ControllerAdvice
@Slf4j
@Order(2) // 后加载
public class DivGlobalExceptionHandler {
    
}

直接看结果
在这里插入图片描述

总结下:当有多个异常处理类时,如果某个异常在所有的类中都有,则会进入先加载的异常处理类中,若先加载的类中没有某个特定异常,而后加载的类中有,则会进入后加载的类。一般情况下,我们可以通过@Order注解来改变异常处理类加载的顺序,比如将特殊的异常处理类先加载。

全局数据绑定@InitBinder

  通过 @InitBinder 注解标记的方法可以自定义数据绑定规则,将请求参数转换为控制器方法的参数类型。比如,将请求参数字符串转换为日期对象、对字符串进行特殊处理等等。

/**
  * 初始化参数绑定器,用于自定义数据绑定规则。
  *
  * @param webDataBinder WebDataBinder对象,负责将HTTP请求参数转换为控制器方法的参数类型。
  */
@InitBinder
public void initParam(WebDataBinder webDataBinder) {
    // 注册自定义的日期编辑器,将字符串形式的日期参数转换为Date对象
    webDataBinder.registerCustomEditor(Date.class, new CustomDateEditor(new SimpleDateFormat("yyyy-MM-dd"), false));

    // 注册自定义的字符串编辑器,将字符串在绑定前添加后缀"qwe"
    webDataBinder.registerCustomEditor(String.class, new PropertyEditorSupport() {
        @Override
        public void setAsText(String text) throws IllegalArgumentException {
            setValue(text + "qwe");
        }
    });

}

// 请求地址:http://localhost:8000/api/v1/param?date=2024-5-9&str=123
@GetMapping("/param")
public Map<String, Object> testParam(Date date, String str) {
    Map<String, Object> map = new HashMap<>();
    map.put("string", str);
    map.put("date", date);
    return map;
}

访问地址http://localhost:8000/api/v1/param?date=2024-5-9&str=123
在这里插入图片描述

已经达到了我们想要的效果。

全局数据预处理@ModelAttribute

  可以将一些常用的数据在每个请求处理方法之前添加到模型Model中,可以避免在每个方法中重复添加相同的数据。

/**
 * 使用 @ModelAttribute 注解定义一个方法,在每个请求处理方法之前生成一个随机的 UUID,并将其添加到模型中。
 * 方法的返回值将会被添加到模型中,并且可以在控制器中的其他方法中使用。
 *
 * @return 
 */
@ModelAttribute("uuid")
public String generateRandomUUID() {
    return UUID.randomUUID().toString();
}

/**
 * 使用 @ModelAttribute 注解将之前生成的随机UUID注入到方法参数中。
 *
 * @param str 从模型中注入的随机UUID字符串
 * @return 
 */
@GetMapping("/modelParam")
public Map<String, Object> testModelAttrParam(@ModelAttribute("uuid") String str) {
    Map<String, Object> map = new HashMap<>();
    map.put("uuid", str);
    return map;
}

请求http://localhost:8000/api/v1/modelParam 结果如下:
在这里插入图片描述

这里就会自动生成一个uuid字符串

再举个例子:

@ModelAttribute // 注意这里没有name
public void addGlobalAttributes(Model model) {
    // 添加一个版本号
    model.addAttribute("version", "1.0.0");
}


@GetMapping("/modelParam")
public Map<String, Object> testModelAttrParam(@ModelAttribute("uuid") String str, Model model) {
    Map<String, Object> map = new HashMap<>();
    map.put("uuid", str);
    Map<String, Object> modelMap = model.asMap();
    map.put("version", (String) modelMap.get("version"));// 从model里面获取注入的version
    return map;
}

访问下就可以看到已经取到了注入的version字段
在这里插入图片描述
  其实无论哪种方式都是从Model中存储属性的Map里取数据,通过key值就可以获取相应的属性值,非常的方便。

  • 15
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
如果不想使用@ControllerAdvice注解,我们也可以通过实现HandlerExceptionResolver接口来实现全局异常处理。具体步骤如下: 1. 创建一个类,实现HandlerExceptionResolver接口。 2. 在类中实现resolveException方法,该方法会在全局异常发生时被执行。 3. 在resolveException方法中,我们可以根据不同的异常类型进行不同的处理,比如返回自定义错误信息或者跳转到指定页面。 4. 最后,我们需要将该类注册到Spring框架中,可以通过在配置文件中进行配置或者使用注解的方式进行注册。 以下是一个简单的示例: ```java public class GlobalExceptionHandler implements HandlerExceptionResolver { @Override public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { // 返回自定义错误信息 ModelAndView modelAndView = new ModelAndView(); modelAndView.addObject("errorMsg", "系统错误,请稍后重试!"); modelAndView.setViewName("error"); return modelAndView; } } ``` 在上述代码中,我们实现了HandlerExceptionResolver接口并重写了resolveException方法。在该方法中,我们将错误信息存入ModelAndView中,并将viewName设置为"error",表示跳转到error页面显示错误信息。 最后,我们需要将该类注册到Spring框架中。可以通过在配置文件中进行配置或者使用注解的方式进行注册。例如,在Spring配置文件中添加如下配置: ```xml <bean id="globalExceptionHandler" class="com.example.GlobalExceptionHandler"/> ``` 在上述配置中,我们将GlobalExceptionHandler类注册为Spring的一个bean,并使用该类处理全局异常

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值