2

I have developed a framework and corresponding API which includes a runtime-visible annotation. The API also supplies some helper methods intended for client use on objects whose classes have that annotation. Understandably, the helpers are tightly-coupled with the annotation, but it is important that their internals be encapsulated from the client. The helper methods are currently provided via static inner class within the annotation type...

@Target(TYPE)
@Retention(RUNTIME)
public @interface MyAnnotation {
   // ... annotation elements, e.g. `int xyz();` ...

   public static final class Introspection {
       public static Foo helper(Object mightHaveMyAnnotation) {
           /* ... uses MyAnnotation.xyz() if annotation is present ... */
      }
   }
}

... but the helpers could just as easily exist in some other top-level utility class. Either way provides the necessary amount of encapsulation from client code, but both incur extra costs to maintain a completely separate type, prevent them from instantiation since all useful methods are static, etc.

When Java 8 introduced static methods on Java interface types (see JLS 9.4), the feature was touted as providing the ability to...

... organize helper methods in your libraries; you can keep static methods specific to an interface in the same interface rather than in a separate class.

— from Java Tutorials Interface Default Methods

This has been used within the JDK libraries to provide implementations such as List.of(...), Set.of(...), etc., whereas previously such methods were relegated to a separate utility class such as java.util.Collections. By locating the utility methods within their related interfaces, it improves their discoverability and removes arguably unnecessary helper class types from the API domain.

Since I the current JVM bytecode representation for annotation types is very closely related to normal interfaces, I wondered if the annotations would also support static methods. When I moved the helpers into the annotation type, such as:

@Target(TYPE)
@Retention(RUNTIME)
public @interface MyAnnotation {
   // ... annotation elements ...

   public static Foo helper(Object mightHaveMyAnnotation) { /* ... */ }
}

... I was slightly surprised that javac complained with the following compile-time errors:

OpenJDK Runtime Environment 18.3 (build 10+46)

  • modifier static not allowed here
  • elements in annotation type declarations cannot declare formal parameters
  • interface abstract methods cannot have body

Clearly, the Java language does not currently allow this. It may be that there are good design reasons to disallow it or, as previously presumed for static interface methods, "there [was] no compelling reason to do so; consistency isn't sufficiently compelling to change the status quo".

It is specifically not the goal of this question to ask "why doesn't it work?" or "should the language support it?", as to avoid opinion-based answers.

The JVM is a powerful technology and in many ways more flexible than what's allowed by the Java Language. At the same time, the Java Language continues to evolve, and today's answer may be obsolete tomorrow. With the understanding that such power must be used with great care...

Is it technically possible to encapsulate static behavior directly within an annotation type, and how?

William Price
  • 4,033
  • 1
  • 35
  • 54
  • This seems like a non-issue, and the only justification for this question is through comparison of interfaces & annotations (similar to "Difference between interfaces & abstract classes"). Although the JVM may support it, your answer (before your edits) is biased & doesn't explain *why* it should be supported, rather that it's possible. This seems like an XY problem, as it's not state *why* objects require a name via annotation, not allowing us to suggest proper work-arounds. – Vince Jun 12 '18 at 07:39

1 Answers1

4

It is technically feasible to accomplish this within the JVM and interoperate with standard Java code, but it comes with important caveats:

  1. Java-compatible source code, per the JLS, cannot define static methods in annotation types.
  2. Java source code appears able to use such methods if they exist, including at compile-time and at runtime via reflection.
  3. The subject annotation may need to be placed in a separate compilation unit so that its binary class is available to IDEs and javac when processing code.
  4. This has been verified on OpenJDK 10 HotSpot, but there is the potential that the observed behavior may depend on internal details subject to change in later releases.
  5. Carefully consider the impacts to long-term maintenance and compatibility before deciding to adopt this approach.

A proof-of-concept was successful, using a mechanism that directly manipulates JVM bytecode.

The mechanism is simple enough. Use an alternate language, or a bytecode manipulation tool (i.e. ASM), which will emit a JVM *.class file that both (1) matches the function and appearance of a legal Java (language) annotation, and (2) also contains the desired method implementation with the static access modifier set. This class file can be compiled separately and packaged into a JAR or placed onto the classpath directly, at which point it becomes usable by your other normal Java code.

The following steps will create the working bytecode corresponding to the following not-quite-legal Java annotation type, which defines a trivial strlen static function for simplicity in the POC:

@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation {

    String value();

    // not legal in Java, through at least JDK 10:
    public static int strlen(java.lang.String str) {
        return str.length(); // boring!
    }
}

First, set up the annotation class with a "normal" value() parameter as a String without a default value:

import static org.objectweb.asm.Opcodes.*;
import java.util.*;
import org.objectweb.asm.*;
import org.objectweb.asm.tree.*;

/* ... */

final String fqcn = "com.example.MyAnnotation";
final String methodName = "strlen";
final String methodDesc = "(Ljava/lang/String;)I"; // int function(String)

ClassNode cn = new ClassNode(ASM6);
cn.version = V1_8; // Java 8
cn.access = ACC_SYNTHETIC | ACC_PUBLIC | ACC_INTERFACE | ACC_ABSTRACT | ACC_ANNOTATION;
cn.name = fqcn.replace(".", "/");
cn.superName = "java/lang/Object";
cn.interfaces = Arrays.asList("java/lang/annotation/Annotation");

// String value();
cn.methods.add(
    new MethodNode(
        ASM6, ACC_PUBLIC | ACC_ABSTRACT, "value", "()Ljava.lang.String;", null, null));

Optionally annotate the annotation with @Retention(RUNTIME), if appropriate:

AnnotationNode runtimeRetention = new AnnotationNode(ASM6, "Ljava/lang/annotation/Retention;");
runtimeRetention.values = Arrays.asList(
    "value", // parameter name; related value follows immediately next:
    new String[] { "Ljava/lang/annotation/RetentionPolicy;", "RUNTIME" } // enum type & value
);
cn.visibleAnnotations = Arrays.asList(runtimeRetention);

Next, add the desired static method:

MethodNode method = new MethodNode(ASM6, 0, methodName, methodDesc, null, null);
method.access = ACC_PUBLIC | ACC_STATIC;
method.annotationDefault = Integer.MIN_VALUE; // see notes
AbstractInsnNode invokeStringLength =
    new MethodInsnNode(INVOKEVIRTUAL, "java/lang/String", "length", "()I", false);
method.instructions.add(new IntInsnNode(ALOAD, 0)); // push String method arg
method.instructions.add(invokeStringLength);        // invoke .length()
method.instructions.add(new InsnNode(IRETURN));     // return an int value
method.maxLocals = 1;
method.maxStack = 1;
cn.methods.add(method);

Finally, output the JVM bytecode for this annotation to a *.class file on the classpath, or load it directly in memory using a custom ClassLoader (not shown):

ClassWriter cw = new ClassWriter(0);
cn.accept(cw);
byte[] bytecode = cw.toByteArray();

Notes:

  1. This requires generating bytecode version 52 (Java 8) or later, and will only run under JVMs that support that version.
  2. Annotations have java.lang.Object as their super type, and they implement the java.lang.annotation.Annotation interface.
  3. The two null parameters to the MethodNode constructors are for generics and declared exceptions, neither are used in this example.
  4. OpenJDK 10's HotSpot required setting MethodNode.annotationDefault to a non-null value (of the appropriate type) on the static method, even though setting/overriding strlen would never be an option when applying the annotation to another element. This is a gray area for this approach being "legal". The HS bytecode verifier seemed to ignore the ACC_STATIC flag and assumes all defined methods are normal annotation elements.
William Price
  • 4,033
  • 1
  • 35
  • 54
  • You answered your own question, but the answer feels very opinion-based. You assume the language doesn't support this for unspecified reasons - it currently doesn't support it, but should/will in the future. – Vince Jun 12 '18 at 06:10
  • I'd question a few statements in your answer. "*While annotations share the same general abstraction as normal interfaces*" - This isn't true at all. Interfaces abstract behaviors, while runtime annotations seem to abstract state (an annotated member cannot specify behavior for users of that annotation to access - they can only specify values). "*there doesn't appear to be a technical reason why they cannot exist*" - Annotations are for meta data: specifying data about your program's data. Interfaces are for users to interact with objects while remaining unaware of their implementation. – Vince Jun 12 '18 at 06:25
  • 1
    Well now we're just arguing over different interpretations. The JLS itself groups annotations and interfaces _together_ and the **quoted** and cited sentence calls out that they're both "kinds of interfaces"... so they do share a common (read: general) abstraction _at some level_. Second, my demonstrated approach is a statement _of fact_ that they _can_ exist in an unmodified JVM at runtime, which backs up my "technical reason" statement. You're arguing about whether it's appropriate to _use_ it that way, or whether one _should_, which is fair criticism but neither my argument nor question. – William Price Jun 12 '18 at 06:31
  • Buttons on a TV & voice command are both types of interfaces. So is a car's steering wheel. And we view them on the same abstraction when talking about interfaces. But this doesn't mean they're all the same, and should support the same features. Just because two things can exist on the same level of abstraction doesn't mean they should support the same features when those abstractions are thought of in a more concrete sense. Are you sure you aren't a victim of the XY problem, conjuring a code smell? – Vince Jun 12 '18 at 06:37
  • (Continued) What I'm saying is that your answer is more of a justification as to why they should exist - something that should be mentioned in your question. It is not an answer as to why it does not exist (why it's not supported). Since you both asked & answered this question, it feels as if you're trying to promote beliefs, rather than give an answer. – Vince Jun 12 '18 at 06:39
  • Abstract classes VS interface is *exactly* what's going on here. And the guy who implemented default methods into the language already talked about this (how they were thinking of extension methods, and how people would complain about default methods how you are now). I don't believe this will be "superseded", as the philosophy wouldn't line up - behaviors involving runtime annotations *should* be handled by some external actor at runtime since it only provides state and not behavior, while compile time annotations should be handled by annotation processors. – Vince Jun 12 '18 at 06:50