Java統(tǒng)一異常處理(配置文件集中化定義)
作者:xcbeyond
瘋狂源自夢想,技術(shù)成就輝煌!微信公眾號:《程序猿技術(shù)大咖》號主,專注后端開發(fā)多年,擁有豐富的研發(fā)經(jīng)驗,樂于技術(shù)輸出、分享,現(xiàn)階段從事微服務(wù)架構(gòu)項目的研發(fā)工作,涉及架構(gòu)設(shè)計、技術(shù)選型、業(yè)務(wù)研發(fā)等工作。對于Java、微服務(wù)、數(shù)據(jù)庫、Docker有深入了解,并有大量的調(diào)優(yōu)經(jīng)驗。
0、前言
無論任何項目,都避免不了在運行期間出現(xiàn)的一些異常,并伴隨著因業(yè)務(wù)邏輯的需要而給出相應(yīng)的提示,使得系統(tǒng)變得更加友好,這類提示處理,我們統(tǒng)稱為異常處理(exceptiona handling)。
在項目中異常處理所拋出的異常碼、異常提示 ,都需要進行一定的封裝,以確保異常的統(tǒng)一,提高程序的可維護性。而不是隨心所欲的來進行異常提醒,如:一些硬編碼異常信息(throw new Exception("系統(tǒng)處理異常")),隨著想項目的變大、開發(fā)人員的不同,這些異常碼可能會五花八門,沒有統(tǒng)一標準,給用戶提示、給開發(fā)很容易帶來些許的困惑。
本文不是講解如何正確使用try、catch、finally等進行異常捕獲,而是就異常碼、異常信息進行封裝,通過配置文件進行集中化定義,來統(tǒng)一異常處理,讓異常處理變得更標準化、統(tǒng)一化,方便維護、管理。
1、異常處理
異常處理,又稱為錯誤處理,提供了處理程序運行時出現(xiàn)的任何意外或異常情況的方法。異常處理使用 try、catch 和 finally 關(guān)鍵字來嘗試可能未成功的操作,處理失敗,以及在事后清理資源。
異常發(fā)生的原因有很多,通常包含以下幾大類:
用戶輸入了非法數(shù)據(jù)。
要打開的文件不存在。
網(wǎng)絡(luò)通信時連接中斷,或者JVM內(nèi)存溢出。
這些異常有的是因為用戶錯誤引起,有的是程序錯誤引起的。
要理解Java異常處理是如何工作的,你需要掌握以下三種類型的異常:
檢查性異常:最具代表的檢查性異常是用戶錯誤或問題引起的異常,這是程序員無法預見的。例如要打開一個不存在文件時,一個異常就發(fā)生了,這些異常在編譯時不能被簡單地忽略。
運行時異常: 運行時異常是可能被程序員避免的異常。與檢查性異常相反,運行時異??梢栽诰幾g時被忽略。
錯誤: 錯誤不是異常,而是脫離程序員控制的問題。錯誤在代碼中通常被忽略。例如,當棧溢出時,一個錯誤就發(fā)生了,它們在編譯也檢查不到的。
所有的異常類是從 java.lang.Exception 類繼承的子類。
Exception 類是 Throwable 類的子類。除了Exception類外,Throwable還有一個子類Error 。
Java 程序通常不捕獲錯誤。錯誤一般發(fā)生在嚴重故障時,它們在Java程序處理的范疇之外。
Error 用來指示運行時環(huán)境發(fā)生的錯誤。例如,JVM 內(nèi)存溢出。一般地,程序不會從錯誤中恢復。
異常類有兩個主要的子類:IOException 類和 RuntimeException 類。
本文就針對處理的是Exception類異常。
2、統(tǒng)一異常處理
本實戰(zhàn)中將異常碼、異常信息進行封裝,通過properties配置文件進行集中化定義,并支持國際化異常碼的定義,來統(tǒng)一異常處理。
2.1 消息結(jié)果的封裝
全系統(tǒng)統(tǒng)一返回的數(shù)據(jù)格式為:
{
"statusCode":"00000000",
"msg":"成功",
"data": {
"username":"xcbeyond",
"sex":"男",
"age":18
}
}
標準的json字符串,statusCode:狀態(tài)碼,msg:提示信息,data:結(jié)果數(shù)據(jù)(以實際數(shù)據(jù)而定的json)。
定義一個實體類Result,用來封裝消息返回數(shù)據(jù),如下:
package com.xcbeyond.execption.data;
import com.alibaba.fastjson.JSONObject;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.xcbeyond.execption.util.ObjectUtils;
import java.io.Serializable;
/**
* 返回結(jié)果
* @Auther: xcbeyond
* @Date: 2019/5/24 17:55
*/
@JsonInclude(value = JsonInclude.Include.NON_NULL)
public class Result implements Serializable {
//狀態(tài)碼
private String statusCode;
//提示信息
private String msg;
//結(jié)果數(shù)據(jù)
private Object data;
public Result() {
}
public Result(String statusCode, String msg) {
this.statusCode = statusCode;
this.msg = msg;
}
public String getStatusCode() {
return statusCode;
}
public void setStatusCode(String statusCode) {
this.statusCode = statusCode;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
/**
* 重寫toString方法,讓Result對象以json字符串形式存在
* @return
* Json字符串
*/
@Override
public String toString() {
JSONObject json = new JSONObject();
json.put("statusCode", this.statusCode);
json.put("msg", this.msg);
if (null != this.data) {
json.put("data", ObjectUtils.modelToMap(this.data));
}
return json.toJSONString();
}
}
2.2 異常碼、異常信息配置文件定義化 *
將異常碼、異常信息統(tǒng)一集中定義到properties配置文件中,避免硬編碼在代碼中,方便維護,便于后期變動統(tǒng)一修改。異常碼文件位于項目resources目錄下\resources\error\,如下:
異常碼文件名統(tǒng)一格式:模塊名_error_zh_CN.properties/模塊名_error_en_US.properties(zh_CN、en_US區(qū)分國際化定義)
異常碼統(tǒng)一格式定義,具體以實際項目情況而定,可參考如下標準定義:
#錯誤碼定義8位
# ┌─1─┬─2─┬─3─┬─4─┬─5─┬─6─┬─7─┬─8─┐
# │預留 │C/B端│ 模塊名 │ 錯誤碼 │
# └─1─┴─2─┴─3─┴─4─┴─5─┴─6─┴─7─┴─8─┘
#第1位:
# 預留
#第2位:
# C/B端(客戶端或服務(wù)端) 0-服務(wù)端, 1-客戶端
#第3、4位:
# 2位模塊名
#第5、6、7、8位:
# 4位錯誤碼(后4位),各位含義如下:
# 第5為:類別,可按業(yè)務(wù)分類、接口分類等劃分,0-9
# 第6-8位:3位具體錯誤碼
# 第6位:按以下含義定義分類:
# 0:預留
# 1:非空檢查類提示,數(shù)據(jù)為空、不為空檢查
# 2:有效性檢查提示,數(shù)據(jù)有效性檢查(如格式、存在、不存在、不在有效值范圍等)
# 3:業(yè)務(wù)邏輯類提示,合法性/一致性/完整性檢查提示
# 4:預留/待擴展定義
# 5:預留/待擴展定義
# 6:預留/待擴展定義
# 7:預留/待擴展定義
# 8:預留/待擴展定義
# 9:預留/待擴展定義
# 第7、8位:二位順序標號,00-99
封裝異常碼工具類ErrorUtils,用于從異常碼文件中獲取錯誤提示信息等,如下:
package com.xcbeyond.execption.util;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.NoSuchMessageException;
import org.springframework.context.support.ResourceBundleMessageSource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
/**
* 錯誤工具類,用于從錯誤碼配置文件中獲取錯誤提示信息等
* 支持國際化。
* @Auther: xcbeyond
* @Date: 2019/5/24 17:16
*/
public class ErrorUtils {
private static final Logger log = LoggerFactory.getLogger(ErrorUtils.class);
private static ResourceBundleMessageSource resourceBundle = new ResourceBundleMessageSource();
private static final String ZH_LANGUAGE = "CHINESE";
private static final String EN_LANGUAGE = "AMERICAN/ENGLISH";
private static final String FILE_KEYWORKS = "error";
private static final String JAR_RESOURCES = "classpath*:error/*error*.properties";
private static final String RESOURCES = "classpath*:*error*.properties";
/**
* 靜態(tài)代碼塊。
* 用于加載錯誤碼配置文件
*/
static {
try {
PathMatchingResourcePatternResolver patternResolver = new PathMatchingResourcePatternResolver();
List nameListCn = new ArrayList();
Resource[] jarResources = patternResolver.getResources(JAR_RESOURCES);
if (log.isDebugEnabled())
log.debug("加載CLASSPATH下[error]文件夾錯誤碼配置文件[" + jarResources.length + "]");
for (Resource resource : jarResources) {
String fileName = resource.getFilename();
fileName = fileName.substring(0, fileName.indexOf(FILE_KEYWORKS) + 5);
if (log.isDebugEnabled())
log.debug("加載[error]下錯誤碼配置文件[" + resource.getFilename() + "][" + fileName + "]");
nameListCn.add("error/" + fileName);
}
Resource[] resources = patternResolver.getResources(RESOURCES);
if (log.isDebugEnabled())
log.debug("加載CLASSPATH根目錄錯誤碼配置文件[" + resources.length + "]");
for (Resource resource : resources) {
String fileName = resource.getFilename();
fileName = fileName.substring(0, fileName.indexOf(FILE_KEYWORKS) + 5);
if (log.isDebugEnabled())
log.debug("加載錯誤碼配置文件[" + resource.getFilename() + "][" + fileName + "]");
nameListCn.add(fileName);
}
resourceBundle.setBasenames((String[]) nameListCn.toArray(new String[0]));
resourceBundle.setCacheSeconds(5);
} catch (Throwable localThrowable) {
}
}
/**
* 獲取錯誤碼描述信息
* @param errCode 錯誤碼
* @return
*/
public static String getErrorDesc(String errCode) {
return getErrorDesc(errCode, "CHINESE");
}
/**
* 獲取錯誤碼描述信息
* @param errCode 錯誤碼
* @param userLang 國際化語言
* @return
*/
public static String getErrorDesc(String errCode, String userLang) {
String errDesc = "";
try {
if ((null == userLang) || (ZH_LANGUAGE.equals(userLang))) {
errDesc = resourceBundle.getMessage(errCode, null, Locale.SIMPLIFIED_CHINESE);
} else if (EN_LANGUAGE.equals(userLang)) {
errDesc = resourceBundle.getMessage(errCode, null, Locale.US);
}
} catch (NoSuchMessageException localNoSuchMessageException) {
}
return errDesc;
}
/**
* 獲取錯誤碼描述信息
* @param errCode 錯誤碼
* @param args 錯誤描述信息中參數(shù)
* @return
*/
public static String getParseErrorDesc(String errCode, String[] args) {
return getParseErrorDesc(errCode, ZH_LANGUAGE, args);
}
/**
* 獲取錯誤碼描述信息
* @param errCode 錯誤碼
* @param userLang 國際化語言
* @param args 錯誤描述信息中參數(shù)
* @return
*/
public static String getParseErrorDesc(String errCode, String userLang, String[] args) {
String errDesc = "";
try {
if ((null == userLang) || (ZH_LANGUAGE.equals(userLang)))
errDesc = resourceBundle.getMessage(errCode, args, Locale.SIMPLIFIED_CHINESE);
else if (EN_LANGUAGE.equals(userLang))
errDesc = resourceBundle.getMessage(errCode, args, Locale.US);
} catch (NoSuchMessageException localNoSuchMessageException) {
}
return errDesc;
}
}
2.3 異常類封裝
本文封裝兩類異常:
系統(tǒng)級異常:指系統(tǒng)級別的,如:網(wǎng)絡(luò)通信時連接中斷、系統(tǒng)連接、超時等異常
業(yè)務(wù)處理異常:指用戶輸入了非法數(shù)據(jù)等業(yè)務(wù)邏輯存在的異常
(其他類別異常,可自行封裝,如SQL類異常)
(1)異?;怋aseException,所有異常類都繼承此類,如下:
package com.xcbeyond.execption;
import java.io.Serializable;
/**
* 異?;?br style="box-sizing: border-box; outline: 0px; overflow-wrap: break-word;"> * @Auther: xcbeyond
* @Date: 2019/5/28 16:27
*/
public class BaseException extends RuntimeException implements Serializable {
public BaseException() {
}
public BaseException(String message) {
super(message);
}
public BaseException(Throwable cause) {
super(cause);
}
public BaseException(String message, Throwable cause) {
super(message, cause);
}
}
(2)系統(tǒng)級異常SystemException,如下:
package com.xcbeyond.execption;
import com.xcbeyond.execption.data.Result;
/**
* 系統(tǒng)級異常。
* 指系統(tǒng)級別的,如:網(wǎng)絡(luò)通信時連接中斷、系統(tǒng)連接、超時等異常
* @Auther: xcbeyond
* @Date: 2019/5/28 16:26
*/
public class SystemException extends BaseException{
private Result result = new Result();
public SystemException(Result result) {
super(result.getStatusCode()+ ":" + result.getMsg());
this.result = result;
}
public SystemException(String code, String msg) {
super(code + ":" + msg);
this.result.setStatusCode(code);
this.result.setMsg(msg);
}
public SystemException(Result result, Throwable cause) {
super(result.getStatusCode() + ":" + result.getMsg(), cause);
this.result = result;
}
public SystemException(String code, String msg, Throwable cause) {
super(code + ":" + msg, cause);
this.result.setStatusCode(code);
this.result.setMsg(msg);
}
public Result getResult() {
return result;
}
public void setResult(Result result) {
this.result = result;
}
}
(3)業(yè)務(wù)處理異常類BusinessException,如下:
package com.xcbeyond.execption;
import com.xcbeyond.execption.data.Result;
/**
* 業(yè)務(wù)處理異常
* 指用戶輸入了非法數(shù)據(jù)等業(yè)務(wù)邏輯存在的異常
* @Auther: xcbeyond
* @Date: 2018/12/24 11:20
*/
public class BusinessException extends BaseException {
private Result result = new Result();
public BusinessException(Result result) {
super(result.getStatusCode()+ ":" + result.getMsg());
this.result = result;
}
public BusinessException(String code, String msg) {
super(code + ":" + msg);
this.result.setStatusCode(code);
this.result.setMsg(msg);
}
public BusinessException(Result result, Throwable cause) {
super(result.getStatusCode() + ":" + result.getMsg(), cause);
this.result = result;
}
public BusinessException(String code, String msg, Throwable cause) {
super(code + ":" + msg, cause);
this.result.setStatusCode(code);
this.result.setMsg(msg);
}
public Result getResult() {
return result;
}
public void setResult(Result result) {
this.result = result;
}
}
(4)異常工具類ExecptionUtils
為方便在業(yè)務(wù)代碼中進行統(tǒng)一異常調(diào)用,特封裝異常工具類ExecptionUtils,如下:
package com.xcbeyond.execption.util;
import com.xcbeyond.execption.BusinessException;
import com.xcbeyond.execption.SystemException;
import com.xcbeyond.execption.data.Result;
/**
* 異常工具類
* @Auther: xcbeyond
* @Date: 2019/5/27 09:37
*/
public class ExecptionUtils {
/**
* 業(yè)務(wù)處理異常
* @param errCode 異常碼
* @return
*/
public static BusinessException businessException(String errCode) {
return new BusinessException(createResult(errCode));
}
/**
* 業(yè)務(wù)處理異常
* @param errCode 自定義異常碼
* @param msg 自定義異常提示
* @return
*/
public static BusinessException businessException(String errCode, String msg) {
return new BusinessException(createResult(errCode, msg));
}
/**
* 業(yè)務(wù)處理異常
* @param errCode 異常碼
* @param args 錯誤描述信息中的參數(shù)
* @return
*/
public static BusinessException businessException(String errCode, String... args) {
return new BusinessException(createResult(errCode, args));
}
/**
* 系統(tǒng)級異常
* @param errCode 異常碼
* @return
*/
public static SystemException systemException(String errCode) {
return new SystemException(createResult(errCode));
}
/**
* 業(yè)務(wù)處理異常
* @param errCode 自定義異常碼
* @param msg 自定義異常提示
* @return
*/
public static SystemException systemException(String errCode, String msg) {
return new SystemException(createResult(errCode, msg));
}
/**
* 系統(tǒng)級異常
* @param errCode 異常碼
* @param args 錯誤描述信息中的參數(shù)
* @return
*/
public static SystemException systemException(String errCode, String... args) {
return new SystemException(createResult(errCode, args));
}
private static Result createResult(String errCode) {
return new Result(errCode, getErrorMsg(errCode));
}
private static Result createResult(String errCode, String msg) {
return new Result(errCode, msg);
}
private static Result createResult(String errCode, String[] args) {
return new Result(errCode, getErrorMsg(errCode, args));
}
/**
* 獲取錯誤信息
* @param errCode 錯誤碼
* @return
*/
private static String getErrorMsg(String errCode) {
return ErrorUtils.getErrorDesc(errCode);
}
/**
* 獲取錯誤信息
* @param errCode 錯誤碼
* @param args 錯誤描述信息中的參數(shù)
* @return
*/
private static String getErrorMsg(String errCode, String[] args) {
return ErrorUtils.getParseErrorDesc(errCode, args);
}
}
2.4 全局異常捕獲
一般異常捕獲都是通過try/catch、throw new等方式進行捕獲,而頻繁的這樣操作,有時讓人覺得麻煩,代碼變得不是那么的干凈,尤其業(yè)務(wù)復雜的場合。就像下面這種:
try{
..........
}catch(Exception1 e){
..........
}catch(Exception2 e){
...........
}catch(Exception3 e){
...........
}
這樣的代碼既不簡潔好看 ,我們敲著也煩, 一般我們可能想到用攔截器去處理。而在spring中提供了更好的方案,注解@ControllerAdvice和@ExceptionHandler,進行全局統(tǒng)一異常處理。
本文定義全局異常捕獲類GlobalExceptionHandler,如下:
package com.xcbeyond.execption.handler;
import com.xcbeyond.execption.BusinessException;
import com.xcbeyond.execption.SystemException;
import com.xcbeyond.execption.data.Result;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
/**
* 全局異常捕獲處理
* @Auther: xcbeyond
* @Date: 2019/5/28 15:19
*/
@ControllerAdvice
public class GlobalExceptionHandler {
/**
* 業(yè)務(wù)邏輯異常。
* HTTP響應(yīng)狀態(tài)為200
* @param businessException
* @return
*/
@ExceptionHandler(value = BusinessException.class)
public ResponseEntity businessExceptionHandler(BusinessException businessException) {
Result result = businessException.getResult();
return new ResponseEntity(result, HttpStatus.OK);
}
/**
* 系統(tǒng)異常。
* HTTP響應(yīng)狀態(tài)為400
* @param systemException
* @return
*/
@ExceptionHandler(value = SystemException.class)
public ResponseEntity systemExceptionHandler(SystemException systemException) {
Result result = systemException.getResult();
return new ResponseEntity(result, HttpStatus.BAD_REQUEST);
}
}
2.5 應(yīng)用
將上述定義封裝的異常,進行實際應(yīng)用。
下述只是為了進行異常應(yīng)用測試,并不符合實際業(yè)務(wù)場景。
以用戶登錄接口的service層UserServiceImpl類實現(xiàn)講解,代碼如下:
package com.xcbeyond.execption.service.impl;
import com.xcbeyond.execption.model.User;
import com.xcbeyond.execption.service.UserService;
import com.xcbeyond.execption.util.ExecptionUtils;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
/**
* @Auther: xcbeyond
* @Date: 2019/5/28 17:04
*/
@Service
public class UserServiceImpl implements UserService {
public ResponseEntity login(User user) {
if (StringUtils.isEmpty(user.getUsername())) {
throw ExecptionUtils.businessException("EE3001");
}
if (StringUtils.isEmpty(user.getPassword())) {
throw ExecptionUtils.businessException("EE3002");
}
if (!"xcbeyond".equals(user.getUsername())) {
throw ExecptionUtils.businessException("EE4001", user.getUsername());
}
/**
* 測試系統(tǒng)級異常.
* 通過用戶名和密碼相同時,來模擬網(wǎng)絡(luò)連接異常
*/
if (user.getPassword().equals(user.getUsername())) {
throw ExecptionUtils.systemException("999999", "網(wǎng)絡(luò)鏈接異常");
}
return new ResponseEntity(HttpStatus.OK);
}
}
此例有三類場合異常處理:
(1)不帶參的邏輯異常處理
throw ExecptionUtils.businessException("EE3002");
返回數(shù)據(jù):
(2)帶參的邏輯異常處理
throw ExecptionUtils.businessException("EE4001", user.getUsername());
(3)系統(tǒng)級異常處理
throw ExecptionUtils.systemException("EE9999");
如果你有更好的異常統(tǒng)一處理建議,歡迎一起討論完善。
源碼:https://github.com/xcbeyond/JavaExecptionFramework