Spring @Transactional on @Bean declaration instead of class Implementation
Asked Answered
R

4

16

I'd like to configure "transactional" beans from my Spring @Configuration class instead of annotating the class implementation itself with @Transactional.

Kind of like the old school way, configuring transactional advice from an XML file, but without needing a String reference to my class/method names to create pointcuts.

The reason is that the bean implementation is in another code base, and the module it belongs to doesn't depend on Spring. Read : I'm not touching the source code of that bean, just instanciating it. The class is final, can't extend it either to add Spring annotations to the child class. Let's say that all the methods must be transactional, for simplicity.

The bean implementation :

/** This class has no Spring dependency... */
// @Transactional <- which means I can't use this here
public final class ComplexComponentImpl implements ComplexComponent {

    private SomeRepository repo;

    public ComplexComponentImpl(SomeRepository repository) { this.repo = repository }

    public void saveEntities(SomeEntity e1, SomeEntity e2) {
        repo.save(e1);
        throw new IllegalStateException("Make the transaction fail");
    }

What I want to do in my configuration class (and which doesn't work in my unit test) :

@Configuration
@EnableTransactionManagement
public class ComplexComponentConfig {

    @Bean
    @Transactional // <- Make the bean transactional here
    public ComplexComponent complexComponent() {
        return new ComplexComponentImpl(repository());
    }

    // ...
}

The example above doesn't work, indeed, as nothing gets "transactional" at runtime : entity e1 is persisted even though the exception is thrown.

Note that my transaction management setup works works perfectly well with an implementation class marked with @Transactional.

Question : Is is it possible to declare @Beans transactional from a @Configuration class, or is there any alternative taking into accounts the constraints above ?

Receptor answered 24/4, 2015 at 16:58 Comment(0)
R
11

Found something built-in that is the sum of @Mecon's and @Erik Gillespie's answers, with limited boilerplate.

Spring already provides a TransactionProxyFactoryBean that just sets up a transactional proxy on any object. Much of the setup could be refactored to some utility method :

@Configuration
@EnableTransactionManagement
public class ComplexComponentConfig {

    /** NOT A @Bean, this object will be wrapped with a transactional proxy */
    public ComplexComponent complexComponentImpl() {
        return new ComplexComponentImpl(repository());
    }

    @Bean
    public ComplexComponent complexComponent() {
        TransactionProxyFactoryBean proxy = new TransactionProxyFactoryBean();

        // Inject transaction manager here
        proxy.setTransactionManager(txManager());

        // Define wich object instance is to be proxied (your bean)
        proxy.setTarget(complexComponentImpl());

        // Programmatically setup transaction attributes
        Properties transactionAttributes = new Properties();
        transactionAttributes.put("*", "PROPAGATION_REQUIRED");
        proxy.setTransactionAttributes(transactionAttributes);

        // Finish FactoryBean setup
        proxy.afterPropertiesSet();
        return (ComplexComponent) proxy.getObject;
    }

// ...
}
Receptor answered 27/4, 2015 at 8:29 Comment(3)
have you tried: @EnableTransactionManagement(proxyTargetClass = true)Niven
Using Spring 4.3.1, this solution fails with java.lang.ClassCastException: com.sun.proxy.$Proxy460 cannot be cast to foo.bar.ComplexComponentImpl from the return statement. Have not dug into the issue yet.Tejada
...**Update from previous comment** Was not using interfaces so proxy.setProxyTargetClass(true) was required.Tejada
K
1

I think you probably can't use @Transactional in that manner. One of spring's in-built PostProcessors, are supposed to scan all classes (beans) that have that annotation, and the create Aspects accordingly.

About alternatives: I would write an Adapter class for each 3rd party class I have to use. And then make those Adapter classes be Spring Beans.

Kittykitwe answered 24/4, 2015 at 17:11 Comment(0)
S
1

You can't use @Transactional in that way but you can programmatically configure aspects with Spring.

Spring documentation for programmatically defining aspects:

  1. http://docs.spring.io/spring/docs/3.0.x/spring-framework-reference/html/aop.html#aop-aspectj-programmatic
  2. http://docs.spring.io/spring/docs/3.0.x/spring-framework-reference/html/aop-api.html

The examples in the documentation are very simple. Defining transaction aspects will likely be more complicated and I wouldn't be surprised if you find it easier to just use the convenience of XML-based proxies or take @Mecon's advice and write adapters.

Scrambler answered 24/4, 2015 at 17:48 Comment(0)
C
0

You can use spring's AOP capabilities to add the transaction interceptor to your bean. Just create an Advisor bean that specifies a pointcut and adds a TranscationInterceptor.

@Bean
public ComplexComponent complexComponentImpl() {
    return new ComplexComponentImpl(repository());
}


@Bean
public Advisor advisorBean(TransactionManager txManager) {
  Class<?> targetClass = ComplexComponent .class;
  int propagationBehavior = TransactionDefinition.PROPAGATION_REQUIRED;

  return allMethodsTxAdvice(txManager, targetClass, propagationBehavior);
}

/**
  * Extracted method for reuse.
  */
private DefaultPointcutAdvisor allMethodsTxAdvice(TransactionManager txManager, Class<?> targetClass, int propagationBehavior) {
  AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
  String pointcutExpression = MessageFormat.format("execution(* {0}.*(..)))", targetClass.getName());
  pointcut.setExpression(pointcutExpression);

  MatchAlwaysTransactionAttributeSource tas = new MatchAlwaysTransactionAttributeSource();
  TransactionAttribute transactionAttribute = new DefaultTransactionAttribute(propagationBehavior);
  tas.setTransactionAttribute(transactionAttribute);
  TransactionInterceptor transactionInterceptor = new TransactionInterceptor(txManager, tas);

  return new DefaultPointcutAdvisor(pointcut, transactionInterceptor);
}

PS: You don't need to call afterPropertiesSet just return the FactoryBean and spring will handle all lifecycle callbacks. E.g.

@Bean
public FactoryBean<Object> complexComponent(TransactionManager tx) {
    TransactionProxyFactoryBean proxyFactory = new TransactionProxyFactoryBean();
    proxyFactory.setTransactionManager(tx);
    proxyFactory.setTarget(complexComponentImpl());

    Properties transactionAttributes = new Properties();
    transactionAttributes.put("*", "PROPAGATION_REQUIRED");
    proxyFactory.setTransactionAttributes(transactionAttributes);
    return proxyFactory;
  }
Carbineer answered 13/4, 2022 at 10:30 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.