手写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框架的IOC
和AOP
源码之后,很早就像自己动手造一次轮子,这次全当做一次熟悉过程。
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);
}
}
}