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: a per-directory ResourceManager cache, verbosity handling (the verbosity processor option,
info/debug), and a cleanup() that closes all open resources. It always reports SourceVersion.latest().
It also carries an optional source directory, which neither the processors of this module nor the Tentackle
Maven plugins use.
AnalyzeProcessor and the @Analyze meta-annotation¶
AnalyzeProcessor supports all annotations
(@SupportedAnnotationTypes("*")) and claims those it found a handler for.
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 a limit on the nesting level of meta-annotations (100) and on the number of handlers (1000).
For every match the processor loads the named handler class, instantiates it, injects itself, and calls
processAnnotation(...). A new handler instance is created for each annotation type and round.
The handler classes — and the annotation classes a handler needs to load, such as @RemoteMethod — are loaded via
the processor's processing classloader. The Maven plugin sets it to a classloader that also searches the
project's classpath; without one, the processor falls back to its own classloader.
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 slash-separated relative resource names (creating
intermediate directories as needed, using the encoding configured by Settings). It remembers the names of all
resources it handed out, even after close(). 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; honors value, meta, method) |
the servicing class appended to META-INF/services/<serviced class> (or META-INF/meta-services/… if meta = true) |
MappedServiceAnalyzeHandler |
@MappedService (as a meta-annotation) |
a line <annotated class> = <key> in META-INF/mapped-services/<service>, the key being taken from the annotated annotation's element named by method |
TableNameAnalyzeHandler |
@TableName |
a line <class> = "<table>" in META-INF/mapped-services/org.tentackle.session.TableName; needs its own handler to apply mapSchema and prefix (see Table Names) |
BundleAnalyzeHandler |
@Bundle and similar annotations (e.g. @GuiProviderService, @TableConfigurationProviderService) |
a line <bundle> [= <class>] in META-INF/bundles/<annotation>; the bundle name defaults to the class name and is overridden by value, noBundle = true suppresses the line (for i18n) |
FxControllerBundleAnalyzeHandler |
@FxControllerService |
like BundleAnalyzeHandler (its subclass), but the bundle name comes from resources, and RESOURCES_NONE means no bundle |
InterceptionAnalyzeHandler |
annotations annotated with org.tentackle.reflect.Interception |
the interceptor annotation appended to META-INF/services/org.tentackle.apt.<Type>InterceptorAnnotationProcessor for the interception type (ALL / PUBLIC / HIDDEN), which registers it with the matching annotation processor of tentackle-core |
RemoteMethodAnalyzeHandler |
@RemoteMethod |
an info file <class>/<method>.remote (RemoteMethodInfo) describing the method, later consumed by the RemoteMethod wurblet; the annotation's value overrides the file name |
RecordDTOAnalyzeHandler |
@RecordDTO |
an info file <record>/record.info (RecordDTOInfo) describing a record DTO, used for DTO generation |
The descriptors go to the services directory, the info files to the analyze directory. All handlers that write
descriptors except the bundle handlers also append a line to <class>/service.log in the analyze directory — a
trail of what was registered for which class, useful when debugging a missing service.
Dependency discipline. Handlers reference Tentackle layers above
tentackle-commononly by string (e.g.,InterceptionAnalyzeHandlernamesorg.tentackle.reflect.Interceptionand its APT classes as literals, andTableNameAnalyzeHandlernever refers toTableName) 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 and its generic part, superclass, parameters, modifiers, varargs) into versioned files ending in.remote. It also classifies the return type and each parameter: whether it is aSession, aDomainContext, a PDO or low-level db object, a collection of those, or a cursor. TheRemoteMethodwurblet reads these files to generate the TRIP remote-delegate code.RecordDTOInfo(withRecordDTOInfoParameter) — captures a record DTO's components into arecord.infofile used for DTO generation. See The DTO Wurblet and@RecordDTOfor how the wurblet consumes it.
Each info type defines its own INFO_FILE_VERSION, writes itself through a PrintWriter 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.
The format is line- and whitespace-separated, but type strings are not whitespace-free — javac renders a wildcard
bound as ? extends Foo and a type annotation as @Bar Foo. Types are therefore escaped on the way out and
unescaped on the way in (AnnotationProcessingHelper.escapeToken(): whitespace becomes \s, a backslash \\).
Since a java type never contains a backslash, unescaping is a no-op for info files written before the escaping was
introduced, and a stale target/analyze from an older build still reads correctly.
Code Generation Helpers (codegen)¶
The org.tentackle.buildsupport.codegen sub-package is a thin
wrapper over FreeMarker used by the build-time code generators of the
SQL,
wizard and
jlink Maven plugins:
| 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: escaping info-file tokens, reading/checkingModifiers, joining object arrays with their plaintoString()(unlikeStringHelper.objectArrayToString, which formats for logging), and a recursiveisTypeInstanceOf(...)that walksTypeMirrorsuper-types (with optional generic-argument matching).ClassNames.properties— the fully-qualified class namesRemoteMethodInfoclassifies types against (Collection,Session,AbstractDbObject,ScrollableResource,DomainContext,PersistentDomainObject), referenced by string to avoid hard dependencies. Being a resource bundle, it can be overridden by derived frameworks to point at their own base classes. Since a stale name would silently never match,AnalyzeClassNamesTestintentackle-test-pdoverifies that the classes still exist.
Module Dependencies¶
tentackle-common— for the@Analyze,@Service,@ServiceName,@MappedService,@RemoteMethodannotations,Constants(theMETA-INFpaths),Settings(the encoding),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.