我们们之前对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注解源码来说,有着非常重要的意义。

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐