跳至主要內容

Spring-SpringIOC

holic-x...大约 9 分钟JAVA框架

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:什么是控制、什么是反转)

image-20240605133234493

  • 谁控制谁,控制什么
    • 传统 Java SE 程序设计,直接通过 new 创建对象,是程序主动去创建依赖对象
    • IOC有专门一个容器来创建这些对象,即由IOC容器来控制对象的创建;由IOC容器控制对象,控制了外部资源获取(不只是对象包括比如文件等)
  • 为何是反转,哪些方面反转了:由容器来查找及注入依赖对象,对象只是被动的接受依赖对象,依赖对象的获取被反转了
    • 传统应用程序是在对象中主动控制去直接获取依赖对象,也就是正转;
    • 反转则是由容器来帮忙创建及注入依赖对象;

image-20240605111010100

IOC是如何来管理对象的?

​ IOC容器就是具有依赖注入功能的容器。IOC 容器负责实例化、定位、配置应用程序中的对象及建立这些对象间的依赖。应用程序无需直接在代码中 new 相关的对象,应用程序由 IOC 容器进行组装。在 Spring 中 BeanFactory 是 IOC 容器的实际代表者。

​ Spring IOC 容器如何知道哪些是它管理的对象呢?配置文件,Spring IOC 容器通过读取配置文件中的配置元数据,通过元数据对应用中的各个对象进行实例化及装配。Spring 与配置文件完全解耦的,常用的方式有xml配置文件、注解方式、基于Java文件、基于属性文件等方式来配置元数据

IOC容器

1.IOC容器分类

在 Spring 中,有两种 IOC 容器:BeanFactoryApplicationContext,负责bean的实例化、配置、组装

  • BeanFactoryBeanFactory 是 Spring 基础 IoC 容器BeanFactory 提供了 Spring 容器的配置框架和基本功能
  • ApplicationContextApplicationContext 是具备应用特性的 BeanFactory 的子接口。它还扩展了其他一些接口,以支持更丰富的功能,如:国际化、访问资源、事件机制、更方便的支持 AOP、在 web 应用中指定应用层上下文等。

一般实际开发中推荐使用ApplicationContext,其功能较为丰富

2.IOC容器使用(工作步骤)

配置元数据:需要配置一些元数据来告诉 Spring,希望容器如何工作

实例化容器:定位资源文件(例如xml)、读取配置信息(Reource)并转为Spring可识别的数据形式

​ 由 IOC 容器解析配置的元数据(BeanReader读取并解析配置文件,根据 BeanDefinition实例化、配置、组装Bean)

使用容器:由客户端实例化容器,获取需要的 Bean

image-20240605113241599

IOC的加载过程分析

image-20240605142010439

【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)
评论
  • 按正序
  • 按倒序
  • 按热度
Powered by Waline v3.1.3