Spring-SpringIOC
Spring-SpringIOC
学习核心
- SpringIOC核心概念
- 名词理解:IOC、DI、DL等
- IOC的作用
参考资料
IOC核心
IOC基础概念
IOC(控制反转:Inverser Of Control):把原来在程序中创建Java对象的控制权限交给Spring管理
简单的说,把原来在程序中创建HelloService对象的控制权限交给Spring管理(HelloService对象控制器被反转到Spring框架内)
IOC 又称为依赖倒置原则(设计模式六大原则之一),它的要点在于:程序要依赖于抽象接口,不要依赖于具体实现。它的作用就是用于降低代码间的耦合度。
SpringIOC容器就像一个工厂一样,当需要创建一个对象的时候,只需要配置好配置文件/注解即可,而不需要关注对象是如何被创建出来的。IOC容器负责创建对象并管理这些对象的生命周期,直到它们被完全销毁。
在实际项目中一个 Service 类如果有几百甚至上千个类作为它的底层,当需要实例化这个 Service,可能要每次都要搞清这个 Service 所有底层类的构造函数,无形中增加开发成本。如果利用 IOC 的话,只需要配置好,然后在需要的地方引用就行了,这大大增加了项目的可维护性且降低了开发难度
IOC的实现方式
- 依赖注入(Dependency Injection,简称 DI):不通过
new()
的方式在类内部创建依赖类对象,而是将依赖的类对象在外部创建好之后,通过构造函数、函数参数等方式传递(或注入)给类使用。 - 依赖查找(Dependency Lookup):容器中的受控对象通过容器的 API 来查找自己所依赖的资源和协作对象
IOC要点(理解IOC:什么是控制、什么是反转)
- 谁控制谁,控制什么:
- 传统 Java SE 程序设计,直接通过 new 创建对象,是程序主动去创建依赖对象;
- IOC有专门一个容器来创建这些对象,即由IOC容器来控制对象的创建;由IOC容器控制对象,控制了外部资源获取(不只是对象包括比如文件等)
- 为何是反转,哪些方面反转了:由容器来查找及注入依赖对象,对象只是被动的接受依赖对象,依赖对象的获取被反转了
- 传统应用程序是在对象中主动控制去直接获取依赖对象,也就是正转;
- 反转则是由容器来帮忙创建及注入依赖对象;
IOC是如何来管理对象的?
IOC容器就是具有依赖注入功能的容器。IOC 容器负责实例化、定位、配置应用程序中的对象及建立这些对象间的依赖。应用程序无需直接在代码中 new 相关的对象,应用程序由 IOC 容器进行组装。在 Spring 中 BeanFactory 是 IOC 容器的实际代表者。
Spring IOC 容器如何知道哪些是它管理的对象呢?配置文件,Spring IOC 容器通过读取配置文件中的配置元数据,通过元数据对应用中的各个对象进行实例化及装配。Spring 与配置文件完全解耦的,常用的方式有xml配置文件、注解方式、基于Java文件、基于属性文件等方式来配置元数据
IOC容器
1.IOC容器分类
在 Spring 中,有两种 IOC 容器:BeanFactory
和 ApplicationContext
,负责bean的实例化、配置、组装
BeanFactory
:BeanFactory
是 Spring 基础 IoC 容器。BeanFactory
提供了 Spring 容器的配置框架和基本功能ApplicationContext
:ApplicationContext
是具备应用特性的BeanFactory
的子接口。它还扩展了其他一些接口,以支持更丰富的功能,如:国际化、访问资源、事件机制、更方便的支持 AOP、在 web 应用中指定应用层上下文等。
一般实际开发中推荐使用ApplicationContext,其功能较为丰富
2.IOC容器使用(工作步骤)
配置元数据:需要配置一些元数据来告诉 Spring,希望容器如何工作
实例化容器:定位资源文件(例如xml)、读取配置信息(Reource)并转为Spring可识别的数据形式
由 IOC 容器解析配置的元数据(BeanReader读取并解析配置文件,根据 BeanDefinition实例化、配置、组装Bean)
使用容器:由客户端实例化容器,获取需要的 Bean
IOC的加载过程分析
【1】通过BeanDefinitionReader读取指定配置文件生成的bean定义信息
【2】在BeanDefinition和完整的BeanDefinition之间会通过一道关口进行功能增强(通过实现BeanFactoryPostProcessor接口扩展扩展功能,实现多个就会执行多次),执行完成得到完整的BeanDefinition对象
【3】随后执行Bean的实例化操作,即创建对象
案例1:xml方式
【1】配置元数据(此处使用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">
<!--1.快速入门 ========================================================= -->
<!-- - bean:就是定义一个具体的类 可以是一个对象 id 属性 就是一个合法的标识符 class 是当前类的全名称 -->
<bean id="helloService" class="com.noob.spring.a_quickstart.HelloServiceImpl">
<property name="info" value="hello"></property>
</bean>
<!--1.end快速入门 ====================================================== -->
</beans>
【2】实例化容器并使用
public class HelloServiceTest {
public static void main(String[] args) {
// 实例化容器
ApplicationContext applicationContext =new ClassPathXmlApplicationContext("spring/applicationContext.xml");
// 使用容器
HelloService service =(HelloService) applicationContext.getBean("helloService");
service.sayHello();
}
}
案例2:注解方式
Spring 默认是不启用注解的。如果想使用注解,需要先在 xml 中配置启动注解
<context:annotation-config/>
注解 | 说明 |
---|---|
@Required | 只能用于修饰 bean 属性的 setter 方法 受影响的 bean 属性必须在配置时被填充在 xml 配置文件中,否则容器将抛出 BeanInitializationException |
📌@Autowired | 用于修饰属性、setter 方法、构造方法 |
📌@Qualifier | 搭配@Autowired 使用,指定 bean 名称来锁定真正需要的那个 bean |
📌@Resource | 根据指定的名称来注入 bean |
@PostConstruct 和 @PreDestroy | 用于规定生命周期的方式 |
@Inject | @Inject 和 @Autowired 一样,可以修饰属性、setter 方法、构造方法如果要使用 @Inject 注解,则需要引入外部依赖(javax.inject) |
案例3:Java配置
基于 Java 配置 Spring IoC 容器,实际上是Spring 允许用户定义一个类,在这个类中去管理 IOC 容器的配置
为了让 Spring 识别这个定义类为一个 Spring 配置类,需要用到两个注解:@Configuration
和@Bean
可以将@Configuration
等价于<beans>
标签;将@Bean
等价于<bean>
标签
@Configuration
public class AppConfig {
@Bean
public MyService myService() {
return new MyServiceImpl();
}
}
// 等价于xml配置
<beans>
<bean id="myService" class="com.acme.services.MyServiceImpl"/>
</beans>
3.循环依赖场景
A 类通过构造器注入需要 B 类的实例,B 类通过构造器注入需要 A 类的实例。Spring IoC 容器会在运行时检测到此循环引用,并抛出 BeanCurrentlyInCreationException
。
解决方案1:是使用 setter 方法注入替代构造器注入
解决方案2:bean A 和 bean B 之间的循环依赖关系,强制其中一个 bean 在完全初始化之前注入另一个 bean(典型的先有鸡还是先有蛋的场景)
Spring 会在容器加载时检测配置问题,例如引用不存在的 bean 或循环依赖。在实际创建 bean 时,Spring 会尽可能晚地设置属性并解析依赖关系。这意味着,如果在创建该对象或其依赖项之一时出现问题,则正确加载的 Spring 容器稍后可以在请求对象时生成异常 — 例如,bean 由于丢失或无效而引发异常。某些配置问题的这种潜在的延迟可见性是默认情况下 ApplicationContext 实现预实例化单例 bean 的原因。以在实际需要之前创建这些 bean 的一些前期时间和内存为代价,会在创建 ApplicationContext 时发现配置问题,而不是稍后。仍然可以覆盖此默认行为,以便单例 bean 延迟初始化,而不是急切地预先实例化
扩展
1.singleton 的 Bean 如何注入 prototype 的 Bean(最佳实践)
Spring 创建的 Bean 默认是单例的,但当 Bean 遇到继承的时候,可能会忽略这一点。
假设有一个 SayService 抽象类,其中维护了一个类型是 ArrayList 的字段 data,用于保存方法处理的中间数据。每次调用 say 方法都会往 data 加入新数据,可以认为 SayService 是有状态,如果 SayService 是单例的话必然会 OOM。
/**
* SayService 是有状态,如果 SayService 是单例的话必然会 OOM
*/
@Slf4j
public abstract class SayService {
List<String> data = new ArrayList<>();
public void say() {
data.add(IntStream.rangeClosed(1, 1000000)
.mapToObj(__ -> "a")
.collect(Collectors.joining("")) + UUID.randomUUID().toString());
log.info("I'm {} size:{}", this, data.size());
}
}
但实际开发的时候,没有过多思考就把 SayHello 和 SayBye 类加上了 @Service 注解,让它们成为了 Bean,也没有考虑到父类是有状态的。
@Service
@Slf4j
public class SayBye extends SayService {
@Override
public void say() {
super.say();
log.info("bye");
}
}
@Service
@Slf4j
public class SayHello extends SayService {
@Override
public void say() {
super.say();
log.info("hello");
}
}
在为类标记上 @Service 注解把类型交由容器管理前,首先评估一下类是否有状态,然后为 Bean 设置合适的 Scope。
调用代码:
@Slf4j
@RestController
@RequestMapping("beansingletonandorder")
public class BeanSingletonAndOrderController {
@Autowired
List<SayService> sayServiceList;
@Autowired
private ApplicationContext applicationContext;
@GetMapping("test")
public void test() {
log.info("====================");
sayServiceList.forEach(SayService::say);
}
}
可能有人认为,为 SayHello 和 SayBye 两个类都标记了 @Scope 注解,设置了 PROTOTYPE 的生命周期就可以解决上面的问题。
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
但实际上还是有问题。因为@RestController 注解 =@Controller 注解 +@ResponseBody 注解,又因为 @Controller 标记了 @Component 元注解,所以 @RestController 注解其实也是一个 Spring Bean。
Bean 默认是单例的,所以单例的 Controller 注入的 Service 也是一次性创建的,即使 Service 本身标识了 prototype 的范围也没用。
修复方式是,让 Service 以代理方式注入。这样虽然 Controller 本身是单例的,但每次都能从代理获取 Service。这样一来,prototype 范围的配置才能真正生效。
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE, proxyMode = ScopedProx)