Lesson 06 / 19
Aspect-Oriented Programming
A wrap marker placed on a managed component adds behavior without ever touching the call site: a cross-cutting concern (a counter, in this example) steps in on every call, and the mechanism is a proxy. But the marker existing does not mean it is applicable — the same marker placed on a class with no interface stays ineffective without throwing any error at all.
Published Updated
In this lesson
- Shows by running it that a wrap annotation adds behavior through a proxy without touching the call site
- Measures by comparing that a manually built object is not wrapped, while the same class built through the container is
- Proves that a wrap marker placed on a class with no interface stays ineffective without throwing any error
Contents
The previous five lessons measured how a managed component is constructed, which dependency it
is equipped with, how long it lives, where it gets its settings from, and which implementation is
chosen automatically — all of it concerned the component’s own internal state. The previous
lesson’s Cache and Metric examples worked without the call site ever being touched, but the
calling side still always talked to the object that actually does the work. This lesson asks
a different question: is it possible to add something to a component’s behavior, from
outside, without ever changing its source code at all? The Server Fundamentals course had named
this kind of behavior — one scattered across more than one class, belonging to no single class —
a cross-cutting concern: behaviors like logging, keeping a counter, checking authorization,
which accompany the business logic itself without being related to it. This lesson measures how
we add that cross-cutting concern to a managed component, without ever touching the call site.
Wrapping: Stepping In Without Touching the Call Site
The InMemoryStore class carries @Container.Wrap. If the container sees this annotation while
constructing a component and the class implements exactly one interface, it does not return the
constructed object directly — instead it returns a proxy. A proxy is an intermediate object
that implements the Store interface but does not do the real work; it routes every call through
itself first: the Proxy.newProxyInstance call wraps the InMemoryStore instance (target) and
builds a handler that does wrapCalls++ on every method call and then forwards the call to the
real object. Service itself never knows about this proxy at all — when it calls store.read(),
it cannot tell whether the reference it holds is a real InMemoryStore or a proxy wrapping one,
because both carry the same Store interface.
This is the concrete form of a cross-cutting concern being added without ever touching the call
site: Service.report() still calls store.read() — not a single character
changed, but now every call passes through a counter increment. What adds the counter is neither
Service’s source code nor InMemoryStore‘s read() body — what adds it is the container itself
seeing the Wrap marker. This is a mechanism from the same family as the previous lesson’s
automatic configuration: there, the container decided which class would be constructed for an
interface; here, the container decides what gets wrapped around the constructed object. Both
are container decisions that leave no trace in the source.
The third argument given to Proxy.newProxyInstance is a handler —
(p, m, a) -> { wrapCalls++; return m.invoke(target, a); }. This three-letter parameter
abbreviation comes from a standard contract: p is the proxy object the call was made on itself,
m is a Method object representing the called method, a is the array of arguments given to
that call. Every call made to the proxy — whether it is read() or some other method added
later — passes through this single handler, because Proxy.newProxyInstance does not produce a
separate method per call, it builds a single interceptor and tells which method was called using
the m parameter. In this measurement the Store interface has a single method (read()), so
m’s value is always the same; but the mechanism stays the same no matter how many methods are
added to the interface.
- CC26. The proxy only increases the call count, it forwards the real call to the target with
m.invoke(target, a); no call is blocked or changed by the proxy. - CC27. Wrapping is applied only to classes implementing exactly one interface
(
type.getInterfaces().length == 1); this is notProxy.newProxyInstance‘s own constraint, it is this measurement’s own simplification. A class implementing more than one interface is not tried in this measurement. - CC28. The counter only tracks the call count; which thread called it, how long it took, or with which arguments it was called are not in this measurement.
- CC29. A manually built object never enters the container’s
getmethod at all, so it never reaches the proxy logic either — wrapping applies only to objects the container constructs. - CC30. Identity is read only with
Proxy.isProxyClass; no proxy instance’s identity hash is ever printed.
// Container.java — how a wrap marker's producing behavior via a proxy, without the source ever changing, comes from a call, an annotation, or the container's own default
import java.lang.annotation.*;
import java.lang.reflect.*;
import java.util.*;
public class Container {
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE)
@interface Component {}
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE)
@interface Wrap {}
static int annotation = 0, byDefault = 0;
static int ineffective = 0;
static void observe(String behavior, String source) {
if (source.equals("default")) byDefault++; else annotation++;
System.out.printf("%-38s %s%n", behavior, source);
}
static void rule(String name, String result) {
if (result.equals("ineffective")) ineffective++;
System.out.printf("%-38s %s%n", name, result);
}
static void summary() {
System.out.printf("behavior: %d annotation, %d default%n", annotation, byDefault);
System.out.printf("defect : %d ineffective%n", ineffective);
}
int wrapCalls = 0;
final Map<Class<?>, Class<?>> bindings = new HashMap<>();
Container bind(Class<?> iface, Class<?> implementation) { bindings.put(iface, implementation); return this; }
Object get(Class<?> type) throws Exception {
if (type.isInterface()) {
Class<?> implementation = bindings.get(type);
if (implementation == null)
throw new IllegalStateException("no implementation bound to interface: " + type.getSimpleName());
return get(implementation);
}
Component c = type.getAnnotation(Component.class);
if (c == null) throw new IllegalStateException("no component annotation: " + type.getSimpleName());
Constructor<?> constructor = type.getDeclaredConstructors()[0];
constructor.setAccessible(true);
Object[] arg = new Object[constructor.getParameterCount()];
for (int i = 0; i < arg.length; i++) arg[i] = get(constructor.getParameterTypes()[i]);
Object instance = constructor.newInstance(arg);
if (type.isAnnotationPresent(Wrap.class) && type.getInterfaces().length == 1) {
Object target = instance;
instance = Proxy.newProxyInstance(type.getClassLoader(), type.getInterfaces(),
(p, m, a) -> { wrapCalls++; return m.invoke(target, a); });
}
return instance;
}
public static void main(String[] args) throws Exception {
Container container = new Container().bind(Store.class, InMemoryStore.class);
Service service = (Service) container.get(Service.class);
int previousWraps = container.wrapCalls;
service.report();
Container.observe("call wrapped", container.wrapCalls > previousWraps ? "annotation" : "default");
InMemoryStore manual = new InMemoryStore();
Container.observe("wrapping on a manually built object", Proxy.isProxyClass(manual.getClass()) ? "annotation" : "default");
Object noIface = container.get(InterfacelessComponent.class);
Container.rule("wrap put on an interfaceless class", noIface instanceof Proxy ? "error" : "ineffective");
Container.summary();
}
}
interface Store { String read(); }
@Container.Component @Container.Wrap
class InMemoryStore implements Store {
public String read() { return "data"; }
}
@Container.Component
class Service {
final Store store;
Service(Store store) { this.store = store; }
String report() { return store.read(); }
}
@Container.Component @Container.Wrap
class InterfacelessComponent { String hello() { return "hello"; } }
call wrapped annotation wrapping on a manually built object default wrap put on an interfaceless class ineffective behavior: 1 annotation, 1 default defect : 1 ineffective
This topic’s first lesson measured this same question across twelve behaviors and four defects,
arriving at a total of 3 call, 6 annotation, 3 default and 1 error, 2 silent, 1 ineffective;
this lesson stays only in that core’s wrapping slice.
The Counter Rose Without the Call Site Ever Being Touched
The call wrapped line comes out annotation: the previousWraps variable holds the counter’s
value right before service.report() is called, then service.report() is called, and
afterward container.wrapCalls > previousWraps comes out true — the counter really did rise. It
is worth noting that this line calls service.report(), not store.read() directly: Service. report() internally goes to store.read(), and since the store field is actually a proxy,
this internal call passes through the proxy’s invoke handler. There is not a single word in
Service’s source showing the proxy — the Store store field only carries the interface type,
it does not say which concrete class (a real InMemoryStore or a proxy wrapping one) stands
behind it.
wrapCalls is an instance field, not the Container class’s own — since this measurement uses a
single container object, we see a single counter, but if two separate Container instances had
each wrapped the same InMemoryStore class separately, their two counters would stay independent
of each other too. The counter is part of the container’s own state, not the wrapped class’s —
this also explains why Service never keeps its own counter at all, the counter always sits in
the container.
The manually built InMemoryStore never receives this wrapping at all:
Proxy.isProxyClass(manual.getClass()) returns false, so the wrapping on a manually built object line comes out default — a situation the container never got involved in at all. The
first lesson had already noted this in advance; here the reason becomes fully visible: wrapping
is applied only inside the get method, right after the object is constructed. Since
new InMemoryStore() falls outside this method, the proxy never comes into play at all, and the
manually built object stays permanently unwrapped — it cannot be “converted” into a proxy
afterward, because in Java an object’s identity does not change once it is constructed.
Limiting Measurement: A Marker That Shows But Does Not Work
The InterfacelessComponent class carries @Container.Component @Container.Wrap — exactly like
InMemoryStore. But with one difference: InterfacelessComponent implements no interface at all,
it is only a class on its own. The wrapping check in the get method looks for two conditions at
once: type.isAnnotationPresent(Wrap.class) && type.getInterfaces().length == 1. For
InterfacelessComponent, the first is true but the second is false —
getInterfaces().length is zero. The check reaches a false result and the wrapping block is
never entered at all; the object is returned in its bare form.
Object noIface = container.get(InterfacelessComponent.class);
Container.rule("wrap put on an interfaceless class", noIface instanceof Proxy ? "error" : "ineffective");
noIface instanceof Proxy comes out false, and the rule line falls into the "ineffective"
class. There is no exception at all — since Proxy.newProxyInstance is never called, there is
no error to throw either. Result: InterfacelessComponent is constructed, it is usable, it looks
in no way broken — but the @Container.Wrap marker standing on it is entirely ineffective.
This is the shared model’s blind spot: an annotation standing on a class does not mean that
annotation is actually applied. Whoever reads the source might see @Container.Wrap and think
“every call on this class is being counted,” but that is not true — the Proxy mechanism can
only work through an interface, because Java’s dynamic proxy mechanism can only wrap interfaces,
not concrete classes.
The reason this defect stays in the ineffective class is exactly this technical boundary: the
container does not refuse to wrap InterfacelessComponent, it cannot wrap it at all, and
it never says so at all. Of the four defect families, this is the quietest one — error shouts,
silent produces a wrong value, but ineffective leaves no trace at all; the program appears to
work as expected, only the one expected behavior (the counter rising) never happens at all.
As CC27 points out, this measurement only runs the zero-interface edge case; it never tries
its symmetric counterpart — a class implementing more than one interface. But the check’s own
code line (type.getInterfaces().length == 1) already tells us what would happen in that case
too: a class implementing two interfaces would also get false from the check, just like
InterfacelessComponent, and would return unwrapped — the reason is different (not a shortage of
interfaces, but an excess of them), but the result is the same, a silent ineffectiveness. This is
an example of a single condition line dropping two separate edge cases into the same defect
class: Proxy.newProxyInstance itself is capable of gathering more than one interface behind the
same proxy — its third argument accepts a Class<?>[] array — but this measurement’s own check
never uses that capability at all, it only correctly handles the case of exactly one
interface. Whoever reads the source, on seeing @Container.Wrap, might think “this class will be
wrapped” without looking at how many interfaces the class carries; but the correct answer depends
only on whether getInterfaces().length is exactly 1.
Six Lessons, the Same Question
This topic’s six lessons asked the same question over and over — did a call in the source
produce this behavior, an annotation, or neither — and each time measured a different corner.
The first lesson answered this question with Container.java‘s full core: it measured, in a
single run, an object’s construction and staying a singleton, injection through the constructor,
scope and the init method, setting sources, and wrapping, arriving at a total of twelve behaviors
and four defects: 3 call, 6 annotation, 3 default and 1 error, 2 silent, 1 ineffective. The
second, third, and this lesson rebuilt and ran only the slice of that same core touching their
own question: injection through the constructor and an unbound interface’s error, scope and the
init method and an unrecognized scope name’s silence, and now wrapping and an ineffective marker
— all three are subsets of the first lesson’s total, none of them claims to arrive at the same
twelve/four total on its own. The fourth and fifth lessons stepped outside this core and built
their own measurements — the precedence order of three configuration sources, conditional
automatic defaults — but both kept the same vocabulary: continuing to ask where a behavior is
born from in the source, and to distinguish whether it stays noisy or silent once it breaks.
Summary
- A wrap annotation adds behavior by building a proxy that steps in during the call, without
ever touching the call site;
Service’s source code never changes at all. - The wrapped behavior (a counter, in this measurement) appears nowhere in the source as a call — it can only be confirmed by running it, once the counter itself rises.
- A manually built object never passes through the container’s
getmethod, so it is not wrapped, and it stays permanently unwrapped. - A wrap marker placed on a class with no interface stays ineffective without throwing any error at all — an annotation existing does not mean it is applicable.
- Of the twelve behaviors measured in the first lesson, three came from a call in the source, six from an annotation, three from the container’s own default; the wrapping and the ineffective marker added in this lesson are two new observations layered on top of that total.
Next Step
This topic’s six lessons measured a single object container’s own internal workings: an object’s construction, a dependency being supplied, scope and lifecycle, where settings come from, conditional defaults, and now behavior added without ever touching the call site. This last lesson also drew a boundary: a proxy can only add behavior as far as the interface can reach, it cannot go beyond that. The next topic follows the same container outside its own internal workings, while it serves a request — endpoints, validation, and data access are also born from exactly the same three sources measured throughout this topic: a call, an annotation, or the container’s own default.
To keep your progress and take notes, Log in
My notes
Log in to take notes.