Spring WS interceptors with injected DAO's @Tr

2019-07-20 10:55发布

问题:

We have a legacy XML based configuration spring-ws application that contains endpointInterceptors that have DAOs injected to obtain configuration from the database. These DAOs have the hibernate sessionFactory injected.

When we have upgraded to spring 4.2.0.RELEASE (from spring 3.2.5.RELEASE) and spring-ws 2.2.1.RELEASE (from spring-ws 2.1.4.RELEASE) I noticed that the DAO was not a proxy object and it seemed that the intercetor was going to the AnnotationActionEndpointMapping class instead of the PayloadRootAnnotationMethodEndpointMapping class.

I therefore created a spring-boot version 1.3.0.RELEASE based example that outlines our legacy application and the problem is evident in both XML base configuration and annotation based configuration. Please note that annotation @EnableTransactionManagement exists within the example and that exists within the legacy application.

If you commented out the from the application context or @EnableWS from the @Congiuration object then the DAO was a proxy object, the interceptor seemed to be going to the correct endpoing (i.e PayloadRootAnnotationMethodEndpointMapping) and the unit test worked with no transaction error.

StackTrace when or EnableWS is not commented out.

org.springframework.ws.soap.client.SoapFaultClientException: Could not obtain transaction-synchronized Session for current thread
    at org.springframework.ws.soap.client.core.SoapFaultMessageResolver.resolveFault(SoapFaultMessageResolver.java:38)
    at org.springframework.ws.client.core.WebServiceTemplate.handleFault(WebServiceTemplate.java:830)
    at org.springframework.ws.client.core.WebServiceTemplate.doSendAndReceive(WebServiceTemplate.java:624)
    at org.springframework.ws.client.core.WebServiceTemplate.sendAndReceive(WebServiceTemplate.java:555)
    at org.springframework.ws.client.core.WebServiceTemplate.marshalSendAndReceive(WebServiceTemplate.java:390)
    at org.springframework.ws.client.core.WebServiceTemplate.marshalSendAndReceive(WebServiceTemplate.java:378)
    at hello.ApplicationTests.testSendAndReceive(ApplicationTests.java:61)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
    at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
    at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
    at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26)
    at org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.java:75)
    at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:86)
    at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:84)
    at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:254)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:89)
    at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
    at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
    at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
    at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
    at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61)
    at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:70)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:193)
    at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
    at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:78)
    at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:212)
    at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:68)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at com.intellij.rt.execution.application.AppMain.main(AppMain.java:140)

Extract of the XML configuration that causes the above exception :

    <sws:annotation-driven>
    <sws:interceptors>
           <ref bean="loggingInterceptorAU"/>
    </sws:interceptors>


<bean id="loggingInterceptorAU" class="hello.interceptor.LoggingEndpointInterceptor"/>

Extract of the annotation configuration that causes the above exception :

@EnableWs
@Configuration
public class WebServiceConfig extends WsConfigurerAdapter {

    @Bean
    public ServletRegistrationBean messageDispatcherServlet(ApplicationContext applicationContext) {
        MessageDispatcherServlet servlet = new MessageDispatcherServlet();
        servlet.setApplicationContext(applicationContext);
        servlet.setTransformWsdlLocations(true);
        return new ServletRegistrationBean(servlet, "/ws/*");
    }

    @Bean(name = "countries")
    public DefaultWsdl11Definition defaultWsdl11Definition(XsdSchema countriesSchema) {
        DefaultWsdl11Definition wsdl11Definition = new DefaultWsdl11Definition();
        wsdl11Definition.setPortTypeName("CountriesPort");
        wsdl11Definition.setLocationUri("/ws");
        wsdl11Definition.setTargetNamespace("http://spring.io/guides/gs-producing-web-service");
        wsdl11Definition.setSchema(countriesSchema);
        return wsdl11Definition;
    }

    @Bean
    public XsdSchema countriesSchema() {
        return new SimpleXsdSchema(new ClassPathResource("countries.xsd"));
    }

    /**
     * Declaring the loggingInterceptor.
     * @return the new logging interceptor.
     */
    @Bean
    public LoggingEndpointInterceptor loggingInterceptor() {
        LoggingEndpointInterceptor loggingEndpointInterceptor = new LoggingEndpointInterceptor();
        return loggingEndpointInterceptor;
    }

    /**
     * Adds interceptors.
     * @param interceptors
     */
    @Override
    public void addInterceptors(List<EndpointInterceptor> interceptors) {
          // if these rows are uncommented
          // and payloadRootAnnotationMethodEndpointMapping method is commented you get
          // Error: SoapFaultClientException: Could not obtain transaction-synchronized Session for current thread
          interceptors.add(loggingInterceptor());
          super.addInterceptors(interceptors);
    }


    /**
     * Spring Boot with Plain Hibernate
     * @see {https://github.com/mdeinum/samples/tree/master/spring-boot-plain-hibernate}
     *
     * Need to also set within application.properties.
     * spring.jpa.properties.hibernate.current_session_context_class=org.springframework.orm.hibernate4.SpringSessionContext
     * @return
     */
    @Bean(name="sessionFactory")
    public HibernateJpaSessionFactoryBean sessionFactory() {
        return new HibernateJpaSessionFactoryBean();
    }
}

After a closer inspection of the makeup of AnnotationActionEndpointMapping i have noticed that it implements BeanPostProcessor. The spring doco http://docs.spring.io/spring/docs/current/spring-framework-reference/html/beans.html suggests that “... Because AOP auto-proxying is implemented as a BeanPostProcessor itself, neither BeanPostProcessors nor the beans they reference directly are eligible for auto-proxying, and thus do not have aspects woven into them.” and therefore I understand that @Transactional will not work.

public class AnnotationActionEndpointMapping extends AbstractActionMethodEndpointMapping implements BeanPostProcessor

My questions are : *what has changed that has caused spring-ws interceptors to be mapped by default to the AnnotationActionEndpointMapping class? * As per Spring documentation it is suggested that both and or @EnableWs and method addInterceptors are requred. Is there any impact if is commented out in our legacy application?

Please note we have interceptors that are only invoked for certain requests using the following and we do not want to specicially create a PayloadRootAnnotationMethodEndpointMapping bean with the list of interceptors to overcome this problem:

<sws:interceptors>
 <sws:payloadRoot localPart="TestRequest" namespaceUri="http://www.test.com/test/request/1.0">
...

回答1:

what you can do is moving/copying your transactional methods (the methods which will be used by interceptors) to a new class and create a transactional bean programmatically by using TransactionProxyFactoryBean.

something like this:

@Bean
@Autowired
public TransactionProxyFactoryBean transactionalBeanForInterceptor(PlatformTransactionManager
        transactionManager, SessionFactory sessionFactory) {
    TransactionProxyFactoryBean factoryBean = new TransactionProxyFactoryBean();
    factoryBean.setTransactionManager(transactionManager);
    factoryBean.setTarget(new InterceptorService(sessionFactory)); // its just an example
    Properties transactionAttributes = new Properties();
    transactionAttributes.put("validate*", "PROPAGATION_REQUIRED"); // validate* is a regex with the name of the methods which are transactionals
    factoryBean.setTransactionAttributes(transactionAttributes);
    return factoryBean;
}

@Bean
@Autowired
public EndpointInterceptor myInterceptor(InterceptorService interceptorService) { // this will inject a proxied instance (transactional) of InterceptorService
    return new MyEndpointInterceptor(interceptorService);
}


回答2:

If anyone is still having issues then please read on. Once our projects were upgraded to spring 4.3.10.RELEASE (from spring 4.2.0.RELEASE) and spring-ws 2.4.0.RELEASE (from spring-ws 2.2.1.RELEASE), the reported errors above were no longer an issue even when the following code was re-introduced in our code base:

XML configuration:

<sws:annotation-driven>

Annotations:

@EnableWS from the @Congiuration object 

Once this was done endpointinterceptors that required @Transactional worked as they were intended to. Also the spring ticket SWS-974 still hasn't been assigned. I will attempt to inform Spring that they can close it.



回答3:

Unlike others have claimed, this bug persists still in Spring-core 5.1.5 and Spring-ws 3.0.7. It relates to this question: Why @EnableWs removed aop proxy from spring bean. In short the issue comes from the fact that the method

@Override
public void addInterceptors(List<EndpointInterceptor> interceptors) {

gets called before Spring dependency injection has time to register the bean under transaction management. It seems that the bean lifecycle initialization logic in Spring-WS is different from normal. No idea why.

Here's my take on walking past the issue. Fortunately for us Spring-WS uses mutable collections instead of immutable. When the addInterceptors() method gets called, we can just save the collection and thus we have a reference to the same collection instance that is used by Spring-WS. Later on you can initialize your interceptor bean properly and add it to the collection.

You also have to get around the fact that if you use @Autowired the bean gets prepared before the annotations can take place. Thus you have to create it manually by calling ApplicationContext.getBean() method.

@EnableWs
@Configuration
// The magic is to implement both ApplicationContextAware 
// that injects the applicationContext for us 
// and BeanPostProcessor that gives us postProcessBeforeInitialization() 
// where we initialize our interceptor correctly 
// and add it to the collection
public class WebServiceConfig extends WsConfigurerAdapter implements ApplicationContextAware, BeanPostProcessor {

    // This is the interceptor that uses dependencies with @Transactional annotation.
    // It will not work with @Autowired
    private MyInterceptorThatHasTransactionalDependencies myInterceptorThatHasTransactionalDependencies;
    // Fortunately Spring WS uses mutable collections so we can fill 
    // this list later on as long as we just initialize it with  
    private List<EndpointInterceptor> interceptors;
    // This is our application context where all the beans are defined
    private ApplicationContext context;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        // save application context for later use
        this.context = applicationContext;
    }

    @Nullable
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        // This method gets called multiple times so initialize interceptor just once
        if(myInterceptorThatHasTransactionalDependencies == null){
            myInterceptorThatHasTransactionalDependencies = context.getBean(MyInterceptorThatHasTransactionalDependencies.class);
            interceptors.add(myInterceptorThatHasTransactionalDependencies);
        }
        return bean;
    }

    @Override
    public void addInterceptors(List<EndpointInterceptor> interceptors) {
        // Save the list of interceptors so we can modify it later on
        this.interceptors = interceptors; 
        if (myInterceptorThatHasTransactionalDependencies == null) {
            System.out.println("myInterceptorThatHasTransactionalDependencies was null like we expected");
        } else {
            interceptors.add(myInterceptorThatHasTransactionalDependencies);
        }
    }
}

Just to let you know that I am not Spring bean lifecycle expert, so there might be a better place to situate the interceptor initialization than postProcessBeforeInitialization(). That said, this works.