0

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> {
}
Radek Anuszewski
  • 1,812
  • 8
  • 34
  • 62
  • In runtime checked exceptions doesn't exists. And checked exceptions exists only in compile time. Please show stack trace of `UndeclaredThrowableException`. – Ruslan Nov 20 '16 at 22:18
  • @IRus Question updated with stack trace - `EntityValidationException` is thrown intentionally, but problem is with `UndeclaredThrowableException` - `EntityValidationException` is written also in Kotlin and I will update question with its code within few seconds. – Radek Anuszewski Nov 20 '16 at 22:26
  • https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.jvm/-throws/ – JB Nizet Nov 20 '16 at 22:29
  • 1
    @JBNizet Thank you, but unfortunately it didn't help, annotation `@Throws(EntityValidationException::class)` don't work on `validate` method of `ValidatorAdvisor`. Maybe reason is that I use also `@Around` annotation, but even when I separate code to private method with annotation `@Throws` it didn't work either. – Radek Anuszewski Nov 20 '16 at 22:42
  • Maybe problem is that I created `@Aspect` `@Around` Spring Data repository? – Radek Anuszewski Nov 20 '16 at 22:55
  • Downloaded your project and can't find this test in it. Can you publish it as well? – Ruslan Nov 20 '16 at 23:01
  • @IRus I see test in repo: https://github.com/radek-anuszewski/film-site/blob/master/src/main/test/ActorValidationTest.java - first test in test class. – Radek Anuszewski Nov 20 '16 at 23:03
  • 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. – JB Nizet Nov 20 '16 at 23:04
  • That said, I'm not sure what you're trying to achieve with this: Hibernate validator can validate entities before they are saved, and most of the time, changes to entities are made without ever calling save(), thus bypassing your checks. – JB Nizet Nov 20 '16 at 23:06
  • @JBNizet Thank you - when `EntityValidationException` extends `RuntimeException` it worked, please place your comment as regular answer and then I can accept it. – Radek Anuszewski Nov 20 '16 at 23:07
  • @JBNizet as far as I know, in situation you describe changes to entities are made when transaction ends, and when I throw an exception it will be rolled back and changes would not be saved - but I may be wrong, cause I am front-end guy and don't do Hibernate/Spring profesionally. – Radek Anuszewski Nov 20 '16 at 23:11
  • Yes, that's right. But that's not the problem. The problem is that if you do, for example `Actor actor = repo.findById(1L); actor.setName(null);`, you'll never call the repository save() method, and the unwanted change will thus bypass your valication check and be saved in the database. – JB Nizet Nov 20 '16 at 23:16
  • @JBNizet I always try to do explicite saves/updated (`save()` or `update()` ale always called), but you are right I didn't know about that, I have to validate DTOs or create another object which will be validated - thank you very much for pointing the problem. – Radek Anuszewski Nov 20 '16 at 23:26

1 Answers1

2

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.

JB Nizet
  • 678,734
  • 91
  • 1,224
  • 1,255