ExerciseWarm-up
Warm-up
Watch the proxy get bypassed
5 minjunior1–6 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 StarterDone 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?