写了这么久的业务连异常都不知道怎么处理吗

简介: 前言文本已收录至我的GitHub仓库,欢迎Star:github.com/bin39232820…种一棵树最好的时间是十年前,其次是现在

今天六脉神剑第二章又来问我问题了,还特么给我发了一堆表情,我特么直呼好家伙


网络异常,图片无法展示
|


当然我是不可能这么回他的,毕竟是我为数不多的粉丝,所以来关注下小六六吧,我的每位粉丝都能得到我精准的呵护,所以呀,就回了它下面的话

网络异常,图片无法展示
|


大家发现没有,这回六哥硬气了,没有秒回这位粉丝了,哈哈 好家伙! 不再是舔狗了,言归正传:下面就给大家分享分享我们之前异常到底要怎么处理,才是真正的最佳实践!


一个案例


是这样的,这个案例是小六六自身经历的一个例子哈,我想大家都用过微服务吧!我先说说背景哈,我有这样的一个场景,就是我支付完成之后,我是不是要去给用户发货,但是给用户发挥之前,我需要调用一下风控的服务,如果这个用户风控判断有风险的话,我就会拦截这个发货行为,因为这个系统是做海外系统的,那我再调用风控系统之前要做这样一个事情,就是把海外的本地币种转化为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的类是非运行时异常。

  • ErrorError类对象由 Java 虚拟机生成并抛出,大多数错误与代码编写者所执行的操作无关。例如,Java虚拟机运行错误(Virtual MachineError),当JVM不再有继续执行操作所需的内存资源时,将出现 OutOfMemoryError。这些异常发生时,Java虚拟机(JVM)一般会选择线程终止;还有发生在虚拟机试图执行应用时,如类定义错误(NoClassDefFoundError)、链接错误(LinkageError)。这些错误是不可查的,因为它们在应用程序的控制和处理能力之 外,而且绝大多数是程序运行时不允许出现的状况。对于设计合理的应用程序来说,即使确实发生了错误,本质上也不应该试图去处理它所引起的异常状况。在Java中,错误通常是使用Error的子类描述。
  • Exception:在Exception分支中有一个重要的子类RuntimeException(运行时异常),该类型的异常自动为你所编写的程序定义ArrayIndexOutOfBoundsException(数组下标越界)、NullPointerException(空指针异常)、ArithmeticException(算术异常)、MissingResourceException(丢失资源)、ClassNotFoundException(找不到类)等异常,这些异常是不检查异常,程序中可以选择捕获处理,也可以不处理。这些异常一般是由程序逻辑错误引起的,程序应该从逻辑角度尽可能避免这类异常的发生;而RuntimeException之外的异常我们统称为非运行时异常,类型上属于Exception类及其子类,从程序语法角度讲是必须进行处理的异常,如果不处理,程序就不能编译通过。如IOExceptionSQLException等以及用户自定义的Exception异常,一般情况下不自定义检查异常。


Java 异常的处理机制


Java的异常处理本质上是抛出异常捕获异常

  • 抛出异常:要理解抛出异常,首先要明白什么是异常情形(exception condition),它是指阻止当前方法或作用域继续执行的问题。其次把异常情形和普通问题相区分,普通问题是指在当前环境下能得到足够的信息,总能处理这个错误。对于异常情形,已经无法继续下去了,因为在当前环境下无法获得必要的信息来解决问题,你所能做的就是从当前环境中跳出,并把问题提交给上一级环境,这就是抛出异常时所发生的事情。抛出异常后,会有几件事随之发生。首先,是像创建普通的java对象一样将使用new在堆上创建一个异常对象;然后,当前的执行路径(已经无法继续下去了)被终止,并且从当前环境中弹出对异常对象的引用。此时,异常处理机制接管程序,并开始寻找一个恰当的地方继续执行程序,这个恰当的地方就是异常处理程序或者异常处理器,它的任务是将程序从错误状态中恢复,以使程序要么换一种方式运行,要么继续运行下去。

举个简单的例子,假使我们创建了一个学生对象Student的一个引用stu,在调用的时候可能还没有初始化。所以在使用这个对象引用调用其他方法之前,要先对它进行检查,可以创建一个代表错误信息的对象,并且将它从当前环境中抛出,这样就把错误信息传播到更大的环境中。

if(stu == null){
    throw new NullPointerException();
}
复制代码


  • 捕获异常:在方法抛出异常之后,运行时系统将转为寻找合适的异常处理器(exception handler)。潜在的异常处理器是异常发生时依次存留在调用栈中的方法的集合。当异常处理器所能处理的异常类型与方法抛出的异常类型相符时,即为合适的异常处理器。运行时系统从发生异常的方法开始,依次回查调用栈中的方法,直至找到含有合适异常处理器的方法并执行。当运行时系统遍历调用栈而未找到合适的异常处理器,则运行时系统终止。同时,意味着Java程序的终止。

Java异常处理涉及到五个关键字,分别是:trycatchfinallythrowthrows。下面将骤一介绍,通过认识这五个关键字,掌握基本异常处理知识。

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端产品的严谨性,如果仅仅是一个后台管理,确实是不必要这样的,但是对于面向用户的产品,我觉得异常处理的好坏,决定了你这个产品的系统质量!好了,我是小六六,三天打鱼,两天晒网!


相关文章
|
7月前
|
SQL 存储 监控
实用技巧:排查数据异常/数据波动问题,该如何下手?
在我做开发的这些年,让我很头痛的一类问题,不是线上故障,而是数据异常,不知道有没有程序员跟我感同身受。大多数的服务故障都有较为直观的异常日志,再结合产品表象,相对排查起来还有迹可循,但数据异常的原因就太多了,很多时候连报错日志都没有,排查起来简直无从下手。
实用技巧:排查数据异常/数据波动问题,该如何下手?
|
7月前
|
测试技术
线上问题,如何处理?
线上问题,如何处理?
180 37
|
Java 编译器 程序员
【JavaSE专栏67】谈谈异常的那些事,学会预判异常、捕获异常、转移异常
【JavaSE专栏67】谈谈异常的那些事,学会预判异常、捕获异常、转移异常
101 0
【JavaSE专栏67】谈谈异常的那些事,学会预判异常、捕获异常、转移异常
|
27天前
|
测试技术 开发工具 git
写了BUG还想跑——闲鱼异常日志问题自动追踪-定位-分发机制
为了高效地发现、定位和解决预发问题,闲鱼团队研发了一套异常日志问题自动追踪-定位-分发机制。这套机制通过自动化手段,实现了异常日志的定时扫描、精准定位和自动分发,显著降低了开发和测试的成本,提高了问题解决的效率。
写了BUG还想跑——闲鱼异常日志问题自动追踪-定位-分发机制
|
17天前
|
存储 缓存 Java
程序员血泪史:上线出错后,我做了这三件事儿...
小米,29岁程序员,分享了系统上线遇到的两个问题及其解决方法:一是限售规则错误导致非配置地区也能购买,通过改进匹配逻辑和细化地区限制解决;二是商品详情页信息被误清空,采用深拷贝对象避免直接影响JPA缓存。总结了代码精确匹配、谨慎处理持久化对象及重视用户反馈的重要性。
33 6
|
前端开发 Cloud Native 大数据
坑爹,线上同步近 3w 个用户导致链路阻塞引入发的线上问题,你经历过吗?
坑爹,线上同步近 3w 个用户导致链路阻塞引入发的线上问题,你经历过吗?
|
Dubbo Java 应用服务中间件
浅谈踩坑记之一个Java线程池参数,差点引起线上事故(下)
浅谈踩坑记之一个Java线程池参数,差点引起线上事故
300 0
|
Dubbo Java 应用服务中间件
浅谈踩坑记之一个Java线程池参数,差点引起线上事故(上)
浅谈踩坑记之一个Java线程池参数,差点引起线上事故
172 0
|
缓存 前端开发 Java
支付宝二面:使用 try-catch 捕获异常会影响性能吗?大部分人都会答错!
支付宝二面:使用 try-catch 捕获异常会影响性能吗?大部分人都会答错!
159 0
支付宝二面:使用 try-catch 捕获异常会影响性能吗?大部分人都会答错!