06.实现BeanPostProcessor


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。因为原本的BeanBeanPostProcessor扔掉了。

上面的例子在实际运用中可能很少能用到,但这种替换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 txcommit tx,说明“事务”生效了。

现在我们有代理类和被代理类,因此我们还要解决将原本Bean的实例替换后我们还需要解决两个问题:

  • 我们注入依赖时是注入到被代理类还是代理类。(注入到哪?)
  • 我们注入时是将被代理类注入还是将代理类注入。(注入谁?)

加入代理后的注入

下面是一个举一个实际例子,UserService是用户编写的业务代码,需要注入JdbcTemplate

@Component
class UserService {
    @Autowired JdbcTemplate jdbcTemplate;
    
    public void register(String email, String password) {
        jdbcTemplate.update("INSERT INTO ...");
    }
}

而我们这里使用PostBeanProcessorUserService的实例替换为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

此时我们有两个问题:

  1. 注意到UserServiceProxy是从UserService继承的,它也有一个@Autowired JdbcTemplate,那JdbcTemplate实例应注入到原始的UserService还是UserServiceProxy

因为代理类调用自己的register方法时实际会调用原始对象的register方法,因此不注入到原始对象时就会报NullPointerException异常。也就是说,代理对象实际依赖于原始对象,因此需要注入到原始对象

  1. MvcController需要注入的UserService,应该是原始的UserService还是UserServiceProxy

若是注入原始对象,那么我们将原始对象替换为代理对象就没有任何作用了,因此MvcController需要注入的是UserServiceProxy,也就是代理对象。

它们的关系如下:

image-20231227200551849

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

经过上面的分析,我们可以总结出两条注入的原则:

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

实现BeanPostProcessor

在上面我们知道我们加入BeanPostProcessor后我们的注入应该有下面两个原则:

  1. 一个Bean如果被Proxy替换,则依赖它的Bean应注入Proxy

  2. 一个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实例返回前插入执行BeanPostProcessorpostProcessBeforeInitialization方法:

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

总结

我们对两个条件的实现做一个总结:

  1. 一个Bean如果被Proxy替换,则依赖它的Bean应注入Proxy

实例化Bean阶段创建实例之前,我们先创建BeanPostProcessor的实例,并在创建实例之后调用BeanPostProcessorpostProcessBeforeInitialization方法替换实例后,替换之后依赖它的Bean注入的实例就是Proxy了。

  1. 一个Bean如果被Proxy替换,如果要注入依赖,则应该注入到原始对象

初始化Bean阶段进行注入之前,我们调用BeanPostProcessorpostProcessOnSetProperty方法处理实例,拿到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();
        ...
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值