Spring与Dubbo整合原理与源码分析

Spring与Dubbo整合原理与源码分析

整体架构和流程

Spring与Dubbo整合原理与源码分析

  • 主配置启动类
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配置文件
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

应用配置类为ProviderConfiguration, 在配置上有两个比较重要的注解

  1. @PropertySource表示将dubbo-provider.properties中的配置项添加到Spring容器中,可以通过@Value的方式获取到配置项中的值
  2. @EnableDubbo(scanBasePackages = “org.apache.dubbo.demo.provider”)表示对指定包下的类进行扫描,扫描@Service与@Reference注解,并且进行处理

@EnableDubbo

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

    // @EnableDubboConfig注解用来将properties文件中的配置项转化为对应的Bean
    // @DubboComponentScan注解用来扫描服务提供者和引用者(@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;

}

在EnableDubbo注解上,有另外两个注解,也是研究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 {};

}

注意两个注解中对应的@Import注解所导入的类:

  1. DubboConfigConfigurationRegistrar
  2. DubboComponentScanRegistrar

Spring在启动时会解析这两个注解,并且执行对应的Registrar类中的registerBeanDefinitions方法(这是Spring中提供的扩展功能。)

Dubbo中propertie文件解析以及处理原理

Spring与Dubbo整合原理与源码分析

DubboConfigConfigurationRegistrar

Spring启动时,会调用DubboConfigConfigurationRegistrar的registerBeanDefinitions方法,该方法是利用Spring中的AnnotatedBeanDefinitionReader来读取:
DubboConfigConfiguration.Single.class
DubboConfigConfiguration.Multiple.class
这两个类上的注解。
Spring与Dubbo整合原理与源码分析
Spring与Dubbo整合原理与源码分析

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

}

这两个类主要用到的就是@EnableDubboConfigBindings注解:

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

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

}

@EnableDubboConfigBindings注解上也有一个@Import注解,导入的是DubboConfigBindingsRegistrar.class。该类会获取@EnableDubboConfigBindings注解中的value,也就是多个@EnableDubboConfigBinding注解,然后利用DubboConfigBindingRegistrar去处理这些@EnableDubboConfigBinding注解。

DubboConfigBindingRegistrar

此类中的主要方法是registerDubboConfigBeans()方法,主要功能就是获取用户所设置的properties文件中的内容,对Properties文件进行解析,根据Properties文件的每个配置项的前缀、参数名、参数值生成对应的BeanDefinition。

  • 举个例子
dubbo.application.name=dubbo-demo-provider1-application
dubbo.application.logger=log4j

前缀为"dubbo.application"的配置项,会生成一个ApplicationConfig类型的BeanDefinition,并且name和logger属性为对应的值。

  • 例子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

比如前缀为"dubbo.protocols"的配置项,会生成两个ProtocolConfig类型的BeanDefinition,两个BeanDefinition的beanName分别为p1和p2

并且还会针对生成的每个BeanDefinition生成一个和它一对一绑定的BeanPostProcessor,类型为DubboConfigBindingBeanPostProcessor.class。
Spring与Dubbo整合原理与源码分析
Spring与Dubbo整合原理与源码分析
Spring与Dubbo整合原理与源码分析

DubboConfigBindingBeanPostProcessor

DubboConfigBindingBeanPostProcessor是一个BeanPostProcessor,在Spring启动过程中,会针对所有的Bean对象进行后置加工,但是在DubboConfigBindingBeanPostProcessor中有如下判断

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

所以DubboConfigBindingBeanPostProcessor并不会处理Spring容器中的所有Bean,它只会处理上文由Dubbo所生成的Bean对象

并且,在afterPropertiesSet()方法中,会先创建一个DefaultDubboConfigBinder。

DefaultDubboConfigBinder

当某个AbstractConfig类型的Bean,在经过DubboConfigBindingBeanPostProcessor处理时,此时Bean对象中的属性是没有值的,会利用DefaultDubboConfigBinder进行赋值。底层就是利用Spring中的DataBinder技术,结合properties文件对对应的属性进行赋值

对应一个AbstractConfig类型(针对的其实是子类,比如ApplicationConfig、RegistryConfig)的Bean,每个类都有一些属性,而properties文件是一个key-value对,所以实际上DataBinder就是将属性名和properties文件中的key进行匹配,如果匹配成功,则把value赋值给属性。具体DataBinder技术是如何工作的,请自行学习(不难)。

举个例子:

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

对于此配置,它对应ApplicationConfig对象(beanName是自动生成的),所以最终ApplicationConfig对象的name属性的值为“dubbo-demo-provider1-application”,logger属性的值为“log4j”。
对于

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

它对应ProtocolConfig对象(beanName为p1),所以最终ProtocolConfig对象的name属性的值为“dubbo”,port属性的值为20880,host属性的值为“0.0.0.0”。

这样就完成了对properties文件的解析。

总结

DubboConfigConfigurationRegistrar的主要作用就是对propties文件进行解析并根据不同的配置项项生成对应类型的Bean对象。

DubboComponentScanRegistrar

DubboConfigConfigurationRegistrar的作用是向Spring容器中注册两个Bean:

  1. ServiceAnnotationBeanPostProcessor
  2. ReferenceAnnotationBeanPostProcessor

Spring与Dubbo整合原理与源码分析

Dubbo中@Service注解解析以及处理原理

Spring与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) {
        // 生成一个RootBeanDefinition,对应的beanClass为ServiceAnnotationBeanPostProcessor.class
        BeanDefinitionBuilder builder = rootBeanDefinition(ServiceAnnotationBeanPostProcessor.class);
        // 将包路径作为在构造ServiceAnnotationBeanPostProcessor时调用构造方法时的传入参数
        builder.addConstructorArgValue(packagesToScan);
        builder.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
        AbstractBeanDefinition beanDefinition = builder.getBeanDefinition();
        BeanDefinitionReaderUtils.registerWithGeneratedName(beanDefinition, registry);

    }

ServiceAnnotationBeanPostProcessor是一个BeanDefinitionRegistryPostProcessor,是用来注册BeanDefinition的。

它的主要作用是扫描Dubbo的@Service注解一旦扫描到某个@Service注解就把它以及被它注解的类当做一个Dubbo服务,进行服务导出
Spring与Dubbo整合原理与源码分析

 /**
* 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);

    // 扫描被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
        // 扫描Dubbo自定义的@Service注解
        scanner.scan(packageToScan);

        // 查找被@Service注解的类的BeanDefinition(无论这个类有没有被@ComponentScan注解标注了)
        // Finds all BeanDefinitionHolders of @Service whether @ComponentScan scans or not.
        Set<BeanDefinitionHolder> beanDefinitionHolders =
                findServiceBeanDefinitionHolders(scanner, packageToScan, registry, beanNameGenerator);

        if (!CollectionUtils.isEmpty(beanDefinitionHolders)) {

            // 扫描到BeanDefinition开始处理它
            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是Dubbo自定义的扫描器,继承了Spring中的ClassPathBeanDefinitionScanner了。

DubboClassPathBeanDefinitionScanner相对于ClassPathBeanDefinitionScanner并没有做太多的改变,只是把useDefaultFilters设置为了false,主要是因为Dubbo中的@Service注解是Dubbo自定义的,在这个注解上并没有用@Component注解(因为Dubbo不是一定要结合Spring才能用),所以为了能利用Spring的扫描逻辑,需要把useDefaultFilters设置为false。

没扫描到一个@Service注解,就会得到一个BeanDefinition,这个BeanDefinition的beanClass属性就是具体的服务实现类。

但,如果仅仅只是这样,这只是得到了一个Spring中的Bean,对于Dubbo来说此时得到的Bean是一个服务,并且,还需要解析@Service注解的配置信息,因为这些都是服务的参数信息,所以在扫描完了之后,会针对所得到的每个BeanDefinition,都会额外的再生成一个ServiceBean类型的Bean对象。

/**
 * 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) {
    // 服务实现类
    Class<?> beanClass = resolveClass(beanDefinitionHolder);
    // @Service注解
    Annotation service = findServiceAnnotation(beanClass);

    /**
     * The {@link AnnotationAttributes} of @Service annotation
     */
    // @Service注解上的信息
    AnnotationAttributes serviceAnnotationAttributes = getAnnotationAttributes(service, false, false);

    // 服务实现类对应的接口
    Class<?> interfaceClass = resolveServiceInterfaceClass(serviceAnnotationAttributes, beanClass);
    // 服务实现类对应的bean的名字,比如:demoServiceImpl
    String annotatedServiceBeanName = beanDefinitionHolder.getBeanName();

    // 生成一个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

        // 把ServiceBean注册进去,对应的beanName为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表示一个Dubbo服务,它有一些参数,比如:

  1. ref,表示服务的具体实现类
  2. interface,表示服务的接口
  3. parameters,表示服务的参数(@Service注解中所配置的信息)
  4. application,表示服务所属的应用
  5. protocols,表示服务所使用的协议
  6. registries,表示服务所要注册的注册中心

所以在扫描到一个@Service注解后,其实会得到两个Bean:

  • 一个就是服务实现类本身一个Bean对象
  • 一个就是对应的ServiceBean类型的一个Bean对象

并且需要注意的是,ServiceBean实现了ApplicationListener接口,所以当Spring启动完成后会触发onApplicationEvent()方法的调用,而在这个方法内会调用export(),这个方法就是服务导出的入口方法

Dubbo中@Reference注解解析以及处理原理

Spring与Dubbo整合原理与源码分析

ReferenceAnnotationBeanPostProcessor

ReferenceAnnotationBeanPostProcessor是处理@Reference注解的。

ReferenceAnnotationBeanPostProcessor的父类是AnnotationInjectedBeanPostProcessor,是一个InstantiationAwareBeanPostProcessorAdapter,是一个BeanPostProcessor。

Spring在对Bean进行依赖注入时会调用AnnotationInjectedBeanPostProcessor的postProcessPropertyValues()方法来给某个Bean按照ReferenceAnnotationBeanPostProcessor的逻辑进行依赖注入。

在注入之前会查找注入点,被@Reference注解的属性或方法都是注入点。

针对某个Bean找到所有注入点之后,就会进行注入了,注入就是给属性或给set方法赋值,但是在赋值之前得先得到一个值,此时就会调用ReferenceAnnotationBeanPostProcessor的doGetInjectedBean()方法来得到一个对象,而这个对象的构造就比较复杂了,因为对于Dubbo来说,注入给某个属性的应该是当前这个属性所对应的服务接口的代理对象

@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}
     */
    // 按ServiceBean的beanName生成规则来生成referencedBeanName, 规则为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
    // 根据@Reference注解的信息生成referenceBeanName
    String referenceBeanName = getReferenceBeanName(attributes, injectedType);

    // 生成一个ReferenceBean对象
    ReferenceBean referenceBean = buildReferenceBeanIfAbsent(referenceBeanName, attributes, injectedType);

    // 把referenceBean添加到Spring容器中去
    registerReferenceBean(referencedBeanName, referenceBean, attributes, injectedType);

    cacheInjectedReferenceBean(referenceBean, injectedElement);

    // 创建一个代理对象,Service中的属性被注入的就是这个代理对象
    // 内部会调用referenceBean.get();
    return getOrCreateProxy(referencedBeanName, referenceBeanName, referenceBean, injectedType);
}

但是在生成这个代理对象之前,还要考虑问题:

  1. 当前所需要引入的这个服务,是不是在本地就存在?不存在则要把按Dubbo的逻辑生成一个代理对象
  2. 当前所需要引入的这个服务,是不是已经被引入过了(是不是已经生成过代理对象了),如果是应该是不用再重复去生成了。

首先如何判断当前所引入的服务是本地的一个服务(就是当前应用自己所提供的服务)。
我们前面提到,Dubbo通过@Service来提供一个服务,并且会生成两个Bean:

  • 一个服务实现类本身Bean
  • 一个ServiceBean类型的Bean,这个Bean的名字是这么生成的:
private String generateServiceBeanName(AnnotationAttributes serviceAnnotationAttributes, Class<?> interfaceClass) {
	ServiceBeanNameBuilder builder = create(interfaceClass, environment)
                .group(serviceAnnotationAttributes.getString("group"))
                .version(serviceAnnotationAttributes.getString("version"));
	return builder.build();
}

是通过接口类型+group+version来作为ServiceBean类型Bean的名字的。

所以现在对于服务引入,也应该提前根据@Reference注解中的信息和属性接口类型去判断一下当前Spring容器中是否存在对应的ServiceBean对象,如果存在则直接取出ServiceBean对象的ref属性所对应的对象,作为要注入的结果。

然后如何判断当前所引入的这个服务是否已经被引入过了(是不是已经生成过代理对象了)。
这就需要在第一次引入某个服务后(生成代理对象后)进行缓存(记录一下)。Dubbo中是这么做的:

  1. 首先根据@Reference注解的所有信息+属性接口类型生成一个字符串
  2. 然后@Reference注解的所有信息+属性接口类型生成一个ReferenceBean对象(ReferenceBean对象中的get方法可以得到一个Dubbo生成的代理对象,可以理解为服务引入的入口方法
  3. 把字符串作为beanName,ReferenceBean对象作为bean注册到Spring容器中,同时也会放入referenceBeanCache中。

小结

有了这些逻辑,@Reference注解服务引入的过程是这样的:

  1. 得到当前所引入服务对应的ServiceBean的beanName(源码中叫referencedBeanName)
  2. 根据@Reference注解的所有信息+属性接口类型得到一个referenceBeanName
  3. 根据referenceBeanName从referenceBeanCache获取对应的ReferenceBean,如果没有则创建一个ReferenceBean
  4. 根据referencedBeanName(ServiceBean的beanName)判断Spring容器中是否存在该bean,如果存在则给ref属性所对应的bean取一个别名,别名为referenceBeanName。
    a. 如果Spring容器中不存在referencedBeanName对应的bean,则判断容器中是否存在referenceBeanName所对应的Bean,如果不存在则将创建出来的ReferenceBean注册到Spring容器中(此处这么做就支持了可以通过@Autowired注解也可以使用服务了,ReferenceBean是一个FactoryBean
  5. 如果referencedBeanName存在对应的Bean,则额外生成一个代理对象,代理对象的InvocationHandler会缓存在localReferenceBeanInvocationHandlerCache中,这样如果引入的是同一个服务,并且这个服务在本地
  6. 如果referencedBeanName不存在对应的Bean,则直接调用ReferenceBean的get()方法得到一个代理对象
上一篇:4面蚂蚁金服Java岗,已拿offer,分享攻下面试的学习资料


下一篇:面试官:要不我们聊一下“心跳”的设计?