`@ControllerAdvice`和`@RestControllerAdvice`的使用

@ControllerAdvice@RestControllerAdvice的使用

作用

其实两个注解是同一个,只不过一个是针对与@RestController的另一个是针对@Controller的, 前后端分离我们都会使用@RestController, 因此后面直接针对这个来说,两个的区别是@RestController相当于在所有@Controller里的@RequestMapping方法上添加了注解@RequestBody

可以简单理解为就是特意为@RestController设计的拦截器, 通过basePackages属性指定扫描的包, 则代表逻辑会织入到相关扫描到的RestController, 并且根据实际使用时机,来确定行为。

常用的方式有两种

  1. 在实现org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice接口的类上添加注解@RestControllerAdvice指定扫描包,则在扫描到的RestController执行我们写的RequestMapping方法后进入到这个逻辑,这样我们可以很方便包装全局返回对象类
  2. 配合注解@ExceptionHandler使用,定义一个类将注解@RestControllerAdvice添加到类上,然后定义本地方法,将注解@ExceptionHandler, 通过@ExceptionHandlervalue属性指定异常类, 则被扫描到的RestController方法出现异常,就会被@ExceptionHandler标识的方法拦截,这个机制可以让我们用来做全局异常处理。

全局对象返回类包装

步骤

  • 定义全局响应对象类
  • 考虑某些方法可能不需要拦截,要允许使用方跳过
  • 定义拦截处理逻辑
  1. 定义响应对象类

    package com.ddf.boot.common.core.response;
    
    import com.ddf.boot.common.core.exception200.BaseErrorCallbackCode;
    import com.ddf.boot.common.core.exception200.BusinessException;
    import java.util.Objects;
    import lombok.Data;
    import lombok.NoArgsConstructor;
    import lombok.experimental.Accessors;
    import lombok.extern.slf4j.Slf4j;
    
    /**
     * 统一响应内容类
     *
     * @author dongfang.ding
     * @date 2019/6/27 11:17
     */
    @Data
    @NoArgsConstructor
    @Accessors(chain = true)
    @Slf4j
    public class ResponseData<T> {
    
        /**
         * 返回消息代码
         */
        private String code;
        /**
         * 返回消息
         */
        private String message;
        /**
         * 错误堆栈信息
         */
        private String stack;
        /**
         * 响应时间
         */
        private long timestamp;
        /**
         * 返回数据
         */
        private T data;
    
        /**
         * 扩展字段
         * 比如在某些情况下正常逻辑返回的是data
         * 某些异常逻辑下返回的是另外一套数据
         */
        private Object extra;
    
    
        public ResponseData(String code, String message, String stack, long timestamp, T data) {
            this.code = code;
            this.message = message;
            this.stack = stack;
            this.timestamp = timestamp;
            this.data = data;
        }
    
        /**
         * 成功返回数据方法
         *
         * @param data
         * @param <T>
         * @return
         */
        public static <T> ResponseData<T> success(T data) {
            return new ResponseData<>(BaseErrorCallbackCode.COMPLETE.getCode(),
                    BaseErrorCallbackCode.COMPLETE.getDescription(), "", System.currentTimeMillis(), data
            );
        }
    
        /**
         * 失败返回消息方法
         *
         * @param code
         * @param message
         * @param stack
         * @param <T>
         * @return
         */
        public static <T> ResponseData<T> failure(String code, String message, String stack) {
            return new ResponseData<>(code, message, stack, System.currentTimeMillis(), null);
        }
    
    
        /**
         * 判断返回结果是否是成功
         *
         * @return
         */
        public boolean isSuccess() {
            return Objects.equals(code, BaseErrorCallbackCode.COMPLETE.getCode());
        }
    
    
        /**
         * 获取返回数据, 如果响应码非成功,则抛出异常
         *
         * @return
         */
        public T requiredSuccess() {
            if (isSuccess()) {
                return data;
            }
            throw new BusinessException(code, message);
        }
    
    
        /**
         * 获取返回数据, 如果响应码非成功,返回指定默认值
         *
         * @param defaultValue
         * @return
         */
        public T failureDefault(T defaultValue) {
            if (!isSuccess()) {
                return defaultValue;
            }
            return data;
        }
    
    }
    
  2. 定义一个注解来标识在方法上,则方法不会被包装

    package com.ddf.boot.common.core.controllerwrapper;
    
    import java.lang.annotation.Documented;
    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    
    /**
     * <p>忽略包装标识的方法</p >
     *
     * @author Snowball
     * @version 1.0
     * @date 2021/08/26 20:19
     */
    @Target({ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface WrapperIgnore {
    }
    
  3. 定义处理逻辑

    package com.ddf.boot.common.core.controllerwrapper;
    
    import com.ddf.boot.common.core.response.ResponseData;
    import java.lang.reflect.AnnotatedElement;
    import java.util.Arrays;
    import org.springframework.core.MethodParameter;
    import org.springframework.core.annotation.Order;
    import org.springframework.http.MediaType;
    import org.springframework.http.converter.HttpMessageConverter;
    import org.springframework.http.server.ServerHttpRequest;
    import org.springframework.http.server.ServerHttpResponse;
    import org.springframework.web.bind.annotation.DeleteMapping;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.PutMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestControllerAdvice;
    import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
    
    /**
     * 允许在执行一个@ResponseBody 或一个ResponseEntity控制器方法之后但在使用一个主体写入正文之前自定义响应HttpMessageConverter。
     *
     * @author dongfang.ding
     * @date 2019/6/27 11:15
     */
    @RestControllerAdvice(basePackages = {"com"})
    @Order
    public class CommonResponseBodyAdvice implements ResponseBodyAdvice<Object> {
    
        private static final Class[] ANNOTATIONS = {
                RequestMapping.class, GetMapping.class, PostMapping.class, DeleteMapping.class, PutMapping.class
        };
    
        /**
         * @param returnType
         * @param converterType
         * @return
         */
        @Override
        public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
            if (returnType.getMethod() == null || returnType.getMethod().isAnnotationPresent(WrapperIgnore.class)) {
                return false;
            }
            AnnotatedElement element = returnType.getAnnotatedElement();
            return Arrays.stream(ANNOTATIONS).anyMatch(
                    annotation -> annotation.isAnnotation() && element.isAnnotationPresent(annotation));
        }
    
        @Override
        public ResponseData<Object> beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
                Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request,
                ServerHttpResponse response) {
            return ResponseData.success(body);
        }
    }
    
  4. 特殊处理

    有一种情况是方法返回值就是String, 这个时候包装可能会出现问题, 需要添加一个格式转换器,如MappingJackson2HttpMessageConverter并且将顺序放到最前面

    @Configuration
    public class CoreWebConfig implements WebMvcConfigurer {
         @Override
         public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
             converters.add(0, new MappingJackson2HttpMessageConverter());
         }
    }
    
  5. 使用

    1. 遵循上面定义的扫描包的规则创建Controller类,添加注解@RestController

    2. 在方法上定义上面拦截规则中出现的注解, 如GetMapping

    3. 方法返回值不要返回上面定义的包装对象, 直接返回数据原始类型即可, 在最终返回前端时会经过上面写的拦截逻辑处理一层然后包装进去,完成统一返回对象的作用

    4. 当然如果是SpringCloud,基于Feign这种调用模式的系统,就不要使用这种包装方式了,否则会包装两层。但是定义的统一对象还是可以使用的,里面也提供了一些静态方法可以使用

全局异常处理

在做之前先想一下,需要考虑和解决哪些问题?

  1. 要能抛出自己的系统,能自定义状态码和异常消息
  2. 为了方便管理状态码和消息,最好能定义类来进行管理。考虑到后续状态码越来越多问题,要使用接口来定义规范,实现类实现接口来保证异常码的维护,实现类可以选择枚举,每个系统或者模块单独定义
  3. 考虑到消息中可能需要可变量,因为异常要支持占位符
  4. 异常信息中可以包含详细的异常堆栈消息以及有些异常消息,仅仅是异常消息,比如代码中的check报错要把具体不满足的条件写出来,但是并不方便返回给用户,还需要返回额外的业务消息来隐藏系统信息。注意这个和堆栈是两个问题,不是指的同一个事情。
  5. 异常处理类定义为抽象类,因为无法确定应用使用方要扫描的包对象。还有我们已经定义了异常处理类,万一使用方想在异常的时候额外处理一些逻辑或者接管逻辑怎么办?所以处理异常也要预留扩展接口,允许使用方介入到异常处理逻辑中。
  6. 异常返回给前端也需要使用统一对象包装,使用自定义对象中的状态码来标识异常。那么还有没有其它信息返回?需要考虑将异常堆栈返回,这样方便测试和联调直接看到第一现场异常堆栈信息,但是又不能所有环境都返回,要在生产环境屏蔽。

开始编码

根据上述需求定义响应类,还是上面那个统一包装对象

package com.ddf.boot.common.core.response;

import com.ddf.boot.common.core.exception200.BaseErrorCallbackCode;
import com.ddf.boot.common.core.exception200.BusinessException;
import java.util.Objects;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import lombok.extern.slf4j.Slf4j;

/**
 * 统一响应内容类
 *
 * @author dongfang.ding
 * @date 2019/6/27 11:17
 */
@Data
@NoArgsConstructor
@Accessors(chain = true)
@Slf4j
public class ResponseData<T> {

    /**
     * 返回消息代码
     */
    private String code;
    /**
     * 返回消息
     */
    private String message;
    /**
     * 错误堆栈信息
     */
    private String stack;
    /**
     * 响应时间
     */
    private long timestamp;
    /**
     * 返回数据
     */
    private T data;

    /**
     * 扩展字段
     * 比如在某些情况下正常逻辑返回的是data
     * 某些异常逻辑下返回的是另外一套数据
     */
    private Object extra;


    public ResponseData(String code, String message, String stack, long timestamp, T data) {
        this.code = code;
        this.message = message;
        this.stack = stack;
        this.timestamp = timestamp;
        this.data = data;
    }

    /**
     * 成功返回数据方法
     *
     * @param data
     * @param <T>
     * @return
     */
    public static <T> ResponseData<T> success(T data) {
        return new ResponseData<>(BaseErrorCallbackCode.COMPLETE.getCode(),
                BaseErrorCallbackCode.COMPLETE.getDescription(), "", System.currentTimeMillis(), data
        );
    }

    /**
     * 失败返回消息方法
     *
     * @param code
     * @param message
     * @param stack
     * @param <T>
     * @return
     */
    public static <T> ResponseData<T> failure(String code, String message, String stack) {
        return new ResponseData<>(code, message, stack, System.currentTimeMillis(), null);
    }


    /**
     * 判断返回结果是否是成功
     *
     * @return
     */
    public boolean isSuccess() {
        return Objects.equals(code, BaseErrorCallbackCode.COMPLETE.getCode());
    }


    /**
     * 获取返回数据, 如果响应码非成功,则抛出异常
     *
     * @return
     */
    public T requiredSuccess() {
        if (isSuccess()) {
            return data;
        }
        throw new BusinessException(code, message);
    }


    /**
     * 获取返回数据, 如果响应码非成功,返回指定默认值
     *
     * @param defaultValue
     * @return
     */
    public T failureDefault(T defaultValue) {
        if (!isSuccess()) {
            return defaultValue;
        }
        return data;
    }
}

定义异常错误码*接口规范

所有错误码定义类必须实现该接口,方便后续逻辑类直接针对接口编码

package com.ddf.boot.common.core.exception200;

/**
 * <p>异常消息代码统一接口</p >
 *
 * @author dongfang.ding
 * @version 1.0
 * @date 2020/06/17 14:58
 */
public interface BaseCallbackCode {

    /**
     * 响应状态码
     *
     * @return
     */
    String getCode();

    /**
     * 响应消息
     *
     * @return
     */
    String getDescription();

    /**
     * 响应业务消息, 最终返回给用户的,如果需要隐藏系统异常细节,要记得重写这个方法
     *
     * @return
     */
    default String getBizMessage() {
        return getDescription();
    }
}

来一个基础实现样例

package com.ddf.boot.common.core.exception200;

import lombok.Getter;

/**
 * <p>异常定义枚举</p >
 *
 * @author dongfang.ding
 * @version 1.0
 * @date 2020/06/28 13:14
 */
public enum BaseErrorCallbackCode implements BaseCallbackCode {

    /**
     * 异常状态码,可持续补充
     */
    DEMO_BLA_BLA("base_0001", "系统内部出现问题,bla bla...", "系统异常,请稍后确认"),

    TEST_FILL_EXCEPTION("base_0002", "带占位符的异常演示[{0}]"),

    TEST_FILL_BIZ_EXCEPTION("base_0003", "带占位符的异常演示[{0}],客户端隐藏详细信息", "报错啦"),

    PAGE_NUM_NOT_ALLOW_NULL("base_0004", "当前页数不能为空"),

    PAGE_SIZE_NOT_ALLOW_NULL("base_0005", "每页大小不能为空"),

    COMPLETE("200", "请求成功"),

    BAD_REQUEST("400", "错误请求"),

    UNAUTHORIZED("401", "未通过认证"),

    ACCESS_FORBIDDEN("403", "权限未通过,访问被拒绝"),

    SERVER_ERROR("500", "服务端异常"),

    ;

    /**
     * 异常code码
     */
    @Getter
    private final String code;

    /**
     * 异常消息
     */
    @Getter
    private final String description;

    /**
     * 返回给用户的异常消息
     */
    @Getter
    private final String bizMessage;

    BaseErrorCallbackCode(String code, String description) {
        this.code = code;
        this.description = description;
        this.bizMessage = description;
    }

    BaseErrorCallbackCode(String code, String description, String bizMessage) {
        this.code = code;
        this.description = description;
        this.bizMessage = bizMessage;
    }
}

定义异常基类

这个异常基类要能存储占位符信息、自定义异常状态码、以及能够快速的使用自定义的异常类转换为异常

package com.ddf.boot.common.core.exception200;

import java.text.MessageFormat;
import lombok.Getter;
import org.springframework.context.MessageSource;

/**
 * <p>基准异常类</p >
 *
 * @author dongfang.ding
 * @version 1.0
 * @date 2020/06/17 15:55
 */
public abstract class BaseException extends RuntimeException {

    /**
     * 异常code码
     */
    @Getter
    private String code;

    /**
     * 异常消息
     */
    @Getter
    private String description;

    /**
     * 某些消息需要提供占位符希望运行时填充数据,这里可以传入占位符对应的参数
     * 注意格式化参数使用的是{@link MessageSource}, 所以请注意原展位参数需使用{0} {1} 方式
     */
    @Getter
    private Object[] params;

    /**
     * 保存业务异常相关信息的类
     */
    @Getter
    private BaseCallbackCode baseCallbackCode;


    /**
     * 用来包装其它异常来转换为自定义异常
     *
     * @param throwable
     */
    public BaseException(Throwable throwable) {
        super(throwable);
        initCallback(defaultCallback());
    }

    /**
     * 推荐使用的系统自定义的一套体系的异常使用方式,传入异常错误码类
     *
     * @param baseCallbackCode
     */
    public BaseException(BaseCallbackCode baseCallbackCode) {
        super(baseCallbackCode.getDescription());
        initCallback(baseCallbackCode);
    }

    /**
     * 同上,但是额外提供一种消息占位符的方式, baseCallbackCode中的message包含占位符, 使用的时候格式化参数后作为最终异常消息
     * 占位字符串采用{0} {1}这种角标方式
     *
     * @param baseCallbackCode
     * @param params
     */
    public BaseException(BaseCallbackCode baseCallbackCode, Object... params) {
        super(MessageFormat.format(baseCallbackCode.getDescription(), params));
        initCallback(baseCallbackCode, params);
    }

    /**
     * 只简单抛出消息异常
     *
     * @param description
     */
    public BaseException(String description) {
        super(description);
        initCallback(defaultCallback());
    }

    /**
     * 不走系统定义的错误码定义体系, 但是使用错误码和消息体系
     *
     * @param code
     * @param description
     */
    public BaseException(String code, String description) {
        super(description);
        initCallback(code, description);
    }

    /**
     * 同上,但是支持占位符
     *
     * @param code
     * @param description
     * @param params
     */
    public BaseException(String code, String description, Object... params) {
        super(MessageFormat.format(description, params));
        initCallback(code, description, params);
    }


    private void initCallback(BaseCallbackCode baseCallbackCode, Object... params) {
        this.baseCallbackCode = baseCallbackCode;
        initCallback(
                baseCallbackCode.getCode() == null ? defaultCallback().getCode() : baseCallbackCode.getCode(),
                baseCallbackCode.getDescription()
        );
    }


    /**
     * 初始化状态码
     *
     * @param code
     * @param description
     */
    private void initCallback(String code, String description, Object... params) {
        this.code = code == null ? defaultCallback().getCode() : code;
        this.params = params;
        this.description = MessageFormat.format(description, params);
    }


    /**
     * 当前异常默认响应状态码
     *
     * @return
     */
    public abstract BaseCallbackCode defaultCallback();
}

提供实现类,仅供思路参考,用来针对系统业务异常

package com.ddf.boot.common.core.exception200;

/**
 * <p>通用业务异常,只提供预定义的消息状态码构造函数</p >
 *
 * @author dongfang.ding
 * @version 1.0
 * @date 2020/06/28 15:13
 */
public class BusinessException extends BaseException {

    /**
     * @param baseCallbackCode
     */
    public BusinessException(BaseCallbackCode baseCallbackCode) {
        super(baseCallbackCode);
    }


    public BusinessException(String description) {
        super(description);
    }

    public BusinessException(String code, String description) {
        super(code, description);
    }

    public BusinessException(String code, String description, Object... params) {
        super(code, description, params);
    }

    /**
     * 提供一种消息占位符的方式, baseCallbackCode中的message包含占位符, 使用的时候格式化参数后作为最终异常消息
     *
     * @param baseCallbackCode
     * @param params
     */
    public BusinessException(BaseCallbackCode baseCallbackCode, Object... params) {
        super(baseCallbackCode, params);
    }

    /**
     * 当前异常默认响应状态码
     *
     * @return
     */
    @Override
    public BaseCallbackCode defaultCallback() {
        return BaseErrorCallbackCode.SERVER_ERROR;
    }
}

定义扩展点

现在处理扩展点问题,简单有以下几个前面提到的问题

  1. 是否将错误堆栈信息返回到前端?简单点来使用配置类,默认返回,但是可以指定匹配的profile不返回
  2. 如何让外部接管异常处理?提供接口,如果有实现的话,则委托给自定义实现处理。但是提供一种策略, 如果实现方接管后返回null,仅仅代表只是监听,并不想接管异常体系,那处理完成后依然继续走系统定义的异常逻辑,否则直接将使用方返回值返回
  1. 属性类

    package com.ddf.boot.common.core.config;
    
    import java.util.List;
    import lombok.Getter;
    import lombok.Setter;
    import org.springframework.boot.context.properties.ConfigurationProperties;
    import org.springframework.stereotype.Component;
    
    /**
     * 存放一些全局的自定义属性,根据需要决定是否可配置
     *
     * @author dongfang.ding on 2019/1/25
     */
    @Component
    @ConfigurationProperties(prefix = "customs.global-properties")
    @Getter
    @Setter
    public class GlobalProperties {
    
        /**
         * 默认异常处理类会返回一个trace字段,将当前错误堆栈信息返回,方便调试时查看错误,提供该参数指定的
         * profile不会返回该字段,如生产环境
         */
        private List<String> ignoreErrorTraceProfile;
    
    }
    
    
  2. 异常接管接口

    package com.ddf.boot.common.core.exception200;
    
    import com.ddf.boot.common.core.response.ResponseData;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    
    /**
     * <p>将捕获的异常暴露出去,允许实现方实现接口根据这个异常自定义返回数据</p >
     *
     * @author dongfang.ding
     * @version 1.0
     * @date 2020/06/28 10:33
     */
    public interface ExceptionHandlerMapping {
    
        /**
         * 捕捉到的异常给实现方自定义实现返回数据
         *
         * @param exception
         * @return 如果当前异常不是自己要处理的类型,请返回{@code null}
         * @see AbstractExceptionHandler#handlerException(Exception, HttpServletRequest, HttpServletResponse)
         */
        ResponseData<?> handlerException(Exception exception);
    }
    

异常处理父类

这里定义为抽象类的原因,是无法解决应用的扫描规则, 因此只定义逻辑, 应用方自己继承, 然后直接调用父类方法即可。

package com.ddf.boot.common.core.exception200;

import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.net.NetUtil;
import com.ddf.boot.common.core.config.GlobalProperties;
import com.ddf.boot.common.core.helper.EnvironmentHelper;
import com.ddf.boot.common.core.helper.SpringContextHolder;
import com.ddf.boot.common.core.logaccess.AccessLogAspect;
import com.ddf.boot.common.core.response.ResponseData;
import com.ddf.boot.common.core.util.WebUtil;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.stream.Collectors;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource;
import org.springframework.http.HttpStatus;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

/**
 * <p>description</p >
 * <p>
 * <p>
 * FIXME 这种方式,无法决定系统使用方的basePackages, 所以这里暂时用了com
 * <p>
 * 也可以将该类定义为抽象普通类,不再交给spring管理, 然后应用使用方自己定义拦截规则, 继承这个类, 使用父类的逻辑,这里只提供逻辑
 * 这么做,至少也能保证如果在微服务项目中的话,可以统一管理多个模块的异常处理机制
 *
 * @author dongfang.ding
 * @version 1.0
 * @date 2020/06/28 10:20
 */
@Slf4j
public abstract class AbstractExceptionHandler {

    @Autowired
    private GlobalProperties globalProperties;
    @Autowired(required = false)
    private ExceptionHandlerMapping exceptionHandlerMapping;
    @Autowired
    private EnvironmentHelper environmentHelper;
    @Autowired(required = false)
    private MessageSource messageSource;

    /**
     * 处理异常类,某些异常类需要特殊处理,在具体根据当前异常去判断是否是期望的异常类型,
     * 这样可以只使用一个方法来处理,否则方法太多,看起来有点凌乱,也不太好做一些通用处理
     *
     * @param exception
     * @return
     */
    @ExceptionHandler(value = Exception.class)
    @ResponseBody
    public ResponseData<?> handlerException(Exception exception, HttpServletRequest httpServletRequest,
            HttpServletResponse response) {
        // 这个可选的日志处理器会在异常时打印异常日志, 如果已经处理了,这里就不要重复打印了
        if (!SpringContextHolder.containsBeanType(AccessLogAspect.class)) {
            log.error("异常信息: ", exception);
        }
        // 是否将当前错误堆栈信息返回,默认返回,但提供某些环境下隐藏信息
        boolean ignoreErrorStack = false;
        List<String> ignoreErrorTraceProfile = globalProperties.getIgnoreErrorTraceProfile();
        if (CollectionUtil.isNotEmpty(ignoreErrorTraceProfile) && environmentHelper.checkIsExistOr(
                ignoreErrorTraceProfile)) {
            ignoreErrorStack = true;
        }

        // 允许扩展实现类接管异常处理,可以在业务层面实现一些异常情况下的额外处理,但记得如果不接管异常处理,最后要返回null
        if (exceptionHandlerMapping != null) {
            ResponseData<?> responseData = exceptionHandlerMapping.handlerException(exception);
            if (responseData != null) {
                if (ignoreErrorStack) {
                    responseData.setStack(null);
                }
                return responseData;
            }
        }

        String exceptionCode;
        String message;
        if (exception instanceof BaseException) {
            BaseException baseException = (BaseException) exception;
            exceptionCode = baseException.getCode();
            // 解析异常类消息代码,并根据当前Local格式化资源文件
            Locale locale = httpServletRequest.getLocale();
            String description = baseException.getDescription();
            if (Objects.nonNull(baseException.getBaseCallbackCode())) {
                description = baseException.getBaseCallbackCode().getBizMessage();
            }
            // 没有定义资源文件的使用直接使用异常消息,定义了这里会根据异常状态码走i18n资源文件
            message = messageSource.getMessage(baseException.getCode(), baseException.getParams(), description, locale);
        } else if (exception instanceof IllegalArgumentException) {
            exceptionCode = BaseErrorCallbackCode.BAD_REQUEST.getCode();
            message = exception.getMessage();
        } else if (exception instanceof MethodArgumentNotValidException) {
            exceptionCode = BaseErrorCallbackCode.BAD_REQUEST.getCode();
            MethodArgumentNotValidException exception1 = (MethodArgumentNotValidException) exception;
            message = exception1.getBindingResult().getAllErrors().stream().map(ObjectError::getDefaultMessage).collect(
                    Collectors.joining(";"));
        } else {
            exceptionCode = BaseErrorCallbackCode.SERVER_ERROR.getCode();
            message = exception.getMessage();
        }

        if (globalProperties.isExceptionCodeToResponseStatus()) {
            String numberRegex = "\\d+";
            // 可能会出现超过int最大值的问题,暂时不管
            if (exceptionCode.matches(numberRegex)) {
                response.setStatus(Integer.parseInt(exceptionCode));
            } else {
                response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
            }
        }
        // 附加服务消息
        String extraServerMessage = String.format("[%s:%s]", environmentHelper.getApplicationName(),
                NetUtil.getLocalhostStr()
        );
        return ResponseData.failure(exceptionCode, message,
                ignoreErrorStack ? "" : extraServerMessage + ":" + ExceptionUtils.getStackTrace(exception)
        );
    }


    /**
     * 解析业务异常消息
     *
     * @param exception
     * @return
     */
    public static String resolveExceptionMessage(Exception exception) {
        try {
            if (exception instanceof BaseException) {
                BaseException baseException = (BaseException) exception;
                // 解析异常类消息代码,并根据当前Local格式化资源文件
                Locale locale = WebUtil.getCurRequest().getLocale();
                String description = baseException.getDescription();
                if (Objects.nonNull(baseException.getBaseCallbackCode())) {
                    description = baseException.getBaseCallbackCode().getBizMessage();
                }
                // 没有定义资源文件的使用直接使用异常消息,定义了这里会根据异常状态码走i18n资源文件
                return SpringContextHolder.getBean(MessageSource.class).getMessage(baseException.getCode(),
                        baseException.getParams(), description, locale);
            }
            return exception.getMessage();
        } catch (Exception e) {
            log.error("解析异常消息时失败, 原始异常消息 = {}", exception, e);
            return exception.getMessage();
        }
    }
}

应用接入使用

  1. 继承抽象处理类,定义拦截规则

    package com.ddf.boot.quick.common.exception;
    
    import com.ddf.boot.common.core.exception200.AbstractExceptionHandler;
    import com.ddf.boot.common.core.response.ResponseData;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import org.springframework.web.bind.annotation.ExceptionHandler;
    import org.springframework.web.bind.annotation.ResponseBody;
    import org.springframework.web.bind.annotation.RestControllerAdvice;
    
    /**
     * 通过集成通用包的异常逻辑来异常处理
     * <p>
     * 因为通用包无法确定包,所以提供抽象类,让使用方确定包范围,然后继承逻辑
     *
     * @author dongfang.ding
     * @date 2020/11/22 0022 22:10
     */
    @RestControllerAdvice(basePackages = "com.ddf.boot.quick")
    public class BootExceptionAdvice extends AbstractExceptionHandler {
    
        @ExceptionHandler(value = Exception.class)
        @ResponseBody
        @Override
        public ResponseData<?> handlerException(Exception exception, HttpServletRequest httpServletRequest,
                HttpServletResponse response) {
            return super.handlerException(exception, httpServletRequest, response);
        }
    }
    
  2. 实现异常编码接口,定义自己的异常编码

    package com.ddf.boot.quick.common.exception;
    
    import com.ddf.boot.common.core.exception200.BaseCallbackCode;
    import lombok.Getter;
    
    /**
     * <p>description</p >
     *
     * @author Snowball
     * @version 1.0
     * @date 2020/10/13 16:59
     */
    public enum BizCode implements BaseCallbackCode {
        /**
         * 异常
         */
        TEST_SIMPLE_BIZ_MESSAGE("10014", "基本异常"),
        TEST_BIZ_MESSAGE("10015", "boot-quick演示业务异常", "啦啦啦啦,请重试"),
        TEST_FILL_EXCEPTION("10016", "带占位符的异常演示[{0}]"),
        TEST_FILL_BIZ_EXCEPTION("10017", "带占位符的异常演示[{0}],客户端隐藏详细信息", "报错啦")
    
        ;
    
        BizCode(String code, String description) {
            this.code = code;
            this.description = description;
            this.bizMessage = description;
        }
    
        BizCode(String code, String description, String bizMessage) {
            this.code = code;
            this.description = description;
            this.bizMessage = bizMessage;
        }
    
        @Getter
        private final String code;
    
        @Getter
        private final String description;
    
        @Getter
        private final String bizMessage;
    }
    
  3. 使用

    以下演示返回字段的stack中是实际错误堆栈,可选不返回给前端的。

    1. 不走定义的异常错误码体系,直接用错误消息

      throw new BusinessException("异常演示");
      

      返回结果

      {
        "code": "500",
        "message": "异常演示",
        "stack": "[boot-quick:127.0.0.1]:com.ddf.boot.common.core.exception200.BusinessException: 异常演示\n\tat 省略其它.........",
        "timestamp": 1630587288896,
        "data": null,
        "extra": null,
        "success": false
      }
      
    2. 异常体系基本异常, 返回错误码和定义的消息

      throw new BusinessException(BizCode.TEST_SIMPLE_BIZ_MESSAGE);
      

      返回结果

      {
        "code": "10014",
        "message": "基本异常",
        "stack": "[boot-quick:127.0.0.1]:com.ddf.boot.common.core.exception200.BusinessException: 基本异常\n\tat com.ddf.boot.quick.controller.features.QuickStartController.simpleBizException1(QuickStartController.java:95)\n\tat 省略...............",
        "timestamp": 1630587899925,
        "data": null,
        "extra": null,
        "success": false
      }
      
      
    3. 异常体系隐藏系统异常,返回用户非敏感信息

      throw new BusinessException(BizCode.TEST_BIZ_MESSAGE);
      

      返回结果, 后台日志堆栈错误信息是boot-quick演示业务异常,但是返回给用户的却是啦啦啦啦,请重试

      {
        "code": "10015",
        "message": "啦啦啦啦,请重试",
        "stack": "[boot-quick:127.0.0.1]:com.ddf.boot.common.core.exception200.BusinessException: boot-quick演示业务异常\n\tat com.ddf.boot.quick.controller.features.QuickStartController.simpleBizException2(QuickStartController.java:105)\n\tat 省略...............",
        "timestamp": 1630659452294,
        "data": null,
        "extra": null,
        "success": false
      }
      
    4. 演示定义的异常消息中与占位符,抛出异常时自动填充

      throw new BusinessException(BizCode.TEST_FILL_EXCEPTION, System.currentTimeMillis());
      

      返回结果

      {
        "code": "10016",
        "message": "带占位符的异常演示[1,630,660,463,298]",
        "stack": "[boot-quick:127.0.0.1]:com.ddf.boot.common.core.exception200.BusinessException: 带占位符的异常演示[1,630,660,463,298]\n\tat com.ddf.boot.quick.controller.features.QuickStartController.fillBizException(QuickStartController.java:115)\n\tat 省略............",
        "timestamp": 1630660463594,
        "data": null,
        "extra": null,
        "success": false
      }
      
    5. 演示定义的异常消息中与占位符,抛出异常时自动填充, 但是返回给用户隐藏详细信息

      throw new BusinessException(BizCode.TEST_FILL_BIZ_EXCEPTION, System.currentTimeMillis());
      
      {
        "code": "10017",
        "message": "报错啦",
        "stack": "[boot-quick:127.0.0.1]:com.ddf.boot.common.core.exception200.BusinessException: 带占位符的异常演示[1,630,660,776,819],客户端隐藏详细信息\n\tat com.ddf.boot.quick.controller.features.QuickStartController.fillBizException1(QuickStartController.java:125)\n\tat 省略...........",
        "timestamp": 1630660777008,
        "data": null,
        "extra": null,
        "success": false
      }
      

    辅助工具类

    定义一个断言帮助类,可以在进行某些条件不满足时抛出异常

    package com.ddf.boot.common.core.util;
    
    import com.ddf.boot.common.core.exception200.BadRequestException;
    import com.ddf.boot.common.core.exception200.BaseCallbackCode;
    import com.ddf.boot.common.core.exception200.BaseErrorCallbackCode;
    import com.ddf.boot.common.core.exception200.BusinessException;
    import java.text.MessageFormat;
    import java.util.Iterator;
    import java.util.Objects;
    import java.util.Set;
    import javax.validation.ConstraintViolation;
    import javax.validation.Validation;
    import javax.validation.Validator;
    import org.springframework.lang.NonNull;
    
    /**
     * <p>提供断言,抛出系统自定义异常信息</p >
     *
     * @author dongfang.ding
     * @version 1.0
     * @date 2020/10/23 18:38
     */
    public class PreconditionUtil {
    
        /**
         * Validator instances can be pooled and shared by the implementation.
         * 这个东西不缓存下来,并发一上来,tomcat线程会刷刷的创建然后blocked,非常非常非常影响qps
         */
        private static final Validator VALIDATOR = Validation.buildDefaultValidatorFactory()
                .getValidator();
    
        /**
         * 检查参数
         *
         * @param expression
         * @param message
         */
        public static void checkArgument(boolean expression, String message) {
            if (!expression) {
                throw new BusinessException(message);
            }
        }
    
        /**
         * 检查参数
         *
         * @param expression
         * @param code
         * @param message
         */
        public static void checkArgument(boolean expression, String code, String message) {
            if (!expression) {
                throw new BusinessException(code, message);
            }
        }
    
        /**
         * 检查参数
         *
         * @param expression
         * @param callbackCode
         */
        public static void checkArgument(boolean expression, BaseCallbackCode callbackCode) {
            if (!expression) {
                throw new BusinessException(callbackCode);
            }
        }
    
        /**
         * 校验参数抛出外部传入运行时异常
         *
         * @param expression
         * @param exception
         */
        public static void checkArgument(boolean expression, RuntimeException exception) {
            if (!expression) {
                throw exception;
            }
        }
    
        /**
         * 检查参数并格式化占位符消息
         *
         * @param expression
         * @param callbackCode
         * @param args
         */
        static void checkArgumentAndFormat(boolean expression, @NonNull BaseCallbackCode callbackCode, Object... args) {
            checkArgument(expression, callbackCode.getCode(), MessageFormat.format(callbackCode.getDescription(), args));
        }
    
    
        /**
         * 检查参数
         *
         * @param expression
         * @param message
         */
        public static void checkBadRequest(boolean expression, String message) {
            if (!expression) {
                throw new BusinessException("400", message);
            }
        }
    
        /**
         * 提供一种手动式的必传参数校验
         *
         * @param request
         */
        public static <T> void requiredParamCheck(T request) {
            PreconditionUtil.checkArgument(
                    Objects.nonNull(request), BaseErrorCallbackCode.BAD_REQUEST
            );
            Set<ConstraintViolation<T>> constraintViolations = VALIDATOR.validate(request);
            if (constraintViolations.size() == 0) {
                return;
            }
            Iterator<ConstraintViolation<T>> iterator = constraintViolations.iterator();
            throw new BadRequestException(iterator.next().getMessage());
        }
    }
    
    

    使用

    PreconditionUtil.checkArgument(1 != 1, BizCode.TEST_BIZ_MESSAGE);
    // 等同于
    if (1 != 1) {
      throw new BusinessException(BizCode.TEST_BIZ_MESSAGE);
    }
    
    
    PreconditionUtil.checkArgument(1 != 1, BizCode.TEST_FILL_BIZ_EXCEPTION, System.currentTimeMillis());
    // 等同于
    if (1 != 1) {
      throw new BusinessException(BizCode.TEST_FILL_BIZ_EXCEPTION, System.currentTimeMillis());
    }
    
    
上一篇:aspx+mysql+iis站点搭建


下一篇:设计模式系列-适配器模式