What is behavior when overriding method annotated by @Transactional in Spring

127 views Asked by At

There are 'duplicate' questions regarding spring annotation 'inheritance', with resolute, but probably incorrect answer: NO(not inherited). Update: it's provably wrong.

Annotations aren't inherited when we override method, sure. But mere existence of

org.springframework.core.annotation.AnnotationUtils#findAnnotation 

which follows method overrides begs the question if (and how) are @Transactional annotations "inherited". I can also find(through SpringTransactionAnnotationParser):

org.springframework.core.annotation.MergedAnnotations.SearchStrategy

which TYPE_HIERARCHY enum especially suggests that spring 'inherits' them.

Question:

  1. So having in parent class method

    @Transactional public void a() {}

and it child class method

@Overrides
public void a() {}

will there be opened transaction or not if I call a()? Lets assume, that proxy is correctly created, method is public, etc. etc. Everything else if fine.

  1. is there some document/best practices how to configure this behavior, considering there are evidently multiple strategies for this.

  2. does anyone know where are these rules documented, I'd like to know what happens say if parent is interface, or abstract method etc. and code doing the scanning isn't that easy not to be sure I'm not overlooking something.


EDIT 2: User dan1st tried code from 'edit 1' below and got different behavior for testTxInheritanceB. Unless I'm wrong somewhere/somehow, this proves that this annotation interpretation is really configurable (via SpringTransactionAnnotationParser or differently) and it's configured differently based on spring version (3.2.0<>2.6.6) or it's based on some autoconfigured property (my code base has LOT of dependencies, which potentially might alter behavior). I will (in a week) dissect project and compare it with fresh springboot project, trying to find reasoning behind it/mwo.

EDIT 1: it's very easy to actually test both cases; I'm asking here instead to get sources to wisdom to find why it actually works like this. The truth is, in default spring setting, when you're implementing abstract method it does not matter if you put @Transactional over abstract method or it's implementation. If you call overriding non-abstract method, in both cases TX will be opened. So that is the answer for my question 1, what will happen. Now we're missing why it's like that and what rules leads to this behavior, if there are some recommendations/implementation patterns when overriding such methods, and how we can configure strategies I mentioned, if/when and when/why not to do that.

For code below methods testTxInheritanceA and testTxInheritanceB yields: "Transaction active: true", method testTxInheritanceC obviously yields: Transaction active: false.

Sample RestController code:

    import lombok.AllArgsConstructor;
    import lombok.extern.slf4j.Slf4j;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.http.MediaType;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Transactional;
    import org.springframework.transaction.support.TransactionSynchronizationManager;
    import org.springframework.validation.annotation.Validated;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    @RestController()
    @RequestMapping(path = {"/test"})
    @Slf4j
    @Validated
    @AllArgsConstructor
    public class TestController {
    
        @Autowired
        TestController.ParentNoChildYes_Child parentNoChildYesChild;
        @Autowired
        TestController.ParentYesChildNo_Child parentYesChildNoChild;
        @Autowired
        TestController.NotTranstactional notTranstactional;
    
    
        @GetMapping(path = "/testTxInheritanceA", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.TEXT_PLAIN_VALUE)
        public String testTxInheritanceA() {
            return "Transaction active: "+parentNoChildYesChild.test();
        }
    
        @GetMapping(path = "/testTxInheritanceB", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.TEXT_PLAIN_VALUE)
        public String testTxInheritanceB() {
            return "Transaction active: "+parentYesChildNoChild.test();
        }
    
        @GetMapping(path = "/testTxInheritanceC", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.TEXT_PLAIN_VALUE)
        public String testTxInheritanceC() {
            return "Transaction active: "+notTranstactional.test();
        }
    
        public static abstract class ParentYesChildNo_Parent {
            @Transactional
            public abstract boolean test();
        }
    
        @Service
        public static class ParentYesChildNo_Child extends TestController.ParentYesChildNo_Parent {
            @Override
            public boolean test() {
                return TransactionSynchronizationManager.isActualTransactionActive();
            }
        }
    
        public static abstract class ParentNoChildYes_Parent {
            public abstract boolean test();
        }
    
        @Service
        public static class ParentNoChildYes_Child extends TestController.ParentNoChildYes_Parent {
            @Override
            @Transactional
            public boolean test() {
                return TransactionSynchronizationManager.isActualTransactionActive();
            }
        }
    
        @Service
        public static class NotTranstactional {
            public boolean test() {
                return TransactionSynchronizationManager.isActualTransactionActive();
            }
        }
    
    }
1

There are 1 answers

5
dan1st might be happy again On

Check the Javadoc of @Transactional:

When this annotation is declared at the class level, it applies as a default to all methods of the declaring class and its subclasses. Note that it does not apply to ancestor classes up the class hierarchy; inherited methods need to be locally redeclared in order to participate in a subclass-level annotation. For details on method visibility constraints, consult the Transaction Management section of the reference manual.

So, if you have

@Transactional
public class YourSuperClass {}
@Component
public class YourSubClass {
    public void someMethod(){
        //whatever
    }

}

then someMethod is logically @Transactional (from Spring's perspective). This also applies if someMethod is overwritten. It should also be mentioned that (as stated in the Javadoc) @Transactional on a subclass does not affect methods declared only in superclasses.

However, it is different for

public class YourSuperClass {
    @Transactional
    public void someMethod(){

    }
}
@Component
public class YourSubClass extends YourSuperClass {
    @Override
    public void someMethod(){
        //whatever
    }

}

In this case, someMethod will not be transactional unless you annotate the overridden version explicitly.

So, for your questions:

  1. So having in parent class method and it child class method, will there be opened transaction or not if I call a()? Lets assume, that proxy is correctly created, method is public, etc. etc. Everything else if fine.

No. As previously described, the overridden method won't be transactional. However, you can get around this by creating a wrapper around that method:

public class YourSuperClass {
    @Transactional
    public final void yourMethod(){
        yourMethodImpl();
    }
    protected void yourMethodImpl(){
        //whatever
    }
}
@Component
public class YourSubClass extends YourSuperClass{
    @Override
    protected void yourMethodImpl(){
        //whatever
    }
}

then yourMethodImpl will be executed in a transaction if yourMethod is called.

  1. is there some document/best practices how to configure this behavior, considering there are evidently multiple strategies for this.

As mentioned, you can check the Javadoc of @Transactional which mentioned it being inherited on class level but not when overriding methods. I don't know of any other strategies and I don't think you can customize the SearchStrategy of @Transactional.

  1. does anyone know where are these rules documented, I'd like to know what happens say if parent is interface, or abstract method etc. and code doing the scanning isn't that easy not to be sure I'm not overlooking something.

These things should do the same but I recommend you to try it yourself if you want to be sure.