注解#

什么是注解(Annotation)#

注解是放在Java源码的类、方法、字段、参数前的一种特殊 “注释”:

// this is a component:
@Resource("hello")
public class Hello {
    @Inject
    int n;

    @PostConstruct
    public void hello(@Param String name) {
        System.out.println(name);
    }

    @Override
    public String toString() {
        return "Hello";
    }
}

注释会被编译器直接忽略,注解则可以被编译器打包进入 .class 文件,因此,注解是一种用作标注的 “元数据”。

元注解#

元注解就是定义其他注解的注解。 比如 @Override 这个注解就不是一个元注解,而是通过元注解定义出来的。

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {}

这里用到的 @Target@Retention 就是元注解。

元注解只有四个:

  • @Documented :被修饰的注解类将被 javadoc 工具提取成文档

  • @Inherited :被修饰的注解类将具有继承性

  • @Retention :用于指定被修饰的注解可以存在于哪里(源代码、 .class 文件、运行时)

  • @Target :用于指定被修饰的注解修饰哪些程序单元(类、方法、字段)

自定义注解#

除了元注解,都是自定义注解。比如我们常用的 @Override@Autowire

Java 语言使用 @interface 语法来定义自己的注解,格式如下:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Report {
    int type() default 0;
    String level() default "info";
    String value() default "";
}

注解的参数类似无参数方法,可以用 default 设定一个默认值(强烈推荐)。 最常用的参数应当命名为 value

一些常用的注解#

以下是 Java 中常用的注解:

  • @Override 表示当前方法覆盖了父类的方法。

  • @Deprecation 表示方法已经过时,方法上有横线,使用时会警告。

  • @SuppressWarnings 表示关闭一些警告信息。

  • @SafeVarages 表示专门抑制 “堆污染” 警告提供的。

  • @FunctionalInterface 用来指定某个接口必须是函数式接口,否则编译出错。

以下是 Spring 中常用的注解:

1https://www.liaoxuefeng.com/wiki/1252599548343744/1266265100383840

备注

在学习 Spring 框架时,我们遇到的第一个也是最核心的概念就是容器 1https://www.liaoxuefeng.com/wiki/1252599548343744/1266265100383840

什么是容器?容器是一种为某种特定组件的运行提供必要支持的一个软件环境。 例如,Tomcat 就是一个 Servlet 容器,它可以为 Servlet 的运行提供运行环境。 类似 Docker 这样的软件也是一个容器,它提供了必要的 Linux 环境以便运行一个特定的 Linux 进程。

通常来说,使用容器运行组件,除了提供一个组件运行环境之外,容器还提供了许多底层服务。 例如,Servlet 容器底层实现了 TCP 连接,解析 HTTP 协议等非常复杂的服务,如果没有容器来提供这些服务,我们就无法编写像 Servlet 这样代码简单,功能强大的组件。

早期的 JavaEE 服务器提供的 EJB 容器最重要的功能就是通过 声明式事务 服务,使得 EJB 组件的开发人员不必自己编写冗长的事务处理代码,所以极大地简化了事务处理。

Spring 的核心 就是提供了一个 IoC 容器,它可以管理所有轻量级的 JavaBean 组件,提供的底层服务包括组件的生命周期管理、配置和组装服务、AOP 支持,以及建立在 AOP 基础上的 声明式事务 服务等。

IoC(控制反转)容器提供了 对组件进行生命周期管理和配置组装服务 的功能。

我们可以将 Spring 想象成一个组装工人,他在众多零件中找到我们需要的零件后,组装起来为我们提供服务。

而我们使用的注解,就可以声明这是一个什么类型的组件。之后当我们后面需要使用这个某个组件的时候,Spring 会自动帮我们组装。

如果一个类被 Configuration 注解,那它就是一个 IoC 容器,那些被标注了 @Bean 的方法就是该容器中的基础服务,给全局提供服务。在我们编写的某个具体应用中,通常有以下的数据交换过程:

Database --- JDBC --- DAO --- Service --- Controller --- Axios --- Vue.js

因此,通常将软件按照三个层次编写组件:

  • @Controller 表示控制层(Controller)组件,用于给前后端的通信。

  • @Service 表示业务(Service)组件。

  • @Repository 表示数据访问层(DAO)组件,用于访问数据库。

当组件不好归类的时候,我们可以使用 @Component 进行标注。

那么我们如果已经将组件都写好了,如何装配呢? @Autowired 默认按类型装配,如果我们想使用按名称装配,可以结合 @Qualifier 注解一起使用。 在我们没有声明使用哪个组件时,默认将使用被 @Primary 注解的组件,找不见就抛出异常。 @Resource 默认按名称装配,当找不到与名称匹配的 Bean 才会按类型装配。

除了这些组件外,还有一些其他的组件,比如 @Scope 注解作用域、@Lazy(true) 表示延迟初始化。

然后,Spring 提供了一些声明周期相关的注解:

../_images/spring-life-cycle.png

@PostConstruct 用来修饰方法,标记在项目启动的时候执行这个方法,一般用来执行某些初始化操作比如全局配置。 被注解的方法会在构造函数之后执行,Servlet 的 init() 方法之前执行。 另外,可以用 @DependsOn 定义 Bean 初始化及销毁时的顺序。

注解的存活时间#

由编译器使用的注解

比如 @Override@SuppressWarnings,编译后,这些注解不会进入 .class 文件,编译后就被扔掉了。

由工具处理 ``.class`` 文件使用的注解

比如在加载 .class 文件到内存的时候,对 .class 文件做动态修改,以实现一些特殊功能。

在运行时能够读取的注解

他们加载后一直存在于 JVM 中,比如 @PostConstruct 的方法会在调用构造方法后自动被调用。

注解和反射的结合#

注解和反射经常结合在一起,在很多框架中都能看到他们的影子。

可以通过反射来判断类、方法、字段上是否有某个注解,以及获取注解中的值。

当开发者使用注解修饰了类、方法、字段后,这些注解不会自己生效,必须由开发者提供相应的代码来提取并处理注解信息。 这些处理提取和处理注解的代码统称为 APT(Annotation Processing Tool)。

作为示例,我们首先提出一个需求: 项目经理想跟踪一个项目中现在一共实现了多少个的用例。如果某个用例已经实现了,那么就添加一个 @UseCase 标记。 这样,我们就可以很方便地掌控项目的进展。而且,当用户需求发生变更时,也更容易定位代码,修改需求实现。

然后,我们开始写代码,首先定义 @UseCase 注解:

//: annotations/UseCase.java
import java.lang.annotation.*;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface UseCase {
    public int id();
    public String description() default "no description";
} ///:~

然后,尝试使用这个注解。把这个注解用于 PasswordUtils 类的某个方法上:

//: annotations/PasswordUtils.java
import java.util.*;

public class PasswordUtils {
    @UseCase(id = 47, description = "Passwords must contain at least one numeric")
    public boolean validatePassword(String password) {
        return (password.matches("\\w*\\d\\w*"));
    }
    @UseCase(id = 48) // 注解并不会对方法的代码产生什么影响(也就是没有侵入性)
    public String encryptPassword(String password) {
    return new StringBuilder(password).reverse().toString();
    }
    @UseCase(id = 49, description = "New passwords can't equal previously used ones")
    public boolean checkForNewPassword(List<String> prevPasswords, String password) {
        return !prevPasswords.contains(password);
    }
} ///:~

最后,我们需要解析注解。因为注解存活于运行时,我们使用反射机制,查找 @UseCase 标记,并获取它的值。

//: annotations/UseCaseTracker.java
import java.lang.reflect.*;
import java.util.*;

public class UseCaseTracker {
    public static void trackUseCases(List<Integer> useCases, Class<?> cl) {
        for(Method m : cl.getDeclaredMethods()) {
            UseCase uc = m.getAnnotation(UseCase.class); // 对某个方法检查是否有对应的注解对象
            if(uc != null) { // 找到了,就拿出注解中的值
                System.out.println("Found Use Case:" + uc.id() + " " + uc.description());
                useCases.remove(new Integer(uc.id())); // 实现一个用例移除一个用例
            }
        }
        for(int i : useCases) { // 看看还有哪些用例没有实现
            System.out.println("Warning: Missing use case-" + i);
        }
    }
    public static void main(String[] args) {
        List<Integer> useCases = new ArrayList<Integer>();
        Collections.addAll(useCases, 47, 48, 49, 50); // 共有 47 48 49 50 四个用例需要实现
        trackUseCases(useCases, PasswordUtils.class);
    }
} /* Output:
Found Use Case:47 Passwords must contain at least one numeric
Found Use Case:48 no description
Found Use Case:49 New passwords can't equal previously used ones
Warning: Missing use case-50
*///:~