• 手写Spring——bean的扫描、加载和实例化


    1、前言

    在之前看了Spring框架的IOCAOP源码之后,很早就像自己动手造一次轮子,这次全当做一次熟悉过程。

    2、Spring有什么内容

    既然说到要手写Spring,既然是模仿,自然先得知道待模仿之物的结构。

    众所周知,Spring框架是一个容器,关于什么是容器,之前也有博客做了一些大致的说明。姑且理解那就是一个哆啦A梦的百宝袋了。

    Spring具有很多好的思维方式,负责管理容器启动BeanDefinition 修饰单例或多例bean实例依赖注入AOP切面切点初始化BeanPostProcessor 等。

    3、手写Spring开始

    手写spring,首先需要新建一个工程项目。我习惯创建Maven工程项目作为测试项。

    只是无任何依赖,任何实现都需要自己手写。

    3.1、创建工程

    暂时定义工程名为my_spring,其中分为两个包:

    • cn.xj.spring spring轮子
    • com.test 测试类

    在这里插入图片描述

    3.2、容器创建

    既然知道Spring具备容器的功能,那么则需要创建一个容器。
    这里就创建一个MySpringAppAnnotationContext 类,充当容器的概念。

    在Spring源码中,通常加载解析xml文件或者扫描配置类等方式,将bean对象进行构建。

    本次就不写xml的解析,采取最为直观的配置扫描类的方式进行。

    只是各种方式下,具备不同的实现,对于bean的创建而言,大致上是相似的。

    3.3、新建扫描配置类和@MyComponentScan注解

    既然需要一个配置类,用来保证扫描包路径生效,同时也能加载指定包路径下的所有.class文件,那么首先就需要创建一个@MyComponentScan

    package cn.xj.spring;
    
    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    
    /**
     * 自定义注解,用于解析获取 bean 扫描路径
     **/
    @Target(ElementType.TYPE) // 只能用于类
    @Retention(RetentionPolicy.RUNTIME) // 运行时有效
    public @interface MyComponentScan {
    
        String value() default "";
    }
    
    

    在测试包下,编写一个扫描配置类,用上我们定义的扫描注解。

    package com.test;
    
    import cn.xj.spring.MyComponentScan;
    
    /**
     * 配置类
     **/
    //@MyComponentScan("com.test")
    @MyComponentScan
    public class BeanScan {
    }
    
    

    3.4、测试类编写调用容器

    说到容器,其实还需要编写一个自定义的容器,用来进行bean的扫描bean的创建、以及bean的生成

    自定义容器类为MySpringAppAnnotationContext

    public class MySpringAppAnnotationContext {
    
        // 指定配置类属性
        private Class aClass;
    
    	// 构造函数,传递对应的 class类
        public MySpringAppAnnotationContext(Class aClass) throws Exception{
    		this.aClass = aClass;
    	}
    	//  提供bean的获取方法
    	public Object getBean(String className) {
    		// 暂定返回为null
    		return null;
    	}
    }
    

    一个简单的容器对象就创建好了,接下来创建一个测试类,用来进行测试调用。

    import cn.xj.spring.MySpringAppAnnotationContext;
    
    /**
     * 测试类
     **/
    public class Test {
        public static void main(String[] args) throws Exception {
            MySpringAppAnnotationContext context = 
            		new MySpringAppAnnotationContext(BeanScan.class);
    
            System.out.println(context.getBean("userService"));
            System.out.println(context.getBean("userService"));
            System.out.println(context.getBean("userService"));
            System.out.println(context.getBean("userService"));
        }
    }
    

    3.5、创建需要加载的bean和@MyComponent注解

    正常来说,在spring中并非是所有的Java对象都是bean对象,只有在显示或者隐式的加了@Component才能算作上是一个bean类。

    所以,需要先创建一个注解,标识哪些类能够作为bean

    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    
    /**
     * 标识类是一个bean
     **/
    @Target(ElementType.TYPE) // 暂时只用于类上
    @Retention(RetentionPolicy.RUNTIME)
    public @interface MyComponent {
        String value() default "";
    }
    

    有了注解标识后,需要编写一个测试的bean类,用来构建bean对象。

    /**
     * 定义一个bean
     **/
    @MyComponent
    public class UserService{
    
    }
    

    有了bean类并且针对bean类加了@MyComponent注解后,此时则需要进行bean类的解析和创建操作了。

    3.6、bean的扫描实现

    通常情况下,都是根据一个配置的扫描路径,加载路径下的所有能够成为bean的类,并将其实例化加载至容器中。

    所以,可以在MySpringAppAnnotationContext的构造函数中,进行扫描、解析、创建、加载的工作。

    那么,接下来就来修改MySpringAppAnnotationContext的构造函数。

    由于MySpringAppAnnotationContext构造函数中,传递的参数信息为BeanScan.java对象。

    为了安全,并非所有的类都值得来获取对应的扫描路径。

    可以通过类上是否具备@MyComponentScan注解标识来区分。

    public class MySpringAppAnnotationContext {
    
        // 指定配置类属性
        private Class aClass;
    
    	// 构造函数,传递对应的 class类
        public MySpringAppAnnotationContext(Class aClass) throws Exception{
    		this.aClass = aClass;
    		
    		// 进行扫描操作
    		// 安全操作:如果这个配置类上存在@MyComponentScan 注解,那么就继续执行其他逻辑
    		if (aClass.isAnnotationPresent(MyComponentScan.class)) {
    		
    		}
    	}
    	//  提供bean的获取方法
    	public Object getBean(String className) {
    		// 暂定返回为null
    		return null;
    	}
    }
    

    当根据aClass.isAnnotationPresent(MyComponentScan.class)判断到是需要识别的配置类后,此时就需要根据@MyComponentScan注解来获取需要扫描的路径信息。

    省略其他代码,只写构造逻辑。

    构造方法中,就可以进行下面的操作。

    if (aClass.isAnnotationPresent(MyComponentScan.class)) {
    	MyComponentScan myComponentScan = 
    		(MyComponentScan) aClass.getDeclaredAnnotation(MyComponentScan.class);
    }
    

    获取MyComponentScan 注解对象的方法有两种:

    方法名区别
    getAnnotation(xxx)包括继承的所有注解
    getDeclaredAnnotation会忽略继承的其他注解

    当获取到MyComponentScan 注解对象后,通过获取其中设定的value属性值,用来作为扫描路径

    但想过一个问题没有:
    如果这里的value未设定值时,那么获取到的值就是 “”

    if (aClass.isAnnotationPresent(MyComponentScan.class)) {
    	// 获取 MyComponentScan 注解对象
    	MyComponentScan myComponentScan = 
    		(MyComponentScan) aClass.getDeclaredAnnotation(MyComponentScan.class);
    		// 获取注解对象中的属性值
    		String value = myComponentScan.value();
    }
    

    在springboot中,如果配置类上@ComponentScan未设定扫描路径时,他会将这个配置类所在的包路径作为扫描路径。

    我们也能基于这种思想进行实现!

    增加路径获取值为空时的判断。

    if (aClass.isAnnotationPresent(MyComponentScan.class)) {
    	// 获取 MyComponentScan 注解对象
    	MyComponentScan myComponentScan = 
    		(MyComponentScan) aClass.getDeclaredAnnotation(MyComponentScan.class);
    		// 获取注解对象中的属性值
    		String value = myComponentScan.value();
    		// 如果未设定扫描的路径值,则默认 配置类 所在的包作为父包
            if(value == null || value.length() == 0){
                 value = aClass.getPackage().getName();
             }
    }
    

    此时,获取识别到的value属性为com.test

    有了包名,但这个包名是一个相对路径

    相对于项目而言,是一个相对路径,并非绝对路径。

    需要扫描加载路径下的所有符合要求的bean类,必须保证路径是绝对路径

    【疑问:】如何才能保证能够随着不同项目路径,自动包装为绝对路径

    相当于有的人项目名为Spring,有的人是Demo
    有的人项目在D盘,有的人项目在F盘。

    不知道大家在项目启动时,看过控制台打印的前面几天日志没有,如下所示:
    在这里插入图片描述
    将该处打印的日志展开,可以看到一个classpath指令,其中包含项目启动时所需要的所有加载的东西。如下图所示:
    在这里插入图片描述
    那么,为了实现动态地获取绝对路径,可以使用类加载器实现!

    代码修改逻辑如下所示:

    public MySpringAppAnnotationContext(Class aClass) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
            this.aClass = aClass;
    
            // 容器初始化,解析对应的配置类
            // 1、 扫描
            // 判断对象上是否有对应的注解
            if (aClass.isAnnotationPresent(MyComponentScan.class)) {
                // getDeclaredAnnotation 会忽略继承
                // getAnnotation 包括继承的所有注解
                //MyComponentScan myComponentScan = (MyComponentScan) aClass.getAnnotation(MyComponentScan.class);
                MyComponentScan myComponentScan = (MyComponentScan) aClass.getDeclaredAnnotation(MyComponentScan.class);
                String value = myComponentScan.value();
                //System.out.println(value);
    
                // 如果未设定扫描的路径值,则默认 配置类 所在的包作为父包
                if(value == null || value.length() == 0){
                    value = aClass.getPackage().getName();
    
                }
    
                // 当前value是注解中的value自定义路径,属于相对路径。
                // 需要获取绝对路径,可以依据classpath
                // 获取ClassLoader
                ClassLoader classLoader = this.getClass().getClassLoader();
                // *****
    			URL resource = classLoader.getResource(value);
    }
    

    但是使用classLoader.getResource时,需要注意:

    传递的参数不是包名,而是路径名,也就是 “com/test”这种!

    所以在调用classLoader.getResource(value)之前,还需要将value值进行转换。

    // getResource需要传递路径,但value只是包(xx.xx.xx),需要转换成路径(xx/xx/xx)
    value = value.replace(".","/");
    // 获取相对于 -classpath 的路径
    URL resource = classLoader.getResource(value);
    

    再通过获取的到URL对象中的file属性值,就能得到绝对路径信息,代码如下所示:

    // getResource需要传递路径,但value只是包(xx.xx.xx),需要转换成路径(xx/xx/xx)
    value = value.replace(".","/");
    // 获取相对于 -classpath 的路径
    URL resource = classLoader.getResource(value);
    
    String filePath = resource.getFile();
    
    

    控制台打印输出,其绝对路径如下所示:

    /E:/study/my_spring/target/classes/com/test

    完美!

    3.7、根据包路径扫描其下所有满足要求的文件

    既然此处是一个包路径,需要获取包下的所有.class文件,那么就需要判断这个路径到底是不是一个包路径

    因为可能这个路径不存在,或者不是包路径!

    修改上述的代码逻辑,增加是否是包路径判断。

    // 将路径信息,封装成一个 File 对象,
    // File 对象中有 isDirectory() 方法,可以通过该方法判断是否是包路径!
    File file = new File(filePath);
    
    // 如果当前文件对象是文件夹
    if (file.isDirectory()) {
    	// 是包路径,则继续执行剩下的逻辑
    	
    }
    

    当识别到是包路径后,那么就可以开始获取包下的所有.class文件。

    但,必须是所有的文件都需要进行加载么?

    并非包路径下的所有文件都需要进行加载操作,加载生成bean的文件需要满足下面两个条件:

    • 必须是java文件,但此处加载的是编译之后的,并非源文件
    • 必须是有@MyComponent注解修饰的才是bean

    结合这两点要求,可以编写下面的逻辑代码:

    // 如果当前文件对象是文件夹
    if (file.isDirectory()) {
    	// 获取文件夹下的所有文件
    	File[] files = file.listFiles();
    	for (File file1 : files) {
    		String fileName = file1.getAbsolutePath();
    		// 判断是不是 class文件
    		if(!fileName.endsWith(".class")){
    			continue;
    		}
    		// 如何根据class文件获取Class对象信息
    	}
    }
    

    【疑问】如何根据class文件获取对应的Class对象信息?
    通过绝对的包路径信息,封装为File对象后,能够获取其下的所有文件对象File。但此时的文件只是文件路径和文件名称而已。并不是Class对象。

    3.8、使用类加载器loadClass将class文件转换为Class对象

    如果需要将.class文件转换为Class对象,则此处依旧需要使用类加载器,将class文件加载成Class对象

    Class<?> loadClass = classLoader.loadClass(className);
    

    但这里需要注意一点:

    classLoader.loadClass传递的参数格式是xx.xx.xx这种全路径信息,并不是文件路径。

    所以需要将获取到的各个文件路径转换为全路径。那么整体代码如下所示:

    // 如果当前文件对象是文件夹
    if (file.isDirectory()) {
    	// 获取文件夹下的所有文件
    	File[] files = file.listFiles();
    	for (File file1 : files) {
    		String fileName = file1.getAbsolutePath();
    		// 判断是不是 class文件
    		if(!fileName.endsWith(".class")){
    			continue;
    		}
    		// 如何根据class文件获取Class对象信息
    		// 截取包名——类名(这里代码写的比较死)
            String className = fileName.substring(fileName.indexOf("com"), fileName.indexOf(".class"));
            // com/test/xxx
            // 因为名称是文件路径,并不是包名称,需要再处理一次
            className = className.replace("\\", ".");
            // 使用类类加载器,将class文件加载成Class对象
            Class<?> loadClass = classLoader.loadClass(className);
            // 并不是每个Class对象都是我们需要的bean类,此处还需要根据@MyComponent注解进行过滤
            if (!loadClass.isAnnotationPresent(MyComponent.class)) {
            	continue;
            }
            // 满足要求的bean类,不过这里是Class对象
            // 进行后续处理
            // ......
    	}
    }
    

    此时,已经将满足class文件是@MyComponent修饰的对象转换成了Class对象

    再spring源码中,保存bean实例化的方式是一个Map集合,其中的key信息保存的是bean的别名称信息。所以此处还需要获取bean的别名称信息。

    3.9、获取bean的别名称

    获取别名称的方式很多,再自定义@MyComponent就能通过提前设定value属性的方式,进行解析获取设定值作为bean的别名称。

    但是再平时使用Spring框架的时候,发现当value属性未指定数据值时,依旧是可以获取到类的别名。这种是如何实现的呢?

    再上面的代码逻辑中,既然我们已经获取到了每个.class文件转换成的Class对象。

    既然是Class对象,在JDK开发API中存在一个方法,就可以获取类的首字母小写的名称。

    就能将其作为类的别名处理了。

    接下来的逻辑,可以这么写:

    如果设定了value属性值,那么就用设定值;
    如果未设定,则获取类的首字母小写的名称作为bean的别名。

    代码逻辑如下所示:

    // 这里其实还需要判断是否是单例  需要定义 @MyScope  暂时不考虑,统一为单例
    MyComponent myComponent = loadClass.getAnnotation(MyComponent.class);
    String beanName = myComponent.value();
    // 如果在 MyComponent 注解中未指定value属性,此处如何获取?
    if(beanName == null || beanName.length() == 0){
       	beanName = Introspector.decapitalize(loadClass.getSimpleName());
    }
    

    3.10、封装BeanDefinition

    当逻辑写到了此处,本可以直接将beanName作为别名,将Class对象转换成bean对象,实现bean的实例化操作了。

    但是,是否考虑过一个问题:

    对象涉及到单例和多例。
    如果是单例,此处直接生成bean对象,放入对应的缓存中,那没问题。
    如果是多例,多例的要求是每次调用时都需要生成一个全新的对象体。

    如果多例的实例化也放在此处,如何保证每次调用时,保证内存地址不同?

    有人可能会说,那就放在getBean中生成吧。

    spring源码中bean的实例化操作,的确是在getBean中进行处理的。

    但是对象的生成如果在扫描结束后,立即生成。考虑到bean是单例还是多例的情况,如果在getBean中进行实例化,又需要解析对象是否有对应的单例/多例 标识,然后再来根据具体的设定进行实例化,显得很耗时。浪费性能!

    在spring中提出了一种bean的定义的思想。

    在扫描结束后,将单例/多例是否懒加载bean的Class对象等信息,封装至一个BeanDefinition对象中存储。

    在getBean的时候,就去判断是否是单例还是多例,再根据具体的逻辑判断是否需要创建bean对象。

    如果需要创建对象,直接从BeanDefinition中获取相关的配置信息即可。
    就不需要重复的获取注解配置信息参数。

    那么结合这个思想,我们可以定义一个BeanDefinition的类,再扫描到bean的时候,就将对应的设置信息存储起来。

    package cn.xj.spring;
    
    /**
     * bean 的定义修饰类,用于保存bean的修饰信息
     * 
     **/
    public class BeanDefinition {
    
        // 是哪个类
        private Class clazz;
    
        // 单例还是多例
        private String scopeType;
    
        public Class getClazz() {
            return clazz;
        }
    
        public void setClazz(Class clazz) {
            this.clazz = clazz;
        }
    
        public String getScopeType() {
            return scopeType;
        }
    
        public void setScopeType(String scopeType) {
            this.scopeType = scopeType;
        }
    }
    
    

    暂时未考虑懒加载等其他复杂情况!

    在将.class文件转换成Class对象后,判断对象上是否有单例/多例的标识。

    此处还需要写一个标识注解:

    package cn.xj.spring;
    
    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    
    /**
     * 标识bean是单例还是多例
     **/
    @Target(ElementType.TYPE) // 用于类上
    @Retention(RetentionPolicy.RUNTIME) // 运行时有效
    public @interface MyScope {
        String value() default "";
    }
    

    同时指定枚举对象,对其设定值做一个限定:

    package cn.xj.spring;
    
    /**
     * scope 可选参数设置值
     **/
    public enum ScopeEnum {
        SINGLETON("singleton"),
        PROTOTYPE("prototype");
    
        private String value;
    
        ScopeEnum(String value) {
            this.value = value;
        }
    
        public String getValue() {
            return value;
        }
    
        public void setValue(String value) {
            this.value = value;
        }
    }
    

    在对应的bean对象上增加上述的注解设置信息:

    package com.test;
    
    import cn.xj.spring.*;
    
    /**
     * 定义一个bean
     **/
    @MyComponent
    //@MyScope("prototype")
    public class UserService {
    }
    

    在spring框架中,如果未标注 @Scope参数时,默认表示这个bean是一个单例

    接下来继续写MySpringAppAnnotationContext中的逻辑:

    既然有了bean别名Class对象
    那么就只需要解析是否有单例多例标识即可!

    // 扫描中只负责处理类的加载,但不生成bean,考虑到多例bean的获取形式,会在getbean中获取
    // 所以此处是将bean的修饰信息进行保存
    BeanDefinition beanDefinition = new BeanDefinition();
    beanDefinition.setClazz(loadClass);
    // 获取类的scope属性
    if (loadClass.isAnnotationPresent(MyScope.class)) {
        // 存在注解,则获取注解中设定的值,根据值进行判断是单例还是多例
        MyScope myScope = loadClass.getAnnotation(MyScope.class);
        String scopeVal = myScope.value();
        if(ScopeEnum.SINGLETON.getValue().equalsIgnoreCase(scopeVal)){
            beanDefinition.setScopeType(ScopeEnum.SINGLETON.getValue());
        }else{
            // 设置的多例值或者其他值的话,就设置为多例
            beanDefinition.setScopeType(ScopeEnum.PROTOTYPE.getValue());
        }
    }else{
        // 没有这个注解,默认就是单例
        beanDefinition.setScopeType(ScopeEnum.SINGLETON.getValue());
    }
    // 修饰好了类,就将修饰类放入集合
    beanDefinitionMap.put(beanName,beanDefinition);
    

    当然,生成好的BeanDefinition 对象信息,还需要保存至缓存中。

    这里直接写一个Map集合进行代替。

    Map<String,BeanDefinition> beanDefinitionMap = new ConcurrentHashMap<>();
    

    3.11、扫描的整体代码展示

    到此时,bean类的扫描和BeanDefinition 包装已经完成。完整的代码如下所示:

    import java.beans.Introspector;
    import java.io.File;
    import java.lang.reflect.Field;
    import java.lang.reflect.InvocationTargetException;
    import java.net.URL;
    import java.util.Map;
    import java.util.Objects;
    import java.util.concurrent.ConcurrentHashMap;
    
    /**
     * spring自定义容器
     **/
    public class MySpringAppAnnotationContext {
    
        // 指定配置类属性
        private Class aClass;
    
        // bean定义类的集合
        Map<String,BeanDefinition> beanDefinitionMap = new ConcurrentHashMap<>();
        
    	public MySpringAppAnnotationContext(Class aClass) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
    	        this.aClass = aClass;
    	
    	        // 容器初始化,解析对应的配置类
    	        // 1、 扫描
    	        // 判断对象上是否有对应的注解
    	        if (aClass.isAnnotationPresent(MyComponentScan.class)) {
    	            // getDeclaredAnnotation 会忽略继承
    	            // getAnnotation 包括继承的所有注解
    	            //MyComponentScan myComponentScan = (MyComponentScan) aClass.getAnnotation(MyComponentScan.class);
    	            MyComponentScan myComponentScan = (MyComponentScan) aClass.getDeclaredAnnotation(MyComponentScan.class);
    	            String value = myComponentScan.value();
    	            //System.out.println(value);
    	
    	            // 如果未设定扫描的路径值,则默认 配置类 所在的包作为父包
    	            if(value == null || value.length() == 0){
    	                value = aClass.getPackage().getName();
    	
    	            }
    	
    	            // 当前value是注解中的value自定义路径,属于相对路径。
    	            // 需要获取绝对路径,可以依据classpath
    	            // 获取ClassLoader
    	            ClassLoader classLoader = this.getClass().getClassLoader();
    	
    	            // getResource需要传递路径,但value只是包(xx.xx.xx),需要转换成路径(xx/xx/xx)
    	            value = value.replace(".","/");
    	            // 获取相对于 -classpath 的路径
    	            URL resource = classLoader.getResource(value);
    	            // 为了判断当前的包是否是文件夹(filePath 为 绝对路径)
    	            String filePath = resource.getFile();
    	            //System.out.println(filePath); // /E:/study/my_spring/target/classes/com/test
    	            File file = new File(filePath);
    	            // 如果当前文件对象是文件夹
    	            if (file.isDirectory()) {
    	                // 获取文件夹下的所有文件
    	                File[] files = file.listFiles();
    	                for (File file1 : files) {
    	                    String fileName = file1.getAbsolutePath();
    	                    //System.out.println(fileName);
    	                    // 启动可能存在非class文件,需要过滤
    	                    if(!fileName.endsWith(".class")){
    	                        continue;
    	                    }
    	                    //System.out.println("是class文件");
    	                    // 截取包名——类名
    	                    String className = fileName.substring(fileName.indexOf("com"), fileName.indexOf(".class"));
    	                    //System.out.println(className);
    	                    // 因为名称是文件路径,并不是包名称
    	                    className = className.replace("\\", ".");
    	                    //System.out.println(className);
    	
    	                    // 如何将包名+类名  变为class对象,就需要使用到类加载器
    	                    Class<?> loadClass = classLoader.loadClass(className);
    	                    // 并非是每个class文件都是我们所需要的,bean只需要保证携带@Component注解即可
    	                    if (!loadClass.isAnnotationPresent(MyComponent.class)) {
    	                        continue;
    	                    }
    	
    	                    // 满足要求,那么接下来就是将bean构建出来
    	                    // 这里其实还需要判断是否是单例  需要定义 @MyScope  暂时不考虑,统一为单例
    	                    MyComponent myComponent = loadClass.getAnnotation(MyComponent.class);
    	                    String beanName = myComponent.value();
    	                    // 如果在 MyComponent 注解中未指定value属性,此处如何获取?
    	                    if(beanName == null || beanName.length() == 0){
    	                        beanName = Introspector.decapitalize(loadClass.getSimpleName());
    	                    }
    	
    	                    // 扫描中只负责处理类的加载,但不生成bean,考虑到多例bean的获取形式,会在getbean中获取
    	                    // 所以此处是将bean的修饰信息进行保存
    	                    BeanDefinition beanDefinition = new BeanDefinition();
    	                    beanDefinition.setClazz(loadClass);
    	                    // 获取类的scope属性
    	                    if (loadClass.isAnnotationPresent(MyScope.class)) {
    	                        // 存在注解,则获取注解中设定的值,根据值进行判断是单例还是多例
    	                        MyScope myScope = loadClass.getAnnotation(MyScope.class);
    	                        String scopeVal = myScope.value();
    	                        if(ScopeEnum.SINGLETON.getValue().equalsIgnoreCase(scopeVal)){
    	                            beanDefinition.setScopeType(ScopeEnum.SINGLETON.getValue());
    	                        }else{
    	                            // 设置的多例值或者其他值的话,就设置为多例
    	                            beanDefinition.setScopeType(ScopeEnum.PROTOTYPE.getValue());
    	                        }
    	                    }else{
    	                        // 没有这个注解,默认就是单例
    	                        beanDefinition.setScopeType(ScopeEnum.SINGLETON.getValue());
    	                    }
    	                    // 修饰好了类,就将修饰类放入集合
    	                    beanDefinitionMap.put(beanName,beanDefinition);
    	                }
    	            }
    	        }
    	
    	    }
    }
    

    3.12、单例bean的实例化

    扫描结束后,对于单例对象而言,就可以直接先进行实例化操作了。

    继续在构造函数中,编写单例bean的实例化逻辑,其逻辑如下所示:

    // 2、将bean的修饰对象进行遍历,创建bean
    for (String beanName : beanDefinitionMap.keySet()) {
        BeanDefinition beanDefinition = beanDefinitionMap.get(beanName);
        // 判断单例还是多例
        if (ScopeEnum.SINGLETON.getValue().equalsIgnoreCase(beanDefinition.getScopeType())) {
            // 单例bean全局只会有一个对象
            Object bean = createBean(beanName,beanDefinition);
            // 放入bean的单例池中
            singletonObjMap.put(beanName,bean);
        }
    }
    

    既然有Class对象,如何将Class对象转换成对应的实例化对象

    可以使用反射技术,将Class对象进行实例化转换。

    编写创建bean对象方法流程

    // 不需要给外部调用,private处理
    private Object createBean(String beanName,BeanDefinition beanDefinition){
        // 从 BeanDefinition 中获取保存的Class对象
        Class clazz = beanDefinition.getClazz();
        Object obj = null;
        
        try {
        	// 获取无参构造,实例化出对象
            obj = clazz.getDeclaredConstructor().newInstance();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        }
        return obj;
    }
    

    其中,需要将生成的bean对象,保存至缓存中,这里使用map集合替代。

    private Map<String,Object> singletonObjMap = new ConcurrentHashMap<>();
    

    3.13、getBean方法完善

    getBean需要考虑到bean对象可能是多例。如果是多例,则需要创建一个新的bean对象并返回出去。

    public Object getBean(String className) {
        // 通过bean的别名称,获取对应的bean实例化对象
        // 这里需要判断是否是单例还是多例,暂时不考虑复杂的,统一按照单例处理
        BeanDefinition beanDefinition = beanDefinitionMap.get(className);
        if(Objects.isNull(beanDefinition)){
            // 说明bean没有被扫描到
            throw new NullPointerException("没有这个bean");
        }
        // 获取他的作用域
        String scopeType = beanDefinition.getScopeType();
        if(ScopeEnum.SINGLETON.getValue().equalsIgnoreCase(scopeType)){
            // 单例的
            // 判断bean实例池中是否存在bean
            Object obj = singletonObjMap.get(className);
            if(Objects.isNull(obj)){
                obj = createBean(className,beanDefinition);
                // 保存bean池
                singletonObjMap.put(className,obj);
            }
            return obj;
        }else{
            // 多例每次都创建
            return createBean(className,beanDefinition);
        }
    }
    

    4、单例多例对象生成测试

    编写测试类,如下所示:

    package com.test;
    
    import cn.xj.spring.MySpringAppAnnotationContext;
    
    /**
     * 测试类
     **/
    public class Test {
        public static void main(String[] args) throws Exception {
            MySpringAppAnnotationContext context = new MySpringAppAnnotationContext(BeanScan.class);
    
            System.out.println(context.getBean("userService"));
            System.out.println(context.getBean("userService"));
            System.out.println(context.getBean("userService"));
            System.out.println(context.getBean("userService"));
        }
    }
    

    由于此时在com.test.UserService无@MyScope注解修饰,那么默认就是单例

    执行上述的代码,控制台输出信息如下所示:
    在这里插入图片描述

    对象地址一致,每次获取到的bean是同一个对象,满足单例要求。

    com.test.UserService类增加@MyScope("prototype")注解,将其标识为多例。再次执行测试代码,控制台如下所示:
    在这里插入图片描述

    5、完整 MySpringAppAnnotationContext 代码

    package cn.xj.spring;
    
    
    import java.beans.Introspector;
    import java.io.File;
    import java.lang.reflect.Field;
    import java.lang.reflect.InvocationTargetException;
    import java.net.URL;
    import java.util.Map;
    import java.util.Objects;
    import java.util.concurrent.ConcurrentHashMap;
    
    /**
     * spring自定义容器
     **/
    public class MySpringAppAnnotationContext {
    
        // 指定配置类属性
        private Class aClass;
    
        // bean定义类的集合
        Map<String,BeanDefinition> beanDefinitionMap = new ConcurrentHashMap<>();
    
        // 单例对象缓存池
        private Map<String,Object> singletonObjMap = new ConcurrentHashMap<>();
    
        // 构造函数,传递对应的 class类
        public MySpringAppAnnotationContext(Class aClass) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
            this.aClass = aClass;
    
            // 容器初始化,解析对应的配置类
            // 1、 扫描
            // 判断对象上是否有对应的注解
            if (aClass.isAnnotationPresent(MyComponentScan.class)) {
                // getDeclaredAnnotation 会忽略继承
                // getAnnotation 包括继承的所有注解
                //MyComponentScan myComponentScan = (MyComponentScan) aClass.getAnnotation(MyComponentScan.class);
                MyComponentScan myComponentScan = (MyComponentScan) aClass.getDeclaredAnnotation(MyComponentScan.class);
                String value = myComponentScan.value();
                //System.out.println(value);
    
                // 如果未设定扫描的路径值,则默认 配置类 所在的包作为父包
                if(value == null || value.length() == 0){
                    value = aClass.getPackage().getName();
    
                }
    
                // 当前value是注解中的value自定义路径,属于相对路径。
                // 需要获取绝对路径,可以依据classpath
                // 获取ClassLoader
                ClassLoader classLoader = this.getClass().getClassLoader();
    
                // getResource需要传递路径,但value只是包(xx.xx.xx),需要转换成路径(xx/xx/xx)
                value = value.replace(".","/");
                // 获取相对于 -classpath 的路径
                URL resource = classLoader.getResource(value);
                // 为了判断当前的包是否是文件夹(filePath 为 绝对路径)
                String filePath = resource.getFile();
                //System.out.println(filePath); // /E:/study/my_spring/target/classes/com/test
                File file = new File(filePath);
                // 如果当前文件对象是文件夹
                if (file.isDirectory()) {
                    // 获取文件夹下的所有文件
                    File[] files = file.listFiles();
                    for (File file1 : files) {
                        String fileName = file1.getAbsolutePath();
                        //System.out.println(fileName);
                        // 启动可能存在非class文件,需要过滤
                        if(!fileName.endsWith(".class")){
                            continue;
                        }
                        //System.out.println("是class文件");
                        // 截取包名——类名
                        String className = fileName.substring(fileName.indexOf("com"), fileName.indexOf(".class"));
                        //System.out.println(className);
                        // 因为名称是文件路径,并不是包名称
                        className = className.replace("\\", ".");
                        //System.out.println(className);
    
                        // 如何将包名+类名  变为class对象,就需要使用到类加载器
                        Class<?> loadClass = classLoader.loadClass(className);
                        // 并非是每个class文件都是我们所需要的,bean只需要保证携带@Component注解即可
                        if (!loadClass.isAnnotationPresent(MyComponent.class)) {
                            continue;
                        }
    
                        // 满足要求,那么接下来就是将bean构建出来
                        // 这里其实还需要判断是否是单例  需要定义 @MyScope  暂时不考虑,统一为单例
                        MyComponent myComponent = loadClass.getAnnotation(MyComponent.class);
                        String beanName = myComponent.value();
                        // 如果在 MyComponent 注解中未指定value属性,此处如何获取?
                        if(beanName == null || beanName.length() == 0){
                            beanName = Introspector.decapitalize(loadClass.getSimpleName());
                        }
    
                        // 扫描中只负责处理类的加载,但不生成bean,考虑到多例bean的获取形式,会在getbean中获取
                        // 所以此处是将bean的修饰信息进行保存
                        BeanDefinition beanDefinition = new BeanDefinition();
                        beanDefinition.setClazz(loadClass);
                        // 获取类的scope属性
                        if (loadClass.isAnnotationPresent(MyScope.class)) {
                            // 存在注解,则获取注解中设定的值,根据值进行判断是单例还是多例
                            MyScope myScope = loadClass.getAnnotation(MyScope.class);
                            String scopeVal = myScope.value();
                            if(ScopeEnum.SINGLETON.getValue().equalsIgnoreCase(scopeVal)){
                                beanDefinition.setScopeType(ScopeEnum.SINGLETON.getValue());
                            }else{
                                // 设置的多例值或者其他值的话,就设置为多例
                                beanDefinition.setScopeType(ScopeEnum.PROTOTYPE.getValue());
                            }
                        }else{
                            // 没有这个注解,默认就是单例
                            beanDefinition.setScopeType(ScopeEnum.SINGLETON.getValue());
                        }
                        // 修饰好了类,就将修饰类放入集合
                        beanDefinitionMap.put(beanName,beanDefinition);
                    }
                }
            }
    
            // 2、将bean的修饰对象进行遍历,创建bean
            for (String beanName : beanDefinitionMap.keySet()) {
                BeanDefinition beanDefinition = beanDefinitionMap.get(beanName);
                // 判断单例还是多例
                if (ScopeEnum.SINGLETON.getValue().equalsIgnoreCase(beanDefinition.getScopeType())) {
                    // 单例bean全局只会有一个对象
                    Object bean = createBean(beanName,beanDefinition);
                    // 放入bean的单例池中
                    singletonObjMap.put(beanName,bean);
                }
            }
    
            // 3、依赖注入  -- 应该在createbean的时候  就进行属性的注入
    
        }
    
        private Object createBean(String beanName,BeanDefinition beanDefinition){
            Class clazz = beanDefinition.getClazz();
            Object obj = null;
            // 获取无参构造,实例化出对象
            try {
                obj = clazz.getDeclaredConstructor().newInstance();
            } catch (InstantiationException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            } catch (NoSuchMethodException e) {
                e.printStackTrace();
            }
            return obj;
        }
    
        // 获取bean对象
        public Object getBean(String className) {
            // 通过bean的别名称,获取对应的bean实例化对象
            // 这里需要判断是否是单例还是多例,暂时不考虑复杂的,统一按照单例处理
            BeanDefinition beanDefinition = beanDefinitionMap.get(className);
            if(Objects.isNull(beanDefinition)){
                // 说明bean没有被扫描到
                throw new NullPointerException("没有这个bean");
            }
            // 获取他的作用域
            String scopeType = beanDefinition.getScopeType();
            if(ScopeEnum.SINGLETON.getValue().equalsIgnoreCase(scopeType)){
                // 单例的
                // 判断bean实例池中是否存在bean
                Object obj = singletonObjMap.get(className);
                if(Objects.isNull(obj)){
                    obj = createBean(className,beanDefinition);
                    // 保存bean池
                    singletonObjMap.put(className,obj);
                }
                return obj;
            }else{
                // 多例每次都创建
                return createBean(className,beanDefinition);
            }
        }
    }
    
    

    6、bean的扫描代码优化

    博客开始说明的扫描方式,主要是为了演示,为了更容易简单的理解流程,但是如果考虑到下面的这种情况时,可能导致扫描不全:

    如果配置扫描类所在的包下还存在其他的文件夹呢?

    此时则需要将扫描获取className的流程逻辑进行优化操作。

    【思路:】当识别到File对象时文件夹时,可以考虑迭代的方式继续遍历

    迭代能省略很多考虑情况。

    编写单独的解析指定包下的所有classname的方法:

    /**
     * 遍历文件夹,判断文件夹中是否包含文件夹,如果 isRecursion 是true,则遍历到子文件夹继续遍历
     * @param filePath
     * @param packageName
     * @param isRecursion
     * @return
     */
    public Set<String> getClassNameFromDir(String filePath, String packageName, boolean isRecursion){
        Set<String> className = new HashSet<>();
        // 将文件路径转换为对象
        File file = new File(filePath);
        // 获取该File对象下的所有文件信息(含文件夹)
        File[] files = file.listFiles();
        // 遍历判断是文件还是文件夹
        for (File chileFile : files) {
            if(chileFile.isDirectory()){
                // 是文件夹
                if(isRecursion){
                    className.addAll(getClassNameFromDir(chileFile.getPath(),packageName+"."+chileFile.getName(),isRecursion));
                }
            }else{
                // 如果是文件,则判断是否是class文件
                String fileName = chileFile.getAbsolutePath();
                //System.out.println(fileName);
                // 启动可能存在非class文件,需要过滤
                if(!fileName.endsWith(".class")){
                    continue;
                }
                //System.out.println("是class文件");
                // 截取包名——类名
                // 不能把参数写死,依据传递的包名称进行拆分
                String[] split = packageName.split("/");
                String classFileName = fileName.substring(fileName.indexOf(split[0]), fileName.indexOf(".class"));
                //System.out.println(className);
                // 因为名称是文件路径,并不是包名称
                classFileName = classFileName.replace("\\", ".");
                className.add(classFileName);
            }
        }
        return className;
    }
    

    方法定义后,修改扫描的逻辑流程,如下所示:

    // 容器初始化,解析对应的配置类
    // 1、 扫描
    // 判断对象上是否有对应的注解
    if (aClass.isAnnotationPresent(MyComponentScan.class)) {
        // getDeclaredAnnotation 会忽略继承
        // getAnnotation 包括继承的所有注解
        //MyComponentScan myComponentScan = (MyComponentScan) aClass.getAnnotation(MyComponentScan.class);
        MyComponentScan myComponentScan = (MyComponentScan) aClass.getDeclaredAnnotation(MyComponentScan.class);
        String value = myComponentScan.value();
        //System.out.println(value);
    
        // 如果未设定扫描的路径值,则默认 配置类 所在的包作为父包
        if(value == null || value.length() == 0){
            value = aClass.getPackage().getName();
    
        }
    
        // 当前value是注解中的value自定义路径,属于相对路径。
        // 需要获取绝对路径,可以依据classpath
        // 获取ClassLoader
        ClassLoader classLoader = this.getClass().getClassLoader();
    
        // getResource需要传递路径,但value只是包(xx.xx.xx),需要转换成路径(xx/xx/xx)
        value = value.replace(".","/");
        // 获取相对于 -classpath 的路径
        URL resource = classLoader.getResource(value);
        // 为了判断当前的包是否是文件夹(filePath 为 绝对路径)
        String filePath = resource.getFile();
        //System.out.println(filePath); // /E:/study/my_spring/target/classes/com/test
    	
    	// 迭代扫描子包路径
        Set<String> classNameFromDir = getClassNameFromDir(filePath, value, true);
    
        for (String className : classNameFromDir) {
            // 如何将包名+类名  变为class对象,就需要使用到类加载器
            Class<?> loadClass = classLoader.loadClass(className);
            // 并非是每个class文件都是我们所需要的,bean只需要保证携带@Component注解即可
            if (!loadClass.isAnnotationPresent(MyComponent.class)) {
                continue;
            }
    
            // 满足要求,那么接下来就是将bean构建出来
            // 这里其实还需要判断是否是单例  需要定义 @MyScope  暂时不考虑,统一为单例
            MyComponent myComponent = loadClass.getAnnotation(MyComponent.class);
            String beanName = myComponent.value();
            // 如果在 MyComponent 注解中未指定value属性,此处如何获取?
            if(beanName == null || beanName.length() == 0){
                // 将类名的首字母小写处理后,将其作为bean的别名称
                beanName = Introspector.decapitalize(loadClass.getSimpleName());
            }
    
            // 扫描中只负责处理类的加载,但不生成bean,考虑到多例bean的获取形式,会在getbean中获取
            // 所以此处是将bean的修饰信息进行保存
            BeanDefinition beanDefinition = new BeanDefinition();
            beanDefinition.setClazz(loadClass);
            // 获取类的scope属性
            if (loadClass.isAnnotationPresent(MyScope.class)) {
                // 存在注解,则获取注解中设定的值,根据值进行判断是单例还是多例
                MyScope myScope = loadClass.getAnnotation(MyScope.class);
                String scopeVal = myScope.value();
                if(ScopeEnum.SINGLETON.getValue().equalsIgnoreCase(scopeVal)){
                    beanDefinition.setScopeType(ScopeEnum.SINGLETON.getValue());
                }else{
                    // 设置的多例值或者其他值的话,就设置为多例
                    beanDefinition.setScopeType(ScopeEnum.PROTOTYPE.getValue());
                }
            }else{
                // 没有这个注解,默认就是单例
                beanDefinition.setScopeType(ScopeEnum.SINGLETON.getValue());
            }
            // 修饰好了类,就将修饰类放入集合
            beanDefinitionMap.put(beanName,beanDefinition);
        }
    
    }
    
  • 相关阅读:
    在unity中如何利用Xcharts插件进行绘制散点图
    聊聊linux的文件缓存
    STM32使用HAL库驱动DS18B20
    【PyTorch深度学习项目实战100例】—— 基于AlexNet实现宠物小精灵(宝可梦)分类任务 | 第49例
    C++继承与派生解析(继承、重载/转换运算符、多重继承、多态、虚函数/纯虚函数、抽象类)
    钱岭:别担心“35岁危机”,要成为“老专家”
    关于电影的HTML网页设计-威海影视网站首页-电影主题HTM5网页设计作业成品
    LeetCode Hot-100
    超800万辆「巨量」市场,谁在进攻智能驾驶「普及型」赛道
    2022/8/14 考试总结
  • 原文地址:https://blog.csdn.net/qq_38322527/article/details/124954871