SSH 的环境是四份配置文件:web.xml、Spring、Hibernate、Struts。
四份里只有 web.xml 决定「别人什么时候被加载」,所以它的顺序错了,
另外三份写得再对也没用。
启动失败最常见的原因不是写错语法,是「配置里引用的类,代码里没有」。
教材 24.6 讲项目环境搭建:配置 Struts 2、配置 Hibernate、配置 Spring、配置 web.xml。 四小节看起来是四件互不相干的事,其实是一条装载链。 这一篇不复述四个 XML 的每个标签,只讲三件调试时真正会卡住人的事:加载顺序、类名对账、 连接串。 演示里用一个三十行的「小容器」复刻启动顺序,把过滤器拿到 Bean 的时机显式跑出来; 再写一个脚本,把四份 XML 里引用的类名和「已有的类」对一遍。
一、四份文件各管什么
| 文件 | 管什么 | 写错了的症状 |
|---|---|---|
web.xml |
谁先启动、哪个 URL 进 Struts | 整个应用 404,或过滤器不生效 |
applicationContext.xml |
数据源、会话工厂、Bean | 启动即报错,Bean 找不到 |
hibernate.cfg.xml |
连哪个库、方言、映射文件 | 表名对不上,SQL 语法错 |
struts.xml |
Action 名、方法名、跳转页面 | 页面 404 或方法调不到 |
四份文件里,web.xml 是唯一一份「决定别人什么时候被加载」的。
另外三份都是「被加载」,它们只负责在被叫到的时候说清自己是什么。
这也是为什么排错要从 web.xml 开始:它错了,后面三份的错误信息根本不会出现。
很多人一上来就改 Hibernate 的方言配置,其实问题在更前面。
二、启动顺序:监听器早于过滤器
web.xml 里有两类东西:监听器和过滤器。规范里监听器一定先于过滤器初始化。
而 Spring 的容器正是在监听器里建的(ContextLoaderListener),Struts 是过滤器。
所以正常配置下顺序天然是对的。
| web.xml 里的顺序 | 过滤器启动时拿到的 Bean |
|---|---|
| listener → filter | GoodsAction 实例 |
| filter → listener | None,后面必然空指针 |
第二行就是新手最常踩的那一脚:Struts 的过滤器先起来,Spring 容器还没建,
Action 里的 @Autowired 全是 null。
报错是 NullPointerException,指向的是业务代码,看不出跟顺序有关——这才是它难查的原因。
会踩到这个坑的场合,通常是有人「优化启动速度」,把 Spring 容器改成在过滤器里手动初始化。
改完能跑,但在某些容器里过滤器会先于监听器被调用,于是偶发空指针。
偶发、空指针、指向业务代码——三个特征凑齐,基本就是顺序问题。
三、配置与代码的对账
SSH 项目启动失败,十次有六次是这一类:XML 里写了一个类名,代码里没有。
原因通常是三种:类名拼错、包名改了没同步、pom.xml 里少一个依赖。
它们的报错都是 ClassNotFoundException,而且不会告诉你是哪份 XML 的第几行。
所以「写完配置扫一遍」这个小脚本很值:把 XML 里所有带点的类名抽出来,和「已有的类」对一遍,
缺的直接列出来。
这个脚本只有十几行,但它能把「启动失败后翻日志猜」变成「启动前就知道缺什么」。
演示里扫出两处:一个是连接池的实现类(少依赖),一个是自己写的 Dao(包名对不上)。
四、连接串:四条 URL,四种结局
| 写法 | 结局 |
|---|---|
| 带全参数 | 通,字符集和时区都对 |
| 不带参数 | 通,但中文可能乱码 |
| 缺端口 | 通,按默认 3306 连;同样没带参数 |
缺 jdbc: 前缀 |
认不出来,报 No suitable driver |
第二行最坑:本地测试一切正常,上线后中文全变问号。 因为默认字符集不是 utf8mb4,而本地库碰巧是,所以测不出来。 第四行更容易误判:报「找不到驱动」,人就去重装驱动,其实是 URL 少写了五个字符。 「缺端口」和「乱码」会一起出现,这也有个坑:改的人看到「缺端口」,补上端口就以为修完了, 乱码还在。 所以连接串要么写全,要么一项都别省——省下来的字符,排查时要花十倍时间补回去。
五、动手 Demo:四个配置文件,和它们的启动顺序
这一段先列四份配置各管什么,再用一个三十行的小容器复刻「监听器建容器、过滤器取 Bean」的顺序, 然后扫一遍 XML 里引用的类名,最后拿四条连接串过一遍检查函数。
import re
WEBXML = u"""<web-app>
<listener><listener-class>org.springframework.web.context.
ContextLoaderListener</listener-class></listener>
<filter><filter-class>org.apache.struts2.dispatcher.filter.
StrutsPrepareAndExecuteFilter</filter-class></filter>
</web-app>"""
# 复刻启动顺序:listener 建容器,filter 取 Bean
beans = {}
def boot(order):
beans.clear()
got = []
for step in order:
if step == u"listener":
beans[u"goodsAction"] = u"GoodsAction 实例"
else:
got.append(beans.get(u"goodsAction"))
return got[0]
print(u"listener→filter:%s" % boot([u"listener", u"filter"]))
print(u"filter→listener:%s" % boot([u"filter", u"listener"]))
# 配置里引用的类,代码里真的存在吗
def classes(xml):
out = [x for x in re.findall(u'class="([^"]+)"', xml) if u"." in x]
out += re.findall(u"<listener-class>([^<]+)</listener-class>", xml)
out += re.findall(u"<filter-class>([^<]+)</filter-class>", xml)
return out
xmls = [(u"web.xml", WEBXML), (u"applicationContext.xml", SPRING)]
have = set([u"org.springframework.web.context.ContextLoaderListener",
u"org.apache.struts2.dispatcher.filter.StrutsPrepareAndExecuteFilter"])
for name, x in xmls:
for cls in classes(x):
if cls not in have:
print(u"%s 引用了 %s,找不到" % (name, cls))
# 连接串检查
pat = re.compile(u"^jdbc:mysql://([^:/]+)(:(\\d+))?/([^?]+)(\\?.*)?$")
def check(url):
m = pat.match(url)
if not m:
return u"认不出来:缺 jdbc:mysql:// 前缀"
host, port, db, q = m.group(1), m.group(3), m.group(4), m.group(5)
notes = []
if not port:
notes.append(u"缺端口,按 3306 试")
if not q:
notes.append(u"没带参数,中文可能乱码")
return u"通;" + u";".join(notes) if notes else u"通:%s/%s" % (host, db)
for u_ in (u"jdbc:mysql://localhost:3306/shop",
u"jdbc:mysql://localhost:3306/shop?characterEncoding=utf8",
u"jdbc:mysql://localhost/shop",
u"mysql://localhost:3306/shop"):
print(u"%s -> %s" % (u_, check(u_)))
实跑输出:
--------------------------------------------------------------------
四个配置文件,和它们的启动顺序
--------------------------------------------------------------------
**教材 24.6 讲项目环境搭建:Struts 2、Hibernate、Spring、web.xml 四份配置。**
**四份文件里最容易出错的不是内容,是【顺序】和【引用】。**
① 四份文件各管什么
--------------------------------------------------------------------
文件 管什么 写错了的症状
------------------------------------------------------------------------------------
`web.xml` 谁先启动、哪个 URL 进 Struts 整个应用 404,或过滤器不生效
`applicationContext.xml` 数据源、会话工厂、Bean 启动即报错,Bean 找不到
`hibernate.cfg.xml` 连哪个库、方言、映射文件 表名对不上,SQL 语法错
`struts.xml` Action 名、方法名、跳转页面 页面 404 或方法调不到
**四份文件里,`web.xml` 是唯一一份「决定别人什么时候被加载」的。**
**所以它的顺序错了,另外三份写得再对也没用。**
② 启动顺序:监听器必须早于过滤器
--------------------------------------------------------------------
web.xml 里的顺序 过滤器启动时拿到的 Bean
------------------------------------------
listener → filter GoodsAction 实例
filter → listener None,后面必然空指针
**第二行就是新手最常踩的那一脚:Struts 的过滤器先起来,Spring 的容器还没建,**
**Action 里的 `@Autowired` 全是 null。**
**报错信息是 `NullPointerException`,指向的是业务代码,**
**看不出跟顺序有关——这才是它难查的原因。**
**`ContextLoaderListener` 是监听器,Struts 是过滤器,规范里监听器一定先于过滤器初始化。**
**所以正常情况下这个顺序是对的,会踩坑的是有人把 Spring 容器改成在过滤器里手动初始化。**
③ 配置里引用的类,代码里真的存在吗
--------------------------------------------------------------------
文件 引用了但找不到的类
-----------------------------------------------------------------
applicationContext.xml com.mchange.v2.c3p0.ComboPooledDataSource
applicationContext.xml com.shop.dao.GoodsDao
**扫出 2 处。清单里没有的,就是「配置写了、依赖没引」或者「包名写错了」。**
**SSH 项目启动失败,十次有六次是这一类:类名拼错、包名改了没同步、`pom.xml` 里少一个依赖。**
**它们的报错都是 `ClassNotFoundException`,**
**但不会告诉你是哪份 XML 的第几行——所以「写完配置扫一遍」这个小脚本很值。**
④ 连接串:错一个字符,报错完全不同
--------------------------------------------------------------------
① 全参数 jdbc:mysql://localhost:3306/shop?useUnicode=true&characterEncoding=utf8
② 无参数 jdbc:mysql://localhost:3306/shop
③ 缺端口 jdbc:mysql://localhost/shop
④ 缺前缀 mysql://localhost:3306/shop
写法 检查结果
--------------------------------------------------------
① 全参数 通:host=localhost port=3306 db=shop 参数 2 项
② 无参数 通;没带参数,中文可能乱码
③ 缺端口 通;缺端口,按 3306 试;没带参数,中文可能乱码
④ 缺前缀 认不出来:缺 jdbc:mysql:// 前缀
**第二行最坑:本地测试一切正常,上线后中文全变问号——默认字符集不是 utf8mb4。**
**第四行少了 `jdbc:` 前缀,真跑起来报 `No suitable driver`,看起来像驱动没装,**
**其实是 URL 写错了。**
**第三行缺端口本身不算错,按默认 3306 连就行;**
**但它同样没带参数,所以「缺端口」和「乱码」会一起出现,很容易只改了前一个。**
boot() 只有十行,但它把「顺序」这件事跑出来了:同一份配置,换一下顺序,
过滤器拿到的就是 None。
真实容器里这个 None 会在第一次用到 Bean 时才炸,所以报错位置和真正的原因隔了好几层。
类名对账那一段扫出两条:一条是别人的类(少依赖),一条是自己的类(包名对不上)。
两类错误的表象完全一样,
修法完全不同——所以对账脚本要能区分「不是自己的」和「是自己的但名字不对」。
连接串那四条里,只有第一条是完全正确的。
第二条看着没问题,但在默认字符集不是 utf8mb4 的库上,
中文会变问号——这也是为什么很多项目在本地跑得好好的,一上线就乱码。
30 秒实验
- 把
boot()的顺序改成filter → listener → filter,看第二次取到的 Bean 是不是有值了。 - 往
have清单里加一个类,重跑对账,看少报了几条。 - 给连接串检查加一条:
useSSL=false没写时报什么(提示:很多 8.0 驱动会警告)。
常见误区
- 启动报错就先改 Hibernate 方言。
web.xml或 Spring 的配置错了,Hibernate 根本没被加载, 改它一点用都没有。排错从最外层开始。 - 配置里的类名靠手打。包名一改就漏一处,而报错只说「找不到类」。 写完扫一遍,比启动失败后翻日志快得多。
- 连接串省掉参数。本地能跑、上线乱码,是这类问题最典型的症状。字符集和时区一开始就写全。
自测三问(点开对答案)
1. 为什么监听器要早于过滤器? —— 因为 Spring 容器是在监听器里建的,而 Struts 是过滤器。 过滤器先起来就去取 Bean,容器还没建,取到的是 null。 2. `ClassNotFoundException` 常见的原因有哪三种? —— 类名拼错、包名改了没同步、依赖没引(`pom.xml` 少一条)。 三种报错文本几乎一样,只能靠「配置里的类名和代码里的类」对账来区分。 3. 连接串少写参数会怎样? —— 多数情况能连上,但中文可能乱码、时间可能差八小时。 本地库碰巧是 utf8mb4 时测不出来,上线才发现。环境能起来了,下一篇写第一个业务模块:登录注册。 这一篇的重点不是表单校验,是「Action 不是单例」和「密码为什么要加盐」。
- ☐ (本篇完,下一篇 24.7 登录注册模块设计)