Java 每日一刊(第17期):注解


前言

这里是分享 Java 相关内容的专刊,每日一更。

本期将为大家带来以下内容:

  1. 什么是 Java 注解
  2. 为什么需要注解
  3. 注解的类型
  4. 注解的生命周期
  5. 注解的工作原理
  6. 注解继承

什么是 Java 注解

Java 注解(Annotations)就是在代码里加上一些 特殊的标记,这些标记可以告诉编译器、运行时系统或者一些工具,这段代码应该怎么被处理。它们就像是给代码加上了“说明书”,但是这些“说明书”不会直接改变代码的执行效果。

为什么需要注解

在日常编程中,我们写的代码可能会被很多工具处理,比如编译器会检查我们有没有写错,框架(比如 Spring)可能会根据我们的配置来管理对象。注解可以帮助这些工具理解我们的代码,并让这些工具为我们做更多的事情,比如减少我们写配置文件的麻烦、生成一些常见的代码等。

注解的类型

Java 中的注解分为三大类:内置注解元注解自定义注解。此外,注解根据生命周期还可分为:编译时注解运行时注解

内置注解

Java 提供了一些常用的注解,用来帮助开发者进行代码检查或优化。以下是最常见的内置注解:

  • @Override:标识方法重写。如果一个方法没有正确重写父类的方法,编译器会报错。
  • @Deprecated:标识某个类或方法已过时,提醒开发者不要使用。编译器会发出警告。
  • @SuppressWarnings:用于忽略编译器的某些警告。常见参数如 "unchecked" 用来忽略未经检查的操作。

元注解

元注解是用来修饰其他注解的注解。常见的元注解有:

  • @Retention:指定注解的生命周期(SOURCE、CLASS、RUNTIME)。
  • @Target:限定注解能被应用于哪些元素(类、方法、字段等)。
  • @Inherited:允许子类继承父类的注解。
  • @Documented:将注解包含在 Javadoc 中。
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MyAnnotation {
    String value();
}

@Retention 决定注解的保留时间,如 RetentionPolicy.RUNTIME 表示注解在运行时可以通过反射读取。

@Target 限制注解的使用范围,如 ElementType.METHOD 表示注解只能用于方法。

自定义注解

Java 允许开发者定义自己的注解,来适应特殊需求。自定义注解通常有属性,这些属性可以设置默认值或由使用者来赋值。

@interface MyAnnotation {
    String name() default "defaultName";
    int version() default 1;
}

使用自定义注解:

@MyAnnotation(name = "Test", version = 2)
public void someMethod() {
    // 方法内容
}

注解的生命周期

注解的生命周期由 @Retention 元注解决定,注解可以有以下三种保留策略:

  • SOURCE:注解只存在于源码中,编译时被丢弃,无法在 .class 文件中看到,常用于编译时检查。
  • CLASS:注解被保存在 .class 文件中,但不会加载到 JVM 中,运行时不可见(默认)。
  • RUNTIME:注解在运行时依然存在,JVM 加载类时不会丢弃,可以通过反射获取和使用注解。
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface LogExecutionTime {
}

上述注解使用了 RetentionPolicy.RUNTIME,意味着它在运行时依然存在,并可以通过反射机制读取。

注解的工作原理

Java 注解的工作原理主要体现在两个方面:

  1. 编译时处理(Annotation Processing):注解处理器在编译期间通过扫描和处理源代码上的注解,生成代码或配置。
  2. 运行时处理(Reflection Processing):注解保留到运行时,通过反射机制获取注解信息,并执行相应的操作。

编译时处理(Annotation Processing)

编译时注解处理器(Annotation Processor) 是一种机制,它允许开发者在代码编译时分析注解并生成额外的代码。这个机制在 Java 5 中引入,并在 Java 6 中通过 javax.annotation.processing 包标准化。编译时处理是通过注解处理器类实现的,这些类会扫描代码中的特定注解,并对其做出相应的响应。

注解处理器的工作机制

编译时注解处理器的工作流程可以分为以下几个步骤:

  1. 注册注解处理器:注解处理器(Annotation Processor)是实现了 javax.annotation.processing.Processor 接口的类,处理器通过 @SupportedAnnotationTypes 指定处理哪些注解。
  2. 解析注解:编译器(如 javac)会在编译期间扫描所有带有注解的元素(类、方法、字段等),并将这些元素及其注解传递给注解处理器。
  3. 生成代码或报告错误:注解处理器会基于解析的注解信息执行某些操作,例如生成新的 Java 源代码、修改已有代码、生成配置文件或在编译阶段报告错误。
  4. 多轮处理:注解处理可能需要多轮处理(rounds)。当某个注解处理器生成了新的 Java 源代码时,编译器会触发下一轮处理,直到没有新的代码生成为止。
编写自定义注解处理器

下面是一个简单的编译时注解处理器的例子:

Step 1:定义一个注解

// 定义注解
@Retention(RetentionPolicy.SOURCE)  // 注解只存在于源码中
@Target(ElementType.METHOD)         // 注解可以应用在方法上
public @interface PrintAnnotation {
    String value() default "Hello, Annotation Processor!";
}

Step 2:编写注解处理器

// 注册处理器,指定该处理器处理哪些注解
@SupportedAnnotationTypes("PrintAnnotation")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class PrintAnnotationProcessor extends AbstractProcessor {

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        for (Element element : roundEnv.getElementsAnnotatedWith(PrintAnnotation.class)) {
            // 获取注解
            PrintAnnotation annotation = element.getAnnotation(PrintAnnotation.class);
            // 打印注解信息
            System.out.println("Processing method: " + element.getSimpleName() + " with message: " + annotation.value());
        }
        return true;  // 表示注解处理完成
    }
}

Step 3:使用注解

public class Test {

    @PrintAnnotation(value = "Custom message")
    public void myMethod() {
        System.out.println("Executing myMethod.");
    }
}

Step 4:编译和执行

在编译时,注解处理器会捕获 @PrintAnnotation 的使用,并根据注解信息输出处理结果。

多轮处理

注解处理器可以生成新的源文件,如果在第一轮处理生成了新的注解代码,编译器会再次调用注解处理器以处理这些新的注解。这个过程可以发生多轮,直到没有新的注解需要处理为止。

常见的编译时注解应用

Lombok@Data@Getter@Setter 等注解,自动生成 getter 和 setter 等方法。

Dagger:依赖注入框架,在编译时生成依赖注入的代码。

AutoValue:Google 的库,用于生成不可变类的代码。

运行时处理(Reflection Processing)

Java 中有一类注解保留到运行时(通过 @Retention(RetentionPolicy.RUNTIME) 标记)。这些注解在程序运行期间可以通过 反射机制 进行处理。反射是 Java 提供的一个非常强大的特性,它允许程序在运行时检查和操作类的结构(包括类、方法、字段等)。

运行时注解的获取

当注解使用 @Retention(RetentionPolicy.RUNTIME) 进行标记时,它将在字节码中保留,并且在运行时仍然存在。开发者可以使用 Java 的反射 API 来获取这些注解,并基于这些注解执行特定逻辑。

例如,Spring 框架在依赖注入(@Autowired)、事务管理(@Transactional)等场景中,都会在运行时通过反射处理注解,决定如何处理代码中的逻辑。

通过反射获取注解的例子

下面是一个示例,演示如何在运行时通过反射获取注解信息并处理:

// 定义运行时注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface LogExecutionTime {
}

// 使用注解的类
public class TestClass {

    @LogExecutionTime
    public void executeTask() {
        System.out.println("Task executed.");
    }
}

// 运行时反射处理注解
public class AnnotationProcessor {

    public static void main(String[] args) throws Exception {
        // 获取 TestClass 类的 Method 对象
        Method method = TestClass.class.getMethod("executeTask");

        // 检查方法上是否存在 LogExecutionTime 注解
        if (method.isAnnotationPresent(LogExecutionTime.class)) {
            long startTime = System.currentTimeMillis();

            // 调用方法
            method.invoke(new TestClass());

            long endTime = System.currentTimeMillis();
            System.out.println("Execution time: " + (endTime - startTime) + " ms");
        }
    }
}
反射获取注解信息的步骤
  1. 获取类、方法或字段的反射对象:通过 Class.getMethod()Class.getField()Class.getDeclaredMethods() 来获取反射对象。
  2. 检查是否存在注解:使用 isAnnotationPresent(Class<? extends Annotation> annotationClass) 来判断某个元素上是否标记了特定注解。
  3. 获取注解实例:使用 getAnnotation(Class<T> annotationClass) 来获取注解实例,之后可以调用注解的属性。
  4. 执行逻辑:根据获取到的注解信息,在运行时执行相应的逻辑。

编译时和运行时注解处理的比较

特性编译时注解处理运行时注解处理
注解生命周期RetentionPolicy.SOURCECLASSRetentionPolicy.RUNTIME
处理时机编译期间程序运行期间
典型应用代码生成、编译时校验依赖注入、AOP、事务管理
性能影响无运行时开销运行时反射可能影响性能
工具Annotation ProcessorReflection API
常见库Lombok、Dagger、AutoValueSpring、Hibernate、JAX-RS

注解继承

尽管 Java 的注解不能真正被继承,但可以通过将注解标记为 @Inherited 来实现类似的行为。当你给一个类加上 @Inherited 注解时,它的子类也会继承这个注解。不过,注解只会被类继承,不能被方法继承

示例:

@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface MyInheritedAnnotation {
    String value() default "Inherited Annotation";
}

@MyInheritedAnnotation
class ParentClass {}

class ChildClass extends ParentClass {}

当你通过反射获取 ChildClass 的注解时,尽管它没有显式声明 @MyInheritedAnnotation,但仍然会继承 ParentClass 的注解。

public class Main {
    public static void main(String[] args) {
        boolean isAnnotated = ChildClass.class.isAnnotationPresent(MyInheritedAnnotation.class);
        System.out.println("ChildClass has @MyInheritedAnnotation: " + isAnnotated);
    }
}

输出:ChildClass has @MyInheritedAnnotation: true

本期小知识

Java 8 引入了 类型注解(Type Annotations),可以将注解应用在几乎所有类型使用的地方,而不仅仅是类或方法声明上。以下是一些少见但可能有用的例子:

应用在数组中:

String @MyAnnotation [] myArray;

应用在类型转换中:

MyClass obj = (@MyAnnotation MyClass) anotherObject;

应用在构造函数中:

public MyClass() throws @MyAnnotation IOException {
    // constructor code
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值