
“兄弟我Spring Boot项目加了FreeMarkerController也写了模板也建了一访问就404这咋回事”这差不多是Spring Boot整合FreeMarker场景下我被问得最多的一句话。老实说404这个错误码在前后端交互里太“皮”了它既可能是请求压根没到后端也可能是Controller正常执行了但视图解析翻车还可能是模板文件根本没被编译进classpath。每次遇到我都说“先看日志”但次数多了我发现很多人连第一步该看什么日志都不太清楚。这篇文章就把Spring Boot整合FreeMarker后页面跳转404的完整链路拆开讲清楚底层机制是什么、哪些配置容易埋雷、一次标准排查该按什么顺序走。适合刚接触Spring Boot模板引擎的同学也适合那些“明明网上配置都对但自己项目就是404”的焦灼人群。1. 先把“404”拆成两类路由级404与视图级4041.1 路由级404请求压根没进ControllerSpring MVC处理一个页面请求的完整链路是这样的浏览器把请求交给内嵌TomcatTomcat把请求转发给DispatcherServletDispatcherServlet通过HandlerMapping找到对应的Controller方法方法执行完返回一个逻辑视图名再由ViewResolver解析物理模板并渲染。404出现在不同环节修复方向完全不同。第一类“路由级404”发生在“找到Controller方法”这一步之前。特征是后端控制台没有任何动静加了日志也不打印Spring Boot的访问日志如果有打开也看不到记录。这类404最常见的原因其实很朴素URL没对上。我见过不少真人真事Controller里写着GetMapping(/)浏览器里访问/index当然404。先把浏览器地址、Controller的RequestMapping、方法上的GetMapping三者对齐这是零成本检查。另一个典型原因是应用根本没正常启动。比如端口被占用启动报错但IDE只提示“端口被占用”很多人忽略了又比如项目里引入了一个老版本的FreeMarker和Spring Boot自动配置版本冲突启动到一半就崩了但IDE控制台没有被展开开发以为服务在跑访问全是404。这种情况先确认Spring Boot启动日志最后的“Started DemoApplication”字样接着看端口curl http://localhost:8080/能被访问再谈模板问题。1.2 视图级404Controller执行了但视图解析翻车第二类“视图级404”发生在视图解析阶段。特征是Controller方法正常执行日志里能看到你的System.out或自定义日志但浏览器最终找不到页面。为什么Controller执行了还会404关键在于Spring MVC的视图解析机制。方法返回的字符串“index”并不是真正的页面地址而是“逻辑视图名”。Spring MVC会从ViewResolver链中逐个找能处理这个视图名的解析器FreeMarkerViewResolver收到后把它拼成物理模板路径加载模板文件进行渲染。这条链路里任何一个环节出问题浏览器端看到的都很可能是404。最常见的三种情况FreeMarkerViewResolver根本不存在自动配置没生效、模板文件物理路径不对、配置改过suffix之类导致拼出来的路径与文件不符。接下来两节会分别讲清楚每种情况的判定方式。1.3 浏览器错误码只是表象真正的现场在后端日志这里说一个容易被误导的事实浏览器地址栏显示404并不等于后端真的返回了404状态码。在某些场景下后端可能已经抛出了View解析异常最终500或者被默认错误页替换成404。也有时候Controller返回视图名失败Spring Boot的默认错误处理机制会把它染成404。所以遇到页面404先别急着改代码去后端日志里翻。就算日志级别默认没开到DEBUG只要你把日志级别调到debug或者临时加一行系统输出就能确认请求是否真正进入过Controller。排查永远不要从浏览器端猜后端状态。2. FreeMarker接管视图解析的先决条件依赖与自动配置2.1 FreemarkerAutoConfiguration的触发条件很多人理解的“Spring Boot整合FreeMarker”是把freemarker依赖加到pom完事。但Spring Boot的配置是条件装配不是无条件自动生效。FreemarkerAutoConfiguration这个自动配置类上有几个关键条件注解ConditionalOnClassclasspath里必须存在freemarker.template.ConfigurationConditionalOnWebApplication必须是Web环境默认还有一个enable属性spring.freemarker.enabled没写时默认为true只有这些条件全部满足Spring Boot才会注册FreeMarkerConfigurer和FreeMarkerViewResolver。其中freemarker.template.Configuration这个核心类就来自freemarker的jar本身。而spring-boot-starter-freemarker这个Starter会把freemarker以及Spring Boot对它的适配类一起带进classpath。所以最标准的做法是dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-freemarker/artifactId /dependency只要这个依赖不缺席FreeMarker的自动配置链基本就不会因为“类不在classpath”而失效。2.2 只引入web依赖、漏掉Starter的后果我在实际帮人排查时发现不少项目pom里只有spring-boot-starter-web和手写的freemarker依赖dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.freemarker/groupId artifactIdfreemarker/artifactId /dependency这样看起来freemarker的核心类也在classpath里但缺失的是spring-context-support和spring-webmvc对FreeMarker的适配类比如org.springframework.web.servlet.view.freemarker.FreeMarkerViewResolver。缺了这些类FreemarkerAutoConfiguration同样不会生效因为它的内部还有针对FreeMarkerConfigurer等类的存在条件判断。结果就是你引入的FreeMarker只是个模板引擎库并没有接入Spring MVC的视图解析链。Controller返回“index”FreeMarkerViewResolver压根不在链里Spring MVC退而求其次把字符串当响应体或交给其他解析器最终页面404或者直接显示一行“index”。这种情况建议一律用Starter不要自己单独引freemarker的jar。2.3 用启动日志和依赖树确认自动配置是否生效遇到FreeMarker相关疑难杂症最快的确认办法是看Spring Boot的Auto Configuration Report。在application.yml里加一行debug: true启动时控制台会打印“Positive matches”和“Negative matches”两部分。如果自动配置生效Positive matches里会出现FreemarkerAutoConfiguration matched: - ConditionalOnClass found required classes freemarker.template.Configuration...如果这一项跑到了Negative matches里说明依赖有问题对照报错里的提示补依赖即可。检查完记得把debug开关关掉别一直开着。另外可以用Maven依赖树确认freeMarker相关依赖是否冲突mvn dependency:tree -Dincludesorg.freemarker,org.springframework:spring-context-support,org.springframework:spring-webmvc依赖树里如果出现多个freemarker版本或者根本没有spring-context-support问题基本就锁定了。2.4 Spring Boot 3.x下的版本差异Spring Boot 3.x已经强制要求Java 17并且整体迁移到Jakarta EE规范。如果你从2.x项目升级顺便升级了Spring Boot而忘了调整FreeMarker相关依赖可能出现类冲突或者视图解析器不工作的现象。Spring Boot 3.x的spring-boot-starter-freemarker底层依赖版本会有较大变动由Spring Boot的dependencyManagement统一管理。我自己踩过的一个坑是老项目pom里手动指定了freemarker 2.3.28这种版本号Spring Boot 3.x的BOM想把freemarker管理到更高版本但因为pom里显式加了版本号实际生效的还是老的结果FreemarkerAutoConfiguration初始化时读取配置失败启动不报错但页面全部404排查到崩溃。解决方案是显式声明的版本号要么删掉要么跟BOM保持一致。用mvn dependency:tree拿到当前freemarker版本和Spring Boot的默认版本对一下是最快的。3. 逻辑视图名与模板路径的映射规则约定比配置更难防3.1 默认四件套templates目录、.ftl后缀、空前缀、UTF-8Spring Boot给FreeMarker预设的默认配置可以用一个口诀记根目录templates后缀.ftl前缀空编码UTF-8。具体对应的配置项spring.freemarker.template-loader-pathclasspath:/templates/ spring.freemarker.prefix spring.freemarker.suffix.ftl spring.freemarker.charsetUTF-8当一个Controller方法返回“index”FreeMarkerViewResolver的解析逻辑基本等价于templateLoaderPath prefix index suffix → classpath:/templates/ index .ftl所以模板文件必须出现在src/main/resources/templates/index.ftl并且编译后被复制到classpath的templates目录下。这里的关键词是“classpath”。templates不是传统Web应用的webapp目录它是classpath下的一个资源目录。模板文件被打进jar包之后位于BOOT-INF/classes/templates/下面。如果你习惯于把模板放到src/main/webapp/WEB-INF/templates/Spring Boot默认不会去找因为那个路径不在classpath:/templates/里。3.2 三种经典错误摆位static、webapp、大小写我总结了三个特别常见的错误摆放方式模板放到了src/main/resources/static/templates/下。static目录的用途是存放静态资源——图片、CSS、JS、纯HTML。Spring Boot会把static映射为根路径下的静态资源服务但它不会经过FreeMarker渲染。你访问/static/templates/index.ftl时浏览器要么下载要么按纯文本返回访问/index时永远404。模板放到了src/main/webapp/下。有些从JSP时代转过来的同学保留了这个习惯。Spring Boot的Web项目如果走了嵌入式容器webapp目录不是不能访问但它不在classpath中FreeMarker的template-loader-path根本扫不到。磁盘上目录叫Templates代码里写的是templates。Windows下大小写不敏感本地跑得好好的部署到Linux服务器后全部404。这个问题出现频率不低尤其在用jar包部署到云服务器的时候。3.3 视图名细节前导斜杠、多余后缀、语义对应Controller返回视图名也有一些约定俗成的细节返回“index”和返回“/index”在大部分Spring Boot版本里效果一样但为了保险不要带前导斜杠。部分版本视图名带前导斜杠后再和template-loader-path拼接可能出现双斜杠或者资源路径异常。返回“index.ftl”而不是“index”会让解析器把物理路径拼成templates/index.ftl.ftl直接404。视图名不写后缀后缀交给suffix配置统一处理。模板文件的主文件名和Controller映射路径的语义可以不一样。比如方法路径是/user/list方法可以返回“user/list”也可以返回“userList”只要物理模板和返回值对应上即可。当然为了可维护性建议路径层级和视图名保持一致。4. 一次实战404排查全过程从URL下钻到target/classes下面用一个模拟项目走一遍完整排查。项目结构demo/ ├── pom.xml ├── src/main/java/com/example/demo/DemoApplication.java ├── src/main/java/com/example/demo/controller/IndexController.java └── src/main/resources/templates/index.ftlIndexController的内容Controller public class IndexController { GetMapping(/index) public String index() { System.out.println(进入index方法); return index; } }访问http://localhost:8080/index浏览器返回404。4.1 第1步确认请求是否命中Controller首先看后端控制台有没有打印“进入index方法”。如果没打印检查三件事应用是否真的启动看启动日志里有没有Started DemoApplication端口是否监听。Controller是否被Spring扫描到DemoApplication所在的包路径必须是com.example.demoIndexController必须在它的子包下。放错包路径Spring Boot默认的组件扫描根本扫不到。请求路径是否匹配GetMapping的值和浏览器URL是否一致大小写是否敏感。如果打印了说明请求成功到达Controller问题在视图解析阶段进入下一步。4.2 第2步打开视图解析的Debug日志把答案从“猜”变成“看”最快的办法是开日志logging: level: org.springframework.web: DEBUG重启访问重点看DispatcherServlet和ViewResolver相关的日志。如果项目正常整合了FreeMarker日志里会出现类似“Rendering view [org.springframework.web.servlet.view.freemarker.FreeMarkerView...]”的字样。这说明视图解析已经走到了FreeMarker这一步。如果完全没有FreeMarkerViewResolver字样大概率是依赖或自动配置问题回到第2节检查Starter。如果能看到ViewResolver日志里通常会带出它尝试解析的视图名以及找不到文件的异常线索。这一步能直接区分“解析器没挂上”和“解析器挂了但找错文件”。4.3 第3步检查编译产物里的模板文件这一步很多人会漏掉但它能定位一批“本地跑着对部署就404”的问题。打开项目的target/classes/templates/目录确认index.ftl是否真的编译进来了。如果没有IDE构建问题确保resources目录被标记为Resources Root并执行mvn clean package或Rebuild Project。Maven资源过滤问题如果pom里配置了resources的include/exclude过滤检查是否排除了templates。多模块项目子模块资源没打进来要在父模块的依赖关系里确认子模块产物包含templates。还有个检查技巧打开最终打的jar包看BOOT-INF/classes/templates/index.ftl是否存在。jar包里的情况才是运行时真实情况。4.4 第4步逐项对照FreeMarker配置模板文件在、依赖在、ViewResolver也在那就要查配置。打开application.yml或application.properties逐一核对spring.freemarker.template-loader-path是否还是默认值。有些人改路径时写成了classpath:/又没配好目录导致解析路径和文件对不上。suffix是否被改成.html而文件实际是.ftl。spring.mvc.view.prefix和spring.mvc.view.suffix有没有被误配。这两个配置项属于InternalResourceViewResolver和FreeMarker无关但一旦存在会让视图解析链变得混乱。spring.freemarker.view-names是否设置了筛选条件。这个配置表示只处理匹配的视图名如果配了/page/*而返回“index”FreeMarkerViewResolver会直接跳过视图解析自然失败。多个配置文件里的相同key冲突application.properties和application.yml如果都写了同一项properties优先生效yaml里那份会被忽略。排查时可以临时把所有配置精简成一份。4.5 第5步验证修复并确认模板热更新改完配置或者依赖后最重要的一步是clean后重启不要只做增量编译。因为Java源代码变化可能触发增量编译但resources目录下的资源文件有时不会被自动复制到target需要手动执行mvn clean package或者清掉target目录再Rebuild。重启完再访问同时观察日志。如果请求进入Controller且日志里没有模板解析异常页面自然就出来了。顺便建议试一下修改模板内容在cache: false模式下刷新页面确认热生效这能证明模板文件确实在classpath且被正确加载。5. 修复之外的避坑清单与衍生问题5.1 FreeMarker配置项速查表把经常和FreeMarker有关的配置项集中列一下方便直接抄配置项作用推荐值备注spring.freemarker.template-loader-path模板根目录classpath:/templates/结尾斜杠保持spring.freemarker.prefix视图名前缀空不要乱加/spring.freemarker.suffix视图名后缀.ftl改html需要全链路一致spring.freemarker.charset模板编码UTF-8和文件编码一致spring.freemarker.content-type响应Content-Typetext/html一般不用改spring.freemarker.cache模板缓存开发false、生产true开发改模板免重启spring.freemarker.settings.template_update_delay缓存刷新间隔0或1生产环境可适当调大spring.freemarker.view-names限定解析的视图名空配错会直接跳过视图这里特别说一下cache的误区。开发阶段把spring.freemarker.cache设为false改完模板刷新浏览器就能生效不用重启但生产环境必须设为true或直接用默认。有些团队把cache: false带到了生产高并发下每次请求都重新解析模板文件性能虽然不会差到崩溃但没必要。5.2 RestController与Controller的反馈差异整合FreeMarker做页面跳转时Controller必须用Controller。这是一个容易被忽略却非常致命的设计。如果你用了RestController方法返回的字符串会被框架直接当作HTTP响应体输出。浏览器地址正确时页面会显示一串“index”或者“hello freemarker”而不是渲染后的HTML。如果方法路径和模板入参搭配奇怪甚至可能出现400/500。这种情况的特征是服务端没有视图解析日志响应体就是源码字符串本身。从JSP时代走过来的老手偶尔也会犯这个错——做接口开发时用RestController习惯了没意识到模板引擎场景下它直接禁用了视图解析。排查时先抬头看一眼类上的注解比什么都快。5.3 Maven多模块与资源根目录问题Maven多模块项目也是FreeMarker 404的高发区。常见结构父模块demo-parent子模块demo-common放工具类web模块放Controller和templates。如果templates目录放在了一个还没被打成jar包的模块里或者在子模块pom中没有正确声明resources运行主应用时classpath里就没有模板。排查多模块资源问题时不要只盯编辑器里左侧文件树存在要盯最终jar包。Maven的resources插件对“src/main/resources下的文件会被打进类路径”处理比较直白但如果子模块之间的依赖构建顺序有问题或者子模块的pom里配置了include来排除文件模板照样进不来。一个稳妥的口诀模板文件与Controller放在同一个模块的src/main/resources/templates下不要跨模块引用模板。5.4 乱码、空模板与新版本兼容小细节最后补充几个排查时顺带注意的小问题。模板文件中文乱码多半是文件编码和charset配置不一致。建议统一UTF-8IDEA里右下角文件编码也要改成UTF-8否则模板内容里的中文在编译时已经变成乱码。如果还不行可以在freeMarker原生setting里补上spring.freemarker.settings.default_encodingUTF-8 spring.freemarker.settings.classic_compatibletrueclassic_compatible是FreeMarker的一个兼容模式开关能兼容一部分老模板写法缓解模板迁移问题但别指望它解决所有语法问题。模板文件为空或者只有一处语法错误时FreeMarker渲染会抛异常。日志里会显示“Error while processing template”或具体行号这时打开模板文件、空值判断、缩进检查比继续猜路径高效得多。还有一种“伪404”配置全部正常模板也存在但页面渲染出来的内容为空或部分缺失。这往往不是路径问题而是模板中引用了不存在的对象属性FreeMarker默认会报错。可以在渲染时观察日志或者用FreeMarker内置的空值判断处理可选值。最后说一点个人体会。Spring Boot整合FreeMarker绝大多数404都不是什么高深原理导致的而是依赖、目录、命名、配置这几件最基础的事情出了偏差。遇到404先别急着在浏览器里反复刷新按“接口通不通、视图解析器在不在、模板进没进classpath、配置对不对”这个顺序查一遍大部分问题都能在十分钟内定位。我自己现在再遇到类似问题第一件事就是打开target/classes/templates目录看一眼这一眼通常能省下后面所有的猜测。如果你也在排查过程中发现过什么有趣的坑欢迎在评论里聊聊说不定下次的踩坑清单就用上你的素材。