Spring IOC (Inversion of Control, 控制反转 )是Spring的一个核心思想,其包括两层含义:
控制 反转
在Spring中, 控制反转 也可以简单地理解为将对象的创建和管理交由IOC容器来完成。
IOC将依赖对象的控制权转移到外部,但当前对象还是需要依赖对象的,这时候就需要使用 依赖注入 将所需要的依赖对象从外部注入进来。
控制反转是一种思想,而依赖注入是一种实现方式。
为什么需要使用Spring IOC?从IOC思想来看,主要有两大优点:
从Spring IOC的实际实现来看,还有如下好处:
没有IOC的情况下,使用依赖的对象需要 手动new一个对象 出来,根据构造器是否需要参数,可以分为有参对象和无参对象。而Spring只需要在xml文件中集中配置一次,或者使用注解就可以实现依赖注入,不需要手动new对象出来。
接下来考虑几种需要修改具体对象实现的情况下 代码重构的成本 ,来理解Spring IOC如何实现松耦合。
比如需要将类名从Message修改为News,那么此时可以借助开发工具的重构功能实现代码重构。
重构难度:1星。
比如需要将对象类型名从Student修改为Teacher,那么此时可以借助查找替换功能实现代码重构。 无重写方法情况下,重构难度:2星; 有重写方法情况下,重构难度:3星;
比如需要在创建Student对象时,增加年龄参数,此时借助查找功能也需要一个个手动修改代码,增加参数。
重构难度:5星。
如果使用Spring IOC,即便对难度最大的第三种情况,也只需要在xml文件中修改下注入的参数,或者在对应对象中增加一个属性,并使用注解自动注入即可。
重构难度:1星。
IOC容器 是Spring提供的一个 工厂 ,用于管理所有的bean、以及bean之间的关系。
Java反射机制
Spring中主要提供两类IOC容器,分别是 BeanFactory 和 ApplicationContext 。
两者间继承关系如下图,可见ApplicationContext间接继承自BeanFactory。
BeanFactory是Spring提供的最基础的IOC容器,用于帮助完成bean的注册和bean之间关系的绑定。
特点:
延加载 策略。当容器中bean被访问时,才进行bean的初始化和依赖注入工作。 BeanFactory最基础的实现类是 DefaultListableBeanFactory 。
旧版本中BeanFactory最常见实现类是XmlBeanFactory(现已被废弃),新版中使用 XmlBeanDefinitionReader + DefaultListableBeanFactory 替代 XmlBeanFactory 。
出于方便,可直接建立了一个SpringBoot工程。
<?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-3.0.xsd">
</beans>
复制代码
这里使用@Data注解省略了getter和setter方法。
@Data
public class User {
private Integer age;
private Nation nation;
public User() {
}
public User(Integer age) {
this.age = age;
}
public User(Nation nation) {
this.nation = nation;
}
}
复制代码
<bean id="user" class="com.example.springdemo.xml.model.User">
<constructor-arg name="age" type="java.lang.Integer" value="26"></constructor-arg>
</bean>
复制代码
public static void main(String[] args) {
// 1. XmlBeanFactory方式,已废弃
// XmlBeanFactory factory = new XmlBeanFactory(new ClassPathResource("application.xml"));
// 2. XmlBeanDefinitionReader + DefaultListableBeanFactory 替代XmlBeanFactory
Resource resource = new ClassPathResource("application.xml");
BeanFactory factory = new DefaultListableBeanFactory();
BeanDefinitionReader bdr = new XmlBeanDefinitionReader((BeanDefinitionRegistry) factory);
bdr.loadBeanDefinitions(resource);
// 根据bean名从容器中获取bean
User user1 = (User) factory.getBean("user");
System.out.println(user1.getAge());
}
复制代码
ApplicationContext 是在BeanFactory的基础上构建的,是相对比较高级的容器实现,除了拥有 BeanFactory的所有支持,ApplicationContext还提供了其他高级特性,比如:
ApplicationContext常见实习类有:FileSystemXmlApplicationContext、ClassPathXmlApplicationContext和XmlWebApplicationContext。
仍然使用BeanFactory示例中的配置文件,同时使用 ClassPathXmlApplicationContext 来读取xml文件。
// 3. ApplicationContext拥有比BeanFactory更高级的特性
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("application.xml");
User user3 = (User) applicationContext.getBean("user");
System.out.println(user3.getAge());
复制代码
依赖注入方式 主要包括:
构造器注入 属性注入
其中常用的两种方式是 构造器注入 和 属性注入 。虽然官方文档建议能用构造器注入就用构造器注入,因为这样可以使得依赖关系明确,并且如果缺少依赖的话在初始化阶段就可以发现问题。但 属性注入 更加 灵活 ,并且 构造器注入 方式无法解决循环依赖问题,所以一般使用 属性注入 , 强制依赖 建议使用 构造器方式 注入。
基于构造器的依赖注入是指,在bean的构造器中指明依赖的bean,并在初始化时完成注入。
User类见2.2.1.2中,存在如下构造器:
public User(Integer age) {
this.age = age;
}
复制代码
<!-- 构造器方式注入 -->
<bean id="user" class="com.example.springdemo.xml.model.User">
<constructor-arg name="age" type="java.lang.Integer" value="26"></constructor-arg>
</bean>
复制代码
基于属性的依赖注入是指,容器通过无参构造器初始化bean,再通过调用setter方法来完成注入。
User类如上,有属性age(通过@Data注解省略了getter/setter方法):
private Integer age; 复制代码
<!-- 属性注入 -->
<bean id="user2" class="com.example.springdemo.xml.model.User">
<property name="age" value="27"></property>
</bean>
复制代码
// user使用了构造器注入
User user1 = (User) factory.getBean("user");
System.out.println("DI by constructor:" + user1.getAge());
// user2使用了属性注入
User user2 = (User) factory.getBean("user2");
System.out.println("DI by setter:" + user2.getAge());
复制代码
输出:
在3.1小节中,User bean注入了int型的age属性,同样可以注入其它由IOC容器管理的bean。
User类还存在如下构造器,表示User bean依赖Nation bean。
public User(Nation nation) {
this.nation = nation;
}
复制代码
Nation类:
public class Nation {
public String name;
public Nation(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
复制代码
注册Nation bean,同时注册User bean,并使用构造器方式对User bean注入Nation bean。
<bean id="nation" class="com.example.springdemo.xml.model.Nation">
<constructor-arg name="name" type="java.lang.String" value="China"></constructor-arg>
</bean>
<!-- 手动注入依赖bean -->
<bean id="user3" class="com.example.springdemo.xml.model.User">
<constructor-arg name="nation" type="com.example.springdemo.xml.model.Nation" ref="nation"></constructor-arg>
</bean>
复制代码
// 注入IOC容器管理的bean,user注入时需要nation。
// 这时候也能明显体现Spring Ioc解耦的优点,如果不使用依赖注入,将对象的创建交由bean来做的话,代码如下。
// 如果需要修改传入参数,项目中所有地方都需要修改
Nation nation = new Nation("China");
User user4 = new User(nation);
System.out.println("Name of nation in user3 by code: " + user4.getNation().getName());
// 而使用依赖注入的话,只需要在xml中一个地方集中配置管理,在配置文件中注入取值或者内部bean,而不需要每个创建对象的地方都要修改
User user5 = (User) factory.getBean("user3");
System.out.println("Name of nation in user3 by IOC: " + user5.getNation().getName());
复制代码
输出:
在实际项目中,一般对于Dao、Service都需要单例,不会创建过多的bean,那么很多时候这些bean并不存在name、type的冲突,这时候是不是可以根据特定的规则,来简化bean的装配。
Spring IOC容器还提供了 自动装配 ,可以根据bean的type、name进行自动装配,而不需要显示地声明bean间依赖关系。
<!-- 自动装配bean,这里是byType方式。自动装配时会将满足条件的bean注入到构造器和setter方法中 -->
<!-- 自动装配缺点:(1)不适用构造器重写的情况;(2)不能装配基本类型和字符串 -->
<bean id="user4" class="com.example.springdemo.xml.model.User" autowire="byType">
<constructor-arg name="age" type="java.lang.Integer" value="28"></constructor-arg>
</bean>
复制代码
// 自动装配bean
User user6 = (User) factory.getBean("user4");
System.out.println("自动装配:" + user6.getAge());
System.out.println("自动装配:" + user6.getNation().getName());
复制代码
输出:
Spring IOC容器 读取依赖关系方式 有:
xml配置文件 注解
相比较而言, xml配置文件 方式拥有 集中配置 的优点,而 注解 方式则拥有 简化配置 的优点。
Spring IOC容器可以通过加载xml配置文件,来读取依赖关系。
由于xml配置文件拥有繁杂、不易配置的缺点,Spring IOC容器还同时支持 注解 来简化配置。
使用注解来读取依赖关系的步骤如下:
在bean类上使用注解@Component声明bean,使用注解@Autowired来声明依赖关系:
@Component
@Data
public class User {
private Integer age;
@Autowired
private Nation nation;
}
复制代码
创建ApplicationContext类型的IOC容器(BeanFactory实测不支持),并设置要扫描注解的包路径。
// 可以传入要扫描的包路径动态数组,也可以传入Class动态数组
ApplicationContext applicationContext = new AnnotationConfigApplicationContext("com.example.springdemo.annation");
User user1 = (User) applicationContext.getBean("user");
System.out.println(user1.getNation().getName());
复制代码
Spring IOC容器其实还支持xml配置文件和注解混用的方式来读取bean间依赖关系。
@Autowired private Nation nation; 复制代码
使用构造器方式注入age属性,但没有注入nation字段
<!-- 注解与xml混用方式,这里并没有注入nation,也没有使用自动装配 -->
<bean id="user5" class="com.example.springdemo.xml.model.User">
<constructor-arg name="age" type="java.lang.Integer" value="30"></constructor-arg>
</bean>
复制代码
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("application.xml");
// xml与注解混用,通过@Autowired注入nation bean(实际测试BeanFactory不支持注解,需要使用ApplicationContext)
User user7 = (User) applicationContext.getBean("user5");
System.out.println(user7.getAge());
System.out.println(user7.getNation().getName());
复制代码
从输出可以发现age和nation属性都被注入了。
在3.3.2中介绍了注解的简单使用,由于注解已经成为现在主流的使用方式,接下来详细介绍些常用注解知识。
@Bean 、 @Component 等注解用于将对象注册为bean,交由Spring容器管理。
@Component 注解在 类 上使用,将类的定义与bean的声明绑定,@Bean则将两者分离。一般@Component更适合声明service、dao等单例对象,而@Bean更适合用于在不同场景下创建不同对象的情况。 @Bean 注解注册同一类的多个bean,该注解使用在方法上。 @Componet 、 @Configuration 注解声明的类里面使用声明里面使用; @Bean
// @Primary表示默认bean,当有多个相同类型的bean时,可以使用@Primary来表示默认bean,其它地方注入依赖时就不需要都指明具体bean名。
@Primary
public Nation nation1() {
return new Nation("China");
}
@Bean
public Nation nation2() {
return new Nation("England");
}
// 使用name属性为bean命名
@Bean(name = "nation3")
public Nation nation() {
return new Nation("America");
}
复制代码
当有多个相同类型的bean时,可以使用 @Primary 来表示默认bean,其它地方注入默认bean时就不需要指明具体bean名。
可以使用 @Value 注解来注入外部属性,而 @ConfigurationProperties 则可用来注入一整个对象的所有属性,或者说批量注入外部属性。
@Configuration
@PropertySource("classpath:application.properties")
public class JavaConfiguration {}
复制代码
@Bean
// 使用@Value来注入外部属性,需要使用@PropertySource引入资源文件。还可以使用@ConfigurationProperties来批量注入外部属性
public Nation nation4(@Value("${nation4.name:#{null}}") String nation4name) {
return new Nation(nation4name);
}
复制代码
@Autowired 和 @Required 用来注入依赖的bean。
@Autowired 注解可以在 字段 上使用,也可以在 setter方法 上使用,同时可以在 构造器 上使用。 @Autowired 默认采用了 自动装配 ,且优先根据 类型 自动装配,同类型则优先注入primary bean,然后再依据bean名(与属性同名)进行注入。 @Autowired 还可以结合 @Qualifier 来指定要注入的bean名。例如:@Qualifier("nation2")。 @Required 注解也可以用来注入依赖,但已经被废弃。
Spring同时支持JSR-250的注解,同样可以使用 @Resource 来注入依赖(字段或者setter方法上),并使用name字段来指定bean名。例如:@Resource(name = "nation2")。
Spring3.0还支持JSR-330注解,可以使用 @Inject 来注入依赖。
@Configuration 注解表明该类是一个Java配置类,用于声明bean,在该类中可以使用@Bean注解来声明bean。一般情况下会将同一功能或用途的bean在一个配置累中声明。
Spring IOC中Bean有多种不同作用域,主要有:
接下来主要介绍下singleton和prototype作用域。
singleton 模式(单例模式)是默认模式,该模式的bean有以下特点:
(1)注册bean 注册一个bean名为nation1的单例bean。
@Bean
public Nation nation1() {
return new Nation("China");
}
复制代码
(2)从容器获取bean 两次从容器中获取同一个bean,是同一对象地址相同。
// 创建的nation1和nation2对象是相同bean
Nation nation1 = (Nation) applicationContext.getBean("nation1");
Nation nation2 = (Nation) applicationContext.getBean("nation1");
System.out.println("nation1 == nation2: " + (nation1 == nation2));
复制代码
输出:
prototype 模式(原型模式、多例模式)的bean有以下特点:
(1)注册bean 注册一个名为nation7的多例bean。
@Bean
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public Nation nation7() {
return new Nation();
}
复制代码
(2)从容器获取bean 两次从容器获取同一个bean,是不同的对象,地址不同。
// nation3和nation4是不同bean。
// 此处需要注意,如果bean类上使用了lombok注解,不要被输出所迷惑,因为lombok重写了toString(),使得两者看起来像是一个对象,实际应该比较对象地址
Nation nation3 = (Nation) applicationContext.getBean("nation7");
Nation nation4 = (Nation) applicationContext.getBean("nation7");
System.out.println("nation3 == nation4: " + (nation3 == nation4));
System.out.println("nation3:" + nation3);
System.out.println("nation4:" + nation4);
复制代码
输出:
未完待续。。。。。
Spring IOC可以说是Spring全家桶的基石,AOP等都依赖IOC的实现,要理解学习Spring应该先掌握Spring IOC的知识。
本文先是简单介绍了Spring IOC的思想和一些基本概念,然后针对Spring IOC容器实现依赖注入相关的知识点,并且分别给出了xml形式和注解形式依赖注入代码实现。
https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/index.html https://juejin.im/post/5bf51d4c5188256d9832b0d3 https://segmentfault.com/a/1190000013700859 https://segmentfault.com/a/1190000014979704 https://juejin.im/post/5b399eb1e51d4553156c0525 复制代码