1. 简介

Java 注解(Annotation)是一种向源代码中添加元数据的方式。它是从 JDK5 开始引入的强大特性,提供了一种替代 XML 配置文件和标记接口的手段。

虽然我们可以将注解应用于包、类、接口、方法或字段,但注解本身并不会影响程序的执行流程。

本文将重点介绍如何创建并处理自定义注解。如果你对注解的基本概念还不熟悉,可以先参考 Java 注解基础 这篇文章。

2. 创建自定义注解

我们的目标是实现一个简单的对象转 JSON 字符串的功能,为此我们将创建三个自定义注解:

  • 一个用于类级别,表示该类可被序列化;
  • 一个用于字段级别,标记哪些字段需要包含在 JSON 输出中;
  • 一个用于方法级别,标记初始化方法。

2.1. 类级别的注解示例

创建自定义注解的第一步是使用 @interface 关键字声明:

public @interface JsonSerializable {
}

接下来,我们通过元注解指定其作用范围和目标:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface JsonSerializable {
}

✅ 我们的第一个注解具有运行时可见性,并且只能应用在类上。它没有方法,因此只是一个简单的标记注解,用来标识可以被序列化的类。

2.2. 字段级别的注解示例

同样地,我们创建第二个注解,用于标记要包含在 JSON 中的字段:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface JsonElement {
    public String key() default "";
}

⚠️ 该注解定义了一个名为 key 的 String 类型参数,默认值为空字符串。

当我们创建带有方法的注解时需要注意以下几点:

  • 方法不能有参数;
  • 方法不能抛出异常;
  • 返回类型只能是基本类型、String、Class、枚举、其他注解及其数组;
  • 默认值不能为 null。

2.3. 方法级别的注解示例

假设我们在序列化前需要调用某个方法来初始化对象状态,为此我们可以创建如下注解:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Init {
}

✅ 这是一个运行时可见的方法级注解,可用于类中的任意方法。

2.4. 应用注解

现在我们看看如何使用这些自定义注解。比如我们有一个 Person 类,希望将其序列化为 JSON 字符串,并且在序列化前调用一个方法来格式化姓名:

@JsonSerializable
public class Person {

    @JsonElement
    private String firstName;

    @JsonElement
    private String lastName;

    @JsonElement(key = "personAge")
    private String age;

    private String address;

    @Init
    private void initNames() {
        this.firstName = this.firstName.substring(0, 1).toUpperCase() 
          + this.firstName.substring(1);
        this.lastName = this.lastName.substring(0, 1).toUpperCase() 
          + this.lastName.substring(1);
    }

    // 标准 getter 和 setter 省略
}

在这个例子中:

  • 使用 @JsonSerializable 表示该类可被序列化;
  • 使用 @JsonElement 标记了三个字段,表示它们会出现在最终的 JSON 中;
  • 使用 @Init 标记了 initNames() 方法,在序列化前会自动调用;
  • 通过设置 key = "personAge" 指定 JSON 中的字段名为 personAge 而不是默认的 age

⚠️ 注意:我们将 initNames() 设为私有方法,不能手动调用,也无法通过构造函数触发。

3. 处理注解

前面我们完成了注解的创建和使用,现在来看如何通过 Java 的反射机制处理这些注解。

首先检查对象是否为空,以及其类是否带有 @JsonSerializable 注解:

private void checkIfSerializable(Object object) {
    if (Objects.isNull(object)) {
        throw new JsonSerializationException("The object to serialize is null");
    }
        
    Class<?> clazz = object.getClass();
    if (!clazz.isAnnotationPresent(JsonSerializable.class)) {
        throw new JsonSerializationException("The class " 
          + clazz.getSimpleName() 
          + " is not annotated with JsonSerializable");
    }
}

然后查找并执行带有 @Init 注解的方法,完成对象初始化:

private void initializeObject(Object object) throws Exception {
    Class<?> clazz = object.getClass();
    for (Method method : clazz.getDeclaredMethods()) {
        if (method.isAnnotationPresent(Init.class)) {
            method.setAccessible(true);
            method.invoke(object);
        }
    }
 }

✅ 使用 method.setAccessible(true) 可以调用私有方法,如 initNames()

初始化完成后,遍历所有字段,提取带有 @JsonElement 注解的字段名与值,存入 Map 并生成 JSON 字符串:

private String getJsonString(Object object) throws Exception {    
    Class<?> clazz = object.getClass();
    Map<String, String> jsonElementsMap = new HashMap<>();
    for (Field field : clazz.getDeclaredFields()) {
        field.setAccessible(true);
        if (field.isAnnotationPresent(JsonElement.class)) {
            jsonElementsMap.put(getKey(field), (String) field.get(object));
        }
    }        
     
    String jsonString = jsonElementsMap.entrySet()
        .stream()
        .map(entry -> "\"" + entry.getKey() + "\":\"" 
          + entry.getValue() + "\"")
        .collect(Collectors.joining(","));
    return "{" + jsonString + "}";
}

再次强调:因为字段是私有的,所以我们使用了 field.setAccessible(true) 来访问。

完整的 JSON 序列化器整合了以上逻辑:

public class ObjectToJsonConverter {
    public String convertToJson(Object object) throws JsonSerializationException {
        try {
            checkIfSerializable(object);
            initializeObject(object);
            return getJsonString(object);
        } catch (Exception e) {
            throw new JsonSerializationException(e.getMessage());
        }
    }
}

最后通过单元测试验证结果:

@Test
public void givenObjectSerializedThenTrueReturned() throws JsonSerializationException {
    Person person = new Person("soufiane", "cheouati", "34");
    ObjectToJsonConverter serializer = new ObjectToJsonConverter(); 
    String jsonString = serializer.convertToJson(person);
    assertEquals(
      "{\"personAge\":\"34\",\"firstName\":\"Soufiane\",\"lastName\":\"Cheouati\"}",
      jsonString);
}

4. 总结

在这篇文章中我们完成了以下内容:

✅ 学会了如何创建不同类型的自定义注解;
✅ 掌握了如何使用注解来装饰类、字段和方法;
✅ 了解了如何通过 Java 反射 API 动态处理注解信息,实现灵活的对象序列化功能。

这只是一个入门级的例子,实际项目中你可以结合 Spring、Jackson 等框架做更复杂的应用。注解虽小,威力不小,掌握好它是进阶 Java 工程师的必修课之一。


原始标题:Creating a Custom Annotation in Java | Baeldung