在我們平時的項目研發過程中,異常一般都是程序員最為頭疼的問題,異常的拋出、捕獲、處理等既涉及事務回滾,還會涉及返回前端消息提醒信息。那么我們怎么設計可以解決上面的兩個的痛點呢?我們可不可以統一處理業務邏輯然后給出前端對應的異常提醒內容呢?
免費教程專題
恒宇少年在博客整理三套免費學習教程專題
,由于文章偏多
特意添加了閱讀指南
,新文章以及之前的文章都會在專題內陸續填充
,希望可以幫助大家解惑更多知識點。
本章目標
基于SpringBoot
平臺構建業務邏輯異常統一處理,異常消息內容格式化。
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>
項目創建完成后除了.idea
、iml
、pom.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);
}
}
在重寫的構造函數內需要傳遞兩個參數errCode
、params
,其目的是為了初始化類內的全局變量。
-
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
就可以直接從接口實現類實例中獲取到未格式化的異常描述!
當然實現類可以是以
Redis
、Map集合
、數據庫
、文本
作為數據來源。
獲取到未格式化的異常描述后通過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
。
異常信息實體
/**
* 系統異?;拘畔嶓w
* ========================
* 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配套源碼地址:https://gitee.com/hengboy/spring-boot-chapter
SpringCloud配套源碼地址:https://gitee.com/hengboy/spring-cloud-chapter