Search code examples
javaspringkotlinspring-aopkotlin-interop

JUnit, @ControllerAdvice and lack of checked exceptions in Kotlin


I wrote a validation advisor in Kotlin which throws EntityValidationException when validation fails:

@Aspect
@Named
class ValidationAdvisor
@Inject constructor(val validator: EntityValidator) {
    @Around(EVERY_SAVE_AND_UPDATE_TO_DATABASE)
    fun validate(point: ProceedingJoinPoint): Any {
        val result: List<ConstraintViolation<Any>> = validator.validate(getEntity(point))
        if (isEntityValid(result))
            return point.proceed()

        throw EntityValidationException(
            violationInfos = result as List<ConstraintViolationInfo>
        )
    }

    private fun getEntity(point: ProceedingJoinPoint): Any {
        return point.args[0]
    }

    private fun isEntityValid(result: List<ConstraintViolation<Any>>): Boolean {
        return result.isEmpty()
    }

    companion object {
        const val EVERY_SAVE_AND_UPDATE_TO_DATABASE = "execution(* beans.repositories.BaseRepository.save(..))"
    }
}

I have a JUnit test (I write some parts in Kotlin and some in Java, to learn Kotlin in existing private project) which checks if exception is thrown:

@Test(expected = EntityValidationException.class)
public void test_WhenNameIsNotPresent_ThenExceptionIsThrown() throws Exception {
    repository.save(/* entity to save */);
}

But then I saw that test fails because it throws UndeclaredThrowableException instead of EntityValidationException. Next, I read that Kotlin does not have checked exception. I think not only me have this problem - I am pretty sure that way of handling exceptions with @ControllerAdvice in Spring and Java also may be problematic when we decide to switch to Kotlin in production projects - because it rely on class which is thrown, and throwing UndeclaredThrowableException take away from us this possibility. Is there a way to by-pass this behaviour, to make sure that is possible to switch to Kotlin and old Spring way of handling exceptions (and unit tests for ValidationAdvisor) will remain unchanged? Thank you in advance for any help.

P.S. Github repo for project is available here: Playgroud for Kotlin, Spring Data etc..

P.S.2 Stacktrace:

at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:191)
    at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
    at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
    at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:51)
    at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:237)
    at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:497)
    at com.intellij.rt.execution.application.AppMain.main(AppMain.java:147)
Caused by: java.lang.reflect.UndeclaredThrowableException
    at com.sun.proxy.$Proxy65.save(Unknown Source)
    at ActorValidationTest.test_WhenNameIsNotPresent_ThenExceptionIsThrown(ActorValidationTest.java:26)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:497)
    at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
    at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
    at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
    at org.junit.internal.runners.statements.ExpectException.evaluate(ExpectException.java:19)
    ... 26 more
Caused by: beans.validator.exceptions.EntityValidationException
    at beans.aop.validation.ValidationAdvisor.validate(ValidationAdvisor.kt:23)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:497)
    at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethodWithGivenArgs(AbstractAspectJAdvice.java:629)
    at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethod(AbstractAspectJAdvice.java:618)
    at org.springframework.aop.aspectj.AspectJAroundAdvice.invoke(AspectJAroundAdvice.java:70)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
    at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:92)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
    at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:213)
    ... 37 more

For me looks like EntityValidationException (which I want to be thrown) causes UndeclaredThrowableException. EntityValidationException Kotlin class code:

package beans.validator.exceptions

import beans.validator.constraintViolation.ConstraintViolationInfo

class EntityValidationException (val violationInfos: List<ConstraintViolationInfo>) : Exception() {
}

P.S.3 This also didn't work:

@Around(EVERY_SAVE_AND_UPDATE_TO_DATABASE)
@Throws(EntityValidationException::class)
fun validate(point: ProceedingJoinPoint): Any /* rest of code*/

P.S.4 Maybe the problem is that @Aspect is @Around Spring Data repository?

companion object {
        const val EVERY_SAVE_AND_UPDATE_TO_DATABASE = "execution(* beans.repositories.BaseRepository.save(..))"
    }
....
@NoRepositoryBean
public interface BaseRepository<ENTITY, IDENTIFIER extends Serializable>
        extends JpaRepository<ENTITY, IDENTIFIER>, JpaSpecificationExecutor<ENTITY> {
}

Solution

  • You're trying to make the method save() or your repository throw a checked exception that it doesn't declare. So even if the aspect declares it, that can't work. You need to make your exception extend RuntimeException.