Spring源码深度解析系列——Bean的基本实现

导语

笔者之前虽然草草的读了关于Spring方面的源码,但也仅仅是草草的读了一下。并未作任何笔记,无奈记性太差,近些时间想起前人的话:好记性不如烂笔头。于是想着再重新阅读一下Spring方面的源码,顺便复习一下一些基础知识。接下来便开始Spring源码之旅了。

首先在Spring源码系列中,笔者所使用的是 spring-boot-starter-parent 2.1.6.RELEASE 版本,依赖的spring framework版本为 5.1.8.RELEASE。关于Spring入门示例代码如下:

1.实体类:

@Data
public class Student {

    private String id;
    private String Name;
    private int age;

}

2.配置文件:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="student" class="com.zfy.spring.test.bean.Student" />
</beans>

3.测试类:

    @Test
    public void stringTest() {
        BeanFactory bf = new XmlBeanFactory(new ClassPathResource("springTest.xml"));
        Student st = (Student) bf.getBean("student");
        System.out.println(st);
        /*ApplicationContext ctx = new ClassPathXmlApplicationContext("springTest.xml");
        Student student = (Student) ctx.getBean("student");
        System.out.println(student);*/

    }

一、Bean的核心类介绍

如果说到Bean 的核心类,那么是避不开 DefaultListableBeanFactory  XmlBeanDefinitionReader 这两个类的。那么我们就来先对着两个核心类分别进行介绍一下。先看一下 Bean的相关加载容器类图:

首先来说下DefaultListableBeanFactory ,从上面的类图中可以看出XmlBeanFactory是继承自DefaultListableBeanFactory的,但DefaultListableBeanFactory却是整个Bean容器的核心。从上图中我们还可以看出XmlBeanFactory类上划了一条横线,这代表着这个类已经过期,但这个并不影响我们阅读源码的精华所在。对于XmlBeanFactory和DefaultListableBeanFactory之间的区别,是因为XmlBeanFactory中使用了自定义的XML读取器XmlBeanDefinitionReader,实现了个性化的BeanDefinitionReader读取。DefaultListableBeanFactory继承自AbstractAutowireCapableBeanFactory类,且实现了ConfigurableListableBeanFactory、BeanDefinitionRegistry接口。

其实,从上面的类图中,是可以清晰的看出DefaultListableBeanFactory的脉络的,但是这里还是需要对每个类的作用进行介绍一下:

1.DefaultListableBeanFactory

  • AliasRegistry:定义对Alias的简单的增删改查操作。
  • SimpleAliasRegistry:主要是使用map对alias进行缓存,且实现了接口AliasRegistry。
  • SingletonBeanRegistry:定义对单例的注册及获取。
  • BeanFactory:定义获取Bean及Bean的各种属性。
  • DefaultSingletonBeanRegistry:对接口SingletonBeanRegistry的实现。
  • HierarchicalBeanFactory:继承自Beanfactory,且在Beanfactory所定义的功能的基础之上,增加了对parentFactory的支持。
  • BeanDefinitionRegistry:定义了对BeanDefinition的增删改查操作。
  • FactoryBeanRegistrySupport:在DefaultSingletonBeanRegistry基础上增加了对FactoryBean的特殊处理功能。
  • ConfigurableBeanFactory:提供配置Factory的各种方法。
  • ListableBeanFactory:根据各种条件获取Bean的配置清单。
  • AbstractBeanFactory:综合FactoryBeanRegistrySupport和ConfigurableBeanFactory的功能。
  • AutowireCapableBeanFactory:提供创建bean、自动注入、初始化以及应用Bean的后处理器。
  • AbstractAutowireCapableBeanFactory:综合AbstractBeanFactory并对接口AutowireCapableBeanFactory进行实现。
  • ConfigurableListableBeanFactory:BeanFactory配置清单,指定忽略类型及接口等。
  • DefaultListableBeanFactory:综合上述所有功能,此类主要是对Bean注册后的处理。

2.XMLBeanDefinitionReader

XML配置文件的读取是Spring中的一个重要的功能,因为在Spring中大部分的功能都是以配置作为切入点,那么我们可以从XmlBeanDefinitionReader中梳理一下资源文件读取、解析及注册的大致脉络,首先介绍下各个类的功能:

  • ResourceLoader:定义资源加载器,主要应用于根据给定的资源文件地址返回对应的Resource。
  • BeanDefinitionReader:主要定义资源文件读取,并转换为BeanDefinition的各个功能。
  • EnvironmentCapable:定义获取了Environment方法。
  • DocumentLoader:定义从资源文件加载到转换为Document的功能。
  • AbstractBeanDefinitionReader:对EnvironmentCapable、BeanDefinitionReader类定义的功能进行实现。
  • BeanDefinitionDocumentReader:定义读取Document并注册BeanDefinition功能。
  • BeanDefinitionParserDelegate:定义解析Element的各种方法。

 

从上面的梳理中,我们可以得知XMLBeanDefinitionReader的几个重要的处理流程如下:

  1. 通过继承AbstractBeanDefinitionReader的方法,来使用ResourceLoader把资源文件转换为对应 的Resource文件。
  2. 通过DocumentLoader对Resource文件进行转换,将Resource文件转换为Document文件。
  3. 通过实现接口BeanDefinitionDocumentReader的DefaultBeanDefinitionDocumentReader类对Document进行解析,并使用BeanDefinitionParserDelegate对Element进行解析。

基础的核心类的相关功能及作用都已介绍完了,接下来就开始源码的解析之旅吧!

 

二、XMLBeanFactory的基本原理

从开始的示例中,可以得知Spring对配置文件的读取,是通过ClassPathResource进行操作的。当调用完ClassPathResource时,会调用其构造函数来构造Resource资源文件的示例对象。这样在后续的操作中,便可以利用Resource提供的各种功能来进行操作了。当我们获得了Resource的实例对象后,便可以对XMLBeanFactory进行初始化了。

1.XmlBeanFactory的初始化

// 实现了自定义的XML读取器
private final XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(this);


public XmlBeanFactory(Resource resource) throws BeansException {
	// 调用XmlBeanFactory(Resource,BeanFactory)构造方法
	this(resource, null);
}


// parentBeanFactory为父类BeanFactory用于factory合并,可以为空
public XmlBeanFactory(Resource resource, BeanFactory parentBeanFactory) throws BeansException {
        // 调用父类构造函数
	super(parentBeanFactory);
	this.reader.loadBeanDefinitions(resource);
}

从上述代码中可以得知,在XMLBeanFactory初始化时还需调用XmlBeanFactory(Resource,BeanFactory)构造方法,而在这个构造方法中this.reader.loadBeanDefinitions(resource)这一步才是资源加载的真正实现,XmlBeanDefinitionReader 加载数据就是在这里完成的。

3.Bean的加载

前面提到的在XMLFactory构造函数中调用了XMLDefinitionReader类中的loadBeanDefinitions(resource)方法,这句代码便是整个资源的切入点了。我们可以看下关于这个方法的时序图:

      从上图中,可以看出处理的过程大概如下:

  1. 封装资源文件。当进入XmlBeanDefinitionReader后,首先对传入的参数Resource使用EncodedResource类进行封装。
  2. 获取输入流。从Resource中焯去对应的InputStream,并构造InputResource。
  3. 通过构造的InputSource实例和Resource实例,并继续调用函数doLoadBeanDefinitions。

 接下来看看loadBeanDefinitions方法的具体实现:

    public int loadBeanDefinitions(Resource resource) throws BeanDefinitionStoreException {
	    return loadBeanDefinitions(new EncodedResource(resource));
    }

从上面的代码中,我们可以看出首先通过EncodedResource把传入的资源文件进行编码处理,这里面的主要核心逻辑是体现在getReader()方法中的,在设置了编码属性的时候,Spring会使用相应的编码作为输入流的编码,代码如下:

	public Reader getReader() throws IOException {
		if (this.charset != null) {
			return new InputStreamReader(this.resource.getInputStream(), this.charset);
		}
		else if (this.encoding != null) {
			return new InputStreamReader(this.resource.getInputStream(), this.encoding);
		}
		else {
			return new InputStreamReader(this.resource.getInputStream());
		}
	}

上面的代码中构造了一个有编码的InputStreamReader,在构造好encodeResource对象后,再次转入了可复用的方法loadBeanDefinitions(new EncodedResource(resource))。这个方法便是真正的数据准备阶段,在时序图中也有所描述:

	public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException {
		Assert.notNull(encodedResource, "EncodedResource must not be null");
		if (logger.isInfoEnabled()) {
			logger.info("Loading XML bean definitions from " + encodedResource);
		}

		// 通过属性来记录已经加载的资源
		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 {
			// 从encodedResource中获取已经封装的Resource对象并再次从Resource中获取其中的inputStream
			InputStream inputStream = encodedResource.getResource().getInputStream();
			try {
				// InputSource这个类并不是来自Spring,它的全路径是org.xml.sax.InputSource
				InputSource inputSource = new InputSource(inputStream);
				if (encodedResource.getEncoding() != null) {
					inputSource.setEncoding(encodedResource.getEncoding());
				}
				// 真正进入了逻辑核心部分
				return doLoadBeanDefinitions(inputSource, encodedResource.getResource());
			}
			finally {
				// 关闭输入流
				inputStream.close();
			}
		}
		catch (IOException ex) {
			throw new BeanDefinitionStoreException(
					"IOException parsing XML document from " + encodedResource.getResource(), ex);
		}
		finally {
			currentResources.remove(encodedResource);
			if (currentResources.isEmpty()) {
				this.resourcesCurrentlyBeingLoaded.remove();
			}
		}
	}

这里首先对传入的resource参数进行封装,目的是考虑到Resource可能存在编码要求的情况,其次再通过SAX读取XML文件的方式来准备InputResource对象,最后将准备的数据通过把参数传入真正的核心部分doLoadBeanDefinitions(inputSource, encodedResource.getResource())。代码如下:

	protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource)
			throws BeanDefinitionStoreException {
		try {
			/**
			 * 	1.获取对XML文件的验证模式
			 * 	2.加载XML文件,并得到对应的 Document
			 */
			Document doc = doLoadDocument(inputSource, resource);
			// 根据返回的Document注册Bean信息
			return registerBeanDefinitions(doc, resource);
		}
		catch (BeanDefinitionStoreException ex) {
			throw ex;
		}
		catch (SAXParseException ex) {
			throw new XmlBeanDefinitionStoreException(resource.getDescription(),
					"Line " + ex.getLineNumber() + " in XML document from " + resource + " is invalid", ex);
		}
		catch (SAXException ex) {
			throw new XmlBeanDefinitionStoreException(resource.getDescription(),
					"XML document from " + resource + " is invalid", ex);
		}
		catch (ParserConfigurationException ex) {
			throw new BeanDefinitionStoreException(resource.getDescription(),
					"Parser configuration exception parsing XML from " + resource, ex);
		}
		catch (IOException ex) {
			throw new BeanDefinitionStoreException(resource.getDescription(),
					"IOException parsing XML document from " + resource, ex);
		}
		catch (Throwable ex) {
			throw new BeanDefinitionStoreException(resource.getDescription(),
					"Unexpected exception parsing XML document from " + resource, ex);
		}
	}

在这段冗长的代码中,如果不考虑解决异常的代码,也就只是做了三件事(代码中已注释)。

 

3.获取XML的验证模式

了解XML的,应该知道XML的验证模式是为了保证XML的正确性,而比较常用的验证模式有:DTD和XSD,对于这个两个模式的区别本文暂不做赘述了,有兴趣的小伙伴可以去自行了解一下。

在前面的代码中我们了解了,在 doLoadDocument(inputSource, resource) 方法中完成了,对获取对XML文件的验证模式和加载XML文件,并得到对应的 Document,那么我们来看下代码中是如何实现的:

	protected Document doLoadDocument(InputSource inputSource, Resource resource) throws Exception {
		return this.documentLoader.loadDocument(inputSource, getEntityResolver(), this.errorHandler,
				getValidationModeForResource(resource), isNamespaceAware());
	}


	protected int getValidationModeForResource(Resource resource) {
		int validationModeToUse = getValidationMode();
		// 如果手动指定了验证模式,则使用指定的验证模式
		if (validationModeToUse != VALIDATION_AUTO) {
			return validationModeToUse;
		}
		int detectedMode = detectValidationMode(resource);
		// 如果未指定则使用自动监测
		if (detectedMode != VALIDATION_AUTO) {
			return detectedMode;
		}
		
		return VALIDATION_XSD;
	}

这里的代码的意思是说,如果已经设定了验证模式,就是用设定的验证模式(可以通过对调用XmlBeanDefinition中的setValidationMode方法进行设定),否则就会使用自动检测的方式。而自动检验模式的功能在函数detectValidationMode方法中实现的,在detectValidationMode方法中又将自动检测验证模式的工作专门委托给了专门的处理类XmlValidationModeDetector,调用了XmlValidationModeDetector的validationModeDetecto方法,具体实现如下:

	protected int detectValidationMode(Resource resource) {
		if (resource.isOpen()) {
			throw new BeanDefinitionStoreException(
					"Passed-in Resource [" + resource + "] contains an open stream: " +
					"cannot determine validation mode automatically. Either pass in a Resource " +
					"that is able to create fresh streams, or explicitly specify the validationMode " +
					"on your XmlBeanDefinitionReader instance.");
		}

		InputStream inputStream;
		try {
			inputStream = resource.getInputStream();
		}
		catch (IOException ex) {
			throw new BeanDefinitionStoreException(
					"Unable to determine validation mode for [" + resource + "]: cannot open InputStream. " +
					"Did you attempt to load directly from a SAX InputSource without specifying the " +
					"validationMode on your XmlBeanDefinitionReader instance?", ex);
		}

		try {
			// 主要调用
			return this.validationModeDetector.detectValidationMode(inputStream);
		}
		catch (IOException ex) {
			throw new BeanDefinitionStoreException("Unable to determine validation mode for [" +
					resource + "]: an error occurred whilst reading from the InputStream.", ex);
		}
	}

XmlValidationModeDetector:

	public int detectValidationMode(InputStream inputStream) throws IOException {
		// Peek into the file to look for DOCTYPE.
		BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
		try {
			boolean isDtdValidated = false;
			String content;
			while ((content = reader.readLine()) != null) {
				content = consumeCommentTokens(content);
				// 如果读取的行是空,或者是注释则略过
				if (this.inComment || !StringUtils.hasText(content)) {
					continue;
				}
				if (hasDoctype(content)) {
					isDtdValidated = true;
					break;
				}
				// 读取到<开始符号,验证模式一定会在开始符号之前
				if (hasOpeningTag(content)) {
					// End of meaningful data...
					break;
				}
			}
			return (isDtdValidated ? VALIDATION_DTD : VALIDATION_XSD);
		}
		catch (CharConversionException ex) {
			// Choked on some character encoding...
			// Leave the decision up to the caller.
			return VALIDATION_AUTO;
		}
		finally {
			reader.close();
		}
	}

在理解XSD和DTD的使用方法后,理解上面的代码是没什么难点的。Spring用来检测验证模式的办法就是判断是否包含DOCTYPE,如果包含就是DTD,否则就是XSD。

 

4.获取Document

在经过验证模式准备的步骤后,就可以进行Document加载了。同样在这里XmlBeanFactoryReader类并没有自己去做这样的工作,而是委托给了DocumentLoader去执行了,这里的DocumentLoader其实只是个接口,其真正的实现还是DefaultDocumentLoader,代码如下:

	public Document loadDocument(InputSource inputSource, EntityResolver entityResolver,
			ErrorHandler errorHandler, int validationMode, boolean namespaceAware) throws Exception {

		// 创建 DocumentBuilderFactory 对象
		DocumentBuilderFactory factory = createDocumentBuilderFactory(validationMode, namespaceAware);
		if (logger.isDebugEnabled()) {
			logger.debug("Using JAXP provider [" + factory.getClass().getName() + "]");
		}
		// 再通过 DocumentBuilderFactory 创建 DocumentBuilder
		DocumentBuilder builder = createDocumentBuilder(factory, entityResolver, errorHandler);
		// 使用 DocumentBuilder 解析 inputSource
		return builder.parse(inputSource);
	}

这块的代码就如上面注释所说的那几个步骤,来完成相关的操作。

	protected EntityResolver getEntityResolver() {
		if (this.entityResolver == null) {
			// Determine default EntityResolver to use.
			ResourceLoader resourceLoader = getResourceLoader();
			if (resourceLoader != null) {
				this.entityResolver = new ResourceEntityResolver(resourceLoader);
			}
			else {
				this.entityResolver = new DelegatingEntityResolver(getBeanClassLoader());
			}
		}
		return this.entityResolver;
	}

EntityResolver是项目提供的一个寻找DTD的声明方法,根据之前通过getEntityResolver()方法对EntityResolver的获取,我们知道在Spring中使用DelegatingEntityResolver为EntityResolver的实现类,实现如下:

	public InputSource resolveEntity(@Nullable String publicId, @Nullable String systemId)
			throws SAXException, IOException {

		if (systemId != null) {
			if (systemId.endsWith(DTD_SUFFIX)) {
				// 如果是 dtd,从这里解析
				return this.dtdResolver.resolveEntity(publicId, systemId);
			}
			else if (systemId.endsWith(XSD_SUFFIX)) {
				// 通过调用META-INF/Spring.schemas解析
				return this.schemaResolver.resolveEntity(publicId, systemId);
			}
		}

		// Fall back to the parser's default behavior.
		return null;
	}

在这里可以看到,对于不同的验证模式,Spring使用了不同的解析器来解析。在这里比如加载DTD类型的BeanDtdResolver的resolverEntity是直接截取systemId最后的xx.dtd,然后在当前的路径下寻找,而加载XSD类型的PuggableSchemaResolver类的resolverEntity是默认到META-INF/Spring.schemas文件中找到systemid所对应的的XSD文件并加载。

BeansDtdResolver.java

	public InputSource resolveEntity(@Nullable String publicId, @Nullable String systemId) throws IOException {
		if (logger.isTraceEnabled()) {
			logger.trace("Trying to resolve XML entity with public ID [" + publicId +
					"] and system ID [" + systemId + "]");
		}
		// DTD_EXTENSION = ".dtd";
		if (systemId != null && systemId.endsWith(DTD_EXTENSION)) {
			int lastPathSeparator = systemId.lastIndexOf('/');
			int dtdNameStart = systemId.indexOf(DTD_NAME, lastPathSeparator);
			if (dtdNameStart != -1) {
				String dtdFile = DTD_NAME + DTD_EXTENSION;
				if (logger.isTraceEnabled()) {
					logger.trace("Trying to locate [" + dtdFile + "] in Spring jar on classpath");
				}
				try {
					Resource resource = new ClassPathResource(dtdFile, getClass());
					InputSource source = new InputSource(resource.getInputStream());
					source.setPublicId(publicId);
					source.setSystemId(systemId);
					if (logger.isDebugEnabled()) {
						logger.debug("Found beans DTD [" + systemId + "] in classpath: " + dtdFile);
					}
					return source;
				}
				catch (FileNotFoundException ex) {
					if (logger.isDebugEnabled()) {
						logger.debug("Could not resolve beans DTD [" + systemId + "]: not found in classpath", ex);
					}
				}
			}
		}

		// Fall back to the parser's default behavior.
		return null;
	}

5.解析及注册BeanDefinition

在文件转换为Document之后,接下来就是提取及注册Bean了,当得到XML文档的Document实例对象之后,就会引入下面这个方法:

	public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException {
		// 使用 DefaultBeanDefinitionDocumentReader 实例化 BeanDefinitionDocumentReader
		BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader();
		// 在实例化BeanDefinitionReader的时候会将BeanDefinitionRegistry传入,默认使用继承自DefaultListableBeanFactory
		// 记录统计前BeanDefinition的加载个数
		int countBefore = getRegistry().getBeanDefinitionCount();
		// 加载及注册Bean
		documentReader.registerBeanDefinitions(doc, createReaderContext(resource));
		// 记录本次加载的BeanDefinition的个数
		return getRegistry().getBeanDefinitionCount() - countBefore;
	}

代码中的参数doc便是通过前面的loadDocument加载转换出来的,这个方法很好的使用了面向对象的单一职责的原则,将逻辑处理委托给单一的类进行处理,而这个逻辑处理类就是BeanDefinitionDocumentReader,但是BeanDefinitionDocumentReader只是一个接口,其实例化的工作是在createBeanDefinitionDocumentReader()方法中完成的。BeanDefinitionDocumentReader的真正的实现类是DefaultBeanDefinitionDocumentReader,在进入DefaultBeanDefinitionDocumentReader这个类之后,可以发现这个方法的重要目的就是提取root,以便于继续将root作为参数继续注册。

	public void registerBeanDefinitions(Document doc, XmlReaderContext readerContext) {
		this.readerContext = readerContext;
		logger.debug("Loading bean definitions");
		Element root = doc.getDocumentElement();
                // 核心逻辑
		doRegisterBeanDefinitions(root);
	}

经过前面如此复杂的代码解析,终于达到了核心逻辑doRegisterBeanDefinitions(root),其实前面只是XML 的加载解析阶段,而doRegisterBeanDefinitions才算真正的开始解析,那我们继续开始吧!

	protected void doRegisterBeanDefinitions(Element root) {
		// 专门处理解析
		BeanDefinitionParserDelegate parent = this.delegate;
		this.delegate = createDelegate(getReaderContext(), root, parent);

		if (this.delegate.isDefaultNamespace(root)) {
			// 处理profile属性
			String profileSpec = root.getAttribute(PROFILE_ATTRIBUTE);
			if (StringUtils.hasText(profileSpec)) {
				String[] specifiedProfiles = StringUtils.tokenizeToStringArray(
						profileSpec, BeanDefinitionParserDelegate.MULTI_VALUE_ATTRIBUTE_DELIMITERS);
				if (!getReaderContext().getEnvironment().acceptsProfiles(specifiedProfiles)) {
					if (logger.isInfoEnabled()) {
						logger.info("Skipped XML bean definition file due to specified profiles [" + profileSpec +
								"] not matching: " + getReaderContext().getResource());
					}
					return;
				}
			}
		}

		// 解析前处理,留给子类去实现
		preProcessXml(root);
		parseBeanDefinitions(root, this.delegate);
		// 解析后处理,留给子类去实现
		postProcessXml(root);

		this.delegate = parent;
	}

上面代码中,首先对profile进行处理,然后才开始解析,但在preProcessXml(root)和postProcessXml(root)中是没有做任何实现的,那么这两个方法设计的目的是什么呢?其实这里就是我们所知道的设计模式中的模板方法模式,如果继承自DefaultBeanDefinitionDocumentReader的子类需要在Bean解析的前后做一些处理的话,那么只需要重写这两个方法就行了。

在处理完了profile之后,就可以进行XML的读取了,那我们继续来看下parseBeanDefinitions(root, this.delegate)方法中做了哪些操作:

	protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {
		// 对Bean的处理
		if (delegate.isDefaultNamespace(root)) {
			NodeList nl = root.getChildNodes();
			for (int i = 0; i < nl.getLength(); i++) {
				Node node = nl.item(i);
				if (node instanceof Element) {
					Element ele = (Element) node;
					if (delegate.isDefaultNamespace(ele)) {
						// 对Bean的处理
						parseDefaultElement(ele, delegate);
					}
					else {
						// 对Bean的处理
						delegate.parseCustomElement(ele);
					}
				}
			}
		}
		else {
			delegate.parseCustomElement(root);
		}
	}

 

参考:Spring源码深度解析(第二版)(郝佳)

 

发布了41 篇原创文章 · 获赞 8 · 访问量 4264

猜你喜欢

转载自blog.csdn.net/zfy163520/article/details/93735731
今日推荐