一、前言

一共8个类,撸一个IOC容器。当然,我们是很轻量级的,但能够满足基本需求。想想典型的 Spring 项目,是不是就是各种Service/DAO/Controller,大家互相注入,就组装成了我们的业务bean,然后再加上 Spring MVC,再往容器里一放,基本齐活。

我们这篇文章,就是要照着 spring 来撸一个 相当简单的 IOC 容器,这个容器可以完成以下功能:

1、在 xml 配置文件里配置 bean 的扫描路径,语法目前只支持 component-scan,但基本够用了;

2、Bean 用 Component 注解,bean 中属性可以用 Autowired 来进行自动注入。

3、可以解决循环依赖问题。

bean的长相,基本就是下面这样:

@Data
@Component
public class Girl {
private String name = "catalina";
private String height;
private String breast;
private String legLength; private Boolean isPregnant; @Autowired
private com.ckl.littlespring.Coder coder;
}

  

xml,长下面这样:

<?xml version="1.0" encoding="UTF-8"?>
<beans>
<component-scan base-package="com.ckl.littlespring"/>
</beans>

二、思路

1、要解析xml,这个可以用Tomcat Digester 实现,这个是神器,用这个基本解决了读配置文件的问题;

2、读取xml配置的包名下的所有class,这个可以参考Spring,我是在网上找的一个工具类,反正就是利用类加载器获取classpath下的jar、class等,然后根据包名来过滤;

3、从第二步获取的class集合中,过滤出来注解了 @Component 的类,并利用反射读取其name、type、field集合等,其中field集合需要把带有 @Autowired 的过滤出来,用所有这些信息,构造一个 BeanDefinition 对象,放到 BeanDefinition 集合;

4、遍历第三步的BeanDefinition集合,根据 BeanDefinition 生成 Bean,如果该 BeanDefinition 中的field依赖了其他bean,则递归处理,获取到 field 后,反射设置到 bean中。

三、实现

1、代码结构、效果展示

强烈建议大家直接把代码拉下来跑,跑一跑,打个断点,几乎都不用看我写的了。源码路径:

https://github.com/cctvckl/tomcat-saxtest/blob/master/src/main/java/com/ckl/littlespring/TestLittleSpring.java

代码结构如下图:

大家看上图,测试类中,主要是 new了 BeanDefinitionRegistry,这个就是我们的 bean 容器,可理解为 Spring 里面的 org.springframework.beans.factory.support.DefaultListableBeanFactory,当然,我们的是玩具而已。该类的构造函数,接收一个参数,就是配置文件的位置,默认会去classpath下查找该文件。bean 容器生成后,我们手动调用 refresh 来初始化容器,并生成 bean。 最后,我们既可以通过  getBeanByType(Class clazz) 来获取想要的 bean了。

在测试代码中,Girl 和 Coder 循环依赖,咱们可以看看实际执行效果:

 package com.ckl.littlespring;

 import com.ckl.littlespring.annotation.Autowired;
import com.ckl.littlespring.annotation.Component;
import lombok.Getter;
import lombok.Setter; @Getter
@Setter
@Component
public class Coder {
private String name = "xiaoming"; private String sex; private String love;
/**
* 女朋友
*/
@Autowired
private com.ckl.littlespring.Girl girl; }
 package com.ckl.littlespring;

 import com.ckl.littlespring.annotation.Autowired;
import com.ckl.littlespring.annotation.Component;
import com.coder.SaxTest;
import lombok.Data; @Data
@Component
public class Girl {
private String name = "catalina";
private String height;
private String breast;
private String legLength; private Boolean isPregnant; @Autowired
private com.ckl.littlespring.Coder coder; }

可以看到,没什么问题,好了,接下来,看实现,我们按初始化--》使用的步骤来。

2、BeanDefinitionRegistry 初始化

    /**
* bean定义解析器
*/
private BeanDefinitionParser parser; public BeanDefinitionRegistry(String configFileLocation) {
parser = new BeanDefinitionParser(configFileLocation);
}

该bean容器中,构造函数中,将配置文件直接传给了解析器,解析器 BeanDefinitionParser 会真正负责从 xml 文件内读取 BeanDefinition。

3、 BeanDefinitionParser 初始化

 @Data
public class BeanDefinitionParser {
/**
* xml 解析器
*/
private Digester digester;
  
private String configFileLocation; private List<MyBeanDefiniton> myBeanDefinitonList = new ArrayList<>(); public BeanDefinitionParser(String configFileLocation) {
this.configFileLocation = configFileLocation;
digester = new Digester();
}
}

BeanDefinitionParser 中一共三个field,一个为配置文件位置,一个为Tomcat Digester,一个用于存储解析到的 BeanDefinition。Tomcat Digester用于解析 xml,这个一会实际的解析过程我们再说它。构造函数中,主要是给配置文件赋值,以及生成 Digester实例。

4、refresh 方法解析

初始化完成后,调用BeanDefinitionRegistry 的 refresh 解析:

     public void refresh() {
/**
* 判断是否已经解析完成bean定义。如果没有完成,则先进行解析
*/
if (!hasBeanDefinitionParseOver) {
6 parser.parse();
hasBeanDefinitionParseOver = true;
} /**
* 初始化所有的bean,完成自动注入
*/
for (MyBeanDefiniton beanDefiniton : getBeanDefinitions()) {
getBean(beanDefiniton);
}
}

这里,关注第6行,因为是首次解析,所以要进入BeanDefinitionParser .parse方法。

 /**
* 根据指定规则,解析xml
*/
public void parse() {
digester.setValidating(false);
digester.setUseContextClassLoader(true); // Configure the actions we will be using
digester.addRule("beans/component-scan",
10 new ComponentScanRule(this)); InputSource inputSource = null;
InputStream inputStream = null;
try {
inputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream(configFileLocation);
inputSource = new InputSource(inputStream);
inputSource.setByteStream(inputStream);
18 Object o = digester.parse(inputSource);
System.out.println(o);
} catch (Exception e) {
e.printStackTrace();
}
}

先关注第9、10行,配置解析规则,在解析xml时,遇到 beans元素下的component-scan时,则回调 ComponentScanRule 的规则。第18行,真正开始解析xml。

我们看看 ComponentScanRule 的实现:

 package com.ckl.littlespring.parser;

 import org.apache.commons.digester3.Rule;
import org.xml.sax.Attributes; public class ComponentScanRule extends Rule { private String basePackage; private BeanDefinitionParser beanDefinitionParser; public ComponentScanRule(BeanDefinitionParser beanDefinitionParser) {
this.beanDefinitionParser = beanDefinitionParser;
} @Override
public void begin(String namespace, String name, Attributes attributes) throws Exception {
25 basePackage = attributes.getValue("base-package");
26 beanDefinitionParser.doScanBasePackage(basePackage);
} @Override
public void end(String namespace, String name) throws Exception { }
}

关注25/26行,这里从xml中获取属性 base-package,然后再调用 com.ckl.littlespring.parser.BeanDefinitionParser#doScanBasePackage 来进行处理。

 /**
* 当遇到component-scan元素时,该函数被回调,解析指定包下面的bean 定义,并加入bean 定义集合
* @param basePackage
*/
public void doScanBasePackage(String basePackage) {
Set<Class<?>> classSet = ClassUtil.getClasses(basePackage); if (classSet == null) {
return;
} //过滤出带有Component注解的类,并将其转换为beanDefinition
List<Class<?>> list = classSet.stream().filter(clazz -> clazz.getAnnotation(Component.class) != null).collect(Collectors.toList()); for (Class<?> clazz : list) {
MyBeanDefiniton myBeanDefiniton = BeanDefinitionUtil.convert2BeanDefinition(clazz);
myBeanDefinitonList.add(myBeanDefiniton);
} }

以上方法执行结束时,basePackage下的被 Component 注解的 class就收集完毕。具体怎么实现的?

1、调用  ClassUtil.getClasses(basePackage); 来获取指定包下面的全部class

2、从第一步的集合中,过滤出带有 Component 注解的class

3、利用 工具类 BeanDefinitionUtil.convert2BeanDefinition,从class 中提取 bean 定义的各类属性。

先看看 BeanDefinition 的定义:

 package com.ckl.littlespring.parser;

 import lombok.Data;

 import java.lang.reflect.Field;
import java.util.List; @Data
public class MyBeanDefiniton { /**
* bean的名字,默认使用类名,将首字母变成小写
*/
private String beanName; /**
* bean的类型
*/
private String beanType; /**
* bean的class
*/
private Class<?> beanClazz; /**
* field依赖的bean
*/
private List<Field> dependencysByField; }

就几个属性,相当简单,下面看 BeanDefinitionUtil.convert2BeanDefinition:

  public static MyBeanDefiniton convert2BeanDefinition(Class<?> clazz){
MyBeanDefiniton definiton = new MyBeanDefiniton();
String name = clazz.getName();
definiton.setBeanName(name.substring(0,1).toLowerCase() + name.substring(1));
definiton.setBeanType(clazz.getCanonicalName());
definiton.setBeanClazz(clazz); Field[] fields = clazz.getDeclaredFields();
if (fields == null || fields.length == 0){
return definiton;
} ArrayList<Field> list = new ArrayList<>();
list.addAll(Arrays.asList(fields));
List<Field> dependencysField = list.stream().filter(field -> field.getAnnotation(Autowired.class) != null).collect(Collectors.toList());
16 definiton.setDependencysByField(dependencysField); return definiton;
}

最重要的就是第 15/16行,从所有的 field 中获取 带有 autowired 注解的field。这些 field 都是需要进行自动注入的。

执行完以上这些后,com.ckl.littlespring.parser.BeanDefinitionParser#myBeanDefinitonList 就持有了所有的 BeanDefinition。 下面就开始进行自动注入了,let's go!

5、bean初始化,完成自动注入

我们接下来,再看一下 BeanDefinitionRegistry 中的refresh,上面我们完成了 parser.parse 方法,此时,BeanDefinitionParser#myBeanDefinitonList 已经准备就绪了。

 public void refresh() {
/**
* 判断是否已经解析完成bean定义。如果没有完成,则先进行解析
*/
if (!hasBeanDefinitionParseOver) {
parser.parse();
hasBeanDefinitionParseOver = true;
} /**
* 初始化所有的bean,完成自动注入
*/
for (MyBeanDefiniton beanDefiniton : getBeanDefinitions()) {
getBean(beanDefiniton);
}
}

我们要关注的是,第13行,getBeanDefinitions()主要是从 parser 中获取 BeanDefinition 集合。因为是内部使用,我们定义为private。

     private List<MyBeanDefiniton> getBeanDefinitions() {
return parser.getBeanDefinitions();
}

然后,我们关注第14行,getBean 会真正完成 bean 的创建,如果有依赖的field,则会进行注入。

 /**
* 根据bean 定义获取bean
* 1、先查bean容器,查到则返回
* 2、生成bean,放进容器(此时,依赖还没注入,主要是解决循环依赖问题)
* 3、注入依赖
*
* @param beanDefiniton
* @return
*/
private Object getBean(MyBeanDefiniton beanDefiniton) {
Class<?> beanClazz = beanDefiniton.getBeanClazz();
Object bean = beanMapByClass.get(beanClazz);
if (bean != null) {
return bean;
} //没查到的话,说明还没有,需要去生成bean,然后放进去
try {
bean = beanClazz.newInstance();
} catch (InstantiationException | IllegalAccessException e) {
e.printStackTrace();
return null;
} // 先行暴露,解决循环依赖问题
beanMapByClass.put(beanClazz, bean); //注入依赖,如果没有依赖的field,直接返回
List<Field> dependencysByField = beanDefiniton.getDependencysByField();
if (dependencysByField == null) {
return bean;
} for (Field field : dependencysByField) {
try {
36 autowireField(beanClazz, bean, field);
} catch (Exception e) {
throw new RuntimeException(beanClazz.getName() + " 创建失败",e);
}
} return bean;
}

在这个方法里,我们主要就是,创建了 bean,并且放到了 容器中(一个hashmap,key是class,value就是对应的bean实例)。我们关注第36行,这里会进行field 的注入:

 private void autowireField(Class<?> beanClazz, Object bean, Field field) {
Class<?> fieldType = field.getType();
List<MyBeanDefiniton> beanDefinitons = getBeanDefinitions();
if (beanDefinitons == null) {
return;
} // 根据类型去所有beanDefinition看,哪个类型是该类型的子类;把满足的都找出来
List<MyBeanDefiniton> candidates = beanDefinitons.stream().filter(myBeanDefiniton -> {
10 return fieldType.isAssignableFrom(myBeanDefiniton.getBeanClazz());
}).collect(Collectors.toList()); if (candidates == null || candidates.size() == 0) {
throw new RuntimeException(beanClazz.getName() + "根据类型自动注入失败。field:" + field.getName() + " 无法注入,没有候选bean");
}
if (candidates.size() > 1) {
throw new RuntimeException(beanClazz.getName() + "根据类型自动注入失败。field:" + field.getName() + " 无法注入,有多个候选bean" + candidates);
} MyBeanDefiniton candidate = candidates.get(0);
Object fieldBean;
try {
// 递归调用
24 fieldBean = getBean(candidate);
field.setAccessible(true);
field.set(bean, fieldBean);
} catch (Exception e) {
throw new RuntimeException("注入属性失败:" + beanClazz.getName() + "##" + field.getName(), e);
} }

这里,我们先看第10行,我们要根据field 的 类型,看看当前的bean 容器中有没有 field 类型的bean,比如我们的 field 的类型是个接口,那我们就会去看有没有实现类。

这里有两个异常可能会抛出,如果一个都没找到,无法注入;如果找到了多个,我们也判断为无法注入。(基础版本,暂没考虑 spring 中的 qualifier 注解)

最后,我们在第24行,根据找到的 beanDefinition 查找 bean,这里是个递归调用。 找到之后,会设置到 对应的 field 中。

注意的是,该递归的终结条件就是,该 bean 没有依赖需要注入。 完成所有这些步骤后,我们的 bean 都注册到了 BeanDefinitionRegistry#beanMapByClass 中。

     /**
* map:存储 bean的class-》bean实例
*/
private Map<Class, Object> beanMapByClass = new ConcurrentHashMap<>();

后续,只需要根据class来查找对应的bean即可。

     /**
* 根据类型获取bean对象
*
* @param clazz
* @return
*/
public Object getBeanByType(Class clazz) {
return beanMapByClass.get(clazz);
}

四、总结

一个简易的ioc,大概就是这样子了。后边有时间,再把 aop 的功能加进去。当然,加进去了依然是玩具,我们造轮子的意义在哪里呢?大概就是让你更懂我们现在在用的轮子,知道它的核心代码大概是什么样子的。我们虽然大部分时候都是api 调用者,写点胶水,但是真正出问题的时候,当框架不满足的时候,我们还是得有搞定问题和扩展框架的能力。

个人水平也很有限,大家可以批评指正,欢迎加入下发二维码的 Java 交流群一起沟通学习。

源码在github,链接在上文发过了哈。

参考的工具类链接:https://www.cnblogs.com/Leechg/p/10058763.html  其中有可以优化的空间,不过用着还是不错。

曹工说Tomcat4:利用 Digester 手撸一个轻量的 Spring IOC容器的更多相关文章

  1. 使用Java Socket手撸一个http服务器

    原文连接:使用Java Socket手撸一个http服务器 作为一个java后端,提供http服务可以说是基本技能之一了,但是你真的了解http协议么?你知道知道如何手撸一个http服务器么?tomc ...

  2. 五分钟,手撸一个Spring容器!

    大家好,我是老三,Spring是我们最常用的开源框架,经过多年发展,Spring已经发展成枝繁叶茂的大树,让我们难以窥其全貌. 这节,我们回归Spring的本质,五分钟手撸一个Spring容器,揭开S ...

  3. 【手撸一个ORM】MyOrm的使用说明

    [手撸一个ORM]第一步.约定和实体描述 [手撸一个ORM]第二步.封装实体描述和实体属性描述 [手撸一个ORM]第三步.SQL语句构造器和SqlParameter封装 [手撸一个ORM]第四步.Ex ...

  4. 第二篇-用Flutter手撸一个抖音国内版,看看有多炫

    前言 继上一篇使用Flutter开发的抖音国际版 后再次撸一个国内版抖音,大部分功能已完成,主要是Flutter开发APP速度很爽,  先看下图 项目主要结构介绍 这次主要的改动在api.dart 及 ...

  5. 通过 Netty、ZooKeeper 手撸一个 RPC 服务

    说明 项目链接 微服务框架都包括什么? 如何实现 RPC 远程调用? 开源 RPC 框架 限定语言 跨语言 RPC 框架 本地 Docker 搭建 ZooKeeper 下载镜像 启动容器 查看容器日志 ...

  6. C#基于Mongo的官方驱动手撸一个Super简易版MongoDB-ORM框架

    C#基于Mongo的官方驱动手撸一个简易版MongoDB-ORM框架 如题,在GitHub上找了一圈想找一个MongoDB的的ORM框架,未偿所愿,就去翻了翻官网(https://docs.mongo ...

  7. 手撸一个SpringBoot-Starter

    1. 简介 通过了解SpringBoot的原理后,我们可以手撸一个spring-boot-starter来加深理解. 1.1 什么是starter spring官网解释 starters是一组方便的依 ...

  8. 手撸一个springsecurity,了解一下security原理

    手撸一个springsecurity,了解一下security原理 转载自:www.javaman.cn 手撸一个springsecurity,了解一下security原理 今天手撸一个简易版本的sp ...

  9. Golang:手撸一个支持六种级别的日志库

    Golang标准日志库提供的日志输出方法有Print.Fatal.Panic等,没有常见的Debug.Info.Error等日志级别,用起来不太顺手.这篇文章就来手撸一个自己的日志库,可以记录不同级别 ...

随机推荐

  1. WPF: FishEyePanel/FanPanel - 自定义Panel

    原文:WPF: FishEyePanel/FanPanel - 自定义Panel 原文来自CodeProject,主要介绍如何创建自定义的Panel,如同Grid和StackPanel. 1) Int ...

  2. 【值转换器】 WPF中Image数据绑定Icon对象

    原文:[值转换器] WPF中Image数据绑定Icon对象        这是原来的代码:        <Image Source="{Binding MenuIcon}" ...

  3. Bootstrap 固定在顶部导航条

    @{    Layout = null;}<!DOCTYPE html><html><head>    <meta name="viewport&q ...

  4. passed into methods by value java专题

    java没有引用传递只有按值传递,没有引用传递只有按值传递,值传递.因为Primitive类型的值不能改变,所以method不能更改调用方传的primitive 值.因为method更改的是Primi ...

  5. NET实现RSA AES DES 字符串 加密解密以及SHA1 MD5加密

    本文列举了    数据加密算法(Data Encryption Algorithm,DEA) 密码学中的高级加密标准(Advanced EncryptionStandard,AES)RSA公钥加密算法 ...

  6. iostat命令浅析

    报告中央处理器(CPU)统计信息.整个系统.适配器.TTY 设备.磁盘 CD-ROM.磁带和文件系统的异步输入/输出(AIO)与输入/输出统计信息,iostat也有一个弱点,就是它不能对某个进程进行深 ...

  7. 【转】ORACLE AWR报告

    转自:http://blog.csdn.net/liqfyiyi/article/details/8236864 About Oracle AWR Oracle AWR is a powerful m ...

  8. UWP-ListView到底部自动加载更多数据

    原文:UWP-ListView到底部自动加载更多数据 ListView绑定的数据当需要“更多”时自动加载 ListView划到底部后,绑定的ObservableCollection列表数据需要加载的更 ...

  9. Docker笔记03-docker 网络模式

    docker网络模式分为5种 Nat (Network Address Translation) Host other container none overlay 第一种 Nat模式 docker的 ...

  10. Setting up multi nodes live migration in Openstack Juno with devstack

    Setting up multi nodes live migration in Openstack Juno with devstack Summary Live migration overvie ...