Class AopEligibility

java.lang.Object
com.ultikits.ultitools.aop.AopEligibility

public final class AopEligibility extends Object
Decides whether a class can be proxied, and explains why not when it cannot.

Diagnosis, not policy. check(Class, Set) names every reason a class or one of its annotated methods cannot be proxied, with a remedy for each; AopProxyResolver decides what to do with them. Today it fails the load for FINAL_CLASS - nothing can subclass it, so no proxy exists at all - and warns for the rest, which are single methods the proxy cannot reach. That split follows Spring, which throws for a final class and ignores an annotation on a method it cannot advise. Ignoring is not silence: every one is named in a warning, because an annotation that quietly does nothing is what left @ExceptionCatch inert from 6.2.0 to 6.3.0. isProxyable(Method, Class) is the same rule set as a plain predicate, used to decide what actually goes into the proxy. See issue #309.

private and static methods are not a capability gap but a usage error. The JVM dispatches them with invokespecial and invokestatic, neither of which consults the subclass, so no inheritance-based AOP framework can intercept them.

Since:
6.3.0
Author:
wisdomme
  • Field Details

    • AOP_ANNOTATIONS

      private static final List<Class<? extends Annotation>> AOP_ANNOTATIONS
      Annotations that request interception.
    • DESCRIBE_CALLS

      private static final AtomicInteger DESCRIBE_CALLS
      Counts how many times a rule's remediation text has been built, process-wide.

      Exists only so isProxyable(Method, Class)'s own test can prove it never reaches AopEligibility.Rule.describe(Method, Class): a counter that stays at zero across a call is stronger evidence than reading the method body, because it also catches a future edit that accidentally reintroduces the cost. Nothing in production code reads this value.

    • RULES

      private static final AopEligibility.Rule[] RULES
      Cached once; AopEligibility.Rule.values() clones its backing array on every call.
  • Constructor Details

    • AopEligibility

      private AopEligibility()
  • Method Details

    • getAopAnnotations

      public static List<Class<? extends Annotation>> getAopAnnotations()
      The annotations that request interception.

      Exposed so that wiring code can prove every one of them is either served by an advisor or explicitly declared unavailable. Without that check, an annotation nobody wired up fails silently on beans that also carry an annotation that is wired. See issue #190.

      Returns:
      an unmodifiable view of the recognised AOP annotations
    • findAopAnnotatedMethods

      public static Set<Method> findAopAnnotatedMethods(Class<?> beanClass)
      Collects the methods that carry an AOP annotation on the method itself, walking the whole inheritance hierarchy.

      Class-level annotations are deliberately not represented here. Everything this method returns is fed to check(Class, Set), which turns an unproxyable entry into a load failure - correct for a method the author explicitly named, wrong for one a class-level annotation happened to cover. Class-level coverage is resolved by AopProxyResolver.collectInterceptedMethods instead. See issue #309.

      Scans the class itself - callers on the startup path that already hold a MethodScan should call findAopAnnotatedMethods(MethodScan) instead, so the hierarchy is not walked a second time (D-37).

      Parameters:
      beanClass - the class to scan
      Returns:
      the methods carrying a method-level AOP annotation, subclass overrides first
    • findAopAnnotatedMethods

      static Set<Method> findAopAnnotatedMethods(MethodScan scan)
      Same as findAopAnnotatedMethods(Class), but reads from a scan the caller already has instead of walking the hierarchy again.

      AopProxyResolver.resolve(Class) builds one MethodScan per resolve and hands it to this method, the intercepted-method collector, and the diagnostic pass, instead of each repeating the reflective hierarchy walk MethodScan already carries the result of (D-37).

      Parameters:
      scan - the scan to read from
      Returns:
      the methods carrying a method-level AOP annotation, subclass overrides first
    • check

      public static List<AopEligibility.Problem> check(Class<?> beanClass, Set<Method> annotatedMethods)
      Checks whether the given class can be proxied.
      Parameters:
      beanClass - the class to check
      annotatedMethods - the method-level annotated methods, from findAopAnnotatedMethods(Class); may be empty when interception was requested at class level only
      Returns:
      the blocking problems, empty if the class can be proxied
    • isProxyable

      public static boolean isProxyable(Method method, Class<?> beanClass)
      Whether an inheritance-based proxy generated for beanClass can intercept the given method - meaning it can both override it and reach the original through super.

      Walks the same AopEligibility.Rule enum as check(Class, Set), which is what replaces the old javadoc sentence asking a human to keep the two in sync: adding a sixth rule fails to compile until it supplies both violates() and describe(), so the two callers cannot drift apart again. They exist separately because they serve opposite audiences. A method-level annotation is an explicit request, so an unproxyable one becomes a startup failure. A class-level annotation is a bulk request the author never vetted method by method, so the class-level path filters with this predicate instead of failing the whole module.

      Only AopEligibility.Rule.violates(Method, Class) runs here - never AopEligibility.Rule.describe(Method, Class), which builds the remediation string and is reserved for check(java.lang.Class<?>, java.util.Set<java.lang.reflect.Method>)'s reporting path. This method runs once per method per bean on the startup path, so it must cost nothing to call even for a method that fails every rule. See issue #309 and issue #317.

      The bean class is a parameter rather than being derived from the method because two of the five rules are relative to it. A method is not intrinsically unproxyable; it is unproxyable from a particular subclass. See issue #309.

      Parameters:
      method - the method to test, may be null
      beanClass - the class the proxy will extend, may be null
      Returns:
      true if an inheritance-based proxy can intercept it
    • isInaccessible

      private static boolean isInaccessible(Method method, Class<?> beanClass)
      Whether the proxy could not even see the method: it is package-private and declared in a different package than the class the proxy extends.

      The generated proxy lives in the bean's package, so a package-private declaration from elsewhere is neither overridable nor reachable through super. The test delegates to ReflectionUtil rather than re-deriving it, because ReflectionUtil.overrides decides the same question when it collapses override slots. Two copies reading different sources would let the scan treat two declarations as one method while this treats them as different packages.

    • isShadowed

      private static boolean isShadowed(Method method, Class<?> beanClass)
      Whether a class between the bean and the method's declaring class already declares that exact name and parameter list, which makes the candidate unreachable through super.

      This is the erased half of a generic override. Given Base<T>.take(T) and Child extends Base<String> overriding take(String), the compiler emits a bridge Child.take(Object). ReflectionUtil.getAllMethods skips bridges and compares erased parameter types, so take(String) does not collapse take(Object) and both come back as separate methods - but only the first is super-invokable. Bridge and synthetic declarations are deliberately included in the scan here, because they are exactly what does the shadowing.

      Parameters:
      method - the method to test
      beanClass - the class the proxy will extend
      Returns:
      true if a nearer declaration makes it unreachable through super
    • describeInvocationCountForTesting

      static int describeInvocationCountForTesting()
      Test-only read of DESCRIBE_CALLS. Package-private and read-only: nothing outside this package, and nothing in production code at all, depends on its value.
      Returns:
      how many times a rule's remediation text has been built since the JVM started