ExerciseChallenge
Challenge
Make the annotation take effect
20 minintermediate2–8 yrs
Edge cases. You have to reason, and two valid fixes differ.
What this teaches
- Only calls crossing the proxy boundary are advised
- Private and final methods cannot be intercepted at all
- Swallowing an exception inside the method commits the transaction
- The rollback default is unchecked-only, and it is rarely what you want
Starter
Starter.java
import java.lang.reflect.*;
import java.util.*;
/**
* Challenge: four call sites, and only some of them get a transaction.
*
* Predict which before running. Then fix the ones that should be transactional
* and are not.
*/
public class Starter {
interface AccountService {
void debit(String account, int paise) throws Exception;
void transfer(String from, String to, int paise) throws Exception;
void audit(String message);
}
static class RealAccountService implements AccountService {
@Override
public void debit(String account, int paise) {
System.out.println(" debit " + account + " " + paise);
}
@Override
public void transfer(String from, String to, int paise) throws Exception {
debit(from, paise); // self-invocation
if (paise > 100_000) {
throw new Exception("limit exceeded"); // checked
}
System.out.println(" credit " + to + " " + paise);
}
@Override
public void audit(String message) {
try {
System.out.println(" audit " + message);
throw new IllegalStateException("audit store unavailable");
} catch (Exception e) {
// Swallowed. What does the proxy see?
System.out.println(" (audit failure logged and ignored)");
}
}
}
static AccountService transactional(AccountService target) {
return (AccountService) Proxy.newProxyInstance(
AccountService.class.getClassLoader(),
new Class<?>[] { AccountService.class },
(proxy, method, args) -> {
System.out.println(" BEGIN " + method.getName());
try {
Object r = method.invoke(target, args);
System.out.println(" COMMIT " + method.getName());
return r;
} catch (InvocationTargetException e) {
Throwable cause = e.getCause();
// Spring's default rule, reproduced.
if (cause instanceof RuntimeException || cause instanceof Error) {
System.out.println(" ROLLBACK " + method.getName()
+ " — " + cause.getMessage());
} else {
System.out.println(" COMMIT " + method.getName()
+ " — checked: " + cause.getMessage());
}
throw cause;
}
});
}
public static void main(String[] args) {
AccountService service = transactional(new RealAccountService());
System.out.println("1. direct call:");
try { service.debit("ACC1", 5_000); } catch (Exception e) { }
System.out.println("2. transfer within the limit:");
try { service.transfer("ACC1", "ACC2", 5_000); } catch (Exception e) { }
System.out.println("3. transfer over the limit (checked exception):");
try { service.transfer("ACC1", "ACC2", 500_000); } catch (Exception e) { }
System.out.println("4. audit, which swallows its own exception:");
service.audit("transfer attempted");
// TODO 1: in case 2, how many transactions were opened, and how many
// debits happened? Is the debit inside the transfer's transaction?
// TODO 2: case 3 debited and then threw. What is committed? Change ONE
// thing in the proxy so a checked exception rolls back, and say what
// the Spring equivalent of that change is.
// TODO 3: case 4 swallows its exception. Did the proxy roll back?
// Explain what Spring would do here, including what
// UnexpectedRollbackException is and when you would see it.
// TODO 4: make the debit inside transfer genuinely transactional
// without using AopContext. Two classes.
}
}Run it locally:
cd exercises/java/spring-data/transactional-internals/02-challenge
javac Starter.java -d /tmp/out && java -cp /tmp/out StarterHints
Hint 1
Ask one question per call site: does this call go through the proxy, or straight to the target object?
Hint 2
A JDK proxy implements an interface; a CGLIB proxy subclasses the class. Neither can intercept something it cannot override.
Hint 3
If the proxy never sees an exception, it has no reason to roll back.
Hint 4
Spring's default rolls back on RuntimeException and Error only.
Done when
- Every call that should be transactional is, and you can prove it from the output
- A checked exception rolls back, and you changed one thing to make it
- A comment explains why catching inside the method broke the rollback
← Back to How does @Transactional actually work, and when does it silently do nothing?