统一结果返回
目前的java 项目大部分都采用前后端分离,而且大部分数据的传输格式都是json,因此定义一个统一规范的数据格式有利于前后端的交互与UI展示。

统一结果的一般形式
- 是否响应成功;
- 响应状态码;
- 状态码描述;
- 响应数据;
- 其他标识符;
结果类枚举
- 常用的结果以枚举定义,适用于前三者,如:success,code,message
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| 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; } }
|
结果统一类
注意:
- 所有的控制器返回只可以调用结果同一类的方法返回结果,不可以直接创建,因此构造器需私有,
- 内置静态方法,返回结果统一对象;
- 为了便于自定义结果统一信息,我们可以采用链式编程,将返回对象设类本身,即return this;
- 相应数据一般为json 格式,可定义为JSONOBject 或者jsonObject对象;
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
| 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 UserController {
@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 25
| { "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
Springboot 提供@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
| 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 39
| 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()); } }
|
接口异常返回示例
以下为展示当遇到null指定异常时,返回的结果信息
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 192
| <?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插件可实现控制台的自定义颜色输出