Module: io.github.microsphere-projects:microsphere-lang-model · Source line: release branch, 0.3.20-SNAPSHOT
(tag 0.3.20 published; root pom.xml:54), groupId at root pom.xml:12.
Packages analysed: io.microsphere.lang.model.element (1 type) + io.microsphere.lang.model.util (15 types) =
16 main-source types, covered by 17 test classes.
Companion user-facing page: User Guide — Language Model · Chinese version: 中文开发者指南.
All file:line references are relative to microsphere-lang-model/src/main/java/io/microsphere/ (main) or
microsphere-lang-model/src/test/java/io/microsphere/ (test), read from the release worktree, then shortened to
lang/model/util/TypeUtils.java:60-88. Cross-module references keep the module prefix, e.g.
microsphere-java-core/src/main/java/io/microsphere/reflect/TypeUtils.java:1543.
io.microsphere.lang.model is the compile-time counterpart of the runtime io.microsphere.reflect layer. That is
not a metaphor — the method surfaces are deliberately name-mirrored, and the module even imports from reflect
directly:
| Concept | Compile-time (javax.lang.model) |
Runtime (java.lang.reflect) |
Evidence |
|---|---|---|---|
| Same-type test | lang/model/util/TypeUtils.isSameType(TypeMirror, CharSequence) |
no direct twin — reflect has getTypeName(Type) |
lang/model/util/TypeUtils.java:261-266, microsphere-java-core/src/main/java/io/microsphere/reflect/TypeUtils.java:1543 |
| Type name extraction | lang/model/util/TypeUtils.getTypeName(TypeMirror) |
reflect/TypeUtils.getTypeName(Type) / getTypeNames(Type...) |
lang/model/util/TypeUtils.java:2457, …reflect/TypeUtils.java:1543,1568 |
| Type-name reuse across layers | ElementUtils.matchParameterTypes imports io.microsphere.reflect.TypeUtils.getTypeNames |
— | lang/model/util/ElementUtils.java:38, :656-659 |
| Method lookup | lang/model/util/MethodUtils.findMethod(TypeElement, String, Type...) |
reflect/MethodUtils.findMethod(Class, String, Class...) |
lang/model/util/MethodUtils.java:635,664, …reflect/MethodUtils.java:593,629 |
| Hierarchical method enumeration | getAllDeclaredMethods(TypeElement) |
getAllDeclaredMethods(Class) |
lang/model/util/MethodUtils.java:136,161, …reflect/MethodUtils.java:354 |
| Field lookup | lang/model/util/FieldUtils.findField(Element, String) |
reflect/FieldUtils.findField(Class, String) |
lang/model/util/FieldUtils.java:267,294, …reflect/FieldUtils.java:109,152,184 |
| Constructor lookup | lang/model/util/ConstructorUtils.findConstructor(TypeElement, Type...) |
reflect/ConstructorUtils.findConstructor(Class, Class...) |
lang/model/util/ConstructorUtils.java:151,179, …reflect/ConstructorUtils.java:264 |
| Generic-argument resolution | absent (the model gives it to you for free via DeclaredType#getTypeArguments) |
reflect/TypeUtils.resolveTypeArguments(Class) |
…reflect/TypeUtils.java:1198 |
Two edges make the layering concrete:
lang-model→microsphere-java-core(compile scope, the only one).pom.xml:24-28. The imports actually used areio.microsphere.util.*(Utils,ArrayUtils,StringUtils,ClassLoaderUtils,Assert,CharSequenceComparator),io.microsphere.collection.*(CollectionUtils,ListUtils,MapUtils),io.microsphere.lang.function.*(Predicates,Streams),io.microsphere.reflect.*(MethodUtils,TypeUtils),io.microsphere.text.FormatUtils,io.microsphere.logging.*,io.microsphere.json.JSONUtils,io.microsphere.constants.SymbolConstantsandio.microsphere.annotation.*— seelang/model/util/TypeUtils.java:20-61,lang/model/util/JSONAnnotationValueVisitor.java:31-40.annotation-processor→lang-model+core. This module has no dependency onannotation-processorand must not gain one; the direction is one-way.microsphere-jdk-toolsandmicrosphere-annotation-testsit below the test boundary (pom.xml:44-49,:52-57) and are never reachable fromsrc/main/java.
Boundary it owns: everything that must hold while javac is running, before classes exist. The moment a real
Class is on the processor's classpath, ClassUtils.loadClass (lang/model/util/ClassUtils.java:78) crosses over
into io.microsphere.util.ClassLoaderUtils.resolveClass and hands control to the runtime layer.
graph TD
subgraph JDK["javax.* (JDK, java.compiler module)"]
JM["javax.lang.model<br/>Element · TypeMirror · DeclaredType<br/>AnnotationMirror · AnnotationValue"]
AP["javax.annotation.processing<br/>ProcessingEnvironment · Elements · Types · Messager"]
end
LM["«module» microsphere-lang-model<br/>io.microsphere.lang.model.element (1)<br/>io.microsphere.lang.model.util (15)"]
CORE["«module» microsphere-java-core<br/>io.microsphere.reflect · util · collection<br/>json · text · logging · constants · annotation"]
AP2["«module» microsphere-annotation-processor"]
LM -- "compile-only dependency<br/>pom.xml:24-28" --> CORE
LM -- "wraps, never replaces" --> JM
LM -- "requires a live env" --> AP
AP2 -- "builds on" --> LM
AP2 -- "also depends on" --> CORE
LM -. "-> test scope only<br/>pom.xml:44-49" .-> JT["«module» microsphere-jdk-tools"]
LM -. "-> test scope only<br/>pom.xml:52-57" .-> AT["«module» microsphere-annotation-test<br/>AbstractAnnotationProcessingTest"]
CORE -. "runtime twin<br/>io.microsphere.reflect" .- LM
| # | Type | Kind | Wraps (javax.lang.model type) |
Key static / public members |
|---|---|---|---|---|
| 1 | StringAnnotationValue |
concrete class, @Immutable |
implements AnnotationValue |
ctor :37-39, getValue() :41-44, accept() → visitString :46-49 — lang/model/element/StringAnnotationValue.java |
| 2 | AnnotationUtils |
interface of static methods (extends Utils) |
AnnotatedConstruct, AnnotationMirror, AnnotationValue, DeclaredType, TypeMirror |
4 constants :106,111,117,119; getAnnotation :141-175; findAnnotation :509-612; findMetaAnnotation :640-692; getElementValues :1373-1529; getAttributesMap :1190-1329; getAttribute :1695-1782; getElementTypes :1841-1875 |
| 3 | ClassUtils |
interface of static methods |
TypeMirror → java.lang.Class |
getClassName(TypeMirror) :57, loadClass(TypeMirror) :78, loadClass(String) :98 — lang/model/util/ClassUtils.java |
| 4 | ConstructorUtils |
interface of static methods |
ExecutableElement (kind CONSTRUCTOR) |
getDeclaredConstructors :87,123, findConstructor :151,179, findDeclaredConstructors :220,239, filterDeclaredConstructors :278 |
| 5 | ElementUtils |
interface of static methods (extends Utils) |
Element, ElementKind, ExecutableElement, VariableElement |
8 kind predicates :87-296; toElementKind :389-400; 3 matchesElementType :430-502; matchesElementKind :526-528; hasModifiers :578-589; filterElements :622-633; matchParameterType(s/Names) :656-733 |
| 6 | ExecutableElementComparator |
final-ish class, singleton, implements Comparator<ExecutableElement> |
ExecutableElement, VariableElement |
INSTANCE :60, private ctor :62, compare :66-92 |
| 7 | FieldUtils |
interface of static methods |
VariableElement (kind FIELD) |
getDeclaredField(s) :107-187, getAllDeclaredFields :213-239, findField :267-294, find/findAllDeclaredFields :330-445, filterDeclaredFields :488, isEnumMemberField :524, isNonStaticField :554, isField :580,618, get(All)NonStaticFields :644-734, equalsFieldName :760 |
| 8 | JSONAnnotationValueVisitor |
concrete class, extends SimpleAnnotationValueVisitor6<StringBuilder, ExecutableElement> |
AnnotationValue |
ctor :76-79, 11 scalar/TypeMirror visits :81-145, visitAnnotation :147-165, visitArray :167-184, visitUnknown :186-189, doAppend :191-195 |
| 9 | JSONElementVisitor |
abstract class, extends ElementKindVisitor6<Boolean, StringBuilder> |
Element hierarchy |
ctor w/ FALSE fallback :59-61; 5 final visits :63-109; visitMembers :111-119; supports + 5 supportsXxx :130-197; doVisitPackage/doVisitTypeParameter :206-219 |
| 10 | AnnotatedElementJSONElementVisitor |
abstract class, extends JSONElementVisitor |
Element + TypeElement of a named annotation |
ctor :102-112; fields processingEnv/elements/annotationClassName/annotationType/annotationTypeElement/elementTypes :90-100; getAnnotationClassName() :119-122; supports(Element) :124-126 |
| 11 | LoggerUtils |
interface of static methods (extends Utils) |
nothing — SLF4J-style façade over io.microsphere.logging |
LOGGER :32, trace/debug/info/warn/error :34-52 |
| 12 | MemberUtils |
interface of static methods |
Element (any member kind) |
getDeclaredMembers :98,122,196,222, getAllDeclaredMembers :146,170, findDeclaredMembers :250,278,388,419, findAllDeclaredMembers :310,349 |
| 13 | MessagerUtils |
interface of static methods (extends Utils) |
Messager, ProcessingEnvironment, Diagnostic.Kind |
5×2 printX pairs :63-246; printMessage :268-309 |
| 14 | MethodUtils |
interface of static methods |
ExecutableElement (methods) |
getDeclaredMethods :86,111, getAllDeclaredMethods :136,161, find/findAllDeclaredMethods :196-451, findPublicNonStaticMethods :338,373, filterDeclaredMethods :487, isMethod :522, findMethod ×6 :575-728, getOverrideMethod :764, getMethodName :841, getReturnTypeName :866, getMethodParameterType(Mirrors/Names) :896,938, matches* :967-1127 |
| 15 | ResolvableAnnotationValueVisitor |
concrete class, extends SimpleAnnotationValueVisitor6<Object, ExecutableElement> |
AnnotationValue → runtime objects |
2 static final reflective hooks :76-78; 3 ctors :88-99; 13 visit methods :101-200; package-private getComponentType :189-195 |
| 16 | TypeUtils |
interface of static methods (extends Utils) |
TypeMirror, DeclaredType, TypeElement, Element, ProcessingEnvironment |
SIMPLE_TYPE_NAMES :77-82; 2 traversal functions :88,94; ~100 statics in 5 families :120-2505; typeElementFinder :2500-2505 |
Two structural observations:
- 11 of 16 are
interfaces containing onlystaticmethods (theUtilsconvention) —AnnotationUtils,ClassUtils,ConstructorUtils,ElementUtils,FieldUtils,LoggerUtils,MemberUtils,MessagerUtils,MethodUtils,TypeUtils, plusExecutableElementComparatorwhich is the odd one out as a realclass. Interfaces were chosen over final classes purely to hoststaticmethods on JDK 8 without a utility-class idiom. - Exactly two types are abstract and one is a singleton — the visitor pair
JSONElementVisitor/AnnotatedElementJSONElementVisitorandExecutableElementComparator.INSTANCE. Everything else is directly usable.
The comparison primitive is one method; everything else delegates to it:
static boolean isSameType(TypeMirror type, CharSequence typeName) { // TypeUtils.java:261
if (type == null && typeName == null) {
return true;
}
return Objects.equals(valueOf(type), valueOf(typeName)); // :265
}lang/model/util/TypeUtils.java:261-266. The three sibling overloads just normalise their arguments first:
(Element, Type) :177-179, (Element, CharSequence) :203-205, (TypeMirror, Type) :235-237.
Consequences worth stating plainly:
javax.lang.model.util.Typesis never consulted. The module obtains noTypeshandle in this path, soisSameTypeworks with zero infrastructure — but it is a textual test, not the compiler's equivalence relation.TypeMirror#toString()is specified as the canonical name, so declared types and primitives behave correctly; type-variable usages, capture wildcards and anonymous types do not have a canonical-name guarantee.null/nullistruewhile every sibling predicate returnsfalseonnull. CompareisArrayType:289-291,isEnumType:340-343,isSimpleType:149-151— allx != null && …. TheisSameTyperule is an "equal nulls" special case, which makesisSameType(null, null)a tautology rather than a check.isSimpleTypeis a membership test against a hard-coded name list built fromio.microsphere.util.ClassUtils.SIMPLE_TYPES(:77-82), so it is source-of-truth-literal, not model-derived.
The conversion lattice is acyclic and total:
flowchart TD
E["Element"] -->|asType()| TM["TypeMirror"]
E -->|isTypeElement :555| TE["TypeElement"]
TM -->|ofDeclaredType :763-765<br/>isDeclaredType ? cast : null| DT["DeclaredType"]
DT -->|asElement()| TE
TE -->|asType()| DT
E -->|ofTypeElement :652-654| TE
TM -->|ofTypeElement :680-683| TE
TE -->|getTypeParameters| TPE["TypeParameterElement"]
TE -->|getEnclosedElements| M["members (Element)"]
M -->|ElementFilter.fieldsIn / methodsIn| V["VariableElement · ExecutableElement"]
P["ProcessingEnvironment"] -. "getTypeElement(name) :2381<br/>getDeclaredType :2401-2433" .-> TE
Three properties of the of* family matter for callers:
- They return
null, never throw.ofTypeElement(Element):652-654,ofTypeElement(TypeMirror):680-683,ofTypeElement(DeclaredType):706-711,ofDeclaredType(Element):737-739,ofDeclaredType(TypeMirror):763-765. APackageElementor a primitiveTypeMirroryieldsnulland callers must handle it —AnnotationUtilsdoes, which is whyfindAllAnnotations(Element, …)has an explicittypeElement == nullbranch (lang/model/util/AnnotationUtils.java:868-874). - Traversal is function-injected.
TYPE_ELEMENT_GET_SUPERCLASS(:88) andTYPE_ELEMENT_GET_INTERFACES(:94-97) arepublic staticfields passed intoio.microsphere.util.TypeFinderbytypeElementFinder(:2500-2505). The same BFS enginecoreuses at runtime is therefore reused here with a different pair of edge functions — this is the single most load-bearing design decision in the module. getTypeNameis the only reflective call.:2456-2478— it asks the element forgetTypeParameters()and, if non-empty, reflectively invokes"getTypeArguments"on theTypeMirror(:2466,invokeMethod(true, …)) becauseTypeMirrorhas no such method, then renders<+List<TypeMirror>.toString()+>(:2470-2472). It is the one place where aTypeMirrorimplementation's private surface is relied upon.
sequenceDiagram
autonumber
participant Proc as "AbstractProcessor subclass"
participant Env as "ProcessingEnvironment"
participant EU as "lang/model TypeUtils"
participant MU as "lang/model MemberUtils"
participant JE as "javax Elements / Types"
participant EL as "TypeElement / Element"
Proc->>Env: getProcessingEnvironment()
Proc->>EU: getTypeElement(env, "com.acme.Foo") TypeUtils.java:2381
EU->>JE: elements.getTypeElement(name) (null env -> null, silent)
JE-->>EU: TypeElement
EU-->>Proc: TypeElement (null if not yet entered this round)
Proc->>MU: getAllDeclaredMembers(typeElement.asType()) MemberUtils.java:170
MU->>JE: Elements.getAllMembers(element)
JE-->>MU: List<? extends Element>
MU-->>Proc: members (emptyList on null, never throws)
Proc->>EL: enclosed.accept(JSONElementVisitor, StringBuilder)
Note over EL: JSONElementVisitor.visitMembers<br/>lang/model/util/JSONElementVisitor.java:111-119<br/>re-enters accept() per member
EL-->>Proc: Boolean appended? + JSON text in the builder
Both visitors extend the JDK-6 SimpleAnnotationValueVisitor6, so AnnotationValue.accept(v, p) — not the
visitors' own code — chooses the branch. p is the ExecutableElement attribute method throughout.
flowchart TD
START["AnnotationValue.accept(visitor, p)"] --> SW{"value kind"}
SW -- boolean/byte/char/double/float/int/long/short/string --> SC["JSONAnnotationValueVisitor :81-133<br/>append(builder, getAttributeName(p), value)"]
SW -- "Class (TypeMirror)" --> T["visitType :135-139<br/>JSON: type name<br/>RESOLVABLE :146-149: ClassUtils.loadClass or toString"]
SW -- "enum constant (VariableElement)" --> EC["visitEnumConstant :141-145<br/>JSON: getSimpleName()<br/>RESOLVABLE :151-155: loadClass + Enum.valueOf"]
SW -- "nested AnnotationMirror" --> AN["visitAnnotation"]
SW -- "List<? extends AnnotationValue>" --> AR["visitArray"]
SW -- "anything newer than JDK 6" --> UN["visitUnknown<br/>JSON :186-189 → returns builder unchanged<br/>RESOLVABLE :197-200 → returns the AnnotationValue itself"]
AN --> ANJ["JSONAnnotationValueVisitor :147-165<br/>getElementValues(a) → child visitor on a NEW StringBuilder<br/>{ e1, e2, … } separated by COMMA_CHAR<br/>then doAppend(p, fragment) :191-195"]
AN --> ANR["ResolvableAnnotationValueVisitor :157-174<br/>LinkedHashMap<String,Object><br/>if nestedAnnotationsAsMap \|\| hook missing → return the Map<br/>else AnnotationParser.annotationForMap(proxy)"]
AR --> ARJ["JSONAnnotationValueVisitor :167-184<br/>child visitor with p = null<br/>[ v1, v2, … ] separated by COMMA_CHAR<br/>nameless because appendName skips blanks<br/>JSONUtils.java:500-508"]
AR --> ARR["ResolvableAnnotationValueVisitor :176-195<br/>getComponentType(p) = (ArrayType) p.getReturnType()<br/>reflect.Array.set per element"]
SC --> OUTJ["StringBuilder (the one passed to the ctor)"]
ARJ --> OUTJ
ANJ --> OUTJ
The grammar implemented by JSONAnnotationValueVisitor is:
| Input | Output | Producing lines |
|---|---|---|
scalar (boolean…String) |
"name":literal |
:81-133 |
Class value |
"name":"fully.qualified.Name" |
:135-139 |
| enum constant | "name":"CONSTANT" (simple name only — the owning type is not emitted) |
:141-145 |
| nested annotation | "name":{ "a":x,"b":y } |
:147-165 |
| array | "name":[e1,e2,…], each ei rendered namelessly |
:167-184 |
| unknown | nothing appended | :186-189 |
Notes that are easy to miss:
- The visitor emits fragments, not documents. It never writes a top-level
{/}; the caller owns the enclosing object, and the leading{seen invisitAnnotationis written into a child builder (:151-152). The only delimiter characters come fromSymbolConstants(:31-35). - The attribute name is resolved from
p, not from the value.getAttributeNameisMethodUtils.getMethodName(AnnotationUtils.java:1015-1017→MethodUtils.java:841-843), which returnsnullfor anullmethod;JSONUtils.appendNamethen skips writing the name (…json/JSONUtils.java:500-508). Array elements exploit this:visitArraypassesnulldeliberately (:176). visitAnnotationrecurses throughgetElementValues(a)with the default-on overload (:149→AnnotationUtils.java:1464-1466), so defaults appear in JSON — see the assertion"since":{"module":"","value":"1.0.0"}where"module":""is a default (lang/model/util/JSONAnnotationValueVisitorTest.java:113-116).ResolvableAnnotationValueVisitorreaches intosun.reflect.annotation.AnnotationParser— resolved once intostatic finalfields (:76-78) withio.microsphere.reflect.MethodUtils.findMethod, then invoked withinvokeStaticMethod(true, …)(:173). Thenull-tolerant branch (:168) means a JDK that hides that class simply returns theMapinstead of an annotation proxy — degradation, not failure. ButgetComponentType(:189-195) performs an unchecked(ArrayType)cast and dereferencesp, so visiting an array withp == nullthrowsNullPointerException/ClassCastException.visitArrayin the resolvable visitor needspto know the component type, whereasStringAnnotationValue(lang/model/element/StringAnnotationValue.java:46-49) hard-wiresvisitStringand therefore never touchesp— making it safe to feed to a visitor withnullparameter, as the test does (lang/model/element/StringAnnotationValueTest.java:49-52).
JSONElementVisitor is the other half of the JSON story and it is not a JSON emitter: it appends nothing itself. It
is a filtering traversal framework — a gate (supportsXxx), a recursion (visitMembers :111-119), and a
"did anything get written?" boolean protocol (TRUE means "content appended"). visitType :87-101 is the only
composite: it runs super.visitType for the type header, then recurses into getEnclosedElements() and ORs the
result. AnnotatedElementJSONElementVisitor :124-126 supplies the concrete filter — "only elements whose kind is in
the annotation's @Target", computed once in the constructor from getElementTypes(annotationType)
(:111, AnnotationUtils.java:1871-1875).
compare (lang/model/util/ExecutableElementComparator.java:66-92) is a four-stage cascade:
| Stage | Code | Line | Notes |
|---|---|---|---|
| 0 | if (e1.equals(e2)) return 0; |
:68-70 |
Uses Element#equals, which in javac is identity of the symbol table entry. It dereferences e1 unconditionally. |
| 1 | CharSequenceComparator.INSTANCE.compare(e1.getSimpleName(), e2.getSimpleName()) |
:73 |
From io.microsphere.util.CharSequenceComparator (:19,23) |
| 2 | ps1.size() - ps2.size() |
:77-80 |
Arithmetic difference, not Integer.compare — overflow is unreachable in practice |
| 3 | per-index asType().toString() comparison, first non-zero wins |
:82-89 |
Compares the TypeMirror text, i.e. exactly the isSameType notion of §3.1 |
Ordering properties:
- Not consistent with
equals. Two structurally identical methods from differentTypeElements compare0(lang/model/util/ExecutableElementComparatorTest.java:59-60provesAutoCloseable#close()reached via the test type and viaAutoCloseable.classcompare0), whilee1.equals(e2)isfalse. ATreeSetwith this comparator will therefore deduplicate overloads across the hierarchy — which is what metadata generation wants, but is not what aHashSetwould do. - Total only within one simple-name bucket. Stage 1 short-circuits, so ordering never reaches stage 3 for methods of different names.
- Name-first, not signature-first. Generated output is grouped by method name.
No Prioritized interaction exists. Prioritized is referenced nowhere in the module's main sources (grep across
all 16 types finds no occurrence; the singleton comparator is Comparator, not Prioritized). The warning belongs to
callers who compose the two: io.microsphere.lang.Prioritized's constants are inverted relative to their names —
MAX_PRIORITY = Integer.MIN_VALUE (:81), MIN_PRIORITY = Integer.MAX_VALUE (:86), NORMAL_PRIORITY = 0 (:91) —
so if you sort a member list with Prioritized.COMPARATOR after ExecutableElementComparator, "highest priority"
means the numerically smallest value. Do not chain the two without stating which one wins.
printMessage(Messager, Kind, String, Object...) (lang/model/util/MessagerUtils.java:292-309) is the single funnel.
The five ProcessingEnvironment overloads (:63-65, :127-129, :174-176, :221-223, :268-270) are pure adapters
that call processingEnv.getMessager(), and the four level-specific Messager overloads are pure adapters that fix the
Kind (:104, :151, :198, :245).
Diagnostic.Kind |
Messager.printMessage |
LoggerUtils call |
Switch branch |
|---|---|---|---|
NOTE |
yes (:294) |
info(pattern, args) |
:303-304 |
WARNING |
yes | warn(pattern, args) |
:299-301 |
MANDATORY_WARNING |
yes | warn(pattern, args) — same branch as WARNING |
:299-301 |
ERROR |
yes | error(pattern, args) |
:296-297 |
OTHER (and any future Kind) |
yes | debug(pattern, args) |
:306-307 (default) |
Three design facts follow:
- The message is formatted twice along two paths.
messager.printMessage(kind, format(pattern, args))sends the substituted text (:293-294), whileLoggerUtilsreceives the rawpatternplusargs(:297–:307), so the logging backend applies its own placeholder rules. A pattern valid for{}(microsphere-java-core/src/main/java/io/microsphere/text/FormatUtils.java:17,:38-39) is not necessarily valid for the backend, and vice versa. The javadoc ofMessagerUtilsclaims{0}/ "String#formatsyntax" (lang/model/util/MessagerUtils.java:56-61,:78-82) — this is a documentation defect; the tests use"Hello, {}!"(lang/model/util/MessagerUtilsTest.java:55-56). MANDATORY_WARNINGis deliberately collapsed ontowarn. The distinction survives only in theMessagerchannel; the log channel cannot recover it.LoggerUtils.LOGGERis astaticinterface constant named"microsphere-lang-model"(lang/model/util/LoggerUtils.java:32) — the module, not the caller, is the logger name. Processor authors cannot route this module's diagnostics to a per-processor category, andLogbackis a test-scope dependency (pom.xml:60-64) precisely so the module itself never binds a backend at compile scope.
There is no null handling: :294 dereferences messager, :295 dereferences kind, and the five
ProcessingEnvironment overloads dereference processingEnv before the call. The assertDoesNotThrow wrapper in the
tests (MessagerUtilsTest.java:55-80) is only evidence about the happy path.
| Pattern | Proving class(es) | Evidence |
|---|---|---|
| Visitor (double dispatch) | JSONAnnotationValueVisitor, ResolvableAnnotationValueVisitor, StringAnnotationValue |
The dispatch lives in AnnotationValue.accept, and StringAnnotationValue proves it: its whole behaviour is return v.visitString(this.value, p) (lang/model/element/StringAnnotationValue.java:46-49). The two visitors override per-kind hooks only (lang/model/util/JSONAnnotationValueVisitor.java:81-189, lang/model/util/ResolvableAnnotationValueVisitor.java:101-200) |
| Visitor (acceptor-driven traversal) | JSONElementVisitor, AnnotatedElementJSONElementVisitor |
Element.accept(this, jsonBuilder) inside visitMembers (lang/model/util/JSONElementVisitor.java:111-119); the subclass narrows which elements are visited, not how (lang/model/util/AnnotatedElementJSONElementVisitor.java:124-126) |
| Template Method + Strategy hybrid | JSONElementVisitor |
final gates visitPackage/visitVariable/visitExecutable/visitType/visitTypeParameter (:63-109) with protected extension points supports* (:130-197) and doVisit* (:206-219) |
| Wrapper / Adapter | StringAnnotationValue |
Implements the JDK AnnotationValue interface over a plain String so caller-produced values enter JDK-shaped APIs (lang/model/element/StringAnnotationValue.java:33-49), marked @Immutable (:32) |
| Facade / Utils | TypeUtils, ElementUtils, AnnotationUtils, MemberUtils, FieldUtils, MethodUtils, ConstructorUtils, ClassUtils, MessagerUtils, LoggerUtils |
All are interfaces of static methods extending io.microsphere.util.Utils (e.g. lang/model/util/TypeUtils.java:69, lang/model/util/ElementUtils.java:58, lang/model/util/AnnotationUtils.java:101), collapsing Elements + Types + Element + AnnotationMirror + AnnotationValue + Messager into name-based calls |
| Comparator / Strategy | ExecutableElementComparator |
implements Comparator<ExecutableElement> (:55), private ctor (:62), shared immutable INSTANCE (:60); the strategy is fixed (no key extractor is injectable) |
| Singleton (canonical instance) | ExecutableElementComparator, AnnotationUtils.DEFAULT_ANNOTATION_VALUE_VISITOR |
:60 / lang/model/util/AnnotationUtils.java:111 — the shared visitor is stateless and therefore safe to reuse (lang/model/util/ResolvableAnnotationValueVisitor.java:84-99 fields are final) |
| Null Object / silent degradation | every Utils interface |
emptyList()/emptyMap()/null/false instead of throwing — lang/model/util/ElementUtils.java:622-633, lang/model/util/AnnotationUtils.java:1509-1512, lang/model/util/TypeUtils.java:652-654 |
| Registry (read-only, injected) | TypeUtils.TYPE_ELEMENT_GET_SUPERCLASS / TYPE_ELEMENT_GET_INTERFACES |
Function constants (:88, :94-97) handed to io.microsphere.util.TypeFinder (:2500-2505) so the shared BFS engine can walk a non-reflective graph |
| Reflective bridge across a layer boundary | ClassUtils, ResolvableAnnotationValueVisitor |
TypeMirror → Class (lang/model/util/ClassUtils.java:78-98) and AnnotationMirror → annotation proxy via AnnotationParser.annotationForMap (lang/model/util/ResolvableAnnotationValueVisitor.java:76-78,173) |
Not present: no Builder, no Decorator (the child JSONAnnotationValueVisitor instances in visitAnnotation/visitArray
are fresh collaborators over a different buffer, :153/:173 — not wrappers around an existing one), and no SPI or
META-INF/services file in src/main/resources (there is no src/main/resources directory at all).
The policy holds for 14 of 16 types. Complete list of the sites that can throw, verified line by line:
| # | Site | Trigger | Exception |
|---|---|---|---|
| 1 | lang/model/util/AnnotatedElementJSONElementVisitor.java:103 |
processingEnv == null |
Assert.assertNotNull → unchecked |
| 2 | lang/model/util/AnnotatedElementJSONElementVisitor.java:104 |
annotationClassName == null |
same |
| 3 | lang/model/util/AnnotatedElementJSONElementVisitor.java:110 |
the annotation is not resolvable via Elements.getTypeElement |
same, message "The annotation can't be found by name : …" |
| 4 | lang/model/util/MessagerUtils.java:64 |
printNote(null, …) |
NullPointerException |
| 5 | lang/model/util/MessagerUtils.java:128 |
printWarning(null, …) |
NullPointerException |
| 6 | lang/model/util/MessagerUtils.java:175 |
printMandatoryWarning(null, …) |
NullPointerException |
| 7 | lang/model/util/MessagerUtils.java:222 |
printError(null, …) |
NullPointerException |
| 8 | lang/model/util/MessagerUtils.java:269 |
printMessage(null, kind, …) |
NullPointerException |
| 9 | lang/model/util/MessagerUtils.java:294 |
messager == null |
NullPointerException |
| 10 | lang/model/util/MessagerUtils.java:295 |
kind == null |
NullPointerException (switch on null) |
| 11 | lang/model/util/ExecutableElementComparator.java:68 |
e1 == null |
NullPointerException |
| 12 | lang/model/util/ExecutableElementComparator.java:73 |
e1 != null, e2 == null |
NullPointerException |
| 13 | lang/model/util/TypeUtils.java:1360-1365 |
a null element in typeFilters |
declared throws IllegalArgumentException (:1356) |
| 14 | lang/model/util/TypeUtils.java:2034-2068 |
same, DeclaredType family |
documented @throws (:2034, :2064) |
| 15 | lang/model/util/TypeUtils.java:2498-2505 |
typeElementFinder with invalid parameters / failing assertions |
documented IllegalArgumentException |
| 16 | lang/model/util/JSONElementVisitor.java:72-77,79-85,87-101 |
null element with the default supports (returns true, :130-132) |
NullPointerException inside ElementKindVisitor6 |
| 17 | lang/model/util/ResolvableAnnotationValueVisitor.java:189-195 |
visitArray with p == null or a non-array return type |
NullPointerException / ClassCastException |
| 18 | lang/model/util/AnnotationUtils.java:1781 |
an Entry whose value is null |
NullPointerException (annotationValue.accept(...) unchecked) |
- Javadoc vs. code, placeholder syntax.
MessagerUtils.java:56,:60,:78,:82,:100,:124,:147,:161,:183,:206,:241,:265,:289advertiseString#format/{0}; the implementation is{}-based (FormatUtils.java:17). - Javadoc vs. code, non-existent hook.
AnnotatedElementJSONElementVisitor.java:67-74instructs subclasses to@Override protected boolean doVisitType(TypeElement, StringBuilder). No such method is declared anywhere:JSONElementVisitordefines onlydoVisitPackageanddoVisitTypeParameter(:206-219), andvisitTypeisfinal(:88), so the documented override cannot compile. - Duplicated javadoc blocks.
AnnotationUtils.java:1816-1839andMessagerUtils.java:85-102each carry the same javadoc twice — the second block wins, and the first is dead text that drifts. - Typo in a javadoc source line.
MessagerUtils.java:4reads "additional inpatternion". getAnnotationjavadoc overstates inheritance.AnnotationUtils.java:132-134says "an element that may inherit annotations from its parent" —getAnnotationreadsgetAnnotationMirrors()only (:797); inheritance isgetAllAnnotations/findAllAnnotations(:315-320,:863-875).matchesAttributeValuejavadoc vs.one == another.AnnotationUtils.java:1072-1073implyfalsefornulls;:1082-1083returnstruewhen both are the same reference (including bothnull).
ExecutableElementComparator.INSTANCE— stateless (:55-63); the only fields are none. Safe to share, and theprivateconstructor makes that the only option.ResolvableAnnotationValueVisitorstatic fields —ANNOTATION_PARSER_CLASSandannotationForMapMethod(:76-78) are initialised in the interface/class initialiser under the JVM's<clinit>lock, arestatic final, and are never reassigned; the resolvedMethodmay benullon JDKs that hidesun.reflect.annotation, and the:168branch handles that. Instance fieldsclassValuesAsString/nestedAnnotationsAsMaparefinal(:84-86). But the shared singletonAnnotationUtils.DEFAULT_ANNOTATION_VALUE_VISITOR(AnnotationUtils.java:111) is the one object everygetAttributecall funnels through — it is safe only becauseResolvableAnnotationValueVisitorkeeps no per-call state.visitAnnotationbuilds a freshLinkedHashMap(:159-166) andvisitArraya fresh array (:178-186), so no field is written after construction. Adding any mutable field to that class would immediately makeAnnotationUtils.getAttributeracy.JSONAnnotationValueVisitorholds afinal StringBuilder(:74-79) and mutates it.StringBuilderis not thread-safe, so one instance must stay confined to one thread. Its nested children (:153,:173) are per-call and confined — fine.LoggerUtils.LOGGER(:32) — an interface constant, one per JVM, delegated toio.microsphere.logging.- No
Element/TypeElementcache exists anywhere in the module, so there is no stale-symbol hazard. Every traversal (getAllTypeElementsTypeUtils.java:1169,getAllDeclaredMembersMemberUtils.java:146,170) recomputes, which is correct underjavac(symbol tables mutate between rounds) but is the module's main performance characteristic. Note also thatjavax.lang.modelelements are documented as not safe to hold across rounds — so this module's no-cache stance is the right one, and a future cache would be a defect, not an optimisation.
- New annotation attribute rendering → subclass
JSONAnnotationValueVisitorand override only the kinds you care about; the recursion usesnew JSONAnnotationValueVisitor(...)literally (:153,:173), so overridden methods are not used for nested values. If you need recursion to honour your subclass, you must reimplementvisitAnnotation/visitArray. This is a real limitation, not a style preference. - New element-to-JSON pipeline → extend
AnnotatedElementJSONElementVisitor, pass a resolvable annotation class name (:102-112), overridesupports(Element)or the basevisitTypeAsClass/visitVariableAsFieldfamily fromElementKindVisitor6— seelang/model/util/JSONElementVisitorTest.java:61-136for the complete working override set. Do not try to overridevisitType/visitVariable/visitExecutable; they arefinal. - A new member-kind utility → follow the existing convention:
interface XUtils extends Utils, pairs ofTypeElement/TypeMirroroverloads,getDeclaredX→getAllDeclaredX→findX→filterX,@Nonnull @Immutableon every list-returning method,emptyList()onnull. - A new ordering →
ExecutableElementComparatorcannot be parameterised (private ctor,final-less but with a hard-coded cascade). Add a second comparator class rather than mutatingINSTANCE, because downstream code already shares the singleton.
17 test classes under src/test/java/, all but StringAnnotationValueTest extending UtilTest
(lang/model/util/UtilTest.java:48), which itself extends
io.microsphere.test.annotation.processing.AbstractAnnotationProcessingTest from microsphere-annotation-test
(:20) — that is how a live ProcessingEnvironment, Elements and Messager appear inside Surefire, and why
-Dsurefire.useSystemClassLoader=false is required in CI (.github/workflows/maven-build.yml:43). UtilTest fixes
the test subject to io.microsphere.test.service.TestServiceImpl (:52), and AnnotationUtilsTest.java:127-134
depends on that fixture having exactly four directly-present annotations and five hierarchically
(AnnotationUtilsTest.java:175-181) — assertions a downstream fork will break immediately.
The suite's structure tells you what the module considers its contract: every Utils test has an
…OnNull sibling (ElementUtilsTest.java:106-110, :121-123, :132-136, :146-148;
AnnotationUtilsTest.java:112-124, :136-140, :183-187), which is how the silent-degradation policy is enforced.
The visible gaps:
- No test exercises
MessagerUtilswith anullMessagerorProcessingEnvironment, so §8.1 rows 4–10 are undocumented by the build. - No test exercises
ExecutableElementComparatorwith anullelement (rows 11–12). JSONElementVisitorTest's*OnUnsupportedcases (:146-149,:174-178,:194-198,:228-233,:247-251) passnullelements, but only becausesupportsis overridden to returnfalse(:63-67) — the default-supports path (row 16) is untested and would throw.ResolvableAnnotationValueVisitor'sclassValuesAsString/nestedAnnotationsAsMapconstructors (:92-99) and itssun.reflectfallback branch (:168) have no coverage of the fallback itself.ExecutableElementComparatorTest.java:67-70overridesequals(Object)on the test class with a puresuper.equals(object)call — dead code, harmless, but it shows the comparator'sequalspath being probed from the wrong side.AnnotatedElementJSONElementVisitorTest.java:37-49tests onlygetAnnotationClassName()andsupports(); no visitor run and therefore no JSON output is asserted anywhere in that test.
- microsphere-lang-model — User Guide — coordinates, API surface and pitfalls for users
- microsphere-annotation-processor — Developer Guide — the module that consumes these visitors during a processing round
- microsphere-annotation-test — Developer Guide — the in-process
javacharness that compiles against this module - microsphere-jdk-tools — Developer Guide — where the
ProcessingEnvironmentthis layer reads is created - JSON — Developer Guide — the
io.microsphere.jsonlayer thatJSONElementVisitorwrites into - Reflection, types and beans — Developer Guide — the runtime-reflection counterpart of the same
AnnotatedElementmodel - Developer Guide index · 中文版本:语言模型开发者指南