• 如何在SpringBoot项目中实现统一数据返回格式及统一异常处理?
  • 发布于 2个月前
  • 406 热度
    0 评论
在我们的项目开发中,我们都会对数据返回格式进行统一的处理,这样可以方便前端人员取数据,当然除了正常流程的数据返回格式需要统一以外,我们也需要对异常的情况进行统一的处理,以及项目必备的日志这些,以下都会说明如何添加,写该文的目的是为了让新人了解在项目开发中,这些是咋做的。

一.统一返回格式
在项目开发中返回的是json格式的数据,也就是统一json数据返回格式,一般情况下返回数据的基本格式包含是否成功、响应状态码、返回的消息、以及返回的数据。格式如下:
{
  "success": 布尔,     // 是否成功
  "code": 数字,     // 响应状态码
  "message": 字符串,   // 返回的消息
  "data": {}          //  放置响应的数据
}
下面开始实操:
1.添加枚举类
该类定义了以上统一格式的前三部分:是否成功、响应状态码、返回的消息;可自行根据项目需要进行后续的添加或者删改。
创建一个result包,下面放置ResultCodeEnum枚举类
/**
 * 状态码
 * 堆代码 duidaima.com
 * @date 2023/12/30
 */
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 + '\'' + '}';
    }
}
2.添加统一返回格式的类
该类是用来和前端交互的类,定义的就是本文开头所说的格式。
在result包下创建一个统一返回格式的类R
/**
 * 统一返回格式类
 *
 * @author 爷爷的茶七里香
 * @date 2022/05/30
 */
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;
    }
}
3.测试
下面我们提供一个controller来测试下
/**
 * 测试控制器
 *
 * @date 2023/12/30
 */
@RestController
@RequestMapping("testR")
public class TestController {
 
    /**
     * @return {@link R}
     */
    @GetMapping("ok")
    public R testOk() {
        Map<String, Object> data = new HashMap<>();
        data.put("name", "李太白");
        return R.ok().data(data);
    }
}
效果

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

结果

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

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

下面进行实操:
添加统一异常处理器(创建一个handler包,在该包下面添加GlobalExceptionHandler类)
/**
 * 统一异常处理
 *
 * @堆代码 duidaima.com
 * @date 2023/12/30 ControllerAdvice注解的含义是当异常抛到controller层时会拦截下来
 */
@ControllerAdvice
public class GlobalExceptionHandler {
 
    /**
     * 使用ExceptionHandler注解声明处理Exception异常
     *
     * @param e e
     * @return {@link R}
     */
    @ResponseBody
    @ExceptionHandler(Exception.class)
    public R exception(Exception e) {
        // 控制台打印异常
        e.printStackTrace();
        // 返回错误格式信息
        return R.error();
    }
 
}
测试统一异常处理

可以看到现在出现异常之后返回的格式已经是我们所需要的格式了,如果我们想让这个错误信息更加明确,我们可以通过添加自定义异常来实现,下面开始实操:
添加自定义异常类 (新建exception包,在该包下添加自定义异常类)
/**
 * 测试自定义异常类
 *
 * @date 2023/05/30 需要继承运行时异常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;
    }
}
在统一异常处理类GlobalExceptionHandler中添加一个自定义异常的处理
/**
 * 统一异常处理
 * @date 2023/12/30 ControllerAdvice注解的含义是当异常抛到controller层时会拦截下来
 */
@ControllerAdvice
public class GlobalExceptionHandler {
 
    /**
     * 使用ExceptionHandler注解声明处理Exception异常
     *
     * @param e e
     * @return {@link R}
     */
    @ResponseBody
    @ExceptionHandler(Exception.class)
    public R exception(Exception e) {
        // 控制台打印异常
        e.printStackTrace();
        // 返回错误格式信息
        return R.error();
    }
 
    /**
     * 使用ExceptionHandler注解声明处理TestException异常
     *
     * @param e e
     * @return {@link R}
     */
    @ResponseBody
    @ExceptionHandler(TestException.class)
    public R exception(TestException e) {
        // 控制台打印异常
        e.printStackTrace();
        // 返回错误格式信息
        return R.error().message(e.getMessage()).code(e.getCode());
    }
 
}
测试自定义异常
在枚举类中添加一个状态信息
TEST_NUMBER(false, 500, "计算错误");

输出结果:

现在自定义异常也能统一返回格式了

三.统一日志处理
为了更方便我们进行错误的调式,一般会在项目中集成日志,下面我们开始实操吧:
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>
2.添加application.properties配置
配置文件需要设置下环境,需要跟日志配置文件中的<springProfile name="dev">对应上,不然不生效
# 设置环境
spring.profiles.active=dev
修改GlobalExceptionHandler类
具体修改看下面的代码:

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

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

我们可以进一步的完善下,将日志堆栈信息输出到文件
定义工具类(新建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.再修改GlobalExceptionHandler类
/**
 * 统一异常处理
 */
@ControllerAdvice
public class GlobalExceptionHandler {
 
    /**
     * 打印日志 如果项目有集成lombok可使用@Slf4j注解代替
     */
    private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
 
    /**
     * 使用ExceptionHandler注解声明处理Exception异常
     *
     * @param e e
     * @return {@link R}
     */
    @ResponseBody
    @ExceptionHandler(Exception.class)
    public R exception(Exception e) {
        // 控制台打印异常  借助工具类将错误堆栈输出到文件
        log.error(ExceptionUtils.getMessage(e));
        // 返回错误格式信息
        return R.error();
    }
 
    /**
     * 使用ExceptionHandler注解声明处理TestException异常
     *
     * @param e e
     * @return {@link R}
     */
    @ResponseBody
    @ExceptionHandler(TestException.class)
    public R exception(TestException e) {
        // 控制台打印异常   借助工具类将错误堆栈输出到文件
        log.error(ExceptionUtils.getMessage(e));
        // 返回错误格式信息
        return R.error().message(e.getMessage()).code(e.getCode());
    }
 
}
测试效果

四.总结(源代码)
到这就已经完成了统一返回格式、统一异常处理、已经统一的日志处理,通过文章不一定能看明白,所以我在此提供源代码,配合文章食用;
gitee仓库(本文章源代码):https://gitee.com/sgdygb/demo20220530
用户评论