简介:基于 Struts、Hibernate、Spring 与 Oracle 数据库构建的车辆管理系统完整项目,面向 Java Web 入门及进阶学习者,也适合毕业设计或课程设计场景,用于解决车辆状态、维修记录、保养周期、燃油消耗等信息的统一管理问题。工程严格按 MVC 设计思路分层,控制器、业务服务、DAO 持久层与 JSP 视图清晰分离,完整呈现 SSH 框架的整合方式。压缩包共 594 个文件,主要包含 122 个 Java 源码及编译后 class、112 个 jar 依赖库、68 个 JSP 页面、54 个 XML 配置,以及 GIF、JS、CSS、HTML 等前端资源,整体大小约 35.89MB;Java 与 class 对应核心业务代码,jar 支撑框架运行,JSP 和静态资源组成展示层,XML 与 properties 负责框架及数据库连接配置,便于按类型检索和学习。已有 504 人浏览学习。从工程类结构可梳理出登录认证、车辆档案、油耗管理、驾驶员管理、年检管理等业务模块;通过阅读 Service 实现类、DAO 映射及 Oracle 表结构,可理解事务控制、依赖注入、ORM 映射等关键写法,是系统学习 SSH 项目开发的完整参考资料。
1. 一套 SSH 车辆管理系统拆包之后,先看 class 文件再谈架构
拿到手的是一个带 .rar 后缀的编译产物,里面LoginAction.class、CarbstbDAO.class、UserServiceImpl.class一目了然:这不是前后端分离项目,也不是 Spring Boot 工程,而是典型的 Struts + Hibernate + Spring + Oracle 组合。Carbstb对应车辆台账,Driverm是驾驶员主档,Oilm管油料,Annum管年审——把这些实体类名和 DAO 类名拼起来,整个系统的业务边界就出来了。它适合两类人:一类是想把课堂上的 SSH 理论落成可运行工程的在校生,另一类是手头正在维护老项目、需要快速摸清请求链路的在职工程师——UcapliDAO管用车申请,OutcmDAO管出车记录,从这些命名能直接反推数据库表结构,这是这包资料最值钱的地方。
2. Struts 层拆解:从 LoginAction 到 DAO,一条 URL 请求在 SSH 项目里怎么走完
Struts 在 SSH 里只承担 MVC 中的 Controller 和 View 调度。你要判断这套系统用的是 Struts1 还是 Struts2,最直接的办法就是看 Action 是否继承框架基类:LoginAction.class和UserServiceImpl.class放在一起,且没有明显的 ActionSupport 痕迹,基本可以按 Struts1 的execute方法来解读。Struts1 用ActionServlet作为前端控制器,所有.do请求先进它,再由struts-config.xml分发给具体 Action。
2.1 请求路由先看 struts-config.xml,Action 的 path 就是 URL
在 Struts1 项目中,WEB-INF/classes下一定有一份struts-config.xml,它定义了所有请求的映射关系。下面是一个典型的登录配置片段:
<struts-config> <!-- 表单 Bean 与 JSP 表单字段绑定 --> <form-beans> <form-bean name="loginForm" type="com.vehicle.web.form.LoginForm"/> </form-beans> <action-mappings> <!-- path 即浏览器访问的 /login.do --> <action path="/login" type="com.vehicle.web.action.LoginAction" name="loginForm" scope="request" validate="true" input="/login.jsp"> <!-- Action 返回逻辑名后由框架转发到对应页面 --> <forward name="success" path="/main.jsp"/> <forward name="failed" path="/login.jsp?error=1"/> </action> </action-mappings> </struts-config>这段配置里,path="/login"表示浏览器访问/login.do时进入该 Action,type指定处理类全限定名,name="loginForm"让框架自动把表单字段封装进LoginForm对象,scope="request"控制表单对象存放到 request 还是 session,input是校验失败后的回退页。forward的name是逻辑跳转名,Action 里返回字符串后由它决定目标页面。注意validate="true"时,框架会先调用表单 Bean 的validate()方法,如果账号密码为空,请求会直接打回input页面,根本不进execute。
2.2 execute 方法里的标准动作:取参数 → 调 Service → 返回 Forward
Struts1 的 Action 生命周期中,框架只替你做参数封装和转发调度,真正的业务逻辑要交给 Service。看LoginAction的典型写法:
public class LoginAction extends Action { @Override public ActionForward execute(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) { // 从 ActionForm 获取页面提交参数 LoginForm loginForm = (LoginForm) form; String username = loginForm.getUsername(); String password = loginForm.getPassword(); // 从工厂拿 Service,内部实际取 Spring 容器中的 Bean UserService userService = ServiceFactory.getUserService(); User user = userService.login(username, password); if (user != null) { // 登录成功写入 session,跳转主页面 request.getSession().setAttribute("currentUser", user); return mapping.findForward("success"); } return mapping.findForward("failed"); } }这段代码的关键在ServiceFactory.getUserService()。在这个包里的.class文件中能看到UserServiceImpl,说明 Service 接口和实现是分开的。ServiceFactory是一个简单的静态工厂,内部通常从 Spring 容器里取userService这个 Bean——如果直接new UserServiceImpl(),等于是绕开了 Spring 的事务管理。登录这个操作没有事务问题还好,一旦后续执行出车登记、油料扣减这种多表写入,没有 Spring 代理的方法就会各自提交,数据一致性立刻出问题。
2.3 从 DAO 类名反推业务模块,先建立表资源地图
CarbstbDAO、DrivermDAO、OilmDAO、AnnumDAO、OutcmDAO、UcapliDAO、XxuserDAO这七个 DAO 类已经把这个系统的模块边界标得很清楚:
| 类名 | 对应业务域 | 常见表名 | 推测用途 |
|---|---|---|---|
| XxuserDAO | 系统用户 | XX_USER | 账号登录、权限控制 |
| CarbstbDAO | 车辆台账 | CAR_BASE | 车辆基本信息、状态 |
| DrivermDAO | 驾驶员档案 | DRIVER_MAIN | 驾驶员信息、准驾车型 |
| UcapliDAO | 车辆申请 | USE_CAR_APPLI | 用车申请单 |
| OutcmDAO | 出车管理 | CAR_OUT_CM | 出车记录、归还记录 |
| OilmDAO | 油料管理 | OIL_MAIN | 加油记录、油费统计 |
| AnnumDAO | 年审管理 | AVE_YEAR_NUM | 年检、保险日期提醒 |
画这张表的意义在于,接手一个只有.class和配置文件的项目,第一件事不是看 JSP,而是先把 DAO 类对应到表。OilmDAO里的方法大概率是findByDateRange、sumOilFee这类统计操作,AnnumDAO里则是查最近到期记录。后续做报表或加功能时,直接以这个映射为基础扩展,比反复倒推 XML 要快得多。Struts 层本身不写 SQL,它只负责把页面上来的请求参数转成 Service 方法入参,再把返回结果塞到 request 里。
3. Hibernate 持久层:Carbstb 实体映射、one-to-many 配置与 Oracle 方言坑
Carbstb和Driverm这些类名后半段的tb、m是表前缀简写,这种命名在老旧项目里很常见,意味着 Hibernate 映射关系多数写在.hbm.xml里而不是用注解。实体类负责把 Java 对象与 Oracle 表结构完成双向绑定,CarbstbDAO里任何findById或findByProperty调用,最终都会翻译成对CAR_BASE表的 SQL。
3.1 实体类与表字段的映射关系:Carbstb 的 hbm 配置
假设车辆台账表的主要字段是ID、CAR_NO(车牌号)、CAR_TYPE(车辆类型)、BUY_DATE(购置日期)、STATE(在库/出车/维修),对应的映射文件开头的写法如下:
<hibernate-mapping package="com.vehicle.entity"> <!-- 车辆台账表:Java 类与 Oracle 表一一对应 --> <class name="Carbstb" table="CAR_BASE"> <!-- 主键使用 Oracle 序列生成 --> <id name="id" column="ID" type="long"> <generator class="sequence"> <param name="sequence">SEQ_CAR_BASE</param> </generator> </id> <property name="carNo" column="CAR_NO" type="string" length="20" not-null="true"/> <property name="carType" column="CAR_TYPE" type="string" length="10"/> <property name="buyDate" column="BUY_DATE" type="date"/> <property name="state" column="STATE" type="integer"/> <!-- 一辆车对应多条出车记录,一对多集合用 set 表达 --> <set name="outRecords" table="CAR_OUT_CM" inverse="true" cascade="save-update"> <key column="CAR_ID"/> <one-to-many class="Outcm"/> </set> </class> </hibernate-mapping>主键生成方式选择了sequence,这是 Oracle 项目的标准做法,对应数据库里必须预先存在SEQ_CAR_BASE序列;如果换成 MySQL,一般用identity或native。not-null="true"限制车牌不能为空,length="20"和 Oracle 里VARCHAR2(20)对应,避免插入超长字符串时在 JDBC 层才报错。<set>里的inverse="true"表示CAR_BASE不是关系维护方,真正维护外键的是CAR_OUT_CM表中的CAR_ID字段,这样保存车辆时不会产生多余的 update 语句。
3.2 集合映射 one-to-many 的 fetch 策略,直接影响出车记录查询性能
用车申请和出车记录是一对多的典型场景,Ucapli(用车申请)对应多次审批或多次出车。如果只配置one-to-many而不指定加载策略,Hibernate 默认对集合使用延迟加载,在 session 关闭后再调用getOutRecords()就会抛LazyInitializationException。常见做法是在映射中显式声明:
<set name="applyItems" table="UCA_APPLY_ITEM" lazy="true" batch-size="20"> <key column="APPLY_ID"/> <one-to-many class="UcapliItem"/> </set>lazy="true"保证查询车辆列表时不会立即把每辆车的用车申请全查出来,batch-size="20"会在遍历 N 辆车时把每 20 辆车对应的申请表一次性用IN查询装载,把 1+N 次查询压缩成 1+⌈N/20⌉ 次。注意:批量抓取只在延迟加载触发的时刻生效,如果项目里出现“页面加载很慢,单条 SQL 很快”的现象,十有八九是这里没调好。
3.3 Oracle 方言与分页:别再直接把 LIMIT 带入 HQL
Hibernate 的dialect配置决定了它生成的 SQL 语法。Oracle 用Oracle10gDialect或Oracle12cDialect,分页时 Hibernate 生成三层嵌套 ROWNUM 查询,效果等价于:
SELECT * FROM ( SELECT t.*, ROWNUM rn FROM ( SELECT * FROM CAR_OUT_CM ORDER BY OUT_DATE DESC ) t WHERE ROWNUM <= 20 ) WHERE rn > 10;这句 SQL 里内层先做排序和行数截断,外层再按行号过滤偏移量。使用 HQL 时不需要手写 ROWNUM,直接调用:
Query query = session.createQuery("from Outcm o order by o.outDate desc"); query.setFirstResult(10); query.setMaxResults(10); List<Outcm> list = query.list();setFirstResult(10)对应从第 11 行开始取,setMaxResults(10)取 10 行。这里要特别注意:在 Oracle 中,如果查询没有明确的order by,分页结果是不可预期的,因为 ROWNUM 在排序前就被分配了。统计跨天数据时也常用trunc,比如查当日油费总额:
String hql = "select sum(o.fee) from Oilm o where o.feeDate >= trunc(sysdate)"; List<Long> result = session.createQuery(hql).list();trunc(sysdate)在 Oracle 里会把时间截断到当天零点,配合 HQL 参数化查询不会破坏索引。如果换成 MySQL,要用DATE(o.feeDate)或CAST,方言差异在这里体现得最直接。
4. Spring 容器接管:依赖注入、事务边界与 SSH 三条配置线怎么衔接
Spring 在 SSH 项目中处于中间层的核心位置:Struts 的 Action 从 Spring 拿 Service,Service 从 Spring 拿到 Hibernate 的 SessionFactory,事务管理器控制 Service 方法的提交回滚。.class清单里UserServiceImpl这种写法,默认就假设了 bean 定义在applicationContext.xml中,Action 不可能直接 new 一个UserServiceImpl再手动管理事务,否则就没有使用 Spring 的必要。
4.1 applicationContext.xml 里的 bean 定义顺序
配置文件里最核心的三组定义是数据源、SessionFactory、事务管理器:
<!-- 数据源:DBCP 连接池,对应 Oracle 的 JDBC 连接信息 --> <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"> <property name="driverClassName" value="oracle.jdbc.driver.OracleDriver"/> <property name="url" value="jdbc:oracle:thin:@127.0.0.1:1521:ORCL"/> <property name="username" value="vehicle_app"/> <property name="password" value="encrypted_placeholder"/> <property name="initialSize" value="2"/> <property name="maxActive" value="20"/> </bean> <!-- SessionFactory 由 Spring 创建,Hibernate 去加载映射文件 --> <bean id="sessionFactory" class="org.springframework.orm.hibernate3.LocalSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <property name="mappingLocations" value="classpath:com/vehicle/entity/*.hbm.xml"/> <property name="hibernateProperties"> <props> <prop key="hibernate.dialect">org.hibernate.dialect.Oracle10gDialect</prop> <prop key="hibernate.show_sql">true</prop> <prop key="hibernate.jdbc.batch_size">20</prop> </props> </property> </bean> <!-- Service 和 DAO 都交给容器管理,依赖通过 setter 注入 --> <bean id="userService" class="com.vehicle.service.impl.UserServiceImpl"> <property name="userDAO" ref="userDAO"/> </bean> <bean id="userDAO" class="com.vehicle.dao.impl.XxuserDAOImpl"> <property name="sessionFactory" ref="sessionFactory"/> </bean>dataSource使用 DBCP 连接池,initialSize和maxActive决定启动时建几个连接、峰值最多用几个。LocalSessionFactoryBean会把mappingLocations中所有 hbm 文件加载进 SessionFactory,hibernate.show_sql=true在开发阶段打开,生产环境要关掉。最关键的是userDAO要把sessionFactory注入进去,DAO 实现类再通过getHibernateTemplate()或直接持有 Session 操作数据库。
4.2 声明式事务:把 rollback 规则写在配置文件里
UserServiceImpl里如果只有login()一个读方法,事务边界的作用不明显,但涉及到出车登记插入CAR_OUT_CM同时更新CAR_BASE.STATE时,这两个操作必须在一个事务内。Spring 用 AOP 声明事务的经典配置是:
<!-- 事务管理器绑定到 Hibernate SessionFactory --> <bean id="txManager" class="org.springframework.orm.hibernate3.HibernateTransactionManager"> <property name="sessionFactory" ref="sessionFactory"/> </bean> <tx:advice id="txAdvice" transaction-manager="txManager"> <tx:attributes> <!-- 写操作必须加入事务 --> <tx:method name="add*" propagation="REQUIRED"/> <tx:method name="update*" propagation="REQUIRED"/> <tx:method name="delete*" propagation="REQUIRED"/> <!-- 查询方法只读,不做脏检查,提升性能 --> <tx:method name="find*" read-only="true" propagation="SUPPORTS"/> <tx:method name="login" read-only="true"/> <tx:method name="*" propagation="REQUIRED"/> </tx:attributes> </tx:advice> <aop:config> <aop:pointcut id="servicePointcut" expression="execution(* com.vehicle.service.impl.*.*(..))"/> <aop:advisor advice-ref="txAdvice" pointcut-ref="servicePointcut"/> </aop:config>事务配置的作用范围是service.impl包下的所有方法。add*、update*、delete*以REQUIRED传播进来,表示如果已经处于事务中就加入,没有则新建;read-only="true"的声明让 Hibernate 跳过脏检查,查询性能会好一些。最后一条*兜底,避免新加的方法因为没匹配到前缀而漏掉事务。注意一旦切到 Service 层,就不要再在 DAO 里调用session.beginTransaction(),否则两处事务叠加会互相干扰。
4.3 Spring 与 Struts 的桥接方式,决定 Action 里能不能用依赖注入
Struts1 的 Action 是框架自己new出来的,不在 Spring 管控范围内,要让 Action 能拿到 Spring 容器里的userService,需要做两件事。第一件是让 Struts 启动时先加载 Spring 上下文,在struts-config.xml里加插件:
<plug-in className="org.springframework.web.struts.ContextLoaderPlugIn"> <set-property property="contextConfigLocation" value="/WEB-INF/applicationContext.xml"/> </plug-in>第二件是把 Action 的type指到 Spring 的代理类上,由代理去容器里找真正的 Action Bean:
<action path="/login" type="org.springframework.web.struts.DelegatingActionProxy" name="loginForm" scope="request"> </action>DelegatingActionProxy会根据path自动到 Spring 容器中查找名为/login的 Bean。这种机制下,Action 里只需要声明UserService的 setter 方法,剩下的交给 Spring 完成。理解了这条链,就掌握了 Spring 容器在这里的核心作用:Struts 负责转发,Service 层由 Spring 管理生命周期,Bean 的创建、装配、代理都由容器统一控制,这也正是“手写 Spring”和真正 SSM/SSH 工程之间最大的差别。
5. 把编译产物部署回 Oracle 环境时,最先碰到的三个现实问题
到这里,代码逻辑已经梳理清楚,接下来需要把 .class 文件和配置文件还原成一个能跑的 Web 应用。这个步骤在实际操作中常被低估,所以用一章专门讲验证手段。
5.1 监听服务和 JDBC URL 对不上,Hibernate 报错只会显示连接超时
Oracle 安装完后最常见的故障就是监听服务无法启动,报错日志里出现ORA-12541: TNS:no listener。先检查监听状态:
# 查看监听是否在运行 lsnrctl status # 启动监听服务 lsnrctl start # 测试服务名连通性 tnsping ORCLtnsping ORCL返回OK后,再用 SQL*Plus 验证工具账号:
sqlplus vehicle_app/password@127.0.0.1:1521/ORCL如果sqlplus能连上但 JDBC 连不上,问题几乎都在 URL 的 service name 上。项目里如果是jdbc:oracle:thin:@127.0.0.1:1521:ORCL,这种写法连接的是 SID;如果数据库装的是 12c 以后的 CDB/PDB 模式,需要改成/pdborcl格式的 service name。手动改完applicationContext.xml里的url后,不要直接重启应用服务器,先执行一下sqlplus确认改法是否正确,因为 Tomcat 打印出的堆栈往往是Io exception: The Network Adapter could not establish the connection,不会告诉你具体是哪个配置值错了。
5.2 用 SQL 验证核心表数据和序列状态
部署前还要确认表、序列、测试数据都齐全。Hibernate 的hbm2ddl一般不设为update,所以表要由 DBA 脚本建好。快速检查数据可用性:
-- 检查主表和流水表是否有数据 SELECT COUNT(*) FROM CAR_BASE; SELECT MAX(ID) FROM CAR_OUT_CM; -- 验证主键序列可用,避免插入时报 ORA-02289 SELECT SEQ_CAR_BASE.NEXTVAL FROM DUAL; -- 出车费用按天汇总,模拟页面月度统计报表 SELECT TRUNC(OUT_DATE) 出车日期, SUM(FEE) 费用合计 FROM CAR_OUT_CM WHERE OUT_DATE >= TRUNC(SYSDATE-30) GROUP BY TRUNC(OUT_DATE);第一段验证表是否为空、主键序列能否自增;第二段是出车费用按天汇总,直接对应页面上的油耗统计和月度报表。如果SUM(FEE)的结果在页面里显示成科学计数法,问题出在 JDBC 映射或前端格式化,可以在查询时用TO_CHAR(SUM(FEE),'FM999,999.00')转成字符串再回传。
5.3 排错顺序:先看 Hibernate 生成的 SQL,再查 Action 跳转
SSH 项目排错和 Spring Boot 不同,没有统一 starter 日志。最有效的下探方式是把show_sql打开并配好 log4j,让日志输出标准 SQL 绑定参数,然后按三层链条定位:页面点按钮后 URL 是否变成预期的.do,Action 返回的forward是否有对应定义,最后看 SQL 执行结果。如果 SQL 没打印出来而页面报 500,问题一定在 Spring 容器初始化或 Struts 插件加载阶段,优先翻 Tomcat 启动日志里BeanCreationException所在的 Bean 名。这套从请求链路反查的排查方式,适用于任何 Struts + Hibernate + Spring 的老项目,比在 JSP 里逐行打断点要快得多。
本文还有配套的精品资源,点击获取