1. 首页配置的真正难点在哪里
1.1 拆解一次 index 请求的完整链路
说一个玩 SSM 项目时几乎每次都躲不开的事:把应用跑起来,敲下http://localhost:8080/项目名/,结果要么白屏,要么 404,要么直接给你跳到某个既不对也不像首页的页面。网上搜 “SSM 配置 index 页面的实现方式”,多数答案会告诉你 “在 web.xml 里加个 welcome-file 就行”,但等你自己跟着做,问题根本没解决。根子在于大家没有把 index 页面放进 Spring MVC 的处理链路里去想。
SSM 这个词看着是三个框架的缩写,但首页路由这件事,真正参与的只有 Spring MVC 和 Servlet 容器。一个请求从浏览器发出来,到最终渲染出一个 index 页面,至少要经过这么几步:
浏览器先请求http://localhost:8080/项目名/,容器(Tomcat、Jetty 这类)收到请求后,会先根据web.xml里的配置做判断。这里就有一个关键点:容器找 index 页面的顺序,和你直觉里想的不一样。它不会一上来就把请求交给 Spring MVC,而是先看你的 webapp 根目录下有没有 index.html、index.jsp 这类欢迎文件。如果有,容器直接用自带的 JSP 或静态资源处理器把这个文件返回出去;如果没有,才轮到 Spring MVC 的 DispatcherServlet 接管这个请求。
这个先后顺序,就是大多数 “SSM 首页配不明白” 案例的根源。你以为是 Spring MVC 没配好,其实是容器和框架之间抢了一次请求归属;你以为 Controller 写好了就能进首页,结果根目录下一个多余的 index.jsp 就把你的 Controller 短路了。
Spring MVC 接管之后,会通过HandlerMapping找到处理/这个 URL 的 Controller 方法,执行完之后返回一个逻辑视图名。这个名字不会被浏览器直接看到,它会交给InternalResourceViewResolver,由这个视图解析器在配置好的前缀目录和后缀名里拼出真正的物理路径,最终把 JSP 渲染成 HTML 返回给浏览器。
我的建议是:把 “配置 index 页面” 这件事,拆成两个独立的问题去看。第一个问题是 “根路径的请求由谁来接”,第二个问题是 “接到之后渲染哪个物理视图”。只要把这两个问题分开,配置就永远不会乱。
1.2 三种首页路线的取舍
工程里常见的 index 页面方案,大致可以归成三条路线。
第一条路线是纯静态首页。放一个 index.html 在 webapp 根目录,不接任何后台逻辑。这种适合官网展示页、项目说明页、或者前后端分离之后把静态资源塞进同一个 war 包里的场景。优点是简单、启动即用;缺点是拿不到登录用户信息、菜单权限这类动态数据,一但需要动态内容就得改造。
第二条路线是 Controller 动态渲染。在 Controller 里写一个处理根路径的方法,返回一个视图名,由视图解析器渲染 JSP。这是 SSM 项目里最主流的做法,因为首页通常不只是个静态壳,它可能需要加载用户菜单、展示公告列表、校验登录状态。Controller 一接管,这些逻辑都能自然塞进去。
第三条路线是 Spring MVC 的<mvc:view-controller>配置。如果你只需要从一个 URL 跳转到某个 JSP,中间不需要任何业务方法,可以用一行配置代替一个空 Controller 类。这种方案适合快速搭骨架、写原型的时候用。
三条路线之间不是互斥的。实际项目里往往入口是一个 Controller 方法,但某些如 “/login” 这种纯页面跳转又用 view-controller 处理,静态资源则走专门的资源映射。我强烈建议在动手之前先问自己一个问题:首页需不需要在渲染前去取数据?需要就老老实实走 Controller,不需要就别写空方法,用 view-controller 减少代码量。
2. web.xml 与 spring-mvc.xml 中容易踩雷的三个配置点
2.1 DispatcherServlet 的 url-pattern:/与/*
SSM 项目里 DispatcherServlet 的映射路径,初学者最容易在这两个符号上面翻车。
配置写成/,意思是 “除了 JSP 之外的请求都交给 Spring MVC”。注意,/不会拦截对 JSP 的直接访问,JSP 文件还是由容器自带的 JspServlet 处理。这也是为什么你的 Spring MVC 能转发到/WEB-INF/views/index.jsp,而不会和 JspServlet 产生冲突。
配置写成/*就不一样了。/*会把所有请求都拦截下来,包括后缀是.jsp的请求。一旦你用了/*,Spring MVC 去转发 JSP 时,这个转发请求又会被 DispatcherServlet 再拦一次,最终可能抛出 “Circular view path” 一类的异常,或者页面直接 500。这是老生常谈,但每次帮人看项目都能遇到。
正确的做法是:
<servlet-mapping> <servlet-name>springMVC</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping>/还有一个隐含的作用:容器里所有没有被其他 Servlet 明确匹配的请求,都会走到 DispatcherServlet。所以就算 webapp 根目录下没有 welcome-file,你访问http://localhost:8080/项目名/时,容器也会把请求交给 DispatcherServlet,再由它去匹配 Controller。
2.2 欢迎文件列表的优先级
web.xml里的<welcome-file-list>是个很微妙的东西。我见过不少项目,web.xml 里配了 welcome-file,根目录也放了 index.jsp,Controller 也写了@RequestMapping("/"),结果访问根路径时,页面上永远看不到 Controller 里往 Model 放的值。
原因就是前面说的优先级问题。容器的处理顺序很死板:如果请求的路径是一个目录,并且应用里存在与之匹配的欢迎文件,容器会先把这个欢迎文件找出来,用内部转发的方式交给它处理。这个内部转发发生在 Spring MVC 之前,所以你的 DispatcherServlet 根本没机会接触请求。
一个典型的反面配置是这样的:
<welcome-file-list> <welcome-file>index.jsp</welcome-file> </welcome-file-list>如果 webapp 根目录下恰好也存在 index.jsp,那么浏览器输入根路径,看到的永远是根目录这个 JSP,而不是你 Controller 方法里返回的视图。根目录的 index.jsp 不经过 Spring MVC 的拦截器,也拿不到 Model 里的数据,这个坑很隐蔽。
处理办法有两种。要么把根目录的 index.jsp 删掉,让欢迎文件机制找不到文件,请求自然落到 DispatcherServlet;要么利用这个根目录的 index.jsp 做一次内部转发,把它当作进入 MVC 路由的跳板,这个思路我在第三章的第三种实现方式里会展开讲。
2.3 视图解析器的前缀后缀与 JSP 位置
SSM 项目的 JSP 通常被放在/WEB-INF/views/下,这个目录是受容器保护的,浏览器无法直接访问,但服务端可以安全地转发到里面的 JSP。Controller 返回逻辑视图名之后,靠InternalResourceViewResolver拼出真实路径。
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/WEB-INF/views/"/> <property name="suffix" value=".jsp"/> </bean>当 Controller 返回"index"时,视图解析器会把逻辑名拼成/WEB-INF/views/index.jsp,然后由服务端内部转发到这个 JSP。这里有个很多人不理解的细节:这个内部转发发生的过程中,浏览器地址栏里的 URL 并不会变化。你访问的是/项目名/,页面显示的是/WEB-INF/views/index.jsp的内容,但地址栏一直是/项目名/。这是服务端渲染的正常表现,不是 Bug。
JSP 放在 WEB-INF 下的好处是安全,用户没法猜一个.jsp路径去直接访问页面,所有页面都必须经过 Spring MVC 的路由,逻辑统一。坏处是,容器自带的欢迎文件机制不一定能直接找到 WEB-INF 下的 JSP,至少不建议这样用。你最好在根目录放一个入口文件,或者干脆不走欢迎文件,直接把根路径交给 Controller。
3. 四种可落地的 index 页面实现
3.1 方式一:Controller 接管根路径,返回动态首页
这是 SSM 项目里最正统的做法,也是我最推荐的主路线。它把首页变成一个完全受 Spring MVC 管理的动态页面,要带数据、要做权限判断都方便。
项目结构大致是这样:
src/main/webapp ├── WEB-INF │ ├── web.xml │ └── views │ └── index.jsp ├── index.html(可选,建议删掉) └── static └── css └── jsController 里写一个最简方法:
@Controller public class IndexController { @RequestMapping(value = "/", method = RequestMethod.GET) public String index(Model model) { model.addAttribute("appName", "SSM 示例项目"); model.addAttribute("currentUser", "访客"); return "index"; } }配合视图解析器配置,"index"最终指向/WEB-INF/views/index.jsp。web.xml 那边不需要再画蛇添足加 welcome-file,或者即便加了,也确保根目录没有那个同名文件。
如果用了 Maven 的 war 插件,脚手架里经常会自动生成一个 webapp/index.jsp,或者 index.html。这种自动生成的文件是麻烦之源,打包前一定要检查,不然你 Controller 写得再好,根路径也可能被这个文件截胡。
还要提一个细节:如果你继承了某个 BaseController,或者项目里配置了拦截器,那么对根路径/的请求也会照常经过拦截器链。首页要做什么前置校验,比如判断用户有没有登录、是不是要从重定向链接带参数,都可以在这个方法里处理。
3.2 方式二:用 mvc:view-controller 去掉空 Controller 方法
有时候你真的只是想把根路径转发到一个页面,数据全在页面上写死,或者数据由前端 JS 去拉接口。这时写一个空 Controller 方法有点小题大做,Spring MVC 提供了一个配置层面的解决方案。
在 spring-mvc.xml 里增加这样一段:
<mvc:view-controller path="/" view-name="index"/>它的意思很直白:访问/时,不走 Controller,直接把逻辑视图名设置成index,交给视图解析器去拼 JSP 路径。加上这行之后,根目录里的 home 页面就少了一个 Java 方法。
使用 view-controller 时建议同时保留<mvc:annotation-driven/>,因为我见过只配 view-controller 不配 annotation-driven 导致静态资源映射和注解 Controller 全部失效的案例。尤其是你的项目里既有注解 Controller,又有 view-controller,缺少 annotation-driven 时 HandlerMapping 可能没有被正确初始化,最终表现为部分路径 404。
<mvc:annotation-driven/> <mvc:view-controller path="/" view-name="index"/>view-controller 适合 “无脑转发” 的场景,比如/index、/login、/register这类页面。一旦首页需要从数据库取菜单或者判断登录态,就回到方式一,别硬用 view-controller。
3.3 方式三:根目录 index.jsp 内部转发到 MVC 路由
你可能会遇到一个不得已的情况:项目是接手别人的,根目录下已经躺着一个 index.jsp,大家约定俗成把它当作欢迎页,你要改造成 SSM 结构但不想大动。
这时可以在根目录的 index.jsp 里写一个内部转发,让请求进入 Spring MVC 的路由体系。
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <jsp:forward page="/index"/>注意这里的/index必须在 Controller 里有对应映射,否则会继续 404。转发之后,由 Controller 返回真正的视图名,视图解析器再渲染 WEB-INF 下的页面。
也有人用重定向:
<% response.sendRedirect(request.getContextPath() + "/index"); %>转发和重定向的区别在于:转发是服务端内部的,浏览器地址栏不变,一次请求搞定;重定向会返回 302,让浏览器再发一次请求,地址栏变成/index。如果首页里有需要放进 request 作用域的数据,千万别用重定向,因为重定向之后 request 就丢掉了。
这个方案的坏处是多了一层跳板,看起来不那么优雅。但它的好处也很实际:保留根目录的 index.jsp,就仍然可以利用欢迎文件的机制,不需要去 web.xml 里删 welcome-file,对老项目改动最小。我自己的经验是,这个方案一般只用来做过渡,等 Controller 路由稳定之后,还是会回到方式一的结构。
3.4 方式四:纯静态首页与静态资源映射
有时首页根本不需要服务端渲染,你可能引入了一个纯静态的 HTML 原型,或者把 Vue、React 打包出来的文件直接放进 webapp。这时候你要处理的不只是根路径,还包括 CSS、JS、图片这些静态资源在 Spring MVC 下的访问问题。
首先把静态首页命名为index.html放在 webapp 根目录,它会被容器当作默认欢迎文件。然后配置 Spring MVC 对静态资源放行,否则 DispatcherServlet 会去 Controller 里找/css/main.css、/js/app.js这种路径,结果自然是 404。
spring-mvc.xml 中常见两种配置。
第一种是直接交给容器默认 Servlet:
<mvc:default-servlet-handler/>第二种是显式指定静态资源目录:
<mvc:resources mapping="/static/**" location="/static/"/>我建议在纯静态场景下把两种都用上。default-servlet-handler让未被 Controller 匹配的路径回退到容器原始方式,适合零散分布在根目录的 favicon、robots.txt 之类;resources映射则更清晰,适合统一管理前端打包产物。
值得注意的一点是:纯静态index.html虽然能作为欢迎文件被容器直接返回,但如果你在 web.xml 里配置了 Spring 的字符编码过滤器,而且过滤器的url-pattern配的是/*,那么对index.html的访问也会经过过滤器,这在绝大多数情况下是好事,保证页面内容不乱码。
4. 实操中的问题排查和个人习惯
4.1 访问根路径 404 的排查顺序
首页 404 是最常见的问题,我把排查顺序固定成下面这套,能节省大量时间。
先看访问根路径时,Tomcat 控制台有没有输出 Spring MVC 的日志。如果完全没有日志,说明请求根本没进 DispatcherServlet,优先查 web.xml 的 servlet-mapping,再看根目录下是不是有 index.jsp 或 index.html 截胡了。如果日志显示进入 Spring MVC 但还是 404,再检查@RequestMapping的 value 是不是写的/index而不是/,或者 Controller 包名有没有被 component-scan 扫到。
<context:component-scan base-package="com.example.demo.controller"/>这个包名如果写错一个字母,Controller 就静默不生效,Spring 不会报错,页面就是 404。我帮人排查时发现,很多所谓的 “首页无法配置” 其实是这段扫描路径写错了。
4.2 静态资源被拦截与 500
页面能打开了,但 CSS、JS 全部 404,这个排查起来快一些。DispatcherServlet 配了/之后,所有静态文件请求都先经过 Spring MVC,如果你没有配资源映射,自然全部 404。
在 spring-mvc.xml 里补上:
<mvc:resources mapping="/static/**" location="/static/"/>还有个容易混淆的点:JSP 页面渲染时报 “Circular view path”,或者 500 错误里提示找不到视图。这种问题通常是把逻辑视图名写成了物理路径,比如 Controller 里返回/WEB-INF/views/index.jsp,而视图解析器又把前缀后缀拼了一遍,结果变成/WEB-INF/views//WEB-INF/views/index.jsp.jsp。Controller 只需要返回逻辑名index,物理拼装交给视图解析器。
4.3 编码、路径、依赖三件套
静态页面中文乱码,JSP 页面中文也乱码,或者提交表单之后中文成问号,这些在 SSM 项目里基本是同一个问题。
这里我展开说下编码过滤器。它虽然和 index 页面配置没有 100% 直接关系,但它是所有页面能正确显示中文的前提。最常见的做法是在 web.xml 里配置一个 Spring 自带的编码过滤器:
<filter> <filter-name>encodingFilter</filter-name> <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> <init-param> <param-name>forceEncoding</param-name> <param-value>true</param-value> </init-param> </filter> <filter-mapping> <filter-name>encodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>forceEncoding设为true很关键,它既强制了请求编码,也强制了响应编码,不然 JSP 页面头声明了UTF-8,但响应头还是 ISO-8859-1 一样会乱。然后用UTF-8的前提是页面文件本身必须是 UTF-8 保存。
Maven 项目里,spring-webmvc 依赖缺失也会造成启动就报 ClassNotFound:org.springframework.web.servlet.DispatcherServlet。不要只在本地 IDE 里能跑就以为没事,打成 war 包放到独立 Tomcat 时才会暴露依赖问题。
运行老项目时,Tomcat 9 之后对 JSP 和 EL 的版本要求更严格,如果 JSP 页面里用到 JSTL 标签库,务必加上javax.servlet.jsp.jstl:jstl依赖,否则首页渲染时经常抛 “Unable to find taglib” 的异常,表现为整个页面 500。这类问题最坑,因为问题不见得是首页路由配错,而是页面本身依赖的标签库没带全。
4.4 首页方案速查表
项目接手多了之后,我习惯把首页方案浓缩成一张对照表,遇到新工程先在表里定位自己属于哪类,再动手配置。
| 场景 | 推荐方案 | 关键配置 | 备注 |
|---|---|---|---|
| 首页需要动态数据、登录态、权限菜单 | 方式一:Controller 接管/ | web.xml配/,spring-mvc.xml 配视图解析器 | 根目录不要放同名 index.jsp |
| 只是跳转到某个页面,无业务逻辑 | 方式二:mvc:view-controller | view-controller path="/" view-name="index" | 同时保留 annotation-driven |
| 老项目保留根索引页,改造 SSM | 方式三:根 index.jsp 内部转发 | 根 JSP 写<jsp:forward> | 过渡期用,不建议长期保留 |
| 纯前端静态首页,页面不渲染数据 | 方式四:静态首页 + 资源映射 | welcome-file+mvc:resources | 记得配 default-servlet-handler |
最后分享一个我一直在用的习惯
各种配置方式讲完,还是要回到工程实践。我自己的习惯是:新项目从一开始就明确 “首页是动态的”,所以 web.xml 里不写 welcome-file-list,根目录下也不让脚手架生成任何 index.jsp 或 index.html,根路径/由 Controller 统一接管。这样请求链路是单线的,不存在容器欢迎文件机制和 Spring MVC 抢请求的问题。页面需要静态资源时,用mvc:resources单独把/static/**映射出来,跟首页路由互不干扰。
如果你只是想让项目先跑起来看效果,建议直接用mvc:view-controller配一个首页跳转,等后面需要动态数据了,再把view-controller换成 Controller 方法,改动量也不大。