【每天学点Spring】BeanFactoryPostProcessor介绍以及场景运用

【本文内容】

  • 介绍了BeanFactoryPostProcessor
  • BeanFactoryPostProcessor在Bean生命周期中何时被调用。
  • BeanFactoryPostProcessor在Spring中的运用场景:类PropertyResourceConfigurer和类ServletComponentRegisteringPostProcessor
  • 自定义场景:【启动时打印bean的一些信息】,【在启动的时候把Spring的某个原生Bean,替换为自己的Bean。】

1. BeanFactoryPostProcessor 介绍

官方文档:https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/beans/factory/config/BeanFactoryPostProcessor.html

工厂级别的勾子,用来允许修改application context中的bean的definition,作用于bean实例化前。

这个接口就一个方法:

public interface BeanFactoryPostProcessor {

    void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException;

}

2. 何时被调用

参考:https://stackoverflow.com/questions/30455536/beanfactorypostprocessor-and-beanpostprocessor-in-lifecycle-events

从图中可以看到实现了接口BeanFactoryPostProcessor的类会在Bean实例化以及bean内部的一些依赖注入之前被调用

即在bean定义被加载后,就开始BeanFactoryPostProcessor的调用了(毕竟这个接口主要就是为了修改bean的definition,如果在bean实例化后被调用,那么修改的definition就没有意义了!)。

书:《Spring 5 Design Patterns》

3. 在Spring中的运用场景

3.1 抽象类 PropertyResourceConfigurer (官方文档)

这个抽象类主要是为了让bean的property values可以从配置文件中读取。它有两个实现类:

  • PropertyOverrideConfigurer:比如配置为"dataSource.driverClassName=com.mysql.jdbc.Driver",这个类负责将这个value从配置文件中(比如叫datasource.properties)推到相应的bean定义(bean definition)中。
  • PropertyPlaceholderConfigurer ,这个类可以将代码中定义的"${...}"替换为配置文件中的实际的值。(注:这个类在5.2版本后被淘汰了,后续使用的是PropertySourcesPlaceholderConfigurer

具体来看抽象类PropertyResourceConfigurer的源码:

  • 可以看到首先它实现了Ordered接口(这是必须的),因为需要控制各个BeanFactoryPostProcessor的执行顺序。而Ordered.LOWEST_PRECEDENCEInteger.MAX_VALUE,表示顺序无限延后,也就是最后执行。
  • 另外这个抽象类实现了BeanFactoryPostProcessor接口,所以需要实现postProcessBeanFactory方法。可以看到在这个方法里主要做的就是读取properties文件,做一些必要的convert,并process这些properties到bean Definition中。
public abstract class PropertyResourceConfigurer extends PropertiesLoaderSupport
        implements BeanFactoryPostProcessor, PriorityOrdered {

    private int order = Ordered.LOWEST_PRECEDENCE;  // default: same as non-Ordered

    // 略:serOrder / getOrder

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        try {
            Properties mergedProps = mergeProperties();

            // Convert the merged properties, if necessary.
            convertProperties(mergedProps);

            // Let the subclass process the properties.
            processProperties(beanFactory, mergedProps);
        }
        catch (IOException ex) {
            throw new BeanInitializationException("Could not load properties", ex);
        }
    }
}

具体看如何process到beanDefinition中:
在实现类PropertyOverrideConfigurer中有个方法:可以看到从factory中拿到beanDefinition后,将通过读取到的property/value,组合成新的PropertyValue对象,放回BeanDefinition中。

这就是整个BeanFactoryPostProcessor接口修改BeanDefinition的过程。

protected void applyPropertyValue(
            ConfigurableListableBeanFactory factory, String beanName, String property, String value) {

        BeanDefinition bd = factory.getBeanDefinition(beanName);
        BeanDefinition bdToUse = bd;
        while (bd != null) {
            bdToUse = bd;
            bd = bd.getOriginatingBeanDefinition();
        }
        PropertyValue pv = new PropertyValue(property, value);
        pv.setOptional(this.ignoreInvalidKeys);
        bdToUse.getPropertyValues().addPropertyValue(pv);
    }
3.2 ServletComponentRegisteringPostProcessor (源码)

ServletComponentRegisteringPostProcessor位于是Spring Boot中的类,主要的作用是配置注解@ServletComponentScan,可以扫描出标注在类上的@WebServlet@WebFilter以及@WebListener,并进行解析。

  • 可以看到ServletComponentRegisteringPostProcessor实现了BeanFactoryPostProcessor接口,并实现了postProcessBeanFactory方法。
  • postProcessBeanFactory中,主要是基于@ServletComponentScan注解的包,进行扫描。
  • scanPackage()方法即拿到上述介绍的被标记的三个注解(@WebServlet@WebFilter以及@WebListener)的类,然后调用各自的handler(三个注解的handler不一样)的handle方法,handle方法下文有介绍。
class ServletComponentRegisteringPostProcessor implements BeanFactoryPostProcessor, ApplicationContextAware {
    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        if (isRunningInEmbeddedWebServer()) {
            ClassPathScanningCandidateComponentProvider componentProvider = createComponentProvider();
            for (String packageToScan : this.packagesToScan) {
                scanPackage(componentProvider, packageToScan);
            }
        }
    }

    private void scanPackage(ClassPathScanningCandidateComponentProvider componentProvider, String packageToScan) {
        for (BeanDefinition candidate : componentProvider.findCandidateComponents(packageToScan)) {
            if (candidate instanceof AnnotatedBeanDefinition) {
                for (ServletComponentHandler handler : HANDLERS) {
                    handler.handle(((AnnotatedBeanDefinition) candidate),
                            (BeanDefinitionRegistry) this.applicationContext);
                }
            }
        }
    }
}

下述的源码来自处理@WebServlet注解的handler,类WebServletHandler
看到ServletRegistrationBean是不是很熟悉?因为在Spring中我们通常定义一个Serlvet,可以在Configuration中加个@Bean,然后手动new出ServletRegistrationBean,所以的doHandle也在做这件事。

    @Override
    public void doHandle(Map<String, Object> attributes, AnnotatedBeanDefinition beanDefinition,
            BeanDefinitionRegistry registry) {
        BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(ServletRegistrationBean.class);
        builder.addPropertyValue("asyncSupported", attributes.get("asyncSupported"));
        builder.addPropertyValue("initParameters", extractInitParameters(attributes));
        builder.addPropertyValue("loadOnStartup", attributes.get("loadOnStartup"));
        String name = determineName(attributes, beanDefinition);
        builder.addPropertyValue("name", name);
        builder.addPropertyValue("servlet", beanDefinition);
        builder.addPropertyValue("urlMappings", extractUrlPatterns(attributes));
        builder.addPropertyValue("multipartConfig", determineMultipartConfig(beanDefinition));
        registry.registerBeanDefinition(name, builder.getBeanDefinition());
    }

【总结】
我们自定义的Servlet类,注入方式有两种:

  • 第一种即Spring中定义@Bean:
    @Bean
    public ServletRegistrationBean myServlet(){
        MyServlet myServlet = new MyServlet();
        return new ServletRegistrationBean(myServlet,"/my","/my02");
    }
  • 第二种(适用Spring Boot),即在MyServlet类上加上注解@WebServlet,并在启动Application类上加@ServletComponentScan
@WebServlet(urlPatterns = "/myUrl")
public class MyServlet extends HttpServlet {
}
而第二种方式,恰恰是通过BeanFactoryPostProcessor接口来实现的!

4. 自定义场景

4.1 在Spring启动时打印以下信息:
  • a. bean的个数。
  • b. 所有bean类名中包含auto,打印前两个bean name。
  • c. 实现了JpaRepository接口的bean的名字。

为此,我们定义了类PrintBeanFactoryPostProcessor,在方法中通过beanFactory就能拿到上述需要打印的信息。

@Component
public class PrintBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        int count = beanFactory.getBeanDefinitionCount();
        System.out.println("Get " + count + " beans...");

        System.out.println("--------------------------");

        Arrays.stream(beanFactory.getBeanDefinitionNames())
                .filter(name -> name.toLowerCase().contains("auto")).limit(2).forEach(System.out::println);

        System.out.println("--------------------------");
        Arrays.stream(beanFactory.getBeanNamesForType(JpaRepository.class)).forEach(System.out::println);
    }
}

【打印的结果如下】
image.png
4.2 在启动的时候把Spring的某个原生Bean,替换为自己的Bean

比如在事务invoke之前和之后打印以下信息:a. 打印出何时开始何时结束。b. 统计时间。

参考:https://www.folkstalk.com/tech/overriding-transaction-propagation-levels-for-methods-having-springs-transactional-with-code-solutions/

那么,可以自定义一个类(比如叫MyTransactionInterceptor),然后继承TransactionInterceptor,在我们自己的类中重写方法invoke(MethodInvocation invocation)(比如前后打印,中间可以调用父类即真正的TransactionInterceptor的方法:super.invoke(invocation))。

我们用以下代码来模拟整个替换Bean的过程:

  • 类:Apple.java (代表Spring中的某个原生Bean,如TransactionInterceptor)。
@Component
public class Apple {
    public String getName() {
        return "apple";
    }
}

我们写一个Controller取读getName()方法:

@RestController
@RequestMapping("apple")
public class AppleController {
    @Autowired
    private Apple apple;

    @GetMapping
    public String apple() {
        return apple.getName();
    }
}

结果:
image.png

由于Apple类是Spring自已的Bean(假设),那么我们希望用自定义的定去替换它。首先,新建一个Peach.java类:

public class Peach extends Apple {
    public String getName() {
        return "peach";
    }
}
利用本文介绍的BeanFactoryPostProcessor,在启动的时候,用Peach类替换掉原先的Apple类:
@Configuration
public class AppleFactoryPostProcessor implements BeanFactoryPostProcessor, PriorityOrdered {

    @Override
    public int getOrder() {
        return Ordered.LOWEST_PRECEDENCE;
    }

    public void postProcessBeanFactory(@NonNull ConfigurableListableBeanFactory beanFactory) throws BeansException {
        String[] beanNames = beanFactory.getBeanNamesForType(Apple.class);
        for (String beanName : beanNames) {
            BeanDefinition beanDefinition = beanFactory.getBeanDefinition(beanName);
            beanDefinition.setBeanClassName(Peach.class.getName());
            beanDefinition.setFactoryBeanName(null);
            beanDefinition.setFactoryMethodName(null);
        }
    }
}

上述的Controller不变,继续访问:
image.png

可以看到Apple已经被替换成我们自己的Peach类了。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,569评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,499评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,271评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,087评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,474评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,670评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,911评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,636评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,397评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,607评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,093评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,418评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,074评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,092评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,865评论 0 196
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,726评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,627评论 2 270

推荐阅读更多精彩内容