CustomValue 注解处理:如何创建 @Value 的类似注解

问题描述 投票:0回答:1

我刚刚开始使用注释处理并尝试将值注入到用我的注释注释的字段上。从昨天开始我就遇到了这个错误消息:编译失败

我尝试研究和修复,但无法避免这个错误,这是我的

CustomValueProcessor.java
类和我的注释:

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CustomValue {
    String value(); // This attribute will hold the property value
}
import com.google.auto.service.AutoService;
import report.configurations.PropertyLoader;
import javax.annotation.processing.*;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.TypeMirror;
import javax.tools.Diagnostic;
import java.lang.reflect.Field;
import java.util.Map;
import java.util.Set;

@SupportedAnnotationTypes("report.annotations.CustomValue")
@SupportedSourceVersion(SourceVersion.RELEASE_17)
@AutoService(javax.annotation.processing.Processor.class)
public class CustomValueProcessor extends AbstractProcessor {
    private Messager messager;
    private final Map<String, String> PropMap = PropertyLoader.parsePropertiesFile();

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        messager = processingEnv.getMessager();
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        for (TypeElement annotation : annotations) {
            for (Element element : roundEnv.getElementsAnnotatedWith(annotation)) {
                if (element.getKind() == ElementKind.FIELD) {
                    CustomValue annotationValue = element.getAnnotation(CustomValue.class);
                    String newParaName = annotationValue.value().substring(2, annotationValue.value().length() - 1);
                    String propertyValue = PropMap.get(newParaName);
                    //messager.printMessage(Diagnostic.Kind.ERROR, propertyValue);
                    VariableElement fieldElement = (VariableElement) element;
                    String fieldName = fieldElement.getSimpleName().toString();
                    //messager.printMessage(Diagnostic.Kind.ERROR, fieldName);
                    // Get the class type from the TypeElement
                    TypeElement enclosingClass = (TypeElement) fieldElement.getEnclosingElement();
                    TypeMirror classType = enclosingClass.asType();
                    //messager.printMessage(Diagnostic.Kind.ERROR, classType.toString());
                    // Assuming PdfService is in the same package as enclosingClass
                    Class<?> clazz = enclosingClass.getClass();
                    // Get the field
                    try {
                        Field field = clazz.getDeclaredField(fieldName);
                        //messager.printMessage(Diagnostic.Kind.ERROR,field.toString());
                        field.setAccessible(true);
                        // Set the value of the field
                        field.set(null, propertyValue);  // Pass an object instance instead of null if the field is non-static
                    } catch (NoSuchFieldException | IllegalAccessException e) {
                        e.printStackTrace();
                    }
                }
            }
            return false;
        }
        return false;
    }
}

请建议/建议我做错了什么。

java annotations annotation-processing
1个回答
0
投票

背景:

据我了解,您的意思是 Spring Boot 的

@Value
注释,并且据我从 Spring Boot 文档中读到,用
@Value
注释的元素在运行时使用
@Value
的值进行初始化。在这个答案中,我还将为您的自定义注释解决
@Value
类似功能的非常有限的子集。

在我所知道的简单情况下,Java 中的注释处理器不会与正在处理的源代码一起运行,而是在编译时运行。也就是说,您无法通过

Processor
访问您注释的代码运行时生成的对象。这就是您在处理时无法访问您想要的带注释的
Field
的原因。另一个问题是,您试图在处理时获取
Field
的实现
Class
TypeElement
,这(据我所知)不是您想要做的。这些是您收到该错误的原因。另一方面,您可以通过 Java 注释处理 API(通过
javax.lang.model.element.Element
s、
javax.lang.model.type.TypeMirror
s 和所有相关接口)访问源代码(正在处理)本身。

根据另一篇文章

标准注释处理API支持直接修改源代码

因此,为了遵循标准 API,您可以生成 new 代码,该代码将使用简单的

CustomValue
注释的值。您的应用程序需要以某种方式调用此新代码,以便用其值初始化带注释的字段。例如,在 Spring Boot 中,您需要调用一个方法来启动整个应用程序,如以下代码片段中的代码:

@SpringBootApplication
public class MyApplication {
    public static void main(final String[] args) {
        SpringApplication.run(MyApplication.class, args);
    }
}

此方法的控制流负责为您创建和初始化数据对象(至少就我对 Spring Boot 的抽象理解而言)。以(非常)相似的方式,为了让事情变得简单,让我们创建一个

Processor
它将生成所需的功能,但调用此功能将是您的应用程序的责任。

有限解概述:

您可以将代码分成两个项目:一个用于注释处理器和注释本身,另一个项目用于注释的客户端代码。 客户端项目将取决于注释处理器的项目。

注释处理器项目代码:

注释本身可以如您所声明的那样:

package annotations;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME) //This has to be RUNTIME since we need the value of each CustomValue to exist at runtime.
public @interface CustomValue {
    String value();
}

虽然注释处理器需要更改,并且出于此答案的目的,实现为:

package annotations;

import java.io.IOException;
import java.io.Writer;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Filer;
import javax.annotation.processing.Messager;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.PackageElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;
import javax.tools.Diagnostic;

@SupportedSourceVersion(SourceVersion.RELEASE_8)
@SupportedAnnotationTypes("annotations.CustomValue")
public class CustomValueProcessor extends AbstractProcessor {
    
    private final Map<String, String> propMap = Collections.singletonMap("person.id", "undefined"); //PropertyLoader.parsePropertiesFile();
    private final Map<TypeElement, Map<VariableElement, String>> classToFieldToValueMap = new LinkedHashMap<>();

    @Override
    public boolean process(final Set<? extends TypeElement> annotations,
                           final RoundEnvironment roundEnv) {
        final Messager messager = processingEnv.getMessager();
        final Elements elementUtils = processingEnv.getElementUtils();
        final Types typeUtils = processingEnv.getTypeUtils();
        final Filer filer = processingEnv.getFiler();
        annotations.forEach(annotation -> {
            roundEnv.getElementsAnnotatedWith(annotation).forEach(element -> {
                /*Checking for "element.getKind() == ElementKind.FIELD" is not really neeeded here
                as the code currently stands, since the CustomValue only supports FIELDs according
                to its Target meta-annotation, but let's indeed make the check since in the future
                we may support more Targets...*/
                if (element.getKind() == ElementKind.FIELD) {
                    final VariableElement fieldElement = (VariableElement) element;
                    final CustomValue annotationElement = element.getAnnotation(CustomValue.class);
                    final String annotationElementValue = annotationElement.value().trim();
                    
                    //Check for proper field declared type (String):
                    if (!typeUtils.isSameType(element.asType(), elementUtils.getTypeElement("java.lang.String").asType()))
                        messager.printMessage(Diagnostic.Kind.ERROR, "CustomValue annotated fields must be of type String.", element);

                    //Check for the specified value's sanity according to some simple parsing rules:
                    else if (annotationElementValue.isEmpty() || annotationElementValue.startsWith("="))
                        messager.printMessage(Diagnostic.Kind.ERROR, "Empty parameter identifier for CustomValue.", element);

                    else {
                        //Parse with simple rules:
                        final String[] param = annotationElementValue.split("=", 2);
                        final String paramID = param[0].trim(),
                                     paramDefaultValue = (param.length > 1)? param[1].trim(): "N/A";
                        
                        //Find value:
                        final String paramValue = propMap.getOrDefault(paramID, paramDefaultValue);
                        final Element enclosingElement = element.getEnclosingElement();

                        //Reject enum constants (or anything else that is not a class, such as records):
                        if (enclosingElement.getKind() != ElementKind.CLASS)
                            messager.printMessage(Diagnostic.Kind.ERROR, "Only class fields can be annotated with CustomValue.", element);
                        else {
                            final TypeElement enclosingClass = (TypeElement) enclosingElement;
                            final PackageElement pkg = elementUtils.getPackageOf(enclosingClass);

                            //Safety belt against inner classes (because, for simplicity, the rest of the code inside 'else' does not support this):
                            if (!Objects.equals(pkg, enclosingClass.getEnclosingElement()))
                                messager.printMessage(Diagnostic.Kind.ERROR, "Only fields declared in top level classes are supported for CustomValue.", element);
                            else
                                classToFieldToValueMap.computeIfAbsent(enclosingClass, key -> new LinkedHashMap<>()).put(fieldElement, paramValue);
                        }
                    }
                }
            });
        });
        
        /*
        Write to output files once at the end (or at least this is the intended behaviour).
        Source: https://stackoverflow.com/a/4146080/6746785
        */
        if (roundEnv.processingOver()) { // && !roundEnv.errorRaised()) {
            classToFieldToValueMap.forEach((enclosingClass, fieldToValue) -> {
                final PackageElement pkg = elementUtils.getPackageOf(enclosingClass);
                final String enclosingClassSimpleName = enclosingClass.getSimpleName().toString();
                final String newClassQualifiedName = enclosingClass.getQualifiedName().toString() + "Utils",
                             newClassSimpleName =    enclosingClassSimpleName                     + "Utils";
                try {
                    final VariableElement[] originatingElements = fieldToValue.keySet().toArray(new VariableElement[fieldToValue.size()]);
                    try (final Writer src = filer.createSourceFile(newClassQualifiedName, originatingElements).openWriter()) { //Use inherited source code encoding.
                        final String pkgQualifiedName = pkg.getQualifiedName().toString();
                        if (!pkgQualifiedName.isEmpty())
                            src.write("package " + pkgQualifiedName + ";\n\n");
                        src.write("import annotations.CustomValue;\n" +
                                  "import java.lang.annotation.Annotation;\n" +
                                  "import java.lang.reflect.Field;\n\n" +
                                  "public class " + newClassSimpleName + " {\n\n" +
                                  "    private static Field findFirstAnnotatedDeclaredField(final Class<?> clazz,\n" +
                                  "                                                         final String name,\n" +
                                  "                                                         final Class<? extends Annotation> annotationClass) {\n" +
                                  "        for (Class<?> cur = clazz; cur != null; cur = cur.getSuperclass()) {\n" +
                                  "            try {\n" +
                                  "                final Field field = cur.getDeclaredField(name);\n" +
                                  "                if (field.getAnnotationsByType(annotationClass).length > 0)\n" +
                                  "                    return field;\n" +
                                  "            }\n" +
                                  "            catch (final NoSuchFieldException | SecurityException e) {\n" +
                                  "            }\n" +
                                  "        }\n" +
                                  "        return null;\n" +
                                  "    }\n\n" +
                                  "    private static void suppressedWriteField(final Object object,\n" +
                                  "                                             final Field field,\n" +
                                  "                                             final Object value) {\n" +
                                  "        if (field != null) {\n" +
                                  "            field.setAccessible(true);\n" +
                                  "            try {\n" +
                                  "                field.set(object, value);\n" +
                                  "            }\n" +
                                  "            catch (IllegalArgumentException | IllegalAccessException e) {\n" +
                                  "            }\n" +
                                  "        }\n" +
                                  "    }\n\n" +
                                  "    private static void defaultSuppressedWriteField(final Object object,\n" +
                                  "                                                    final String name,\n" +
                                  "                                                    final String value) {\n" +
                                  "        suppressedWriteField(object, findFirstAnnotatedDeclaredField(" + enclosingClassSimpleName + ".class, name, CustomValue.class), value);\n" +
                                  "    }\n\n" +
                                  "    public static void setToDefaults(final " + enclosingClassSimpleName + " refTo" + enclosingClassSimpleName + ") {\n");
                        
                        //WARNING: f2v.getValue() MUST be escaped to be used as a String literal in the generated code, but for simplicity I assume here it is valid already!
                        for (final Map.Entry<VariableElement, String> f2v: fieldToValue.entrySet())
                            src.write("        defaultSuppressedWriteField(refTo" + enclosingClassSimpleName + ", \"" + f2v.getKey().getSimpleName() + "\", \"" + f2v.getValue() + "\");\n");
                        src.write("    }\n" +
                                  "}\n");
                    }
                }
                catch (final IOException ioe) {
                    messager.printMessage(Diagnostic.Kind.ERROR, ioe.toString(), enclosingClass);
                    ioe.printStackTrace();
                }
            });
            classToFieldToValueMap.clear();
        }
        return true;
    }
}

在每轮执行健全性检查后,处理器会在映射

classToFieldToValueMap
中维护所有带注释的字段及其值,以便在最后一轮中生成源代码。

注意:我使用 Java SE 版本 8(如果移植到 9+,代码可能不支持模块,以及其他可能的东西)。不要忘记更正

@SupportedSourceVersion
如果需要的话,以防将代码移植到较新的版本。

客户端项目代码:

import annotations.CustomValue;

public class Person {
    
    @CustomValue("person.id")
    private String id;
    
    @CustomValue("person.name = Random Name")
    private String name;
    
    @CustomValue("person.surname = The Surname")
    private String surname;

    @Override
    public String toString() {
        return "Person{" + "id=" + id + ", name=" + name + ", surname=" + surname + '}';
    }
    
    public static void main(final String[] args) {
        final Person p = new Person();
        System.out.println(p); //Prints null fields.
        PersonUtils.setToDefaults(p); //PersonUtils was generated by the processor in the same package as Person.
        System.out.println(p); //Prints fields with their values assigned by CustomValue.
    }
}

替代有限解决方案(无
Processor
):

需要注意的是,在这个简单的场景中,我们甚至不需要实现注释处理。我们可以在运行时直接使用反射本身。我们可以用更通用的版本稍微修改一下上面

Processor
生成的代码,该版本接受任何对象,并使用反射检测并将
CustomValue
值分配给对象的字段(如果适用)。在这种情况下,可以直接在 client 项目中使用以下代码(以及注释本身):

import annotations.CustomValue;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;

public class CustomValueUtils {

    private static List<Field> findAllAnnotatedDeclaredFields(final Class<?> clazz,
                                                              final Class<? extends Annotation> annotationClass) {
        final ArrayList<Field> annotatedFields = new ArrayList<>();
        for (Class<?> cur = clazz; cur != null; cur = cur.getSuperclass()) {
            try {
                for (final Field field: cur.getDeclaredFields())
                    if (field.getAnnotationsByType(annotationClass).length > 0)
                        annotatedFields.add(field);
            }
            catch (final SecurityException e) {
            }
        }
        annotatedFields.trimToSize();
        return Collections.unmodifiableList(annotatedFields);
    }

    private static void suppressedWriteField(final Object object,
                                             final Field field,
                                             final Object value) {
        if (field != null) {
            field.setAccessible(true);
            try {
                field.set(object, value);
            }
            catch (IllegalArgumentException | IllegalAccessException e) {
            }
        }
    }

    public static void setToDefaults(final Object object) {
        final Map<String, String> propMap = Collections.singletonMap("person.id", "undefined"); //PropertyLoader.parsePropertiesFile();
        findAllAnnotatedDeclaredFields(object.getClass(), CustomValue.class).forEach(field -> {
            final CustomValue customVal = field.getAnnotation(CustomValue.class);
            final String annotationElementValue = customVal.value().trim();
            
            //Check for the specified value's sanity according to some simple parsing rules:
            if (annotationElementValue.isEmpty() || annotationElementValue.startsWith("="))
                throw new IllegalStateException("Empty parameter identifier for CustomValue2.");
            
            //Parse with simple rules:
            final String[] param = annotationElementValue.split("=", 2);
            final String paramID = param[0].trim(),
                         paramDefaultValue = (param.length > 1)? param[1].trim(): "N/A";

            //Find and set value:
            final String paramValue = propMap.getOrDefault(paramID, paramDefaultValue);
            suppressedWriteField(object, field, paramValue);
        });
    }
}

并将其用作:

import annotations.CustomValue;

public class Person {
    
    @CustomValue("person.id")
    private String id;
    
    @CustomValue("person.name = Random Name")
    private String name;
    
    @CustomValue("person.surname = The Surname")
    private String surname;

    @Override
    public String toString() {
        return "Person{" + "id=" + id + ", name=" + name + ", surname=" + surname + '}';
    }
    
    public static void main(final String[] args) {
        final Person p = new Person();
        System.out.println(p);
        CustomValueUtils.setToDefaults(p); //Can be any Object, but is intended for objects with fields annotated with CustomValue.
        System.out.println(p);
    }
}

请注意,为了简单起见,我在每个解决方案中用

PropertyLoader.parsePropertiesFile();
伪造了
Collections.singletonMap("person.id", "undefined");

© www.soinside.com 2019 - 2024. All rights reserved.