基于SSH框架的固定资产管理系统:Java企业级应用实战解析
简介本资源是一套基于Java Web主流技术栈开发的固定资产管理系统完整实现方案面向计算机专业本科生毕业设计与课程实践需求解决企事业单位资产登记、入库、领用、归还、报废等全生命周期管理问题。压缩包共376个文件涵盖75个核心Java业务类、31个JSP页面、22个XML配置文件含Struts、Spring、Hibernate配置、69个Jar依赖库及配套CSS/JS/GIF资源另有3个PPT设计文档与3个Word说明文档整体容量152.28MB结构清晰、模块划分明确。已有548人学习下载所有源码均经实测可直接运行包含员工管理、资产信息维护、出入库操作、借用归还处理等完整功能模块预览可见InOutAssetsDAOImpl、SaveLendZCAction等典型类名体现SSH三层架构与MVC流程控制逻辑。配套提供系统部署说明、数据库SQL脚本及关键操作辅导视频助力快速理解架构设计与业务落地细节。1. 项目缘起为什么我们还需要一个“固定资产管理系统”几年前我在一家中型科技公司负责内部IT系统维护当时公司资产管理还停留在Excel表格和纸质标签的阶段。每到年底盘点行政、财务、IT几个部门就得抽调人手花上一两周时间拿着打印出来的表格一个工位一个工位地去核对电脑、显示器、服务器、甚至桌椅板凳。最头疼的是经常发现资产“对不上号”——要么是设备已经报废但没在表格里更新要么是员工离职后设备交接不清甚至还有资产被部门间私自调拨导致财务折旧计算和实物完全脱节。这种混乱不仅造成资产流失和重复采购的浪费更让IT运维和成本核算变得异常困难。正是这种切肤之痛让我下定决心要自己动手设计并实现一套贴合中小企业实际需求的固定资产管理系统。市面上当然有成套的ERP或专门的资产管理软件但它们要么功能过于庞杂、价格昂贵要么定制化程度低无法满足我们公司特有的审批流程和报表需求。所以我的目标很明确用最经典、最稳定的Java技术栈打造一个轻量、易用、可二次开发的管理系统。这就是今天要分享的“基于JavaSSHJSP的固定资产管理系统”项目的由来。它不是什么颠覆性的创新但绝对是一个能解决实际业务痛点、代码结构清晰、非常适合Java学习者深入企业级应用开发的实战案例。2. 技术选型复盘为什么是SSH框架而不是Spring Boot看到“SSH”Struts2 Spring Hibernate这个组合很多现在的开发者可能会觉得有点“复古”。确实在微服务和Spring Boot大行其道的今天传统的SSH框架似乎不再是主流选择。但在当时的环境下以及对于特定类型的学习和项目而言这个选择有其深刻的合理性。2.1 框架组合的职责与优势首先我们明确一下这三个框架在项目中的核心分工Struts2充当MVC框架中的控制器Controller层。它负责接收来自JSP页面的HTTP请求调用相应的业务逻辑并决定将哪个视图JSP页面返回给用户。它的核心配置文件是struts.xml通过配置Action来映射请求路径。选择Struts2是因为它的拦截器Interceptor机制非常强大可以方便地实现权限验证、日志记录、异常处理等通用功能通过一个统一的配置就能应用到所有Action上。Spring充当项目的“大管家”核心是IoC控制反转容器和AOP面向切面编程。项目中所有的Bean如Service业务层、DAO数据访问层、事务管理器等都由Spring容器来创建和管理并通过依赖注入DI的方式组装在一起。这极大地降低了模块间的耦合度。例如AssetService的实现类不需要自己new一个AssetDao而是由Spring在运行时注入。同时Spring的声明式事务管理让我们只需在配置文件中或使用注解就能轻松管理数据库事务无需在代码中手动编写冗长的try-catch-finally。Hibernate充当ORM对象关系映射框架负责数据持久化层。它的作用是将Java对象如Asset、Employee、Department与数据库中的表自动映射起来。开发者主要操作这些Java对象而由Hibernate自动生成SQL语句来执行增删改查。这避免了编写大量重复、易错的JDBC代码提高了开发效率和数据操作的安全性一定程度上防止SQL注入。这个组合的优势在于分层清晰、职责单一。每一层做什么事由哪个框架负责界限非常明确。对于学习和理解经典Java EE的分层架构表现层、控制层、业务层、持久层有极大的帮助。你能清楚地看到请求是如何从JSP页面发出经过Struts2的Action调用Spring管理的Service再通过Hibernate与数据库交互最后数据流又一层层返回页面的完整生命周期。2.2 与Spring Boot的对比思考那么为什么不直接用更现代的Spring Boot呢这里涉及到项目背景和目的的权衡。Spring Boot的核心优势是约定大于配置和快速启动。它通过自动配置和起步依赖Starter极大地简化了项目搭建和部署过程让你能快速构建一个可运行的微服务。然而这种“简化”有时会像一把双刃剑隐藏了细节对于初学者Spring Boot自动配置了几乎一切你可能在不清楚背后原理比如Spring MVC的DispatcherServlet是如何初始化的事务管理器是如何配置的的情况下就让项目跑起来了。这对于快速开发是好事但对于深度理解框架原理和整合过程却是不利的。技术栈耦合Spring Boot生态虽然丰富但通常更鼓励使用其全家桶Spring MVC, Spring Data JPA等。而SSH项目让你有机会手动整合三个不同的、曾经是业界标准的框架这个过程本身就是一项极具价值的技能。你需要手动编写applicationContext.xml来配置Spring Bean编写struts.xml来配置Action编写hibernate.cfg.xml来配置数据库映射。这个过程能让你深刻理解框架间是如何协作的。所以这个项目的技术选型其价值更偏向于“教学”和“原理理解”。它像一个经典的解剖标本把企业级应用的骨骼、肌肉、神经对应MVC各层清晰地展示出来。完成这样一个项目后你再去看Spring Boot就会明白它到底帮你简化了什么底层是如何工作的从而具备更强的技术把控和问题排查能力。当然如果是用于当今的生产环境我会毫不犹豫地推荐使用Spring Boot MyBatis-Plus或Spring Data JPA的组合以获得更高的开发效率和更现代的生态支持。3. 系统核心功能模块设计与实现拆解一个完整的固定资产管理系统远不止是对数据库表的增删改查。它需要围绕资产“从生到死”的全生命周期进行管理。下面我将结合关键代码片段和配置详细拆解几个核心模块的实现。3.1 资产信息管理核心实体与Hibernate映射这是系统的基石。首先我们需要设计核心的实体类Asset并通过Hibernate注解将其映射到数据库表。// Asset.java 实体类 Entity Table(name t_asset) public class Asset implements Serializable { Id GeneratedValue(strategy GenerationType.IDENTITY) private Integer id; // 资产ID主键自增 Column(name asset_code, unique true, nullable false, length 50) private String assetCode; // 资产编号唯一标识如 IT-LAPTOP-001 Column(name asset_name, nullable false, length 100) private String assetName; // 资产名称 Column(name category, length 50) private String category; // 资产类别电脑、服务器、办公家具等 Column(name specification, length 200) private String specification; // 规格型号 Column(name purchase_date) Temporal(TemporalType.DATE) private Date purchaseDate; // 采购日期 Column(name purchase_price, precision 10, scale 2) private BigDecimal purchasePrice; // 采购价格 Column(name current_status, length 20) private String currentStatus; // 当前状态在用、闲置、维修、报废 Column(name location, length 100) private String location; // 存放地点 // 关联到使用人员工。多对一关系一个资产在同一时间只能被一个员工使用。 ManyToOne(fetch FetchType.LAZY) // 懒加载提高性能 JoinColumn(name user_id) private Employee user; // 关联到所属部门。多对一关系。 ManyToOne(fetch FetchType.LAZY) JoinColumn(name department_id) private Department department; // 省略 getter, setter, toString 方法... }关键点解析Entity和Table声明这是一个JPA实体并指定对应的数据库表名。Id和GeneratedValue定义主键及其生成策略。GenerationType.IDENTITY依赖于数据库的自增主键如MySQL的AUTO_INCREMENT。Column定义字段属性。这里特别注意uniquetrue确保了资产编号的唯一性这是业务上的强需求。ManyToOne定义了资产与员工、部门的多对一关系。fetch FetchType.LAZY是重要的性能优化手段意味着只有在真正访问asset.getUser()时Hibernate才会去查询关联的Employee数据避免了不必要的联表查询。为什么用BigDecimal表示价格这是金融计算中的铁律。float和double在计算时会有精度丢失绝不可用于货币。BigDecimal可以精确表示和计算小数。对应的我们需要Employee和Department实体并与Asset建立关联。同时需要在Spring的配置文件中配置Hibernate的SessionFactory和数据源。!-- applicationContext.xml 片段 -- !-- 1. 配置数据源 -- bean iddataSource classorg.apache.commons.dbcp2.BasicDataSource destroy-methodclose property namedriverClassName valuecom.mysql.jdbc.Driver/ property nameurl valuejdbc:mysql://localhost:3306/asset_db?useUnicodetrueamp;characterEncodingutf8/ property nameusername valueroot/ property namepassword valueyourpassword/ property nameinitialSize value5/ property namemaxTotal value20/ /bean !-- 2. 配置Hibernate SessionFactory -- bean idsessionFactory classorg.springframework.orm.hibernate5.LocalSessionFactoryBean property namedataSource refdataSource/ property namepackagesToScan list valuecom.yourcompany.asset.entity/value !-- 实体类所在包 -- /list /property property namehibernateProperties props prop keyhibernate.dialectorg.hibernate.dialect.MySQL5Dialect/prop prop keyhibernate.show_sqltrue/prop !-- 开发时显示SQL便于调试 -- prop keyhibernate.format_sqltrue/prop prop keyhibernate.hbm2ddl.autoupdate/prop !-- 自动更新表结构生产环境慎用 -- /props /property /bean !-- 3. 配置事务管理器 -- bean idtransactionManager classorg.springframework.orm.hibernate5.HibernateTransactionManager property namesessionFactory refsessionFactory/ /bean !-- 开启注解驱动的事务管理 -- tx:annotation-driven transaction-managertransactionManager/3.2 资产入库与领用流程Struts2 Action与Service层协作资产采购回来后需要录入系统这就是“入库”操作。员工需要使用时则发起“领用”申请。这个过程涉及到表单提交、业务逻辑处理和事务管理。JSP页面asset_add.jsp提供一个表单用于填写资产信息。form actionasset_save.action methodpost 资产编号input typetext nameasset.assetCode requiredbr 资产名称input typetext nameasset.assetName requiredbr 采购价格input typetext nameasset.purchasePricebr 当前状态select nameasset.currentStatus option valueIN_STOCK库存/option option valueIN_USE在用/option /selectbr !-- 其他字段... -- input typesubmit value保存 /formStruts2 ActionAssetAction.java接收请求调用Service。public class AssetAction extends ActionSupport { // 使用模型驱动Struts2会自动将表单参数注入到asset对象中 private Asset asset; // 依赖注入的Service private AssetService assetService; // 保存资产的方法 public String save() { try { // 可以在这里做一些简单的数据校验 if (asset.getAssetCode() null || asset.getAssetCode().trim().isEmpty()) { addFieldError(asset.assetCode, 资产编号不能为空); return INPUT; // 返回输入页面显示错误 } // 调用业务层方法 assetService.saveAsset(asset); addActionMessage(资产保存成功); return SUCCESS; } catch (Exception e) { addActionError(保存资产失败 e.getMessage()); e.printStackTrace(); return ERROR; } } // 省略 getter, setter... }Struts2配置struts.xmlstruts package nameasset extendsstruts-default action nameasset_save classassetAction methodsave result namesuccess/asset_list.jsp/result result nameinput/asset_add.jsp/result result nameerror/error.jsp/result /action /package /strutsSpring Service层AssetServiceImpl.java实现核心业务逻辑和事务控制。Service // Spring的Service注解标识这是一个业务层Bean Transactional // Spring的Transactional注解声明此类的所有方法都需要事务管理 public class AssetServiceImpl implements AssetService { Autowired // Spring自动注入DAO private AssetDao assetDao; Override public void saveAsset(Asset asset) { // 业务逻辑例如检查资产编号是否已存在 Asset existingAsset assetDao.findByAssetCode(asset.getAssetCode()); if (existingAsset ! null !existingAsset.getId().equals(asset.getId())) { throw new RuntimeException(资产编号已存在请勿重复添加); } // 设置默认值如入库时间为当前时间 if (asset.getPurchaseDate() null) { asset.setPurchaseDate(new Date()); } // 调用DAO层保存 assetDao.save(asset); // 这里可以扩展其他逻辑如记录操作日志、发送通知等 // logService.log(新增资产 asset.getAssetCode()); } Override Transactional(readOnly true) // 查询方法标记为只读事务优化性能 public Asset getAssetById(Integer id) { return assetDao.findById(id); } // 其他方法... }关键点与踩坑经验Struts2的模型驱动在Action中使用private Asset asset并提供getter/setter配合nameasset.assetCode的表单命名可以自动封装对象非常方便。但要注意命名规范。Spring事务管理Transactional注解是保证数据一致性的关键。在saveAsset方法中如果检查编号重复后抛出异常Spring会回滚整个事务确保无效数据不会进入数据库。务必注意默认情况下Transactional只对RuntimeException及其子类进行回滚对Exception不回滚。如果你抛出的自定义异常是Exception的子类而非RuntimeException需要在注解中指定Transactional(rollbackFor Exception.class)。异常处理Action层捕获Service层的异常并转换为用户友好的错误信息addActionError。Service层抛出的异常应该是业务异常而不是一堆晦涩的SQLException或HibernateException。良好的异常设计是系统健壮性的体现。DAO层设计AssetDao通常继承自一个泛型的BaseDao利用HibernateTemplate或JPA的EntityManager提供通用的save,delete,findById等方法。这里不展开但这是减少重复代码的常用模式。3.3 资产盘点与报表统计复杂查询与分页实现盘点功能需要根据多种条件部门、状态、时间范围组合查询资产并且结果通常需要分页显示。这是考察DAO层和Service层设计能力的地方。Service层接口增加方法public interface AssetService { // ... 其他方法 /** * 分页条件查询资产 * param condition 查询条件封装对象 * param pageNo 页码 * param pageSize 每页大小 * return 包含分页数据和总记录数的Page对象 */ PageAsset findAssetsByCondition(AssetCondition condition, int pageNo, int pageSize); }查询条件封装类AssetCondition.javapublic class AssetCondition { private String assetCode; private String assetName; private String category; private String departmentId; // 部门ID private String status; private Date purchaseDateStart; private Date purchaseDateEnd; // ... getter, setter }DAO层实现AssetDaoImpl.java动态查询这里展示使用Hibernate的CriteriaAPI进行动态查询也可以使用HQL。Repository public class AssetDaoImpl extends HibernateDaoSupport implements AssetDao { // 继承HibernateDaoSupport获得模板支持 Autowired public void setSessionFactoryBean(SessionFactory sessionFactory) { super.setSessionFactory(sessionFactory); } Override public PageAsset findAssetsByCondition(AssetCondition condition, int pageNo, int pageSize) { // 1. 创建Criteria对象 DetachedCriteria dc DetachedCriteria.forClass(Asset.class, a); // 2. 创建别名用于关联查询如果需要按部门名称查询 dc.createAlias(a.department, dept, JoinType.LEFT_OUTER_JOIN); // 3. 动态添加查询条件 if (StringUtils.isNotBlank(condition.getAssetCode())) { dc.add(Restrictions.like(a.assetCode, % condition.getAssetCode() %)); } if (StringUtils.isNotBlank(condition.getAssetName())) { dc.add(Restrictions.like(a.assetName, % condition.getAssetName() %)); } if (StringUtils.isNotBlank(condition.getDepartmentId())) { dc.add(Restrictions.eq(dept.id, Integer.parseInt(condition.getDepartmentId()))); } if (condition.getPurchaseDateStart() ! null) { dc.add(Restrictions.ge(a.purchaseDate, condition.getPurchaseDateStart())); } if (condition.getPurchaseDateEnd() ! null) { dc.add(Restrictions.le(a.purchaseDate, condition.getPurchaseDateEnd())); } // 4. 执行查询获取总记录数用于分页 DetachedCriteria countDc dc; // 注意这里最好复制一个Criteria专门用于count避免排序等影响 Long totalCount (Long) this.getHibernateTemplate().findByCriteria( countDc.setProjection(Projections.rowCount())).get(0); // 5. 设置分页和排序执行查询获取当前页数据 ListAsset list (ListAsset) this.getHibernateTemplate().findByCriteria(dc, (pageNo - 1) * pageSize, // 起始索引 pageSize); // 每页数量 // 可以添加排序 dc.addOrder(Order.desc(a.purchaseDate)); // 6. 封装分页结果 PageAsset page new Page(); page.setPageNo(pageNo); page.setPageSize(pageSize); page.setTotalCount(totalCount.intValue()); page.setList(list); page.setTotalPage((int) Math.ceil((double) totalCount / pageSize)); return page; } }JSP页面分页展示asset_list.jsp!-- 显示查询结果表格 -- table trth资产编号/thth名称/thth部门/thth状态/thth操作/th/tr s:iterator valuepage.list varasset tr tds:property value#asset.assetCode//td tds:property value#asset.assetName//td tds:property value#asset.department.deptName//td !-- 通过关联对象获取部门名 -- tds:property value#asset.currentStatus//td tda hrefasset_edit.action?asset.ids:property value#asset.id/编辑/a | .../td /tr /s:iterator /table !-- 分页条 -- div classpagination 共 s:property valuepage.totalCount/ 条记录 第 s:property valuepage.pageNo//s:property valuepage.totalPage/ 页 s:if testpage.hasPrev a hrefasset_list.action?pageNos:property valuepage.pageNo-1/上一页/a /s:if s:iterator begin%{page.startPage} end%{page.endPage} vari s:if test#i page.pageNo strongs:property value#i//strong /s:if s:else a hrefasset_list.action?pageNos:property value#i/s:property value#i//a /s:else /s:iterator s:if testpage.hasNext a hrefasset_list.action?pageNos:property valuepage.pageNo1/下一页/a /s:if /div报表统计除了列表系统还需要统计报表如“各部门资产总值”、“资产状态分布图”。这类功能通常在Service层编写专门的统计方法使用HQL进行分组聚合查询然后将结果集可能是ListObject[]传递给JSP页面或者使用JFreeChart等库生成图表图片返回。这部分代码更复杂但核心思想不变DAO负责复杂查询Service组织业务逻辑Action传递数据JSP展示。踩坑经验N1查询问题在asset_list.jsp中我们通过#asset.department.deptName获取部门名。如果Hibernate映射中配置的是FetchType.LAZY懒加载并且查询资产列表时没有主动抓取fetch部门信息那么渲染这N条资产记录时Hibernate会为每一条资产额外发起一次SQL查询去获取其部门信息这就是著名的N1查询问题会导致性能急剧下降。解决方案在DAO的查询方法中使用Criteria的setFetchMode(“department”, FetchMode.JOIN)或HQL的left join fetch来主动进行关联抓取将关联数据在一次查询中全部取出。3.4 权限控制与日志记录Struts2拦截器与Spring AOP的应用一个管理系统必须有权限控制。例如普通员工只能查看和领用资产部门经理可以审批领用和调拨管理员则拥有所有权限。使用Struts2拦截器实现权限验证定义拦截器创建一个实现Interceptor接口的类AuthInterceptor。public class AuthInterceptor extends AbstractInterceptor { Override public String intercept(ActionInvocation invocation) throws Exception { ActionContext ctx invocation.getInvocationContext(); MapString, Object session ctx.getSession(); User loginUser (User) session.get(LOGIN_USER); // 获取当前请求的Action名称和命名空间 String actionName invocation.getProxy().getActionName(); String namespace invocation.getProxy().getNamespace(); String fullActionPath namespace / actionName; // 判断是否为登录、注销等公开Action if (isPublicAction(fullActionPath)) { return invocation.invoke(); // 放行 } // 检查用户是否登录 if (loginUser null) { ctx.put(message, 请先登录); return login; // 返回登录页面 } // 检查用户权限这里简化处理实际应从数据库或缓存加载用户权限列表 if (!hasPermission(loginUser, fullActionPath)) { ctx.put(message, 您没有执行此操作的权限); return error; } // 权限验证通过继续执行后续拦截器和Action return invocation.invoke(); } // ... 实现 isPublicAction 和 hasPermission 方法 }配置拦截器栈在struts.xml中配置自定义拦截器并将其应用到需要权限控制的package或action上。struts package namesecure extendsstruts-default interceptors interceptor nameauth classcom.yourcompany.interceptor.AuthInterceptor/ !-- 定义一个新的拦截器栈包含默认栈和我们的权限拦截器 -- interceptor-stack namesecureStack interceptor-ref namedefaultStack/ interceptor-ref nameauth/ /interceptor-stack /interceptors !-- 将此包下的所有Action默认使用secureStack -- default-interceptor-ref namesecureStack/ global-results result namelogin/login.jsp/result result nameerror/error.jsp/result /global-results action nameasset_* classassetAction method{1} !-- 此Action会继承package的默认拦截器栈 -- /action /package /struts使用Spring AOP实现操作日志记录权限控制是“事前”的操作日志则是“事后”的审计追踪。Spring AOP非常适合处理这种横切关注点。定义日志注解Target(ElementType.METHOD) Retention(RetentionPolicy.RUNTIME) Documented public interface OperateLog { String module() default ; // 操作模块如“资产管理” String type() default ; // 操作类型如“新增”、“修改”、“删除” String description() default ; // 操作描述 }定义切面类Component Aspect public class OperateLogAspect { Autowired private OperateLogService logService; // 注入日志服务 // 定义切点所有被OperateLog注解的方法 Pointcut(annotation(com.yourcompany.annotation.OperateLog)) public void logPointCut() {} // 环绕通知 Around(logPointCut()) public Object around(ProceedingJoinPoint joinPoint) throws Throwable { // 方法执行前 MethodSignature signature (MethodSignature) joinPoint.getSignature(); Method method signature.getMethod(); OperateLog logAnnotation method.getAnnotation(OperateLog.class); String module logAnnotation.module(); String type logAnnotation.type(); String desc logAnnotation.description(); String methodName joinPoint.getTarget().getClass().getName() . method.getName(); // 获取当前登录用户可以从Session或ThreadLocal中取 User currentUser getCurrentUser(); String operator currentUser ! null ? currentUser.getUsername() : 未知用户; // 获取请求参数简化处理 Object[] args joinPoint.getArgs(); String params JSON.toJSONString(args); long startTime System.currentTimeMillis(); Object result null; try { // 执行目标方法 result joinPoint.proceed(); long endTime System.currentTimeMillis(); // 方法执行成功记录日志 saveLog(operator, module, type, desc, methodName, params, 成功, (endTime - startTime)); } catch (Exception e) { long endTime System.currentTimeMillis(); // 方法执行失败记录日志 saveLog(operator, module, type, desc, methodName, params, 失败 e.getMessage(), (endTime - startTime)); throw e; // 将异常继续抛出 } return result; } // ... 实现 saveLog 和 getCurrentUser 方法 }在Service方法上使用注解Service public class AssetServiceImpl implements AssetService { Override OperateLog(module 资产管理, type 新增, description 新增固定资产) Transactional public void saveAsset(Asset asset) { // ... 业务逻辑 } }这样任何被OperateLog注解的Service方法其操作人、操作时间、参数、结果和耗时都会被自动记录到数据库实现了无侵入式的日志记录。4. 项目部署、优化与常见问题排查4.1 环境搭建与项目部署环境准备JDK建议JDK 8或11配置好JAVA_HOME环境变量。IDEEclipse或IntelliJ IDEA。IDEA对SSH框架的支持更友好特别是XML配置文件的提示。Web服务器Tomcat 8.x或9.x。数据库MySQL 5.7或以上。创建数据库asset_db并导入项目SQL脚本如果有。导入项目将下载的源码解压在IDE中导入为Maven项目或普通Web项目。关键检查点检查WEB-INF/lib下是否包含了所有必要的Jar包Struts2-core, Spring-core, Spring-context, Hibernate-core, MySQL-connector等或者检查pom.xml如果是Maven项目依赖是否正确。配置文件修改src/main/resources/jdbc.properties修改数据库连接URL、用户名和密码确保与本地环境一致。web.xml检查Spring的ContextLoaderListener和Struts2的Filter配置是否正确。部署运行将项目添加到Tomcat服务器启动Tomcat。访问http://localhost:8080/项目上下文路径/。通常首页是login.jsp或index.jsp。4.2 性能优化与安全考量数据库连接池我们使用了DBCP2。在生产环境中可以考虑性能更好的HikariCP。只需在Spring配置中更换dataSource的Bean定义即可。二级缓存对于不常修改的静态数据如部门、资产类别字典可以启用Hibernate的二级缓存如Ehcache减少数据库访问。SQL优化使用show_sql监控生成的SQL警惕N1查询优化复杂查询的索引。输入验证与防XSSStruts2自带一些XSS防护但前端JSP和后端Action/Service都应进行输入验证和过滤。可以使用StringEscapeUtils.escapeHtml4()对用户输入进行转义防止脚本注入。密码安全用户密码绝不应明文存储。使用BCryptPasswordEncoder等强哈希算法进行加密存储。4.3 常见问题与排查指南启动时报ClassNotFoundException或NoClassDefFoundError原因缺少某个Jar包或者Jar包版本冲突。排查检查WEB-INF/lib或Maven依赖树。特别关注Spring、Hibernate、Struts2核心包及其传递依赖的版本兼容性。一个常见的坑是不同库引入了不同版本的asm或javassist。访问Action报404错误原因URL映射错误。排查检查web.xml中Struts2过滤器的url-pattern是否为/*。检查struts.xml中action的name和namespace配置是否正确。检查Tomcat控制台是否有Struts2启动成功的日志。事务不生效数据操作没回滚原因Service类没有被Spring管理缺少Service注解或未在XML中配置。事务方法被同类中的其他非事务方法调用导致AOP代理失效Spring事务基于AOP。抛出的异常不是RuntimeException且未在Transactional中指定rollbackFor。排查在Spring配置中开启调试日志查看事务管理器是否被正确创建和调用。确保事务方法是从Spring代理对象调用的。JSP页面显示乱码原因字符编码不一致。解决确保JSP文件头部有% page contentType“text/html;charsetUTF-8” language“java” %。在web.xml中配置字符编码过滤器Spring提供了CharacterEncodingFilter。数据库连接URL加上参数?useUnicodetruecharacterEncodingutf8。Hibernate懒加载异常LazyInitializationException现象在JSP页面中访问关联对象如asset.getDepartment().getName()时抛出异常。原因Session在Service层方法结束后已关闭Open Session in View模式未开启到了视图层无法再延迟加载数据。解决方案一推荐在查询时使用join fetch或FetchMode.JOIN主动抓取关联数据。方案二配置Open Session in View过滤器Spring有OpenSessionInViewFilter但需谨慎使用可能延长Session生命周期影响性能。这个项目麻雀虽小五脏俱全。从需求分析、数据库设计、框架整合、业务编码到部署调试完整地走完了一个Java Web项目的开发流程。虽然技术栈是经典的SSH但其中蕴含的分层思想、设计模式如MVC、依赖注入、AOP、问题排查思路在任何Java技术体系中都是相通的。希望这份详细的拆解能帮助你不仅“跑通”这个系统更能理解其背后的“所以然”为你的Java企业级开发之路打下坚实的基础。本文还有配套的精品资源点击获取