Spring的那些开发小技巧(中)
创始人
2025-05-30 01:50:58
0

BeanPostProcessor

BeanPostProcessor,中文名 Bean的后置处理器,在Bean创建的过程中起作用。

BeanPostProcessor是Bean在创建过程中一个非常重要的扩展点,因为每个Bean在创建的各个阶段,都会回调BeanPostProcessor及其子接口的方法,传入正在创建的Bean对象,这样如果想对Bean创建过程中某个阶段进行自定义扩展,那么就可以自定义BeanPostProcessor来完成。

说得简单点,BeanPostProcessor就是在Bean创建过程中留的口子,通过这个口子可以对正在创建的Bean进行扩展。只不过Bean创建的阶段比较多,然后BeanPostProcessor接口以及他的子接口InstantiationAwareBeanPostProcessor、DestructionAwareBeanPostProcessor就提供了很多方法,可以使得在不同的阶段都可以拿到正在创建的Bean进行扩展。

来个Demo

现在需要实现一个这样的需求,如果Bean的类型是User,那么就设置这个对象的username属性为 ”三友的java日记“。

那么就可以这么写:

public class UserBeanPostProcessor implements BeanPostProcessor {@Overridepublic Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {if (bean instanceof User) {//如果当前的Bean的类型是 User ,就把这个对象 username 的属性赋值为 三友的java日记((User) bean).setUsername("三友的java日记");}return bean;}}

测试:

public class Application {public static void main(String[] args) {AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();//将 UserBeanPostProcessor 和  User 注册到容器中applicationContext.register(UserBeanPostProcessor.class);applicationContext.register(User.class);applicationContext.refresh();User user = applicationContext.getBean(User.class);System.out.println("获取到的Bean为" + user + ",属性username值为:" + user.getUsername());}}

测试结果:

获取到的Bean为com.sanyou.spring.extension.User@21a947fe,属性username值为:三友的java日记

从结果可以看出,每个生成的Bean在执行到某个阶段的时候,都会回调UserBeanPostProcessor,然后UserBeanPostProcessor就会判断当前创建的Bean的类型,如果是User类型,那么就会将username的属性设置为 ”三友的java日记“。

Spring内置的BeanPostProcessor

这里我列举了常见的一些BeanPostProcessor的实现以及它们的作用

通过列举的这些BeanPostProcessor的实现可以看出,Spring Bean的很多注解的处理都是依靠BeanPostProcessor及其子类的实现来完成的,这也回答了上一小节的疑问,处理@Autowired、@PostConstruct、@PreDestroy注解是如何起作用的,其实就是通过BeanPostProcessor,在Bean的不同阶段来调用对应的方法起作用的。

BeanPostProcessor在Dubbo中的使用

在Dubbo中可以通过@DubboReference(@Reference)来引用生产者提供的接口,这个注解的处理也是依靠ReferenceAnnotationBeanPostProcessor,也就是 BeanPostProcessor 的扩展来实现的。

public class ReferenceAnnotationBeanPostProcessor extends AbstractAnnotationBeanPostProcessor implements ApplicationContextAware, BeanFactoryPostProcessor {// 忽略
}

当Bean在创建的某一阶段,走到了ReferenceAnnotationBeanPostProcessor这个类,就会根据反射找出这个类有没有@DubboReference(@Reference)注解,有的话就构建一个动态搭理注入就可以了。

BeanPostProcessor在Spring Bean的扩展中扮演着重要的角色,是Spring Bean生命周期中很重要的一部分。正是因为有了BeanPostProcessor,你就可以在Bean创建过程中的任意一个阶段扩展自己想要的东西。

BeanFactoryPostProcessor

通过上面一节我们知道 BeanPostProcessor 是对Bean的处理,那么BeanFactoryPostProcessor很容易就猜到是对BeanFactory,也就是Spring容器的处理。

举个例子,如果我们想禁止循环依赖,那么就可以这么写。

public class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor {@Overridepublic void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {// 禁止循环依赖((DefaultListableBeanFactory) beanFactory).setAllowCircularReferences(false);}}

后面只需要将注入到Spring容器中就会生效。

BeanFactoryPostProcessor是可以对Spring容器做处理的,方法的入参就是Spring的容器,通过这个接口,就对容器进行为所欲为的操作。

Spring SPI机制

SPI全称为 (Service Provider Interface),是一种动态替换发现的机制,一种解耦非常优秀的思想,SPI可以很灵活的让接口和实现分离, 让api提供者只提供接口,第三方来实现,然后可以使用配置文件的方式来实现替换或者扩展,在框架中比较常见,提高框架的可扩展性。

JDK有内置的SPI机制的实现ServiceLoader,Dubbo也有自己的SPI机制的实现ExtensionLoader,但是这里我们都不讲。。

SpringFactoriesLoader

Spring的SPI机制规定,配置文件必须在classpath路径下的META-INF文件夹内,文件名必须为spring.factories,文件内容为键值对,一个键可以有多个值,只需要用逗号分割就行,同时键值都需要是类的全限定名。但是键和值可以没有任何关系,当然想有也可以有。

show me the code

这里我自定义一个类,MyEnableAutoConfiguration作为键,值就是User

public class MyEnableAutoConfiguration {
}

spring.factories文件

com.sanyou.spring.extension.spi.MyEnableAutoConfiguration=com.sanyou.spring.extension.User

然后放在META-INF底下

测试:

public class Application {public static void main(String[] args) {List classNames = SpringFactoriesLoader.loadFactoryNames(MyEnableAutoConfiguration.class, MyEnableAutoConfiguration.class.getClassLoader());classNames.forEach(System.out::println);}}

结果:

com.sanyou.spring.extension.User

可以看出,通过SpringFactoriesLoader的确可以从spring.factories文件中拿到MyEnableAutoConfiguration键对应的值。

到这你可能说会,这SPI机制也没啥用啊。的确,我这个例子比较简单,拿到就是遍历,但是在Spring中,如果Spring在加载类的话使用SPI机制,那我们就可以扩展,接着往下看。

SpringBoot启动扩展点

SpringBoot项目在启动的过程中有很多扩展点,这里就来盘点一下几个常见的扩展点。

1、自动装配

说到SpringBoot的扩展点,第一时间肯定想到的就是自动装配机制,面试贼喜欢问,但是其实就是一个很简单的东西。当项目启动的时候,会去从所有的spring.factories文件中读取@EnableAutoConfiguration键对应的值,拿到配置类,然后根据一些条件判断,决定哪些配置可以使用,哪些不能使用。

spring.factories文件?键值?不错,自动装配说白了就是SPI机制的一种运用场景。

@EnableAutoConfiguration注解:

@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {//忽略
}

我擦,这个注解也是使用@Import注解,而且配置类还实现了ImportSelector接口,跟前面也都对上了。在SpringBoot中,@EnableAutoConfiguration是通过@SpringBootApplication来使用的。

在AutoConfigurationImportSelector中还有这样一段代码

所以,这段代码也明显地可以看出,自动装配也是基于SPI机制实现的。

那么我想实现自动装配怎么办呢?很简单,只需两步。

第一步,写个配置类:

@Configuration
public class UserAutoConfiguration {@Beanpublic UserFactoryBean userFactoryBean() {return new UserFactoryBean();}}

这里我为了跟前面的知识有关联,配置了一个UserFactoryBean。

第二步,往spring.factories文件配置一下

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.sanyou.spring.extension.springbootextension.UserAutoConfiguration

到这就已经实现了自动装配的扩展。

接下来进行测试:

@SpringBootApplication
public class Application {public static void main(String[] args) {ConfigurableApplicationContext applicationContext = SpringApplication.run(Application.class);User user = applicationContext.getBean(User.class);System.out.println("获取到的Bean为" + user);}}

运行结果:

调用 UserFactoryBean 的 getObject 方法生成 Bean:com.sanyou.spring.extension.User@3406472c
获取到的Bean为com.sanyou.spring.extension.User@3406472c

从运行结果可以看出,自动装配起了作用,并且虽然往容器中注入的Bean的class类型为UserFactoryBean,但是最终会调用UserFactoryBean的getObject的实现获取到User对象。

自动装配机制是SpringBoot的一个很重要的扩展点,很多框架在整合SpringBoot的时候,也都通过自动装配来的,实现项目启动,框架就自动启动的,这里我举个Mybatis整合SpringBoot。

2、PropertySourceLoader

PropertySourceLoader,这是干啥的呢?

我们都知道,在SpringBoot环境下,外部化的配置文件支持properties和yaml两种格式。但是,现在不想使用properties和yaml格式的文件,想使用json格式的配置文件,怎么办?

当然是基于该小节讲的PropertySourceLoader来实现的。

public interface PropertySourceLoader {//可以支持哪种文件格式的解析String[] getFileExtensions();// 解析配置文件,读出内容,封装成一个PropertySource结合返回回去List> load(String name, Resource resource) throws IOException;}

对于PropertySourceLoader的实现,SpringBoot两个实现

PropertiesPropertySourceLoader:可以解析properties或者xml结尾的配置文件

YamlPropertySourceLoader:解析以yml或者yaml结尾的配置文件

所以可以看出,要想实现json格式的支持,只需要自己实现可以用来解析json格式的配置文件的PropertySourceLoader就可以了。

动手来一个。

实现可以读取json格式的配置文件

实现这个功能,只需要两步就可以了

第一步:自定义一个PropertySourceLoader

JsonPropertySourceLoader,实现PropertySourceLoader接口

public class JsonPropertySourceLoader implements PropertySourceLoader {@Overridepublic String[] getFileExtensions() {//这个方法表明这个类支持解析以json结尾的配置文件return new String[]{"json"};}@Overridepublic List> load(String name, Resource resource) throws IOException {ReadableByteChannel readableByteChannel = resource.readableChannel();ByteBuffer byteBuffer = ByteBuffer.allocate((int) resource.contentLength());//将文件内容读到 ByteBuffer 中readableByteChannel.read(byteBuffer);//将读出来的字节转换成字符串String content = new String(byteBuffer.array());// 将字符串转换成 JSONObjectJSONObject jsonObject = JSON.parseObject(content);Map map = new HashMap<>(jsonObject.size());//将 json 的键值对读出来,放入到 map 中for (String key : jsonObject.keySet()) {map.put(key, jsonObject.getString(key));}return Collections.singletonList(new MapPropertySource("jsonPropertySource", map));}}
第二步:配置PropertySourceLoader

JsonPropertySourceLoader 已经有了,那么怎么用呢?当然是SPI机制了,SpringBoot对于配置文件的处理,就是依靠SPI机制,这也是能扩展的重要原因。

SpringBoot会先通过SPI机制加载所有PropertySourceLoader,然后遍历每个PropertySourceLoader,判断当前遍历的PropertySourceLoader,通过getFileExtensions获取到当前PropertySourceLoader能够支持哪些配置文件格式的解析,让后跟当前需要解析的文件格式进行匹配,如果能匹配上,那么就会使用当前遍历的PropertySourceLoader来解析配置文件。

PropertySourceLoader其实就属于策略接口,配置文件的解析就是策略模式的运用。

所以,只需要按照这种格式,在spring.factories文件中配置一下就行了。

org.springframework.boot.env.PropertySourceLoader=\
com.sanyou.spring.extension.springbootextension.propertysourceloader.JsonPropertySourceLoader

到此,其实就扩展完了,接下来就来测试一下。

测试

先创建一个application.json的配置文件

改造User

public class User {// 注入配置文件的属性@Value("${sanyou.username:}")private String username;
}

启动项目

@SpringBootApplication
public class Application {public static void main(String[] args) {ConfigurableApplicationContext applicationContext = SpringApplication.run(Application.class);User user = applicationContext.getBean(User.class);System.out.println("获取到的Bean为" + user + ",属性username值为:" + user.getUsername());}@Beanpublic User user() {return new User();}}

运行结果:

获取到的Bean为com.sanyou.spring.extension.User@481ba2cf,属性username值为:三友的java日记

成功将json配置文件的属性注入到User对象中。

至此,SpringBoot就支持了以json为结尾的配置文件格式。

相关内容

热门资讯

【MySQL】锁 锁 文章目录锁全局锁表级锁表锁元数据锁(MDL)意向锁AUTO-INC锁...
【内网安全】 隧道搭建穿透上线... 文章目录内网穿透-Ngrok-入门-上线1、服务端配置:2、客户端连接服务端ÿ...
GCN的几种模型复现笔记 引言 本篇笔记紧接上文,主要是上一篇看写了快2w字,再去接入代码感觉有点...
数据分页展示逻辑 import java.util.Arrays;import java.util.List;impo...
Redis为什么选择单线程?R... 目录专栏导读一、Redis版本迭代二、Redis4.0之前为什么一直采用单线程?三、R...
【已解决】ERROR: Cou... 正确指令: pip install pyyaml
关于测试,我发现了哪些新大陆 关于测试 平常也只是听说过一些关于测试的术语,但并没有使用过测试工具。偶然看到编程老师...
Lock 接口解读 前置知识点Synchronized synchronized 是 Java 中的关键字,...
Win7 专业版安装中文包、汉... 参考资料:http://www.metsky.com/archives/350.htm...
3 ROS1通讯编程提高(1) 3 ROS1通讯编程提高3.1 使用VS Code编译ROS13.1.1 VS Code的安装和配置...
大模型未来趋势 大模型是人工智能领域的重要发展趋势之一,未来有着广阔的应用前景和发展空间。以下是大模型未来的趋势和展...
python实战应用讲解-【n... 目录 如何在Python中计算残余的平方和 方法1:使用其Base公式 方法2:使用statsmod...
学习u-boot 需要了解的m... 一、常用函数 1. origin 函数 origin 函数的返回值就是变量来源。使用格式如下...
常用python爬虫库介绍与简... 通用 urllib -网络库(stdlib)。 requests -网络库。 grab – 网络库&...
药品批准文号查询|药融云-中国... 药品批文是国家食品药品监督管理局(NMPA)对药品的审评和批准的证明文件...
【2023-03-22】SRS... 【2023-03-22】SRS推流搭配FFmpeg实现目标检测 说明: 外侧测试使用SRS播放器测...
有限元三角形单元的等效节点力 文章目录前言一、重新复习一下有限元三角形单元的理论1、三角形单元的形函数(Nÿ...
初级算法-哈希表 主要记录算法和数据结构学习笔记,新的一年更上一层楼! 初级算法-哈希表...
进程间通信【Linux】 1. 进程间通信 1.1 什么是进程间通信 在 Linux 系统中,进程间通信...
【Docker】P3 Dock... Docker数据卷、宿主机与挂载数据卷的概念及作用挂载宿主机配置数据卷挂载操作示例一个容器挂载多个目...