問題場景
項目使用的框架:Spring 4.1.4 + Hibernate 4.3.8 + MySQL。
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>
項目使用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>
采用annotation方式聲明Spring Bean。
遇到的問題是:通過Hibernate執行save方法后,數據未能插入到DB中並且控制台也沒有打印出SQL(控制台沒有輸出)。
解決問題
通過仔細排查,閱讀網絡文章后,發現問題出現在spring-servlet.xml中:
<!--掃描注解包 配置這條便可移除 <context:annotation-config/> --> <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的實例。
問題解決:
將SpringMVC的包掃描限定在Controller。
<context:component-scan base-package="com.zhimajp.auction.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加載 。
打印結果如下:

子容器管理所有的bean,而父容器為null(代碼拋出了空指針)。
項目同樣運行正常。
總結
1、當事務交由Spring管理時,Spring負責管理session中事務的開啟、關閉、flush等步驟,開發者只需調用例如save、update方法即可。
2、當web項目框架中存在父子容器,且事務由父容器管理時,就應當注意SpringMVC對包的掃描范圍並且只需掃描Controller組件。官方推薦:父子容器應當各執其責。
3、如果子容器加載了Service的話,則在該實例上事務並不會生效。也就是Spring不會在service的方法被調用時自動開啟事務。
4、基於2中的前提:SpringMVC應只加載web相關配置(視圖配置、Controller注解掃描),由Spring加載數據源、事務配置、Service和Dao注解掃描。
