看完此文,别说你不懂IoC是什么?

看完此文,别说你不懂IoC是什么?

盆友圈都在晒十八岁,
真羡慕你们可以发18岁的照片,我还要再等两年才能发。哎~惆怅..

前言

很多小伙伴会对Spring的IoC有些疑惑,什么是控制反转?
我为何要使用IoC 把控制权交给容器这样对我有什么好处?
书上只讲理论, 我现在都不能体会Spring的IoC用于不用相比有什么好处,
由spring托管有什么好处呢?
我现在感觉用spring 的set注入就是看起来代码NB点,
完全不理解到底有什么优势啊……
基于如上让你彻底搞定理解IoC,从此不再困惑

废话不多说(正文依旧有),我们开始吧!

原生Servlet

我们先从原生Servlet时代的三层架构(MVC)开始切入

创建maven工程 引入依赖

 <dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>javax.servlet-api</artifactId>
    <version>3.1.0</version>
    <scope>provided</scope>
</dependency>

创建测试

@WebServlet(urlPatterns = "/poXing")
public class PoXingServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        response.getWriter().println("PoXingServlet run ......");
    }

}

浏览器访问

访问路径
http://localhost:8080/spring-framework-ioc-learning/poXing
(http://ip:端口号/[context-path(默认是工程名称可以自行修改)]/uri)

浏览器可输出 PoXingServlet run ...... 此时已完成基础Servlet

进一步优化 添加 Service 和 Dao

这里为演示简单 并未引入输入库相关依赖 故此处 Dao 只是模拟有数据库的存在,
不深入

工程目录创建以下类与接口

01.png

三层架构中组件与依赖应如下图所示

02.png

添加Dao

public interface IDemoDao {
    List<String> findAll();
}


public class DemoDaoImpl implements IDemoDao {
    @Override
    public List<String> findAll() {
        // 此处为演示方便不连接数据库 模拟返回数据库list结果
        return Arrays.asList("aaa", "bbb", "ccc");
    }

}

添加Service

public interface IDemoService {
    List<String> findAll();
}

public class DemoServiceImpl implements IDemoService {

    private IDemoDao demoDao = new DemoDaoImpl();

    @Override
    public List<String> findAll() {
        return demoDao.findAll();
    }
}

修改DemoServlet

将我们前面创建的 PoXingServlet 内容拷贝 并加以修改

@WebServlet(urlPatterns = "/demoServlet")
public class DemoServlet extends HttpServlet {

    IDemoService demoService = new DemoServiceImpl();

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        resp.getWriter().println(demoService.findAll().toString());
    }
}

运行测试

访问路径
http://localhost:8080/spring-framework-ioc-learning/demoServlet
(http://ip:端口号/[context-path(默认是工程名称可以自行修改)]/uri)

浏览器可输出 ['aaa', 'bbb', 'ccc'] 此时已完成基础Servlet

基础搭建工作完成,下面才是我们的重点! 我们的重点!! 我们的重点!!!

好吧 需求开始变更

现在你已经完成了开发工作,数据库使用的Mysql
增删改查都以实现(你就当做已经实现了...虽然我并没有写增删改)
项目马上交付,此时你的领导(XX领导完全不懂技术)电话da来了.

那个谁谁谁 最新我听说大公司都用Oracle数据库
客户好像都觉得Oracle数据库更靠谱呢
这样吧, 你也给我换成Oracle数据库

此时你的内心应该是这样的 XX领导, XX不干了,

03.png
04.jpg
05.png
06.jpg

木头办法哎, 找下家太麻烦, 当前还是要恰饭的, 服从吧 咱小咱卑微 咱是干饭人 咱的错
咱改

修改数据

对于数据库的切换 我们知道 不仅要修改数据库连接池等相关的配置 还要修改
相应的SQL语句(特定的SQL对于不同的数据库写法是不一样滴,分页就不一样), 咋办?
改Dao层吧 于是乎 开始修改项目里所有的Dao实现类 也就是DaoImpl

public class DemoDaoImpl implements IDemoDao {
    @Override
    public List<String> findAll() {
        // 此处为演示方便不连接数据库 模拟返回数据库list结果
//        return Arrays.asList("aaa", "bbb", "ccc");
        // 模拟修改成了Orcale的SQL的语句了
        return Arrays.asList("xx领导(Oracle数据库)", "xx领导(Oracle数据库)", "xx领导(Oracle数据库)");
    }

}


哈哈 全部检查一遍 自动化测试也执行完 没毛病 搜易贼...
可以摸鱼了...

嘟嘟嘟 需求再次变更

项目终于改完要交付了, 干了杯枸杞红枣茶, 摸鱼准备下班中...

那个那个那个啥... 怎么Oracle数据库要花钱??? 公司最近客户还没有回款
你把数据库还是换回MySQL吧 后面等客户给回款了我们再考虑用Oracle

此时的你...

07.jpg

哎 再一再二 我想大概也会有再三吧 整吧 这次我要想想办法了
我把你的再三再四直接给你弄上 省的影响我摸鱼.

引入静态工厂

我要创建一个静态工厂, 你想要啥数据库我给你整出来一个啥数据来
这里暂时起名BeanFactory 别问为啥 我就想起这个名 我卑微我任性

public class BeanFactory {
    public static IDemoDao getDemoDao() {
        // 返回 mysql 的 Dao
        // return new DemoDaoImpl();
        // 返回 oracle 的 Dao
        return new DemoOracleDaoImpl();
    }
}


改造Service实现即 ServiceImpl

ServiceImpl 中引用的 Dao 不再可以是手动 new 出来的了,
而应该由 BeanFactory 的静态方法返回来获得

public class DemoServiceImpl implements IDemoService {

//    private IDemoDao demoDao = new DemoDaoImpl();
    private IDemoDao demoDao = BeanFactory.getDemoDao();

    @Override
    public List<String> findAll() {
        return demoDao.findAll();
    }
}

好了 即使你再发生变更(改回Oracle) 也不会影响我摸鱼了
我只是改改BeanFactory中的静态方法而已

现在终于可以交付项目了, 终于.....

抱歉(并不是你心里所想的会出啥事) 一切顺利 老板满意 客户满意
开会要给你先画个饼 鼓励一下咯

又出现了新的问题

系统已经上线运行 这个时候你已经着手进行下一个项目了,
可是..可是 领导觉得你的任务最近不多呀 提出让你对上一个项目进行优化
和扩展的需求, 这时候你再次打开了旧项目 居然发现项目编译不通过. 此处为了
演示编译无法通过,删除DemoDaoImpl.java

此时你的脑海里一定在翻滚,为么为么为么之前好好的,机器也没换啊,找到编译
出错的类BeanFactory,我x, 我的类去哪儿了,去哪儿了,去哪儿了,三连问之后
少了DemoDaoImpl.java 导致无法编译

依赖关系导致紧耦合


public class BeanFactory {
    public static IDemoDao getDemoDao() {
        // 返回 mysql 的 Dao
        // 因为 DemoDaoImpl.java不存在导致编译失败
         return new DemoDaoImpl();
        // 返回 oracle 的 Dao
//        return new DemoOracleDaoImpl();
    }
}


BeanFactory类中因 DemoDaoImpl.java 的不存在 而报红, 导致编译没法通过
像当前 BeanFactory 强依赖于 DemoDaoImpl (没有DemoDaoImpl就报红给你看)
这就是紧耦合

解决紧耦合

没有这个Java文件 我没法干活呀 要不我自己造一个空的??? 不行不行
万一明天 DemoOracleDaoImpl 这个又没有了呢, 我不能总造空的玩呀
突然此时 你灵光一现 好像 好像 反射大概似乎可能也许可以解决这个问题呢
反射可以声明一个类的全限定类名,进而获取它的字节码,这样也可以构造一个对象

于是乎 开干吧 BeanFactory 就成这样的了

public class BeanFactory {

    public static IDemoDao getDemoDao() {
        try {
            return (IDemoDao) Class.forName("com.huodd.dao.impl.DemoDaoImpl").newInstance();
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("IDemoDao instantiation error, cause: " + e.getMessage());
        }
    }
}

现在编译问题得以解决,虽然 DemoService在初始化的时候 还有有问题 但是整个项目
起码不会无法编译了

弱依赖的引入

当前使用了反射 编译通过 但是工程启动后 由于BeanFactory 要构造 DemoDaoImpl
时确实还没有该类,所以会抛出ClassNotFoundException
但是此时的BeanFactoryDemoDaoImpl 的依赖程度(你愿意丢你就丢 别影响我编译)
就算的上是 弱依赖

硬编码

弱依赖的完成 你找遍电脑的各个盘 终于把 DemoDaoImpl.java 该类给找回来了,这下一切OK
不影响编译了,不影响运行了,但是好像还哪里不大对呢, 要是再切换数据库呢, 我在工厂类里面
写死了全限定类名,切换数据库的时候我还得去重新编译一遍工程才可以正常运行,
应该还可以有办法. 可以考虑下.

引入外部化的配置文件

机智如你呀,终究被你想到了利用IO实现文件的存储配置 每次BeanFactory
被初始化时 就让他去读取配置文件就好了 我下次就改改配置文件就行了
硬编码问题得以解决

factory.properties 创建
demoDao=com.huodd.dao.impl.DemoDaoImpl

为方便取到全限定类名,前面是我们给类起的一个"别名"(类似于mybatis的xml中的alias思想)
这样后面可以直接通过 "别名找到对应类的全限定名"

BeanFactory 改造
public class BeanFactory {
    private static Properties properties;

    // 使用静态代码块初始化properties,加载factord.properties文件
    static {
        properties = new Properties();
        try {
            // 必须使用类加载器读取resource文件夹下的配置文件
            properties.load(BeanFactory.class.getClassLoader().getResourceAsStream("factory.properties"));
        } catch (IOException e) {
            // BeanFactory类的静态初始化都失败了,那后续也没有必要继续执行了
            throw new ExceptionInInitializerError("BeanFactory initialize error, cause: " + e.getMessage());
        }
    }
    
    public static IDemoDao getDemoDao() {
        String beanName = properties.getProperty("demoDao");
        try {
            Class<?> beanClazz = Class.forName(beanName);
            return (IDemoDao) beanClazz.newInstance();
        } catch (ClassNotFoundException e) {
            throw new RuntimeException("BeanFactory have not [" + beanName + "] bean!", e);
        } catch (IllegalAccessException | InstantiationException e) {
            throw new RuntimeException("[" + beanName + "] instantiation error!", e);
        }
    }

}



等等 等等 这里我们好像又把起的那个别名demoDao给写死了,得改 干脆传参
要哪个全限定类名参数传哪个全限定类名对应的别名 此时 getDemoDao这个名字得改还要加参数 此时
就叫getBean吧 根据别名获取相应的Bean对象

public static IDemoDao getBean(String beanAlias) {
        String beanName = properties.getProperty("beanAlias");
        try {
            Class<?> beanClazz = Class.forName(beanName);
            return (IDemoDao) beanClazz.newInstance();
        } catch (ClassNotFoundException e) {
            throw new RuntimeException("BeanFactory have not [" + beanName + "] bean!", e);
        } catch (IllegalAccessException | InstantiationException e) {
            throw new RuntimeException("[" + beanName + "] instantiation error!", e);
        }
    }

ServiceImpl 改造

DemoServiceImpl 中不能调用 getDao 方法(让我们给改成了getBean(String beanAlias))了,

public class DemoServiceImpl implements IDemoService {

    IDemoDao demoDao = (IDemoDao) BeanFactory.getBean("demoDao");
    
    @Override
    public List<String> findAll() {
        return demoDao.findAll();
    }
}

到这里,你突然发现一个现象:
这下你可以把所有想抽取出来的组件都可以做成外部化配置了!

PS: 自行去DemoServlet里面把对DemoServiceImpl的紧耦合改正

算了 简单说一下
factory.properties 外部配置化文件 加入 demoService=com.huodd.service.impl.DemoServiceImpl
DemoServlet获取DemoServiceImpl改成 IDemoService demoService = (IDemoService) BeanFactory.getBean("DemoServiceImpl");

外部配置化

对于这种可能会变化的配置、属性等,通常不会直接硬编码在源代码中,
而是抽取为一些配置文件的形式( properties 、xml 、json 、yml 等),
配合程序对配置文件的加载和解析,从而达到动态配置、降低配置耦合的目的。
由此大概我们可以知道 原来并不是那些老外拍脑袋随便就乱搞才出来的这个IoC思想

多重构建问题

细心的小伙伴可能已经发现 项目还会存在问题 影响性能的问题 也是很大的问题
就是BeanFactory#getBean(String beanAlias) 会重复创建同一个对象的多个实例
而我们根本就不需要每次都给我去创建新的对象实例,旧的就够用了
因此处不是本文重点 小伙伴自行改进 这里给出一个大概的思路

可以考虑引入缓存 将创建出来的实例缓存起来 每次我们从缓存中读取
PS: 要多考虑一点哦 别忘了多线程并发问题

到这里,不知道小伙伴是否对IOC有了个全新的认识和理解呢

这里总结一下里面出现的几个关键点

  • 静态工厂可将多处依赖进行抽取分离
  • 外部化配置文件+反射可解决配置的硬编码问题
  • 缓存可控制对象实例数(这里我们并没有具体去实现小伙伴自行动手哦)

接下来 是否解决了文章开始时小伙伴们的困惑呢?

IOC的思想引入


private IDemoDao dao = new DemoDaoImpl();

private IDemoDao dao = (IDemoDao) BeanFactory.getBean("demoDao");

对比如上两种方法获取dao

  • 前者强依赖/紧耦合 后者弱依赖/松散耦合
  • 前者需保证DemoDaoImpl存在才能通过编译
    后者无需保证DemoDaoImpl存在就可以通过编译 倘若factory.properties 中声明的全限定类名出现错误,
    则会出现ClassCastException

仔细体会下面这种对象获取的方式,本来咱开发者可以使用上面的方式,
主动声明实现类,但如果选择下面的方式,
那就不再是咱自己去声明,而是将获取对象的方式交给了BeanFactory
这种将控制权交给别人的思想,就可以称作:控制反转( Inverse of Control , IOC )
而 BeanFactory 根据指定的 beanName 去获取和创建对象的过程,
就可以称作:依赖查找( Dependency Lookup , DL )

源码获取

本文源代码获取 可添加公众号"PoXing" 回复【IOC】进行获取

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,132评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,802评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,566评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,858评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,867评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,695评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,064评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,705评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,915评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,677评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,796评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,432评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,041评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,992评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,223评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,185评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,535评论 2 343

推荐阅读更多精彩内容