当前位置: 首页 > news >正文

第 2 章:Spring Framework 中的 IoC 容器

控制反转(Inversion of Control,IoC)与 面向切面编程(Aspect Oriented Programming,AOP)是 Spring Framework 中最重要的两个概念,本章会着重介绍前者,内容包括 IoC 容器以及容器中 Bean 的基础知识。容器为我们预留了不少扩展点,让我们能定制各种行为,本章的最后我会和大家一起了解一些容器提供的抽象机制。通过这些介绍,希望大家可以对 IoC 容器有个大概的认识。

2.1 IoC 容器基础知识

Spring Framework 为 Java 开发者提供了强大的支持,开发者可以把底层基础的杂事抛给 Spring Framework,自己则专心于业务逻辑。本节我们会聚焦在 Spring Framework 的核心能力上,着重了解 IoC 容器的基础知识。

2.1.1 什么是 IoC 容器

在介绍 Spring Framework 的 IoC 容器前,我们有必要先理解什么是“控制反转”。 控制反转 是一种决定容器如何装配组件的 模式。只要遵循这种模式,按照一定的规则,容器就能将组件组装起来。这里所谓的 容器,就是用来创建组件并对它们进行管理的地方。它牵扯到组件该如何定义、组件该何时创建、又该何时销毁、它们互相之间是什么关系等——这些本该在组件内部管理的东西,被从组件中剥离了出来。

需要着重指出一点,组件之间的依赖关系原先是由组件自己定义的,并在其内部维护,而现在这些依赖被定义在容器中,由容器来统一管理,并据此将其依赖的内容注入组件中。在好莱坞,演艺公司具有极大的控制权,艺人将简历投递给演艺公司后就只能等待,被动接受演艺公司的安排。这就是知名的好莱坞原则,它可以总结为这样的一句话“不要给我们打电话,我们会打给你的”(Don’t call us, we’ll call you)。IoC 容器背后的思想正是好莱坞原则,即所有的组件都要被动接受容器的控制。

Martin Fowler那篇著名的“Inversion of Control Containers and the Dependency Injection pattern” 中提到“控制反转”不能很好地描述这个模式,“依赖注入”(Dependency Injection)能更好地描述它的特点。正因如此,我们经常会看到这两个词一同出现。

Spring Framework、Google Guice、PicoContainer 都提供了这样的容器,后文中我们也会把 Spring Framework 的 IoC 容器称为 Spring 容器

图 2-1 是 Spring Framework 的官方文档中的一幅图,它非常直观地表达了 Spring IoC 容器的作用,即将业务对象(也就是组件,在 Spring 中这些组件被称为 Bean,2.2 节会详细介绍 Bean 的内容)和关于组件的配置元数据(比如依赖关系)输入 Spring 容器中,容器就能为我们组装出一个可用的系统。

image.png
图 2-1 Spring IoC 容器

Spring Framework 的模块按功能进行了拆分,spring-core 和 spring-beans 模块提供了最基础的功能,其中就包含了 IoC 容器。 BeanFactory 是容器的基础接口,我们平时使用的各种容器都是它的实现,后文中会看到这些实现的具体用法与区别。

2.1.2 容器的初始化

从图 2-1 中可以看到,Spring 容器需要配置元数据和业务对象,因此在初始化容器时,我们需要提供这些信息。早期的配置元数据只能以 XML 配置文件的形式提供,从 2.5 版本开始,官方逐步提供了 XML 文件以外的配置方式,比如基于注解的配置和基于 Java 类的配置,本书中的大部分示例将采用后两种方式进行配置。

容器初始化的大致步骤如下(在本章后续的几节中,我们会分别介绍其中涉及的内容)。

(1) 从 XML 文件、Java 类或其他地方加载配置元数据。

(2) 通过 BeanFactoryPostProcessor 对配置元数据进行一轮处理。

(3) 初始化 Bean 实例,并根据给定的依赖关系组装对象。

(4) 通过 BeanPostProcessor 对 Bean 进行处理,期间还会触发 Bean 被构造后的回调方法。

比如,我们有一个如代码示例 2-1 所示的业务对象,它会返回一个字符串,可以看到它就是一个最普通的 Java 类。

代码示例 2-1 最基本的 Hello

package learning.spring.helloworld;public class Hello {public String hello() {return "Hello World!";}
}

在没有 IoC 容器时,我们需要像代码示例 2-2 那样自己管理 Hello 实例的生命周期,通常是在代码中用 new 关键字新建一个实例,然后把它传给具体要调用它的对象,下面的代码只是个示意,所以就使用 new 关键字创建实例后直接调用方法了。

代码示例 2-2 手动创建并调用 Hello.hello() 方法

public class Application {public static void main(String[] args) {Hello hello = new Hello();System.out.println(hello.hello());}
}

如果是把实例交给 Spring 容器托管,则可以将它配置到一个 XML 文件中,让容器来管理它的相关生命周期。可以看到代码示例 2-3 只是一个普通的 XML 文件,通过 <beans/> 这个 Schema 来配置 Spring 的 Bean(Bean 的配置会在 2.2.2 节详细展开)。为了使用 Spring 的容器,需要在 pom.xml 文件中引入 org.springframework:spring-beans 依赖。

代码示例 2-3 配置 hello Bean 的 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/beanshttps://www.springframework.org/schema/beans/spring-beans.xsd"><bean id="hello" class="learning.spring.helloworld.Hello" /></beans>

最后像代码示例 2-4 那样,将配置文件载入容器。 BeanFactory 只是一个最基础的接口,我们需要选择一个合适的实现类——在实际工作中,更多情况下会用到 ApplicationContext 的各种实现。此处,我们使用 DefaultListableBeanFactory 这个实现类,它并不关心配置的方式, XmlBeanDefinitionReader 能读取 XML 文件中的元数据,我们通过它加载 CLASSPATH 中的 beans.xml 文件,将其保存到 DefaultListableBeanFactory 中,随后就可以通过 BeanFactorygetBean() 方法取得对应的 Bean 了。

getBean() 方法有很多不同的参数列表,例子里就有两种,一种是取出 Object 类型的 Bean,然后自己做类型转换;另一种则是在参数里指明返回 Bean 的类型,如果实际类型不同的话则会抛出 BeansException

代码示例 2-4 加载配置文件并执行的 Application 类代码片段

public class Application {private BeanFactory beanFactory;public static void main(String[] args) {Application application = new Application();application.sayHello();}public Application() {beanFactory = new DefaultListableBeanFactory();XmlBeanDefinitionReader reader =new XmlBeanDefinitionReader((DefaultListableBeanFactory) beanFactory);reader.loadBeanDefinitions("beans.xml");}public void sayHello() {// Hello hello = (Hello) beanFactory.getBean("hello");Hello hello = beanFactory.getBean("hello", Hello.class);System.out.println(hello.hello());}
}

在这个例子的 sayHello() 方法中,我们完全不用关心 Hello 这个类的实例是如何创建的,只需获取实例对象然后使用即可。虽然看起来比代码示例 2-3 的行数要多,但当工程复杂度增加之后,IoC 托管 Bean 生命周期的优势就体现出来了。

2.1.3 BeanFactoryApplicationContext

spring-context 模块在 spring-core 和 spring-beans 的基础上提供了更丰富的功能,例如事件传播、资源加载、国际化支持等。前面说过, BeanFactory 是容器的基础接口, ApplicationContext 接口继承了 BeanFactory,在它的基础上增加了更多企业级应用所需要的特性,通过这个接口,我们可以最大化地发挥 Spring 上下文的能力。表 2-1 列举了常见的一些 ApplicationContext 实现。

表 2-1 常见的 ApplicationContext 实现

类名说明
ClassPathXmlApplicationContext从 CLASSPATH 中加载 XML 文件来配置ApplicationContext
FileSystemXmlApplicationContext从文件系统中加载 XML文件来配置ApplicationContext
AnnotationConfigApplicationContext根据注解和 Java 类配置ApplicationContext

相比 BeanFactory,使用 ApplicationContext 也会更加方便一些,因为我们无须自己去注册很多内容,例如 AnnotationConfigApplicationContext 把常用的一些后置处理器都直接注册好了,为我们省去了不少麻烦。所以,在绝大多数情况下,建议大家使用 ApplicationContext 的实现类。

如果要将代码示例 2-4 中的 Application.java 从使用 BeanFactory 修改为使用 ApplicationContext,只需做两处改动:

(1) 在 pom.xml 文件中,把引入的 org.springframework:spring-beans 修改为 org.springframework: spring-context

(2) 在 Application.java 中,使用 ClassPathXmlApplicationContext 代替 DefaultListableBeanFactoryXmlBeanDefinitionReader 的组合,具体见代码示例 2-5。

代码示例 2-5 调整后的 Application 类代码片段

public class Application {private ApplicationContext applicationContext;public static void main(String[] args) {Application application = new Application();application.sayHello();}public Application() {applicationContext = new ClassPathXmlApplicationContext("beans.xml");}public void sayHello() {Hello hello = applicationContext.getBean("hello", Hello.class);System.out.println(hello.hello());}
}

2.1.4 容器的继承关系

Java 类之间有继承的关系,子类能够继承父类的属性和方法。同样地,Spring 的容器之间也存在类似的继承关系,子容器可以继承父容器中配置的组件。在使用 Spring MVC 时就会涉及容器的继承。

先来看一个例子,如代码示例 2-6 所示(修改自上一节的 HelloWorld), Hello 类在输出的字符串中加入一段注入的信息。

代码示例 2-6 可以输出特定信息的 Hello

package learning.spring.helloworld;public class Hello {private String name;public String hello() {return "Hello World! by " + name;}public void setName(String name) {this.name = name;}
}

随后,我们也要调整一下 XML 配置文件,父容器与子容器分别用不同的配置,ID 既有相同的,也有不同的,具体如代码示例 2-7 与代码示例 2-8 所示。

代码示例 2-7 父容器配置 parent-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/beanshttps://www.springframework.org/schema/beans/spring-beans.xsd"><bean id="parentHello" class="learning.spring.helloworld.Hello"><property name="name" value="PARENT" /></bean><bean id="hello" class="learning.spring.helloworld.Hello"><property name="name" value="PARENT" /></bean>
</beans>

代码示例 2-8 子容器配置 child-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/beanshttps://www.springframework.org/schema/beans/spring-beans.xsd"><bean id="childHello" class="learning.spring.helloworld.Hello"><property name="name" value="CHILD" /></bean><bean id="hello" class="learning.spring.helloworld.Hello"><property name="name" value="CHILD" /></bean>
</beans>

Application 类中,我们尝试从不同的容器中获取不同的 Bean(关于 Bean 的内容,我们会在 2.2 节展开),以测试继承容器中 Bean 的可见性和覆盖情况,具体如代码示例 2-9 所示。

代码示例 2-9 修改后的 Application 类代码片段

public class Application {private ClassPathXmlApplicationContext parentContext;private ClassPathXmlApplicationContext childContext;public static void main(String[] args) {new Application().runTests();}public Application() {parentContext = new ClassPathXmlApplicationContext("parent-beans.xml");childContext = new ClassPathXmlApplicationContext(new String[] {"child-beans.xml"}, true, parentContext);parentContext.setId("ParentContext");childContext.setId("ChildContext");}public void runTests() {testVisibility(parentContext, "parentHello");testVisibility(childContext, "parentHello");testVisibility(parentContext, "childHello");testVisibility(childContext, "childHello");testOverridden(parentContext, "hello");testOverridden(childContext, "hello");}private void testVisibility(ApplicationContext context, String beanName) {System.out.println(context.getId() + " can see " + beanName + ": "+ context.containsBean(beanName));}private void testOverridden(ApplicationContext context, String beanName) {System.out.println("sayHello from " + context.getId() +": "+ context.getBean(beanName, Hello.class).hello());}
}

这段程序的运行结果如下:

ParentContext can see parentHello: true
ChildContext can see parentHello: true
ParentContext can see childHello: false
ChildContext can see childHello: true
sayHello from ParentContext: Hello World! by PARENT
sayHello from ChildContext: Hello World! by CHILD

通过这个示例,我们可以得出如下关于容器继承的通用结论——它们和 Java 类的继承非常相似,二者的对比如表 2-2 所示。

表 2-2 容器继承 vs . Java 类继承

容器继承Java 类继承
子上下文可以看到父上下文中定义的 Bean,反之则不行子类可以看到父类的 protectedpublic 属性和方法,父类看不到子类的
子上下文中可以定义与父上下文同 ID 的 Bean,各自都能获取自己定义的 Bean子类可以覆盖父类定义的属性和方法

关于同 ID 覆盖 Bean,有时也会引发一些意料之外的问题。如果希望关闭这个特性,也可以考虑禁止覆盖,通过容器的 setAllowBeanDefinitionOverriding() 方法可以控制这一行为。

2.2 Bean 基础知识

Bean 是 Spring 容器中的重要概念,这一节就让我们来着重了解一下 Bean 的概念、如何注入 Bean 的依赖,以及如何在容器中进行 Bean 的配置。

2.2.1 什么是 Bean

Java 中有个比较重要的概念叫做“JavaBeans”,维基百科中有如下描述:

JavaBeans 是 Java 中一种特殊的类,可以将多个对象封装到一个对象(Bean)中。特点是可序列化,提供无参构造器,提供 Getter 方法和 Setter 方法访问对象的属性。名称中的 Bean 是用于 Java 的可重用软件组件的惯用叫法。

从中可以看到: Bean 是指 Java 中的 可重用软件组件,Spring 容器也 遵循 这一惯例,因此将容器中管理的可重用组件称为 Bean。容器会根据所提供的元数据来创建并管理这些 Bean,其中也包括它们之间的依赖关系。Spring 容器对 Bean 并没有太多的要求,无须实现特定接口或依赖特定库,只要是最普通的 Java 对象即可,这类对象也被称为 POJO(Plain Old Java Object)。

一个 Bean 的定义中,会包含如下部分:

  • Bean 的名称,一般是 Bean 的 id,也可以为 Bean 指定别名(alias);
  • Bean 的具体类信息,这是一个全限定类名;
  • Bean 的作用域,是单例(singleton)还是原型(prototype);
  • 依赖注入相关信息,构造方法参数、属性以及自动织入(autowire)方式;
  • 创建销毁相关信息,懒加载模式、初始化回调方法与销毁回调方法。

我们可以自行设定 Bean 的名字,也可以让 Spring 容器帮我们设置名称。Spring 容器的命名方式为类名的首字母小写,搭配驼峰(camel-cased)规则。比如类型为 HelloService 的 Bean,自动生成的名称就为 helloService

2.2.2 Bean 的依赖关系

所谓“依赖注入”,很重要的一块就是管理依赖。在 Spring 容器中,“管理依赖”主要就是管理 Bean 之间的依赖。有两种基本的注入方式——基于构造方法的注入和基于 Setter 方法的注入。

所谓基于构造方法的注入,就是通过构造方法来注入依赖。仍旧以 HelloWorld 为例,如代码示例 2-10 所示。

代码示例 2-10 通过构造方法传入字符串的 Hello

package learning.spring.helloworld;public class Hello {private String name;public Hello(String name) {this.name = name;}public String hello() {return "Hello World! by " + name;}
}

对应的 XML 配置文件需要使用 <constructor-arg/> 传入构造方法所需的内容,如代码示例 2-11 所示。

代码示例 2-11 通过构造方法配置 Bean 的 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/beanshttps://www.springframework.org/schema/beans/spring-beans.xsd"><bean id="hello" class="learning.spring.helloworld.Hello"><constructor-arg value="Spring"/></bean></beans>

<constructor-arg> 中有不少属性可以配置,具体如表 2-3 所示。

表 2-3 <constructor-arg/> 的可配置属性

属性作用
value要传给构造方法参数的值
ref要传给构造方法参数的 Bean ID
type构造方法参数对应的类型
index构造方法参数对应的位置,从 0 开始计算
name构造方法参数对应的名称

基于 Setter 方法的注入,顾名思义,就是通过 Bean 的 Setter 方法来注入依赖。我们在第 2.1.4 节已经看到了对应的例子,具体可以参考代码示例 2-6 与代码示例 2-7。 <property/> 中的 value 属性是直接注入的值,用 ref 属性则可注入其他 Bean。也可以像代码示例 2-12 这样来为属性注入依赖。

代码示例 2-12 <property/> 的用法演示

<bean id="..." class="..."><property name="xxx"><!-- 直接定义一个内部的Bean --><bean class="..."/></property><property name="yyy"><!-- 定义依赖的Bean --><ref bean="..."/></property><property name="zzz"><!-- 定义一个列表 --><list><value>aaa</value><value>bbb</value></list></property>
</bean>

手动配置依赖在 Bean 少时还能接受,当 Bean 的数量变多后,这种配置就会变得非常繁琐。在合适的场合,可以让 Spring 容器替我们自动进行依赖注入,这种机制称为 自动织入。自动织入有几种模式,具体见表 2-4。

表 2-4 自动织入的模式

名称说明
no不进行自动织入
byName根据属性名查找对应的 Bean 进行自动织入
byType根据属性类型查找对应的 Bean 进行自动织入
constructorbyType,但用于构造方法注入

<bean/> 中可以通过 autowire 属性来设置使用何种自动织入方式,也可以在 <beans/> 中设置 default-autowire 属性指定默认的自动织入方式。在使用自动织入时,需要注意以下事项:

  • 开启自动织入后,仍可以手动设置依赖,手动设置的依赖优先级高于自动织入;
  • 自动织入无法注入基本类型和字符串;
  • 对于集合类型的属性,自动织入会把上下文里找到的 Bean 都放进去,但如果属性不是集合类型,有多个候选 Bean 就会有问题。

为了避免第三点中说到的问题,可以将 <bean/>autowire-candidate 属性设置为 false,也可以在你所期望的候选 Bean 的 <bean/> 中将 primary 设置为 true,这就表明在多个候选 Bean 中该 Bean 是主要的(如果使用基于 Java 类的配置方式,我们可以通过选择 @Primary 注解实现一样的功能)。

最后,再简单提一下如何指定 Bean 的初始化顺序。一般情况下,Spring 容器会根据依赖情况自动调整 Bean 的初始化顺序。不过,有时 Bean 之间的依赖并不明显,容器可能无法按照我们的预期进行初始化,这时我们可以自己来指定 Bean 的依赖顺序。 <bean/>depends-on 属性可以指定当前 Bean 还要依赖哪些 Bean(如果使用基于 Java 类的配置方式, @DependsOn 注解也能实现一样的功能)。

2.2.3 Bean 的三种配置方式

Spring Framework 提供了多种不同风格的配置方式,早期仅支持 XML 配置文件的方式,Spring Framework 2.0 引入了基于注解的配置方式,到了 3.0 则又增加了基于 Java 类的配置方式。这几种方式没有明确的优劣之分,选择合适的或者喜欢的方式就好,很多时候我们也会混合使用这几种配置方式。

鉴于 Spring 容器的元数据配置本质上就是配置 Bean(AOP 和事务的配置背后也是配置各种 Bean),因此我们会在本节中详细展开说明如何配置 Bean。

  1. 基于 XML 文件的配置

    Spring Framework 提供了 <beans/> 这个 Schema来配置 Bean,前文中已经用过了 XML 文件方式的配置,这里再简单回顾一下。

    我们通过 <bean/> 可以配置一个 Bean, id 指定 Bean 的标识, class 指定 Bean 的全限定类名,一般会通过类的构造方法来创建 Bean,但也可以使用一个静态的 factory-method,比如下面就使用了 create() 静态方法:

<bean id="xxx" class="learning.spring.Yyy" factory-method="create" />

<constructor-arg/><property/> 用来注入所需的内容。如果是另一个 Bean 的依赖,一般会用 ref 属性,2.2.3 节中已经有过说明,此处就不再赘述了。

<bean/> 中还有几个重要的属性, scope 表明当前 Bean 是单例还是原型, lazy-init 是指当前 Bean 是否是懒加载的, depends-on 明确指定当前 Bean 的初始化顺序,就像下面这样:

<bean id="..." class="..." scope="singleton" lazy-init="true" depends-on="xxx"/>
  1. 基于注解的配置

    Spring Framework 2.0 引入了 @Required 注解,Spring Framework 2.5 又引入了 @Autowired@Component@Service@Repository 等重要的注解,使用这些注解能简化 Bean 的配置。我们可以像代码示例 2-13 那样开启对这些注解的支持。

代码示例 2-13 启用基于注解的配置

   <?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:context="http://www.springframework.org/schema/context"xsi:schemaLocation="http://www.springframework.org/schema/beanshttps://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/contexthttps://www.springframework.org/schema/context/spring-context.xsd"><context:component-scan base-package="learning.spring"/></beans>

上述配置会扫描 learning.spring 包内的类,在类上添加如下四个注解都能让 Spring 容器把它们配置为 Bean,如表 2-5 所示。

注释说明
@Component将类标识为普通的组件,即一个 Bean
@Service将类标识为服务层的服务
@Repository将类标识为数据层的数据仓库,一般是 DAO(Data Access Object)
@Controller将类标识为 Web 层的 Web 控制器(后来针对 REST 服务又增加了一个 @RestController 注解)

如果不指定 Bean 的名称,Spring 容器会自动生成一个名称,当然,也可以明确指定名称,比如:

@Component("helloBean")
public class Hello {...}

如果要注入依赖,可以使用如下的注解,如表 2-6 所示。

表 2-6 可注入依赖的注解

注解说明
@Autowired根据类型注入依赖,可用于构造方法、 Setter 方法和成员变量
@ResourceJSR-250 的注解,根据名称注入依赖
@InjectJSR-330 的注解,同 @Autowired

从 Spring Framework 6.0 开始, @Resource@PostConstruct@PreDestroy 注解都换了新的包名,建议使用 jakarta.annotation 包里的注解,但也兼容 javax.annotation 包中的注解;@Inject 注解则是建议使用 jakarta.inject 包里的,但也兼容 javax.inject 包中的。

@Autowired 注解比较常用,下面的例子中可以指定是否必须存在依赖项,并指定目标依赖的 Bean ID:

@Autowired(required = false)
@Qualifier("helloBean")
public void setHello(Hello hello) {...}

除此之外,还可以使用 @Value 注解注入环境变量、Properties 或 YAML 中配置的属性和 SpEL 表达式的计算结果。JSR-250 中还有 @PostConstruct@PreDestroy 注解,把这两个注解加在方法上用来表示该方法要在初始化后调用或者是在销毁前调用,在聊到 Bean 的生命周期时我们还会看到它们。

  1. 基于 Java 类的配置

    从 Spring Framework 3.0 开始,我们可以使用 Java 类代替 XML 文件,使用 @Configuration@Bean@ComponentScan 等一系列注解,基本可以满足日常所需。

    通过 AnnotationConfigApplicationContext 可以构建一个支持基于注解和 Java 类的 Spring 上下文:

ApplicationContext ctx = new AnnotationConfigApplicationContext(Config.class);

其中的 Config 类就是一个加了 @Configuration 注解的 Java 类,它可以是代码示例 2-14 这样的。

代码示例 2-14 Java 配置类示例

@Configuration 
@ComponentScan("learning.spring")
public class Config {@Bean@Lazy@Scope("prototype")public Hello helloBean() {return new Hello();}
}

类上的 @Configuration 注解表明这是一个 Java 配置类, @ComponentScan 注解指定了类扫描的包名,作用与 <context:component-scan/> 类似。在 @ComponentScan 中, includeFiltersexcludeFilters 可以用来指定包含和排除的组件,例如官方文档中就有如下示例:

@Configuration
@ComponentScan(basePackages = "org.example",includeFilters = @Filter(type = FilterType.REGEX,pattern = ".*Stub.*Repository"),excludeFilters = @Filter(Repository.class))
public class AppConfig { ... }

如果 @Configuration 没有指定扫描的基础包路径或者类,默认就从该配置类的包开始扫描。

Config 类中的 helloBean() 方法上添加了 @Bean 注解,该方法的返回对象会被当做容器中的一个 Bean, @Lazy 注解说明这个 Bean 是延时加载的, @Scope 注解则指定了它是原型 Bean。 @Bean 注解有如下属性,如表 2-7 所示。

表 2-7 @Bean 注解的属性

属性默认值说明
name{}Bean 的名称,默认同方法名
value{}name
autowireAutowire.NO自动织入方式
autowireCandidatetrue是否是自动织入的候选 Bean
initMethod""初始化方法名
destroyMethodAbstractBeanDefinition.INFER_METHOD销毁方法名

自动推测销毁 Bean 时该调用的方法,会自动去调用修饰符为 public、没有参数且方法名是 closeshutdown 的方法。如果类实现了 java.lang.AutoCloseablejava.io.Closeable 接口,也会调用其中的 close() 方法。

在 Java 配置类中指定 Bean 之间的依赖关系有两种方式,通过方法的参数注入依赖,或者直接调用类中带有 @Bean 注解的方法。

代码示例 2-15 中, foo() 创建了一个名为 foo 的 Bean, bar() 方法通过参数 foo 注入了 foo 这个 Bean, baz() 方法内则通过调用 foo() 获得了同一个 Bean。

代码示例 2-15 依赖示例

   @Configurationpublic class Config {@Beanpublic Foo foo() {return new Foo();}@Beanpublic Bar bar(Foo foo) {return new Bar(foo);}@Beanpublic Baz baz() {return new Baz(foo());}}

需要重点说明的是,Spring Framework 针对 @Configuration 类中带有 @Bean 注解的方法通过 CGLIB(Code Generation Library)做了特殊处理,针对返回单例类型 Bean 的方法,调用多次返回的结果是一样的,并不会真的执行多次。

在配置类中也可以导入其他配置,例如,用 @Import 导入其他配置类,用 @ImportResource 导入配置文件,就像下面这样:

@Configuration
@Import({ CongfigA.class, ConfigB.class })
@ImportResource("classpath:/spring/*-applicationContext.xml")
public class Config {}

2.3 定制容器与 Bean 的行为

通常,Spring Framework 包揽了大部分工作,替我们管理 Bean 的创建与依赖,将各种组件装配成一个可运行的应用。然而,有些情况下,我们会有自己的特殊需求。例如,在 Bean 的依赖被注入后,我们想要触发 Bean 的回调方法做一些初始化;在 Bean 销毁前,我们想要执行一些清理工作;我们想要 Bean 感知容器的一些信息,拿到当前的上下文自行进行判断或处理……

这时候,怎么做?Spring Framework 为我们预留了发挥空间。本节我们就来探讨一下如何根据自己的需求来定制容器与 Bean 的行为。

2.3.1 Bean 的生命周期

Spring 容器接管了 Bean 的整个生命周期管理,具体如图 2-2 所示。一个 Bean 先要经过 Java 对象的创建(也就是通过 new 关键字创建一个对象),随后根据容器里的配置注入所需的依赖,最后调用初始化的回调方法,经过这三个步骤才算完成了 Bean 的初始化。若不再需要这个 Bean,则要进行销毁操作,在正式销毁对象前,会先调用容器的销毁回调方法。

image.png

图 2-2 Bean 的生命周期

由于一切都是由 Spring 容器管理的,所以我们无法像自己控制这些动作时那样任意地在 Bean 创建后Bean 销毁前 增加某些操作。为此,Spring Framework 为我们提供了几种途径,在这两个时间点调用我们提供给容器的回调方法。可以根据不同情况选择以下三种方式之一:

  • 实现 InitializingBeanDisposableBean 接口;

  • 使用 JSR-250 的 @PostConstruct@PreDestroy 注解;

  • <bean/>@Bean 里配置初始化和销毁方法。

  • 创建 Bean 后的回调动作

    如果我们希望在创建 Bean 后做一些特别的操作,比如查询数据库初始化缓存等,Spring Framework 可以提供一个初始化方法。 InitializingBean 接口有一个 afterPropertiesSet() 方法——顾名思义,就是在所有依赖都注入后自动调用该方法。在方法上添加 @PostConstruct 注解也有相同的效果。

    也可以像下面这样,在 XML 文件中进行配置

    <bean id="hello" class="learning.spring.helloworld.Hello" init-method="init" />
    

    或者在 Java 配置中指定:

        @Bean(initMethod="init")public Hello hello() {...}
    
  • 销毁 Bean 前的回调动作

    Spring Framework 既然有创建 Bean 后的回调动作,自然也有销毁 Bean 前的触发操作。 DisposableBean 接口中的 destroy() 方法和添加了 @PreDestroy 注解的方法都能实现这个目的,如代码示例 2-16 所示。

    代码示例 2-16 实现了 DisposableBean 接口的类

     package learning.spring.helloworld;import org.springframework.beans.factory.DisposableBean;public class Hello implements DisposableBean {public String hello() {return "Hello World!";}@Overridepublic void destroy() throws Exception {System.out.println("See you next time.");}}
    

    当然,也可以在 <bean/> 中指定 destroy-method,或者在 @Bean 中指定 destroyMethod

  • 生命周期动作的组合

    如果我们混合使用上文提到的几种不同的方式,而且这些方式指定的方法还不尽相同,那就需要明确它们的执行顺序了。

    无论是初始化还是销毁,Spring 都会按照如下顺序依次进行调用:

    (1) 添加了 @PostConstruct@PreDestroy 的方法;

    (2) 实现了 InitializingBeanafterPropertiesSet() 方法,或 DisposableBeandestroy() 方法;

    (3) 在 <bean/> 中配置的 init-methoddestroy-method@Bean 中配置的 initMethoddestroyMethod

    在代码示例 2-17 的 Java 类中,同时提供了三个销毁的方法。

    代码示例 2-17 添加了多个销毁方法的 Hello

     package learning.spring.helloworld;import javax.annotation.PreDestroy;import org.springframework.beans.factory.DisposableBean;public class Hello implements DisposableBean {public String hello() {return "Hello World!";}@Overridepublic void destroy() throws Exception {System.out.println("destroy()");}public void close() {System.out.println("close()");}@PreDestroypublic void shutdown() {System.out.println("shutdown()");}}
    

    在对应的 XML 文件中配置 destroy-method,要用 <context:annotation-config /> 开启注解支持,如代码示例 2-18 所示。

    代码示例 2-18 对应的 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"xmlns:context="http://www.springframework.org/schema/context"xsi:schemaLocation="http://www.springframework.org/schema/beanshttps://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/contexthttps://www.springframework.org/schema/context/spring-context.xsd"><context:annotation-config /><bean id="hello" class="learning.spring.helloworld.Hello" destroy-method="close" /></beans>
    

    代码示例 2-19 对运行类简单做了些调整,增加了关闭容器的动作,以便能让我们观察到 Bean 销毁的动作。

    代码示例 2-19 启动用的 Application 类代码片段

     public class Application {private ClassPathXmlApplicationContext applicationContext;public static void main(String[] args) {Application application = new Application();application.sayHello();application.close();}public Application() {applicationContext = new ClassPathXmlApplicationContext("beans.xml");}public void sayHello() {Hello hello = (Hello) applicationContext.getBean("hello");System.out.println(hello.hello());}public void close() {applicationContext.close();}}
    

    这段代码运行后的输出即代表了三个方法的调用顺序,是与上述顺序一致的:

     Hello World!shutdown()destroy()close()
    

    当然,在一般情况下,我们并不会在一个 Bean 上写几个作用不同的初始化或销毁方法。这种情况并不常见,大家了解即可。

2.3.2 Aware 接口的应用

在大部分情况下,我们的 Bean 感知不到 Spring 容器的存在,也无须感知。但总有那么一些场景中我们要用到容器的一些特殊功能,这时就可以使用 Spring Framework 提供的很多 Aware 接口,让 Bean 能感知到容器的诸多信息。此外,有些容器相关的 Bean 不能由我们自己来创建,必须由容器创建后注入我们的 Bean 中。

例如,如果希望在 Bean 中获取容器信息,可以通过如下两种方式:

  • 实现 BeanFactoryAwareApplicationContextAware 接口;
  • @Autowired 注解来注入 BeanFactoryApplicationContext

两种方式的本质都是一样的,即让容器注入一个 BeanFactoryApplicationContext 对象。 ApplicationContextAware 是下面这样的:

    public interface ApplicationContextAware {void setApplicationContext(ApplicationContext applicationContext) throws BeansException;}

在拿到 ApplicationContext 后,就能操作该对象,比如调用 getBean() 方法取得想要的 Bean。能直接操作 ApplicationContext 有时可以带来很多便利,因此这个接口相比其他的 Aware 接口“出镜率”更高一些。

如果 Bean 希望获得自己在容器中定义的 Bean 名称,可以实现 BeanNameAware 接口。这个接口的 setBeanName() 方法就是注入一个代表名称的字符串,也算是一个依赖,因此会在 2.3.1 节提到的初始化方法前被执行。

在 2.3.3 节中会提到 Spring 容器的事件机制,这时就会用到 ApplicationEventPublisher 来发送事件,可以实现 ApplicationEventPublisherAware 接口,从容器中获得 ApplicationEventPublisher 实例。

Spring Framework 中还有很多其他 Aware 接口,感兴趣的话,大家可以查阅官方文档了解更多详情。

2.3.3 事件机制

ApplicationContext 提供了一套事件机制,在容器发生变动时我们可以通过 ApplicationEvent 的子类通知到 ApplicationListener 接口的实现类,做对应的处理。例如, ApplicationContext 在启动、停止、关闭和刷新时,分别会发出 ContextStartedEventContextStoppedEventContextClosedEventContextRefreshedEvent 事件,这些事件就让我们有机会感知当前容器的状态。

我们也可以自己监听这些事件,只需实现 ApplicationListener 接口或者在某个 Bean 的方法上增加 @EventListener 注解即可,例如代码示例 2-20 和代码示例 2-21就用以上两种方式分别处理了 ContextClosedEvent 事件。

代码示例 2-20 用 ApplicationListener 接口处理 ContextClosedEvent 事件的 ContextClosedEventListener 类代码片段

    @Component@Order(1)public class ContextClosedEventListener implements ApplicationListener<ContextClosedEvent> {@Overridepublic void onApplicationEvent(ContextClosedEvent event) {System.out.println("[ApplicationListener]ApplicationContext closed.");}}

代码示例 2-21 用 @EventListener 注解处理 ContextClosedEvent 事件的 ContextClosedEventAnnotationListener 类代码片段

    @Componentpublic class ContextClosedEventAnnotationListener {@EventListener@Order(2)public void onEvent(ContextClosedEvent event) {System.out.println("[@EventListener]ApplicationContext closed.");}}

在运行 ch2/helloworld-event 中的 Application 后会得到如下输出:

    上略……[ApplicationListener]ApplicationContext closed.[@EventListener]ApplicationContext closed.

可以看到两个类都处理了 ContextClosedEvent 事件,我们通过 @Order 可以指定处理的顺序。

这套机制不仅适用于 Spring Framework 的内置事件,也非常方便我们定义自己的事件,不过该事件必须继承 ApplicationEvent,而且产生事件的类需要实现 ApplicationEventPublisherAware,还要从上下文中获取到 ApplicationEventPublisher,用它来发送事件。代码示例 2-22 是事件生产者 CustomEventPublisher 类的代码片段。

代码示例 2-22 生产事件的 CustomEventPublisher 类代码片段

    @Componentpublic class CustomEventPublisher implements ApplicationEventPublisherAware {private ApplicationEventPublisher publisher;public void fire() {publisher.publishEvent(new CustomEvent("Hello"));}@Overridepublic void setApplicationEventPublisher( ApplicationEventPublisher applicationEventPublisher) {this.publisher = applicationEventPublisher;}}

对应的事件监听代码也非常简单,对应方法如下:

    @EventListenerpublic void onEvent(CustomEvent customEvent) {System.out.println("CustomEvent Source: " + customEvent.getSource());}

在运行 ch2/helloworld-event 中的 Application 后会得到如下输出:

    上略……CustomEvent Source: Hello下略……

@EventListener 还有一些其他的用法,比如,在监听到事件后希望再发出另一个事件,这时可以将方法返回值从 void 修改为对应事件的类型; @EventListener 也可以与 @Async 注解结合,实现在另一个线程中处理事件。关于 @Async 注解,我们会在 2.4.2 节中进行说明。

2.3.4 容器的扩展点

Spring 容器是非常灵活的,Spring Framework 中有很多机制是通过容器自身的扩展点来实现的,比如 Spring AOP 等。如果我们想在 Spring Framework 上封装自己的框架或功能,也可以充分利用容器的扩展点。

BeanPostProcessor 接口是用来定制 Bean 的,顾名思义,这个接口是 Bean 的后置处理器,在 Spring 容器初始化 Bean 时可以加入我们自己的逻辑。该接口中有两个方法, postProcessBeforeInitialization() 方法在 Bean 初始化前执行, postProcessAfterInitialization() 方法在 Bean 初始化之后执行。如果有多个 BeanPostProcessor,可以通过 Ordered 接口或者 @Order 注解来指定运行的顺序。代码示例 2-23 演示了 BeanPostProcessor 的基本用法。

代码示例 2-23 打印信息的 HelloBeanPostProcessor 类代码片段

    public class HelloBeanPostProcessor implements BeanPostProcessor {@Overridepublic Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {if ("hello".equals(beanName)) {System.out.println("Hello postProcessBeforeInitialization");}return bean;}@Overridepublic Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {if ("hello".equals(beanName)) {System.out.println("Hello postProcessAfterInitialization");}return bean;}}

我们在对应的 Hello 中,也增加一个带 @PostConstruct 注解的方法,执行 ch2/helloworld-processor 中的 Application 类,验证一下方法的执行顺序是否与大家预想的一样:

    Hello postProcessBeforeInitializationHello PostConstructHello postProcessAfterInitialization

如果说 BeanPostProcessor 是 Bean 的后置处理器,那 BeanFactoryPostProcessor 就是 BeanFactory 的后置处理器,我们可以通过它来定制 Bean 的配置元数据,其中的 postProcessBeanFactory() 方法会在 BeanFactory 加载所有 Bean 定义但尚未对其进行初始化时介入。它的用法与 BeanPostProcessor 类似,此处就不再赘述了。需要注意的是,如果用 Java 配置类来注册,那么方法需要声明为 static。2.4.1 节中会讲到的 PropertySourcesPlaceholderConfigurer 就是一个 BeanFactoryPostProcessor 的实现。

需要重点说明一下,由于 Spring AOP 也是通过 BeanPostProcessor 实现的,因此实现该接口的类,以及其中直接引用的 Bean 都会被特殊对待, 不会 被 AOP 增强。此外, BeanPostProcessorBeanFactoryPostProcessor 都仅对当前容器上下文的 Bean 有效,不会去处理其他上下文。

2.3.5 优雅地关闭容器

Java 进程在退出时,我们可以通过 Runtime.getRuntime().addShutdownHook() 方法添加一些钩子,在关闭进程时执行特定的操作。如果是 Spring 应用,在进程退出时也要能正确地执行一些清理的方法。

ConfigurableApplicationContext 接口扩展自 ApplicationContext,其中提供了一个 registerShutdownHook()AbstractApplicationContext 类实现了该方法,正是调用了前面说到的 Runtime.getRuntime().addShutdownHook(),并且在其内部调用了 doClose() 方法。

设想在生产代码里有这么一种情况:一个 Bean 通过 ApplicationContextAware 注入了 ApplicationContext,业务代码根据逻辑判断从 ApplicationContext 中取出对应名称的 Bean,再进行调用;问题出现在应用程序关闭时,容器已经开始销毁 Bean 了,可是这段业务代码还在执行,仍在继续尝试从容器中获取 Bean,而且代码还没正确处理此处的异常……这该如何是好?

针对这种情况,我们可以借助 Spring Framework 提供的 Lifecycle 来感知容器的启动和停止,容器会将启动和停止的信号传播给实现了该接口的组件和上下文。为了让例子能够简单一些,我们把问题简化一下: Hello.hello() 在容器关闭前后返回不同的内容,如代码示例 2-2423 所示。

代码示例 2-24 实现了 Lifecycle 接口的 Hello 类代码片段

    public class Hello implements Lifecycle {private boolean flag = false;public String hello() {return flag ? "Hello World!" : "Bye!";}@Overridepublic void start() {System.out.println("Context Started.");flag = true;}@Overridepublic void stop() {System.out.println("Context Stopped.");flag = false;}@Overridepublic boolean isRunning() {return flag;}}

我们将对应的 Application 类也做相应调整,具体如代码示例 2-25 所示。

代码示例 2-25 调整后的 Application 类代码片段

    @Configurationpublic class Application {public static void main(String[] args) {AnnotationConfigApplicationContext applicationContext =new AnnotationConfigApplicationContext(Application.class);applicationContext.start(); // 这会触发Lifecycle的start()Hello hello = applicationContext.getBean("hello", Hello.class);System.out.println(hello.hello());applicationContext.close(); // 这会触发Lifecycle的stop()System.out.println(hello.hello());}@Beanpublic Hello hello() {return new Hello();}}

上述代码的执行结果如下:

    Context Started.Hello World!Context Stopped.Bye!

除此之外,我们还可以借助 Spring Framework 的事件机制,在上下文关闭时会发出 ContextClosedEvent,监听该事件也可以触发业务代码做对应的操作。

茶歇时间:Linux 环境下如何关闭进程

在 Linux 环境下,大家常用 kill 命令来关闭进程,其实是 kill 命令给进程发送了一个信号(通过 kill -l 命令可以查看信号列表)。不带参数的“ kill 进程号”发送的是 SIGTERM(15),一般程序在收到这个信号后都会先释放资源,再停止;但有时程序可能还是无法退出,这时就可以使用“ kill -9 进程号”,发送 SIGKILL(9),直接杀死进程。

一般不建议直接使用 -9,因为非正常地中断程序可能会造成一些意料之外的情况,比如业务逻辑处理到一半,恢复手段不够健全的话,可能需要人工介入处理那些执行到一半的内容。

2.4 容器中的几种抽象

Spring Framework 针对研发和运维过程中的很多常见场景做了抽象处理,比如本节中会讲到的针对运行环境的抽象,后续章节中会聊到的事务抽象等。正是因为存在这些抽象层,Spring Framework 才能为我们屏蔽底层的很多细节。

2.4.1 环境抽象

自诞生之日起,Java 程序就一直宣传自己是“Write once, run anywhere”,但往往现实并非如此——虽然有 JVM 这层隔离,但我们的程序还是需要应对不同的运行环境细节:比如使用了 WebLogic 的某些特性会导致程序很难迁移到 Tomcat 上;此外,程序还要面对开发、测试、预发布、生产等环境的配置差异;在云上,不同可用区(availability zone)可能也有细微的差异。Spring Framework 的环境抽象可以简化大家在处理这些问题时的复杂度,代表程序运行环境的 Environment 接口包含两个关键信息——Profile 和 Properties,下面我们将详细展开这两项内容。

  1. Profile 抽象

    假设我们的系统在测试环境中不需要加载监控相关的 Bean,而在生产环境中则需要加载;亦或者针对不同的客户要求,A 客户要求我们部署的系统直接配置数据库连接池,而 B 客户要求通过 JNDI 获取连接池。此时,就可以利用 Profile 帮我们解决这些问题。

    如果使用 XML 进行配置,可以在 <beans/>profile 属性中进行设置。如果使用 Java 类的配置方式,可以在带有 @Configuration 注解的类上,或者在带有 @Bean 注解的方法上添加 @Profile 注解,并在其中指定该配置生效的具体 Profile,就像代码示例 2-26 那样。

    代码示例 2-26 针对开发和测试环境的不同 Java 配置

        @Configuration@Profile("dev")public class DevConfig {@Beanpublic Hello hello() {Hello hello = new Hello();hello.setName("dev");return hello;}}@Configuration@Profile("test")public class TestConfig {@Beanpublic Hello hello() {Hello hello = new Hello();hello.setName("test");return hello;}}
    

    通过如下两种方式可以指定要激活的 Profile(多个 Profile 用逗号分隔):

    • ConfigurableEnvironment.setActiveProfiles() 方法指定要激活的 Profile(通过 ApplicationContext.getEnvironment() 方法可获得 Environment);
    • spring.profiles.active 属性指定要激活的 Profile(可以用系统环境变量、JVM 参数等方式指定,能通过 PropertySource 找到即可,在第 4 章会详细介绍 Spring Boot 的属性加载机制)。

例如,启动程序时,在命令行中增加 spring.profiles.active

java -Dspring.profiles.active="dev" -jar xxx.jar

Spring Framework 还提供了默认的 Profile,一般名为 default,但也可以通过 ConfigurableEnvironment.setDefaultProfiles()spring.profiles.default 来修改这个名称。

  1. PropertySource 抽象

    Spring Framework 中会频繁用到属性值,而这些属性又来自于很多地方, PropertySource 抽象就屏蔽了这层差异,例如,可以从 JNDI、JVM 系统属性( -D 命令行参数, System.getProperties() 方法能取得系统属性)和操作系统环境变量中加载属性。

    在 Spring 中,一般属性用小写单词表示并用点分隔,比如 foo.bar,如果是从环境变量中获取属性,会按照 foo.barfoo_barFOO.BARFOO_BAR 的顺序来查找。4.3.2 节还有加载属性相关的内容,届时还会进一步说明。

    我们可以像下面这样来获得属性 foo.bar

        public class Hello {@Autowiredprivate Environment environment;public void hello() {System.out.println("foo.bar: " + environment.getProperty("foo.bar"));}}
    

    我们在 2.2.3 节中看到过 @Value 注解,它也能获取属性,获取不到时则返回默认值:

        public class Hello {@Value("$") // :后是默认值private String value;public void hello() {System.out.println("foo.bar: " + value);}}
    

    ${} 占位符可以出现在 Java 类配置或 XML 文件中,Spring 容器会试图从各种已经配置了的来源中解析属性。要添加属性来源,可以在 @Configuration 类上增加 @PropertySource 注解,例如:

        @Configuration@PropertySource("classpath:/META-INF/resources/app.properties")public class Config {...}
    

    如果使用 XML 进行配置,可以像下面这样:

        <context:property-placeholder location="classpath:/META-INF/resources/app.properties" />
    

    通常我们的预期是一定能找到需要的属性,但也有这个属性可有可无的情况,这时将注解的 ignoreResourceNotFound 或者 XML 文件的 ignore-resource-not-found 设置为 true 即可。如果存在多个配置,则可以通过 @Order 注解或 XML 文件的 order 属性来指定顺序。

    也许大家会好奇,Spring Framework 是如何实现占位符解析的,这一切要归功于 PropertySourcesPlaceholderConfigurer 这个 BeanFactoryPostProcessor。如果使用 <context:property-placeholder/>,Spring Framework 会自动注册一个 PropertySourcesPlaceholderConfigurer,如果是 Java 配置,则需要我们自己用 @Bean 来注册一个,例如:

        @Beanpublic static PropertySourcesPlaceholderConfigurer configurer() {return new PropertySourcesPlaceholderConfigurer();}
    

    在它的 postProcessBeanFactory() 方法中,Spring 会尝试用找到的属性值来替换上下文中的对应占位符,这样在 Bean 正式初始化时我们就不会再看到占位符,而是实际替换后的值。

    我们也可以定义自己的 PropertySource 实现,将它添加到 ConfigurableEnvironment.getPropertySources() 返回的 PropertySources 中即可,Spring Cloud Config 其实就使用了这种方式。

2.4.2 任务抽象

看过了与环境相关的抽象后,我们再来看看与任务执行相关的内容。Spring Framework 通过 TaskExecutorTaskScheduler 这两个接口分别对任务的异步执行与定时执行进行了抽象,接下来就让我们一起来了解一下。

  1. 异步执行

    Spring Framework 的 TaskExecutor 抽象是在 2.0 版本时引入的, Executor 是 Java 5 对线程池概念的抽象,如果了解 JUC( java.util.concurrent)的话,一定会知道 java.util.concurrent.Executor 这个接口,而 TaskExecutor 就是在它的基础上又做了一层封装,让我们可以方便地在 Spring 容器中配置多线程相关的细节。

    TaskExecutor 有很多实现,例如,同步的 SyncTaskExecutor;每次创建一个新线程的 SimpleAsyncTaskExecutor;内部封装了 Executor,非常灵活的 ConcurrentTaskExecutor;还有我们用的最多的 ThreadPoolTaskExecutor

    我们可以像下面这样直接配置一个 ThreadPoolTaskExecutor

        <bean id="taskExecutor" class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor"><property name="corePoolSize" value="4"/><property name="maxPoolSize" value="8"/><property name="queueCapacity" value="32"/></bean>
    

    也可使用 <task:executor/>,下面是一个等价的配置:

        <task:executor id="taskExecutor" pool-size="4-8" queue-capacity="32"/>
    

    茶歇时间:该怎么配置线程池

    如果要在程序中使用线程,请不要自行创建 Thread,而应该尽可能考虑使用线程池,并且明确线程池的大小—不能无限制地创建线程。

    网上有这样的建议,对于 CPU 密集型的系统,要尽可能减少线程数,建议线程池大小配置为“CPU 核数 +1”;对于 IO 密集型系统,为了避免 CPU 浪费在等待 IO 上,建议线程池大小为“CPU 核数 ×2”。当然,这只是一个建议值,具体还是可以根据情况来做调整的。

    线程池的等待队列默认为 Integer.MAX_VALUE,这样可能会造成任务的大量堆积,所以设置一个合理的等待队列大小后,就要应对“队列满”的情况。“队列满”时的处理策略是由 RejectedExecutionHandler 决定的,默认是 ThreadPoolExecutor.AbortPolicy,即直接抛出一个 RejectedExecutionException 异常。如果我们能接受直接抛弃任务,也可以将策略设置为 ThreadPoolExecutor.DiscardPolicyThreadPoolExecutor.DiscardOldestPolicy

    此外, ThreadPoolTaskExecutor 还有一个 keepAliveSeconds 的属性,通过它可以调整空闲状态线程的存活时间。如果当前线程数大于核心线程数,到存活时间后就会清理线程。

    在配置好了 TaskExecutor 后,可以直接调用它的 execute() 方法,传入一个 Runnable 对象;也可以在方法上使用 @Async 注解,这个方法可以是空返回值,也可以返回一个 Future

        @Asyncpublic void runAsynchronous() {...}
    

    为了让该注解生效,需要在配置类上增加 @EnableAsync 注解,或者在 XML 文件中增加 <task:annotation-driven/> 配置,开启对它的支持。

    默认情况下,Spring 会为 @Async 寻找合适的线程池定义:例如上下文里唯一的 TaskExecutor;如果存在多个,则用 ID 为 taskExecutor 的那个;前面两个都找不到的话会降级使用 SimpleAsyncTaskExecutor。当然,也可以在 @Async 注解中指定一个。

    请注意 对于异步执行的方法,由于在触发时主线程就返回了,我们的代码在遇到异常时可能根本无法感知,而且抛出的异常也不会被捕获,因此最好我们能自己实现一个 AsyncUncaughtExceptionHandler 对象来处理这些异常,最起码打印一个异常日志,方便问题排查。

  2. 定时任务

    定时任务,顾名思义,就是在特定的时间执行的任务,既可以是在某个特定的时间点执行一次的任务,也可以是多次重复执行的任务。

    TaskScheduler 对两者都有很好的支持,其中的几个 schedule() 方法是处理单次任务的,而 scheduleAtFixedRate()scheduleWithFixedDelay() 则是处理多次任务的。 scheduleAtFixedRate() 按固定频率触发任务执行, scheduleWithFixedDelay() 在第一次任务执行完毕后等待指定的时间后再触发第二次任务。

    TaskScheduler.schedule() 可以通过 Trigger 来指定触发的时间,其中最常用的就是接收 Cron 表达式的 CronTrigger 了,可以像下面这样在周一到周五的下午 3 点 15 分触发任务:

        scheduler.schedule(task, new CronTrigger("0 15 15 * * 1-5"));
    

    TaskExecutor 类似,Spring Framework 也提供了不少 TaskScheduler 的实现,其中最常用的也是 ThreadPoolTaskScheduler。上述例子中的 scheduler 就可以是一个注入的 ThreadPoolTaskScheduler Bean。

    我们可以选择用 <task:scheduler/> 来配置 TaskScheduler

        <task:scheduler id="taskScheduler" pool-size="10" />
    

    也可以使用注解,默认情况下,Spring 会在同一上下文中寻找唯一的 TaskScheduler Bean,有多个的话用 ID 是 taskScheduler 的,再不行就用一个单线程的 TaskScheduler。在配置任务前,需要先在配置类上添加 @EnableScheduling 注解或在 XML 文件中添加 <task:annotation-driven/> 开启注解支持:

        @Configuration@EnableSchedulingpublic class Config {...}
    

    随后,在方法上添加 @Scheduled 注解就能让方法定时执行,例如:

        @Scheduled(fixedRate=1000) // 每隔1000ms执行public void task1() {...}@Scheduled(fixedDelay=1000) // 每次执行完后等待1000ms再执行下一次public void task2() {...}@Scheduled(initialDelay=5000, fixedRate=1000) // 先等待5000ms开始执行第一次,后续每隔1000ms执行一次public void task3() {...}@Scheduled(cron="0 15 15 * * 1-5") // 按Cron表达式执行public void task4() {...}
    

    茶歇时间:本地调度 vs. 分布式调度

    上文提到的调度任务都是在一个 JVM 内部执行的,一般我们的系统都是以集群方式部署的,因此并非所有任务都需要在每台服务器上执行,同一时间,集群中的一台服务器能执行就够了。这时,仅有本节所提供的调度任务支持是不够的,我们还可以借助其他调度服务来实现我们的需求,例如,当当开源的 ElasticJob。

    举个例子,为了提升性能,我们会使用多级缓存,代码优先读取 JVM 本地缓存,没有命中的话再去读取 Redis 分布式缓存。缓存要靠定时任务来刷新,此时本地调度任务就用来刷新 JVM 缓存,而分布式调度任务就用来刷新 Redis 缓存。当然,我们也可以通过分布式调度来管理每台机器上的调度任务。

    甚至在一些场景中我们还需要对调度任务进行复杂的拆分:一台机器接收到任务被触发,接着进行一系列的准备工作,随后将任务分发到集群中的其他节点上进行后续处理,以此充分发挥集群的作用。

    总之,调度任务可以是非常复杂的,本节只是简单地引入这个话题,感兴趣的话,大家可以再深入研究。

2.5 小结

通过本章的学习,我们对 Spring Framework 的核心容器及 Bean 的概念已经有了一个大概的了解。不仅知道了如何去使用它们,更是深入了解了如何对一些特性进行定制,如何通过类似事件通知这样的机制来应对某些问题。

Spring Framework 为了让大家专心于业务逻辑,为我们提供了很多抽象来屏蔽底层的实现。本章中的环境抽象和任务抽象就是很好的例子。

下一章,我们将看到 Spring Framework 中的另一个重要内容——AOP 支持。

相关文章:

第 2 章:Spring Framework 中的 IoC 容器

控制反转&#xff08;Inversion of Control&#xff0c;IoC&#xff09;与 面向切面编程&#xff08;Aspect Oriented Programming&#xff0c;AOP&#xff09;是 Spring Framework 中最重要的两个概念&#xff0c;本章会着重介绍前者&#xff0c;内容包括 IoC 容器以及容器中 …...

构造函数、实例、原型对象三者之间的关系

在 JavaScript 中&#xff0c;构造函数、实例和原型对象之间有着密切的关系。下面是对它们之间关系的详细解析和代码示例&#xff1a; 构造函数&#xff1a;构造函数是一个特殊的函数&#xff0c;用于创建对象的模板。它定义了对象的属性和方法。构造函数通常以大写字母开头&a…...

人工智能抢走了他们的工作。现在他们得到报酬,让它听起来像人类

人工智能抢走了他们的工作。现在他们得到报酬&#xff0c;让它听起来像人类 如果你担心人工智能会如何影响你的工作&#xff0c;那么广告文案的世界或许能让你窥见未来。 作家本杰明米勒(化名)在2023年初非常红火。他领导了一个由60多名作家和编辑组成的团队&#xff0c;发表博…...

大模型微调出错的解决方案(持续更新)

大家好,我是herosunly。985院校硕士毕业,现担任算法研究员一职,热衷于机器学习算法研究与应用。曾获得阿里云天池比赛第一名,CCF比赛第二名,科大讯飞比赛第三名。拥有多项发明专利。对机器学习和深度学习拥有自己独到的见解。曾经辅导过若干个非计算机专业的学生进入到算法…...

企业多云策略的优势与实施指南

企业在选择云服务提供商时&#xff0c;常见的选项包括亚马逊AWS、微软Azure、谷歌云GCP、阿里云、腾讯云和华为云。为了避免过度依赖单一供应商&#xff0c;许多企业选择采用多云策略&#xff0c;这样可以充分利用不同云服务的优势&#xff0c;同时避免重复工作和其他额外的工作…...

vue分页

先看效果 再看代码 <!-- 分页 --><div v-if"pageParams.pageCount > 1" class"flex justify-end mt-6"><n-paginationv-model:page"pageParams.page" v-model:page-size"pageParams.pageSize" :page-count"pa…...

服务器上设置pnpm环境变量

首先&#xff0c;确认 pnpm 是否已经安装&#xff1a; ls /www/server/nodejs/v20.10.0/bin/pnpm如果输出包含 pnpm&#xff0c;那么说明 pnpm 已经安装。 如果没有看到 pnpm&#xff0c;你可能需要重新安装它&#xff1a; npm install -g pnpm接下来&#xff0c;确保 PATH …...

Java中BIO、NIO、AIO详解

参考&#xff1a; https://blog.csdn.net/s2152637/article/details/98777686 https://blog.csdn.net/bigorsmallorlarge/article/details/137292669 1、几个基本概念 Java中IO模型简介 在Java中&#xff0c;主要有三种IO模型&#xff0c;分别是&#xff1a; 同步阻塞IO&…...

cloud_enum:一款针对不同平台云环境安全的OSINT工具

关于cloud_enum cloud_enum是一款功能强大的云环境安全OSINT工具&#xff0c;该工具支持AWS、Azure和Google Cloud三种不同的云环境&#xff0c;旨在帮助广大研究人员枚举目标云环境中的公共资源&#xff0c;并尝试寻找其中潜在的安全威胁。 功能介绍 当前版本的cloud_enum支…...

图像的对比度和亮度

目标 访问像素值用0来初始化矩阵cv::saturate_cast像素转换提高一张图像的亮度 原理 图像处理 图像变换可以被视作两个步骤&#xff1a; 点操纵&#xff08;像素转换&#xff09;相邻区域转换&#xff08;以面积为基础&#xff09; 像素转换 在这种图像处理的转换过程中…...

手撕设计模式——计划生育之单例模式

1.业务需求 ​ 大家好&#xff0c;我是菠菜啊。80、90后还记得计划生育这个国策吗&#xff1f;估计同龄的小伙伴们&#xff0c;小时候常常被”只生一个好“”少生、优生“等宣传标语洗脑&#xff0c;如今国家已经放开并鼓励生育了。话说回来&#xff0c;现实生活中有计划生育&…...

Mac M3 Pro 部署Flink-1.16.3

目录 1、下载安装包 2、解压及配置 3、启动&测试 4、测试FlinkSQL读取hive数据 以上是mac硬件配置 1、下载安装包 官网&#xff1a;Downloads | Apache Flink 网盘&#xff1a; Flink 安装包 https://pan.baidu.com/s/1IN62_T5JUrnYUycYMwsQqQ?pwdgk4e Flink 已…...

Mysql 的分布式策略

1. 前言 MySQL 作为最最常用的数据库&#xff0c;了解 Mysql 的分布式策略对于掌握 MySQL 的高性能使用方法和更安全的储存方式有非常重要的作用。 它同时也是面试中最最常问的考点&#xff0c;我们这里就简单总结下 Mysq 的常用分布式策略。 2. 复制 复制主要有主主复制和…...

记录一个利用winhex进行图片隐写分离的

前提 是一次大比武里面的题目&#xff0c;属实给我开了眼&#xff0c;跟我之前掌握的关于隐写合并的操作都不一样。 它不是直接在文件里面进行输入文件隐写&#xff0c;叫你输入密码&#xff0c;或者更改颜色&#xff0c;或者偏移位置&#xff1b; 它不是单纯几个文件合并&a…...

压缩映射定理证明

收缩映射定理&#xff08;又称Banach不动点定理&#xff09;是一个重要的结果&#xff0c;特别是在分析和应用数学中。 定理&#xff08;收缩映射定理&#xff09;&#xff1a;假设是一个从度量空间 (X,d) 到自身的函数&#xff0c;如果 是一个收缩映射&#xff0c;即存在常数 …...

Ubuntu20.04.6操作系统安装教程

一、VMware Workstation16安装 选择安装VMware Workstation&#xff0c;登录其官网下载安装包&#xff0c;链接如下&#xff1a; 下载 VMware Workstation Pro 下载后运行安装向导&#xff0c;一直Next即可。 二、Ubuntu镜像下载 ubuntu20.04 选择需要下载的镜像类型下载即…...

(分治算法3)leecode 53 最大子数组和(最大子段和)

题目描述 给你一个整数数组 nums &#xff0c;请你找出一个具有最大和的连续子数组&#xff08;子数组最少包含一个元素&#xff09;&#xff0c;返回其最大和。 子数组是数组中的一个连续部分。 分治解法 这个问题可以分成从左半边数组找最大子段和从右半部分找最大子段和…...

【C++】模板初级

【C】模板初级 泛型编程函数模板函数模板的概念函数模板格式函数模板的原理函数模板的实例化模板参数的匹配原则 类模板类模板格式类模板的实例化 泛型编程 当我们之前了解过函数重载后可以知道&#xff0c;一个程序可以出现同名函数&#xff0c;但参数类型不同。 //整型 voi…...

eslint 使用单引号,Prettier使用双引号冲突

当 ESLint 规则要求使用单引号 (quotes: single) 而 Prettier 默认使用双引号时&#xff0c;会发生配置冲突。为了解决这个问题&#xff0c;你需要统一这两个工具的配置&#xff0c;确保它们遵循相同的规则。这里推荐两种解决方案&#xff1a; 解决方案 1: 修改 ESLint 配置以…...

进化生物学的数学原理 知识点总结

1、进化论与自然选择 1.1 进化论 1、进化论 过度繁殖 -> 生存竞争 -> 遗传和变异 -> 适者生存 2、用进废退学说与自然选择理论 用进废退&#xff1a;一步适应&#xff1a;变异 适应 自然选择&#xff1a;两步适应&#xff1a;变异 选择 适应 3、木村资生的中性…...

如何挑到高质量的静态IP代理?

在数字化时代&#xff0c;静态住宅IP代理已成为网络活动中不可或缺的一部分。无论是数据采集、网站访问&#xff0c;还是其他需要隐藏真实IP地址的在线活动&#xff0c;高质量的静态住宅IP代理都发挥着至关重要的作用。今天IPIDEA代理IP将详细介绍如何获取高质量的静态住宅IP代…...

vagrant putty错误的解决

使用Vagrant projects for Oracle products and other examples 新创建的虚机&#xff0c;例如vagrant-projects/OracleLinux/8。 用vagrant ssh可以登录&#xff1a; $ vagrant ssh > vagrant: Getting Proxy Configuration from Host...Welcome to Oracle Linux Server …...

图像分割——U-Net论文介绍+代码(PyTorch)

0、概要 原理大致介绍了一下&#xff0c;后续会不断精进改的更加详细&#xff0c;然后就是代码可以对自己的数据集进行一个训练&#xff0c;还会不断完善&#xff0c;相应其他代码可以私信我。 一、论文内容总结 摘要&#xff1a;人们普遍认为&#xff0c;深度网络成功需要数…...

C#进阶-ASP.NET的WebService跨域CORS问题解决方案

在现代的Web应用程序开发中&#xff0c;跨域资源共享&#xff08;Cross-Origin Resource Sharing, CORS&#xff09;问题是开发者经常遇到的一个挑战。特别是当前端和后端服务部署在不同的域名或端口时&#xff0c;CORS问题就会显得尤为突出。在这篇博客中&#xff0c;我们将深…...

如何利用TikTok矩阵源码实现自动定时发布和高效多账号管理

在如今社交媒体的盛行下&#xff0c;TikTok已成为全球范围内最受欢迎的短视频平台之一。对于那些希望提高效率的内容创作者而言&#xff0c;手动发布和管理多个TikTok账号可能会是一项繁琐且耗时的任务。幸运的是&#xff0c;通过利用TikTok矩阵源码&#xff0c;我们可以实现自…...

Java高级编程技术详解:从多线程到算法优化的全面指南

复杂度与优化 复杂度与优化在算法中的应用 算法复杂度是衡量算法效率的重要指标。了解和优化算法复杂度对提升程序性能非常关键。本文将介绍时间复杂度和空间复杂度的基本概念&#xff0c;并探讨一些优化技术。 时间复杂度和空间复杂度 时间复杂度表示算法执行所需时间随输…...

Redis 分布式锁过期了,还没处理完怎么办?

为了防止死锁&#xff0c;我们会给分布式锁加一个过期时间&#xff0c;但是万一这个时间到了&#xff0c;我们业务逻辑还没处理完&#xff0c;怎么办&#xff1f; 这是一个分布式应用里很常见到的需求&#xff0c;关于这个问题&#xff0c;有经验的程序员会怎么处理呢&#xff…...

Vue2+Element-ui后台系统常用js方法

el-dialog弹框关闭清空form表单并清空验证 cancelDialog(diaLog, formRef) {this[diaLog] falseif (formRef) {this.$refs[formRef].resetFields()} }页面使用&#xff1a; <el-dialog :visible.sync"addSubsidyDialog.dialog" close"cancelDialog(addSub…...

Kafka高频面试题整理

文章目录 1、什么是Kafka?2、kafka基本概念3、工作流程4、Kafka的数据模型与消息存储机制1)索引文件2)数据文件 5、ACKS 机制6、生产者重试机制:7、kafka是pull还是push8、kafka高性能高吞吐的原因1&#xff09;磁盘顺序读写&#xff1a;保证了消息的堆积2&#xff09;零拷贝机…...

uniapp地图自定义文字和图标

这是我的结构&#xff1a; <map classmap id"map" :latitude"latitude" :longitude"longitude" markertap"handleMarkerClick" :show-location"true" :markers"covers" /> 记住别忘了在data中定义变量…...

网站建设与管理相关工作岗位/seo基础入门

原文地址为&#xff1a; Python MySQLdb 学习总结任何应用都离不开数据&#xff0c;所以在学习python的时候&#xff0c;当然也要学习一个如何用python操作数据库了。MySQLdb就是python对mysql数据库操作的模块。官方Introduction : MySQLdb is an thread-compatible interface…...

网上拿手工做的网站/淘宝seo优化排名

作业要求&#xff1a; 做一个MP3文件播放器。具体要实现的是程序能够打开MP3文件&#xff0c;并可以播放这个文件。控制台程序或是gui程序都可以。gui是Graphical User Interface的简写&#xff0c;简单理解就是窗口的意思。思路&#xff1a;1、程序要知道文件的路径&#xff0…...

大连工业大学专升本/seo网站关键词优化快速官网

第一篇稿子&#xff0c;也是激励自己的。...

景观建设网站/百度seo关键词怎么做

目录消息方法消息处理消息宏消息机制回调函数一个好的验证系统应该具有的消息管理特性&#xff1a; 通过一种标准化的方式打印信息&#xff1b;过滤&#xff08;重要级别&#xff09;信息&#xff1b;打印通道。 这些特性在UVM中均有支持。UVM提供了一系列丰富的类和方法来生…...

wordpress主题修改软件/b站推广网站入口mmm

以太网转串口是工控领域最常见的智能通信模块&#xff0c;有的是一网口转1串口&#xff0c;有的是一网口转4串口&#xff0c;最多的可以达到一转16串口&#xff08;好像有的最多可以支持32串口&#xff09;。如果该类模块做的足够完善&#xff0c;可以提供一个windows系统的设备…...

建设与管理局网站/sem竞价代运营

1.常用事件 onload&#xff1a;当页面中的所有的标签&#xff0c;文档&#xff0c;图片等资源加载完毕后会触发onload事件      onclick&#xff1a;鼠标单击事件      ondblclick&#xff1a;鼠标双击事件      onmousedown&#xff1a;鼠标按下事件    …...