IOC的由来

96 阅读4分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第18天,点击查看活动详情
本文主要通过Servlet架构和传统开发带来的问题来引出我们的IOC,以便于大家理解。

Servlet时代的MVC架构

在正式引出IOC之前,我们先来讲一下Servlet时代的MVC三层架构。Servlet在MVC三层架构中充当controller的功能,前端发来请求达到servlet上,servlet执行本身的一些方法如重写doget来处理逻辑,它可以将具体的处理逻辑丢给service来实现,service再调用Dao,简单来说这就Servlet时代MVC三层架构的处理方式。如下图所示(该图摘自网络内容)。

image.png

需求变更

数据库变更

假设我们在前面用servlet开发项目的时候使用的数据库是MySQL,并且已经基本开发完成。此时客户来了个需求,要求我们更换成Oracle数据库。

MySQL改到Oracle不只是简单的将相关的连接配置修改了就行的,他们两者之间的SQL语句有着细微的差别,也是需要修改的,这时候工作量就大了,搞不好每个DaoImpl都要修改。

于是你就开始改项目中的DaoImpl了,已完成数据库的更换:

public class DemoDaoImpl implements DemoDao {
    @Override
    public List<String> findAll() {
        return Arrays.asList("oracleA","oracleB","oracleC");
    }
}

突然,你改好了,客户又让你改回MySQL,那你怎么办?再改回来吗? 针对这种问题,你想到了用静态工厂来解决。

引入静态工厂

实现把这些Mysql和Oracle这两套的Dao都写好,然后用静态工厂来创建指定的实现类,需求改变,你只需要改一下抛出的对象就行了。问题解决,搞一段路!

源码丢失问题

有一天,你的源码DemoDaoImpl这个实现类源文件丢了,你怎么也运行不来你的项目了。


public class BeanFactory {
    public static DemoDao getDemoDao() {
        return new DemoDaoImpl(); // DemoDaoImpl.java不存在导致编译失败
    }
}

像上面这种问题就是因为丢失DemoDaoImpl导致项目编译不能通过,BeanFactory强依赖于DemoDaoImpl这种叫做“紧耦合”。

反射解决紧耦合问题

那针对于这种问题,反射是可以解决的。具体思路是通过反射读取它的字节码文件从而创建实例。代码实现如下:

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

代码中,通过try...catch来捕获异常。反射是在运行中,才会去检查这个class文件是否存在的。总之,到这里编译问题解决了,项目可以拉起来了。使用反射之后,项目不再会因为DemoDaoImpl不存在而导致编译失败,BeanFactory对DemoDaoImpl的依赖程度降低了(弱依赖)。

硬编码

当然,现在存在一种硬编码的问题,类的全限定名在程序中被写死了。而在java中有一个Properties类来读取外部properties文件,我们可以将类的全限定名写到配置文件中,当然也可以写一些其他配置。然后去读取配置文件,这样就解决了硬编码问题。关于BeanFactory类的所有代码,我在下面贴出来。

Properties操作放入类的静态代码块中,类初始化编执行配置的读取。我们引入了HashMap作为缓冲机制,加入双检锁,主要是为了来确保它是单例的。

public class BeanFactory {

    private static Properties properties;
    // 使用静态代码块初始化properties,加载factord.properties文件
    //缓存区,保存已经创建好的对象
    private static Map<String,Object> beanMap = new HashMap<>();
    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 DemoDao getDemoDao(){
//        return new DemoDaoImpl();
//        return new DemoOracleDaoImpl();
        //版本一:未解决硬编码went
        try {
            return (DemoDao) Class.forName("com.lyz.dao.impl.DemoDaoImpl").newInstance();
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("DemoDao instantiation error, cause: " + e.getMessage());
        }

    }

    //为了控制线程并发,需要引入双检锁保证对象只有一个
    public static Object getBean(String beanName){
        // 双检锁保证beanMap中确实没有beanName对应的对象
        if (!beanMap.containsKey(beanName)) {
            // synchronized作用应该是阻塞其他访问该对象的线程
            synchronized (BeanFactory.class) {
                if (!beanMap.containsKey(beanName)) {
                    // 过了双检锁,证明确实没有,可以执行反射创建
                    try {
                        Class<?> beanClazz = Class.forName(properties.getProperty(beanName));
                        Object bean = beanClazz.newInstance();
                        // 反射创建后放入缓存再返回
                        beanMap.put(beanName, bean);
                    } catch (ClassNotFoundException e) {
                        throw new RuntimeException("BeanFactory have not [" + beanName + "] bean!", e);
                    } catch (IllegalAccessException | InstantiationException e) {
                        throw new RuntimeException("[" + beanName + "] instantiation error!", e);
                    }
                }
            }
        }
        return beanMap.get(beanName);
    }
}

IOC思想

针对于上述的构建,我们生成实例的不再是new DemoDaoImpl()这样了,而是变成了BeanFactory.getBean("demoDao")。这两种方式的不同之处在于前者是主动声明实现类,后者不需要我们声明,它将对象的获取方式交给了BeanFactory。这种将控制权交给别人的思想就是控制反转,也即我们的IOC。IOC的底层是通过反射的方式来获取对象,可见的好处就是没有硬编码、项目启动时跳过编译检查等。