Cannot expose bind macro helper springMacroRequestContext because of an existing model object of

本文介绍了一种在SpringBoot项目中遇到的Freemarker与siteMesh集成时出现的异常情况,详细解释了错误原因并提供了解决方案,即调整spring.freemarker.expose-spring-macro-helpers配置项。

环境 springBoot + Freemark + siteMesh

完整异常:
javax.servlet.ServletException: Cannot expose bind macro helper ‘springMacroRequestContext’ because of an existing model object of the same name
at org.springframework.web.servlet.view.AbstractTemplateView.renderMergedOutputModel(AbstractTemplateView.java:164)
at org.springframework.web.servlet.view.AbstractView.render(AbstractView.java:316)
at org.springframework.web.servlet.DispatcherServlet.render(DispatcherServlet.java:1373)
at org.springframework.web.servlet.DispatcherServlet.processDispatchResult(DispatcherServlet.java:1118)
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1057)
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:943)
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006)
at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:898)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:645)
at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:750)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:103)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:103)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.apache.catalina.core.ApplicationDispatcher.invoke(ApplicationDispatcher.java:712)
at org.apache.catalina.core.ApplicationDispatcher.processRequest(ApplicationDispatcher.java:461)
at org.apache.catalina.core.ApplicationDispatcher.doForward(ApplicationDispatcher.java:384)
at org.apache.catalina.core.ApplicationDispatcher.forward(ApplicationDispatcher.java:312)
at org.apache.catalina.core.StandardHostValve.custom(StandardHostValve.java:394)
at org.apache.catalina.core.StandardHostValve.status(StandardHostValve.java:253)
at org.apache.catalina.core.StandardHostValve.throwable(StandardHostValve.java:348)
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:173)
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92)
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74)
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:343)
at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:373)
at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65)
at org.apache.coyote.AbstractProtocolConnectionHandler.process(AbstractProtocol.java:868)atorg.apache.tomcat.util.net.NioEndpointConnectionHandler.process(AbstractProtocol.java:868) at org.apache.tomcat.util.net.NioEndpointConnectionHandler.process(AbstractProtocol.java:868)atorg.apache.tomcat.util.net.NioEndpointSocketProcessor.doRun(NioEndpoint.java:1590)
at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutorWorker.run(ThreadPoolExecutor.java:624)atorg.apache.tomcat.util.threads.TaskThreadWorker.run(ThreadPoolExecutor.java:624) at org.apache.tomcat.util.threads.TaskThreadWorker.run(ThreadPoolExecutor.java:624)atorg.apache.tomcat.util.threads.TaskThreadWrappingRunnable.run(TaskThread.java:61)
at java.lang.Thread.run(Thread.java:748)

解决方案:
该设置项调整为false

# 设定是否以springMacroRequestContext的形式暴露RequestContext给Spring’s macro library使用
spring.freemarker.expose-spring-macro-helpers=true

你遇到的错误: ``` Cannot read properties of undefined (reading 'resizeTextarea') ``` 说明:`this.$refs.textareaRef` 是 `undefined`,所以无法调用其方法 `resizeTextarea()`。 --- ## ✅ 错误原因分析 这个错误的根本原因是: > ❌ **`$refs.textareaRef` 没有正确获取到 Element Plus 的 `<el-input>` 组件实例** 这通常由以下几种情况导致: | 原因 | 说明 | |------|------| | 1. `ref` 绑定错误 | `ref` 写在了父容器(如 `el-form-item`)上,而不是 `<el-input>` 上 | | 2. 访问时机太早 | 在 DOM 渲染完成前就尝试访问 `$refs` | | 3. 条件渲染问题 | 使用 `v-if` 控制显示,但值更新时组件还未挂载 | | 4. 多个相同 ref 名称冲突 | 或拼写错误 | | 5. 使用了 `v-for` 导致 ref 变成数组或覆盖 | --- ## ✅ 正确解决方案 ### ✅ 场景:Vue 3 + Element Plus 中动态设置 textarea 值后手动触发 autosize ### ✅ 正确代码示例 ```vue <template> <el-form :model="form"> <!-- ✅ 关键:ref 必须绑定在 el-input 上 --> <el-form-item label="描述"> <el-input ref="textareaRef" v-model="form.description" type="textarea" :autosize="{ minRows: 2, maxRows: 6 }" placeholder="请输入内容" /> </el-form-item> </el-form> <el-button @click="loadData">加载数据</el-button> </template> <script setup> import { ref, nextTick } from 'vue'; const form = ref({ description: '' }); // 获取 template 中的 ref const textareaRef = ref(null); const loadData = () => { form.value.description = `这是一段很长的内容... 换行测试 自动撑开高度`; // ✅ 等待 DOM 更新后再访问组件实例 nextTick(() => { console.log(textareaRef.value); // 应该是一个 ElInput 实例 if (textareaRef.value) { // 调用 Element Plus 提供的 resize 方法 textareaRef.value.resize?.(); // ⚠️ 注意:不是 resizeTextarea,而是 resize! } else { console.warn('textareaRef 未绑定'); } }); }; </script> ``` --- ## 🔍 关键点解释 ### ✅ 1. `ref` 必须绑定在 `<el-input>` 上 ```html <!-- ✅ 正确 --> <el-input ref="textareaRef" type="textarea" /> <!-- ❌ 错误:ref 在外面 --> <el-form-item ref="textareaRef"> <el-input type="textarea" /> </el-form-item> ``` 只有绑定在组件本身才能拿到 `resize()` 方法。 --- ### ✅ 2. 使用 `nextTick`(Vue 2)或 `await nextTick()`(Vue 3) 确保 DOM 已经更新,`v-model` 的值已经渲染进 textarea。 ```js this.$nextTick(() => { ... }) // Vue 2 await nextTick() // Vue 3 ``` --- ### ✅ 3. 方法名是 `.resize()`,不是 `.resizeTextarea()` ⚠️ 很多老教程写的是 `resizeTextarea()`,这是 **Element UI(Vue 2)早期版本的私有方法**。 从 **Element Plus 开始,正确的方法是 `.resize()`** #### Element Plus 文档中: - `<el-input type="textarea">` 支持一个内部方法:`resize()` - 它会重新计算并调整 textarea 高度以适应内容 👉 所以你应该调用: ```js textareaRef.value?.resize(); ``` 而不是: ```js textareaRef.value?.resizeTextarea(); // ❌ 不存在,报错 ``` 这就是你报错的核心原因! --- ### ✅ 4. 如何验证是否拿到了正确的实例? 加个调试日志: ```js nextTick(() => { console.log('textareaRef:', textareaRef.value); }); ``` 输出应该类似: ``` Proxy { ... $attrs: ..., $slots: ..., $props: ..., expose: { ... resize: fn } } ``` 如果你看到的是 `undefined`,说明 `ref` 没绑定成功。 --- ## ✅ 排查清单(必看) ✅ 请逐项检查: | 检查项 | 是否满足 | |-------|--------| | `ref="textareaRef"` 是否写在 `<el-input>` 标签上? | ✅ / ❌ | | 是否使用了 `v-if` 隐藏了该组件?需要等它挂载后再操作 | ✅ / ❌ | | 是否在 `setup()` 或 `mounted` 前就访问了 `$refs`? | ✅ / ❌ | | 方法名是否写成了 `resizeTextarea()`?应改为 `resize()` | ✅ / ❌ | | 是否使用了 `v-for` 导致 `ref` 是数组或被覆盖? | ✅ / ❌ | --- ## ✅ 最佳实践建议 ### ✅ 方案一:依赖 `autosize` 自动行为(推荐) 如果设置了: ```html :autosize="{ minRows: 2, maxRows: 6 }" ``` 那么大多数情况下,**只要 `v-model` 更新,Element Plus 会自动触发高度调整**,无需手动调用 `resize()`。 ✅ 只有在以下场景才需要手动触发: - 动态显示隐藏(如 `v-show` 切换) - 弹窗中首次加载内容 - 数据异步回填且初始为空 --- ### ✅ 方案二:watch 数据变化自动 resize ```js import { watch } from 'vue'; watch( () => form.value.description, () => { nextTick(() => { textareaRef.value?.resize?.(); }); } ); ``` 这样每次描述变化都会自动撑开。 --- ## ✅ 总结 | 问题 | 解决方案 | |------|----------| | `Cannot read property 'resizeTextarea' of undefined` | 因为 `textareaRef` 是 `undefined` 或方法名错误 | | `textareaRef` 为 `undefined` | 检查 `ref` 是否绑定正确、是否在 `nextTick` 中访问 | | `resizeTextarea is not a function` | 方法名错误!Element Plus 应使用 `resize()` | ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值