javaspringspring-bootspring-transactions

Method with @Transactional called on target not on proxy instance


I'm currently migrating one of my projects form "self configured spring" to spring boot. While most of the stuff is already working I have a problem with a @Transactional method where when it is called the context is not present as set before due to a call to the "target" instance instead of the "proxy" instance (I'll try to elaborate below).

First a stripped down view of my class hierarchy:

@Entity
public class Config {
    // fields and stuff
}

public interface Exporter {

    int startExport() throws ExporterException;

    void setConfig(Config config);
}


public abstract class ExporterImpl implements Exporter {
    protected Config config;

    @Override
    public final void setConfig(Config config) {
        this.config = config;
        // this.config is a valid config instance here
    }

    @Override
    @Transactional(readOnly = true)
    public int startExport() throws ExporterException {
        // this.config is NULL here
    }

    // other methods including abstract one for subclass
}

@Scope("prototype")
@Service("cars2Exporter")
public class Cars2ExporterImpl extends ExporterImpl {

    // override abstract methods and some other
    // not touching startExport()
}

// there are other implementations of ExporterImpl too 
// in all implementations the problem occurs

the calling code is like this:

@Inject
private Provider<Exporter> cars2Exporter;

public void scheduleExport(Config config) {
    Exporter exporter = cars2Exporter.get();
    exporter.setConfig(config);
    exporter.startExport();
    // actually I'm wrapping it here in a class implementing runnable
    // and put it in the queue of a `TaskExecutor` but the issue happens
    // on direct call too. :(
}

What exactly is the issue?

In the call to startExport() the field config of ExporterImpl is null although it has been set right before.

What I found so far: With a breakpoint at exporter.startExport(); I checked the id of the exporter instance shown by eclipse debugger. In the debug round while writing this post it is 16585. Continuing execution into the call/first line of startExport() where I checked the id again (of this this time) expecting it to be the same but realizing that it is not. It is 16606 here... so the call to startExport() is done on another instance of the class... in a previous debug round I checked to which instance/id the call to setConfig() is going... to the first on (16585 in this case). This explains why the config field is null in the 16606 instance.

To understand what happens between the line where I call exporter.startExport(); and the actual first line of startExport() I clicked into the steps between those both lines in eclipse debugger.

There I came to line 655 in CglibAopProxy that looks like this:

retVal = new CglibMethodInvocation(proxy, target, method, args, targetClass, chain, methodProxy).proceed();

checking the arguments here I found that proxy is the instance with id 16585 and target the one with 16606.

unfortunately I'm not that deep into springs aop stuff to know if that is how it should be...

I just wonder why there are two instances that get called on different methods. the call to setConfig() goes to the proxy instance and the call do startExport() reaches the target instance and thus does not have access to the config previously set...

As mentioned the project has been migrated to spring boot but we where before already using the Athens-RELEASE version of spring platform bom. From what I can tell there where no special AOP configurations before the migration and no explicitly set values after the migration.

To get this problem fixed (or at least somehow working) I already tried multiple things:

Currently I'm out of clues on how to get this back working...

Thanks in advance

*hopes someone can help*


Solution

  • Spring Boot tries to create a cglib proxy, which is a class based proxy, before you probably had an interface based (JDK Dynamic Proxy).

    Due to this a subclass of your Cars2ExporterImpl is created and all methods are overridden and the advices will be applied. However as your setConfig method is final that cannot be overridden and as a result that method will be actually called on the proxy instead on the proxied instance.

    So either remove the final keyword so that CgLib proxy can be created or explicitly disable class based proxies for transactions. Add @EnableTransationManagement(proxy-target-class=false) should also do the trick. Unless there is something else triggering class based proxies that is.