Bootstrap

做了这么多年程序员,你真的还是只会用@ControllerAdvice来做全局的异常处理吗?

1、@ControllerAdvice注解说明

@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 {};
}
1.1、内部注解说明

这段代码定义了一个名为 ControllerAdvice 的注解。让我们来逐个说明它的各个部分:

  • @Target({ElementType.TYPE}): 这表示该注解可以应用在类上。因此,ControllerAdvice 注解可以用于标记类。

  • @Retention(RetentionPolicy.RUNTIME): 该注解说明在运行时可以通过反射获取到这个注解的信息。

  • @Documented: 这表示当生成 Javadoc 文档时,注解会被包含在文档中。

  • @Component: 由于 @ControllerAdvice 是一个特殊类型的组件,因此使用了 Spring Framework 中的 @Component 注解,表明这是一个Spring管理的组件。

  • @AliasFor("basePackages"): 这个注解表示 value() 属性和 basePackages() 属性是互相别名,它们具有相同的作用。也就是说,你可以使用其中之一来指定要扫描的基础包。

  • String[] value() default {}: 通过 @AliasFor 表明 value 属性是用来指定需要扫描的基础包路径。

  • String[] basePackages() default {}: 通过 @AliasFor 表明 basePackages 属性也是用来指定需要扫描的基础包路径。

  • Class<?>[] basePackageClasses() default {}: 允许以类的方式指定需要扫描的基础包路径。

  • Class<?>[] assignableTypes() default {}: 允许以类的方式指定适用的控制器类型。

  • Class<? extends Annotation>[] annotations() default {}: 允许指定需要扫描的控制器注解类型。

总的来说,ControllerAdvice 注解提供了一种方便的方式来定义全局控制器建议。通过指定需要扫描的基础包路径、适用的控制器类型以及需要扫描的控制器注解类型,可以集中处理应用程序中抛出的异常,并进行全局的预处理。

1.2、联合其它注解使用

@ControllerAdvice 注解可以与以下注解联合使用:

  1. @ExceptionHandler: 用于定义全局的异常处理方法。

  2. @InitBinder: 用于自定义数据绑定规则,通常用于日期格式化等操作。

  3. @ModelAttribute: 用于添加全局范围的模型属性,以便在每个请求的视图中都可用。

这些注解的结合使用可以让我们更方便地管理全局性的异常处理、数据绑定规则和模型属性,从而提高代码的复用性和整体的灵活性。

2、实战demo

2.1、项目目录说明

2.2、测试代码[Controller]
package com.example.demo.controller;

import com.example.demo.response.MidGroundResult;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author fyw
 * @version 1.0
 * @description: TODO
 * @date 2024/1/22 17:21
 */
@RestController
@RequestMapping("/test1")
public class Test1Controller {


    @RequestMapping("/test")
    public MidGroundResult test() {
        int i = 3 / 0;
        return MidGroundResult.ok("");
    }
}
package com.example.demo.controller;

import com.example.demo.response.MidGroundResult;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author fyw
 * @version 1.0
 * @description: TODO
 * @date 2024/1/22 17:21
 */
@RestController
@RequestMapping("/test2")
public class Test2Controller {

    @RequestMapping("/test")
    public MidGroundResult test() {
        int i = 3 / 0;
        return MidGroundResult.ok("");
    }
}
package com.example.demo.controller.group1;

import com.example.demo.response.MidGroundResult;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author fyw
 * @version 1.0
 * @description: TODO
 * @date 2024/1/22 17:21
 */
@RestController
@RequestMapping("/test3")
public class Test3Controller {
    @RequestMapping("/test")
    public MidGroundResult test() {
        int i = 3 / 0;
        return MidGroundResult.ok("");
    }
}
package com.example.demo.controller.group1;

import com.example.demo.response.MidGroundResult;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Date;

/**
 * @author fyw
 * @version 1.0
 * @description: TODO
 * @date 2024/1/22 17:22
 */
@RestController
@RequestMapping("/test4")
public class Test4Controller {


    @RequestMapping("/test")
    public MidGroundResult test(Date date) {
        return MidGroundResult.ok(date);
    }
}
package com.example.demo.controller.group1;

import cn.hutool.json.JSONObject;
import com.example.demo.response.MidGroundResult;
import org.springframework.ui.Model;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.*;

import java.util.Date;

/**
 * @author fyw
 * @version 1.0
 * @description: TODO
 * @date 2024/1/22 17:22
 */
@RestController
@RequestMapping("/test5")
public class Test5Controller {



    @RequestMapping("/test")
    public MidGroundResult test(Date date) {
        return MidGroundResult.ok(date);
    }

    /**
     *
     * @param modelMap 全局参数
     * @param jsonObject 具体的业务参数
     * @return
     */
    @PostMapping("/test1")
    public MidGroundResult test(ModelMap modelMap, @RequestBody JSONObject jsonObject) {
        Object globalAttr = modelMap.getAttribute("globalAttr");
        Object map = modelMap.getAttribute("map");
        jsonObject.set("globalAttr",globalAttr);
        jsonObject.set("map",map);
        return MidGroundResult.ok(jsonObject);
    }
}
2.2、异常处理
2.2.1、全局异常类
package com.example.demo.exception;

import com.example.demo.response.MidGroundResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.annotation.Order;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

/**
 * @author fyw
 * @version 1.0
 * @description: TODO
 * @date 2024/1/23 13:35
 */
@Slf4j
@ControllerAdvice
@Order(value = 0)
public class GobalException {

    @ExceptionHandler(RuntimeException.class)
    @ResponseBody
    public MidGroundResult<RuntimeException> runTimeExcepetion(RuntimeException ex){
        log.error("{}全局异常处理器","GobalException",ex);
        return MidGroundResult.error("全局异常处理器");
    }

    @ExceptionHandler(Exception.class)
    @ResponseBody
    public MidGroundResult excetion(RuntimeException ex){
        log.error("{}全局异常处理器","GobalException",ex);
        return MidGroundResult.error("全局异常处理器");
    }
}
2.2.2、指定包异常捕获类
package com.example.demo.exception;

import com.example.demo.response.MidGroundResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.annotation.Order;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

/**
 * @author fyw
 * @version 1.0
 * @description: 仅在某一个包生效【注意order对应的值可以介于全局和指定类之间】
 * basePackageClasses = {Test1Controller.class}代表这个类所在的包和子包下面的controller都会被捕获
 * value/basePackages = {"com.example.demo.controller.group1"}代表这个包和子包下面的controller都会被捕获
 * @date 2024/1/23 13:35
 */
@Slf4j
//@ControllerAdvice(basePackageClasses = {Test1Controller.class})
@ControllerAdvice(value = {"com.example.demo.controller.group1"})
@Order(value = 2)
public class SepcialPackagelException {

    @ExceptionHandler(RuntimeException.class)
    @ResponseBody
    public MidGroundResult<RuntimeException> runTimeExcepetion(RuntimeException ex){
        log.error("{},{}","SepcialPackagelException",ex);
        return MidGroundResult.error(ex);
    }

    @ExceptionHandler(Exception.class)
    @ResponseBody
    public MidGroundResult excetion(RuntimeException ex){
        log.error("{},{}","SepcialPackagelException",ex);
        return MidGroundResult.error("");
    }
}
2.2.3、指定注解异常捕获类
package com.example.demo.exception;

import com.example.demo.response.MidGroundResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.annotation.Order;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author fyw
 * @version 1.0
 * @description: 仅针对某一类注解生效【注意order对应的值可以介于全局和指定类之间】
 * annotations = {RestController.class}将会扫描所有被 @RestController 注解标记的控制器类,然后应用在这些类上
 * @date 2024/1/23 13:35
 */
@Slf4j
@ControllerAdvice(annotations = {RestController.class})
@Order(value = 101)
public class SepcialAnnotationException {

    @ExceptionHandler(RuntimeException.class)
    @ResponseBody
    public MidGroundResult<RuntimeException> runTimeExcepetion(RuntimeException ex){
        log.error("{}注解过滤器","SepcialAnnotationException",ex);
        return MidGroundResult.error("注解过滤器");
    }

    @ExceptionHandler(Exception.class)
    @ResponseBody
    public MidGroundResult excetion(RuntimeException ex){
        log.error("{}注解过滤器","SepcialAnnotationException",ex);
        return MidGroundResult.error("注解过滤器");
    }
}
2.2.4、指定某个类的异常捕获类
package com.example.demo.exception;

import com.example.demo.controller.Test1Controller;
import com.example.demo.response.MidGroundResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.annotation.Order;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

/**
 * @author fyw
 * @version 1.0
 * @description: 仅在某一个类上生效【注意order对应的值可以设置为最小】
 * @date 2024/1/23 13:35
 */
@Slf4j
@ControllerAdvice(assignableTypes = {Test1Controller.class})
@Order(value = 1)
public class SepcialClasslException {

    @ExceptionHandler(RuntimeException.class)
    @ResponseBody
    public MidGroundResult<RuntimeException> runTimeExcepetion(RuntimeException ex){
        log.error("{}针对Test1Controller类淡出捕获的异常,不会走全局异常捕获类","SepcialClasslException",ex);
        return MidGroundResult.error("针对Test1Controller类淡出捕获的异常,不会走全局异常捕获类");
    }

    @ExceptionHandler(Exception.class)
    @ResponseBody
    public MidGroundResult excetion(RuntimeException ex){
        log.error("{}针对Test1Controller类淡出捕获的异常,不会走全局异常捕获类","SepcialClasslException",ex);
        return MidGroundResult.error("针对Test1Controller类淡出捕获的异常,不会走全局异常捕获类");
    }
}
2.3、测试场景
2.3.1、将GobalException异常处理器的order顺序优先级调整为最高0,进行异常捕获测试

<span style="background-color:#f8f8f8"><span style="color:#333333">测试结果是无论在controller目录还是group1目录下的controller类都是走的全局过滤器;
</span></span>
2.3.2、注解异常捕获类测试,将SepcialAnnotationException的order调为0,GobalException调为100

<span style="background-color:#f8f8f8"><span style="color:#333333">测试结果是无论在controller目录还是group1目录下的controller类都是走的注解过滤器,因为controller上面都有@RestController注解;
</span></span>
2.3.3、指定包异常捕获类测试,将SepcialAnnotationException的order调整为101,SepcialPackagelException的order调整为0

<span style="background-color:#f8f8f8"><span style="color:#333333">测试结果test3在group1包下面吗,走的是包过滤器;test2在controller包下面走的是全局过滤器
</span></span>
2.3.4、在2.3.3的基础上测试test1应该走的是SepcialClasslException,因为SepcialClasslException上指定了具体的Test1Controller类,并且order小于全局异常捕获器和注解异常捕获器

<span style="background-color:#f8f8f8"><span style="color:#333333">由以上测试场景,就可以在项目里面随心所欲的定义异常处理器了;
</span></span>
2.4、全局数据绑定
2.4.1、全局数据绑定类
package com.example.demo.bind;

import com.example.demo.controller.group1.Test5Controller;
import org.springframework.beans.propertyeditors.CustomDateEditor;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.InitBinder;

import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * @author fyw
 * @version 1.0
 * @description: 仅作用于Test5Controller类
 * @date 2024/1/23 16:25
 */
@ControllerAdvice(assignableTypes = {Test5Controller.class})
public class InitBinderController {

    @InitBinder
    public void initBinder(WebDataBinder binder) {
        System.out.println("进入initBinder方法");
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
        binder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, true));
    }
}

测试结果如下:

2.5、全局模型属性
2.5.1、全局模型属性添加类
package com.example.demo.model;

import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ModelAttribute;

import java.util.HashMap;
import java.util.Map;

/**
 * @author fyw
 * @version 1.0
 * @description: TODO
 * @date 2024/1/23 16:37
 */
@ControllerAdvice
public class ModelVController {

    @ModelAttribute
    public void presetParam(Model model){
        model.addAttribute("globalAttr","this is a global attribute");
    }

    @ModelAttribute("map")
    public Map<String, String> presetParam(){
        Map<String, String> map = new HashMap<String, String>();
        map.put("key1", "value1");
        map.put("key2", "value2");
        map.put("key3", "value3");
        return map;
    }
}

测试结果:

2.6、公用类代码

package com.example.demo.response;

import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.IdUtil;
import com.example.demo.enums.MidGroundEnum;
import lombok.Data;

import java.time.LocalDateTime;

/**
 * @author fyw
 * @version 1.0
 * @description: 
 * @date 2024/1/16 8:53
 */
@Data
public class MidGroundResult<T> {

    //返回码,00000成功
    private String errorCode;

    //返回值,说明信息
    private String errorMessage;

    //返回数据
    private T data;

    //业务响应时间 2023-05-25 18:18:18
    private String resultTime;

    //排查id
    private String traceId;

    public MidGroundResult(String errorCode, String errorMessage, T data, String resultTime, String traceId) {
        this.errorCode = errorCode;
        this.errorMessage = errorMessage;
        this.data = data;
        this.resultTime = resultTime;
        this.traceId = traceId;
    }


    public String getErrorCode() {
        return errorCode;
    }

    public void setErrorCode(String errorCode) {
        this.errorCode = errorCode;
    }

    public String getErrorMessage() {
        return errorMessage;
    }

    public void setErrorMessage(String errorMessage) {
        this.errorMessage = errorMessage;
    }

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }

    public String getResultTime() {
        return resultTime;
    }

    public void setResultTime(String resultTime) {
        this.resultTime = resultTime;
    }

    public String getTraceId() {
        return traceId;
    }

    public void setTraceId(String traceId) {
        this.traceId = traceId;
    }

    //正常响应
    public static <T> MidGroundResult ok(T data) {
        return new MidGroundResult(MidGroundEnum.SUCCESS.getCode(), MidGroundEnum.SUCCESS.getMsg(), data,
                DateUtil.format(LocalDateTime.now(), DatePattern.NORM_DATETIME_PATTERN),
                IdUtil.getSnowflake(1, 1).nextIdStr());
    }

    //系统异常
    public static <T> MidGroundResult error(T data) {
        return new MidGroundResult(MidGroundEnum.ERROR.getCode(), MidGroundEnum.ERROR.getMsg(), data,
                DateUtil.format(LocalDateTime.now(), DatePattern.NORM_DATETIME_PATTERN),
                IdUtil.getSnowflake(1, 1).nextIdStr());
    }

    //主动性返回异常信息-校验不通过等
    public static <T> MidGroundResult fail(T data, String code, String msg) {
        return new MidGroundResult(code, msg, data,
                DateUtil.format(LocalDateTime.now(), DatePattern.NORM_DATETIME_PATTERN),
                IdUtil.getSnowflake(1, 1).nextIdStr());
    }

}
package com.example.demo.enums;

/**
 * @Author fyw
 * @Description 
 * @Date 8:59 2024/1/16
 * @Param
 * @return
 **/
public enum MidGroundEnum {

    SUCCESS("00000", "成功"),
    INTERNAL_CALL_ERROR("C1001", "内部服务调用错误"),

    ERROR("B1001", "系统错误");

    private final String code;

    private final String msg;

    MidGroundEnum(String code, String msg) {
        this.code = code;
        this.msg = msg;
    }

    public String getCode() {
        return code;
    }

    public String getMsg() {
        return msg;
    }
}

添加公众号了解更多,定期分享、绝对实用,绝对对你有帮助!

;