@ControllerAdvice
和@RestControllerAdvice
的使用
作用
其实两个注解是同一个,只不过一个是针对与@RestController
的另一个是针对@Controller
的, 前后端分离我们都会使用@RestController
, 因此后面直接针对这个来说,两个的区别是@RestController
相当于在所有@Controller
里的@RequestMapping
方法上添加了注解@RequestBody
可以简单理解为就是特意为@RestController
设计的拦截器, 通过basePackages
属性指定扫描的包, 则代表逻辑会织入到相关扫描到的RestController
, 并且根据实际使用时机,来确定行为。
常用的方式有两种
- 在实现
org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice
接口的类上添加注解@RestControllerAdvice
指定扫描包,则在扫描到的RestController
执行我们写的RequestMapping
方法后进入到这个逻辑,这样我们可以很方便包装全局返回对象类 - 配合注解
@ExceptionHandler
使用,定义一个类将注解@RestControllerAdvice
添加到类上,然后定义本地方法,将注解@ExceptionHandler
, 通过@ExceptionHandler
value属性指定异常类, 则被扫描到的
RestController方法出现异常,就会被@ExceptionHandler
标识的方法拦截,这个机制可以让我们用来做全局异常处理。
全局对象返回类包装
步骤
- 定义全局响应对象类
- 考虑某些方法可能不需要拦截,要允许使用方跳过
- 定义拦截处理逻辑
-
定义响应对象类
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.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 { }
-
定义处理逻辑
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); } }
-
特殊处理
有一种情况是方法返回值就是String, 这个时候包装可能会出现问题, 需要添加一个格式转换器,如
MappingJackson2HttpMessageConverter
并且将顺序放到最前面@Configuration public class CoreWebConfig implements WebMvcConfigurer { @Override public void configureMessageConverters(List<HttpMessageConverter<?>> converters) { converters.add(0, new MappingJackson2HttpMessageConverter()); } }
-
使用
-
遵循上面定义的扫描包的规则创建
Controller
类,添加注解@RestController
-
在方法上定义上面拦截规则中出现的注解, 如
GetMapping
-
方法返回值不要返回上面定义的包装对象, 直接返回数据原始类型即可, 在最终返回前端时会经过上面写的拦截逻辑处理一层然后包装进去,完成统一返回对象的作用
-
当然如果是
SpringCloud
,基于Feign
这种调用模式的系统,就不要使用这种包装方式了,否则会包装两层。但是定义的统一对象还是可以使用的,里面也提供了一些静态方法可以使用
-
全局异常处理
在做之前先想一下,需要考虑和解决哪些问题?
- 要能抛出自己的系统,能自定义状态码和异常消息
- 为了方便管理状态码和消息,最好能定义类来进行管理。考虑到后续状态码越来越多问题,要使用接口来定义规范,实现类实现接口来保证异常码的维护,实现类可以选择枚举,每个系统或者模块单独定义
- 考虑到消息中可能需要可变量,因为异常要支持占位符
- 异常信息中可以包含详细的异常堆栈消息以及有些异常消息,仅仅是异常消息,比如代码中的check报错要把具体不满足的条件写出来,但是并不方便返回给用户,还需要返回额外的业务消息来隐藏系统信息。注意这个和堆栈是两个问题,不是指的同一个事情。
- 异常处理类定义为抽象类,因为无法确定应用使用方要扫描的包对象。还有我们已经定义了异常处理类,万一使用方想在异常的时候额外处理一些逻辑或者接管逻辑怎么办?所以处理异常也要预留扩展接口,允许使用方介入到异常处理逻辑中。
- 异常返回给前端也需要使用统一对象包装,使用自定义对象中的状态码来标识异常。那么还有没有其它信息返回?需要考虑将异常堆栈返回,这样方便测试和联调直接看到第一现场异常堆栈信息,但是又不能所有环境都返回,要在生产环境屏蔽。
开始编码
根据上述需求定义响应类,还是上面那个统一包装对象
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;
}
}
定义扩展点
现在处理扩展点问题,简单有以下几个前面提到的问题
- 是否将错误堆栈信息返回到前端?简单点来使用配置类,默认返回,但是可以指定匹配的
profile
不返回- 如何让外部接管异常处理?提供接口,如果有实现的话,则委托给自定义实现处理。但是提供一种策略, 如果实现方接管后返回null,仅仅代表只是监听,并不想接管异常体系,那处理完成后依然继续走系统定义的异常逻辑,否则直接将使用方返回值返回
-
属性类
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; }
-
异常接管接口
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();
}
}
}
应用接入使用
-
继承抽象处理类,定义拦截规则
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); } }
-
实现异常编码接口,定义自己的异常编码
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; }
-
使用
以下演示返回字段的stack中是实际错误堆栈,可选不返回给前端的。
-
不走定义的异常错误码体系,直接用错误消息
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 }
-
异常体系基本异常, 返回错误码和定义的消息
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 }
-
异常体系隐藏系统异常,返回用户非敏感信息
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 }
-
演示定义的异常消息中与占位符,抛出异常时自动填充
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 }
-
演示定义的异常消息中与占位符,抛出异常时自动填充, 但是返回给用户隐藏详细信息
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()); }
-