1. 面試官 | Spring Boot 項目如何統(tǒng)一結(jié)果,統(tǒng)一異常,統(tǒng)一日志?

        共 30963字,需瀏覽 62分鐘

         ·

        2022-06-29 15:16

        往期熱門文章:

        1、為什么不建議使用ON DUPLICATE KEY UPDATE?

        2、Java8 Stream,過分絲滑!

        3、8 種最坑SQL語法,工作中踩過嗎?

        4、Java 語言“坑爹” TOP 10

        5、你還不明白如何解決分布式Session?看這篇就夠了!

        作者永動的圖靈機(jī)

        來源:https://juejin.cn/post/6844904033488994317

        統(tǒng)一結(jié)果返回

        目前的前后端開發(fā)大部分?jǐn)?shù)據(jù)的傳輸格式都是json,因此定義一個統(tǒng)一規(guī)范的數(shù)據(jù)格式有利于前后端的交互與UI的展示。

        統(tǒng)一結(jié)果的一般形式

        1. 是否響應(yīng)成功;
        2. 響應(yīng)狀態(tài)碼;
        3. 狀態(tài)碼描述;
        4. 響應(yīng)數(shù)據(jù)
        5. 其他標(biāo)識符

        結(jié)果類枚舉

        • 前三者可定義結(jié)果枚舉,如:success,code,message
        @Getter
        public enum ResultCodeEnum {
            SUCCESS(true,20000,"成功"),
            UNKNOWN_ERROR(false,20001,"未知錯誤"),,
            PARAM_ERROR(false,20002,"參數(shù)錯誤"),
            ;

            // 響應(yīng)是否成功
            private Boolean success;
            // 響應(yīng)狀態(tài)碼
            private Integer code;
            // 響應(yīng)信息
            private String message;

            ResultCodeEnum(boolean success, Integer code, String message) {
                this.success = success;
                this.code = code;
                this.message = message;
            }
        }

        統(tǒng)一結(jié)果類

        • 第5個屬于自定義返回,利用前4者可定義統(tǒng)一返回對象

        注意:

        1. 外接只可以調(diào)用統(tǒng)一返回類的方法,不可以直接創(chuàng)建,影刺構(gòu)造器私有;
        2. 內(nèi)置靜態(tài)方法,返回對象;
        3. 為便于自定義統(tǒng)一結(jié)果的信息,建議使用鏈?zhǔn)骄幊?,將返回對象設(shè)類本身,即return this;
        4. 響應(yīng)數(shù)據(jù)由于為json格式,可定義為JsonObject或Map形式;
        @Data
        public class R {
            private Boolean success;

            private Integer code;

            private String message;

            private Map<String, Object> data = new HashMap<>();

            // 構(gòu)造器私有
            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;
            }

            // 設(shè)置結(jié)果,形參為結(jié)果枚舉
            public static R setResult(ResultCodeEnum result) {
                R r = new R();
                r.setSuccess(result.getSuccess());
                r.setCode(result.getCode());
                r.setMessage(result.getMessage());
                return r;
            }

            /**------------使用鏈?zhǔn)骄幊?,返回類本?----------**/
            
            // 自定義返回數(shù)據(jù)
            public R data(Map<String,Object> map) {
                this.setData(map);
                return this;
            }

            // 通用設(shè)置data
            public R data(String key,Object value) {
                this.data.put(key, value);
                return this;
            }

            // 自定義狀態(tài)信息
            public R message(String message) {
                this.setMessage(message);
                return this;
            }

            // 自定義狀態(tài)碼
            public R code(Integer code) {
                this.setCode(code);
                return this;
            }

            // 自定義返回結(jié)果
            public R success(Boolean success) {
                this.setSuccess(success);
                return this;
            }
        }

        控制層返回

        • 視圖層使用統(tǒng)一結(jié)果
        @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("用戶列表");
            }
        }    
        • json結(jié)果
        {
          "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"
              }
            ]
          }
        }

        統(tǒng)一結(jié)果類的使用參考了mybatis-plus中R對象的設(shè)計。


        統(tǒng)一異常處理

        使用統(tǒng)一返回結(jié)果時,還有一種情況,就是程序的保存是由于運行時異常導(dǎo)致的結(jié)果,有些異常我們可以無法提前預(yù)知,不能正常走到我們return的R對象返回。

        因此,我們需要定義一個統(tǒng)一的全局異常來捕獲這些信息,并作為一種結(jié)果返回控制層

        @ControllerAdvice

        該注解為統(tǒng)一異常處理的核心

        是一種作用于控制層的切面通知(Advice),該注解能夠?qū)⑼ㄓ玫腀ExceptionHandler、@InitBinder和@ModelAttributes方法收集到一個類型,并應(yīng)用到所有控制器上

        該類中的設(shè)計思路:

        1. 使用@ExceptionHandler注解捕獲指定或自定義的異常;
        2. 使用@ControllerAdvice集成@ExceptionHandler的方法到一個類中;
        3. 必須定義一個通用的異常捕獲方法,便于捕獲未定義的異常信息;
        4. 自定一個異常類,捕獲針對項目或業(yè)務(wù)的異常;
        5. 異常的對象信息補(bǔ)充到統(tǒng)一結(jié)果枚舉中;

        自定義全局異常類

        @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() + '}';
            }
        }

        統(tǒng)一異常處理器

        // ...
        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(); // 通用異常結(jié)果
            }

            /**-------- 指定異常處理方法 --------**/
            @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());
            }
        }

        控制層展示

        以下為展示當(dāng)遇到null指定異常時,返回的結(jié)果信息

        {
          "success"false,
          "code": 20007,
          "message""空指針異常",
          "data": {}
        }

        統(tǒng)一日志收集

        日志是追蹤錯誤定位問題的關(guān)鍵,尤其在生產(chǎn)環(huán)境中,需要及時修復(fù)熱部署,不會提供開發(fā)者debug的環(huán)境,此時日志將會是最快解決問題的關(guān)鍵

        日志的框架比較豐富,由于spring boot對logback的集成,因此推薦使用logback在項目中使用。

        Logback

        配置

        以下直接貼出配置信息,介紹信息科直接參考備注

        <?xml version="1.0" encoding="UTF-8"?>
        <!-- 日志級別從低到高分為TRACE < DEBUG < INFO < WARN < ERROR < FATAL,如果設(shè)置為WARN,則低于WARN的信息都不會輸出 -->
        <!-- scan:當(dāng)此屬性設(shè)置為true時,配置文檔如果發(fā)生改變,將會被重新加載,默認(rèn)值為true -->
        <!-- scanPeriod:設(shè)置監(jiān)測配置文檔是否有修改的時間間隔,如果沒有給出時間單位,默認(rèn)單位是毫秒。
                         當(dāng)scan為true時,此屬性生效。默認(rèn)的時間間隔為1分鐘。 -->
        <!-- debug:當(dāng)此屬性設(shè)置為true時,將打印出logback內(nèi)部日志信息,實時查看logback運行狀態(tài)。默認(rèn)值為false。 -->
        <configuration  scan="true" scanPeriod="10 seconds">
            <contextName>logback</contextName>

            <!-- name的值是變量的名稱,value的值時變量定義的值。通過定義的值會被插入到logger上下文中。定義后,可以使“${}”來使用變量。 -->
            <property name="log.path" value="D:/Documents/logs/edu" />

            <!--0. 日志格式和顏色渲染 -->
            <!-- 彩色日志依賴的渲染類 -->
            <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}}"/>

            <!--1. 輸出到控制臺-->
            <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
                <!--此日志appender是為開發(fā)使用,只配置最底級別,控制臺輸出的日志級別是大于或等于此級別的日志信息-->
                <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
                    <level>debug</level>
                </filter>
                <encoder>
                    <Pattern>${CONSOLE_LOG_PATTERN}</Pattern>
                    <!-- 設(shè)置字符集 -->
                    <charset>UTF-8</charset>
                </encoder>
            </appender>

            <!--2. 輸出到文檔-->
            <!-- 2.1 level為 DEBUG 日志,時間滾動輸出  -->
            <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> <!-- 設(shè)置字符集 -->
                </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>
                    <!--日志文檔保留天數(shù)-->
                    <maxHistory>15</maxHistory>
                </rollingPolicy>
                <!-- 此日志文檔只記錄debug級別的 -->
                <filter class="ch.qos.logback.classic.filter.LevelFilter">
                    <level>debug</level>
                    <onMatch>ACCEPT</onMatch>
                    <onMismatch>DENY</onMismatch>
                </filter>
            </appender>

            <!-- 2.2 level為 INFO 日志,時間滾動輸出  -->
            <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>
                    <!--日志文檔保留天數(shù)-->
                    <maxHistory>15</maxHistory>
                </rollingPolicy>
                <!-- 此日志文檔只記錄info級別的 -->
                <filter class="ch.qos.logback.classic.filter.LevelFilter">
                    <level>info</level>
                    <onMatch>ACCEPT</onMatch>
                    <onMismatch>DENY</onMismatch>
                </filter>
            </appender>

            <!-- 2.3 level為 WARN 日志,時間滾動輸出  -->
            <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> <!-- 此處設(shè)置字符集 -->
                </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>
                    <!--日志文檔保留天數(shù)-->
                    <maxHistory>15</maxHistory>
                </rollingPolicy>
                <!-- 此日志文檔只記錄warn級別的 -->
                <filter class="ch.qos.logback.classic.filter.LevelFilter">
                    <level>warn</level>
                    <onMatch>ACCEPT</onMatch>
                    <onMismatch>DENY</onMismatch>
                </filter>
            </appender>

            <!-- 2.4 level為 ERROR 日志,時間滾動輸出  -->
            <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> <!-- 此處設(shè)置字符集 -->
                </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>
                    <!--日志文檔保留天數(shù)-->
                    <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>用來設(shè)置某一個包或者具體的某一個類的日志打印級別、
                以及指定<appender>。<logger>僅有一個name屬性,
                一個可選的level和一個可選的addtivity屬性。
                name:用來指定受此logger約束的某一個包或者具體的某一個類。
                level:用來設(shè)置打印級別,大小寫無關(guān):TRACE, DEBUG, INFO, WARN, ERROR, ALL 和 OFF,
                      還有一個特俗值INHERITED或者同義詞NULL,代表強(qiáng)制執(zhí)行上級的級別。
                      如果未設(shè)置此屬性,那么當(dāng)前l(fā)ogger將會繼承上級的級別。
                addtivity:是否向上級logger傳遞打印信息。默認(rèn)是true。
                <logger name="org.springframework.web" level="info"/>
                <logger name="org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor" level="INFO"/>
            -->

            <!--
                使用mybatis的時候,sql語句是debug下才會打印,而這里我們只配置了info,所以想要查看sql語句的話,有以下兩種操作:
                第一種把<root level="info">改成<root level="DEBUG">這樣就會打印sql,不過這樣日志那邊會出現(xiàn)很多其他消息
                第二種就是單獨給dao下目錄配置debug模式,代碼如下,這樣配置sql語句會打印,其他還是正常info級別:
                【logging.level.org.mybatis=debug logging.level.dao=debug】
             -->

            <!--
                root節(jié)點是必選節(jié)點,用來指定最基礎(chǔ)的日志輸出級別,只有一個level屬性
                level:用來設(shè)置打印級別,大小寫無關(guān):TRACE, DEBUG, INFO, WARN, ERROR, ALL 和 OFF,
                不能設(shè)置為INHERITED或者同義詞NULL。默認(rèn)是DEBUG
                可以包含零個或多個元素,標(biāo)識這個appender將會添加到這個logger。
            -->

            <!-- 4. 最終的策略 -->
            <!-- 4.1 開發(fā)環(huán)境:打印控制臺-->
            <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>


            <!-- 4.2 生產(chǎn)環(huán)境:輸出到文檔-->
            <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>

        日志收集異常信息

        日志信息往往伴隨著異常信息的輸出,因此,我們需要修改統(tǒng)一異常的處理器,將異常信息以流的方式寫到日志文件中

        • 異常信息文件工具類
        @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;
            }
        }
        • 修改統(tǒng)一異常處理器,將異常方法中的直接打印改為日志輸入并打印
        // ...
        import lombok.extern.slf4j.Slf4j;

        @ControllerAdvice
        @Slf4j
        public class GlobalExceptionHandler {

            /**-------- 通用異常處理方法 --------**/
            @ExceptionHandler(Exception.class)
            @ResponseBody
            public R error(Exception e) {
                // e.printStackTrace();
                log.error(ExceptionUtil.getMessage(e));
                return R.error();
            }
            
           // ...
        }   

        注意

        1. 日志的環(huán)境即spring.profiles.acticve,跟隨項目啟動;
        2. 啟動后,即可到自定目錄查找到生成的日志文件;
        3. 本地idea調(diào)試時,推薦Grep Console插件可實現(xiàn)控制臺的自定義顏色輸出


        版權(quán)申明:內(nèi)容來源網(wǎng)絡(luò),版權(quán)歸原創(chuàng)者所有。除非無法確認(rèn),都會標(biāo)明作者及出處,如有侵權(quán),煩請告知,我們會立即刪除并致歉!

        最近熱文閱讀:

        1、為什么不建議使用ON DUPLICATE KEY UPDATE?
        2、Java8 Stream,過分絲滑!
        3、8 種最坑SQL語法,工作中踩過嗎?
        4、Java 語言“坑爹” TOP 10
        5、你還不明白如何解決分布式Session?看這篇就夠了!
        6、能解決 80% 故障的排查思路
        7、程序員坐牢了,會被安排寫代碼嗎?
        8、面試被問Nginx,怎么破?
        9、為什么很多 SpringBoot 開發(fā)者放棄了 Tomcat,選擇了 Undertow?
        10、數(shù)據(jù)庫日期類型字段設(shè)計,應(yīng)該如何選擇?
        關(guān)注公眾號,你想要的Java都在這里

        瀏覽 36
        點贊
        評論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報
        評論
        圖片
        表情
        推薦
        點贊
        評論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報
          
          

            1. 少妇夜夜爽 | 雷电将军与丘丘人繁衍后代动画 | 日韩十九禁 | 韩国成人在线观看 | 电影操逼网 |