spring|我撸了一个 Spring 容器

01. Spring 如何根据注解创建容器?
我们都知道Spring提供了根据注解和xml文件两种方式来创建容器和管理bean的,而在此我们将使用Spring提供的注解创建出容器,并从容器中获取到bean对象。
1. 创建配置类MySpringContext.java,类上添加Spring提供的ComponentScan注解生命扫描包的路径

@ComponentScan({"com.it120"}) public class MySpringContext { public MySpringContext(){ System.out.println("容器初始化中。。。。"); } }

注解中的“com.it120”,表示Spring应该把该路径下贴上@Component注解的类加载到容器中
2. 在需要被Spring容器加载的类上贴上@Component注解:
@Component public class MyBean { public voidtest(){ System.out.println("执行test方法"); } }

以上代码中我们定义了一个MyBean类,并提供了test()方法,类上我们贴上了@Component注解,表示该类将会被加载到Spring容器中
3. 根据配置类创建一个容器,并根据名称获取某个bean:
public static void main(String[] args) { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MySpringContext.class); MyBean myBean = (MyBean) context.getBean("myBean"); myBean.test(); }

以上代码中我们使用了Spring提供的AnnotationConfigApplicationContext类创建了一个容器上下文对象,入参为配置类的Class对象,通过容器上下文getBean(String beanNaem)方法 获取到我们加载到Spring容器中的bean对象,强转之后再调用test()方法,运行结果如图示:
spring|我撸了一个 Spring 容器
文章图片
以上就是根据Spring提供的注解和方法创建的容器和从容器中获取Bean的简单案例,我们暂且不深究其中奥妙,因为我们将会通过自己的创建的注解来实现以上的案例。
02. 创建容器类和自定义组件
创建容器类和自定注解
上一篇中我们使用了Spring提供的AnnotationConfigApplicationContext类来创建了一个容器上下文对象,入参为配置类的Class文件对象。并且该容器上下文对象提供了一个getBean(String beanName)的方法
那么我们可以简化思考为 AnnotationConfigApplicationContext类其实就是一个拥有Class类型成员变量和一个参数的构造器再加上一个getBean()方法的类,我们可以依此创建出容器类如下:
public class MyApplicationContext {// Class类型的成员变量 publicClass clazz; // 构造方法 public MyApplicationContext(Class clazz){ this.clazz=clazz; }// getBean方法,根据名称获取一个bean对象 public Object getBean(String name){ // 先返回null,后续代码补上 return null; } }

创建自定义@ComponentScan()和@Component注解,这两个注解里面都拥有一个String类型的属性,前者中的属性表示包扫描的路径,后者的属性代表某个bean在容器中的名称
@ComponentScan()实现:
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface ComponentScan { String value(); }

@Component实现:
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface Component { String value(); }

注解中的@Target(ElementType.TYPE)表示这个注解可以使用在 类、接口上,@Retention(RetentionPolicy.RUNTIME)表示注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在
定义一个配置类,类上使用@ComponentScan()注解,并指定包扫描路径:
@ComponentScan("com.spring_container.service") public class AppConfig { }

再定义一个将被加载到容器中的类,类上使用@Component注解,指定该bean在容器中的名称:
@Component("myService") public class MyService { }

到此我们已经把基本的类结构和注解定义完成可以在main方法中进行一个“假容器”的创建了如下:
public static void main(String[] args) throws ClassNotFoundException { //手写实现Spring容器 MyApplicationContext myApplicationContext= new MyApplicationContext(AppConfig.class); System.out.println(myApplicationContext.getBean("myService")); }

但是现在我们运行其实也不会返回什么,因为我们还没完成bean对象的创建,所以这是个“空壳容器”
03. 解析注解获取包扫描路径
解析@ComponentScan注解获取注解属性值,获取该路径下所有.class文件
在上文中我们通过自定义注解和自定义容器类搭建了一个“空壳容器”,在本篇内容我们将逐步完成包扫描的过程。包扫描的流程大致可分为如下步骤:
  1. 解析注解获取注解的属性值
  2. 根据注解属性值,获取该路径下所有文件
  3. 通过ClassLoader 加载.class文件
包扫描和bean对象的创建都是需要在容器类中的构造方法进行创建处理的,我们可以把包扫描的步骤定义在一个方法内 名为scan(Class clazz) 之后在构造器中调用此方法即可:构造方法如下:
// 构造方法 public MyApplicationContext(Class aClass) throws ClassNotFoundException { this.aClass=aClass; // 扫描路径- scan(aClass); }

scan方法:
private void scan(Class aClass) { //扫描包的逻辑代码 }

在Scan方法中我们第一步需要获取到注解中的扫描路径:在scan方法中添加如下代码:
//1.获取传入的配置类上的@ComponentScan里面的参数,包的扫描路径 ComponentScan componentScan = (ComponentScan)aClass.getDeclaredAnnotation(ComponentScan.class); String path = componentScan.value(); System.out.println(path);

输出的包扫描路径如图:
spring|我撸了一个 Spring 容器
文章图片
获取到包扫描路径后,需要根据该路径获取到该路径下所有的文件
//1.获取传入的配置类上的@ComponentScan里面的参数,包的扫描路径 ComponentScan componentScan = (ComponentScan)aClass.getDeclaredAnnotation(ComponentScan.class); String path = componentScan.value(); ClassLoader classLoader = MyApplicationContext.class.getClassLoader(); // 获取path下所有资源 URL resource = classLoader.getResource(path.replace(".", "/")); // 获取文件 File file = new File(resource.getFile()); if(file.isDirectory()){ // 如果是文件夹 File[] files = file.listFiles(); for (File f: files) { // 输出每一个文件的地址 System.out.println(f.getAbsolutePath()); } }

运行结果如图所示:
spring|我撸了一个 Spring 容器
文章图片
三种类加载器
  1. 启动类加载器(Bootstrap classLoader),加载的是jre/lib下的文件
  2. 拓展类加载器(Extension classLoader),加载的是/jre/ext/lib下的文件
  3. 应用类加载器(appclassloader)这个加载器就是加载用户所自定义的类的,加载的是classpath路径下的文件,那classpath路经指的是哪?看下图
spring|我撸了一个 Spring 容器
文章图片
我们从idea的启动参数log中看到有一个Classpath对应的参数,而这里的classpath指的是相对于Target/classes/下的文件,所以我们的appclassloader将会加载classes/下面的所有文件
第三步根据包扫描路径下所有.class文件生成Class对象,这里分两个小步,第一步获取类的全限定类名,第二步通过全限定类名生成Class对象。
1. 通过字符串的切割和替换最终得到了包下全部类的全限定类名
for (File f: files) { if(f.getAbsolutePath().endsWith(".class")){ String absolutePath = f.getAbsolutePath(); String filePath = absolutePath.substring(absolutePath.indexOf("com"), absolutePath.indexOf(".class")); String className = filePath.replace("\\", "."); System.out.println(className); }}

spring|我撸了一个 Spring 容器
文章图片
2. 通过类加载器获取到Class对象:
// 通过类加载器,加载类 Class clazz = classLoader.loadClass(className); System.out.println(clazz);

spring|我撸了一个 Spring 容器
文章图片
在上文中我们通过获取注解属性值,并通过该值加载.calss文件,最终通过类加载器获取到了Class对象,获取到类的Class对象之后我们就可以通过反射来创建出类的对象了
04. 单例池和BeanDefinition对象
在上文中我们通过获取注解属性值,并通过该值加载.calss文件,最终通过类加载器获取到了Class对象,获取到类的Class对象之后我们就可以通过反射来创建出类的对象了
我们都知道Spirng中Bean的作用域有以下几种:
  1. 原型(prototype):每次通过 Spring 容器获取 prototype 定义的 bean 时,容器都将创建一个新的 Bean 实例,每个 Bean 实例都有自己的属性和状态
  2. 单例(singleton):Spring IoC 容器中只会存在一个共享的 Bean 实例,无论有多少个Bean 引用它,始终指向同一对象。该模式在多线程下是不安全的。Singleton 作用域是Spring 中的缺省作用域,也可以显示的将 Bean 定义为 singleton 模式
  3. request:在一次 Http 请求中,容器会返回该 Bean 的同一实例。而对不同的 Http 请求则会产生新的 Bean,而且该 bean 仅在当前 Http Request 内有效,当前 Http 请求结束,该 bean实例也将会被销毁
  4. session:在一次 Http Session 中,容器会返回该 Bean 的同一实例。而对不同的 Session 请求则会创建新的实例,该 bean 实例仅在当前 Session 内有效。同 Http 请求相同,每一次session 请求创建新的实例,而不同的实例之间不共享属性,且实例仅在自己的 session 请求内有效,请求结束,则实例将被销毁
  5. global Session:在一个全局的 Http Session 中,容器会返回该 Bean 的同一个实例,仅在使用 portlet context 时有效
在本篇文章中我们将讲解原型bean和单例bean的区别和实现,首先我们先来看以下代码:
/** * 单例模式 */ public class Singleton { privatestatic Singleton singleton; // 声明为私有之后在其他内就无法使用 该构造器来创建新的对象 private Singleton(){} //只提供唯一一个访问此对象的方法 public static Singleton getSingleton(){ if(singleton==null){ return new Singleton(); } return singleton; } }//调用 public static void main(String[] args){ System.out.println(Singleton.getSingleton()); System.out.println(Singleton.getSingleton()); System.out.println(Singleton.getSingleton()); }

运行截图:
spring|我撸了一个 Spring 容器
文章图片

以上就是一个单例模式的小案例,通过运行我们发现多次调用getSingleton()方法返回的都是通一个对象,正是对应了作用域为单例的Spring bean 如果我们把代码改为以下:
public static Singleton getSingleton(){ return new Singleton(); }

则运行结果如图下所示:
spring|我撸了一个 Spring 容器
文章图片

我们修改代码之后每调用一个getSingleton(),就会创建出一个新的对象,所有打印出来的对象自然是不相同的,而这种对象在Spring中称为原型bean
通过以上代码我们基本了解了什么是单例bean和原型bean的区别了,那Spring容器是如何区分这两bean呢?是不是也像上面一样写了个单例模式呢?
单例池
单例池顾名思义 “池”中保存的bean都是单例bean,当你想要获取的bean是存在这个“池”中的,存在即可返回不用再创建,这样也就实现了每次获取都是同一个对象,那这个单例池的是何种数据结构呢? 在此我们使用了ConcurrentHashMap 来作为单例池的存储结构
BeanDefinition对象
BeanDefinition对象是bean的定义,而非bean的对象,在beanDefinition对象中我们仅提供了 Class 类型的成员变量和bean作用域的成员变量:如下
//bean定义对象 public class BeanDefinition { // 类型 private Class clazz; // bean的作用域 private String scope; }

我们接着上篇文章的进度,上篇文章中我们是通过类加载器加载了指定扫描包路径下的所有Class对象。本文我们将会完成以下内容:
  1. 判断某个bean是否为单例类型,如果为单例类型则将该bean对象存入到单例池中
  2. 在扫描包的过程中将生成bean的定义对象,将bean定义对象存入到一个ConcurrentHashMap中,以供后续创建bean和获取bean时通过bean名称获取Class对象
第一步:我们需要定义一个注解表示该bean为单例bean还是原型bean
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface Scope { String value(); }

注解的定义如上,在使用该注解时定义某个bean为单例时需要,传入bean的类型 如 :“@Scope("prototype")” 则表示该bean的类型为原型bean,如果不用此注解则默认为单例bean 在容器中声明两个map成员如下:
//单例池 private ConcurrentHashMap singletonMap =new ConcurrentHashMap<>(); // BeanDefinition private ConcurrentHashMap beanDefinitionMap =new ConcurrentHashMap<>();

我们在scan方法中补充以下代码:
// 通过类加载器,加载类 Class clazz = classLoader.loadClass(className); if(clazz.isAnnotationPresent(Component.class)){ // 如果该类上存在@Component注解 Component component =clazz.getDeclaredAnnotation(Component.class); // 定义一个beanDefinition对象 BeanDefinition beanDefinition = new BeanDefinition(); beanDefinition.setClazz(clazz); if(clazz.isAnnotationPresent(Scope.class) ){ //该类上有@Scope注解注释 beanDefinition.setScope(clazz.getDeclaredAnnotation(Scope.class).value()); }else{ //默认单例bean beanDefinition.setScope("singleton"); } String beanName = component.value(); beanDefinitionMap.put(beanName,beanDefinition); }

再以上代码中我们已经生成好了每个bean的bean定义对象,并以bean的名称作为key,beanDefination对象为value存到了beanDefinitionMap中。
到此包扫描的过程已经完成了,但是我们还是需要把单例的bean存入到单例池的map中 在构造器中添加以下代码:
// 构造方法 public MyApplicationContext(Class aClass) throws ClassNotFoundException { this.aClass=aClass; // 扫描路径----->beanDefinition---->beanDefinitionMap scan(aClass); // 单例bean处理 for (Map.Entry entry : beanDefinitionMap.entrySet()) { String key = entry.getKey(); BeanDefinition beanDefinition = (BeanDefinition)entry.getValue(); if(beanDefinition.getScope().equals("singleton")){ Object o= createBean(beanDefinition); singletonMap.put(key,o); } } }

以上代码块中,我们遍历了了beanDefinitionMap,把map中定义为单例bean的bean对象存到单例池中。
getBean()方法的改造:在getBean()方法中我们首先区判断该bean是否为单例bean如果为单例bean则从单例池中获取即可,如不是单例类型则需要进行bean的创建,补充getBean()方法如下:
// getBean方法,根据名称获取一个bean对象 public Object getBean(String name){ // 根据bean名称,获取bean定义 if(beanDefinitionMap.containsKey(name)){ BeanDefinition beanDefinition =(BeanDefinition) beanDefinitionMap.get(name); if(beanDefinition.getScope().equals("singleton")){ // 从单例池中获取 returnsingletonMap.get(name); }else { // 创建bean return createBean(beanDefinition); } }else{ // 不存在对应的bean throw new NullPointerException(); } }

创建以上代码中createBean()方法,并补充createBean()代码,在createBean中我们暂且使用反射来创建一个简单的对象如下:
private Object createBean(BeanDefinition beanDefinition) { try { // 根据beanDefinition对象创建bean对象 Class clazz = beanDefinition.getClazz(); //通过反射生成对象 return clazz.newInstance(); } catch (Exception e) { e.printStackTrace(); return null; } }

测试单例池是否生效,此时我们的MyService类如下:
@Component("myService")@Scope("prototype")public class MyService {}

我们使用了@Scope("prototype")表示bean的类型为原型bean,每次调用都会创建一个新的bean
而另外一个xxxService则只使用了@Component注解,默认为单例bean:如下
@Component("xxxService")public class xxxService {}

测试结果如图所示:
spring|我撸了一个 Spring 容器
文章图片

看到上图的运行结果,确实是如我们代码所写的单例bean无论多次获取都是返回的是同一个对象,而原型bean则是每次都创建了一个新的对象
05. 简易版@Autowired依赖注入实现
在上文中我们主要讲解了bean的单例和原型作用域的区别以及单例池和BeanDefinition对象的作用和使用,将扫描路径下的bean的定义,存入到了map中,也将作用域为单例的bean存入了单例池中。
在本篇文章中我们主要讲解简易版@Autowired依赖注入的实现。
创建@Autowired注解实现:
@Target({ElementType.FIELD,ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface Autowired { }

新增一个类并使用@Component注解,表示该类将会被加载到容器中
@Component("orderService") public class OrderService { }

改造MyService类,引入OrderService 成员变量如下:
@Component("myService") @Scope("prototype") public class MyService {@Autowired private OrderService orderService; public OrderService getOrderService() { return orderService; } }

在以上代码中我们使用了@Autowired 注入了一个OrderService类,并提供了get方法,修改测试类如下:
public class MainTest { public static void main(String[] args) throws ClassNotFoundException { //手写实现Spring容器 MyApplicationContext myApplicationContext= new MyApplicationContext(AppConfig.class); MyService myService = (MyService) myApplicationContext.getBean("myService"); System.out.println(myService.getOrderService()); } }

此时我们运行main方法结果如下:
spring|我撸了一个 Spring 容器
文章图片

以上图示我们虽然使用了@Autowired把OrderService 注入到了MyService中,但是其真正本质的代码却还没有写,所有此时我们从MyService对象中取出OrderService自然为空的,接下来我们需要在createBean()方法中,构造其类于类的关系。
改造createBean()方法如下:
private Object createBean(BeanDefinition beanDefinition) { try { // 根据beanDefinition对象创建bean对象 Class clazz = beanDefinition.getClazz(); Object instance = clazz.newInstance(); //获取类的所有成员变量 Field[] fields = clazz.getDeclaredFields(); for (Field field: fields) { // 如果该成员变量被@Autowired注解标识 if(field.isAnnotationPresent(Autowired.class)){ // 成员变量名称 String name = field.getName(); // 根据名称获取bean Object bean = getBean(name); field.setAccessible(true); // 将获取到的bean设置到类对象中 field.set(instance,bean); } } return instance; } catch (Exception e) { e.printStackTrace(); return null; } }

在以上代码块中我们,通过反射创建了类对象,遍历类中的所有成员变量,如果成员变量被@Autowired注解标识,我们则通过成员变量的名称,创建bean 再把生成的bean设置到类对象中,这样就可以实现一个简单的依赖注入
此时的运行结果如下:
spring|我撸了一个 Spring 容器
文章图片

从运行结果来看我们自定义的简易版依赖注入是成功得到效果的!
BeanNameAware接口 在Spirng中提供了一个接口叫做BeanNameAware,这个接口中提供了一个setBeanName()的方法,用来实现让Bean获取自己在BeanFactory配置中的名字(根据情况是id或者name),在此我们创建一个BeanNameAware接口来实现这个功能,让生成的bean知道自己的bean
创建beanNameAware接口:
public interface BeanNameAware { void setBeanName(String name); }

在Myservice类中添加 成员变量,并实现BeanNameAware接口,重写其方法如下:
@Component("myService") @Scope("prototype") public class MyService implements BeanNameAware {@Autowired private OrderService orderService; private String beanName; public String getBeanName() { return beanName; }public OrderService getOrderService() { return orderService; }@Override public void setBeanName(String name) { this.beanName=name; } }

继续改造createBean()方法添加如下:
if(instance instanceof BeanNameAware){ // 如果instance实现类BeanNameAware接口 ((BeanNameAware) instance).setBeanName(beanName); }

完整的createBean方法为:
在此需要修改createBean方法,加入一个beanName参数
private Object createBean(String beanName,BeanDefinition beanDefinition) { try { // 根据beanDefinition对象创建bean对象 Class clazz = beanDefinition.getClazz(); Object instance = clazz.newInstance(); //获取类的所有成员变量 Field[] fields = clazz.getDeclaredFields(); for (Field field: fields) { // 如果该成员变量被@Autowired注解标识 if(field.isAnnotationPresent(Autowired.class)){ // 成员变量名称 String name = field.getName(); // 根据名称获取bean Object bean = getBean(name); field.setAccessible(true); // 将获取到的bean设置到类对象中 field.set(instance,bean); } } if(instance instanceof BeanNameAware){ // 如果instance实现类BeanNameAware接口 ((BeanNameAware) instance).setBeanName(beanName); } return instance; } catch (Exception e) { e.printStackTrace(); return null; } }

修改测试类,从bean中获取BeanName,运行结果如下:
spring|我撸了一个 Spring 容器
文章图片

从上图中我们可以看到,通过代码改造我们是可以获取出bean的名称的,本篇文章中我们简易实现了依赖注入和BeanNameAware接口的实现,虽然不及Spring源码级别深奥但是对于我们理解Spring源码还是很有帮助的
InitializingBean和BeanPostProcessor接口
在上文中我们通过bean实现beanAware接口实现了给类的成员变量回调赋值,在Spring中提供了一个名为InitializingBean的接口,通过实现该接口,可以在bean初始化的时候根据用户的需要实现InitializingBean接口中并重写其中的方法
如下是Spring中提供的InitializingBean接口:
public interface InitializingBean { void afterPropertiesSet() throws Exception; }

第一步 同样我们也模拟实现这个接口:首先我们也自定义接口如上接口和抽象方法,修改MyService 实现InitializingBean 并从写其方法如下:
@Component("myService") @Scope("prototype") public class MyService implements BeanNameAware, InitializingBean {@Autowired private OrderService orderService; private String beanName; public String getBeanName() { return beanName; }public OrderService getOrderService() { return orderService; }@Override public void setBeanName(String name) { this.beanName=name; }// 重写 InitializingBean 抽象方法 @Override public void afterPropertiesSet() throws Exception { System.out.println("调用了afterPropertiesSet方法"); } }

以上代码中我们重写InitializingBean接口中的方法时只是简单的打印输出一句话
第二步 在createBean方法中改造代码,如果当前bean实现了InitializingBean接口,则需要调用其方法:
// 初始化 if(instance instanceof InitializingBean){ //如果instance实现类InitializingBean接口,则调用其抽象方法 ((InitializingBean) instance).afterPropertiesSet(); }

运行结果如下:
spring|我撸了一个 Spring 容器
文章图片

通过以上运行截图来看,我们在createBean()代码中判断,如果当前bean是InitializingBean的子类时我们直接调用了接口中的方法,在此我们仅仅重写了其接口的方法并只是打印出了一句话,在Spirng中提供了一个可扩展性的接口BeanPostProcessor
BeanPostProcessor
BeanPostProcessor接口我们可以理解为Spring提供的一个扩展接口,在源码中该接口和抽象方法如下:
public interface BeanPostProcessor { @Nullable default Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { return bean; } @Nullable default Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { return bean; } }

上文中我们自定义了InitializingBean接口,而在BeanPostProcessor源码我们可以看到其中两个方法,一个是初始化之前执行一个是初始化之后执行的,我们同样自定义一个BeanPostProcessor并提供类似的两个抽象方法,修改扫描包阶段的代码逻辑如果该类是实现了BeanPostProcessor接口则反射创建出该对象实例,放到一个集合里边,当在bean初始化之前调用,修改scan代码如下:
在容器类中声明一个List集合用于存放bean对象:
//BeanPostProcessor private List beanPostProcessorList=new ArrayList<>();

spring|我撸了一个 Spring 容器
文章图片

修改createBean方法,在初始化之前,和初始化之后分别执行:
spring|我撸了一个 Spring 容器
文章图片

修改MyService实现BeanPostProcessor接口和重写其中方法
@Component("myService") @Scope("prototype") public class MyService implements BeanNameAware, InitializingBean,BeanPostProcessor {...// 重写 InitializingBean 抽象方法 @Override public void afterPropertiesSet() throws Exception { System.out.println("调用了afterPropertiesSet方法"); }@Override public Object postProcessBeforeInitialization(Object bean, String beanName) { System.out.println("初始化之前运行"+beanName); return bean; }@Override public Object postProcessAfterInitialization(Object bean, String beanName) { System.out.println("初始化之后运行"+beanName); return bean; } }

启动项目运行测试:
spring|我撸了一个 Spring 容器
文章图片

从上图中我们可以看到并不是只有实现了BeanPostProcessor接口的类会执行执行之前和执行之后的方法,凡是所有的注入容器的bean都会执行初始化之前和初始化之后的方法。
总结:本文主要讲解了初始化InitializingBean接口和BeanPostProcessor接口,主要的功能就是我们可以在bean创建和扫描的过程加入自定义的处理逻辑,这也是Spring源码提供的扩展性接口。
【spring|我撸了一个 Spring 容器】推荐阅读
1. GitHub 上有什么好玩的项目?
2. 这个牛逼哄哄的数据库开源了
3. SpringSecurity + JWT 实现单点登录
4. 100 道 Linux 常见面试题
spring|我撸了一个 Spring 容器
文章图片

    推荐阅读