曹工说Tomcat4:利用 Digester 手撸一个轻量的 Spring IOC容器
一、前言
一共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、代码结构、效果展示
强烈建议大家直接把代码拉下来跑,跑一跑,打个断点,几乎都不用看我写的了。源码路径:
代码结构如下图:
大家看上图,测试类中,主要是 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容器的更多相关文章
- 使用Java Socket手撸一个http服务器
原文连接:使用Java Socket手撸一个http服务器 作为一个java后端,提供http服务可以说是基本技能之一了,但是你真的了解http协议么?你知道知道如何手撸一个http服务器么?tomc ...
- 五分钟,手撸一个Spring容器!
大家好,我是老三,Spring是我们最常用的开源框架,经过多年发展,Spring已经发展成枝繁叶茂的大树,让我们难以窥其全貌. 这节,我们回归Spring的本质,五分钟手撸一个Spring容器,揭开S ...
- 【手撸一个ORM】MyOrm的使用说明
[手撸一个ORM]第一步.约定和实体描述 [手撸一个ORM]第二步.封装实体描述和实体属性描述 [手撸一个ORM]第三步.SQL语句构造器和SqlParameter封装 [手撸一个ORM]第四步.Ex ...
- 第二篇-用Flutter手撸一个抖音国内版,看看有多炫
前言 继上一篇使用Flutter开发的抖音国际版 后再次撸一个国内版抖音,大部分功能已完成,主要是Flutter开发APP速度很爽, 先看下图 项目主要结构介绍 这次主要的改动在api.dart 及 ...
- 通过 Netty、ZooKeeper 手撸一个 RPC 服务
说明 项目链接 微服务框架都包括什么? 如何实现 RPC 远程调用? 开源 RPC 框架 限定语言 跨语言 RPC 框架 本地 Docker 搭建 ZooKeeper 下载镜像 启动容器 查看容器日志 ...
- C#基于Mongo的官方驱动手撸一个Super简易版MongoDB-ORM框架
C#基于Mongo的官方驱动手撸一个简易版MongoDB-ORM框架 如题,在GitHub上找了一圈想找一个MongoDB的的ORM框架,未偿所愿,就去翻了翻官网(https://docs.mongo ...
- 手撸一个SpringBoot-Starter
1. 简介 通过了解SpringBoot的原理后,我们可以手撸一个spring-boot-starter来加深理解. 1.1 什么是starter spring官网解释 starters是一组方便的依 ...
- 手撸一个springsecurity,了解一下security原理
手撸一个springsecurity,了解一下security原理 转载自:www.javaman.cn 手撸一个springsecurity,了解一下security原理 今天手撸一个简易版本的sp ...
- Golang:手撸一个支持六种级别的日志库
Golang标准日志库提供的日志输出方法有Print.Fatal.Panic等,没有常见的Debug.Info.Error等日志级别,用起来不太顺手.这篇文章就来手撸一个自己的日志库,可以记录不同级别 ...
随机推荐
- WPF: FishEyePanel/FanPanel - 自定义Panel
原文:WPF: FishEyePanel/FanPanel - 自定义Panel 原文来自CodeProject,主要介绍如何创建自定义的Panel,如同Grid和StackPanel. 1) Int ...
- 【值转换器】 WPF中Image数据绑定Icon对象
原文:[值转换器] WPF中Image数据绑定Icon对象 这是原来的代码: <Image Source="{Binding MenuIcon}" ...
- Bootstrap 固定在顶部导航条
@{ Layout = null;}<!DOCTYPE html><html><head> <meta name="viewport&q ...
- passed into methods by value java专题
java没有引用传递只有按值传递,没有引用传递只有按值传递,值传递.因为Primitive类型的值不能改变,所以method不能更改调用方传的primitive 值.因为method更改的是Primi ...
- NET实现RSA AES DES 字符串 加密解密以及SHA1 MD5加密
本文列举了 数据加密算法(Data Encryption Algorithm,DEA) 密码学中的高级加密标准(Advanced EncryptionStandard,AES)RSA公钥加密算法 ...
- iostat命令浅析
报告中央处理器(CPU)统计信息.整个系统.适配器.TTY 设备.磁盘 CD-ROM.磁带和文件系统的异步输入/输出(AIO)与输入/输出统计信息,iostat也有一个弱点,就是它不能对某个进程进行深 ...
- 【转】ORACLE AWR报告
转自:http://blog.csdn.net/liqfyiyi/article/details/8236864 About Oracle AWR Oracle AWR is a powerful m ...
- UWP-ListView到底部自动加载更多数据
原文:UWP-ListView到底部自动加载更多数据 ListView绑定的数据当需要“更多”时自动加载 ListView划到底部后,绑定的ObservableCollection列表数据需要加载的更 ...
- Docker笔记03-docker 网络模式
docker网络模式分为5种 Nat (Network Address Translation) Host other container none overlay 第一种 Nat模式 docker的 ...
- 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 ...