业务系统开发中的异常处理:从程序员视角看

点击下方“程序员的时光”,选择“设为星标”

业务系统开发中的异常处理:从程序员视角看

本文仅从业务系统开发的角度来阐述对异常的一些处理观点。其中不包含关于 java 的异常基础知识部分,若想了解相关内容,可自行进行查阅。

《Java 核心技术 卷 I》能够让我们获取到更多的基础信息。《java 编程思想》也可以让我们获得更多的基础信息。

写在前面的话

笔者的文笔功力还比较浅薄,所说的话可能有不妥之处,希望您能不吝赐教,我一定会非常感激。本文提出了几个概念,包括处理反馈业务的异常代码错误,希望您能认真思考一下这些概念之间的各种区别。

在开发业务系统时,目前我们大多采用 MVC 模式。然而,时常有人会将 service 与 controller 紧密耦合在一起,甚至直接利用 Threadlocal 来进行隐式传值。并且,复杂的逻辑几乎只能依靠 service 中存储的全局对象来传递处理结果,其中也包括异常。

这样一来首先会违背 MVC 模式,其次逻辑显得十分不清晰,也难以进行维护。本文依据工作经验,给出一些关于异常使用的建议,并且通过使用 spring 来实际体验异常为我们带来的好处。

我们常常读完各种 Java 的书后,对各种机制和特性都非常清楚,然而却始终不知道该如何使用。甚至把相关概念都背下来了,却依然不知道怎样将其运用到实际中。

我们开发的业务系统,或者是产品,常常面临着这样的问题:

什么情况需要自定义异常

经常能看到一些项目,会在全局定义一个 AppException。接着,所有的地方都仅仅抛出这个异常,并且把捕获到的异常都归类到这个 AppException 中。这样会存在如下的问题:

什么情况需要手动处理异常

我不会将书上的内容直接进行复制。在此,我来讲一下那些容易被记住的,同时又适合业务开发的内容。

自定义业务异常

考虑这样的场景:系统有一个 API,这个 API 是用于修改用户信息的。在服务器端,采用的是 json 数据进行交互。首先,我们定义了 ServiceException,它是用来表示业务逻辑受理失败的。也就是说,当我们在处理业务时,发现无法继续进行下去了,就会出现这种情况。

/**
 * 业务受理失败异常
 */
类 ServiceException 是 RuntimeException 的子类。
    //接收reason参数用来描述业务失败原因.
创建一个名为 publicServiceException 的构造函数,该构造函数接受一个字符串参数 reason。在构造函数内部,首先调用父类的构造函数 super(reason),将传入的 reason 传递给父类。
}

接下来看下Controller层.

// UserController.java 
 /**
   * 修改用户信息
   * @param userID 用户ID
   * @param user 修改用户信息表单数据
   */
  @PutMapping("{userID}")
public JSONResult 对用户进行更新操作,通过传入路径变量(PathVariable)来指定要更新的用户信息。"userID"Integer userID,将 @RequestBody 中的 UpdateUserForm 作为 userForm 参数传入。
创建了一个新的 User 对象,用于准备业务逻辑层所使用的领域模型。
BeanUtils 用于将 userForm 中的属性值复制到 user 中,以实现拷贝要修改的值的操作。
设置主键到用户数据中,即将 userID 设置到用户数据中。
调用了更新业务逻辑,即对用户进行更新操作,具体是更新 userService 中的 user 。
准备要响应的数据,创建一个 JSONResult 类型的对象 json ,即 new JSONResult() 。
    json.put("user", user); //把修改后的用户数据还给页面
    return json; // --  
  }

上述 Controller 的写法乍一看会显得有些冗余。若无法理解,那就仔细研读一下 MVC 设计模式。先暂且不考虑 service,我们来进行思考。一个业务系统必然要对用户提交的数据进行验证,而验证包含两个方面,即有效性和合法性。

有效性检查可以由 Java 的校验框架来执行,例如 JSR303。假如用户提交的数据经过验证是合法的,但仍存在一些情况不能调用修改逻辑。

要修改的用户ID不存在.

用户被锁定,不允许修改.

乐观锁机制发现用户已经被被人修改过.

由于某种原因,我们的程序无法保存到数据库.

一些程序员在开发代码时出现了错误,这使得在保存过程中出现了异常情况,例如 NPE。

对于前 3 种情况,我们认为是有效性检查出现了失败。第 4 种属于我们无法处理的异常情况。第 5 种则是程序员所导致的 bug。

现在的问题是,前三种情况我们如何通知用户呢?

ccontroller 调用了 userService 的 checkUserExist()方法。

在controller直接书写业务逻辑.

服务有一个响应状态码的机制,其中 1、2、3 这些状态码表示错误信息,而 0 这个状态码则表示没有任何错误。

显然前两种方法不可取。因为 MVC 设计模式告诉我们,controller 的作用是接收页面参数,同时调用逻辑处理,最后组织页面响应。我们不能在 controller 中进行逻辑处理,controller 只应负责用户 API 入口和响应的处理。(试想一下,如果有一天 service 的代码打包成 jar 放到另一个平台,而没有 controller 了,那该怎么办呢?)

状态码机制是个不错的选择。然而,这样一来,用户保存逻辑发生了变化。例如增加了一种情况,即不允许修改已经离职的用户。那么,我们就需要修改 controller 的代码,这会导致代码量增加,维护成本增高,并且还会与 service 产生耦合,不符合 MVC 设计模式。

那么怎么办呢?现在我们来看下service代码如何编写

  /**
   * 修改用户信息
   * @param user 要修改的用户数据
   */
公共的 void 方法 updateUser,它接收一个 User 对象作为参数。
User 的 userDao 通过获取用户的用户 ID(user.getUserID())来获取原始的 User。
    if (null == userOrig) {
抛出一个新的 ServiceException 。"用户不存在");
    }
    if (userOrig.isLocked()) {
      throw new ServiceException("用户被锁定,不允许修改");
    }
    if如果当前用户的版本与原始用户的版本不相等,则执行以下操作。
      throw new ServiceException("用户已经被别人修改过,请刷新重试");
    }
    // TODO 保存用户数据  ... 
  }

如果我们检查到不允许保存的项目,那么我们就能够直接抛出一个新的异常,而异常机制会协助我们中断代码的执行。

接下来有2种选择:

在controller 使用try-catch进行处理.

直接把异常抛给上层框架统一处理.

第 1 种方式不可取。要注意我们抛出的 ServiceException,它只是用于逻辑处理异常。并且我们的方法前面未声明 throws ServiceException,这意味着它是一个非受查异常。同时,controller 也没有关注会发生何种异常。

为什么要定义为受查异常呢?若为受查异常,就表明 controller 必须对该异常进行处理。并且倘若有一天业务逻辑发生变化,或许会多一种检查项,此时就需要新增一个异常,反之则需删除一个异常,这样一来,方法签名也需要进行更改,controller 也得随之改变,这就变成了紧耦合,这与用状态码 123 来表示处理结果并无差别。

我们能否为每一种检查项定义一个异常呢?可以。然而那样做显得有些多余。因为当业务逻辑处理失败时,按照我们的需求,只需通知用户失败的原因(通常是一段字符串),以及服务器受理失败的一个状态码(有时可能不需要状态码,这取决于你的设计),这样只需一个包含原因属性的异常就能满足我们的需求。

我们最后决定这个异常由 RuntimeException 继承而来。它包含一个构造器,这个构造器能够接收一个错误原因。如此一来,controller 层无需知晓该异常,只需在全局捕获到 ServiceException 并进行统一处理即可。这种方式在 struct1、2 时代,以及 springMVC 中,甚至 servlet 年代,都是极为便捷的!

异常不会提供无参构造器。因为绝对不允许你在抛出逻辑处理异常时不指明原因。要知道,你必须要告诉用户受理失败的原因!

我们只需要对 ServiceException 进行全局统一处理。这很好,因为 spring 为我们提供了 ControllerAdvice 机制。关于 ControllerAdvice,可查阅 springMVC 使用文档。以下是一个简单的示例:

@ControllerAdvice 这个注解的作用是在指定的包及其子包下进行一些全局的控制器增强处理。它可以用于处理控制器层的异常、统一数据绑定、统一数据验证等各种与控制器相关的操作。通过在这个注解中指定包路径,就可以对该包及其子包下的所有控制器进行统一的配置和处理。这样可以提高代码的复用性和可维护性,避免在每个控制器中都重复编写相同的代码。"com.xxx.xxx.bussiness.xxx" })
公共类 ModuleControllerAdvice 。
创建了一个名为 LOGGER 的私有静态常量,它的类型是 Logger,通过 LoggerFactory.getLogger(ModuleControllerAdvice.class) 来获取这个 Logger 的实例。


  /**
   * 业务受理失败
   */
  @ResponseBody
使用了 @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) 这个注解。它表明在特定情况下会返回内部服务器错误的状态码。这个注解通常用于在处理请求时,如果出现了服务器端的错误,就会触发相应的处理逻辑并返回指定的状态码。
在处理异常时,如果异常类型为 ServiceException ,则会调用该方法进行处理。

    String message = "业务受理失败,原因:"获取异常的本地化消息。
SERVICE_LOGGER 实现了消息的输出动作。
创建了一个 JSONResult 类型的对象 json 。
json.serCode 表示的是一个编码相关的操作;其中 500001 这个编码代表着业务逻辑出现了异常;而 500000 这个编码则表示系统出现了异常。
json 将消息设置为 message 。
    return json;
  }
}

在这个时候,我们就可以很轻松的处理各种情况了.

这是很有用的。

接下来你可以在修改用户的时候想客户端响应这样的JSON

{
    code: 200001,
    message: "业务受理失败,原因:用户名称不存在!"
}

没有任何地方需要关心异常,也不需要关心业务逻辑校验失败的情况。这样一来,用户能够得到很友好的错误提示。

如何对异常进行分类

如果你仅需一句概括,那就直接定义一个简单的异常,此异常用于中断处理,同时要与用户保持友好的交互。

如果无法用一句话清晰地描述,并且需要包含附加信息,例如在日志或数据库中记录消息 ID 时,这种情况下可能会专门为这种重要或复杂的业务创建独立的异常。

上述两种情况涉及到 web 系统,在用户发起请求之后,需要等待程序给出响应结果。

如果是后台作业,或者复杂业务需要追溯性,通常会用流程判断语句来控制,并且需要使用异常处理。我们认为这些流程判断必须在一个原子性处理中。同时,检查到的问题(而非异常)需要记录到用户可友好查看的日志中。这种情况属于处理反馈,而不叫异常。

综上,笔者通常分为如下几类:

逻辑出现异常,这类异常的作用是描述业务无法依照预期的状况继续处理,它属于用户所导致的意外情况。

代码存在错误。这类异常是用来描述开发过程中出现的代码错误的。像 NPE 和 ILLARG 这样的情况,都属于程序员制造的 BUG 。

专有异常通常在特定的业务场景中被使用,它是用来描述指定作业出现了意外情况,并且这种情况无法预先进行处理。

各类异常需要有单独的日志记录,或者进行分级以及分类管理。有时候只是想要让三方运维看到逻辑异常。

写在后面的注意

异常设计的初衷是为了解决程序运行中的各类意外状况。同时,异常的处理效率相较于条件判断方式而言要低许多。

上面这句话出自,但是我们思考如下几点:

业务逻辑检查,也是意外情况

UnknownHostException 意味着找不到这样的主机。这个异常与 NoUserException 有什么区别呢?也就是说,没有这样的主机是一种异常情况,而没有这样的用户不是异常情况。所以,一定要清楚什么是用异常来控制逻辑,什么是定义程序异常。

异常处理效率很低

书中所举的例子,是在循环过程中大量运用 try-catch 来进行检查。然而,业务系统中,用户发起请求的次数与该场景有着极大的差异。像淘宝的 11·11 就是一个很好的反面例子。但是,只有当你的系统达到这个级别时,才需要考虑这类问题。

系统具有千万并发的能力,不会再去考虑那些遵循常规、按部就班的方式。要知道,MVC 本身就会浪费大量资源,并且会使代码量大幅增加。

业务系统存在很多大量任务处理的情况。然而那些任务是原子性的。现在 MVC 中的 controller 和 service 并非原子性的,不然就不会区分这么多层了。

如果很在乎效率,那就考虑重写 Throwable 的 fillStackTrace 方法。你需要知道异常的开销大具体体现在哪些方面,fillStackTrace 是一个 native 方法,它会填充异常类内部的运行轨迹。

不要用异常进行业务逻辑处理

我们先来看一个例子:

    //这是一个非常典型的反例,也是一个误区.
  /**
   * 处理业务消息
   * @param message 要处理的消息
   */
公共的方法用于处理消息,这个消息是 Message 类型的。它的主要功能是对传入的消息进行特定的处理操作。 message) {
    try{
        // 处理消息验证
        // 处理消息解析
        // 处理消息入库
如果捕获到了 ValidateException 类型的异常 e,则会执行以下操作。
        // 验证失败
如果捕获到了 ParseException 异常,那么就会执行以下操作。
        // 解析失败
如果捕获到 PersistException 异常,则会执行以下操作:
        // 入库失败
    }
  }

上述代码属于典型的利用异常来处理业务逻辑。这种做法是需要被严格禁止的!上述代码最大的问题在于,我们该怎样利用异常来自动处理事务呢?

然而这与我们的异常中断 service 不存在冲突,也并非是同一回事。

改正后的逻辑

  /**
   * 处理业务消息
   * @param message 要处理的消息
   */
  public void processMessage(Message message) {
    // 处理消息验证
    if(!message.isValud()){
MessageLogService 进行了日志记录操作。它记录了相关的信息。这些信息被记录在日志中。日志服务负责记录这些信息。它确保信息的准确记录和存储。"消息校验失败"+message.errors())
        return ;
    }
    // 处理消息解析
    if(!message.parse()){
        MessageLogService.log("消息解析失败"+message.errors())
        return ;
    }
     // TODO ....
  }

在微服务盛行的当下,我们直接在 action 中进行业务处理,这是可以理解的,没有什么过错。

来源是 my.oschina.net 这个网站,其中 c5ms 发表了一篇博客,博客的标题是 1827907。

你可能想看:
« 上一篇
下一篇 »

相关推荐

java中什么是冒泡,冒泡算法

2026年09月06日

27阅读

java中冒号,java双冒号

2026年09月06日

26阅读

java中的->,java中的是什么意思

2026年09月06日

24阅读

java中的super用法,Java中super的说明

2026年09月06日

28阅读

java中反射的应用,java中反射的应用场景

2026年09月06日

25阅读

java中开关的定义,Java关键字有哪些

2026年09月06日

25阅读