随笔 - 181, 文章 - 2, 评论 - 85, 引用 - 0
数据加载中……

6月19日----- 通过服务模拟来简化 SOA 开发

 

 

通过用例和模拟对象简化 SOA 开发——特别在您的项目涉及多个团队时——并提高 SOA 应用程序质量。

借助于可重用服务和需要很少的新代码的应用程序(因为可以依赖这些可重用服务),面向服务的体系结构 (SOA) 可以大幅度提高应用程序开发的速度。但是 SOA 还可能大大增加应用程序开发的复杂性,因为团队需要同时进行应用程序的不同部分的工作,而且要在最后成功地将各个部分组合起来。本文将探讨致使 SOA 开发变得困难的原因,并提供了一个可以对其进行简化的流程。各种组织可以使用此流程来极大地增加其 SOA 成功的几率。

SOA 开发问题

使用 SOA 开发应用程序可提供更多的应用程序部署选项,但也使得开发工作变得越发困难。这是因为 SOA 将应用程序开发拆分为两个截然不同的部分:

  • SOA 服务提供程序(SOA Service Provider,SOA-SP) ——该层的代码实现服务。它具有服务 API,以对服务进行声明和为客户机提供调用服务的方法。
  • SOA 服务协调程序(SOA Service Coordinator,SOA-SC)——该层的代码通过一个或多个 SOA-SP 中的服务提供用户功能。它可能具有 UI 或 GUI,以便同传统应用程序一样与 SOA-SC 进行交互。

例如,某个金融应用程序的 SOA 可能如图 1 中所示。


图 1:个人金融应用程序和服务
个人金融应用程序和服务

通常由独立的团队分别负责同时开发这两个独立的部分。一个团队负责开发 SOA-SC——对用户而言的应用程序。另一个团队负责开发 SOA-SP,或许存在多个负责此项任务的团队,而每个团队负责开发若干服务。虽然可能已经实现了一些提供程序,但可能仍然需要专门为此应用程序实现其他的提供程序。

而这给我们提出了第一个问题:如果某些提供程序尚未实现,协调程序开发团队如何开发其负责的应用程序部分呢?

这两个团队——开发服务的团队和开发协调程序的团队——需要使其活动同步。他们必须就服务 API 达成一致;服务 API 可以是简单的 Java™ 接口或 Java Message Service (JMS) 消息格式和队列名称,但必须就此达成一致。但仅就接口达成一致是不够的;服务具有行为,因此团队必须就服务的行为达成一致。服务并不会始终成功地工作,因此团队还必须就错误情况和相应的响应达成一致。

在理想的情况下,多个团队可以非常容易地协调工作,最初的决策可以稍后进行修改,而所造成的影响却非常小,并且评估工作非常灵活,此外还会不断地进行改进。在现实世界中,团队之间经常存在协作问题,往往较早地(甚至不成熟地)做出不可更改的决策,而且评估方法是固定的。

这样就带来了第二个问题:协调程序和提供程序团队如何较早而可靠地就服务如何工作达成一致?

经常通过多个提供程序来实现服务冗余。这样就可以在提供程序之间提供负载平衡和故障转移功能,从而使得协调程序的服务体验更为一致和可靠。冗余可以通过多次部署同一服务实现来实现。不过,当不同的供应商部署了不同的提供程序时,服务几乎肯定具有不同的实现。尽管如此,对于协调程序,所有提供程序的全部实现的工作方式都必须一致,以便协调程序可以采用可交换的方式来调用提供程序。服务的不同实现(特别是来自不同供应商的不同实现)是由不同的团队开发的,而这些团队必须加以协调,以确保开发的是相同的服务。

因此,第三个问题就是:多个提供程序团队如何实现相同的服务,以确保他们的实现具有兼容性?

以上是使用 SOA 进行开发的主要问题。必须实现尚未开发的服务来开发协调程序,协调程序团队和提供程序团队必须就服务如何工作达成一致,而且,多个提供程序团队也必须就服务如何工作达成一致。如果没有良好的流程,这就会导致一片混乱,如果不对其进行检查,将会导致 SOA 项目失败。




 
 


SOA 开发流程

下面给出用于对协调程序和提供程序团队进行同步的简单流程,以帮助确保可重用服务以及使用这些服务的应用程序能够成功地进行开发:

  1. 使用服务用例描述服务。
  2. 开发体现服务用例的服务测试。
  3. 开发通过服务测试的服务模拟。
  4. 提供程序团队采用服务模拟作为原型,并将服务测试作为要求,从而实现服务。
  5. 协调程序团队将服务测试作为服务使用者可以进行的工作的示例,并在实际的服务仍然处于开发过程的同时使用服务模拟对其代码进行测试。

这个简单的流程处理了以下问题:如何确定服务的范围以及如何保持团队的一致性和高效率,从而避免发生意外。公正地说,还有许多其他问题仍然没有通过此流程得到解决。该流程并没有涉及服务自身如何开发或协调应用程序如何开发的问题。它并不涉及服务的质量问题(即服务的可靠性问题),而是只定义服务如何提供必要的行为。该流程总体上也不处理传统独立应用程序如何使用 SOA 重新进行体系结构设计,以及如何发现或设计服务。所有这些问题都是必要的,但其并不在此流程的范围之内。

此流程使协调应用程序和服务实现协同工作,并允许团队以相当独立的方式同时对这两个部分进行开发。这并非 SOA 项目所需的全部内容,但却是一个不错的起点。

为了说明此流程,我将讨论如何实现一个简单服务。它就是大家都喜欢用的服务示例,即股票报价服务。为了让内容更丰富一些,我提供了以下三种类型的信息:

  1. 简单的当前价格报价
  2. 包含当前价格、当日最高价和最低价以及当天交易量的复杂报价
  3. 包含过去某天的复杂报价的历史报价

此示例应该足以阐释该流程的工作过程。

服务用例

此 SOA 开发流程中的第一步是开发描述服务的服务用例。

Alistair Cockburn 将用例 定义为“系统涉众之间有关系统的行为的协定”(请参阅参考资料部分列出的 Writing Effective Use Cases)。用例必须适合所定义的系统范围,能够代表此情况下使用系统的主要参与者的观点,且能够在一致的抽象层次上表示参与者的系统使用情况。

Alistair 给出的一个例子是网上的股票购买服务,其中,购买者使用与股票代理的网站协同工作的个人金融应用程序来购买股票。系统范围既包含金融应用程序,又包含代理的网站。购买者是主要的参与者。抽象级别为应用程序和网站之间的交互,而不是应用程序或网站内的详细信息。用例将描述主要成功方案(购买者根据这些方案购买股票)和一些可能出现错误的扩展。

因此,用例是关于以下内容的文本描述:希望系统如何工作、将涉及到哪些人以及他们之间如何交互、系统在正常运行时如何工作,以及出现错误时应该如何处理。它关心的是系统将做什么,而不考虑将如何 实现。

现在,假定所讨论的不是系统或应用程序,而是一个 SOA 服务。用例技术仍然适用。可以采用此技术来描述服务使用者与服务提供程序如何进行交互,说明服务做什么 而不用描述其如何 实现。服务用例 的最初草稿应将重点放在服务的行为上。由于这是必须调用的服务,因此后面的用例草稿还应该指定调用协议——将用于调用服务的技术、传输和数据格式。(用例纯粹主义者甚至可能说协议不属于用例的实现细节,他们是对的。但服务用例不仅描述服务,而且还要描述如何调用该服务,因此协议是使用者和提供程序参与者之间的协定的一部分,必须在某个地方加以指定。)

因此,开发用例的第一步是对服务完成的操作进行充分的描述。此描述代表了使用者对提供程序必须提供的行为的要求,主要由协调程序开发团队创建,但同样以提供程序开发团队提供的输出为基础。这两种类型的开发团队必须对用例满意才行,因为这些用例是对所有团队开发其负责的应用程序部分的要求。

服务不仅要在条件良好的情况下正常工作,而且还要能够恰当地处理出现错误的情况,这非常重要。因此,您的服务用例应该对错误情况和服务无法成功处理的错误输入加以处理。其中很多错误路径最终都表现为用例的备用路径。其他错误场景也可能非常极端,因而需要各自的错误用例。在这两种方法中,用例都必须记录服务如何像成功路径一样全面地处理错误。

示例用例

例如,让我们看看股票报价示例的服务用例。它需要做三件事,因此需要以下三个服务用例:

  1. 简单报价:使用者传入股票代码;提供程序返回指定的股票的当前价格。
  2. 复杂报价:使用者传入股票代码;提供程序返回指定股票的当前价格,当前的最高价格、最低价格以及交易量。
  3. 历史报价:使用者传入股票代码和日期;提供程序返回指定股票和日期的复杂报价。

即便对于这样的简单示例,仍然需要确定很多问题并将其添加到用例中,如下所示:

  • 如果股票代码无效,或者提供程序所属的交易所不支持该股票,该如何处理?
  • 应该为价格使用何种格式?浮点数可能存在舍入误差。小数更为准确,但不标准。字符串效率较低,但更为明确。
  • 应该为复杂报价使用何种格式?逗号分隔值?数组?对象?XML 文档?SOAP 响应?
  • 如果所请求的日期是当日或将来的时间,该如何处理?如果日期是过去的某个市场不开放的日期,该如何处理?对于交易所尚未开始进行股票交易的日期该如何处理?如果日期过早,而不存在相关记录了,该如何处理?如果股票代码或交易从那以后发生了变化,又该如何处理?

即使开发本文中的简单用例也不简单。用例非常麻烦,必须考虑周全才能圆满地完成开发工作。此时的细心工作是非常不错的一项投资;利用好的服务用例可以开发良好的服务测试和服务模拟,从而帮助开发团队正常进行开发工作。




 
 


服务测试

此 SOA 开发流程的第二步是开发服务测试,以将用例系统编写为可执行格式。仅当服务恰当地实现了用例时,测试才能通过。

Kent Beck 指出,测试 应该是自动而独立的,而且应该对可能出现问题的部分进行检查(请参阅参考资料部分,以获得有关他的书籍 Extreme Programming ExplainedTest Driven Development 的信息)。测试——通过测试开发工作软件——是 Beck 称为极限编程 (XP) 的方法所包含的十二项实践之一。它是测试驱动的开发 (TDD) 的核心——如果您只能遵循一个实践,该如何执行 XP 呢?当采用 XP 和 TDD 时,将首先开发测试,然后开发软件以通过测试,接着重复这些步骤,直到软件足够完善为止。

应该测试什么?测试的概念源于许多地方,但用例是测试的最佳来源。用例文本和关系图描述用户对需求的理解。测试以更明确的方式表述这种理解,并以可靠和重复执行的代码加以表示。用例和测试是面向不同的受众(人和计算机)以不同形式表示相同内容的对等项。

服务用例的服务测试 没有什么不同,不过更多地将其看作功能测试,而不是单元测试。服务测试不会验证服务如何实现;提供程序开发团队可以自行实现此用途的单元测试。服务测试验证服务是否提供了服务用例认为其应该提供的行为。这些测试还需要对错误路径进行测试。

测试将最终定义服务的期望接口。此接口通常为 Web 服务的 Web 服务描述语言(Web Services Description Language,WSDL)文件、Java 接口或 Java 组件的 EJB 远程接口等等。如果首先开发接口,然后根据接口实现测试,可能会看起来更简单,不过更直接的方法是首先开发测试,然后开发接口,以使测试能够成功编译。

示例服务测试

可以使用简单的单元测试框架(JUnit 或 Cactus)来开发测试。该框架将充当服务的使用者,并进行使用者将进行的操作。下面是一些可能的测试:

  • 使用 IBM 调用 simple quote ,以验证获得的结果是“$100.00”。
  • 使用 MSFT 调用 simple quote,以验证获得的结果是“$30.00”。
  • 使用 BOGUS 调用 simple quote,以验证获得的结果是 invalid stock symbol 错误。

对复杂报价和历史报价的测试将与此类似。另外,还有针对可能的基础结构错误的测试,如远程异常和 HTTP 400 错误。最后,测试应该对服务用例中指定的所有内容进行验证;如果在用例中指定了操作,但却不在一个或多个测试中进行检查,则意味着使用者不能期望提供程序将实际执行该操作。




 
 


服务模拟

此 SOA 开发流程的第三步是开发服务模拟——通过服务测试的模拟对象。这些服务模拟是实际服务提供程序的简单原型。

Kent Beck 将模拟对象 描述为测试对象,该对象可以使用常量进行响应,从而实现开销大或复杂的资源的模仿版本。例如,模拟数据库是一个简单对象,但具有数据库的 API,可以接受一些已知的 SQL 字符串,并为每个字符串返回一组固定的结果。模拟对象允许您对组件进行测试,而不必依赖于外部资源。

现在假定此外部资源是一个 SOA 服务。如果您的组件使用该服务,则测试此组件时也在测试该服务。如果服务工作不正常,或者不可用,则即使组件工作正常,测试也会失败。如果服务很慢(通过网络远程调用服务时就是这样),您的测试也会运行得很慢——这样就不能如您所愿频繁地运行测试了。而且,如果服务尚未实现,则根本就不能对您的组件进行测试。

因此,一个不错的方法就是开发服务模拟,模拟对象是实际服务的简单仿真程序。服务具有与实际服务相同的 API;它会实现针对服务测试而开发的接口。服务模拟应该如何工作?它应该通过您已经开发的服务测试,这表明模拟真的和实际服务的工作方式一样。

在某些情况下,服务模拟实际上比实际服务更适合用于进行测试工作。假定您的组件使用返回股票报价的服务。如果传入代码 IBM,您将获得什么样的结果呢?$50?$100?$150?具体取决于当前的股票价格,但这是测试的一个“鸡与蛋”问题。通过使用服务模拟,已硬编码的模拟将始终返回 $100,然后据此进行测试,与测试实际服务相比,这实际上更加可靠。

谁开发服务模拟?提供程序团队(而非协调程序团队)应该开发服务模拟。服务模拟表示提供程序团队计划实际实现的内容的简单实现。如果相同的服务有多个提供程序团队,则他们必须进行协调,以产生一个他们都认可的模拟服务。

示例服务模拟

此示例服务模拟需要通过我前面编写的示例服务测试。因此,它的简单报价实现是一个 case 语句。如果服务只是一个传统 Java 对象(plain old Java object,POJO),则对应的模拟将为通用接口的特殊实现,如下所示:


清单 1:作为通用接口的特殊实现的服务模拟
																				
																						public class StockQuoteMock implements StockQuoteService

																				
																		

简单报价则将为 StockQuoteService 中声明的一个方法,并在 StockQuoteMock 实现如下方法:


清单 2:在 StockQuoteService 中声明并在 StockQuoteMock 中实现的简单报价方法
																				
																						public String getSimpleQuote(String symbol) throws InvalidSymbolException {
	if (symbol == null) throw new InvalidSymbolException(symbol);
	if (symbol.equals("IBM")) return "$100.00";
	if (symbol.equals("MSFT")) return "$30.00";
	if (symbol.equals("BOGUS")) throw new InvalidSymbolException(symbol);
	throw new InvalidSymbolException(symbol);
}

																				
																		

如果服务更复杂(如无状态会话 Bean 或 SOAP Web 服务),此 POJO 代码仍然可以作为更复杂的模拟实现的基础。在任何情况下,模拟实现肯定都不应该试图处理每个可能的股票代码或访问具有实时数据的数据库。模拟实现应该足以通过服务测试即可。




 
 


提供程序开发

此 SOA 开发流程的第四步是由提供程序开发团队实现通过服务测试的服务。

此时,提供程序团队已经准备好,可以进行服务开发了。既然他们还没有开始实现服务,怎么可能进行服务开发呢?幸运的是,他们已经开发了描述服务应如何工作的服务用例,开发人员就是开发人员,他们已经开始考虑如何实现服务了。开发人员已经创建了服务测试,这些测试可说明服务的 API 是什么,并帮助演示服务的行为。他们已经开发了服务模拟,这些模拟是表示实际服务将如何工作的快速原型。

因此,开发人员已经非常明确如何实现服务,只是尚未实现其相关的任何代码而已。

这几乎是毫无疑问的,但开发人员实现的服务必须通过服务测试。他们如何知道自己已经完成服务实现工作了呢?当服务通过了所有测试后,服务就已完全实现了。在开发期间,团队的开发活动可能会让他们添加要测试的其他功能。训练有素的团队不会放弃这些想法,而将对其进行捕获并添加到服务的测试集中。服务实现也必须通过这些测试。提供程序团队还应该向其他提供程序团队和协调程序团队通报这些添加的测试,以便所有团队的测试集保持同步。

理想的情况下,提供程序团队将可以成功地实现通过测试的服务,而不必修改已达成一致的测试。不过,这经常被证明是不切实际的。当开发人员实现服务时,他们有时会发现需要更改服务的接口或行为。如果服务测试的质量良好,而开发人员更改了服务的工作方式,则测试就不能再通过了。为了使测试仍然有效,开发人员必须修改测试,使其可以验证新的设计。更改测试意味着服务模拟现在将不能通过测试,因此也必须对其进行更改,以实际模拟服务现在的工作方式。

如果提供程序开发人员更改了服务测试或服务模拟,他们需要尽快通知协调程序开发人员和该服务的任何其他提供程序的开发人员。任何使用旧测试和模拟的人员都在根据一个现在已经过时的协议进行开发,因此各个团队需要根据新测试和模拟进行同步。如果其他团队拒绝接受新测试和模拟,则重新同步的工作就变成了团队之间的重新协商点。他们仍然有希望就服务用例达成一致,以从此处继续开展工作,从而开发一组一致认可的测试和模拟。

示例提供程序实现

提供程序开发团队将开发实现 StockQuoteService 的类或组件,其工作方式与 StockQuoteMock 类似,并使股票报价测试能够通过。模拟是一个包含硬编码响应的简单对象,而此提供程序是提供实际行为的组件。该实现应该执行以下操作:

  • 支持所有有效股票代码(至少支持提供程序的股票交易中涉及到的所有股票)。
  • 确保使用包含所支持股票的实时价格的数据库。
  • 将数据库使用的价格格式转换为服务返回的格式。
  • 将数据库针对无效代码的输入结果(如空查询结果)转换为服务希望的错误。
  • 实现服务的协议,如 EJB 远程接口、HTTP Web 服务或 JMS 请求和答复消息。

由于模拟和实际提供程序实现相同的接口——在此例中为 Java 接口 StockQuoteService,因此服务测试可以使用两个实现中的任何一个;只要使用要实例化的正确类对测试进行配置即可。为了运行测试,您还需要使用测试期望的股票价格对数据库进行配置。




 
 


协调程序开发

此 SOA 开发流程的第五步是由协调程序开发团队实现使用服务的应用程序。在实际服务实现就绪之前,应用程序都将使用服务模拟。

此时,由于具有大量的服务模拟,因此协调程序团队可以继续进行其相关工作,就像已经实现并提供了服务提供程序一样。而且,协调程序团队不仅具有一组可以使用的服务(也就是模拟),而且也有了可以演示服务如何工作的和客户机如何使用服务的一组测试。该团队可以将这些测试作为可以如何实现其协调程序的简单原型使用。和提供程序团队一样,尽管尚未实现任何代码,但协调程序开发团队已经早就在进行协调程序的工作了。

理想情况下,协调程序团队将可以使用达成一致的服务模拟来成功地实现他们的协调程序。不过有时候这样做有些困难。模拟并不提供某些需要的行为或希望的接口。协调程序客户还需要比模拟提供的服务更细粒度的服务。如果服务协调程序需要其他功能,则可以尝试自行实现此功能。如果协调程序需要不同的接口,则可以尝试实现一个适配器,来将其所希望的接口转换为模拟实现的接口。如果协调程序希望更细粒度的功能,则该团队需要对模拟及其测试进行修改。

这些更改会使得有必要重新与提供程序团队进行同步。让我们假定协调程序团队实现了额外的功能或不同的接口来提高服务的可用性。如果添加的行为不是特定于协调程序,而是会涉及到服务,则添加的行为可以潜在地由其他服务使用者重用。因此应将其内置到提供程序中。提供程序所需的更改可以也应该建模为对模拟及其测试的更改。当协调程序团队必须修改模拟及其测试时——既可能是为了增强其他功能也可能是为了对功能进行进一步细化——必须将这些更改应用到提供程序和其他所有的工作内容。已更改的模拟和测试成为协调程序团队、提供程序团队以及其他协调程序提供团队之间的重新协商点。他们必须针对达成一致的一组新模拟和测试重新进行同步。

示例协调程序实现

协调程序开发团队将实现一个委托给 StockQuoteService 的实现的客户端组件。它的行为将与服务测试相似,不同的是,它将使用服务类为 GUI 或客户端应用程序提供真正的功能。协调程序实现只能使用 StockQuoteService 中经服务测试证明可用的功能。Java 编译器将确保协调程序代码只能调用服务接口声明的方法;保持协调程序实现与测试实现的一致可以确保服务按预期的要求工作。




 
 


将流程组合起来

那么,该流程在实践中是如何工作的呢?

第一步,开发服务用例。服务用例团队可以包括来自提供程序团队和协调程序团队的代表。或者,这个团队可以仅由那些专门进行需求收集和用例开发的分析人员组成。传统用例开发主要关注人们如何使用应用程序,而这个团队必须将重点放在组件如何集成上。他们不应关心提供程序将如何实现,也不用考虑协调程序可以如何实现。相反,他们应将重点放在服务是什么、它们完成什么工作以及如何对其进行调用上。

第二步,将服务用例编写为服务测试。用例是人可读的,而服务测试表示相同的需求,但采用的却是计算机可执行的方式。这些测试必须由开发人员实现,而不是由开发用例的分析人员实现。测试开发人员可以是提供程序团队和协调程序团队的成员,也可以是可用且有能力实现测试的人员。在最终确定测试之前,每个团队的代表都必须对其进行认可,从而表示所有团队已就其达成了一致,而不考虑谁开发了哪个测试。

第三步,开发通过测试的服务模拟。开发测试的团体通常也实现模拟。模拟证明测试可以通过,作为原型供提供程序团队使用,并支持协调程序团队继续进行开发。与测试一样,除非所有团队都认可模拟并表示同意,否则就不能认为已最终确定了模拟。换句话说,任何团队都不能强制别的团队接受一组测试和模拟,大家必须一致认可,否则迟早会出现混乱。

第四步,提供程序团队部署提供程序,这些提供程序的行为与模拟相似,且均已通过了测试。如果将这些提供程序添加到测试和模拟,尤其是在更改了测试和模拟的情况下,则他们必须分发这些更改,以便其他团队重新进行同步。他们不能强制让其他团队接受这些修改;所有团队必须就此达成一致。

第五步,协调程序团队必须使用模拟开发可以正常工作的协调程序。如果需要更改模拟,他们还需要对测试进行更新。他们需要随后将其更改分发给其他组,所有的团队必须找到一个大家都认可的点——一组共同的测试和模拟,并据此重新进行同步。

这些步骤一起的确可以形成一个简单的开发流程。




 
 


解决 SOA 问题

那么,这个流程是否解决了我在本文开始时提出的问题?

1. 如果某些提供程序尚未开发,则协调程序团队如何开发其负责的应用程序部分?

模拟解决了此问题。模拟可以快速地进行开发。虽然实际提供程序的开发需要更长的时间,但协调程序团队可以使用模拟同步开发协调程序。只要满足了以下条件,此工作就可以顺利地进行:

  1. 模拟的功能全面,即意味着其编写的测试和用例也全面。
  2. 没有团队必须更改模拟。只要某个团队必须对模拟进行更改,他们就应该在进一步脱离同步之前尽可能快地重新进行同步。

2. 协调程序团队和提供程序团队如何较早而可靠地就服务如何工作达成一致?

因为测试和模拟可以快速进行开发,而且它们是真正可以运行的实际代码,所以提供了早期验证,从而可以确保用例有意义,并且团队真的达成了一致。有了经验后,可以在前期投入更多的精力,以确保测试和模拟的全面性,这些构件需要更改的几率越小,剩下的开发过程中需要重新进行同步的几率也就越小。

3. 实现相同服务的多个提供程序团队如何能确保他们的实现是兼容的?

一组公共测试以及通过这些测试的模拟可以作为公共参考框架使用,以确保独立的提供程序实现始终兼容且可互换。早期的测试越前面,需要更改的几率就越小,从而团队需要重新同步的情况也就越少。

这样一来,该流程就解决了多个团队实现 SOA 的不同部分时所面临的主要问题。




 
 


结束语

本文讨论了以下内容:

  • 在尝试使用 SOA 开发应用程序时,并行的独立团队可能遇到的常见开发问题。
  • 一个简单的五步开发流程,该流程通过使用服务用例、服务测试和服务模拟解决了这些问题。

您可以将此流程应用到您的组织中,从而大幅度提高使用 SOA 进行开发的成功几率。

 

posted on 2006-06-19 18:54 wsdfsdf 阅读(287) 评论(0)  编辑 收藏 引用 所属分类: 技术文章


只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理