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 复制代码