Java 注解详解
- 参考资料
上面几篇参考资料应该是网上最好的几篇资料了,下面都是我看这几篇文章做的笔记.
主要是参考资料 2,后面涉及具体的 spring 框架源码解析,暂时有点难理解,先跳过,之后再回过头来学习.
什么是注解?
比较官方的说法是:注解是那些插入到源代码中,使用其他工具可以对其进行处理的标签。
这就话可以解读出几个意思:
- 注解只是源码中的一个标记符号
- 注解需要其他工具进行处理
举个例子,下面的@Test 就是注解,它标记了下面的 test 方法是我们要进行单元测试的方法.
但是只有这个注解,程序本身并不会做任何事情,需要安装 JUnit 测试工具,它才会扫描调用标识为@Test 的方法.
所以我们可以说,注解=注解定义+工具支持
import org.junit.Test;
public class SomeTest {
@Test
public void test(){
// TODO
}
}
下面我们通过一个 demo 来了解注解是如何定义和解析的.
- 我们将定义一个@BeanAnnotation,@ConfigurationAnnotation 两个注解.
- 然后分别用它们来修饰一个名为 Book 的 po,和我们的 BookConfig,两个类.
- 接着在 main 类中,通过定义 parseConfiguration 方法,解析 BookConfig 配置类,注册 bean.
(注册 bean 具体过程:将配置类中用@BeanAnnotation 声明的构造函数建立实例并和 beanName 的映射添加到 beanMap).
- 最后在 main 类的 main 方法中,调用 parseConfiguration()方法,获取注册好 bean 的 config 实例,再通过它来 getBean(),测试 BookConfig 中注册的 Bean 是否正常.
如何定义注解?
Java 中有两类注解,一类是我们平常使用修饰类和方法的注解,另一类是修饰注解的注解,一般在定义注解的时候使用,我们叫它元注解.
一共四个:
- @Retention
- 意如其名,retention 保留的意思,它用来标记这个注解可以保留到程序编译运行的哪个阶段
- 它有这么几个参数
- RetentionPolicy.SOURCE 保留在源文件里(编译时会被丢弃)
- RetentionPolicy.CLASS 保留在 class 文件里(虚拟机不会载入它们)
- RetentionPolicy.RUNTIME (保留在 class 文件里,并由虚拟机将它们载入,通过反射可以获取到它们)
- 通常没有特殊需要,我们都会选择使用 RUNTIME,并通过反射来解析注解
- @Target
- 标记注解的使用目标
- 它有以下参数
- ElementType.TYPE 用于类和接口
- ElementType.FIELD 用于成员域
- ElementType.METHOD 用于方法
- ElementType.PARAMETER 用于方法或者构造器里的参数
- ElementType.CONSTRUCTOR 用于构造器
- ElementType.LOCAL_VARIABLE 用于局部变量
- ElementType.ANNOTATION_TYPE 用于注解类型声明
- ElementType.PACKAGE 用于包
- ElementType.TYPE_PARAMETER 类型参数
- ElementType.TYPE_USE 类型用法
- @Document
- 为归档工具(如 Javadoc)提供提示
- @Inherited
- 只能应用于对类的注解。
- 指明当这个注解应用于一个类 A 的时候,能够自动被类 A 的子类继承。
了解了元注解,接着我们就能通过几个元注解的组合来定义我们这个 demo 需要的两个注解了.
package tech.rpish.annotation;
import java.lang.annotation.*;
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface BeanAnnotation {
String value()default"";
}
通过元注解我们可以知道@BeanAnnotation,是修饰方法,保留到运行时,有以供为 value 参数的注解.
下面的@ConfigurationAnnotation 和它有点不一样,它的目标对象是类和接口.
packagetech.rpish.annotation;
import java.lang.annotation.*;
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ConfigurationAnnotation {
String value()default"";
}
使用注解
我们先建以供 pojo,用于之后 bean 的注册.
package tech.rpish.po;
import lombok.Data;
@Data
public classBook {
private String name;
publicBook(String name) {
this.name=name;
}
}
接着在 BookConfig 类中,使用注解标记配置类和 bean
package tech.rpish;
importtech.rpish.annotation.BeanAnnotation;
importtech.rpish.annotation.ConfigurationAnnotation;
importtech.rpish.po.Book;
importjava.util.LinkedHashMap;
@ConfigurationAnnotation
public classBookConfig {
privateLinkedHashMap<String, Object>beans;
public <T>T getBean(String name, Class<T>clazz) {
Object o=beans.get(name);
return(T) o;
}
@BeanAnnotation
publicBook book() {
return newBook("A Book");
}
@BeanAnnotation("anobook")
publicBook book2() {
return newBook("Another Book");
}
}
解析注解
这边基本每个重要的语句我都做了注释,大家可以看注释.
package tech.rpish;
import tech.rpish.annotation.BeanAnnotation;
import tech.rpish.annotation.ConfigurationAnnotation;
import tech.rpish.po.Book;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.LinkedHashMap;
public classMain {
public static voidmain(String[]args)throwsException{
// 调用解析配置方法,获取解析后(将@BeanAnnotation标记的bean的 实例和beanName 添加到 beans字段的hashmap)的配置类实例
BookConfig config=parseConfiguration(BookConfig.class);
// 通过配置类的getBean方法,传入beanName获取实例
Book book=config.getBean("book", Book.class);
System.out.println(book);
Book book1=config.getBean("anobook",Book.class);
System.out.println(book1);
Book book2=config.getBean("book2", Book.class);
System.out.println(book2);
}
public static <T>T parseConfiguration(Class<T>clazz)throwsIllegalAccessError, InstantiationException, InvocationTargetException, NoSuchFieldException, IllegalAccessException {
// 如果该类中没有 ConfigurationAnnotation(配置注解) 返回null
if(!clazz.isAnnotationPresent(ConfigurationAnnotation.class)) {
return null;
}
// (如果存在配置类/有配置注解)实例化配置类
T instance=clazz.newInstance();
// 获取配置类中的字段(beanName和class的映射表) 和 声明的方法
LinkedHashMap<String, Object>hashMap= newLinkedHashMap<>();
Field beans=clazz.getDeclaredField("beans");
Method[]methods=clazz.getDeclaredMethods();
// 遍历寻找标记了 BeanAnnotation 的方法 (也就是我们在配置类中注册的bean)
// 将bean的构造器,构造对象和beanName,添加到beanName和class的映射表
for(Method m:methods) {
if(m.isAnnotationPresent((BeanAnnotation.class))) {
Object o=m.invoke(instance);
BeanAnnotation t=m.getAnnotation(BeanAnnotation.class);
// beanName有在注解中指定value的话使用指定的,否则使用声明的构造方法名
String name;
if(t.value()!= null && !t.value().equals("")) {
name=t.value();
}else{
name=m.getName();
}
hashMap.put(name, o);
}
}
// 将beanName和class的映射表设为可访问,并添加新值(配置类中用@BeanAnnotation标记的bean)
beans.setAccessible(true);
beans.set(instance, hashMap);
// 返回配置类实例
returninstance;
}
}
这个 demo,大致介绍了自定义@Bean,@Configuration 注解,然后在配置类中标记配置类和要注册的 bean 的构造方法.最后在 main 中调用 parseConfiguration()方法解析配置类,返回注册了 bean 的 config 对象,再通过它来 getBean().
和 Spring 的使用体验是基本一直的,但是 spring 的实现过程会复杂得多,具体的推荐大家看上述的参考资料 2,我这篇也只是笔记罢了.
很感谢写这篇文章的老哥,解决了困惑了我很久的问题.