Spring Boot整合Dubbo Provider

一、前言

前一篇《Spring Boot整合Dubbo Consumer》介绍了Dubbo Consumer与Spring Boot的整合流程,这一篇接着补齐Spring Boot整合Dubbo Provider。Consumer和Provider各自与Spring整合的流程比较独立,分别用到的是Spring两大核心扩展机制:前者是基于BeanPostProcessor,后者是基于BeanFactoryPostProcessor扩展机制。不仅仅只Dubbo,其他开源组件与Spring整合(如MyBatis、Kafka),基本也是这利用两种扩展机制。故本文虽说是讲Dubbo与Spring整合,但更多的是希望读者通过Dubbo这个具体case,熟悉、掌握Spring两大扩展机制,更加深刻地理解Spring。


二、Provider 整合

Consumer整合,利用的是 ReferenceAnnotationBeanPostProcessor 这种由Spring提供的BeanPostProcessor扩展机制;同样,Provider整合也是充分利用了Spring扩展机制,不过稍有不同的是,这次使用的是基于BeanDefinition的扩展机制BeanFactoryPostProcessor。这里有必要补充下 BeanPostProcessor 和 BeanFactoryPostProcessor 的区别:

  • BeanPostProcessor: 主要作用于Bean对象生命周期中实例化(对象实例化前、后)、初始化(Bean初始化前、后)这两个阶段;对Spring Bean的生命周期还不太清晰的话,可参加楼主之前的博文【俯瞰Spring】二、Bean的生命周期
  • BeanFactoryPostProcessor: 作用于Bean对象的BeanDefinition的生成和注册阶段;
  • BeanPostProcessor提供的方法在每一个Bean的生命周期中都会触发一次调用,也就是说如果有100个Bean,每个BeanPostProcessor的各生命周期回调方法都会被触发100次;而BeanFactoryPostProcessor可理解为容器级别的扩展机制,整个ioc容器启动过程中,仅随ioc容器启动触发一次。
  • 在Spring中,是先有BeanDefinition,然后根据BeanDefinition创建出Bean对象的,故BeanFactoryPostProcessor 的触发时间先与BeanPostProcessor。

2.1 ServiceAnnotationBeanPostProcessor 类层次结构

具体而言,在Apache官方提供的dubbo-spring-boot-starter中,使用到的是BeanFactoryPostProcessor的实现类ServiceAnnotationBeanPostProcessor;这里吐槽下ServiceAnnotationBeanPostProcessor这个名字取得有歧义,本身并非BeanPostProcessor子类,而是BeanFactoryPostProcessor的子类,按Spring的命名习惯,叫XXXFacotroyPostProcessor应该更贴切!
在这里插入图片描述

从ServiceAnnotationBeanPostProcessor 的类层次结构图,可以看到其实现了 BeanDefinitionRegistryPostProcessor 这个接口

-BeanDefinitionRegistryPostProcessor
作用:在bean实例之前,对ApplicationContext已存在的BeanDefinition进行修改,也可以用来继续向ApplicationContext生成并注册新的BeanDefinition

public interface BeanDefinitionRegistryPostProcessor extends BeanFactoryPostProcessor {

   /**
    * Modify the application context's internal bean definition registry after its
    * standard initialization. All regular bean definitions will have been loaded,
    * but no beans will have been instantiated yet. This allows for adding further
    * bean definitions before the next post-processing phase kicks in.
    * @param registry the bean definition registry used by the application context
    * @throws org.springframework.beans.BeansException in case of errors
    */
   void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException;

}

2.2 Provider 整合流程

在这里插入图片描述

简要概括下:

  • 1、 @EnableDubbo注解组合了@DubboComponentScan@EnableDubboConfig 两个注解;

  • 2、一方面,@EnableDubboConfig通过import机制引入了DubboConfigConfigurationRegistrar,注册了DubboConfigConfiguration.Multiple 或 DubboConfigConfiguration.Single 的BeanDefition;这条分支可理解为Dubbo是在为其启动加载配置文件;

  • 3、另一方面,@DubboComponentScan 通过import机制引入了DubboComponentScanRegistrar,其做了两件事:a.向容器导入了Dubbo启动环境需要的5大组件(上图天蓝色部分); b.向容器导入了ServiceAnnotationBeanPostProcessor 这个BeanFactoryPostProcessor组件,当容器启动BeanDefiniton加载解析阶段,用来做包扫描,对扫描到的带Dubbo的@Service 注解的类,生成BeanDefinition, 并注册到容器。这一步就是Dubbo Service托管到Spring的核心原理!

  • 4、上面3个步骤,发生在Bean定义解析、注册阶段,除BeanPostProcessor类型组件外,其Dubbo Serice对应的Bean还未开始实例化。当Spring Ioc容器完成刷新时,会触发ContextRefreshedEvent事件,DubboBootstrapApplicationListener接收到事件后,直接调用DubboBootstrap的start()方法,全部Dubbo Service服务导出逻辑就统一收敛在该方法里面!

经过以上步骤,Dubbo Service对应的bean已托管到Spring,并且也DubboBootstrap.start()之后,所以的Dubbo服务都已完成服务导出。

2.3 源码佐证

2.3.1 注解驱动

结合源码串一下2.2节中提到的几个核心注解

  • @EnableDubbo 注解
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@EnableDubboConfig
@DubboComponentScan
public @interface EnableDubbo {

    @AliasFor(annotation = DubboComponentScan.class, attribute = "basePackages")
    String[] scanBasePackages() default {};

    @AliasFor(annotation = DubboComponentScan.class, attribute = "basePackageClasses")
    Class<?>[] scanBasePackageClasses() default {};

    @AliasFor(annotation = EnableDubboConfig.class, attribute = "multiple")
    boolean multipleConfig() default true;

}
  • @DubboComponentScan 注解
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(DubboComponentScanRegistrar.class)
public @interface DubboComponentScan {

    String[] value() default {};

    String[] basePackages() default {};

    Class<?>[] basePackageClasses() default {};

}
  • @EnableDubboConfig 注解
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@Import(DubboConfigConfigurationRegistrar.class)
public @interface EnableDubboConfig {

    /**
     * It indicates whether binding to multiple Spring Beans.
     *
     * @return the default value is <code>false</code>
     * @revised 2.5.9
     */
    boolean multiple() default true;

}

2.3.2 Dubbo配置文件加载

通过 DubboConfigConfigurationRegistrar,导入的 DubboConfigConfiguration.Single 或 DubboConfigConfiguration.Multiple,集中完成Dubbo所需的Application、Regsitry等配置文件加载

  • DubboConfigConfiguration
public class DubboConfigConfiguration {

    /**
     * Single Dubbo {@link AbstractConfig Config} Bean Binding
     */
    @EnableConfigurationBeanBindings({
            @EnableConfigurationBeanBinding(prefix = "dubbo.application", type = ApplicationConfig.class),
            @EnableConfigurationBeanBinding(prefix = "dubbo.module", type = ModuleConfig.class),
            @EnableConfigurationBeanBinding(prefix = "dubbo.registry", type = RegistryConfig.class),
            @EnableConfigurationBeanBinding(prefix = "dubbo.protocol", type = ProtocolConfig.class),
            @EnableConfigurationBeanBinding(prefix = "dubbo.monitor", type = MonitorConfig.class),
            @EnableConfigurationBeanBinding(prefix = "dubbo.provider", type = ProviderConfig.class),
            @EnableConfigurationBeanBinding(prefix = "dubbo.consumer", type = ConsumerConfig.class),
            @EnableConfigurationBeanBinding(prefix = "dubbo.config-center", type = ConfigCenterBean.class),
            @EnableConfigurationBeanBinding(prefix = "dubbo.metadata-report", type = MetadataReportConfig.class),
            @EnableConfigurationBeanBinding(prefix = "dubbo.metrics", type = MetricsConfig.class),
            @EnableConfigurationBeanBinding(prefix = "dubbo.ssl", type = SslConfig.class)
    })
    public static class Single {

    }

    /**
     * Multiple Dubbo {@link AbstractConfig Config} Bean Binding
     */
    @EnableConfigurationBeanBindings({
            @EnableConfigurationBeanBinding(prefix = "dubbo.applications", type = ApplicationConfig.class, multiple = true),
            @EnableConfigurationBeanBinding(prefix = "dubbo.modules", type = ModuleConfig.class, multiple = true),
            @EnableConfigurationBeanBinding(prefix = "dubbo.registries", type = RegistryConfig.class, multiple = true),
            @EnableConfigurationBeanBinding(prefix = "dubbo.protocols", type = ProtocolConfig.class, multiple = true),
            @EnableConfigurationBeanBinding(prefix = "dubbo.monitors", type = MonitorConfig.class, multiple = true),
            @EnableConfigurationBeanBinding(prefix = "dubbo.providers", type = ProviderConfig.class, multiple = true),
            @EnableConfigurationBeanBinding(prefix = "dubbo.consumers", type = ConsumerConfig.class, multiple = true),
            @EnableConfigurationBeanBinding(prefix = "dubbo.config-centers", type = ConfigCenterBean.class, multiple = true),
            @EnableConfigurationBeanBinding(prefix = "dubbo.metadata-reports", type = MetadataReportConfig.class, multiple = true),
            @EnableConfigurationBeanBinding(prefix = "dubbo.metricses", type = MetricsConfig.class, multiple = true)
    })
    public static class Multiple {

    }
}

2.3.3 ServiceAnnotationBeanPostProcessor 包扫描

包扫描要做的事情,就是筛选出带 Dubbo @Service 注解的Java类,逐一生成对应的 BeanDefinition,并进行注册。

  • 导入 ServiceAnnotationBeanPostProcessor
public class DubboComponentScanRegistrar implements ImportBeanDefinitionRegistrar {

    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {

        Set<String> packagesToScan = getPackagesToScan(importingClassMetadata);
        // 导入 ServiceAnnotationBeanPostProcessor 组件
        registerServiceAnnotationBeanPostProcessor(packagesToScan, registry);

        // @since 2.7.6 Register the common beans
        registerCommonBeans(registry);
    }

    /**
     * Registers {@link ServiceAnnotationBeanPostProcessor}
     *
     * @param packagesToScan packages to scan without resolving placeholders
     * @param registry       {@link BeanDefinitionRegistry}
     * @since 2.5.8
     */
    private void registerServiceAnnotationBeanPostProcessor(Set<String> packagesToScan, BeanDefinitionRegistry registry) {

        // 1.生成 ServiceAnnotationBeanPostProcessor 对应的BeanDefinition
        BeanDefinitionBuilder builder = rootBeanDefinition(ServiceAnnotationBeanPostProcessor.class);
        // 指定要扫描的包
        builder.addConstructorArgValue(packagesToScan);
        builder.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
        AbstractBeanDefinition beanDefinition = builder.getBeanDefinition();
        // 2.注册 BeanDefinition
        BeanDefinitionReaderUtils.registerWithGeneratedName(beanDefinition, registry);

    }

    private Set<String> getPackagesToScan(AnnotationMetadata metadata) {
        // 常用套路: 解析得到注解的属性信息
        AnnotationAttributes attributes = AnnotationAttributes.fromMap(
                metadata.getAnnotationAttributes(DubboComponentScan.class.getName()));
        String[] basePackages = attributes.getStringArray("basePackages");
        Class<?>[] basePackageClasses = attributes.getClassArray("basePackageClasses");
        String[] value = attributes.getStringArray("value");
        // Appends value array attributes
        Set<String> packagesToScan = new LinkedHashSet<String>(Arrays.asList(value));
        packagesToScan.addAll(Arrays.asList(basePackages));
        for (Class<?> basePackageClass : basePackageClasses) {
            packagesToScan.add(ClassUtils.getPackageName(basePackageClass));
        }
        if (packagesToScan.isEmpty()) {
            // 未指定包名,则以当前类所在的包名作为扫描起点
            return Collections.singleton(ClassUtils.getPackageName(metadata.getClassName()));
        }
        return packagesToScan;
    }

}
  • 包扫描

真正进入到包扫描执行流程了,这块知识比较硬核,需耐心点看懂。

public class ServiceAnnotationBeanPostProcessor implements BeanDefinitionRegistryPostProcessor, EnvironmentAware,
        ResourceLoaderAware, BeanClassLoaderAware {

    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {

        // @since 2.7.5
        registerBeans(registry, DubboBootstrapApplicationListener.class);

        // 要扫描的包:packagesToScan是从构造函数传入的
        Set<String> resolvedPackagesToScan = resolvePackagesToScan(packagesToScan);

        if (!CollectionUtils.isEmpty(resolvedPackagesToScan)) {
            // 筛选出 @Service 注解的类,生成BeanDefinition并注册
            registerServiceBeans(resolvedPackagesToScan, registry);
        } else {
            if (logger.isWarnEnabled()) {
                logger.warn("packagesToScan is empty , ServiceBean registry will be ignored!");
            }
        }

    }

    private void registerServiceBeans(Set<String> packagesToScan, BeanDefinitionRegistry registry) {

        // 【核心】1.自定义一个包扫描器(其实大部分就是覆写父类方法,最大的改动就是禁止使用默认的筛选器, 即useDefaultFilters=false)
        DubboClassPathBeanDefinitionScanner scanner =
                new DubboClassPathBeanDefinitionScanner(registry, environment, resourceLoader);

        BeanNameGenerator beanNameGenerator = resolveBeanNameGenerator(registry);

        scanner.setBeanNameGenerator(beanNameGenerator);

        // 2.指定包扫描通过 @Service 来过滤筛选!
        scanner.addIncludeFilter(new AnnotationTypeFilter(Service.class));

        /**
         * Add the compatibility for legacy Dubbo's @Service
         *
         * The issue : https://github.com/apache/dubbo/issues/4330
         * @since 2.7.3
         */
        scanner.addIncludeFilter(new AnnotationTypeFilter(com.alibaba.dubbo.config.annotation.Service.class));

        for (String packageToScan : packagesToScan) {

            // Registers @Service Bean first
            scanner.scan(packageToScan);

            // 3.粗略生成全部 @Service 注解类的 BeanDefinition; 
            // 这时的 BeanDefinition 实际类型都是 ScannedGenericBeanDefinition,没什么特别有价值的信息,还需要再加工
            // Finds all BeanDefinitionHolders of @Service whether @ComponentScan scans or not.
            Set<BeanDefinitionHolder> beanDefinitionHolders =
                    findServiceBeanDefinitionHolders(scanner, packageToScan, registry, beanNameGenerator);

            if (!CollectionUtils.isEmpty(beanDefinitionHolders)) {

                for (BeanDefinitionHolder beanDefinitionHolder : beanDefinitionHolders) {
                    // 【核心】4、进一步加工并注册 BeanDefinition
                    registerServiceBean(beanDefinitionHolder, registry, scanner);
                }

                if (logger.isInfoEnabled()) {
                    logger.info(beanDefinitionHolders.size() + " annotated Dubbo's @Service Components { " +
                            beanDefinitionHolders +
                            " } were scanned under package[" + packageToScan + "]");
                }

            } else {

                if (logger.isWarnEnabled()) {
                    logger.warn("No Spring Bean annotating Dubbo's @Service was found under package["
                            + packageToScan + "]");
                }

            }

        }

    }


    private void registerServiceBean(BeanDefinitionHolder beanDefinitionHolder, BeanDefinitionRegistry registry,
                                     DubboClassPathBeanDefinitionScanner scanner) {

        Class<?> beanClass = resolveClass(beanDefinitionHolder);
       
        Annotation service = findServiceAnnotation(beanClass);
        // 解析类上面的 @Service注解,得到属性信息
        AnnotationAttributes serviceAnnotationAttributes = getAnnotationAttributes(service, false, false);

        Class<?> interfaceClass = resolveServiceInterfaceClass(serviceAnnotationAttributes, beanClass);

        String annotatedServiceBeanName = beanDefinitionHolder.getBeanName();

        // 对包扫描得到的 ScannedGenericBeanDefinition 进一步加工,转换成beanClass为ServiceBean 的 BeanDefinition
        AbstractBeanDefinition serviceBeanDefinition =
                buildServiceBeanDefinition(service, serviceAnnotationAttributes, interfaceClass, annotatedServiceBeanName);

        // ServiceBean Bean name
        String beanName = generateServiceBeanName(serviceAnnotationAttributes, interfaceClass);

        if (scanner.checkCandidate(beanName, serviceBeanDefinition)) { // check duplicated candidate bean
            registry.registerBeanDefinition(beanName, serviceBeanDefinition);

            if (logger.isInfoEnabled()) {
                logger.info("The BeanDefinition[" + serviceBeanDefinition +
                        "] of ServiceBean has been registered with name : " + beanName);
            }

        } else {

            if (logger.isWarnEnabled()) {
                logger.warn("The Duplicated BeanDefinition[" + serviceBeanDefinition +
                        "] of ServiceBean[ bean name : " + beanName +
                        "] was be found , Did @DubboComponentScan scan to same package in many times?");
            }

        }

    }

    private AbstractBeanDefinition buildServiceBeanDefinition(Annotation serviceAnnotation,
                                                              AnnotationAttributes serviceAnnotationAttributes,
                                                              Class<?> interfaceClass,
                                                              String annotatedServiceBeanName) {

        // 对 ScannedGenericBeanDefinition 进一步加工,转换成beanClass为ServiceBean 的 BeanDefinition
        BeanDefinitionBuilder builder = rootBeanDefinition(ServiceBean.class);

        AbstractBeanDefinition beanDefinition = builder.getBeanDefinition();

        // 后面的逻辑都是往 propertyValues 塞一些数据,省略部分。。。
        MutablePropertyValues propertyValues = beanDefinition.getPropertyValues();

        String[] ignoreAttributeNames = of("provider", "monitor", "application", "module", "registry", "protocol",
                "interface", "interfaceName", "parameters");

        propertyValues.addPropertyValues(new AnnotationPropertyValuesAdapter(serviceAnnotation, environment, ignoreAttributeNames));

        // References "ref" property to annotated-@Service Bean
        addPropertyReference(builder, "ref", annotatedServiceBeanName);
        // Set interface
        builder.addPropertyValue("interface", interfaceClass.getName());
        // Convert parameters into map
     
        // 后面的逻辑都是往 propertyValues 塞一些数据,省略部分。。。
        return builder.getBeanDefinition();

    }

}

至此,就完成了Dubbo原生的 @Service 注解的包扫描,依次为每个类并生成、注册了beanClass为ServiceBean的BeanDefinition;当Spring Ioc容器完成刷新后,遍历这些BeanDefiniton,Spring就会完成对应Bean的实例化、依赖注入、初始化生命周期,各ServiceBean也统一交给Spring容器托管了。


三、Provider 侧服务导出

上面已经理清Dubbo Service托管到Spring的过程,这一小节再单独分析下Dubbo服务是在哪个环节执行了服务导出。

3.1 服务导出

为了将Dubbo服务导出流程与Dubbo Service托管到Spring的流程解耦,这里又会见识到Spring提供的第三种扩展机制:事件监听!对照本文最开始的那副图,可以看到DubboBootstrapApplicationListener 这个对象是Spring容器一启动就注册了。当IoC容器初始化完成时,会触发ContextRefreshedEvent事件,该事件监听器接收到此类事件后,直接调用DubboBootstrap.start()方法完成全部Dubbo Service的服务导出。

  • DubboBootstrapApplicationListener 事件监听器
public class DubboBootstrapApplicationListener extends OneTimeExecutionApplicationContextEventListener
        implements Ordered {

    public static final String BEAN_NAME = "dubboBootstrapApplicationListener";

    private final DubboBootstrap dubboBootstrap;

    public DubboBootstrapApplicationListener() {
        // DubboBootstrap 是一个单例,Dubbo启动环境统一
        this.dubboBootstrap = DubboBootstrap.getInstance();
    }

    @Override
    public void onApplicationContextEvent(ApplicationContextEvent event) {
        if (event instanceof ContextRefreshedEvent) {
            // 处理上下文刷新事件
            onContextRefreshedEvent((ContextRefreshedEvent) event);
        } else if (event instanceof ContextClosedEvent) {
            onContextClosedEvent((ContextClosedEvent) event);
        }
    }

    private void onContextRefreshedEvent(ContextRefreshedEvent event) {
        dubboBootstrap.start();
    }

    private void onContextClosedEvent(ContextClosedEvent event) {
        dubboBootstrap.stop();
    }

    @Override
    public int getOrder() {
        return LOWEST_PRECEDENCE;
    }
}
  • DubboBootstrap 启动类
public class DubboBootstrap extends GenericEventListener {

    private static DubboBootstrap instance;

    private AtomicBoolean initialized = new AtomicBoolean(false);

    private AtomicBoolean started = new AtomicBoolean(false);


    /**
     * See {@link ApplicationModel} and {@link ExtensionLoader} for why DubboBootstrap is designed to be singleton.
     */
    public static synchronized DubboBootstrap getInstance() {
        if (instance == null) {
            instance = new DubboBootstrap();
        }
        return instance;
    }

    /**
     * Start the bootstrap
     */
    public DubboBootstrap start() {
        // CAS机制保证只启动一次
        if (started.compareAndSet(false, true)) {
            // DubboBootstrap 初始化全流程,妥妥滴模板方法;也用到了CAS机制保证只初始化一次
            initialize();
            if (logger.isInfoEnabled()) {
                logger.info(NAME + " is starting...");
            }
            // 【核心】 导出Dubbo服务
            // 1. export Dubbo Services
            exportServices();

            // Not only provider register
            if (!isOnlyRegisterProvider() || hasExportedServices()) {
                // 2. export MetadataService
                exportMetadataService();
                //3. Register the local ServiceInstance if required
                registerServiceInstance();
            }

            referServices();

            if (logger.isInfoEnabled()) {
                logger.info(NAME + " has started.");
            }
        }
        return this;
    }

    @Deprecated
    public void init() {
        initialize();
    }

    /**
     * Initialize
     */
    private void initialize() {
        if (!initialized.compareAndSet(false, true)) {
            return;
        }

        ApplicationModel.initFrameworkExts();

        startConfigCenter();

        useRegistryAsConfigCenterIfNecessary();

        loadRemoteConfigs();

        checkGlobalConfigs();

        initMetadataService();

        initEventListener();

        if (logger.isInfoEnabled()) {
            logger.info(NAME + " has been initialized!");
        }
    }

    private void exportServices() {
        configManager.getServices().forEach(sc -> {
            // TODO, compatible with ServiceConfig.export()
            ServiceConfig serviceConfig = (ServiceConfig) sc;
            serviceConfig.setBootstrap(this);

            if (exportAsync) {
                // 异步服务导出
                ExecutorService executor = executorRepository.getServiceExporterExecutor();
                Future<?> future = executor.submit(() -> {
                    sc.export();
                });
                asyncExportingFutures.add(future);
            } else {
                // 同步服务导出
                sc.export();
                exportedServices.add(sc);
            }
        });
    }

  
}

四、总结

前一篇《Spring Boot整合Dubbo Consumer》 加上本篇,分别以图示+源码的方式,分析了Spring Boot与Dubbo Consumer、Provider整合流程及实现机制,实现思路概括下来就是充分利用Spring的两大核心扩展机制:BeanPostProcessorBeanFactoryPostProcessor

  • 对于Dubbo Consumer,采用 ReferenceAnnotationBeanPostProcessor ,完成对 @Reference 属性或方法的依赖注入;
  • 对于Dubbo Provider,采用 ServiceAnnotationBeanPostProcessor,完成对 @Service 注解的包扫描和BeanDefinition注册。

由于本文写作的重点放在探究Spring与Dubbo的整合上,故对于Dubbo consumer 的「服务引入」和provider的「服务导出」在前后两篇中均未做详细介绍。但如果要用好Dubbo,还是建议花时间钻研细节加深理解。通过这两篇文章,在了解Dubbo与Spring整合流程之外,楼主认为有以下(小)技巧是值得学习借鉴的:

  • 如何自定义一个类注解,让Spring能将注解所在的类加载到IoC容器?(结合@Reference注解依赖注入过程、参照@Resource注解依赖注入实现原理)

  • 如何自定义一个属性或注解,让Spring能够对注解的字段或方法进行依赖注入?(参照Dubbo @Service 包扫描器,再了解下Mybatis的@MapperScan实现原理)

  • 如何保证Spring事件监听器不重复处理事件(参照org.apache.dubbo.config.spring.context.OneTimeExecutionApplicationContextEventListener)

  • 获得一个类带某种注解的属性或方法(见org.springframework.util.ReflectionUtils)

码字不易,如果有收获,顺手点个赞呗。全文完~

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值