Java项目中使用异常优雅的处理业务.

前言

在初次接触java时, 对于某个业务或者方法有可能返回正确或错误结果时, 最初我是定义结果封装对象来表示业务处理成功与否. 例如:

public ResultBean<User> getByName(String name){
	User dbUser = userDao.get(Method.where(User::getName, C.EQ, name));
	if(null == dbUser){
		return ResultBean.error("用户名不存在");
	}
	return ResultBean.success(dbUser);
}

后来随着对Java语言的深入理解逐渐放弃了这种方式. 这种方式普遍存在C系列中, 这是面向过程的代码.
我认为之所以会通过手动判断结果状态是因为语言本身表达不健全的原因. 而Java本身表达足够健全, 并且拥有完全可控的异常机制, 因此我改向通过异常来控制一些业务流程. 大致如下:

public User getByName(String name){
	User dbUser = userDao.get(Method.where(User::getName, C.EQ, name));
	if(null == dbUser){
		throw new ValidationException("用户名不存在")
	}
	return dbUser;
}

为何放弃 return status方式

  1. 不利于阅读: 当业务处理过程中出现不符合预期的正确结果时, 没必要进行后续的处理, 此处应该中断处理操作. 而手动返回控制状态时, 在一个稍微复杂一点的业务方法中, 可能存在多处return关键字. 并且开发者必须结合上下文来判断每个返回的状态对应哪种情况. 不利于阅读. 业务复杂是不利于扩展, 产生bug的几率偏高.
  2. 与事务产生冲突: 当一个方法处理到一般时, 且事务已经开始时, 直接return某个自定义的错误状态会导致事务不回滚, 因为本质上这个方法是执行成功的, 但业务是失败的. 计算机可不会关心你的业务, 它只关心成功或失败. 因此必须在return fail之前手动回滚事务. 但这除了增加代码复杂性外, 无法解决分布式事务问题. 随着分布式系统越来越常态化, 这个问题会显得尤其严重.
  3. 耦合严重: 不利于业务抽离, 一些优秀的架构难以实现. 举个例子, 当用户提交某个表单时, 需要对这个表单的数据进行验证, 数据验证通过后才会进行业务处理. 而这些验证的过程是应该抽离出来的. 它不应该侵入业务. 比如spring validator注解. 那么问题来了, 如果是前者return不同状态时则无法对该部分进行抽离, 它的表单校验必须和业务混合在进行处理. 而该业务或方法的调用方势必会增加额外的代码量(除了处理正常结果还要考虑异常结果). 想象一下, service方法中返回1, 2, 3, 4几种状态, 此时controller必须要对这几种状态进行二次判断, 改动service时会影响controller. 相互耦合, 这完全违背了MVC思想.

随之而来的问题

使用异常来控制业务流程显然是更优的解决方案, 也是在Java中最正统的解决方案, 但众所周知异常会给系统带来额外的性能开销. 很巧合的我和公司的一位架构在这个问题上产生了分歧.

架构主张使用 return status 的方式来控制流程. 因为这样更方便.
我则更倾向于throw exception的方式控制流程. 因为这样更方便.

这是一个分布式的项目, 最终拿出性能问题使我我暂时妥协了, 讨论后的结果就是其中一个模块使用return status的方式控制业务. 其他的模块保持不变, 依然采用throw exception的方式控制业务. 当然这个特殊的模块成为了异类, 微服务之间新型通信时, 都需要进行额外的处理来保证这个模块的正常调用. 无形之中增加了很多的开发负担. 但这不是重点, 重点是异常的确会带来性能损耗.

为何会损耗性能

当一个业务中出现异常时, jvm会追中当前栈信息, 因为有栈信息我们可以清楚的知道在代码某个模块某个类某个方法的第几行发生了什么样的异常, 可以快速的定位并解决问题. 但正是这个栈追踪导致创建异常的时候性能开销远大于return一个错误码的开销. 这也是我最终妥协的原因.

如何提高性能

妥协归妥协但不可否认善用异常的确是一个非常好的开发方式, 它唯一的缺点就是性能问题, 因此只要解决性能问题, 那么这就完美了. 因为当时项目紧张, 暂时没有纠结这个问题. 最近有时间便翻阅了一下相关的资料, 发现远远比我想象个更简单.
通常在一些业务场景中, 我们手动抛出的异常是已知异常, 属于可预测的异常, 这类异常也是出现频率最多的. 我们没必要因为用户提交的表单中输错一项内容就追踪一次栈信息. 而java中是可以指定异常是否追踪栈的. (这一点我前期是不知道的, 也没有重点关注这方面, 再加上网络上这方面的资料也比较少, 所以一小时前我还不知道可以这么玩)
因此我们可以创建一个FlowException 这个异常用来控制业务流程, 可以不用追踪栈信息:

package com.aiyi.core.exception;

/**
 * @Author: 郭胜凯
 * @Date: 2020/7/21 10:01
 * @Email [email protected]
 * @Description: 流程控制异常类, 通常情况下不记录栈信息, 因此不会对性能产生损耗
 */
public class FlowException extends RuntimeException {
    /**
     * 指定一个是否追踪栈信息的异常
     * @param msg
     *      异常消息
     * @param recordStackTrace
     *      是否追踪栈信息
     */
    public FlowException(String msg, boolean recordStackTrace) {
        super(msg, null, false, recordStackTrace);
    }

    /**
     * 指定一个不追踪栈信息的异常
     * @param msg
     *      异常消息
     */
    public FlowException (String msg){
        this(msg, false);
    }

    /**
     * 指定一个带有自定义消息的包装异常(会生追踪栈信息)
     * @param msg
     *      异常消息
     * @param cause
     *      要包装的异常信息
     */
    public FlowException(String msg, Throwable cause) {
        super(msg, cause, false, true);
    }

    /**
     * 指定一个包装异常(会生追踪栈信息)
     * @param cause
     *      要包装的信息
     */
    public FlowException(Throwable cause) {
        super(cause.getMessage(), cause, false, true);
    }
}

其次将所有参与流程控制的异常类继承这个FlowException即可, 例如BadRequestException, ValidationException, AccessOAuthException等.
最终测试结果两种方式性能不分伯仲, 但实际业务中考虑到前者冗余代码和状态重复判断的情况, 性能应该会略高于return status的方式.
在以后的项目中我打算用这种方式, 并且已经集成到了我的框架中. 以上新的分享给打算走技术或架构路线的朋友们作为参考.

本文参考资料: https://blog.csdn.net/neosmith/article/details/82626960

猜你喜欢

转载自blog.csdn.net/guoshengkai373/article/details/107482725