ExerciseChallenge
Challenge
Find every method the proxy cannot see
25 minintermediate2–10 yrs
Edge cases. You have to reason, and two valid fixes differ.
What this teaches
- The two proxy kinds have different blind spots, not the same one
- A public method missing from the interface is invisible to a JDK proxy
- Private, final, static and self-invoked calls are invisible to both
- A method reference bound to `this` is self-invocation with the evidence removed
- Predicting first is the skill; the proxy only confirms what you reasoned out
Starter
Starter.java
import java.lang.reflect.*;
import java.util.*;
/**
* Challenge: eight call sites, two kinds of proxy, one rule.
*
* Fill in the prediction table BEFORE you run this. Reasoning it out is the
* exercise; running it only tells you whether you were right.
*
* The rule you are applying, stated once: a JDK dynamic proxy can advise
* exactly what the interface declares, and a subclass proxy can advise exactly
* what a subclass can override. Everything else follows.
*/
public class Starter {
interface Notifier {
void send(String to);
void sendAll(List<String> recipients);
}
static class NotificationService implements Notifier {
NotificationService() {
// 8. called from the constructor, before any proxy could exist
send("startup@example.com");
}
@Override
public void send(String to) { // 1. on the interface
System.out.println(" sending to " + to);
}
@Override
public void sendAll(List<String> recipients) { // 2. on the interface
// 3. self-invocation, in the form that hides it best
recipients.forEach(this::send);
}
public void resend(String to) { // 4. public, NOT on the interface
System.out.println(" resending to " + to);
}
protected void audit(String to) { // 5. protected
System.out.println(" audited " + to);
}
private void validate(String to) { // 6. private
System.out.println(" validated " + to);
}
public final void flush() { // 7. final
System.out.println(" flushed");
}
public static void configure() { // 9. static
System.out.println(" configured");
}
}
/*
* TODO 1 — predict. true = the advice runs, false = the call is invisible.
*
* JDK proxy subclass proxy
* 1. send(), called from outside ____ ____
* 2. sendAll(), from outside ____ ____
* 3. send(), from inside sendAll ____ ____
* 4. resend() ____ ____
* 5. audit() ____ ____
* 6. validate() ____ ____
* 7. flush() ____ ____
* 8. send(), from the constructor ____ ____
* 9. configure() ____ ____
*
* Two of these rows differ between the two columns. Find them before you
* look at anything else — that difference is the whole JDK-versus-CGLIB
* question, and it is the one interviewers actually ask.
*/
/** A JDK dynamic proxy. It can only ever see what Notifier declares. */
static Notifier jdkProxy(NotificationService target) {
return (Notifier) Proxy.newProxyInstance(
Notifier.class.getClassLoader(),
new Class<?>[] { Notifier.class },
(proxy, method, args) -> {
System.out.println(" advice → " + method.getName());
Object result = method.invoke(target, args);
System.out.println(" advice ← " + method.getName());
return result;
});
}
// TODO 2: write the subclass proxy — `extends NotificationService`, holding
// a target, overriding everything it is allowed to override and printing
// the same "advice →" / "advice ←" lines.
//
// Write it method by method, and when the compiler refuses one, stop and
// write down which rule you just hit. Those refusals are the answer sheet.
//
// Expect one runtime surprise too: your subclass constructor runs super()
// first, NotificationService's constructor calls send(), and send() is now
// your override — which dereferences a target field that has not been
// assigned yet. That is not a flaw in the exercise. It is precisely why
// CGLIB does not run your constructor when it builds a proxy, and why a
// CGLIB proxy's own fields are left null.
public static void main(String[] args) {
NotificationService target = new NotificationService();
Notifier viaJdk = jdkProxy(target);
System.out.println("── through the JDK proxy ──");
viaJdk.send("a@example.com");
viaJdk.sendAll(List.of("b@example.com", "c@example.com"));
// TODO 3: try to call resend() through viaJdk. It does not compile.
// Say why in one sentence — the answer is not "it is missing", it is
// about what type the proxy actually has.
// TODO 4: run the subclass proxy through the same calls plus resend(),
// audit(), flush() and configure(). Correct your table.
// TODO 5: change sendAll to use an indexed for-loop with an explicit
// `this.send(...)`. Does the behaviour change? Should the readability
// change how likely a reviewer is to catch this bug?
// TODO 6: in a comment, state the one fact that makes all nine rows
// predictable without running anything.
}
}Run it locally:
cd exercises/java/spring-aop/aop-proxies/02-challenge
javac Starter.java -d /tmp/out && java -cp /tmp/out StarterHints
Hint 1
Ask one question per method: could a class implementing only the interface intercept this? Could a subclass override it?
Hint 2
A static method is not dispatched on an instance at all, so there is nothing for either proxy to stand in front of.
Hint 3
`this::send` captures `this`. Which object is that inside the bean?
Hint 4
The constructor runs before the proxy exists. Nothing it calls is advised.
Done when
- Your prediction table is filled in before you run the file
- Both proxies are built and every prediction is confirmed or corrected
- You can state the JDK-versus-CGLIB difference without naming annotations
- A comment explains which single fact makes all eight rows predictable
← Back to How does Spring AOP work, and what are its limits?