Spring Boot 优雅实现统一数据返回格式+统一异常处理+统一日志处理

发布时间:2024年01月18日

????????在我们的项目开发中,我们都会对数据返回格式进行统一的处理,这样可以方便前端人员取数据,当然除了正常流程的数据返回格式需要统一以外,我们也需要对异常的情况进行统一的处理,以及项目必备的日志。

1. 统一返回格式

????????在项目开发中返回的是json格式的数据,也就是统一json数据返回格式,一般情况下返回数据的基本格式包含是否成功、响应状态码、返回的消息、以及返回的数据。格式如下:

{
  "success": 布尔,      // 是否成功
  "code": 数字,         // 响应状态码
  "message": 字符串,    // 返回的消息
  "data": {}           //  放置响应的数据
}

1.1?添加枚举类

????????该类定义了以上统一格式的前三部分:是否成功、响应状态码、返回的消息;可自行根据项目需要进行后续的添加或者删改。

创建一个result包,下面放置ResultCodeEnum枚举类

/**
 * 状态码
 *
 */
public enum ResultCodeEnum {
 
    SUCCESS(true, 20000, "成功"),
 
    UNKNOWN_REASON(false, 20001, "未知错误");
 
    private final Boolean success;
 
    private final Integer code;
 
    private final String message;
 
    ResultCodeEnum(Boolean success, Integer code, String message) {
        this.success = success;
        this.code = code;
        this.message = message;
    }
 
    public Boolean getSuccess() {
        return success;
    }
 
    public Integer getCode() {
        return code;
    }
 
    public String getMessage() {
        return message;
    }
 
    @Override
    public String toString() {
        return "ResultCodeEnum{" + "success=" + success + ", code=" + code + ", message='" + message + '\'' + '}';
    }
}

1.2 添加统一返回格式的类

该类是用来和前端交互的类,定义的就是本文开头所说的格式

在result包下创建一个统一返回格式的类R

/**
 * 统一返回格式类
 *
 */
public class R {
 
    /**
     * 是否成功
     */
    private Boolean success;
 
    /**
     * 状态码
     */
    private Integer code;
 
    /**
     * 返回的消息
     */
    private String message;
 
    /**
     * 放置响应的数据
     */
    private Map<String, Object> data = new HashMap<>();
 
    public 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_REASON.getSuccess());
        r.setCode(ResultCodeEnum.UNKNOWN_REASON.getCode());
        r.setMessage(ResultCodeEnum.UNKNOWN_REASON.getMessage());
        return r;
    }
 
    public static R setResult(ResultCodeEnum resultCodeEnum) {
        R r = new R();
        r.setSuccess(resultCodeEnum.getSuccess());
        r.setCode(resultCodeEnum.getCode());
        r.setMessage(resultCodeEnum.getMessage());
        return r;
    }
 
    public R success(Boolean success) {
        this.setSuccess(success);
        return this;
    }
 
    public R message(String message) {
        this.setMessage(message);
        return this;
    }
 
    public R code(Integer code) {
        this.setCode(code);
        return this;
    }
 
    public R data(String key, Object value) {
        this.data.put(key, value);
        return this;
    }
 
    public R data(Map<String, Object> map) {
        this.setData(map);
        return this;
    }
 
    /** 以下是get/set方法,如果项目有集成lombok可以使用@Data注解代替 */
 
    public Boolean getSuccess() {
        return success;
    }
 
    public void setSuccess(Boolean success) {
        this.success = success;
    }
 
    public Integer getCode() {
        return code;
    }
 
    public void setCode(Integer code) {
        this.code = code;
    }
 
    public String getMessage() {
        return message;
    }
 
    public void setMessage(String message) {
        this.message = message;
    }
 
    public Map<String, Object> getData() {
        return data;
    }
 
    public void setData(Map<String, Object> data) {
        this.data = data;
    }
}

?1.3?测试

/**
 * 测试控制器
 *
 */
@RestController
@RequestMapping("testR")
public class TestController {
 
    @GetMapping("ok")
    public R testOk() {
        Map<String, Object> data = new HashMap<>();
        data.put("name", "李太白");
        return R.ok().data(data);
    }
}

????????可以看到格式是正确的,只要我们返回数据的时候使用R这个类返回就行了,不过有一种情况,就是当我们代码中抛出异常之后返回的格式就不是这样子了,下面我演示一下在代码中添加int a = 1/0的语句,肯定导致抛异常的;?

/**
 * 测试控制器
 *
 */
@RestController
@RequestMapping("testR")
public class TestController {
 
    @GetMapping("ok")
    public R testOk() {
        int a = 1/0;
        Map<String, Object> data = new HashMap<>();
        data.put("name", "李太白");
        return R.ok().data(data);
    }
}

????????可以发现返回的格式已经不是我们所需要的格式了,这种情况会给前端人员带来不必要的麻烦,所以我们也需要对异常情况进行统一的格式处理;?

2. 统一异常处理

????????经过上面的演示,相信你已经明白我们为什么需要进行统一的异常处理了,当然处理统一的异常处理以外我们在开发项目中也会主动的抛出异常,像这种情况我们需要配合自定义异常来完成;

2.1 添加统一异常处理器

创建一个handler包,在该包下面添加GlobalExceptionHandler类

/**
 * 统一异常处理
 * ControllerAdvice注解的含义是当异常抛到controller层时会拦截下来
 */
@ControllerAdvice
public class GlobalExceptionHandler {
 
    /**
     * 使用ExceptionHandler注解声明处理Exception异常
     *
     */
    @ResponseBody
    @ExceptionHandler(Exception.class)
    public R exception(Exception e) {
        // 控制台打印异常
        e.printStackTrace();
        // 返回错误格式信息
        return R.error();
    }
 
}

2.2?测试统一异常处理

????????可以看到现在出现异常之后返回的格式已经是我们所需要的格式了,如果我们想让这个错误信息更加明确,我们可以通过添加自定义异常来实现。

2.3?添加自定义异常类

新建exception包,在该包下添加自定义异常类

/**
 * 测试自定义异常类
 * 需要继承运行时异常RuntimeException
 */
public class TestException extends RuntimeException {
    private Integer code;
 
    public TestException(ResultCodeEnum resultCodeEnum) {
        // 调用父类的方法添加信息
        super(resultCodeEnum.getMessage());
        this.code = resultCodeEnum.getCode();
    }
 
    public Integer getCode() {
        return code;
    }
}

?2.4?在统一异常处理类GlobalExceptionHandler中添加一个自定义异常的处理

/**
 * 统一异常处理
 * ControllerAdvice注解的含义是当异常抛到controller层时会拦截下来
 */
@ControllerAdvice
public class GlobalExceptionHandler {
 
    /**
     * 使用ExceptionHandler注解声明处理Exception异常
     *
     */
    @ResponseBody
    @ExceptionHandler(Exception.class)
    public R exception(Exception e) {
        // 控制台打印异常
        e.printStackTrace();
        // 返回错误格式信息
        return R.error();
    }
 
    /**
     * 使用ExceptionHandler注解声明处理TestException异常
     *
     */
    @ResponseBody
    @ExceptionHandler(TestException.class)
    public R exception(TestException e) {
        // 控制台打印异常
        e.printStackTrace();
        // 返回错误格式信息
        return R.error().message(e.getMessage()).code(e.getCode());
    }
 
}

?2.5?测试自定义异常

在枚举类中添加一个状态信息

TEST_NUMBER(false, 500, "计算错误");
/**
 * 测试控制器
 *
 */
@RestController
@RequestMapping("testR")
public class TestController {
 
    @GetMapping("ok")
    public R testOk() {
    try{

        int a = 1/0;
    }catch{
        throw new TestException(ResultCodeEnum.TEST_NUMBER);    
    }
        Map<String, Object> data = new HashMap<>();
        data.put("name", "李太白");
        return R.ok().data(data);
    }
}

3. 统一日志处理?

为了更方便我们进行错误的调式,一般会在项目中集成日志。

3.1?添加日志配置文件

在resources下添加日志的配置,文件名必须是logback-spring.xml

以下配置一般不需要修改,要改的话也只是修改日志的输出目录

<property name="log.path" value="D:/javaWeb/log" />

value就是日志的输出位置

<?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:/javaWeb/log" />
 
    <!--控制台日志格式:彩色日志-->
    <!-- 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)"/>
 
    <!--文件日志格式-->
    <property name="FILE_LOG_PATTERN"
              value="%date{yyyy-MM-dd HH:mm:ss} |%-5level |%thread |%file:%line |%logger |%msg%n" />
 
    <!--编码-->
    <property name="ENCODING"
              value="UTF-8" />
 
    <!--输出到控制台-->
    <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>${ENCODING}</charset>
        </encoder>
    </appender>
 
    <!--输出到文件-->
    <appender name="INFO_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!--日志过滤器:此日志文件只记录INFO级别的-->
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>INFO</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
        <!-- 正在记录的日志文件的路径及文件名 -->
        <file>${log.path}/log_info.log</file>
        <encoder>
            <pattern>${FILE_LOG_PATTERN}</pattern>
            <charset>${ENCODING}</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>500MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
            <!--日志文件保留天数-->
            <maxHistory>15</maxHistory>
        </rollingPolicy>
    </appender>
 
    <appender name="WARN_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!-- 日志过滤器:此日志文件只记录WARN级别的 -->
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>WARN</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
        <!-- 正在记录的日志文件的路径及文件名 -->
        <file>${log.path}/log_warn.log</file>
        <encoder>
            <pattern>${FILE_LOG_PATTERN}</pattern>
            <charset>${ENCODING}</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>
    </appender>
 
    <appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!-- 日志过滤器:此日志文件只记录ERROR级别的 -->
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>ERROR</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
        <!-- 正在记录的日志文件的路径及文件名 -->
        <file>${log.path}/log_error.log</file>
        <encoder>
            <pattern>${FILE_LOG_PATTERN}</pattern>
            <charset>${ENCODING}</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>
    </appender>
 
    <!--开发环境-->
    <springProfile name="dev">
        <!--可以灵活设置此处,从而控制日志的输出-->
        <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="ERROR">
            <appender-ref ref="ERROR_FILE" />
        </root>
    </springProfile>
 
</configuration>

3.2?添加application.properties配置

配置文件需要设置下环境,需要跟日志配置文件中的<springProfile name="dev">对应上,不然不生效

# 设置环境
spring.profiles.active=dev

3.3?修改GlobalExceptionHandler类

/**
 * 统一异常处理
 * ControllerAdvice注解的含义是当异常抛到controller层时会拦截下来
 */
@ControllerAdvice
public class GlobalExceptionHandler {
 
    /**
     * 打印日志 
     * 如果项目有集成lombok可使用@Slf4j注解代替
     */
    private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
 
    /**
     * 使用ExceptionHandler注解声明处理Exception异常
     *
     */
    @ResponseBody
    @ExceptionHandler(Exception.class)
    public R exception(Exception e) {
        // 控制台打印异常
        log.error(e.getMessage());
        // 返回错误格式信息
        return R.error();
    }
 
    /**
     * 使用ExceptionHandler注解声明处理TestException异常
     *
     */
    @ResponseBody
    @ExceptionHandler(TestException.class)
    public R exception(TestException e) {
        // 控制台打印异常
        log.error(e.getMessage());
        // 返回错误格式信息
        return R.error().message(e.getMessage()).code(e.getCode());
    }
 
}

3.4?测试效果

日志生效了,而且在我们的D盘javaWeb目录下也有对应的日志文件了

?我们可以进一步的完善下,将日志堆栈信息输出到文件

3.5?定义工具类

?新建utils包,在该包下添加ExceptionUtils类

/**
 * 日志堆栈信息输出到文件工具类
 *
 */
public class ExceptionUtils {
    public static String getMessage(Exception e) {
        StringWriter sw = null;
        PrintWriter pw = null;
        try {
            sw = new StringWriter();
            pw = new PrintWriter(sw);
            // 将出错的栈信息输出到printWriter中
            e.printStackTrace(pw);
            pw.flush();
            sw.flush();
        } finally {
            if (sw != null) {
                try {
                    sw.close();
                } catch (IOException e1) {
                    e1.printStackTrace();
                }
            }
            if (pw != null) {
                pw.close();
            }
        }
        return sw.toString();
    }
}

3.6?再修改GlobalExceptionHandler类

/**
 * 统一异常处理
 * ControllerAdvice注解的含义是当异常抛到controller层时会拦截下来
 */
@ControllerAdvice
public class GlobalExceptionHandler {
 
    /**
     * 打印日志 如果项目有集成lombok可使用@Slf4j注解代替
     */
    private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
 
    /**
     * 使用ExceptionHandler注解声明处理Exception异常
     *
     */
    @ResponseBody
    @ExceptionHandler(Exception.class)
    public R exception(Exception e) {
        // 控制台打印异常  借助工具类将错误堆栈输出到文件
        log.error(ExceptionUtils.getMessage(e));
        // 返回错误格式信息
        return R.error();
    }
 
    /**
     * 使用ExceptionHandler注解声明处理TestException异常
     *
     */
    @ResponseBody
    @ExceptionHandler(TestException.class)
    public R exception(TestException e) {
        // 控制台打印异常   借助工具类将错误堆栈输出到文件
        log.error(ExceptionUtils.getMessage(e));
        // 返回错误格式信息
        return R.error().message(e.getMessage()).code(e.getCode());
    }
 
}

以上是根据一位博主的文章编写的,现在找不到那篇文章了,还请见谅。

这篇文章就到这里了,下次见!

🥇原创不易,还希望各位大佬支持一下!

👍点赞,你的认可是我创作的动力?!

🌟收藏,你的青睐是我努力的方向!

??评论,你的意见是我进步的财富!

文章来源:https://blog.csdn.net/zoroly/article/details/135672595
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。