spring-native icon indicating copy to clipboard operation
spring-native copied to clipboard

Add reflection hints for custom validation annotations

Open RichardJd opened this issue 3 years ago • 4 comments

Hello,

I'm trying to create a annotation custom validation, when i execute this in JAR file, work fine, but when i execute a native image i get this error

CustomAnnotation:

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = MaxPageSizeValidator.class)
public @interface MaxPageSizeValidation {

    String message() default "";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};
}

And the class:

@RequiredArgsConstructor
public class MaxPageSizeValidator implements ConstraintValidator<MaxPageSizeValidation, Integer> {

    private final Internationalization internationalization;

    @Override
    public boolean isValid(Integer pageSize, ConstraintValidatorContext constraintValidatorContext) {
        if (pageSize > 1000) {
            throw new MyException(
                Set.of(
                    ErrorResponse.valueOf(
                        String.valueOf(HttpStatus.UNPROCESSABLE_ENTITY.value()),
                        ErrorCode.INVALID_PARAMETER.name(),
                        internationalization.getMessage(constraintValidatorContext.getDefaultConstraintMessageTemplate())
                    )
                )
            );
        }
        return true;
    }
}

My controller is like this:

@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/endpoint")
public class MyController {

    private final MyHandler myHandler;

    @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity<MyResponse> findAll(
        @RequestParam(value = "page", required = false, defaultValue = "1")
        @Min(value = 1, message = "{page.must-be-greater-than-or-equal-1}")
            Integer page,
        @RequestParam(value = "page-size", required = false, defaultValue = "10")
        @Min(value = 1, message = "{page-size.must-be-greater-than-0}")
        @MaxPageSizeValidation(message = "{page-size.must-be-between-0-and-1000}")
            Integer pageSize
    ) {
        return ResponseEntity.ok().body(myHandler.findAll(page, pageSize));
    }
}

And then when i call endpoint, the error:

javax.validation.ConstraintDefinitionException: HV000074: br.com.rj.systems.pocserviceproviders.external.validations.annotations.MaxPageSizeValidation contains Constraint annotation, but does not contain a message parameter.
	at org.hibernate.validator.internal.metadata.core.ConstraintHelper.assertMessageParameterExists(ConstraintHelper.java:1057) ~[na:na]
	at org.hibernate.validator.internal.metadata.core.ConstraintHelper.lambda$isConstraintAnnotation$3(ConstraintHelper.java:1003) ~[na:na]
	at java.util.concurrent.ConcurrentHashMap.computeIfAbsent(ConcurrentHashMap.java:1705) ~[na:na]
	at org.hibernate.validator.internal.metadata.core.ConstraintHelper.isConstraintAnnotation(ConstraintHelper.java:1002) ~[na:na]
	at org.hibernate.validator.internal.metadata.provider.AnnotationMetaDataProvider.findConstraintAnnotations(AnnotationMetaDataProvider.java:534) ~[na:na]
	at org.hibernate.validator.internal.metadata.provider.AnnotationMetaDataProvider.findConstraints(AnnotationMetaDataProvider.java:479) ~[na:na]
	at org.hibernate.validator.internal.metadata.provider.AnnotationMetaDataProvider.getParameterMetaData(AnnotationMetaDataProvider.java:412) ~[na:na]
	at org.hibernate.validator.internal.metadata.provider.AnnotationMetaDataProvider.findExecutableMetaData(AnnotationMetaDataProvider.java:308) ~[na:na]
	at org.hibernate.validator.internal.metadata.provider.AnnotationMetaDataProvider.getMetaData(AnnotationMetaDataProvider.java:292) ~[na:na]
	at org.hibernate.validator.internal.metadata.provider.AnnotationMetaDataProvider.getMethodMetaData(AnnotationMetaDataProvider.java:279) ~[na:na]
	at org.hibernate.validator.internal.metadata.provider.AnnotationMetaDataProvider.retrieveBeanConfiguration(AnnotationMetaDataProvider.java:130) ~[na:na]
	at org.hibernate.validator.internal.metadata.provider.AnnotationMetaDataProvider.getBeanConfiguration(AnnotationMetaDataProvider.java:120) ~[na:na]
	at org.hibernate.validator.internal.metadata.BeanMetaDataManagerImpl.getBeanConfigurationForHierarchy(BeanMetaDataManagerImpl.java:234) ~[na:na]
	at org.hibernate.validator.internal.metadata.BeanMetaDataManagerImpl.createBeanMetaData(BeanMetaDataManagerImpl.java:201) ~[na:na]
	at org.hibernate.validator.internal.metadata.BeanMetaDataManagerImpl.getBeanMetaData(BeanMetaDataManagerImpl.java:165) ~[na:na]
	at org.hibernate.validator.internal.engine.ValidatorImpl.validateParameters(ValidatorImpl.java:267) ~[na:na]
	at org.hibernate.validator.internal.engine.ValidatorImpl.validateParameters(ValidatorImpl.java:235) ~[na:na]
	at org.springframework.validation.beanvalidation.MethodValidationInterceptor.invoke(MethodValidationInterceptor.java:110) ~[na:na]
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[na:na]
	at org.springframework.aop.framework.Interceptors$DynamicAdvisedInterceptor.intercept(Interceptors.java:120) ~[na:na]
	at br.com.rj.systems.pocserviceproviders.external.controller.ServiceProviderController$$SpringProxy$43f1de91.findAll(Unknown Source) ~[poc-service-providers:0.0.1-SNAPSHOT]
	at java.lang.reflect.Method.invoke(Method.java:566) ~[na:na]
	at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:205) ~[na:na]
	at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:150) ~[na:na]
	at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:117) ~[na:na]
	at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:895) ~[na:na]
	at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:808) ~[na:na]
	at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87) ~[na:na]
	at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1067) ~[na:na]
	at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:963) ~[na:na]
	at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006) ~[na:na]
	at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:898) ~[na:na]
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:655) ~[poc-service-providers:4.0.FR]
	at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883) ~[na:na]
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:764) ~[poc-service-providers:4.0.FR]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:227) ~[na:na]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[na:na]
	at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53) ~[poc-service-providers:9.0.60]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[na:na]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[na:na]
	at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100) ~[na:na]
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117) ~[na:na]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[na:na]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[na:na]
	at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93) ~[na:na]
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117) ~[na:na]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[na:na]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[na:na]
	at org.springframework.boot.actuate.metrics.web.servlet.WebMvcMetricsFilter.doFilterInternal(WebMvcMetricsFilter.java:96) ~[na:na]
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117) ~[na:na]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[na:na]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[na:na]
	at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201) ~[na:na]
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117) ~[na:na]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[na:na]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[na:na]
	at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:197) ~[na:na]
	at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:97) ~[na:na]
	at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:541) ~[na:na]
	at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:135) ~[na:na]
	at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92) ~[poc-service-providers:9.0.60]
	at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:78) ~[na:na]
	at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:360) ~[na:na]
	at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:399) ~[na:na]
	at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65) ~[na:na]
	at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:889) ~[na:na]
	at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1743) ~[na:na]
	at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) ~[na:na]
	at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191) ~[na:na]
	at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659) ~[na:na]
	at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) ~[na:na]
	at java.lang.Thread.run(Thread.java:829) ~[na:na]
	at com.oracle.svm.core.thread.JavaThreads.threadStartRoutine(JavaThreads.java:597) ~[na:na]
	at com.oracle.svm.core.posix.thread.PosixJavaThreads.pthreadStartRoutine(PosixJavaThreads.java:194) ~[na:na]

Edit: I'm using spring-boot-starter-parent version 2.6.5 and spring-native.version 0.11.3.

RichardJd avatar Apr 04 '22 16:04 RichardJd

If you'd like us to spend some time investigating, please take the time to provide a complete minimal sample (something that we can unzip or git clone, build, and deploy) that reproduces the problem.

mhalbritter avatar Apr 05 '22 08:04 mhalbritter

I created a example here: https://github.com/RichardJd/custom-validation-spring-native

Bellow the commands i execute:

  • build and generate native image: mvn -DskipTests=true -Pnative clean package

  • execute image: ./target/custom-validation

Access endpoint: http://localhost:8080/animes?id=1 (here the error will happen).

If execute jar how bellow, and access endpoint it's not happen error.

execute jar: java -jar ./target/custom-validation-0.0.1-SNAPSHOT-exec.jar

RichardJd avatar Apr 05 '22 17:04 RichardJd

Thanks for the example.

The reason this fails is because the validator uses reflection on the custom validation annotation to call the message method. This method is not in the native image, because reflection hints are missing.

This is a bug in spring-native, as we could find all @Constraint annotated annotations and then generate reflection hints for them.

mhalbritter avatar Apr 06 '22 08:04 mhalbritter

I've moved the issue to the backlog. We will take care of this in Spring Boot 3 / Spring Framework 6. In the meantime, you can work around by adding @TypeHints.

mhalbritter avatar Jun 13 '22 09:06 mhalbritter

Spring Native is now superseded by Spring Boot 3 official native support, see the related reference documentation for more details.

As a consequence, I am closing this issue, and recommend trying your use case with latest Spring Boot 3 version. If you still experience the issue reported here, please open an issue directly on the related Spring project (Spring Framework, Data, Security, Boot, Cloud, etc.) with a reproducer.

Thanks for your contribution on the experimental Spring Native project, we hope you will enjoy the official native support introduced by Spring Boot 3.

sdeleuze avatar Jan 02 '23 11:01 sdeleuze