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

在咱們平時的項目研發過程當中,異常通常都是程序員最爲頭疼的問題,異常的拋出、捕獲、處理等既涉及事務回滾,還會涉及返回前端消息提醒信息。那麼咱們怎麼設計能夠解決上面的兩個的痛點呢?咱們可不能夠統一處理業務邏輯而後給出前端對應的異常提醒內容呢?前端

本章目標

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

福利來了

騰訊雲特惠服務器10元/月,點擊參團mysql

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添加公共使用的依賴,配置內容以下所示:git

<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並重寫構造函數,代碼以下所示:web

/**
 * 自定義業務邏輯異常類
 * ========================
 * 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,其目的是爲了初始化類內的全局變量。redis

  • errCode:該字段是對應的異常碼,咱們在後續文章內容中建立一個存放異常錯誤碼的枚舉,而errCode就是枚舉對應的字符串的值。
  • params:這裏是對應errCode字符串含義描述時所須要的參數列表。
  • errMsg:格式化後的業務邏輯異常消息描述,咱們在構造函數內能夠看到調用了ErrorMessageTools.getErrorMessage(errCode,params);,這個方法做用是經過異常碼在數據庫內獲取未格式化的異常描述,經過傳遞的參數進行格式化異常消息描述。

建立異常核心包的目的就是讓其餘模塊直接添加依賴,那異常描述內容該怎麼獲取呢?spring

定義異常消息獲取接口

咱們在springboot-exception-core模塊內添加一個接口LogicExceptionMessage,該接口提供經過異常碼獲取未格式化的異常消息描述內容方法,接口定義以下所示:sql

/**
 * 邏輯異常接口定義
 * 使用項目須要實現該接口方法並提供方法實現
 * 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學習目錄,感謝閱讀!

微信掃碼關注 - 專一分享

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

知識星球
相關文章
相關標籤/搜索