开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第18天,点击查看活动详情
本文主要通过Servlet架构和传统开发带来的问题来引出我们的IOC,以便于大家理解。
Servlet时代的MVC架构
在正式引出IOC之前,我们先来讲一下Servlet时代的MVC三层架构。Servlet在MVC三层架构中充当controller的功能,前端发来请求达到servlet上,servlet执行本身的一些方法如重写doget来处理逻辑,它可以将具体的处理逻辑丢给service来实现,service再调用Dao,简单来说这就Servlet时代MVC三层架构的处理方式。如下图所示(该图摘自网络内容)。
需求变更
数据库变更
假设我们在前面用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的底层是通过反射的方式来获取对象,可见的好处就是没有硬编码、项目启动时跳过编译检查等。