title: 06.实现BeanPostProcessor.md
tag: 笔记 手写SSM IOC
初始化Bean之后提供BeanPostProcessor后置处理器扩展。
Spring的BeanPostProcessor
现在,我们已经完成了扫描Class
名称、创建BeanDefinition
、创建Bean
实例、初始化Bean
,理论上一个可用的IoC容器就已经就绪。而在开启实现AOP之前我们还还需要实现一个BeanPostProcessor
,它翻译过来就是Bean的后处理器,下面我们介绍BeanPostProcessor
的作用。
Spring
允许用户自定义一种特殊的Bean
,即实现了BeanPostProcessor
接口。它的作用就是在Bean的创建过程中去将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
扔掉了。
上面的例子在实际运用中可能很少能用到,但这种替换Bean的思想我们可以想到能不能把原本的Bean的实例替换为它的代理对象Proxy呢?这样我们就可以在代理对象去扩展功能而不去修改到原本的对象。这种方式可以运用到后面的AOP和事务功能实现。我们先来介绍以下代理设计模式。
代理(Proxy)模式
下面是一个代理模式的简单实现:
public interface Print {
void print();
}
class PrintImpl implements Print {
@Override
public void print() {
System.out.println("PrintImpl");
}
}
class PrintProxy implements Print{
private final PrintImpl target = new PrintImpl();
@Override
public void print() {
target.print();
}
public static void main(String[] args) {
PrintProxy printProxy = new PrintProxy();
printProxy.print();
}
}
这里被代理类和代理类拥有相同的实现,而代理类并不由自己去实现接口的方法,而是委托给被代理的类来实现。我们为何要多此一举添加代理类呢?因为我们可以在不修改被代理类的前提下扩展被代理类的功能,这就是实现AOP的前提。
当然,在该设计模式中,代理类和被代理类Proxy并不需要拥有相同的接口,只要代理类能够用某种方式去代言实现类,即可实现代理模式基本的思路。并且上面的代码的可以使用继承实现相同的效果。
替换原始对象为代理对象
现在我们知道BeanPostProcessor
可以替换IOC容器中Bean的实例,而代理模式可以拓展原始Bean的功能。顺理成章的我们就可以想到将IOC容器中的Bean实例替换为代理对象来实现AOP或者事务(事务也就是基于AOP)的功能。
下面我们使用BeanPostProcessor
模拟实现事务功能:
@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
,说明“事务”生效了。
现在我们有代理类和被代理类,因此我们还要解决将原本Bean的实例替换后我们还需要解决两个问题:
- 我们注入依赖时是注入到被代理类还是代理类。(注入到哪?)
- 我们注入时是将被代理类注入还是将代理类注入。(注入谁?)
加入代理后的注入
下面是一个举一个实际例子,UserService
是用户编写的业务代码,需要注入JdbcTemplate
:
@Component
class UserService {
@Autowired JdbcTemplate jdbcTemplate;
public void register(String email, String password) {
jdbcTemplate.update("INSERT INTO ...");
}
}
而我们这里使用PostBeanProcessor
将UserService
的实例替换为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容器进行分析:
一开始,由IoC容器创建的Bean包括:
- JdbcTemplate
- UserService
- MvcController
接着,由于BeanPostProcessor
的介入,原始的UserService
被替换为UserServiceProxy
:
- JdbcTemplate
- UserServiceProxy
- MvcController
此时我们有两个问题:
- 注意到
UserServiceProxy
是从UserService
继承的,它也有一个@Autowired JdbcTemplate
,那JdbcTemplate
实例应注入到原始的UserService
还是UserServiceProxy
?
因为代理类调用自己的register
方法时实际会调用原始对象的register
方法,因此不注入到原始对象时就会报NullPointerException
异常。也就是说,代理对象实际依赖于原始对象,因此需要注入到原始对象。
MvcController
需要注入的UserService
,应该是原始的UserService
还是UserServiceProxy
?
若是注入原始对象,那么我们将原始对象替换为代理对象就没有任何作用了,因此MvcController
需要注入的是UserServiceProxy
,也就是代理对象。
它们的关系如下:
注意到上图的**UserService
已经脱离了IoC容器的管理**,因为此时UserService
对应的BeanDefinition
中,存放的instance是UserServiceProxy
。
经过上面的分析,我们可以总结出两条注入的原则:
- 一个Bean如果被Proxy替换,则依赖它的Bean应注入Proxy,即上图的
MvcController
应注入UserServiceProxy
; - 一个Bean如果被Proxy替换,如果要注入依赖,则应该注入到原始对象,即上图的
JdbcTemplate
应注入到原始的UserService
。
实现BeanPostProcessor
在上面我们知道我们加入BeanPostProcessor
后我们的注入应该有下面两个原则:
-
一个Bean如果被Proxy替换,则依赖它的Bean应注入Proxy。
-
一个Bean如果被Proxy替换,如果要注入依赖,则应该注入到原始对象。
第一个条件实现
基于这个原则,要满足条件1是很容易的,因为只要创建Bean完成后,立刻调用BeanPostProcessor
就实现了替换,后续其他Bean引用的肯定就是Proxy了。先改造创建Bean的流程,在创建@Configuration
后,接着创建BeanPostProcessor
,再创建其他普通Bean:
protected void createBean(){
createConfigurationBean();
createBeanProcessorBean();
createCommonBean();
}
private void createBeanProcessorBean() {
beanPostProcessors.addAll(beans.values()
.stream()
.filter(this::isBeanPostProcessorDefinition)
.sorted()
.map(def -> (BeanPostProcessor) createBeanAsEarlySingleton(def))
.toList());
}
/**
* 通过BeanDefinition的getBeanClass属性该类判断是否为BeanPostProcessor的实现类
* @param def BeanDefinition
* @return true or false
*/
private boolean isBeanPostProcessorDefinition(BeanDefinition def) {
return BeanPostProcessor.class.isAssignableFrom(def.getBeanClass());
}
这样我们就可以在创建普通Bean
的过程中插入执行BeanProcessor
中替换Bean
的操作,我们修改createBeanAsEarlySingleton
拿到Bean实例返回前插入执行BeanPostProcessor
的postProcessBeforeInitialization
方法:
Object instance = null;
try {
if(definition.getFactoryName() == null){
instance = definition.getConstructor().newInstance(args);
}else{
Object bean = getBean(definition.getFactoryName());
instance = definition.getFactoryMethod().invoke(bean,args);
}
}catch (Exception e){
throw new BeanCreationException(String.format("Exception when create bean '%s': %s",
definition.getName(), definition.getBeanClass().getName()), e);
}
definition.setInstance(instance);
instance = callPostProcessor(definition);//BeanPostProcessor处理Bean
return instance;
/**
* 在创建Bean实例之前,调用BeanPostProcessor的postProcessBeforeInitialization方法对Bean实例进行处理
* @param def BeanDefinition
* @return 处理后的Bean实例
*/
private Object callPostProcessor(BeanDefinition def) {
Object instance = def.getInstance();
for (BeanPostProcessor processor : beanPostProcessors) {
Object processed = processor
.postProcessBeforeInitialization(def.getInstance(), def.getName());
if (processed == null) {
throw new BeanCreationException(String.format("PostBeanProcessor returns null when process bean '%s' by %s", def.getName(), processor));
}
//处理后的Bean实例与原本的实例不同,则替换原来的Bean实例
if(processed != def.getInstance()){
logger.atDebug().log("Bean '{}' was replaced by post processor {}.",
def.getName(), processor.getClass().getName());
instance = processed;
def.setInstance(processed);
}
}
return instance;
}
这样处理之后,BeanDefinition
中的instance
已经是Proxy了,这时,对这个Bean进行依赖注入会有问题,因为注入的是Proxy而不是原始Bean,这就不符合第二个条件了。
第二个条件实现
在我们将原始对象替换为代理对象后原始对象去哪了呢?原始Bean实际上是被BeanPostProcessor
给丢了!如果BeanPostProcessor
能保存原始Bean,那么,注入前先找到原始Bean,就可以把依赖正确地注入给原始Bean。我们给BeanPostProcessor
加一个postProcessOnSetProperty()
方法,在给Bean注入值时对实例进行操作,可以让它返回原始Bean:
public interface BeanPostProcessor {
// 注入依赖时,应该使用的Bean实例:
default Object postProcessOnSetProperty(Object bean, String beanName) {
return bean;
}
}
再继续把依赖注入的实例改一下,不要直接拿BeanDefinition.getInstance()
,而是拿到原始Bean,getProxiedInstance()
就是为了获取原始Bean,我们调用调用BeanPostProcessors
的``postProcessOnSetProperty`方法:
/**
* 调用BeanPostProcessors的postProcessOnSetProperty方法在对实例注入实例时进行处理
* @param def bean定义
* @return 处理后的实例
*/
private Object getProxiedInstance(BeanDefinition def) {
Object beanInstance = def.getInstance();
List<BeanPostProcessor> reversedBeanPostProcessors = new ArrayList<>(this.beanPostProcessors);
Collections.reverse(reversedBeanPostProcessors);
for (BeanPostProcessor processor : reversedBeanPostProcessors) {
Object processedInstance = processor.postProcessOnSetProperty(beanInstance, def.getName());
if(processedInstance != beanInstance){
logger.atDebug().log("BeanPostProcessor {} specified injection from {} to {}.",
processor.getClass().getSimpleName(),
beanInstance.getClass().getSimpleName(), processedInstance.getClass().getSimpleName());
beanInstance = processedInstance;
}
}
return beanInstance;
}
这里我们还能处理多次代理的情况,即一个原始Bean,比如UserService
,被一个事务处理的BeanPostProcsssor
代理为UserServiceTx
,又被一个性能监控的BeanPostProcessor
代理为UserServiceMetric
,还原的时候,对BeanPostProcsssor
做一个倒序,先还原为UserServiceTx
,再还原为UserService
。
总结
我们对两个条件的实现做一个总结:
- 一个Bean如果被Proxy替换,则依赖它的Bean应注入Proxy。
在实例化Bean阶段创建实例之前,我们先创建BeanPostProcessor
的实例,并在创建实例之后调用BeanPostProcessor
的postProcessBeforeInitialization
方法替换实例后,替换之后依赖它的Bean
注入的实例就是Proxy
了。
- 一个Bean如果被Proxy替换,如果要注入依赖,则应该注入到原始对象。
在初始化Bean阶段进行注入之前,我们调用BeanPostProcessor
的postProcessOnSetProperty
方法处理实例,拿到Bean
的原始对象再进行注入。
测试
我们可以写一个测试来验证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<String, Object> 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<String, Object> 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();
...
}
}