Spring 与 Hibernate 整合
除了 JdbcTemplate 外,Spring 还可通过 Hibernate 来完成 Dao 的工作。即将 Spring 与Hibernate 进行整合。
举例:项目 spring_hibernate(在 dao_jdbcTemplate 基础上修改)
1、导入 Jar 包
除了 Spring 的基本 Jar 包外,还需要以下几种 Jar 包:
(1)Spring AOP 的两个 Jar 包
(2)AspectJ 的两个 Jar 包
(3)Spring 的 JDBC 的 Jar 包
(4)Spring 整合 ORM 的 Jar 包
Spring 整合 ORM 框架的 Jar 包,在 Spring 框架解压目录下的 libs 目录中。
(5)Spring 的事务 Jar 包
Spring 的事务 Jar 包,在 Spring 框架解压目录下的 libs 目录中。
(6)Hibernate 的基本 Jar 包
Hibernate 的基本 Jar 包中已经包含了 C3P0 数据源的 Jar 包,且版本高于 Spring 中提供
的版本。所以直接使用 Hibernate 中的即可。
Hibernate 的基本 Jar 包中已经包含了 MySql 的驱动 Jar 包,所以不用再导入了。
2、实体映射文件的配置
3、注册数据源
Spring 配置文件中数据源与 jdbc 属性文件的注册,与 template_jdbc 项目中的相同。
4、配置 SessionFactory
Spring 的精髓是,所有的 Bean 均由 Spring 容器统一管理,所以在 SPring 中若要使用Hibernate,就需要将 SessionFactory 交由 Spring 来管理。
配置 SessionFactory 的 Bean,将 hibernate.cfg.xml 文件替换掉。使用的实现类为LocalSessionFactoryBean,注意,是 hibernate5 包下的。其用于设置的属性主要有三个:数据源,映射文件,及 hibernate 特性。其设置内容,与 Hibernate 主配置文件的基本相同。
(1)注意映射文件的注册方式
这里直接指定的为映入文件所在的包,其中可能包含多个映射文件。也可写为如下形式:
若只有一个映射文件
对于有多个映射文件,可使用< list/>列出:
当然,若有多个存放映射文件的目录,也可使用< list/>列出。
(2)注意当前 Session 上下文所使用的类
至于 Hibernate 特性的设置,除了 hibernate.current_session_context_class 外,其余 key与 Hibernate 主配置文件中的属性名相同,值也相同。
属性 hibernate.current_session_context_class,用于指定当前 Session 所执行的上下文环境。其值不再是 thread,而是 SpringSessionContext,表示 Session 将交由 Spring 容器来管理。
5、定义 Dao 实现类
由于 Dao 实现类要通过 Hibernate 来操作 DB,所以在该类中需要获取到 Session 工厂对象 SessionFactory。当然,最终目的是获取到 Hibernate 的 Session 对象。而 SessionFactory 对象的创建,也是由 Spring 容器来管理的,所以,需要在 Dao 实现类中添加 SessionFactory 属性,以便 Spring 容器通过 setter 将 Session 工厂注入。
注意,Dao 实现类无需继承自任何父类,只要将 SessionFactory 定义为 set 属性即可。而 Hibernate 的 Session 对象,是通过 SessionFactory 的 getCurrentSession()方法获取的。
6、注册 Dao 与 Service 的 Bean
在 Spring 配置文件中注册 dao 与 service 对象,注意需将 sessionFactory 注入给 Dao。
7、配置事务管理器
由于 Hibernate 的 Session 要求必须在事务环境下才能运行,所以在 Spring 中使用Hibernate,必须要配置事务管理器,以开启事务环境。此时使用的事务管理器为HibernateTransactionManager。需要注意的是,使用 Jdbc 的事务管理器,需要注入一个数据源 dataSource,而使用 Hibernate 的事务管理器,则需要注入一个 sessionFactory 属性。
8、配置事务通知及顾问
9、在 Spring 中一般不使用 Hibernate 模板对象(了解)
在 SSH 中,Dao 获取 Hibernate 的 Session,还有一种方式,通过 HibernateTemplate 获取。但一般不使用 Hibernate 模板,而是通过在 Dao 中定义 SessionFactory,由容器向其注入。在Dao 中,通过 SessionFactory 的方法 getCurrentSession()获取 Session。
为什么不使用 Hibernate 模板呢?查看 HibernateTemplate 源码,发现 Hibernate 模板对象中方法的执行所使用的 Session,就是通过 getCurrentSession()获取的。使用模板对象后,遇到复杂查询,还需要使用其 execute()方法,通过回调接口 HibernateCallback 在获取到Session 后,再执行 HQL 语句。转了一大圈后,又绕到了获取当前 Session,而且获取的这个Session 也是通过 getCurrentSession()获得的。
所以,最终不如放弃 Hibernate 模板,直接通过 getCurrentSession()获得 Session 对象。
下面以模板对象的 save()方法的源码为例,来查看其 Session 的获取情况。
Spring 在 Web 项目中的使用
在 Web 项目中使用 Spring 框架,首先要解决在 Servlet 中(暂时不使用 Struts2)获取到Spring 容器的问题。只要在 View 层获取到了 Spring 容器,便可从容器中获取到 Service 对象。
1、不使用 Spring 的 Web 插件
举例:springweb 项目(在 dao_hibernate 基础上修改)
Step1:新建一个 Web Project
Step2:定义 index 页面
Step3:定义 LoginServlet(重点代码)Step4:定义 success 页面
Step5:复制 dao_hibernate 中内容
将 dao_hibernate 项目中以下内容复制到当前项目中:
(1)Service 层、Dao 层全部代码
(2)配置文件 applicationContext.xml 及 jdbc.properties
(3)所有 Jar 包
Step6:直接发布运行
Step7:运行结果分析
当表单提交,跳转到 success.jsp 后,多刷新几次页面,查看后台输出,发现每刷新一次页面,就 new 出一个新的 Spring 容器。即,每提交一次请求,就会创建一个新的 Spring 容器。对于一个应用来说,只需要一个 Spring 容器即可。所以,将 Spring 容器的创建语句放在 Servlet 的 doGet()或 doPost()方法中是有问题的。
此时,可以考虑,将 Spring 容器的创建放在 Servlet 进行初始化时进行,即执行 init()方法时执行。并且,Servlet 还是单例多线程的,即一个业务只有一个 Servlet 实例,所有执行该业务的用户执行的都是这一个 Servlet 实例。这样,Spring 容器就具有了唯一性了。
但是,Servlet 是一个业务一个 Servlet 实例,即 LoginServlet 只有一个,但还会有StudentServlet、TeacherServlet 等。每个业务都会有一个 Servlet,都会执行自己的 init()方法,也就都会创建一个 Spring 容器了。这样一来,Spring 容器就又不唯一了。
2、使用 Spring 的 Web 插件
举例:springweb2 项目 (在 springweb 项目基础上修改)
对于 Web 应用来说,ServletContext 对象是唯一的,一个 Web 应用,只有一个ServletContext 对象。该对象是在 Web 应用装载时初始化的,即在 Web 应用装载时会自动执行接口 ServletContext 的初始化方法。该初始化方法在整个应用中只会执行一次。若将 Spring
容器的创建语句放到 ServletContext 的初始化方法中执行,并将创建好的 Spring 容器作为ServletContext 的属性放入其中。以后再需要 Spring 容器,直接读取该属性值即可。
ServletContext 对象生命周期与 Web 应用的相同。即放在其中的属性为全局属性。所以,放入 ServletContext 中的 Spring 容器,在整个应用的生命周期中,均可被访问。这样就可以保证 Spring 容器在 Web 应用中的唯一性了。
上述的这些工作,已经被封装在了如下的 Spring 的 Jar 包的相关 API 中:spring-web-4.2.1.RELEASE.jar
(1)导入 Jar 包
在 Web 项 目 中 使 用 Spring , 需 要 导 入 Spring 对 Web 的 支 持 包 :
spring-web-4.2.1.RELEASE.jar。
该包在 Spring 框架的解压目录下的 libs 目录中。
(2)注册监听器 ContextLoaderListener
若要在 ServletContext 初始化 时 创 建 Spring 容 器 , 就 需 要 使 用 监 听 器 接 口ServletContextListener 对 ServletContext 进行监听。Spring 为该监听器接口定义了一个实现类
ContextLoaderListener,专门用于在 ServletContext 初始化时创建 Spring 容器。查看源码:
注意,监听器是需要在 web.xml 中注册的。
(3)指定 Spring 配置文件的位置
ContextLoaderListener 在对 Spring 容器进行创建时,需要加载 Spring 配置文件。其默认的 Spring 配置文件位置与名称为:WEB-INF/applicationContext.xml。但,一般会将该配置文件放置于项目的 classpath 下,即 src 下,所以需要在 web.xml 中对 Spring 配置文件的位置及名称进行指定。
从监听器 ContextLoaderListener 的父类 ContextLoader 的源码中可以看到其要读取的配置文件位置参数名称 contextConfigLocation。
(4)修改 Spring 配置文件中映射文件路径的写法
导入的 Jar 包 spring-web 中代码要求,Spring 配置文件中映射文件的路径前必须添加classpath:,以表示其在类路径下。
(5)通过 WebApplicationContextUtils 获取 Spring 容器
工具类 WebApplicationContextUtils 有一个方法专门用于从 ServletContext 中获取 Spring容器对象:getRequiredWebApplicationContext(ServletContext sc)
查其源码,看其调用关系,就可看到其是从 ServletContext 中读取的属性值,即 Spring容器
刷新 success 页面后,可看到代码中使用的 Spring 容器为同一个对象。
Spring 与 Struts2 整合
Spring 与 Struts2 整合的目的有两个:
(1)在 Struts2 的 Action 中,即 View 层,获取到 Service。然后就实现了在 Struts2 项
目中 View 层、Service 层、Dao 层的联通。
(2)将 Action 实例交由 Spring 容器管理。
为了更为清楚的讲解这两个目的的实现,下面分两步逐步来完成这两个目的。
(1)Action 实例由 Struts2 自己创建,Service 由容器注入给 Action。 (2)Action 实例由 Spring 容器创建。
当然,以上两步均需要导入 Struts2 的基本 Jar 包。
1、Action 实例由 Struts2 自己创建,Servcie 由容器注入给 Action
举例:项目 spring_struts2(在 springweb2 项目基础上修改)
(1)导入 Jar 包
此方式除了要导入 Spring 与 Struts 的 Jar 包外,还需要导入 Strut2 与 Spring 连接的插件Jar 包 struts2-spring-plugin。该 Jar 包在 Struts2 框架解压目录的 lib 目录中。
(2)将 Service 声明为 Action 的属性
代码中,只需将 Servcie 定义为 Action 的属性即可。需要注意,默认情况下,要使该属
性名称与 Spring 配置文件中的 Service 的 id 相同。
从 Struts2 角度来说,Struts2 要与 Spring 整合,要求开启一个常量 struts.objectFactory,用于将 Action 交由 Spring 容器来管理。其在 Struts2 中默认是关闭的。
struts.objectFactory 的 开 启 会 使 另 一 常 量 , 按 名 称 自 动 装 配 的 常 量Beanstruts.objectFactory.spring.autoWire 自动起作用。该常量默认是开启的。在 default.properties 中可以看到。
由于导入了 struts2-spring-plugin 的 Jar 包,打开它,可看到一个文件 struts-plugin.xml。打开这个文件,可看到
打开 struts-plugin.xml 文件,可看到其已开启了 strus.objectFactory 常量。
所以,无需任何配置,直接将 Service 作为 Action 的属性,并使属性名与 Spring 中的ServiceBean 的 id 名相同,即可完成将 Service 注入到 Action。
2、Action 实例由 Spring 容器创建
举例:项目 spring_struts2_hibernate(在 spring_struts2 基础上修改)
上面的程序完成了 Service 由容器注入给 Action 的目的。但不足是,Action 是由 Struts2自己创建维护的。而 SSH 的精髓是,所有的 Bean 均由 Spring 容器统一管理。所以这一步要实现的目的是将 Action 交由 Spring 来管理。
方式二的用法:
(1)将 Service 声明为 Action 的属性
此时仍需将 Service 声明为 Action 的属性,只不过,对属性名没有要求。无需与 Spring配置文件中 Service 的 Bean 的 id 相同。
(2)在 Spring 配置文件中注册 Action 的 Bean
在 Spring 的配置文件中将 Action 作为一个普通 Bean 进行定义,并在配置文件中完成Service 的注入。
(3)设置 Action 的 Bean 的 scope 为 prototype
Action 是多例的,但由于 Spring 的默认创建的为 singleton,即单例的。所以,需要设置 scope=“prototype”。
(4)Struts2 配置文件中的 class 使用伪类名
Struts2 配置文件中的标签的 class 属性值不再是 Action 的全类名了,而是个伪类名:Spring 配置文件中该 Bean 的 id 名。
SSH 整合 Jar 包总结
1、Struts2 中的 Jar 包 (16 个)
(1)Struts2 的基本 Jar 包 (13 个)
Struts2框架的解压目录下apps/ struts2-blank.war中解压后WEB-INF/lib下的13个Jar包。
(2)Struts2 与 Spring 整合插件 Jar 包 在 Struts2 框架的解压目录下/lib 目录下:
struts2-spring-plugin-2.3.24.jar
(3)Struts2 与 JSon 整合插件 Jar 包
若项目使用了 Ajax,则需要整合了 JSon,此时还需导入整合 JSon 的 Jar 包。在 Struts2框架的解压目录下/lib 目录下:
struts2-json-plugin-2.3.24.jar
(4)Struts2 注解开发 Jar 包
若项目中使用了 Struts2 的注解开发,则需要导入以下的包。在在 Struts2 框架的解压目录下/lib 目录下:struts2-convention-plugin-2.3.24.jar
2、Spring 中的 Jar 包 (14 个)
(1)Spring 的基本 Jar 包
(2)AOP 开发需要的 Jar 包
spring-aop-4.2.1.RELEASE.jar
spring-aspects-4.2.1.RELEASE.jar
com.springsource.org.aopalliance-1.0.0.jar
com.springsource.org.aspectj.weaver-1.6.8.RELEASE.jar
(3)Spring 整合 ORM 所需 Jar 包
spring-orm-4.2.1.RELEASE.jar
(4)Spring JDBC 开发需要的 Jar 包
spring-jdbc-4.2.1.RELEASE.jar
spring-tx-4.2.1.RELEASE.jar
(5)Spring 事务管理需要的 Jar 包
spring-tx-4.2.1.RELEASE.jar(与上面的是同一个包)
(6)Spring 在 WEB 项目中使用所需 Jar 包
spring-web-4.2.1.RELEASE.jar
3、Hibernate 中的 Jar 包(19)
(1)reqired 目录下所有 Jar (9 个)
(2)optional/ehcache/slf4j-api-1.6.1.jar
(3)optional/c3p0 目录下所有 Jar (3 个)
(4)jpa/hibernate-entitymanager-5.0.1.Final.jar
(5)jpa-metamodel-generator 目录下的 Jar(1 个)
(6)若用到了 ehcache 二级缓存,则需导入 optional/ehcache 中的所有包。
(7)junit-4.9.jar
(8)slf4j-log4j12-1.7.12.jar
(9)mysql 驱动
4、包冲突
在不同的框架中,若存在相同的 Jar 包,即使版本号不同,也将会引发包冲突问题,导致程序运行出错。一般是舍弃低版本,保留高版本。
(1)javassist 的 Jar 包冲突
在 Hibernate 的 Jar 包中有一个 javassist 的 Jar 包(3.18.1 版本),而在 Struts2 中也有一个 javassist 的 Jar 包(3.11.0 版本)。保留 3.18.1 版本。
(2)C3P0 的 Jar 包冲突
在 Hibernate 框架的基本 Jar 包中曾引入了 C3P0 的 Jar 包(0.9.2.1 版本),但在 Spring中,也曾引入过 C3P0 的 Jar 包(0.9.1.2 版本)。保留 0.9.2.1 版本。
(4)log4j2 的 Jar 包冲突
在 Struts2 的基本 Jar 包引入了 log4j2 的两个 Jar 包(2.2 版本),而在 Hibernate 的基本Jar 包中也引入了 log4j2 的两个 Jar 包(2.3 版本)。保留 2.3 版本。
OpenSessionInView
1、问题的引出
当将事务织入到 Service 层后,在 Dao 层通过 load()等具有延迟加载功能的方法加载实体时,会出现异常。
举例:项目 open_session_in_view
Step1:定义 index.jsp
Step2:定义 Action
Step3:定义 Service 接口
Step4:定义 Service 实现类
Step5:定义 Dao 接口
Step6:定义 Dao 实现类
Step7:修改 struts.xml
Step8:修改 Spring 配置文件
2、原因分析
View 层要读取一个对象的某属性值,所以在 View 层调用了 Service 层的对象获取方法。
此时 Service 层就会去调用 Dao 层,欲从 DB 中读取数据。但由于 Service 层并非是真正需要数据的一方,即 Servcie 层并非真正需要数据的详情。又由于 Dao 层使用了延迟加载技术,所以返回给 Service 层一个数据为空的代理对象。当 Service 获取到了这个对象后,马上将这个对象返回给了 View 层。Service 层认为任务完成,所以将事务进行了关闭。由于 Session必须在事务环境下运行,且事务在提交或回滚后,会首先将 Session 关闭,而后关闭事务。
所以事务关闭前,Session 就已经消失。当 View 层收到 Service 层发送来的对象,并使用其详情时,发现为空。此时 View 层向 Service 层发出进行真正查询请求时,Service 层的 Session
已经不存在,所以,会报出 Session 为空的异常。
当执行完 View 层的 user = service.find(5); 语句时,只是将一个无数据的 user 的字节码增强代理对象返回给了 user。但此时 Service 层的 Session 已经消失。
当执行 View 层的 int age = user.getAge(); 语句时,发现 user 无真正的值,所以要引发一个真正的 DB 查询。但此时 Session 已经不存在了。
抛出异常:org.hibernate.LazyInitializationException: could not initialize proxy - no Session
3、解决办法
问题的主要原因是事务应用在业务层,而延迟加载使得对 Hibernate 的 Session 的连续使用放在了 View 层。若要 Session 不进行自动关闭,必须使其所在的事务环境不能在 Service层就结束,所以可以将 Session 的开启放在 View 层。
只需在 web.xml 中注册一个过滤器 OpenSessionInViewFilter(开启 Session 在 View),只要用户提交请求,就会先执行该过滤器,将 Session 开启。
注册过滤器时需要注意:
(1)该过滤器会自动从 Spring 配置文件中查找名称为 sessionFactory 的 Bean。
查看 OpenSessionInViewFilter 的源码:
若定义的不是这个名称,则需通过初始化参数进行指定。
(2)该过滤器应注册在 StrutsPrepareAndExecuteFilter 的前面,即在进入 Struts2 之前先执行过滤,否则还会出现同样的问题。其注册顺序可以从 Struts2 的流程图中看出。
过滤器的执行都是通过过滤器链串起来执行的,即当前的 Filter 通过在自己的 doFilter()方法中执行 chain.doFilter()来调用执行下一下过滤器。
查看 StrutsPrepareAndExecuteFilter 的源码可以看出,只有当没有 Action 要执行时,才可能会执行其它的过滤器,即调用 chain.doFilter()。否则,只要有 Action 能够匹配上执行,那么就没有 chain.doFilter()方法要执行了,即 Struts2 的 StrutsPrepareAndExecuteFilter 将成为最后一个被执行的 Filter。
所以,只有将 OpenSessionInViewFilter 放到 Struts2 的过滤器
StrutsPrepareAndExecuteFilter 的前面才会被执行到。
SSH 全注解开发
在项目 spring_struts2_hibernate 基础上修改。
1、添加 Struts2 与 Spring 注解
使用了 Struts2 注解后,无需再使用 struts.xml 了。然而 Strut2 通过在中使用伪类,在 Spring 中定义该 Action 的 Bean 的方式,将 Service 对象注入给了 Action。一旦删除了struts.xml,在 Spring 中定义的 Action 的 Bean,将与 Struts2 无关。而这个关系,通过 Spring的注解可以再次建立。所以,不能在 SSH 中单独演示 Struts2 的注解。
Step1:先将数据库及表建好
Step2:修改 Dao 的实现类 UserDaoImpl
Step3:修改 Service 的实现类 UserServiceImpl
Step4:修改 LoginAction
Step5:在 Spring 配置文件中注册组件扫描的基本包
Step6:在 Spring 配置文件中开启 Spring 事务注解驱动
Step7:在 Spring 配置文件中删除 dao、service、action 的 Bean 定义
Step8:删除 struts.xml
完成 Struts2 与 Spring 的注解。
2、添加 Hibernate 的注解
Step1:修改实体类 User
Step2:删除映射文件
Step3 : 在 Spring 配 置 文 件 的 SessionFactory 定 义 中 , 删 除 映 射 文 件 的 相 关 配 置mappingResources
Step4:在 Spring 配置文件的 SessionFactory 定义中,添加实体包扫描器。SSH 全注解完成。