大部分内容来自 华为云社区 砖业洋 的文章 https://bbs.huaweicloud.com/blogs/399722 网络上好多文章都过时了,他的这个时间比较近写的很好。 --------------------------------------------------类路径-------------------------------------------------- 上面我们说到类路径,什么是类路径? resources目录就是类路径(classpath)的一部分。所以当我们说"类路径下"的时候,实际上也包含了"resources“目录。JVM在运行时,会把”src/main/resources"目录下的所有文件和文件夹都添加到类路径中。 例如有一个XML文件位于"src/main/resources/config/some-context.xml",那么可以用以下方式来引用它: @Configuration @ImportResource("classpath:config/some-context.xml") public class AppConfig { //... } 这里可以描述为在类路径下的’config‘目录中查找’some-context.xml'文件。 为什么说JVM在运行时,会把"src/main/resources"目录下的所有文件和文件夹都添加到类路径中? 当你编译并运行一个Java项目时,JVM需要知道去哪里查找.class文件以及其他资源文件。这个查找的位置就是所谓的类路径(Classpath)。类路径可以包含文件系统上的目录,也可以包含jar文件。简单的说,类路径就是JVM查找类和资源的地方。 在一个标准的Maven项目结构中,Java源代码通常在src/main/java目录下,而像是配置文件、图片、静态网页等资源文件则放在src/main/resources目录下。 当你构建项目时,Maven(或者其他的构建工具,如Gradle)会把src/main/java目录下的.java文件编译成.class文件,并把它们和src/main/resources目录下的资源文件一起复制到项目的输出目录(通常是target/classes目录)。 然后当你运行程序时,JVM会把target/classes目录(即编译后的src/main/java和src/main/resources)添加到类路径中,这样JVM就可以找到程序运行所需的类和资源了。 如果有一个名为application.properties的文件在src/main/resources目录下,就可以使用类路径来访问它,就像这样:classpath:application.properties。在这里classpath:前缀告诉JVM这个路径是相对于类路径的,所以它会在类路径中查找application.properties文件。因为src/main/resources在运行时被添加到了类路径,所以JVM能找到这个文件。 ----------------------------------------依赖查找和注入------------------------------------------------------------------ 我们可以调用context.getBean()方法来查找获取到bean,Spring在进行依赖注入前也需要进行隐性的依赖查找获取bean。 使用context.getBean("beanName"),是按名称进行依赖查找。返回类型需要强转。。。 使用context.getBean(ClassType)是按类型进行依赖查找。如果有多个同类型的bean,则会抛出异常。但返回类型不需要强转。。。 使用context.getBean("beanName",ClassType)完美。。。 依赖注入中的按名称和按类型两种方式,主要体现在注入时如何选择合适的bean进行注入。 按名称进行依赖注入: 是指在进行依赖注入时,根据名称来查找合适的bean。这种方式的优点是明确指定了注入的bean。 按类型进行依赖注入: 是指在进行依赖注入时,根据类型来查找合适的bean。缺点是当有多个相同类型的bean存在时,可能会导致选择错误的bean。 注解方式依赖注入 @Resource 默认按name查找bean。 注解写在字段上时,name取字段名。 注解写在setter方法上时,name取属性名。 当找不到与name匹配的bean时才按照类型进行装配。 也可以指定type属性按类型查找。 @Autowired 只能按类型查找bean。 加上@Qualifier 注解对同一类型的不同实例进行精确选择。 如果在容器中存在多个同类型的Bean,Spring会优先注入被@Primary注解标记的Bean。 @Inject 默认按类型查找bean。 如果需要按名称进行装配,则需要配合@Named。 @Value 注解对简单类型的值进行注入。 XML配置方式已经过时了,知道ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");就行。 BeanA依赖于BeanB。在这种情况下,当你尝试获取BeanA的实例时,Spring会首先创建BeanB的实例,然后把这个实例注入到BeanA中,最后创建BeanA的实例。在这个例子中,BeanB会先于BeanA被创建。只有当一个Bean的所有依赖都已经被创建并注入后,这个Bean才能被创建。这就是Spring框架的IoC(控制反转)和DI(依赖注入)的机制。 ----------------------------------------ApplicationContext------------------------------------------------------------------ ApplicationContext context = new AnnotationConfigApplicationContext(LibraryConfiguration.class)这个语句创建了一个Spring的应用上下文,它是以配置类LibraryConfiguration.class作为输入的,这里明确指定配置类的Spring应用上下文,适用于更一般的Spring环境。对比一下ApplicationContext context = SpringApplication.run(DemoApplication.class, args);这个语句则是Spring Boot应用的入口,启动一个Spring Boot应用。SpringApplication.run()方法会创建一个Spring Boot应用上下文(也就是一个SpringApplication对象),这个上下文包含了Spring Boot应用所有的Bean和配置类,还有大量的默认配置。这个方法之后,Spring Boot的自动配置就会起作用。你可以把SpringApplication.run()创建的Spring Boot上下文看作是更加功能丰富的Spring上下文。 @SpringBootApplication是一个复合注解,它等效于同时使用了@Configuration,@EnableAutoConfiguration和@ComponentScan。这三个注解的作用是: @Configuration:指明该类是一个配置类,它可能会有零个或多个@Bean注解,方法产生的实例由Spring容器管理。 @EnableAutoConfiguration:告诉Spring Boot根据添加的jar依赖自动配置你的Spring应用。 @ComponentScan:Spring Boot会自动扫描该类所在的包以及子包,查找所有的Spring组件,包括@Configuration类。 在非Spring Boot的传统Spring应用中,我们通常使用AnnotationConfigApplicationContext或者ClassPathXmlApplicationContext等来手动创建和初始化Spring的IOC容器。 Spring IoC容器:为了降低类初始化依赖配置的复杂度,减少他们之间的耦合,这个时候我们就需要用上Spring容器了。程序里的对象都可以扔到容器里,对象只需要告诉容器它需要哪些依赖就行,容器自动初始化好并给它。比如上面ABCD那个例子,C告诉容器它需要A和B,B告诉容器它需要D,这样ABCD四个对象都可以扔到容器里了,当你想用C的时候,就从容器里面拿出来,这时候C的成员就已经包含了A和B了(我们目前只讨论单例的情况,就是一个类只有一个对象)。 容器通过“配置”来了解对象之间的依赖关系。 在Spring框架里,ApplicationContext就代表了容器(又叫应用程序上下文),容器里的对象,又叫Bean。配置有两种方式,一种是xml,一种是Java配置(或者说代码配置)。早些年的时候,Java Web(SSH)开发为人诟病的,就是臃肿的xml配置,虽然目前Spring仍然支持xml配置和混合配置,不过Spring Boot已经建议使用Java配置了。 ---------------------------------------属性注入---------------------------------------------------------------- 在 Spring 应用中使用 @PropertySource 注解来加载一个 .properties 文件时,这个文件中的所有配置项都会被读取,并存储在一个内部的 Map 结构中。这个 Map 的键是配置项的名称,值是配置项的值。Spring 中的一些内置配置项也会被添加到这个 Map 中。 当我们使用 ${...}` 占位符语法来引用一个配置项时,`Spring` 会查找这个 `Map`,取出与占位符名称相应的配置项的值。例如有一个配置项 `blue.title=blue-value-properties`,我们可以在代码中使用 `${blue.title} 占位符来引用这个配置项的值。 如果想通过 Environment 类的方法来获取属性值,可以像下面这样做: @Component public class SomeComponent { @Autowired private Environment env; public void someMethod() { String title = env.getProperty("blue.title"); int rank = Integer.parseInt(env.getProperty("blue.rank")); // ... } } 在上述代码中,Environment 类的 getProperty 方法用于获取属性值。注意,getProperty 方法返回的是 String,所以如果属性是非字符串类型(如 int),则需要将获取的属性值转换为适当的类型。 注意:@PropertySource 无法加载 YAML 格式的文件,只能加载 properties 格式的文件。如果需要加载 YAML 格式的文件,而且使用的是 Spring Boot框架,那么可以使用@ConfigurationProperties或@Value注解。例如以下的YAML文件: application.yml appTest: name: MyApp version: 1.0.0 可以使用@ConfigurationProperties来加载这些属性: @Configuration @ConfigurationProperties(prefix = "appTest") public class AppConfig { private String name; private String version; // getters and setters... } @ConfigurationProperties注解主要用于指定配置属性的前缀,@ConfigurationProperties注解本身并不直接指定配置文件的位置, 而是由Spring Boot的自动配置机制处理的。 这样,name字段就会被自动绑定到appTest.name配置属性,version字段就会被自动绑定到appTest.version配置属性。 默认情况下,Spring Boot会在启动时自动加载src/main/resources目录下的application.properties或application.yml文件。我们可以通过设置spring.config.name和spring.config.location属性来改变默认的配置文件名或位置。 注意:@ConfigurationProperties注解需要配合@EnableConfigurationProperties注解或@Configuration注解使用,以确保Spring能够发现并处理这些注解。 或者,你也可以使用@Value注解来加载这些属性: @Component public class AppConfig { @Value("${appTest.name}") private String name; @Value("${appTest.version}") private String version; // getters and setters... } Blue类的属性注入 对于properties类型的属性,我们这里选择@Value注解和占位符来注入属性: @Value("${blue.title}") private String title; @Value("${blue.rank}") private Integer rank; 如果你熟悉jsp的el表达式,会发现这和它非常相似! 从Spring 4.3开始,如果类只有一个构造方法,那么Spring将会自动把这个构造方法当作是我们希望进行自动装配的构造方法,无需显式地添加@Autowired或@inject注解。如果类有多个构造方法,并且没有在任何构造方法上使用@Autowired或@inject注解,那么Spring将会使用无参数的构造方法(如果存在的话)来创建这个类的实例。Spring会尝试在已经创建的bean中寻找能够满足构造器参数要求的bean,并自动将这些bean注入到构造方法中,这就是所谓的自动装配。 ------------------------------------------------------SpEL表达式----------------------------------------------------- 当我们谈到属性注入的时候,我们可能会遇到一些复杂的需求,例如我们需要引用另一个Bean的属性,或者我们需要动态处理某个属性值。这种需求无法通过使用${}的占位符方式实现,我们需要一个更强大的工具:SpEL表达式。 Spring Expression Language(SpEL)是从Spring框架 3.0开始支持的强大工具。SpEL不仅是Spring框架的重要组成部分,也可以独立使用。它的功能丰富,包括调用属性值、属性参数、方法调用、数组存储以及逻辑计算等。它与开源项目OGNL(Object-Graph Navigation Language)相似,但SpEL是Spring框架推出的,并默认内嵌在Spring框架中。 SpEL的表达式用#{}表示,花括号中就是我们要编写的表达式。 我们创建一个Bean,命名为Azure,同样地,我们声明属性name和priority,并提供getter和setter方法以及toString()方法。然后我们使用@Component注解标注它。 使用@Value配合SpEL完成属性注入,如下: @Component public class Azure { @Value("#{'spel-for-azure'}") private String name; @Value("#{10}") private Integer priority; } SpEL的功能远不止这些,它还可以获取IOC容器中其他Bean的属性,让我们来展示一下。 我们已经注册了Azure Bean,现在我们再创建一个Bean,命名为Emerald。我们按照上述方法对字段和方法进行声明,然后使用@Component注解标注。 我们希望name属性直接复制Azure的name属性,而priority属性则希望比Azure的priority属性大1,我们可以这样编写: @Component public class Emerald { @Value("#{'copy of ' + azure.name}") private String name; @Value("#{azure.priority + 1}") private Integer priority; } 在Spring的SpEL中可以通过bean的名称访问到对应的bean,并通过.操作符访问bean的属性。在这个例子中,azure就是一个bean的名称,它对应的bean就是Azure类的实例。所以,azure.name就是访问Azure类实例的name属性。 如果你在一个不涉及Spring的环境中使用SpEL,这个特性是不会生效的。这是因为这个特性依赖于Spring的IoC容器。 SpEL表达式不仅可以引用对象的属性,还可以直接引用类的常量,以及调用对象的方法。下面我们通过示例进行演示。 我们新建一个Bean,命名为Ivory。我们按照上述方法初始化属性、toString()方法、注解。 假设我们有一个需求,让name取azure属性的前3个字符,priority取Integer的最大值。那么我们可以使用SpEL这样写: @Component public class Ivory { @Value("#{azure.name.substring(0, 3)}") private String name; @Value("#{T(java.lang.Integer).MAX_VALUE}") private Integer priority; } 注意,直接引用类的属性,需要在类的全限定名外面使用T()包围。 注意:在XML中使用SpEL需要使用#{},而不是${}。 -------------------------------------------------Spring的内置作用域------------------------------------------------- 我们来看看Spring内置的作用域类型。在5.x版本中,Spring内置了六种作用域: singleton:在IOC容器中,对应的Bean只有一个实例,所有对它的引用都指向同一个对象。这种作用域非常适合对于无状态的Bean,比如工具类或服务类。 prototype:每次请求都会创建一个新的Bean实例,适合对于需要维护状态的Bean。 request:在Web应用中,为每个HTTP请求创建一个Bean实例。适合在一个请求中需要维护状态的场景,如跟踪用户行为信息。 session:在Web应用中,为每个HTTP会话创建一个Bean实例。适合需要在多个请求之间维护状态的场景,如用户会话。 application:在整个Web应用期间,创建一个Bean实例。适合存储全局的配置数据等。 websocket:在每个WebSocket会话中创建一个Bean实例。适合WebSocket通信场景。 我们需要重点学习两种作用域:singleton和prototype。在大多数情况下singleton和prototype这两种作用域已经足够满足需求。 Singleton是Spring的默认作用域。 在prototype作用域中,Spring容器会为每个请求创建一个新的bean实例。@Scope(BeanDefinition.SCOPE_PROTOTYPE)可以写成@Scope("prototype") -------------------------------------------------理解Bean的生命周期------------------------------------------------- 生命周期的各个阶段 在Spring IOC容器中,Bean的生命周期大致如下: 实例化:当启动Spring应用时,IOC容器就会为在配置文件中声明的每个创建一个实例。 属性赋值:实例化后,Spring就通过反射机制给Bean的属性赋值。 调用初始化方法:如果Bean配置了初始化方法,Spring就会调用它。初始化方法是在Bean创建并赋值之后调用,可以在这个方法里面写一些业务处理代码或者做一些初始化的工作。 Bean运行期:此时,Bean已经准备好被程序使用了,它已经被初始化并赋值完成。 应用程序关闭:当关闭IOC容器时,Spring会处理配置了销毁方法的Bean。 调用销毁方法:如果Bean配置了销毁方法,Spring会在所有Bean都已经使用完毕,且IOC容器关闭之前调用它,可以在销毁方法里面做一些资源释放的工作,比如关闭连接、清理缓存等。 这就是Spring IOC容器管理Bean的生命周期,帮助我们管理对象的创建和销毁,以及在适当的时机做适当的事情。 我们可以将生命周期的触发称为回调,因为生命周期的方法是我们自己定义的,但方法的调用是由框架内部帮我们完成的,所以可以称之为“回调”。 @Bean(initMethod = "init", destroyMethod = "destroy") 在Spring框架中配置Bean的初始化和销毁方法时,需要按照Spring的规范来配置这些方法,否则Spring可能无法正确地调用它们。下面给每个特性提供一个解释和示例: 方法的访问权限无限制:无论方法是public、protected、private还是default,Spring通过反射来调用这些方法,所以它可以忽略Java的访问权限限制。 方法没有参数:由于Spring不知道需要传递什么参数给这些方法,所以这些方法不能有参数。 方法没有返回值:由于返回的值对Spring来说没有意义,所以这些方法不应该有返回值。示例: 方法可以抛出异常:如果在初始化或销毁过程中发生错误,这些方法可以抛出异常来通知Spring。示例: 方法不应是静态的:由于Spring需要一个Bean实例来调用初始化或销毁方法,静态方法属于类级别,不依赖于实例。如果标注在一个静态方法上,就失去了作用于实例生命周期的意义。 在JSR250规范中,有两个与Bean生命周期相关的注解,即@PostConstruct和@PreDestroy。这两个注解对应了Bean的初始化和销毁阶段。@PostConstruct注解标记的方法会在bean属性设置完毕后(即完成依赖注入),但在bean对外暴露(即可以被其他bean引用)之前被调用,这个时机通常用于完成一些初始化工作。@PreDestroy注解标记的方法会在Spring容器销毁bean之前调用,这通常用于释放资源。@PostConstruct和@PreDestroy可用于任何Java类,初始化和销毁方法与init-method和destroy-method类似,但也有一定的区别。不能被final修饰,如果使用final修饰这两个注解的方法,在编译时不会报错,可以正常编译。但是在运行时,Spring容器在解析和设置注解时,会尝试使用CGLIB或JDK动态代理生成子类,由于方法被final修饰,子类无法覆盖该方法,所以Spring容器会抛出异常,表示无法为生命周期方法生成代理,这会导致标注了final的生命周期方法无法被Spring调用。init-method和destroy-method方法被final修饰也无影响,因为Spring通过反射机制来调用init-method和destroy-method,不需要生成代理子类,并没有试图覆盖这些方法。不过生命周期方法都不被建议设计为final的,这需要注意。 InitializingBean和DisposableBean这两个接口是 Spring 预定义的两个关于生命周期的接口。他们被触发的时机与上文中的 init-method / destroy-method 以及 JSR250 规范的注解相同,都是在 Bean 的初始化和销毁阶段回调的。InitializingBean接口只有一个方法:afterPropertiesSet()。在Spring框架中,当一个bean的所有属性都已经被设置完毕后,这个方法就会被调用。也就是说,这个bean一旦被初始化,Spring就会调用这个方法。我们可以在bean的所有属性被设置后,进行一些自定义的初始化工作。DisposableBean接口也只有一个方法:destroy()。当Spring容器关闭并销毁bean时,这个方法就会被调用。我们可以在bean被销毁前,进行一些清理工作。 三种方法执行顺序:初始化顺序:@PostConstruct → InitializingBean → init-method。销毁顺序:@PreDestroy → DisposableBean → destroy-method 原型Bean不会随着IOC容器的启动而初始化。当我们明确请求一个实例时,我们会看到所有的初始化方法按照预定的顺序执行,这个顺序跟单例Bean完全一致。 将原型Bean和单例Bean进行对比后发现,调用IOC容器的destroyBean()方法销毁原型Bean时,只有@PreDestroy注解和DisposableBean接口的destroy方法会被触发,而被destroy-method标记的自定义销毁方法并不会被执行。从这里我们可以得出结论:在销毁原型Bean时,Spring不会执行由destroy-method标记的自定义销毁方法,所以原型Bean的destroy-method的也有局限性。如果有重要的清理逻辑需要在Bean销毁时执行,那么应该将这部分逻辑放在@PreDestroy注解的方法或DisposableBean接口的destroy方法中。 ----------------------------------------事件机制Event与监听器Listener------------------------------------------------------------------ 1. Spring中的观察者模式 观察者模式是一种行为设计模式,它定义了对象之间的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都会得到通知并被自动更新。在这个模式中,改变状态的对象被称为主题,依赖的对象被称为观察者。 举个实际的例子: 事件源(Event Source):可以视为“主题(Subject)”,当其状态发生变化时(比如播放新的内容),会通知所有的观察者。想象我们正在听广播,广播电台就是一个事件源,它提供了大量的新闻、音乐和其他内容。 事件(Event):这是主题状态改变的具体表示,对应到广播例子中,就是新闻、音乐和其他内容。每当电台播放新的内容时,就相当于一个新的事件被发布了。 广播器(Event Publisher / Multicaster):广播器起到的是中介的作用,它将事件从事件源传递到监听器。在这个例子中,广播塔就充当了这个角色,它将电台的节目的无线电信号发送到空气中,以便无线电接收器(监听器)可以接收。 监听器(Listener):监听器就是“观察者”,它们监听并响应特定的事件。在例子中,无线电接收器就是监听器,它接收广播塔发出的信号,然后播放电台的内容。 在Spring中,事件模型的工作方式也是类似的: 当Spring应用程序中发生某个行为时(比如一个用户完成了注册),那么产生这个行为的组件(比如用户服务)就会创建一个事件,并将它发布出去。 事件一旦被发布,Spring的ApplicationContext就会作为广播器,把这个事件发送给所有注册的监听器。 各个监听器接收到事件后,就会根据事件的类型和内容,进行相应的处理(比如发送欢迎邮件,赠送新用户优惠券等)。 这就是Spring事件模型的工作原理,它实现了事件源、广播器和监听器之间的解耦,使得事件的生产者和消费者可以独立地进行开发和修改,增强了程序的灵活性和可维护性。 2. 监听器 2.1 实现ApplicationListener接口创建监听器 首先,我们创建一个监听器。在Spring框架中,内置的监听器接口是ApplicationListener,这个接口带有一个泛型参数,代表要监听的具体事件。我们可以通过实现这个接口来创建自定义的监听器。 2.2 @EventListener注解创建监听器 除了通过实现ApplicationListener接口来创建监听器,我们还可以通过注解来创建。Spring提供了@EventListener注解,我们可以在任何一个方法上使用这个注解来指定这个方法应该在收到某种事件时被调用。 3. Spring的事件机制 在 Spring 中,事件(Event)和监听器(Listener)是两个核心概念,它们共同构成了 Spring 的事件机制。这一机制使得在 Spring 应用中,组件之间可以通过发布和监听事件来进行解耦的交互。 在 Spring中有4个默认的内置事件 ApplicationEvent ApplicationContextEvent ContextRefreshedEvent 和 ContextClosedEvent ContextStartedEvent 和 ContextStoppedEvent 我们分别来看一下 3.1 ApplicationEvent 在 Spring 中,ApplicationEvent 是所有事件模型的抽象基类,它继承自 Java 原生的 EventObject,ApplicationEvent 本身是一个抽象类,它并未定义特殊的方法或属性,只包含事件发生时的时间戳,这意味着我们可以通过继承ApplicationEvent来创建自定义的事件。 3.2 ApplicationContextEvent ApplicationContextEvent 是 ApplicationEvent 的子类,它代表了与 Spring 应用上下文(ApplicationContext)有关的事件。这个抽象类在构造方法中接收一个 ApplicationContext 对象作为事件源(source)。这意味着在事件触发时,我们可以通过事件对象直接获取到发生事件的应用上下文,而不需要进行额外的操作。 Spring 内置了一些事件,如 ContextRefreshedEvent 和 ContextClosedEvent,这些都是 ApplicationContextEvent 的子类。ApplicationContextEvent 是 ApplicationEvent 的子类,专门用来表示与Spring应用上下文相关的事件。虽然 ApplicationContextEvent 是一个抽象类,但在实际使用时,通常会使用其具体子类,如 ContextRefreshedEvent 和 ContextClosedEvent,而不是直接使用 ApplicationContextEvent。另外,虽然我们可以创建自定义的 ApplicationContextEvent 子类或 ApplicationEvent 子类来表示特定的事件,但这种情况比较少见,因为大多数情况下,Spring内置的事件类已经能满足我们的需求。 3.3 ContextRefreshedEvent 和 ContextClosedEvent ContextRefreshedEvent 事件在 Spring 容器初始化或者刷新时触发,此时所有的 Bean 都已经被完全加载,且 post-processor 也已经被调用,但此时容器尚未开始接收请求。 ContextClosedEvent 事件在 Spring 容器关闭时触发,此时容器尚未销毁所有 Bean。当接收到这个事件后可以做一些清理工作。 这里我们再次演示实现接口来创建监听器,不过和2.3节不同,我们只创建的1个类,同时处理ContextRefreshedEvent 和ContextClosedEvent事件。这里实现ApplicationListener接口,泛型参数使用ContextRefreshedEvent 和 ContextClosedEvent的父类ApplicationEvent。然后在onApplicationEvent方法中,我们检查事件的类型,并根据事件的类型执行相应的操作。这样我们就可以在同一个监听器中处理多种类型的事件了。 3.4 ContextStartedEvent 和 ContextStoppedEvent ContextStartedEvent:这是Spring应用上下文的启动事件。当调用实现了 Lifecycle 接口的 Bean 的 start 方法时,Spring会发布这个事件。这个事件的发布标志着Spring应用上下文已经启动完成,所有的Bean都已经被初始化并准备好接收请求。我们可以监听这个事件来在应用上下文启动后执行一些自定义逻辑,比如开启一个新线程,或者连接到一个远程服务等。 ContextStoppedEvent:这是Spring应用上下文的停止事件。当调用实现了 Lifecycle 接口的 Bean 的 stop 方法时,Spring会发布这个事件。这个事件的发布标志着Spring应用上下文开始停止的过程,此时Spring将停止接收新的请求,并开始销毁所有的Singleton Bean。我们可以监听这个事件来在应用上下文停止前执行一些清理逻辑,比如关闭数据库连接,释放资源等。 有人可能会疑问了,实现了 Lifecycle 接口的 Bean 的 start 方法和stop方法是什么?这与@PostConstruct 和 @PreDestroy有什么区别? Lifecycle 接口有start个stop这2个方法,start() 方法将在所有 Bean 都已被初始化后,整个应用上下文启动时被调用,而 stop() 方法将在应用上下文关闭,但是在所有单例 Bean 被销毁之前被调用。这意味着这些方法将影响整个应用上下文的生命周期。 总的来说,@PostConstruct 和 @PreDestroy 主要关注单个 Bean 的生命周期,而 Lifecycle 接口则关注整个应用上下文的生命周期。 AnnotationConfigApplicationContext 继承自 GenericApplicationContext,GenericApplicationContext继承自 AbstractApplicationContext抽象类。AbstractApplicationContext 类实现了 ConfigurableApplicationContext 接口,这个ConfigurableApplicationContext接口继承了 Lifecycle 接口。 所以,从类的继承层次上来看,AnnotationConfigApplicationContext 是间接实现了 Lifecycle 接口的。当我们在 AnnotationConfigApplicationContext 的对象上调用 start() 或 stop() 方法时,它就会触发相应的 ContextStartedEvent 或 ContextStoppedEvent 事件。 在实际的Spring应用中,很少需要手动调用start()和stop()方法。这是因为Spring框架已经为我们处理了大部分的生命周期控制,比如bean的创建和销毁,容器的初始化和关闭等。 4. 自定义事件开发 4.1 注解式监听器和接口式监听器对比触发时机 需求背景:假设正在开发一个论坛应用,当新用户成功注册后,系统需要进行一系列的操作。这些操作包括: 向用户发送一条确认短信; 向用户的电子邮箱发送一封确认邮件; 向用户的站内消息中心发送一条确认信息; 为了实现这些操作,我们可以利用Spring的事件驱动模型。具体来说,当用户注册成功后,我们可以发布一个“用户注册成功”的事件。这个事件将包含新注册用户的信息。 然后,我们可以创建多个监听器来监听这个“用户注册成功”的事件。这些监听器分别对应于上述的三个操作。当监听器监听到“用户注册成功”的事件后,它们将根据事件中的用户信息,执行各自的操作。 前一篇生命周期的顺序中,我们提到了初始化Bean的时候属性赋值、@PostConstruct注解、实现InitializingBean接口后的afterPropertiesSet方法和init-method指定的方法。那这里注解式监听器的顺序和这些生命周期的顺序又有什么关系呢? 对于监听器: 使用@EventListener注解的监听器:当应用程序的ApplicationContext被刷新后,这类监听器就会被触发。这个"刷新"指的是所有的Bean定义都已被加载,自动装配完成,即已经执行了构造函数和setter方法。但此时,初始化回调(如@PostConstruct,InitializingBean接口的afterPropertiesSet方法或定义的init-method)还未开始执行。 我们可以将此简化为:@EventListener注解的监听器在所有Bean已被加载和自动装配,但初始化回调还未执行时,开始工作。 实现了ApplicationListener接口的监听器:这种类型的监听器首先作为一个Bean,会经历所有常规的Bean生命周期阶段。这包括构造函数的调用,setter方法的调用,以及初始化回调。在所有单例Bean的初始化完成后,即在ContextRefreshedEvent事件被发布后,这些监听器将会被触发。 我们可以将此简化为:实现ApplicationListener接口的监听器在经历完常规Bean生命周期并且所有单例Bean初始化完成之后,开始工作。 这两种监听器的主要区别在于它们开始工作的时间:使用@EventListener注解的监听器在初始化回调之前开始工作,而实现ApplicationListener接口的监听器在所有单例Bean初始化完成之后开始工作。 4.2 @Order注解调整监听器的触发顺序 刚刚的例子中,因为发送短信的监听是接口式的,而注解式监听器的触发时机比接口式监听器早,所以一直在会后才触发。这里我们利用@Order注解来强制改变一下触发顺序。 @Order注解可以用在类或者方法上,它接受一个整数值作为参数,这个参数代表了所注解的类或者方法的“优先级”。数值越小,优先级越高,越早被调用。@Order的数值可以为负数,在int范围之内都可以。 可能是因为版本原因,经过我的测试,如果是注解式创建的监听器,@Order写在类上面会让所有的监听器的顺序控制失效。所以,接口式监听器如果要加@Order就放在类上,注解式监听器的@Order就放在方法上。 对于实现ApplicationListener接口的监听器(即接口式监听器),如果不指定@Order,它的执行顺序通常在所有指定了@Order的监听器之后。这是因为Spring默认给未指定@Order的监听器赋予了LOWEST_PRECEDENCE的优先级。 对于使用@EventListener注解的方法(即注解式监听器),如果不显式指定@Order,那么它的执行顺序就默认指定为@Order(0)。 注意:我们应该减少对事件处理顺序的依赖,以便更好地解耦我们的代码。虽然 @Order 可以指定监听器的执行顺序,但它不能改变事件发布的异步性质。@Order注解只能保证监听器的调用顺序,事件监听器的调用可能会在多个线程中并发执行,这样就无法保证顺序,而且在分布式应用也不适用,无法在多个应用上下文环境保证顺序。 --------------------------------Bean模块装配@Import-------------------------------------------------- 模块装配就是将我们的类或者组件注册到Spring的IoC(Inversion of Control,控制反转)容器中,以便于Spring能够管理这些类,并且在需要的时候能够为我们自动地将它们注入到其他的组件中。 在Spring框架中,有多种方式可以实现模块装配,包括: 基于Java的配置:通过使用@Configuration和@Bean注解在Java代码中定义的Bean。这是一种声明式的方式,我们可以明确地控制Bean的创建过程,也可以使用@Value和@PropertySource等注解来处理配置属性。 基于XML的配置:Spring也支持通过XML配置文件定义Bean,这种方式在早期的Spring版本中更常见,但现在基于Java的配置方式更为主流。 基于注解的组件扫描:通过使用@Component、@Service、@Repository、@Controller等注解以及@ComponentScan来自动检测和注册Bean。这是一种隐式的方式,Spring会自动扫描指定的包来查找带有这些注解的类,并将这些类注册为Bean。默认情况下,使用 @Bean声明一个bean,bean的名称是方法名。此外,可以通过@Bean注解里面的name属性主动设置bean的名称。 使用@Import:这是一种显式的方式,可以通过它直接注册类到IOC容器中,无需这些类带有@Component或其他特殊注解。 每种方式都有其应用场景,根据具体的需求,我们可以选择合适的方式来实现模块装配。比如在Spring Boot中,我们日常开发可能会更多地使用基于Java的配置和基于注解的组件扫描来实现模块装配。 3.1 @Import注解的功能介绍 在Spring中,有时候我们需要将某个类(可能是一个普通类,可能是一个配置类等等)导入到我们的应用程序中。Spring提供了四种主要的方式来完成这个任务,后面我们会分别解释。 @Import注解可以有以下几种使用方式: 导入普通类:可以将普通类(没有被@Component或者@Service等注解标注的类)导入到Spring的IOC容器中,Spring会为这个类创建一个Bean,这个Bean的名字默认为类的全限定类名。 导入配置类:可以将一个或多个配置类(被@Configuration注解标注的类)导入到Spring的IOC容器中,这样我们就可以一次性地将这个配置类中定义的所有Bean导入到Spring的IOC容器中。 使用ImportSelector接口:如果想动态地导入一些Bean到Spring的IOC容器中,那么可以实现ImportSelector接口,然后在@Import注解中引入ImportSelector实现类,这样Spring就会将ImportSelector实现类返回的类导入到Spring的IOC容器中。 使用ImportBeanDefinitionRegistrar接口:如果想在运行时动态地注册一些Bean到Spring的IOC容器中,那么可以实现ImportBeanDefinitionRegistrar接口,然后在@Import注解中引入ImportBeanDefinitionRegistrar实现类,这样Spring就会将ImportBeanDefinitionRegistrar实现类注册的Bean导入到Spring的IOC容器中。 @Import注解主要用于手动装配,它可以让我们显式地导入特定的类或者其他配置类到Spring的IOC容器中。特别是当我们需要引入第三方库中的类,或者我们想要显式地控制哪些类被装配进Spring的IOC容器时,@Import注解会非常有用。它不仅可以直接导入普通的 Java 类并将其注册为 Bean,还可以导入实现了 ImportSelector 或 ImportBeanDefinitionRegistrar 接口的类。这两个接口提供了更多的灵活性和控制力,使得我们可以在运行时动态地注册 Bean,这是通过 @Configuration + @Bean 注解组合无法做到的。 例如,通过 ImportSelector 接口,可以在运行时决定需要导入哪些类。而通过 ImportBeanDefinitionRegistrar 接口,可以在运行时控制 Bean 的定义,包括 Bean 的名称、作用域、构造参数等等。 虽然 @Configuration + @Bean 在许多情况下都足够使用,但 @Import 注解由于其更大的灵活性和控制力,在处理更复杂的场景时,可能会是一个更好的选择。 创建Import自定义注解 @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @Documented @Import({Librarian.class, Book.class, BookShelf.class}) public @interface ImportLibrary { } 这个@ImportLibrary自定义注解内部实际上使用了@Import注解。当Spring处理@Import注解时,会将其参数指定的类添加到Spring应用上下文中。当我们在Library类上使用@ImportLibrary注解时,Spring会将Librarian.class、Book.class和BookShelf.class这三个类添加到应用上下文中。 3.3 导入配置类的策略 这里使用Spring的 @Import注解导入配置类,我们将创建一个BookConfig类和LibraryConfig类,然后在主应用类中获取Book实例。 3.4 使用ImportSelector进行选择性装配 使用Class.getName()方法获取全限定类名的方式,比直接硬编码类的全名为字符串更推荐,原因如下: 避免错误:如果类名或包名有所改动,硬编码的字符串可能不会跟随变动,这可能导致错误。而使用Class.getName()方法,则会随类的改动自动更新,避免此类错误。 代码清晰:使用Class.getName()能让读代码的人更清楚地知道你是要引用哪一个类。 增强代码的可读性和可维护性:使用类的字节码获取全限定类名,使得代码阅读者可以清晰地知道这是什么类,增加了代码的可读性。同时,也方便了代码的维护,因为在修改类名或者包名时,不需要手动去修改硬编码的类名。 在 Spring Boot 中,ImportSelector 被大量使用,尤其在自动配置(auto-configuration)机制中起着关键作用。例如,AutoConfigurationImportSelector 类就是间接实现了 ImportSelector,用于自动导入所有 Spring Boot 的自动配置类。 我们通常会在Spring Boot启动类上使用 @SpringBootApplication 注解,实际上,@SpringBootApplication 注解中也包含了 @EnableAutoConfiguration,@EnableAutoConfiguration 是一个复合注解,它的实现中导入了普通类 @Import(AutoConfigurationImportSelector.class),AutoConfigurationImportSelector 类间接实现了 ImportSelector接口,用于自动导入所有 Spring Boot 的自动配置类。 3.5 使用ImportBeanDefinitionRegistrar进行动态装配 Spring Boot就广泛地使用了ImportBeanDefinitionRegistrar。例如,它的@EnableConfigurationProperties注解就是通过使用一个ImportBeanDefinitionRegistrar来将配置属性绑定到Beans上的,这就是ImportBeanDefinitionRegistrar在实践中的一个实际应用的例子。 ImportBeanDefinitionRegistrar接口的主要功能是在运行时动态的往Spring容器中注册Bean,实现该接口的类需要重写registerBeanDefinitions方法,这个方法可以通过参数中的BeanDefinitionRegistry接口向Spring容器注册新的类,给应用提供了更大的灵活性。 下面来详细解释一下接口的registerBeanDefinitions方参数。 AnnotationMetadata importingClassMetadata: 这个参数表示当前被@Import注解导入的类的所有注解信息,它包含了该类上所有注解的详细信息,比如注解的名称,注解的参数等等。 BeanDefinitionRegistry registry: 这个参数是Spring的Bean定义注册类,我们可以通过它往Spring容器中注册Bean。 ----------------------------------------条件装配------------------------------------------------------------------- 1. 条件装配 1.1 理解条件装配及其在Spring中的重要角色 在Spring框架中,条件装配(Conditional Configuration)是一个非常重要的特性,它允许开发者根据满足的条件,动态地进行Bean的注册或是创建。这样就可以根据不同的环境或配置,创建不同的Bean实例,这一特性对于创建可配置和模块化的应用是非常有用的。 Spring提供了一系列的注解来实现条件装配,包括: @Profile:这是 Spring 的注解,这个注解表示只有当特定的Profile被激活时,才创建带有该注解的Bean,我们可以在应用的配置文件中设置激活的Profile。 @Conditional:这是 Spring 的注解,它接受一个或多个Condition类,这些类需要实现Condition接口,并重写其matches方法。只有当所有Condition类的matches方法都返回true时,带有@Conditional注解的Bean才会被创建。 以下的注解是 Spring Boot 提供的,主要用于自动配置功能: @ConditionalOnProperty:这个注解表示只有当一个或多个给定的属性有特定的值时,才创建带有该注解的Bean。 @ConditionalOnClass 和 @ConditionalOnMissingClass:这两个注解表示只有当Classpath中有(或没有)特定的类时,才创建带有该注解的Bean。 @ConditionalOnBean 和 @ConditionalOnMissingBean:这两个注解表示只有当Spring ApplicationContext中有(或没有)特定的Bean时,才创建带有该注解的Bean。 通过组合这些注解,开发者可以实现复杂的条件装配逻辑,灵活地控制Spring应用的配置和行为。 2. @Profile 在 Spring 中,Profile 用于解决在不同环境下对不同配置的需求,它可以按照特定环境的要求来装配应用程序。例如我们可能有一套配置用于开发环境,另一套配置用于生产环境,就可以使用Profile,它可以在运行时决定哪个环境是活动的,进而决定注册哪些bean。 2.1 基于 @Profile 的实际应用场景 举个例子,我们可能需要使用不同的数据库或不同的服务端点。 这里我们可以以数据库配置为例。在开发环境、测试环境和生产环境中数据库可能是不同的,我们可以通过 @Profile 注解来分别配置这些环境的数据库。 @Configuration public class DataSourceConfiguration { @Value("${spring.datasource.username}") private String username; @Value("${spring.datasource.password}") private String password; @Value("${spring.datasource.url}") private String url; @Bean @Profile("dev") public DataSource devDataSource() { return DataSourceBuilder.create() .username(username) .password(password) .url(url + "?useSSL=false&serverTimezone=Asia/Shanghai") .driverClassName("com.mysql.cj.jdbc.Driver") .build(); } @Bean @Profile("test") public DataSource testDataSource() { return DataSourceBuilder.create() .username(username) .password(password) .url(url + "?useSSL=false&serverTimezone=Asia/Shanghai") .driverClassName("com.mysql.cj.jdbc.Driver") .build(); } @Bean @Profile("prod") public DataSource prodDataSource() { return DataSourceBuilder.create() .username(username) .password(password) .url(url + "?useSSL=true&serverTimezone=Asia/Shanghai") .driverClassName("com.mysql.cj.jdbc.Driver") .build(); } } 实际开发中不同的环境有不同的Apollo配置,Apollo上写有数据库连接配置,生产和测试的代码不需要多个Bean,只需要加载不同的Apollo配置建立数据库连接即可。 Apollo是由携程框架部门开源的一款分布式配置中心,它能够集中化管理应用程序的配置信息。Apollo的主要目标是使应用程序可以在运行时动态调整其配置,而无需重新部署。 Apollo和Spring的Profile解决的是同一个问题——如何管理不同环境的配置,但Apollo提供了更强大的功能,特别是在大规模和复杂的分布式系统中。另外,Apollo还支持配置的版本控制、审计日志、权限管理等功能,为配置管理提供了全面的解决方案。 2.2 理解 @Profile 的工作原理和用途 我们来用图书馆开放时间例子,并且使用 Spring Profiles 来控制开放时间的配置。 全部代码如下: 首先,我们需要一个表示开放时间的类 LibraryOpeningHours: package com.example.demo.bean; public class LibraryOpeningHours { private final String openTime; private final String closeTime; public LibraryOpeningHours(String openTime, String closeTime) { this.openTime = openTime; this.closeTime = closeTime; } @Override public String toString() { return "LibraryOpeningHours{" + "openTime='" + openTime + '\'' + ", closeTime='" + closeTime + '\'' + '}'; } } 然后,我们需要一个 Library 类来持有这个开放时间: package com.example.demo.bean; public class Library { private final LibraryOpeningHours openingHours; public Library(LibraryOpeningHours openingHours) { this.openingHours = openingHours; } public void displayOpeningHours() { System.out.println("Library opening hours: " + openingHours); } } 接着,我们需要定义两个不同的配置,分别表示工作日和周末的开放时间: package com.example.demo.configuration; import com.example.demo.bean.Library; import com.example.demo.bean.LibraryOpeningHours; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Profile; @Configuration @Profile("weekday") public class WeekdayLibraryConfiguration { @Bean public LibraryOpeningHours weekdayOpeningHours() { return new LibraryOpeningHours("8:00 AM", "6:00 PM"); } @Bean public Library library(LibraryOpeningHours openingHours) { return new Library(openingHours); } } 工作日开放时间是早上8点晚上6点。 package com.example.demo.configuration; import com.example.demo.bean.Library; import com.example.demo.bean.LibraryOpeningHours; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Profile; @Configuration @Profile("weekend") public class WeekendLibraryConfiguration { @Bean public LibraryOpeningHours weekendOpeningHours() { return new LibraryOpeningHours("10:00 AM", "4:00 PM"); } @Bean public Library library(LibraryOpeningHours openingHours) { return new Library(openingHours); } } 周末开放时间是早上10点,下午4点。 最后我们在主程序中运行,并通过选择不同的 Profile 来改变图书馆的开放时间: package com.example.demo.application; import com.example.demo.bean.Library; import com.example.demo.configuration.WeekdayLibraryConfiguration; import com.example.demo.configuration.WeekendLibraryConfiguration; import org.springframework.context.annotation.AnnotationConfigApplicationContext; public class DemoApplication { public static void main(String[] args) { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); context.getEnvironment().setActiveProfiles("weekday"); context.register(WeekdayLibraryConfiguration.class, WeekendLibraryConfiguration.class); context.refresh(); Library library = context.getBean(Library.class); library.displayOpeningHours(); } } 这里有小伙伴可能会疑问了,为什么setActiveProfiles之后还要有register和refresh方法? setActiveProfiles方法用于指定哪些Profile处于活动状态,而仅仅设置活动的Profile并不会触发Spring容器去实例化相应的bean。 register方法是将配置类注册到Spring的应用上下文中,它并不会立即创建配置类中声明的bean,这些bean的创建过程是在上下文的refresh阶段中进行的。在这个阶段,Spring容器会读取所有的配置类,并对配置类中使用了@Bean注解的方法进行解析,然后才会创建并初始化这些bean。 所以,如果在调用refresh方法之前就尝试去获取配置类中的某个bean,就会找不到,因为那个bean可能还没有被创建。只有在上下文被刷新(也就是调用了refresh方法)之后,所有的bean才会被创建并注册到Spring容器中,然后才能通过上下文获取到这些bean。 注意: register方法、@Component、@Bean、@Import都是注册Bean到Spring容器的方式,它们有不同的适用场景和工作方式: register方法:这个方法用于将一个或多个配置类(即标注了@Configuration注解的类)注册到Spring的ApplicationContext中。这个过程是将配置类的元信息添加到上下文中,但并不会立即实例化Bean。实际的Bean实例化过程会在ApplicationContext刷新时(即调用refresh方法时)发生,而且这个过程可能受到如@Profile, @Conditional等条件装配注解的影响。 @Component:这是一个通用性的注解,可以用来标记任何类作为Spring组件。如果一个被@Component注解的类在Spring的组件扫描路径下,那么Spring会自动创建这个类的Bean并添加到容器中。 @Bean:这个注解通常用在配置类中的方法上。被@Bean注解的方法表示了如何实例化、配置和初始化一个新的Bean对象。Spring IoC容器将会负责在适当的时候(在ApplicationContext刷新阶段)调用这些方法,创建Bean实例。 @Import:这个注解用于在一个配置类中导入其他的配置类。通过使用这个注解,我们可以将Bean的定义分散到多个配置类中,以实现更好的模块化和组织。 在Spring框架中,以上的这些方法和注解共同工作,提供了强大的依赖注入和管理能力,支持我们创建复杂的、模块化的应用。 在Spring框架中,refresh方法被用来启动Spring应用上下文的生命周期,它主导了ApplicationContext中Bean的解析、创建和初始化过程。以下是refresh方法在实际操作中的主要步骤: 读取所有已注册的配置类:refresh方法首先会扫描ApplicationContext中所有已经注册的配置类(通常是标注了@Configuration注解的类)。它会寻找这些配置类中所有被@Bean注解标记的方法。 解析@Bean方法:对于每一个@Bean方法,Spring会根据方法的签名、返回类型、以及可能的其他注解(例如@Scope、@Lazy、@Profile等)来决定如何创建和配置对应的Bean。 Bean的创建和依赖注入:基于解析得到的信息,Spring IoC容器会按需创建Bean的实例。在实例化Bean后,Spring还会处理这个Bean的依赖关系,即它会自动将这个Bean所依赖的其他Bean注入到它的属性或构造函数参数中。 初始化:如果Bean实现了InitializingBean接口或者定义了@PostConstruct注解的方法,那么在所有依赖注入完成后,Spring会调用这些初始化方法。 因此,在调用refresh方法之后,我们可以确信所有在配置类中定义的Bean都已经被正确地解析、创建、并注册到了Spring的ApplicationContext中。这包括了Bean的实例化、依赖注入,以及可能的初始化过程。 上面这些步骤不一定顺序执行,实际上,Spring的IoC容器在处理这些步骤时,可能会进行一些优化和调整。具体的处理顺序可能会受到以下因素的影响: Bean的依赖关系:如果一个Bean A依赖于另一个Bean B,那么Spring需要先创建和初始化Bean B,然后才能创建Bean A。这可能会导致Bean的创建顺序与它们在配置类中定义的顺序不同。 Bean的生命周期和作用域:例如,如果一个Bean是单例的(默认的作用域),那么它通常会在容器启动时就被创建。但如果它是原型的,那么它只有在被需要时才会被创建。 条件注解:像@Profile和@Conditional这样的条件注解也可能影响Bean的创建。如果条件未满足,对应的Bean将不会被创建。 虽然在一般情况下,Spring确实会按照"读取配置类——解析@Bean方法——创建Bean——依赖注入——初始化"这样的步骤来处理Bean的生命周期,但具体的处理过程可能会受到上面提到的各种因素的影响。 2.3 为什么要有@Profile,application不是有各种环境的配置文件吗? application-dev.yml、application-test.yml和 application-prod.yml 这些配置文件可以用于在不同环境下指定不同的配置参数,比如数据库连接字符串、服务地址等等。 相比于application配置文件,@Profile 注解在 Spring 应用中提供了更高级别的环境差异控制,它可以控制整个 Bean 或者配置类,而不仅仅是配置参数。有了 @Profile,我们可以让整个 Bean 或配置类只在特定环境下生效,这意味着我们可以根据环境使用完全不同的 Bean 实现或者不同的配置方式。 举一个例子,考虑一个邮件服务,我们可能在开发环境中使用一个模拟的邮件服务,只是简单地把邮件内容打印出来,而在生产环境中我们可能需要使用一个实际的邮件服务。我们可以创建两个 Bean,一个用 @Profile("dev") 注解,一个用 @Profile("prod") 注解。这样,我们就可以在不改动其它代码的情况下,根据环境选择使用哪个邮件服务。 总的来说,application-{profile}.yml 文件和 @Profile 注解都是 Spring 提供的环境差异管理工具,它们分别用于管理配置参数和 Bean/配置类,根据应用的具体需求选择使用。 2.4 如何确定Spring中活动的Profile? Spring可以通过多种方式来指定当前的活动Profile,优先级排序从高到低如下: ConfigurableEnvironment.setActiveProfiles JVM的-Dspring.profiles.active参数或环境变量SPRING_PROFILES_ACTIVE(仅Spring Boot可用) SpringApplicationBuilder.profiles(仅Spring Boot可用) SpringApplication.setDefaultProperties(仅Spring Boot可用) 配置文件属性spring.profiles.active 如果上面都有配置,那么优先级高的会覆盖优先级低的配置。 3. @Conditional 3.1 @Conditional注解及其用途 @Conditional是Spring 4.0中引入的一个核心注解,用于将Bean的创建与特定条件相关联。这种特性在Spring Boot中被大量使用,以便在满足特定条件时创建和装配Bean。 @Conditional注解接受一个或多个实现了Condition接口的类作为参数。Condition接口只有一个名为matches的方法,该方法需要返回一个布尔值以指示条件是否满足。如果matches方法返回true,则Spring会创建和装配标记为@Conditional的Bean;如果返回false,则不会创建和装配这个Bean。 @Conditional注解的应用非常灵活。它可以用于标记直接或间接使用@Component注解的类,包括但不限于@Configuration类。此外,它还可以用于标记@Bean方法,或者作为元注解组成自定义注解。 如果一个@Configuration类被@Conditional注解标记,那么与该类关联的所有@Bean方法,以及任何@Import和@ComponentScan注解,也都会受到相同的条件限制。这就意味着,只有当@Conditional的条件满足时,这些方法和注解才会被处理。 总的来说,@Conditional提供了一种强大的机制,可以用于基于特定条件来控制Bean的创建和装配。通过使用它,我们可以更灵活地控制Spring应用的配置,以便适应各种不同的运行环境和需求。 3.2 使用@Conditional实现条件装配 假设我们有一个图书馆应用程序,其中有两个类,Librarian和Library,我们希望只有当 Librarian 类存在时,Library 才被创建。这个时候@Profile就无法实现了,因为@Profile 无法检查其他 bean 是否存在。 定义一个名为LibrarianCondition的条件类,实现了Condition接口并重写了matches方法。在matches方法中,检查了Spring应用上下文中是否存在名为"librarian"的Bean定义。在创建Library Bean的方法上,添加了@Conditional(LibrarianCondition.class)注解,指定了只有当 LibrarianCondition 返回 true 时,Library bean 才会被创建。 3.2 @Conditional在Spring Boot中的应用 Spring Boot 在很多地方使用了 @Conditional 来实现条件化配置,我们分别来看一下。 3.2.1 @ConditionalOnBean 和 @ConditionalOnMissingBean @ConditionalOnBean 和 @ConditionalOnMissingBean 是 Spring Boot 提供的一对条件注解,用于条件化的创建 Spring Beans,可以检查 Spring 容器中是否存在特定的bean。如果存在(或者不存在)这样的bean,那么对应的配置就会被启用(或者被忽略)。 具体来说: @ConditionalOnBean:当 Spring 容器中存在指定类型的 Bean 时,当前被标注的 Bean 才会被创建。 @ConditionalOnMissingBean:当 Spring 容器中不存在指定类型的 Bean 时,当前被标注的 Bean 才会被创建。 有人可能会疑问了,会不会有这种可能,Librarian 在Library 后面才注册,导致这个条件会认为Librarian不存在? 答案是并不会。Spring 在处理 @Configuration 类时,会预先解析所有的 @Bean 方法,收集所有的 Bean 定义信息,然后按照依赖关系对这些 Bean 进行实例化。 那如果Librarian 不是写在配置类的,而是被@Component注解修饰的,会不会因为顺序问题导致条件判断为不存在? 即使 Librarian 类的定义被 @Component 注解修饰并通过组件扫描注册到 Spring 容器,Spring 仍然可以正确地处理依赖关系,它依赖的是 Bean 的定义,而不是 Bean 的实例化。 当 Spring 容器启动时,它会先扫描所有的 Bean 定义并收集信息,这包括由 @Configuration 类的 @Bean 方法定义的,还有通过 @Component,@Service,@Repository等注解和组件扫描机制注册的。 当执行到 @ConditionalOnBean 或者 @ConditionalOnMissingBean 的条件判断时,Spring 已经有了全局的视野,知道所有的 Bean 定义。所以,即使某个 Bean 是通过 @Component 注解定义并由组件扫描机制注册的,也不会因为顺序问题导致判断失败。同样的,@Conditional条件判断也不会存在这个问题。 总的来说,Spring 提供了强大的依赖管理和自动装配功能,可以确保 Bean 的各种条件判断,无论 Bean 是如何定义和注册的。 3.2.2 @ConditionalOnProperty 这个注解表示只有当一个或多个给定的属性有特定的值时,才创建带有该注解的Bean。 @ConditionalOnProperty是Spring Boot中的一个注解,用于检查某个配置属性是否存在,或者是否有特定的值,只有满足条件的情况下,被该注解标记的类或方法才会被创建或执行。这个注解可以用来在不同的环境下开启或关闭某些功能,或者调整功能的行为。这样,我们可以通过修改配置文件,灵活地开启或关闭某个功能,而不需要修改代码。比如,我们可能有一些在开发环境和生产环境下行为不同的功能,或者一些可以根据需求开启或关闭的可选功能。实际开发中这些功能也可以考虑使用Apollo,只需要在对应环境的Apollo配置即可获取对应属性值,从而实现不同功能。 3.2.3 @ConditionalOnClass 和 @ConditionalOnMissingClass 这两个注解可以检查Classpath中是否存在或不存在某个特定的类。 这个2个注解实际的业务开发中使用的情况比较少,因为它主要用于处理底层框架或者库的一些通用逻辑。但它在框架或库的开发中确实非常有用,让框架或库可以更加灵活地适应不同的使用环境和配置。 比如在 Spring Boot 中,很多自动配置类都会使用条件装配。比如 DataSourceAutoConfiguration 类,这个类负责自动配置数据库连接池,它使用 @ConditionalOnClass 注解来判断 Classpath 中是否存在相关的数据库驱动类,只有当存在相关的数据库驱动类时,才会进行自动配置。 再比如,我们可能开发了一个功能强大的日志记录库,它可以将日志记录到数据库,但是如果用户的项目中没有包含 JDBC 驱动,那么我们的库应该退化到只将日志记录到文件。这个时候就可以使用 @ConditionalOnClass 来检查是否存在 JDBC 驱动,如果存在则创建一个将日志记录到数据库的 Bean,否则创建一个将日志记录到文件的 Bean。 ----------------------------------------组件扫描------------------------------------------------------------------ 🐱️组件扫描是Spring框架中一个重要的特性,它可以自动检测并实例化带有特定注解(如@Component, @Service, @Controller等)的类,并将它们注册为Spring上下文中的bean。 🐱️@ComponentScan注解是用于指定Spring在启动时需要扫描的包路径,从而自动发现并注册组件。我们设置组件扫描路径包括两种方式: 直接指定包名:如@ComponentScan("com.example.demo"),等同于@ComponentScan(basePackages = {"com.example.demo"}), Spring会扫描指定包下的所有类,并查找其中带有@Component、@Service、@Repository等注解的组件,然后将这些组件注册为Spring容器的bean。 指定包含特定类的包:如@ComponentScan(basePackageClasses = {ExampleService.class}), Spring会扫描ExampleService类所在的包以及其所有子包。 为了简化配置,我们通常会将 @ComponentScan 放在主程序上,因为主程序一般会位于根包下,这样可以扫描到所有的子包。 🐱️除了基本的包路径扫描,Spring还提供了过滤功能,允许我们通过设定过滤规则,只包含或排除带有特定注解的类。 🐱️通过@ComponentScan的includeFilters属性来实现注解包含过滤,通过excludeFilters属性来排除带有特定注解的类。 Filters类型可以是注解、正则表达式、Assignable类型、CUSTOM自定义。 在Spring中,当使用@ComponentScan注解进行组件扫描时,Spring提供了默认的过滤规则。这些默认规则包括以下几种类型的注解: @Component @Repository @Service @Controller @RestController @Configuration 默认不写useDefaultFilters属性的情况下,useDefaultFilters属性的值为true,Spring在进行组件扫描时会默认包含以上注解标记的组件, 如果将useDefaultFilters设置为false,Spring就只会扫描明确指定过滤规则的组件,不再包括以上默认规则的组件。 🐱️使用@ComponentScans注解进行组合包扫描。这个特性允许在一次操作中完成多次包扫描,实现对Spring组件扫描行为的精细控制。 🐱️当我们在Spring中使用注解进行bean的定义和管理时,通常会用到@Component, @Service, @Repository, @Controller等注解。在使用这些注解进行bean定义的时候,如果我们没有明确指定bean的名字,那么Spring会根据一定的规则为我们的bean生成一个默认的名字。这个默认的名字一般是类名的首字母小写。例如,对于一个类名为MyService类,如果我们用上述注解,那么Spring会为我们的bean生成一个默认的名字myService。我们可以在应用的其他地方通过这个名字来引用这个bean。例如,我们可以在其他的bean中通过@Autowired注解和这个名字来注入这个bean:@Autowired private MyService myService;。这个默认的名字是通过BeanNameGenerator接口的实现类AnnotationBeanNameGenerator来生成的。AnnotationBeanNameGenerator会检查我们的类是否有明确的指定了bean的名字,如果没有,那么它就会按照类名首字母小写的规则来生成一个默认的名字。 Import生成的beanname和全限定类名一样,比如site.zhangzhuo.learn_springboot.beanimport.Book 在Java内省机制(Introspection)中,如果类名的前两个字母都是大写,那么在进行首字母小写的转换时,会保持原样不变。也就是说,对于这种情况,bean的名称和类名是一样的。这种设计是为了遵守Java中的命名约定,即当一个词作为类名的开始并且全部大写时(如URL,HTTP),应保持其全部大写的格式。Java内省机制(Introspection)是Java语言对Bean类的一种自我检查的能力,它属于Java反射的一个重要补充。它允许Java程序在运行时获取Bean类的类型信息以及Bean的属性和方法的信息。这个规则主要是为了处理一些类名或方法名使用大写字母缩写的情况。例如,对于一个名为"getURL“的方法,我们会得到”URL“作为属性名,而不是”uRL"。 默认情况下在配置类中通过 @Bean 注解的方法创建实例,bean的名称是方法名。此外,可以通过@Bean注解里面的name属性主动设置bean的名称。 要么在配置类中通过 @Bean 注解的方法创建实例,要么在bean类上写 @Component 注解。两者不能同时出现,不然会报错A bean with that name has already been defined。。如果不是第三方库,我们一般选择后者。 为什么要有配置类出现?所有的Bean上面使用@Component,用@ComponentScan注解扫描不就能解决了吗? 我们在使用一些第三方库时,需要对这些库进行一些特定的配置。这些配置信息,我们可能无法直接通过注解或者XML来完成,或者通过这些方式完成起来非常麻烦。而配置类可以很好地解决这个问题。通过配置类,我们可以在Java代码中完成任何复杂的配置逻辑。 假设你正在使用 MyBatis,在这种情况下可能需要配置一个SqlSessionFactory,在大多数情况下,我们无法(也不应该)直接修改第三方库的代码,所以无法直接在SqlSessionFactory类或其他类上添加@Configuration、@Component等注解。为了能够在Spring中使用和配置这些第三方库,我们需要创建自己的配置类,并在其中定义@Bean方法来初始化和配置这些类的实例。这样就可以灵活地控制这些类的实例化过程,并且可以利用Spring的依赖注入功能。比如MyBatis的SqlSessionFactory对象。 有没有这么一种可能,一个旧的Spring项目,里面有很多旧的XML配置,现在你接手了,想要全部用注解驱动,不想再写XML配置了,那应该怎么兼容呢? 我们编写一个新的注解驱动的配置类,这个新的配置类中,我们使用 @ImportResource 注解来引入旧的XML配置文件,并可以定义新的bean。@ImportResource("classpath:old-config.xml")告诉Spring在初始化AppConfig配置类时,去类路径下寻找old-config.xml文件,并加载其中的配置,这样就可以在不打断旧的XML配置的基础上逐步迁移至新的注解配置。 ---------------------------------------------BeanDefinition------------------------------------------------------------ BeanDefinition包含了大量的配置信息,这些信息可以指导Spring如何创建Bean,包括Bean的构造函数参数,属性值,初始化方法,静态工厂方法名称等等。此外,子BeanDefinition还可以从父BeanDefinition中继承配置信息,同时也可以覆盖或添加新的配置信息。这种设计模式有效减少了冗余的配置信息,使配置更为简洁。 BeanDefinition接口定义了Bean的所有元信息,主要包含以下方法: get/setBeanClassName() - 获取/设置Bean的类名 get/setScope() - 获取/设置Bean的作用域 isSingleton() / isPrototype() - 判断是否单例/原型作用域 get/setInitMethodName() - 获取/设置初始化方法名 get/setDestroyMethodName() - 获取/设置销毁方法名 get/setLazyInit() - 获取/设置是否延迟初始化 get/setDependsOn() - 获取/设置依赖的Bean get/setPropertyValues() - 获取/设置属性值 get/setAutowireCandidate() - 获取/设置是否可以自动装配 get/setPrimary() - 获取/设置是否首选的自动装配Bean 1.4 BeanDefinition深层信息结构梳理 在 Spring 中,BeanDefinition 包含了以下主要信息: Class:这是全限定类名,Spring 使用这个信息通过反射创建 Bean 实例。例如,com.example.demo.bean.Book,当 Spring 需要创建 Book bean 的实例时,它将根据这个类名通过反射创建 Book 类的实例。 Name:这是 Bean 的名称。在应用程序中,我们通常使用这个名称来获取 Bean 的实例。例如,我们可能有一个名称为 "bookService" 的 Bean,我们可以通过 context.getBean("bookService") 来获取这个 Bean 的实例。 Scope:这定义了 Bean 的作用域,例如 singleton 或 prototype。如果 scope 是 singleton,那么 Spring 容器将只创建一个 Bean 实例并在每次请求时返回这个实例。如果 scope 是 prototype,那么每次请求 Bean 时,Spring 容器都将创建一个新的 Bean 实例。 Constructor arguments:这是用于实例化 Bean 的构造函数参数。例如,如果我们有一个 Book 类,它的构造函数需要一个 String 类型的参数 title,那么我们可以在 BeanDefinition 中设置 constructor arguments 来提供这个参数。 Properties:这些是需要注入到 Bean 的属性值。例如,我们可能有一个 Book 类,它有一个 title 属性,我们可以在 BeanDefinition 中设置 properties 来提供这个属性的值。这些值也可以通过 标签或 @Value 注解在配置文件或类中注入。 Autowiring Mode:这是自动装配的模式。如果设置为 byType,那么 Spring 容器将自动装配 Bean 的属性,它将查找容器中与属性类型相匹配的 Bean 并注入。如果设置为 byName,那么容器将查找容器中名称与属性名相匹配的 Bean 并注入。还有一个选项是 constructor,它指的是通过 Bean 构造函数的参数类型来自动装配依赖。 Lazy Initialization:如果设置为 true,Bean 将在首次请求时创建,而不是在应用启动时。这可以提高应用的启动速度,但可能会在首次请求 Bean 时引入一些延迟。 Initialization Method and Destroy Method:这些是 Bean 的初始化和销毁方法。例如,我们可能有一个 BookService 类,它有一个名为 init 的初始化方法和一个名为 cleanup 的销毁方法,我们可以在 BeanDefinition 中设置这两个方法,那么 Spring 容器将在创建 Bean 后调用 init 方法,而在销毁 Bean 之前调用 cleanup 方法。 Dependency beans:这些是 Bean 的依赖关系。例如,我们可能有一个 BookService Bean,它依赖于一个 BookRepository Bean,那么我们可以在 BookService 的 BeanDefinition 中设置 dependency beans 为 "bookRepository",那么在创建 BookService Bean 之前,Spring 容器将首先创建 BookRepository Bean。 以上就是 BeanDefinition 中主要包含的信息,这些信息将会告诉 Spring 容器如何创建和配置 Bean。不同的 BeanDefinition 实现可能会有更多的配置信息。例如,RootBeanDefinition、ChildBeanDefinition、GenericBeanDefinition 等都是 BeanDefinition 接口的具体实现类,它们可能包含更多的配置选项。 2.1 BeanDefinition的类型及其应用 在Spring中,一个bean的配置信息就是由BeanDefinition对象来保存的。根据bean配置的不同来源和方式,BeanDefinition又被分为很多种类型,我们选取其中几种讲解一下 RootBeanDefinition:这个实例会保存所有的配置信息,比如类名、属性值等。 ChildBeanDefinition:如果有一个bean,并且想创建一个新的bean,这个新的bean需要继承原有bean的所有配置,但又要添加或修改一些配置信息,Spring就会创建一个ChildBeanDefinition实例。 GenericBeanDefinition:这是一种通用的BeanDefinition,可以根据需要转化为RootBeanDefinition或者ChildBeanDefinition。 AnnotatedBeanDefinition:在类上使用注解(如@Component, @Service, @Repository等)来定义一个bean时,Spring会创建一个实现了AnnotatedBeanDefinition接口的实例,如AnnotatedGenericBeanDefinition或ScannedGenericBeanDefinition。这个实例会保存类名、类的类型,以及类上的所有注解信息。 GenericBeanDefinition和AnnotatedBeanDefinition的主要区别在于,AnnotatedBeanDefinition保存了类上的注解信息,而GenericBeanDefinition没有。这就使得Spring能够在运行时读取和处理这些注解,提供更丰富的功能。在大多数情况下,我们并不需要关心Spring为bean创建的是哪一种BeanDefinition。Spring会自动管理这些BeanDefinition,并根据它们的类型以及它们所包含的信息来创建和配置bean。 2.2 生成BeanDefinition的原理剖析 这个 BeanDefinition 对象是在 Spring 启动过程中由各种 BeanDefinitionReader 实现类读取配置并生成的。 2.3 AttributeAccessor实战:属性操作利器 AttributeAccessor是Spring框架中的一个重要接口,它提供了一种灵活的方式来附加额外的元数据到Spring的核心组件。在Spring中,包括BeanDefinition在内的许多重要类都实现了AttributeAccessor接口,这样就可以动态地添加和获取这些组件的额外属性。这样做的一个显著好处是,开发人员可以在不改变原有类定义的情况下,灵活地管理这些组件的额外信息。 让我们来看一个例子,全部代码如下: 先创建一个Book对象 class Book { private String title; private String author; public Book() {} public Book(String title, String author) { this.title = title; this.author = author; } // getter 和 setter 省略... } 主程序: package com.example.demo; import com.example.demo.bean.Book; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.support.RootBeanDefinition; public class DemoApplication { public static void main(String[] args) { // 创建一个BeanDefinition, BeanDefinition是AttributeAccessor的子接口 BeanDefinition bd = new RootBeanDefinition(Book.class); // 设置属性 bd.setAttribute("bookAttr", "a value"); // 检查和获取属性 if(bd.hasAttribute("bookAttr")) { System.out.println("bookAttr: " + bd.getAttribute("bookAttr")); // 移除属性 bd.removeAttribute("bookAttr"); System.out.println("bookAttr: " + bd.getAttribute("bookAttr")); } } } 在这个例子中,我们创建了一个RootBeanDefinition实例来描述如何创建一个Book类的实例。RootBeanDefinition是BeanDefinition的实现,而BeanDefinition实现了AttributeAccessor接口,因此RootBeanDefinition也就继承了AttributeAccessor的方法。 有人可能会疑问,Book并没有bookAttr这个成员变量,这是怎么赋值的? 在Spring框架中,AttributeAccessor接口定义的方法是为了附加、获取和移除与某个对象(例如RootBeanDefinition)相关联的元数据,而不是操作对象(例如Book)本身的字段。 所以,在RootBeanDefinition实例上调用setAttribute("bookAttr", "a value")方法时,其实并不是在Book实例上设置一个名为bookAttr的字段。而是在RootBeanDefinition实例上附加了一个元数据,元数据的键是"bookAttr",值是"a value"。 后续使用getAttribute("bookAttr")方法时,它将返回之前设置的元数据值"a value",而不是尝试访问Book类的bookAttr字段(实际上Book类并没有bookAttr字段)。 简单来说,这些元数据是附加在RootBeanDefinition对象上的,而不是附加在由RootBeanDefinition对象描述的Book实例上的。 ----------------------------------------BeanDefinitionRegistry------------------------------------------------------------------- 1. 什么是BeanDefinitionRegistry? BeanDefinitionRegistry 是一个非常重要的接口,存在于 Spring 的 org.springframework.beans.factory.support 包中,它是 Spring 中注册和管理 BeanDefinition 的核心组件。 BeanDefinitionRegistry 的主要职责就是注册和管理这些 BeanDefinition。我们可以把它看作是一个存放 BeanDefinition 的注册表,向其中注册新的 BeanDefinition,或者检索和删除现有的 BeanDefinition。它提供了一些方法,如 registerBeanDefinition(String, BeanDefinition), removeBeanDefinition(String),和 getBeanDefinition(String),用于执行这些操作。 2. 为什么需要BeanDefinitionRegistry? 如果BeanDefinitionRegistry不存在,Spring的某些核心功能会受到什么样的影响? 资源解析的统一性:BeanDefinition作为一个统一的数据结构存储了Bean的配置信息。如果没有BeanDefinitionRegistry,每种配置方式(XML、注解、Java配置)都需要各自的专门数据结构。这不仅会导致资源解析的代码复杂度增加,还可能在不同的解析机制之间产生不一致性。 依赖查找和注入:BeanDefinitionRegistry提供了一个中心位置,充当了Bean定义的中央存储,可以快速查找Bean的定义。如果没有它,当需要注入一个Bean的依赖时,Spring不仅需要遍历所有的配置源来查找对应的Bean,而且还可能遭遇Bean定义不一致的问题,这会显著降低性能和准确性。 延迟初始化和作用域管理:BeanDefinitionRegistry存储了BeanDefinition,BeanDefinition中包含了Bean的作用域和其他元数据。如果没有这个BeanDefinitionRegistry,Spring在执行Bean的延迟加载或根据作用域创建Bean时,需要重新解析原始的配置资源,这增加了处理时间并可能导致潜在的配置错误。 配置验证:当所有BeanDefinition注册到BeanDefinitionRegistry后,Spring可以进行配置的校验,例如检查循环依赖、确保Bean定义的完整性等。如果没有BeanDefinitionRegistry,Spring需要在每次Bean初始化时进行检查,这不仅导致性能下降,还可能漏掉某些隐晦的配置问题。 生命周期管理:没有BeanDefinitionRegistry存储生命周期回调、初始化方法等信息,Spring在管理Bean的生命周期时,需要从原始的配置源获取这些信息。这不仅增加了管理的复杂度,还会使生命周期回调会变得复杂和笨重。 简而言之,没有BeanDefinitionRegistry,Spring会失去中心化的Bean管理,导致效率下降、错误处理分散、以及增加生命周期管理的复杂度。BeanDefinitionRegistry确保了Spring的高效、一致和稳定运行。 4. BeanDefinition的合并 我们前一篇讲解BeanDefinition的时候没有讲解BeanDefinition的合并,这里补充说明。 BeanDefinition 在 Spring 中,BeanDefinition 是一个接口,它定义了 Bean 的配置信息,例如 Bean 的类名,是否是单例,依赖关系等。在 Spring 中,每一个 Bean 都对应一个 BeanDefinition 对象。 合并的意义 在 Spring 中,有一种特殊的 BeanDefinition,叫做子 BeanDefinition,也就是我们在 XML 配置文件中通过 parent 属性指定的那种。这种子 BeanDefinition 可以继承父 BeanDefinition 的配置信息。 合并的过程,就是把子 BeanDefinition 的配置信息和父 BeanDefinition 的配置信息合并起来,形成一个完整的配置信息。合并后的 BeanDefinition 对象包含了 Bean 创建所需要的所有信息,Spring 将使用这个完整的 BeanDefinition 来创建 Bean 实例。 合并的过程 Spring 在需要创建 Bean 实例的时候,会先获取对应的 BeanDefinition 对象。如果这个 BeanDefinition 是一个子 BeanDefinition,Spring 就会找到它的父 BeanDefinition,然后把两者的配置信息合并起来,形成一个完整的 BeanDefinition。 这个过程是在 DefaultListableBeanFactory 的 getMergedBeanDefinition 方法中进行的,如果大家有兴趣,可以在这个方法中设置断点,看一看具体的合并过程。 在Java配置中,我们无法直接模拟XML配置的BeanDefinition合并过程,因为这是Spring XML配置的一项特性,配置类通常会采用Java代码的继承或组合来重用bean定义,不会涉及到配置元数据层面的BeanDefinition合并。XML配置中的BeanDefinition合并特性允许我们定义一个父Bean,然后定义一些子Bean,子Bean可以继承父Bean的一些属性。这个特性在Java配置中并没有直接的替代品,因为Java配置通常更加依赖实例化过程中的逻辑,而不是元数据(即BeanDefinition)。在Java配置中,我们可以使用继承和组合等普通的Java特性来实现类似的结果,但这不是真正的BeanDefinition合并。因此,当我们从XML配置转换为Java配置时,通常需要手动将共享的属性复制到每个Bean的定义中。 在 Spring 的内部,BeanDefinitionRegistry 通常由 BeanFactory 实现,特别是 DefaultListableBeanFactory 和 GenericApplicationContext,它们都实现了这个接口。 为什么有两个不同的 BeanDefinition 类型( GenericBeanDefinition 和 RootBeanDefinition)? GenericBeanDefinition: 这是一个通用的BeanDefinition实现类,可以配置任何类型的bean。 它通常用于读取XML、注解或其他形式的配置。 与其它特定的BeanDefinition相比,它是比较简单和轻量级的。 当使用元素在XML中定义bean时,通常会为该bean创建一个GenericBeanDefinition实例。 RootBeanDefinition: 这是一个完整的bean定义,包含了bean的所有配置信息,如构造函数参数、属性值、方法覆盖等。 它通常用于合并父子bean定义。也就是说,当一个bean定义继承另一个bean定义时,RootBeanDefinition负责持有合并后的最终配置。 除了GenericBeanDefinition之外,它还包含许多与bean的实例化、依赖解析和初始化相关的内部细节。 在Spring的内部工作流中,尽管开始时可以有各种BeanDefinition实现,但在容器的后期处理阶段,它们通常都会转化为RootBeanDefinition,因为在这个阶段需要一个完整和固定的bean定义来进行bean的创建。 你可以使用context.getBeansOfType(Ink.class)方法获取所有类型为Ink的bean,然后自行决定如何使用这些bean。 +++++++++++++++++++++++++BeanDefinition合并与Spring初始化关系++++++++++++++++++++ 🐱️资源定位 在此阶段,Spring会根据用户的配置来确定需要加载的资源位置,资源可能来源于多种配置方式,如XML、Java注解或Java配置。 🐱️读取配置 Spring从确定的配置源中读取Bean定义信息 对于XML配置,解析器会处理每一个元素。在这个时候,特别是存在父子Bean关系的定义,这些定义被解析为原始的BeanDefinition,但并没有合并。 对于注解和Java配置,BeanDefinition被解析为独立的定义,通常不涉及父子关系。 🐱️注册BeanDefinition Spring会将所有解析得到的BeanDefinition注册到BeanDefinitionRegistry中。 🐱️处理BeanDefinition 在这个阶段,Spring进行BeanDefinition的预处理。 如果从XML配置中读取的Bean之间存在父子关系,这时会进行合并,合并后的BeanDefinition确保子Bean继承了父Bean的所有属性,并且能够覆盖它们。 而基于注解或Java配置的Bean定义,由于没有明确的父子关系,这种合并操作通常不会发生。 🐱️Bean的实例化与属性填充 此阶段标志着Spring生命周期的开始。 所有的BeanDefinition,无论是原始的还是经过合并的,都会在此阶段转化为实际的Bean实例。 Spring容器将负责管理这些Bean的完整生命周期,包括但不限于依赖注入、属性设置。 🐱️Bean的初始化 包括调用Bean的初始化方法,例如实现了InitializingBean接口的afterPropertiesSet方法或者通过init-method属性指定的自定义初始化方法。 在此阶段,Bean已经完全准备好,可以供应用程序使用。 🐱️注册Bean的销毁方法 Spring会跟踪并注册Bean的销毁方法。 这确保了当Spring容器关闭时,它会正确地调用每个Bean的销毁方法, 例如实现了DisposableBean接口的destroy方法或通过destroy-method属性指定的自定义方法 ----------------------------------------后置处理器(BeanPostProcessor)-------------------------------------------------- --Spring从各种来源(如XML文件、Java配置、注解)加载配置信息 --Spring根据加载的配置,创建对应的BeanDefinition --Spring将这些BeanDefinition对象注册到BeanDefinitionRegistry中 BeanDefinitionRegistryPostProcessor接口postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry)方法 BeanFactoryPostProcessor接口postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory)方法 --Bean实例化、属性依赖注入 BeanPostProcessor接口 postProcessBeforeInitialization(Object bean, String beanName)方法 --Bean自定义初始化方法执行 postProcessAfterInitialization(Object bean, String beanName)方法 --Bean完全初始化 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ BeanDefinitionRegistryPostProcessor是Spring中的一个高级扩展接口,继承自 BeanFactoryPostProcessor。它提供了更为深入的方式来干预bean定义的注册过程。它的特殊之处在于,除了能够像 BeanFactoryPostProcessor 那样修改已经注册的BeanDefinition,还能向注册中心BeanDefinitionRegistry 中动态地添加或移除bean定义,通过核心方法:postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry)。BeanDefinitionRegistryPostProcessor的方法在所有其他 BeanFactoryPostProcessor方法之前执行,这确保了它可以在其他处理器操作前先注册或修改bean定义。 在BeanFactory子类中有一个DefaultListableBeanFactory类,它实现了包含基本Spirng IoC容器所具有的重要功能,我们开发时不论是使用BeanFactory系列还是ApplicationContext系列来创建容器基本都会使用到DefaultListableBeanFactory类。在平时我们说BeanFactory提供了IOC容器最基本的功能和规范,但真正可以作为一个可以独立使用的IOC容器还是DefaultListableBeanFactory,因为它真正实现了BeanFactory接口中的方法。所以DefaultListableBeanFactory 是整个Spring IOC的始祖,在Spring中实际上把它当成默认的IoC容器来使用。 public class SpringFactoriesLoader public class DefaultBootstrapContext implements ConfigurableBootstrapContext public interface ConfigurableBootstrapContext extends BootstrapRegistry, BootstrapContext public interface BootstrapContext public interface BootstrapRegistry public interface ApplicationContextFactory class DefaultApplicationContextFactory implements ApplicationContextFactory private ConfigurableApplicationContext createDefaultApplicationContext() { if (!AotDetector.useGeneratedArtifacts()) { return new AnnotationConfigApplicationContext(); } return new GenericApplicationContext(); } public class AnnotationConfigApplicationContext extends GenericApplicationContext implements AnnotationConfigRegistry public class GenericApplicationContext extends AbstractApplicationContext implements BeanDefinitionRegistry public abstract class AbstractApplicationContext extends DefaultResourceLoader implements ConfigurableApplicationContext public class DefaultResourceLoader implements ResourceLoader public interface ResourceLoader 在Spring中,所有的beans在被完全实例化之前都是以BeanDefinition的形式存在的。BeanFactoryPostProcessor为我们提供了一个机会,使我们能够在bean完全实例化之前调整和修改这些BeanDefinition。对BeanDefinition的任何修改都会影响后续的bean实例化和初始化过程。 加载配置: Spring从各种来源(如XML文件、Java配置、注解)加载配置信息。 解析配置: 根据加载的配置,Spring创建对应的BeanDefinition。 注册BeanDefinition: 解析完成后,Spring将这些BeanDefinition对象注册到BeanDefinitionRegistry中。 执行BeanDefinitionRegistryPostProcessor: 这个后置处理器提供了一个重要的扩展点,允许在所有BeanDefinition注册完毕后,但在Bean实例化之前进行一些操作。例如:注册新的BeanDefinition、修改或删除现有的BeanDefinition。 执行BeanFactoryPostProcessor: 这个后置处理器提供了另一个扩展点,它主要允许查看或修改已经注册的BeanDefinition。例如,根据某些条件更改Bean的作用域或属性值。 实例化Bean: 这是将BeanDefinition转换为实际的Bean实例的过程。 依赖注入 : 在这一步,Spring 框架会按照 BeanDefinition 的描述为 bean 实例注入所需的依赖。 Bean初始化: 在所有依赖都注入后,特定的初始化方法(如通过@PostConstruct指定的)将会被调用,完成Bean的最后设置。 执行BeanPostProcessor的方法: BeanPostProcessor提供了拦截的能力,允许在Bean初始化阶段结束之前和之后进行操作。 Bean完全初始化: 在此阶段,Bean 完全初始化并准备好被应用程序使用。 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 根据Lion类打印日志我们可以分析出 首先,Bean Constructor Method Invoked! 表明 Lion 的构造器被调用,创建了一个新的 Lion 实例。 接着,Bean Setter Method Invoked! name: my lion 和 Bean Setter Method Invoked! elephant: com.example.demo.bean.Elephant@7364985f 说明 Spring 对 Lion 实例的依赖注入。在这一步,Spring 调用了 Lion 的 setter 方法,为 name 属性设置了值 “my lion”,同时为 elephant 属性注入了一个 Elephant 实例。 然后,postProcessBeforeInitialization Method Invoked! 说明 MyBeanPostProcessor 的 postProcessBeforeInitialization 方法被调用,这是在初始化 Lion 实例之前。 @PostConstruct Method Invoked! 说明 @PostConstruct 注解的方法被调用,这是在 Bean 初始化之后,但是在 Spring 执行任何进一步初始化之前。 afterPropertiesSet Method Invoked! 说明 Spring 调用了 InitializingBean 的 afterPropertiesSet 方法 customInitMethod Method Invoked! 表示调用了 Lion 实例的 init-method 方法。 postProcessAfterInitialization Method Invoked! 说明 MyBeanPostProcessor 的 postProcessAfterInitialization 方法被调用,这是在初始化 Lion 实例之后。 然后 Spring 完成了整个初始化过程。 主程序中手动调用了 Lion 实例的 setter 方法,因此在 Bean Setter Method Invoked! name: oh!!! My Bean set new name 可见,name 属性被设置了新的值 "oh!!! My Bean set new name"。 当容器准备关闭时: @PreDestroy Method Invoked! 说明 @PreDestroy 注解的方法被调用,这是在 Bean 销毁之前。 destroy Method Invoked! 表示 Lion 实例开始销毁。在这一步,Spring 调用了 DisposableBean 的 destroy 方法。 customDestroyMethod Method Invoked! 表示 Lion 实例开始销毁,调用了Lion 实例的 destroy-method 方法。 最后,Spring 完成了整个销毁过程,容器关闭。 这个日志提供了 Spring Bean 生命周期的完整视图,显示了从创建到销毁过程中的所有步骤。 我们可以注册多个 BeanPostProcessor。在这种情况下,Spring 会按照它们的 Ordered 接口或者 @Order 注解指定的顺序来调用这些后置处理器。如果没有指定顺序,那么它们的执行顺序是不确定的。 --------------------------------------SPI (Service Provider Interface) ----------------------------------------------- 1. SPI解读:什么是SPI? SPI (Service Provider Interface) 是一种服务发现机制,它允许第三方提供者为核心库或主框架提供实现或扩展。这种设计允许核心库/框架在不修改自身代码的情况下,通过第三方实现来增强功能。 JDK原生的SPI: 定义和发现:JDK的SPI主要通过在META-INF/services/目录下放置特定的文件来指定哪些类实现了给定的服务接口。这些文件的名称应为接口的全限定名,内容为实现该接口的全限定类名。 加载机制:ServiceLoader类使用Java的类加载器机制从META-INF/services/目录下加载和实例化服务提供者。例如,ServiceLoader.load(MyServiceInterface.class)会返回一个实现了MyServiceInterface的实例迭代器。 缺点:JDK原生的SPI每次通过ServiceLoader加载时都会初始化一个新的实例,没有实现类的缓存,也没有考虑单例等高级功能。 Spring的SPI: 更加灵活:Spring的SPI不仅仅是服务发现,它提供了一套完整的插件机制。例如,可以为Spring定义新的PropertySource,ApplicationContextInitializer等。 与IoC集成:与JDK的SPI不同,Spring的SPI与其IoC (Inversion of Control) 容器集成,使得在SPI实现中可以利用Spring的全部功能,如依赖注入。 条件匹配:Spring提供了基于条件的匹配机制,这允许在某些条件下只加载特定的SPI实现,例如,可以基于当前运行环境的不同来选择加载哪个数据库驱动。 配置:Spring允许通过spring.factories文件在META-INF目录下进行配置,这与JDK的SPI很相似,但它提供了更多的功能和灵活性。 举个类比的例子: 想象我们正在建造一个电视机,SPI就像电视机上的一个USB插口。这个插口可以插入各种设备(例如U盘、游戏手柄、电视棒等),但我们并不关心这些设备的内部工作方式。这样只需要提供一个标准的接口,其他公司(例如U盘制造商)可以为此接口提供实现。这样,电视机可以在不更改自己内部代码的情况下使用各种新设备,而设备制造商也可以为各种电视机制造兼容的设备。 总之,SPI是一种将接口定义与实现分离的设计模式,它鼓励第三方为一个核心产品或框架提供插件或实现,从而使核心产品能够轻松地扩展功能。 2. SPI在JDK中的应用示例 在Java的生态系统中,SPI 是一个核心概念,允许开发者提供扩展和替代的实现,而核心库或应用不必更改,下面举出一个例子来说明。 全部代码和步骤如下: 步骤1:定义一个服务接口,文件名: MessageService.java package com.example.demo.service; public interface MessageService { String getMessage(); } 步骤2:为服务接口提供实现,这里会提供两个简单的实现类。 HelloMessageService.java package com.example.demo.service; public class HelloMessageService implements MessageService { @Override public String getMessage() { return "Hello from HelloMessageService!"; } } HiMessageService.java package com.example.demo.service; public class HiMessageService implements MessageService { @Override public String getMessage() { return "Hi from HiMessageService!"; } } 这些实现就像不同品牌或型号的U盘或其他USB设备。每个设备都有自己的功能和特性,但都遵循相同的USB标准。 步骤3:注册服务提供者 在资源目录(通常是src/main/resources/)下创建一个名为META-INF/services/的文件夹。在这个文件夹中,创建一个名为com.example.demo.service.MessageService的文件(这是我们接口的全限定名),这个文件没有任何文件扩展名,所以不要加上.txt这样的后缀。文件的内容应为我们的两个实现类的全限定名,每个名字占一行: com.example.demo.service.HelloMessageService com.example.demo.service.HiMessageService META-INF/services/ 是 Java SPI (Service Provider Interface) 机制中约定俗成的特定目录。它不是随意选择的,而是 SPI 规范中明确定义的。因此,当使用 JDK 的 ServiceLoader 类来加载服务提供者时,它会特意去查找这个路径下的文件。 请确保文件的每一行只有一个名称,并且没有额外的空格或隐藏的字符,文件使用UTF-8编码。 步骤4:使用ServiceLoader加载和使用服务 package com.example.demo; import com.example.demo.service.MessageService; import java.util.ServiceLoader; public class DemoApplication { public static void main(String[] args) { ServiceLoader loaders = ServiceLoader.load(MessageService.class); for (MessageService service : loaders) { System.out.println(service.getMessage()); } } } ServiceLoader成功地加载了我们为MessageService接口提供的两个实现,并且我们可以在不修改Main类的代码的情况下,通过添加更多的实现类和更新META-INF/services/com.example.MessageService文件来扩展我们的服务。 3. SPI在Spring框架中的应用 Spring官方在其文档和源代码中多次提到了SPI(Service Provider Interface)的概念。但是,当我们说“Spring的SPI”时,通常指的是Spring框架为开发者提供的一套可扩展的接口和抽象类,开发者可以基于这些接口和抽象类实现自己的版本。 在Spring中,SPI的概念与Spring Boot使用的spring.factories文件的机制不完全一样,但是它们都体现了可插拔、可扩展的思想。 Spring的SPI: Spring的核心框架提供了很多接口和抽象类,如BeanPostProcessor, PropertySource, ApplicationContextInitializer等,这些都可以看作是Spring的SPI。开发者可以实现这些接口来扩展Spring的功能。这些接口允许开发者在Spring容器的生命周期的不同阶段介入,实现自己的逻辑。 Spring Boot的spring.factories机制: spring.factories是Spring Boot的一个特性,允许开发者自定义自动配置。通过spring.factories文件,开发者可以定义自己的自动配置类,这些类在Spring Boot启动时会被自动加载。 在这种情况下,SpringFactoriesLoader的使用,尤其是通过spring.factories文件来加载和实例化定义的类,可以看作是一种特定的SPI实现方式,但它特定于Spring Boot。 3.1 传统Spring框架中的SPI思想 在传统的Spring框架中,虽然没有直接使用名为"SPI"的术语,但其核心思想仍然存在。Spring提供了多个扩展点,其中最具代表性的就是BeanPostProcessor。 3.2 Spring Boot中的SPI思想 Spring Boot有一个与SPI相似的机制,但它并不完全等同于Java的标准SPI。 Spring Boot的自动配置机制主要依赖于spring.factories文件。这个文件可以在多个jar中存在,并且Spring Boot会加载所有可见的spring.factories文件。我们可以在这个文件中声明一系列的自动配置类,这样当满足某些条件时,这些配置类会自动被Spring Boot应用。 接下来会展示Spring SPI思想的好例子,但是它与Spring Boot紧密相关。 定义接口 package com.example.demo.service; public interface MessageService { String getMessage(); } 这里会提供两个简单的实现类。 HelloMessageService.java package com.example.demo.service; public class HelloMessageService implements MessageService { @Override public String getMessage() { return "Hello from HelloMessageService!"; } } HiMessageService.java package com.example.demo.service; public class HiMessageService implements MessageService { @Override public String getMessage() { return "Hi from HiMessageService!"; } } 注册服务 在resources/META-INF下创建一个文件名为spring.factories。这个文件里,可以注册MessageService实现类。 com.example.demo.service.MessageService=com.example.demo.service.HelloMessageService,com.example.demo.service.HiMessageService 注意这里com.example.demo.service.MessageService是接口的全路径,而com.example.demo.service.HelloMessageService,com.example.demo.service.HiMessageService是实现类的全路径。如果有多个实现类,它们应当用逗号分隔。 spring.factories文件中的条目键和值之间不能有换行,即key=value形式的结构必须在同一行开始。但是,如果有多个值需要列出(如多个实现类),并且这些值是逗号分隔的,那么可以使用反斜杠(\)来换行。spring.factories 的名称是约定俗成的。如果试图使用一个不同的文件名,那么 Spring Boot 的自动配置机制将不会识别它。 这里spring.factories又可以写为 com.example.demo.service.MessageService=com.example.demo.service.HelloMessageService,\ com.example.demo.service.HiMessageService 直接在逗号后面回车IDEA会自动补全反斜杠,保证键和值之间不能有换行即可。 使用SpringFactoriesLoader来加载服务 package com.example.demo; import com.example.demo.service.MessageService; import org.springframework.core.io.support.SpringFactoriesLoader; import java.util.List; public class DemoApplication { public static void main(String[] args) { List services = SpringFactoriesLoader.loadFactories(MessageService.class, null); for (MessageService service : services) { System.out.println(service.getMessage()); } } } SpringFactoriesLoader.loadFactories的第二个参数是类加载器,此处我们使用默认的类加载器,所以传递null。 这种方式利用了Spring的SpringFactoriesLoader,它允许开发者提供接口的多种实现,并通过spring.factories文件来注册它们。这与JDK的SPI思想非常相似,只是在实现细节上有所不同。这也是Spring Boot如何自动配置的基础,它会查找各种spring.factories文件,根据其中定义的类来初始化和配置bean。 4. SPI在JDBC驱动加载中的应用 数据库驱动的SPI主要体现在JDBC驱动的自动发现机制中。JDBC 4.0 引入了一个特性,允许驱动自动注册到DriverManager。这是通过使用Java的SPI来实现的。驱动jar包内会有一个META-INF/services/java.sql.Driver文件,此文件中包含了该驱动的Driver实现类的全类名。这样,当类路径中有JDBC驱动的jar文件时,Java应用程序可以自动发现并加载JDBC驱动,而无需明确地加载驱动类。 这意味着任何数据库供应商都可以编写其自己的JDBC驱动程序,只要它遵循JDBC驱动程序的SPI,它就可以被任何使用JDBC的Java应用程序所使用。 当我们使用DriverManager.getConnection()获取数据库连接时,背后正是利用SPI机制加载合适的驱动程序。 以下是SPI机制的具体工作方式: 定义服务接口: 在这里,接口已经由Java平台定义,即java.sql.Driver。 为接口提供实现: 各大数据库厂商(如Oracle, MySQL, PostgreSQL等)为其数据库提供了JDBC驱动程序,它们都实现了java.sql.Driver接口。例如,MySQL的驱动程序中有一个类似于以下的类: public class com.mysql.cj.jdbc.Driver implements java.sql.Driver { // 实现接口方法... } 对于MySQL的驱动程序,可以在其JAR文件的META-INF/services目录下找到一个名为java.sql.Driver的文件,文件内容如下:com.mysql.cj.jdbc.Driver 使用SPI来加载和使用服务: 当我们调用DriverManager.getConnection(jdbcUrl, username, password)时,DriverManager会使用ServiceLoader来查找所有已注册的java.sql.Driver实现。然后,它会尝试每一个驱动程序,直到找到一个可以处理给定jdbcUrl的驱动程序。 以下是一个简单的示例,展示如何使用JDBC SPI获取数据库连接: import java.sql.Connection; import java.sql.DriverManager; public class JdbcExample { public static void main(String[] args) { String jdbcUrl = "jdbc:mysql://localhost:3306/mydatabase"; String username = "root"; String password = "password"; try { Connection connection = DriverManager.getConnection(jdbcUrl, username, password); System.out.println("Connected to the database!"); connection.close(); } catch (Exception e) { e.printStackTrace(); } } } 在上述代码中,我们没有明确指定使用哪个JDBC驱动程序,因为DriverManager会自动为我们选择合适的驱动程序。 这种模块化和插件化的机制使得我们可以轻松地为不同的数据库切换驱动程序,只需要更改JDBC URL并确保相应的驱动程序JAR在类路径上即可。 在Spring Boot中,开发者通常不会直接与JDBC的SPI机制交互来获取数据库连接。Spring Boot的自动配置机制隐藏了许多底层细节,使得配置和使用数据库变得更加简单。 一般会在application.properties或application.yml中配置数据库连接信息。 例如: spring.datasource.url=jdbc:mysql://localhost:3306/mydatabase spring.datasource.username=root spring.datasource.password=password spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver 在上述步骤中,Spring Boot的自动配置机制会根据提供的依赖和配置信息来初始化和配置DataSource对象,这个对象管理数据库连接。实际上,添加JDBC驱动依赖时,Spring Boot会使用JDK的SPI机制(在JDBC规范中应用)来找到并加载相应的数据库驱动。开发者虽然不直接与JDK的SPI交互,但在背后Spring Boot确实利用了JDK SPI机制来获取数据库连接。 5. 如何通过Spring Boot自动配置理解SPI思想 这种机制有点类似于Java的SPI,因为它允许第三方库提供一些默认的配置。但它比Java的SPI更为强大和灵活,因为Spring Boot提供了大量的注解(如@ConditionalOnClass、@ConditionalOnProperty、@ConditionalOnMissingBean等)来控制自动配置类是否应该被加载和应用。 总的来说,Spring Boot的spring.factories机制和Java的SPI在概念上是相似的,但它们在实现细节和用途上有所不同。 6. SPI(Service Provider Interface)总结 SPI,即服务提供者接口,是一种特定的设计模式。它允许框架或核心库为第三方开发者提供一个预定义的接口,从而使他们能够为框架提供自定义的实现或扩展。 核心目标: 解耦:SPI机制让框架的核心与其扩展部分保持解耦,使核心代码不依赖于具体的实现。 动态加载:系统能够通过特定的机制(如Java的ServiceLoader)动态地发现和加载所需的实现。 灵活性:框架用户可以根据自己的需求选择、更换或增加新的实现,而无需修改核心代码。 可插拔:第三方提供的服务或实现可以轻松地添加到或从系统中移除,无需更改现有的代码结构。 价值: 为框架或库的用户提供更多的自定义选项和灵活性。 允许框架的核心部分保持稳定,同时能够容纳新的功能和扩展。 SPI与“开闭原则”: “开闭原则”提倡软件实体应该对扩展开放,但对修改封闭。即在不改变现有代码的前提下,通过扩展来增加新的功能。 SPI如何体现“开闭原则”: 对扩展开放:SPI提供了一种标准化的方式,使第三方开发者可以为现有系统提供新的实现或功能。 对修改封闭:添加新的功能或特性时,原始框架或库的代码不需要进行修改。 独立发展:框架与其SPI实现可以独立地进化和发展,互不影响。 总之,SPI是一种使软件框架或库更加模块化、可扩展和可维护的有效方法。通过遵循“开闭原则”,SPI确保了系统的稳定性和灵活性,从而满足了不断变化的业务需求。 -----------------------------------Spring事件监听器的内部逻辑与实现--------回头需要再细看------------------------------- 1. 事件的层次传播 在Spring中,ApplicationContext可以形成一个层次结构,通常由主容器和多个子容器组成。一个常见的疑问是:当一个事件在其中一个容器中发布时,这个事件会如何在这个层次结构中传播? 结论:在Spring的父子容器结构中,事件会从子容器向上传播至其父容器,但父容器中发布的事件不会向下传播至子容器。 这种设计可以帮助开发者在父容器中集中处理所有的事件,而不必担心事件在多个子容器之间的传播。 2. PayloadApplicationEvent的使用 PayloadApplicationEvent是Spring提供的一种特殊事件,用于传递数据(称为"payload")。所以不需要自定义事件,PayloadApplicationEvent可以直接传递任何类型的数据,只需要指定它的类型即可。 3. 为什么选择自定义事件? 虽然PayloadApplicationEvent提供了简化事件监听的能力,但其可能不足以满足特定的业务需求,尤其是当需要更多上下文和数据时。 4. 事件广播原理 4.1 Spring 5.x的事件模型概述 核心概念: ApplicationEvent:这是所有Spring事件的超类。用户可以通过继承此类来创建自定义事件。 ApplicationListener:这是所有事件监听器的接口。它定义了一个onApplicationEvent方法,用于处理特定类型的事件。 ApplicationEventPublisher:这是一个接口,定义了发布事件的方法。ApplicationContext继承了这个接口,因此任何Spring bean都可以发布事件。 ApplicationEventMulticaster:这个组件负责将事件广播到所有匹配的监听器。 事件发布: 用户可以通过ApplicationEventPublisher接口或ApplicationContext来发布事件。通常情况下,当我们在Spring bean中需要发布事件时,可以让这个bean实现ApplicationEventPublisherAware接口,这样Spring容器会注入一个事件发布器。 异步事件: 从Spring 4.2开始,我们可以轻松地使事件监听器异步化。在Spring 5中,这一功能仍然得到支持。只需要在监听器方法上添加@Async注解并确保启用了异步支持。这使得事件处理可以在单独的线程中执行,不阻塞发布者。 泛型事件: Spring 4.2引入了对泛型事件的支持,这在Spring 5中得到了维护。这意味着监听器现在可以根据事件的泛型类型进行过滤。例如,一个ApplicationListener>将只接收到携带String负载的事件。 事件的排序: 监听器可以实现Ordered接口或使用@Order注解来指定事件的执行顺序。 新的事件类型: Spring 5引入了新的事件类型,如ServletRequestHandledEvent,为web请求处理提供更多的钩子。而像ContextRefreshedEvent这样的事件,虽然不是Spring 5新引入的,但它为特定的生命周期回调提供了钩子。 Reactive事件模型: 与Spring 5引入的WebFlux一起,还引入了对反应式编程模型的事件监听和发布的支持。 总结: 在Spring 5.x中,事件模型得到了进一步的增强和优化,增加了对异步、泛型和反应式编程的支持,提供了更强大、灵活和高效的机制来处理应用程序事件。对于开发者来说,这为在解耦的同时实现复杂的业务逻辑提供了便利。 4.2 发布事件publishEvent源码分析 这个方法究竟做了什么? 事件非空检查:为了确保事件对象不为空,进行了初步的断言检查。这是一个常见的做法,以防止无效的事件被广播。 事件类型检查与封装:Spring允许使用任意类型的对象作为事件。如果传入的不是ApplicationEvent的实例,它会使用PayloadApplicationEvent来进行封装。这种设计提供了更大的灵活性。 早期事件的处理:在Spring的生命周期中,ApplicationContext可能还没有完全初始化,这时会有一些早期的事件。如果earlyApplicationEvents不为空,这些事件会被添加到此列表中,稍后再广播。 事件广播:如果ApplicationContext已初始化,事件会被广播给所有的监听器。这是通过ApplicationEventMulticaster完成的,它是Spring中负责事件广播的核心组件。 处理父ApplicationContext:在有些应用中,可以存在父子ApplicationContext。当子容器广播一个事件时,也可以考虑在父容器中广播这个事件。这是为了确保在整个上下文层次结构中的所有感兴趣的监听器都能收到事件。 通过这种方式,Spring的事件发布机制确保了事件在不同的上下文和生命周期阶段都能被正确处理和广播。 上面说到事件广播是ApplicationEventMulticaster完成的,这个是什么?下面来看看 4.3 Spring事件广播:从ApplicationEventMulticaster开始 当我们在Spring中讨论事件,我们实际上是在讨论两件事:事件(即发生的事情)和监听器(即对这些事件感兴趣并作出反应的实体)。 ApplicationEventMulticaster的主要职责是管理事件监听器并广播事件给这些监听器。我们主要关注SimpleApplicationEventMulticaster,因为这是默认的实现,但请注意,Spring允许替换为自定义的ApplicationEventMulticaster。以下是SimpleApplicationEventMulticaster中的相关代码与分析,有兴趣的小伙伴可以自行查看: ResolvableType 在 Spring 中的主要用途是提供了一种方式来解析和操作运行时的泛型类型信息,特别是在事件发布和监听中。 当我们发布一个事件: ApplicationEvent event = new MyCustomEvent(this, "data"); applicationContext.publishEvent(event); Spring 内部会使用 ResolvableType.forInstance(event) 来获取这个事件的类型。然后,它会找到所有注册的监听器,查看它们监听的事件类型是否与此事件匹配(通过比较 ResolvableType)。匹配的监听器会被调用。 为什么 Spring 选择使用 ResolvableType 而不是直接使用 Java 类型?最主要的原因是 Java 的泛型擦除。 在 Java 中,泛型只存在于编译时,一旦代码被编译,泛型信息就会被擦除,运行时就不能直接获取到泛型的实际类型。 为了解决这个问题,Spring 引入了 ResolvableType,一个能够解析泛型类型信息的工具类。 举个例子: 假设有如下的类定义: public class Sample { private List names; } 我们可以这样获取 names 字段的泛型类型: ResolvableType t = ResolvableType.forField(Sample.class.getDeclaredField("names")); Class genericType = t.getGeneric(0).resolve(); // 得到 String.class 4.5 监听器内部逻辑 再来看看监听器内部逻辑,我们来分析在multicastEvent方法中调用的getApplicationListeners(event, type)来分析下 监听器内部做了什么? 在getApplicationListeners方法中,采用了一种优化检索的缓存机制来提高性能并确保线程安全性。 具体分析如下: 首次检查: AbstractApplicationEventMulticaster.CachedListenerRetriever existingRetriever = (AbstractApplicationEventMulticaster.CachedListenerRetriever)this.retrieverCache.get(cacheKey); 这里,我们首先从retrieverCache中检索existingRetriever。 判断是否需要进入同步代码块: if (existingRetriever == null && (this.beanClassLoader == null || ...)) { ... } 如果existingRetriever为空,那么我们可能需要创建一个新的CachedListenerRetriever并放入缓存中。但是,为了确保线程安全性,我们必须在这之前进行进一步的检查。 双重检查: 在创建新的CachedListenerRetriever之前,我们使用了putIfAbsent方法。这个方法会尝试添加一个新值,但如果该值已存在,它只会返回现有的值。该机制采用了一种缓存优化策略:通过ConcurrentMap的putIfAbsent方法,即使多个线程同时到达这个代码段,也确保只有一个线程能够成功地放入新的值,从而保证线程安全性。 newRetriever = new AbstractApplicationEventMulticaster.CachedListenerRetriever(); existingRetriever = (AbstractApplicationEventMulticaster.CachedListenerRetriever)this.retrieverCache.putIfAbsent(cacheKey, newRetriever); 这里的逻辑使用了ConcurrentMap的putIfAbsent方法来确保线程安全性,而没有使用传统的synchronized块。 所以,我们可以说getApplicationListeners中的这部分逻辑采用了一种优化检索的缓存机制。它利用了并发容器的原子性操作putIfAbsent来保证线程安全,而不是依赖于传统的双重检查锁定模式。 总体概括一下,对于getApplicationListeners和retrieveApplicationListeners两个方法的功能可以总结为以下三个步骤: 从默认检索器筛选监听器: 这部分代码直接从defaultRetriever中获取监听器,并检查它们是否支持当前事件。在retrieveApplicationListeners方法中,代码首先从defaultRetriever中获取已经编程式注入的监听器,并检查每个监听器是否支持当前的事件类型。 listeners = new LinkedHashSet(this.defaultRetriever.applicationListeners); for (ApplicationListener listener : listeners) { if (this.supportsEvent(listener, eventType, sourceType)) { ... // 添加到筛选出来的监听器列表 } } 从IOC容器中筛选监听器: 在retrieveApplicationListeners方法中,除了从defaultRetriever中获取已经编程式注入的监听器,代码还会尝试从IOC容器(通过bean名称)获取监听器,并检查它们是否支持当前的事件。 if (!listenerBeans.isEmpty()) { ConfigurableBeanFactory beanFactory = this.getBeanFactory(); for (String listenerBeanName : listenerBeans) { ... // 检查并添加到筛选出来的监听器列表 } } 监听器排序: 最后,为确保监听器按照预定的顺序响应事件,筛选出的所有监听器会经过排序。排序基于Spring的@Order注解或Ordered接口,如AnnotationAwareOrderComparator.sort(allListeners)所示 AnnotationAwareOrderComparator.sort(allListeners); 5. Spring事件传播、异步处理等机制 说明: 容器和事件广播: ApplicationContext 是Spring的应用上下文容器。在图中,我们有一个主容器和一个子容器。 当我们想发布一个事件时,我们调用 publishEvent 方法。 ApplicationEventMulticaster 负责实际地将事件广播到各个监听器。 主容器和子容器关系: 在Spring中,可以有多个容器,其中一个是主容器,其他的则是子容器。 通常,子容器可以访问主容器中的bean,但反之则不行。但在事件传播的上下文中,子容器发布的事件默认不会在主容器中传播。这一点由 Note1 注释标明。 异步处理: 当事件被发布时,它可以被异步地传播到监听器,这取决于是否配置了异步执行器。 是否使用异步执行器? 这个决策点说明了基于配置,事件可以同步或异步地传播到监听器。 事件生命周期: 在Spring容器的生命周期中,有些事件在容器初始化前触发,这些被称为 early events。这些事件会被缓存起来,直到容器初始化完成。 一旦容器初始化完成,这些早期的事件会被处理,并开始处理常规事件。 在容器销毁时,也可能触发事件。 注意事项 (Note1): 这个部分强调了一个特定的行为,即在某些配置下,子容器发布的事件可能也会在主容器中传播,但这并不是默认行为。 -------------------------------------动态代理-------------------------------------------------------------- 动态代理的拦截处理代码可以同时对对象的所有方法生效,不需要像子类那样一一重写父类的方法。同时可以动态传入要代理的对象,也就是说可以对所有对象的所有方法生效。。。这是继承和新实现接口远远做不到的。。。 JDK Proxy::newProxyInstance InvocationHandler::invoke CGLIB Enhancer MethodInterceptor::intercept 1. 背景 动态代理是一种强大的设计模式,它允许开发者在运行时创建代理对象,用于拦截对真实对象的方法调用。这种技术在实现面向切面编程(AOP)、事务管理、权限控制等功能时特别有用,因为它可以在不修改原有代码结构的前提下,为程序动态地注入额外的逻辑。 2. JDK动态代理 2.1 定义和演示 JDK动态代理是Java语言提供的一种基于接口的代理机制,允许开发者在运行时动态地创建代理对象,而无需为每个类编写具体的代理实现。 这种机制主要通过 java.lang.reflect.Proxy 类和 java.lang.reflect.InvocationHandler 接口实现。下面是JDK动态代理的核心要点和如何使用它们的概述。 使用前提:被代理对象有接口。 使用步骤 创建 InvocationHandler 实现:定义一个 InvocationHandler 的实现,这个实现中的 invoke 方法可以包含自定义逻辑。 创建代理对象:使用 Proxy.newProxyInstance 方法,传入目标对象的类加载器、需要代理的接口数组以及 InvocationHandler 的实现,来创建一个实现了指定接口的代理对象。 InvocationHandler 是动态代理的核心接口之一,当我们使用动态代理模式创建代理对象时,任何对代理对象的方法调用都会被转发到一个实现了 InvocationHandler 接口的实例的 invoke 方法上。 我们经常看到InvocationHandler 动态代理的匿名内部类,这会在代理对象的相应方法被调用时执行。具体地说,每当对代理对象执行方法调用时,调用的方法不会直接执行,而是转发到实现了InvocationHandler 的 invoke 方法上。在这个 invoke 方法内部,我们可以定义拦截逻辑、调用原始对象的方法、修改返回值等操作。 在这个例子中,当调用 proxyInstance.sayHello() 方法时,实际上执行的是 InvocationHandler 的匿名内部类中的 invoke 方法。这个方法中,我们可以在调用实际对象的 sayHello 方法前后添加自定义逻辑(比如这里的打印消息)。这就是动态代理和 InvocationHandler 的工作原理。 我们来看关键的一句代码 Object result = method.invoke(realObject, args); 在Java的动态代理中,method.invoke(realObject, args) 这句代码扮演着核心的角色,因为它实现了代理对象方法调用的转发机制。下面分别解释一下这行代码的两个主要部分:method.invoke() 方法和 args 参数。 method.invoke(realObject, args) 作用:这行代码的作用是调用目标对象(realObject)的具体方法。在动态代理的上下文中,invoke 方法是在代理实例上调用方法时被自动调用的。通过这种方式可以在实际的方法执行前后加入自定义的逻辑,比如日志记录、权限检查等。 method:method 是一个 java.lang.reflect.Method 类的实例,代表了正在被调用的方法。在 invoke 方法中,这个对象用来标识代理对象上被调用的具体方法。 注意:如果尝试直接在invoke方法内部使用method.invoke(proxy, args)调用代理对象的方法,而不是调用原始目标对象的方法,则会导致无限循环。这是因为调用proxy实例上的方法会再次被代理拦截,从而无限调用invoke方法,传参可别传错了。 invoke:Method 类的 invoke 方法用于执行指定方法。第一个参数是指明方法应该在哪个对象上调用(在这个例子中是 realObject),后续的参数 args 是调用方法时传递的参数。 args 定义:args 是一个对象数组,包含了调用代理方法时传递给方法的参数值。如果被调用的方法没有参数,args 将会是 null 或者空数组。 作用:args 允许在 invoke 方法内部传递参数给实际要执行的方法。这意味着可以在动态代理中不仅控制是否调用某个方法,还可以修改调用该方法时使用的参数。 通过在 invoke 方法中加入这些逻辑,我们能够在不修改原有业务代码的情况下,为系统添加复杂的控制和监控功能。如果到达流量阈值或系统处于熔断状态,可以阻止对后端服务的进一步调用,直接返回一个默认值或错误响应,避免系统过载。 3. CGLIB动态代理 CGLIB(Code Generation Library)是一个强大的高性能代码生成库,它在运行时动态生成新的类。与JDK动态代理不同,CGLIB能够代理那些没有实现接口的类。这使得CGLIB成为那些因为设计限制或其他原因不能使用接口的场景的理想选择。 3.1 定义和演示 工作原理 CGLIB通过继承目标类并在运行时生成子类来实现动态代理。代理类覆盖了目标类的非final方法,并在调用方法前后提供了注入自定义逻辑的能力。这种方法的一个关键优势是它不需要目标对象实现任何接口。 使用CGLIB的步骤 创建MethodInterceptor:实现MethodInterceptor接口,这是CGLIB提供的回调类型,用于定义方法调用的拦截逻辑。 生成代理对象:使用Enhancer类来创建代理对象。Enhancer是CGLIB中用于生成新类的类。 CGLIB vs JDK动态代理 接口要求:JDK动态代理只能代理实现了接口的对象,而CGLIB能够直接代理类。 性能:CGLIB在生成代理对象时通常比JDK动态代理要慢,因为它需要动态生成新的类。但在调用代理方法时,CGLIB通常会提供更好的性能。 方法限制:CGLIB不能代理final方法,因为它们不能被子类覆盖。 CGLIB是一个强大的工具,特别适用于需要代理没有实现接口的类的场景。然而,选择JDK动态代理还是CGLIB主要取决于具体的应用场景和性能要求。 注意:在CGLIB中,如果使用MethodProxy.invoke(obj, args) ,而不是MethodProxy.invokeSuper(obj, args),并且obj是代理实例本身(CGLIB通过Enhancer创建的代理对象,而不是原始的被代理的目标对象),就会导致无限循环。invoke方法实际上是尝试在传递的对象上调用方法,如果该对象是代理对象,则调用会再次被拦截,造成无限循环。 在JDK动态代理中,确保调用method.invoke时使用的是目标对象,而不是代理对象。 在CGLIB代理中,使用MethodProxy.invokeSuper而不是MethodProxy.invoke来调用被代理的方法,以避免无限循环。 选择建议 如果类已经实现了接口,或者希望强制使用接口编程,那么JDK动态代理是一个好选择。 如果需要代理没有实现接口的类,或者对性能有较高的要求,特别是在代理方法的调用上,CGLIB可能是更好的选择。 在现代的Java应用中,很多框架(如Spring)都提供了对这两种代理方式的透明支持,并且可以根据实际情况自动选择使用哪一种。例如,Spring AOP默认会使用JDK动态代理,但如果遇到没有实现接口的类,它会退回到CGLIB。 6. 动态代理的实际应用场景 面向切面编程(AOP): 问题解决:在不改变原有业务逻辑代码的情况下,为程序动态地添加额外的行为(如日志记录、性能监测、事务管理等)。 应用实例:Spring AOP 使用动态代理为方法调用提供了声明式事务管理、安全性检查和日志记录等服务。根据目标对象是否实现接口,Spring AOP可以选择使用JDK动态代理或CGLIB代理。 事务管理: 问题解决:自动化处理数据库事务的边界,如开始、提交或回滚事务。 应用实例:Spring框架中的声明式事务管理使用代理技术拦截那些被@Transactional注解标记的类或方法,确保方法执行在正确的事务管理下进行。 权限控制和安全性: 问题解决:在执行敏感操作之前自动检查用户权限,确保只有拥有足够权限的用户才能执行某些操作。 应用实例:企业应用中,使用代理技术拦截用户的请求,进行权限验证后才允许访问特定的服务或执行操作。 延迟加载: 问题解决:对象的某些属性可能加载成本较高,通过代理技术,可以在实际使用这些属性时才进行加载。 应用实例:Hibernate和其他ORM框架使用代理技术实现了延迟加载(懒加载),以提高应用程序的性能和资源利用率。 服务接口调用的拦截和增强: 问题解决:对第三方库或已有服务进行包装,添加额外的逻辑,如缓存结果、参数校验等。 应用实例:在微服务架构中,可以使用代理技术对服务客户端进行增强,实现如重试、熔断、限流等逻辑。 在现代框架中的应用 Spring框架:Spring的AOP模块和事务管理广泛使用了动态代理技术。根据目标对象的类型(是否实现接口),Spring可以自动选择JDK动态代理或CGLIB代理。 Hibernate:Hibernate使用动态代理技术实现懒加载,代理实体类的关联对象,在实际访问这些对象时才从数据库中加载它们的数据。 MyBatis:MyBatis框架使用动态代理技术映射接口和SQL语句,允许开发者通过接口直接与数据库交互,而无需实现类。 ------------------------------------------------AOP------------------------------------------------------------- 1. 背景 在现代软件开发中,面向切面编程(AOP)是一种强大的编程范式,允许开发者跨越应用程序的多个部分定义横切关注点(如日志记录、事务管理等),在不改变主业务逻辑的情况下增强应用程序的功能。 2. 基于AspectJ注解来实现AOP 定义切面 创建切面类MyAspect.java,并使用注解定义切面和通知: package com.example.demo.aop; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.springframework.stereotype.Component; @Aspect @Component public class MyAspect { @Before("execution(* com.example.demo.aop.MyServiceImpl.performAction(..))") public void logBeforeAction() { System.out.println("Before performing action"); } }  @Aspect注解是用来标识一个类作为AspectJ切面的一种方式,这在基于注解的AOP配置中是必需的。它相当于XML配置中定义切面的方式,但使用注解可以更加直观和便捷地在类级别上声明切面,而无需繁琐的XML配置。 配置Spring以启用注解和AOP 通过@EnableAspectJAutoProxy启用AspectJ自动代理  @EnableAspectJAutoProxy注解在Spring中用于启用对AspectJ风格的切面的支持。它告诉Spring框架去寻找带有@Aspect注解的类,并将它们注册为Spring应用上下文中的切面,以便在运行时通过代理方式应用这些切面定义的通知(Advice)和切点(Pointcuts)。 如果不写@EnableAspectJAutoProxy,Spring将不会自动处理@Aspect注解定义的切面,则定义的那些前置通知(@Before)、后置通知(@After、@AfterReturning、@AfterThrowing)和环绕通知(@Around)将不会被自动应用到目标方法上。这意味着定义的AOP逻辑不会被执行,失去了AOP带来的功能增强。 @Before 注解定义了一个前置通知(Advice),它会在指定方法执行之前运行。切点表达式execution(* com.example.demo.aop.MyServiceImpl.performAction(..))精确地定义了这些连接点的位置。在这个例子中,切点表达式指定了MyServiceImpl类中的performAction方法作为连接点,而@Before注解标识的方法(logBeforeAction)将在这个连接点之前执行,即logBeforeAction方法(前置通知)会在performAction执行之前被执行。 4. AOP通知讲解 在Spring AOP中,通知(Advice)定义了切面(Aspect)在目标方法调用过程中的具体行为。Spring AOP支持五种类型的通知,它们分别是:前置通知(Before)、后置通知(After)、返回通知(After Returning)、异常通知(After Throwing)和环绕通知(Around)。通过使用这些通知,开发者可以在目标方法的不同执行点插入自定义的逻辑。 @Before(前置通知) 前置通知是在目标方法执行之前执行的通知,通常用于执行一些预处理任务,如日志记录、安全检查等。 @Before("execution(* com.example.demo.aop.MyServiceImpl.performAction(..))") public void logBeforeAction() { System.out.println("Before performing action"); } @AfterReturning(返回通知) 返回通知在目标方法成功执行之后执行,可以访问方法的返回值。 @AfterReturning(pointcut = "execution(* com.example.demo.aop.MyServiceImpl.performAction(..))", returning = "result") public void logAfterReturning(Object result) { System.out.println("Method returned value is : " + result); } 这里在@AfterReturning注解中指定returning = "result"时,Spring AOP框架将目标方法的返回值传递给切面方法的名为result的参数,因此,切面方法需要有一个与之匹配的参数,类型兼容目标方法的返回类型。如果两者不匹配,Spring在启动时会抛出异常,因为它无法将返回值绑定到切面方法的参数。 @AfterThrowing(异常通知) 异常通知在目标方法抛出异常时执行,允许访问抛出的异常。 @AfterThrowing(pointcut = "execution(* com.example.demo.aop.MyServiceImpl.performAction(..))", throwing = "ex") public void logAfterThrowing(JoinPoint joinPoint, Throwable ex) { String methodName = joinPoint.getSignature().getName(); System.out.println("@AfterThrowing: Exception in method: " + methodName + "; Exception: " + ex.toString()); } 在@AfterThrowing注解的方法中包含JoinPoint参数是可选的,当想知道哪个连接点(即方法)引发了异常的详细信息时非常有用,假设有多个方法可能抛出相同类型的异常,而我们想在日志中明确指出是哪个方法引发了异常。通过访问JoinPoint提供的信息,可以记录下引发异常的方法名称和其他上下文信息,从而使得日志更加清晰和有用。 @After(后置通知) 后置通知在目标方法执行之后执行,无论方法执行是否成功,即便发生异常,仍然会执行。它类似于finally块。 @After("execution(* com.example.demo.aop.MyServiceImpl.performAction(..))") public void logAfter() { System.out.println("After performing action"); } @Around(环绕通知) 环绕通知围绕目标方法执行,可以在方法调用前后执行自定义逻辑,同时决定是否继续执行目标方法。环绕通知提供了最大的灵活性和控制力。 @Around("execution(* com.example.demo.aop.MyServiceImpl.performAction(..))") public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable { System.out.println("Before method execution"); Object result = joinPoint.proceed(); // 继续执行目标方法 System.out.println("After method execution"); return result; } 这里要强调几点: @Around环绕通知常见用例是异常捕获和重新抛出。在这个例子中,我们通过ProceedingJoinPoint的proceed()方法调用目标方法。如果目标方法执行成功,记录执行后的消息并返回结果。如果在执行过程中发生异常,在控制台上打印出异常信息,然后重新抛出这个异常。这样做可以确保异常不会被吞没,而是可以被上层调用者捕获或由其他异常通知处理。 @AfterThrowing注解标明这个通知只有在目标方法因为异常而终止时才会执行。throwing属性指定了绑定到通知方法参数上的异常对象的名称。这样当异常发生时,异常对象会被传递到afterThrowingAdvice方法中,方法中可以对异常进行记录或处理。 @AfterThrowing和@AfterReturning通知不会在同一个方法调用中同时执行。这两个通知的触发条件是互斥的。@AfterReturning 通知只有在目标方法成功执行并正常返回后才会被触发,这个通知可以访问方法的返回值。@AfterThrowing 通知只有在目标方法抛出异常时才会被触发,这个通知可以访问抛出的异常对象。 假设想要某个逻辑总是在方法返回时执行,不管是抛出异常还是正常返回,则考虑放在@After或者@Around通知里执行。 5. AOP时序 客户端调用方法: 客户端(Client)发起对某个方法的调用。这个调用首先被AOP代理(AOP Proxy)接收,这是因为在Spring AOP中,代理负责在真实对象(Target)和外界之间进行中介。 环绕通知开始 (@Around): AOP代理首先调用切面(Aspect)中定义的环绕通知的开始部分。环绕通知可以在方法执行前后执行代码,并且能决定是否继续执行方法或直接返回自定义结果。这里的“开始部分”通常包括方法执行前的逻辑。 前置通知 (@Before): 在目标方法执行之前,执行前置通知。这用于在方法执行前执行如日志记录、安全检查等操作。 执行目标方法: 如果环绕通知和前置通知没有中断执行流程,代理会调用目标对象(Target)的实际方法。 方法完成: 方法执行完成后,控制权返回到AOP代理。这里的“完成”可以是成功结束,也可以是抛出异常。 返回通知或异常通知: 返回通知 (@AfterReturning):如果方法成功完成,即没有抛出异常,执行返回通知。这可以用来处理方法的返回值或进行某些后续操作。 异常通知 (@AfterThrowing):如果方法执行过程中抛出异常,执行异常通知。这通常用于异常记录或进行异常处理。 后置通知 (@After): 独立于方法执行结果,后置通知总是会执行。这类似于在编程中的finally块,常用于资源清理。 环绕通知结束 (@Around): 在所有其他通知执行完毕后,环绕通知的结束部分被执行。这可以用于执行清理工作,或者在方法执行后修改返回值。 返回结果: 最终,AOP代理将处理的结果返回给客户端。这个结果可能是方法的返回值,或者通过环绕通知修改后的值,或者是异常通知中处理的结果。 -----------------------------------------------------Spring Boot 编程模式-------------------------------- Spring Boot中用到的编程模式主要包括以下几种‌: ‌单例模式(Singleton Pattern)‌:在Spring中,bean默认是单例模式,即每个bean只有一个实例,并且全局共享。这种模式确保了全局共享且只需一个实例的配置或服务‌12。 ‌工厂模式(Factory Pattern)‌:Spring Boot通过@Bean注解在配置类中实现工厂方法,用于创建对象而不指定具体的创建类。这种模式在创建各种Bean对象时非常有用,可以根据条件或配置返回不同的实例‌12。 ‌代理模式(Proxy Pattern)‌:Spring AOP(面向切面编程)功能使用代理模式,在不修改原始代码的情况下,实现对方法的拦截和增强。这种模式常用于事务管理、日志记录等场景‌23。 ‌模板方法模式(Template Method Pattern)‌:Spring的JdbcTemplate、RestTemplate等模板类使用模板方法模式,提供一个通用的方法骨架,并将具体实现细节留给子类。这种模式简化了重复代码,提高了代码复用性‌3。 ‌策略模式(Strategy Pattern)‌:Spring的Resource接口使用策略模式,根据不同的资源类型使用不同的资源加载策略。这种模式允许在运行时选择算法或策略‌3。 ‌观察者模式(Observer Pattern)‌:Spring事件监听机制使用观察者模式,当某个事件发生时,通知所有注册的监听器。这种模式常用于事件驱动编程‌3。 ‌适配器模式(Adapter Pattern)‌:Spring的HttpMessageConverter接口实现了适配器模式,将不同的数据类型转换为HTTP消息。这种模式用于解决接口不兼容的问题‌3。 ‌原型模式(Prototype Pattern)‌:Spring通过prototype作用域允许Bean的原型复制,实现每次请求都创建一个新的实例。这种模式适用于每次都需要独立实例的场景‌3。 ‌外观模式(Facade Pattern)‌:Spring的ApplicationContext提供了一个简化的外观接口,使得用户可以方便地访问和管理应用程序的组件。这种模式简化了子系统之间的交互‌3。 ‌装饰器模式(Decorator Pattern)‌:Spring的DataSource接口使用装饰器模式,可以在不修改原始代码的情况下,为数据源添加额外的功能。这种模式常用于添加额外的功能或修改行为‌3。 这些设计模式在Spring Boot中的应用极大地提高了代码的可维护性、可扩展性和复用性,使得开发过程更加高效和灵活。 -------------------------------------------------------------------------------------------------------------------------------- ------------------------------------Spring Boot启动过程----------------------------------------------------------- SpringApplication() this.bootstrapRegistryInitializers = new ArrayList<>(getSpringFactoriesInstances(BootstrapRegistryInitializer.class)); setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class)); setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class)); SpringApplication.run() createBootstrapContext this.bootstrapRegistryInitializers.forEach((initializer) -> initializer.initialize(bootstrapContext)); SpringApplicationRunListeners listeners = getRunListeners(args); listeners = getSpringFactoriesInstances(SpringApplicationRunListener.class, listeners.starting prepareEnvironment printBanner context = createApplicationContext(); prepareContext(bootstrapContext, context, applyInitializers(context); listeners.contextPrepared bootstrapContext.close Set sources = getAllSources(); load(context, sources.toArray(new Object[0])); BeanDefinitionLoader loader = createBeanDefinitionLoader(getBeanDefinitionRegistry(context), sources); this.annotatedReader = new AnnotatedBeanDefinitionReader(registry); AnnotationConfigUtils.registerAnnotationConfigProcessors(this.registry); new RootBeanDefinition(ConfigurationClassPostProcessor.class); new RootBeanDefinition(AutowiredAnnotationBeanPostProcessor.class); new RootBeanDefinition(CommonAnnotationBeanPostProcessor.class); new RootBeanDefinition(EventListenerMethodProcessor.class); new RootBeanDefinition(DefaultEventListenerFactory.class); this.scanner = new ClassPathBeanDefinitionScanner(registry); loader.setBeanNameGenerator(this.beanNameGenerator); loader.setResourceLoader(this.resourceLoader); loader.setEnvironment(this.environment); loader.load();//此行注册了主类bean listeners.contextLoaded refreshContext(context); postProcessBeanFactory this.scanner.scan this.reader.register invokeBeanFactoryPostProcessors回调 registerBeanPostProcessors initApplicationEventMulticaster registerListeners getApplicationEventMulticaster().addApplicationListener(listener); getApplicationEventMulticaster().multicastEvent(earlyEvent); finishBeanFactoryInitialization beanFactory.preInstantiateSingletons(); finishRefresh publishEvent(new ContextRefreshedEvent(this)); listeners.started callRunners 感觉BootstrapContext存在的作用只是给SpringApplicationRunListener提供事件广播支持。而SpringApplicationRunListener用于记录Application整个启动过程。 在 prepareContext 方法中,SpringBoot会把主类注册到Spring容器中,为什么要这么做昵?主类上的注解 @SpringBootApplication 需要 ConfigurationClassPostProcessor 解析,才能发挥@Import,@ComponentScan的作用,想要 ConfigurationClassPostProcessor 处理主类的前提是主类的BeanDefinition需要在Spring容器中。 invokeBeanFactoryPostProcessors回调processor,而ConfigurationClassPostProcessor回调函数调用processConfigBeanDefinitions()函数创建了ConfigurationClassParser对象,执行parse解析出了注解bean,生成了beandefinition。 我们看到在prepareContext阶段new了5个RootBeanDefinition用来解析注解代码生成beandefinition,这5个其中只有ConfigurationClassPostProcessor实现了BeanDefinitionRegistryPostProcessor接口,所以后续在invokeBeanFactoryPostProcessors阶段最先被回调。代码如下: // PostProcessorRegistrationDelegate类中。。。 // First, invoke the BeanDefinitionRegistryPostProcessors that implement PriorityOrdered. String[] postProcessorNames = beanFactory.getBeanNamesForType(BeanDefinitionRegistryPostProcessor.class, true, false); ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ package org.springframework.context.annotation; public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPostProcessor, BeanRegistrationAotProcessor, BeanFactoryInitializationAotProcessor, PriorityOrdered, ResourceLoaderAware, ApplicationStartupAware, BeanClassLoaderAware, EnvironmentAware { +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {//这两个回调函数都会调用processConfigBeanDefinitions processConfigBeanDefinitions(registry); postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {//这两个回调函数都会调用processConfigBeanDefinitions processConfigBeanDefinitions((BeanDefinitionRegistry) beanFactory); +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) { 。。。。。。 ConfigurationClassParser parser = new ConfigurationClassParser( this.metadataReaderFactory, this.problemReporter, this.environment, this.resourceLoader, this.componentScanBeanNameGenerator, registry); Set candidates = new LinkedHashSet<>(configCandidates); Set alreadyParsed = new HashSet<>(configCandidates.size()); do { StartupStep processConfig = this.applicationStartup.start("spring.context.config-classes.parse"); parser.parse(candidates);//此行解析出了注解bean,生成了beandefinition ---------------------------------------------知识点---------------------------------------------------------- @ImportResource("classpath:config/some-context.xml")`classpath:application.properties`src/main/resources`src/main/java`target/classes context.getBean("beanName",ClassType)`@Resource`@Autowired`@Qualifier`@Inject`@Named`@Value @SpringBootApplication`@Configuration`@EnableAutoConfiguration`@ComponentScan @PropertySource`env.getProperty("...")`@ConfigurationPropertie`@EnableConfigurationProperties`@Configuration`spring.config.name`spring.config.location ${}`Spring Expression Language(SpEL)`#{} @Scope(BeanDefinition.SCOPE_PROTOTYPE)`@Scope("prototype")`singleton`prototype`request`session`application`websocket @Bean(initMethod = "init", destroyMethod = "destroy")`初始化顺序:@PostConstruct → InitializingBean → init-method。销毁顺序:@PreDestroy → DisposableBean → destroy-method ApplicationListener`@EventListener`EventObject`ApplicationEvent`ApplicationContextEvent`ContextRefreshedEvent`ContextClosedEvent`ContextStartedEvent`ContextStoppedEvent`事件源(Event Source)`Lifecycle`@Order @Import`ImportSelector`@Import(AutoConfigurationImportSelector.class)`Class.getName()`ImportBeanDefinitionRegistrar`registerBeanDefinitions @Profile`@Conditional`@ConditionalOnProperty`@ConditionalOnClass`@ConditionalOnMissingClass`@ConditionalOnBean`@ConditionalOnMissingBean`setActiveProfiles`register`refresh`ConfigurableEnvironment.setActiveProfiles`-Dspring.profiles.active`SPRING_PROFILES_ACTIVE`SpringApplicationBuilder.profiles`SpringApplication.setDefaultProperties`spring.profiles.active @ComponentScan`@Component`@Repository`@Service`@Controller`@RestController`@Configuration`useDefaultFilters`includeFilters`excludeFilters`内省机制(Introspection)`beanName:@Bean-getBook,@Import-site.zhangzhuo.learn_springboot.Book,@Component-book BeanDefinition`Class`Name`Scope`Constructor arguments`Properties`Autowiring Mode`Initialization Method and Destroy Method`Dependency beans`RootBeanDefinition`AttributeAccessor BeanDefinitionRegistry`org.springframework.beans.factory.support`BeanFactory`DefaultListableBeanFactory`context.getBeansOfType(Ink.class) BeanDefinitionRegistryPostProcessor`BeanFactoryPostProcessor`BeanPostProcessor` SPI (Service Provider Interface) `JDK原生的SPI`META-INF/services/`ServiceLoader.load`Spring的SPI`SpringFactoriesLoader.loadFactories(MessageService.class, null);`实现接口和抽象类扩展Spring的功能`spring.factories`public class com.mysql.cj.jdbc.Driver implements java.sql.Driver`DriverManager.getConnection`开闭原则 Spring事件监听器`PayloadApplicationEvent`ApplicationEvent`ApplicationEvent<>`ApplicationListener`ApplicationEventPublisher`ApplicationEventMulticaster`ApplicationEventPublisherAware`@Async`@Order`ServletRequestHandledEvent`WebFlux`ResolvableType 动态代理`JDK`Proxy::newProxyInstance`InvocationHandler::invoke`CGLIB`Enhancer`MethodInterceptor::intercept AOP`AspectJ`@Aspect`@EnableAspectJAutoProxy`@Around`@Before`@AfterReturning、@AfterThrowing`@After、@Around`Pointcuts`Advice` Spring Boot`prepareContext`BeanDefinitionLoader`new RootBeanDefinition(ConfigurationClassPostProcessor.class)`refreshContext`invokeBeanFactoryPostProcessors