Skip to main content

Child Resources

Child Resources are REST servlets or objects that are linked to parent resources through the @Rest(children) annotation.

Example
/** Parent Resource */
@Rest(
path="/parent",
children={MyChildResource.class}
)
public MyRootResources extends BasicRestServletGroup {...}
/** Child Resource */
@Rest(
path="/child" // Path relative to parent resource.
)
// Note that we don't need to extend from RestServlet.
public MyChildResource extends BasicRestResource {...}

The path of the child resource gets appended to the path of the parent resource. So in the example above, the child resource is accessed through the URL /parent/child.

One advantage of using child resources is that they do not need to be declared in the JEE web.xml file.

Initialization of and access to the child resources occurs through the parent resource.

Children can be nested arbitrary deep to create complex REST interfaces with a single top-level REST servlet.

As explained earlier, child REST objects typically extend from BasicRestResource or BasicRestResourceGroup and not from one of the servlet classes. They also technically don't even need to extend from those classes and can instead just be a normal class annotated with the bare-minimum @Rest and @RestOp annotations.

Dynamic Child Resources​

In addition to declaring children statically via @Rest(children=…), parent resources that extend BasicRestServletGroup or BasicRestResourceGroup can add and remove child resources at runtime. This is useful for plug-in architectures, integration tests, and other scenarios where the set of mounted endpoints is not known ahead of time.

The convenience methods on the group classes delegate to RestChildren, which holds children in a thread-safe, copy-on-write snapshot — reads on the request hot path are lock-free, while mutations are serialized through a write lock.

Example
@Rest(path="/root")
public class MyServer extends BasicRestServletGroup {}

@Rest(path="/alpha")
public class AlphaChild extends BasicRestResource {
@RestGet("/ping")
public String ping() { return "alpha-pong"; }
}

var server = new MyServer();
// ... bootstrap into Jetty / Spring Boot / MockRestClient ...

// Add by class — path is derived from the child's @Rest(path) annotation.
server.addChild(AlphaChild.class);

// Add by instance — useful when the child is hand-constructed or comes from a bean store.
var beta = new BetaChild();
server.addChild(beta);

// Add with an explicit path override.
server.addChild("/gamma", new AlphaChild());

// Remove by path or by class.
server.removeChild("/alpha");
server.removeChild(AlphaChild.class);

// Inspect what is currently mounted.
var keys = server.getChildResources().asMap().keySet();

API surface​

MethodDescription
addChild(Class<?>)Instantiate from the bean store (or via no-arg constructor) and mount at the child's @Rest(path).
addChild(Object)Mount an existing instance at the child's @Rest(path).
addChild(Object, boolean replace)Same as above but, when replace=true, atomically destroys any existing child mounted at the same path.
addChild(String path, Object)Mount an existing instance at an explicit path, ignoring any @Rest(path) on the resource.
addChild(String path, Object, boolean replace)Explicit-path variant with replace semantics.
removeChild(String path)Remove the child mounted at the given path. Returns the removed RestContext (or null).
removeChild(Class<?>)Remove the child whose resource is an instance of the given class. Returns the removed RestContext (or null).
getChildResources()Returns the underlying RestChildren for direct inspection.

Lifecycle and concurrency​

When a child is added, its @RestInit, @RestPostInit, and @RestPostInitChildFirst hooks run before addChild returns. When a child is removed, its @RestDestroy hooks run and the resource's Servlet.destroy() is invoked (if applicable) — including for any grandchildren mounted underneath.

Mutations are atomic: route matching either sees the full pre-add state or the full post-add state, never a partially-mounted child. Adding a second child at the same path without replace=true throws IllegalStateException to prevent silent route shadowing.

Lazy Materialization​

By default, all children declared via @Rest(children=...) are constructed eagerly at parent startup. For resource trees with many rarely-touched children (admin panels, diagnostic endpoints, heavyweight integration bridges), this can add significant startup cost. The 10.0 lazy-children feature defers construction until first access.

Enabling lazy children​

@Rest(
path="/root",
lazyChildren="true",
children={
HeavyAdminResource.class,
DiagnosticsResource.class
}
)
public class RootResources extends BasicRestServletGroup {}

Programmatic equivalent via AbstractRestBuilder:

MyRest.builder().lazyChildInit(true).build();

Behavior​

AspectDescription
URL matchingRouting entries are registered immediately; URLs are reachable from the first request.
ConstructionThe full RestContext for each lazy child — including bean-store setup, lifecycle hooks, and memoizers — is built on the first inbound request to that child's URL prefix.
ConcurrencyConcurrent first-requests to the same lazy child are serialized via double-checked locking. Only one thread runs construction; others block until it completes.
Never-invoked childrenA lazy child that receives zero requests is never constructed. Its @RestDestroy / shutdown hooks are skipped at parent destruction time.

Use cases​

  • Large microservice trees: dozens of rarely-touched admin, debug, or compatibility endpoints that don't need to be materialized unless an operator actually navigates to them.
  • Expensive integration children: a child that initializes a database connection pool or external API client only pays that cost on first use.

Caveats​

The first request to a lazy child pays the full construction cost, which can be significant for heavyweight children. If predictable first-request latency is required, do not opt in — keep the default eager behavior.

Host-side seeding with @Child (10.0.0)​

Children are deliberately isolated from the host's resolution chain (see Mixin-vs-child divergence) — a child's serializers, parsers, guards, call logger, etc. are resolved against the child's own RestContext only, never walked through the parent. @Rest(childrenDefs=@Child(...)) doesn't change that isolation; it gives the host an explicit, opt-in way to seed a curated set of settings onto a specific child's otherwise-isolated context, at the point the host declares it:

@Rest(
childrenDefs = @Child(type = AdminResource.class, callLogger = StructuredJsonLogger.class, guards = AdminBearerGuard.class)
)
public class ApiResource extends BasicRestServlet {
@RestGet("/items") public List<Item> items() { ... }
}

AdminResource's endpoints now log through StructuredJsonLogger and require AdminBearerGuard — as declared by the host — without editing AdminResource itself and without those settings leaking into ApiResource's own /items resolution chain.

childrenDefs is additive to and coexists with children: bare-class entries in children= and rich @Child entries in childrenDefs= are discovered together (bare entries first, then childrenDefs entries). A @Child(type=X.class) with no seed members is the exact equivalent of a bare children=X.class entry; if both name the same class, the rich entry wins (the bare entry is upgraded in place) — the same equivalence rule @Mixin establishes for mixins/mixinDefs.

The unifying principle: seeds never remove or override the child's own config​

Unlike a host @Mixin override — which wins over the mixin class's own declarations — a @Child seed can only add to or fill gaps in the child's own configuration. It never lets the child weaken or discard something the host imposed, and it never masks something the child explicitly declared for itself. Every seed member falls into exactly one of two buckets:

BucketMembersBehavior
Additive-securityguards, converters, roleGuard, rolesDeclaredThe host's contribution is added alongside whatever the child declares — the child can never remove or weaken it.
Child-wins scalarscallLogger, partSerializer, partParser, debug, defaultCharset, maxInputThe seed is a fallback/default. If the child's own @Rest chain explicitly declares the same setting, the child's value wins; the seed applies only when the child is silent.

Additive-security, list-shaped (guards, converters) — PREPEND. The host-seeded entries run first (outermost), then the child's own declared entries. A seeded guard degrades to a plain "set" when the child declares no guards of its own, and the child's own guards/converters are never dropped:

@Rest(path = "/admin", guards = AdminOwnGuard.class)  // the child's own guard
public class AdminResource {
@RestGet(path = "/threads") public String threads() { ... }
}

@Rest(childrenDefs = @Child(type = AdminResource.class, guards = HostGateGuard.class))
public class ApiResource extends BasicRestServlet { ... }

A request to /admin/threads must satisfy both HostGateGuard (seeded, runs first) and AdminOwnGuard (the child's own, runs second) — neither is optional.

Additive-security, role-based (roleGuard/rolesDeclared) — AND-STACK. A host-seeded roleGuard/rolesDeclared and the child's own explicit value both apply as independent, ANDed role checks, rather than one overriding the other. If the host seeds roleGuard="admin" and the child declares its own roleGuard="on-call", a request must satisfy both roles.

Child-wins scalars — fallback only. If the child leaves partSerializer (or any other child-wins member) undeclared, the host's seeded value takes effect. If the child declares its own @Rest(partSerializer=...), the child's value wins outright and the seed is ignored for that property — same "does this property have an explicit value anywhere more-derived?" resolution the framework already uses everywhere else.

Not yet supported: defaultAccept / defaultContentType​

defaultAccept and defaultContentType are not @Child seed members. They're resolved through the framework's shared default-request-headers mechanism (HttpHeaderList.setDefault(...)), which keeps the first value set while walking the annotation chain least-derived-first — independently of @Child, this already resolves in favor of the least-derived declaration in any class hierarchy. Seeding these two through @Child would therefore make the host's seed win over the child's own explicit value, the opposite of the child-wins contract every other scalar in this bucket honors. Rather than ship that contract violation, they're deferred until HttpHeaderList's first-wins resolution is revisited.

noInherit interaction​

@Child itself has no noInherit() member — there's no inherited chain to cut in an isolated context. But a child's own @Rest(noInherit="<property>") does cut the corresponding host seed too, for any seedable property, additive-security or child-wins-scalar alike:

@Rest(path = "/admin", noInherit = "guards")  // opts out of the host's seeded guard entirely
public class AdminResource {
@RestGet(path = "/threads") public String threads() { ... }
}

@Rest(childrenDefs = @Child(type = AdminResource.class, guards = HostGateGuard.class))
public class ApiResource extends BasicRestServlet { ... }

With noInherit = "guards" on AdminResource, HostGateGuard is not applied at all — the child stays fully in control, exactly as if the host had never seeded it. This falls out of the same generic noInherit cutoff the framework already applies uniformly to every @Rest property; there's no special-casing for seeded vs. non-seeded values.

Lazy children​

A @Child seed survives deferred construction under @Rest(lazyChildren="true") — a lazily materialized child receives its seed on first request exactly as an eagerly-built child would.

See also​