Spring是一个开源的设计层面框架,解决了业务逻辑层和其他各层的松耦合问题,将面向接口的编程思想贯穿整个系统应用,同时它也是Java工作中必备技能之一...前言
紧跟上篇 Spring解密 - XML解析 与 Bean注册 ,我们接着往下分析源码
解密在 Spring 的 XML 配置里面有两大类声明,一个是默认的如
parseDefaultElement 对 4 种不同的标签 import、alias、bean、beans 做了不同的处理,其中 bean 标签的解析最为复杂也最为重要,所以我们将从 bean 开始深入分析,如果能理解此标签的解析过程,其他标签的解析自然会迎刃而解。上一篇中只是简单描述了一下,本篇我们围绕解析模块详细的探讨一下
public class DefaultBeanDefinitionDocumentReader implements BeanDefinitionDocumentReader { private void parseDefaultElement(Element ele, BeanDefinitionParserDelegate delegate) { // import 标签解析 if (delegate.nodeNameEquals(ele, IMPORT_ELEMENT)) { importBeanDefinitionResource(ele); } // alias 标签解析 else if (delegate.nodeNameEquals(ele, ALIAS_ELEMENT)) { processAliasRegistration(ele); } // bean 标签解析 else if (delegate.nodeNameEquals(ele, BEAN_ELEMENT)) { processBeanDefinition(ele, delegate); } // import 标签解析 else if (delegate.nodeNameEquals(ele, NESTED_BEANS_ELEMENT)) { // beans标签解析 递归方式 doRegisterBeanDefinitions(ele); } } }
首先我们来分析下当类中的 processBeanDefinition(ele, delegate)
protected void processBeanDefinition(Element ele, BeanDefinitionParserDelegate delegate) { // 委托BeanDefinitionDelegate类的parseBeanDefinitionElement方法进行元素解析 BeanDefinitionHolder bdHolder = delegate.parseBeanDefinitionElement(ele); if (bdHolder != null) { // 当返回的bdHolder不为空的情况下若存在默认标签的子节点下再有自定义属性,还需要再次对自定义标签进行解析 bdHolder = delegate.decorateBeanDefinitionIfRequired(ele, bdHolder); try { // 解析完成后需要对解析后的bdHolder进行注册,注册操作委托给了BeanDefinitionReaderUtils的registerBeanDefinition方法 BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder, getReaderContext().getRegistry()); } catch (BeanDefinitionStoreException ex) { getReaderContext().error("Failed to register bean definition with name "" + bdHolder.getBeanName() + """, ele, ex); } // 最后发出响应事件,通知相关监听器这个bean已经被加载 getReaderContext().fireComponentRegistered(new BeanComponentDefinition(bdHolder)); } }
这段代码中:
首先委托 BeanDefinitionParseDelegate 对节点做了解析,并返回了一个 BeanDefinitionHolder 的实例,在这个实例中已经包含了配置文件中配置的各种属性了
如果在当前子节点中存在自定义属性,则还需要对自定义标签进行解析
解析完成后,需要对解析后的 bdHolder 进行注册,同样注册操作委托给了 BeanDefinitionReaderUtils
最后发出响应事件,通知相关监听器这个 bean 已经被加载
下面我们详细分析下,Spring 是如何解析各个标签和节点的
bean 标签解析public class BeanDefinitionParserDelegate { @Nullable public BeanDefinitionHolder parseBeanDefinitionElement(Element ele, @Nullable BeanDefinition containingBean) { // 获取Bean标签的ID属性 String id = ele.getAttribute(ID_ATTRIBUTE); // 获取Bean标签的Name属性 String nameAttr = ele.getAttribute(NAME_ATTRIBUTE); Listaliases = new ArrayList<>(); if (StringUtils.hasLength(nameAttr)) { // 将name属性的值通过,; 进行分割 转为字符串数字(即在配置文件中如配置多个name 在此做处理) String[] nameArr = StringUtils.tokenizeToStringArray(nameAttr, MULTI_VALUE_ATTRIBUTE_DELIMITERS); aliases.addAll(Arrays.asList(nameArr)); } String beanName = id; // 如果ID为空 使用配置的第一个name属性作为ID if (!StringUtils.hasText(beanName) && !aliases.isEmpty()) { beanName = aliases.remove(0); if (logger.isDebugEnabled()) { logger.debug("No XML "id" specified - using "" + beanName + "" as bean name and " + aliases + " as aliases"); } } if (containingBean == null) { // 校验beanName和aliases的唯一性 // 内部核心为使用usedNames集合保存所有已经使用了的beanName和alisa checkNameUniqueness(beanName, aliases, ele); } // 进一步解析其他所有属性到GenericBeanDefinition对象中 AbstractBeanDefinition beanDefinition = parseBeanDefinitionElement(ele, beanName, containingBean); if (beanDefinition != null) { // 如果bean没有指定beanName 那么使用默认规则为此Bean生成beanName if (!StringUtils.hasText(beanName)) { try { if (containingBean != null) { beanName = BeanDefinitionReaderUtils.generateBeanName(beanDefinition, this.readerContext.getRegistry(), true); } else { beanName = this.readerContext.generateBeanName(beanDefinition); // Register an alias for the plain bean class name, if still possible, // if the generator returned the class name plus a suffix. // This is expected for Spring 1.2/2.0 backwards compatibility. String beanClassName = beanDefinition.getBeanClassName(); if (beanClassName != null && beanName.startsWith(beanClassName) && beanName.length() > beanClassName.length() && !this.readerContext.getRegistry().isBeanNameInUse(beanClassName)) { aliases.add(beanClassName); } } if (logger.isDebugEnabled()) { logger.debug("Neither XML "id" nor "name" specified - " + "using generated bean name [" + beanName + "]"); } } catch (Exception ex) { error(ex.getMessage(), ele); return null; } } String[] aliasesArray = StringUtils.toStringArray(aliases); // 将信息封装到BeanDefinitionHolder对象中 return new BeanDefinitionHolder(beanDefinition, beanName, aliasesArray); } return null; } }
该方法主要处理了 id、name、alias 等相关属性,生成了 beanName,并且在重载函数 parseBeanDefinitionElement(ele, beanName, containingBean)方法中完成核心的标签解析。
接下来重点分析parseBeanDefinitionElement(Element ele, String beanName, @Nullable BeanDefinition containingBean)
看下它是如何完成标签解析操作的
@Nullable public AbstractBeanDefinition parseBeanDefinitionElement( Element ele, String beanName, @Nullable BeanDefinition containingBean) { this.parseState.push(new BeanEntry(beanName)); // 获取Bean标签的class属性 String className = null; if (ele.hasAttribute(CLASS_ATTRIBUTE)) { className = ele.getAttribute(CLASS_ATTRIBUTE).trim(); } // 获取Bean标签的parent属性 String parent = null; if (ele.hasAttribute(PARENT_ATTRIBUTE)) { parent = ele.getAttribute(PARENT_ATTRIBUTE); } try { // 创建用于承载属性的AbstractBeanDefinition AbstractBeanDefinition bd = createBeanDefinition(className, parent); // 获取bean标签各种属性 parseBeanDefinitionAttributes(ele, beanName, containingBean, bd); // 解析description标签 bd.setDescription(DomUtils.getChildElementValueByTagName(ele, DESCRIPTION_ELEMENT)); // 解析meta标签 parseMetaElements(ele, bd); // 解析lookup-method标签 parseLookupOverrideSubElements(ele, bd.getMethodOverrides()); // 解析replaced-method标签 parseReplacedMethodSubElements(ele, bd.getMethodOverrides()); // 解析constructor-arg标签 parseConstructorArgElements(ele, bd); // 解析property标签 parsePropertyElements(ele, bd); // 解析qualifier标签 parseQualifierElements(ele, bd); bd.setResource(this.readerContext.getResource()); bd.setSource(extractSource(ele)); return bd; } catch (ClassNotFoundException ex) { error("Bean class [" + className + "] not found", ele, ex); } catch (NoClassDefFoundError err) { error("Class that bean class [" + className + "] depends on not found", ele, err); } catch (Throwable ex) { error("Unexpected failure during bean definition parsing", ele, ex); } finally { this.parseState.pop(); } return null; }
进一步解析其他属性和元素(元素和属性很多,所以这是一个庞大的工作量)并统一封装至 GenericBeanDefinition 中, 解析完成这些属性和元素之后,如果检测到 bean 没有指定的 beanName,那么便使用默认的规则为 bean 生成一个 beanName。
// BeanDefinitionParserDelegate.java protected AbstractBeanDefinition createBeanDefinition(@Nullable String className, @Nullable String parentName) throws ClassNotFoundException { return BeanDefinitionReaderUtils.createBeanDefinition( parentName, className, this.readerContext.getBeanClassLoader()); } public class BeanDefinitionReaderUtils { public static AbstractBeanDefinition createBeanDefinition( @Nullable String parentName, @Nullable String className, @Nullable ClassLoader classLoader) throws ClassNotFoundException { GenericBeanDefinition bd = new GenericBeanDefinition(); // parentName可能为空 bd.setParentName(parentName); // 如果classLoader不为空 // 则使用传入的classLoader同一虚拟机加载类对象 否则只记录classLoader if (className != null) { if (classLoader != null) { bd.setBeanClass(ClassUtils.forName(className, classLoader)); } else { bd.setBeanClassName(className); } } return bd; } }
BeanDefinition 是
至此 createBeanDefinition(className, parent); 已经说完了,而且我们也获得了 用于承载属性的AbstractBeanDefinition,接下来看看 parseBeanDefinitionAttributes(ele, beanName, containingBean, bd); 是如何解析 bean 中的各种标签属性的
public class BeanDefinitionParserDelegate { public AbstractBeanDefinition parseBeanDefinitionAttributes(Element ele, String beanName, @Nullable BeanDefinition containingBean, AbstractBeanDefinition bd) { // ...省略详细代码,该部分代码主要就是通过 if else 判断是否含有指定的属性,如果有就 bd.set(attribute); return bd; } }
bean 标签的完整解析到这就已经全部结束了,其中 bean 标签下的元素解析都大同小异,有兴趣的可以自己跟踪一下源代码看看 qualifier、lookup-method 等解析方式(相对 bean 而言不复杂)。自定义标签内容较多会在下一章详细介绍。
最后将获取到的信息封装到 BeanDefinitionHolder 实例中
// BeanDefinitionParserDelegate.java @Nullable public BeanDefinitionHolder parseBeanDefinitionElement(Element ele, @Nullable BeanDefinition containingBean) { // ... return new BeanDefinitionHolder(beanDefinition, beanName, aliasesArray); }注册解析的 BeanDefinition
在解析完配置文件后我们已经获取了 bean 的所有属性,接下来就是对 bean 的注册了
public class BeanDefinitionReaderUtils { public static void registerBeanDefinition( BeanDefinitionHolder definitionHolder, BeanDefinitionRegistry registry) throws BeanDefinitionStoreException { // 使用 beanName 做唯一标识符 String beanName = definitionHolder.getBeanName(); // 注册bean的核心代码 registry.registerBeanDefinition(beanName, definitionHolder.getBeanDefinition()); // 为bean注册所有的别名 String[] aliases = definitionHolder.getAliases(); if (aliases != null) { for (String alias : aliases) { registry.registerAlias(beanName, alias); } } } }
以上代码主要完成两个功能,一是使用 beanName 注册 beanDefinition,二是完成了对别名的注册
BeanName 注册 BeanDefinitionpublic class DefaultListableBeanFactory { @Override public void registerBeanDefinition(String beanName, BeanDefinition beanDefinition) throws BeanDefinitionStoreException { Assert.hasText(beanName, "Bean name must not be empty"); Assert.notNull(beanDefinition, "BeanDefinition must not be null"); if (beanDefinition instanceof AbstractBeanDefinition) { try { // 注册前的最后一次校验,这里的校验不同于XML文件校验 // 主要是对于AbstractBeanDefinition属性中的methodOverrides校验 // 校验methodOverrides是否与工厂方法并存或者methodOverrides对于的方法根本不存在 ((AbstractBeanDefinition) beanDefinition).validate(); } catch (BeanDefinitionValidationException ex) { throw new BeanDefinitionStoreException(beanDefinition.getResourceDescription(), beanName, "Validation of bean definition failed", ex); } } BeanDefinition oldBeanDefinition; // 获取缓存中的 beanDefinition oldBeanDefinition = this.beanDefinitionMap.get(beanName); if (oldBeanDefinition != null) { // 如果缓存中存在 判断是否允许覆盖 if (!isAllowBeanDefinitionOverriding()) { throw new BeanDefinitionStoreException(beanDefinition.getResourceDescription(), beanName, "Cannot register bean definition [" + beanDefinition + "] for bean "" + beanName + "": There is already [" + oldBeanDefinition + "] bound."); } else if (oldBeanDefinition.getRole() < beanDefinition.getRole()) { // e.g. was ROLE_APPLICATION, now overriding with ROLE_SUPPORT or ROLE_INFRASTRUCTURE if (this.logger.isWarnEnabled()) { this.logger.warn("Overriding user-defined bean definition for bean "" + beanName + "" with a framework-generated bean definition: replacing [" + oldBeanDefinition + "] with [" + beanDefinition + "]"); } } else if (!beanDefinition.equals(oldBeanDefinition)) { if (this.logger.isInfoEnabled()) { this.logger.info("Overriding bean definition for bean "" + beanName + "" with a different definition: replacing [" + oldBeanDefinition + "] with [" + beanDefinition + "]"); } } else { if (this.logger.isDebugEnabled()) { this.logger.debug("Overriding bean definition for bean "" + beanName + "" with an equivalent definition: replacing [" + oldBeanDefinition + "] with [" + beanDefinition + "]"); } } // 如果允许覆盖,保存beanDefinition到beanDefinitionMap中 this.beanDefinitionMap.put(beanName, beanDefinition); } else { // 判断是否已经开始创建bean if (hasBeanCreationStarted()) { // Cannot modify startup-time collection elements anymore (for stable iteration) synchronized (this.beanDefinitionMap) { // 保存beanDefinition到beanDefinitionMap中 this.beanDefinitionMap.put(beanName, beanDefinition); // 更新已经注册的beanName ListupdatedDefinitions = new ArrayList<>(this.beanDefinitionNames.size() + 1); updatedDefinitions.addAll(this.beanDefinitionNames); updatedDefinitions.add(beanName); this.beanDefinitionNames = updatedDefinitions; if (this.manualSingletonNames.contains(beanName)) { Set updatedSingletons = new LinkedHashSet<>(this.manualSingletonNames); updatedSingletons.remove(beanName); this.manualSingletonNames = updatedSingletons; } } } else { // 还没开始创建bean this.beanDefinitionMap.put(beanName, beanDefinition); this.beanDefinitionNames.add(beanName); this.manualSingletonNames.remove(beanName); } this.frozenBeanDefinitionNames = null; } if (oldBeanDefinition != null || containsSingleton(beanName)) { // 重置beanName对应的缓存 resetBeanDefinition(beanName); } } }
对 AbstractBeanDefinition 的校验,主要是针对 AbstractBeanDefinition 的 methodOverrides 属性的
对 beanName 已经注册的情况的处理,如果设置了不允许 bean 的覆盖,则需要抛出异常,否则直接覆盖
使用 beanName 作为 key,beanDefinition 为 Value 加入 beanDefinitionMap 存储
如果缓存中已经存在,并且该 bean 为单例模式则清楚 beanName 对应的缓存
注册别名注册好了 beanDefinition,接下来就是注册 alias。注册的 alias 和 beanName 的对应关系存放在了 aliasMap 中,沿着类的继承链会发现 registerAlias 的方法是在 SimpleAliasRegistry 中实现的
public class SimpleAliasRegistry { /** Map from alias to canonical name */ private final MapaliasMap = new ConcurrentHashMap<>(16); public void registerAlias(String name, String alias) { Assert.hasText(name, ""name" must not be empty"); Assert.hasText(alias, ""alias" must not be empty"); if (alias.equals(name)) { // 如果beanName与alias相同的话不记录alias 并删除对应的alias this.aliasMap.remove(alias); } else { String registeredName = this.aliasMap.get(alias); if (registeredName != null) { if (registeredName.equals(name)) { // 如果别名已经注册过并且指向的name和当前name相同 不做任何处理 return; } // 如果alias不允许被覆盖则抛出异常 if (!allowAliasOverriding()) { throw new IllegalStateException("Cannot register alias "" + alias + "" for name "" + name + "": It is already registered for name "" + registeredName + ""."); } } // 校验循环指向依赖 如A->B B->C C->A则出错 checkForAliasCircle(name, alias); this.aliasMap.put(alias, name); } } }
通过 checkForAliasCircle() 方法来检查 alias 循环依赖,当 A -> B 存在时,若再次出现 A -> C -> B 则会抛出异常:
protected void checkForAliasCircle(String name, String alias) { if (hasAlias(alias, name)) { throw new IllegalStateException("Cannot register alias "" + alias + "" for name "" + name + "": Circular reference - "" + name + "" is a direct or indirect alias for "" + alias + "" already"); } } public boolean hasAlias(String name, String alias) { for (Map.Entryentry : this.aliasMap.entrySet()) { String registeredName = entry.getValue(); if (registeredName.equals(name)) { String registeredAlias = entry.getKey(); return (registeredAlias.equals(alias) || hasAlias(registeredAlias, alias)); } } return false; }
至此,注册别名也完成了,主要完成了以下几个工作
如果 beanName 与 alias 相同的话不记录 alias 并删除对应的 alias
如果别名已经注册过并且指向的name和当前name相同 不做任何处理
如果别名已经注册过并且指向的name和当前name不相同 判断是否允许被覆盖
校验循环指向依赖 如A->B B->C C->A则出错
发送通知通知监听器解析及注册完成
//DefaultBeanDefinitionDocumentReader.java protected void processBeanDefinition(Element ele, BeanDefinitionParserDelegate delegate) { // Send registration event. getReaderContext().fireComponentRegistered(new BeanComponentDefinition(bdHolder)); }
通过 fireComponentRegistered 方法进行通知监听器解析及注册完成工作,这里的实现只为扩展,当程序开发人员需要对注册 BeanDefinition事件进行监听时,可以通过注册监听器的方式并将处理逻辑写入监听器中,目前 Spring 中并没有对此事件做任何处理
其中 ReaderContext 是在类 XmlBeanDefinitionReader 中调用 createReaderContext 生成的,然后调用 fireComponentRegistered()
alias 标签解析Spring 提供了
public class DefaultBeanDefinitionDocumentReader { protected void processAliasRegistration(Element ele) { // 获取 alisa 标签 name 属性 String name = ele.getAttribute(NAME_ATTRIBUTE); // 获取 alisa 标签 alias 属性 String alias = ele.getAttribute(ALIAS_ATTRIBUTE); boolean valid = true; if (!StringUtils.hasText(name)) { getReaderContext().error("Name must not be empty", ele); valid = false; } if (!StringUtils.hasText(alias)) { getReaderContext().error("Alias must not be empty", ele); valid = false; } if (valid) { try { // 进行别名注册 getReaderContext().getRegistry().registerAlias(name, alias); } catch (Exception ex) { getReaderContext().error("Failed to register alias "" + alias + "" for bean with name "" + name + """, ele, ex); } // 别名注册后告知监听器做相应处理 getReaderContext().fireAliasRegistered(name, alias, extractSource(ele)); } } }
首先对 alias 标签属性进行提取校验,校验通过后进行别名注册,别名注册和 bean 标签解析中的别名注册一直,此处不再赘述
import 标签解析public class DefaultBeanDefinitionDocumentReader { protected void importBeanDefinitionResource(Element ele) { // 获取import标签的resource属性 String location = ele.getAttribute(RESOURCE_ATTRIBUTE); // 如果不存在则不做任何处理 if (!StringUtils.hasText(location)) { getReaderContext().error("Resource location must not be empty", ele); return; } // 解析占位符属性 格式如"${user.dir}" location = getReaderContext().getEnvironment().resolveRequiredPlaceholders(location); SetactualResources = new LinkedHashSet<>(4); // 判断资源是绝对路径还是相对路径 boolean absoluteLocation = false; try { absoluteLocation = ResourcePatternUtils.isUrl(location) || ResourceUtils.toURI(location).isAbsolute(); } catch (URISyntaxException ex) { // cannot convert to an URI, considering the location relative // unless it is the well-known Spring prefix "classpath*:" } // 如果是绝对路径则直接根据地址加载对应的配置文件 if (absoluteLocation) { try { int importCount = getReaderContext().getReader().loadBeanDefinitions(location, actualResources); if (logger.isDebugEnabled()) { logger.debug("Imported " + importCount + " bean definitions from URL location [" + location + "]"); } } catch (BeanDefinitionStoreException ex) { getReaderContext().error("Failed to import bean definitions from URL location [" + location + "]", ele, ex); } } else { try { int importCount; // 根据相对路径加载资源 Resource relativeResource = getReaderContext().getResource().createRelative(location); if (relativeResource.exists()) { importCount = getReaderContext().getReader().loadBeanDefinitions(relativeResource); actualResources.add(relativeResource); } else { String baseLocation = getReaderContext().getResource().getURL().toString(); importCount = getReaderContext().getReader().loadBeanDefinitions(StringUtils.applyRelativePath(baseLocation, location), actualResources); } if (logger.isDebugEnabled()) { logger.debug("Imported " + importCount + " bean definitions from relative location [" + location + "]"); } } catch (IOException ex) { getReaderContext().error("Failed to resolve current resource location", ele, ex); } catch (BeanDefinitionStoreException ex) { getReaderContext().error("Failed to import bean definitions from relative location [" + location + "]", ele, ex); } } // 解析后进行监听器激活处理 Resource[] actResArray = actualResources.toArray(new Resource[actualResources.size()]); getReaderContext().fireImportProcessed(location, actResArray, extractSource(ele)); } }
完成了对 import 标签的处理,首先就是获取
熬过几个无人知晓的秋冬春夏,撑过去一切都会顺着你想要的方向走...
说点什么全文代码:https://gitee.com/battcn/battcn-spring-source/tree/master/Chapter1
个人QQ:1837307557
battcn开源群(适合新手):391619659
微信公众号:battcn(欢迎调戏)
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/68201.html
摘要:自定义标签在讲解自定义标签解析之前,先看下如何自定义标签定义文件定义一个文件描述组件内容声明命名空间值得注意的是与可以是不存在,只要映射到指定就行了。 Spring是一个开源的设计层面框架,解决了业务逻辑层和其他各层的松耦合问题,将面向接口的编程思想贯穿整个系统应用,同时它也是Java工作中必备技能之一... 前言 在 上一节 Spring解密 - 默认标签的解析 中,重点分析了...
摘要:解密是注册及加载的默认实现,整个模板中它可以称得上始祖。中是这样介绍的自动装配时忽略给定的依赖接口,比如通过其他方式解析上下文注册依赖,类似于通过进行的注入或者通过进行的注入。解析是资源文件读取解析注册的实现,要重点关注该类。 Spring是一个开源的设计层面框架,解决了业务逻辑层和其他各层的松耦合问题,将面向接口的编程思想贯穿整个系统应用,同时它也是Java工作中必备技能之一......
摘要:,这是标记配置文件集版本化的服务器端特性。要配置对称密钥,需要将设置为秘密字符串或使用环境变量将其排除在纯文本配置文件之外。 Spring Cloud Config Server Spring Cloud Config Server为外部配置提供基于HTTP资源的API(名称—值对或等效的YAML内容),通过使用@EnableConfigServer注解,服务器可嵌入Spring Bo...
摘要:判断调用哪个构造方法的过程会采用缓存机制,如果已经解析过则不需要重复解析而是从中的属性缓存的值去取,否则需再次解析。 Spring是一个开源的设计层面框架,解决了业务逻辑层和其他各层的松耦合问题,将面向接口的编程思想贯穿整个系统应用,同时它也是Java工作中必备技能之一... 前言 在 Spring解密 - XML解析 与 Bean注册 中,讲了 Bean的解析,本章将详细讲解Sp...
摘要:它们的优先级低于或以及作为创建应用程序过程的正常部分添加到子级的任何其他属性源。为引导配置类使用单独的包名称,并确保或注解的配置类尚未涵盖该名称。在这种情况下,它会在刷新时重建,并重新注入其依赖项,此时,它们将从刷新的重新初始化。 Spring Cloud Context:应用程序上下文服务 Spring Boot有一个关于如何使用Spring构建应用程序的主见,例如,它具有通用配置文...
阅读 2738·2023-04-25 18:46
阅读 644·2021-11-19 09:40
阅读 2021·2021-09-28 09:36
阅读 3272·2021-09-10 11:11
阅读 3415·2019-08-30 15:55
阅读 1753·2019-08-30 15:54
阅读 2551·2019-08-29 16:16
阅读 3478·2019-08-29 15:08