Usage of findByIdOrNull Kotlin extension ignores proxy annotations
Opened this issue · 7 comments
Imagine you define a repository in Kotlin like:
@Repository
interface MyRepository : CrudRepository<MyObject, String> {
@Cacheable("myCache")
override fun findById(id: String): Optional<MyObject>
}
And you then try to use this repository with the extension function of findByIdOrNull, you'll find that the @Cacheable annotation is ignored.
This is due to the fact that the findByIdOrNull will proxy the request to the interface method of findById(Object) instead of findById(String).
As the first one doesn't have any annotation in place, the annotation on the overridden method is never used.
I'm wondering if making the extension an inline extension would already fix the problem (but I didn't try that).
Thanks for having a look into this.
Technically, you're defining a new method that is bridged into findById(Object) and the caching annotation only takes an effect if you call that exact method. findByIdOrNull is calling (wired to the bytecode) findById(Object) and the Kotlin extension doesn't know about the method accepting String.
Sure, I do understand the technical reasoning, but this is extremely easy to miss from a programmers perspective resulting in unwanted side-effects like caching not working (or other aspects that should be applied).
So from a coders point of view that should be resolved to avoid hard to find issues.
I stumbled upon a similar problem but with another annotation. Consider this repository:
interface AssignmentRepository : JpaRepository<Assignment, String> {
@EntityGraph(attributePaths = ["tags"])
override fun findById(id: String): Optional<Assignment>
}
In the service class, if I call assignmentRepository.findById() it works as expected, eagerly loading the tags but if I call assignmentRepository.findByIdOrNull() the annotation of the overridden method is ignored and the tags are not loaded.
Hi @christophstrobl,
I hope you are doing well. I'm looking for ways to contribute to the Spring Data Commons project and this particular issue (#3326) caught my attention. It's a very interesting problem regarding the interaction between Kotlin extensions and Spring AOP.
I noticed that this issue was assigned to you about a month ago. I just wanted to respectfully check if you are still actively working on it.
If your priorities have shifted or you haven't had a chance to start, I would be happy to investigate this further and work on a pull request. I've already looked into the discussion and agree that making the extension function inline seems like a promising approach.
Of course, no pressure at all if you are already on it. Just wanted to offer my help to keep the project moving forward.
Thanks for your time!
Thanks for reaching out. We hadn't the bandwidth yet to work on this one. Feel free to investigate the actual cause and how the issue could be resolved. On my mind, this ranges somewhere between using reflection and something inside the proxy interceptors.
@rla124 the submitted fix doesn't work and the test passes even with the inline change removed.
A reflective fix would look somewhat like:
inline fun <T: Any, reified ID: Any> CrudRepository<T, ID>.findByIdOrNull(id: ID): T? {
val findById = org.springframework.util.ReflectionUtils.findMethod(
this.javaClass,
"findById",
ID::class.java
)
if(findById != null) {
val mostSpecificMethod =
org.springframework.util.ClassUtils.getMostSpecificMethod(
findById,
this.javaClass
)
mostSpecificMethod.trySetAccessible()
val value = org.springframework.util.ReflectionUtils.invokeMethod(
mostSpecificMethod,
this,
id
) as Optional<T>
return value.orElse(null)
}
return findById(id).orElse(null)
}