Here is a solution using APT (annotation processing tool) via AspectJ. It adds the specified annotations to getter methods, but does not remove them from fields. So this is a "copy" action, not a "move".
Annotation processing support was added to AspectJ in version 1.8.2 and described in the release notes. Here is some self-consistent sample code. I compiled it from the command line because from Eclipse I failed to get it running according to AspectJ maintainer Andy Clement's description.
Okay, let's say we have one (Eclipse or other) project directory and the directory layout be like this:
SO_AJ_APT_MoveAnnotationsFromMemberToGetter
compile_run.bat
src
de/scrum_master/app/Person.java
src_apt
de/scrum_master/app/AnnotatedGetterProcessor.java
de/scrum_master/app/AnnotateGetter.java
de/scrum_master/app/CollationType.java
de/scrum_master/app/SortOrder.java
de/scrum_master/app/Unique.java
META-INF/services/javax.annotation.processing.Processor
Both src and src_apt are source directories, compile_run.bat is a Windows batch file building the project in two stages (annotation processor first, then the rest of the project) and running the final result in order to prove that it actually does what it should.
Annotations to be used for fields and later copied to getter methods:
package de.scrum_master.app;
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.FIELD, ElementType.METHOD })
public @interface Unique {}
package de.scrum_master.app;
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.FIELD, ElementType.METHOD })
public @interface SortOrder {
String value() default "ascending";
}
package de.scrum_master.app;
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.FIELD, ElementType.METHOD })
public @interface CollationType {
String value() default "alphabetical";
String language() default "EN";
}
Meta annotation designating field annotations to be copied to getter methods:
Please note that this meta annotation is only needed for annotation processing and thus has a SOURCE
retention scope.
package de.scrum_master.app;
import java.lang.annotation.*;
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.TYPE)
public @interface AnnotateGetter {
Class<? extends Annotation>[] value();
}
Driver application:
Things to be noted:
There are four fields with annotations (id, firstName, lastName, fieldWithoutGetter), but only the first three have corresponding getter methods, the last one does not. So we expect fieldWithoutGetter to be handled gracefully later, either an empty or no ITD aspect being generated later via APT.
The meta annotation @AnnotateGetter({ Unique.class, SortOrder.class, CollationType.class })
on class Person
specifies which annotations to consider for copying them to getter methods. Later you can play around with it and see how the result changes if you remove any of them.
We also have some dummy methods doSomething()
and doSomethingElse()
which should be unaffected by any annotation copying later, i.e. they should not get any new annotations via AspectJ. (It is always good to have a negative test case.)
The main(..)
method uses reflection to print all fields and methods including their annotations.
package de.scrum_master.app;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
@AnnotateGetter({ Unique.class, SortOrder.class, CollationType.class })
public class Person {
@Unique
private final int id;
@SortOrder("descending")
@CollationType("alphabetical")
private final String firstName;
@SortOrder("random")
@CollationType(value = "alphanumeric", language = "DE")
private final String lastName;
@SortOrder("ascending")
@CollationType(value = "numeric")
private final int fieldWithoutGetter;
public Person(int id, String firstName, String lastName, int fieldWithoutGetter) {
this.id = id;
this.firstName = firstName;
this.lastName = lastName;
this.fieldWithoutGetter = fieldWithoutGetter;
}
public int getId() { return id; }
public String getFirstName() { return firstName; }
public String getLastName() { return lastName; }
public void doSomething() {}
public void doSomethingElse() {}
public static void main(String[] args) {
System.out.println("Field annotations:");
for (Field field : Person.class.getDeclaredFields()) {
System.out.println(" " + field.getName());
for (Annotation annotation : field.getAnnotations())
System.out.println(" " + annotation);
}
System.out.println();
System.out.println("Method annotations:");
for (Method method : Person.class.getDeclaredMethods()) {
System.out.println(" " + method.getName());
for (Annotation annotation : method.getAnnotations())
System.out.println(" " + annotation);
}
}
}
Console output without APT + AspectJ:
As you can see, the field annotations are printed, but no method annotations because we have yet to define an annotation processor (see further below).
Field annotations:
id
@de.scrum_master.app.Unique()
firstName
@de.scrum_master.app.SortOrder(value=descending)
@de.scrum_master.app.CollationType(value=alphabetical, language=EN)
lastName
@de.scrum_master.app.SortOrder(value=random)
@de.scrum_master.app.CollationType(value=alphanumeric, language=DE)
fieldWithoutGetter
@de.scrum_master.app.SortOrder(value=ascending)
@de.scrum_master.app.CollationType(value=numeric, language=EN)
Method annotations:
main
getId
doSomething
doSomethingElse
getFirstName
getLastName
Annotation processor:
Now we need an annotation processor generating an aspect for each combination of field and annotation to be copied. Such an aspect should look like this:
package de.scrum_master.app;
public aspect AnnotateGetterAspect_Person_CollationType_lastName {
declare @method : * Person.getLastName() : @de.scrum_master.app.CollationType(value = "alphanumeric", language = "DE");
}
Very simple, is it not? The annotation processor should generate those aspects into a directory .apt_generated. The AspectJ compiler will handle that for us, as we will see later. But first here is the annotation processor (sorry for the lenghty code, but this is what you asked for):
package de.scrum_master.app;
import java.io.*;
import java.util.*;
import javax.tools.*;
import javax.annotation.processing.*;
import javax.lang.model.*;
import javax.lang.model.element.*;
import javax.lang.model.type.*;
@SupportedAnnotationTypes(value = { "*" })
@SupportedSourceVersion(SourceVersion.RELEASE_7)
public class AnnotatedGetterProcessor extends AbstractProcessor {
private Filer filer;
@Override
public void init(ProcessingEnvironment env) {
filer = env.getFiler();
}
@SuppressWarnings("unchecked")
@Override
public boolean process(
Set<? extends TypeElement> elements,
RoundEnvironment env
) {
// Get classes annotated with something like: @AnnotateGetter({ Foo.class, Bar.class, Zot.class })
env.getElementsAnnotatedWith(AnnotateGetter.class)
.stream()
.filter(annotatedClass -> annotatedClass.getKind() == ElementKind.CLASS)
// For each filtered class, copy designated field annotations to corresponding getter method, if present
.forEach(annotatedClass -> {
String packageName = annotatedClass.getEnclosingElement().toString().substring(8);
String className = annotatedClass.getSimpleName().toString();
/*
* Unfortunately when we do something like this:
* AnnotateGetter annotateGetter = annotatedClass.getAnnotation(AnnotateGetter.class);
* Class<? extends Annotation> annotationToBeConverted = annotateGetter.value()[0];
* We will get this exception:
* Internal compiler error:
* javax.lang.model.type.MirroredTypesException:
* Attempt to access Class objects for TypeMirrors
* [de.scrum_master.app.Unique, de.scrum_master.app.SortOrder, de.scrum_master.app.CollationType]
* at org.aspectj.org.eclipse.jdt.internal.compiler.apt.model.AnnotationMirrorImpl.getReflectionValue
*
* Thus, we have to use annotation mirrors instead of annotation classes directly,
* then tediously extracting annotation values from a nested data structure. :-(
*/
// Find @AnnotateGetter annotation and extract its array of values from deep within
((List<? extends AnnotationValue>) annotatedClass.getAnnotationMirrors()
.stream()
.filter(annotationMirror -> annotationMirror.getAnnotationType().toString().equals(AnnotateGetter.class.getName()))
.map(AnnotationMirror::getElementValues)
.map(Map::values)
.findFirst()
.get()
.stream()
.map(AnnotationValue::getValue)
.findFirst()
.get()
)
.stream()
.map(annotationValueToBeCopied -> (TypeElement) ((DeclaredType) annotationValueToBeCopied.getValue()).asElement())
// For each annotation to be copied, get all correspondingly annotated fields
.forEach(annotationTypeElementToBeCopied -> {
env.getElementsAnnotatedWith(annotationTypeElementToBeCopied)
.stream()
.filter(annotatedField -> ((Element) annotatedField).getKind() == ElementKind.FIELD)
// For each annotated field create an ITD aspect
.forEach(annotatedField -> {
String fieldName = annotatedField.getSimpleName().toString();
String aspectName =
"AnnotateGetterAspect_" + className + "_" +
annotationTypeElementToBeCopied.getSimpleName() + "_" + fieldName;
StringBuilder annotationDeclaration = new StringBuilder()
.append("@" + annotationTypeElementToBeCopied.getQualifiedName() + "(");
annotatedField.getAnnotationMirrors()
.stream()
.filter(annotationMirror -> annotationMirror.getAnnotationType().toString().equals(annotationTypeElementToBeCopied.getQualifiedName().toString()))
.map(AnnotationMirror::getElementValues)
.forEach(annotationParameters -> {
annotationParameters.entrySet()
.stream()
.forEach(annotationParameter -> {
ExecutableElement annotationParameterType = annotationParameter.getKey();
AnnotationValue annotationParameterValue = annotationParameter.getValue();
annotationDeclaration.append(annotationParameterType.getSimpleName() + " = ");
if (annotationParameterType.getReturnType().toString().equals("java.lang.String"))
annotationDeclaration.append("\"" + annotationParameterValue + "\"");
else
annotationDeclaration.append(annotationParameterValue);
annotationDeclaration.append(", ");
});
if (!annotationParameters.entrySet().isEmpty())
annotationDeclaration.setLength(annotationDeclaration.length() - 2);
annotationDeclaration.append(")");
});
// For each field with the current annotation, create an ITD aspect
// adding the same annotation to the member's getter method
String aspectSource = createAspectSource(
annotatedClass, packageName, className,
annotationDeclaration.toString(), fieldName, aspectName
);
writeAspectSourceToDisk(packageName, aspectName, aspectSource);
});
});
});
return true;
}
private String createAspectSource(
Element parentElement,
String packageName,
String className,
String annotationDeclaration,
String fieldName,
String aspectName
) {
String getterMethodName = "get" + fieldName.substring(0, 1).toUpperCase() + fieldName.substring(1);
StringBuilder aspectSource = new StringBuilder()
.append("package " + packageName + ";\n\n")
.append("public aspect " + aspectName + " {\n");
for (Element childElement : parentElement.getEnclosedElements()) {
// Search for methods
if (childElement.getKind() != ElementKind.METHOD)
continue;
ExecutableElement method = (ExecutableElement) childElement;
// Search for correct getter method name
if (!method.getSimpleName().toString().equals(getterMethodName))
continue;
// Parameter list for a getter method must be empty
if (!method.getParameters().isEmpty())
continue;
// Getter method must be public
if (!method.getModifiers().contains(Modifier.PUBLIC))
continue;
// Getter method must be non-static
if (method.getModifiers().contains(Modifier.STATIC))
continue;
// Add call to found method
aspectSource.append(
" declare @method : * " + className + "." + getterMethodName + "() : " +
annotationDeclaration + ";\n"
);
}
aspectSource.append("}\n");
return aspectSource.toString();
}
private void writeAspectSourceToDisk(
String packageName,
String aspectName,
String aspectSource
) {
try {
JavaFileObject file = filer.createSourceFile(packageName + "." + aspectName);
file.openWriter().append(aspectSource).close();
System.out.println("Generated aspect " + packageName + "." + aspectName);
} catch (IOException ioe) {
// Message "already created" can appear if processor runs more than once
if (!ioe.getMessage().contains("already created"))
ioe.printStackTrace();
}
}
}
I am not going to say much about the annotation processor, please read it carefully. I also added some source code comments, hopefully they are good enough to be understood.
src_apt/META-INF/services/javax.annotation.processing.Processor:
We need this file for the annotation processor to work later in connection with the AspectJ compiler (ajc).
de.scrum_master.app.AnnotatedGetterProcessor
Batch file building and running the project:
Sorry if this is platform-specific, but I guess you can easily convert it into a UNIX/Linux shell script, it is quite straightforward.
@echo off
set SRC_PATH=C:\Users\Alexander\Documents\java-src
set ASPECTJ_HOME=C:\Program Files\Java\AspectJ
echo Building annotation processor
cd "%SRC_PATH%\SO_AJ_APT_MoveAnnotationsFromMemberToGetter"
rmdir /s /q bin
del /q processor.jar
call "%ASPECTJ_HOME%\bin\ajc.bat" -1.8 -sourceroots src_apt -d bin -cp "%ASPECTJ_HOME%\lib\aspectjrt.jar"
jar -cvf processor.jar -C src_apt META-INF -C bin .
echo.
echo Generating aspects and building project
rmdir /s /q bin .apt_generated
call "%ASPECTJ_HOME%\bin\ajc.bat" -1.8 -sourceroots src -d bin -s .apt_generated -inpath processor.jar -cp "%ASPECTJ_HOME%\lib\aspectjrt.jar";processor.jar -showWeaveInfo
echo.
echo Running de.scrum_master.app.Person
java -cp bin;"%ASPECTJ_HOME%\lib\aspectjrt.jar" de.scrum_master.app.Person
Console log for build + run process:
Building the processor + annotation classes, then packaging them into a processor.jar:
Building annotation processor
Manifest wurde hinzugefügt
Eintrag META-INF/ wird ignoriert
META-INF/services/ wird hinzugefügt(ein = 0) (aus = 0)(0 % gespeichert)
META-INF/services/javax.annotation.processing.Processor wird hinzugefügt(ein = 45) (aus = 46)(-2 % verkleinert)
de/ wird hinzugefügt(ein = 0) (aus = 0)(0 % gespeichert)
de/scrum_master/ wird hinzugefügt(ein = 0) (aus = 0)(0 % gespeichert)
de/scrum_master/app/ wird hinzugefügt(ein = 0) (aus = 0)(0 % gespeichert)
de/scrum_master/app/AnnotatedGetterProcessor.class wird hinzugefügt(ein = 8065) (aus = 3495)(56 % verkleinert)
de/scrum_master/app/AnnotateGetter.class wird hinzugefügt(ein = 508) (aus = 287)(43 % verkleinert)
de/scrum_master/app/CollationType.class wird hinzugefügt(ein = 520) (aus = 316)(39 % verkleinert)
de/scrum_master/app/SortOrder.class wird hinzugefügt(ein = 476) (aus = 296)(37 % verkleinert)
de/scrum_master/app/Unique.class wird hinzugefügt(ein = 398) (aus = 248)(37 % verkleinert)
Aspect generation + project build (done with just one AspectJ compiler call because of its built-in annotation processing support):
Generating aspects and building project
Generated aspect de.scrum_master.app.AnnotateGetterAspect_Person_Unique_id
Generated aspect de.scrum_master.app.AnnotateGetterAspect_Person_SortOrder_fieldWithoutGetter
Generated aspect de.scrum_master.app.AnnotateGetterAspect_Person_SortOrder_firstName
Generated aspect de.scrum_master.app.AnnotateGetterAspect_Person_SortOrder_lastName
Generated aspect de.scrum_master.app.AnnotateGetterAspect_Person_CollationType_fieldWithoutGetter
Generated aspect de.scrum_master.app.AnnotateGetterAspect_Person_CollationType_firstName
Generated aspect de.scrum_master.app.AnnotateGetterAspect_Person_CollationType_lastName
'public int de.scrum_master.app.Person.getId()' (Person.java:31) is annotated with @de.scrum_master.app.Unique method annotation from 'de.scrum_master.app.AnnotateGetterAspect_Person_Unique_id' (AnnotateGetterAspect_Person_Unique_id.java:4)
'public java.lang.String de.scrum_master.app.Person.getFirstName()' (Person.java:32) is annotated with @de.scrum_master.app.SortOrder method annotation from 'de.scrum_master.app.AnnotateGetterAspect_Person_SortOrder_firstName' (AnnotateGetterAspect_Person_SortOrder_firstName.java:4)
'public java.lang.String de.scrum_master.app.Person.getFirstName()' (Person.java:32) is annotated with @de.scrum_master.app.CollationType method annotation from 'de.scrum_master.app.AnnotateGetterAspect_Person_CollationType_firstName' (AnnotateGetterAspect_Person_CollationType_firstName.java:4)
'public java.lang.String de.scrum_master.app.Person.getLastName()' (Person.java:33) is annotated with @de.scrum_master.app.CollationType method annotation from 'de.scrum_master.app.AnnotateGetterAspect_Person_CollationType_lastName' (AnnotateGetterAspect_Person_CollationType_lastName.java:4)
'public java.lang.String de.scrum_master.app.Person.getLastName()' (Person.java:33) is annotated with @de.scrum_master.app.SortOrder method annotation from 'de.scrum_master.app.AnnotateGetterAspect_Person_SortOrder_lastName' (AnnotateGetterAspect_Person_SortOrder_lastName.java:4)
Last, but not least, we run the driver application again. This time we should see the annotations copied from the annotated fields to the corresponding getter methods (if such methods exist):
Running de.scrum_master.app.Person
Field annotations:
id
@de.scrum_master.app.Unique()
firstName
@de.scrum_master.app.SortOrder(value=descending)
@de.scrum_master.app.CollationType(value=alphabetical, language=EN)
lastName
@de.scrum_master.app.SortOrder(value=random)
@de.scrum_master.app.CollationType(value=alphanumeric, language=DE)
fieldWithoutGetter
@de.scrum_master.app.SortOrder(value=ascending)
@de.scrum_master.app.CollationType(value=numeric, language=EN)
Method annotations:
main
getId
@de.scrum_master.app.Unique()
doSomethingElse
getLastName
@de.scrum_master.app.CollationType(value=alphanumeric, language=DE)
@de.scrum_master.app.SortOrder(value=random)
getFirstName
@de.scrum_master.app.SortOrder(value=descending)
@de.scrum_master.app.CollationType(value=alphabetical, language=EN)
doSomething
Voilà! Enjoy and feel free to ask questions. :-)
Update (2015-05-03): Attention, in my annotation processor code I initially forgot to also copy the annotation parameter values, thus only the default values were created for each annotation. I have just fixed that, making the annotation processor code even more lengthy. Because I wanted to make it worthwhile refactoring the code and learn something from it, even though you have already accepted the original answer and solved your problem in another way anyway, I played around with Java 8 stuff like lambdas, streams, filters, maps. This is not particularly readable if the concept is new to you, especially for nested forEach
loops, but I wanted to try and see how far I can get with it. ;-)