0201自动配置类的导入-自动装配原理-springboot2.7.x系列

news2024/11/15 7:21:01

1简介

Spring Boot是基于Spring框架的,它的原理也是基于Spring框架的。

Spring框架是一个非常强大的框架,它提供了一系列的模块来帮助开发人员构建企业级的应用程序。Spring框架的核心是控制反转(Inversion of Control,IoC)和面向切面编程(Aspect Oriented Programming,AOP)。

Spring Boot是在Spring框架的基础上,提供了自动配置、快速开发和更好的性能等功能,主要原理包括:

  1. 自动配置:Spring Boot基于约定优于配置的原则,提供了大量的自动配置,避免了繁琐的XML配置。
  2. 内嵌式容器:Spring Boot提供了内嵌式的Tomcat、Jetty、Undertow等容器,简化了应用程序的部署和启动。
  3. Starter依赖:Spring Boot提供了一系列的Starter依赖,开发人员可以只添加必要的Starter依赖,而不必担心版本冲突和依赖问题。
  4. 自动装配:Spring Boot的自动装配机制可以根据classpath中的依赖自动配置应用程序。
  5. Actuator:Spring Boot提供了Actuator模块,用于监控应用程序的健康状态、性能指标等。
  6. 外部化配置:Spring Boot可以将配置文件外部化,方便在不同的环境中部署应用程序。

总之,Spring Boot的原理是基于Spring框架的,它提供了一系列的功能来简化应用程序的开发、部署和运行,从而提高开发效率和应用程序的性能。

我们学习springboot步骤:

  • 学习springboot的主要底层原理
    • 自动装配
    • 条件注解Conditional
    • starter机制
  • 手写实现一个简单springboot框架

下面优先讲解下springboot的核心功能:自动装配。

2 springboot 启动

2.1 引入依赖

springboot在项目中依赖引入方式:

  • 方式一:

    <parent>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-parent</artifactId>
      <version>2.7.2</version>
    </parent>
    <dependencies>
      <dependy>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-xxx</artifactId>
      </dependy>
    </dependencies>
    
  • 方式二

     <properties>
            <spring-boot.version>2.7.10</spring-boot.version>
    </properties>
     <dependencyManagement>
              <!-- spring boot 依赖 -->
              <dependency>
                  <groupId>org.springframework.boot</groupId>
                  <artifactId>spring-boot-dependencies</artifactId>
                  <version>${spring-boot.version}</version>
                  <type>pom</type>
                  <scope>import</scope>
              </dependency>
    </dependencyManagement>
    <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-xxx</artifactId>
            </dependency>
    </dependencies>
              
    

2.2 @SpringBootApplication

引入依赖之后,在启动类上加上@SpringBootApplication注解,它怎么就可以完成那么多jar包的Bean自动配置呢?

我们来看下@SpringBootApplication注解源码:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
		@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
  // 省略...
}

@Target(ElementType.TYPE),@Retention(RetentionPolicy.RUNTIME),@Documented,@Inherited这几个元注解我们不在详述,下面我们重点看下其他3个注解。

2.3 @EnableAutoConfiguration

@SpringBootConfiguration是Spring Boot中的一个注解,它是@Configuration注解的特化版本,用于标识一个类是Spring Boot应用程序的配置类。

在Spring Boot中,配置类通常用于定义应用程序中的各种组件和配置信息,例如数据源、Web服务器、消息队列等。通过@Configuration注解和相关注解,可以将这些组件注入到Spring容器中,从而让它们在应用程序中起作用。

与@Configuration注解不同的是,@SpringBootConfiguration注解还会触发Spring Boot的自动配置机制。它会扫描应用程序中的类路径和类库,找到符合条件的组件并进行自动配置。因此,使用@SpringBootConfiguration注解可以简化应用程序的配置和部署,提高开发效率和可维护性。

2.4 @SpringBootConfiguration

@ComponentScan是Spring Framework中的一个注解,它用于指定要扫描的组件包。在Spring Boot中,@ComponentScan注解通常用于扫描应用程序中的所有组件,并将它们注入到Spring容器中。

通过@ComponentScan注解,可以指定要扫描的包和类,也可以通过excludeFilters和includeFilters属性来排除或包含特定的组件。例如,可以使用excludeFilters属性来排除某些组件,例如某些自动配置类或不需要注入到容器中的类。同时,也可以使用includeFilters属性来只包含特定的组件,例如只包含某些接口的实现类。

在Spring Boot中,@ComponentScan注解通常被应用于启动类或配置类中,用于扫描应用程序中的所有组件。如果没有指定@ComponentScan注解,则默认会扫描启动类所在的包及其子包中的所有组件。

2.5 @EnableAutoConfiguration

@EnableAutoConfiguration是Spring Boot中的一个注解,它用于开启自动配置机制。通过@EnableAutoConfiguration注解,可以让Spring Boot自动根据应用程序中的依赖和配置来完成自动配置,从而简化应用程序的开发和部署

看下@EnableAutoConfigutaiton注解源码:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
	// 省略...
}

2.6 @Import

@Import是Spring Framework中的一个注解,它用于将一个或多个类导入到当前配置类中。通过@Import注解,可以将其他配置类、普通类、甚至是第三方库中的类导入到当前配置类中,从而让它们在Spring容器中起作用。

在Spring Boot中,@Import注解通常用于导入一些自定义的配置类或第三方库中的类。例如,可以使用@Import注解导入一个自定义的配置类,从而将其中定义的Bean注入到Spring容器中。也可以使用@Import注解导入一个第三方库中的类,从而在应用程序中使用该类提供的功能。

自动装配的核心功能,通过AutoConfigurationImportSelector完成。

3 自动配置类的导入流程

下面我们就从启动类的run()方法,通过debugger来追踪下自动装配的流程,我们测试是开源项目pig的gateway模块即网关模块。

第一步:在ApplicationContext#refresh()方法invokeBeanFactoryPostProcessors处设置断点,该方法用于向容器中注册BeanFactory。代码3-1如下所示:

@Override
	public void refresh() throws BeansException, IllegalStateException {
		synchronized (this.startupShutdownMonitor) {
			// 省略。。。
			try {
				// Allows post-processing of the bean factory in context subclasses.
				postProcessBeanFactory(beanFactory);

				StartupStep beanPostProcess = this.applicationStartup.start("spring.context.beans.post-process");
				// Invoke factory processors registered as beans in the context.
				invokeBeanFactoryPostProcessors(beanFactory);
			  // 省略。。。
	}

断点截图3-1如下所示:

在这里插入图片描述

第二步:程序继续执行,忽略中间步骤,我们去看关于AutoConfigurationImportSelector开启自动配置类的导包的选择器中相关代码执行,即执行getAutoConfigurationEntry()方法,代码如下:

protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
   if (!isEnabled(annotationMetadata)) {
      return EMPTY_ENTRY;
   }
   AnnotationAttributes attributes = getAttributes(annotationMetadata);
  // 获取候选配置类
   List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
  // 移除重复类
   configurations = removeDuplicates(configurations);
  // 根据过滤器过滤不符合条件的类
   Set<String> exclusions = getExclusions(annotationMetadata, attributes);
   checkExcludedClasses(configurations, exclusions);
   configurations.removeAll(exclusions);
   configurations = getConfigurationClassFilter().filter(configurations);
   fireAutoConfigurationImportEvents(configurations, exclusions);
   return new AutoConfigurationEntry(configurations, exclusions);
}

继续追踪下getCandidateConfigurations()获取候选配置类方法,代码如下:

protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
  // 加载jar包下META-INFO/spring.factories下的配置类,兼容springboot2.7.0以前版本
   List<String> configurations = new ArrayList<>(
         SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader()));
  // 加载jar包下META-INFO/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports中的类,为springboot2.7.0之后自动配置类存放路径
   ImportCandidates.load(AutoConfiguration.class, getBeanClassLoader()).forEach(configurations::add);
   // 省略空校验
   return configurations;
}
  • getSpringFactoriesLoaderFactoryClass()返回:EnableAutoConfiguration.class

追踪下loadFactoryNames()加载jar包下META-INFO/spring.factories下的配置类方法,代码如下:

public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
  ClassLoader classLoaderToUse = classLoader;
  if (classLoaderToUse == null) {
    classLoaderToUse = SpringFactoriesLoader.class.getClassLoader();
  }
  String factoryTypeName = factoryType.getName();
  return loadSpringFactories(classLoaderToUse).getOrDefault(factoryTypeName, Collections.emptyList());
}

private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) {
  // 缓存获取给定加载器key对应的值
  Map<String, List<String>> result = cache.get(classLoader);
  if (result != null) {
    return result;
  }

  result = new HashMap<>();
  try {
   // 加载META-INFO/spring.factories中的配置
    Enumeration<URL> urls = classLoader.getResources(FACTORIES_RESOURCE_LOCATION);
    while (urls.hasMoreElements()) {
      URL url = urls.nextElement();
      UrlResource resource = new UrlResource(url);
      Properties properties = PropertiesLoaderUtils.loadProperties(resource);
      for (Map.Entry<?, ?> entry : properties.entrySet()) {
        String factoryTypeName = ((String) entry.getKey()).trim();
        String[] factoryImplementationNames =
            StringUtils.commaDelimitedListToStringArray((String) entry.getValue());
        for (String factoryImplementationName : factoryImplementationNames) {
          result.computeIfAbsent(factoryTypeName, key -> new ArrayList<>())
              .add(factoryImplementationName.trim());
        }
      }
    }

    // 去重且map值list设置为不可修改
    result.replaceAll((factoryType, implementations) -> implementations.stream().distinct()
        .collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList)));
    cache.put(classLoader, result);
  }
  // 省略异常处理
  return result;
}
  • FACTORIES_RESOURCE_LOCATION:META-INF/spring.factories
  • 加载jar包下META-INF/spring.factories中配置
  • 其中EnableAutoConfiguration实现类,有的jar包放在spring.factories中,有的单独迁移放置在其他文件中,下面会将。
  • result为HashMap结构,key存放=前的key,值为ArrayList结构,里面存放实现类。
  • loadSpringFactories(classLoaderToUse).getOrDefault(factoryTypeName, Collections.emptyList());获取key为EnableAutoConfiguration(完整包路径)对应的配置类。

我当前springboot版本2.7.10,我们看下spring-boot-autoconfigure-2.7.10.jar包下META-INF/spring.factories中内容

# Initializers
org.springframework.context.ApplicationContextInitializer=\
org.springframework.boot.autoconfigure.SharedMetadataReaderFactoryContextInitializer,\
org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener

# Application Listeners
org.springframework.context.ApplicationListener=\
org.springframework.boot.autoconfigure.BackgroundPreinitializer

# Environment Post Processors
# 省略

# Auto Configuration Import Listeners
# 省略

# Auto Configuration Import Filters
org.springframework.boot.autoconfigure.AutoConfigurationImportFilter=\
org.springframework.boot.autoconfigure.condition.OnBeanCondition,\
org.springframework.boot.autoconfigure.condition.OnClassCondition,\
org.springframework.boot.autoconfigure.condition.OnWebApplicationCondition

# Failure analyzers
# 省略

# Template availability providers
# 省略

# DataSource initializer detectors
# 省略

# Depends on database initialization detectors
# 省略
  • properties格式配置文件,key为父类或者父接口,值为,分隔的实现类

下面我们看下ImportCandidates.load(AutoConfiguration.class, getBeanClassLoader()).forEach(configurations::add);执行

  • ImportCandidates.load(AutoConfiguration.class, getBeanClassLoader())加载META-INFO/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports中配置类
  • forEach(configurations::add),把这些配置类和之前加载的配置类合并。

ImportCandidates#load()方法:

public static ImportCandidates load(Class<?> annotation, ClassLoader classLoader) {
   Assert.notNull(annotation, "'annotation' must not be null");
   ClassLoader classLoaderToUse = decideClassloader(classLoader);
   // location:META-INF/spring/%s.imports
   String location = String.format(LOCATION, annotation.getName());
   Enumeration<URL> urls = findUrlsInClasspath(classLoaderToUse, location);
   List<String> importCandidates = new ArrayList<>();
   while (urls.hasMoreElements()) {
      URL url = urls.nextElement();
      importCandidates.addAll(readCandidateConfigurations(url));
   }
   return new ImportCandidates(importCandidates);
}

展示部分org/springframework/boot/spring-boot-autoconfigure/2.7.10/spring-boot-autoconfigure-2.7.10.jar!/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports中的配置类:

org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration
org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration
org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration
org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration
org.springframework.boot.autoconfigure.context.ConfigurationPropertiesAutoConfiguration
org.springframework.boot.autoconfigure.context.LifecycleAutoConfiguration
org.springframework.boot.autoconfigure.context.MessageSourceAutoConfiguration
org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration
# 省略

到此springboot导入自动配置类流程部分介绍完成,剩下过滤部分,我们会放在下一篇@Conditionalxxx中讲解。

4 小结

  • loadSpringFactories()该方法就是从“META-INF/spring.factories”中加载给定类型的工厂实现的完全限定类名放到map中

  • loadFactoryNames()是根据SpringBoot的启动生命流程,当需要加载自动配置类时,就会传入org.springframework.boot.autoconfigure.EnableAutoConfiguration参数,从map中查找key为org.springframework.boot.autoconfigure.EnableAutoConfiguration的值,这些值通过反射加到容器中,之后的作用就是用它们来做自动配置,这就是Springboot自动配置开始的地方

  • load():方法从“META-INFO/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports”中加载AutoConfiguration实现的完全限定类名与放入自动配置类map中

  • 只有这些自动配置类进入到容器中以后,接下来这个自动配置类才开始进行启动

  • 当需要其他的配置时如监听相关配置:listenter,就传不同的参数,获取相关的listenter配置

关于

结语

如果小伙伴什么问题或者指教,欢迎交流。

❓QQ:806797785

⭐️源代码仓库地址:https://gitee.com/gaogzhen/springboot-custom

参考:

[1]Springboot视频教程[CP/OL].P13.

[2]一文搞懂🔥SpringBoot自动配置原理[CP/OL].P13.

[3]ChatGPT

[4]spring boot 自动装配的实现原理和骚操作,不同版本实现细节,debug 到裂开…[CP/OL].P13.

[5]SpringBoot 自动装配原理[CP/OL].P13.

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/467169.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

大数据技术之Spark Streaming概述

前言 数据处理延迟的长短 实时数据处理&#xff1a;毫秒级别离线数据处理&#xff1a;小时 or 天 数据处理的方式 流式&#xff08;streaming&#xff09;数据处理批量&#xff08;batch&#xff09;数据处理 spark Streaming也是基于sparkCore&#xff0c;所以底层的核心没有变…

FreeRTOS系统学习第一步:新建 FreeRTOS 工程—软件仿真

创建一个FreeRTOS系统工程 1.新建工程文件夹2.Keil新建工程2.1 New Project2.2 Select Device For Target2.3 Manage Run-Time Environment 3. 在 KEIL 工程里面新建文件组3.1在 KEIL 工程里面添加文件 4. 编写 main 函数5. 调试配置5.1 设置软件仿真5.2 修改时钟大小在时钟相关…

你是否线上有使用ThreadLocal,如果结合多线程,请慎用

随着业务的增加&#xff0c;数据量的增加&#xff0c;多线程的使用会越来越频繁&#xff0c;提升单机的处理能力。 前些日子我们线上出现了一个比较严重的故障&#xff0c;这个故障是多线程使用不当引起的&#xff0c;挺有代表性的&#xff0c;所以分享给大家&#xff0c;希望…

前端学习笔记:CSS的引入,元素选择器

这是本人学习的总结&#xff0c;主要学习资料如下 马士兵教育 目录 1、引入CSS1.1、引入CSS的方式1.2、优先级 2、元素选择器2.1、基本选择器2.1.1、选择器2.1.2、优先级 2.2、关系选择器2.2.1、优先级 2.3、属性选择器2.4、伪类选择器 1、引入CSS 1.1、引入CSS的方式 第一个…

互斥锁深度理解与使用

大家好&#xff0c;我是易安! 我们知道一个或者多个操作在CPU执行的过程中不被中断的特性&#xff0c;称为“原子性”。理解这个特性有助于你分析并发编程Bug出现的原因&#xff0c;例如利用它可以分析出long型变量在32位机器上读写可能出现的诡异Bug&#xff0c;明明已经把变量…

SpringCloud--gateway 网关

在Spring Cloud中&#xff0c;使用Gateway网关访问服务可以有多种好处&#xff0c;包括但不限于以下几点&#xff1a; 统一入口管理&#xff1a;Gateway作为统一的服务入口&#xff0c;可以对所有的请求进行统一管理和控制&#xff0c;实现微服务集中管理。 动态路由&#xff…

056:cesium 七种方法设置颜色

第056个 点击查看专栏目录 本示例的目的是介绍如何在vue+cesium中设置颜色,这里用到了7种方法,查看API,还有很多种方法 直接复制下面的 vue+cesium源代码,操作2分钟即可运行实现效果. 文章目录 示例效果配置方式示例源代码(共115行)相关API参考:专栏目标示例效果 配置…

深入理解Go语言中的接口编程【17】

文章目录 接口接口接口类型为什么要使用接口接口的定义实现接口的条件接口类型变量值接收者和指针接收者实现接口的区别值接收者实现接口指针接收者实现接口下面的代码是一个比较好的面试题 类型与接口的关系一个类型实现多个接口多个类型实现同一接口接口嵌套 空接口空接口的定…

TCP教程:详解TCP连接过程

目录标题 一 、简述二 、TCP建立连接协议&#xff08;三次握手&#xff09;2.1 概述及目的2.2 第一次握手&#xff1a;客户端发送SYN报文2.3 第二次握手&#xff1a;服务器回应SYN-ACK报文2.4 第三次握手&#xff1a;客户端回应ACK报文2.5 顾客预定座位场景2.6底层原理2.7 TCP …

嵌入式之Samba服务器搭建

在嵌入式系统开发应用平台中&#xff0c;tftp、nfs和samba服务器是最常用的文件传输工具 tftp和nfs是在嵌入式Linux开发环境中经常使用的传输工具 samba则是Linux和Windows之间的文件传输工具。 下面演示在linux上搭建Samba服务器 sudo apt-get install samba chmod -R 77…

会场安排问题——算法实现(C实现)

问题描述&#xff1a;加入要在足够多的会场里安排一批活动&#xff0c;并希望使用尽可能少的会场。设计一个有效的贪心算法进行安排。&#xff08;这个问题实际上是著名的图着色问题。若每个活动作为图的一个顶点&#xff0c;不相容活动之间用边相连。使相连顶点着有不同颜色的…

数据库原理容易出错的点

一个数据库只存在一个内模式和一个模式&#xff0c;可以存在多个外模式除了删除表或视图的使用的是DELETE以外其他数据库对象均是使用DROP遵守两段封锁的协议的并发事务一定是可串行化的哪些情况下不适合创建索引&#xff1a; 对于查询过程中很少使用或参考的列对于那些只有很少…

【无人车】用于无人地面车辆的路径跟踪算法(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

CMIP6数据处理:WRF模式动力降尺度、单点降尺度、统计方法区域降尺度、SWAT数据、Biome-BGC数据制备

查看原文>>>WRF模式、CMIP6数据处理、机器学习气象海洋水文应用、python地球科学 目录 CMIP6月数据、日数据、全球VIPPHEN物候数据、ERA5数据 一、CMIP6中的模式比较计划 二、数据下载 三、基础知识 四、单点降尺度 五、统计方法的区域降尺度 六、基于WRF模式…

助力低碳出行 | 基于ACM32 MCU的电动滑板车方案

前言 随着智能科技的快速发展&#xff0c;电动滑板车的驱动系统也得到了长足的发展。国内外的电动滑板车用电机驱动系统分为传统刷式电机和无刷电机两种类型。其中&#xff0c;传统的刷式电机已经逐渐被无刷电机所取代&#xff0c;无刷电机的性能和寿命都更出色&#xff0c;已…

STM32F4 HAL库使用DMA进行ADC采样实时发送波形到串口显示(包含傅里叶变换)

1.总体逻辑 按下STM32F4的KEY0按键&#xff0c;通过外部中断的方式对按键进行检测&#xff0c;然后开启一次带DMA的固定点数的ADC采集&#xff0c;采集完成后在DMA的中断发送采集到的数据&#xff0c;然后清空数据区准备下一次的按键中断。电脑接受到串口数据后对数据进行简单…

大厂齐出海:字节忙种草,网易爱社交

配图来自Canva可画 随着国内移动互联网红利逐渐触顶&#xff0c;互联网市场日趋饱和&#xff0c;国内各互联网企业之间的竞争便愈发激烈起来。在此背景下&#xff0c;广阔的海外市场就成为了腾讯、阿里、字节、京东、拼多多、百度、网易、快手、B站等互联网公司关注和争夺的重…

算法--前缀和技巧 (蓝桥杯123-灵能传输--求和)

文章目录 什么是前缀和用途什么时候用java的前缀和例题[蓝桥杯 2022 省 A] 求和题目描述思路代码 [蓝桥杯 2021 国 ABC] 123题目描述思路代码 [蓝桥杯 2019 省 B] 灵能传输(蓝桥杯96%&#xff0c;洛谷ac)题目描述思路代码 什么是前缀和 如果一个数组a的元素为 a 1 , a 2 , a 3…

Springboot +Flowable,详细解释啥叫流程实例(一)

一.简介 上一篇中学习了Flowable 中的流程模板&#xff08;流程定义&#xff09;的部署问题&#xff0c;这一篇来学习什么叫流程实例。 部署之后的流程模板&#xff0c;还不能直接运行&#xff0c;例如我们部署了一个请假流程&#xff0c;现在 张三想要请假&#xff0c;他就需…

WhatsApp多开攻略,低成本高效率多开账号聊单的方法献上~

WhatsApp多开攻略&#xff01;低成本高效率多开账号聊单的方法献上~ WhatsApp多开是指在同一台设备上同时登录多个WhatsApp账号&#xff0c;这种技术通常被跨境电商从业者用于在不同的WhatsApp账号之间切换&#xff0c;以便更好地管理跨境电商业务。 图中工具&#xff1a; ss客…