实现BeanPostProcessor

7039 / 2026-01-08 14:11:33 2018世界杯球队

廖雪峰

资深软件开发工程师,业余马拉松选手。

现在,我们已经完成了扫描Class名称、创建BeanDefinition、创建Bean实例、初始化Bean,理论上一个可用的IoC容器就已经就绪。

然而,BeanPostProcessor的出现改变了这一切。Spring允许用户自定义一种特殊的Bean,即实现了BeanPostProcessor接口,它有什么用呢?其实就是替换Bean。我们举个例子,下面的代码是基于Spring代码:

@Configuration

@ComponentScan

public class AppConfig {

public static void main(String[] args) {

var ctx = new AnnotationConfigApplicationContext(AppConfig.class);

// 可以获取到ZonedDateTime:

ZonedDateTime dt = ctx.getBean(ZonedDateTime.class);

System.out.println(dt);

// 错误:NoSuchBeanDefinitionException:

System.out.println(ctx.getBean(LocalDateTime.class));

}

// 创建LocalDateTime实例

@Bean

public LocalDateTime localDateTime() {

return LocalDateTime.now();

}

// 实现一个BeanPostProcessor

@Bean

BeanPostProcessor replaceLocalDateTime() {

return new BeanPostProcessor() {

@Override

public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {

// 将LocalDateTime类型实例替换为ZonedDateTime类型实例:

if (bean instanceof LocalDateTime) {

return ZonedDateTime.now();

}

return bean;

}

};

}

}

运行可知,我们定义的@Bean类型明明是LocalDateTime类型,但却被另一个BeanPostProcessor替换成了ZonedDateTime,于是,调用getBean(ZonedDateTime.class)可以拿到替换后的Bean,调用getBean(LocalDateTime.class)会报错,提示找不到Bean。那么原始的Bean哪去了?答案是被BeanPostProcessor扔掉了。

可见,BeanPostProcessor是一种特殊Bean,它的作用是根据条件替换某些Bean。上述的例子中,LocalDateTime被替换为ZonedDateTime其实没啥意义,但实际应用中,把原始Bean替换为代理后的Bean是非常常见的,比如下面的基于Spring的代码:

@Configuration

@ComponentScan

public class AppConfig {

public static void main(String[] args) {

var ctx = new AnnotationConfigApplicationContext(AppConfig.class);

UserService u = ctx.getBean(UserService.class);

System.out.println(u.getClass().getSimpleName()); // UserServiceProxy

u.register("bob@example.com", "bob12345");

}

@Bean

BeanPostProcessor createProxy() {

return new BeanPostProcessor() {

@Override

public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {

// 实现事务功能:

if (bean instanceof UserService u) {

return new UserServiceProxy(u);

}

return bean;

}

};

}

}

@Component

class UserService {

public void register(String email, String password) {

System.out.println("INSERT INTO ...");

}

}

// 代理类:

class UserServiceProxy extends UserService {

UserService target;

public UserServiceProxy(UserService target) {

this.target = target;

}

@Override

public void register(String email, String password) {

System.out.println("begin tx");

target.register(email, password);

System.out.println("commit tx");

}

}

如果执行上述代码,打印出的Bean类型不是UserService,而是UserServiceProxy,因此,调用register()会打印出begin tx和commit tx,说明“事务”生效了。

迄今为止,创建Proxy似乎没有什么影响。让我们把代码再按实际情况扩展一下,UserService是用户编写的业务代码,需要注入JdbcTemplate:

@Component

class UserService {

@Autowired JdbcTemplate jdbcTemplate;

public void register(String email, String password) {

jdbcTemplate.update("INSERT INTO ...");

}

}

而PostBeanProcessor一般由框架本身提供事务功能,所以它会动态创建一个UserServiceProxy:

class UserServiceProxy extends UserService {

UserService target;

public UserServiceProxy(UserService target) {

this.target = target;

}

@Override

public void register(String email, String password) {

System.out.println("begin tx");

target.register(email, password);

System.out.println("commit tx");

}

}

调用用户注册的页面由MvcController控制,因此,将UserService注入到MvcController:

@Controller

class MvcController {

@Autowired UserService userService;

@PostMapping("/register")

void register() {

userService.register(...);

}

}

一开始,由IoC容器创建的Bean包括:

JdbcTemplate

UserService

MvcController

接着,由于BeanPostProcessor的介入,原始的UserService被替换为UserServiceProxy:

JdbcTemplate

UserServiceProxy

MvcController

那么问题来了:注意到UserServiceProxy是从UserService继承的,它也有一个@Autowired JdbcTemplate,那JdbcTemplate实例应注入到原始的UserService还是UserServiceProxy?

从业务逻辑出发,JdbcTemplate实例必须注入到原始的UserService,否则,代理类UserServiceProxy执行target.register()时,相当于对原始的UserService调用register()方法,如果JdbcTemplate没有注入,将直接报NullPointerException错误。

这时第二个问题又来了:MvcController需要注入的UserService,应该是原始的UserService还是UserServiceProxy?

还是从业务逻辑出发,MvcController需要注入的UserService必须是UserServiceProxy,否则,事务不起作用。

我们用图描述一下注入关系:

┌───────────────┐

│MvcController │

├───────────────┤ ┌────────────────┐

│- userService ─┼──▶│UserServiceProxy│

└───────────────┘ ├────────────────┤

│- jdbcTemplate │

├────────────────┤ ┌────────────────┐

│- target ─┼──▶│UserService │

└────────────────┘ ├────────────────┤ ┌────────────┐

│- jdbcTemplate ─┼──▶│JdbcTemplate│

└────────────────┘ └────────────┘

注意到上图的UserService已经脱离了IoC容器的管理,因为此时UserService对应的BeanDefinition中,存放的instance是UserServiceProxy。

可见,引入BeanPostProcessor可以实现Proxy机制,但也让依赖注入变得更加复杂。

但是我们仔细分析依赖关系,还是可以总结出两条原则:

一个Bean如果被Proxy替换,则依赖它的Bean应注入Proxy,即上图的MvcController应注入UserServiceProxy;

一个Bean如果被Proxy替换,如果要注入依赖,则应该注入到原始对象,即上图的JdbcTemplate应注入到原始的UserService。

基于这个原则,要满足条件1是很容易的,因为只要创建Bean完成后,立刻调用BeanPostProcessor就实现了替换,后续其他Bean引用的肯定就是Proxy了。先改造创建Bean的流程,在创建@Configuration后,接着创建BeanPostProcessor,再创建其他普通Bean:

public AnnotationConfigApplicationContext(Class configClass, PropertyResolver propertyResolver) {

...

// 创建@Configuration类型的Bean:

this.beans.values().stream()

// 过滤出@Configuration:

.filter(this::isConfigurationDefinition).sorted().map(def -> {

createBeanAsEarlySingleton(def);

return def.getName();

}).collect(Collectors.toList());

// 创建BeanPostProcessor类型的Bean:

List processors = this.beans.values().stream()

// 过滤出BeanPostProcessor:

.filter(this::isBeanPostProcessorDefinition)

// 排序:

.sorted()

// 创建BeanPostProcessor实例:

.map(def -> {

return (BeanPostProcessor) createBeanAsEarlySingleton(def);

}).collect(Collectors.toList());

this.beanPostProcessors.addAll(processors);

// 创建其他普通Bean:

createNormalBeans();

...

}

再继续修改createBeanAsEarlySingleton(),创建Bean实例后,调用BeanPostProcessor处理:

public Object createBeanAsEarlySingleton(BeanDefinition def) {

...

// 创建Bean实例:

Object instance = ...;

def.setInstance(instance);

// 调用BeanPostProcessor处理Bean:

for (BeanPostProcessor processor : beanPostProcessors) {

Object processed = processor.postProcessBeforeInitialization(def.getInstance(), def.getName());

// 如果一个BeanPostProcessor替换了原始Bean,则更新Bean的引用:

if (def.getInstance() != processed) {

def.setInstance(processed);

}

}

return def.getInstance();

}

现在,如果一个Bean被替换为Proxy,那么BeanDefinition中的instance已经是Proxy了,这时,对这个Bean进行依赖注入会有问题,因为注入的是Proxy而不是原始Bean,怎么办?

这时我们要思考原始Bean去哪了?原始Bean实际上是被BeanPostProcessor给丢了!如果BeanPostProcessor能保存原始Bean,那么,注入前先找到原始Bean,就可以把依赖正确地注入给原始Bean。我们给BeanPostProcessor加一个postProcessOnSetProperty()方法,让它返回原始Bean:

public interface BeanPostProcessor {

// 注入依赖时,应该使用的Bean实例:

default Object postProcessOnSetProperty(Object bean, String beanName) {

return bean;

}

}

再继续把injectBean()改一下,不要直接拿BeanDefinition.getInstance(),而是拿到原始Bean:

void injectBean(BeanDefinition def) {

// 获取Bean实例,或被代理的原始实例:

Object beanInstance = getProxiedInstance(def);

try {

injectProperties(def, def.getBeanClass(), beanInstance);

} catch (ReflectiveOperationException e) {

throw new BeanCreationException(e);

}

}

getProxiedInstance()就是为了获取原始Bean:

Object getProxiedInstance(BeanDefinition def) {

Object beanInstance = def.getInstance();

// 如果Proxy改变了原始Bean,又希望注入到原始Bean,则由BeanPostProcessor指定原始Bean:

List reversedBeanPostProcessors = new ArrayList<>(this.beanPostProcessors);

Collections.reverse(reversedBeanPostProcessors);

for (BeanPostProcessor beanPostProcessor : reversedBeanPostProcessors) {

Object restoredInstance = beanPostProcessor.postProcessOnSetProperty(beanInstance, def.getName());

if (restoredInstance != beanInstance) {

beanInstance = restoredInstance;

}

}

return beanInstance;

}

这里我们还能处理多次代理的情况,即一个原始Bean,比如UserService,被一个事务处理的BeanPostProcsssor代理为UserServiceTx,又被一个性能监控的BeanPostProcessor代理为UserServiceMetric,还原的时候,对BeanPostProcsssor做一个倒序,先还原为UserServiceTx,再还原为UserService。

测试

我们可以写一个测试来验证Bean的注入是否正确。先定义原始Bean:

@Component

public class OriginBean {

@Value("${app.title}")

public String name;

@Value("${app.version}")

public String version;

public String getName() {

return name;

}

}

通过FirstProxyBeanPostProcessor代理为FirstProxyBean:

@Order(100)

@Component

public class FirstProxyBeanPostProcessor implements BeanPostProcessor {

// 保存原始Bean:

Map originBeans = new HashMap<>();

@Override

public Object postProcessBeforeInitialization(Object bean, String beanName) {

if (OriginBean.class.isAssignableFrom(bean.getClass())) {

// 检测到OriginBean,创建FirstProxyBean:

var proxy = new FirstProxyBean((OriginBean) bean);

// 保存原始Bean:

originBeans.put(beanName, bean);

// 返回Proxy:

return proxy;

}

return bean;

}

@Override

public Object postProcessOnSetProperty(Object bean, String beanName) {

Object origin = originBeans.get(beanName);

if (origin != null) {

// 存在原始Bean时,返回原始Bean:

return origin;

}

return bean;

}

}

// 代理Bean:

class FirstProxyBean extends OriginBean {

final OriginBean target;

public FirstProxyBean(OriginBean target) {

this.target = target;

}

}

通过SecondProxyBeanPostProcessor代理为SecondProxyBean:

@Order(200)

@Component

public class SecondProxyBeanPostProcessor implements BeanPostProcessor {

// 保存原始Bean:

Map originBeans = new HashMap<>();

@Override

public Object postProcessBeforeInitialization(Object bean, String beanName) {

if (OriginBean.class.isAssignableFrom(bean.getClass())) {

// 检测到OriginBean,创建SecondProxyBean:

var proxy = new SecondProxyBean((OriginBean) bean);

// 保存原始Bean:

originBeans.put(beanName, bean);

// 返回Proxy:

return proxy;

}

return bean;

}

@Override

public Object postProcessOnSetProperty(Object bean, String beanName) {

Object origin = originBeans.get(beanName);

if (origin != null) {

// 存在原始Bean时,返回原始Bean:

return origin;

}

return bean;

}

}

// 代理Bean:

class SecondProxyBean extends OriginBean {

final OriginBean target;

public SecondProxyBean(OriginBean target) {

this.target = target;

}

}

定义一个Bean,用于检测是否注入了Proxy:

@Component

public class InjectProxyOnConstructorBean {

public final OriginBean injected;

public InjectProxyOnConstructorBean(@Autowired OriginBean injected) {

this.injected = injected;

}

}

测试代码如下:

var ctx = new AnnotationConfigApplicationContext(ScanApplication.class, createPropertyResolver());

// 获取OriginBean的实例,此处获取的应该是SendProxyBeanProxy:

OriginBean proxy = ctx.getBean(OriginBean.class);

assertSame(SecondProxyBean.class, proxy.getClass());

// proxy的name和version字段并没有被注入:

assertNull(proxy.name);

assertNull(proxy.version);

// 但是调用proxy的getName()会最终调用原始Bean的getName(),从而返回正确的值:

assertEquals("Scan App", proxy.getName());

// 获取InjectProxyOnConstructorBean实例:

var inject = ctx.getBean(InjectProxyOnConstructorBean.class);

// 注入的OriginBean应该为Proxy,而且和前面返回的proxy是同一实例:

assertSame(proxy, inject.injected);

从上面的测试代码我们也能看出,对于使用Proxy模式的Bean来说,正常的方法调用对用户是透明的,但是,直接访问Bean注入的字段,如果获取的是Proxy,则字段全部为null,因为注入并没有发生在Proxy,而是原始Bean。这也是为什么当我们需要访问某个注入的Bean时,总是调用方法而不是直接访问字段:

@Component

public class MailService {

@Autowired

UserService userService;

public String sendMail() {

// 错误:不要直接访问UserService的字段,因为如果UserService被代理,则返回null:

ZoneId zoneId = userService.zoneId;

// 正确:通过方法访问UserService的字段,无论是否被代理,返回值均是正确的:

ZoneId zoneId = userService.getZoneId();

...

}

}

可以从GitHub或Gitee下载源码。

GitHub