IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> Java知识库 -> dubbo与spring整合源码 -> 正文阅读

[Java知识库]dubbo与spring整合源码

@EnableDubbo 注解

spring整合dubbo开启dubbo服务的入口注解

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

@EnableDubboConfig

在这里插入图片描述

该注解主要加载dubbo的配置,并将配置分好类 应用级别、服务级别…

TIPS<Spring知识>: @Import 注解引入的类
1:**ImportBeanDefinitionRegistrar.class **spring会回调 registerBeanDefinitions()方法并将BeanDefinitionRegistry传入可以往spring中添加BeanDefinition。添加了BeanDefinition就会被实例化到IOC
2:ImportSelector.class spring会回调selectImports()方法并将返回的类名数组加载成BeanDefinition
3:**Configuration.class **会引入一个配置类并解析配置类
_4:普通的一个类会被加载成_BeanDefinition,最终添加到IOC

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

DubboConfigConfigurationRegistrar

该Registrar 就是往spring里面注入了两个Configuration对象
DubboConfigConfiguration.Single.class
DubboConfigConfiguration.Multiple.class

public class DubboConfigConfiguration {

    /**
     * Single Dubbo {@link AbstractConfig Config} Bean Binding
     */
    @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 {

    }

    /**
     * Multiple Dubbo {@link AbstractConfig Config} Bean Binding
     */
    @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 {

    }
}

从代码看其实两个类也是引入了一对配置。
DubboConfigConfiguration.Single.class 为了解析并绑定 dubbo.xxx
DubboConfigConfiguration.Multiple.class 为了解析并绑定 dubbo.xxxs

@EnableDubboConfigBindings

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

DubboConfigBindingsRegistrar.class

读取EnableDubboConfigBindings 注解上的value,并解析
例如:@EnableDubboConfigBinding(prefix = “dubbo.application”, type = ApplicationConfig.class)
会读取 注解里面的 prefix 和 type
1、将type生成BeanDefinition。
2、根据prefix从spring的environment(spring已经提前将dubbo的配置文件加载成kv结构了) 获取对应的配置值。并赋值给对应type。完成了配置解析

public class DubboConfigBindingsRegistrar implements ImportBeanDefinitionRegistrar, EnvironmentAware {

    private ConfigurableEnvironment environment;

    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        System.out.println("执行DubboConfigBindingsRegistrar");
        AnnotationAttributes attributes = AnnotationAttributes.fromMap(
                importingClassMetadata.getAnnotationAttributes(EnableDubboConfigBindings.class.getName()));

        // 拿到多个@EnableDubboConfigBinding注解
        AnnotationAttributes[] annotationAttributes = attributes.getAnnotationArray("value");

        DubboConfigBindingRegistrar registrar = new DubboConfigBindingRegistrar();
        registrar.setEnvironment(environment);

        for (AnnotationAttributes element : annotationAttributes) {

            // 逐个解析@EnableDubboConfigBinding注解,比如@EnableDubboConfigBinding(prefix = "dubbo.application", type = ApplicationConfig.class)
            registrar.registerBeanDefinitions(element, registry);

        }
    }

    @Override
    public void setEnvironment(Environment environment) {

        Assert.isInstanceOf(ConfigurableEnvironment.class, environment);

        this.environment = (ConfigurableEnvironment) environment;

    }

}
   protected void registerBeanDefinitions(AnnotationAttributes attributes, BeanDefinitionRegistry registry) {

        // prefix = "dubbo.application"
        String prefix = environment.resolvePlaceholders(attributes.getString("prefix"));

        // type = ApplicationConfig.class
        Class<? extends AbstractConfig> configClass = attributes.getClass("type");

        boolean multiple = attributes.getBoolean("multiple");

        registerDubboConfigBeans(prefix, configClass, multiple, registry);

    }
   private void registerDubboConfigBeans(String prefix,
                                          Class<? extends AbstractConfig> configClass,
                                          boolean multiple,
                                          BeanDefinitionRegistry registry) {

        // 从properties文件中根据前缀拿对应的配置项,比如根据dubbo.application前缀,
        // 就可以拿到:
        // dubbo.application.name=dubbo-demo-provider-application
        // dubbo.application.logger=log4j
        Map<String, Object> properties = getSubProperties(environment.getPropertySources(), prefix);

        // 如果没有相关的配置项,则不需要注册BeanDefinition
        if (CollectionUtils.isEmpty(properties)) {
            if (log.isDebugEnabled()) {
                log.debug("There is no property for binding to dubbo config class [" + configClass.getName()
                        + "] within prefix [" + prefix + "]");
            }
            return;
        }

        // 根据配置项生成beanNames,为什么会有多个?
        // 普通情况一个dubbo.application前缀对应一个ApplicationConfig类型的Bean
        // 特殊情况下,比如dubbo.protocols对应了:
//        dubbo.protocols.p1.name=dubbo
//        dubbo.protocols.p1.port=20880
//        dubbo.protocols.p1.host=0.0.0.0

//        dubbo.protocols.p2.name=http
//        dubbo.protocols.p2.port=8082
//        dubbo.protocols.p2.host=0.0.0.0
        // 那么就需要对应两个ProtocolConfig类型的Bean,那么就需要两个beanName:p1和p2

        // 这里就是multiple为true或false的区别,名字的区别,根据multiple用来判断是否从配置项中获取beanName
        // 如果multiple为false,则看有没有配置id属性,如果没有配置则自动生成一个beanName.
        Set<String> beanNames = multiple ? resolveMultipleBeanNames(properties) :
                Collections.singleton(resolveSingleBeanName(properties, configClass, registry));

        for (String beanName : beanNames) {

            // 为每个beanName,注册一个空的BeanDefinition
            registerDubboConfigBean(beanName, configClass, registry);

            // 为每个bean注册一个DubboConfigBindingBeanPostProcessor的Bean后置处理器
            registerDubboConfigBindingBeanPostProcessor(prefix, beanName, multiple, registry);

        }

        // 注册一个NamePropertyDefaultValueDubboConfigBeanCustomizer的bean
        // 用来把某个XxConfig所对应的beanName设置到name属性中去
        registerDubboConfigBeanCustomizers(registry);

    }

配置类的属性赋值。利用springbean的后置处理器

 @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {

        // 每个XxConfig对应一个BeanPostProcessor,所以每个DubboConfigBindingBeanPostProcessor只处理对应的beanName

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

            AbstractConfig dubboConfig = (AbstractConfig) bean;
            // 从properties文件中获取值,并设置到dubboConfig对象中
            bind(prefix, dubboConfig);

            // 设置dubboConfig对象的name属性,设置为beanName
            customize(beanName, dubboConfig);

        }

        return bean;

    }

总结:

至此就已经将dubbo的配置解析成了bean对象。
并根据配置文件为这些bean填充了属性和beanName
ApplicationConfig.class,
ModuleConfig.class,
RegistryConfig.class,
ProtocolConfig.class,
MonitorConfig.class,
ProviderConfig.class,
ConsumerConfig.class,
ConfigCenterBean.class,
MetadataReportConfig.class,
MetricsConfig…class

@DubboComponentScan

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

DubboComponentScanRegistrar

该注解是dubbo的核心。功能就是扫描Dubbo服务类,进行服务的导出和服务的引入

@Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        System.out.println("执行DubboComponentScanRegistrar");

        // 拿到DubboComponentScan注解所定义的包路径,扫描该package下的类,识别这些类上
        Set<String> packagesToScan = getPackagesToScan(importingClassMetadata);

        // 注册ServiceAnnotationBeanPostProcessor一个Bean
        // 实现了BeanDefinitionRegistryPostProcessor接口,所以在Spring启动时会调用postProcessBeanDefinitionRegistry方法
        // 该方法会进行扫描,扫描@Service注解了的类,然后生成BeanDefinition(会生成两个,一个普通的bean,一个ServiceBean),后续的Spring周期中会生成Bean
        // 在ServiceBean中会监听ContextRefreshedEvent事件,一旦Spring启动完后,就会进行服务导出
        registerServiceAnnotationBeanPostProcessor(packagesToScan, registry);

        // 注册ReferenceAnnotationBeanPostProcessor
        // 实现了AnnotationInjectedBeanPostProcessor接口,继而实现了InstantiationAwareBeanPostProcessorAdapter接口
        // 所以Spring在启动时,在对属性进行注入时会调用AnnotationInjectedBeanPostProcessor接口中的postProcessPropertyValues方法
        // 在这个过程中会按照@Reference注解的信息去生成一个RefrenceBean对象
        registerReferenceAnnotationBeanPostProcessor(registry);

    }

服务导出

在这里插入图片描述

向Spring添加一个ServiceAnnotationBeanPostProcessor.class
这个名字起的有点误解应该交xxxBeanDefinitionRegistryPostProcessor 会更好点
public class ServiceAnnotationBeanPostProcessor implements BeanDefinitionRegistryPostProcessor, EnvironmentAware,
ResourceLoaderAware, BeanClassLoaderAware {

    /**
     * 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#postProcessBeanDefinitionRegistry() spring生命周期方法

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

        Set<String> resolvedPackagesToScan = resolvePackagesToScan(packagesToScan);

        if (!CollectionUtils.isEmpty(resolvedPackagesToScan)) {
            // 扫描包,进行Bean注册
            registerServiceBeans(resolvedPackagesToScan, registry);
        } else {
            if (logger.isWarnEnabled()) {
                logger.warn("packagesToScan is empty , ServiceBean registry will be ignored!");
            }
        }

    }



  /**
     * 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 + "]");
                }

            }

        }

    }

从上面可以看到基本就是 spring扫描bean的类似方式。扫描指定包下面的@Service 标注的类,加载成beanDefinition。所以核心方法是扫描到@Service 注解后的beanDefinition 通过registerServiceBean方法注册Dubbo服务所代表的的beanDefinition


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

        }

    }


private AbstractBeanDefinition buildServiceBeanDefinition(Annotation serviceAnnotation,
                                                              AnnotationAttributes serviceAnnotationAttributes,
                                                              Class<?> interfaceClass,
                                                              String annotatedServiceBeanName) {
        // 生成一个ServiceBean对应的BeanDefinition
        BeanDefinitionBuilder builder = rootBeanDefinition(ServiceBean.class);
            ......
        // References "ref" property to annotated-@Service Bean
        // ref属性赋值为另外一个bean, 对应的就是被@Service注解的服务实现类对应的bean
        addPropertyReference(builder, "ref", annotatedServiceBeanName);
        .......
        return builder.getBeanDefinition();

    }

这里精简了一下buildServiceBeanDefinition()方法 主要做了一些事情
1、手动构造了一个ServiceBean.class的 BeanDefinition
2、并且将ref属性赋值为真正的服务对象BeanName,所以当Spring实例化Bean时。可以根据ref的beanName找到对应的服务实现bean注入。例如demoService。
所以ServiceBean 标识Duboo服务,其中通过ref来指向真正的服务处理bean:demoService

疑问: 方法已经结束了,并没有看到导出服务的地方啊?

这个答案其实就是隐藏在Spring的bean生命周期中。
ServiceBean.class 继承了ApplicationListener.class 。spring在初始化的时候回回调onApplicationEvent()

public class ServiceBean<T> extends ServiceConfig<T> implements InitializingBean, DisposableBean,
        ApplicationContextAware, ApplicationListener<ContextRefreshedEvent>, BeanNameAware,
        ApplicationEventPublisherAware
    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
        // 当前服务没有被导出并且没有卸载,才导出服务
        if (!isExported() && !isUnexported()) {
            if (logger.isInfoEnabled()) {
                logger.info("The service ready on spring started. service: " + getInterface());
            }
            // 服务导出(服务注册)
            export();
        }
    }

后面的导出逻辑比较繁杂。单独一篇文章解释

服务引入

类似 服务的导入。向spring中添加了ReferenceAnnotationBeanPostProcessor.class

    /**
     * Registers {@link ReferenceAnnotationBeanPostProcessor} into {@link BeanFactory}
     *
     * @param registry {@link BeanDefinitionRegistry}
     */
    private void registerReferenceAnnotationBeanPostProcessor(BeanDefinitionRegistry registry) {

        // Register @Reference Annotation Bean Processor
        // 注册一个ReferenceAnnotationBeanPostProcessor做为bean,ReferenceAnnotationBeanPostProcessor是一个BeanPostProcessor
        BeanRegistrar.registerInfrastructureBean(registry,
                ReferenceAnnotationBeanPostProcessor.BEAN_NAME, ReferenceAnnotationBeanPostProcessor.class);

    }
// 该方法得到的对象会赋值给@ReferenceBean注解的属性
    //
    @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);
    }

生成了一个ReferenceBean.class bean。最后通过referenceBean.get() 方法来返回业务代码需要注入的duboo服务
服务引入的逻辑更加复杂。也需要单独的一篇文章解释

  Java知识库 最新文章
计算距离春节还有多长时间
系统开发系列 之WebService(spring框架+ma
springBoot+Cache(自定义有效时间配置)
SpringBoot整合mybatis实现增删改查、分页查
spring教程
SpringBoot+Vue实现美食交流网站的设计与实
虚拟机内存结构以及虚拟机中销毁和新建对象
SpringMVC---原理
小李同学: Java如何按多个字段分组
打印票据--java
上一篇文章      下一篇文章      查看所有文章
加:2022-05-25 11:32:41  更:2022-05-25 11:33:27 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/23 20:02:44-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码