今天六脉神剑第二章又来问我问题了,还特么给我发了一堆表情,我特么直呼好家伙
当然我是不可能这么回他的,毕竟是我为数不多的粉丝,所以来关注下小六六吧,我的每位粉丝都能得到我精准的呵护,所以呀,就回了它下面的话
大家发现没有,这回六哥硬气了,没有秒回这位粉丝了,哈哈 好家伙! 不再是舔狗了
,言归正传:下面就给大家分享分享我们之前异常到底要怎么处理,才是真正的最佳实践!
一个案例
是这样的,这个案例是小六六自身经历的一个例子哈,我想大家都用过微服务吧!我先说说背景哈,我有这样的一个场景,就是我支付完成之后,我是不是要去给用户发货,但是给用户发挥之前,我需要调用一下风控的服务,如果这个用户风控判断有风险的话,我就会拦截这个发货行为,因为这个系统是做海外系统的,那我再调用风控系统之前要做这样一个事情,就是把海外的本地币种转化为USD的统一的汇率,这就设计到了汇率的服务了,然后呢有一天假设我做了一些更改,然后需要把我支付这个服务,和我调用汇率接口的服务一起升级,但是因为我不小心,导致忘记发汇率接口,然后就会出现一个问题,因为我汇率服务本身,里面是处理了异常的,就算我没有升级这个服务,你一样也可以调通这个服务,但是我会把我自己的异常转换成code,然后支付调用汇率这个服务,根据code的值成功或者失败去做逻辑,但是呢因为我支付服务,如果调用成功的话,我就把转换后的金额传给风控,如果转换返回失败,我就给一个默认值0,但是这样一写逻辑就碰到一个问题,风控那边会根据金额来做策略,它有一个策略就是拦截金额为0的订单,不发货,所以那一次升级,就是因为这个逻辑导致了有6k多单的发货没有被发货,导致了这个事故,像上面这个问题,其实我们觉得我们不应该说帮人家去做决策,如果失败的话,我们不应该说给一个默认值,而是抛异常出去才对,这才是正确的做法,就是很多时候,我们自己并不知道是给一个业务code的错误,还是抛一个Exception,像很多其他的不那么严谨的业务,可能并不说考虑的那么清楚,但是我们支付就必须一点点都得考虑的很严谨了,像这个事故,我们还有很多不足的地方,延迟发货的告警没有告,调用汇率接口失败了,也没有告警,而是把原生的错误转换了等等。所以小六六这边才觉得,很多的时候,我们自己确实是不知道如何的处理一些业务的异常,应该怎么样给其他服务返回,才能让调用你的服务的人,觉得你这个服务的设计上好的,等等,这就是我想跟大家聊的这篇文章。
不过我们还是先来了解下Java的异常体系吧!
什么是异常
异常是程序中的一些错误,但并不是所有的错误都是异常,并且错误有时候是可以避免的。
比如说,你的代码少了一个分号,那么运行出来结果是提示是错误java.lang.Error
;如果你用System.out.println(11/0)
,那么你是因为你用0做了除数,会抛出java.lang.ArithmeticException
的异常。
异常发生的原因有很多,通常包含以下几大类:
- 用户输入了非法数据。
- 要打开的文件不存在。
- 网络通信时连接中断,或者JVM内存溢出。
要理解Java异常处理是如何工作的,你需要掌握以下三种类型的异常:
- 检查性异常: 最具代表的检查性异常是用户错误或问题引起的异常,这是程序员无法预见的。例如要打开一个不存在文件时,一个异常就发生了,这些异常在编译时不能被简单地忽略。
- 运行时异常: 运行时异常是可能被程序员避免的异常。与检查性异常相反,运行时异常可以在编译时被忽略。
- 错误: 错误不是异常,而是脱离程序员控制的问题。错误在代码中通常被忽略。例如,当栈溢出时,一个错误就发生了,它们在编译也检查不到的。
Java异常的体系结构
Java把异常当作对象来处理,并定义一个基类java.lang.Throwable
作为所有异常的超类。
在Java API中已经定义了许多异常类,这些异常类分为两大类,错误Error
和异常Exception
。
Java异常层次结构图如下图所示:
在Java中,所有异常类的父类是Throwable类,Error类是error类型异常的父类,Exception类是exception类型异常的父类,RuntimeException类是所有运行时异常的父类,RuntimeException以外的并且继承Exception的类是非运行时异常。
Error
:Error
类对象由 Java 虚拟机生成并抛出,大多数错误与代码编写者所执行的操作无关。例如,Java虚拟机运行错误(Virtual MachineError
),当JVM不再有继续执行操作所需的内存资源时,将出现OutOfMemoryError
。这些异常发生时,Java虚拟机(JVM)一般会选择线程终止;还有发生在虚拟机试图执行应用时,如类定义错误(NoClassDefFoundError
)、链接错误(LinkageError
)。这些错误是不可查的,因为它们在应用程序的控制和处理能力之 外,而且绝大多数是程序运行时不允许出现的状况。对于设计合理的应用程序来说,即使确实发生了错误,本质上也不应该试图去处理它所引起的异常状况。在Java中,错误通常是使用Error
的子类描述。Exception
:在Exception
分支中有一个重要的子类RuntimeException
(运行时异常),该类型的异常自动为你所编写的程序定义ArrayIndexOutOfBoundsException
(数组下标越界)、NullPointerException
(空指针异常)、ArithmeticException
(算术异常)、MissingResourceException
(丢失资源)、ClassNotFoundException
(找不到类)等异常,这些异常是不检查异常,程序中可以选择捕获处理,也可以不处理。这些异常一般是由程序逻辑错误引起的,程序应该从逻辑角度尽可能避免这类异常的发生;而RuntimeException
之外的异常我们统称为非运行时异常,类型上属于Exception
类及其子类,从程序语法角度讲是必须进行处理的异常,如果不处理,程序就不能编译通过。如IOException
、SQLException
等以及用户自定义的Exception
异常,一般情况下不自定义检查异常。
Java 异常的处理机制
Java的异常处理本质上是抛出异常和捕获异常。
抛出异常
:要理解抛出异常,首先要明白什么是异常情形(exception condition),它是指阻止当前方法或作用域继续执行的问题。其次把异常情形和普通问题相区分,普通问题是指在当前环境下能得到足够的信息,总能处理这个错误。对于异常情形,已经无法继续下去了,因为在当前环境下无法获得必要的信息来解决问题,你所能做的就是从当前环境中跳出,并把问题提交给上一级环境,这就是抛出异常时所发生的事情。抛出异常后,会有几件事随之发生。首先,是像创建普通的java对象一样将使用new
在堆上创建一个异常对象;然后,当前的执行路径(已经无法继续下去了)被终止,并且从当前环境中弹出对异常对象的引用。此时,异常处理机制接管程序,并开始寻找一个恰当的地方继续执行程序,这个恰当的地方就是异常处理程序或者异常处理器,它的任务是将程序从错误状态中恢复,以使程序要么换一种方式运行,要么继续运行下去。
举个简单的例子,假使我们创建了一个学生对象Student的一个引用stu,在调用的时候可能还没有初始化。所以在使用这个对象引用调用其他方法之前,要先对它进行检查,可以创建一个代表错误信息的对象,并且将它从当前环境中抛出,这样就把错误信息传播到更大的环境中。
if(stu == null){ throw new NullPointerException(); } 复制代码
捕获异常
:在方法抛出异常之后,运行时系统将转为寻找合适的异常处理器(exception handler)。潜在的异常处理器是异常发生时依次存留在调用栈中的方法的集合。当异常处理器所能处理的异常类型与方法抛出的异常类型相符时,即为合适的异常处理器。运行时系统从发生异常的方法开始,依次回查调用栈中的方法,直至找到含有合适异常处理器的方法并执行。当运行时系统遍历调用栈而未找到合适的异常处理器,则运行时系统终止。同时,意味着Java程序的终止。
Java异常处理涉及到五个关键字,分别是:try
、catch
、finally
、throw
、throws
。下面将骤一介绍,通过认识这五个关键字,掌握基本异常处理知识。
• try -- 用于监听。将要被监听的代码(可能抛出异常的代码)放在try语句块之内,当try语句块内发生异常时,异常就被抛出。
• catch -- 用于捕获异常。catch用来捕获try语句块中发生的异常。
• finally -- finally语句块总是会被执行。它主要用于回收在try块里打开的物力资源(如数据库连接、网络连接和磁盘文件)。只有finally块,执行完成之后,才会回来执行try或者catch块中的return或者throw语句,如果finally中使用了return或者throw等终止方法的语句,则就不会跳回执行,直接停止。
• throw -- 用于抛出异常。
• throws -- 用在方法签名中,用于声明该方法可能抛出的异常。
项目中到底要怎么去处理异常呢
小六六这边分2种情况来说说,一种就是我们一般的后台管理系统,一种是类似于支付系统的C端项目,再我的感觉中,它们对异常处理的细粒度是不一样的。
一般的后台管理系统的方式
一个异常枚举类,所有异常码定义在这里:
public enum BizExceptionEnum { APPLICATION_ERROR(1000, "网络繁忙,请稍后再试"), INVALID_USER(1001, "用户名或密码错误"), INVALID_REQ_PARAM(1002, "参数错误"), EXAM_NOT_FOUND(1003, "未查到考试信息"), ; BizExceptionEnum(Integer errorCode, String errorMsg) { this.errorCode = errorCode; this.errorMsg = errorMsg; } private final Integer errorCode; private final String errorMsg; // get...... } 复制代码
一个业务异常类:
public class BizException extends RuntimeException { private final BizExceptionEnum bizExceptionEnum; public BizException(BizExceptionEnum bizExceptionEnum) { super(bizExceptionEnum.getErrorMsg()); this.bizExceptionEnum = bizExceptionEnum; } public BizExceptionEnum getBizExceptionEnum() { return bizExceptionEnum; } } 复制代码
一个全局的异常处理类:
@RestControllerAdvice public class GlobalHandler { private final Logger logger = LoggerFactory.getLogger(GlobalHandler.class); @ExceptionHandler(MethodArgumentNotValidException.class) public Result exceptionHandler(MethodArgumentNotValidException e) { Result result = new Result(BizExceptionEnum.INVALID_REQ_PARAM.getErrorCode(), BizExceptionEnum.INVALID_REQ_PARAM.getErrorMsg()); logger.error("req params error", e); return result; } @ExceptionHandler(BizException.class) public Result exceptionHandler(BizException e) { BizExceptionEnum exceptionEnum = e.getBizExceptionEnum(); Result result = new Result(exceptionEnum.getErrorCode(), exceptionEnum.getErrorMsg()); logger.error("business error", e); return result; } @ExceptionHandler(value = Exception.class) public Result exceptionHandler(Exception e) { Result result = new Result(BizExceptionEnum.APPLICATION_ERROR.getErrorCode(), BizExceptionEnum.APPLICATION_ERROR.getErrorMsg()); logger.error("application error", e); return result; } } 复制代码
其中Result类的定义:
public class Result<T> { private Boolean success; private Integer errorCode; private String errorMsg; private T data; public Result(T data) { this(true, null, null, data); } public Result(Integer errorCode, String errorMsg) { this(false, errorCode, errorMsg, null); } public Result(Boolean success, Integer errorCode, String errorMsg, T data) { this.success = success; this.errorCode = errorCode; this.errorMsg = errorMsg; this.data = data; } // get set...... } 复制代码
示例Controller类:
@RestController @RequestMapping("/json/exam") public class ExamController { @Autowired private IExamService examService; @PostMapping("/getExamList") public Result<List<GetExamListResVo>> getExamList(@Validated @RequestBody GetExamListReqVo reqVo, @AuthenticationPrincipal UserDetails userDetails) throws IOException { List<GetExamListResVo> resVos = examService.getExamList(reqVo, userDetails); Result<List<GetExamListResVo>> result = new Result(resVos); return result; } } 复制代码
其中Result类的定义:
public class Result<T> { private Boolean success; private Integer errorCode; private String errorMsg; private T data; public Result(T data) { this(true, null, null, data); } public Result(Integer errorCode, String errorMsg) { this(false, errorCode, errorMsg, null); } public Result(Boolean success, Integer errorCode, String errorMsg, T data) { this.success = success; this.errorCode = errorCode; this.errorMsg = errorMsg; this.data = data; } // get set...... } 复制代码
示例Controller类:
@RestController @RequestMapping("/json/exam") public class ExamController { @Autowired private IExamService examService; @PostMapping("/getExamList") public Result<List<GetExamListResVo>> getExamList(@Validated @RequestBody GetExamListReqVo reqVo, @AuthenticationPrincipal UserDetails userDetails) throws IOException { List<GetExamListResVo> resVos = examService.getExamList(reqVo, userDetails); Result<List<GetExamListResVo>> result = new Result(resVos); return result; } } 复制代码
示例Service类:
@Service public class IExamServiceImpl implements IExamService { @Autowire private ManualMicrowebsiteMapper microwebsiteMapper; @Override public List<GetExamListResVo> getExamList(GetExamListReqVo reqVo, UserDetails userDetails) throws IOException { List<MicrowebsiteExam> examEntities = microwebsiteMapper.select(reqVo.getExamType(), userDetails.getUsername()); // 按照业务的定义要求,此处考试列表必须不为空,一旦为空,则说明后台配置有误或其它未知原因,这种情况视为一种业务异常 if (examEntities.isEmpty()) { // 未查到考试信息,抛出相应的业务异常 throw new BizException(BizExceptionEnum.EXAM_NOT_FOUND); } // 此处代码还有其它各类异常抛出...... List<GetExamListResVo> resVos = examEntities.stream().map(examEntity -> { GetExamListResVo resVo = new GetExamListResVo(); BeanUtils.copyProperties(examEntity, resVo); return resVo; }).collect(toList()); return resVos; } } 复制代码
C端项目的例子
- 其实,C端项目大体和上面说一致的,但是我们一般都是微服务进行开发,那么我们应该一开始就给每个服务的业务异常码返回一个范围,这样就能从请求的源头就能知道错误的点在哪个系统,这是第一个点吧
- 第二个,其实对于每个微服务,和上面的异常处理上一样的,但是我想说的是对于上面处理的Service,我们应该对里面的业务异常更加细腻的去处理,因为我们只是抛出了一些我们能预判到的一些业务异常,但是一些比如JSON转换异常等等异常,我们要到最外层去处理,但是最外层也只是把这个异常转换成大异常了,这样就是说对于C端项目来说,这样异常的力度,应该不是不够的,我们应该再细分一下,就是尽可能的把一些可能的异常转换成我们业务异常,这样的话,我们代码的健壮性就会好很多,而且很多的异常展示给用户的文案也是可以统一转换的。我们来看下面一个Service解绑的业务的例子吧!
// 1根据传入条件判断该派安盈用户存在,状态是normal ThirdAcct thirdAcct = null; try { thirdAcct = thirdAcctRelationService.getNormalThirdAcct(chId, userType, thirdUserId, payMethod, chAccountId); } catch (DataAccessException e) { throw new UIDataAccessException(e.getCode(), e.getMessage(), "fail.thirdAcct.queryfail",e); } //1.1不存在说明已经删除过了 if (ObjectUtil.isNull(thirdAcct)) { return Boolean.TRUE; } //1.2存在,证明等待删除 // 2.获取账号的渠道用于调取派安盈接口 ChAccount chAccount = null; try { chAccount = chAccountRelationService.getChAccount(appId, chId, payMethod, chAccountId); } catch (DataAccessException e) { //di装bi throw new UIDataAccessException(e.getCode(), e.getMessage(), "fail.chAccount.miss",e); } if (ObjectUtil.isNull(chAccount)) { throw new UIDataAccessException("fail.chAccount.miss", ",获取渠道账号信息失败", "fail.chAccount.miss"); } ChannelRelationService channelService = this.getChannelRelationService(chId); if (channelService == null) { //没有相关渠道 throw new UIDataAccessException("fail.channel.unfound", "找不到相关渠道的service,chId:"+chId, "fail.channel.unfo``` //解绑 try { channelService.unbind(chAccount,thirdAcct); } catch (DataAccessException e) { throw new UIDataAccessException(e.getCode(), e.getMessage(), "fail.thirdChannel.bindFail",e); } //4.解绑,注意这些多个联合组件的,要根据条件来解绑的, try { thirdAcctRelationService.unbindThirdAcct(thirdAcct); } catch (DataAccessException e) { throw new UIDataAccessException(e.getCode(), e.getMessage(), "fail.dbthirdacct.bindFail",e); } 复制代码
我们可以看到,一个业务可能拆分多个子业务,那么每个子业务都有可能抛不同的异常,你要再不同的子业务中把它们的业务转换成ui的异常,只要你的系统够完善,那么意外的异常就会非常少,这样下去你的系统会越来越稳定的。
结束
好了,今天小六六的分享就到这了,可能很多小伙伴看了会觉得没啥东西,那是因为你没有体验过一个C端产品的严谨性,如果仅仅是一个后台管理,确实是不必要这样的,但是对于面向用户的产品,我觉得异常处理的好坏,决定了你这个产品的系统质量!好了,我是小六六,三天打鱼,两天晒网!