【问题标题】:What strategy do you use for package naming in Java projects and why? [closed]您在 Java 项目中使用什么策略来命名包,为什么? [关闭]
【发布时间】:2010-10-06 16:40:28
【问题描述】:

我不久前就想到了这个问题,最近我的商店正在开发它的第一个真正的 Java Web 应用程序时它又重新出现了。

作为介绍,我看到了两种主要的包命名策略。 (要清楚,我指的不是整个 'domain.company.project' 部分,我说的是下面的包约定。)无论如何,我看到的包命名约定如下:

  1. 功能性:根据架构上的功能而不是根据业务领域的身份来命名您的包。另一个术语可能是根据“层”命名。因此,您将拥有一个 *.ui 包、一个 *.domain 包和一个 *.orm 包。您的包裹是水平切片而不是垂直切片。

    很多比逻辑命名更常见。事实上,我相信我从未见过或听说过这样做的项目。这当然让我怀疑(有点像认为你已经想出了一个解决 NP 问题的方法),因为我不是很聪明,而且我认为每个人都必须有充分的理由这样做。另一方面,我并不反对人们只是想念房间里的大象并且我从来没有听到过真正的论点支持以这种方式命名包。这似乎是事实上的标准。

  2. 逻辑:根据业务域标识命名您的包,并将与该垂直功能切片有关的每个类放入该包中。

    正如我之前提到的,我从未见过或听说过这个,但这对我来说很有意义。

    1. 我倾向于垂直而不是水平地接近系统。我想进去开发订单处理系统,而不是数据访问层。显然,我很有可能会在该系统的开发中触及数据访问层,但关键是我不这么认为。当然,这意味着当我收到变更单或想要实现一些新功能时,不必为了找到所有相关的类而在一堆包中四处寻找会很好。相反,我只是查看 X 包,因为我所做的与 X 有关。

    2. 从开发的角度来看,我认为让你的包记录你的业务领域而不是你的架构是一个重大的胜利。我觉得域几乎总是系统中更难理解的部分,因为系统的架构,特别是在这一点上,在其实现中几乎变得平凡。事实上,我可以使用这种类型的命名约定并立即从包的命名中知道它处理订单、客户、企业、产品等的系统,这似乎非常方便。

    3. 似乎这样可以让您更好地利用 Java 的访问修饰符。这使您可以更清晰地将接口定义到子系统中,而不是系统的层中。因此,如果您有一个希望透明持久化的订单子系统,理论上您可以通过不必在 dao 层中为其持久性类创建公共接口,而是将 dao 类包装在只有它处理的类。显然,如果你想公开这个功能,你可以为它提供一个接口或者将它公开。通过将系统功能的垂直部分拆分到多个包中,您似乎失去了很多。

    4. 我想我可以看到的一个缺点是它确实使剥离图层变得更加困难。而不是仅仅删除或重命名一个包,然后用另一种技术将一个新的包放到适当的位置,您必须进入并更改所有包中的所有类。但是,我认为这没什么大不了的。这可能是由于缺乏经验,但我不得不想象,与您在系统中进入和编辑垂直特征切片的次数相比,您更换技术的次数相形见绌。

所以我猜你会问这个问题,你如何命名你的包,为什么?请理解,我不一定认为我在这里偶然发现了金鹅或其他东西。我对这一切都很陌生,主要是学术经验。但是,我无法发现我的推理中的漏洞,所以我希望你们都可以,以便我可以继续前进。

【问题讨论】:

  • 到目前为止我所听到的似乎表明这是一种判断电话。但是,大多数答案并没有集中在实际考虑上。这就是我想要的。不过,感谢您迄今为止的帮助!
  • 我不认为这是一个判断电话。首先按层划分,然后按功能划分绝对是要走的路。我正在更新我的答案。
  • @eljenso:告诉 Eclipse 人 :) 在 Eclipse 中,第一部分是“特性”,它是部署单元和功能单元。在“功能”内部,有“插件”,通常按层划分——但同一“功能”内的插件必须始终部署在一起。如果您只有一个部署单元,那么先按层划分,然后再按功能划分可能是有意义的。对于多个部署单元,您不想只部署表示层——您需要额外的功能。
  • 我有一个关于应用程序业务层的具体问题,命名约定应该是什么?

标签: java naming-conventions packages


【解决方案1】:

对于封装设计,我先按层划分,然后按其他功能划分。

还有一些额外的规则:

  1. 层从最一般(底部)到最具体(顶部)堆叠
  2. 每一层都有一个公共接口(抽象)
  3. 一层只能依赖另一层的公共接口(封装)
  4. 一个层只能依赖于更通用的层(从上到下的依赖关系)
  5. 一个层最好依赖于它正下方的层

因此,例如,对于 Web 应用程序,您的应用程序层中可能有以下层(从上到下):

  • 表示层:生成将在客户端层中显示的 UI
  • 应用层:包含特定于应用的逻辑,有状态
  • 服务层:按域对功能进行分组,无状态
  • 集成层:提供对后端层的访问(db、jms、email、...)

对于生成的包布局,这些是一些附加规则:

  • 每个包名的根是<prefix.company>.<appname>.<layer>
  • 层的接口进一步按功能划分:<root>.<logic>
  • 层的私有实现以私有为前缀:<root>.private

这是一个示例布局。

表示层按视图技术划分,并且可选地按(组)应用程序划分。

com.company.appname.presentation.internal
com.company.appname.presentation.springmvc.product
com.company.appname.presentation.servlet
...

应用层分为用例。

com.company.appname.application.lookupproduct
com.company.appname.application.internal.lookupproduct
com.company.appname.application.editclient
com.company.appname.application.internal.editclient
...

服务层分为业务领域,受后端层领域逻辑的影响。

com.company.appname.service.clientservice
com.company.appname.service.internal.jmsclientservice
com.company.appname.service.internal.xmlclientservice
com.company.appname.service.productservice
...

集成层分为“技术”和访问对象。

com.company.appname.integration.jmsgateway
com.company.appname.integration.internal.mqjmsgateway
com.company.appname.integration.productdao
com.company.appname.integration.internal.dbproductdao
com.company.appname.integration.internal.mockproductdao
...

像这样分离包的优点是更容易管理复杂性,并提高了可测试性和可重用性。虽然这看起来开销很大,但根据我的经验,它实际上是非常自然的,并且在这种结构(或类似结构)上工作的每个人都可以在几天内完成它。

为什么我认为垂直方法不太好?

在分层模型中,几个不同的高层模块可以使用同一个低层模块。例如:可以为同一个应用构建多个视图,多个应用可以使用同一个服务,多个服务可以使用同一个网关。这里的诀窍是,当通过层移动时,功能级别会发生变化。更具体层中的模块不会在更通用层中的模块上映射 1-1,因为它们表达的功能级别不会映射 1-1。

当您使用垂直方法进行包装设计时,即首先按功能划分,然后将具有不同功能级别的所有构建块强制放入相同的“功能外套”中。您可以为更具体的模块设计通用模块。但这违反了更一般的层不应该知道更具体的层的重要原则。例如,服务层不应该按照应用层的概念建模。

【讨论】:

  • “像这样分离包的优点是更容易管理复杂性,并且提高了可测试性和可重用性。”你真的不去探究为什么会这样。
  • 你不会去探究它不是的原因。但无论如何......组织规则相当清晰明了,因此它降低了复杂性。由于组织本身,它更具可测试性和可重用性。每个人都可以尝试一下,然后我们可以同意或不同意。我简要解释了垂直方法的一些缺点,隐含地给出了我认为水平方法更容易的原因。
  • This article 很好地解释了为什么使用 package-by-feature 而不是 package-by-layer 更好。
  • @eljenso “你不去探究为什么不是这样的原因”,试图转移举证责任?我认为 Tom 发布的文章很好地解释了为什么按功能打包更好,我很难接受“我不认为这是一个判断电话。首先按层划分,然后按功能划分绝对是方式去”认真。
【解决方案2】:

我发现自己坚持使用鲍勃叔叔的package design principles。简而言之,要一起重用和一起更改的类(出于相同的原因,例如依赖项更改或框架更改)应该放在同一个包中。 IMO,在大多数应用程序中,功能分解比垂直/业务特定分解更有可能实现这些目标。

例如,领域对象的水平切片可以被不同类型的前端甚至应用程序重用,而当底层 Web 框架需要更改时,Web 前端的水平切片可能会一起更改。另一方面,如果将跨不同功能区域的类分组在这些包中,则很容易想象这些更改在许多包中的连锁反应。

显然,并非所有类型的软件都是相同的,并且在某些项目中垂直分解可能是有意义的(就实现可重用性和可更改性的目标而言)。

【讨论】:

  • 谢谢,这很清楚。正如我在问题中所说,我觉得您更换技术的次数远远少于您围绕垂直功能切片的次数。这不是你的经历吗?
  • 这不仅仅是关于技术。仅当垂直切片是通过某些接口(SOA,如果您愿意)相互通信的独立应用程序/服务时,您原始帖子的第 1 点才有意义。更多如下...
  • 现在,当您进入每个具有自己的 gui/业务/数据的细粒度应用程序/服务的细节时,我几乎无法想象垂直切片的变化,无论是关于技术、依赖、规则/工作流、日志、安全、UI风格,可以与其他切片完全隔离。
  • 我很想知道您对my answer的反馈
  • @BuuNguyen 包装设计原则的链接已损坏。
【解决方案3】:

通常存在两个级别的划分。从顶部开始,有部署单元。这些被命名为“逻辑上”(用你的话说,想想 Eclipse 特性)。在部署单元内部,您可以对包进行功能划分(想想 Eclipse 插件)。

例如,特征是com.feature,它由com.feature.clientcom.feature.corecom.feature.ui插件组成。在插件内部,我对其他包的划分很少,虽然这也很正常。

更新:顺便说一句,Juergen Hoeller 在 InfoQ 上有关于代码组织的精彩演讲:http://www.infoq.com/presentations/code-organization-large-projects。 Juergen 是 Spring 的架构师之一,对这些东西非常了解。

【讨论】:

  • 我不太明白。通常您可能会看到 com.apache.wicket.x 其中 x 是函数式或逻辑式。我通常看不到 com.x。我猜你是说你会使用 com.company.project.feature.layer 结构?你有理由吗?
  • 原因是“com.company.project.feature”是部署单元。在此级别,某些功能是可选的,可以跳过(即不部署)。但是,在功能内部,事情不是可选的,您通常都想要它们。在这里,按层划分更有意义。
【解决方案4】:

我参与过的大多数 java 项目首先对 java 包进行功能切片,然后再进行逻辑切片。

通常部分足够大,以至于它们被分解为单独的构建工件,您可以将核心功能放入一个 jar,将 apis 放入另一个 jar,将 Web 前端内容放入 warfile 等。

【讨论】:

  • 那会是什么样子? domain.company.project.function.logicalSlice?
  • 差不多!例如d.c.p.web.profiles、d.c.p.web.registration、d.c.p.apis、d.c.p.persistence 等通常效果很好,您的里程可能会有所不同。还取决于您是否使用域建模 - 如果您有多个域,您可能希望先按域拆分。
  • 我个人更喜欢相反的逻辑,然后是功能。 apples.rpc,apples.model,然后是banana.model,banana.store
  • 将所有苹果的东西组合在一起比把所有的网络东西组合在一起更有价值。
【解决方案5】:

包将作为一个单元进行编译和分发。在考虑哪些类属于一个包时,关键标准之一是它的依赖关系。此类依赖于哪些其他包(包括第三方库)。一个组织良好的系统会将具有相似依赖关系的类聚集在一个包中。这限制了对一个库进行更改的影响,因为只有少数定义明确的包会依赖它。

听起来你的逻辑垂直系统可能倾向于“涂抹”大多数包之间的依赖关系。也就是说,如果每个功能都打包为一个垂直切片,那么每个包都将依赖于您使用的每个第三方库。对库的任何更改都可能波及整个系统。

【讨论】:

  • 啊。所以做水平切片的一件事是保护你免受你正在使用的库的实际升级。我认为你是对的,垂直切片涂抹了你的系统在每个包中的每个依赖项。为什么会有这么大的问题?
  • 对于一个“功能”来说,这没什么大不了的。无论如何,它们往往更加不稳定并且具有更多的依赖关系。另一方面,这种“客户端”代码的可重用性往往很低。对于您打算成为可重用库的东西,将客户与每一个小变化隔离开来是一项巨大的投资。
【解决方案6】:

我个人更喜欢对类进行逻辑分组,然后在其中包含每个功能参与的子包。

包装目标

包毕竟是将事物组合在一起 - 相关类彼此靠近的想法。如果他们住在同一个包中,他们可以利用包私有来限制可见性。问题是将所有视图和持久性内容集中到一个包中可能会导致许多类混入一个包中。因此,下一个明智的做法是创建视图、持久性、util 子包并相应地重构类。不幸的是,受保护的包私有范围不支持当前包和子包的概念,因为这将有助于执行此类可见性规则。

我现在通过功能看到了分离的价值,因为将所有与视图相关的东西分组有什么价值。此命名策略中的事物与视图中的某些类断开连接,而另一些则处于持久性等等。

我的逻辑包装结构示例

为了便于说明,让我们命名两个模块 - 将模块名称用作将类分组到包树的特定分支下的概念。

苹果模型 苹果商店 香蕉模型 香蕉商店

优势

使用 Banana.store.BananaStore 的客户端只暴露于我们希望提供的功能。 hibernate 版本是一个实现细节,他们不需要知道也不应该看到这些类,因为它们给存储操作增加了混乱。

其他逻辑与功能优势

越靠近根,范围就越广,属于一个包的东西开始表现出越来越多的依赖于属于她模块的东西。例如,如果要检查“香蕉”模块,大多数依赖项将仅限于该模块内。事实上,“香蕉”下的大多数助手根本不会在这个包范围之外被引用。

为什么是功能?

根据功能将事物归为一类可以实现什么价值。在这种情况下,大多数类相互独立,很少或不需要利用包私有方法或类。将它们重构到自己的子包中收益不大,但确实有助于减少混乱。

开发者对系统的改动

当开发人员的任务是进行一些微不足道的更改时,如果他们的更改可能包含来自包树所有区域的文件,这似乎很愚蠢。使用逻辑结构化方法,它们的更改在包树的同一部分中更加本地化,​​这似乎是正确的。

【讨论】:

  • “当开发人员 [...] 来自包树的所有区域的文件时。”当你说这看起来很愚蠢时,你是对的。因为这正是正确分层的重点:更改不会波及应用程序的整个结构。
  • 感谢您的建议。我不确定我是否清楚地理解你。我相信你是在为逻辑包争论,我不太清楚为什么。你能试着让你的答案更清楚一点,可能会改写吗?谢谢!
【解决方案7】:

功能(架构)和逻辑(功能)包装方法都有一席之地。许多示例应用程序(那些在教科书等中找到的应用程序)遵循将表示、业务服务、数据映射和其他架构层放入单独的包中的功能方法。在示例应用程序中,每个包通常只有几个或只有一个类。

这种初始方法很好,因为一个人为的示例通常用于:1) 从概念上绘制出所呈现框架的架构,2) 这样做有一个单一的逻辑目的(例如添加/删除/更新/删除宠物诊所)。问题是许多读者将此视为没有界限的标准。

随着“业务”应用程序扩展到包含越来越多的功能,遵循功能 方法成为一种负担。虽然我知道在哪里寻找基于架构层的类型(例如“web”或“ui”包下的 web 控制器等),但开发单个 logical 功能开始需要在许多包之间来回跳转。至少,这很麻烦,但比这更糟。

由于逻辑上相关的类型没有打包在一起,API过于公开;逻辑相关类型之间的交互被强制为“公共”,以便类型可以导入并相互交互(失去了最小化默认/包可见性的能力)。

如果我正在构建一个框架库,我的包无论如何都将遵循功能/架构打包方法。我的 API 消费者甚至可能会欣赏他们的导入语句包含以架构命名的直观包。

相反,在构建业务应用程序时,我会按功能打包。我可以将 Widget、WidgetService 和 WidgetController 全部放在同一个“com.myorg.widget.”包中,然后利用默认可见性(并且导入语句和包间的数量更少)依赖项)。

但是,也有交叉的情况。如果我的 WidgetService 被许多逻辑域(功能)使用,我可能会创建一个“com.myorg.common.service.”包。还有一个很好的机会是,我创建了旨在跨功能重用的类,并最终得到诸如“com.myorg.common.ui.helpers.”和“”之类的包com.myorg.common.util.”。我什至可能最终将所有这些后来的“通用”类移动到一个单独的项目中,并将它们作为 myorg-commons.jar 依赖项包含在我的业务应用程序中。

【讨论】:

    【解决方案8】:

    这取决于您的逻辑流程的粒度?

    如果它们是独立的,您通常会在源代码控制中为它们创建一个新项目,而不是一个新包。

    我目前正在进行的项目错误地倾向于逻辑拆分,有一个用于 jython 方面的包,一个用于规则引擎的包,一个用于 foo、bar、binglewozzle 等的包。我正在寻找该包中每个模块的 XML 特定解析器/编写器,而不是有一个 XML 包(我以前做过),尽管仍然会有一个核心 XML 包用于共享逻辑。然而,这样做的一个原因是它可能是可扩展的(插件),因此每个插件还需要定义其 XML(或数据库等)代码,因此集中这可能会在以后引入问题。

    最后,这似乎是对特定项目最明智的方式。但是,我认为按照典型项目分层图的方式进行打包很容易。您最终会得到合乎逻辑和功能性的包装。

    需要的是带标签的命名空间。一些 Jython 功能的 XML 解析器可以同时标记 Jython 和 XML,而不必选择其中之一。

    也许我在颤抖。

    【讨论】:

    • 我不完全理解你的意思。我认为你的意思是你应该只做对你的项目最有意义的事情,对你来说这是合乎逻辑的,并加入了一些功能。这样做有具体的实际原因吗?
    • 正如我所说,我将使用插件来增强内置功能。插件必须能够解析和写入其 XML(并最终写入 DB)并提供功能。因此,我有 com.xx.feature.xml 还是 com.xx.xml.feature?前者看起来更整洁。
    【解决方案9】:

    我尝试以这样一种方式设计包结构,即如果我要绘制依赖关系图,它会很容易遵循并使用一致的模式,尽可能少地循环引用。

    对我来说,这在垂直命名系统中比在水平命名系统中更容易维护和可视化。如果 component1.display 有对 component2.dataaccess 的引用,这会比 display.component1 有对 dataaccess 的引用发出更多的警告。组件2。

    当然,双方共享的组件放在各自的包中。

    【讨论】:

    • 据我了解,您会提倡垂直命名约定。当您需要跨多个切片的类时,我认为这就是 *.utils 包的用途。
    【解决方案10】:

    我完全遵循并提出逻辑(“按功能”)组织!一个包应该尽可能地遵循“模块”的概念。职能组织可能会将一个模块分散到一个项目中,从而导致封装较少,并且易于更改实现细节。

    我们以 Eclipse 插件为例:将所有视图或操作放在一个包中会一团糟。相反,一个特性的每个组件都应该进入特性的包,或者如果有很多,则进入子包(featureA.handlers、featureA.preferences 等)

    当然,问题在于分层包系统(其中包括 Java 有),这使得正交关注点的处理变得不可能或至少非常困难 - 尽管它们无处不在!

    【讨论】:

      【解决方案11】:

      我个人会选择功能命名。简短的原因:它避免了代码重复或依赖噩梦。

      让我详细说明一下。当您使用带有自己的包树的外部 jar 文件时会发生什么?您正在有效地将(编译的)代码导入到您的项目中,并带有一个(功能分离的)包树。同时使用这两种命名约定是否有意义?不,除非那是对你隐藏的。如果你的项目足够小并且只有一个组件,它就是这样。但是如果你有几个逻辑单元,你可能不想重新实现,比如说数据文件加载模块。您希望在逻辑单元之间共享它,在逻辑上不相关的单元之间不存在人为依赖关系,并且不必选择要将特定共享工具放入哪个单元。

      我想这就是为什么函数式命名最常用于达到或打算达到一定规模的项目中,并且在类命名约定中使用逻辑命名来跟踪特定角色(如果有的话)包中的类。

      我将尝试更准确地回应您关于逻辑命名的每一点。

      1. 如果您在计划更改时必须在旧类中钓鱼以修改功能,这是抽象不良的标志:您应该构建提供明确定义的功能的类,可以用一句话来定义。只有少数顶级课程应该组合所有这些以反映您的商业智能。这样,您将能够重用更多代码、更容易维护、更清晰的文档和更少的依赖问题。

      2. 这主要取决于您了解项目的方式。当然,逻辑和功能视图是正交的。因此,如果您使用一种命名约定,则需要将另一种应用于类名以保持某种顺序,或者在某个深度从一种命名约定分叉到另一种。

      3. 访问修饰符是允许其他了解您的处理的类访问您的类的内部的好方法。逻辑关系并不意味着理解算法或并发约束。功能可能,虽然它没有。我非常厌倦 public 和 private 之外的访问修饰符,因为它们经常隐藏缺乏适当的架构和类抽象。

      4. 在大型商业项目中,技术变化发生的频率超出您的想象。例如,我已经更改了 3 次 XML 解析器、2 次缓存技术和 2 次地理定位软件。还好我把所有的细节都隐藏在了一个专门的包里……

      【讨论】:

      • 如果我错了,请原谅我,但在我看来,您更像是在谈论开发一个供许多人使用的框架。我认为这种类型的开发和开发最终用户系统之间的规则会发生变化。我错了吗?
      • 我认为对于许多垂直切片使用的通用类,拥有类似utils 包的东西是有意义的。然后,任何需要它的类都可以简单地依赖于该包。
      • 再一次,规模很重要:当一个项目变得足够大时,它需要几个人的支持,并且会出现某种专业化。为了简化交互并防止错误,将项目分成几部分很快就变得很有必要了。而且 utils 包很快就会变得庞大!
      【解决方案12】:

      完全不使用包(根包除外)是一个有趣的实验。

      然后出现的问题是,何时以及为什么引入软件包是有意义的。据推测,答案与您在项目开始时的答案不同。

      我认为您的问题完全出现了,因为包就像类别,有时很难决定其中一个。有时标签会更适合传达一个类在许多上下文中可用的信息。

      【讨论】:

        【解决方案13】:

        从纯粹实用的角度来看,java 的可见性构造允许同一包中的类访问具有 protecteddefault 可见性以及 public 可见性的方法和属性。从完全不同的代码层使用非公共方法肯定会产生很大的代码气味。所以我倾向于把同一层的类放到同一个包中。

        我不经常在其他地方使用这些受保护或默认方法 - 可能在类的单元测试中除外 - 但当我这样做时,它总是来自同一层的一个类

        【讨论】:

        • 总是依赖于下一层难道不是多层系统的本质吗?例如,UI 依赖于你的服务层,而服务层又依赖于你的领域层等。将垂直切片打包在一起似乎可以屏蔽过度的包间依赖关系,不是吗?
        • 我几乎从不使用受保护的和默认的方法。 99% 是公共的或私人的。一些例外情况:(1) 仅由单元测试使用的方法的默认可见性,(2) 仅用于抽象基类的受保护抽象方法。
        • Tim Visher,包之间的依赖关系不是问题,只要依赖关系总是指向同一个方向并且依赖关系图中没有循环。
        【解决方案14】:

        这取决于。在我的工作中,我们有时会按功能(数据访问、分析)或资产类别(信贷、股票、利率)来拆分包。只需选择对您的团队最方便的结构即可。

        【讨论】:

        • 有什么理由选择任何一种方式吗?
        • 按资产类别(更一般地:按业务领域)拆分使新人更容易通过代码找到自己的方式。按功能拆分有利于封装。对我来说,Java 中的“包”访问类似于 C++ 中的“朋友”。
        • @quant_dev 我不同意“..by 函数有利于封装”。我认为你的意思完全相反。另外,不要只选择“方便”。每个都有一个案例(见我的回答)。
        【解决方案15】:

        根据我的经验,可重用性带来的问题多于解决问题。使用最新且便宜的处理器和内存,我更喜欢重复代码而不是紧密集成以重用。

        【讨论】:

        • 代码重用不是硬件价格,而是代码维护成本。
        • 同意,但是在一个简单的模块中修复某些东西不应该影响整个代码。你说的维护成本是多少?
        • 模块中的错误修复应该会影响整个应用程序。为什么要多次修复同一个错误?
        • 在编写代码多年后,您是否必须维护您的代码?我诅咒那些在我之前对计算机编程采取如此懒散的方法的人。由于重复而花时间多次修复代码既低效又烦人。在编写代码 18 年后,我不得不编写自己的代码,而我犯的每一个 ameturish 错误都让我的生活变得更加艰难。冗余是我最终付出的那些可怕的错误之一。
        猜你喜欢
        • 2010-09-22
        • 2010-11-29
        • 2018-09-30
        • 2010-09-30
        • 2011-12-21
        • 2023-03-23
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多