背景
使用kotlin+springboot+spring-data-jpa搭建的项目,在引入query-dsl依赖来提供通用查询服务时出现以下错误:
原来的代码
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) }
}
}
代码中的
QUser
是query-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行,也就是抛出错误的地方看看:
可以看到这里原来是多出来一个叫customize
的字段,但我们实际上又没有这个字段,所以获取字段信息的时候报错了,跟着堆栈往上看看为什么会多出来这么一个字段吧!一直往上找,知道找到这个customize
字段的来源,可以看到PartTreeJpaQuery
构造方法,第89行,原来是这里把之前写的customize
方法当做字段名来解析了。
看来是被识别为jpa的一个查询方法了,按理来说,customize
方法是接口默认方法,不应该被识别出来的,我再往上看看(⊙︿⊙)
追到QueryExecutorMethodInterceptor#mapMethodsToQuery
方法,可以看到这里应该是获取了所有查询方法去解析出对应的查询sql,看来是这个获取所有查询方法的方法里错误地将customize
方法也放了进来。
点进去看一下,在RepositoryInformation
的默认实现DefaultRepositoryInformation.getQueryMethods
中,可以看到调用了isQueryMethodCandidate
方法来判断是否是查询方法。
看一下这个方法的逻辑:
可以看到是有是否是默认方法的判断的,为什么会失效呢,debug到这里看一下
可以看到,customize
方法到这里的时候,并不是default
的,甚至可以看到abstract
修饰符,这有点不太符合常识,考虑到kotlin实际上是把代码编译成java的字节码在jvm上执行,有可能是kotlin在编译的时候并没有将这个方法编译为default方法,那就反编译出java代码来看一下:
原来并不是我们想的那样编译为了默认方法,而是生成了一个静态内部类,将原本customize
方法的逻辑迁到了静态内部类中,这样子,jpa肯定会认为customize
方法是查询方法了
原因
搜索了一下相关资料,在这篇文章Kotlin JVM常用注解参数解析中找到了答案:
总而言之,就是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代码:
可以看到已经没有静态内部类了,customize
方法也已经是default方法了,问题解决.
附
新版本的kotlin已经将@JvmDefault
标记为Deprecated
了,后续可以使用jvm参数-Xjvm-default=all-compatibility
或-Xjvm-default=all
将所有接口类统一处理