Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Method not being intercepted by transaction advisor even though "adding transactional method" seen in logs

I have a @Transactional @Controller, but its methods are being invoked by the Spring MVC framework without a transaction. In the exception trace I do not find the transaction advisor intercepting the call:

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)

On the other hand, the log clearly indicates that the controller methods were detected as transactional:

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)

A snippet from the controller class:

@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();
    ...
  }
}

This is my web application initializer class, I don't have a 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);
    }
}

This is the referenced root configuration:

package org.example.config;

@Configuration
@ComponentScan
public class RootConfig
{
}

It is in the same package as these, which get picked up by the default component scan range:

@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;
  ...
}

When the same configuration is used by Spring-test's SpringJUnit4ClassRunner, the methods do get advised and transactions work.

I also tried to extract the userLoggedIn method to an @Autowired @Transactional @Component, but the result was identical.

In which direction should I inveltigate to resolve this issue?

I am on Spring 4.0.5.

Update 1

The key problem is that my root config is pulling in all other config classes as well, including WebMvcConfig, which is loaded again as the child servlet config.

Quite counterintuitively, things only start working when I remove the servlet config class, replacing

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

with

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

which goes directly against the documentation: may not be empty or null. If I do the reverse, giving null for rootConfigClasses and RootConfig for servletConfigClasses, then everything fails even harder, with "servlet context not found.".

Update 2

The failure occuring without root app context has been traced to Spring Web Security, which must apparently be configured at the root level in order to be picked up by the SecurityWebApplicationInitializer, as this seems to executed at a stage when the root app context already exists, but not the web app context. So my problem resolution was to introduce a separation between root and webapp contexts, where the root loads security and webapp everything else.

like image 619
Marko Topolnik Avatar asked Jun 23 '14 13:06

Marko Topolnik


2 Answers

If you haven't already read them

  • What is the difference between ApplicationContext and WebApplicationContext in Spring MVC?
  • Difference between applicationContext.xml and spring-servlet.xml in Spring Framework

The same applies to AbstractAnnotationConfigDispatcherServletInitializer's getRootConfigClasses() and getServletConfigClasses(). Basically that WebApplicationInitializer will construct (and register) a ContextLoaderListener with a AnnotationConfigWebApplicationContext registering all the @Configuration (and other @Component annotated) classes from getRootConfigClasses(). It will then construct and register a DispatcherServlet with all the @Configuration (and other...) classes from the getServletConfigClasses().

As part of the Servlet lifecycle, the container will first initialize all ServletContextListener objects. This means ContextLoaderListener will load first and refresh the AnnotationConfigWebApplicationContext that was given to it (if it wasn't already refreshed, which ideally it shouldn't be). It will also put this ApplicationContext as an attribute in the ServletContext.

The container will then initialize the registered DispatcherServlet. Here's some more reading

  • How does the web container manage the lifecycle of a spring controller
  • SpringMVC lifecycle-- the overall view

Basically, the DispatcherServlet will refresh the ApplicationConfigWebApplicationContext that it received by first setting its parent to the ApplicationContext in the ServletContext (set by the ContextLoaderListener), if there is one.

It will then start picking and choosing beans from its ApplicationContext to set up the MVC stack, controllers, handler methods, interceptors, etc. By default, it will only look up its handler beans, @Controller beans, in the ApplicationContext it loaded, not its parent(s).


What you seem to have done is

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

and

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

In this case, the ContextLoaderListener will load RootConfig which will create a bunch of beans, including ones for your @Controller classes which will be advised with the @Transactional configuration.

The DispatcherServlet will then load WebMvcConfig which has its own @ComponentScan and this will create new @Controller beans, but these won't be advised because no TransactionInterceptor was registered (no @EnableTransactionManagement in this context). The DispatcherServlet will then try to find all @Controller beans (and other beans that have @RequestMapping methods) in its own ApplicationContext. It will find these @Controller beans which aren't advised. Those are the ones it will register as handlers, not the ones loaded by the ContextLoaderListener.

If you look further down in your logs, you should see a new controller bean(s) being created.


Suggestions:

  • Root context: things that should be visible to the entire application
  • Servlet context: things that should be visible to the MVC stack

Controllers are not components that the whole application should have access to. Only the DispatcherServlet should care about them. Put them in the servlet context.

Now I obviously don't know your whole application, but I recommend you refactor all the transactional logic out of the handler methods and into some @Service methods. It will make it easier to maintain your configs and make your controllers more controller-y, ie. delegate to the model.

like image 83
Sotirios Delimanolis Avatar answered Nov 06 '22 12:11

Sotirios Delimanolis


You are doing something wrong: both RootConfig and WebMvcConfig are in the same package. RootConfig does component scanning in its own package, discovers WebMvcConfig which in turn does component scanning. In the end the root application context will contain all transactional related stuff (txManager, datasource, sessionfactorybean etc) but, also, everything web related: controllers, handlermappings etc.

Then, WebMvcConfig kicks in (because it's defined in WebApplicationInitializer) and all web-related stuff is re-defined again. And I think it's happening the way it does, because the root context has one version of your controller (the transactional one) and the servlet context has another version (the simple one).

I think you need to keep your RootConfig and WebMvcConfig in separate packages.

like image 1
Andrei Stefan Avatar answered Nov 06 '22 12:11

Andrei Stefan