照虎画猫写自己的Spring——依赖注入

前言

上篇《照虎画猫写自己的Spring》从无到有讲述并实现了下面几点

  • 声明配置文件,用于声明需要加载使用的类
  • 加载配置文件,读取配置文件
  • 解析配置文件,需要将配置文件中声明的标签转换为Fairy能够识别的类
  • 初始化类,提供配置文件中声明的类的实例

一句话概括:不借助Spring容器,实现了Bean的加载和实例化

要想契合Fairy取名时的初衷(东西不大,但是能量无穷),只有一套加载Bean的机制是远远不够的,所以还是需要照虎画猫,完善这个小精灵。

Spring之所以在Java企业级开发的众多框架中崭露头角光芒万丈,与他的依赖注入(又名控制反转IOC)面向切面(AOP)两大杀手锏是密不可分的。在Fairy实现了加载实例化Bean的功能后,我们再往前走一步,看看依赖注入是如何实现的。

依赖注入

举个例子,大概介绍下依赖注入。
没有依赖注入之前,我们买白菜的时候,需要挎着篮子去菜市场挑选并购买;
有了依赖注入之后,我们需要白菜的时候,菜装在篮子里,已经放在你家门口。
这就是依赖注入。

对于Fairy,如果要实现依赖注入的功能,需要在上一版的代码上做一些小小的改动。
将原来的FairyBean接口和实现类FairyBeanImpl改为FairyDao接口和实现类FairyDaoImpl,除此以外,我们需要新加一个接口FairyService和实现类FairyServiceImpl。
这么声明,相信你一定明白这是为了使用依赖注入功能。

配置

我们依旧采用读取配置文件的方式来初始化容器。新建一个配置文件application-context-inject.xml

<beans>
    <bean id="fairyService" class="com.jackie.fairy.bean.impl.FairyServiceImpl">
        <property name="fairyDao" ref="fairyDao"></property>
        <property name="lightColor" value="blue"></property>
    </bean>

    <bean id="fairyDao" class="com.jackie.fairy.bean.impl.FairyDaoImpl">
    </bean>
</beans>

同时我们需要FairyService和FairyServiceImpl
FairyService

package com.jackie.fairy.bean;

/**
 * Created by jackie on 17/11/25.
 */
public interface FairyService {
    void greet();

    void fly();

    void lighting();
}

FairyServiceImpl


package com.jackie.fairy.bean.impl;

import com.jackie.fairy.bean.FairyDao;
import com.jackie.fairy.bean.FairyService;

/**
 * Created by jackie on 17/11/25.
 */
public class FairyServiceImpl implements FairyService {
    private FairyDao fairyDao;
    private String lightColor;

    public FairyDao getFairyDao() {
        System.out.println("===getFairyDao===: " + fairyDao.toString());
        return fairyDao;
    }

    public void setFairyDao(FairyDao fairyDao) {
        System.out.println("===setFairyDao===: " + fairyDao.toString());
        this.fairyDao = fairyDao;
    }

    public String getLightColor() {
        return lightColor;
    }

    public void setLightColor(String lightColor) {
        this.lightColor = lightColor;
    }

    @Override
    public void greet() {
        fairyDao.greet();
    }

    @Override
    public void fly() {
        fairyDao.fly();
    }

    @Override
    public void lighting() {
        System.out.println("----------Hi, I am light fairy. Exactly, " + lightColor + " color light fairy----------");
    }
}
  • 没有使用@Autowired注入FairyDao,这是Spring的那一套
  • 将FairyDao作为成员变量,添加setter和getter方法(后续做注入使用)
  • 添加FairyService自己的实现方法lighting,这是一个会发光的小精灵的feature,小精灵的发光属性取决于lightColor,这个属性需要注入,所以也有相应的setter和getter方法

升级解析器类

上篇的XmlReaderUtil解析器只能解析这样的配置结构

<parent>
    <child>
    </child>
    ...
    <child>
    </child>
<parent>

但是我们现在需要支持的配置文件如上面的配置文件所示,所以需要升级解析器类,支持读取子标签的属性标签。
在此之前,需要新建模型PropertyDefinition,用于存储属性值

package com.jackie.fairy.model;

/**
 * Created by jackie on 17/11/25.
 */
public class PropertyDefinition {
    private String name;
    private String ref;
    private String value;

    public PropertyDefinition(String name, String ref, String value) {
        this.name = name;
        this.ref = ref;
        this.value = value;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getRef() {
        return ref;
    }

    public void setRef(String ref) {
        this.ref = ref;
    }

    public String getValue() {
        return value;
    }

    public void setValue(String value) {
        this.value = value;
    }

    @Override
    public String toString() {
        return "PropertyDefinition{" +
                "name='" + name + '\'' +
                ", ref='" + ref + '\'' +
                ", value='" + value + '\'' +
                '}';
    }
}

同时,需要在BeanDefinition模型中加入List<PropertyDefinition>,因为属性值是依附在BeanDefinition下面的。

XmlReaderUtil将核心代码改为

for (Iterator iterator = rootElement.elementIterator(); iterator.hasNext(); ) {
    Element element = (Element)iterator.next();
    String id = element.attributeValue(Constants.BEAN_ID_NAME);
    String clazz = element.attributeValue(Constants.BEAN_CLASS_NAME);
    BeanDefinition beanDefinition = new BeanDefinition(id, clazz);
    
    // 遍历属性标签
    for (Iterator propertyIterator = element.elementIterator(); propertyIterator.hasNext();) {
        Element propertyElement = (Element) propertyIterator.next();
        String name = propertyElement.attributeValue(Constants.PROPERTY_NAME_NAME);
        String ref = propertyElement.attributeValue(Constants.PROPERTY_REF_NAME);
        String value = propertyElement.attributeValue(Constants.PROPERTY_VALUE_NAME);
        propertyDefinitions.add(new PropertyDefinition(name, ref, value));
    }

    beanDefinition.setPropertyDefinitions(propertyDefinitions);
    beanDefinitions.add(beanDefinition);
    // 清空propertyDefinitions集合,因为有些bean没有property标签
    propertyDefinitions = Lists.newArrayList(); 
}

即添加了对于属性标签的解析和存储,详细代码可进入GitHub项目查看。

实现依赖注入函数

在FairyApplicationContext中添加实现依赖注入功能的函数,主要思路就是对某个需要依赖注入的主体(这里的FairyService),找到要依赖注入的类(这里的FairyDao),借助反射机制,通过setter方法将FairyDao注入到FairyService中。

injectObject()

private void injectObject() {
    for (BeanDefinition beanDefinition : beanDefinitions) {
        Object bean = instanceBeans.get(beanDefinition.getId());
        if (bean != null) {
            try {
                BeanInfo beanInfo = Introspector.getBeanInfo(bean.getClass());
                /**
                 * 通过BeanInfo来获取属性的描述器(PropertyDescriptor)
                 * 通过这个属性描述器就可以获取某个属性对应的getter/setter方法
                 * 然后我们就可以通过反射机制来调用这些方法。
                 */
                PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors();

                for (PropertyDefinition propertyDefinition : beanDefinition.getPropertyDefinitions()) {
                    for (PropertyDescriptor propertyDescriptor : propertyDescriptors) {
                        // 用户定义的bean属性与java内省后的bean属性名称相同时
                        if (StringUtils.equals(propertyDescriptor.getName(), propertyDefinition.getName())) {
                            // 获取setter方法
                            Method setter = propertyDescriptor.getWriteMethod();
                            if (setter != null) {
                                Object value = null;
                                if (StringUtils.isNotEmpty(propertyDefinition.getRef())) {
                                    // 根据bean的名称在instanceBeans中获取指定的对象值
                                    value = instanceBeans.get(propertyDefinition.getRef());
                                } else {
                                    value = ConvertUtils.convert(propertyDefinition.getValue(), propertyDescriptor.getPropertyType());
                                }

                                // //保证setter方法可以访问私有
                                setter.setAccessible(true);
                                try {
                                    // 把引用对象注入到属性
                                    setter.invoke(bean, value);
                                } catch (Exception e) {
                                    LOG.error("invoke setter.invoke failed", e);
                                }
                            }
                            break;
                        }
                    }
                }
            } catch (Exception e) {
                LOG.error("invoke getBean failed", e);
            }
        }
    }
}
  • 用到了Java内省获取Bean各个属性的setter和getter方法
  • 使用了反射调用setter方法,将其注入FairyService类中

测试

编写测试代码

/**
 * bean依赖注入
 */
FairyApplicationContext autowiredApplicationContext =
        new FairyApplicationContext("application-context-inject.xml");
FairyService fairyService = (FairyService) autowiredApplicationContext.getBean("fairyService");
fairyService.greet();
fairyService.lighting();

得到结果

===setFairyDao===: com.jackie.fairy.bean.impl.FairyDaoImpl@6615435c
Hi, I am fairy
----------Hi, I am light fairy. Exactly, blue color light fairy----------

其中第一行打印结果是在通过反射执行setter.invoke(bean, value);时触发打印的。

至此,我们为Fairy实现了依赖注入的功能,项目地址
https://github.com/DMinerJackie/fairy

项目结构

1125-fairy目录结构.png

Fairy项目改动盘点

  • 添加FairyApplicationContext(String configLocation)构造函数,默认加载的配置文件是xml格式
  • 添加Json配置文件解析器,可以解析Json格式的配置文件并加载bean
  • 重构测试Bean,将接口FairyBean改为FairyDao,并新增FairyService接口及实现类,方便本文的用例测试
  • 升级XmlReaderUtil,支持Bean的自标签Property的解析
  • 添加依赖注入函数,用户实现依赖注入功能
  • 添加PropertyDefinition模型,用于存储property属性值
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容