在整合新的ssh框架的时候,spring+springmvc+hibernate的时候发现一个问题,就是持久层在使用hibernateTemplate的时候,并不会自动实现事务的提交,SpringMVC对包的扫描范围扩大后,导致的事务配置不生效问题
首先配置的是Spring容器的初始化加载的application文件,然后是SpringMVC的前端控制器(DispatchServlet),当配置完DispatchServlet后会在Spring容器中创建一个新的容器
其实这是两个容器,Spring作为父容器,SpringMVC作为子容器
web.xml中对Spring的配置
<!-- 把 Spring 容器集成到 Web 应用里面 -->
<listener>
<listener-class>
org.springframework.web.context.ContextLoaderListener
</listener-class>
</listener>
<!-- spring配置文件 -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:/applicationContext.xml</param-value>
</context-param>
<!--DispatcherServlet是前端控制器设计模式的实现,提供Spring Web MVC的集中访问点,而且负责职责的分派,
而且与Spring IoC容器无缝集成,从而可以获得Spring的所有好处。-->
<!--DispatcherServlet会默认加载[servlet-name]-servlet.xml文件-->
<servlet>
<servlet-name>spring</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>spring</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
applicationContext.xml使用AOP声明式事务配置
<!-- 声明式容器事务管理 ,transaction-manager指定事务管理器为transactionManager -->
<tx:advice id="transactionAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="query*" read-only="true"/>
<tx:method name="get*" read-only="true"/>
<tx:method name="save*" rollback-for="Exception" propagation="REQUIRED"/>
<tx:method name="update*" rollback-for="Exception" propagation="REQUIRED"/>
</tx:attributes>
</tx:advice>
<aop:config>
<!--只对业务逻辑层实施事务 -->
<aop:pointcut expression="execution(* com.zhimajp.auction.service.impl..*.*(..))" id="busiLogicService"/>
<aop:advisor advice-ref="transactionAdvice" pointcut-ref="busiLogicService"/>
</aop:config>
遇到的问题是:通过Hibernate执行save方法后,数据未能插入到DB中并且控制台也没有打印出SQL(控制台没有输出)
通过仔细排查,阅读网络文章后,发现问题出现在spring-servlet.xml中:<context:component-scan base-package="com.zhimajp.auction" />
上述配置的结果是:SpringMVC对Service和Dao的所有package进行了扫描装载
问题分析:
1、Spring与SpringMVC属于父子容器关系。框架启动时先启动Spring容器,而后启动SpringMVC容器。子容器可以访问父容器中的Bean,而父容器不能访问子容器中的Bean
2、由于SpringMVC在扫描时扩大了扫描范围,装载了@Service标识的类的实例,从而导致Controller层在注入Service时,实际注入的是子容器中的Service实例
3、事务被配置在父容器中,Spring父容器在装载Service时会同时应用事务配置,而SpringMVC只是单纯加载Service的实例
解决的办法如下
applicationContext.xml扫包排除掉Controller
<context:component-scan base-package="com.bdqn.cc"> <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/> </context:component-scan>
springmvc.xml扫包只扫描controller
<context:component-scan base-package="com.bdqn.cc" use-default-filters="false"> <context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/> </context:component-scan>
springmvc配置文件 use-default-filters="false",因为use-default-filters的值默认是true,也就是扫描全部的带有@Controller、@Service等注解的包了,加上之后则只扫描controller
进一步证明
打印容器管理的bean名称
我们使SpringMVC扫描Controller和Service,Spring扫描Service和DAO。
使用以下代码打印父子窗口管理的bean名称:
WebApplicationContext webApplicationContext = WebApplicationContextUtils.getWebApplicationContext(request.getSession().getServletContext());
String[] definationBeanNames = webApplicationContext.getBeanNamesForAnnotation(Service.class);
List<String> names = new ArrayList<String>(Arrays.asList(definationBeanNames));
Collections.addAll(names, webApplicationContext.getBeanNamesForAnnotation(Controller.class));
Collections.addAll(names, webApplicationContext.getBeanNamesForAnnotation(Repository.class));
System.out.println("Spring 父容器管理的Bean:");
for(String beanName : names){
System.out.println(beanName);
}
webApplicationContext = RequestContextUtils.getWebApplicationContext(request);
definationBeanNames = webApplicationContext.getBeanNamesForAnnotation(Service.class);
names = new ArrayList<String>(Arrays.asList(definationBeanNames));
Collections.addAll(names, webApplicationContext.getBeanNamesForAnnotation(Controller.class));
Collections.addAll(names, webApplicationContext.getBeanNamesForAnnotation(Repository.class));
System.out.println("SpringMVC 子容器管理的Bean:");
for(String beanName : names){
System.out.println(beanName);
}
我们发现父子容器同时维护了Service层的类的实例,并且应该是两个独立的实例。
只使用子容器,而完全不使用父容器
现在我们测试另外一个场景
将web.xml中,注释掉ContextLoaderListener,修改配置为:
<!--DispatcherServlet是前端控制器设计模式的实现,提供Spring Web MVC的集中访问点,而且负责职责的分派,
而且与Spring IoC容器无缝集成,从而可以获得Spring的所有好处。-->
<!--DispatcherServlet会默认加载[servlet-name]-servlet.xml文件-->
<servlet>
<servlet-name>spring</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:/applicationContext.xml;/WEB-INF/spring-servlet.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>spring</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
移除了父容器,所有配置文件全部交由SpringMVC加载 。
打印结果如下:
总结
1、当事务交由Spring管理时,Spring负责管理session中事务的开启、关闭、flush等步骤,开发者只需调用例如save、update方法即可
2、当web项目框架中存在父子容器,且事务由父容器管理时,就应当注意SpringMVC对包的扫描范围并且只需扫描Controller组件。官方推荐:父子容器应当各执其责
3、如果子容器加载了Service的话,则在该实例上事务并不会生效。也就是Spring不会在service的方法被调用时自动开启事务
4、基于2中的前提:SpringMVC应只加载web相关配置(视图配置、Controller注解扫描),由Spring加载数据源、事务配置、Service和Dao注解扫描
详细的案例demo可以查看我的码云https://gitee.com/chenduotang/projects new-ssh项目demo