Spring注解源码解析一
我们们之前对Spring容器的初始化和bean的加载过程都做了相应的分析,不过我们之前分析源码入口是xml配置和BeanFactory,在我们的实际项目中,现在都不使用xml配置bean了。我们更多的是使用Spring提供的各种注解,比如@Bean、@Controller、@Component、@Service、@Autowired等注解。所以我们从本节开始就要对这些注解源码进行分析了。注解是什么
我们们之前对Spring容器的初始化和bean的加载过程都做了相应的分析,不过我们之前分析源码入口是xml配置和BeanFactory,在我们的实际项目中,现在都不使用xml配置bean了。我们更多的是使用Spring提供的各种注解,比如@Bean、@Controller、@Component、@Service、@Autowired等注解。所以我们从本节开始就要对这些注解源码进行分析了。
注解是什么呢?
我们这里定义了一个名字叫做MyComponent的注解
其实,注解本质就是一个接口,只不过是继承了Annotation接口的接口而已。
我们通过一个例子来验证一下:
我们先找到@MyComponent注解的class文件,接着我们反编译一下class文件,通过javap -c命令进行反编译。
从上图中我们可以看到,我们自定义的这个@MyComponent注解,本质就是一个接口,并且还是继承了Annotation接口的接口,并且我们看到,在反编译的结果中有一个value()方法,那这个value()方法的作用是什么呢?
注解的本质是接口,我们知道在接口中是可以定义常量和方法的,其实在注解中定义的方法就是注解的属性,说白了就是这里定义value()方法,可以理解为是注解的value属性。我们在使用注解时是可以给注解设置一些属性的,比如value属性,此时就是通过value()方法来设置value属性的。所以我在自定义注解时,是可以选择为注解定义一些属性的,比如这个@MyComponent注解,我们就定义了一个value属性,如下图:
并且每个属性还可以设置默认值,比如这个value属性的默认值我们就设置为了空。
每个注解都是有一些特定功能的,那这些特定功能又是怎么和注解绑定呢?我们新增的这个@MyComponent注解的功能又是什么呢?解析来我们为@MyComponent注解添加一些功能:
首先我们需要定义出来一个User类来使用这个@MyComponent注解
我们在这个User类上添加这@MyComponent注解我们怎么使用呢?
我们写一个案例:
首先我们获取User类的class对象,然后调用class对象的isAnnotationPresent()方法就可以判断类上有没有加指定的注解。如果User类加了@MyComponent注解,那么User类就是我们要处理的目标类,此时我们可以加一些特定的逻辑来处理这个User类。
此时我们来执行一下这个main方法来看下效果:
我们在这个User类上加了@MyComponent注解,但是却没有获取到结果,就好像这个@MyComponent注解没生效一样。其实呢,我们自定义注解时,必须要搭配元注解一起使用才行,那么元注解又是什么呢?
元注解是什么?
元注解其实很简单,说白了元注解就是用于修饰注解的注解,通常用在注解的定义上,java已经帮我们定义了一些元注解,我们直接使用元注解就行了,我们看这里:
这个@Retention注解就是一个元注解,它可以控制我们定义的这个@MyComponent注解在什么时候有效,这里指定为RetentionPolicy.RUNTIME的意思是在代码运行时有效。因为平常我们自定义注解,都是要在代码运行时读取这个注解的,所以一定要加上这个@Retention元注解。给@MyComponent注解添加上@Retention元注解后,我们再来运行main方法来看下效果:
可以看到,我们可以通过class对象提供的isAnnotationPresent()方法,来判断指定类上有没有加特定的注解,如果加了特定的注解。我们可以通过getAnnotation()方法获取了特定注解的元信息,这样我们就可以根据注解的元信息,对加了注解的这个类,单独加一些特殊的逻辑。虽然我们这里的代码很简单,但是说明了注解就是一个实现了Annotation接口的接口,它只是一个标记的作用,就跟注释一样,我们可以灵活的给注解绑定上一些功能,来方便我们的日常开发。
我们了解完注解和元注解之后,我们现通过@MyComponent来模拟一下Spring的@Component注解。我们先看一下Spring中@Component注解的定义:
可以看到,@Component注解上也加了元注解,只不过这里同时加了三个元注解,比如这个@Retention元注解,我们前边是讲过的,它可以控制定义的这个@Component注解在什么时候有效,这里指定为RetentionPolicy.RUNTIME的意思是在代码运行时有效。其实这个@Component注解和@MyComponent注解是差不多的。
我们们先梳理一下@MyComponent的实现流程:
1、我们会扫描我们指定的包路径,比如我们上面的com.younger.eshop.common.annotaion。
2、我们根据指定的包路径扫描出该目录下的class文件以及子目录下的class文件。
3、遍历扫描出的class文件然后判断class文件上是否有注解@MyComponet,如果没有我们就不做处理。如果有我们对当前的class 通过反射进行实例化。
4、最后把实例化的bean放入到IOC容器,也就是一个map中。
我们们进行代码编写:
1、自定义@MyComponent注解
2、在User类上面添加@MyCompo注解
3、为我们@MyComponent注解添加功能
新建ApplicationContext类:
package com.younger.eshop.common.annotaion;
import java.io.File;
import java.net.URL;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class ApplicationContext {
/**
* IOC容器
*/
private Map<String, Object> beanMap = new ConcurrentHashMap<String, Object>();
/**
* 通过beanName名称获取bean的实例
* @param beanName
* @return
*/
public Object getBean(String beanName) {
return beanMap.get(beanName);
}
/**
* 构造方法
* @param packagePath 包路径
*/
public ApplicationContext(String packagePath) {
// 扫描指定的包路径
scanPackagePath(packagePath);
}
/**
* 扫描指定的包路径
* @param packagePath 包路径
*/
private void scanPackagePath(String packagePath) {
//1、获取这个目录下面所有的class文件
File[] classFile = getClassFile(packagePath);
//2、处理所有的class文件,对添加了@MyComponent注解类创建实例并且添加到IOC容器
processClassFile(packagePath,classFile);
}
/**
* 处理所有的class文件
* @param packagePath 包路径
* @param classFile 文件对象
*/
private void processClassFile(String packagePath, File[] classFile) {
for (File file : classFile) {
//1、去除后缀,获取class文件名
String className = file.getName().substring(0, file.getName().lastIndexOf("."));
//2、拼接全限定类名
String fullClassName = packagePath + "." + className;
//3、将类名称首字母转小写,得到beanName
String beanName = String.valueOf(className.charAt(0)).toLowerCase() + className.substring(1);
//4、创建实例并放入到ICO容器中
createBean(beanName,fullClassName);
}
}
/**
* 创建实例并放入到IOC容器中
* @param beanName bean的
* @param fullClassName
*/
private void createBean(String beanName, String fullClassName) {
try {
//1、通过反射创建出class对象
Class<?> cls = Class.forName(fullClassName);
//2、判断这个类上是否加了@MyCompent注解
if (cls.isAnnotationPresent(MyComponent.class)) {
// 3、通过反射创建出实例
Object instance = cls.newInstance();
// 4、将实例放入到IOC容器中
beanMap.put(beanName,instance);
}else {
System.out.println(fullClassName+ " 不需要创建实例");
}
}catch (Exception e) {
System.out.println(fullClassName + " 通过实例出现异常");
}
}
/**
* 获取这个目录下面所有的class文件
* @param packagePath
* @return
*/
private File[] getClassFile(String packagePath) {
// 1、通过packagePath 获取对应的file对象
File file = getFile(packagePath);
//2、过滤出所有的class文件
return filterClassFile(packagePath,file);
}
/**
* 过滤出所有的class文件
* @param packagePath 包路径
* @param file 文件对象
* @return
*/
private File[] filterClassFile(final String packagePath, File file) {
//1、过滤出文件下面的所有class文件
return file.listFiles(f -> {
String fileName = f.getName();
//2、如果是目录,那么需要在此扫描这个目录下所有文件(递归调用)
if (f.isDirectory()) {
scanPackagePath(packagePath+"."+fileName);
}else {
//3、如果文件后缀是.class,就返回true
if (fileName.endsWith(".class")) {
return true;
}
}
return false;
});
}
/**
* 获取对应的file文件
* @param packagePath 包路径
* @return
*/
private File getFile(String packagePath) {
//1、将包路径中的"."替换为"/"
String packageDir = packagePath.replaceAll("\\.", "/");
//2、获取这个目录在类路径中的位置
URL url = getClass().getClassLoader().getResource(packageDir);
//3、通过目录获取到文件对象
return new File(url.getFile());
}
}
1、首先通过构造方法,在构造方法的入参是一个包路径,这个包路径说白了就是需要扫描的包路径,也就是user类所在的包路径,即com.younger.eshop.common.annotaion.
2、在构造方法中调用了一个scanPackage(packagePath)方法,这个scanPackage(packagePath)方法是专门用来扫表包路径的,它里边最核心的逻辑有2快,第一块是会调用getClassFiles(packagePath)方法来获取packagePath目录下所有的class文件。而第二块逻辑是会调用processClassFiles(packagePath, classFiles)来处理扫描出来的class文件,就是为加了@MyComponent注解的类创建实例,并放入到IOC容器中。
那么我们先来看第一块逻辑,来看下getClassFiles(packagePath)方法是怎么来扫描指定目录下的class文件的,getClassFiles(packagePath)方法:
在getClassFiles(packagePath)方法中,首先会调用getFile(packagePath)方法,来获取packagePath路径对应的file对象,接着调用filterClassFiles(packagePath, file)方法来处理这个file对象,就是来过滤出所有的class文件。filterClassFiles(packagePath, file)方法中就是会看下当前的file对象是目录类型还是文件类型,如果是目录类型,那么就递归调用scanPackage(packagePath)方法,那如果file对象是文件,那么就过滤出来class文件。
上面的逻辑就是来看下这个class上是否添加了@MyComponent注解,如果添加了@MyComponent注解,那么我们就使用反射来创建类对应的实例,并将创建好的实例放入到IOC容器中,如果这个class没有添加@MyComponent注解,那么我们不处理就可以了。因为我们要使用到反射,所以我们要提前拼接出来类的全限定类名fullyClassName,为了方便,我们这里顺便将beanName也给处理好了,这个beanName后边会作为IOC容器的key来使用。当fullyClassName和beanName都处理好后,最后会调用createBean(beanName, fullyClassName)方法来完成bean的创建。
首先使用Class.forName(fullyClassName)拿到了全限定类名对应的class对象,然后调用了class对象的isAnnotationPresent(MyComponent.class)方法,来判断当前的class是否添加了@MyComponent注解。如果当前的class没有添加@MyComponent注解,那么isAnnotationPresent()方法就会返回false,此时就不会对这个class做任何处理。如果当前的class添加了@MyComponent注解,那么就会通过反射来创建类对应的实例,也就是会执行这行代码 Object instance = c.newInstance()。最后会将创建好的instance放入到IOC容器beanMap中。
我们可以通过getBean方法获取到容器中的实例对象了。
我们测试验证一下:
看下结果:
通过打印结果我们可以知道,没有加@MyComponent注解的Teacher类,没有创建实例,而加了@MyComponent注解的User类,通过反射创建了实例,并被放到了IOC容器中,且可以从IOC容器中获取到实例,并可以正常调用实例的study()方法。
到这里为止,我们仿照Spring中@Component注解的功能,自己手写了一个@MyComponent注解,这个@MyComponent注解的功能和Spring中@Component注解的功能是完全一样的,通过手写这个代码,我们了解了注解的真正使用方法,更重要的是,它对我们理解这个Spring注解源码来说,有着非常重要的意义。
更多推荐
所有评论(0)