spring源码扩展点与实战(⼆)
在上⼀篇⽂章,我们简单的介绍了 spring 的⼏个常⽤扩展点,了解了 BeanPostProcessor, BeanFactoryPostProcessor, BeanDefinitionRegistryPostProcessor, ApplicationListener, ApplicationContextInitializer 的作⽤,接下来主要介绍下在项⽬中的实际应⽤dubbo 服务改造
原有的 dubbo 架构如下图所⽰,Consumer 通过注册中⼼发现 Provider 服务,并进⾏调⽤
⽽现在需要进⾏改造,由 Consumer 通过 esb 完成对 Provider 端的调⽤,如下图所⽰:
我们来思考下解决⽅案,⾸先解决 dubbo 接⼝的问题,要求是注册的接⼝必须是 String ⼊参、出参的接⼝(⽅便 esb 进⾏泛化调⽤),这个还⽐较好弄,只要按照原来定义的接⼝,利⽤ asm ⽣成⼀个新的接⼝即可。下⾯是⼀个简单的⽰例,我们注册到 zookeeper 的是PaymentServiceStringFacade 接⼝,该接⼝实现类采⽤动态代理实现,主要是负责数据解析,并且调⽤ PaymentService 实现类对应的⽅法,这⼀块的代码就不贴出来了,有兴趣的童鞋可以在下⽅留⾔
public interface PaymentService {
public PayOffRespon payOff( PayOffRequest payOff );
public ConfirmPayResp confirmPay( ConfirmPayRequest confirmRequest );
}
public interface PaymentServiceStringFacade {
public String payOff( String payOff );
public String confirmPay( String confirmRequest );
}
针对 consumer 端,我们需要注⼊⾃⼰的实现类,⽽不再是 dubbo 默认的动态代理类,接下来,我们看看如何解决 consumer 端注⼊的PaymentService 实现类问题。由于我们的项⽬中使⽤的是 xml,因此这⾥以 xml 为例。我们知道,dubbo 会根据 xml 的 <dubbo:reference interface="xxx" /> 配置创建 ReferenceBean 的 BeanDefinition,那么我需要定义⼀个 BeanDefinitionRegistryPostProcessor 的实现类,把原来的dubbo 的 BeanDefinition 从 spring 容器中移除,然后根据这个 BeanDefinition 创建动态代理类,现在 spring 容器中没有对应的实现类,肯定是没法注⼊的,我们还需要通过这种调⽤⽅式 isterSingleton( BeanName(), proxy ) 把该动态代理类注册到 spring 容器中。这样 consumer 端注⼊的便是我们⾃⼰的定义的动态代理类了,⽽这个动态代理类要做的事情就是负责调⽤ esb(⾛http协议),并且把 esb 返回的结果反序列化成对象。关键的代码如下所⽰:
public class DubboConsumerAdapterSupport implements BeanDefinitionRegistryPostProcessor, InitializingBean {
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { // do nothing }
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
String[] names = BeanDefinitionNames();
for ( String name : names ) {
BeanDefinition definition = BeanDefinition( name );
// dubbo reference 对应的 bean
String beanClassName = BeanClassName();
if ( Name().equals( beanClassName ) ) {
Object proxy = ......; // ⽣成动态代理类
String sourceBeanName = StringUtils.uncapitalize( beanClassName ) + "$Adapter";
}
}
}
}
spring-task 动态任务
有⼀个定时任务的需求,需要⽀持动态地修改调度时间,我们知道使⽤ @Scheduled,或者基于 xml 的标签 <scheduled-tasks>...
</scheduled-tasks>,这种⽅式⼀旦指定 cron 表达式,是不能修改的,并且 spring 也没有提供 操作的 API
针对 @Scheduled, <scheduled-tasks>...</scheduled-tasks>,spring 分别使⽤了 ScheduledAnnotationBeanPostProcessor, ContextLifecycleScheduledTaskRegistrar 来处理定时任务的,并且这俩 bean 都是被注册到 spring 容器中的。既然他们不⽀持动态地修改cron 时间,我们可以写个⼦类扩展下功能就可以了,这个⽐较简单。另外就是需要替换 spring 中的实现类,也不难,可以⽤BeanFactoryPostProcessor 修改实现类。下⾯我以 <scheduled-tasks>...</scheduled-tasks> 这种 xml 配置为例,阐述下是如何实现扩展
spring task,让它⽀持动态修改调度时间,基于注解的配置也是同样的思路,这⾥只列出⼀个例⼦。
⾸先,写个类继承 ContextLifecycleScheduledTaskRegistrar,添加⽅法 modifyCronTask ⽤于修改定时任务的 cron 表达式,关键代码如下所⽰,完整代码请查看我的 。其中 ScheduledTask 是 spring 对定时任务的封装,由于是私有字段,因此使⽤了反射修改 cron 表达式,并且在修改之后重新执⾏ schedule ⽅法,确保修改后的 cron 表达式⽣效。关于修改 cron 表达式的过程有点繁锁,这⾥不做讲解,我们主要来看看对 spring task 源码扩展的思路
public class DynamicScheduledTaskRegistrar extends ContextLifecycleScheduledTaskRegistrar {
/**
* 动态修改Cron表达式,在运⾏的时候改变其执⾏时机
*/
public void modifyCronTask( Method method, String newCronExpression ) {
CronTask task = findCronTaskByMethod( method );
if ( task == null ) {
throw new IllegalArgumentException( "找不到对应的定时任务!" );
}
ScheduledTask scheduledTask = ( task );
if ( scheduledTask == null ) {
throw new IllegalArgumentException( "找不到对应的定时任务!" );
}
//构造⽅法⾥⾯会判断cron表达式是否正确
CronTrigger trigger = new CronTrigger( newCronExpression );
//使⽤反射的⽅法修改表达式的值,因为future对象都是protected的,不能直接操作
this.doMofidyCronIfSupported( scheduledTask, trigger );
logger.info( "Modify cron task success! Old cron:{}, new cron:{}", Expression(), newCronExpression );
}
}
上⾯我们实现了动态定时任务的逻辑,现在需要替换 spring task 的实现类,我们借助 BeanFactoryPostProcessor 来个移花接⽊,神不知⿁不觉地替换了默认的实现类,关键代码如下,完整代码同样参考我的 。实现思路很简单,遍历 BeanFactory 内部注册的所有BeanDefinition,并且找到 ContextLifecycleScheduledTaskRegistrar 的 bean 定义,然后调⽤ BeanDefinition#tBeanClassName() 修改具体的实现类
/**
* 改变Spring的{@link ContextLifecycleScheduledTaskRegistrar},实现对定时任务的动态操作
* @author huangxf
* @date 2017年5⽉1⽇
*/
public class DynamicScheduledTaskRegistrarSupport implements BeanFactoryPostProcessor {
/**
* 改变Spring默认注册Task的任务Bean
*/
public void postProcessBeanFactory(
ConfigurableListableBeanFactory beanFactory) throws BeansException {
for ( String bfName : BeanDefinitionNames() ) {
BeanDefinition bf = BeanDefinition( bfName );
// 在容器中可能有多个ContextLifecycleScheduledTaskRegistrar
if ( Name().equals( bf.getBeanClassName() ) ) {
// 改变默认的BeanClassName,改成⾃定义的DynamicScheduledTaskRegistrar实现对Task的操作扩展
bf.tBeanClassName( Name() );
}
}
}
}
接下来,只需要把这个 DynamicScheduledTaskRegistrarSupport 注册到 spring 容器中即可,有很多种⽅式,这⾥只介绍下注解的⽅式使⽤ @Bean 注解
@Configuration
public class AppConfig {
@Bean
public BeanFactoryPostProcessor dynamicScheduledTaskRegistrarSupport() {
return new DynamicScheduledTaskRegistrarSupport();
}
}
模仿 spring 的 @EnableXXX 注解,这种⽅式看上去⽐较洋⽓,其实道理都是⼀样的,不过是利⽤ spring 提供的 @Import 注解引⼊bean ⽽已。可以在框架层⾯编写 @EnableDynamicTask 注解,利⽤ @Import 导⼊需要的类,然后在 AppConfig 配置类上⾯使⽤@EnableDynamicTask 注解即可
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(DynamicScheduledTaskRegistrarSupport.class)
public @interface EnableDynamicTask {
}
@Configuration
@EnableDynamicTask
public class AppConfig {
}
Spring IoC
在某些场景,⽐如与 resteasy 框架集成,我们需要在 spring 容器启动前,提前把某些对象 new 出来,但是我们⼜希望利⽤ spring 容器的 IoC 对 new 出来的对象进⾏依赖注⼊。
这种场景下,我们需要借助 ApplicationListener 来完成这个任务。⾸先,想办法往 ConfigurableWebApplicationContext 注册ApplicationListener<ContextRefreshedEvent> 的实现类,可以重写 ContextLoaderListener 的 customizeContext ⽅法,或者往web容器中添加ApplicationContextInitializer。⽽这个 ApplicationListener<ContextRefreshedEvent> 的实现类⾥⾯要做的事情是在 onApplicationEvent(event) ⽅法⾥⾯,调⽤ AutowireCapableBeanFactory().autowireBean( bean ),我们便可以对⾃⼰实例化的类进⾏依赖注⼊了。
下⾯,我们以扩展 resteasy 功能的代码为例,我们要达到的⽬的是简化 spring 与 resteasy 的集成,并且扩展 resteasy 的注⼊功能,因此 PaymentInjectorFactory 重写了 InjectorFactoryImpl,但是 Pay
mentInjectorFactory 需要注⼊ spring 容器中的 bean,并且PaymentInjectorFactory 对象需要在 spring 容器之前创建。关键代码如下所⽰,PaymentInjectorFactory 实现了
ApplicationListener<ContextRefreshedEvent> 接⼝,在接到 spring 的 ContextRefreshedEvent 事件之后,会对⾃⼰、以及它创建的PaymentMethodInjector 对象进⼊依赖注⼊,以下完整代码请查看
public class PaymentInjectorFactory extends InjectorFactoryImpl implements ApplicationListener<ContextRefreshedEvent> {
// 由spring完成依赖注⼊
@Resource
private PartnerSignService partnerSignService;
private Set<PaymentMethodInjector> providerProxySet = wSetFromMap(new ConcurrentHashMap<PaymentMethodInjector, Boolean>());
/**
* 返回{@link PaymentMethodInjector}
*/
@Override
public MethodInjector createMethodInjector(ResourceLocator method,
ResteasyProviderFactory factory) {
PaymentMethodInjector paymentMethodInjector = new PaymentMethodInjector( method, factory );
providerProxySet.add(paymentMethodInjector);
return paymentMethodInjector;
}
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
ApplicationContext application = ApplicationContext();
/
/ 对创建的PaymentMethodInjector进⾏依赖注⼊
for ( PaymentMethodInjector injector : providerProxySet ) {
}
// 注⼊ PartnerSignService
}
}
现在还要做的事情,是想办法把该 PaymentInjectorFactory 作为⼀个 ApplicationListener 注册到 spring 容器中。我们可以重写ContextLoaderListener#customizeContext() ⽅法,通过调⽤ ConfigurableWebApplicationContext#addApplicationListener(),从⽽往ApplicationContext 注册 ApplicationListener,关键代码如下所⽰
public class SpringResteasyBootstrap extends ContextLoaderListener implements ServletContextListener {
private SpringContextLoaderSupport springContextLoaderSupport = new SpringContextLoaderSupport();
private static final PaymentInjectorFactory injectorFactory = new PaymentInjectorFactory();
public void contextInitialized( ServletContextEvent event ) {
// 完成 spring 与 resteasy的集成
}
/**
* 主要⽬的是将SpringBeanProcessor注册到Spring容器中
* @e SpringContextLoaderListener
*/
@Override
protected void customizeContext( ServletContext rvletContext, ConfigurableWebApplicationContext webContext ) {
webContext.addApplicationListener( injectorFactory );
}
}
于是,只要把该 SpringResteasyBootstrap 配置到 l 中即可