重新认识Ioc

286 阅读10分钟

自从Spring问世以后,在中国大部分项目是基于Spring来搭建的,导致大部分程序员只会Spring Ioc来实现业务代码,却鲜有人了解为什么要使用Ioc。今天我们需要重新认识一下Ioc,深入了解为什么需要Ioc。

控制反转(inversion of control,简称ioc)是一种设计原则,顾名思义,它就是把原来代码里需要实现的对象创建,依赖,反转给容器来帮忙实现。

传统编程模型

MVC是经典的三层架构,大致代码如下:

public class UserServlet extends HttpServlet{
    // 创建userService
    private final UserService userService = new UserServiceImpl();

    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

        userService.doSth();
    }
    
}


public class UserServiceImpl implements UserService{
    // 创建UserDao
    private final UserDao userDao = new UserDaoImpl();

    // doSth()
}

public class UserDaoImpl implements UserDao{

    private DataSource datasource;

    public UserDaoImpl(){
        // 创建Jdbc Connection
    }

    // doSth()
}

三层架构的主要目的是为了确保单一职责,视图层主要为了页面展示,服务层则为了处理复杂的业务逻辑,持久层则为了与数据库进行交互。层次越低复用程度越高,比如一个DAO对象往往会被多个 Service 对象使用,一个 Service 对象往往也会被多个Servlet对象使用:

这种结构本身没有太大的问题,逻辑复用率得到了提高,但资源复用率则没有提高。实际上多个Servlet可以复用相同的Service实例,多个Service也可以复用相同的dao实例。针对这个问题,我们自然而然想到的是单例模式,虽说我们可以用单例模式来解决问题,这样每个类都需要编写单例,会存在大量重复的代码,其弊端不言而明。

即使我们使用了单例模式,也会有另外的问题,比如Dao层,UserDao这个接口,我可以实现一个UserDaoJdbcImpl类,通过访问数据库的方式来存储user对象,我也可以实现一个UserDaoRedisImpl类,可以通过访问redis的方式存储User对象。假设我有2个Service需要用到UserDaoJdbcImpl,有1个Service需要用UserDaoRedisImpl,我需要根据需求将具体的实现类显示得在Service里创建出来,未来换个需求可能就要修改代码。当然这个问题的本质就是面向抽象实现而非具体实现编程。

即使我们使用了面向接口编程,还会有另外的问题。还是以Dao层为例,UserDaoJdbcImpl实现类,通过访问数据库的方式来存储user对象,需要创建数据库连接。如果环境比较多,有开发环境,测试环境,uat环境,生产环境,每个环境的数据库连接是不一样的,如果需要根据环境切换数据库连接,通过硬代码的方式,即使通过配置文件能够写出来,也不是非常优雅的。这个问题的本质就是创建和管理配置。

我们稍微整理一下问题:

  1. 如何提高资源利用率,又能避免单例模式存在大量重复代码的问题。
  2. 面向接口实现而非具体实现。
  3. 创建和管理相关配置。

其实就是需要一个管理者,能够帮助我们管理对象。

实际上在Spring出来之前,也是有一些解决方案的,我们也一起来学习一下,看看Spring Ioc与其他方案的优缺点。

Ioc容器需要具备的功能或者特性

  • 能够管理应用程序代码
  • 启动速度快
  • 无需任务特殊依赖,就可以创建对象
  • 可以提高资源利用率,依赖低,基本对业务代码无侵入性,能够运行在多种环境中
  • 性能开销低
  • 可测试性高
  • 更好的面向对象

Ioc实现方法

Ioc主要有两种实现方法:

  • 依赖注入
  • 依赖查找
类型依赖处理实现便利性代码侵入性API依赖性可读性
依赖查找主动获取相对繁琐侵入业务逻辑依赖容器API良好
依赖注入被动获取相对便利低侵入性不依赖容器API一般

依赖注入方式:

  • 构造器注入:不可变对象,保证必须依赖项不为空
  • setter方法注入:主要用于可选依赖项,且这些非必需依赖项在使用之前都必须进行非空检查,主要是因为不能想构造器注入那样,保证必须依赖项不为空。可以部分避免循环依赖的问题。
  • 字段注入

传统Ioc实现

Java Beans

JavaBeans是Java中一种特殊的类,可以将多个对象封装到一个对象(bean)中。特点是可序列化,提供无参构造器(英语:Nullary constructor),提供getter方法和setter方法访问对象的属性。名称中的“Bean”是用于Java的可重用软件组件的惯用叫法。

优点

  • Bean可以控制它的属性、事件和方法是否暴露给其他程序。
  • Bean可以接收来自其他对象的事件,也可以产生事件给其他对象。
  • 有软件可用来配置Bean。
  • Bean的属性可以被序列化,以供日后重用。

规范如下:

  • 有一个public的无参数构造函数。
  • 属性可以透过get、set、is(可替代get,用在布尔型属性上)方法或遵循特定命名规则的其他方法访问。
  • 可序列化。

代码示例:

public class StudentsBean implements java.io.Serializable{
   private String firstName = null;
   private String lastName = null;
   private int age = 0;

   public StudentsBean() {
   }
   public String getFirstName(){
      return firstName;
   }
   public String getLastName(){
      return lastName;
   }
   public int getAge(){
      return age;
   }

   public void setFirstName(String firstName){
      this.firstName = firstName;
   }
   public void setLastName(String lastName){
      this.lastName = lastName;
   }
   public void setAge(int age) {
      this.age = age;
   }
}

UI JavaBean访问方式:

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<html>
<head>
<title>get 和 set 属性实例</title>
</head>
<body>

<jsp:useBean id="students" 
                    class="com.runoob.StudentsBean"> 
   <jsp:setProperty name="students" property="firstName"
                    value="小强"/>
   <jsp:setProperty name="students" property="lastName" 
                    value="王"/>
   <jsp:setProperty name="students" property="age"
                    value="10"/>
</jsp:useBean>

<p>学生名字: 
   <jsp:getProperty name="students" property="firstName"/>
</p>
<p>学生姓氏: 
   <jsp:getProperty name="students" property="lastName"/>
</p>
<p>学生年龄: 
   <jsp:getProperty name="students" property="age"/>
</p>

</body>
</html>

通过内省Introspector方式访问

public class User implements Serializable {
  private int id;
  private String username;
  private String password;

  public int getId() {
    return id;
  }

  public void setId(int id) {
    this.id = id;
  }

  public String getUsername() {
    return username;
  }

  public void setUsername(String username) {
    this.username = username;
  }

  public String getPassword() {
    return password;
  }

  public void setPassword(String password) {
    this.password = password;
  }

  public static void main(String[] args) throws Exception{
    BeanInfo beanInfo = Introspector.getBeanInfo(User.class);
    BeanDescriptor beanDescriptor = beanInfo.getBeanDescriptor();
    MethodDescriptor[] methodDescriptors = beanInfo.getMethodDescriptors();

    for (MethodDescriptor methodDescriptor: methodDescriptors){

    }

    PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors();
    User user = new User();
    for (PropertyDescriptor propertyDescriptor : propertyDescriptors) {
      if("class".equals(propertyDescriptor.getName())){
        continue;
      }
      Method readMethod = propertyDescriptor.getReadMethod();
      Method writeMethod = propertyDescriptor.getWriteMethod();
      String name = writeMethod.getName();
      if(name.equals("setUsername")){
        writeMethod.invoke(user, "shawn");
      }

    }

    System.out.println(user.getUsername());
  }
}

上述代码可以参考javax.el.BeanELResolver。

Java Spi

在JavaSE中有一个服务提供发现的机制,Service Provider Interface。例如java.sql.Driver接口,不同的数据库厂商有不同的实现方式,Java会通过SPI机制,找到对应的实现类。Java中SPI机制主要思想是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要,其核心思想就是解耦。

我们已java.sql.Driver接口,MySQL驱动实现为例,看看SPI是怎么实现的。

com.mysql.cj.jdbc.Driver实现了Driver接口,然后在classpath目录下,建立了目录META-INF/services。

然后在该目录下,创建一个文件名为java.sql.Driver的文件,其文件名就是接口的全限定名,文件内容为com.mysql.cj.jdbc.Driver,为MySQL的驱动全限定名。

最后应该如何发现MySQL Driver呢?查看DriverManager类型,我本地JDK的版本为11,其源码如下:

private static void ensureDriversInitialized() {
        if (driversInitialized) {
            return;
        }

        synchronized (lockForInitDrivers) {
            // ...省略部分代码
            AccessController.doPrivileged(new PrivilegedAction<Void>() {
                public Void run() {

                    ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                    Iterator<Driver> driversIterator = loadedDrivers.iterator();

                    try {
                        while (driversIterator.hasNext()) {
                            driversIterator.next();
                        }
                    } catch (Throwable t) {
                        // Do nothing
                    }
                    return null;
                }
            });

            // 。。。省略部分代码
        }
    }

JDK中查找服务的实现的工具类是:java.util.ServiceLoader。我们发现ServiceLoader实现了Iterable接口,也就是说ServiceLoader实际上也是一个迭代器,作用可以根据接口类型依赖查找该接口所有的实现类。

JNDI

Java 命名和目录接口 (Java Naming and Directory Interface, JNDI) 是一种应用编程接口 (application programming interface, API),用于访问不同类型的命名和目录服务。Java EE 组件通过调用 JNDI 查找方法,根据名称来进行依赖查找。

这些对象可以存储在不同的命名或目录服务中,例如远程方法调用(RMI),公共对象请求代理体系结构(CORBA),轻型目录访问协议(LDAP)或域名服务(DNS)。

每一个对象都有一组唯一的键值绑定,将名字和对象绑定,可以通过名字检索指定的对象,而该对象可能存储在RMI、LDAP、CORBA等等。

下表列出了用于 Application Server 所使用的 J2EE 资源的 JNDI 查找及其关联的引用。

JNDI 查找名称关联的引用
java:comp/env应用程序环境项
java:comp/env/jdbcJDBC 数据源资源管理器连接工厂
java:comp/env/ejbEJB 引用
java:comp/UserTransactionUserTransaction 引用
java:comp/env/mailJavaMail 会话连接工厂
java:comp/env/urlURL 连接工厂
java:comp/env/jmsJMS 连接工厂和目的地
java:comp/ORB应用程序组件之间共享的 ORB 实例

传统的Jdbc编程如下:

class.forName("") //加载驱动
Connection conn = DriverManager.getConnection(url, username, password);
// conn相关操作
conn.close();

即使有了Java SPI,省去了class.forName()这一行代码,剩下来的代码还是需要的。

目前代码存在的问题如下:

  1. 如果连接的数据库发生了变化或者username, password发生了变化,就需要重新修改代码然后打包,比较繁琐
  2. 如果mysql数据库切换成了oralce数据库,也需要重新打包。
  3. 目前还只是一个连接,怎么使用连接池,连接池参数怎么调整。

实际上这些都可以通过配置文件来解决,程序员只需要关注配置,然后让程序引用配置即可。所以我们也可以把JNDI看成是一个配置文件,主要配置资源。

我们以tomcat为例,看看tomcat如何实现jndi的。

  1. 在src/main/webapp/META-INF目录下创建一个context.xml文件,如果没有META-INF目录,则先创建该目录
  2. context.xml内容如下:
<Context>

    <!-- maxTotal: Maximum number of database connections in pool. Make sure you
         configure your mysqld max_connections large enough to handle
         all of your db connections. Set to -1 for no limit.
         -->

    <!-- maxIdle: Maximum number of idle database connections to retain in pool.
         Set to -1 for no limit.  See also the DBCP 2 documentation on this
         and the minEvictableIdleTimeMillis configuration parameter.
         -->

    <!-- maxWaitMillis: Maximum time to wait for a database connection to become available
         in ms, in this example 10 seconds. An Exception is thrown if
         this timeout is exceeded.  Set to -1 to wait indefinitely.
         -->

    <!-- username and password: MySQL username and password for database connections  -->

    <!-- driverClassName: Class name for the old mm.mysql JDBC driver is
         org.gjt.mm.mysql.Driver - we recommend using Connector/J though.
         Class name for the official MySQL Connector/J driver is com.mysql.jdbc.Driver.
         -->

    <!-- url: The JDBC connection url for connecting to your MySQL database.
         -->

  <Resource name="jdbc/TestDB" auth="Container" 
    type="javax.sql.DataSource"
    maxTotal="100" 
    maxIdle="30" 
    maxWaitMillis="10000"
    username="root" 
    password="123456" 
    driverClassName="com.mysql.cj.jdbc.Driver"
    url="jdbc:mysql://localhost:3306/demo"/>

</Context>
  1. 在WEB-INF/web.xml文件内添加
  <resource-ref>
      <description>DB Connection</description>
      <res-ref-name>jdbc/TestDB</res-ref-name>
      <res-type>javax.sql.DataSource</res-type>
      <res-auth>Container</res-auth>
  </resource-ref>
  1. 然后可以通过InitialContext类进行访问
InitialContext context = new InitialContext();
Context envContext = (Context)context.lookup("java:comp/env");
DataSource ds = (DataSource)envContext.lookup("jdbc/TestDB");
Connection conn = ds.getConnection();
// 

参考文档:Apache Tomcat 9 (9.0.98) - JNDI Datasource How-To

Servlet

实际上Servlet设计的主要思想也是控制反转,但是容器只会控制Servlet对象。

Servlet容器会实例化和调用Servlet,那Servlet是怎么注册到Servlet容器中的呢?一般来说,我们是以Web应用程序的方式来部署Servlet的,而根据Servlet规范,Web应用程序有一定的目录结构,在这个目录下分别放置了Servlet的类文件、配置文件以及静态资源,Servlet容器通过读取配置文件,就能找到并加载Servlet。Web应用的目录结构大概是下面这样的:

| -  MyWebApp
      | -  WEB-INF/web.xml        -- 配置文件,用来配置Servlet等
      | -  WEB-INF/lib/           -- 存放Web应用所需各种JAR包
      | -  WEB-INF/classes/       -- 存放你的应用类,比如Servlet类
      | -  META-INF/              -- 目录存放工程的一些信息

Servlet规范里定义了ServletContext这个接口来对应一个Web应用。Web应用部署好后,Servlet容器在启动时会加载Web应用,并为每个Web应用创建唯一的ServletContext对象。你可以把ServletContext看成是一个全局对象,一个Web应用可能有多个Servlet,这些Servlet可以通过全局的ServletContext来共享数据,这些数据包括Web应用的初始化参数、Web应用目录下的文件资源等。由于ServletContext持有所有Servlet实例,你还可以通过它来实现Servlet请求的转发。

Ioc框架

除了Spring,还有Google Guice(github.com/google/guic…

参考文档:developer.aliyun.com/article/518…