当前位置: 首页 > news >正文

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

文章目录

  • 1、前言
  • 2、Spring有什么内容
  • 3、手写Spring开始
    • 3.1、创建工程
    • 3.2、容器创建
    • 3.3、新建扫描配置类和@MyComponentScan注解
    • 3.4、测试类编写调用容器
    • 3.5、创建需要加载的bean和@MyComponent注解
    • 3.6、bean的扫描实现
    • 3.7、根据包路径扫描其下所有满足要求的文件
    • 3.8、使用类加载器loadClass将class文件转换为Class对象
    • 3.9、获取bean的别名称
    • 3.10、封装BeanDefinition
    • 3.11、扫描的整体代码展示
    • 3.12、单例bean的实例化
    • 3.13、getBean方法完善
  • 单例多例对象生成测试
  • 完整 MySpringAppAnnotationContext 代码

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);
    }
}

单例多例对象生成测试

编写测试类,如下所示:

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")注解,将其标识为多例。再次执行测试代码,控制台如下所示:
在这里插入图片描述

完整 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);
        }
    }
}

相关文章:

  • 【Vue】Vue的v-if、v-if-else、v-else-if、v-show的使用
  • 【设计模式】创建型模式:单例模式
  • Sentry、Loki 轻量级日志系统部署及应用
  • Spring Boot 统一功能处理
  • qsort:我很强,了解一下(详解过程)
  • 因为一道题,我把 try-catch-finally 的细节都整理了一遍(1500字)
  • 32、学习 Java 中的注解(参照官方教程)
  • 【第一部分 | HTML】1:揭露HTML的神秘面纱
  • 安装finalshell
  • 怎么找到贵人?
  • pix2pix-论文阅读笔记
  • Idea中重构从精通到陌生
  • springcloud
  • 【机器学习】树模型决策的可解释性与微调(Python)
  • SHRM在中国的认可度如何?这里说了实话
  • Android开发 - 掌握ConstraintLayout(四)创建基本约束
  • co模块的前端实现
  • Effective Java 笔记(一)
  • js
  • Python十分钟制作属于你自己的个性logo
  • session共享问题解决方案
  • 飞驰在Mesos的涡轮引擎上
  • 复习Javascript专题(四):js中的深浅拷贝
  • 官方解决所有 npm 全局安装权限问题
  • 简单易用的leetcode开发测试工具(npm)
  • 聊一聊前端的监控
  • 前端技术周刊 2018-12-10:前端自动化测试
  • 日剧·日综资源集合(建议收藏)
  • 如何进阶一名有竞争力的程序员?
  • 实现简单的正则表达式引擎
  • 找一份好的前端工作,起点很重要
  • 扩展资源服务器解决oauth2 性能瓶颈
  • #Linux杂记--将Python3的源码编译为.so文件方法与Linux环境下的交叉编译方法
  • (1/2)敏捷实践指南 Agile Practice Guide ([美] Project Management institute 著)
  • (52)只出现一次的数字III
  • (Java数据结构)ArrayList
  • (附源码)springboot掌上博客系统 毕业设计063131
  • (附源码)ssm高校志愿者服务系统 毕业设计 011648
  • (十八)三元表达式和列表解析
  • (实战)静默dbca安装创建数据库 --参数说明+举例
  • (未解决)macOS matplotlib 中文是方框
  • (已解决)vue+element-ui实现个人中心,仿照原神
  • (译)2019年前端性能优化清单 — 下篇
  • (转)c++ std::pair 与 std::make
  • (转)JVM内存分配 -Xms128m -Xmx512m -XX:PermSize=128m -XX:MaxPermSize=512m
  • (转)ObjectiveC 深浅拷贝学习
  • (转载)Linux 多线程条件变量同步
  • .apk文件,IIS不支持下载解决
  • .Net 中的反射(动态创建类型实例) - Part.4(转自http://www.tracefact.net/CLR-and-Framework/Reflection-Part4.aspx)...
  • .NET 中使用 Mutex 进行跨越进程边界的同步
  • .netcore 获取appsettings
  • .NET下的多线程编程—1-线程机制概述
  • .NET中使用Protobuffer 实现序列化和反序列化
  • @CacheInvalidate(name = “xxx“, key = “#results.![a+b]“,multi = true)是什么意思
  • [ C++ ] STL_list 使用及其模拟实现