Micronaut集成Thymeleaf模板时#request对象为空问题咨询
问题背景
我正在开发一个使用Micronaut 2.57、Thymeleaf 3.0.12、micronaut-views-thymeleaf 2.21的应用,用于渲染视图。根据Thymeleaf官方文档说明,模板中应该可以直接使用#request对象,但实际使用时该对象始终为null。
复现代码
模板代码
<!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <title>Home</title> </head> <body> <h3>#request.contextPath</h3> <span th:utext="${#request.contextPath}"></span> <h3>#request.requestURI</h3> <span th:utext="${#request.requestURI}"></span> <h3>#request.requestURL</h3> <span th:utext="${#request.requestURL}"></span> </body> </html>
控制器代码
import io.micronaut.http.HttpResponse; import io.micronaut.http.annotation.Controller; import io.micronaut.http.annotation.Get; import io.micronaut.views.View; @Controller public class HomeController { @View("home2") @Get("/") public HttpResponse<Object> showHome() { return HttpResponse.ok(); } }
报错信息
启动应用后访问localhost:8080,出现如下堆栈报错,提示#request对象不存在:
17:52:57.326 [main] INFO io.micronaut.runtime.Micronaut - Startup completed in 2231ms. Server Running: http://localhost:8080 17:53:06.644 [io-executor-thread-1] ERROR org.thymeleaf.TemplateEngine - [THYMELEAF][io-executor-thread-1] Exception processing template "home2": Exception evaluating OGNL expression: "#request.contextPath" (template: "home2" - line 8, col 7) org.thymeleaf.exceptions.TemplateProcessingException: Exception evaluating OGNL expression: "#request.contextPath" (template: "home2" - line 8, col 7) at org.thymeleaf.standard.expression.OGNLVariableExpressionEvaluator.evaluate(OGNLVariableExpressionEvaluator.java:191) at org.thymeleaf.standard.expression.OGNLVariableExpressionEvaluator.evaluate(OGNLVariableExpressionEvaluator.java:95) at org.thymeleaf.standard.expression.VariableExpression.executeVariableExpression(VariableExpression.java:166) at org.thymeleaf.standard.expression.SimpleExpression.executeSimple(SimpleExpression.java:66) at org.thymeleaf.standard.expression.Expression.execute(Expression.java:109) at org.thymeleaf.standard.expression.Expression.execute(Expression.java:138) at org.thymeleaf.standard.processor.StandardUtextTagProcessor.doProcess(StandardUtextTagProcessor.java:87) at org.thymeleaf.processor.element.AbstractAttributeTagProcessor.doProcess(AbstractAttributeTagProcessor.java:74) at org.thymeleaf.processor.element.AbstractElementTagProcessor.process(AbstractElementTagProcessor.java:95) at org.thymeleaf.util.ProcessorConfigurationUtils$ElementTagProcessorWrapper.process(ProcessorConfigurationUtils.java:633) at org.thymeleaf.engine.ProcessorTemplateHandler.handleOpenElement(ProcessorTemplateHandler.java:1314) at org.thymeleaf.engine.OpenElementTag.beHandled(OpenElementTag.java:205) at org.thymeleaf.engine.TemplateModel.process(TemplateModel.java:136) at org.thymeleaf.engine.TemplateManager.parseAndProcess(TemplateManager.java:661) at org.thymeleaf.TemplateEngine.process(TemplateEngine.java:1098) at org.thymeleaf.TemplateEngine.process(TemplateEngine.java:1067) at io.micronaut.views.thymeleaf.ThymeleafViewsRenderer.render(ThymeleafViewsRenderer.java:109) at io.micronaut.views.thymeleaf.ThymeleafViewsRenderer.lambda$render$1(ThymeleafViewsRenderer.java:96) at io.micronaut.core.io.Writable.writeTo(Writable.java:77) at io.micronaut.http.server.netty.RoutingInBoundHandler.lambda$encodeHttpResponse$7(RoutingInBoundHandler.java:1683) at io.micronaut.scheduling.instrument.InvocationInstrumenterWrappedRunnable.run(InvocationInstrumenterWrappedRunnable.java:47) at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128) at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628) at java.base/java.lang.Thread.run(Thread.java:829) Caused by: ognl.OgnlException: source is null for getProperty(null, "contextPath") at ognl.OgnlRuntime.getProperty(OgnlRuntime.java:3229) at ognl.ASTProperty.getValueBody(ASTProperty.java:114) at ognl.SimpleNode.evaluateGetValueBody(SimpleNode.java:212) at ognl.SimpleNode.getValue(SimpleNode.java:258) at ognl.ASTChain.getValueBody(ASTChain.java:141) at ognl.SimpleNode.evaluateGetValueBody(SimpleNode.java:212) at ognl.SimpleNode.getValue(SimpleNode.java:258) at ognl.Ognl.getValue(Ognl.java:537) at ognl.Ognl.getValue(Ognl.java:501) at org.thymeleaf.standard.expression.OGNLVariableExpressionEvaluator.executeExpression(OGNLVariableExpressionEvaluator.java:328) at org.thymeleaf.standard.expression.OGNLVariableExpressionEvaluator.evaluate(OGNLVariableExpressionEvaluator.java:170) ... 23 common frames omitted
其他内置变量测试结果
#locale- 正常可用#ctx- 正常可用#request- 始终为null#response- 始终为null#session- 始终为null#servletContext- 始终为null
排查结论
已知io.micronaut.views.thymeleaf.ThymeleafViewsRenderer会给渲染引擎传递WebContext,调试发现contextVariableMap中包含的key列表为[ctx, root, vars, object, locale, request, response, session, servletContext, conversions, uris, calendars, dates, bools, numbers, objects, strings, arrays, lists, sets, maps, aggregates, messages, ids, execInfo, httpServletRequest, httpSession],但其中的servletContext、httpServletRequest、request取值均为null。
进一步排查org.thymeleaf.standard.expression.StandardExpressionObjectFactory类的逻辑:
if (REQUEST_EXPRESSION_OBJECT_NAME.equals(expressionObjectName)) { if (context instanceof IWebContext) { return ((IWebContext) context).getRequest(); } return null; }
发现传入的上下文类型为WebEngineContext,但instanceof判断IWebContext结果为false。最终定位到根本原因:Micronaut中创建的WebEngineContext属于io.micronaut.views.thymeleaf.WebEngineContext类,继承自EngineContext,和Thymeleaf原生实现了IWebContext接口的org.thymeleaf.context.WebEngineContext不是同一个类,因此无法被Thymeleaf的内置对象工厂识别,无法正确注入request等对象。
解决方案
方案1:升级依赖版本
该问题属于Micronaut Thymeleaf集成模块的历史缺陷,在micronaut-views-thymeleaf 3.0及以上版本中已经修复,将Micronaut框架及对应视图模块升级到3.x最新稳定版即可原生支持#request等内置对象。
方案2:适配内置对象调用方式
Micronaut自定义的WebEngineContext本身提供了getRequest公共方法,无需额外配置,直接在模板中使用${#ctx.getRequest()}即可获取request对象,对应属性可以通过${#ctx.getRequest().getContextPath()}的方式读取。
方案3:手动注入request属性
临时解决方案,可以在控制器中注入HttpRequest对象,放入返回的模型数据中,模板中直接读取对应属性即可:
import io.micronaut.http.HttpRequest; import io.micronaut.http.HttpResponse; import io.micronaut.http.annotation.Controller; import io.micronaut.http.annotation.Get; import io.micronaut.views.View; import java.util.Map; @Controller public class HomeController { @View("home2") @Get("/") public HttpResponse<Map<String, Object>> showHome(HttpRequest<?> request) { return HttpResponse.ok(Map.of( "contextPath", request.getContextPath(), "requestURI", request.getUri(), "requestURL", request.getServerAddress() + request.getUri() )); } }
模板中直接用${contextPath}等对应key取值即可。
内容的提问来源于stack exchange,提问作者Jeffrey Devloo

