Class FinalContractValidator

java.lang.Object
com.ultikits.ultitools.context.FinalContractValidator

@Internal public final class FinalContractValidator extends Object
Enforces the 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
    Modifier and Type
    Field
    Description
    private static final Logger
     
  • Constructor Summary

    Constructors
    Modifier
    Constructor
    Description
    private
     
  • Method Summary

    Modifier and Type
    Method
    Description
    private static void
    enqueueSupertypes(Class<?> from, Deque<Class<?>> queue)
    Adds from's direct superclass and directly implemented/extended interfaces to queue, so a caller doing a breadth-first walk reaches a @Final method declared on an interface of a superclass, not only one reachable through the superclass chain alone.
    private static Method
    Walks the hierarchy looking for a @Final method that method transitively overrides, following both superclasses and implemented interfaces.
    static List<String>
    validate(Class<?> clazz)
    Validates that the given class does not violate any @Final contract.

    Methods inherited from class java.lang.Object

    clone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait
  • Field Details

    • LOGGER

      private static final Logger LOGGER
  • Constructor Details

    • FinalContractValidator

      private FinalContractValidator()
  • Method Details

    • validate

      public static List<String> validate(Class<?> clazz)
      Validates that the given class does not violate any @Final contract.
      Parameters:
      clazz - the class to validate
      Returns:
      one description per violation, empty if the class is compliant
    • findOverriddenSealedMethod

      private static Method findOverriddenSealedMethod(Class<?> clazz, Method method)
      Walks the hierarchy looking for a @Final method that method transitively 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 in clazz that merely shares a signature with an ancestor's @Final method 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 @Final forbids. 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 @Final method to public, which is a real override and precisely what @Final exists to forbid.

      ReflectionUtil.overrides(Method, Method) is the single implementation of that rule, shared with ReflectionUtil.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 method itself, 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 between clazz and a package-private @Final ancestor is exactly the shape that requires this: comparing method directly against the @Final declaration 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 @Final declaration.

    • enqueueSupertypes

      private static void enqueueSupertypes(Class<?> from, Deque<Class<?>> queue)
      Adds from's direct superclass and directly implemented/extended interfaces to queue, so a caller doing a breadth-first walk reaches a @Final method declared on an interface of a superclass, not only one reachable through the superclass chain alone.