Tentackle Build Support — The Annotation Analyze Phase¶
Overview and Motivation¶
tentackle-build-support is a small, build-time-only module that provides the infrastructure for
Tentackle's analyze phase: a Java annotation-processing pass that runs over an application's sources
before they are wurbeled and compiled. During this pass it inspects the Tentackle annotations in the code
and turns them into two kinds of artifacts:
- service descriptors under
META-INF(so theServiceFindercan discover implementations in both modular (JPMS) and classpath runtimes), and - analyze info files under a
targetsubdirectory that later build steps — most importantly the wurblets — read to generate code (for example, the remote-delegate code for@RemoteMethod).
The module is never shipped inside, nor used by, the running application; its package-info states plainly
"Build support only. Not used at runtime." It is packaged with an Automatic-Module-Name of
org.tentackle.buildsupport and depends only on tentackle-common
and FreeMarker.
Why a build-time analyze phase?¶
There are two common ways to wire up services and metadata: scan the classpath and read annotations reflectively at runtime, or extract the same information at build time via annotation processing. Tentackle takes the second route (see Service and Configuration API). The payoff is that the annotated classes never have to be loaded just to read their annotations — the work is done once, during the build — which keeps startup fast and works identically on the classpath and the module path. This module is the engine that performs that extraction.
Driven by the Maven plugin¶
tentackle-build-support only supplies the processor and handlers; it does not bind itself to any build
lifecycle. The tentackle-maven-plugin
registers the AnalyzeProcessor and runs it from its
analyze (and test-analyze) goals during generate-sources, supplying the two output directories
(analyzeDir and servicesDir) and the source/classpath to scan.
The Processing Pipeline¶
@Analyze-annotated annotation in the sources
│
▼
AnalyzeProcessor (javac AbstractProcessor, @SupportedAnnotationTypes("*"))
│ recursively follows meta-annotations to find the @Analyze marker
▼
AnalyzeHandler (instantiated by classname from @Analyze("..."))
│ writes via ResourceManager to one of two locations
├──────────────► servicesDir : META-INF/services/… (+ optional RESOURCE-INDEX.LIST)
└──────────────► analyzeDir : <class>/service.log, *.remote, record.info, …
(later read by wurblets / packaged into META-INF)
AbstractTentackleProcessor — the processor base¶
AbstractTentackleProcessor extends
javax.annotation.processing.AbstractProcessor and adds the cross-cutting concerns shared by Tentackle's
processors: the source directory, a per-directory ResourceManager cache,
verbosity handling (the verbosity processor option, info/debug), and a cleanup() that flushes all open
resources. It always reports SourceVersion.latest().
AnalyzeProcessor and the @Analyze meta-annotation¶
AnalyzeProcessor claims all annotations
(@SupportedAnnotationTypes("*")). For each annotation type it encounters it walks the annotation's own
annotations — recursively — looking for the
@Analyze meta-annotation
(org.tentackle.common.Analyze). @Analyze carries the class name of a handler as a String (not a
Class), precisely so that the application never gains a compile- or run-time dependency on the build-support
handlers:
Because the scan is recursive, an annotation only has to be marked @Analyze once; any annotation
itself meta-annotated with it (directly or through several levels) is routed to the same handler. Loops are
guarded by depth and handler-count limits. For every match the processor loads the named handler class,
instantiates it, injects itself, and calls processAnnotation(...).
AnalyzeHandler / AbstractAnalyzeHandler¶
AnalyzeHandler is the contract a handler implements:
processAnnotation(annotationType, roundEnv) plus the processor accessors.
AbstractAnalyzeHandler adds two conveniences:
getDirectory(baseDir, className) (maps a dotted class name to a directory tree under the analyze dir) and a
print(kind, msg, element) that emits compiler diagnostics tagged with the handler name. A handler "can do
whatever is appropriate" — its purpose is not restricted to services.
ResourceManager — writing the output¶
ResourceManager owns a base directory and hands out
lazily-created, cached PrintWriters/Readers for relative resource names (creating intermediate directories as
needed, using the project's configured encoding). Each processor keeps one ResourceManager per output
directory, so handlers simply ask for getResourceManager(serviceDir) or getResourceManager(analyzeDir) and
append to the right file. The Maven layer later collects the service-directory resource names to build the
optional RESOURCE-INDEX.LIST, and writes an error.log marker (AnalyzeProcessor.COMPILE_ERROR_LOG) when the
analyze compilation reported errors, signaling downstream phases that the analyze info may be incomplete.
The Handlers¶
Each handler reacts to one family of annotations and emits the appropriate descriptor and/or analyze file.
| Handler | Reacts to | Produces |
|---|---|---|
ServiceAnalyzeHandler |
@Service / @ServiceName (direct or as meta-annotations; honours value, meta, method) |
a META-INF/services/<servicedClass> descriptor in the services dir, plus a service.log under the servicing class in the analyze dir |
MappedServiceAnalyzeHandler |
@MappedService |
a mapped-service descriptor (interface → implementation) for the ServiceFinder |
TableNameAnalyzeHandler |
@TableName |
a mapped-service-style descriptor; needs its own handler to deal with mapSchema and the table prefix |
BundleAnalyzeHandler |
@Bundle |
the resource-bundle name associated with a class (for i18n) |
FxControllerBundleAnalyzeHandler |
@FxControllerService |
the bundle name for an FX controller, resolving an overridden resources value (or none); subclass of BundleAnalyzeHandler |
InterceptionAnalyzeHandler |
org.tentackle.reflect.Interception (on annotations) |
registers the matching interceptor annotation processor for the interception type (ALL / PUBLIC / HIDDEN) |
RemoteMethodAnalyzeHandler |
@RemoteMethod |
a .remote info file (RemoteMethodInfo) describing the method, later consumed by the RemoteMethod wurblet |
RecordDTOAnalyzeHandler |
@RecordDTO |
a record.info file (RecordDTOInfo) describing a record DTO, used for DTO generation |
Dependency discipline. Handlers reference other Tentackle layers only by string (e.g.,
InterceptionAnalyzeHandlernamesorg.tentackle.reflect.Interceptionand its APT classes as literals, andAnnotationProcessingHelperre-implements a couple ofStringHelpermethods locally) so that this module stays free of dependencies ontentackle-coreand the rest of the stack.
Info Files — the bridge to wurblets¶
Some annotations carry too much structure to be captured in a one-line descriptor, so their handlers serialize a small record into an info file that a wurblet deserializes in a later build step:
RemoteMethodInfo(withRemoteMethodInfoParameter) — gathers the signature of a@RemoteMethod(return type, parameters, modifiers, …) into versioned files ending in.remote. TheRemoteMethodwurblet reads these to generate the TRIP remote-delegate code.RecordDTOInfo(withRecordDTOInfoParameter) — captures a record DTO's components into arecord.infofile used for DTO generation.
Each info type defines its own INFO_FILE_VERSION, writes itself through a, and reads itself back
from a LineNumberReader, so the write side (this module, during analyze) and the read side (the wurblets) agree
on a stable on-disk format.
Code Generation Helpers (codegen)¶
The org.tentackle.buildsupport.codegen sub-package is a thin
wrapper over FreeMarker used by build-time code generators (for instance the
generation of TRIP remote interfaces/implementations):
| Class | Role |
|---|---|
AbstractGenerator |
Holds the template directory and builds a configured FreeMarker Configuration (UTF-8, rethrowing exception handler). |
TemplateModel |
A Map<String,Object> template model that camel-cases keys when adding Properties/maps and null-safely stringifies values. |
GeneratedFile |
Renders a named template with a model to an output file. |
GeneratedString |
Renders a named template with a model to a string. |
Supporting pieces¶
AnnotationProcessingHelper— static utilities for processors: reading/checkingModifiers, joining object arrays, and a recursiveisTypeInstanceOf(...)that walksTypeMirrorsuper-types (with optional generic-argument matching).ClassNames.properties— a handful of fully-qualified class-name constants (e.g.Session,DomainContext,PersistentDomainObject,ScrollableResource) referenced by string from the build-time code, again to avoid hard dependencies.
Module Dependencies¶
tentackle-common— for the@Analyze,@Service,@ServiceName,@MappedService,@RemoteMethodannotations,Constants,Settings,StringHelperand theServiceFindercontracts the descriptors feed.- FreeMarker — the template engine behind the
codegenpackage.
This is a build-time only module: it runs on the annotation-processor / plugin classpath and is never part of the deployed application.
Related Documentation¶
- Service and Configuration API — the
@Analyze/@Servicemechanism and theServiceFinderthat consumes the generated descriptors. - Tentackle Maven Plugin — the
analyze/test-analyzegoals that drive theAnalyzeProcessorand configure its output directories. - Tentackle Persistence Wurblets
and Tentackle Wurblets — the code generators that
read the analyze info files (e.g., the
RemoteMethodwurblet). - TRIP — the remoting protocol whose delegates are generated
from
@RemoteMethodanalyze info.