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<String> 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<PropertySource<?>> 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<PropertySource<?>> 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<String, Object> 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为结尾的配置文件格式。