Java統(tǒng)一異常處理(配置文件集中化定義)
作者:xcbeyond
瘋狂源自夢(mèng)想,技術(shù)成就輝煌!微信公眾號(hào):《程序猿技術(shù)大咖》號(hào)主,專注后端開(kāi)發(fā)多年,擁有豐富的研發(fā)經(jīng)驗(yàn),樂(lè)于技術(shù)輸出、分享,現(xiàn)階段從事微服務(wù)架構(gòu)項(xiàng)目的研發(fā)工作,涉及架構(gòu)設(shè)計(jì)、技術(shù)選型、業(yè)務(wù)研發(fā)等工作。對(duì)于Java、微服務(wù)、數(shù)據(jù)庫(kù)、Docker有深入了解,并有大量的調(diào)優(yōu)經(jīng)驗(yàn)。
0、前言
無(wú)論任何項(xiàng)目,都避免不了在運(yùn)行期間出現(xiàn)的一些異常,并伴隨著因業(yè)務(wù)邏輯的需要而給出相應(yīng)的提示,使得系統(tǒng)變得更加友好,這類提示處理,我們統(tǒng)稱為異常處理(exceptiona handling)。
在項(xiàng)目中異常處理所拋出的異常碼、異常提示 ,都需要進(jìn)行一定的封裝,以確保異常的統(tǒng)一,提高程序的可維護(hù)性。而不是隨心所欲的來(lái)進(jìn)行異常提醒,如:一些硬編碼異常信息(throw new Exception("系統(tǒng)處理異常")),隨著想項(xiàng)目的變大、開(kāi)發(fā)人員的不同,這些異常碼可能會(huì)五花八門,沒(méi)有統(tǒng)一標(biāo)準(zhǔn),給用戶提示、給開(kāi)發(fā)很容易帶來(lái)些許的困惑。
本文不是講解如何正確使用try、catch、finally等進(jìn)行異常捕獲,而是就異常碼、異常信息進(jìn)行封裝,通過(guò)配置文件進(jìn)行集中化定義,來(lái)統(tǒng)一異常處理,讓異常處理變得更標(biāo)準(zhǔn)化、統(tǒng)一化,方便維護(hù)、管理。
1、異常處理
異常處理,又稱為錯(cuò)誤處理,提供了處理程序運(yùn)行時(shí)出現(xiàn)的任何意外或異常情況的方法。異常處理使用 try、catch 和 finally 關(guān)鍵字來(lái)嘗試可能未成功的操作,處理失敗,以及在事后清理資源。
異常發(fā)生的原因有很多,通常包含以下幾大類:
用戶輸入了非法數(shù)據(jù)。
要打開(kāi)的文件不存在。
網(wǎng)絡(luò)通信時(shí)連接中斷,或者JVM內(nèi)存溢出。
這些異常有的是因?yàn)橛脩翦e(cuò)誤引起,有的是程序錯(cuò)誤引起的。
要理解Java異常處理是如何工作的,你需要掌握以下三種類型的異常:
檢查性異常:最具代表的檢查性異常是用戶錯(cuò)誤或問(wèn)題引起的異常,這是程序員無(wú)法預(yù)見(jiàn)的。例如要打開(kāi)一個(gè)不存在文件時(shí),一個(gè)異常就發(fā)生了,這些異常在編譯時(shí)不能被簡(jiǎn)單地忽略。
運(yùn)行時(shí)異常: 運(yùn)行時(shí)異常是可能被程序員避免的異常。與檢查性異常相反,運(yùn)行時(shí)異常可以在編譯時(shí)被忽略。
錯(cuò)誤: 錯(cuò)誤不是異常,而是脫離程序員控制的問(wèn)題。錯(cuò)誤在代碼中通常被忽略。例如,當(dāng)棧溢出時(shí),一個(gè)錯(cuò)誤就發(fā)生了,它們?cè)诰幾g也檢查不到的。
所有的異常類是從 java.lang.Exception 類繼承的子類。
Exception 類是 Throwable 類的子類。除了Exception類外,Throwable還有一個(gè)子類Error 。
Java 程序通常不捕獲錯(cuò)誤。錯(cuò)誤一般發(fā)生在嚴(yán)重故障時(shí),它們?cè)贘ava程序處理的范疇之外。
Error 用來(lái)指示運(yùn)行時(shí)環(huán)境發(fā)生的錯(cuò)誤。例如,JVM 內(nèi)存溢出。一般地,程序不會(huì)從錯(cuò)誤中恢復(fù)。
異常類有兩個(gè)主要的子類:IOException 類和 RuntimeException 類。
本文就針對(duì)處理的是Exception類異常。
2、統(tǒng)一異常處理
本實(shí)戰(zhàn)中將異常碼、異常信息進(jìn)行封裝,通過(guò)properties配置文件進(jìn)行集中化定義,并支持國(guó)際化異常碼的定義,來(lái)統(tǒng)一異常處理。
2.1 消息結(jié)果的封裝
全系統(tǒng)統(tǒng)一返回的數(shù)據(jù)格式為:
{
"statusCode":"00000000",
"msg":"成功",
"data": {
"username":"xcbeyond",
"sex":"男",
"age":18
}
}
標(biāo)準(zhǔn)的json字符串,statusCode:狀態(tài)碼,msg:提示信息,data:結(jié)果數(shù)據(jù)(以實(shí)際數(shù)據(jù)而定的json)。
定義一個(gè)實(shí)體類Result,用來(lái)封裝消息返回?cái)?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對(duì)象以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配置文件中,避免硬編碼在代碼中,方便維護(hù),便于后期變動(dòng)統(tǒng)一修改。異常碼文件位于項(xiàng)目resources目錄下\resources\error\,如下:
異常碼文件名統(tǒng)一格式:模塊名_error_zh_CN.properties/模塊名_error_en_US.properties(zh_CN、en_US區(qū)分國(guó)際化定義)
異常碼統(tǒng)一格式定義,具體以實(shí)際項(xiàng)目情況而定,可參考如下標(biāo)準(zhǔn)定義:
#錯(cuò)誤碼定義8位
# ┌─1─┬─2─┬─3─┬─4─┬─5─┬─6─┬─7─┬─8─┐
# │預(yù)留 │C/B端│ 模塊名 │ 錯(cuò)誤碼 │
# └─1─┴─2─┴─3─┴─4─┴─5─┴─6─┴─7─┴─8─┘
#第1位:
# 預(yù)留
#第2位:
# C/B端(客戶端或服務(wù)端) 0-服務(wù)端, 1-客戶端
#第3、4位:
# 2位模塊名
#第5、6、7、8位:
# 4位錯(cuò)誤碼(后4位),各位含義如下:
# 第5為:類別,可按業(yè)務(wù)分類、接口分類等劃分,0-9
# 第6-8位:3位具體錯(cuò)誤碼
# 第6位:按以下含義定義分類:
# 0:預(yù)留
# 1:非空檢查類提示,數(shù)據(jù)為空、不為空檢查
# 2:有效性檢查提示,數(shù)據(jù)有效性檢查(如格式、存在、不存在、不在有效值范圍等)
# 3:業(yè)務(wù)邏輯類提示,合法性/一致性/完整性檢查提示
# 4:預(yù)留/待擴(kuò)展定義
# 5:預(yù)留/待擴(kuò)展定義
# 6:預(yù)留/待擴(kuò)展定義
# 7:預(yù)留/待擴(kuò)展定義
# 8:預(yù)留/待擴(kuò)展定義
# 9:預(yù)留/待擴(kuò)展定義
# 第7、8位:二位順序標(biāo)號(hào),00-99
封裝異常碼工具類ErrorUtils,用于從異常碼文件中獲取錯(cuò)誤提示信息等,如下:
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;
/**
* 錯(cuò)誤工具類,用于從錯(cuò)誤碼配置文件中獲取錯(cuò)誤提示信息等
* 支持國(guó)際化。
* @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)代碼塊。
* 用于加載錯(cuò)誤碼配置文件
*/
static {
try {
PathMatchingResourcePatternResolver patternResolver = new PathMatchingResourcePatternResolver();
List nameListCn = new ArrayList();
Resource[] jarResources = patternResolver.getResources(JAR_RESOURCES);
if (log.isDebugEnabled())
log.debug("加載CLASSPATH下[error]文件夾錯(cuò)誤碼配置文件[" + 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]下錯(cuò)誤碼配置文件[" + resource.getFilename() + "][" + fileName + "]");
nameListCn.add("error/" + fileName);
}
Resource[] resources = patternResolver.getResources(RESOURCES);
if (log.isDebugEnabled())
log.debug("加載CLASSPATH根目錄錯(cuò)誤碼配置文件[" + resources.length + "]");
for (Resource resource : resources) {
String fileName = resource.getFilename();
fileName = fileName.substring(0, fileName.indexOf(FILE_KEYWORKS) + 5);
if (log.isDebugEnabled())
log.debug("加載錯(cuò)誤碼配置文件[" + resource.getFilename() + "][" + fileName + "]");
nameListCn.add(fileName);
}
resourceBundle.setBasenames((String[]) nameListCn.toArray(new String[0]));
resourceBundle.setCacheSeconds(5);
} catch (Throwable localThrowable) {
}
}
/**
* 獲取錯(cuò)誤碼描述信息
* @param errCode 錯(cuò)誤碼
* @return
*/
public static String getErrorDesc(String errCode) {
return getErrorDesc(errCode, "CHINESE");
}
/**
* 獲取錯(cuò)誤碼描述信息
* @param errCode 錯(cuò)誤碼
* @param userLang 國(guó)際化語(yǔ)言
* @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;
}
/**
* 獲取錯(cuò)誤碼描述信息
* @param errCode 錯(cuò)誤碼
* @param args 錯(cuò)誤描述信息中參數(shù)
* @return
*/
public static String getParseErrorDesc(String errCode, String[] args) {
return getParseErrorDesc(errCode, ZH_LANGUAGE, args);
}
/**
* 獲取錯(cuò)誤碼描述信息
* @param errCode 錯(cuò)誤碼
* @param userLang 國(guó)際化語(yǔ)言
* @param args 錯(cuò)誤描述信息中參數(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)級(jí)異常:指系統(tǒng)級(jí)別的,如:網(wǎng)絡(luò)通信時(shí)連接中斷、系統(tǒng)連接、超時(shí)等異常
業(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)級(jí)異常SystemException,如下:
package com.xcbeyond.execption;
import com.xcbeyond.execption.data.Result;
/**
* 系統(tǒng)級(jí)異常。
* 指系統(tǒng)級(jí)別的,如:網(wǎng)絡(luò)通信時(shí)連接中斷、系統(tǒng)連接、超時(shí)等異常
* @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ù)代碼中進(jìn)行統(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 錯(cuò)誤描述信息中的參數(shù)
* @return
*/
public static BusinessException businessException(String errCode, String... args) {
return new BusinessException(createResult(errCode, args));
}
/**
* 系統(tǒng)級(jí)異常
* @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)級(jí)異常
* @param errCode 異常碼
* @param args 錯(cuò)誤描述信息中的參數(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));
}
/**
* 獲取錯(cuò)誤信息
* @param errCode 錯(cuò)誤碼
* @return
*/
private static String getErrorMsg(String errCode) {
return ErrorUtils.getErrorDesc(errCode);
}
/**
* 獲取錯(cuò)誤信息
* @param errCode 錯(cuò)誤碼
* @param args 錯(cuò)誤描述信息中的參數(shù)
* @return
*/
private static String getErrorMsg(String errCode, String[] args) {
return ErrorUtils.getParseErrorDesc(errCode, args);
}
}
2.4 全局異常捕獲
一般異常捕獲都是通過(guò)try/catch、throw new等方式進(jìn)行捕獲,而頻繁的這樣操作,有時(shí)讓人覺(jué)得麻煩,代碼變得不是那么的干凈,尤其業(yè)務(wù)復(fù)雜的場(chǎng)合。就像下面這種:
try{
..........
}catch(Exception1 e){
..........
}catch(Exception2 e){
...........
}catch(Exception3 e){
...........
}
這樣的代碼既不簡(jiǎn)潔好看 ,我們敲著也煩, 一般我們可能想到用攔截器去處理。而在spring中提供了更好的方案,注解@ControllerAdvice和@ExceptionHandler,進(jìn)行全局統(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)用
將上述定義封裝的異常,進(jìn)行實(shí)際應(yīng)用。
下述只是為了進(jìn)行異常應(yīng)用測(cè)試,并不符合實(shí)際業(yè)務(wù)場(chǎng)景。
以用戶登錄接口的service層UserServiceImpl類實(shí)現(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());
}
/**
* 測(cè)試系統(tǒng)級(jí)異常.
* 通過(guò)用戶名和密碼相同時(shí),來(lái)模擬網(wǎng)絡(luò)連接異常
*/
if (user.getPassword().equals(user.getUsername())) {
throw ExecptionUtils.systemException("999999", "網(wǎng)絡(luò)鏈接異常");
}
return new ResponseEntity(HttpStatus.OK);
}
}
此例有三類場(chǎng)合異常處理:
(1)不帶參的邏輯異常處理
throw ExecptionUtils.businessException("EE3002");
返回?cái)?shù)據(jù):
(2)帶參的邏輯異常處理
throw ExecptionUtils.businessException("EE4001", user.getUsername());
(3)系統(tǒng)級(jí)異常處理
throw ExecptionUtils.systemException("EE9999");
如果你有更好的異常統(tǒng)一處理建議,歡迎一起討論完善。
源碼:https://github.com/xcbeyond/JavaExecptionFramework