Skip to main content

Spring AOP riddle

Spring support for aspect oriented programming is very wide, but sometimes you may shoot yourself in the foot if you are not careful enough. Consider the following service interface:

public interface FoobarService {

void foo();

void bar();

}


...and its implementation:

public class DefaultFoobarService implements FoobarService {

@Override
@Transactional
public void foo() {
//some code requiring active transaction
}

@Override
public void bar() {
foo();
}
}


To keep things simple, assume that foo() throws exception if not run in context of active transaction. Since main() is so old-school, we’re going to test both methods through test case:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
public class DefaultFoobarServiceTest {

@Autowired
private FoobarService foobarService;

@Test
public void testFoo() {
foobarService.foo();
}

@Test
public void testBar() {
foobarService.bar();
}
}


Assuming testFoo() succeeded, will testBar() succeed as well? The trick part is transaction propagation...


...


The problem with bar() method is that it is not marked as transactional, but since it calls transactional method foo(), you may expect that foo() is called within a transaction... but it’s not! And since foo() called from bar() isn’t wrapped in a transaction, foo() will throw a exception and the test will fail.

To explain this odd behavior, you must first understand how aspects (including declarative transactions) are applied to Spring beans. When we mark any of beans’ methods with @Transactional annotation, Spring automatically wraps the bean in Java dynamic proxy*. This proxy intercepts all method calls from other beans and if it encounters method marked as transactional, it performs special, transaction related routines (linking to existing or creating new transaction, rolling back on exception, etc.) Of course, the advice calls original method of wrapped object during its execution (like foo()). So as long as you are running foo(), Spring handles transactions transparently and smoothly.

But think what happens when you call bar() method. Method is not marked as transactional, so even though transactional proxy will intercept the call, it will soon discover that method does not require transaction and simply delegate to original method bar(). Then the method invokes foo(). This is the place, where we expect the transaction to be started, but wait! – we are invoking foo() method on this reference, which points directly to the bean, not transactional aspect proxy. Spring does not know anything about this call, since you are calling method on Java object (POJO), not on a proxy instance wrapping this object. Declarative transaction management won’t be applied and foo() will throw unexpected exception.

The answer is pretty obvious if you understand the mechanics of Spring AOP, because the same problem will occur in any code using aspects, not only transactions and not only via annotations. In fact, also the same problem has been solved in EJB by using SessionContext.getBusinessObject() method – passing this is forbidden by the specification. In Spring, the easiest way to avoid this particular bug in our code is to simply annotate bar() method as well.

* If our bean does not implement any business interface, CGLIB will be used instead. Also, you must use <tx:annotation-driven /> to make this magic happen.

UPDATE:

Comments

  1. Hi,
    What do you have in applicationContext?

    tx:annotation-driven transaction-manager="transactionManager"
    mode="proxy"
    OR
    tx:annotation-driven transaction-manager="transactionManager"
    mode="aspectj"

    It makes divrence.

    ReplyDelete
  2. Good point! I used Java dynamic proxy intentionally. If AspectJ or CGLIB were involved, transactions would work as expected simply because they override methods and thanks to polymorphism, proper method will be called. But since I used proxy, this instance references target object rather than transaction-aware wrapping proxy.

    In post above I wanted to show unexpected behavior if wrapping Spring beans in proxies is used without caution. It is important to understand this mechanism before applying it in your project.

    ReplyDelete
  3. Yes it's good to understand.
    But..
    For example, in my recent project mode aspectj doesn't start any transaction. :( In last project done in the same way, every thing is ok.
    That's curiosity.

    And another thing to remember is
    @Transactional(...,rollbackFor = Exception.class)
    Witchout this any changes will be saved even if you throw an exception.
    Yet another trap to avoid :(

    ReplyDelete
  4. That's a good point, and it occurs as well in EJB (as you have noted): there you have the notion of 'client view', which is the only way to actually call EJB logic if you want the call to go through the EJB container. Otherwise it will go directly through the JVM, bypassing all the managed magic that EJB container provides.

    To my understanding being able to work around the proxy/aspect/whatever-implemented wrapping by Spring/EJB, in such a simple way, reveals a general weakness with the wrapping approach. For the very minimum it shouldn't be *that* easy to break it. This shows how flaky and vulnerable enterprise middleware magic is.

    ReplyDelete

Post a Comment