美文网首页
谁动了我的异常?

谁动了我的异常?

作者: AlphaHinex | 来源:发表于2020-06-27 09:01 被阅读0次

原文地址:https://alphahinex.github.io/2020/06/26/undeclared-throwable-exception/

cover

description: "明明抛出的是一个检查型异常,为什么捕获到的异常被包装成了 UndeclaredThrowableException?"
date: 2020.06.26 19:26
categories:
- Spring
tags: [Java, Spring]
keywords: Java, Spring, AOP, Proxy, CGLIB, UndeclaredThrowableException, Checked Exception, Unchecked Exception


场景描述

通常来讲,为方便开发,我们会对异常进行统一的处理。会定义一个异常基类,针对基于这个基类的自定义异常进行统一处理。

当异常基类为检查型异常(Checked Exception)时,如果自定义异常是通过切面等代理抛出的,被代理的方法本身并未抛出且也未声明此异常时,就会遇到这个问题:

统一异常处理方法中,捕获到的异常,已不是代理类中抛出的自定义异常,而是一个将自定义异常包装在内的 java.lang.reflect.UndeclaredThrowableException

那么为什么会这样呢?

追根溯源

以使用 Spring Framework v5.0.13.RELEASE 及切面场景为例。

Spring 文档中关于 AOP 代理 的描述如下:

5.1.3. AOP Proxies
Spring AOP defaults to using standard JDK dynamic proxies for AOP proxies. This enables any interface (or set of interfaces) to be proxied.

Spring AOP can also use CGLIB proxies. This is necessary to proxy classes rather than interfaces. CGLIB is used by default if a business object does not implement an interface. As it is good practice to program to interfaces rather than classes; business classes normally will implement one or more business interfaces. It is possible to force the use of CGLIB, in those (hopefully rare) cases where you need to advise a method that is not declared on an interface, or where you need to pass a proxied object to a method as a concrete type.

It is important to grasp the fact that Spring AOP is proxy-based. See Understanding AOP proxies for a thorough examination of exactly what this implementation detail actually means.

几个重点:

  1. Spring AOP 是基于代理来实现的。
  2. Spring AOP 默认使用 JDK 动态代理来为切面创建代理。这使得所有接口的实现类都可以被代理。
  3. Spring AOP 也可以使用 CGLIB 创建代理。当一个类未实现任何接口时,会默认使用 CGLIB 的方式创建代理。也可以强制使用 CGLIB 进行代理。

DefaultAopProxyFactory 中可以看到对应的逻辑:

* <p>Creates a CGLIB proxy if one the following is true for a given
* {@link AdvisedSupport} instance:
* <ul>
* <li>the {@code optimize} flag is set
* <li>the {@code proxyTargetClass} flag is set
* <li>no proxy interfaces have been specified
* </ul>
*
* <p>In general, specify {@code proxyTargetClass} to enforce a CGLIB proxy,
* or specify one or more interfaces to use a JDK dynamic proxy.
@Override
public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
  if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) {
    Class<?> targetClass = config.getTargetClass();
    if (targetClass == null) {
      throw new AopConfigException("TargetSource cannot determine target class: " +
          "Either an interface or a target is required for proxy creation.");
    }
    if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) {
      return new JdkDynamicAopProxy(config);
    }
    return new ObjenesisCglibAopProxy(config);
  }
  else {
    return new JdkDynamicAopProxy(config);
  }
}

使用 ObjenesisCglibAopProxy 创建代理时,会调用 org.springframework.aop.framework.CglibAopProxy#getProxy(java.lang.ClassLoader) 方法,其中 192 行配置了一个 ClassLoaderAwareUndeclaredThrowableStrategy 策略。

enhancer.setStrategy(new ClassLoaderAwareUndeclaredThrowableStrategy(classLoader));

ClassLoaderAwareUndeclaredThrowableStrategy 策略的 generate 方法调用父类 UndeclaredThrowableStrategy => DefaultGeneratorStrategy 的 generate 方法时,可以获得到增强后的字节码。

Fork

说到这时会发现一个问题,在 Spring Framework 的代码仓库中,没有上面提到的 UndeclaredThrowableStrategyDefaultGeneratorStrategy 的源码,而这两个类明明是包含在 spring-core 的 jar 包中的。

spring-core 模块的构建文件 spring-core.gradle 给我们揭晓了答案:

...

// spring-core includes asm and repackages cglib, inlining both into the spring-core jar.
// cglib itself depends on asm and is therefore further transformed by the JarJar task to
// depend on org.springframework.asm; this avoids including two different copies of asm.
def cglibVersion = "3.2.6"

...

task cglibRepackJar(type: Jar) { repackJar ->
    repackJar.baseName = "spring-cglib-repack"
    repackJar.version = cglibVersion

    doLast() {
        project.ant {
            taskdef name: "jarjar", classname: "com.tonicsystems.jarjar.JarJarTask",
                    classpath: configurations.jarjar.asPath
            jarjar(destfile: repackJar.archivePath) {
                configurations.cglib.each { originalJar ->
                    zipfileset(src: originalJar)
                }
                // Repackage net.sf.cglib => org.springframework.cglib
                rule(pattern: "net.sf.cglib.**", result: "org.springframework.cglib.@1")
                // As mentioned above, transform cglib's internal asm dependencies from
                // org.objectweb.asm => org.springframework.asm. Doing this counts on the
                // the fact that Spring and cglib depend on the same version of asm!
                rule(pattern: "org.objectweb.asm.**", result: "org.springframework.asm.@1")
            }
        }
    }
}

...

所以在 cglib v3.2.6 的仓库中,我们可以找到 UndeclaredThrowableStrategyDefaultGeneratorStrategy,以及 可获取增强后字节码的地方

public byte[] generate(ClassGenerator cg) throws Exception {
    DebuggingClassWriter cw = getClassVisitor();
    transform(cg).generateClass(cw);
    return transform(cw.toByteArray());
}

断点加在 cw.toByteArray() 处,将其输出为 .class 文件,即可获得增强后的字节码,也就是 AOP 的代理类。

Join

回到主线。反编译输出出来的 .class 文件,可与源码进行对比。

源码:

@PostMapping
public ResponseEntity post() {
    return responseOfPost("success");
}

反编译代理类:

public final ResponseEntity post() {
    try {
        MethodInterceptor cglib$CALLBACK_2;
        MethodInterceptor cglib$CALLBACK_0;
        if ((cglib$CALLBACK_0 = (cglib$CALLBACK_2 = this.CGLIB$CALLBACK_0)) == null) {
            CGLIB$BIND_CALLBACKS(this);
            cglib$CALLBACK_2 = (cglib$CALLBACK_0 = this.CGLIB$CALLBACK_0);
        }
        if (cglib$CALLBACK_0 != null) {
            return (ResponseEntity)cglib$CALLBACK_2.intercept((Object)this, TestController$$EnhancerBySpringCGLIB$$ef60194e.CGLIB$post$5$Method, TestController$$EnhancerBySpringCGLIB$$ef60194e.CGLIB$emptyArgs, TestController$$EnhancerBySpringCGLIB$$ef60194e.CGLIB$post$5$Proxy);
        }
        return super.post();
    }
    catch (RuntimeException | Error ex) {
        throw;
    }
    catch (Throwable t) {
        throw new UndeclaredThrowableException(t);
    }
}

顺便吐个槽:老牌反编译软件 jd-gui 反编译上面这段时,内容明显不对。Luyten 反编译出了上述结果,但 Luyten v0.5.4 Rebuilt 里的 Mac 版 无任何响应。上述结果为在 Windows 下编译得到。

总结一下,扣个题:

代理类将被代理调用的方法整个使用 try/catch 包了起来,将除 RuntimeException 和 Error 之外的异常,都包装成了 UndeclaredThrowableException 再向外抛出。

对症下药

所以遇到这个问题的时候,可以通过如下方式处理:

  1. 在被代理的方法上,显示抛出检查型异常
  2. 切面里抛出 RuntimeException,替代检查型异常
  3. 不能更改异常类型又不想在方法上定义方法本身未进行抛出的异常时,可以使用一些迂回策略,比如在切面中直接按照统一异常处理的规则,返回将异常处理好的封装类型

参考资料

相关文章

  • 谁动了我的异常?

    原文地址:https://alphahinex.github.io/2020/06/26/undeclared-t...

  • 谁动了我的异常 Plus

    原文地址:https://alphahinex.github.io/2020/07/19/undeclared-t...

  • 谁动了我的🐟

    今天下班回家,老爸问我“你把我的鱼给猫吃了?” 我愣了一下回答道“你什么时候买的鱼,我怎么不知道?” 老爸说“我昨...

  • 谁动了我的心态!

    “公司怎么能这么做,太不公平了”A怒气冲冲的对我说,“咱们付出的一点不比他们少,咱们默默贡献这么多,到头来,他们加...

  • 谁动了我的琴弦

    出了家门,向西行不久,就到了以前村子的中央,再往西就是祖屋所在的地方。 今天,我又一次前往祖屋所...

  • 谁动了我的婚姻

    一 麦鱼儿和林石结婚了。 婚礼办得很排场,二十两清一色的黑色奥迪,绕着村子转了两圈。这在麦香村的村史上是从没过的隆...

  • 谁动了我的花

    2018.3.8(农历21)周四

  • 谁动了我的梦想?

    很久以前看过一部电影,名字叫做《谁动了我的梦想》,当时并不怎么喜欢,最近闲来无事便又拿来回味,或许是随着自己的成长...

  • 谁动了我的蛋糕?

    最近,我花168元在网上淘了一个迷你电压力锅。拿到后,我才发现压力锅的不锈钢内胆只有2L。不得不说,那锅果然是名不...

  • 谁动了我的房子

    难以置信的事情枚不胜举,但像史蒂夫遭遇的事情,用“难以置信”四个字很难概述清楚。 史蒂夫一路骂骂咧咧地驾驶着他的福...

网友评论

      本文标题:谁动了我的异常?

      本文链接:https://www.haomeiwen.com/subject/fzkmfktx.html