SpringBoot自动装配

清明前夕,我发表了一篇与Spring Cloud有关的文章,原计划在这篇文章之后,就梳理Eureka注册中心的相关知识。然而在跟踪之后,我才发现上来就谈Eureka组件的实现原理是不现实的,因为我根本不清楚SpringBoot是如何集成Eureka组件的。虽然周围人一再强调集成与要梳理的组件没有任何关系,但是我总觉得:不解决这个问题的梳理就像是在半空中建房一样,无处落脚。因此在这篇文章中我想梳理一下SpringBoot自动装配的过程。

1 SpringBoot中的Import注解

记得在梳理《Spring AOP》及《Spring事务》的时候,同事有跟我提过这个注解。当时他明确指出Import注解的解析起点位于ConfigurationClassPostProcessor类的processConfigBeanDefinitions(BeanDefinitionRegistry)方法中,具体如下图所示:

顺着图中红色方框标识的方法继续向下,会进入到ConfigurationClassParser类中,该类中的parser()方法的源码如下所示:

这个方法中,我们主要关注红色方框标出的地方。然后继续顺着这个方法向下看,最后会来到ConfigurationClassParser类的doProcessConfigurationClass(ConfigurationClass, SourceClass)方法中,在这个方法中我们可以看到如下信息,具体如下图所示:

从红色方框可以看出,这个方法是真正调用各注解解析逻辑的地方,这个方法可以处理的注解有:@PropertySource、@ComponentScan、@Import、@ImportResource、@Bean注意:这里我们主要关注@Import注解的解析过程

梳理到这里,先停一下。因为在梳理过程中我发现:如果不明白这个注解的作用,就算弄清楚了它的解析流程,也就是蜻蜓点水,毫无意义。那Spring框架的设计者为什么要提供这样一个注解呢?

在Spring中@Import注解的作用是用来导入额外的配置类或者组件类,以扩展当前上下文中的Bean定义集合。这意味着当我们在一个配置类上使用@Import注解时,Spring容器会在初始化过程中处理被导入的类,并依据类的不同特性执行不同的操作

  1. 导入配置类:如果@Import的参数是一个带有@Configuration注解的类,则Spring容器会像处理其他配置类一样处理这个类,包括扫描并实例化其中通过@Bean注解的方法所定义的Bean
  2. 导入普通类:从Spring 4.2开始,@Import不仅可以导入配置类,还可以导入普通的类。这意味着即使不是配置类,只要通过@Import引入,Spring也会尝试将该类作为Bean进行实例化和管理
  3. 实现ImportSelector接口:当@Import的参数是一个实现了ImportSelector接口的类时,Spring容器会实例化该类,并调用selectImports()方法。此方法返回一个包含类全路径名的字符串数组,Spring容器会按照返回的列表加载并实例化那些类
  4. 实现DeferredImportSelector接口:类似于ImportSelector,但DeferredImportSelector的selectImports()方法调用时机更晚,确保在所有常规的@Configuration类处理完毕后才进行。这对于那些依赖于其他Bean配置完成后才能确定导入哪些类的情况非常有用
  5. 实现ImportBeanDefinitionRegistrar接口:当@Import注解导入一个实现了ImportBeanDefinitionRegistrar接口的类时,Spring容器允许软件开发者直接通过编程方式向BeanDefinitionRegistry注册自定义的Bean定义,这提供了更底层的控制机制,可以在注册Bean时设置更多的属性或执行复杂的逻辑。

总之,@Import注解提供了一种灵活的方式来聚合和整合各个模块或组件的配置,使得Spring容器能够统一管理和初始化应用程序所需的所有Bean。个人理解,@Import注解的主要作用就是向Spring容器中注入Bean(不知这个说法是否准确,若不对,欢迎大家在评论区留言)。了解了@Import注解的作用,下面就来看看其使用案例吧:

1.1 导入普通类

在本小节中我们将使用Import注解向Spring容器中导入一个普通java类,下面就一起看看这种用法的实现过程。首先定义一个普通java类,源码如下:

public class A {
}

接着再定义一个配置类ConfigA,然后在该类中定义一个方法a(),该方法上有一个@Bean注解,源码如下所示:

import org.springframework.context.annotation.Bean;public class ConfigA {@Beanpublic A a() {return new A();}
}

最后再定义一个配置类ConfigB,该类上有两个注解@Configuration和@Import,其中@Import注解中指定ConfigA.class为属性,具体源码如下所示:

@Configuration
@Import(ConfigA.class)
public class ConfigB {
}

最后编写一个测试类,用于验证这种写法能否正常向Spring容器中注入相关对象(ConfigA对象及A对象)。该测试类的源码如下所示:

@SpringBootApplication
public class EurekaServiceApplication {public static void main(String[] args) {ApplicationContext ctx = SpringApplication.run(EurekaServiceApplication.class, args);ConfigA ca = ctx.getBean(ConfigA.class);A a = ctx.getBean(A.class);System.out.println(ca.getClass().getSimpleName());System.out.println(a.getClass().getSimpleName());}}

通过观察控制台输出,我们可以发现ConfigA及A可以正常注入到Spring容器中。

1.2 导入ImportSelector实现类

在本小节中我们将通过在@Import注解中指定ImportSelector接口的实现类的方式向Spring容器中注入一个Bean对象,下面就一起看看这种用法的实现过程。首先定义一个普通java类,源码如下:

public class Tiger {
}

然后再定义一个配置类ZooConfig,然后在该类中定义一个方法tiger (),该方法上有一个@Bean注解,源码如下所示:

import org.springframework.context.annotation.Bean;public class ZooConfig {@Beanpublic Tiger tiger() {return new Tiger();}}

接着再定义一个ZooImportSelector类,该类实现了ImportSelector接口,并实现了该接口中的selectImports()方法,该类的源码如下所示:

public class ZooImportSelector implements ImportSelector {@Overridepublic String[] selectImports(AnnotationMetadata importingClassMetadata) {return new String[]{"org.com.chinasoft.s.ZooConfig"};}}

最后再定义一个配置类ZooConfigB,该类上有两个注解@Configuration和@Import,其中@Import注解中指定ZooImportSelector.class为属性,具体源码如下所示:

@Configuration
@Import(ZooImportSelector.class)
public class ZooConfigB {
}

最后编写一个测试类,用于验证这种写法能否正常向Spring容器中注入相关对象(ZooConfig对象及Tiger对象)。该测试类的源码如下所示:

@SpringBootApplication
public class EurekaServiceApplication {public static void main(String[] args) {ApplicationContext ctx = SpringApplication.run(EurekaServiceApplication.class, args);ZooConfig zooConfig = ctx.getBean(ZooConfig.class);Tiger tiger = ctx.getBean(Tiger.class);System.out.println(zooConfig.getClass().getSimpleName());System.out.println(tiger.getClass().getSimpleName());}}

通过观察控制台输出,我们可以发现ZooConfig及Tiger可以正常注入到Spring容器中。

1.3 导入ImportBeanDefinitionRegistrar实现类

在本小节中我们将通过在@Import注解中指定ImportBeanDefinitionRegistrar接口的实现类的方式向Spring容器中注入一个Bean对象,下面就一起看看这种用法的实现过程。首先定义一个普通java类,源码如下:

public class Dog {
}

然后再定义一个ZooRegistrar类,该类实现了ImportBeanDefinitionRegistrar接口,并实现了该接口中的registerBeanDefinitions ()方法,该类的源码如下所示:

import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.GenericBeanDefinition;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.type.AnnotationMetadata;public class ZooRegistrar implements ImportBeanDefinitionRegistrar {@Overridepublic void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {GenericBeanDefinition gbd = new GenericBeanDefinition();gbd.setBeanClass(Dog.class);registry.registerBeanDefinition("dog", gbd);}}

接着再定义一个配置类ZooConfigBC,该类上有两个注解@Configuration和@Import,其中@Import注解中指定ZooRegistrar.class为属性,具体源码如下所示:

@Configuration
@Import(ZooRegistrar.class)
public class ZooConfigBC {
}

最后编写一个测试类,用于验证这种写法能否正常向Spring容器中注入相关对象(Dog对象)。该测试类的源码如下所示:

@SpringBootApplication
public class EurekaServiceApplication {public static void main(String[] args) {ApplicationContext ctx = SpringApplication.run(EurekaServiceApplication.class, args);Dog dog = ctx.getBean(Dog.class);System.out.println(dog.getClass().getSimpleName());}}

通过观察控制台输出,我们可以发现Dog可以正常注入到Spring容器中。

通过前面的梳理,我们知道@Import注解的作用就是向Spring容器中注入Bean,也了解了通过@Import注解向Spring容器中注入Bean的方法。下面我们将继续梳理Spring解析@Import注解的步骤。

前面梳理到ConfigurationClassParserdoProcessConfigurationClass(ConfigurationClass, SourceClass)方法。在该方法中我们可以看到有这样一段代码,如下所示:

processImports(configClass, sourceClass, getImports(sourceClass), true);

这段代码中的getImports(sourceClass)方法的主要作用就是搜集源类上的@Import注解,这里的源类是我们项目的启动类,即EurekaServiceApplication(这个类上有两个注解,一个是@SpringBootApplication,一个是@EnableDiscoveryClient),具体情况如下所示:

图中的情况与上面的说法一致,下面先看一下getImports(sourceClass)方法,该方法及其相关方法的源码如下所示:

private Set<SourceClass> getImports(SourceClass sourceClass) throws IOException {Set<SourceClass> imports = new LinkedHashSet<SourceClass>();Set<SourceClass> visited = new LinkedHashSet<SourceClass>();collectImports(sourceClass, imports, visited);return imports;
}
private void collectImports(SourceClass sourceClass, Set<SourceClass> imports, Set<SourceClass> visited)throws IOException {if (visited.add(sourceClass)) {for (SourceClass annotation : sourceClass.getAnnotations()) {String annName = annotation.getMetadata().getClassName();if (!annName.startsWith("java") && !annName.equals(Import.class.getName())) {collectImports(annotation, imports, visited);}}imports.addAll(sourceClass.getAnnotationAttributes(Import.class.getName(), "value"));}
}

从CollectImports()方法的源码不难看出:这个方法采用递归调用的方式,逐步找出源类及该类上的注解通过@Import注解导入的类。具体过程如下图所示:

从图中可以看出第一轮循环先处理启动类上的@SpringBootApplication注解,此时annName的值为org.springframework.boot.autoconfigure.SpringBootApplication,接下来用if分支中的判断条件处理后发现if分支中的逻辑可以执行,所以下面会递归调用collectImports()方法,其中sourceClass参数的值为@SpringBootApplication注解,imports参数的值为imports,visited的值为visited集合。此时可以看到下面这样一幅图片:

图中的Evaluate对话框展示的是sourceClass.getAnnotations()操作从SpringBootApplication注解类中拿到的注解信息,其中前四个是java提供的元注解,后三个则是Spring框架提供的注解。因此这轮解析中只有后三个会被处理,其中SpringBootConfiguration注解处理后,imports集合无变更(默认大小为零,处理后依旧为零)、EnableAutoConfiguration注解处理后,imports集合有变化(默认大小为零,处理后变为二)、ComponentScan注解处理后,imports集合无变化(默认大小为二,处理后依旧为二)。这里一起看一下EnableAutoConfiguration注解的详细源码,如下所示:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
/** 注意下面这个注解 */
@Import(EnableAutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";/*** Exclude specific auto-configuration classes such that they will never be applied.* @return the classes to exclude*/Class<?>[] exclude() default {};/*** Exclude specific auto-configuration class names such that they will never be* applied.* @return the class names to exclude* @since 1.3.0*/String[] excludeName() default {};}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
/** 注意下面这个注解 */
@Import(AutoConfigurationPackages.Registrar.class)
public @interface AutoConfigurationPackage {}

从代码可以看出,这里有两个@Import注解,所以前面递归解析后imports集合大小会变成二。由于启动类上还有一个@EnableDiscoveryClient注解,该注解的源码如下所示:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import(EnableDiscoveryClientImportSelector.class)
public @interface EnableDiscoveryClient {/*** If true, the ServiceRegistry will automatically register the local server.*/boolean autoRegister() default true;
}

从源码可以看出这个注解上有个@Import注解,所以最终imports集合的大小为三,最终效果如下图所示:

imports.addAll(sourceClass.getAnnotationAttributes(Import.class.getName(), "value"))这一句代码位于collectImports()方法中。其主要作用就是将定义在sourceClass(比如EurekaServiceApplication启动类)上的@Import注解中的值解析出来并添加到imports集合中。

接下来就可以回到processImports()方法中了,这段方法的主要作用是对import候选类进行处理。在开始梳理前,先来看一下它的源码,如下所示

private void processImports(ConfigurationClass configClass, SourceClass currentSourceClass,Collection<SourceClass> importCandidates, boolean checkForCircularImports) throws IOException {if (importCandidates.isEmpty()) {return;}if (checkForCircularImports && isChainedImportOnStack(configClass)) {this.problemReporter.error(new CircularImportProblem(configClass, this.importStack));}else {this.importStack.push(configClass);try {for (SourceClass candidate : importCandidates) {if (candidate.isAssignable(ImportSelector.class)) {// Candidate class is an ImportSelector -> delegate to it to determine importsClass<?> candidateClass = candidate.loadClass();ImportSelector selector = BeanUtils.instantiateClass(candidateClass, ImportSelector.class);ParserStrategyUtils.invokeAwareMethods(selector, this.environment, this.resourceLoader, this.registry);if (this.deferredImportSelectors != null && selector instanceof DeferredImportSelector) {this.deferredImportSelectors.add(new DeferredImportSelectorHolder(configClass, (DeferredImportSelector) selector));}else {String[] importClassNames = selector.selectImports(currentSourceClass.getMetadata());Collection<SourceClass> importSourceClasses = asSourceClasses(importClassNames);processImports(configClass, currentSourceClass, importSourceClasses, false);}}else if (candidate.isAssignable(ImportBeanDefinitionRegistrar.class)) {// Candidate class is an ImportBeanDefinitionRegistrar ->// delegate to it to register additional bean definitionsClass<?> candidateClass = candidate.loadClass();ImportBeanDefinitionRegistrar registrar =BeanUtils.instantiateClass(candidateClass, ImportBeanDefinitionRegistrar.class);ParserStrategyUtils.invokeAwareMethods(registrar, this.environment, this.resourceLoader, this.registry);configClass.addImportBeanDefinitionRegistrar(registrar, currentSourceClass.getMetadata());}else {// Candidate class not an ImportSelector or ImportBeanDefinitionRegistrar ->// process it as an @Configuration classthis.importStack.registerImport(currentSourceClass.getMetadata(), candidate.getMetadata().getClassName());processConfigurationClass(candidate.asConfigClass(configClass));}}}catch (BeanDefinitionStoreException ex) {throw ex;}catch (Throwable ex) {throw new BeanDefinitionStoreException("Failed to process import candidates for configuration class [" +configClass.getMetadata().getClassName() + "]", ex);}finally {this.importStack.pop();}}
}

为了更清楚的捋顺这个方法的处理逻辑,我们先来看一下这个方法在运行过程中的情况,具体如下图所示:

注意:configClass、currentSourceClass均为启动类,而importCandidates则是前面getImports()执行的结果集。因此图中蓝色条纹标注出来的importCandidates集合有三个值。蓝色条纹所处的代码块的处理逻辑也相对简单:

  • 判断import候选类是否是ImportSelector类型,如果是则加载这个类,然后通过反射的方式创建对象,最后反射调用这个对象上的Aware方法。接着判断这个对象是否是DeferredImportSelector,如果是则将其添加到deferedImportSelectors集合中,如果不是则直接执行这个类上的selectImports()方法,接着将返回结果转化为Collection集合,最后再次调用processImports()方法处理selectImports()方法返回的数据(这正呼应了前面说的实现ImportSelect接口的类,其selectImports()方法返回的数据会被加载到Spring容器中)

未完,请见谅

 

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

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

相关文章

【笔记】mysql版本6以上时区问题

前言 最近在项目中发现数据库某个表的createTime字段的时间比中国时间少了13个小时&#xff0c;只是在数据库中查看显示时间不对&#xff0c;但是在页面&#xff0c;又是正常显示中国时区的时间。 排查 项目中数据库的驱动使用的是8.0.19&#xff0c;驱动类使用的是com.mysq…

Open CASCADE学习|实现Extrude功能

首先定义了一些基本的几何元素&#xff0c;如线、圆和平面&#xff0c;然后使用makeExtrudebydir函数来对一个面进行挤出操作。下面是详细过程&#xff1a; 定义Extrude函数&#xff1a;makeExtrudebydir函数接受一个TopoDS_Shape对象和一个gp_Vec对象作为参数。TopoDS_Shape是…

HashMap与HashSet的不安全问题

HashSet的不安全问题 HashSet与ArrayList一样也存在不安全的问题&#xff0c;当多线程时也会出现ConcurrentModificationException&#xff0c;并发修改异常需要提出解决方案 问题 public static void main(String[] args) {Set<Integer> set new HashSet<>();…

1.MMD模型动作场景镜头的导入及视频导出

界面介绍 MIKUMIKUDANCE926版本 MMD的工具栏模型骨骼帧的窗口&#xff0c;在不同时间做不同动作&#xff0c;可以在这里打帧操作时间曲线操作窗口&#xff0c;控制模型两个动作之间的过渡模型操作窗口&#xff0c;导入模型选择模型相机操作&#xff0c;控制相机远近&#xf…

Vol.44 一个分享网站的网站,每个月8.7万访问量

哈咯&#xff0c;各位朋友好啊&#xff0c;我是欧维&#xff0c;今天要给大家分享的网址是Fuun.fun&#xff0c;奇趣网站收藏家&#xff1b; 它的网址是&#xff1a;FUUN.FUN 这是一个我经常逛的网站&#xff0c;为什么我经常逛呢&#xff1f;因为可以从中发现一些有意思的网站…

上位机图像处理和嵌入式模块部署(智能硬件的开发)

【 声明&#xff1a;版权所有&#xff0c;欢迎转载&#xff0c;请勿用于商业用途。 联系信箱&#xff1a;feixiaoxing 163.com】 目前&#xff0c;用上位机软件虽然可以部署项目&#xff0c;但是它本身有自己的缺点&#xff0c;那就是稳定性差、价格贵。稳定性这部分&#xff0…

Linux 快问快答

如果对于找 Java 后端开发的话&#xff0c;我感觉会这几个差不多了&#xff0c;面试官应该不会问的这么详细吧。一般就问问 Linux 的几个常用的命令&#xff0c;然后做一些简单的性能排查就好了。如果面试被问到另外的问题&#xff0c;那我再补充进来&#xff0c;现在先掌握这么…

CMC学习系列 (8):动态力输出期间的伽马范围皮质相干性

CMC学习系列:动态力输出期间的伽马范围皮质相干性 0. 引言1. 主要贡献2. 方法3. 结果4. 讨论5. 总结欢迎来稿 论文地址&#xff1a;https://www.sciencedirect.com/science/article/abs/pii/S1053811906010238 论文题目&#xff1a;Gamma-range corticomuscular coherence duri…

OSPF动态路由实验(思科)

华为设备参考&#xff1a; 一&#xff0c;技术简介 OSPF&#xff08;Open Shortest Path First&#xff09;是一种内部网关协议&#xff0c;主要用于在单一自治系统内决策路由。它是一种基于链路状态的路由协议&#xff0c;通过链路状态路由算法来实现动态路由选择。 OSPF的…

D. Nene and the Mex Operator

解题思路 若选定一个区间&#xff0c;则可以构造成值全为构造方如下&#xff1a;先将区间全变为0&#xff08;若区间有0且不全为0两次&#xff08;全变为一个值后再全变为0&#xff09;&#xff0c;若没有0则一次&#xff0c;若已经全为0则0次&#xff09;保留r为0&#xff0c…

Vol.45 这个壁纸网址,功能简单,每月37.7万访问量

哈咯&#xff0c;大家好&#xff0c;我是欧维&#xff0c;今天要给大家分享的网站是&#xff1a;极简壁纸&#xff0c;一个专门做电脑壁纸的网站&#xff1b; 它的网址是&#xff1a;极简壁纸_海量电脑桌面壁纸美图_4K超高清_最潮壁纸网站 网站的壁纸质量很高&#xff0c;页面…

Open CASCADE学习|BRepOffsetAPI_DraftAngle

BRepOffsetAPI_DraftAngle 是 Open CASCADE Technology (OCCT) 中用于创建带有草图斜面的几何体的类。草图斜面是一种在零件设计中常见的特征&#xff0c;它可以在零件的表面上创建一个倾斜的面&#xff0c;通常用于便于零件的脱模或是增加零件的强度。 本例演示了如何创建一个…

【系统分析师】数据库部分

文章目录 1、数据库模式2、数据库设计过程2.1ER模型 3、关系代数 ☆5、规范化理论☆5.1 非规范存在的问题5.2 相关概念5.3范式5.3.1 第一范式-1NF5.3.2 第二范式-2NF5.2.3 第三范式5.2.4 BC范式 5.4 函数依赖分解5.4.1保持函数依赖分解5.4.2 无损分解 5.5 Armstong公理系统 6、…

策略为王股票软件源代码\StkUI\View\RealTime.cpp------分时行情界面------程序代码基本都在里面

搜索 成交... C:\Users\Administrator\Desktop\源代码\策略为王股票软件源代码\StkUI\StkUI.rc(395): MENUITEM "以成交均价为准(&A)", ID_VIEW_MAINDATAAVERAGE C:\Users\Administrator\Desktop\源代码\策略为王股票软件源代码\StkUI\StkUI…

shardingsphere从4.1.1升级到5.2.1

作为工程师都希望自己的产品能够大卖&#xff0c;而项目开发中使用的技术不可能所有都是自己写的&#xff0c;使用到的开源组件例如shardingsphere这样好的组件&#xff0c;也会随着用户量的增加而需要升级&#xff0c;没必要非得像hutool那样非得自己写一边&#xff0c;当然hu…

MySQL基础入门上篇

MySQL基础 介绍 mysql -uroot -p -h127.0.0.1 -P3306项目设计 具备数据库一定的设计能力和操作数据的能力。 数据库设计DDL 定义 操作 显示所有数据库 show databases;创建数据库 create database db02;数据库名唯一&#xff0c;不能重复。 查询是否创建成功 加入一些…

JVM修炼之路【12】- GC调优 、性能调优

上一篇中 我们详细讲了内存溢出 内存泄漏 还有相关的案例。 这篇博客中我们主要了解一下GC调优。 有些新手可能会有一点 疑问—— 这两者不是一回事吗&#xff1f;&#xff1f; 其实说一回事 也没错 因为GC调优本质上还是针对 堆上的内存 只不过前面我们关注的侧重点在于 不合…

软件设计师——软件工程基础知识

软件工程基础知识 软件过程软件过程模型软件测试方法进度管理软件复杂性度量环路复杂度耦合聚合和组合 软件过程 软件过程模型 软件测试方法 黑盒测试和白盒测试 白盒测试中&#xff0c;语句覆盖对程序执行逻辑的覆盖很低&#xff0c;因此一般认为它是很弱的逻辑覆盖。 进度管…

【Django开发】0到1美多商城项目md教程第7篇:登录,1. 互联开发者申请步骤【附代码文档】

美多商城完整教程&#xff08;附代码资料&#xff09;主要内容讲述&#xff1a;欢迎来到美多商城&#xff01;&#xff0c;项目准备。展示用户注册页面&#xff0c;创建用户模块子应用。用户注册业务实现&#xff0c;用户注册前端逻辑。图形验证码&#xff0c;图形验证码接口设…

HarmonyOS实战开发-自定义分享

介绍 自定义分享主要是发送方将文本&#xff0c;链接&#xff0c;图片三种类型分享给三方应用,同时能够在三方应用中展示。本示例使用数据请求 实现网络资源的获取&#xff0c;使用屏幕截屏 实现屏幕的截取&#xff0c;使用文件管理 实现对文件&#xff0c;文件目录的管理&…