question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Inherited @Transactional methods use wrong TransactionManager

See original GitHub issue

Setup: In a multi-DB environment, there is an abstract Service/DAO class with @Transactional methods common to all entities from all DBs. Then there are multiple concrete Service/DAO beans, one per DB, each with specific transactionManager in the class-level @Transactional annotation.

Problem: When calling a base-class method via some DB-specific bean, a wrong transactionManager is used (always the @Primary one). The worst is that no exceptions are thrown, no error messages printed in log. The entities are simply silently e.g. not saved/updated in DB. Or, I afraid, they could even be saved to wrong DB.

The problem has already been described at least in #14011, #14295, #17080. Related issue is also #11839. But all the issues have been closed (as outdated) without a solution.

There were also some workarounds: https://stackoverflow.com/questions/51087660/dynamic-selection-of-transactionmanager-spring-boot, https://www.tirasa.net/en/blog/dynamic-springs-at-transactional, also #14295. But they all are not working (anymore).

How can it be solved? What do you, guys, do in such situation? Am I missing something obvious? Thank you for your thoughts.

Issue Analytics

  • State:open
  • Created 4 years ago
  • Reactions:6
  • Comments:13 (2 by maintainers)

github_iconTop GitHub Comments

5reactions
elabcommented, Apr 23, 2020

I finally managed to solve the problem without patching the spring-tx library (now, no need to create a patch for every new version of Spring):

The default “transactionAttributeSource” bean (defined in ProxyTransactionManagementConfiguration) will be replaced (1) by an instance of the own MergeAnnotationTransactionAttributeSource (2).

(1) AnnotationTransactionAttributeSourceReplacer

An important part was to implement the PriorityOrdered interface in the replacer, otherwise it was invoked too late (after instantiation of the “transactionAttributeSource” bean), see PostProcessorRegistrationDelegate#invokeBeanFactoryPostProcessors for details. (Ordered interface would probably suffice, too.)

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.InstantiationAwareBeanPostProcessor;
import org.springframework.core.PriorityOrdered;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.ProxyTransactionManagementConfiguration;
import org.springframework.transaction.interceptor.TransactionAttributeSource;

import lombok.extern.slf4j.Slf4j;

/**
 * Replaces the default "transactionAttributeSource" bean (defined in {@link ProxyTransactionManagementConfiguration}) 
 * with instance of {@link MergeAnnotationTransactionAttributeSource}.
 *
 * @author Eugen Labun
 */
@Slf4j
@Component
public class AnnotationTransactionAttributeSourceReplacer implements InstantiationAwareBeanPostProcessor, PriorityOrdered /*this is important*/ {

    public AnnotationTransactionAttributeSourceReplacer() {
        // to check that the replacer is created before instantiation of the "transactionAttributeSource" bean
        log.trace("AnnotationTransactionAttributeSourceReplacer - constructor");
    }

    @Override
    public Object postProcessBeforeInstantiation(Class<?> beanClass, String beanName) throws BeansException {
        // log.trace("postProcessBeforeInstantiation - beanName: {}, beanClass: {}", beanName, beanClass);
        if (beanName.equals("transactionAttributeSource") && TransactionAttributeSource.class.isAssignableFrom(beanClass)) {
            log.debug("instantiating bean {} as {}", beanName, MergeAnnotationTransactionAttributeSource.class.getName());
            return new MergeAnnotationTransactionAttributeSource();
        } else {
            return null;
        }
    }

    @Override
    public int getOrder() {
        return 0;
    }
}

(2) MergeAnnotationTransactionAttributeSource

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;

import org.springframework.aop.support.AopUtils;
import org.springframework.lang.Nullable;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.annotation.AnnotationTransactionAttributeSource;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.interceptor.AbstractFallbackTransactionAttributeSource;
import org.springframework.transaction.interceptor.DefaultTransactionAttribute;
import org.springframework.transaction.interceptor.RuleBasedTransactionAttribute;
import org.springframework.transaction.interceptor.TransactionAttribute;
import org.springframework.util.ClassUtils;

import lombok.extern.slf4j.Slf4j;

/**
 * Implements a merge policy for transaction attributes (see {@link Transactional} annotation)
 * with following priorities (high to low):
 * <ol>
 * <li>specific method;
 * <li>declaring class of the specific method;
 * <li>target class;
 * <li>method in the declaring class/interface;
 * <li>declaring class/interface.
 * </ol>
 *
 * <p>The merge policy means that all transaction attributes which are not
 * explicitly set [1] on a specific definition place (see above) will be inherited
 * from the place with the next lower priority.
 * 
 * <p>On the contrary, the Spring default {@link AbstractFallbackTransactionAttributeSource} implements a fallback policy, 
 * where all attributes are read from the first found definition place (essentially in the above order), and all others are ignored.
 * 
 * <p>See analysis in <a href="https://github.com/spring-projects/spring-framework/issues/24291">Inherited @Transactional methods use wrong TransactionManager</a>.
 * 
 * <p>[1] If the value of an attribute is equal to its default value, the current implementation 
 * cannot distinguish, whether this value has been set explicitly or implicitly, 
 * and considers such attribute as "not explicitly set". Therefore it's currently impossible to override a non-default value with a default value.
 *
 * @author Eugen Labun
 */
@Slf4j
@SuppressWarnings("serial")
public class MergeAnnotationTransactionAttributeSource extends AnnotationTransactionAttributeSource {

    public MergeAnnotationTransactionAttributeSource() {
        log.info("MergeAnnotationTransactionAttributeSource constructor");
    }

    @Override
    @Nullable
    protected TransactionAttribute computeTransactionAttribute(Method method, @Nullable Class<?> targetClass) {
        // Don't allow no-public methods as required.
        if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
            return null;
        }

        // The method may be on an interface, but we also need attributes from the target class.
        // If the target class is null, the method will be unchanged.
        Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass);

        // 1st priority is the specific method.
        TransactionAttribute txAttr = findTransactionAttribute(specificMethod);

        // 2nd priority is the declaring class of the specific method.
        Class<?> declaringClass = specificMethod.getDeclaringClass();
        boolean userLevelMethod = ClassUtils.isUserLevelMethod(method);
        if (userLevelMethod) {
            txAttr = merge(txAttr, findTransactionAttribute(declaringClass));
        }

        // 3rd priority is the target class
        if (targetClass != null && !targetClass.equals(declaringClass) && userLevelMethod) {
            txAttr = merge(txAttr, findTransactionAttribute(targetClass));
        }

        if (method != specificMethod) {
            // 4th priority is the method in the declaring class/interface.
            txAttr = merge(txAttr, findTransactionAttribute(method));

            // 5th priority is the declaring class/interface.
            txAttr = merge(txAttr, findTransactionAttribute(method.getDeclaringClass()));
        }

        return txAttr;
    }

    /**
     * Set empty and default properties of "primary" object from "secondary" object.
     * <p>Parameter objects should not be used after the call to this method,
     * as they can be changed here or/and returned as a result.
     */
    @Nullable
    private TransactionAttribute merge(@Nullable TransactionAttribute primaryObj, @Nullable TransactionAttribute secondaryObj) {
        if (primaryObj == null) {
            return secondaryObj;
        }
        if (secondaryObj == null) {
            return primaryObj;
        }

        if (primaryObj instanceof DefaultTransactionAttribute && secondaryObj instanceof DefaultTransactionAttribute) {
            DefaultTransactionAttribute primary = (DefaultTransactionAttribute) primaryObj;
            DefaultTransactionAttribute secondary = (DefaultTransactionAttribute) secondaryObj;

            if (primary.getQualifier() == null || primary.getQualifier().isEmpty()) {
                primary.setQualifier(secondary.getQualifier());
            }
            if (primary.getDescriptor() == null || primary.getDescriptor().isEmpty()) {
                primary.setDescriptor(secondary.getDescriptor());
            }
            if (primary.getName() == null || primary.getName().isEmpty()) {
                primary.setName(secondary.getName());
            }

            // The following properties have default values in DefaultTransactionDefinition;
            // we cannot distinguish here, whether these values have been set explicitly or implicitly;
            // but it seems to be logical to handle default values like empty values.
            if (primary.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRED) {
                primary.setPropagationBehavior(secondary.getPropagationBehavior());
            }
            if (primary.getIsolationLevel() == TransactionDefinition.ISOLATION_DEFAULT) {
                primary.setIsolationLevel(secondary.getIsolationLevel());
            }
            if (primary.getTimeout() == TransactionDefinition.TIMEOUT_DEFAULT) {
                primary.setTimeout(secondary.getTimeout());
            }
            if (!primary.isReadOnly()) {
                primary.setReadOnly(secondary.isReadOnly());
            }
        }

        if (primaryObj instanceof RuleBasedTransactionAttribute && secondaryObj instanceof RuleBasedTransactionAttribute) {
            RuleBasedTransactionAttribute primary = (RuleBasedTransactionAttribute) primaryObj;
            RuleBasedTransactionAttribute secondary = (RuleBasedTransactionAttribute) secondaryObj;

            if (primary.getRollbackRules() == null || primary.getRollbackRules().isEmpty()) {
                primary.setRollbackRules(secondary.getRollbackRules());
            }
        }

        return primaryObj;
    }

}
3reactions
elabcommented, Mar 10, 2020

The PR with patch and additional unit test for the initial use case is created.

I also added the “declaring class of the specific method” again to “priorities” (as it was in the original implementation), while also keeping the previously added “target class”.

The new priorities are:

  1. specific method; (this is just the proper name of the former “method in the target class”)
  2. declaring class of the specific method; (added again)
  3. target class; (added in the first version of the patch)
  4. method in the declaring class/interface;
  5. declaring class/interface.

As the new (“merging”) behavior is not backward compatible, we would need some parameter to switch the implementation (TODO), e.g.

transaction-attribute-policy: {fallback, merge} # default: fallback
Read more comments on GitHub >

github_iconTop Results From Across the Web

Spring @Transactional and inheritance - java - Stack Overflow
Transaction manager is inherited class specific, as only then is clear, which of them is needed by particular DAO... Transactional DOES belong at ......
Read more >
16. Transaction Management - Spring
Consistent programming model across different transaction APIs such as Java Transaction API (JTA), JDBC, Hibernate, Java Persistence API (JPA), ...
Read more >
Using Transactions for Read-Only Operations - Baeldung
More concretely, it provides powerful tooling to generate Spring Data JPA repositories and methods, Flyway Versioned Migrations, ...
Read more >
Spring @Transactional Example - ConcretePage.com
The @Transactional annotation does not apply to inherited methods of its ancestor classes. To make them transactional, we need to locally ...
Read more >
Transactional (javadoc 3.1.1 API)
Use Spring's Transactional directly or switch to Micronaut Data's TransactionalAdvice. @Target(value={METHOD,TYPE}) @Retention(value=RUNTIME) @Inherited ...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found