转载于:https://juejin.cn/post/6844904033488994317
统一结果返回
目前的前后端开发大部分数据的传输格式都是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
| @Getter public enum ResultCodeEnum { SUCCESS(true,20000,"成功"), UNKNOWN_ERROR(false,20001,"未知错误"),, PARAM_ERROR(false,20002,"参数错误"), ;
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
| @Data public class R { private Boolean success;
private Integer code;
private String message;
private Map<String, Object> data = new HashMap<>();
private R(){}
public static R ok() { R r = new R(); r.setSuccess(ResultCodeEnum.SUCCESS.getSuccess()); r.setCode(ResultCodeEnum.SUCCESS.getCode()); r.setMessage(ResultCodeEnum.SUCCESS.getMessage()); return r; }
public static R error() { R r = new R(); r.setSuccess(ResultCodeEnum.UNKNOWN_ERROR.getSuccess()); r.setCode(ResultCodeEnum.UNKNOWN_ERROR.getCode()); r.setMessage(ResultCodeEnum.UNKNOWN_ERROR.getMessage()); return r; }
public static R setResult(ResultCodeEnum result) { R r = new R(); r.setSuccess(result.getSuccess()); r.setCode(result.getCode()); r.setMessage(result.getMessage()); return r; }
public R data(Map<String,Object> map) { this.setData(map); return this; }
public R data(String key,Object value) { this.data.put(key, value); return this; }
public R message(String message) { this.setMessage(message); return this; }
public R code(Integer code) { this.setCode(code); return this; }
public R 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 R list() { List<Teacher> list = teacherService.list(null); return R.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": 20000, "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的R对象返回。
因此,我们需要定义一个统一的全局异常来捕获这些信息,并作为一种结果返回控制层
@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
| @Data public class CMSException extends RuntimeException { private Integer code;
public CMSException(Integer code, String message) { super(message); this.code = code; }
public CMSException(ResultCodeEnum resultCodeEnum) { super(resultCodeEnum.getMessage()); this.code = resultCodeEnum.getCode(); }
@Override public String toString() { return "CMSException{" + "code=" + code + ", message=" + this.getMessage() + '}'; } }
|
统一异常处理器
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
| import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseBody;
@ControllerAdvice public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class) @ResponseBody public R error(Exception e) { e.printStackTrace(); return R.error(); }
@ExceptionHandler(NullPointerException.class) @ResponseBody public R error(NullPointerException e) { e.printStackTrace(); return R.setResult(ResultCodeEnum.NULL_POINT); }
@ExceptionHandler(HttpClientErrorException.class) @ResponseBody public R error(IndexOutOfBoundsException e) { e.printStackTrace(); return R.setResult(ResultCodeEnum.HTTP_CLIENT_ERROR); }
@ExceptionHandler(CMSException.class) @ResponseBody public R error(CMSException e) { e.printStackTrace(); return R.error().message(e.getMessage()).code(e.getCode()); } }
|
控制层展示
1 2 3 4 5 6
| { "success": false, "code": 20007, "message": "空指针异常", "data": {} }
|
统一日志收集
日志是追踪错误定位问题的关键,尤其在生产环境中,需要及时修复热部署,不会提供开发者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 name="log.path" value="D:/Documents/logs/edu" />
<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 name="CONSOLE_LOG_PATTERN" value="${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 name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender"> <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 name="DEBUG_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <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 name="INFO_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <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 name="WARN_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <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 name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <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 name="dev"> <logger name="com.cms" level="info"/> <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 name="pro"> <logger name="com.cms" level="warn"/> <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
| @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
| import lombok.extern.slf4j.Slf4j;
@ControllerAdvice @Slf4j public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class) @ResponseBody public R error(Exception e) { log.error(ExceptionUtil.getMessage(e)); return R.error(); }
}
|
注意
- 日志的环境即spring.profiles.acticve,跟随项目启动;
- 启动后,即可到自定目录查找到生成的日志文件;
- 本地idea调试时,推荐Grep Console插件可实现控制台的自定义颜色输出
GitHub 源码
https://github.com/chetwhy/cloud-flow