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

Add support for record DTOs

Open kleino opened this issue 2 years ago • 2 comments

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!

kleino avatar Jul 01 '22 19:07 kleino

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.

mhalbritter avatar Jul 04 '22 08:07 mhalbritter

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:

  • Add support for model.addAttribute via #1152
  • Support an annotation on the said DTO/record

sdeleuze avatar Jul 05 '22 08:07 sdeleuze

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