Core Java

Java 9 Annotations Example

In this article we will cover the topic of annotations by way of some theory and a brief example using Java 9. We will cover what they are, their properties, their purpose and different types. The example code will demonstrate the application of an annotation processor using custom annotations on an interface to automatically generate a stub implementation of said interface.

1. Introduction

Introduced in Java 5, annotations have been with us ever since, augmenting our source code with ancillary intent via syntactic metadata. Being metadata it is clear that it does not usually have a direct effect on the run-time base itself, but rather seeks to augment / inject information / data about our code.

Before annotations a typical alternative to describe code was to use some medium divorced of the code, usually xml. I say usually because this was not actually standardized and was left to the devices of the developers themselves. Obviously this could not continue for too long and annotations were born as a means to keep the descriptions / ancillary intent close to the code.

Annotations can be compiled and made available at run-time, or just compile time or in fact only be available at source level. Through these features, annotations can be used to augment the compilation process, be available at run-time for affecting program flow or simply serves as a means to document source code and be discarded at compile time.

An annotation takes the form of @<Identifier> where Identifier is the name of the annotation when it was declared.

2. Technologies used

The example code in this article was built and run using:

  • Java 9 (jdk-9+180)
  • Maven 3.3.9 (3.3.x will do fine)
  • Eclipse Oxygen (4.7.0)
  • Ubuntu 16.04 (Windows, Mac or Linux will do fine)

3. Setup

To view the code a simple text editor will suffice, however, to compile the artifacts Java 9 and maven 3.3.x needs to be installed. If you are wanting to setup Eclipse as well the I recommend you refer to a previous article (see section 3. Setup) in this Java 9 series which highlights how to setup all the technologies (save Ubuntu) used in the making of this article. To just compile the code, only Java 9 and maven needs to be installed and this can be verified by:

Java and Maven verification

jean-jay@jeanjay-SATELLITE-L750D:~$ mvn --version
Apache Maven 3.3.9
Maven home: /usr/share/maven
Java version: 9, vendor: Oracle Corporation
Java home: /home/jean-jay/runtimes/jdk-9
Default locale: en_ZA, platform encoding: UTF-8
OS name: "linux", version: "4.10.0-33-generic", arch: "amd64", family: "unix"
jean-jay@jeanjay-SATELLITE-L750D:~$ javac -version
javac 9
jean-jay@jeanjay-SATELLITE-L750D:~$ java -version
java version "9"
Java(TM) SE Runtime Environment (build 9+180)
Java HotSpot(TM) 64-Bit Server VM (build 9+180, mixed mode)
jean-jay@jeanjay-SATELLITE-L750D:~$ 

You should see something similar to the above.

  • lines 1 & 2: highlight maven command for checking version and the first line of output confirming version
  • lines 8 & 9: highlight that javac is set to Java 9
  • lines 10 & 11: highlight that java is set to Java 9

4. Application of Annotations

Annotations can be applied on the following code constructs:

  • Class declarations
  • Instance field declarations
  • Method declarations
  • Constructors
  • Local variables
  • Package declarations (typically applied on the package declaration of the package-info.java file)
  • Method parameters
  • Annotations
  • Types – anywhere a type is used in the code base (from Java 8)

The enum construct ElementType solidifies the application possibilities of annotations. Below are some example applications of annotations:

Example applications of Annotations at various code sites

@ClassAnnotation
public class Test {
	
	@InstanceFieldAnnotation
	private String value;
	
	@ConstructorAnnotation
	public Test() {}
	
	@MethodAnnotation	
	public void doSomething(@ParameterAnnotation final Object arg1) {
		
		@VariableAnnotation
		final String result = (@TypeAnnotation String) arg1;
	}
}

Prior to Java 8 the same annotation type was only permissible once per code construct, but since Java 8 we are able to “repeat” the same annotation type at a single code construct and this is now known as “repeating annotations”.

Example of Repeating Annotations

...
@MethodAnnotation
@MethodAnnotation	
public void doSomething(@ParameterAnnotation final Object arg1) {
...

5. Types of Annotations

Annotations can be broadly grouped into two main categories, namely:

  • Pre-defined Annotations: These include those that come bundled with the JDK. eg: @Override @SuppressWarning @Deprecated @FunctionalInterface @Retention @Repeatable etc
  • Custom Annotations: Our own annotation definitions.

5.1 Common Predefined Annotations

  • @Deprecated: element it marks is deprecated and should no longer be used.
  • @Override: indicates to the compiler that the method is overridden from a super class.
  • @SuppressWarnings: suppresses any warning generated from the compiler for code block / statement / expression in question.
  • @SafeVarargs: asserts that the code does not perform unsafe operations on its varargs parameter.
  • @FunctionalInterface : indicates that the type is to be used as a FunctionalInterface type.
  • @Retention: applied to other annotations and indicated the availability of the annotation (run-time or compile time or source).
  • @Documented: applied to other annotations and indicates that the annotation should be documented by the Javadoc tool.
  • @Target: applied to other annotations and indicates where the annotation can be placed. (class or method or field etc).
  • @Repeatable: applied to other annotations and indicates that the said annotation can be apllied to the same declaration spot more than once.

Most of the common pre-defined annotations are quite straightforward, save for the @Repeatable annotation which warrants a closer look. A closer inspection of the @Repeatable annotation reveals the following annotation element: Class<? extends Annotation> value();.

What the @Repeatable annotation requires as it’s sole element is a “container” annotation which will contain the repeating annotation.

An example follows of a repeating annotation:

Example showing Repeating Annotation declaration

...
@Documented
public @interface MyCustomAnnotations {
	MyCustomAnnotation [] value();
}
...
@Repeatable(MyCustomAnnotations.class)
@Documented
@Retention(CLASS)
@Target({ TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE, ANNOTATION_TYPE, PACKAGE, TYPE_PARAMETER, TYPE_USE })
public @interface MyCustomAnnotation {
        // Custom Annotation elements go here
}

5.2 Custom Annotations

Custom Annotations are created using the @ symbol followed immediately by an identifier name. eg: @LoggingAnnotation. Annotation elements must be one of the following types:

  • boolean, byte, short, int, long no wrappers allowed
  • String
  • enum
  • class
  • annotation type
  • array of any of the above

Example custom Annotation declaration

@Documented
@Retention(RUNTIME)
@Target({METHOD})
public @interface LoggingAnnotation {
	// Annotation element definitions go here
	String logFile() default "";
}

6. Example Code

The example code will demonstrate a custom annotation Stub which when is applied to an interface’s methods will, with the help of an annotation processor, generate a stub implementation of the interface at compile time.

NOTE: Not all permutations of method signatures are supported (varargs, generic types) and exception lists are not respected or even generated, but it is enough to demonstrate the simple concept of using an annotation to generate a stub implementation of an interface.

As an aside a little about annotation processing:

6.1. What is Annotation Processing

Annotation processing is a mechanism of the compilation process in Java, where annotations are processed at compile time. Important to note here is that it is not the run-time processing of annotations but rather the compile time processing of annotations and thus usually operates on source code but can also operate on compiled class files. The JDK exposes this mechanism to us by allowing us the ability to write our own custom processors that can plugin to this pipeline.

6.2. What does it do?

Annotation processor’s augment the compilation process by inspecting source code / class files for specific annotations and doing some custom behavior based on those annotations. Typically this would be taking source code and based on the annotations found to be present on said source code, actually generating other artifacts, sometimes other source code.

6.3. How does it work?

Because Annotation processors operate on source code, the API is rather different. The constructs of a Java source file are enumerated in terms of the Element abstraction and it’s sub types. The constructs include:

  • module
  • class / interface
  • package
  • method
  • annotation

The Annotation processor is initialized via it’s public not argument constructor and a life-cycle method init(...). The init(...) method provides us the opportunity to do any post construction initialization. After this the methods getSupportedAnnotationTypes(...), getSupportedOptions(...) and getSupportedSourceVersion(...) are called to allow us to define the annotations, source code level and various options are supported by our Processor.

Annotation processing happens in rounds and the process(...) method defined on Processor is the method we use to act on each round. The process(...) method receives as arguments a RoundEnvironment argument and a set of TypeElement annotations to process. The method process(..) returns a boolean flag indicating if the annotations processed for this round are claimed by this Processor and thus should not be processed by any other Processor instances in the annotation processing pipeline.

6.4. The Code

Below follows snippets from the example code with a few brief explanations of what is being done.

Stub.java

@Documented
@Retention(SOURCE)
@Target(METHOD)
public @interface Stub {

	Class<?> returnType() default Void.class;

	Class<?>[] parameterTypes() default {};
}
  • lines 1-3: indicate the annotation is to be documented, it is to be discarded by the compiler and that it only applies to methods.
  • lines 6 & 8: specify the return type and parameter types.

Shouter.java

public interface Shouter {

	@Stub(returnType = String.class, parameterTypes = String.class)
	String shout(String name);
}
  • line 3: shows the application of the annotation on an interface where we specify the return type and parameter types.

StubAnnotationProcessor.java

...
@Override
	public boolean process(final Set<? extends TypeElement> annotations, final RoundEnvironment roundEnv) {

		System.out.println("Processing round");

		annotations.forEach(annotation -> {
			roundEnv.getElementsAnnotatedWith(annotation).forEach(annotatedElement -> {

				if (annotatedElement.getEnclosingElement().getKind() == ElementKind.INTERFACE) {

					StubBuilder stubBuilder = null;
					final Map<String, Object> annotationElements = getAnnotationElements(annotatedElement);
					final String interfaceName = annotatedElement.getEnclosingElement().getSimpleName().toString();

					System.out.println("Processing " + interfaceName);

					if (!this.builders.containsKey(interfaceName)) {
						stubBuilder = new StubBuilder();
						this.builders.put(interfaceName, stubBuilder);
					} else {
						stubBuilder = this.builders.get(interfaceName);
					}

					stubBuilder.setClassName(interfaceName).setMethod(annotatedElement, annotationElements);
				}
			});
		});


		for (Map.Entry<String, StubBuilder> entry : this.builders.entrySet()) {
				FILE_OPERATIONS.write(this.processingEnv, entry.getValue().getInterfaceName() + "Impl",
						entry.getValue().build());
		}


		this.builders = new HashMap<>();
	
		return true;
	}

	private Map<String, Object> getAnnotationElements(final Element annotatedElement) {
		assert !Objects.isNull(annotatedElement);

		return annotatedElement.getAnnotationMirrors().stream().flatMap(annotationMirror -> {
			final Map<? extends ExecutableElement, ? extends AnnotationValue> elementValues = annotationMirror
					.getElementValues();

			final List<AnnoationElementHolder> results = new ArrayList<>();
			for (Map.Entry<? extends ExecutableElement, ? extends AnnotationValue> entry : elementValues.entrySet()) {

				final String key = entry.getKey().getSimpleName().toString();
				final Object value = entry.getValue().getValue();

				results.add(new AnnoationElementHolder(key, value));
			}
			return results.stream();
		}).collect(Collectors.toMap(AnnoationElementHolder::getKey, AnnoationElementHolder::getValue));
	}

	private static final class AnnoationElementHolder {

		private final String key;
		private final Object value;

		AnnoationElementHolder(final String key, final Object value) {
			this.key = key;
			this.value = value;
		}

		String getKey() {
			return this.key;
		}

		Object getValue() {
			return this.value;
		}
	}
...
  • line 3: defines the process(...) method we implement in our concrete Processor.
  • lines 7 & 8: basically says for each annotation we receive (Stub) and for every element we find annotated with said annotation.
  • line 10: we are only interested in interface types that contain our annotation

Running the program can be done by navigating into the src folder of the download and executing the following:

Running the program and output

javac Stub.java Shouter.java Greeter.java FileOperations.java StubBuilder.java StubAnnotationProcessor.java
javac -processor StubAnnotationProcessor Greeter.java Shouter.java

Processing round
Processing Shouter
Processing Greeter
Processing Greeter
Processing Greeter
Processing Greeter
Processing round
Processing round

You can then view the output through any text editor: eg vim GreeterImpl.java and vim ShouterImpl.java being the two files that were generated by the Processor.

7. Summary

In this example article we briefly covered what annotations are, the different types and some simple applications of annotations on source code.

The example code covered creating a custom annotation in Java 9, that at compile time, via a custom AnnotationProcessor and a custom annotation, was able to generate a stub implementation of a custom interface and subsequently have it compiled during the compilation process.

8. Download the Source Code

This was a Java 9 Annotations Example.

Download
You can download the full source code of this example here: Java 9 Annotations Example

JJ

Jean-Jay Vester graduated from the Cape Peninsula University of Technology, Cape Town, in 2001 and has spent most of his career developing Java backend systems for small to large sized companies both sides of the equator. He has an abundance of experience and knowledge in many varied Java frameworks and has also acquired some systems knowledge along the way. Recently he has started developing his JavaScript skill set specifically targeting Angularjs and also bridged that skill to the backend with Nodejs.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Inline Feedbacks
View all comments
Back to top button