在笔者从业的多年时间内,参与设计了不少系统。从知足业务须要的角度出发,能快速支撑业务发展都能称之为「好」的系统。毕竟,创造价值的是业务,若是没有业务驱动,工程师掌握屠龙之技没有龙也是至关苦闷之事。作为一名卓越的程序员,相信你们都但愿本身开发的系统易维护,更健壮。 固然这只是一种理想主义。且不说互联网行业瞬息万变,单是工期紧,面向deadline
编程就须要使不少工程师放弃对代码维护性的执念。就笔者我的经历而言,早上拿到需求文档晚上上线也是常有的事。尽管如此,我仍是但愿从我的角度来谈谈,一些简单易行、顺手培养的习惯到底能给程序维护性带来怎样的便利。 本文是从异常的使用着手,聊一聊使用不当带来的坏味道。git
在Java
中存在RuntimeException
和Exception
。典型的如NullPointerException
就属于RuntimeException
,这些异常不须要开发者捕获在运行时一旦触发自动抛出。同时,RuntimeException
也被称为非受检异常,见名知意,在编译期不受检查。除了RuntimeException
之外都称为受检异常,在编译期须要强制处理,抛出或捕获,常见的如ClassNotFoundException
,InterruptedException
。程序员
笔者见过不少这样的代码:github
PayOrder selectByOrderId(Long orderId) throw Exception;
复制代码
出于对网络链接/数据库的怀疑,总感受本身写的SQL
会抛出异常,而且想让上游去处理这个异常。美其名曰,面向防护编程,但其实给程序维护增长了不少的烦恼。 首先,由于这种异常的抛出很泛泛,调用方并不知道抛出这个异常的人当时的想法,只能在外层强制捕获这个异常。若是上层调用者也不想处理这个异常,他又会继续往上层抛,这样处理几回后,最外层的程序块则彻底不知道这个异常是什么状况下抛出的了。 可能不少小伙伴会说,个人编程风格就是别人有异常本身能处理就处理,不能处理才往上层抛。那我想说,你真棒。可是,一个项目中并非只有一个开发者,每一个人都有本身的编程习惯。有的小伙伴就是喜欢把异常往外层抛,继续为难上层调用者。 固然,这里并非说抛异常很差。合理的使用异常能使程序的结构更清晰,语义更明确。也方便高层调用者针对不一样的异常进行处理,而不是只能无奈的捕获Exception
。web
接着上文,若是有人从很最底层一路抛出了一个受检异常。在这样的系统中,想一想你们会怎么作呢?很容易,捕获就行。可是,若是下层抛出的是一个非受检异常
也就是RuntimeException
呢?很差意思,一旦团队中有人这么作,在没有全局异常处理的状况下
,有经验的开发者会选择在外层捕获,而缺少一些经验或者不熟悉系统的开发者天然不会捕获这个异常。那这种错误就会抛到容器中,这又是什么意思呢?假设你返回的是一个 JSON
格式,若是抛到容器中返回的内容就是程序的错误信息。其余调用方就没法解析这个返回值,这种状况确定是不能出现的。数据库
今后之后,你可能会看见全部的外层程序都有着丑陋的try catch
块,不管调用的程序是否会抛出异常。编程
在Spring
容器中,你们通常使用声明式事务来管理数据库事务。在@Transaction
的使用过程当中,必须指定对应的异常类型。笔者遇到不少项目中,回滚的异常是RuntimeException
。问及缘由回答是Exception
不会回滚,这实际上是配置不正确致使的。上面已经分析过了使用RuntimeException
的坏处,那咱们如今来讲说Exception
。这种强制须要捕获的异常配合自定义异常有很强的语义,便于高层灵活选择处理,通常在工程中应用比较普遍。可是若是每种异常都定义一个新的类,这样又显得很啰嗦。一种常见的实践是,经过枚举值配合异常来作业务的判断。以下:网络
public enum ResultCodeEnum {
/** * 成功 */ SUCCESS("SUCCESS", "ok"), /** * 操做失败 */ FAIL("FAIL", "操做失败"), /** * 系统错误 */ ERROR("ERROR", "系统繁忙,请稍后再试。"), /** * 验签失败 */ VERIFY_FAILED("VERIFY_FAILED", "验签失败"), /** * 缺乏参数 */ LACK_PARAM("LACK_PARAM", "缺乏参数"), ; @Getter private String code; @Getter private String msg; private ResultCodeEnum(String code, String msg) { this.code = code; this.msg = msg; } 复制代码
异常中配合枚举参数:编辑器
public class BusinessException extends Exception {
private static final long serialVersionUID = -121219158129626814L; @Getter private ResultCodeEnum resultCode; @Getter private String msg; public BusinessException() { } public BusinessException(ResultCodeEnum rsCode) { super(rsCode.getCode() + ":" + rsCode.getMsg()); this.resultCode = rsCode; this.msg = rsCode.getMsg(); } public BusinessException(ResultCodeEnum rsCode, String message) { super(rsCode.getCode() + ":" + message); this.resultCode = rsCode; this.msg = message; } public BusinessException(ResultCodeEnum rsCode, Throwable cause) { super(rsCode.getCode() + ":" + rsCode.getMsg(), cause); this.resultCode = rsCode; this.msg = rsCode.getMsg(); } public BusinessException(ResultCodeEnum rsCode, String message, Throwable cause) { super(rsCode.getCode() + ":" + message, cause); this.resultCode = rsCode; this.msg = message; } } 复制代码
如此,在须要抛出异常的地方使用便可。学习
PayTypeEnum payTypeEnum = PayTypeEnum.toEumByName(payRequestDTO.getPayType());
if (payTypeEnum == null) { throw new BusinessException(ResultCodeEnum.INVALID_PAY_TYPE); } 复制代码
这样外层必须捕获这个异常,能够根据 ResultCodeEnum
的值来区分业务进行相应的处理。this
通常而言,一个服务提供给外界的出参建议是统一的。可使用payload
的模式将返回的结果包装起来,你可能没明白,看下下面这个类:
public class ResultMessageVO<T> {
public static final String SUCCESS = "success"; public static final String ERROR = "error"; private String status; //状态 private String message; //消息 private T data; //返回的数据 ... } 复制代码
这样,若是若是使用了切面或者其它全局异常的处理机制,就很容易规范返回。 以一个验证参数的切面举例:
@Aspect
@Component @Slf4j public class ValidationAspect { @Around("execution(* io.github.pleuvoir.gateway..*.*(..))") public Object around(ProceedingJoinPoint point) throws Throwable { MethodSignature methodSignature = (MethodSignature) point.getSignature(); Method method = methodSignature.getMethod(); Object[] args = point.getArgs(); Parameter[] parameters = method.getParameters(); for (int i = 0; i < parameters.length; i++) { if (parameters[i].isAnnotationPresent(Valid.class)) { Object val = args[i]; ValidationResult validationResult = HibernateValidatorUtils.validateEntity(val); if (validationResult.isHasErrors()) { return ResultMessageVO.fail(ResultCodeEnum.PARAM_ERROR, validationResult.getErrorMessageOneway()); } } } return point.proceed(); } } 复制代码
由于咱们对外返回的都是 ResultMessageVO
因此能够在切面中作统一处理,不然每一个方法都须要单独作参数校验,这就是统一返回值的好处。
固然了,若是有统一兜底异常的地方,也由于这个统一返回值的存在,更好处理:
@Slf4j
@ControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(Exception.class) @ResponseBody public ResultMessageVO<?> exception(HttpServletRequest request, Exception e){ if (e instanceof NoHandlerFoundException) { log.error("页面不存在:{}", e.getMessage()); return new ResultMessageVO(ResultCodeEnum.ERROR, "页面不存在"); } else if (e instanceof BindException) { log.error("参数格式错误:{} url:{}", e.getMessage(), request.getRequestURI()); return new ResultMessageVO(ResultCodeEnum.INVALID_ARGUMENTS, "参数格式错误"); } else if (e instanceof HttpRequestMethodNotSupportedException){ log.error("不支持的请求方式:{} url:{}", e.getMessage(), request.getRequestURI()); return new ResultMessageVO(ResultCodeEnum.ERROR, "不支持的请求方式"); } else if (e instanceof BusinessException) { BusinessException exception = (BusinessException) e; log.warn("业务异常:{} url: {}", exception.getMsg(), request.getRequestURI()); return new ResultMessageVO<>(exception.getResultCodeEnum(), exception.getMsg()); } else { log.error("系统异常:{} \t\r\n url: {} \t\r\n header: {} \t\r\n params: {} \t\r\n body: {}", e.getMessage(), request.getRequestURI(), RequestUtil.getHeaders(request), RequestUtil.getParameterMap(request), getBody(request), e); return new ResultMessageVO<>(ResultCodeEnum.ERROR); } } private String getBody(HttpServletRequest request) { String body = StringUtils.EMPTY; try { body = RequestUtil.getBody(request); } catch (IOException e) { log.error("打印系统异常日志时,读取请求body失败,url:{}", request.getRequestURI(), e); } return body; } } 复制代码
经过以上两步的组合拳,咱们成功使用受检异常配合枚举完成了异常的合理使用,再配合全局异常处理,完成了最后的兜底。这样,程序中只须要捕获 BusinessException
便可。成功的消除了恶心的代码片断。
本文都是本人经验的一些总结,不免疏漏甚至是错误,若是有不合理、不足,还望指正。另外,但愿你们聊聊本身在项目中是如何使用异常的,互相学习。