Warm-up

Build both kinds of proxy

10 minjunior18 yrs

One concept, guided. Near-impossible to fail.

What this teaches

  • A JDK dynamic proxy implements the interface; a CGLIB proxy subclasses the class
  • The proxy is a different object, so it is not an instance of your class
  • A subclass can only advise what a subclass can override
  • The compiler enforces the final-method limit, so you can see it rather than trust it

Starter

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

/**
 * Warm-up: Spring AOP is two mechanisms, and you can build both by hand.
 *
 * A JDK dynamic proxy implements an interface and holds your object inside it.
 * A CGLIB proxy is a generated subclass that overrides your methods. Every
 * limit of Spring AOP falls out of which one you are looking at, so the fastest
 * way to know the limits is to build both.
 */
public class Starter {

    /* ─────────── kind one: a JDK dynamic proxy (done for you) ─────────── */

    interface AuditService {
        String record(String action);
        void recordAll(List<String> actions);
    }

    static class RealAuditService implements AuditService {
        @Override
        public String record(String action) {
            System.out.println("    writing " + action);
            return "ok:" + action;
        }

        @Override
        public void recordAll(List<String> actions) {
            // Called on `this`. Note that carefully before you run anything.
            for (String a : actions) record(a);
        }
    }

    static AuditService advised(AuditService target, String aspect) {
        return (AuditService) Proxy.newProxyInstance(
            AuditService.class.getClassLoader(),
            new Class<?>[] { AuditService.class },
            (proxy, method, args) -> {
                System.out.println("  " + aspect + " → " + method.getName());
                Object result = method.invoke(target, args);
                System.out.println("  " + aspect + " ← " + method.getName());
                return result;
            });
    }

    /* ─────────── kind two: a subclass, which is what CGLIB writes ─────────── */

    static class ReportService {
        public void generate(String quarter) {
            System.out.println("    generating " + quarter);
        }

        public final void archive(String quarter) {
            System.out.println("    archiving " + quarter);
        }

        private void checksum(String quarter) {
            System.out.println("    checksum " + quarter);
        }

        public void publish(String quarter) {
            checksum(quarter);
            System.out.println("    published " + quarter);
        }
    }

    // TODO 3: write AdvisedReportService here — a subclass of ReportService
    // that holds a target and prints "  audit → name" before and
    // "  audit ← name" after every method it is able to override.
    //
    //     static class AdvisedReportService extends ReportService { ... }

    public static void main(String[] args) {
        AuditService proxy = advised(new RealAuditService(), "audit");

        // TODO 1: predict both of these, in writing, before you run the file.
        //
        //   proxy.getClass().getSimpleName()      → ?
        //   proxy instanceof RealAuditService     → ?
        //
        // Print them. Then answer: why would
        //     @Autowired RealAuditService audit;
        // fail to start an application whose bean is this proxy?

        System.out.println("recordAll, which loops and calls record itself:");
        proxy.recordAll(List.of("login", "logout"));

        // TODO 2: count the "audit →" lines above. How many did you expect?
        // Which calls were not advised, and what object were they made on?

        // TODO 4: once AdvisedReportService exists, call generate, archive and
        // publish through it. One of the three gets no advice at all, and one
        // is advised but does something unadvised inside it. Name both, and
        // say why each is invisible to a subclass.

        // TODO 5: now try adding
        //     @Override public void archive(String quarter) { ... }
        // to your subclass. Read the compiler error. That error is the entire
        // reason @Transactional on a final method silently does nothing.
    }
}

Run it locally:

cd exercises/java/spring-aop/aop-proxies/01-warmup
javac Starter.java -d /tmp/out && java -cp /tmp/out Starter

Done when

  • You predicted the proxy's class name and instanceof result before running
  • You wrote a subclass proxy and watched one method get no advice at all
  • You read the compiler error for overriding a final method
  • You can say in one sentence why @Autowired on the concrete class can fail

← Back to How does Spring AOP work, and what are its limits?