Integration principle and source code analysis of Spring and Dubbo

Posted by a.beam.reach on Sun, 19 Dec 2021 18:33:10 +0100

Overall architecture and process

  • Main configuration startup class
package org.apache.dubbo.demo.provider;

import org.apache.dubbo.config.spring.context.annotation.EnableDubbo;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;

public class Application {
    public static void main(String[] args) throws Exception {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(ProviderConfiguration.class);
        context.start();

        System.in.read();
    }

    @Configuration
    @EnableDubbo(scanBasePackages = "org.apache.dubbo.demo.provider")
    @PropertySource("classpath:/spring/dubbo-provider.properties")   // Enviroment
    static class ProviderConfiguration {

    }
}

  • dubbo-provider.properties configuration file
dubbo.application.name=dubbo-demo-provider1-application
dubbo.application.logger=log4j
dubbo.application.timeout=3000


#dubbo.protocol.name=dubbo
#dubbo.protocol.port=20881
#dubbo.protocol.host=0.0.0.0

dubbo.protocols.p1.name=dubbo
dubbo.protocols.p1.port=20880
dubbo.protocols.p1.host=0.0.0.0

dubbo.protocols.p2.name=dubbo
dubbo.protocols.p2.port=20881
dubbo.protocols.p2.host=0.0.0.0



dubbo.registries.r1.address=zookeeper://192.168.1.104:2181
dubbo.registries.r1.timeout=3000
#dubbo.registries.r1.address=zookeeper://127.0.0.1:9999?backup=127.0.0.1:8989,127.0.0.1:2181
#dubbo.registries.r2.address=redis://192.168.99.100:6379


#dubbo.config-center.address=zookeeper://127.0.0.1:2181

#dubbo.metadata-report.address=zookeeper://127.0.0.1:2181

The application configuration class is ProviderConfiguration. There are two important comments on the configuration

  1. @PropertySource means Dubbo provider The configuration item in properties is added to the Spring container, and the Value in the configuration item can be obtained through @ Value
  2. @EnableDubbo(scanBasePackages = "org.apache.dubbo.demo.provider") means to scan the classes under the specified package, scan @ Service and @ Reference annotations, and process them

@EnableDubbo

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

    // @The EnableDubboConfig annotation is used to convert configuration items in the properties file into corresponding beans
    // @The DubboComponentScan annotation is used to scan service providers and references (@ Service)


    
    @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;

}

On the enable Dubbo annotation, there are two other annotations, which are also the two most important annotations for studying Dubbo

  1. @EnableDubboConfig
  2. @DubboComponentScan
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@Import(DubboConfigConfigurationRegistrar.class)
public @interface EnableDubboConfig {
    boolean multiple() default true;
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(DubboComponentScanRegistrar.class)
public @interface DubboComponentScan {
    String[] value() default {};

    String[] basePackages() default {};

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

}

Note the classes imported by the corresponding @ Import annotation in the two annotations:

  1. DubboConfigConfigurationRegistrar
  2. DubboComponentScanRegistrar

Spring will parse these two annotations at startup and execute the registerBeanDefinitions method in the corresponding Registrar class (this is an extended function provided in spring)

Property file parsing and processing principle in Dubbo

DubboConfigConfigurationRegistrar

When Spring starts, it will call the registerBeanDefinitions method of dubboconfigurationregistrar, which uses the AnnotatedBeanDefinitionReader in Spring to read:
DubboConfigConfiguration.Single.class
DubboConfigConfiguration.Multiple.class
Annotations on these two classes.

@EnableDubboConfigBindings({
    @EnableDubboConfigBinding(prefix = "dubbo.application", type = ApplicationConfig.class),
    @EnableDubboConfigBinding(prefix = "dubbo.module", type = ModuleConfig.class),
    @EnableDubboConfigBinding(prefix = "dubbo.registry", type = RegistryConfig.class),
    @EnableDubboConfigBinding(prefix = "dubbo.protocol", type = ProtocolConfig.class),
    @EnableDubboConfigBinding(prefix = "dubbo.monitor", type = MonitorConfig.class),
    @EnableDubboConfigBinding(prefix = "dubbo.provider", type = ProviderConfig.class),
    @EnableDubboConfigBinding(prefix = "dubbo.consumer", type = ConsumerConfig.class),
    @EnableDubboConfigBinding(prefix = "dubbo.config-center", type = ConfigCenterBean.class),
    @EnableDubboConfigBinding(prefix = "dubbo.metadata-report", type = MetadataReportConfig.class),
    @EnableDubboConfigBinding(prefix = "dubbo.metrics", type = MetricsConfig.class)
})
public static class Single {

}
@EnableDubboConfigBindings({
	@EnableDubboConfigBinding(prefix = "dubbo.applications", type = ApplicationConfig.class, multiple = true),
	@EnableDubboConfigBinding(prefix = "dubbo.modules", type = ModuleConfig.class, multiple = true),
	@EnableDubboConfigBinding(prefix = "dubbo.registries", type = RegistryConfig.class, multiple = true),
	@EnableDubboConfigBinding(prefix = "dubbo.protocols", type = ProtocolConfig.class, multiple = true),
	@EnableDubboConfigBinding(prefix = "dubbo.monitors", type = MonitorConfig.class, multiple = true),
	@EnableDubboConfigBinding(prefix = "dubbo.providers", type = ProviderConfig.class, multiple = true),
	@EnableDubboConfigBinding(prefix = "dubbo.consumers", type = ConsumerConfig.class, multiple = true),
	@EnableDubboConfigBinding(prefix = "dubbo.config-centers", type = ConfigCenterBean.class, multiple = true),
	@EnableDubboConfigBinding(prefix = "dubbo.metadata-reports", type = MetadataReportConfig.class, multiple = true),
	@EnableDubboConfigBinding(prefix = "dubbo.metricses", type = MetricsConfig.class, multiple = true)
})
public static class Multiple {

}

These two classes mainly use the @ EnableDubboConfigBindings annotation:

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(DubboConfigBindingsRegistrar.class)
public @interface EnableDubboConfigBindings {

    /**
     * The value of {@link EnableDubboConfigBindings}
     *
     * @return non-null
     */
    EnableDubboConfigBinding[] value();

}

@There is also an @ Import annotation on the EnableDubboConfigBindings annotation, which imports dubboconfigbindingsregistrar class. This class will get the value in the @ EnableDubboConfigBindings annotation, that is, multiple @ EnableDubboConfigBinding annotations, and then use the dubboconfigbindingregister to process these @ EnableDubboConfigBinding annotations.

DubboConfigBindingRegistrar

The main method in this class is the registerDubboConfigBeans() method. Its main function is to obtain the contents of the properties file set by the user, parse the properties file, and generate the corresponding BeanDefinition according to the prefix, parameter name and parameter value of each configuration item in the properties file.

  • for instance
dubbo.application.name=dubbo-demo-provider1-application
dubbo.application.logger=log4j

The configuration item prefixed with "dubbo.application" will generate a BeanDefinition of ApplicationConfig type, and the name and logger properties are the corresponding values.

  • Example 2
dubbo.protocols.p1.name=dubbo
dubbo.protocols.p1.port=20880
dubbo.protocols.p1.host=0.0.0.0

dubbo.protocols.p2.name=dubbo
dubbo.protocols.p2.port=20881
dubbo.protocols.p2.host=0.0.0.0

For example, a configuration item prefixed with "dubbo.protocols" will generate two ProtocolConfig type beandefinitions whose beannames are p1 and p2 respectively.

It will also generate a one-to-one binding BeanPostProcessor for each generated BeanDefinition, with the type of dubboconfigbindingbeanpostprocessor class.


DubboConfigBindingBeanPostProcessor

DubboConfigBindingBeanPostProcessor is a BeanPostProcessor. During Spring startup, post processing will be performed on all Bean objects, but the following judgment is made in DubboConfigBindingBeanPostProcessor

if (this.beanName.equals(beanName) && bean instanceof AbstractConfig)

Therefore, DubboConfigBindingBeanPostProcessor will not process all beans in the Spring container. It will only process the Bean objects generated by Dubbo above.

In addition, in the afterpropertieset () method, a DefaultDubboConfigBinder will be created first.

DefaultDubboConfigBinder

When a Bean of AbstractConfig type is processed by DubboConfigBindingBeanPostProcessor, the attribute in the Bean object has no value, and DefaultDubboConfigBinder will be used for assignment. The bottom layer is to use the DataBinder technology in Spring and the properties file to assign the corresponding properties.

Corresponds to an AbstractConfig type (for the Bean of subclasses, such as ApplicationConfig and RegistryConfig), each class has some properties, and the properties file is a key value pair, so in fact, DataBinder matches the property name with the key in the properties file. If the matching is successful, value is assigned to the property. Please learn how the DataBinder technology works by yourself Practice (not difficult).

for instance:

dubbo.application.name=dubbo-demo-provider1-application
dubbo.application.logger=log4j

For this configuration, it corresponds to the ApplicationConfig object (beanName is automatically generated), so the value of the name attribute of the final ApplicationConfig object is "dubbo-demo-provider1-application", and the value of the logger attribute is "log4j".
about

dubbo.protocols.p1.name=dubbo
dubbo.protocols.p1.port=20880
dubbo.protocols.p1.host=0.0.0.0

It corresponds to the ProtocolConfig object (beanName is p1), so the value of the name attribute of the final ProtocolConfig object is "dubbo", the value of the port attribute is 20880, and the value of the host attribute is "0.0.0.0".

This completes the parsing of the properties file.

summary

The main function of dubboconfigurationregistrar is to parse propties files and generate Bean objects of corresponding types according to different configuration items.

DubboComponentScanRegistrar

The dubboconfigurationregistrar is used to register two beans in the Spring container:

  1. ServiceAnnotationBeanPostProcessor
  2. ReferenceAnnotationBeanPostProcessor

Parsing and processing principle of @ Service annotation in Dubbo

ServiceAnnotationBeanPostProcessor

/**
     * 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) {
        // Generate a RootBeanDefinition, and the corresponding bean class is serviceannotationbeanpostprocessor class
        BeanDefinitionBuilder builder = rootBeanDefinition(ServiceAnnotationBeanPostProcessor.class);
        // Use the package path as an incoming parameter when calling the construction method when constructing ServiceAnnotationBeanPostProcessor
        builder.addConstructorArgValue(packagesToScan);
        builder.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
        AbstractBeanDefinition beanDefinition = builder.getBeanDefinition();
        BeanDefinitionReaderUtils.registerWithGeneratedName(beanDefinition, registry);

    }

Serviceannotation beanpostprocessor is a BeanDefinitionRegistryPostProcessor used to register BeanDefinition.

Its main function is to scan Dubbo's @ Service annotation. Once a @ Service annotation is scanned, it and the annotated class are treated as a Dubbo Service for Service export.

 /**
* Registers Beans whose classes was annotated {@link Service}
 *
 * @param packagesToScan The base packages to scan
 * @param registry       {@link BeanDefinitionRegistry}
 */
private void registerServiceBeans(Set<String> packagesToScan, BeanDefinitionRegistry registry) {

    DubboClassPathBeanDefinitionScanner scanner =
            new DubboClassPathBeanDefinitionScanner(registry, environment, resourceLoader);

    BeanNameGenerator beanNameGenerator = resolveBeanNameGenerator(registry);

    scanner.setBeanNameGenerator(beanNameGenerator);

    // Scan classes marked with Service annotations
    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
        // Scan Dubbo's custom @ Service annotation
        scanner.scan(packageToScan);

        // Find the BeanDefinition of the class annotated by @ Service (whether or not the class is annotated by @ ComponentScan annotation)
        // Finds all BeanDefinitionHolders of @Service whether @ComponentScan scans or not.
        Set<BeanDefinitionHolder> beanDefinitionHolders =
                findServiceBeanDefinitionHolders(scanner, packageToScan, registry, beanNameGenerator);

        if (!CollectionUtils.isEmpty(beanDefinitionHolders)) {

            // Scan to BeanDefinition and start processing it
            for (BeanDefinitionHolder beanDefinitionHolder : beanDefinitionHolders) {
                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 + "]");
            }

        }

    }

}

DubboClassPathBeanDefinitionScanner

DubboClassPathBeanDefinitionScanner is a custom scanner of Dubbo, which inherits the ClassPathBeanDefinitionScanner in Spring.

The DubboClassPathBeanDefinitionScanner has not changed much compared with the ClassPathBeanDefinitionScanner, but set useDefaultFilters to false, mainly because the @ Service annotation in Dubbo is customized by Dubbo, The @ Component annotation is not used on this annotation (because Dubbo does not have to be combined with Spring), so in order to make use of Spring's scanning logic, useDefaultFilters needs to be set to false.

If you don't scan a @ Service annotation, you will get a BeanDefinition. The beanClass attribute of the BeanDefinition is the specific Service implementation class.

However, if it's just like this, it just gets a Bean in Spring. For Dubbo, the Bean obtained at this time is a Service, and the configuration information of @ Service annotation needs to be parsed. Because these are the parameter information of the Service, after scanning, it will focus on each obtained BeanDefinition, Will be additionally regenerated into a Bean object of ServiceBean type.

/**
 * Registers {@link ServiceBean} from new annotated {@link Service} {@link BeanDefinition}
 *
 * @param beanDefinitionHolder
 * @param registry
 * @param scanner
 * @see ServiceBean
 * @see BeanDefinition
 */
private void registerServiceBean(BeanDefinitionHolder beanDefinitionHolder, BeanDefinitionRegistry registry,
                                 DubboClassPathBeanDefinitionScanner scanner) {
    // Service implementation class
    Class<?> beanClass = resolveClass(beanDefinitionHolder);
    // @Service annotation
    Annotation service = findServiceAnnotation(beanClass);

    /**
     * The {@link AnnotationAttributes} of @Service annotation
     */
    // @Information on Service annotation
    AnnotationAttributes serviceAnnotationAttributes = getAnnotationAttributes(service, false, false);

    // Interface corresponding to service implementation class
    Class<?> interfaceClass = resolveServiceInterfaceClass(serviceAnnotationAttributes, beanClass);
    // The name of the bean corresponding to the service implementation class, such as demoServiceImpl
    String annotatedServiceBeanName = beanDefinitionHolder.getBeanName();

    // Generate a ServiceBean
    AbstractBeanDefinition serviceBeanDefinition =
            buildServiceBeanDefinition(service, serviceAnnotationAttributes, interfaceClass, annotatedServiceBeanName);

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

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

        // Register the ServiceBean, and the corresponding beanName is ServiceBean: org apache. dubbo. demo. DemoService
        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?");
        }

    }

}

ServiceBean

ServiceBean represents a Dubbo service, which has some parameters, such as:

  1. ref, which represents the concrete implementation class of the service
  2. Interface, which represents the interface of the service
  3. Parameters, indicating the parameters of the Service (@ information configured in the Service annotation)
  4. Application, indicating the application to which the service belongs
  5. protocols, which represents the protocol used by the service
  6. Registers, indicating the registry to which the service is registered

Therefore, after scanning a @ Service annotation, you will actually get two beans:

  • One is that the service implementation class itself is a Bean object
  • One is a Bean object of the corresponding ServiceBean type

It should also be noted that ServiceBean implements the ApplicationListener interface, so when Spring is started, it will trigger the call of onApplicationEvent() method, and export() will be called in this method, which is the entry method of service export.

Parsing and processing principle of @ Reference annotation in Dubbo

ReferenceAnnotationBeanPostProcessor

ReferenceAnnotationBeanPostProcessor processes @ Reference annotations.

The parent class of ReferenceAnnotationBeanPostProcessor is AnnotationInjectedBeanPostProcessor, an instantiawarebeanpostprocessoradapter, and a BeanPostProcessor.

Spring will call the postProcessPropertyValues() method of AnnotationInjectedBeanPostProcessor to inject dependency into a Bean according to the logic of ReferenceAnnotationBeanPostProcessor when injecting dependency into a Bean.

Before injection, the injection point will be found. The attributes or methods annotated by @ Reference are injection points.

After finding all injection points for a Bean, it will be injected. Injection is to assign a value to the attribute or set method, but a value must be obtained before the assignment. At this time, it will call the doGetInjectedBean() method of ReferenceAnnotationBeanPostProcessor to get an object, and the structure of this object is more complex, because for Dubbo, What is injected into a property should be the proxy object of the service interface corresponding to the current property.

@Override
protected Object doGetInjectedBean(AnnotationAttributes attributes, Object bean, String beanName, Class<?> injectedType,
                                   InjectionMetadata.InjectedElement injectedElement) throws Exception {

    /**
     * The name of bean that annotated Dubbo's {@link Service @Service} in local Spring {@link ApplicationContext}
     */
    // Generate referencedBeanName according to the beanName generation rule of ServiceBean. The rule is ServiceBean:interfaceClassName:version:group
    String referencedBeanName = buildReferencedBeanName(attributes, injectedType);

    /**
     * The name of bean that is declared by {@link Reference @Reference} annotation injection
     */
    // @Reference(methods=[Lorg.apache.dubbo.config.annotation.Method;@39b43d60) org.apache.dubbo.demo.DemoService
    // Generate referenceBeanName according to the information of @ Reference annotation
    String referenceBeanName = getReferenceBeanName(attributes, injectedType);

    // Generate a ReferenceBean object
    ReferenceBean referenceBean = buildReferenceBeanIfAbsent(referenceBeanName, attributes, injectedType);

    // Add the referenceBean to the Spring container
    registerReferenceBean(referencedBeanName, referenceBean, attributes, injectedType);

    cacheInjectedReferenceBean(referenceBean, injectedElement);

    // Create a proxy object, which is the proxy object into which the properties in the Service are injected
    // Referencebean. Is called internally get();
    return getOrCreateProxy(referencedBeanName, referenceBeanName, referenceBean, injectedType);
}

However, before generating this proxy object, you should also consider the following issues:

  1. Does the service that needs to be introduced exist locally? If it does not exist, a proxy object should be generated according to Dubbo's logic
  2. Whether the service to be imported has been imported (whether the proxy object has been generated). If so, it should not be generated repeatedly.

First, how to judge whether the currently introduced service is a local service (that is, the service provided by the current application itself).
As mentioned earlier, Dubbo provides a Service through @ Service and generates two beans:

  • A service implementation class itself Bean
  • A Bean of ServiceBean type. The name of the Bean is generated as follows:
private String generateServiceBeanName(AnnotationAttributes serviceAnnotationAttributes, Class<?> interfaceClass) {
	ServiceBeanNameBuilder builder = create(interfaceClass, environment)
                .group(serviceAnnotationAttributes.getString("group"))
                .version(serviceAnnotationAttributes.getString("version"));
	return builder.build();
}

The interface type + group+version is used as the name of the ServiceBean.

Therefore, for service introduction, you should also judge whether there is a corresponding ServiceBean object in the current Spring container according to the information and attribute interface type in the @ Reference annotation in advance. If so, directly take the object corresponding to the ref attribute of the ServiceBean object as the result of injection.

Then, how to judge whether the currently introduced service has been introduced (whether the proxy object has been generated).
This requires caching (recording) after a service is first introduced (after the proxy object is generated). This is what Dubbo does:

  1. First, a string is generated according to all the information + attribute interface types of the @ Reference annotation
  2. Then, a ReferenceBean object is generated from all the information + attribute interface types of the @ Reference annotation (the get method in the ReferenceBean object can get a proxy object generated by Dubbo, which can be understood as the entry method introduced by the service)
  3. Register the string as beanName and the ReferenceBean object as bean in the Spring container and put it into referenceBeanCache.

Summary

With these logic, the introduction process of @ Reference annotation service is as follows:

  1. Get the beanName of the ServiceBean corresponding to the currently introduced service (called referencedBeanName in the source code)
  2. Get a referenceBeanName according to all the information + attribute interface types of the @ Reference annotation
  3. Obtain the corresponding ReferenceBean from the referenceBeanCache according to the referenceBeanName. If not, create a ReferenceBean
  4. Judge whether the bean exists in the Spring container according to the referencedBeanName (beanName of ServiceBean). If so, give an alias to the bean corresponding to the ref attribute, which is referenceBeanName.
    a. If the bean corresponding to referencedBeanName does not exist in the Spring container, judge whether the bean corresponding to referencedBeanName exists in the container. If it does not exist, register the created ReferenceBean in the Spring container (this supports the use of services through @ Autowired annotation. ReferenceBean is a FactoryBean)
  5. If the referencedbean name has a corresponding Bean, an additional proxy object will be generated, and the InvocationHandler of the proxy object will be cached in the localReferenceBeanInvocationHandlerCache. In this way, if the same service is introduced and the service is local,
  6. If the referencedBeanName does not have a corresponding Bean, you can directly call the get() method of ReferenceBean to get a proxy object

Topics: Java