ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

SSH分层架构实战:Hibernate映射、Spring事务与HQL调优

SSH分层架构实战:Hibernate映射、Spring事务与HQL调优 简介《基于SSH框架的Web网站设计与实现》是一份面向计算机与通信工程专业学生、Java Web初学者及毕业设计撰写者的PDF论文资料围绕Struts、Spring、Hibernate三大开源框架的整合应用讲解Web网站从可行性分析、需求分析到系统设计与实现的完整流程并涵盖摘要、前言、设计技术、系统运行环境与结论建议等章节。压缩包内为1个PDF文件整体约75KB篇幅紧凑便于在电脑或移动端随时查阅。目前已有127人学习下载。文档以官网系统、图书出售系统、办公系统为设计案例依次展开系统架构、数据库设计、持久层Hibernate实现、控制层Struts实现与业务层Spring实现并附有结论与建议可帮助读者理解三层架构的分层职责、框架配置思路与数据库建表逻辑适合作为课程设计、毕业论文的参考范本和SSH整合开发的入门指引。1. 从论文到可运行系统SSH分层架构到底解决了什么问题翻这份《基于SSH框架的Web网站设计与实现》的人第一反应多半是又是三个框架叠在一起的老题。可真正按目录把官网系统、图书出售系统、办公系统三块功能落地会发现它是一份很典型的企业级 Java Web 训练样本Struts 管请求分发与页面跳转Spring 管对象装配和事务Hibernate 管实体类与关系表之间的映射。它处理的正是同一份代码里页面流转、业务编排、SQL 拼装三件事互相缠绕的老毛病——分层之前一个 Servlet 里既写 SQL 又做校验还 forward 页面改一处牵全身分层之后每层只对自己的输入输出负责。适合两类人要照着目录补课程设计或毕业论文的在校生以及想回头把 SSH 这套分层思想摸清、再对照当下 Spring Boot 单体项目的后端工程师。下面按数据库、持久层、业务层、控制层、联调排错五段拆开讲每一步都落到能跑的配置和代码上。2. 数据库设计与Hibernate实体映射落地数据库这一层是整份资料里最容易被写虚的部分论文往往给一张 E-R 图就过了但真正让三个子系统跑起来表结构必须先定死。官网、图书出售、办公三块业务共享一套用户表其余各自独立这种一库多域的划分直接决定了后面 Hibernate 映射文件怎么拆。2.1 三个子系统的表结构拆解表名关键字段归属子系统说明t_userid, username, password, role, status共用role 区分前台会员与后台管理员t_noticeid, title, content, publish_time官网系统公告与新闻t_bookid, book_name, price, stock, category_id图书出售category_id 外键指向 t_categoryt_categoryid, cate_name, parent_id图书出售支持一级/二级分类t_orderid, order_no, user_id, total_amount, order_state图书出售order_state 表示待付款/已发货t_order_itemid, order_id, book_id, quantity, unit_price图书出售订单明细价格快照t_oa_docid, doc_no, title, drafter, flow_state办公系统flow_state 表示审批流转状态设计时有两个点值得较真。一是价格字段统一用DECIMAL(10,2)而不是 float金额运算一旦用浮点累加几十条明细就会出现零点几分的误差对账时非常难查。二是订单明细里冗余存unit_price而不是下单时靠book_id现查因为商品调价后历史订单金额必须保持原样这是订单类表设计的常见做法。2.2 实体类与映射文件怎么写论文里提到 Hibernate 实现2013 年前后主流是 XML 映射也可以直接用注解。用注解更省事实体和映射放一起改字段不容易漏同步Entity Table(name t_book) public class Book implements Serializable { Id GeneratedValue(strategy GenerationType.IDENTITY) private Long id; Column(name book_name, nullable false, length 100) private String bookName; Column(name price, precision 10, scale 2) private BigDecimal price; Column(name stock) private Integer stock; // 多对一多本书属于一个分类LAZY 避免查书时顺带把分类整表拉出来 ManyToOne(fetch FetchType.LAZY) JoinColumn(name category_id) private Category category; // getter / setter 省略 }GeneratedValue(strategy IDENTITY)对应 MySQL 的自增主键交给数据库生成比native更明确。FetchType.LAZY是关键默认多对一在部分版本是 EAGER一旦图书列表页渲染时循环访问book.getCategory().getCateName()N1 查询会瞬间拖慢响应改成 LAZY 后由业务代码决定何时取。如果用 hbm.xml等价写法是class namecom.demo.entity.Book tablet_book id nameid columnid generator classidentity/ /id property namebookName columnbook_name not-nulltrue length100/ property nameprice columnprice typebig_decimal/ many-to-one namecategory columncategory_id lazyproxy fetchselect/ /classlazyproxy表示关联对象用代理访问时才发 SQLfetchselect是默认的单独查询策略如果确认列表页一定会用到分类名可以改成fetchjoin用一条外连接捞回来用空间换查询次数。2.3 泛型DAO基类与HQL分页三个子系统每个都有增删改查如果每个实体写一套重复代码维护成本会很高。常见做法是抽一个泛型基类public abstract class BaseDaoHibernateT, ID extends Serializable extends HibernateDaoSupport { private ClassT entityClass; SuppressWarnings(unchecked) public BaseDaoHibernate() { // 通过反射拿到子类上声明的泛型实参避免手写 Class 参数 Type type getClass().getGenericSuperclass(); this.entityClass (ClassT) ((ParameterizedType) type).getActualTypeArguments()[0]; } public T get(ID id) { return (T) getHibernateTemplate().get(entityClass, id); } public void save(T entity) { getHibernateTemplate().save(entity); } SuppressWarnings(unchecked) public ListT findByHql(String hql, Object... args) { return (ListT) getHibernateTemplate().find(hql, args); } }getHibernateTemplate()由HibernateDaoSupport提供它内部会判断当前线程是否已有事务绑定好的 Session有就复用没有就临时开一个这也是后面 Spring 声明式事务能生效的前提。分页则用Query.setFirstResult / setMaxResults实现public PageBook pageByKeyword(String keyword, int pageNo, int pageSize) { String hql from Book b where b.bookName like ?; String countHql select count(b.id) from Book b where b.bookName like ?; Object[] args new Object[]{% keyword %}; int total ((Long) getHibernateTemplate() .find(countHql, args).get(0)).intValue(); Query query getSession().createQuery(hql); query.setParameter(0, % keyword %); query.setFirstResult((pageNo - 1) * pageSize); // 起始行号从 0 开始 query.setMaxResults(pageSize); // 每页条数 return new PageBook(query.list(), total, pageNo, pageSize); }setFirstResult计算的是偏移量而不是页码这是新手最容易写错的地方写成pageNo * pageSize会直接漏掉第一页数据。另外模糊查询的%必须拼在参数值里不能写成like %?%那样 Hibernate 会当成字面量而不是占位符。提示hibernate.hbm2ddl.auto在开发期设成update方便自动建表上线前务必改成validate或none否则一次误删字段就可能引发线上事故。3. Spring容器接管Bean与声明式事务配置分层之后Action 要拿 ServiceService 要拿 DAODAO 要拿 SessionFactory如果全靠new手动串起来改动一处依赖就得改一片代码。Spring 在这份资料里的角色就是把这个对象装配的活接过去同时用 AOP 给业务方法套上事务。3.1 数据源与SessionFactory装配applicationContext-dao.xml里先把数据源和会话工厂配出来bean iddataSource classorg.apache.commons.dbcp.BasicDataSource destroy-methodclose property namedriverClassName valuecom.mysql.jdbc.Driver/ property nameurl valuejdbc:mysql://localhost:3306/ssh_web?useUnicodetrueamp;characterEncodingutf8/ property nameusername valueroot/ property namepassword valueroot/ property nameinitialSize value5/ property namemaxActive value50/ /bean bean idsessionFactory classorg.springframework.orm.hibernate3.LocalSessionFactoryBean property namedataSource refdataSource/ property namemappingResources list valuecom/demo/entity/Book.hbm.xml/value valuecom/demo/entity/Order.hbm.xml/value /list /property property namehibernateProperties props prop keyhibernate.dialectorg.hibernate.dialect.MySQL5Dialect/prop prop keyhibernate.show_sqltrue/prop prop keyhibernate.format_sqltrue/prop prop keyhibernate.hbm2ddl.autoupdate/prop prop keyhibernate.jdbc.batch_size30/prop /props /property /bean连接串里的useUnicodetruecharacterEncodingutf8必须带上少了它中文入库会变成问号。maxActive默认只有 8图书列表并发一高就会排队等连接按并发量调到 30 到 50 比较稳妥。batch_size在批量导入书籍数据时能明显减少往返数据库的次数。3.2 Service层与AOP事务切面事务配置推荐用tx:adviceaop:config这种切面式写法而不是每个 Bean 上挂TransactionProxyFactoryBean后者类一多配置就失控bean idtxManager classorg.springframework.orm.hibernate3.HibernateTransactionManager property namesessionFactory refsessionFactory/ /bean tx:advice idtxAdvice transaction-managertxManager tx:attributes tx:method namesave* propagationREQUIRED rollback-forException/ tx:method nameadd* propagationREQUIRED/ tx:method nameupdate* propagationREQUIRED/ tx:method namedelete* propagationREQUIRED/ tx:method nameget* read-onlytrue propagationSUPPORTS/ tx:method namefind* read-onlytrue propagationSUPPORTS/ /tx:attributes /tx:advice aop:config aop:pointcut idservicePointcut expressionexecution(* com.demo.service..*.*(..))/ aop:advisor advice-reftxAdvice pointcut-refservicePointcut/ /aop:config命名规则在这里很关键Service 方法名以save/add/update/delete开头的走写事务get/find开头的走只读事务。只读事务里 Hibernate 会关闭脏检查查询性能会好一些。业务层还要负责把多个 DAO 操作拼成一个原子单元比如下单public void createOrder(Order order, ListOrderItem items) { for (OrderItem item : items) { Book book bookDao.get(item.getBookId()); if (book.getStock() item.getQuantity()) { throw new BizException(库存不足 book.getBookName()); } book.setStock(book.getStock() - item.getQuantity()); bookDao.update(book); orderItemDao.save(item); } orderDao.save(order); }扣库存和写明细必须在一个事务里中间任何一步抛异常都要整体回滚。这里抛的是运行时异常配合rollback-for指定的规则BizException才能触发回滚。3.3 事务失效的几个典型坑第一个坑是把业务方法写成 private 或 final。Spring AOP 默认走 JDK 动态代理或 CGLIB 子类代理private 方法拦不住final 类没法被继承事务注解静默失效日志里也不会报错。第二个坑是同类内部自调用比如saveOrder()里直接this.updateStock()调用不经过代理对象事务同样不起作用得注入自身的代理或者把方法挪到另一个 Bean。第三个坑是 Service 方法里吞掉了异常try-catch后只打日志不往外抛事务管理器收不到异常信号就会照常提交。注意数据源如果用DataSourceTransactionManager配 HibernateSession 不会自动绑定到线程getHibernateTemplate()每次都会新开 Session懒加载和一级缓存都会出问题必须用HibernateTransactionManager。4. Struts控制层与web.xml装配打通请求链路Spring 把下半身的对象管起来之后最上面还差一层把浏览器请求接到 Service 上。Struts 负责这件事解析 URL、封装参数、调用业务方法、根据返回值挑一个 JSP 渲染。整条链路能不能通web.xml 的加载顺序是决定性因素。4.1 struts.xml 动作映射与结果视图struts !-- 交给 Spring 创建 ActionAction 里才能直接注入 Service -- constant namestruts.objectFactory valuespring/ constant namestruts.devMode valuefalse/ package namebook extendsstruts-default namespace/book action namelist classbookAction methodlist result namesuccess/WEB-INF/jsp/book/list.jsp/result /action action namedetail classbookAction methoddetail result namesuccess/WEB-INF/jsp/book/detail.jsp/result result nameerror/WEB-INF/jsp/common/error.jsp/result /action /package /strutsstruts.objectFactoryspring是 Struts 与 Spring 集成的开关不设这一项Action 由 Struts 自己反射 new 出来Autowired的 Service 永远是 null。class属性写的是 Spring 容器里 Bean 的 id所以 Action 的 Bean 名要和它对齐。JSP 放在WEB-INF下是刻意的这样浏览器无法直接访问.jsp所有页面都只能经过 Action权限校验才拦得住。4.2 Action与参数封装public class BookAction extends ActionSupport { private BookService bookService; // 由 Spring 注入 private ListBook bookList; private Long id; private int pageNo 1; private PageBook page; public String list() { page bookService.pageByKeyword(, pageNo, 10); this.bookList page.getData(); return SUCCESS; } public String detail() { Book book bookService.getBook(id); if (book null) { return ERROR; } this.bookList Collections.singletonList(book); return SUCCESS; } // setter 必须提供Struts 靠 setter 把请求参数灌进来 public void setBookService(BookService bookService) { this.bookService bookService; } public void setId(Long id) { this.id id; } public void setPageNo(int pageNo) { this.pageNo pageNo; } public PageBook getPage() { return page; } public ListBook getBookList() { return bookList; } }Struts 的参数注入靠 setter?id12会调setId(12L)类型转换由内置转换器完成。pageNo这种基本类型如果不传参默认是 0 而不是 1所以在字段声明时就赋初值 1避免分页计算出现负数偏移。list()返回的字符串SUCCESS对应 struts.xml 里的result namesuccess这是 Struts 的约定全部换成自定义常量也行但读起来不如直接返回SUCCESS直观。4.3 web.xml 加载顺序与过滤器链!-- 1. Spring 容器随应用启动先于任何 Filter -- context-param param-namecontextConfigLocation/param-name param-valueclasspath:applicationContext-*.xml/param-value /context-param listener listener-classorg.springframework.web.context.ContextLoaderListener/listener-class /listener !-- 2. 编码过滤器必须排在 Struts 之前 -- filter filter-nameencodingFilter/filter-name filter-classorg.springframework.web.filter.CharacterEncodingFilter/filter-class init-param param-nameencoding/param-name param-valueUTF-8/param-value /init-param init-param param-nameforceEncoding/param-name param-valuetrue/param-value /init-param /filter filter-mapping filter-nameencodingFilter/filter-name url-pattern/*/url-pattern /filter-mapping !-- 3. Struts 核心过滤器 -- filter filter-namestruts2/filter-name filter-classorg.apache.struts2.dispatcher.FilterDispatcher/filter-class /filter filter-mapping filter-namestruts2/filter-name url-pattern/*/url-pattern /filter-mapping加载顺序有两条硬规则。第一ContextLoaderListener是 ListenerServlet 规范保证 Listener 先于 Filter 初始化所以 Action 被创建时 Spring 容器一定已经就绪。第二多个 Filter 的执行顺序由filter-mapping在文件中出现的先后决定不是由filter定义的顺序决定编码过滤器写在前面才能真正对请求体生效放到 Struts 后面中文参数照样乱码。提示如果页面报No bean named bookAction available八成是 contextConfigLocation 的通配符没匹配到文件或者 Action 的 bean id 与 struts.xml 里class属性的值不一致先把 Spring 启动日志里的 Bean 定义列表打出来核对。4.4 部署到Tomcat与懒加载越界打包成 war 丢进 Tomcat 的 webapps 目录或者用 IDE 的 artifact 部署都可以。这里最常见的运行期报错是LazyInitializationException: could not initialize proxy - no Session原因就是 JSP 渲染时想访问关联对象但事务已经在 Service 方法返回时提交、Session 关闭了。三种解法各有取舍方案做法适用场景代价OpenSessionInView加OpenSessionInViewFilter页面层级简单Session 生命周期拉长连接占用变多提前抓取HQL 用join fetch关联明确查询语句变复杂DTO 转换Service 里转成 VO 返回结构稳定多写转换代码我一般先用join fetch改查询实在绕不开再上OpenSessionInViewFilter后者是个兜底方案而不是首选因为它让 Session 拖到视图渲染完才关闭数据库连接池压力会明显上升。5. 用HQL调优与SQL日志定位性能问题系统能跑起来只是及格线图书列表这种数据量会持续增长的页面才是考验。Hibernate 默认show_sql打出的语句没有参数值排查慢查询时看不到实际入参把日志切到 SQL 参数模式更有用# log4j.properties log4j.logger.org.hibernate.SQLDEBUG log4j.logger.org.hibernate.typeTRACE log4j.logger.org.hibernate.statDEBUGorg.hibernate.type开到 TRACE 后日志里会带上每个绑定参数的实际取值模糊查询里的%关键词%一眼就能看到。org.hibernate.stat打开统计后可以通过sessionFactory.getStatistics()拿到查询次数、缓存命中率这些指标用它来验证一级缓存是不是真的生效。判定 N1 问题有个简单办法打开一本书列表页数一下日志里出现了几条select ... from t_book。如果列表页显示 10 本书日志里却出现了 10 次以上针对t_category的查询那就是典型的懒加载逐条触发。改法是把 HQL 写成String hql select distinct b from Book b left join fetch b.category where b.stock 0;join fetch让分类信息在同一条 SQL 里用左外连接带回来distinct是为了去掉一对多连接可能产生的重复行。要注意join fetch和setFirstResult一起用的时候部分 Hibernate 版本会在内存里做分页数据量大反而更慢这种情况就得退回两步查询——先查主键分页再按主键批量捞关联对象。批量操作的写法也值得注意。逐条save一万本书日志会刷出一万条 insert改成 flush 加 clear 能显著降低内存占用for (int i 0; i books.size(); i) { getHibernateTemplate().save(books.get(i)); if (i % 30 0) { getHibernateTemplate().flush(); // 把缓存里的 SQL 刷到数据库 getHibernateTemplate().clear(); // 清一级缓存防止内存持续膨胀 } }这里的 30 要和前面数据源里配的hibernate.jdbc.batch_size对齐否则 JDBC 层攒不够一批就发出去批处理等于白配。最后留一个判断经验如果同一条 HQL 在开发机几百毫秒、到服务器上要好几秒先看执行计划里的全表扫描再确认like %关键词%这种前置通配是不是让book_name上的索引直接失效了这种情况要么改成前缀匹配要么单独建一张检索用的宽表。本文还有配套的精品资源点击获取
返回列表