Warm-up

Watch the proxy get bypassed

5 minjunior16 yrs

One concept, guided. Near-impossible to fail.

What this teaches

  • @Transactional is a proxy wrapped around the bean, not a change to your class
  • An internal call runs on `this`, so it never crosses the proxy
  • The failure is silent — nothing logs, nothing throws
  • The same mechanism explains @Async, @Cacheable and @PreAuthorize

Starter

Starter.java
import java.lang.reflect.*;
import java.util.*;

/**
 * Warm-up: Spring is not on the classpath, and it does not need to be.
 *
 * @Transactional is java.lang.reflect.Proxy plus a transaction manager. This
 * file is the proxy half, which is where every surprising behaviour comes from.
 */
public class Starter {

    interface OrderService {
        void placeOrder(String id);
        void placeAll(List<String> ids);
    }

    static class RealOrderService implements OrderService {
        @Override
        public void placeOrder(String id) {
            System.out.println("    saving " + id);
        }

        @Override
        public void placeAll(List<String> ids) {
            // Self-invocation: this.placeOrder(...), not proxy.placeOrder(...)
            for (String id : ids) placeOrder(id);
        }
    }

    /** Stands in for Spring's transactional proxy. */
    static OrderService transactional(OrderService target) {
        return (OrderService) Proxy.newProxyInstance(
            OrderService.class.getClassLoader(),
            new Class<?>[] { OrderService.class },
            (proxy, method, args) -> {
                System.out.println("  BEGIN   " + method.getName());
                Object result = method.invoke(target, args);
                System.out.println("  COMMIT  " + method.getName());
                return result;
            });
    }

    public static void main(String[] args) {
        OrderService service = transactional(new RealOrderService());

        System.out.println("called from outside:");
        service.placeOrder("A1");

        System.out.println("placeAll, which calls placeOrder itself:");
        service.placeAll(List.of("B1", "B2"));

        // TODO 1: count the BEGIN lines in the second block. How many
        // transactions did the two inner placeOrder calls get? Why?

        // TODO 2: change RealOrderService so the inner calls DO go through the
        // proxy. One way: give the class a field holding its own proxy and call
        // through that. Spring's equivalent is AopContext.currentProxy().
        //
        // Then say why that fix is a smell rather than a solution.

        // TODO 3: the real fix is to move placeOrder to a second service and
        // call it through an injected reference. Sketch that here — two
        // classes, and placeAll delegating to the other one's proxy.
    }
}

Run it locally:

cd exercises/java/spring-data/transactional-internals/01-warmup
javac Starter.java -d /tmp/out && java -cp /tmp/out Starter

Done when

  • You have seen an annotated call produce no transaction at all
  • You can state in one sentence why the inner calls were not intercepted
  • You made the inner call go through the proxy and saw it start working

← Back to How does @Transactional actually work, and when does it silently do nothing?