🛢️ 第24章 Struts2+Spring+Hibernate+MySQL实现网络商城
📄 一句话概括

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 秒实验

  1. 把 boot() 的顺序改成 filter → listener → filter,看第二次取到的 Bean 是不是有值了。
  2. 往 have 清单里加一个类,重跑对账,看少报了几条。
  3. 给连接串检查加一条:useSSL=false 没写时报什么(提示:很多 8.0 驱动会警告)。

常见误区

  1. 启动报错就先改 Hibernate 方言。web.xml 或 Spring 的配置错了,Hibernate 根本没被加载, 改它一点用都没有。排错从最外层开始。
  2. 配置里的类名靠手打。包名一改就漏一处,而报错只说「找不到类」。 写完扫一遍,比启动失败后翻日志快得多。
  3. 连接串省掉参数。本地能跑、上线乱码,是这类问题最典型的症状。字符集和时区一开始就写全。
自测三问(点开对答案) 1. 为什么监听器要早于过滤器? —— 因为 Spring 容器是在监听器里建的,而 Struts 是过滤器。 过滤器先起来就去取 Bean,容器还没建,取到的是 null。 2. `ClassNotFoundException` 常见的原因有哪三种? —— 类名拼错、包名改了没同步、依赖没引(`pom.xml` 少一条)。 三种报错文本几乎一样,只能靠「配置里的类名和代码里的类」对账来区分。 3. 连接串少写参数会怎样? —— 多数情况能连上,但中文可能乱码、时间可能差八小时。 本地库碰巧是 utf8mb4 时测不出来,上线才发现。

环境能起来了,下一篇写第一个业务模块:登录注册。 这一篇的重点不是表单校验,是「Action 不是单例」和「密码为什么要加盐」。

相关笔记