死磕Spring之IoC篇 - BeanDefinition 的加载阶段(XML 文件)
该系列文章是本人在学习 Spring 的过程中总结下来的,里面涉及到相关源码,可能对读者不太友好,请结合我的源码注释 Spring 源码分析 GitHub 地址 进行阅读
Spring 版本:5.1.14.RELEASE
开始阅读这一系列文章之前,建议先查看《深入了解 Spring IoC(面试题)》这一篇文章
该系列其他文章请查看:《死磕 Spring 之 IoC 篇 - 文章导读》
BeanDefinition 的加载阶段(XML 文件)
上一篇文章 《Bean 的“前身”》 对 BeanDefinition 进行了介绍,Bean 是根据 BeanDefinition 配置元信息对象生成的。我们在 Spring 中通常以这两种方式定义一个 Bean:面向资源(XML、Properties)、面向注解,那么 Spring 是如何将这两种方式定义的信息转换成 BeanDefinition 对象的,接下来会先分析面向资源(XML、Properties)这种方式 Spring 是如何处理的
下来熟悉一段代码:
dependency-lookup-context.xml
:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd">
<!-- <context:component-scan base-package="org.geekbang.thinking.in.spring.ioc.overview" /> -->
<bean id="user" class="org.geekbang.thinking.in.spring.ioc.overview.domain.User">
<property name="id" value="1"/>
<property name="name" value="小马哥"/>
</bean>
</beans>
// 创建 BeanFactory 容器
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(beanFactory);
// XML 配置文件 ClassPath 路径
String location = "classpath:/META-INF/dependency-lookup-context.xml";
// 加载配置
int beanDefinitionsCount = reader.loadBeanDefinitions(location);
System.out.println("Bean 定义加载的数量:" + beanDefinitionsCount);
// 依赖查找
System.out.println(beanFactory.getBean("user"));;
这段代码是 Spring 中编程式使用 IoC 容器,我们可以看到 IoC 容器的使用过程大致如下:
- 创建 BeanFactory 对象(底层 IoC 容器)
- 创建 BeanDefinitionReader 对象(资源解析器),关联第
1
步创建的 BeanFactory - 通过 BeanDefinitionReader 加载 XML 配置文件资源,解析出所有的 BeanDefinition 对象
- 进行依赖查找
上面的第 3
步会解析 Resource 资源,将 XML 文件中定义的 Bean 解析成 BeanDefinition 配置元信息对象,并往 BeanDefinitionRegistry 注册中心注册,此时并没有生成对应的 Bean 对象,需要通过依赖查找获取到 Bean。当然,我们在实际场景中一般不会这样使用 Spring,这些工作都会有 Spring 来完成。接下来我们一起来看看 Sping 是如何加载 XML 文件的
BeanDefinitionReader 体系结构
org.springframework.beans.factory.support.BeanDefinitionReader
接口的类图如下所示:
总览:
org.springframework.beans.factory.support.BeanDefinitionReader
接口,BeanDefinition 读取器org.springframework.beans.factory.support.AbstractBeanDefinitionReader
抽象类,提供通用的实现,具体的资源加载逻辑在由子类实现org.springframework.beans.factory.xml.XmlBeanDefinitionReader
,XML 文件资源解析器,解析出 BeanDefinition 配置元信息对象并注册org.springframework.beans.factory.support.PropertiesBeanDefinitionReader
,Properties 文件资源解析器
BeanDefinitionReader 接口
org.springframework.beans.factory.support.BeanDefinitionReader
接口,BeanDefinition 读取器,定义了加载资源的方法,代码如下:
public interface BeanDefinitionReader {
/** 返回 BeanDefinition 注册中心 */
BeanDefinitionRegistry getRegistry();
/** 返回 Resource 资源加载器,默认为 PathMatchingResourcePatternResolver */
@Nullable
ResourceLoader getResourceLoader();
/** 返回类加载器 */
@Nullable
ClassLoader getBeanClassLoader();
/** 返回 Bean 的名称生成器,默认为 DefaultBeanNameGenerator */
BeanNameGenerator getBeanNameGenerator();
/** 从 Resource 资源中加载 BeanDefinition 并返回数量 */
int loadBeanDefinitions(Resource resource) throws BeanDefinitionStoreException;
int loadBeanDefinitions(Resource... resources) throws BeanDefinitionStoreException;
int loadBeanDefinitions(String location) throws BeanDefinitionStoreException;
int loadBeanDefinitions(String... locations) throws BeanDefinitionStoreException;
}
AbstractBeanDefinitionReader 抽象类
org.springframework.beans.factory.support.AbstractBeanDefinitionReader
抽象类,实现了 BeanDefinitionReader 和 EnvironmentCapable 接口,代码如下:
public abstract class AbstractBeanDefinitionReader implements BeanDefinitionReader, EnvironmentCapable {
private final BeanDefinitionRegistry registry;
@Nullable
private ResourceLoader resourceLoader;
@Nullable
private ClassLoader beanClassLoader;
private Environment environment;
private BeanNameGenerator beanNameGenerator = new DefaultBeanNameGenerator();
protected AbstractBeanDefinitionReader(BeanDefinitionRegistry registry) {
Assert.notNull(registry, "BeanDefinitionRegistry must not be null");
this.registry = registry;
// Determine ResourceLoader to use.
if (this.registry instanceof ResourceLoader) {
this.resourceLoader = (ResourceLoader) this.registry;
}
else {
this.resourceLoader = new PathMatchingResourcePatternResolver();
}
// Inherit Environment if possible
if (this.registry instanceof EnvironmentCapable) {
this.environment = ((EnvironmentCapable) this.registry).getEnvironment();
}
else {
this.environment = new StandardEnvironment();
}
}
@Override
public int loadBeanDefinitions(Resource... resources) throws BeanDefinitionStoreException {
Assert.notNull(resources, "Resource array must not be null");
int count = 0;
for (Resource resource : resources) {
count += loadBeanDefinitions(resource);
}
return count;
}
@Override
public int loadBeanDefinitions(String location) throws BeanDefinitionStoreException {
return loadBeanDefinitions(location, null);
}
public int loadBeanDefinitions(String location, @Nullable Set<Resource> actualResources) throws BeanDefinitionStoreException {
// 获得 ResourceLoader 对象
ResourceLoader resourceLoader = getResourceLoader();
if (resourceLoader == null) {
throw new BeanDefinitionStoreException(
"Cannot load bean definitions from location [" + location + "]: no ResourceLoader available");
}
if (resourceLoader instanceof ResourcePatternResolver) {
// Resource pattern matching available.
try {
// 获得 Resource 数组,因为 Pattern 模式匹配下,可能有多个 Resource 。例如说,Ant 风格的 location
Resource[] resources = ((ResourcePatternResolver) resourceLoader).getResources(location);
// 加载 BeanDefinition 们
int count = loadBeanDefinitions(resources);
if (actualResources != null) {
// 添加到 actualResources 中
Collections.addAll(actualResources, resources);
}
if (logger.isTraceEnabled()) {
logger.trace("Loaded " + count + " bean definitions from location pattern [" + location + "]");
}
return count;
}
catch (IOException ex) {
throw new BeanDefinitionStoreException(
"Could not resolve bean definition resource pattern [" + location + "]", ex);
}
}
else {
// Can only load single resources by absolute URL.
// 获得 Resource 对象
Resource resource = resourceLoader.getResource(location);
// 加载 BeanDefinition 们
int count = loadBeanDefinitions(resource);
if (actualResources != null) {
// 添加到 actualResources 中
actualResources.add(resource);
}
if (logger.isTraceEnabled()) {
logger.trace("Loaded " + count + " bean definitions from location [" + location + "]");
}
return count;
}
}
@Override
public int loadBeanDefinitions(String... locations) throws BeanDefinitionStoreException {
Assert.notNull(locations, "Location array must not be null");
int count = 0;
for (String location : locations) {
count += loadBeanDefinitions(location);
}
return count;
}
// ... 省略相关代码
}
在实现的方法中,最终都会调用 int loadBeanDefinitions(Resource resource)
这个方法,该方法在子类中实现
XmlBeanDefinitionReader
org.springframework.beans.factory.xml.XmlBeanDefinitionReader
,XML 文件资源解析器,解析出 BeanDefinition 配置元信息对象并注册
构造函数
public class XmlBeanDefinitionReader extends AbstractBeanDefinitionReader {
/**
* 禁用验证模式
*/
public static final int VALIDATION_NONE = XmlValidationModeDetector.VALIDATION_NONE;
/**
* 自动获取验证模式
*/
public static final int VALIDATION_AUTO = XmlValidationModeDetector.VALIDATION_AUTO;
/**
* DTD 验证模式
*/
public static final int VALIDATION_DTD = XmlValidationModeDetector.VALIDATION_DTD;
/**
* XSD 验证模式
*/
public static final int VALIDATION_XSD = XmlValidationModeDetector.VALIDATION_XSD;
/** Constants instance for this class. */
private static final Constants constants = new Constants(XmlBeanDefinitionReader.class);
/**
* 验证模式,默认为自动模式。
*/
private int validationMode = VALIDATION_AUTO;
private boolean namespaceAware = false;
private Class<? extends BeanDefinitionDocumentReader> documentReaderClass = DefaultBeanDefinitionDocumentReader.class;
/**
* 解析过程中异常处理器
*/
private ProblemReporter problemReporter = new FailFastProblemReporter();
private ReaderEventListener eventListener = new EmptyReaderEventListener();
private SourceExtractor sourceExtractor = new NullSourceExtractor();
@Nullable
private NamespaceHandlerResolver namespaceHandlerResolver;
private DocumentLoader documentLoader = new DefaultDocumentLoader();
@Nullable
private EntityResolver entityResolver;
private ErrorHandler errorHandler = new SimpleSaxErrorHandler(logger);
/**
* XML 验证模式探测器
*/
private final XmlValidationModeDetector validationModeDetector = new XmlValidationModeDetector();
/**
* 当前线程,正在加载的 EncodedResource 集合。
*/
private final ThreadLocal<Set<EncodedResource>> resourcesCurrentlyBeingLoaded = new NamedThreadLocal<>(
"XML bean definition resources currently being loaded");
/**
* Create new XmlBeanDefinitionReader for the given bean factory.
* @param registry the BeanFactory to load bean definitions into,
* in the form of a BeanDefinitionRegistry
*/
public XmlBeanDefinitionReader(BeanDefinitionRegistry registry) {
super(registry);
}
}
loadBeanDefinitions 方法
loadBeanDefinitions(Resource resource)
方法,解析 Resource 资源的入口,方法如下:
@Override
public int loadBeanDefinitions(Resource resource) throws BeanDefinitionStoreException {
return loadBeanDefinitions(new EncodedResource(resource));
}
public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException {
Assert.notNull(encodedResource, "EncodedResource must not be null");
if (logger.isTraceEnabled()) {
logger.trace("Loading XML bean definitions from " + encodedResource);
}
// <1> 获取当前线程正在加载的 Resource 资源集合,添加当前 Resource,防止重复加载
Set<EncodedResource> currentResources = this.resourcesCurrentlyBeingLoaded.get();
if (currentResources == null) {
currentResources = new HashSet<>(4);
this.resourcesCurrentlyBeingLoaded.set(currentResources);
}
if (!currentResources.add(encodedResource)) { // 将当前资源加入记录中。如果已存在,抛出异常,防止循环加载同一资源出现死循环
throw new BeanDefinitionStoreException(
"Detected cyclic loading of " + encodedResource + " - check your import definitions!");
}
try {
// <2> 从 Resource 资源获取 InputStream 流对象(支持编码)
InputStream inputStream = encodedResource.getResource().getInputStream();
try {
InputSource inputSource = new InputSource(inputStream);
if (encodedResource.getEncoding() != null) {
inputSource.setEncoding(encodedResource.getEncoding());
}
// <3> 【核心】执行加载 Resource 资源过程,解析出 BeanDefinition 进行注册
return doLoadBeanDefinitions(inputSource, encodedResource.getResource());
} finally {
// 关闭流
inputStream.close();
}
} catch (IOException ex) {
throw new BeanDefinitionStoreException(
"IOException parsing XML document from " + encodedResource.getResource(), ex);
} finally {
// <4> 从当前线程移除当前加载的 Resource 对象
currentResources.remove(encodedResource);
if (currentResources.isEmpty()) {
this.resourcesCurrentlyBeingLoaded.remove();
}
}
}
将 Resource 封装成 EncodedResource 对象,目的是让资源对象可设置编码
- 获取当前线程正在加载的 Resource 资源集合,添加当前 Resource,防止重复加载
- 从 Resource 资源获取 InputStream 流对象(支持编码)
- 【核心】调用
doLoadBeanDefinitions(InputSource inputSource, Resource resource)
方法,执行加载 Resource 资源过程,解析出 BeanDefinition 进行注册 - 从当前线程移除当前加载的 Resource 对象
doLoadBeanDefinitions 方法
doLoadBeanDefinitions(InputSource inputSource, Resource resource)
方法,执行加载 Resource 资源过程,解析出 BeanDefinition 进行注册,方法如下:
protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource)
throws BeanDefinitionStoreException {
try {
// <1> 获取 XML Document 实例
Document doc = doLoadDocument(inputSource, resource);
// <2> 根据 Document 实例,解析出 BeanDefinition 们并注册,返回注册数量
int count = registerBeanDefinitions(doc, resource);
if (logger.isDebugEnabled()) {
logger.debug("Loaded " + count + " bean definitions from " + resource);
}
return count;
}
// 省略 catch 各种异常
}
- 调用
doLoadDocument(InputSource inputSource, Resource resource)
方法,获取 XML Document 实例 - 调用
registerBeanDefinitions(Document doc, Resource resource)
方法,根据 Document 实例,解析出 BeanDefinition 们并注册,返回注册数量
doLoadDocument 方法
doLoadDocument(InputSource inputSource, Resource resource)
方法,获取 Resource 资源对应的 XML Document 实例,方法如下:
protected Document doLoadDocument(InputSource inputSource, Resource resource) throws Exception {
// <3> 通过 DefaultDocumentLoader 根据 Resource 获取一个 Document 对象
return this.documentLoader.loadDocument(inputSource,
getEntityResolver(), // <1> 获取 `org.xml.sax.EntityResolver` 实体解析器,ResourceEntityResolver
this.errorHandler,
getValidationModeForResource(resource), isNamespaceAware()); // <2> 获取 XML 文件验证模式,保证 XML 文件的正确性
}
- 获取
org.xml.sax.EntityResolver
实体解析器,ResourceEntityResolver,根据 publicId 和 systemId 获取对应的 DTD 或 XSD 文件,用于对 XML 文件进行验证,这个类比较关键,在后续文章会讲到 - 获取 XML 文件验证模式,保证 XML 文件的正确性,通常情况下都是 XSD 模式
- 获取指定的验证模式,如果手动指定,则直接返回,通常情况下不会
- 从 Resource 资源中获取验证模式,根据 XML 文件的内容进行获取,如果包含
DOCTYPE
内容则为 DTD 模式,否则为 XSD 模式 - 如果还没有获取到验证模式,则默认为 XSD 模式
- 通过 DefaultDocumentLoader 根据 Resource 获取一个 Document 对象
- 创建 DocumentBuilderFactory 对象
factory
,开启校验 - 根据
factory
创建 DocumentBuilder 对象builder
,设置 EntityResolver(第1
步创建的)、ErrorHandler 属性 - 通过
builder
对inputSource
(Resource 资源)进行解析,返回一个 Document 对象
- 创建 DocumentBuilderFactory 对象
上述过程目的就是获取到 Resource 资源对应的 Document 对象,需要经过校验和解析两个过程
registerBeanDefinitions 方法
registerBeanDefinitions(Document doc, Resource resource)
方法,根据 Document 实例,解析出 BeanDefinition 们并注册,返回注册数量,方法如下:
public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException {
// <1> 创建 BeanDefinitionDocumentReader 对象
BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader();
// <2> 获取已注册的 BeanDefinition 数量
int countBefore = getRegistry().getBeanDefinitionCount();
// <3> 创建 XmlReaderContext 对象(读取 Resource 资源的上下文对象)
// <4> 根据 Document、XmlReaderContext 解析出所有的 BeanDefinition 并注册
documentReader.registerBeanDefinitions(doc, createReaderContext(resource));
// <5> 计算新注册的 BeanDefinition 数量
return getRegistry().getBeanDefinitionCount() - countBefore;
}
- 创建 DefaultBeanDefinitionDocumentReader 对象
documentReader
- 获取已注册的 BeanDefinition 数量
- 创建 XmlReaderContext 对象(读取 Resource 资源的上下文对象),注意这里会初始化一个 DefaultNamespaceHandlerResolver 对象,用于处理自定义标签(XML 文件),比较关键,在后续文章会讲到
- 根据 Document、XmlReaderContext 解析出所有的 BeanDefinition 并注册,调用
DefaultBeanDefinitionDocumentReader#registerBeanDefinitions(Document doc, XmlReaderContext readerContext)
方法 - 计算新注册的 BeanDefinition 数量并返回
拓展:DTD 与 XSD 的区别?
DTD(Document Type Definition),即文档类型定义,为 XML 文件的验证机制,属于 XML 文件中组成的一部分。DTD 是一种保证 XML 文档格式正确的有效验证方式,它定义了相关 XML 文档的元素、属性、排列方式、元素的内容类型以及元素的层次结构。其实 DTD 就相当于 XML 中的 “词汇”和“语法”,我们可以通过比较 XML 文件和 DTD 文件 来看文档是否符合规范,元素和标签使用是否正确。
DTD 在一定的阶段推动了 XML 的发展,但是它本身存在着一些缺陷:
- 它没有使用 XML 格式,而是自己定义了一套格式,相对解析器的重用性较差;而且 DTD 的构建和访问没有标准的编程接口,导致解析器很难简单的解析 DTD 文档
- DTD 对元素的类型限制较少;同时其他的约束力也比较弱
- DTD 扩展能力较差
- 基于正则表达式的 DTD 文档的描述能力有限
XSD(XML Schemas Definition),即 XML Schema 语言,针对 DTD 的缺陷由 W3C 在 2001 年推出。XML Schema 本身就是一个 XML 文档,使用的是 XML 语法,因此可以很方便的解析 XSD 文档。相对于 DTD,XSD 具有如下优势:
- XML Schema 基于 XML,没有专门的语法
- XML Schema 可以像其他 XML 文件一样解析和处理
- XML Schema 比 DTD 提供了更丰富的数据类型
- XML Schema 提供可扩充的数据模型
- XML Schema 支持综合命名空间
- XML Schema 支持属性组
总结
我们在 Spring 中通常以这两种方式定义一个 Bean:面向资源(XML、Properties)、面向注解,对于第一种方式如果定义的是一个 XML 文件,Spring 会通过 XmlBeanDefinitionReader 加载该 XML 文件,获取该 Resource 资源的 org.w3c.dom.Document
对象,这个过程会经过校验、解析两个步骤
死磕Spring之IoC篇 - BeanDefinition 的加载阶段(XML 文件)的更多相关文章
- 死磕Spring之IoC篇 - BeanDefinition 的解析阶段(XML 文件)
该系列文章是本人在学习 Spring 的过程中总结下来的,里面涉及到相关源码,可能对读者不太友好,请结合我的源码注释 Spring 源码分析 GitHub 地址 进行阅读 Spring 版本:5.1. ...
- 死磕Spring之IoC篇 - BeanDefinition 的解析过程(面向注解)
该系列文章是本人在学习 Spring 的过程中总结下来的,里面涉及到相关源码,可能对读者不太友好,请结合我的源码注释 Spring 源码分析 GitHub 地址 进行阅读 Spring 版本:5.1. ...
- 死磕Spring之IoC篇 - 文章导读
该系列文章是本人在学习 Spring 的过程中总结下来的,里面涉及到相关源码,可能对读者不太友好,请结合我的源码注释 Spring 源码分析 GitHub 地址 进行阅读 Spring 版本:5.1. ...
- 死磕Spring之IoC篇 - 解析自定义标签(XML 文件)
该系列文章是本人在学习 Spring 的过程中总结下来的,里面涉及到相关源码,可能对读者不太友好,请结合我的源码注释 Spring 源码分析 GitHub 地址 进行阅读 Spring 版本:5.1. ...
- 死磕Spring之IoC篇 - 开启 Bean 的加载
该系列文章是本人在学习 Spring 的过程中总结下来的,里面涉及到相关源码,可能对读者不太友好,请结合我的源码注释 Spring 源码分析 GitHub 地址 进行阅读 Spring 版本:5.1. ...
- 死磕Spring之IoC篇 - Bean 的创建过程
该系列文章是本人在学习 Spring 的过程中总结下来的,里面涉及到相关源码,可能对读者不太友好,请结合我的源码注释 Spring 源码分析 GitHub 地址 进行阅读 Spring 版本:5.1. ...
- 死磕Spring之IoC篇 - @Autowired 等注解的实现原理
该系列文章是本人在学习 Spring 的过程中总结下来的,里面涉及到相关源码,可能对读者不太友好,请结合我的源码注释 Spring 源码分析 GitHub 地址 进行阅读 Spring 版本:5.1. ...
- 死磕Spring之IoC篇 - Spring 应用上下文 ApplicationContext
该系列文章是本人在学习 Spring 的过程中总结下来的,里面涉及到相关源码,可能对读者不太友好,请结合我的源码注释 Spring 源码分析 GitHub 地址 进行阅读 Spring 版本:5.1. ...
- 死磕Spring之IoC篇 - @Bean 等注解的实现原理
该系列文章是本人在学习 Spring 的过程中总结下来的,里面涉及到相关源码,可能对读者不太友好,请结合我的源码注释 Spring 源码分析 GitHub 地址 进行阅读 Spring 版本:5.1. ...
随机推荐
- 如何使用命令将文件夹中的文件名(包括路径)写入到txt文件中
在cmd中使用 cd /d 路径,进入当前文件夹中 使用 dir /s /b > 0.txt 如图:
- UESTC 360(1425) another LCIS
这道题是CD老OJ上面的一道题,现在在新OJ上的题号是360,开始在VJ上做的提交一直RE(囧).后来才知道OJ移位了. 这道题是一个简单的成段更新+区间合并的线段树的题,1A还让我小激动了一下 这道 ...
- 2015 German Collegiate Programming Contest (GCPC 15) + POI 10-T3(12/13)
$$2015\ German\ Collegiate\ Programming\ Contest\ (GCPC 15) + POI 10-T3$$ \(A.\ Journey\ to\ Greece\ ...
- Codeforces Round #681 (Div. 1, based on VK Cup 2019-2020 - Final) B. Identify the Operations (模拟,双向链表)
题意:给你一组不重复的序列\(a\),每次可以选择一个数删除它左边或右边的一个数,并将选择的数append到数组\(b\)中,现在给你数组\(b\),问有多少种方案数得到\(b\). 题解:我们可以记 ...
- UWP(一)开发入门
异常问题总结 Your project file doesn't list 'win10' as a "RuntimeIdentifier". You should add 'wi ...
- 快速获取 Wi-Fi 密码——GitHub 热点速览 v.21.06
作者:HelloGitHub-小鱼干 还有 2 天开启春节七天宅家生活,GitHub 也凑了一把春节热闹,wifi-password 连续霸榜 3 天,作为一个能快速让你连上 Wi-Fi 的小工具,春 ...
- 考研最路径dijkstra和floyd
先来讲个段子:为什么 Dijkstra 不能提出 floyd 算法?因为他的名字是 ijk 而不是 kij. get不到点没有关系.我们今天的任务是看懂这个笑话. dijkstra 的效率是n^2.处 ...
- JVM终结篇
1.1 重新认知JVM 之前我们画过一张图,是从Class文件到类装载器,再到运行时数据区的过程.现在咱们把这张图不妨丰富完善一下,展示了JVM的大体物理结构图. 1.2 GC优化 内存被使用了之后, ...
- POJ 2288 Islands and Bridges(状压DP)题解
题意:n个点,m有向边,w[i]表示i的价值,求价值最大的哈密顿图(只经过所有点一次).价值为:所有点的w之和,加上,每条边的价值 = w[i] * w[j],加上,如果连续的三个点相互连接的价值 = ...
- React 17 All In One
React 17 All In One v17.0.1 https://reactjs.org/blog/2020/10/20/react-v17.html https://reactjs.org/b ...