桥过远

工程 | Rob Harrop | 2007年1月16日 | ...

在我的上一篇文章中,我介绍了一种创建策略类的方法,这些策略类充分利用了应用程序中存在的任何泛型元数据。在该文章的最后,我展示了以下代码片段

EntitlementCalculator calculator = new DividendEntitlementCalculator();
calculator.calculateEntitlement(new MergerCorporateActionEvent());

您会记得DividendEntitlementCalculator被定义为

public class DividendEntitlementCalculator implements EntitlementCalculator<DividendCorporateActionEvent> {

    public void calculateEntitlement(DividendCorporateActionEvent event) {

    }
}

因此,将MergerCorporateActionEvent的实例传递给calculateEntitlement方法是不正确的。DividendEntitlementCalculator类。但是,正如我在上一篇文章中提到的,该代码将编译。为什么?好吧,EntitlementCalculator.calculateEntitlement()被定义为接受扩展CorporateActionEvent的任何类型,因此它应该编译。因此,在这种情况下,运行时会发生什么以及 Java 如何强制执行类型安全?好吧,正如您可能想象的那样,运行此代码会给您一个ClassCastException,说明您不能将MergerCorporateActionEvent转换为DividendCoporateActionEvent。通过这种方式,Java 可以为您应用程序强制执行类型安全 - 无法将MergerCorporateActionEvent潜入期望DividendCorporateActionEvent的方法中。

这里真正的问题是:“那个ClassCastException从哪里来?”答案很简单 - Java 编译器添加了代码以根据需要创建并抛出它,方法是引入一个桥接方法。桥接方法是编译器将生成并添加到您的类中的合成方法,以确保在面对泛型类型时类型安全。

在上面显示的示例中EntitlementCalculator.calculateEntitlement可以使用任何与CorporateActionEvent类型兼容的对象进行调用。但是,DividendEntitlementCalculator仅接受与DividendCorporateActionEvent类型兼容的对象,但是,由于您可以通过DividendEntitlementCalculator接口调用EntitlementCalculator,它也必须接受CorporateActionEvent。那么这在编译后的类文件中如何体现呢?我们有用户提供的 method

public void calculateEntitlement(DividendCorporateActionEvent event) {
    System.out.println(event);
}

它转换为以下字节码

public void calculateEntitlement(bigbank.DividendCorporateActionEvent);
  Code:
   Stack=2, Locals=2, Args_size=2
   0:   getstatic       #2; //Field java/lang/System.out:Ljava/io/PrintStream;
   3:   aload_1
   4:   invokevirtual   #3; //Method java/io/PrintStream.println:(Ljava/lang/Object;)V
   7:   return

但我们还有一个编译器生成的 method

public void calculateEntitlement(bigbank.CorporateActionEvent);
  Code:
   Stack=2, Locals=2, Args_size=2
   0:   aload_0
   1:   aload_1
   2:   checkcast       #4; //class bigbank/DividendCorporateActionEvent
   5:   invokevirtual   #5; //Method calculateEntitlement:(Lbigbank/DividendCorporateActionEvent;)V
   8:   return

它转换为以下 Java 代码

public void calculateEntitlement(CorporateActionEvent event) {
    calculateEntitlement((DividendCorporateActionEvent)event);
}

因此,在这里您可以清楚地看到当传入ClassCastException以外的CorporateActionEvents时,DividendCorporateActionEvents来自哪里 - 编译器生成的桥接方法

现在,这当然是一个极好的功能。我们不希望将泛型添加到 Java 语言中会破坏我们长期以来一直使用的类型安全性。但是,正如预期的那样,并非一切都好。桥接方法在当前 JDK 中的实现主要问题在于,注释不会从桥接方法复制到桥接方法。当您在反射中无意中获取桥接方法并尝试解析某些注释时,这会导致各种问题。

你们中的一些人可能想知道您是如何错误地获取桥接方法的。这是一个有点复杂的问题。一个常见的原因(以及我们在 Spring 中看到它出现最多的地方)是您正在创建委托给某个对象的 JDK 代理,并且您尝试将代理接口中的方法映射到委托上的相应实现方法(通常是为了解析注释)。考虑以下代码

public static void main(String[] args) {
    EntitlementCalculator ec = createProxy(new DividendEntitlementCalculator());
    ec.calculateEntitlement(null);
}

private static EntitlementCalculator createProxy(EntitlementCalculator calculator) {
    InvocationHandler handler = new TransactionLoggingInvocationHandler(calculator);
    return (EntitlementCalculator) Proxy.newProxyInstance(calculator.getClass().getClassLoader(),
                                                                calculator.getClass().getInterfaces(), handler);
}

private static class TransactionLoggingInvocationHandler implements InvocationHandler {

    private final EntitlementCalculator delegate;

    public TransactionLoggingInvocationHandler(EntitlementCalculator delegate) {
        this.delegate = delegate;
    }

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        Method delegateMethod = delegate.getClass().getMethod(method.getName(), method.getParameterTypes());
        Transactional annotation = delegateMethod.getAnnotation(Transactional.class);
        if(annotation != null) {
            System.out.println("Executing transactional method: " + delegateMethod);
        } else {
            System.out.println("Executing non-transactional method: " + delegateMethod);
        }
        return method.invoke(delegate, args);
    }
}

在这里,我们为给定的EntitlementCalculator对象创建一个代理,该代理将记录代理对象上的方法是否为事务性的。如果我们像下面这样注释DividendEntitlementCalculator类,我们可以预期代理在调用calculateEntitlement时记录我们正在执行事务性方法。main.

@Transactional
public void calculateEntitlement(DividendCorporateActionEvent event) {
    System.out.println(event);
}

但是,执行上面的示例会产生以下结果

Executing non-transactional method: public volatile void bigbank.DividendEntitlementCalculator.calculateEntitlement(bigbank.CorporateActionEvent)

请注意,这与我们调用的DividendEntitlementCalculator上的方法不对应。当然,这显然会发生;这里要说明的是,接口方法和委托方法的签名确实不同。一个是用父类型定义的,在本例中是CorporateActionEvent,另一个是用子类型定义的,在本例中是DividendCorporateActionEvent。您还会注意到,我们实际上已经获得了桥接方法 - 因为它的签名确实与接口方法的签名匹配(根据定义)。

也许查找委托方法的更好解决方案是使用传入参数的类型而不是接口方法上的类型。当面对使用继承的参数时,您可以简单地搜索参数类型层次结构中的类型匹配。不幸的是,这种方法无法可靠地工作。考虑以下情况:您有以下接口

public interface Foo<T> {
    void bar(T t);
}

然后是这个实现

public class FooImpl implements Foo<Number>{

    public void bar(Number t) {
    }

    public void bar(Serializable t) {
    }
}

如果您要使用传递给InvocationHandler的具体参数的类型来解析委托方法,那么当面对类型为Integer的参数时,您会选择哪种方法?您无法从(接口方法)得知类型参数是Number,并且由于这两种方法都与Integer类型兼容,因此不可能始终以通用的方式解析正确的方法。

解决此问题只有两种方法(据我所知)。第一种方法涉及使用像ASM这样的库来读取桥接方法的字节码并找出它调用的方法。使用 ASM 读取字节码是一个很好的解决方案,它通常是万无一失的。但是,在安全环境中,它可能需要对不允许的库进行读取权限,这可能会导致问题。第二种解决方案涉及使用桥接方法中的泛型元数据来解析实现类中正在桥接的方法。

在上面的示例中,我们可以看到接口方法是bar,由T参数化。我们可以使用Class.getGenericInterfaces()的泛型接口元数据(FooImpl)来确定T实现为Number。从那里,很容易知道桥接方法是bar(Number)而不是bar(Serializable)。不幸的是,在涉及多个具有边界的类型参数的复杂层次结构的情况下,此方法变得越来越复杂。幸运的是,此逻辑封装在 Spring 的BridgeMethodResolver类中。这是一个完美的例子,说明 Spring 如何解决 Java 开发人员面临的难题并将其集成到应用程序堆栈中。每当在 Spring 中执行注释查找时,桥接方法都会被透明地解析。

的实现BridgeMethodResolver基本完成;但是,我相信我们还没有考虑一些复杂的情况,我很乐意听取遇到此领域任何问题的用户的意见。

获取 Spring Newsletter

通过 Spring Newsletter 保持联系

订阅

领先一步

VMware 提供培训和认证,以加速您的进步。

了解更多

获取支持

Tanzu Spring 在一个简单的订阅中提供 OpenJDK™、Spring 和 Apache Tomcat® 的支持和二进制文件。

了解更多

即将举行的活动

查看 Spring 社区中所有即将举行的活动。

查看全部