原文地址 https://juejin.im/post/5e073980f265da33f8653f2e
参考链接: SpringBoot 之全局异常处理_
推荐博客:glmapper 的 logback 博客,logback-spring.xml 配置文件
代码地址:github
项目构建基础 - 统一结果,统一异常,统一日志
统一结果返回
目前的前后端开发大部分数据的传输格式都是 json,因此定义一个统一规范的数据格式有利于前后端的交互与 UI 的展示。
统一结果的一般形式
- 是否响应成功;
- 响应状态码;
- 状态码描述;
- 响应数据
- 其他标识符
结果类枚举
- 前三者可定义结果枚举,如:success,code,message
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| import lombok.Getter; @Getter public enum ResultCodeEnum { SUCCESS(true,5000,"成功"), FAIL(false,5001,"失败"), UNKNOWN_ERROR(false,5002,"未知错误"), PARAM_ERROR(false,5003,"参数错误") , NULL_POINTER(false,5004,"空指针异常") ;
private Boolean success; private Integer code; private String message;
ResultCodeEnum(boolean success, Integer code, String message) { this.success = success; this.code = code; this.message = message; } }
|
统一结果类
- 第 5 个属于自定义返回,利用前 4 者可定义统一返回对象
注意:
- 外接只可以调用统一返回类的方法,不可以直接创建,影刺构造器私有;
- 内置静态方法,返回对象;
- 为便于自定义统一结果的信息,建议使用链式编程,将返回对象设类本身,即 return this;
- 响应数据由于为 json 格式,可定义为 JsonObject 或 Map 形式;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107
| import lombok.Data; import java.util.HashMap; import java.util.Map;
@Data public class ResultResponse { private Boolean success;
private Integer code;
private String message;
private Map<String, Object> data = new HashMap<>();
private ResultResponse(){}
public static ResultResponse ok() { ResultResponse resultResponse = new ResultResponse(); resultResponse.setSuccess(ResultCodeEnum.SUCCESS.getSuccess()); resultResponse.setCode(ResultCodeEnum.SUCCESS.getCode()); resultResponse.setMessage(ResultCodeEnum.SUCCESS.getMessage()); return resultResponse; }
public static ResultResponse error() { ResultResponse resultResponse = new ResultResponse(); resultResponse.setSuccess(ResultCodeEnum.UNKNOWN_ERROR.getSuccess()); resultResponse.setCode(ResultCodeEnum.UNKNOWN_ERROR.getCode()); resultResponse.setMessage(ResultCodeEnum.UNKNOWN_ERROR.getMessage()); return resultResponse; }
public static ResultResponse setResult(ResultCodeEnum result) { ResultResponse resultResponse = new ResultResponse(); resultResponse.setSuccess(result.getSuccess()); resultResponse.setCode(result.getCode()); resultResponse.setMessage(result.getMessage()); return resultResponse; }
public ResultResponse data(Map<String,Object> map) { this.setData(map); return this; }
public ResultResponse data(String key, Object value) { this.data.put(key, value); return this; }
public ResultResponse message(String message) { this.setMessage(message); return this; }
public ResultResponse code(Integer code) { this.setCode(code); return this; }
public ResultResponse success(Boolean success) { this.setSuccess(success); return this; } }
|
控制层返回
1 2 3 4 5 6 7 8 9 10 11 12 13
| @RestController @RequestMapping("/api/v1/users") public class TeacherAdminController {
@Autowired private UserService userService;
@GetMapping public ResultResponse list() { List<Teacher> list = teacherService.list(null); return ResultResponse.ok().data("itms", list).message("用户列表"); } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| { "success": true, "code": 5000, "message": "查询用户列表", "data": { "itms": [ { "id": "1", "username": "admin", "role": "ADMIN", "deleted": false, "gmtCreate": "2019-12-26T15:32:29", "gmtModified": "2019-12-26T15:41:40" },{ "id": "2", "username": "zhangsan", "role": "USER", "deleted": false, "gmtCreate": "2019-12-26T15:32:29", "gmtModified": "2019-12-26T15:41:40" } ] } }
|
统一结果类的使用参考了 mybatis-plus 中 R 对象的设计
统一异常处理
使用统一返回结果时,还有一种情况,就是程序的保存是由于运行时异常导致的结果,有些异常我们可以无法提前预知,不能正常走到我们 return 的 ResultResponse 对象返回。
因此,我们需要定义一个统一的全局异常来捕获这些信息,并作为一种结果返回控制层
@ControllerAdvice
该注解为统一异常处理的核心
是一种作用于控制层的切面通知(Advice),该注解能够将通用的 @ExceptionHandler、@InitBinder 和 @ModelAttributes 方法收集到一个类型,并应用到所有控制器上
该类中的设计思路:
- 使用 @ExceptionHandler 注解捕获指定或自定义的异常;
- 使用 @ControllerAdvice 集成 @ExceptionHandler 的方法到一个类中;
- 必须定义一个通用的异常捕获方法,便于捕获未定义的异常信息;
- 自定一个异常类,捕获针对项目或业务的异常;
- 异常的对象信息补充到统一结果枚举中;
自定义全局异常类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| import com._4paradigm.data.analysis.common.response.ResultCodeEnum; import org.springframework.beans.factory.annotation.Autowired;
public class CustomException extends RuntimeException{
@Autowired ResultCodeEnum resultCode;
public CustomException(ResultCodeEnum resultCode){ this.resultCode = resultCode; } public ResultCodeEnum getResultCode(){ return resultCode; }
public static void cast(ResultCodeEnum resultCode){ throw new CustomException(resultCode); } }
|
统一异常处理器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70
| import com._4paradigm.data.analysis.common.response.ResultCodeEnum; import com._4paradigm.data.analysis.common.response.ResultResponse; import com.google.common.collect.ImmutableMap; import lombok.extern.slf4j.Slf4j; import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseBody;
@Slf4j @ControllerAdvice public class GlobalExceptionHandler {
private static ImmutableMap<Class<? extends Throwable>,ResultCodeEnum> EXCEPTIONS;
protected static ImmutableMap.Builder<Class<? extends Throwable>,ResultCodeEnum> builder = ImmutableMap.builder();
@ExceptionHandler(CustomException.class) @ResponseBody public ResultResponse customException(CustomException customException){ ResultCodeEnum resultCode = customException.getResultCode(); return ResultResponse.setResult(resultCode); }
@ExceptionHandler(NullPointerException.class) @ResponseBody public ResultResponse error(NullPointerException e) { log.error(ExceptionUtil.getMessage(e)); return ResultResponse.setResult(ResultCodeEnum.NULL_POINTER); }
@ExceptionHandler(Exception.class) @ResponseBody public ResultResponse exception(Exception exception){ log.error(ExceptionUtil.getMessage(exception)); if (EXCEPTIONS == null){ EXCEPTIONS = builder.build(); } ResultCodeEnum resultCode = EXCEPTIONS.get(exception.getClass()); if (resultCode != null){ return ResultResponse.setResult(resultCode); }else { return ResultResponse.setResult(ResultCodeEnum.UNKNOWN_ERROR); } }
static { builder.put(HttpMessageNotReadableException.class, ResultCodeEnum.PARAM_ERROR); } }
|
控制层展示
以下为展示当遇到 null 指定异常时,返回的结果信息
1 2 3 4 5 6
| { "success": false, "code": 5004, "message": "空指针异常", "data": {} }
|
本节介绍统一异常较为简略,推荐博客 SpringBoot 之全局异常处理
统一日志收集
日志是追踪错误定位问题的关键,尤其在生产环境中,需要及时修复热部署,不会提供开发者 debug 的环境,此时日志将会是最快解决问题的关键
日志的框架比较丰富,由于 spring boot 对 logback 的集成,因此推荐使用 logback 在项目中使用。
Logback
关于 logback 的配置和介绍,可以参考官网或推荐博客 glmapper 的 logback 博客,logback-spring.xml 配置文件
配置
以下直接贴出配置信息,介绍信息科直接参考备注
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191
| <?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="10 seconds"> <contextName>logback</contextName>
<property />
<conversionRule conversionWord="clr" converterClass="org.springframework.boot.logging.logback.ColorConverter" /> <conversionRule conversionWord="wex" converterClass="org.springframework.boot.logging.logback.WhitespaceThrowableProxyConverter" /> <conversionRule conversionWord="wEx" converterClass="org.springframework.boot.logging.logback.ExtendedWhitespaceThrowableProxyConverter" /> <property ${CONSOLE_LOG_PATTERN:-%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"/>
<appender > <filter class="ch.qos.logback.classic.filter.ThresholdFilter"> <level>debug</level> </filter> <encoder> <Pattern>${CONSOLE_LOG_PATTERN}</Pattern> <charset>UTF-8</charset> </encoder> </appender>
<appender > <file>${log.path}/edu_debug.log</file> <encoder> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern> <charset>UTF-8</charset> </encoder> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <fileNamePattern>${log.path}/web-debug-%d{yyyy-MM-dd}.%i.log</fileNamePattern> <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP"> <maxFileSize>100MB</maxFileSize> </timeBasedFileNamingAndTriggeringPolicy> <maxHistory>15</maxHistory> </rollingPolicy> <filter class="ch.qos.logback.classic.filter.LevelFilter"> <level>debug</level> <onMatch>ACCEPT</onMatch> <onMismatch>DENY</onMismatch> </filter> </appender>
<appender > <file>${log.path}/edu_info.log</file> <encoder> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern> <charset>UTF-8</charset> </encoder> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <fileNamePattern>${log.path}/web-info-%d{yyyy-MM-dd}.%i.log</fileNamePattern> <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP"> <maxFileSize>100MB</maxFileSize> </timeBasedFileNamingAndTriggeringPolicy> <maxHistory>15</maxHistory> </rollingPolicy> <filter class="ch.qos.logback.classic.filter.LevelFilter"> <level>info</level> <onMatch>ACCEPT</onMatch> <onMismatch>DENY</onMismatch> </filter> </appender>
<appender > <file>${log.path}/edu_warn.log</file> <encoder> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern> <charset>UTF-8</charset> </encoder> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <fileNamePattern>${log.path}/web-warn-%d{yyyy-MM-dd}.%i.log</fileNamePattern> <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP"> <maxFileSize>100MB</maxFileSize> </timeBasedFileNamingAndTriggeringPolicy> <maxHistory>15</maxHistory> </rollingPolicy> <filter class="ch.qos.logback.classic.filter.LevelFilter"> <level>warn</level> <onMatch>ACCEPT</onMatch> <onMismatch>DENY</onMismatch> </filter> </appender>
<appender > <file>${log.path}/edu_error.log</file> <encoder> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern> <charset>UTF-8</charset> </encoder> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <fileNamePattern>${log.path}/web-error-%d{yyyy-MM-dd}.%i.log</fileNamePattern> <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP"> <maxFileSize>100MB</maxFileSize> </timeBasedFileNamingAndTriggeringPolicy> <maxHistory>15</maxHistory> </rollingPolicy> <filter class="ch.qos.logback.classic.filter.LevelFilter"> <level>ERROR</level> <onMatch>ACCEPT</onMatch> <onMismatch>DENY</onMismatch> </filter> </appender>
<springProfile > <logger /> <root level="info"> <appender-ref ref="CONSOLE" /> <appender-ref ref="DEBUG_FILE" /> <appender-ref ref="INFO_FILE" /> <appender-ref ref="WARN_FILE" /> <appender-ref ref="ERROR_FILE" /> </root> </springProfile>
<springProfile > <logger /> <root level="info"> <appender-ref ref="ERROR_FILE" /> <appender-ref ref="WARN_FILE" /> </root> </springProfile>
</configuration>
|
日志收集异常信息
日志信息往往伴随着异常信息的输出,因此,我们需要修改统一异常的处理器,将异常信息以流的方式写到日志文件中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| import lombok.extern.slf4j.Slf4j; import java.io.IOException; import java.io.PrintWriter; import java.io.StringWriter;
@Slf4j public class ExceptionUtil {
public static String getMessage(Exception e) { String swStr = null; try (StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw)) { e.printStackTrace(pw); pw.flush(); sw.flush(); swStr = sw.toString(); } catch (IOException ex) { ex.printStackTrace(); log.error(ex.getMessage()); } return swStr; } }
|
- 修改统一异常处理器,将异常方法中的直接打印改为日志输入并打印
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70
| import com._4paradigm.data.analysis.common.response.ResultCodeEnum; import com._4paradigm.data.analysis.common.response.ResultResponse; import com.google.common.collect.ImmutableMap; import lombok.extern.slf4j.Slf4j; import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseBody;
@Slf4j @ControllerAdvice public class GlobalExceptionHandler {
private static ImmutableMap<Class<? extends Throwable>,ResultCodeEnum> EXCEPTIONS;
protected static ImmutableMap.Builder<Class<? extends Throwable>,ResultCodeEnum> builder = ImmutableMap.builder();
@ExceptionHandler(CustomException.class) @ResponseBody public ResultResponse customException(CustomException customException){ ResultCodeEnum resultCode = customException.getResultCode(); return ResultResponse.setResult(resultCode); }
@ExceptionHandler(NullPointerException.class) @ResponseBody public ResultResponse error(NullPointerException e) { log.error(ExceptionUtil.getMessage(e)); return ResultResponse.setResult(ResultCodeEnum.NULL_POINTER); }
@ExceptionHandler(Exception.class) @ResponseBody public ResultResponse exception(Exception exception){ log.error(ExceptionUtil.getMessage(exception)); if (EXCEPTIONS == null){ EXCEPTIONS = builder.build(); } ResultCodeEnum resultCode = EXCEPTIONS.get(exception.getClass()); if (resultCode != null){ return ResultResponse.setResult(resultCode); }else { return ResultResponse.setResult(ResultCodeEnum.UNKNOWN_ERROR); } }
static { builder.put(HttpMessageNotReadableException.class, ResultCodeEnum.PARAM_ERROR); } }
|
注意
- 日志的环境即 spring.profiles.acticve,跟随项目启动;
- 启动后,即可到自定目录查找到生成的日志文件;
- 本地 idea 调试时,推荐 Grep Console 插件可实现控制台的自定义颜色输出
详细过程,可参考源代码:github.com/chetwhy/clo…