Java 统一异常处理 (配置文件集中化定义)
0、前言
无论任何项目,都避免不了在运行期间出现的一些异常,并伴随着因业务逻辑的需要而给出相应的提示,使得系统变得更加友好,这类提示处理,我们统称为异常处理(exceptiona handling)。
在项目中异常处理所抛出的异常码、异常提示 ,都需要进行一定的封装,以确保异常的统一,提高程序的可维护性。而不是随心所欲的来进行异常提醒,如:一些硬编码异常信息(throw new Exception("系统处理异常")),随着想项目的变大、开发人员的不同,这些异常码可能会五花八门,没有统一标准,给用户提示、给开发很容易带来些许的困惑。
本文不是讲解如何正确使用try、catch、finally等进行异常捕获,而是就异常码、异常信息进行封装,通过配置文件进行集中化定义,来统一异常处理,让异常处理变得更标准化、统一化,方便维护、管理。
1、异常处理
异常处理,又称为错误处理,提供了处理程序运行时出现的任何意外或异常情况的方法。异常处理使用 try、catch 和 finally 关键字来尝试可能未成功的操作,处理失败,以及在事后清理资源。
异常发生的原因有很多,通常包含以下几大类:
- 用户输入了非法数据。
- 要打开的文件不存在。
- 网络通信时连接中断,或者JVM内存溢出。
这些异常有的是因为用户错误引起,有的是程序错误引起的。
要理解Java异常处理是如何工作的,你需要掌握以下三种类型的异常:
- 检查性异常:最具代表的检查性异常是用户错误或问题引起的异常,这是程序员无法预见的。例如要打开一个不存在文件时,一个异常就发生了,这些异常在编译时不能被简单地忽略。
- 运行时异常: 运行时异常是可能被程序员避免的异常。与检查性异常相反,运行时异常可以在编译时被忽略。
- 错误: 错误不是异常,而是脱离程序员控制的问题。错误在代码中通常被忽略。例如,当栈溢出时,一个错误就发生了,它们在编译也检查不到的。
所有的异常类是从 java.lang.Exception 类继承的子类。
Exception 类是 Throwable 类的子类。除了Exception类外,Throwable还有一个子类Error 。
Java 程序通常不捕获错误。错误一般发生在严重故障时,它们在Java程序处理的范畴之外。
Error 用来指示运行时环境发生的错误。例如,JVM 内存溢出。一般地,程序不会从错误中恢复。
异常类有两个主要的子类:IOException 类和 RuntimeException 类。
本文就针对处理的是Exception类异常。
2、统一异常处理
本实战中将异常码、异常信息进行封装,通过properties配置文件进行集中化定义,并支持国际化异常码的定义,来统一异常处理。
2.1 消息结果的封装
全系统统一返回的数据格式为:
{
"statusCode":"00000000",
"msg":"成功",
"data": {
"username":"xcbeyond",
"sex":"男",
"age":18
}
}
标准的json字符串,statusCode:状态码,msg:提示信息,data:结果数据(以实际数据而定的json)。
定义一个实体类Result,用来封装消息返回数据,如下:
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;
/**
* 返回结果
* @Auther: xcbeyond
* @Date: 2019/5/24 17:55
*/
@JsonInclude(value = JsonInclude.Include.NON_NULL)
public class Result implements Serializable {
//状态码
private String statusCode;
//提示信息
private String msg;
//结果数据
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 异常码、异常信息配置文件定义化
将异常码、异常信息统一集中定义到properties配置文件中,避免硬编码在代码中,方便维护,便于后期变动统一修改。异常码文件位于项目resources目录下\resources\error\,如:
异常码文件名统一格式:模块名_error_zh_CN.properties/
异常码统一格式定义,具体以实际项目情况而定,可参考如下标准定义:
#错误码定义8位
# ┌─1─┬─2─┬─3─┬─4─┬─5─┬─6─┬─7─┬─8─┐
# │预留 │C/B端│ 模块名 │错误码 │
# └─1─┴─2─┴─3─┴─4─┴─5─┴─6─┴─7─┴─8─┘
#第1位:
# 预留
#第2位:
# C/B端(客户端或服务端) 0-服务端, 1-客户端
#第3、4位:
# 2位模块名
#第5、6、7、8位:
# 4位错误码(后4位),各位含义如下:
# 第5为:类别,可按业务分类、接口分类等划分,0-9
# 第6-8位:3位具体错误码
# 第6位:按以下含义定义分类:
# 0:预留
# 1:非空检查类提示,数据为空、不为空检查
# 2:有效性检查提示,数据有效性检查(如格式、存在、不存在、不在有效值范围等)
# 3:业务逻辑类提示,合法性/一致性/完整性检查提示
# 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";
/**
* 静态代码块。
* 用于加载错误码配置文件
*/
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 错误描述信息中参数
* @return
*/
public static String getParseErrorDesc(String errCode, String[] args) {
return getParseErrorDesc(errCode, ZH_LANGUAGE, args);
}
/**
* 获取错误码描述信息
* @param errCode 错误码
* @param userLang 国际化语言
* @param args 错误描述信息中参数
* @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 异常类封装
本文封装两类异常:
- 系统级异常:指系统级别的,如:网络通信时连接中断、系统连接、超时等异常
- 业务处理异常:指用户输入了非法数据等业务逻辑存在的异常
(其他类别异常,可自行封装,如SQL类异常)
(1)异常基类BaseException,所有异常类都继承此类,
package com.xcbeyond.execption;
import java.io.Serializable;
/**
* 异常基类
* @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)系统级异常SystemException,如下:
package com.xcbeyond.execption;
import com.xcbeyond.execption.data.Result;
/**
* 系统级异常。
* 指系统级别的,如:网络通信时连接中断、系统连接、超时等异常
* @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)业务处理异常类BusinessException,如下:
package com.xcbeyond.execption;
import com.xcbeyond.execption.data.Result;
/**
* 业务处理异常
* 指用户输入了非法数据等业务逻辑存在的异常
* @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
为方便在业务代码中进行统一异常调用,特封装异常工具类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 {
/**
* 业务处理异常
* @param errCode 异常码
* @return
*/
public static BusinessException businessException(String errCode) {
return new BusinessException(createResult(errCode));
}
/**
* 业务处理异常
* @param errCode 异常码
* @param args 错误描述信息中的参数
* @return
*/
public static BusinessException businessException(String errCode, String... args) {
return new BusinessException(createResult(errCode, args));
}
/**
* 系统级异常
* @param errCode 异常码
* @return
*/
public static SystemException systemException(String errCode) {
return new SystemException(createResult(errCode));
}
/**
* 系统级异常
* @param errCode 异常码
* @param args 错误描述信息中的参数
* @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 错误描述信息中的参数
* @return
*/
private static String getErrorMsg(String errCode, String[] args) {
return ErrorUtils.getParseErrorDesc(errCode, args);
}
}
2.4 全局异常捕获
一般异常捕获都是通过try/catch、throw new等方式进行捕获,而频繁的这样操作,有时让人觉得麻烦,代码变得不是那么的干净,尤其业务复杂的场合。就像下面这种:
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 {
/**
* 业务处理异常
* @param errCode 异常码
* @return
*/
public static BusinessException businessException(String errCode) {
return new BusinessException(createResult(errCode));
}
/**
* 业务处理异常
* @param errCode 异常码
* @param args 错误描述信息中的参数
* @return
*/
public static BusinessException businessException(String errCode, String... args) {
return new BusinessException(createResult(errCode, args));
}
/**
* 系统级异常
* @param errCode 异常码
* @return
*/
public static SystemException systemException(String errCode) {
return new SystemException(createResult(errCode));
}
/**
* 系统级异常
* @param errCode 异常码
* @param args 错误描述信息中的参数
* @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 错误描述信息中的参数
* @return
*/
private static String getErrorMsg(String errCode, String[] args) {
return ErrorUtils.getParseErrorDesc(errCode, args);
}
}
这样的代码既不简洁好看 ,我们敲着也烦, 一般我们可能想到用拦截器去处理。而在spring中提供了更好的方案,注解@ControllerAdvice和@ExceptionHandler,进行全局统一异常处理。
本文定义全局异常捕获类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 {
/**
* 业务逻辑异常。
* HTTP响应状态为200
* @param businessException
* @return
*/
@ExceptionHandler(value = BusinessException.class)
public ResponseEntity businessExceptionHandler(BusinessException businessException) {
Result result = businessException.getResult();
return new ResponseEntity(result, HttpStatus.OK);
}
/**
* 系统异常。
* HTTP响应状态为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 应用
将上述定义封装的异常,进行实际应用。
下述只是为了进行异常应用测试,并不符合实际业务场景。
以用户登录接口的service层UserServiceImpl类实现讲解,代码如下:
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());
}
/**
* 测试系统级异常.
* 通过用户名和密码相同时,来模拟网络连接异常
*/
if (user.getPassword().equals(user.getUsername())) {
throw ExecptionUtils.systemException("EE9999");
}
return new ResponseEntity(HttpStatus.OK);
}
}
此例有三类场合异常处理:
(1)不带参的逻辑异常处理
throw ExecptionUtils.businessException("EE3002");
返回数据:
(2)带参的逻辑异常处理
throw ExecptionUtils.businessException("EE4001", user.getUsername());
返回数据:
(3)系统级异常处理
throw ExecptionUtils.systemException("EE9999");
返回数据:
如果你有更好的异常统一处理建议,欢迎一起讨论完善。
本文为 InfoQ 作者【xcbeyond】的原创文章。