异常简介
在项目中,异常是我们必须要考虑的。在java中异常分为3种:
- check exception
- runtime exception
- error
在决定使用check exception 或者 runtimeExceptin 主要的原则是:
- 如果期望调用者能够适当地恢复,则使用check exception, 抛出异常,强迫调用者catch中处理该异常,或者传播出去。
-
用runtime exception 来表明编程错误。
总的来说,对于可恢复的异常使用check exception ,对于程序错误,则使用runtime exception。
项目中真实情况
然而在真实项目中,异常很被误用。有如下问题:
- 不知道改什么时候用check exception 和 runtime exception
- 不知道何时catch异常,代码充斥着try catch结构。异常处理混乱
- catch住异常不知道如何处理,经常抓住打印一下堆栈信息就不管了。
最佳实践
- 不要忽略异常,如果要忽略请写明注释说明为什么忽略该异常。
- 只针对异常的情况,才使用异常
- 避免不必要的使用check exception,对编程的错误使用runtime exception
- 抛出与抽象相对应的异常
- 再细节消息中包含能捕获失败的信息。
- 努力使失败保持原子性
解决方案
为了解决以上问题,
- 自定义异常类型
- 定义统一返回值
- 统一处理
自定义异常
public abstract class AbstractSaasException extends RuntimeException {
private final int errorCode;
private final String errorMsg;
public AbstractSaasException(int errorCode, String errorMsg) {
super(errorMsg);
this.errorCode = errorCode;
this.errorMsg = errorMsg;
}
public int getErrorCode() {
return errorCode;
}
public String getErrorMsg() {
return errorMsg;
}
}
具体实现
/**
* 业务异常
* Created by chenyao on 2018/5/21.
*/
public class BizException extends AbstractSaasException {
public BizException(int errorCode, String errorMsg) {
super(errorCode, errorMsg);
}
}
errorCode, errorMsg,可以定义一个枚举,对常用异常信息做个定义,如下所示:
public enum ResultEnums {
SUCCESS(true, 1, "success"),
SYSTEM_EXCEPTION(false, -999, "系统异常, 请稍后重试"),
ILLEGAL_ARGUMENT(false, -998, "非法参数"),
;
private final boolean success;
private final int code;
private final String message;
ResultEnums(boolean success, int code, String message) {
this.success = success;
this.code = code;
this.message = message;
}
public boolean isSuccess() {
return success;
}
public int getCode() {
return code;
}
public String getMessage() {
return message;
}
}
定义好了异常,那个我们在开发中,如果遇到业务异常情况,可以直接跑出异常,由上层统一处理(后面会详细说明)。例如:
// 需要把异常情况,展示到前端,只需,抛对应的异常,由公共异常处理Advice处理,封装成Result
if(demoReqVO == null || StringUtils.isBlank(demoReqVO.getFlag())) {
throw new BizException(ResultEnums.ILLEGAL_ARGUMENT.getCode(), ResultEnums.ILLEGAL_ARGUMENT.getMessage());
}
定义返回值
一般架构中,返回前端视图一般为json ,或者jsp等视图
那对json格式数据,我们可以统一定义个返回值,方便后面公共异常处理程序封装返回值。例如:
/**
* 对外统一返回值
* Created by chenyao on 2018/5/16.
*/
public class Result<T> {
private final boolean success;
private final int code;
private final String message;
private final T data;
private Result(boolean success, int code, String message, T data) {
this.success = success;
this.code = code;
this.message = message;
this.data = data;
}
public static <T> Result<T> success() {
return new Result<>(true, 0, "success", null);
}
public static <T> Result<T> success(T data) {
return new Result<>(true, 0, "success", data);
}
public static <T> Result<T> fail(int code, String message) {
return new Result<>(false, code, message, null);
}
public int getCode() {
return code;
}
public String getMessage() {
return message;
}
public boolean isSuccess() {
return success;
}
public T getData() {
return data;
}
@Override
public String toString() {
return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE);
}
}
那在我们返回前端那层就可以统一返回Result, exp:
/**
* 返回值为json形式
* 1. 请求参数,以bean方式封装
* 2. 返回值,同一返回Result
* 3. 不关心的异常,无需自己try catch处理异常,统一抛,由Advice处理
* 4. 禁止自己操作Response,返回jsp or json data; json统一使用@ResponseBody,jsp统一返回ModelAndView
* 5. 文件下载推荐使用ResponseEntity。
* @param demoReqVO 请求参数,如果字段很多,使用vo接收;若不多,直接使用参数列表接收。不推荐使用request.getParameter方式
* @return json统一返回Result,方便公共异常处理,封装
*/
@RequestMapping(value = "/resultJson")
@ResponseBody
public Result<DemoResVO> resultJson(@Valid DemoReqVO demoReqVO) {
//controller 只做参数校验,调用service, 数据转换。不做主要业务逻辑。
//1.一般情况,不关心的异常,可以try catch, 由公共的异常处理Advice处理
//2. 需要把异常情况,展示到前端,只需,抛对应的异常,由公共异常处理Advice处理,封装成Result
if(demoReqVO == null || StringUtils.isBlank(demoReqVO.getFlag())) {
throw new BizException(ResultEnums.ILLEGAL_ARGUMENT.getCode(), ResultEnums.ILLEGAL_ARGUMENT.getMessage());
}
//其他业务操作
demoService.update();
//json统一返回Result,方便公共异常处理,封装
return Result.success(new DemoResVO());
}
异常统一处理
在spring mvc架构中,我们可以使用ControllerAdvice
, 不使用spring mvc框架的项目中(微服务中的dubbo接口,网关api)可以使用spring aop实现。
- ControllerAdvice
/**
* 统一异常处理
*/
@ControllerAdvice
public class CommonControllerAdvice {
private static final Logger logger = LoggerFactory.getLogger(CommonControllerAdvice.class);
private static final Map<Class, Object> DEFAULT_VALUE_OF_PRIMITIVE = new HashMap<Class, Object>() {{
put(int.class, 0);
put(double.class, 0.0D);
put(long.class, 0L);
put(char.class, '\u0000');
put(float.class, 0.0F);
put(short.class, 0);
put(byte.class, 0);
put(boolean.class, false);
put(void.class, null);
}};
@ExceptionHandler(value = Exception.class)
public ModelAndView handler(HttpServletRequest request,
HttpServletResponse response, HandlerMethod handler, Exception ex) {
Method method = handler.getMethod();
//1.打印error日志,包括入参,异常信息
logger.error("[commonControllerAdvice,method:{}, params:{}] 异常", method.getName(), request.getQueryString(), ex);
//2. 处理返回值
//2.1 json (Result, Collection, 基本类型)
//2.2 jsp view
ErrorMessage message = build(ex);
Class<?> returnType = method.getReturnType();
if (isRest(handler)) {
MappingJackson2JsonView jsonView = new MappingJackson2JsonView();
//The effect of setting this flag is similar to using MappingJackson2HttpMessageConverter with an @ResponseBody request-handling method.
//使 @ResponseBody效果一样
jsonView.setExtractValueFromSingleKeyModel(true);
ModelAndView view = new ModelAndView(jsonView);
Object returnObj = null;
if (returnType == Result.class) {
returnObj = Result.fail(message.getCode(), message.getMessage());
} else {
returnObj = create(returnType);
}
view.addObject("_result", returnObj);
return view;
}
//jsp
ModelAndView jspView = new ModelAndView("error");
jspView.addObject("exception", ex.getMessage());
return jspView;
}
private boolean isRest(HandlerMethod handler) {
Method method = handler.getMethod();
ResponseBody annotation = method.getAnnotation(ResponseBody.class);
if(annotation != null) {
return true;
}
Class<?> clazz = handler.getBeanType();
RestController restController = clazz.getAnnotation(RestController.class);
if(restController != null) {
return true;
}
return false;
}
/**
* 创建返回值
*
* @param clazz
* @return
*/
private Object create(Class clazz) {
Object instance;
try {
if (clazz == null) {
return null;
}
if (clazz.isInterface()) {
return interfaceInstance(clazz);
}
if (clazz.isPrimitive()) {
//对基本类型处理
return primitiveInstance(clazz);
}
if (clazz.isArray()) {
return new Object[0];
}
instance = clazz.newInstance();
} catch (InstantiationException | IllegalAccessException e) {
//ignore exception, 对未知类型,或者无默认构造函数,统一返回null
return null;
}
return instance;
}
private Object interfaceInstance(Class clazz) {
if (clazz == List.class || clazz == Collection.class) {
return Collections.emptyList();
}
if (clazz == Map.class) {
return Collections.emptyMap();
}
return null;
}
private Object primitiveInstance(Class clazz) {
return DEFAULT_VALUE_OF_PRIMITIVE.get(clazz);
}
private ErrorMessage build(Exception ex) {
if (ex instanceof AbstractSaasException) {
AbstractSaasException e = (AbstractSaasException) ex;
return new ErrorMessage(e.getErrorCode(), e.getMessage());
}
if(ex instanceof BindException) {
BindException bindException = (BindException) ex;
String message = bindException.getBindingResult().getAllErrors()
.stream()
.findFirst()
.map(DefaultMessageSourceResolvable::getDefaultMessage)
.orElse(ResultEnums.SYSTEM_EXCEPTION.getMessage());
return new ErrorMessage(ResultEnums.ILLEGAL_ARGUMENT.getCode(), message);
}
return new ErrorMessage(ResultEnums.SYSTEM_EXCEPTION.getCode(), ResultEnums.SYSTEM_EXCEPTION.getMessage());
}
private static class ErrorMessage {
private final int code;
private final String message;
ErrorMessage(int code, String message) {
this.code = code;
this.message = message;
}
public int getCode() {
return code;
}
public String getMessage() {
return message;
}
}
}
- aop
@Aspect
@Component
public class ExceptionHandleAspect {
@Around(value = "execution(public * com.pajk.pentos.service.*.*Impl.*(..)) && !@within(com.pajk.pentos.service.aop.IgnoreExceptionHandle) && !@annotation(com.pajk.pentos.service.aop.IgnoreExceptionHandle)")
@Order(100) //数字越小优先级越高
public Object handleException(ProceedingJoinPoint joinPoint) {
try {
return joinPoint.proceed();
} catch (Throwable e) {
ExceptionResultHandler instance = ExceptionResultHandler.createInstance(joinPoint, e);
return instance.handleAndReturned();
}
}
}
具体ExceptionResultHandler实现和上面例子差不多。不在赘述
参考资料
- efftive java 第二版
- spring 官方文档