字节码增强作为代理

作者:    |       Hibernate ORM

一般来说,一个ORM解决方案将支持根据其实体标识值创建一个懒代理。从历史上看,Hibernate支持使用Java的代理功能(请参阅java.lang.reflect.Proxy)来生成这些代理。现在Hibernate支持使用字节码增强来代理实体。

示例模型

为了讨论懒加载的不同解决方案,请考虑以下映射

@Entity
public class Person {
    @Id
    public Integer getId() {...}

    String getName() {...}

    @Lob
    @Basic(fetch=LAZY)
    String getSignature() {...}
    ...
}

和以下调用

Session session = ...;
Person p = session.load( Person.class, 1 );

等同于以下调用

EntityManager em = ...;
Person p = em.getReference( Person.class, 1 );

历史解决方案

如前所述,Hibernate在历史上利用Java代理来实现这一点。在示例中Hibernate调用#load时,会使用java.lang.reflect.Proxy来创建一个仅包含id的代理实例。伪代码如下

HibernateProxy proxy = new HibernateProxy(Person.class, 1);

返回的代理扩展了Person,这样它可以像Person一样作用于应用程序。代理保持懒加载状态,直到访问其中一个非id属性(在这里是namesignature),此时代理被初始化。初始化代理会触发Hibernate直接实例化一个Person实例并将其与代理关联。再次以伪代码的形式表示

Person target = new Person();
target.setId( 1 );
proxy.injectTarget( target );

在此之后,所有应用程序对代理的调用都会委托给“真实”的Person实例。

初始字节码增强支持

例如,如果signature从数据库加载代价很高(它毕竟是一个LOB),我们只想在应用程序访问它时才加载signature。为了支持这种情况,Hibernate添加了对字节码增强的有限支持,允许实体的单个属性稍后加载。

在此方法中,当加载Person时,Hibernate将

  1. 实例化一个Person实例并注入id

  2. 从数据库选择其非懒加载状态(这里为name)并注入到实例化的Person

稍后,如果调用 Person#getSignature,Hibernate 将从数据库加载 signature 并将其注入到 Person 实例中。

此外,如果实体定义了多个延迟加载属性,Hibernate 允许应用程序将这些属性组合成一个“延迟加载组”(见 @LazyGroup),以便一起加载。未明确分配到组的属性将组合到一个隐式组中。当访问任何尚未初始化的属性时,该属性所属组中的所有属性也将被初始化。

作为代理的字节码增强

您可以将这个新功能视为上述两种方法的结合。

此新的增强代理功能被视为“技术预览”,这意味着它目前基于最佳努力原则支持。必须使用 hibernate.bytecode.allow_enhancement_as_proxy 设置来启用此功能。

它使用相同的增强代码,这意味着实体无需重新增强即可从初始字节码支持移动到新的字节码代理功能。

它也适用于延迟加载组。

在高级别上,当加载 Person 时,Hibernate 将

  1. 实例化一个Person实例并注入id

就是这样。此时它不会加载任何数据库状态。

要描述 Hibernate 如何初始化这些代理,我们首先需要定义一些术语

基线组

该组由实体的所有非延迟加载属性定义。

隐式组

所有未明确分配到组的延迟加载属性

显式组

通过 @LazyGroup 明确与组关联的延迟加载属性

隐式组和显式组的区别与我们对传统增强支持的讨论相同。

这里的新特点是基线组。每当实体代理的任何部分初始化被触发时,我们需要加载此基线组,因为它表示非延迟加载状态。加载此基线状态是在加载触发延迟组之外发生的。

例如,假设我们有

Session session = ...;
Person p = session.load( Person.class, 1 );
String name = p.getName();

这里的 p.getName() 调用触发了代理的初始化。 name 是非延迟加载的,因此属于基线组。它在代理初始化时加载(也因为它是访问的属性,但现在忽略这一点)。

然而,由于 signature 被定义为延迟加载,并且由于其延迟加载组(隐式组)未被访问,因此其数据不会从数据库加载。

如果我们做

Session session = ...;
Person p = session.load( Person.class, 1 );
String signature = p.getSignature();

现在 name(作为基线组的一部分)和 signature(作为访问的延迟组的一部分)都被加载。具体来说,两者都在同一个 SQL SELECT 中加载。

现在考虑

Session session = ...;
Person p = session.load( Person.class, 1 );
String name = p.getName();
String signature = p.getSignature();

请注意,这触发了两个不同的 SQL SELECT 语句 - p.getName() 触发加载基线状态,然后 p.getSignature() 触发加载隐式组。

限制

唯一的真正限制与继承层次结构相关。具体来说,我们不会为具有映射子类的实体创建字节码代理。对于这些,我们将回退到使用 Java 代理。我们需要代理固有的间接性来允许“窄化”引用。

考虑一个具有继承的模型

@Entity
@Inheritance
class Payment {
    @Id
    Integer getId() {...}

    @Basic(fetch=LAZY)
    MonetaryAmount getAmount() {...}

    ...
}

@Entity
class CardPayment extends Payment {
    String getTransactionNumber();
    ...
}

@Entity
class CheckPayment extends Payment {
    String getDriversLicenseNumber() {...}
    ...
}

Session session = ...;
session.save( new CardPayment( 1, ... ) );
...

Session session = ...;
Payment p = session.load( Payment.class, 1 );
MonetaryAmount paidAmount = p.getAmount();

应用程序已要求 Hibernate 加载具有 id 为 1Payment。然而,在这个时候,Hibernate 还不知道 Payment#1CardPayment 还是 CheckPayment。它将不知道,直到代理初始化。

调用 p.getAmount() 会触发代理的初始化。在这个时刻,Hibernate 知道 Payment#1 引用实际上指的是一个 CardPayment。此时,Hibernate 需要实例化一个 CardPayment。问题是,Hibernate 没有办法将应用程序持有的 p 引用“交换”成 CardPayment 类型,而不是更通用的 Payment 类型。

Java 代理方法没有这个限制。因为 Java 代理封装了“真实”实体,Hibernate 可以推迟确定实体类型的时刻,直到代理初始化。应用程序仍然持有代理的引用,而代理提供了一个到真实实体的间接引用。

总结

使用增强代理可以提供一种更高效的懒加载方法。因为它是在现有的增强之上工作的,所以它可以无缝地与 Hibernate 支持的其他基于增强的特性(如脏检查等)一起工作。

试试看,并告诉我们您的想法。


返回顶部