kotlin使用spring-data-jpa+query-dsl踩坑记录

1,162 阅读3分钟

背景

​ 使用kotlin+springboot+spring-data-jpa搭建的项目,在引入query-dsl依赖来提供通用查询服务时出现以下错误:

image.png

原来的代码

Entity

@Entity
@Table
class User {

    var username: String ? =null

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    var id : Long ? = null

}

Repository

interface UserRepository : JpaRepository<User, Long>, QuerydslPredicateExecutor<User> {
}

Rest

@RestController
@RequestMapping("/user")
class UserRest(private val userRepository: UserRepository) {


    @GetMapping
    fun userList(pageable: Pageable): Page<User>{
        return userRepository.findAll( pageable)
    }

    @PostMapping
    fun addUser(@RequestBody user: User): Long? {
        return userRepository.save(user).id
    }

}

修改后的代码

Repository

interface UserRepository : JpaRepository<User, Long>, QuerydslPredicateExecutor<User>,
    QuerydslBinderCustomizer<QUser> {

    override fun customize(bindings: QuerydslBindings, root: QUser) {
        //修改查询,让username字段支持模糊查询,且忽略大小写
        bindings.bind(root.username)
            .first { path, value -> path.containsIgnoreCase(value) }
    }
}

代码中的QUserquery-dsl生成的类,包含User的各种信息,用于组成dsl风格的查询语法

Rest

@RestController
@RequestMapping("/user")
class UserRest(private val userRepository: UserRepository) {


    @GetMapping
    fun userList(@QuerydslPredicate(root = User::class) predicate: Predicate,pageable: Pageable): Page<User>{
        return userRepository.findAll(predicate, pageable)
    }

    @PostMapping
    fun addUser(@RequestBody user: User): Long? {
        return userRepository.save(user).id
    }

}

排查

​ 首先看错误语句No property customize found for type User!,翻译出来看是User没有可自定义的字段,很迷惑,感觉跟改动的代码扯不上什么关系,看不懂o(╥﹏╥)o

​ 这只能debug了,把断点打在org.springframework.data.mapping.PropertyPath#PropertyPath方法,第90行,也就是抛出错误的地方看看:

image.png

​ 可以看到这里原来是多出来一个叫customize的字段,但我们实际上又没有这个字段,所以获取字段信息的时候报错了,跟着堆栈往上看看为什么会多出来这么一个字段吧!一直往上找,知道找到这个customize字段的来源,可以看到PartTreeJpaQuery构造方法,第89行,原来是这里把之前写的customize方法当做字段名来解析了。

image.png

​ 看来是被识别为jpa的一个查询方法了,按理来说,customize方法是接口默认方法,不应该被识别出来的,我再往上看看(⊙︿⊙)

​ 追到QueryExecutorMethodInterceptor#mapMethodsToQuery方法,可以看到这里应该是获取了所有查询方法去解析出对应的查询sql,看来是这个获取所有查询方法的方法里错误地将customize方法也放了进来。

image.png

​ 点进去看一下,在RepositoryInformation的默认实现DefaultRepositoryInformation.getQueryMethods中,可以看到调用了isQueryMethodCandidate方法来判断是否是查询方法。

image.png

​ 看一下这个方法的逻辑:

image.png

​ 可以看到是有是否是默认方法的判断的,为什么会失效呢,debug到这里看一下

image.png

​ 可以看到,customize方法到这里的时候,并不是default的,甚至可以看到abstract修饰符,这有点不太符合常识,考虑到kotlin实际上是把代码编译成java的字节码在jvm上执行,有可能是kotlin在编译的时候并没有将这个方法编译为default方法,那就反编译出java代码来看一下:

image.png

​ 原来并不是我们想的那样编译为了默认方法,而是生成了一个静态内部类,将原本customize方法的逻辑迁到了静态内部类中,这样子,jpa肯定会认为customize方法是查询方法了

原因

​ 搜索了一下相关资料,在这篇文章Kotlin JVM常用注解参数解析中找到了答案:

image.png

​ 总而言之,就是kotlin为了适配1.8之前的java版本,采取了另一种方式来实现kotlin接口的默认实现,而springjpa使用jdk1.8之后的逻辑来扫描接口方法,就导致了问题的出现.

解决

​ 上面的文章中已经说明了如何解决问题,我们来尝试一下。

修改Repository

interface UserRepository : JpaRepository<User, Long>, QuerydslPredicateExecutor<User>,
    QuerydslBinderCustomizer<QUser> {

    @JvmDefault
    override fun customize(bindings: QuerydslBindings, root: QUser) {
        bindings.bind(root.username)
            .first { path, value -> path.containsIgnoreCase(value) }
    }
}

在gradle配置文件中增加jvm参数

tasks.withType<KotlinCompile> {
    kotlinOptions {
        freeCompilerArgs = listOf("-Xjsr305=strict","-Xjvm-default=enable")
        jvmTarget = "11"
    }
}

​ 再启动项目,发现已经不抱错了,再看一下反编译出来的的java代码:

image.png

​ 可以看到已经没有静态内部类了,customize方法也已经是default方法了,问题解决.

​ 新版本的kotlin已经将@JvmDefault标记为Deprecated了,后续可以使用jvm参数-Xjvm-default=all-compatibility-Xjvm-default=all将所有接口类统一处理

引用

Kotlin JVM常用注解参数解析