# 统一返返回结果
由于项目体量巨大,一般项目由团队成员开发。如果不进行封装统一返回结果,每个人处理习惯不同,那么对于Api的编写都不尽相同,容易造成前后端开发混乱。
在SpringBoot项目中我们希望希望接口返回的数据包含至少三个属性:
- code:请求接口的返回码,成功或者异常等返回编码,例如定义请求成功。
- message:请求接口的描述,也就是对返回编码的描述。
- data:请求接口成功,返回的结果。
就像下面这个案例:
```json
{
"code":20000,
"message":"成功",
"data":{
"info":"测试成功"
}
}
```
对于Result进行泛型处理。
> 泛型的作用:
>
> **类型的参数化,就是可以把类型像方法的参数那样传递。这一点意义非凡。泛型使编译器可以在编译期间对类型进行检查以提高类型安全,减少运行时由于对象类型不匹配引发的异常。**
```java
package com.zhoujk.yygh.common.result;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
/**
* @author : zhoujiankang
* @Desc: 全局统一返回结果类
* @since : 2022/4/11 21:22
*/
@Data
@ApiModel(value = "全局统一返回结果")
public class Result<T> {
@ApiModelProperty(value = "返回码")
private Integer code;
@ApiModelProperty(value = "返回消息")
private String message;
@ApiModelProperty(value = "返回数据")
private T data;
public Result() {
}
protected static <T> Result<T> build(T data) {
Result<T> result = new Result<T>();
if (data != null) {
result.setData(data);
}
return result;
}
public static <T> Result<T> build(T body, ResultCodeEnum resultCodeEnum) {
Result<T> result = build(body);
result.setCode(resultCodeEnum.getCode());
result.setMessage(resultCodeEnum.getMessage());
return result;
}
public static <T> Result<T> build(Integer code, String message) {
Result<T> result = build(null);
result.setCode(code);
result.setMessage(message);
return result;
}
public static <T> Result<T> ok() {
return Result.ok(null);
}
/**
* 操作成功
*
* @param data
* @param <T>
* @return
*/
public static <T> Result<T> ok(T data) {
return build(data, ResultCodeEnum.SUCCESS);
}
public static <T> Result<T> fail() {
return Result.fail(null);
}
/**
* 操作失败
*
* @param data
* @param <T>
* @return
*/
public static <T> Result<T> fail(T data) {
return build(data, ResultCodeEnum.FAIL);
}
public Result<T> message(String msg) {
this.setMessage(msg);
return this;
}
public Result<T> code(Integer code) {
this.setCode(code);
return this;
}
public boolean isOk() {
return this.getCode().intValue() == ResultCodeEnum.SUCCESS.getCode().intValue();
}
}
```
在这里编写一个枚举类,枚举项目中所有返回结果的状态信息。
```java
package com.zhoujk.yygh.common.result;
import lombok.Getter;
/**
* @author : zhoujiankang
* @Desc: 统一返回结果状态信息类
* @since : 2022/4/11 21:26
*/
@Getter
public enum ResultCodeEnum {
/**
* 统一返回结果状态
*/
SUCCESS(200, "成功"),
FAIL(201, "失败"),
PARAM_ERROR(202, "参数不正确"),
SERVICE_ERROR(203, "服务异常"),
DATA_ERROR(204, "数据异常"),
DATA_UPDATE_ERROR(205, "数据版本异常"),
LOGIN_AUTH(208, "未登陆"),
PERMISSION(209, "没有权限"),
CODE_ERROR(210, "验证码错误"),
// LOGIN_MOBLE_ERROR(211, "账号不正确"),
LOGIN_DISABLED_ERROR(212, "该用户已被禁用"),
REGISTER_MOBILE_ERROR(213, "手机号已被使用"),
LOGIN_REQUIRED(214, "需要登录"),
LOGIN_ACL(215, "没有权限"),
URL_ENCODE_ERROR(216, "URL编码失败"),
ILLEGAL_CALLBACK_REQUEST_ERROR(217, "非法回调请求"),
FETCH_ACCESS_TOKEN_FAILED(218, "获取accessToken失败"),
FETCH_USERINFO_ERROR(219, "获取用户信息失败"),
//LOGIN_ERROR( 23005, "登录失败"),
PAY_RUN(220, "支付中"),
CANCEL_ORDER_FAIL(225, "取消订单失败"),
CANCEL_ORDER_NO(225, "不能取消预约"),
HOSPITAL_CODE_EXIST(230, "医院编号已经存在"),
NUMBER_NO(240, "可预约号不足"),
TIME_NO(250, "当前时间不可以预约"),
SIGN_ERROR(300, "签名错误"),
HOSPITAL_OPEN(310, "医院未开通,暂时不能访问"),
HOSPITAL_LOCK(320, "医院被锁定,暂时不能访问"),
;
private Integer code;
private String message;
private ResultCodeEnum(Integer code, String message) {
this.code = code;
this.message = message;
}
}
```
除此之外,同理还有对异常的统一处理。
# 异常处理和日志
## 全局异常处理
SpringBoot在默认情况下会映射到/error进行异常处理,但提示并不友好,下面自定义异常处理,提供友好提示。Spring 的异常处理,作为一个 Spring 为基础框架的 Web 程序,如果不对程序中出现的异常进行适当的处理比如异常信息友好化,记录异常日志等等,直接将异常信息返回给客户端展示给用户,对用户体验有不好的影响。所以本篇文章主要探讨通过 Spring 进行统一异常处理的几种方式实现,以更优雅的方式捕获程序发生的异常信息并进行适当的处理响应给客户端。。
## 自定义异常类
我们再搭建模块时在`common_util`模块需要添加`YyghException类`。如下:
构建2个构造函数,分别用于`通过状态码和错误消息创建异常对象`和`接收枚举类型对象`。由于是自定义异常,需要手动抛出。在后面的例子中,会演示如何手动抛出异常。
```java
package com.zhoujk.yygh.common.exception;
import com.zhoujk.yygh.common.result.ResultCodeEnum;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
/**
* @author : zhoujiankang
* @Desc: 自定义全局异常类
* @since : 2022/4/12 20:31
*/
@ApiModel(value = "自定义全局异常类")
@Data
public class YyghException extends RuntimeException
{
@ApiModelProperty(value = "异常状态码")
private Integer code;
/**
* 通过状态码和错误消息创建异常对象
*
* @param message
* @param code
*/
public YyghException(String message, Integer code)
{
super(message);
this.code = code;
}
/**
* 接收枚举类型对象
*
* @param resultCodeEnum
*/
public YyghException(ResultCodeEnum resultCodeEnum)
{
super(resultCodeEnum.getMessage());
this.code = resultCodeEnum.getCode();
}
@Override
public String toString()
{
return "YyghException{" + "code=" + code + ",message=" + this.getMessage() + '}';
}
}
```
同时在该模块下添加一个`GlobalExceptionHandler`类。
使用了`@ControllerAdvice`和`@ExceptionHandler`注解。其中`@ControllerAdvice`的作用是开启对全局异常的捕获,这个注解还可以通过`assignableTypes`参数指定特定的`Controller`类,让异常处理类只处理特定类抛出的异常。`@ExceptionHandler`注解,标明了该处理方法体处理的异常类型。
```
package com.zhoujk.yygh.common.exception;
import com.zhoujk.yygh.common.result.Result;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
/**
* @author : zhoujiankang
* @Desc: 全局异常处理
* @since : 2022/4/12 18:14
*/
@ControllerAdvice
public class GlobalExceptionHandler
{
@ExceptionHandler(Exception.class)
@ResponseBody
public Result error(Exception e)
{
e.printStackTrace();
return Result.fail();
}
@ExceptionHandler(YyghException.class)
@ResponseBody
public Result error(YyghException e)
{
e.printStackTrace();
return Result.fail();
}
}
```
## 日志
#### 配置日志级别
日志记录器(Logger)的行为是分等级的,如下所示:
分为:OFF、FATAL、ERROR、WARN、INFO、DEBUG、ALL
默认情况下,SpringBoot从控制台打印出来的日志级别只有INFO及以上级别,可以配置日志级别。
设置日志级别
```properties
logging.level.root=WARN
```
或
```yaml
logging:
level:
root: WARN
```
这种方式只能讲日志打印在控制台上。
#### Logback日志
SpringBoot内部使用Logback作为日志处理实现的框架。
Logback和log4j非常相似,如果你对log4j很熟悉,那么对Logback就容易很快上手。
##### 配置日志
在resources/logback-spring.xml
```xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="10 seconds">
<!-- 日志级别从低到高分为TRACE < DEBUG < INFO < WARN < ERROR < FATAL,如果设置为WARN,则低于WARN的信息都不会输出 -->
<!-- scan:当此属性设置为true时,配置文件如果发生改变,将会被重新加载,默认值为true -->
<!-- scanPeriod:设置监测配置文件是否有修改的时间间隔,如果没有给出时间单位,默认单位是毫秒。当scan为true时,此属性生效。默认的时间间隔为1分钟。 -->
<!-- debug:当此属性设置为true时,将打印出logback内部日志信息,实时查看logback运行状态。默认值为false。 -->
<contextName>logback</contextName>
<!-- name的值是变量的名称,value的值时变量定义的值。通过定义的值会被插入到logger上下文中。定义变量后,可以使“${}”来使用变量。 -->
<property name="log.path" value="D:/yygh_log/edu" />
<!-- 彩色日志 -->
<!-- 配置格式变量:CONSOLE_LOG_PATTERN 彩色日志格式 -->
<!-- magenta:洋红 -->
<!-- boldMagenta:粗红-->
<!-- cyan:青色 -->
<!-- white:白色 -->
<!-- magenta:洋红 -->
<property name="CONSOLE_LOG_PATTERN"
value="%yellow(%date{yyyy-MM-dd HH:mm:ss}) | %highlight(%-5level) | %blue(%thread) | %blue(%file:%line) | %green(%logger) | %cyan(%msg%n)"/>
<!--输出到控制台-->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<!--此日志appender是为开发使用,只配置最底级别,控制台输出的日志级别是大于或等于此级别的日志信息-->
<!-- 例如:如果此处配置了INFO级别,则后面其他位置即使配置了DEBUG级别的日志,也不会被输出 -->
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFO</level>
</filter>
<encoder>
<Pattern>${CONSOLE_LOG_PATTERN}</Pattern>
<!-- 设置字符集 -->
<charset>UTF-8</charset>
</encoder>
</appender>
<!--输出到文件-->
<!-- 时间滚动输出 level为 INFO 日志 -->
<appender name="INFO_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 正在记录的日志文件的路径及文件名 -->
<file>${log.path}/log_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}/info/log-info-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>100MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<!--日志文件保留天数-->
<maxHistory>15</maxHistory>
</rollingPolicy>
<!-- 此日志文件只记录info级别的 -->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>INFO</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<!-- 时间滚动输出 level为 WARN 日志 -->
<appender name="WARN_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 正在记录的日志文件的路径及文件名 -->
<file>${log.path}/log_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}/warn/log-warn-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>100MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<!--日志文件保留天数-->
<maxHistory>15</maxHistory>
</rollingPolicy>
<!-- 此日志文件只记录warn级别的 -->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>warn</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<!-- 时间滚动输出 level为 ERROR 日志 -->
<appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 正在记录的日志文件的路径及文件名 -->
<file>${log.path}/log_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}/error/log-error-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>100MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<!--日志文件保留天数-->
<maxHistory>15</maxHistory>
</rollingPolicy>
<!-- 此日志文件只记录ERROR级别的 -->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<!--
<logger>用来设置某一个包或者具体的某一个类的日志打印级别、以及指定<appender>。
<logger>仅有一个name属性,
一个可选的level和一个可选的addtivity属性。
name:用来指定受此logger约束的某一个包或者具体的某一个类。
level:用来设置打印级别,大小写无关:TRACE, DEBUG, INFO, WARN, ERROR, ALL 和 OFF,
如果未设置此属性,那么当前logger将会继承上级的级别。
-->
<!--
使用mybatis的时候,sql语句是debug下才会打印,而这里我们只配置了info,所以想要查看sql语句的话,有以下两种操作:
第一种把<root level="INFO">改成<root level="DEBUG">这样就会打印sql,不过这样日志那边会出现很多其他消息
第二种就是单独给mapper下目录配置DEBUG模式,代码如下,这样配置sql语句会打印,其他还是正常DEBUG级别:
-->
<!--开发环境:打印控制台-->
<springProfile name="dev">
<!--可以输出项目中的debug日志,包括mybatis的sql日志-->
<logger name="com.zsy" level="INFO" />
<!--
root节点是必选节点,用来指定最基础的日志输出级别,只有一个level属性
level:用来设置打印级别,大小写无关:TRACE, DEBUG, INFO, WARN, ERROR, ALL 和 OFF,默认是DEBUG
可以包含零个或多个appender元素。
-->
<root level="INFO">
<appender-ref ref="CONSOLE" />
<appender-ref ref="INFO_FILE" />
<appender-ref ref="WARN_FILE" />
<appender-ref ref="ERROR_FILE" />
</root>
</springProfile>
<!--生产环境:输出到文件-->
<springProfile name="pro">
<root level="INFO">
<appender-ref ref="CONSOLE" />
<appender-ref ref="DEBUG_FILE" />
<appender-ref ref="INFO_FILE" />
<appender-ref ref="ERROR_FILE" />
<appender-ref ref="WARN_FILE" />
</root>
</springProfile>
</configuration>
```
## 参考
[SpringBoot统一封装返回结果和异常情况](https://www.cnblogs.com/sw-code/p/13956522.html)
统一返返回结果、全局异常处理和日志