目录

前言

在上一章中,我们介绍和简单实现了容器的部分功能,但是这里还留下了很多的问题。比如我们在构造bean实例的时候如果bean是带参数的我们需要怎么来进行,当然这个是可以在init方法中进行的,再比如我们平时在Spring中获取一个对象通过一个注解即可获取到类的实例,这个实例是怎么注入的呢?

IOC和DI

依赖注入(DI)和控制反转(IOC)其实基本上表达的是同一个东西,两者谁也离不开谁。

假设类a需要依赖类b,但a并不控制b的声明周期,仅仅在本类中使用b,而对于类b的创建和销毁等工作交给其他的组件来处理,这叫控制反转(IOC),而类a要依赖类b,则必须要获得类b的实例。这个获得类b的实例的过程则就是依赖注入(DI)。

注入分析

分析我们在那些地方可能会有注入这样一个行为?

要知道那些地方存在依赖注入首先得明白注入的是什么,结合上面分析这里很明显注入实际就是一个实例化的过程,更广泛的说是一个赋值的过程。而我们平时那些地方可能会存在赋值的动作呢?首先肯定是构造函数里,对类的实例化构造函数肯定是跑不了的,大多数赋初值的操作也都在构造函数中完成。然后还有就是另外执行过程中对属性值修改了。

那么需要进行依赖注入的地方就很明显了:

  • 构造参数依赖
  • 属性依赖

我们赋值的类型有那些呢?

很明显在Java中我们赋值类型包括基本数据类型(int,double...)和引用类型。

在分析清楚需要注入的行为和类型后又有另外的问题,我们虽然知道注入的类型是基本数据类型和引用类型,但是实际需要注入的类型是无法预料到的,我们事先并不知道某一个参数需要的是int还是boolean或者是引用类型。幸运的是Java实际上已经帮我们解决了这件事情,java中的所有类型都是继承自Object,我们只需要使用Object来接受值即可。

我们在对值进行注入的时候肯定是需要知道我们注入的具体的类型的,instanceof关键字可以帮助我们确定具体的某个类型,但是实际上也就仅仅限于基本数据类型了,因为引用类型实在太多了,用户自己定义的引用类型我们是没有办法事先确定的。所以我们要想办法能让我们知道注入的具体是哪一个类型。

BeanReference

我们定义一个用来标识bean类型的类,接口只包含一个标识bean的类型的参数beanName,在注入参数的时候只需要定义好beanName的值即可直接从IOC容器中取相应的bean,而对于基本数据类型和String则直接赋值即可。

当然这样还不能解决所有问题,因为传入的参数可能携带多个引用值,比如引用数组,List,Map以及属性文件读取(Properties)等,解决这些问题和上面差不多,引用类型还是使用BeanReference,多个值则进行遍历即可。

构造参数依赖分析

构造参数个数的问题

实际上上面的分析已经将我们的问题解决的差不多了,还存在一个问题就是构造参数的个数是没有办法确定的,我们怎么来存储一个bean实例化所需的全部参数,又如何值和参数对应。

很明显需要一个对应关系的话我们立刻能想到的就是key-value形式的Map,实际上我们还能使用List,根据顺序来存储,取得时候也依然是这个顺序。

如何匹配构造函数

一个类中方法的重载的个数可以是有多个的,我们如何精确的找到我们需要的方法呢?

在JDK中Class类中为我们提供了一系列的方法:

method 介绍
Constructor getConstructor(Class<?>... parameterTypes) 返回一个 Constructor对象,该对象反映 Constructor对象表示的类的指定的公共 类函数。
Method getMethod(String name, Class<?>... parameterTypes) 返回一个 方法对象,它反映此表示的类或接口的指定公共成员方法 类对象。
Method[] getMethods() 返回包含一个数组 方法对象反射由此表示的类或接口的所有公共方法 类对象,包括那些由类或接口和那些从超类和超接口继承的声明。
Constructor<?>[] getConstructors() 返回包含一个数组 Constructor对象反射由此表示的类的所有公共构造 类对象。

前面我们已经取到了参数:

  1. 根据参数个数匹配具有同样个数参数的方法
  2. 根据参数类型精确匹配步骤一种筛选出的方法
单例or原型

简单来讲,单例(Singleton)是指在容器运行过程中,一个bean只创建一次,后面需要使用都是同一个对象。原型(Prototype)在容器运行时不进行创建,只有在使用时才创建,没用一次就新创建一个。

对于单例模式只创建一次,那么上面的匹配过程也只会进行一次,对程序的运行不会有影响。但是原型模式每次创建都重新匹配一次这会在一定程度上拖慢程序的运行。所以这里我们可以考虑将原型bean实例化对应的方法缓存起来,那么后面在同一个地方使用创建时不用重复去匹配。

需要的接口

很明显上面的分析都是和bean定义有关,相应的方法也应该加在bean定义接口上了。

构造参数的注入应当是在bean创建的时候,在前面我们定义类几种不同的bean创建方式,现在应该在这些方法中加上构造参数了。

代码:

BeanReference

public class BeanReference {

    private String beanName;

    public String getBeanName() {
return beanName;
} public void setBeanName(String beanName) {
this.beanName = beanName;
}
}

DefaultBeanDefinition添加代码:

public class DefaultBeanDefinition implements BeanDefinition{

    ...

    private Constructor constructor;

    private Method method;

    private List<?> constructorArg;

    ... 

    //getter setter
}

DefaultBeanFactory添加代码

public class DefaultBeanFactory implements BeanFactory, BeanDefinitionRegistry, Closeable {

    //other method

    /**
* 解析传入的构造参数值
* @param constructorArgs
* @return
*/
private Object[] parseConstructorArgs(List constructorArgs) throws IllegalAccessException, InstantiationException { if(constructorArgs==null || constructorArgs.size()==0){
return null;
} Object[] args = new Object[constructorArgs.size()];
for(int i=0;i<constructorArgs.size();i++){
Object arg = constructorArgs.get(i);
Object value = null;
if(arg instanceof BeanReference){
String beanName = ((BeanReference) arg).getBeanName();
value = this.doGetBean(beanName);
}else if(arg instanceof List){
value = parseListArg((List) arg);
}else if(arg instanceof Map){
//todo 处理map
}else if(arg instanceof Properties){
//todo 处理属性文件
}else {
value = arg;
}
args[i] = value;
}
return args;
} private Constructor<?> matchConstructor(BeanDefinition bd, Object[] args) throws Exception { if(args == null){
return bd.getBeanClass().getConstructor(null);
}
//如果已经缓存了 则直接返回
if(bd.getConstructor() != null)
return bd.getConstructor(); int len = args.length;
Class[] param = new Class[len];
//构造参数列表
for(int i=0;i<len;i++){
param[i] = args[i].getClass();
}
//先进行精确匹配 如果能匹配到相应的构造方法 则后续不用进行
Constructor constructor = null;
try {
constructor = bd.getBeanClass().getConstructor(param);
} catch (Exception e) {
//这里上面的代码如果没匹配到会抛出空指针异常
//为了代码继续执行 这里我们来捕获 但是不需要做其他任何操作
}
if(constructor != null){
return constructor;
} //未匹配到 继续匹配
List<Constructor> firstFilterAfter = new LinkedList<>();
Constructor[] constructors = bd.getBeanClass().getConstructors();
//按参数个数匹配
for(Constructor cons:constructors){
if(cons.getParameterCount() == len){
firstFilterAfter.add(cons);
}
} if(firstFilterAfter.size()==1){
return firstFilterAfter.get(0);
}
if(firstFilterAfter.size()==0){
log.error("不存在对应的构造函数:" + args);
throw new Exception("不存在对应的构造函数:" + args);
}
//按参数类型匹配
//获取所有参数类型
boolean isMatch = true;
for(int i=0;i<firstFilterAfter.size();i++){
Class[] types = firstFilterAfter.get(i).getParameterTypes();
for(int j=0;j<types.length;j++){
if(types[j].isAssignableFrom(args[j].getClass())){
isMatch = false;
break;
}
}
if(isMatch){
//对于原型bean 缓存方法
if(bd.isPrototype()){
bd.setConstructor(firstFilterAfter.get(i));
}
return firstFilterAfter.get(i);
}
}
//未能匹配到
throw new Exception("不存在对应的构造函数:" + args);
}
private List parseListArg(List arg) throws Exception {
//遍历list
List param = new LinkedList();
for(Object value:arg){
Object res = new Object();
if(arg instanceof BeanReference){
String beanName = ((BeanReference) value).getBeanName();
res = this.doGetBean(beanName);
}else if(arg instanceof List){
//递归 因为list中可能还存有list
res = parseListArg(arg);
}else if(arg instanceof Map){
//todo 处理map
}else if(arg instanceof Properties){
//todo 处理属性文件
}else {
res = arg;
}
param.add(res);
}
return param;
}
}
相关代码已经托管到github:myspring

循环依赖

到这里对构造函数的参数依赖基本完成了,经过测试也基本没有问题,但是在测试过程中发现如果构造出的参数存在循环依赖的话,则会导致整个过程失败。

什么是循环依赖?如何解决循环依赖?

如上图,A依赖B,B依赖C,C又依赖A,在初始化的过程中,A需要加载B,B需要加载C,到了C这一步又来加载A,一直重复上面的过程,这就叫循环依赖。

在Spring框架中对Bean进行配置的时候有一个属性lazy-init。一旦将这个属性设置为true,那么循环依赖的问题就不存在了,这是为什么呢?实际上如果配置了懒加载那么这个bean并不会立刻初始化,而是等到使用时才初始化,而在需要使用时其他的bean都已经初始化好了,这是我们直接取实例,依赖的实例并不需要实例化,所以才不会有循环依赖的问题。

那么我们这里怎么解决呢?

根据Spring的启发,需要解决循环依赖那么主要就是对于已经实例化过的bean不在进行实例化,那么我们定义一个用于记录已经实例化后的bean的容器,每一次实例化一个bean是检测一次,如果已经实例化过的bean直接跳过。

添加的代码:

public class DefaultBeanFactory implements BeanFactory, BeanDefinitionRegistry, Closeable {
//记录正在创建的bean
private ThreadLocal<Set<String>> initialedBeans = new ThreadLocal<>(); public Object doGetBean(String beanName) throws InstantiationException, IllegalAccessException { //other operation
// 记录正在创建的Bean
Set<String> beans = this.initialedBeans.get();
if (beans == null) {
beans = new HashSet<>();
this.initialedBeans.set(beans);
} // 检测循环依赖
if (beans.contains(beanName)) {
throw new Exception("检测到" + beanName + "存在循环依赖:" + beans);
} // 记录正在创建的Bean
beans.add(beanName);
//other operation
//创建完成 移除该bean的记录
beans.remove(beanName);
return instance;
}
}

实际上在单例bean中,对于已经创建好的bean是直接从容器中获取实例,不需要再次实例化,所以也不会有循环依赖的问题。但是对于原型bean,创建好的实例并不放到容器中,而是每一次都重新创建初始化,才会存在循环依赖的问题。

属性依赖

除了在构造函数中初始化参数外,我们还可以对属性进行赋值,对属性赋值的好处在于可以在运行中动态的改变属性的值。

和构造参数依赖有什么不同

整体来说没有什么差别,不同在于对构造参数依赖时有具体的对应方法,可以根据参数的个数和顺序来确定构造方法,所以在注入是我们可以使用上面选择的List根据存入顺序作为参数的顺序。而对于属性依赖,我们必须要根据属性的名称来注入值才可以,所以在使用list就不行了。

解决:

  1. 使用一个Map容器,key为属性名,value为属性值,使用时解析map即可
  2. 自定义一个包裹属性的类,参数为属性名和属性值,然后使用list容纳包裹属性的类,实际上和上面的map差不多。

这里我使用map类。

然后其他的地方都基本一样,对于引用类型依旧使用BeanReference。在BeanDefinition中添加获取和设置属性值得方法:

    //属性依赖
Map<String,Object> getPropertyKeyValue();
void setPropertyKeyValue(Map<String,Object> properties);

在BeanFactory的实现中加入解析属性的方法:

private void parsePropertyValues(BeanDefinition bd, Object instance) throws Exception {
Map<String, Object> propertyKeyValue = bd.getPropertyKeyValue();
if(propertyKeyValue==null || propertyKeyValue.size()==0){
return ;
}
Class<?> aClass = instance.getClass();
Set<Map.Entry<String, Object>> entries = propertyKeyValue.entrySet();
for(Map.Entry<String, Object> entry:entries){
//获取指定的字段信息
Field field = aClass.getDeclaredField(entry.getKey());
//将访问权限设置为true
field.setAccessible(true);
Object arg = entry.getValue();
Object value = null;
if(arg instanceof BeanReference){
String beanName = ((BeanReference) arg).getBeanName();
value = this.doGetBean(beanName);
}else if(arg instanceof List){
List param = parseListArg((List) arg);
value = param;
}else if(arg instanceof Map){
//todo 处理map
}else if(arg instanceof Properties){
//todo 处理属性文件
}else {
value = arg;
}
field.set(instance, value);
}
}
相关代码已经托管到github:myspring

完整类图

Spring系列之DI的原理及手动实现的更多相关文章

  1. Spring系列之IOC的原理及手动实现

    目录 Spring系列之IOC的原理及手动实现 Spring系列之DI的原理及手动实现 导语 Spring是一个分层的JavaSE/EE full-stack(一站式) 轻量级开源框架.也是几乎所有J ...

  2. Spring系列之AOP的原理及手动实现

    目录 Spring系列之IOC的原理及手动实现 Spring系列之DI的原理及手动实现 引入 到目前为止,我们已经完成了简易的IOC和DI的功能,虽然相比如Spring来说肯定是非常简陋的,但是毕竟我 ...

  3. Spring系列之手写一个SpringMVC

    目录 Spring系列之IOC的原理及手动实现 Spring系列之DI的原理及手动实现 Spring系列之AOP的原理及手动实现 Spring系列之手写注解与配置文件的解析 引言 在前面的几个章节中我 ...

  4. Spring系列之手写注解与配置文件的解析

    目录 Spring系列之IOC的原理及手动实现 Spring系列之DI的原理及手动实现 Spring系列之AOP的原理及手动实现 引入 在前面我们已经完成了IOC,DI,AOP的实现,基本的功能都已经 ...

  5. 通俗化理解Spring3 IoC的原理和主要组件(spring系列知识二总结)

    ♣什么是IoC? ♣通俗化理解IoC原理 ♣IoC好处 ♣工厂模式 ♣IoC的主要组件 ♣IoC的应用实例 ♣附:实例代码 1.什么是IoC(控制反转)? Spring3框架的核心是实现控制反转(Io ...

  6. Spring系列(1)--IOC 和 DI

    IOC 和 DI IOC 原理 xml 配置文件配置 bean dom4j 读取配置文件 工厂设计模式 反射机制创建对象 applicationContext.xml 配置文件,该配置文件名可自定义: ...

  7. Spring系列.事务管理原理简析

    Spring的事务管理功能能让我们非常简单地进行事务管理.只需要进行简单的两步配置即可: step1:开启事务管理功能 @Configuration //@EnableTransactionManag ...

  8. java Spring系列之 配置文件的操作 +Bean的生命周期+不同数据类型的注入简析+注入的原理详解+配置文件中不同标签体的使用方式

    Spring系列之 配置文件的操作 写在文章前面: 本文带大家掌握Spring配置文件的基础操作以及带领大家理清依赖注入的概念,本文涉及内容广泛,如果各位读者耐心看完,应该会对自身有一个提升 Spri ...

  9. Spring框架系列(13) - SpringMVC实现原理之DispatcherServlet的初始化过程

    前文我们有了IOC的源码基础以及SpringMVC的基础,我们便可以进一步深入理解SpringMVC主要实现原理,包含DispatcherServlet的初始化过程和DispatcherServlet ...

随机推荐

  1. C#学习笔记14——TRACE、DEBUG和TRACESOURCE的使用以及日志设计

    Trace.Debug和TraceSource的使用以及日志设计   .NET Framework 命名空间 System.Diagnostics 包含用于跟踪执行流程的 Trace.Debug 和 ...

  2. 关于python的展望

    在未接触这门课程以前,我完全对编程一类的操作毫无兴趣.但在短短的两星期时间里,我改变了想法,原因有二.其一是老师幽默,其二是课程实用性高.我希望课程以后可以继续沿用现在由浅入深,给予足够提示的方式,引 ...

  3. C语言字符串和十六进制的相互转换方式

    C语言的字符串操作并不像java,Csharp那样提供直接的方法,简单粗暴.所以,在转换的时候往往费力费时,近日做项目正好用到和java程序通讯,java发送过来的数据是十六进制数字组成的字符串,解析 ...

  4. FFMPEG增加和提取字幕流

    转自 https://www.cnblogs.com/satng/p/5514683.html 防抽复制一遍 增加字幕流ffmpeg -i video.avi -i sub.ass -map 0:0 ...

  5. jQuery事件学习

    1.JS事件的基本知识 <!DOCTYPE html> <html lang="en"> <head> <meta charset=&qu ...

  6. 【转载】 .NET框架设计—常被忽视的C#设计技巧

    阅读目录: 1.开篇介绍 2.尽量使用Lambda匿名函数调用代替反射调用(走进声明式设计) 3.被忽视的特性(Attribute)设计方式 4.扩展方法让你的对象如虎添翼(要学会使用扩展方法的设计思 ...

  7. 启动eclipse could not create the java Vittual Machine

    查询并几种方法: 1.都说是 eclipse.ini  环境初始文件的内存问题,续增大堆内存大小,具体配置如,如果找不到问题所在可以试试(该方法是确定环境变量没问题下试行) -Xms64m-Xmx25 ...

  8. Borg, Omega, and Kubernetes读后笔记

    前言 最近又读了一遍 Borg, Omega, and Kubernetes 这篇文章,觉得这个文章写得很好,让我对架构设计有了进一步的认识,所以想写一篇读后笔记. 原文地址,还有篇中文翻译的,这个中 ...

  9. 从git远程仓库Checkout项目到本地

    一.登录coding  并且项目已创建好  已经是项目的组员 二.打开idea 1.弹出如下页面  复制远程项目上的SSH(URL)到下框URL 并且Test测试 成功就Clone即可 2.Clone ...

  10. Nginx 教程(3):SSL 设置

    SSL 和 TLS SSL(Socket Secure Layer 缩写)是一种通过 HTTP 提供安全连接的协议. SSL 1.0 由 Netscape 开发,但由于严重的安全漏洞从未公开发布过.S ...