Lesson 07 / 19
Embedded Server and Request Processing Model
The links of the request pipeline split into three sources: the object container's own default, an annotation, and an explicit call. When the same set of links is declared in a different order, the run order the container produces does not change; when a class carrying an annotation is called by hand outside the container, the annotation has no effect at all.
Published
In this lesson
- Splits the request pipeline's links into three sources: the container's default, an annotation, an explicit call
- Shows, by running it, that the run order the container produces does not change when the same set of links is declared in a different order
- Measures that an annotation has no effect at all when a class carrying it is called by hand outside the container
Contents
The previous topic’s last lesson showed that behavior could be added without ever touching the call site, and that the limit of this was wherever the proxy could reach. This topic follows the same container with one question: while the container is handling a request, what share of it comes from each of the same three sources — call, annotation, default? Endpoints, validation, and data access all arise from the same three sources too; this lesson starts from the outermost layer of the chain and counts which link a request passes through from the moment it enters the container to the moment it leaves.
The server is modeled here. No real socket is opened, no real port is listened on, no real byte is read from the network — the path a request follows while passing through the container is built as a set of small function objects that run in sequence over a string. This simplification does not change the lesson’s question: in a real embedded server too, a request passes through a fixed sequence of links from the listening socket to the application code, and the question of how many links in that sequence have a counterpart in the source stays the same whether the socket is real or not. The server’s own job is to accept the connection and put the request into this chain — the Server-Side Fundamentals course established this, and the chain’s general idea, as a concept, and it is not repeated here. The course’s own term, request pipeline, is kept separate from the same course’s term middleware chain: a middleware chain is a sequence the caller lines up by hand, while a request pipeline, as this lesson will show, is a sequence the container builds according to its own rule.
There is another reason for modeling the server, one tied to the measurement itself: if a real port were opened, which port is free would vary by machine, and the ban on environment-dependent data that holds across this whole lesson series already rules out writing such a value. The string-based model removes this problem from the start — the odds of the same program giving two different outputs on two different machines drop to zero, because what is measured is not a delay on the network or a port number, but the function chain the container applies in a fixed order. In a real embedded server, the outermost link of this chain is reading from and writing to the network; here that link is skipped, and the chain starts directly from the first point the application code sees — the request itself.
Three Sources for the Links
- WD1. The request is modeled here as a string (
body); every link adds its own signature to this string; reading from and writing to the network are not modeled. - WD2. Discovering annotated classes does not perform a real classpath scan; the
container goes through the candidate class list given to the
scancall. This is a modeled counterpart of a real framework’s classpath-walking scan.
A link in the chain can exist through one of three routes. A link written into the
container’s own body, always running without putting any annotation on any class, is a
default — here the two links that add an identifier to the request and catch errors at the
end of the chain fall into this group; both are written inside the Container class’s own
run method and need no outside call. A @Link annotation placed on a class requests
that the container add it to the chain once it recognizes that class; this request does
nothing unless the container’s scan method is given a candidate list. The third route is
the barest of the three: the code itself puts a link directly into the chain with a
container.add(...) call — the exact line in the source that gives rise to it is this very
line.
// Container.java — who added each request-pipeline link: the caller, 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 Link { int priority() default 100; }
interface LinkFunction { String apply(String body, List<String> trace); }
final List<Class<?>> annotatedClasses = new ArrayList<>();
final List<LinkFunction> calledLinks = new ArrayList<>();
final List<String> calledNames = new ArrayList<>();
void scan(Class<?>... candidates) {
for (Class<?> candidate : candidates)
if (candidate.isAnnotationPresent(Link.class)) annotatedClasses.add(candidate);
}
void add(String name, LinkFunction l) {
calledNames.add(name);
calledLinks.add(l);
}
String run(String body, List<String> trace) {
try {
String s = body;
trace.add("request-id(default)");
List<Class<?>> sortedAnnotated = new ArrayList<>(annotatedClasses);
sortedAnnotated.sort(Comparator.comparingInt(c -> c.getAnnotation(Link.class).priority()));
for (Class<?> c : sortedAnnotated) {
LinkFunction l = (LinkFunction) c.getDeclaredConstructor().newInstance();
trace.add(c.getSimpleName() + "(annotation)");
s = l.apply(s, trace);
}
for (int i = 0; i < calledLinks.size(); i++) {
trace.add(calledNames.get(i) + "(call)");
s = calledLinks.get(i).apply(s, trace);
}
trace.add("error-handling(default)");
return s;
} catch (ReflectiveOperationException e) {
throw new RuntimeException(e);
}
}
public static void main(String[] args) {
Container c1 = new Container();
c1.scan(LoggingLink.class, CompressionLink.class);
c1.add("add-header", (b, t) -> b + "+header");
c1.add("write-length", (b, t) -> b + "+length");
List<String> t1 = new ArrayList<>();
String r1 = c1.run("request", t1);
Container c2 = new Container();
c2.scan(CompressionLink.class, LoggingLink.class);
c2.add("add-header", (b, t) -> b + "+header");
c2.add("write-length", (b, t) -> b + "+length");
List<String> t2 = new ArrayList<>();
String r2 = c2.run("request", t2);
Container c3 = new Container();
c3.scan(LoggingLink.class, CompressionLink.class);
c3.add("write-length", (b, t) -> b + "+length");
c3.add("add-header", (b, t) -> b + "+header");
List<String> t3 = new ArrayList<>();
String r3 = c3.run("request", t3);
System.out.println("-- setup-1: L,C scanned; header then length added --");
t1.forEach(x -> System.out.println(" " + x));
System.out.println("-- setup-2: C,L scanned (candidate order reversed); header then length added --");
t2.forEach(x -> System.out.println(" " + x));
System.out.println("-- setup-3: L,C scanned; length then header added (call order reversed) --");
t3.forEach(x -> System.out.println(" " + x));
System.out.println("setup-1 vs setup-2 order the same (annotated part): " + t1.equals(t2));
System.out.println("setup-1 vs setup-3 order the same (call part included): " + t1.equals(t3));
System.out.println("body results the same (1 and 2): " + r1.equals(r2));
int byDefault = 0, byAnnotation = 0, byCall = 0;
for (String line : t1) {
if (line.contains("(default)")) byDefault++;
else if (line.contains("(annotation)")) byAnnotation++;
else if (line.contains("(call)")) byCall++;
}
System.out.printf("chain: %d default, %d annotation, %d call, %d links total%n",
byDefault, byAnnotation, byCall, byDefault + byAnnotation + byCall);
System.out.println("-- manually built chain: container never involved --");
List<String> manualTrace = new ArrayList<>();
CompressionLink comp1 = new CompressionLink();
LoggingLink log1 = new LoggingLink();
String manualBody = "request";
manualTrace.add("CompressionLink(manual)");
manualBody = comp1.apply(manualBody, manualTrace);
manualTrace.add("LoggingLink(manual)");
manualBody = log1.apply(manualBody, manualTrace);
manualTrace.forEach(x -> System.out.println(" " + x));
System.out.println("default links in manual setup: "
+ manualTrace.stream().filter(x -> x.contains("default")).count());
System.out.println("first class called in manual setup is Compression (priority 20 in container, should come after Logging): "
+ manualTrace.get(0).startsWith("Compression"));
System.out.println("defect type: ineffective (no exception, @Link.priority value never read, order unchanged)");
}
}
@Container.Link(priority = 10)
class LoggingLink implements Container.LinkFunction {
public String apply(String body, List<String> trace) { return body + "+log"; }
}
@Container.Link(priority = 20)
class CompressionLink implements Container.LinkFunction {
public String apply(String body, List<String> trace) { return body + "+compress"; }
}
-- setup-1: L,C scanned; header then length added -- request-id(default) LoggingLink(annotation) CompressionLink(annotation) add-header(call) write-length(call) error-handling(default) -- setup-2: C,L scanned (candidate order reversed); header then length added -- request-id(default) LoggingLink(annotation) CompressionLink(annotation) add-header(call) write-length(call) error-handling(default) -- setup-3: L,C scanned; length then header added (call order reversed) -- request-id(default) LoggingLink(annotation) CompressionLink(annotation) write-length(call) add-header(call) error-handling(default) setup-1 vs setup-2 order the same (annotated part): true setup-1 vs setup-3 order the same (call part included): false body results the same (1 and 2): true chain: 2 default, 2 annotation, 2 call, 6 links total -- manually built chain: container never involved -- CompressionLink(manual) LoggingLink(manual) default links in manual setup: 0 first class called in manual setup is Compression (priority 20 in container, should come after Logging): true defect type: ineffective (no exception, @Link.priority value never read, order unchanged)
The total six links split into three sources: request-id and error-handling are
defaults — behavior the Container class writes into its own body without needing any
outside input. LoggingLink and CompressionLink are annotations — there is no line
anywhere in the source that calls these two classes, only a @Link mark on top of them and
a scan call that reads that mark. add-header and write-length are calls — without a
container.add(...) line, neither of these two links would ever exist in the chain. The six
links split evenly across the three sources — two apiece — but this evenness is not a rule,
it is a property of the example this lesson chose; the shared definition’s twelve behaviors
did not split evenly between call, annotation, and default — annotation dominated. The
question the course asks is the same here too: this time what is measured is not a
component’s construction, but the path a request follows while passing through a container.
Someone reading the source text top to bottom sees only two of these six links directly. The
lines container.add("add-header", ...) and container.add("write-length", ...) are right
there, and a reader can infer from the source that these two links exist and in what order
they were added. The remaining four leave their trace somewhere else in the source: for
LoggingLink and CompressionLink the only trace is the @Link line above the class
definition; for request-id and error-handling there is no trace at all — these two can
only be found by reading the body of the Container.run method, and that method sits in a
file the calling code never sees. Someone reading the source only from the caller’s own file
misses where two-thirds of the chain comes from.
The Container Applies Its Own Order
- WD3. The annotated link with the smaller
priorityvalue runs first; this is the container’s own rule and is written nowhere in the source.
The only difference between setup-1 and setup-2 is the order of the classes given to the
scan call: one scans the logging class first, then the compression class; the other does
the opposite. Even though the source text is written differently, the trace list comes out
byte-for-byte identical, because the container re-sorts the scanned classes by its own
priority field and never uses the order of the array given to scan. The run order of
the links is not the order the caller wrote in the source; the container applies its own
order, and two different orderings in the source produce the same run order.
But this rule only holds for links found through an annotation. Setup-3 uses the same scan
order as setup-1; its only difference is the order of the two add calls: length first,
then header. The trace list comes out different this time — write-length appears first,
add-header second. The container applies its own ordering rule to the links it finds
through annotations, but it does no ordering at all for links added through an explicit
add call; it puts them into the chain in exactly the order they were called. The ordering
rule applies only to what the container discovers, not to what it is told — an
annotation is a request left to the container’s own decision, while a call is the caller’s
own decision, and the container does not change it.
This distinction has a practical consequence: if two developers give the same two annotated
classes to scan in a different order — one collected the files with a tool that lists them
alphabetically, the other with one that lists them by modification date — their source texts
look different, but the chain produced is byte-for-byte the same. The order does not live in
the source, it lives in the priority field; someone doing a code review has to look at the
annotation types’ own definitions, not the order in the scan call, to see that both
versions produce the same behavior. There is no such guarantee for an explicit call —
changing the order of the add calls also changes the order of the chain, because the
container applies no rule there at all.
In a Manually Built Chain, the Annotation Has No Effect at All
- WD4. In the manually built call,
CompressionLinkandLoggingLinkare instantiated directly and theirapplymethods are called in sequence; noContainerobject is created at all. - WD5. The
@Linkannotation keeps standing on both classes; it was not removed, not deleted. The reason it has no effect in the manual setup is not that the annotation disappeared, but that there is no party left to read it.
The last measurement takes the container completely out of the picture: CompressionLink
and LoggingLink are constructed by hand with new, and their apply methods are called
by hand, in sequence. Two results stand out. First, there is no line tagged default in the
trace list at all — request-id and error-handling are written into the body of the
Container.run method, and since that method is never called, neither behavior ever
happens. Second, CompressionLink is called first and LoggingLink second — even though
their order inside the container was exactly the reverse, since CompressionLink’s
priority is 20 and LoggingLink‘s is 10. In the manual call, this number is never read at
all; the @Link(priority = 20) annotation sits on the class, but there is no scan call, no
sorting step, left to read it. An annotation is not a property but a request; if there is no
container to fulfill the request, both classes are called exactly like two ordinary Java
objects, and the order stays entirely in the caller’s hands.
This completes the three-source distinction from the start of the lesson. A default behavior is part of the container’s own code, so it does not exist without a container; an annotation carries a request, so it is ineffective without a container to read it; only a call — a method invoked directly — produces the same behavior whether or not a container exists, because a call is already an expression written in the source. The difference these three behaviors show inside and outside the container reveals what the container actually adds: it does not add a new capability, it reads and applies two kinds of request (default and annotation) that are never written in the source.
This result carries a practical warning for whoever tests it: calling the LoggingLink or
CompressionLink class in a unit test outside the container confirms that class’s own
logic, but never tests its place in the chain, its order, or the default links that run
alongside it. Two measurements answer two separate questions: is the class’s own behavior
correct, and does the container put it in the correct place. The first question can be
answered with a manually built object; the second can only be answered by running the
container itself — and that is exactly what setup-1, setup-2, and setup-3 do together in
this lesson.
Summary
- The request pipeline’s six links split into three sources: two are the container’s default, two are an annotation, two are an explicit call.
- The run order of links found through an annotation is decided by the container’s own
priorityrule and is independent of the scan order in the source; two different scan orders produced the same result. - The order of links added through an explicit call plays no part in the container’s rule; the order the caller wrote is kept exactly.
- With the container taken out of the picture and the same classes called by hand, the
default links never ran and the annotation’s
priorityvalue was never read; without a party to read it, the annotation was ineffective. - The body result the two setups produced came out byte-for-byte identical, as long as the order stayed the same.
Next Step
This lesson measured the chain a request follows while passing through the container, but it never settled which job the request is met with at the end of the chain — the links only changed the request, they never routed it. The next lesson closes that gap: it measures which annotation, not which expression, declares an endpoint in the source, and what the container answers when a request does not match any of them.
To keep your progress and take notes, Log in
My notes
Log in to take notes.