Dodge Dodge - 23 days ago 6
Java Question

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 debbug 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 wich 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 actuall 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 diffrent 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:


  • remove @Scope from the sub class

  • move @Transactional from method level to class

  • override startExport() in subclass and put @Transactional here

  • add @EnableAspectJAutoProxy to application class (i wasn't even able to log in - no error message)

  • set spring.aop.proxy-target-class to true

  • the above in diffrent combinations...



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

Thanks in advance

*hopes someone can help*

Answer

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.