SourceCode Java SpringBoot

Java使用异常处理业务优化

Posted on 2021-12-21,5 min read
  • 在我们的业务场景中,经常会有这样的一个需求,比如在用户登录的场景,登录成功返回用户信息

    • 如果用户的密码错误,我们会返回错误提示:用户的密码错误
    • 如果用户账户不存在,我们会返回错误提示:当前的账户不存在
  • 而我们一般业务逻辑写在Service层的,就像这样:

    public User login(String account, String password){
        //...
        return user;
    }
    
  • 但是这样的话会有一个问题,我们的User只能返回数据或者Null,使得我们不知道是什么原因导致登录出错的。这里我们就存在两种解决方案:

    • 将User封装一层,额外添加一个字段来记录失败原因。
    • 直接抛出业务异常,通过获取业务异常的Message来获取失败原因。
  • 前者的缺点是每一层都要封装一下,很不直接和美观而且耦合性高,后者的话会被老开发骂一顿,因为直接抛出异常的效率是比直接返回效率低很多。这里我们在控制台上写一个简单的测试。

    public static timeTest(){
        int loop = 1000_0000;
        // 直接返回耗时测试
        long start = System.currentTimeMillis();
        for (int i = 0; i < loop; i++) {
            getError();
        }
        long end = System.currentTimeMillis();
        System.out.println("直接返回耗时:" + (end - start));
        // 直接抛出异常耗时测试
        start = System.currentTimeMillis();
        for (int i = 0; i < loop; i++) {
            try {
                getRumtimeEx();
            }catch (Exception e){
    
            }
        }
        end = System.currentTimeMillis();
        System.out.println("直接抛出异常耗时:" + (end - start));
    }
    
    直接返回耗时:35
    直接抛出异常耗时:11522
    
  • 我们可以看到其实耗时差距是很明显的,而且这还是在控制台单线程的情况下,多线程情况会更加耗时。

  • 那么为什么抛出异常会耗时呢?这点我们需要查看相关的源码。

    • 在我们抛出运行时异常时候,最终会走到Throwable的构造方法,这里调用了一个fillInStackTrace方法来获取异常时候的堆栈信息:

      public Throwable(String message) {
          fillInStackTrace();
          detailMessage = message;
      }
      
      public synchronized Throwable fillInStackTrace() {
          if (stackTrace != null ||
              backtrace != null /* Out of protocol state */ ) {
              fillInStackTrace(0);
              stackTrace = UNASSIGNED_STACK;
          }
          return this;
      }
      
    • 我们可以看到,fillInStackTrace方法是使用了synchronized关键字修饰的,这也是上面说多线程会更加耗时的原因。

    • 然后调用了一个本地方法去获取调用的堆栈信息。这也是耗时的主要原因。当一个业务中出现异常时,JVM会追中当前栈信息了,因为有栈信息我们可以清楚的知道在代码某个模块某个类某个方法的第几行发生了什么样的异常, 可以快速的定位并解决问题。这就导致了抛出异常比直接返回会有更多的耗时。

  • 那么如何解决呢,很明显,我们只需要关掉栈追踪就行了(一般情况下,我们的业务代码中对于这种业务异常是不需要知道栈情况的)。Throwable有这样一个构造方法:

    protected Throwable(String message, Throwable cause,
                        boolean enableSuppression,
                        boolean writableStackTrace) {
        if (writableStackTrace) {
            fillInStackTrace();
        } else {
            stackTrace = null;
        }
        detailMessage = message;
        this.cause = cause;
        if (!enableSuppression)
            suppressedExceptions = null;
    }
    
  • 其中,有个boolean的字段writableStackTrace可以控制是否开启栈追踪,所以我们只需要自定义一个异常类,然后使用这个构造方法关闭栈追踪就行了。

    public static class ServiceException extends RuntimeException{
        public ServiceException(String message) {
            super(message, null, false, false);
        }
    }
    
  • 然后测试:

    public static timeTest(){
        int loop = 1000_0000;
        // 直接返回耗时测试
        long start = System.currentTimeMillis();
        for (int i = 0; i < loop; i++) {
            getError();
        }
        long end = System.currentTimeMillis();
        System.out.println("直接返回耗时:" + (end - start));
        // 直接抛出异常耗时测试
        start = System.currentTimeMillis();
        for (int i = 0; i < loop; i++) {
            try {
                getRumtimeEx();
            }catch (Exception e){
    
            }
        }
        end = System.currentTimeMillis();
        System.out.println("直接抛出异常耗时:" + (end - start));
    
        // 关闭栈追踪耗时测试
        start = System.currentTimeMillis();
        for (int i = 0; i < loop; i++) {
            try {
                getCustomEx();
            }catch (Exception e){
    
            }
        }
        end = System.currentTimeMillis();
        System.out.println("关闭栈追踪耗时:" + (end - start));
    }
    
    直接返回耗时:19
    直接抛出异常耗时:12071
    关闭栈追踪耗时:200
    
  • 我们可以看到耗时一下少了很多倍(这里还有创建对象的开销)。

  • 所以,我们可以得出一个结论,如果在业务代码中想使用异常来记录返回值的话,我们可以这样做:

    • 定义一个自定义业务异常,默认关闭栈追踪(不建议完全关闭)。
    • 在SpringBoot中使用@RestControllerAdvice@ExceptionHandler来处理业务异常。
  • 这样我们如果出现业务逻辑错误就可以直接抛出异常了(性能不会损失太多,这东西就和反射一样,牺牲一丢丢性能,能简化很多代码)。然后直接通过切面根据异常信息封装返回值。也不用我们手动去封装了。


下一篇: Jenkins捣鼓→

loading...