hi ,大家好,我是三天打鱼,两天晒网的小六六,欢迎大家关注我的公众号:"六脉神剑的程序人生",一起学习,一起进步
今天六脉神剑第二章又来问我问题了,还特么给我发了一堆表情,我特么直呼好家伙
aebb0a06-9124-49d5-859b-e4ecc92130cf.jpg
image.png
当然我是不可能这么回他的,毕竟是我为数不多的粉丝,所以来关注下小六六吧,我的每位粉丝都能得到我精准的呵护,所以呀,就回了它下面的话
2f3713ef-a42a-414a-8d25-038a82ca61d3.jpg
大家发现没有,这回六哥硬气了,没有秒回这位粉丝了,哈哈 好家伙!不再是舔狗了
,言归正传:下面就给大家分享分享我们之前异常到底要怎么处理,才是真正的最佳实践!
是这样的,这个案例是小六六自身经历的一个例子哈,我想大家都用过微服务吧!我先说说背景哈,我有这样的一个场景,就是我支付完成之后,我是不是要去给用户发货,但是给用户发挥之前,我需要调用一下风控的服务,如果这个用户风控判断有风险的话,我就会拦截这个发货行为,因为这个系统是做海外系统的,那我再调用风控系统之前要做这样一个事情,就是把海外的本地币种转化为USD的统一的汇率,这就设计到了汇率的服务了,然后呢有一天假设我做了一些更改,然后需要把我支付这个服务,和我调用汇率接口的服务一起升级,但是因为我不小心,导致忘记发汇率接口,然后就会出现一个问题,因为我汇率服务本身,里面是处理了异常的,就算我没有升级这个服务,你一样也可以调通这个服务,但是我会把我自己的异常转换成code,然后支付调用汇率这个服务,根据code的值成功或者失败去做逻辑,但是呢因为我支付服务,如果调用成功的话,我就把转换后的金额传给风控,如果转换返回失败,我就给一个默认值0,但是这样一写逻辑就碰到一个问题,风控那边会根据金额来做策略,它有一个策略就是拦截金额为0的订单,不发货,所以那一次升级,就是因为这个逻辑导致了有6k多单的发货没有被发货,导致了这个事故,像上面这个问题,其实我们觉得我们不应该说帮人家去做决策,如果失败的话,我们不应该说给一个默认值,而是抛异常出去才对,这才是正确的做法,就是很多时候,我们自己并不知道是给一个业务code的错误,还是抛一个Exception,像很多其他的不那么严谨的业务,可能并不说考虑的那么清楚,但是我们支付就必须一点点都得考虑的很严谨了,像这个事故,我们还有很多不足的地方,延迟发货的告警没有告,调用汇率接口失败了,也没有告警,而是把原生的错误转换了等等。所以小六六这边才觉得,很多的时候,我们自己确实是不知道如何的处理一些业务的异常,应该怎么样给其他服务返回,才能让调用你的服务的人,觉得你这个服务的设计上好的,等等,这就是我想跟大家聊的这篇文章。
不过我们还是先来了解下Java的异常体系吧!
image.png
异常是程序中的一些错误,但并不是所有的错误都是异常,并且错误有时候是可以避免的。
比如说,你的代码少了一个分号,那么运行出来结果是提示是错误java.lang.Error
;如果你用System.out.println(11/0)
,那么你是因为你用0做了除数,会抛出java.lang.ArithmeticException
的异常。
异常发生的原因有很多,通常包含以下几大类:
要理解Java异常处理是如何工作的,你需要掌握以下三种类型的异常:
Java把异常当作对象来处理,并定义一个基类java.lang.Throwable
作为所有异常的超类。
在Java API中已经定义了许多异常类,这些异常类分为两大类,错误Error
和异常Exception
。
Java异常层次结构图如下图所示:
image.png
在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的异常处理本质上是抛出异常和捕获异常。
抛出异常
:要理解抛出异常,首先要明白什么是异常情形(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;
}
}
// 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端产品的严谨性,如果仅仅是一个后台管理,确实是不必要这样的,但是对于面向用户的产品,我觉得异常处理的好坏,决定了你这个产品的系统质量!好了,我是小六六,三天打鱼,两天晒网!
扫码关注腾讯云开发者
领取腾讯云代金券
Copyright © 2013 - 2025 Tencent Cloud. All Rights Reserved. 腾讯云 版权所有
深圳市腾讯计算机系统有限公司 ICP备案/许可证号:粤B2-20090059 深公网安备号 44030502008569
腾讯云计算(北京)有限责任公司 京ICP证150476号 | 京ICP备11018762号 | 京公网安备号11010802020287
Copyright © 2013 - 2025 Tencent Cloud.
All Rights Reserved. 腾讯云 版权所有