Challenge

Find every method the proxy cannot see

25 minintermediate210 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 Starter

Hints

  1. Hint 1

    Ask one question per method: could a class implementing only the interface intercept this? Could a subclass override it?

  2. 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.

  3. Hint 3

    `this::send` captures `this`. Which object is that inside the bean?

  4. 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?