Class FinalContractValidator
Final contract when the framework loads a module's classes.
@Final replaces the final keyword for classes that also need AOP, so the JVM no
longer enforces it. This validator restores the constraint at class-loading time. Because the
framework loads every module, the check is effective across module boundaries, which a
compile-time annotation processor could not achieve.
Known gaps. The check is not compile-time, so an IDE will not flag a violation. Its
coverage is exactly the set of classes ComponentScanner walks, and for a plugin module
that set comes from PluginManager#getPluginScanPackages's three-level fallback: a
module's declared @UltiToolsModule(scanBasePackages), else a @ComponentScan on
the plugin class, else the plugin class's own package. A module that declares neither annotation
is still checked - it falls back to its own package rather than dropping out of scanning
entirely. The framework's own classes are not checked in production: nothing ever invokes
ComponentScanner against com.ultikits.ultitools itself. ContextConfig
used to declare @ComponentScan("com.ultikits.ultitools") for exactly that purpose, but
the declaration was removed in 6.3.0 because the only thing that would have acted on it,
SimpleContainer.processConfigurationClass(Class), had no caller anywhere in
src/main - it is invoked only from tests (ContextConfigTest, ScanTest).
A plugin module's class is unchecked if it sits outside whichever package that module's own scan
actually reaches; a framework class is unchecked unconditionally today.
The method-level check is transitive: it accumulates the chain of declarations that actually
override one another - per ReflectionUtil.overrides(Method, Method), the single
implementation of JLS 8.4.8.1 this class shares with ReflectionUtil.getAllMethods(Class)
- rather than comparing clazz's own declaration directly against each ancestor. A
widening override interposed between clazz and a @Final ancestor no longer
defeats the check, and the walk also follows implemented interfaces, not only the superclass
chain, so a @Final default method or a @Final interface is enforced too.
- Since:
- 6.3.0
- Author:
- wisdomme
-
Field Summary
Fields -
Constructor Summary
Constructors -
Method Summary
Modifier and TypeMethodDescriptionprivate static voidenqueueSupertypes(Class<?> from, Deque<Class<?>> queue) Addsfrom's direct superclass and directly implemented/extended interfaces toqueue, so a caller doing a breadth-first walk reaches a@Finalmethod declared on an interface of a superclass, not only one reachable through the superclass chain alone.private static MethodfindOverriddenSealedMethod(Class<?> clazz, Method method) Walks the hierarchy looking for a@Finalmethod thatmethodtransitively overrides, following both superclasses and implemented interfaces.Validates that the given class does not violate any@Finalcontract.
-
Field Details
-
LOGGER
-
-
Constructor Details
-
FinalContractValidator
private FinalContractValidator()
-
-
Method Details
-
validate
Validates that the given class does not violate any@Finalcontract.- Parameters:
clazz- the class to validate- Returns:
- one description per violation, empty if the class is compliant
-
findOverriddenSealedMethod
Walks the hierarchy looking for a@Finalmethod thatmethodtransitively overrides, following both superclasses and implemented interfaces.Matching on name and parameter types alone is not enough: a
private,static, or cross-package package-private method inclazzthat merely shares a signature with an ancestor's@Finalmethod is not an override at all per JLS 8.4.8.1, and reporting it as one would reject a module that never actually did the thing@Finalforbids. Nor is it enough to compare a symmetric signature key: overriding is directional, so a key comparison misses the mirror-image case - a subclass in the same package widening an ancestor's package-private@Finalmethod topublic, which is a real override and precisely what@Finalexists to forbid.ReflectionUtil.overrides(Method, Method)is the single implementation of that rule, shared withReflectionUtil.getAllMethods(Class)'s own override collapsing, so the two checks agree by construction rather than by two people independently getting the same JLS rule right. See issue #190.The walk accumulates every declaration proven to be part of the override chain, starting from
methoditself, and tests a newly-found candidate against every member of that chain rather than only the most recently added one. A same-package widening override interposed betweenclazzand a package-private@Finalancestor is exactly the shape that requires this: comparingmethoddirectly against the@Finaldeclaration fails the package check, but comparing the widening override (already in the chain because it was found first, one level up) against it succeeds, because that override genuinely shares a package with the@Finaldeclaration. -
enqueueSupertypes
Addsfrom's direct superclass and directly implemented/extended interfaces toqueue, so a caller doing a breadth-first walk reaches a@Finalmethod declared on an interface of a superclass, not only one reachable through the superclass chain alone.
-