谈谈Spring中的BeanPostProcessor接口(转)

原文:谈谈Spring中的BeanPostProcessor接口

作者:特务依昂

 

一. 前言

  这几天正在复习Spring的相关内容,在了解bean的生命周期的时候,发现其中涉及到一个特殊的接口——BeanPostProcessor接口。由于网上没有找到比较好的博客,所有最后花了好几个小时,通过Spring的官方文档对它做了一个大致的了解,下面就来简单介绍一下这个接口。

二. 正文

2.1 BeanPostProcessor的功能

  有时候,我们希望Spring容器在创建bean的过程中,能够使用我们自己定义的逻辑,对创建的bean做一些处理,或者执行一些业务。而实现方式有多种,比如自定义bean的初始化话方法等,而BeanPostProcessor接口也是用来实现类似的功能的。

  如果我们希望容器中创建的每一个bean,在创建的过程中可以执行一些自定义的逻辑,那么我们就可以编写一个类,并让他实现BeanPostProcessor接口,然后将这个类注册到一个容器中。容器在创建bean的过程中,会优先创建实现了BeanPostProcessor接口的bean,然后,在创建其他bean的时候,会将创建的每一个bean作为参数,调用BeanPostProcessor的方法。而BeanPostProcessor接口的方法,即是由我们自己实现的。下面就来具体介绍一下BeanPostProcessor的使用。

2.2 BeanPostProcessor的使用

我们先看一看BeanPostProcessor接口的代码:

public interface BeanPostProcessor {// 注意这个方法名称关键的是before这个单词
    Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException;// 注意这个方法名称关键的是after这个单词
    Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException;
}

  可以看到,BeanPostProcessor接口只有两个抽象方法,由实现这个接口的类去实现(后面简称这两个方法为beforeafter),这两个方法有着相同的参数:

  • bean:容器正在创建的那个bean的引用;
  • beanName:容器正在创建的那个bean的名称;

  那这两个方法何时执行呢?这就涉及到Spring中,bean的生命周期了。下面引用《Spring实战》中的一张图,这张图表现了bean的生命周期,而Spring容器创建bean的具体过程,请参考这篇博客——简单谈谈Spring的IoC。

 

  上图中标红的两个地方就是BeanPostProcessor中两个方法的执行时机。Spring容器在创建bean时,如果容器中包含了BeanPostProcessor的实现类对象,那么就会执行这个类的这两个方法,并将当前正在创建的bean的引用以及名称作为参数传递进方法中。这也就是说,BeanPostProcessor的作用域是当前容器中的所有bean(不包括一些特殊的bean,这个后面说)。

  值得注意的是,我们可以在一个容器中注册多个不同的BeanPostProcessor的实现类对象,而bean在创建的过程中,将会轮流执行这些对象实现的beforeafter方法。那执行顺序如何确定呢?Spring提供了一个接口Ordered,我们可以让BeanPostProcessor的实现类实现这个Ordered接口,并实现接口的getOrder方法。这个方法的返回值是一个int类型,Spring容器会通过这个方法的返回值,对容器中的多个BeanPostProcessor对象进行从小到大排序,然后在创建bean时依次执行它们的方法。也就是说,getOrder方法返回值越小的BeanPostProcessor对象,它的方法将越先被执行。

2.3 一个简单的demo

下面就来写一个简单的demo,来看看BeanPostProcessor的效果。首先定义两个普通的bean,就叫UserCar吧:

public class User {private String name;private int age;// ... 省略getter和setter...
}public class Car {private int speed;private double price;// ... 省略getter和setter...
}

在定义一个BeanPostProcessor的实现类,重写接口的方法:

public class PostBean implements BeanPostProcessor {@Overridepublic Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {// 输出信息,方便我们看效果System.out.println("before -- " + beanName);return bean;}@Overridepublic Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {// 输出信息,方便我们看效果System.out.println("after -- " + beanName);return bean;}}

我们直接使用一个Java类作为Spring的配置,就不使用xml配置文件了。配置如下,在这个配置类中,声明了UserCar以及PostBean这三个bean的工厂方法,前两个是普通bean,而PostBean是实现BeanPostProcessorbean

@Configuration
public class BeanConfig {// 在Spring中注册User这个bean
    @Beanpublic User user() {return new User();}// 在Spring中注册Car这个bean
    @Beanpublic Car car() {return new Car();}// 在Spring中注册PostBean这个bean,这个bean实现了BeanPostProcessor接口
    @Beanpublic PostBean postBean() {return new PostBean();}}

好,有了上面四个类,就可以开始测试了,下面是测试方法:

@Test
public void testConfig() {ApplicationContext context =new AnnotationConfigApplicationContext(BeanConfig.class);
}

上面这个方法啥也不干,就是创建一个Spring的上下文对象,也就是SpringIoC容器。这个容器将去加载BeanConfig这个类的配置,然后创建配置类中声明的对象。在创建User和Car的过程中,就会执行BeanPostProcessor实现类的方法。我们看看执行结果:

before -- org.springframework.context.event.internalEventListenerProcessor
after -- org.springframework.context.event.internalEventListenerProcessor
before -- org.springframework.context.event.internalEventListenerFactory
after -- org.springframework.context.event.internalEventListenerFactory
before -- car
after -- car
before -- user
after -- user

可以看到,BeanPostProcessorbefore方法和after方法都被调用了四次,最后两次调用时,传入的参数正是我们自己定义的Bean——UserCar。那为什么调用了四次呢,明明我们只定义了两个普通bean。我们看上面的输出发现,前两次调用,传入的beanSpring内部的组件。Spring在初始化容器的过程中,会创建一些自己定义的bean用来实现一些功能,而这些bean,也会执行我们注册进容器中的BeanPostProcessor实现类的方法。

2.4 使用BeanPostProcessor时容易踩的坑

  BeanPostProcessor这个接口,在使用的过程中,其实还有许多的限制和坑点,若不了解的话,可能会让你对某些结果感到莫名其妙。下面我就来简单地说一说:

(一)BeanPostProcessor依赖的bean,不会执行BeanPostProcessor的方法

  当我们在BeanPostProcessor的实现类中,依赖了其他的bean,那么被依赖的bean被创建时,将不会执行它所在的BeanPostProcessor实现类实现的方法,比如我们修改PostBean的实现,如下所示:

@Component
public class PostBean implements BeanPostProcessor, Ordered {// 让PostBean依赖User
    @Autowiredprivate User user;@Overridepublic Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {return bean;}@Overridepublic Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {return bean;}
}

此时,容器在创建User这个bean时,不会执行PostBean实现的两个方法,因为由于PostBean依赖于user,所以user需要在PostBean之前创建完成,这也就意味着在user创建时,PostBean还未初始化完成,所以不会调用它的方法。

(二)BeanPostProcessor以及依赖的bean无法使用AOP

  以下是Spring官方文档中的一段话:

Because AOP auto-proxying is implemented as a BeanPostProcessor itself, neither BeanPostProcessor s nor the beans they reference directly are eligible for auto-proxying, and thus do not have aspects woven into them.

  上面这段话的意思大致是说,SpringAOP代理就是作为BeanPostProcessor实现的,所以我们无法对BeanPostProcessor的实现类使用AOP织入通知,也无法对BeanPostProcessor的实现类依赖的bean使用AOP织入通知SpringAOP实现我暂时还没有研究过,所以上面的说AOP作为BeanPostProcessor实现的意思我不是特别明白,但是我们现在只需要关注BeanPostProcessor以及它依赖的bean都无法使用AOP这一点。为了验证上面的说法,我稍微修改一下2.3中的例子,来测试一波。

  首先,我们修改2.3中用到的PostBeanUser这两个类,让PostBean依赖User这个类,同时为了输出更加地简单,我们将beforeafter方法中的println语句删了:

@Component
public class PostBean implements BeanPostProcessor, Ordered {// 让PostBean依赖User
    @Autowiredprivate User user;@Overridepublic Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {return bean;}@Overridepublic Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {return bean;}// 此方法用来测试AOP,作为切点public void testAOP() {System.out.println("Post Bean");}
}@Component
public class User {private String name;private int age;// ... 省略getter和setter...// 此方法用来测试AOP,用作切点public void testAOP() {System.out.println("user bean");}
}

  然后,我们定义一个AOP的切面,在切面中将PostBeantestAOP方法作为切点,代码如下:

@Aspect
public class BeanPostProcessorAspect {// 此方法织入PostBean的testAOP方法@Before("execution(* cn.tewuyiang.pojo.PostBean.testAOP(..))")public void before() {System.out.println("before1");}// 此方法织入User的testAOP方法@Before("execution(* cn.tewuyiang.pojo.User.testAOP(..))")public void before2() {System.out.println("before2");}
}

好,这就准备完毕,可以开始测试了。我们这次使用Spring注解扫描来配置bean以及为bean注入依赖,测试代码如下:

@Test
public void testConfig() {ApplicationContext context =new AnnotationConfigApplicationContext(AutoConfig.class);// 获取User这个bean,执行测试AOP的方法User user = context.getBean(User.class);user.testAOP();// 获取PostBean这个bean,执行测试AOP的方法PostBean bean = context.getBean(PostBean.class);bean.testAOP();
}输出如下:user beanpost Bean

  从输出中可以看到,使用AOP织入的前置通知没有执行,这也就验证了上面所说的,BeanPostProcessor的实现类以及实现类依赖的bean,无法使用AOP为其织入通知。但是这个限制具体有到什么程度,我也不是很确定,因为我使用xml配置依赖,以及上面使用注解扫描两种方式,AOP织入都没法使用,但是我在使用@Bean这种配置方式时,被依赖的bean却成功执行了通知。所以,关于此处提到的限制,还需要深入了解Spring容器的源码实现才能下定论。

 

(三)注册BeanPostProcessor的方式以及限制

  我们如何将BeanPostProcessor注册到Spring容器中?方式主要有两种,第一种就是上面一直在用的,将其声明在Spring的配置类或xml文件中,作为普通的bean,让ApplicationContext对象去加载它,这样它就被自动注册到容器中了。而且Spring容器会对BeanPostProcessor的实现类做特殊处理,即会将它们挑选出来,在加载其他bean前,优先加载BeanPostProcessor的实现类。

  还有另外一种方式就是使用ConfigurableBeanFactory接口的addBeanPostProcessor方法手动添加,ApplicationContext对象中组合了一个ConfigurableBeanFactory的实现类对象。但是这种方式添加BeanPostProcessor有一些缺点。首先,我们一创建Spring容器,在配置文件中配置的单例bean就会被加载,此时addBeanPostProcessor方法还没有执行,那我们手动添加的BeanPostProcessor也就无法作用于这些bean了,所以手动添加的BeanPostProcessor只能作用于那些延迟加载的bean,或者非单例bean

  还有一个就是,使用addBeanPostProcessor方式添加的BeanPostProcessor,Ordered接口的作用将失效,而是以注册的顺序执行。我们前面提过,Ordered接口用来指定多个BeanPostProcessor实现的方法的执行顺序。这是Spring官方文档中提到的:

While the recommended approach for BeanPostProcessor registration is through ApplicationContext auto-detection (as described above), it is also possible to register them programmatically against a ConfigurableBeanFactory using the addBeanPostProcessor method. This can be useful when needing to evaluate conditional logic before registration, or even for copying bean post processors across contexts in a hierarchy. Note however that BeanPostProcessor s added programmatically do not respect the Ordered interface. Here it is the order of registration that dictates the order of execution. Note also that BeanPostProcessor s registered programmatically are always processed before those registered through auto-detection, regardless of any explicit ordering.

 

(四)使用@Bean配置BeanPostProcessor的限制

  如果我们使用Java类的方式配置Spring,并使用@Bean声明一个工厂方法返回bean实例,那么返回值的类型必须是BeanPostProcessor类型,或者等级低于BeanPostProcessor的类型。这里不好口头描述,直接看代码吧。以下是一个BeanPostProcessor的实现类,它实现了多个接口:

/*** 此BeanPostProcessor的实现类,还实现了Ordered接口*/
public class PostBean implements BeanPostProcessor, Ordered {@Overridepublic Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {System.out.println("before -- " + beanName);return bean;}@Overridepublic Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {System.out.println("after -- " + beanName);return bean;}@Overridepublic int getOrder() {return 0;}
}

  我们在配置类中,声明PostBean可以有以下几种方式:

 
@Configuration
public class BeanConfig {// 方式1:PostBean
    @Beanpublic PostBean postBean() {return new PostBean();}// 方式2:返回值为BeanPostProcessor
    @Beanpublic BeanPostProcessor postBean() {return new PostBean();}// 方式3:返回值为Ordered
    @Beanpublic Ordered postBean() {return new PostBean();}
}

以上三种方式都可以让Spring容器创建PostBean实例对象,因为PostBean实现了BeanPostProcessorOrdered接口,所以它的对象也是这两种类型的对象。但是需要注意,上面三种方式中,只有第一种和第二种方式,会让Spring容器将PostBean当作BeanPostProcessor处理;而第三种方式,则会被当作一个普通Bean处理,实现BeanPostProcessor的两个方法都不会被调用。因为在PostBean的继承体系中,OrderedBeanPostProcessor是同级别的,Spring无法识别出这个Ordered对象,也是一个BeanPostProcessor对象;但是使用PostBean却可以,因为PostBean类型就是BeanPostProcessor的子类型。所以,在使用@Bean声明工厂方法返回BeanPostProcessor实现类对象时,返回值必须是BeanPostProcessor类型,或者更低级的类型Spring官方文档中,这一部分的内容如下:

Note that when declaring a BeanPostProcessor using an @Bean factory method on a configuration class, the return type of the factory method should be the implementation class itself or at least the org.springframework.beans.factory.config.BeanPostProcessor interface, clearly indicating the post-processor nature of that bean. Otherwise, the ApplicationContext won’t be able to autodetect it by type before fully creating it. Since a BeanPostProcessor needs to be instantiated early in order to apply to the initialization of other beans in the context, this early type detection is critical.

三. 总结

  以上就对BeanPostProcessor的功能、使用以及需要注意的问题做了一个大致的介绍。需要注意的是,上面所提到的问题,可能根据不同的情况,会有不同的结果,因为文档中的资料只是简单地提了几句,并不详细,上面的内容大部分都是我基于官方文档的描述,以及自己的测试得出,所以可能并不准确。还需要自己在实践中去尝试,或者阅读源码,才能彻底了解BeanPostProcessor的执行机制。

  以上描述若存在错误或不足,希望能够提出来,因为这一部分内容,我也不太了解,所以希望有人帮忙指正

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/793299.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【python】打包神器--pyinstaller

1:简介pyinstaller是一个python的第三方库,它能够在Windows、Linux、 Mac OS 等操作系统下将 Python 源文件打包,通过对源文件打包, Python 程序可以在没有安装 Python 的环境中运行,也可以作为一个独立文件方便传递和管理。在Windows上使用就打包成.exe文件。在Mac上使用…

MYSQL的安装与配置流程

MYSQL的安装与配置流程 1.下载安装包 mysql官方下载链接2.解压并配置文件用管理员身份打开命令提示符进入解压的地址中使用一下命令安装MySQL服务mysqld install MySQL80 若显示下面失败的提示可以使用该指令卸载该服务后再重新安装 mysqld -remove MySQL80//此处为直接修改的服…

eyoucms易优网站后台密码忘记了怎么办?

如果你忘记了易优CMS(EyouCMS)网站后台的登录密码,可以按照以下步骤来重置密码: 方法一:使用官方提供的重置工具下载官方重置工具:访问易优CMS官网提供的重置工具下载链接:https://www.eyoucms.com/uploads/soft/200319/1-2003191Q000.zip。 下载并解压缩该工具包。上传…

eyoucms后台如何更换后台登录logo

扫码添加技术【解决问题】专注中小企业网站建设、网站安全12年。熟悉各种CMS,精通PHP+MYSQL、HTML5、CSS3、Javascript等。承接:企业仿站、网站修改、网站改版、BUG修复、问题处理、二次开发、PSD转HTML、网站被黑、网站漏洞修复等。专业解决各种疑难杂症,您有任何网站问题都…

pbootcms后台可以打开前台打不开

问题:pbootcms后台可以打开前台打不开 原因:大概率是没有授权, 去申请授权添加后台就可以扫码添加技术【解决问题】专注中小企业网站建设、网站安全12年。熟悉各种CMS,精通PHP+MYSQL、HTML5、CSS3、Javascript等。承接:企业仿站、网站修改、网站改版、BUG修复、问题处理、…

pbootcms提示:后端配置项没有正常加载,上传插件不能正常使用

打开PBootCMS程序下的core/extend/ueditor/php/controller.php文件, 将第四行的注释去掉,并且将 chongqing 修改为首字母大写 Chongqing , 修改结果如下:扫码添加技术【解决问题】专注中小企业网站建设、网站安全12年。熟悉各种CMS,精通PHP+MYSQL、HTML5、CSS3、Javascrip…

pbootcms模板忘记后台密码怎么找回?

此工具用于忘记PbootCMS后台用户账号密码时进行重置。新建一个php文件,然后写入下面代码。稍后上传网站根目录, 访问文件就可以进入重置页面, 后续根据提示操作即可。 <?php /*** @copyright (C)2016-2099 Hnaoyun Inc.* @author XingMeng* @email hnxsh@foxmail.com*…

受 LabelImg 启发的基于 web 的图像标注工具,基于 Vue 框架

受 LabelImg 启发的基于 web 的图像标注工具,基于 Vue 框架哟,网友们好,年更鸽子终于想起了他的博客园密码。如标题所述,今天给大家带来的是一个基于 vue2 的图像标注工具。至于它诞生的契机呢,应该是我导 pass 掉了我的提议(让甲方使用 LabelImg 进行数据标注),说是要…

Parse error: syntax error, unexpected function (T_FUNCTION) in core\function\helper.php on line 80

遇到 Parse error: syntax error, unexpected function (T_FUNCTION) 的错误,通常是因为 PHP 代码中存在语法错误。这种错误通常发生在 PHP 版本不兼容的情况下,或者代码本身有语法问题。 分析错误 错误信息指出在 \core\function\helper.php 文件的第 80 行出现了语法错误。…

DNS胶水记录和DNS查询

DNS 胶水记录和DNS查询DNS 胶水记录DNS 系统 胶水记录(Glue Records) 过程抓包 完整解析过程解释 其他DNS 系统 了解过 DNS 的都知道,DNS 是一个层状系统,域名的格式是使用.分割,比如: # 最后一个.代表根 www.example.com.一个终端想要访问域名为www.example.com的web页面…

忘记PbootCMS后台用户账号密码时进行重置resetpw.php

<?php /*** @copyright (C)2016-2099 Hnaoyun Inc.* @license This is not a freeware, use is subject to license terms* @author XingMeng* @email hnxsh@foxmail.com* @date 2018年11月17日* 重置PbootCMS用户密码*/ // 设置字符集编码、IE文档模式 header(Content-Ty…