spring-attic / spring-native

Spring Native is now superseded by Spring Boot 3 official native support
https://docs.spring.io/spring-boot/docs/current/reference/html/native-image.html
Apache License 2.0
2.74k stars 355 forks source link

Add support for record DTOs #1656

Closed kleino closed 1 year ago

kleino commented 2 years ago

Hi, I have an issue acessing dtos/records in a thymeleaf-template when using a docker image generated via spring-native. It works perfectly fine if BP_NATIVE_IMAGE = false

Controller

# Data classes
public class SomeDto {
    private final String name;
    # getter / setter
}
public record SomeRecord(String name) {

}

# IndexController
@Controller
public class IndexController {
    @GetMapping
    public String index(Model model) {
        model.addAttribute("someRecord", new SomeRecord("My Name"));
        model.addAttribute("someDto", new SomeDto("My Name"));
        return "index";
    }
}
# index.html
    ...
    <h2 th:text="${someDto.name}">Selected</h2>
    <h2 th:text="${someRecord.name()}">Selected</h2>
   ...

When opening the page, this results in following exception:

2022-07-01 19:15:31.765 ERROR 1 --- [nio-8080-exec-1] org.thymeleaf.TemplateEngine             : [THYMELEAF][http-nio-8080-exec-1] Exception processing template "index": Exception evaluating SpringEL expression: "someDto.getName()" (template: "index" - line 13, col 9)

org.thymeleaf.exceptions.TemplateProcessingException: Exception evaluating SpringEL expression: "someDto.getName()" (template: "index" - line 13, col 9)
        at org.thymeleaf.spring5.expression.SPELVariableExpressionEvaluator.evaluate(SPELVariableExpressionEvaluator.java:292) ~[na:na]
        at org.thymeleaf.standard.expression.VariableExpression.executeVariableExpression(VariableExpression.java:166) ~[na:na]
        at org.thymeleaf.standard.expression.SimpleExpression.executeSimple(SimpleExpression.java:66) ~[com.example.demo.DemoApplication:3.0.15.RELEASE]
        at org.thymeleaf.standard.expression.Expression.execute(Expression.java:109) ~[com.example.demo.DemoApplication:3.0.15.RELEASE]
        at org.thymeleaf.standard.expression.Expression.execute(Expression.java:138) ~[com.example.demo.DemoApplication:3.0.15.RELEASE]
        at org.thymeleaf.standard.processor.AbstractStandardExpressionAttributeTagProcessor.doProcess(AbstractStandardExpressionAttributeTagProcessor.java:144) ~[com.example.demo.DemoApplication:3.0.15.RELEASE]
        at org.thymeleaf.processor.element.AbstractAttributeTagProcessor.doProcess(AbstractAttributeTagProcessor.java:74) ~[com.example.demo.DemoApplication:3.0.15.RELEASE]
        at org.thymeleaf.processor.element.AbstractElementTagProcessor.process(AbstractElementTagProcessor.java:95) ~[com.example.demo.DemoApplication:3.0.15.RELEASE]
        at org.thymeleaf.util.ProcessorConfigurationUtils$ElementTagProcessorWrapper.process(ProcessorConfigurationUtils.java:633) ~[na:na]
        at org.thymeleaf.engine.ProcessorTemplateHandler.handleOpenElement(ProcessorTemplateHandler.java:1314) ~[na:na]
        at org.thymeleaf.engine.OpenElementTag.beHandled(OpenElementTag.java:205) ~[na:na]
        at org.thymeleaf.engine.TemplateModel.process(TemplateModel.java:136) ~[com.example.demo.DemoApplication:3.0.15.RELEASE]
        at org.thymeleaf.engine.TemplateManager.parseAndProcess(TemplateManager.java:661) ~[na:na]
        at org.thymeleaf.TemplateEngine.process(TemplateEngine.java:1098) ~[com.example.demo.DemoApplication:3.0.15.RELEASE]
        at org.thymeleaf.TemplateEngine.process(TemplateEngine.java:1072) ~[com.example.demo.DemoApplication:3.0.15.RELEASE]
        at org.thymeleaf.spring5.view.ThymeleafView.renderFragment(ThymeleafView.java:366) ~[com.example.demo.DemoApplication:3.0.15.RELEASE]
        at org.thymeleaf.spring5.view.ThymeleafView.render(ThymeleafView.java:190) ~[com.example.demo.DemoApplication:3.0.15.RELEASE]
        at org.springframework.web.servlet.DispatcherServlet.render(DispatcherServlet.java:1401) ~[com.example.demo.DemoApplication:5.3.21]
        at org.springframework.web.servlet.DispatcherServlet.processDispatchResult(DispatcherServlet.java:1145) ~[com.example.demo.DemoApplication:5.3.21]
        at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1084) ~[com.example.demo.DemoApplication:5.3.21]
        at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:963) ~[com.example.demo.DemoApplication:5.3.21]
        at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006) ~[com.example.demo.DemoApplication:5.3.21]
        at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:898) ~[com.example.demo.DemoApplication:5.3.21]
        at javax.servlet.http.HttpServlet.service(HttpServlet.java:655) ~[com.example.demo.DemoApplication:4.0.FR]
        at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883) ~[com.example.demo.DemoApplication:5.3.21]
        at javax.servlet.http.HttpServlet.service(HttpServlet.java:764) ~[com.example.demo.DemoApplication: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) ~[com.example.demo.DemoApplication:9.0.64]
        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) ~[com.example.demo.DemoApplication:5.3.21]
        at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117) ~[com.example.demo.DemoApplication:5.3.21]
        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) ~[com.example.demo.DemoApplication:5.3.21]
        at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117) ~[com.example.demo.DemoApplication:5.3.21]
        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) ~[com.example.demo.DemoApplication:5.3.21]
        at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117) ~[com.example.demo.DemoApplication:5.3.21]
        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) ~[com.example.demo.DemoApplication:9.0.64]
        at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:135) ~[na:na]
        at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92) ~[com.example.demo.DemoApplication:9.0.64]
        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) ~[com.example.demo.DemoApplication:9.0.64]
        at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:890) ~[na:na]
        at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1787) ~[na:na]
        at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) ~[com.example.demo.DemoApplication:9.0.64]
        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:833) ~[com.example.demo.DemoApplication:na]
        at com.oracle.svm.core.thread.PlatformThreads.threadStartRoutine(PlatformThreads.java:704) ~[com.example.demo.DemoApplication:na]
        at com.oracle.svm.core.posix.thread.PosixPlatformThreads.pthreadStartRoutine(PosixPlatformThreads.java:202) ~[na:na]
Caused by: org.springframework.expression.spel.SpelEvaluationException: EL1004E: Method call: Method getName() cannot be found on type com.example.demo.SomeDto
        at org.springframework.expression.spel.ast.MethodReference.findAccessorForMethod(MethodReference.java:225) ~[na:na]
        at org.springframework.expression.spel.ast.MethodReference.getValueInternal(MethodReference.java:135) ~[na:na]
        at org.springframework.expression.spel.ast.MethodReference.access$000(MethodReference.java:55) ~[na:na]
        at org.springframework.expression.spel.ast.MethodReference$MethodValueRef.getValue(MethodReference.java:386) ~[na:na]
        at org.springframework.expression.spel.ast.CompoundExpression.getValueInternal(CompoundExpression.java:92) ~[na:na]
        at org.springframework.expression.spel.ast.SpelNodeImpl.getValue(SpelNodeImpl.java:112) ~[com.example.demo.DemoApplication:5.3.21]
        at org.springframework.expression.spel.standard.SpelExpression.getValue(SpelExpression.java:338) ~[na:na]
        at org.thymeleaf.spring5.expression.SPELVariableExpressionEvaluator.evaluate(SPELVariableExpressionEvaluator.java:265) ~[na:na]
        ... 60 common frames omitted

Minimum viable demo: demo.zip

OS: Kernel: 5.10.124-1-MANJARO x86_64 'org.springframework.boot' version '2.7.0' 'org.springframework.experimental.aot' version '0.12.0' java: 17.0.3-tem gradle: 7.4.1

Could you please provide some support here, on what I'm might missing here?

Thanks!

mhalbritter commented 2 years ago

The main problem here is using records as a DTO.

Method getName() cannot be found on type com.example.demo.SomeDto

You have to add some reflection hints to include all public methods from your DTO into the native image. Then Thmyeleaf with records should work.

I'll reword the issue and add it to the backlog.

sdeleuze commented 2 years ago

For this kind of use case, we don't have explicit trigger from the annotation based programming model, so 2 potential solutions that I am mentioning here more for discussion related to our upcoming Spring Boot 3 support:

sdeleuze commented 1 year ago

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.