Hibernate 一次奇特的异常

反射在 Entity 上的异常

前段时间在线上的 Hibernate 查询出来的 Entity 中反射调用其中 getId method 时,发生了 java.lang.IllegalArgumentException: object is not an instance of declaring class 异常。 由于是在查询出的一个 List 上依次进行反射,提前将反射的方法缓存了下来,却在某个对象上发生了这种异常。 以下是一份非常简单的复现代码(用 Kotlin 写,但 Java 类似,跟语言无关):

1
2
3
4
5
6
7
8
9
10
11
@Entity
class Post(
@get:ManyToOne(fetch = FetchType.LAZY, optional = false)
@get:JoinColumn(nullable = false, insertable = true, updatable = false, foreignKey = ForeignKey(ConstraintMode.NO_CONSTRAINT))
var post: Post?
) {
@Transient
@get:Id
@get:GeneratedValue(strategy = GenerationType.AUTO)
var id: Long = 0L
}

每个 Post 只有 id 和另一个 Post 的对象,在表中也就是主键 id 和一个 post_id 字段。

表中的数据如下:

id post_id
1 2
2 2

复现代码:

1
2
3
4
5
6
7
@Transactional
fun insertAndList() {
val post1 = postRepo.findOne(1L)
val post2 = postRepo.findOne(2L)
val method2 = post2::class.java.getMethod("getId")
val id1 = method2.invoke(post1) // Exception!!
}

调试

首先自然想到是 post1post2 的确属于两个不同的类,链接调试器后发现 post1 的类是 Post,而 post2 的类是 Post_$$_jvst38d_0,很明显这是一个被 Hibernate 子类化的类,除了原有的成员变量外,还多了个 handler: JavassistLazyInitializer 变量。

我们知道以下两个行为导致了这个行为:

  1. Hibernate 会对 LazyFetch 的对象生成代理对象,在 getId 以外的方法上才会真的去数据库中执行查询,因此代理对象必须是一个生成的类,原始类的成员显然无法做到动态进行查询。(注意这一行为仅限 Property-based access,可参考扩展阅读)
  2. Hibernate 在一个 session 中,被管理的 Entity 总是同一个 Java 对象,不论是被 findOnefindAll,或者被其它 Entity 关联的实体。

第一个 findOne 导致 post1 所关联的 id 为 2 的 Post 是一个代理对象,但是为了满足条件 2,Hibernate 必须在第二次 findOne 时返回那个代理对象。由于 post2 的类型是 post1 的子类,显然无法用 post2 的方法在 post1 上反射调用。

扩展阅读