第四十五章:基於SpringBoot 設計業務邏輯異常統一處理

恆宇少年發表於2018-06-23

在我們平時的專案研發過程中,異常一般都是程式設計師最為頭疼的問題,異常的丟擲、捕獲、處理等既涉及事務回滾,還會涉及返回前端訊息提醒資訊。那麼我們怎麼設計可以解決上面的兩個的痛點呢?我們可不可以統一處理業務邏輯然後給出前端對應的異常提醒內容呢?

本章目標

基於SpringBoot平臺構建業務邏輯異常統一處理,異常訊息內容格式化。

福利來了

騰訊雲特惠伺服器10元/月,點選參團

SpringBoot 企業級核心技術學習專題


專題 專題名稱 專題描述
001 Spring Boot 核心技術 講解SpringBoot一些企業級層面的核心元件
002 Spring Boot 核心技術章節原始碼 Spring Boot 核心技術簡書每一篇文章碼雲對應原始碼
003 Spring Cloud 核心技術 對Spring Cloud核心技術全面講解
004 Spring Cloud 核心技術章節原始碼 Spring Cloud 核心技術簡書每一篇文章對應原始碼
005 QueryDSL 核心技術 全面講解QueryDSL核心技術以及基於SpringBoot整合SpringDataJPA
006 SpringDataJPA 核心技術 全面講解SpringDataJPA核心技術
007 SpringBoot核心技術學習目錄 SpringBoot系統的學習目錄,敬請關注點贊!!!

構建專案

我們將邏輯異常核心處理部分提取出來作為單獨的jar供其他模組引用,建立專案在parent專案pom.xml新增公共使用的依賴,配置內容如下所示:

<dependencies>
		<!--Lombok-->
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<optional>true</optional>
		</dependency>
		<!--測試模組依賴-->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
		<!--web依賴-->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
</dependencies>
複製程式碼

專案建立完成後除了.ideaimlpom.xml保留,其他的都刪除。

異常處理核心子模組

我們建立一個名為springboot-core-exception的子模組,在該模組內自定義一個LogicException執行時異常類,繼承RuntimeException並重寫建構函式,程式碼如下所示:

/**
 * 自定義業務邏輯異常類
 * ========================
 * Created with IntelliJ IDEA.
 * User:恆宇少年
 * Date:2018/1/7
 * Time:下午2:38
 * 碼雲:http://git.oschina.net/jnyqy
 * ========================
 *
 * @author yuqiyu
 */
public class LogicException extends RuntimeException {

    /**
     * 日誌物件
     */
    private Logger logger = LoggerFactory.getLogger(LogicException.class);

    /**
     * 錯誤訊息內容
     */
    protected String errMsg;
    /**
     * 錯誤碼
     */
    protected String errCode;
    /**
     * 格式化錯誤碼時所需引數列表
     */
    protected String[] params;


    /**
     * 獲取錯誤訊息內容
     * 根據errCode從redis內獲取未被格式化的錯誤訊息內容
     * 並通過String.format()方法格式化錯誤訊息以及引數
     *
     * @return
     */
    public String getErrMsg() {
        return errMsg;
    }

    /**
     * 獲取錯誤碼
     *
     * @return
     */
    public String getErrCode() {
        return errCode;
    }

    /**
     * 獲取異常引數列表
     *
     * @return
     */
    public String[] getParams() {
        return params;
    }

    /**
     * 建構函式設定錯誤碼以及錯誤引數列表
     *
     * @param errCode 錯誤碼
     * @param params  錯誤引數列表
     */
    public LogicException(String errCode, String... params) {
        this.errCode = errCode;
        this.params = params;
        //獲取格式化後的異常訊息內容
        this.errMsg = ErrorMessageTools.getErrorMessage(errCode, params);
        //錯誤資訊
        logger.error("系統遇到如下異常,異常碼:{}>>>異常資訊:{}", errCode, errMsg);
    }
}
複製程式碼

在重寫的建構函式內需要傳遞兩個引數errCodeparams,其目的是為了初始化類內的全域性變數。

  • errCode:該欄位是對應的異常碼,我們在後續文章內容中建立一個存放異常錯誤碼的列舉,而errCode就是列舉對應的字串的值。
  • params:這裡是對應errCode字串含義描述時所需要的引數列表。
  • errMsg:格式化後的業務邏輯異常訊息描述,我們在建構函式內可以看到呼叫了ErrorMessageTools.getErrorMessage(errCode,params);,這個方法作用是通過異常碼在資料庫內獲取未格式化的異常描述,通過傳遞的引數進行格式化異常訊息描述。

建立異常核心包的目的就是讓其他模組直接新增依賴,那異常描述內容該怎麼獲取呢?

定義異常訊息獲取介面

我們在springboot-exception-core模組內新增一個介面LogicExceptionMessage,該介面提供通過異常碼獲取未格式化的異常訊息描述內容方法,介面定義如下所示:

/**
 * 邏輯異常介面定義
 * 使用專案需要實現該介面方法並提供方法實現
 * errCode對應邏輯異常碼
 * getMessage返回字串為邏輯異常訊息內容
 * ========================
 * Created with IntelliJ IDEA.
 * User:恆宇少年
 * Date:2018/1/7
 * Time:下午2:41
 * 碼雲:http://git.oschina.net/jnyqy
 * ========================
 * @author yuqiyu
 */
public interface LogicExceptionMessage {

    /**
     * 獲取異常訊息內容
     * @param errCode 錯誤碼
     * @return
     */
    public String getMessage(String errCode);
}
複製程式碼

在需要載入springboot-exception-core依賴的專案中,建立實體類實現LogicExceptionMessage介面並重寫getMessage(String errCode)方法我們就可以通過spring IOC獲取實現類例項進行操作獲取資料,下面我們在編寫使用異常模組時會涉及到。

格式化異常訊息工具類

下面我們再回頭看看建構函式格式化異常訊息工具類ErrorMessageTools,該工具類內提供getErrorMessage方法用於獲取格式化後的異常訊息描述,程式碼實現如下所示:

/**
 * 異常訊息描述格式化工具類
 * ========================
 * Created with IntelliJ IDEA.
 * User:恆宇少年
 * Date:2018/1/7
 * Time:下午2:40
 * 碼雲:http://git.oschina.net/jnyqy
 * ========================
 *
 * @author yuqiyu
 */
public class ErrorMessageTools {
    /**
     * 異常訊息獲取
     *
     * @param errCode 異常訊息碼
     * @param params  格式化異常引數所需引數列表
     * @return
     */
    public static String getErrorMessage(String errCode, Object... params) {
        //獲取業務邏輯訊息實現
        LogicExceptionMessage logicExceptionMessage = SpringBeanTools.getBean(LogicExceptionMessage.class);
        if (ObjectUtils.isEmpty(logicExceptionMessage)) {
            try {
                throw new Exception("請配置實現LogicExceptionMessage介面並設定實現類被SpringIoc所管理。");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        //獲取錯誤訊息內容
        String errMsg = logicExceptionMessage.getMessage(errCode);
        //格式化錯誤訊息內容
        return ObjectUtils.isEmpty(params) ? errMsg : String.format(errMsg, params);
    }
}
複製程式碼

注意:由於我們的工具類都是靜態方法呼叫方式,所以無法直接使用Spring IOC註解注入的方式獲取LogicExceptionMessage例項。

由於無法注入例項,在getErrorMessage方法內,我們通過工具類SpringBeanTools來獲取ApplicationContext上下文例項,再通過上下文來獲取指定型別的Bean;獲取到LogicExceptionMessage例項後呼叫getMessage方法,根據傳入的errCode就可以直接從介面實現類例項中獲取到未格式化的異常描述!

當然實現類可以是以RedisMap集合資料庫文字作為資料來源。

獲取到未格式化的異常描述後通過String.format方法以及傳遞的引數直接就可以獲取格式化後的字串,如:

未格式化異常訊息 => 使用者:%s已被凍結,無法操作.
格式化程式碼 => String.format("%s已被凍結,無法操作.","恆宇少年");
格式化後效果 => 使用者:恆宇少年已被凍結,無法操作.
複製程式碼

具體的格式化特殊字元含義可以去檢視String.format文件,如何獲取ApplicationContext上下文物件,請訪問第三十二章:如何獲取SpringBoot專案的applicationContext物件檢視。

我們再回到LogicException建構函式內,這時errMsg欄位對應的值就會是格式化後的異常訊息描述,在外部我們呼叫getErrMsg方法就可以直接得到異常描述。

到目前為止,我們已經將springboot-exception-core模組程式碼編碼完成,下面我們來看下怎麼來使用我們自定義的業務邏輯異常並且獲取格式化後的異常訊息描述。

異常示例模組

基於parent我們來建立一個名為springboot-exception-example的子模組專案,專案內需要新增一些額外的配置依賴,當然也需要將我們的springboot-exception-core依賴新增進入,pom.xml配置檔案內容如下所示:

<dependencies>
        <!--異常核心依賴-->
        <dependency>
            <groupId>com.hengyu</groupId>
            <artifactId>springboot-exception-core</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
        <!--spring data jpa依賴-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <!--資料庫驅動-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <!--druid依賴-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.6</version>
        </dependency>
</dependencies>
複製程式碼

下面我們來配置下我們示例專案application.yml檔案需要的配置,如下所示:

spring:
  application:
    name: springboot-exception-core
    #資料來源配置
  datasource:
    druid:
      url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true
      username: root
      password: 123456
      driver-class-name: com.mysql.jdbc.Driver
  jpa:
    properties:
      hibernate:
        #配置顯示sql
        show_sql: true
        #配置格式化sql
        format_sql: true
複製程式碼

在上面我們有講到LogicExceptionMessage獲取的內容可以從很多種資料來源中讀取,我們還是採用資料庫來進行讀取,建議正式環境放到redis快取內!!!

異常資訊表

接下來在資料庫內建立異常資訊表sys_exception_info,語句如下:

DROP TABLE IF EXISTS `sys_exception_info`;
/*!40101 SET @saved_cs_client     = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `sys_exception_info` (
  `EI_ID` int(11) NOT NULL AUTO_INCREMENT COMMENT '主鍵自增',
  `EI_CODE` varchar(30) DEFAULT NULL COMMENT '異常碼',
  `EI_MESSAGE` varchar(50) DEFAULT NULL COMMENT '異常訊息內容',
  PRIMARY KEY (`EI_ID`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 COMMENT='系統異常基本資訊';
/*!40101 SET character_set_client = @saved_cs_client */;

--
-- Dumping data for table `sys_exception_info`
--

LOCK TABLES `sys_exception_info` WRITE;
/*!40000 ALTER TABLE `sys_exception_info` DISABLE KEYS */;
INSERT INTO `sys_exception_info` VALUES (1,'USER_NOT_FOUND','使用者不存在.'),(2,'USER_STATUS_FAILD','使用者狀態異常.');
/*!40000 ALTER TABLE `sys_exception_info` ENABLE KEYS */;
UNLOCK TABLES;
複製程式碼

我們通過spring-data-jpa來實現資料讀取,下面對應資料表建立對應的Entity

異常資訊實體

/**
 * 系統異常基本資訊實體
 * ========================
 * Created with IntelliJ IDEA.
 * User:恆宇少年
 * Date:2018/1/7
 * Time:下午3:35
 * 碼雲:http://git.oschina.net/jnyqy
 * ========================
 * @author yuqiyu
 */
@Data
@Entity
@Table(name = "sys_exception_info")
public class ExceptionInfoEntity implements Serializable{
    /**
     * 異常訊息編號
     */
    @Id
    @GeneratedValue
    @Column(name = "EI_ID")
    private Integer id;
    /**
     * 異常訊息錯誤碼
     */
    @Column(name = "EI_CODE")
    private String code;
    /**
     * 異常訊息內容
     */
    @Column(name = "EI_MESSAGE")
    private String message;
}
複製程式碼

異常資訊資料介面

/**
 * 異常資料介面定義
 * ========================
 * Created with IntelliJ IDEA.
 * User:恆宇少年
 * Date:2018/1/7
 * Time:下午3:34
 * 碼雲:http://git.oschina.net/jnyqy
 * ========================
 * @author yuqiyu
 */
public interface ExceptionRepository
    extends JpaRepository<ExceptionInfoEntity,Integer>
{
    /**
     * 根據異常碼獲取異常配置資訊
     * @param code 異常碼
     * @return
     */
    ExceptionInfoEntity findTopByCode(String code);
}
複製程式碼

在資料介面內通過spring-data-jpa方法查詢方式,通過errCode讀取異常資訊實體內容。

在開發過程中異常跑出時所用到的errCode一般存放在列舉型別或者常量介面內,在這裡我們選擇可擴充套件相對來說比較強的列舉型別,程式碼如下:

/**
 * 錯誤碼列舉型別
 * ========================
 * Created with IntelliJ IDEA.
 * User:恆宇少年
 * Date:2018/1/7
 * Time:下午3:25
 * 碼雲:http://git.oschina.net/jnyqy
 * ========================
 * @author yuqiyu
 */
public enum ErrorCodeEnum {
    /**
     * 使用者不存在.
     */
    USER_NOT_FOUND,
    /**
     * 使用者狀態異常.
     */
    USER_STATUS_FAILD,
    //...新增其他錯誤碼
}
複製程式碼

異常碼列舉內容項是需要根據資料庫異常資訊表對應變動的,能夠保證我們在丟擲異常時,在資料庫內有對應的資訊。

LogicExceptionMessage實現類定義

我們在springboot-exception-core核心模組內新增了LogicExceptionMessage介面定義,需要我們實現該介面的getMessage方法核心模組,這樣才可以獲取資料庫內對應的異常資訊,實現類如下所示:

/**
 * 業務邏輯異常訊息獲取實現類
 * - 訊息可以從資料庫內獲取
 * - 訊息可從Redis內獲取
 * ========================
 * Created with IntelliJ IDEA.
 * User:恆宇少年
 * Date:2018/1/7
 * Time:下午3:16
 * 碼雲:http://git.oschina.net/jnyqy
 * ========================
 * @author yuqiyu
 */
@Component
public class LogicExceptionMessageSupport implements LogicExceptionMessage {

    /**
     * 異常資料介面
     */
    @Autowired
    private ExceptionRepository exceptionRepository;

    /**
     * 根據錯誤碼獲取錯誤資訊
     * @param errCode 錯誤碼
     * @return
     */
    @Override
    public String getMessage(String errCode) {
        ExceptionInfoEntity exceptionInfoEntity = exceptionRepository.findTopByCode(errCode);
        if(!ObjectUtils.isEmpty(exceptionInfoEntity)) {
            return exceptionInfoEntity.getMessage();
        }
        return "系統異常";
    }
}
複製程式碼

getMessage方法內通過ExceptionRepository資料介面定義的findTopByCode方法獲取指定異常嗎的異常資訊,當存在異常資訊時返回未格式化的異常描述。

統一返回實體定義

對於介面專案(包括前後分離專案)在處理返回統一格式時,我們通常會採用固定實體的方式,這樣對於前端呼叫介面的開發者來說解析內容是比較方便的,同樣在開發過程中會約定遇到系統異常、業務邏輯異常時返回的格式內容,當然這跟請求介面正確返回的格式是一樣的,只不過欄位內容有差異。 統一返回實體ApiResponseEntity<T extends Object>如下:

/**
 * 介面響應實體
 * ========================
 * Created with IntelliJ IDEA.
 * User:恆宇少年
 * Date:2018/1/9
 * Time:下午3:04
 * 碼雲:http://git.oschina.net/jnyqy
 * ========================
 * @author yuqiyu
 */
@Data
@Builder
public class ApiResponseEntity<T extends Object> {
    /**
     * 錯誤訊息
     */
    private String errorMsg;
    /**
     * 資料內容
     */
    private T data;
}
複製程式碼

ApiResponseEntity實體內,採用了Lombok的構造者設計模式@Builder註解,配置該註解的實體會自動在.class檔案內新增內部類實現設計模式,部分自動生成程式碼如下:

// ...
public static class ApiResponseEntityBuilder<T> {
        private String errorMsg;
        private T data;

        ApiResponseEntityBuilder() {
        }

        public ApiResponseEntity.ApiResponseEntityBuilder<T> errorMsg(String errorMsg) {
            this.errorMsg = errorMsg;
            return this;
        }

        public ApiResponseEntity.ApiResponseEntityBuilder<T> data(T data) {
            this.data = data;
            return this;
        }

        public ApiResponseEntity<T> build() {
            return new ApiResponseEntity(this.errorMsg, this.data);
        }

        public String toString() {
            return "ApiResponseEntity.ApiResponseEntityBuilder(errorMsg=" + this.errorMsg + ", data=" + this.data + ")";
        }
    }
// ...
複製程式碼

到目前為止,我們並未新增全域性異常相關的配置,而全域性異常配置這塊,我們採用之前章節講到的@ControllerAdvice來實現,@ControllerAdvice相關的內容請訪問第二十一章:SpringBoot專案中的全域性異常處理

全域性異常通知定義

我們本章節僅僅新增業務邏輯異常的處理,具體編碼如下所示:

/**
 * 控制器異常通知類
 * ========================
 * Created with IntelliJ IDEA.
 * User:恆宇少年
 * Date:2018/1/7
 * Time:下午5:30
 * 碼雲:http://git.oschina.net/jnyqy
 * ========================
 *
 * @author yuqiyu
 */
@ControllerAdvice(annotations = RestController.class)
@ResponseBody
public class ExceptionAdvice {

    /**
     * logback new instance
     */
    Logger logger = LoggerFactory.getLogger(this.getClass());

    /**
     * 處理業務邏輯異常
     *
     * @param e 業務邏輯異常物件例項
     * @return 邏輯異常訊息內容
     */
    @ExceptionHandler(LogicException.class)
    @ResponseStatus(code = HttpStatus.OK)
    public ApiResponseEntity<String> logicException(LogicException e) {
        logger.error("遇到業務邏輯異常:【{}】", e.getErrCode());
        // 返回響應實體內容
        return ApiResponseEntity.<String>builder().errorMsg(e.getErrMsg()).build();
    }
}
複製程式碼

最近技術群內有同學問我,既然我們用的是@RestController為什麼這裡還需要配置@ResponseBody?這裡給大家一個解釋,我們控制器通知確實是監聽的@RestController,而@RestController註解的控制器統一都是返回JSON格式的資料。那麼我們在遇到異常後,請求已經不再控制器內了,已經交付給控制器通知類,那麼我們通知類如果同樣想返回JSON資料,這裡就需要配置@ResponseBody註解來實現。

我們來看上面logicException()方法,該方法返回值是我們定義的統一返回實體,目的是為了遇到業務邏輯異常時同樣返回與正確請求一樣的格式。

  • @ ExceptionHandler配置了將要處理LogicException型別的異常,也就是隻要系統遇到LogicException異常並且拋給了控制器,就會呼叫該方法。
  • @ResponseStatus配置了返回的狀態值,因為我們遇到業務邏輯異常前端肯定需要的不是500錯誤,而是一個200狀態的JSON業務異常描述。

在方法返回時使用構造者設計模式並將異常訊息傳遞給errorMsg()方法,這樣就實現了欄位errorMsg的賦值。

測試

異常相關的編碼完成,下面我們來建立一個測試的控制器模擬業務邏輯發生時,系統是怎麼做出的返回? 測試控制內容如下所示:

/**
 * 測試控制器
 * ========================
 * Created with IntelliJ IDEA.
 * User:恆宇少年
 * Date:2018/1/7
 * Time:下午3:12
 * 碼雲:http://git.oschina.net/jnyqy
 * ========================
 *
 * @author yuqiyu
 */
@RestController
public class IndexController {
    /**
     * 首頁方法
     *
     * @return
     */
    @RequestMapping(value = "/index")
    public ApiResponseEntity<String> index() throws LogicException {
        /**
         * 模擬使用者不存在
         * 丟擲業務邏輯異常
         */
        if (true) {
            throw new LogicException(ErrorCodeEnum.USER_STATUS_FAILD.toString());
        }
        return ApiResponseEntity.<String>builder().data("this is index mapping").build();
    }
}
複製程式碼

根據上面程式碼含義,當我們在訪問/index時就會發生USER_STATUS_FAILD業務邏輯異常,按照我們之前的全域性異常配置以及統一返回實體例項化,訪問後會出現ApiResponseEntity格式JSON資料,下面我們執行專案訪問檢視效果。 介面輸出內容如下所示:

{
    "errorMsg": "使用者狀態異常.",
    "data": null
}
複製程式碼

而在控制檯由於我們編寫了日誌資訊,也同樣有對應的輸出,如下所示:

Hibernate: 
    select
        exceptioni0_.ei_id as ei_id1_0_,
        exceptioni0_.ei_code as ei_code2_0_,
        exceptioni0_.ei_message as ei_messa3_0_ 
    from
        sys_exception_info exceptioni0_ 
    where
        exceptioni0_.ei_code=? limit ?
2018-01-09 18:54:00.647 ERROR 2024 --- [nio-8080-exec-1] c.h.s.exception.core.LogicException      : 系統遇到如下異常,異常碼:USER_STATUS_FAILD>>>異常資訊:使用者狀態異常.
2018-01-09 18:54:00.649 ERROR 2024 --- [nio-8080-exec-1] c.h.s.e.c.advice.ExceptionAdvice         : 遇到業務邏輯異常:【USER_STATUS_FAILD】
複製程式碼

如果業務邏輯異常在Service層時,我們根本不需要去操心事務回滾的問題,因為LogicException本身就是執行時異常,而專案中丟擲執行時異常時事務就會自動回滾。

我們把業務邏輯異常遮蔽掉,把true改成false檢視正確時返回的格式,如下所示:

{
    "errorMsg": null,
    "data": "this is index mapping"
}
複製程式碼

如果想把對應的null改成空字串,請訪問檢視第五章:配置使用FastJson返回Json檢視

總結

本章將之前章節的部分內容進行了整合,主要是全域性異常、統一格式返回等;這種方式是目前我們公司產品中正在使用的方式,已經可以滿足平時的業務邏輯異常定義以及返回,將異常訊息存放到資料庫中我們可以隨時更新提示內容,這一點還是比較易用的。

本章原始碼已經上傳到碼雲: SpringBoot配套原始碼地址:gitee.com/hengboy/spr… SpringCloud配套原始碼地址:gitee.com/hengboy/spr… SpringBoot相關係列文章請訪問:目錄:SpringBoot學習目錄 QueryDSL相關係列文章請訪問:QueryDSL通用查詢框架學習目錄 SpringDataJPA相關係列文章請訪問:目錄:SpringDataJPA學習目錄,感謝閱讀!

微信掃碼關注 - 專注分享

歡迎加入恆宇少年的知識星球,恆宇少年帶你走以後的技術道路!!!

知識星球

相關文章