【问题标题】:Method not being intercepted by transaction advisor even though "adding transactional method" seen in logs即使在日志中看到“添加事务方法”,事务顾问也不会拦截方法
【发布时间】:2014-06-23 13:41:02
【问题描述】:

我有一个@Transactional @Controller,但它的方法正在由 Spring MVC 框架调用而没有事务。在异常跟踪中,我没有找到拦截调用的事务顾问:

org.hibernate.HibernateException: No Session found for current thread
org.springframework.orm.hibernate4.SpringSessionContext.currentSession(SpringSessionContext.java:106)
org.hibernate.internal.SessionFactoryImpl.getCurrentSession(SessionFactoryImpl.java:1014)
org.example.businesslogic.MyController.userLoggedIn(SwiperRest.java:48)
sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
java.lang.reflect.Method.invoke(Method.java:483)
org.springframework.web.method.support.InvocableHandlerMethod.invoke(InvocableHandlerMethod.java:215)
org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:132)
org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:104)
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandleMethod(RequestMappingHandlerAdapter.java:749)
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:689)
org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:83)
org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:938)

另一方面,日志清楚地表明控制器方法被检测为事务性:

DEBUG o.s.b.f.s.DefaultListableBeanFactory - Returning cached instance of singleton bean 'org.springframework.transaction.config.internalTransactionAdvisor'
DEBUG o.s.b.f.s.DefaultListableBeanFactory - Returning cached instance of singleton bean 'metaDataSourceAdvisor'
DEBUG o.s.t.a.AnnotationTransactionAttributeSource - Adding transactional method 'MyController.userLoggedIn' with attribute: PROPAGATION_REQUIRED,ISOLATION_DEFAULT; ''
DEBUG o.s.a.f.a.InfrastructureAdvisorAutoProxyCreator - Creating implicit proxy for bean 'myController' with 0 common interceptors and 1 specific interceptors
DEBUG o.s.a.f.CglibAopProxy - Creating CGLIB proxy: target source is SingletonTargetSource for target object [org.example.businesslogic.MyController@7c0f1b7c]
DEBUG o.s.a.f.CglibAopProxy - Unable to apply any optimisations to advised method: public java.lang.String org.example.businesslogic.MyController.userLoggedIn(java.lang.String,java.lang.String)
DEBUG o.s.t.a.AnnotationTransactionAttributeSource - Adding transactional method 'MyController.locationProfiles' with attribute: PROPAGATION_REQUIRED,ISOLATION_DEFAULT; ''
DEBUG o.s.a.f.CglibAopProxy - Unable to apply any optimisations to advised method: public java.util.List org.example.businesslogic.MyController.locationProfiles(java.lang.String)

来自控制器类的 sn-p:

@Transactional
@Controller
@RequestMapping("/zendor")
public class MyController
{
  @Autowired private SessionFactory sf;

  @RequestMapping(method=POST, value="userLoggedIn")
  public @ResponseBody String userLoggedIn(@RequestParam String u_id, @RequestParam String d_id) {
    Session hb = sf.getCurrentSession();
    ...
  }
}

这是我的网络应用程序初始化类,我没有web.xml

public class WebApplicationInitializer extends AbstractAnnotationConfigDispatcherServletInitializer
{
    @Override
    protected Class<?>[] getRootConfigClasses() { return new Class[] { RootConfig.class }; }
    @Override
    protected Class<?>[] getServletConfigClasses() { return new Class[] { WebMvcConfig.class }; }
    @Override
    protected String[] getServletMappings() { return new String[] { "/" }; }

    @Override public void onStartup(ServletContext ctx) throws ServletException {
      ctx.setInitParameter("spring.profiles.active", "production");
      super.onStartup(ctx);
    }
}

这是引用的根配置:

package org.example.config;

@Configuration
@ComponentScan
public class RootConfig
{
}

它与这些在同一个包中,被默认组件扫描范围拾取:

@Configuration
@EnableWebMvc
@ComponentScan("org.example.businesslogic")
public class WebMvcConfig extends WebMvcConfigurationSupport
{
}

@Configuration
@EnableTransactionManagement
@ComponentScan("org.example.businesslogic")
public class DataConfig implements TransactionManagementConfigurer
{
  @Autowired private DataSource dataSource;
  ...
}

当 Spring-test 的 SpringJUnit4ClassRunner 使用相同的配置时,方法会得到建议并且事务会起作用。

我也尝试将userLoggedIn方法提取为@Autowired@Transactional @Component,但结果是一样的。

我应该从哪个方向研究来解决这个问题?

我在 Spring 4.0.5。

更新 1

关键问题是我的根配置还引入了所有其他配置类,包括 WebMvcConfig,它作为子 servlet 配置再次加载。

非常违反直觉,只有当我删除 servlet 配置类,替换

    @Override
    protected Class<?>[] getServletConfigClasses() { return new Class[] { WebMvcConfig.class }; }

    @Override
    protected Class<?>[] getServletConfigClasses() { return null; }

这直接违背了文档:may not be empty or null。如果我反其道而行之,为rootConfigClasses 提供null,为servletConfigClasses 提供RootConfig,那么一切都会更加失败,“找不到servlet 上下文”。

更新 2

在没有根应用上下文的情况下发生的故障已追溯到 Spring Web Security,显然必须在根级别配置它才能被SecurityWebApplicationInitializer 拾取,因为这似乎是在根时执行的阶段应用上下文已经存在,但网络应用上下文不存在。所以我的问题解决方案是在 root 和 webapp 上下文之间引入分离,其中 root 加载安全性和 webapp 其他所有内容。

【问题讨论】:

  • 一个明显的问题:为什么您希望您的控制器具有事务性?通常,控制器调用事务性方法。
  • 为了简单起见,目前。在应用程序生命周期的这一点上,我不需要额外的层。
  • 你是否尝试过基于类的代理,例如
  • @NathanHughes 这不是 Spring 的默认方式吗?我没有使用 AspectJ。
  • @Marko:再次查看您的示例,我认为它可能不相关。文档说“但是,如果控制器必须实现不是 Spring Context 回调的接口(例如 InitializingBean、*Aware 等),您可能需要显式配置基于类的代理。”docs.spring.io/spring/docs/4.1.0.BUILD-SNAPSHOT/…

标签: java spring spring-mvc transactions spring-jdbc


【解决方案1】:

如果你还没有读过它们

这同样适用于AbstractAnnotationConfigDispatcherServletInitializergetRootConfigClasses()getServletConfigClasses()。基本上WebApplicationInitializer 将构造(并注册)一个ContextLoaderListener 和一个AnnotationConfigWebApplicationContext 注册来自getRootConfigClasses() 的所有@Configuration(和其他@Component 注释)类。然后它将使用来自getServletConfigClasses() 的所有@Configuration(和其他...)类构造和注册DispatcherServlet

作为 Servlet 生命周期的一部分,容器将首先初始化所有 ServletContextListener 对象。这意味着ContextLoaderListener 将首先加载,refresh 将加载给它的AnnotationConfigWebApplicationContext(如果尚未刷新,理想情况下不应该刷新)。它还会将此ApplicationContext 作为属性放入ServletContext

然后容器将初始化注册的DispatcherServlet。这里还有一些阅读内容

基本上,DispatcherServlet 将通过首先将其父级设置为ServletContext 中的ApplicationContext(由ContextLoaderListener 设置)中的ApplicationConfigWebApplicationContext 接收ApplicationConfigWebApplicationContext,如果有的话。

然后它将开始从其ApplicationContext 中挑选和选择 bean,以设置 MVC 堆栈、控制器、处理程序方法、拦截器等。默认情况下,它只会查找其处理程序 bean ,@Controller bean,在它加载的 ApplicationContext 中,而不是它的父级。


你所做的似乎是

@Override
protected Class<?>[] getServletConfigClasses() { return new Class[] { WebMvcConfig.class }; }

@Override
protected Class<?>[] getRootConfigClasses() { return new Class[] { RootConfig.class }; }

在这种情况下,ContextLoaderListener 将加载 RootConfig,这将创建一堆 bean,包括用于 @Controller 类的 bean,它们将通过 @Transactional 配置得到建议。

然后DispatcherServlet 将加载WebMvcConfig,它有自己的@ComponentScan,这将创建新的@Controller bean,但不会建议这些,因为没有注册TransactionInterceptor(没有@EnableTransactionManagement in这个上下文)。然后DispatcherServlet 将尝试在其自己的ApplicationContext 中查找所有@Controller bean(以及具有@RequestMapping 方法的其他bean)。它会找到这些不建议使用的@Controller bean。这些是它将注册为处理程序的那些,而不是由ContextLoaderListener 加载的那些。

如果您进一步查看日志,您应该会看到正在创建一个新的控制器 bean。


建议:

  • 根上下文:应该对整个应用程序可见的内容
  • Servlet 上下文:应该对 MVC 堆栈可见的内容

控制器不是整个应用程序应该可以访问的组件。只有DispatcherServlet 应该关心它们。将它们放在 servlet 上下文中。

现在我显然不了解您的整个应用程序,但我建议您将所有事务逻辑从处理程序方法中重构到一些@Service 方法中。这将使您更容易维护您的配置并使您的控制器更具控制器性,即。委托给模型。

【讨论】:

  • 我的问题的近因很清楚(WebMvcConfig 的双重加载),问题是如何解决它。我的问题是您和文档在此处所说的内容与观察到的行为之间的矛盾: 1. 文档声明根上下文是可选的,而 servlet 上下文是强制性的; 2. 当我没有根上下文时,servlet 容器不执行任何上下文加载器侦听器; 3. 当我只有根容器时,一切正常。
  • 关于拆分为控制器和服务,我完全了解该最佳实践,并且有使用按照这些思路编写的应用程序的经验;但是,如果我看不到从磨难中出现的明显好处,我通常不急于引入样板文件并将班级人数加倍。目前,我能看到的“扁平”控制器的唯一缺点在于明显固执己见的框架,这似乎迫使我朝那个方向发展。
  • @MarkoTopolnik 我不知道他们为什么这么说。源代码不匹配。您可以在没有WebApplicationContext 的情况下创建DispatcherServlet,它将使用默认值(但没有控制器)创建自己的。也许这是一个警告。如果没有自定义处理程序,DispatcherServlet 就没有多大用处。
  • @MarkoTopolnik when all I have is the root container, everything works 你能扩展一下吗?如果您只有根上下文,则不应注册您的控制器。
  • 是的,这正是我现在组织它们的方式。它使您更容易找到所需的内容。
【解决方案2】:

你做错了:RootConfigWebMvcConfig 在同一个包中。 RootConfig 在自己的包中进行组件扫描,发现 WebMvcConfig 反过来进行组件扫描。最后,根应用上下文将包含所有与事务相关的东西(txManager、数据源、sessionfactorybean 等),但也包含与 Web 相关的所有内容:控制器、处理程序映射等。

然后,WebMvcConfig 启动(因为它在 WebApplicationInitializer 中定义)并且所有与 Web 相关的内容都被重新定义。而且我认为它正在以它的方式发生,因为根上下文有一个版本的控制器(事务性的),而 servlet 上下文有另一个版本(简单的)。

我认为您需要将您的 RootConfigWebMvcConfig 放在单独的包中。

【讨论】:

  • 尝试一下:移动org.example.businesslogic 中的所有内容并仅保留org.example.config 中的RootConfig。我想你也可以在WebApplicationInitializer 中评论RootConfig
  • 我已经有了一个可行的解决方案,即RootConfigrootConfigClass 而没有servletConfigClass。我现在很担心,因为根据文档这是非法的,但我不知道如何让它工作和合法。当我按照书本制作并将所有内容放入 servlet 配置(根配置是 可选)时,它就会失败。
  • 我建议尝试我的方法。您说您目前不需要额外的层,并且更喜欢让您的控制器具有事务性。此外,@EnableTransactionManagement 在同一应用上下文中的 bean 上查找 @Transactional,将所有内容都定义在 servlet 上下文中是有意义的。
  • 好的,你的方法是没有root config,一切都在servlet config中?如果我将RootConfig 保留在org.example.config 中,将其他所有内容移开,并从WebApplicationInitializer 中注释掉RootConfig,最终结果将是RootConfig 根本没有加载。
  • 你可以保留RootConfig,但是所有与网络相关的和事务性的东西都应该在org.example.businesslogic中。例如:保持RootConfig,与它在同一个包中放置类似@Configuration DataSourceConfig 的内容(例如,LocalSessionFactoryBeanDataSource)。在 com.foo.businesslogic 中保留您的控制器 (MyController)、WebMvcConfigDataConfig(应该包含事务设置)。
猜你喜欢
  • 2017-03-29
  • 2018-10-11
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-03-03
  • 1970-01-01
相关资源
最近更新 更多