说明:进阶目录下,会对 IOC、AOP 的应用做一次相对全的介绍,同时包括部分源码等的说明,因此,重点在于分析每个部分/组件,因此不会一步一步创建工程等,重点还是理解 Spring 设计的精妙和思想。刚接触的小伙伴,推荐阅读基础部分后,直接跳转 SpringMvc 继续学习。

1. 传统代码

三层结构:client / servlet / controller <==> service(doGet/doPost)<==> dao(mapper)

  1. public class UserServiceImpl implements UserService {
  2. /* 直接 new 实例化 */
  3. private UserDao userDao = new UserDaoImpl();
  4. @Override
  5. public List<User> listAllUser() {
  6. return userDao.listAllUser();
  7. }
  8. }

存在的问题:

  1. 调用方强依赖于被调用方,即 Service 强依赖于 Dao 层实现。一旦 UserDaoImpl 出现丢失或者不可读的情况,则会无法通过编译。

    2. Spring IOC 的思路

    这只是对其思路的仿真,并不是源码,重点是理解思路。

    2.1 核心实现

    创建 bean.properties
    1. userDao=com.ideal.dao.impl.UserDaoImpl
    2. userService=com.ideal.service.impl.UserServiceImpl
    创建工厂(核心) ```java package com.ideal.factory;

import lombok.extern.slf4j.Slf4j;

import java.io.IOException; import java.util.HashMap; import java.util.Map; import java.util.Properties;

/**

  • Bean 工厂 *
  • @author baiwenhuang
  • @date 2022-03-08 5:38 下午 / @Slf4j public class BeanFactory { /

    • properties 文件 */ private static Properties properties;

      /**

    • 缓存 */ private static Map beanCatchMap = new HashMap<>();

      static { properties = new Properties(); try {

       properties.load(BeanFactory.class.getClassLoader().getResourceAsStream("bean.properties"));
      

      } catch (IOException e) {

       throw new ExceptionInInitializerError("load bean.properties error " + e.getMessage());
      

      } }

      public static Object getBean(String beanName) { if (!beanCatchMap.containsKey(beanName)) {

       synchronized (BeanFactory.class) {
           if (!beanCatchMap.containsKey(beanName)) {
               try {
                   Class<?> beanClass = Class.forName(properties.getProperty(beanName));
                   Object beanObject = beanClass.newInstance();
                   beanCatchMap.put(beanName, beanObject);
               } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
                   throw new RuntimeException("getBean error", e);
               }
           }
       }
      

      } return beanCatchMap.get(beanName); } }

      通过工厂获取实例对象
      ```java
      private UserDao userDao = (UserDao) BeanFactory.getBean("userDao");
      

      2.2 解释

      2.2.1 解决强依赖/高耦合问题

      我们应该努力让程序在编译期不依赖,而运行时才可以有一些必要的依赖(依赖是不可能完全消除的),因为 UserDaoImpl 这个类丢失或者不可读,就相当于代码编译时就缺少了这个类,这也是强依赖的体现。而我们可以通过其类的全限定类名,进行反射,这样也可以构造对象。

而反射是在代码中运行时才会执行,虽然代码仍然不可以运行,但是对于这个 UserDaoImpl 丢失的报错已经延迟到了运行时。即强依赖变成了若依赖。

关键代码:

  • properties.getProperty(beanName) 先别管,下面会介绍,这里当做类的全限定名 ```java Class<?> beanClass = Class.forName(properties.getProperty(beanName));

// 上线代码实际执行的样子类似如下(只不过换成动态的了) Class<?> beanClass = Class.forName(“com.ideal.dao.impl.UserDao);

<a name="RZUtE"></a>
### 2.2.2 优化硬编码问题
我们为什么要直接把数据写固定呢,直接通过 Properties 获取不香嘛。<br />在 resources 下创建了 key-value 格式的内容,key 是 bean 的 name,value 是这个 bean 的全限定名

关键代码:

- 通过静态代码块,起到在初始化阶段,加载一次 properties 文件的效果。
- 如果 Factory init 失败了,那么直接抛出异常吧,已经没继续走的必要了。
```java
static {
    properties = new Properties();
    try {
        properties.load(BeanFactory.class.getClassLoader().getResourceAsStream("bean.properties"));
    } catch (IOException e) {
        throw new ExceptionInInitializerError("load bean.properties error " + e.getMessage());
    }
}

这也对应了前面提到的反射中的 properties.getProperty(beanName)调用者传什么,我就再 properties 中找什么,非常灵活。

Class<?> beanClass = Class.forName(properties.getProperty(beanName));

2.2.3 多例到单例

在调用处,打印多次 BeanFactory.getBean("userDao");
看下结果:特别显眼的十次输出,我们的问题也就出来了,我所创建的10个对象是不同的,也就是说,每一次调用,都会实例化一个新的对象,这也叫做多例

com.ideal.dao.impl.UserDaoImpl@53ca01a2
com.ideal.dao.impl.UserDaoImpl@358c99f5
com.ideal.dao.impl.UserDaoImpl@3ee0fea4
com.ideal.dao.impl.UserDaoImpl@48524010
com.ideal.dao.impl.UserDaoImpl@4b168fa9
com.ideal.dao.impl.UserDaoImpl@1a84f40f
com.ideal.dao.impl.UserDaoImpl@23282c25
com.ideal.dao.impl.UserDaoImpl@7920ba90
com.ideal.dao.impl.UserDaoImpl@6b419da
com.ideal.dao.impl.UserDaoImpl@3b2da18f

这有什么问题吗?

  1. 多次创建对象的代价就是消耗性能,导致效率会低一些
  2. 相比较单例,jvm会回收较多的垃圾
  3. 获取速度比单例慢,因为单例除了第一次,其后都是从缓存中获取

所以,我们要试着将它改成单例的,单例从表现上来看,我们查询到的对象都应该是一个。
这里大家回头看前面的代码,我们通过引入了 Map 作为缓存,配合单例模式的双重校验锁模式,实现了线程安全的单例。

补充:单例也不是就全是好处,单例模式下在并发情况下,可能会出现线程安全问题。请规避在类

3. IOC 的思想和启发

3.1 IOC 和 DL

  • new 实例化:强依赖,编译期需要保证代码完整有效,否则报错,但是运行期肯定是能成功的。
  • 工厂实例化:弱依赖,运行时才去通过 BeanFactory 获取 properties 中的 value,然后根据这个全限定类名 value 去强转为 UserDao 对象,如果失败,则抛出异常。

new 的感觉,就是我们自己来创建对象,所以需要保证它一开始就是对的。而使用工厂在运行时创建对象,就是将控制权交给了别人(代码中的工厂)。

所以 ,IOC(Inverse Of Contro) 也就是控制反转,它是一种思想,也就是将在程序中创建对象的控制权,从自己手上交到 Spring 框架中,IOC 可以理解为一个工厂,创建对象的时候,只需要配置好文件或者注解就可以了,对象在背后如何被创建,完全无需操心。不仅效率高了,而且耦合度也大大的降低了。

而这个工厂根据指定的 beanName 获取创建对象的过程就叫做依赖查找( Dependency Lookup , DL )。

3.2 启发

  • 善用静态工厂,它可将多处的依赖进行抽取和分离。
  • 外部化配置文件+反射 = 可解决配置的硬编码问题。
  • 缓存 + 单例可以解决对象实例个数的问题,规避线程风险。