控制反转(Inversion of Control,IoC)是一种设计思想,在Java中就是将设计好的对象交给容器控制,而不是传统的在对象内部直接控制。传统Java SE程序设计,我们直接在对象内部通过new进行创建对象,是程序主动去创建依赖对象;而IoC是有专门一个容器来创建这些对象,即由Ioc容器来控制对象的创建;可以理解为IoC 容器控制了对象和外部资源获取(不只是对象包括比如文件等)。
有反转就有正转,传统应用程序是由我们自己在对象中主动控制去直接获取依赖对象,也就是正转;而反转则是由容器来帮忙创建及注入依赖对象;为何是反转?因为由容器帮我们查找及注入依赖对象,对象只是被动的接受依赖对象,所以是反转;哪些方面反转了?依赖对象的获取被反转了。
IoC 不是一种技术,只是一种思想,一个重要的面向对象编程的法则,它能指导我们如何设计出松耦合、更优良的程序。传统应用程序都是由我们在类内部主动创建依赖对象,从而导致类与类之间高耦合,难于测试;有了IoC容器后,把创建和查找依赖对象的控制权交给了容器,由容器进行注入组合对象,所以对象与对象之间是 松散耦合,这样也方便测试,利于功能复用,更重要的是使得程序的整个体系结构变得非常灵活。
此外,IoC对编程带来的最大改变不是从代码上,而是从思想上,发生了“主从换位”的变化。应用程序原本是老大,要获取什么资源都是主动出击,但是在IoC/DI思想中,应用程序就变成被动的了,被动的等待IoC容器来创建并注入它所需要的资源了。
本项目建立在入门案例中传统三层架构的基础上,项目结构如下:
首先在pom.xml文件中添加如下内容:
<packaging>jar</packaging> <dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.2.5.RELEASE</version> </dependency> </dependencies>
在resource目录下新建beans.xml文件,首先需要导入约束:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> </bean>
这里有一个小细节,在创建xml文件的时候,选择new->XML Configuration File->Spring Config,就会自动创建带有约束的Spring的xml配置文件。如下图:
在bean标签内部添加如下内容:IOC容器本质上是一个map,id就是key,class对应的就是bean对象的全限定类名,Spring可以依据全限定类名来创建bean对象来作为map的value属性。
<!-- 把对象的创建交给Spring来管理 --> <bean id="accountService" class="service.impl.AccountServiceImpl"></bean> <bean id="accountDao" class="dao.impl.AccountDaoImpl"></bean>
在src/main/java目录下创建ui.Client类:
public class Client { /** * 获取Spring的IoC核心容器,并根据id获取对象 * @param args */ public static void main(String[] args) { //1.获取IoC核心容器 ApplicationContext applicationContext = new ClassPathXmlApplicationContext("beans.xml"); //2.根据id获取bean对象 //第一种方法:只传入id获取到对象之后强转为需要的类型 IAccountService accountService = (IAccountService) applicationContext.getBean("accountService"); System.out.println(accountService); //第二种方法:传入id和所需要类型的字节码,这样getBean返回的对象就已经是所需要的对象 IAccountDao accountDao = applicationContext.getBean("accountDao", IAccountDao.class); System.out.println(accountDao); } }
关于ApplicationContext,这里需要说明一下,首先通过选中这个接口然后右键Diagrams->Show Diagrams,可以看到接口的继承关系:其中BeanFactory接口就是IoC容器的底层接口。
在diagram中选中ApplicationContext接口,然后右键Show Implementations,可以看到该接口的实现类:
关于这些实现类需要说明如下几点:
ApplicationContext的实现类: 1.ClassPathXmlApplicationContext:加载类路径下的配置文件,要求配置文件必须在类路径下 2.FileSystemApplicationContext:加载磁盘任意路径下的配置文件,要求配置文件必须有访问权限,这种方法不常用 3.AnnotationApplicationContext:用于读取注解创建容器
为了更加清楚地看到这两个接口之间的区别,我们在AccountDaoImpl和AccountServiceImpl类的无参构造方法中添加如下内容:
//AccountDaoImpl public AccountDaoImpl() { System.out.println("dao创建了"); } //AccountServiceImpl public AccountServiceImpl() { System.out.println("service创建了"); }
对ui.Client类中的main方法添加如下代码:
Resource resource = new ClassPathResource("beans.xml"); BeanFactory factory = new DefaultListableBeanFactory(); BeanDefinitionReader bdr = new XmlBeanDefinitionReader((BeanDefinitionRegistry) factory); bdr.loadBeanDefinitions(resource); System.out.println(factory.getBean("accountDao"));
采用断点调试,我们可以发现:
第一种方式:使用默认构造方法创建
在Spring配置文件中使用bean标签,如果只有id和class属性,就会使用默认构造方法(无参构造方法)创建对象。如果没有默认构造方法,则对象无法创建。例如,之前我们所使用的便是这第一种方式。
<bean id="accountService" class="service.impl.AccountServiceImpl"></bean>
第二种方式:使用其他类(比如工厂类)中的方法创建对象,并存入Spring容器,该类可能是jar包中的类,无法通过修改源码来提供默认构造方法。
为了演示,我们在src/main/java目录新建factory包,在factory包下新建类InstanceFactory:
public class InstanceFactory { //非静态方法 public IAccountService getAccountService() { return new AccountServiceImpl(); } }
instanceFactory对应的就是factory包下的InstanceFactory类的对象,accountService对应的是InstanceFactory类下的getAccountService方法返回的对象。factory-bean属性用于指定创建本次对象的factory,factory-method属性用于指定创建本次对象的factory中的方法。
<bean id="instanceFactory" class="factory.InstanceFactory"></bean> <bean id="accountService" factory-bean="instanceFactory" factory-method= "getAccountService"></bean>
第三种方式:使用其他类(比如工厂类)中的静态方法创建对象,并存入Spring容器,该类可能是jar包中的类,无法通过修改源码来提供默认构造方法。
为了演示,我们在src/main/java目录新建factory包,在factory包下新建类StaticFactory:
public class StaticFactory { //静态方法 public static IAccountService getAccountService() { return new AccountServiceImpl(); } }
由于是静态方法,所以无需指定factory-bean属性。class属性指定创建bean对象的工厂类,factory-method方法指定创建bean对象的工厂类中的静态方法。
<bean id="accountService" class="factory.StaticFactory" factory-method="getAccountService"></bean>
bean标签的scope属性(用于指定bean对象的作用范围),有如下取值:常用的就是单例和多例
这里我们演示单例和多例:
<bean id="accountService" class="service.impl.AccountServiceImpl" scope="singleton"></bean> <bean id="accountDao" class="dao.impl.AccountDaoImpl" scope="prototype"></bean>
此时即便Client类中的main方法使用ApplicationContext接口:
public static void main(String[] args) { //1.获取IoC核心容器 ApplicationContext applicationContext = new ClassPathXmlApplicationContext("beans.xml"); //2.根据id获取bean对象 //第一种方法:只传入id获取到对象之后强转为需要的类型 IAccountService accountService = (IAccountService) applicationContext.getBean("accountService"); System.out.println(accountService); //第二种方法:传入id和所需要类型的字节码,这样getBean返回的对象就已经是所需要的对象 IAccountDao accountDao = applicationContext.getBean("accountDao", IAccountDao.class); IAccountDao accountDao1 = (IAccountDao) applicationContext.getBean("accountDao"); System.out.println(accountDao == accountDao1); }
使用断点调试,我们可以发现:
在执行到ApplicationContext applicationContext = new ClassPathXmlApplicationContext("beans.xml");时,就会输出“service创建了”,不会输出“dao创建了”。
只有当执行到IAccountDao accountDao = applicationContext.getBean("accountDao", IAccountDao.class);和IAccountDao accountDao1 = (IAccountDao) applicationContext.getBean("accountDao");时,才会输出“dao创建了”。
并且accountDao == accountDao1的结果是false。
为了演示,这里需要介绍bean标签的两个属性:init-method属性指定初始化方法,destroy-method属性指定销毁方法
<bean id="accountService" class="service.impl.AccountServiceImpl" scope="singleton" init-method="init" destroy-method="destroy"></bean> <bean id="accountDao" class="dao.impl.AccountDaoImpl" scope="prototype" init-method="init" destroy-method="destroy"></bean>
同时,还有在AccountDaoImpl类和AccountService类中添加如下代码:
//AccountDaoImpl: public void init() { System.out.println("dao初始化了"); } public void destroy() { System.out.println("dao销毁了"); } //AccountServiceImpl: public void init() { System.out.println("service初始化了"); } public void destroy() { System.out.println("service销毁了"); }
为了手动关闭容器需要在Client类中的main方法中最后加入:
//容器需要手动关闭,因为applicationContext是接口类型,所以没有close方法,需要强制转换为实现类对象 ((ClassPathXmlApplicationContext) applicationContext).close();
这个时候,我们再去使用断点调试,可以发现:
在之前的代码中,我们一直没有使用AccountServiceImpl对象中的saveAccount方法,这是因为我们还没有实例化该类中的accountDao对象。我们先看看AccountServiceImpl的源代码:
public class AccountServiceImpl implements IAccountService { //持久层接口对象的引用,为了降低耦合,这里不应该是new AccountDaoImpl private IAccountDao accountDao; public AccountServiceImpl() { System.out.println("service创建了"); } /** 模拟保存账户操作 */ public void saveAccounts() { System.out.println("执行保存账户操作"); //调用持久层接口函数 accountDao.saveAccounts(); } }
在之前的三层架构中,对于accoutDao对象,我们是private IAccountDao accountDao = new AccountDaoImpl(); 实际上,为了降低耦合,我们不应该在此处对accountDao对象进行实例化操作,应该直接是private IAccountDao accountDao; 。为了将该对象实例化,我们就需要用到依赖注入。
依赖注入(Dependency Injection, DI):它是spring框架核心IoC的具体实现(IoC是一种思想,而DI是一种设计模式)。 在编写程序时,通过控制反转,把对象的创建交给了 spring,但是代码中不可能出现没有依赖的情况。IoC 解耦只是降低他们的依赖关系,但不会消除。例如:我们的业务层仍会调用持久层的方法,这种业务层和持久层的依赖关系,在使用 spring 之后,就让 spring 来维护了。简单的说,就是让框架把持久层对象传入业务层,而不用我们自己去获取。
在依赖注入中,能够注入的数据类型有三类:
为了演示依赖注入,我们在src/main/java目录下,新建一个包entity,在该包下新建实体类People:
代码中的字段如下,注意构造方法一定要加上无参构造方法。
public class People { //如果是经常变化的数据,并不适用于依赖注入 private String name; private Integer age; //Date类型不是基本类型,属于Bean类型 private Date birthDay; //以下都是集合类型 private String[] myString; private List<String> myList; private Set<String> mySet; private Map<String, String> myMap; private Properties myProps; //为了节省空间,这里省略了所有的set方法和toString方法,在实际代码中要补上 public People() { } //提供默认构造方法 public People(String name, Integer age, Date birthDay) { this.name = name; this.age = age; this.birthDay = birthDay; } }
注入的方式有三种:
使用构造方法注入
这种方式使用的标签为constructor-arg,在bean标签的内部使用,该标签的属性有五种,其中的1-3种用于指定给构造方法中的哪个参数注入数据:
<bean id="people1" class="entity.People"> <!-- 如果有多个String类型的参数,仅使用type标签无法实现注入 --> <constructor-arg type="java.lang.String" value="Jack"></constructor-arg> <constructor-arg index="1" value="18"></constructor-arg> <constructor-arg name="birthDay" ref="date"></constructor-arg> </bean> <!-- 配置一个日期对象 --> <bean id="date" class="java.util.Date"></bean>
使用set方法注入
这种方式使用的标签为property,在bean标签的内部使用,该标签的属性有三种:
<bean id="people2" class="entity.People"> <property name="name" value="Jack"></property> <property name="age" value="18"></property> <property name="birthDay" ref="date"></property> </bean>
使用注解注入:本篇主要讲解使用xml配置文件的方式注入,因此这种方法暂不做介绍
这里我们使用set方法来向集合中注入数据,对于使用的标签,注意以下三点:
<bean id="people3" class="entity.People"> <property name="myString"> <array> <value>AAA</value> <value>BBB</value> <value>CCC</value> </array> </property> <property name="myList"> <list> <value>ListA</value> <value>ListB</value> <value>ListC</value> </list> </property> <property name="mySet"> <set> <value>SetA</value> <value>SetB</value> <value>SetC</value> </set> </property> <property name="myMap"> <map> <entry key="A" value="MapA"></entry> <entry key="B" value="MapB"></entry> <!-- 对于entry标签,可以使用value属性来指定值,也可以在标签内部使用value标签 --> <entry key="C"> <value>MapC</value> </entry> </map> </property> <property name="myProps"> <props> <!-- 对于prop标签,只有key属性,没有value属性,所以直接将该标签的值作为value --> <prop key="A">PropA</prop> <prop key="B">PropB</prop> <prop key="C">PropC</prop> </props> </property> </bean>
在本部分的开头,我们还有一个问题没有解决,那就是AccountServiceImpl类中的accountDao对象无法实例化。现在我们就可以通过配置的方式来对进行依赖注入:
<bean id="accountService" class="service.impl.AccountServiceImpl"> <property name="accountDao" ref="accountDao"></property> </bean> <bean id="accountDao" class="dao.impl.AccountDaoImpl"></bean>
最后我们再进行统一的测试,修改Client类中的main方法:
public static void main(String[] args) { //验证依赖注入 ApplicationContext applicationContext = new ClassPathXmlApplicationContext("beans.xml"); People people1 = applicationContext.getBean("people1", People.class); System.out.println(people1); People people2 = applicationContext.getBean("people2", People.class); System.out.println(people2); People people3 = applicationContext.getBean("people3", People.class); System.out.println(people3); //向accountService中注入accountDao以调用saveAccounts方法 IAccountService accountService = (IAccountService) applicationContext.getBean("accountService"); System.out.println(accountService); accountService.saveAccounts(); }
运行代码,结果如下: