一、背景
在SpringBoot开发应用的时候,程序抛出的异常如果能进行统一的管理和处理,会极大方便我们排查问题,SpringBoot预留了controller响应的扩展点,我们只需要按照对应的方式去自定义自己的实现即可。
@ControllerAdvice表明GlobalExceptionAdvice是一个异常的处理类; @ExceptionHandler表明当前方法即为将要处理错误信息的逻辑代码,通过value来指定异常类型。异常的处理方法需要传入两个参数,一个是HttpServletRequest请求对象,可以从这里获取请求相关的一些信息,比如请求的url或者参数等,另一个是对应自定义的Exception,它就是抛出的异常
二、集成
1.编写异常枚举类
public interface IStatusCode {
public Integer getCode();
public String getMsg();
}
@Getter
public enum HttpCode implements IStatusCode {
CURRENT_NO_PERMISSION(4001,"当前用户权限不足"),
INTERNAL_ADDRESS_FORBIDDEN_ACCESS(4003,"内部访问地址,禁止访问"),
REQUEST_PARAM_ERROR(4000,"请求参数错误"),
REQUEST_URL_ERROR(4004,"请求URL地址错误"),
REQUEST_METHOD_ERROR(4005,"请求方法错误"),
SERVER_INTERNAL_EXEC_ERROR(5000,"服务器内部执行错误"),
DB_SERVICE_ERROR(5001,"数据库服务错误"),
INTERNAL_INTERFACE_CALL_ERROR(5002,"内部接口调用错误"),
EXTERNAL_INTERFACE_CALL_ERROR(5003,"外部接口调用错误"),
SUCCESS(2000, "请求处理成功");
private Integer code;
private String msg;
HttpCode(Integer code, String msg) {
this.code = code;
this.msg = msg;
}
}
2.编写响应类
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ResultDTO {
private Integer code;
private String msg;
private Object data;
public static ResultDTO getInstance(Integer code, String msg, Object object){
return ResultDTO.builder().code(code).msg(msg).data(object).build();
}
public static ResultDTO success(IStatusCode iStatusCode){
return ResultDTO.builder().code(iStatusCode.getCode()).msg(iStatusCode.getMsg()).build();
}
public static ResultDTO success(IStatusCode iStatusCode, Object object){
return ResultDTO.builder().code(iStatusCode.getCode()).msg(iStatusCode.getMsg()).data(object).build();
}
public static ResultDTO error(IStatusCode iStatusCode){
return ResultDTO.builder().code(iStatusCode.getCode()).msg(iStatusCode.getMsg()).build();
}
public static ResultDTO error(IStatusCode iStatusCode, Object object){
return ResultDTO.builder().code(iStatusCode.getCode()).msg(iStatusCode.getMsg()).data(object).build();
}
}
3.编写异常处理类
@Data
public class HttpException extends RuntimeException {
private Integer code;
private String msg;
// 手动设置异常
public HttpException(IStatusCode iStatusCode, String message){
// message用于用户设置抛出错误详情,例如:当前价格-5,小于0
super(message);
// 状态码
this.code = iStatusCode.getCode();
// 状态码配套的msg
this.msg = iStatusCode.getMsg();
}
// 默认异常使用IAppCode_ERROR状态码
public HttpException(String message) {
super(message);
this.code = HttpCode.SERVER_INTERNAL_EXEC_ERROR.getCode();
this.msg = HttpCode.SERVER_INTERNAL_EXEC_ERROR.getMsg();
}
}
@Slf4j
@RestControllerAdvice
public class GlobalExceptionAdvice {
/**
* @Description: 未自定义的异常,统一抛出:业务异常+手动指定异常信息
*/
@ExceptionHandler({Exception.class})
public ResultDTO exceptionHandler(Exception e){
log.error("", e);
return ResultDTO.error(HttpCode.SERVER_INTERNAL_EXEC_ERROR, e.getMessage());
}
/**
* @Description: 参数校验异常
*/
@ExceptionHandler({BindException.class})
public ResultDTO methodParamNotValidExceptionHandler(BindException e){
log.error("", e);
String defaultMessage = e.getAllErrors().get(0).getDefaultMessage();
return ResultDTO.error(HttpCode.REQUEST_PARAM_ERROR, defaultMessage);
}
/**
* @Description: 请求异常
*/
@ExceptionHandler({HttpException.class})
public ResultDTO httpExceptionHandler(HttpException e){
// 打印手动设置的异常信息
log.error("", e);
return ResultDTO.getInstance(e.getCode(), e.getMsg(), e.getMessage());
}
// /**
// * @Description: 业务异常
// */
// @ExceptionHandler({APIException.class})
// public ResultDTO apiExceptionHandler(APIException e){
// log.error("", e);
// return ResultDTO.getInstance(e.getCode(), e.getMsg(), e.getMessage());
// }
/**
* @Description: 获取异常堆栈信息: log.error(getExceptionDetail(e));
*/
public String getExceptionDetail(Exception e) {
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append(e.getClass() + System.getProperty("line.separator"));
stringBuilder.append(e.getLocalizedMessage() + System.getProperty("line.separator"));
StackTraceElement[] arr = e.getStackTrace();
for (int i = 0; i < arr.length; i++) {
stringBuilder.append(arr[i].toString() + System.getProperty("line.separator"));
}
return stringBuilder.toString();
}
}
4.编写测试类测试
@Slf4j
@RestController
@RequestMapping("/exception")
public class GlobalExceptionController {
@PostMapping("/validParam")
public ResultDTO findByVo1(@Validated IProductInfoVo vo){
ProductInfo productInfo = ProductInfo.builder().build();
BeanUtils.copyProperties(vo, productInfo);
return ResultDTO.success(HttpCode.SUCCESS, productInfo);
}
@PostMapping("/httpException")
public ResultDTO findByVo2(@Validated IProductInfoVo vo){
ProductInfo productInfo = ProductInfo.builder().build();
productInfo = Objects.nonNull(productInfo) ? null : productInfo;
Objects.requireNonNull(productInfo, "当前检索条件对应的产品信息不存在");
return ResultDTO.success(HttpCode.SUCCESS, productInfo);
}
@PostMapping("/httpException2")
public ResultDTO findByVo3(@Validated IProductInfoVo vo){
ProductInfo productInfo = ProductInfo.builder().build();
BeanUtils.copyProperties(vo, productInfo);
// if (!Objects.isNull(productInfo)){
throw new HttpException("当前请求信息不存在,请审核数据库数据"+productInfo.getProductName());
// throw new HttpException(HttpCode.INTERNAL_INTERFACE_CALL_ERROR, "当前请求信息不存在,请审核数据库数据");
// }
return ResultDTO.success(HttpCode.SUCCESS, productInfo);
}
}
5.resources目录下配置logback.xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- 控制台 appender, 几乎是默认的配置 -->
<appender name="stdout" class="ch.qos.logback.core.ConsoleAppender">
<encoder charset="UTF-8">
<!-- 输出的日志文本格式, 其他的 appender 与之相同 -->
<pattern> %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} %L - %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- info 级别的 appender -->
<appender name="info" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 日志写入的文件名, 可以是相对目录, 也可以是绝对目录, 如果上级目录不存在会自动创建 -->
<file>./logs/info/log-stack.log</file>
<!-- 如果是 true, 日志被追加到文件结尾; 如果是 false, 清空现存文件. 默认是true -->
<append>true</append>
<!-- 日志级别过滤器, 只打 INFO 级别的日志-->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>INFO</level>
<!-- 下面2个属性表示: 匹配 level 的接受打印, 不匹配的拒绝打印 -->
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<!-- 最常用的滚动策略, 它根据时间来制定滚动策略, 既负责滚动也负责触发滚动 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 设置滚动文件规则, 如果直接使用 %d, 默认格式是 yyyy-MM-dd -->
<fileNamePattern>./logs/info/log-stack.%d{yyyy-MM-dd}.log</fileNamePattern>
<!-- 保留14天的日志 -->
<maxHistory>30</maxHistory>
</rollingPolicy>
<!-- 定义日志输出格式 -->
<encoder charset="UTF-8">
<pattern> %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} %L - %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- error 级别的 appender -->
<appender name="error" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>./logs/error/log-stack.log</file>
<append>true</append>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>./logs/error/log-stack.%d{yyyy-MM-dd}.log</fileNamePattern>
<!-- 保留7天的日志 -->
<maxHistory>30</maxHistory>
</rollingPolicy>
<!-- 定义日志输出格式 -->
<encoder charset="UTF-8">
<pattern> %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} %L - %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- error 级别的 appender -->
<appender name="debug" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>./logs/debug/log-stack.log</file>
<append>true</append>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>DEBUG</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>./logs/debug/log-stack.%d{yyyy-MM-dd}.log</fileNamePattern>
<!-- 保留7天的日志 -->
<maxHistory>30</maxHistory>
</rollingPolicy>
<!-- 定义日志输出格式 -->
<encoder charset="UTF-8">
<pattern> %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} %L - %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- 指定 com.github 下的日志打印级别, appender -->
<logger name="com.analysis.tool.platform.server" level="debug" additivity="false">
<appender-ref ref="stdout"/>
<appender-ref ref="info"/>
<appender-ref ref="error"/>
<appender-ref ref="debug"/>
</logger>
<root level="info">
<appender-ref ref="stdout"/>
<appender-ref ref="info"/>
<appender-ref ref="error"/>
</root>
</configuration>