Spring 容器原始 Bean 是如何创建的?

以下内容基于 Spring6.0.4。

这个话题其实非常庞大,我本来想从 getBean 方法讲起,但一想这样讲完估计很多小伙伴就懵了,所以我们还是一步一步来,今天我主要是想和小伙伴们讲讲 Spring 容器创建 Bean 最最核心的 createBeanInstance 方法,这个方法专门用来创建一个原始 Bean 实例。

松哥这里就以 Spring 源码中方法的执行顺序为例来和小伙伴们分享。

1. doCreateBean

AbstractAutowireCapableBeanFactory#doCreateBean 就是 Bean 的创建方法,但是 Bean 的创建涉及到的步骤非常多,包括各种需要调用的前置后置处理器方法,今天我主要是想和大家聊聊单纯的创建 Bean 的过程,其他方法咱们后面文章继续。

在 doCreateBean 方法中,有如下一行方法调用:

protected Object doCreateBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args)throws BeanCreationException {// Instantiate the bean.BeanWrapper instanceWrapper = null;if (mbd.isSingleton()) {instanceWrapper = this.factoryBeanInstanceCache.remove(beanName);}if (instanceWrapper == null) {instanceWrapper = createBeanInstance(beanName, mbd, args);}Object bean = instanceWrapper.getWrappedInstance();//...return exposedObject;
}

createBeanInstance 这个方法就是真正的根据我们的配置去创建一个 Bean 了。

2. createBeanInstance

先来看源码:

protected BeanWrapper createBeanInstance(String beanName, RootBeanDefinition mbd, @Nullable Object[] args) {// Make sure bean class is actually resolved at this point.Class<?> beanClass = resolveBeanClass(mbd, beanName);if (beanClass != null && !Modifier.isPublic(beanClass.getModifiers()) && !mbd.isNonPublicAccessAllowed()) {throw new BeanCreationException(mbd.getResourceDescription(), beanName,"Bean class isn't public, and non-public access not allowed: " + beanClass.getName());}Supplier<?> instanceSupplier = mbd.getInstanceSupplier();if (instanceSupplier != null) {return obtainFromSupplier(instanceSupplier, beanName);}if (mbd.getFactoryMethodName() != null) {return instantiateUsingFactoryMethod(beanName, mbd, args);}// Shortcut when re-creating the same bean...boolean resolved = false;boolean autowireNecessary = false;if (args == null) {synchronized (mbd.constructorArgumentLock) {if (mbd.resolvedConstructorOrFactoryMethod != null) {resolved = true;autowireNecessary = mbd.constructorArgumentsResolved;}}}if (resolved) {if (autowireNecessary) {return autowireConstructor(beanName, mbd, null, null);}else {return instantiateBean(beanName, mbd);}}// Candidate constructors for autowiring?Constructor<?>[] ctors = determineConstructorsFromBeanPostProcessors(beanClass, beanName);if (ctors != null || mbd.getResolvedAutowireMode() == AUTOWIRE_CONSTRUCTOR ||mbd.hasConstructorArgumentValues() || !ObjectUtils.isEmpty(args)) {return autowireConstructor(beanName, mbd, ctors, args);}// Preferred constructors for default construction?ctors = mbd.getPreferredConstructors();if (ctors != null) {return autowireConstructor(beanName, mbd, ctors, null);}// No special handling: simply use no-arg constructor.return instantiateBean(beanName, mbd);
}

这里就是核心的 Bean 的创建方法了,因此这个方法我来和大家详细分析一下。

2.1 resolveBeanClass

这个方法是用来解析出来当前的 beanClass 对象,它的核心逻辑就是根据我们在 XML 文件中配置的类的全路径,通过反射加载出来这个 Class

@Nullable
protected Class<?> resolveBeanClass(RootBeanDefinition mbd, String beanName, Class<?>... typesToMatch)throws CannotLoadBeanClassException {if (mbd.hasBeanClass()) {return mbd.getBeanClass();}return doResolveBeanClass(mbd, typesToMatch);
}

首先会调用 mbd.hasBeanClass() 方法去判断是否已经通过反射加载出来 beanClass 了,如果加载出来了就直接返回,没有加载的话,就继续执行下面的 doResolveBeanClass 去加载。

什么时候会走 if 这条线呢?松哥举一个例子,如果我们设置某一个 Bean 的 Scope 是 prototype 的话,那么当第二次获取该 Bean 的实例的时候,就会走 if 这条线。

@Nullable
private Class<?> doResolveBeanClass(RootBeanDefinition mbd, Class<?>... typesToMatch)throws ClassNotFoundException {//...String className = mbd.getBeanClassName();if (className != null) {Object evaluated = evaluateBeanDefinitionString(className, mbd);if (!className.equals(evaluated)) {// A dynamically resolved expression, supported as of 4.2...if (evaluated instanceof Class<?> clazz) {return clazz;}else if (evaluated instanceof String str) {className = str;freshResolve = true;}else {throw new IllegalStateException("Invalid class name expression result: " + evaluated);}}if (freshResolve) {// When resolving against a temporary class loader, exit early in order// to avoid storing the resolved Class in the bean definition.if (dynamicLoader != null) {return dynamicLoader.loadClass(className);}return ClassUtils.forName(className, dynamicLoader);}}// Resolve regularly, caching the result in the BeanDefinition...return mbd.resolveBeanClass(beanClassLoader);
}

按理说,根据我们配置的类的全路径加载出来一个 Class 应该是非常容易的,直接 Class.forName 就可以了。

但是!!!

如果对 Spring 用法比较熟悉的小伙伴就知道,配置 Class 全路径的时候,我们不仅可以像下面这样老老实实配置:

<bean class="org.javaboy.bean.Book"/>

我们甚至可以使用 SpEL 来配置 Bean 名称,例如我有如下类:

public class BeanNameUtils {public String getName() {return "org.javaboy.bean.User";}
}

这里有一个 getName 方法,这个方法返回的是一个类的全路径,现在我们在 XML 文件中可以这样配置:

<bean class="org.javaboy.bean.BeanNameUtils" id="beanNameUtils"/>
<bean class="#{beanNameUtils.name}" id="user"/>

在 XML 的 class 属性中,我们可以直接使用 SpEL 去引用一个方法的执行,用该方法的返回值作为 class 的值。

了解了 Spring 中的这个玩法,再去看上面的源码就很好懂了:

  • 首先调用 mbd.getBeanClassName(); 去获取到类路径。
  • 接下来调用 evaluateBeanDefinitionString 方法进行 SpEL 运算,这个运算的目的是为了解析 className 中的 SpEL 表达式,当然,一般情况下 className 就是一个普通的字符串,不是 SpEL 表达式,那么解析完成之后就还是原本的字符串。如果是 className 是一个 SpEL,那么合法的解析结果分为两种:
    • 首先就是解析之后拿到了一个 Class,那这个就是我们想要的结果,直接返回即可。
    • 要么就是解析出来是一个字符串,松哥上面举的例子就是这种情况,那么就把这个字符串赋值给 className,并且将 freshResolve 属性设置为 true,然后在接下来的 if 分支中去加载 Class。

当然,上面这些都是处理特殊情况,一般我们配置的普通 Bean,都是直接走最后一句 mbd.resolveBeanClass(beanClassLoader),这个方法的逻辑其实很好懂,我把代码贴出来小伙伴们来瞅一瞅:

@Nullable
public Class<?> resolveBeanClass(@Nullable ClassLoader classLoader) throws ClassNotFoundException {String className = getBeanClassName();if (className == null) {return null;}Class<?> resolvedClass = ClassUtils.forName(className, classLoader);this.beanClass = resolvedClass;return resolvedClass;
}

这个方法就相当直白了,根据 className 加载出来 Class 对象,然后给 beanClass 属性也设置上值,这就和一开始的 if (mbd.hasBeanClass()) 对应上了。

好了,到此,我们总算是根据 className 拿到 Class 对象了。

2.2 Supplier 和 factory-method

好了,回到一开始的源码中,接下来该执行如下两行代码了:

Supplier<?> instanceSupplier = mbd.getInstanceSupplier();
if (instanceSupplier != null) {return obtainFromSupplier(instanceSupplier, beanName);
}
if (mbd.getFactoryMethodName() != null) {return instantiateUsingFactoryMethod(beanName, mbd, args);
}

这两个松哥在前面的文章中和小伙伴们已经讲过了(Spring5 中更优雅的第三方 Bean 注入):前面的 obtainFromSupplier 方法是 Spring5 开始推出来的 Supplier,通过回调的方式去获取一个对象;第二个方法 instantiateUsingFactoryMethod 则是通过配置的 factory-method 来获取到一个 Bean 实例。

对这两个方法不熟悉的小伙伴可以参考前面的文章:Spring5 中更优雅的第三方 Bean 注入。

2.3 re-create 逻辑

继续回到一开始的源码中,接下来是一段 re-create 的处理逻辑,如下:

boolean resolved = false;
boolean autowireNecessary = false;
if (args == null) {synchronized (mbd.constructorArgumentLock) {if (mbd.resolvedConstructorOrFactoryMethod != null) {resolved = true;autowireNecessary = mbd.constructorArgumentsResolved;}}
}
if (resolved) {if (autowireNecessary) {return autowireConstructor(beanName, mbd, null, null);}else {return instantiateBean(beanName, mbd);}
}

根据前面的介绍,我们现在已经获取到 Class 对象了,接下来直接调用相应的构造方法就可以获取到 Bean 实例了。但是这个 Class 对象可能存在多个构造方法,所以还需要一堆流程去确定到底调用哪个构造方法。

所以这里会先去判断 resolvedConstructorOrFactoryMethod 是否不为空,不为空的话,说明这个 Bean 之前已经创建过了,该用什么方法创建等等问题都已经确定了,所以这次就不用重新再去确定了(resolved = true)。另一方面,autowireNecessary 表示构造方法的参数是否已经处理好了,这个属性为 true 则表示构造方法的参数已经处理好了,那么就可以调用 autowireConstructor 方法去创建一个 Bean 出来,否则调用 instantiateBean 方法初始化 Bean。

这里涉及到的 autowireConstructor 和 instantiateBean 方法我们先不细说了,因为在后面还会再次涉及到。

2.4 构造器注入

继续回到一开始的源码中,接下来就是针对各种处理器的预处理了:

Constructor<?>[] ctors = determineConstructorsFromBeanPostProcessors(beanClass, beanName);
if (ctors != null || mbd.getResolvedAutowireMode() == AUTOWIRE_CONSTRUCTOR ||mbd.hasConstructorArgumentValues() || !ObjectUtils.isEmpty(args)) {return autowireConstructor(beanName, mbd, ctors, args);
}

先来看 determineConstructorsFromBeanPostProcessors 方法,这个方法主要是考虑到你可能提供了 SmartInstantiationAwareBeanPostProcessor,松哥在前面的文章中和大家专门讲过 BeanPostProcessor(BeanFactoryPostProcessor 和 BeanPostProcessor 有什么区别?),这里的 SmartInstantiationAwareBeanPostProcessor 算是 BeanPostProcessor 的一种,也是 Bean 的一种增强器。SmartInstantiationAwareBeanPostProcessor 中有一个 determineCandidateConstructors 方法,这个方法返回某一个 Bean 的构造方法,将来可以通过这个构造方法初始化某一个 Bean。

我给大家举一个简单例子,假设我有如下类:

public class User {private String username;private String address;public User() {System.out.println("=====no args=====");}public User(ObjectProvider<String> username) {System.out.println("args==username");this.username = username.getIfAvailable();}//省略 getter/setter/toString
}

现在我在 Spring 容器中注册这个对象:

<bean class="org.javaboy.bean.User" id="user">
</bean>

按照我们已有的知识,这个将来会调用 User 的无参构造方法去完成 User 对象的初始化。

但是现在,假设我添加如下一个处理器:

public class MySmartInstantiationAwareBeanPostProcessor implements SmartInstantiationAwareBeanPostProcessor {@Overridepublic Constructor<?>[] determineCandidateConstructors(Class<?> beanClass, String beanName) throws BeansException {if ("user".equals(beanName)) {Constructor<?> constructor = null;try {constructor = beanClass.getConstructor(ObjectProvider.class);} catch (NoSuchMethodException e) {throw new RuntimeException(e);}return new Constructor[]{constructor};}return SmartInstantiationAwareBeanPostProcessor.super.determineCandidateConstructors(beanClass, beanName);}
}

在 determineCandidateConstructors 方法中,返回一个有参构造方法,那么将来 Spring 容器会通过这里返回的有参构造方法去创建 User 对象,而不是通过无参构造方法去创建 User 对象。

最后,将这个处理器注册到 Spring 容器:

<bean class="org.javaboy.bean.MySmartInstantiationAwareBeanPostProcessor"/>

现在,当我们启动 Spring 容器的时候,User 就是通过有参构造方法初始化的,而不是无参构造方法。之所以会这样,就是因为本小节一开始提到的源码 determineConstructorsFromBeanPostProcessors,这个方法就是去查看有无 SmartInstantiationAwareBeanPostProcessor,如果有,就调用对应的方法找到处理器并返回。

这个弄懂之后,if 中其他几种情况就好理解了,mbd.getResolvedAutowireMode() 是查看当前对象的注入方式,这个一般是在 XML 中配置的,不过日常开发中我们一般不会配置这个属性,如果需要配置,方式如下:

<bean class="org.javaboy.bean.User" id="user" autowire="constructor">
</bean>

如果添加了 autowire="constructor" 就表示要通过构造方法进行注入,那么这里也会进入到 if 中。

if 里边剩下的几个条件都好说,就是看是否有配置构造方法参数,如果配置了,那么也直接调用相应的构造方法就行了。

这里最终执行的是 autowireConstructor 方法,这个方法比较长,我就不贴出来了,和大家说一说它的思路:

  1. 首先把能获取到的构造方法都拿出来,如果构造方法只有一个,且目前也没有任何和构造方法有关的参数,那就直接用这个构造方法就行了。
  2. 如果第一步不能解决问题,接下来就遍历所有的构造方法,并且和已有的参数进行参数数量和类型比对,找到合适的构造方法并调用。

2.5 PreferredConstructors

继续回到一开始的源码中,接下来是这样了:

ctors = mbd.getPreferredConstructors();
if (ctors != null) {return autowireConstructor(beanName, mbd, ctors, null);
}

这块代码看字面好理解,就是获取到主构造方法,不过这个是针对 Kotlin 的,跟我们 Java 无关,我就不啰嗦了。

2.6 instantiateBean

最后就是 instantiateBean 方法了,这个方法就比较简单了,我把代码贴一下小伙伴们应该自己都能看明白:

protected BeanWrapper instantiateBean(String beanName, RootBeanDefinition mbd) {try {Object beanInstance = getInstantiationStrategy().instantiate(mbd, beanName, this);BeanWrapper bw = new BeanWrapperImpl(beanInstance);initBeanWrapper(bw);return bw;}catch (Throwable ex) {throw new BeanCreationException(mbd.getResourceDescription(), beanName, ex.getMessage(), ex);}
}
@Override
public Object instantiate(RootBeanDefinition bd, @Nullable String beanName, BeanFactory owner) {// Don't override the class with CGLIB if no overrides.if (!bd.hasMethodOverrides()) {Constructor<?> constructorToUse;synchronized (bd.constructorArgumentLock) {constructorToUse = (Constructor<?>) bd.resolvedConstructorOrFactoryMethod;if (constructorToUse == null) {final Class<?> clazz = bd.getBeanClass();if (clazz.isInterface()) {throw new BeanInstantiationException(clazz, "Specified class is an interface");}try {constructorToUse = clazz.getDeclaredConstructor();bd.resolvedConstructorOrFactoryMethod = constructorToUse;}catch (Throwable ex) {throw new BeanInstantiationException(clazz, "No default constructor found", ex);}}}return BeanUtils.instantiateClass(constructorToUse);}else {// Must generate CGLIB subclass.return instantiateWithMethodInjection(bd, beanName, owner);}
}

从上面小伙伴么可以看到,本质上其实就是调用了 constructorToUse = clazz.getDeclaredConstructor();,获取到一个公开的无参构造方法,然后据此创建一个 Bean 实例出来。

3. 小结

好了,这就是 Spring 容器中 Bean 的创建过程,我这里单纯和小伙伴们分享了原始 Bean 的创建这一个步骤,这块内容其实非常庞杂,以后有空我会再和小伙伴们分享。

最后,给上面分析的方法生成了一个时序图,小伙伴们作为参考。

其实看 Spring 源码,松哥最大的感悟就是小伙伴们一定要了解 Spring 的各种用法,在此基础之上,源码就很好懂,如果你只会 Spring 一些基本用法,那么源码一定是看得云里雾里的。

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

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

相关文章

【Nginx】静态资源部署、反向代理、负载均衡

个人简介&#xff1a;Java领域新星创作者&#xff1b;阿里云技术博主、星级博主、专家博主&#xff1b;正在Java学习的路上摸爬滚打&#xff0c;记录学习的过程~ 个人主页&#xff1a;.29.的博客 学习社区&#xff1a;进去逛一逛~ nginx静态资源部署、反向代理、负载均衡 &…

今年这情况,真想考研了!

眼下&#xff0c;又是一年的毕业季&#xff0c;超千万规模的毕业生大军如“丧尸围城”&#xff0c;浩浩荡荡地涌入职场。与他们一路同行的还有因疫情影响2022年离校未就业的毕业生&#xff0c;以及那些不幸“被优化”的职场人。 今年&#xff0c;1158 万毕业生&#xff0c;再加…

全面解析大语言模型的工作原理

当ChatGPT在去年秋天推出时&#xff0c;在科技行业乃至世界范围内引起了轰动。当时&#xff0c;机器学习研究人员尝试研发了多年的语言大模型&#xff08;LLM&#xff09;&#xff0c;但普通大众并未十分关注&#xff0c;也没有意识到它们变得多强大。 如今&#xff0c;几乎每个…

ASP.NET Core MVC -- 将视图添加到 ASP.NET Core MVC 应用

Index页 右键单击“视图”文件夹&#xff0c;然后单击“添加”>>“新文件夹”&#xff0c;并将文件夹命名为“HelloWorld”。 右键单击“Views/HelloWorld”文件夹&#xff0c;然后单击“添加”>“新项”。 在“添加新项 - MvcMovie”对话框中&#xff1a; 在右上…

区块链实验室(15) - 编译FISCO BCOS的过程监测

首次编译开源项目&#xff0c;一般需要下载很多依赖包&#xff0c;尤其是从github、sourceforge等下载依赖包时&#xff0c;速度很慢&#xff0c;编译进度似乎没有一点反应&#xff0c;似乎陷入死循环&#xff0c;似乎陷入一个没有结果的等待。本文提供一种监测方法&#xff0c…

ClickHouse SQL与引擎--基本使用(一)

1.查看所有的数据库 show databases; 2.创建库 CREATE DATABASE zabbix ENGINE Ordinary; ATTACH DATABASE ck_test ENGINE Ordinary;3.创建本地表 CREATE TABLE IF NOT EXISTS test01(id UInt64,name String,time UInt64,age UInt8,flag UInt8 ) ENGINE MergeTree PARTI…

Linux 中使用 verdaccio 搭建私有npm 服务器

安装 Node Linux中安装Node 安装verdaccio npm i -g verdaccio安装完成 输入verdaccio,出现下面信息代表安装成功&#xff0c;同时输入verdaccio后verdaccio已经处于运行状态&#xff0c;当然这种启动时暂时的&#xff0c;我们需要通过pm2让verdaccio服务常驻 ygiZ2zec61wsg…

探究Vue源码:mustache模板引擎(11) 递归处理循环逻辑并收尾算法处理

好 在上文 探究Vue源码:mustache模板引擎(10) 解决不能用连续点符号找到多层对象问题&#xff0c;为编译循环结构做铺垫 我们解决了js字符串没办法通过 什么点什么拿到对象中的值的问题 这个大家需要记住 因为这个方法的编写之前是当做面试题出现过的 那么 本文 我们就要去写上…

C++ | C++11新特性(上)

目录 前言 一、列表初始化 二、声明 1、auto 2、decltype 3、nullptr 三、STL容器的变化 四、右值引用与移动语义 1、左值与左值引用 2、右值与右值引用 3、右值引用与左值引用的比较 4、右值引用的场景及意义 &#xff08;1&#xff09;做参数 &#xff08;2&a…

【暑期每日一练】 Epilogue

目录 选择题&#xff08;1&#xff09;解析&#xff1a; &#xff08;2&#xff09;解析&#xff1a; &#xff08;3&#xff09;解析&#xff1a; &#xff08;4&#xff09;解析&#xff1a; &#xff08;5&#xff09;解析&#xff1a; 编程题题一描述输入描述&#xff1a;输…

基于Vue+wangeditor实现富文本编辑

目录 前言分析实现具体解决的问题有具体代码实现如下效果图总结前言 一个网站需要富文本编辑器功能的原因有很多,以下是一些常见的原因: 方便用户编辑内容:富文本编辑器提供了类似于Office Word的编辑功能,使得那些不太懂HTML的用户也能够方便地编辑网站内容。提高用户体验…

力扣 -- 139. 单词拆分

一、题目 题目链接&#xff1a;139. 单词拆分 - 力扣&#xff08;LeetCode&#xff09; 二、解题步骤 下面是用动态规划的思想解决这道题的过程&#xff0c;相信各位小伙伴都能看懂并且掌握这道经典的动规题目滴。 三、参考代码 class Solution { public:bool wordBreak(str…

【Change】50 Matplotlib Visualizations, Python实现,源码可复现

详情请参考博客: Top 50 matplotlib Visualizations 因编译更新问题&#xff0c;本文将稍作更改&#xff0c;以便能够顺利运行。 1 Time Series Plot 时间串行图用于可视化给定指标如何随时间变化。在这里&#xff0c;您可以看到1949年至1969年间航空客运量的变化。查看此免费…

总结七大排序!

排序总览 外部排序&#xff1a;依赖硬盘&#xff08;外部存储器&#xff09;进行的排序。对于数据集合的要求特别高&#xff0c;只能在特定场合下使用&#xff08;比如一个省的高考成绩排序&#xff09;。包括桶排序&#xff0c;基数排序&#xff0c;计数排序&#xff0c;都是o…

Kafka入门,保姆级教学

文章目录 Kafka概念消息中间件对比消息中间件对比-选择建议Kafka常用名词介绍Kafka入门1. Kafka安装配置2.Kafka生产者与消费者关系3.Kafka依赖4.生产者发消息5.消费者接受消息6.Kafka高可用性设计6.1集群Kafka备份机制(Reolication) 7.kafka生产者详解7.1 发送类型7.2参数详解…

【数学建模学习(9):模拟退火算法】

模拟退火算法(Simulated Annealing, SA)的思想借 鉴于固体的退火原理&#xff0c;当固体的温度很高的时候&#xff0c;内能比 较大&#xff0c;固体的内部粒子处于快速无序运动&#xff0c;当温度慢慢降 低的过程中&#xff0c;固体的内能减小&#xff0c;粒子的慢慢趋于有序&a…

Stephen Wolfram:嵌入的概念

The Concept of Embeddings 嵌入的概念 Neural nets—at least as they’re currently set up—are fundamentally based on numbers. So if we’re going to to use them to work on something like text we’ll need a way to represent our text with numbers. And certain…

nginx环境部署

目录 一、yum安装 二、源码安装 三、测试结果 一、yum安装 1、先查找本地yum源上有没有nginx包 yum list | grep nginx 2、rpm安装 rpm -Uvh http://nginx.org/packages/centos/7/x86_64/RPMS/nginx-1.14.2-1.el7_4.ngx.x86_64.rpm 3、查看安装是否成功 rpm -pa | grep…

Go 语言面试题(一):基础语法

文章目录 Q1 和 : 的区别&#xff1f;Q2 指针的作用&#xff1f;Q3 Go 允许多个返回值吗&#xff1f;Q4 Go 有异常类型吗&#xff1f;Q5 什么是协程&#xff08;Goroutine&#xff09;Q6 如何高效地拼接字符串Q7 什么是 rune 类型Q8 如何判断 map 中是否包含某个 key &#xf…

如何解决电脑无声问题:排除故障的几种常见方法

大家好&#xff0c;今天我们来讨论一下处理电脑没有声音的故障。当你突然发现电脑静音无声时&#xff0c;需要逐步排除可能的问题&#xff0c;但总体而言&#xff0c;声音故障是相对容易解决的。接下来&#xff0c;我们将介绍一些排除电脑无声问题的方法。 第一步&#xff1a;…