SOA基本架构模式详解.docx

上传人:b****5 文档编号:3428599 上传时间:2022-11-23 格式:DOCX 页数:24 大小:671.92KB
下载 相关 举报
SOA基本架构模式详解.docx_第1页
第1页 / 共24页
SOA基本架构模式详解.docx_第2页
第2页 / 共24页
SOA基本架构模式详解.docx_第3页
第3页 / 共24页
SOA基本架构模式详解.docx_第4页
第4页 / 共24页
SOA基本架构模式详解.docx_第5页
第5页 / 共24页
点击查看更多>>
下载资源
资源描述

SOA基本架构模式详解.docx

《SOA基本架构模式详解.docx》由会员分享,可在线阅读,更多相关《SOA基本架构模式详解.docx(24页珍藏版)》请在冰豆网上搜索。

SOA基本架构模式详解.docx

SOA基本架构模式详解

五种SOA基本架构模式

目前,面向服务的架构(SOA)已成为连接复杂服务系统的主要解决方案。

虽然SOA的理论很容易理解,但要部署一个设计良好、真正实用的SOA系统却非常困难。

本文试图通过解析SOA的模式,提供与架构相关的技术指导,进而对以上问题提供详尽的的解答。

在本文中,一共提到了五种模式。

表1列出了这五种模式以及各自相关的问题。

模式名称

相关问题

服务托管

如何使服务能够适应不同的配置,避免设置监听器、组件连接等重复性常规工作。

主动式服务

如何提高服务的独立性以及如何处理暂时性的问题?

事务处理服务

如何可靠地处理消息?

工作流化

如何提高服务对不断变化的业务流程的适应性?

边缘组件

如何将服务的业务功能从无关的交叉问题(比如安全、登录等)中分离出来。

表1:

模式列表

其中服务托管(ServiceHost)与主动式服务(ActiveService)是两种最常见的模式——即使服务的使用范围很小,通常也会使用这两种模式。

这两种模式的主要内容都与解决服务相关问题有关,即与具体的服务部署有关(见图1)。

模式一:

服务托管

服务托管是我们要讨论的第一个模式。

它是最基本的模式,或者至少是最基本的模式之一。

服务托管模式主要负责运行着服务实例的环境,以及与此相关的路由任务。

问题

随便选一个服务,任何服务都可以,别告诉我具体是哪个:

)。

你可以找到一些处理传入的消息或请求的监听代码;你可以找到一些连接组件的代码,还有一些初始化并激活这个服务的代码;或许你还能找到一些能适当地配置服务的代码。

有没有觉得我很厉害?

实际上,你可以在服务里找到上面的所有代码,至少是大部分。

有许多工作都是重复性的、常见的。

我们可以好好利用这一点。

如何使服务能够适应不同的配置,避免设置监听器、组件连接等重复性常规工作?

第一个办法(实际上也不是什么办法),就是为每一个服务重写所有的连接代码。

很显然,这不是个好方法,因为重写的次数越多,就越可能产生一些缺陷。

并且,对于维护来说,许多重复的代码产生的问题更为严重。

在维护的时候,你不仅要确保每一个服务中的缺陷都已经得到修正,还要保证没有任何疏漏、所有的服务都已经同步更新。

另一个相对较合理的办法,就是创建一个共同任务库,所有的服务都通过API与库相连接。

这样确实会有所帮助,但是为了充分利用库的功能,你仍然需要编写连接代码。

还有一个办法是利用继承创建一个超类,用超类实现共同的功能,然后让各个服务继承这个类。

然而利用继承也有问题,因为服务的功能通常无法通过一个单独的类获得很好的实现。

此外,不同的服务所处理的业务也完全不同——否则它们就是一样的服务了。

因此,也无法让把这些服务归于同一类结构。

继承几乎已经可以解决我们面临的问题——因为我们只需要写一次代码,只有不同的服务才需要定制。

如果想不用继承得到相同的结果,我们就得使用框架。

解决方案

创建一个通用的服务托管组件,把它当作服务的容器或者框架。

容器是可以配置的,并执行服务连接、安装等工作(见图2)。

服务托管就像是一个肩负着许多职责的迷你框架。

它的第一个职责就是正确地示例服务所包含的组件或类。

服务托管还负责读取配置信息,比如,它可以读取服务消费者用来连接服务的端口。

其它职责包括创建服务环境,比如在终端创建监听器。

最后,服务托管可以负责连接组件——终端监听器上的协议绑定或者创建一个与数据库的连接。

所有这些职责的共同特征是它们都与服务的实例化和初始化有关,并且正如我们在问题部分所描述的一样,你很可能会在多个服务中遇到同样的情况。

前面已经提到,服务托管是一个框架,与第二种方法中描述的库的概念有所不同。

库是一个实用类或方法集,你可以调用它们来获取特定的功能。

而框架则包含了一些功能或流程,并通过调用代码来扩展流程或将其转变为具体的流程。

这就是“控制反转(InversionofControl)”原理。

这种原理已经在面向对象的框架中获得广泛的应用,比如Spring或Spring.NET、Picocontainers等。

服务托管模式与其它方法相比有许多优势。

其中一个优势前面已经提到——即服务托管是一种框架,它可以调用代码来改善性能,而无需你亲自进行编排。

另一个优势是它能更好地实现开放/封闭原则(OCP)。

OCP原则认为类应该是可以扩展的,但是不能修改;而框架的概念正是这个原则的具体表现。

一个服务托管可以托管多个服务——虽然这种情况可能并不常见。

我们曾经构建了一个系统,使用的方案规模小到一台计算机即可应付。

这真的很方便。

如果换另一种方案,那么服务可能就要扩展到多台计算机上,这样你就得应用多个服务托管实例,各个主机托管服务的一部分,而不是整个服务。

服务托管模式已得到了技术供应商的广泛应用,这一点我们可以在下面的技术相关部分看到。

技术相关

这一部分我们将简单地了解一下利用当前的技术实现这个模式的意义。

服务托管是一种基本的SOA架构模式,因此大部分技术都支持这种模式。

JAX-WS和WindowsCommunicationFoundation都能用标记语言(XML)进行配置,并使用Web服务器(比如Servlet引擎或IIS)完成大部分的连接工作。

WindowsCommunicationFoundation还提供了一个称为ServiceHost的类。

Microsoft的说明文档是这样解释ServiceHost的:

“如果你不使用InternetInformationServices(IIS)WindowsActivationServices(WAS)提供服务,就用ServiceHost类来配置并提供服务。

IIS和WAS都能与ServiceHost进行交互。

”从根本上说,其内置WCF对ServiceHost的执行与我们所描述的服务托管模式是基本一致的。

如果你要自己实现一个服务托管模式,那么你可以从Spring(或者Spring.NET)、Picocontainers等轻量级容器中学习如何进行连接和实例化。

这方面的技术实现并不多,因为服务托管模式实际上是一种相对简单的模式。

轻量级容器与依赖性注射

Spring和其它一些框架被称为“轻量级容器”。

这些“轻量级容器”的优点是它们提高了方案的松耦合性和可测试性。

这种优点是通过一种非SOA模式——依赖性注射模式(DependencyInjectionPattern)实现的。

依赖性注射,正如名字所示,指一个类通过第三方“汇编程序”提供所需的接口。

通过依赖性注射技术,类不再依赖于特殊的实现,而只依赖于接口和抽象类。

这使可测试性获得了提高,因为你可以使用桩或模拟来为类提供虚拟环境。

这同时也提高了灵活性,因为只要它们之间的关系不变,你就可以轻松地改变具体的实现方式。

服务托管模式是一种简单而有效、并且已经获得广泛应用的的模式。

质量属性场景

这一部分是从需求的角度讨论使用模式的架构效益。

大多架构需求是根据使用场景表现的质量属性(可扩展性、灵活性、性能等)来描述的。

这些场景也可供其它可以应用模式的情况参考。

使用服务托管模式的主要原因是重用性。

由于多个服务需要使用的相同任务只需写一遍代码,因此重用性也随之提高。

这还能带来另一个好处,就是可靠性的提高,因为你只需要调试一次。

另外,服务托管模式还能提供可移植性的质量属性。

由于这个模式的分离问题的效应,因此可移植性也得到了增强。

另外由于可以使用标记语言配置服务环境,可移植性又会得到进一步提高。

下面的表2列出了几个场景,可以给你虑使用服务托管模式的理由。

质量属性(第一层)

质量属性(第二层)

场景示例

重用性

减少开发时间

开发时,在20分钟内设定新服务的环境。

可移植性

安装

系统必须支持以服务为单位对服务器进行配置。

安装过程中,从一个环境切换到另一个环境所用时间应该少于一小时。

一旦配置好并开始运行服务,你就得决定服务应该是被动式的(应请求唤醒)或是主动式的(无需等待服务消费者激活,并做一些零工,比如显示状态或处理超时)。

正如名字所示,主动式服务模式下,服务是主动运行的,而不是被动的。

模式二:

主动式服务

服务的自治性(Autonomous)很重要。

自治性能够提高服务之间的松耦合性,并使整体方案产生更好的灵活性。

但是自治性服务有什么实际意义呢?

有人说,自治性意味着在不同的服务上工作的团队的自治性。

这种定义表示,由于各个服务之间只有契约上的关系,因此服务之间几乎没有依赖性。

这意味着各团队可以独立工作,专心于自己的服务,而不会互相绊脚。

虽然这是一个不错的“功能”,但是同时还有一个更有价值(比如说商业价值)的定义,就是服务是非常自主(self-sufficient)的。

下面我们通过一个示例来解释这个定义。

问题

有一家报纸订阅代理机构(比如Ebsco或Blackwell),它需要为客户创建一份申请。

申请服务的一项内容是产生一份形式上的清单。

要得到这样的一份清单,该机构必须同时有给顾客的折扣率和从各出版商处能够得到的折扣,这样才能计算出这份申请是否有利润。

图3就是这样一个流程的简单示意图。

在场景示例中,申请服务需要等待另外两个服务的信息。

顾客服务是内部服务,与申请服务是同一系统;但出版商的折扣服务却很可能是外部服务——如果出版商的系统没有联机,那么会对我们的申请服务造成什么影响呢?

会造成申请服务无法使用。

即使我们花费了天文数字的资金来保证申请服务的容错性,但现在的问题是完全无法使用,因为申请服务是与外部的出版商的服务随时耦合的。

因此申请服务不具有真正的自治性。

如何提高服务的独立性以及如何处理暂时性的问题?

上面所描述的问题表明,仅仅根据请求唤醒的被动式服务是有问题的,因为服务可能无法满足依赖于外部服务的契约条件(或服务等级协议)。

一个解决办法是让服务对先前的结果进行缓存,但这只能解决部分问题,因为这样做数据就无法得到及时更新,并且时而也会有缓存失效的情况发生,这时仍然需要连接其它服务。

这种方法还有另一个问题,那就是如果传入的请求过多,在处理一个请求的时候,其它的请求就会处于“等待”的状态,这样又会产生资源问题,因为而这些“等待”的请求都需要外部服务的输入。

即使我们解决了前面的缓存问题,我们仍然得处理其它的暂时性事件。

暂时性事件包括重复发生,或者与时间相关的一次性事件。

比如,生成每月账单或发布股票数据或任何其它重复性的报告都是暂时性事件。

一种解决方法是从外部编排服务。

这种方法的问题是你得将服务的业务逻辑具体化。

但是请记住,封闭的服务层是应用SOA的一个重要原因。

因此我们得另寻解决方案。

解决方案

要使服务成为主动式服务至少需要一个主动类(ActiveClass),这个类可以在边界上,或者服务上,或者两者都有。

然后让这个主动类处理暂时性问题和管理自治性。

主动服务模式意味着在服务层上执行“主动类”(见图4)。

在OfficialUML定义中,“主动类”是指“不需要调用方法即可启动自身行为的对象。

”这定义对服务也适用。

就是说,服务可以有独立的线程来处理循环类事件,比如每月账单或发布状态。

主动式服务也可以监控自身的情况,处理超时,甚至可以用来处理请求。

那么,怎么使用主动服务模式来解决我们上面提到的问题呢?

就像帕特·森田在《空手道小子》里扮演的宫城先生所说,“最好的防守就是不要在场。

”如果你要避免等待另一个服务这种事情发生,那么最好的办法就是不要等待;你可以主动地、周期性地从其它服务获取数据,更新你的缓存。

你还能给其它服务减少类似的麻烦,并预先发布自己的变化状态。

表面上看起来,缓存数据可能会引起数据重复的问题,但实际上这种情况是不会发生的(详见下面标注)。

缓存与数据重复问题

我想有些人,特别是那些学过数据库的人,看到我说从远程服务主动获取缓存数据的时候,肯定会从椅子上蹦起来质疑我远程数据复制的动机,认为我是不是大脑出了什么问题。

然而,在我看来,这已经不是相同的数据了。

缓存在服务上的数据是服务的数据,可以用来计算、处理甚至根据服务需要进行修改。

当然,你也必须明白缓存数据的服务并不是负责控制数据的。

一个带有计时器的线程基本上足够应付其它暂时性事件了(如果事件少,你可以为每一个事件安排一个定时器;或者定时唤醒,检查哪些事件需要处理并处理它们)。

使用边界组件的线程处理契约相关的暂时性问题是一个好办法(比如,及时发布状态、超时等),而服务线程则可以处理纯业务类的问题,比如发送每月账单或者处理传入的消息队列。

现在我们看一下如何使用主动服务模式重装安排图3的情况。

简单重复一下,图3是一个申请服务的流程,它需要从外部的发布服务获取外部数据,并同顾客服务一起为顾客生成一份清单(见图5)。

现在我们让申请服务主动地定期获取折扣信息并缓存结果,这样当收到产生一份清单的请求时,申请服务就可以即时计算折扣,更快地返回结果,并且(在处理清单请求时)无需依赖外部服务。

使用了主动式服务,请求服务便与其它服务分离了。

主动服务模式差不多就是一种理念,没有太多的技术成分。

技术相关

这一部分讨论如何使用SOA相关技术实现这个模式;然而,因为这里并没有太多的技术成分,因此我们也只是简单的说明一下。

主动服务模式的技术理念就是在有特殊功能的服务或边界组件上使用主动线程。

从本质上说,主动服务模式取决于线程技术。

你可以用任何语言实现这种线程技术。

关键在于,你要用这个线程做什么。

在上面的段落里我们用了从其它服务缓存数据并处理重复性报告的例子。

下面的问题就是什么时候应该使用主动服务模式?

我们来看一下动机。

质量属性场景

这一部分是从需求的角度讨论使用模式的架构效益。

大多架构需求是根据使用场景表现的质量属性(可扩展性、灵活性、性能等)来描述的。

这些场景也可供其它可以应用模式的情况参考。

主动服务是一些其它模式(比如前面提到的分离调用和blogjectingwatchdog)的先决条件,而这些模式可以帮助处理质量属性问题,比如可靠性与可用性。

并且,即使是单独使用主动服务模式也能满足许多质量属性要求。

通过预先准备的数据,主动式服务可以减少一些潜在的问题。

它可以解决期限的问题,因为它能保证服务在期限之前完成任务(比如按时生成每月账单)。

另外,主动服务模式还有可用性的优势,因为从其它服务查询并缓存数据意味着减少了服务的可用性对其它服务的依赖性。

下面的表3列出了一些主动服务模式可以发挥作用的场景。

质量属性(第一层)

质量属性(第二层)

场景示例

性能

延迟

正常情况下,评估申请的效益只需要2秒不到的时间

性能

期限

未满负载的正常情况下,系统可以每少刷新两次以上股票价格

可用性

正常运行时间

即使断开远程连接,用户仍然可以生成报价单

模式三:

事务处理服务模式

服务构建的另一个重要属性是:

怎么处理从边界组件或服务中得到的信息?

事务处理服务模式(TransactionalServicePattern)可以解决这种问题,并且还能解决可靠性问题。

可以把SOA活动简化为服务收到服务消费者要求做某件任务的请求,服务处理请求(可能还会请求其它服务一起做这件任务),然后回应发起请求的服务消费者。

图6显示了这样的一个商业系统中的活动场景。

前台与订购服务进行对话。

订购服务登记订单,把订单发送到供应商,然后通知账单服务。

这些事件处理完成后,订购服务向电子商务前台应用发送一条确认信息。

这一切看起来井然有序,但万一中途发生错误怎么办?

问题

比如说,订购服务在确认订单与处理的中途产生故障的话,也就是图6中的步骤1.1与2.0之间,会出现什么情况呢?

我想顾客应该会坐在舒适的沙发上,喝着茶水,等着邮递员把她订购的东西送过来。

但是虽然她在等,订单却已经消失得无影无踪了。

那么如果服务是在报账服务处理订单之间出现故障呢,也就是步骤2.3之前。

这种情况下,订购同样会消失——除非订购系统不等待报账便处理了订单,这几乎是不可能的。

更糟糕的是,我们已经向供应商发送了订单,供应商已经把账单发了过来,并且货物也随之送了过来,我们还得给货物准备库存。

消息处理过程处处都可能出现前面提到的这些情况。

我们可以安慰自己,说大部分情况下系统会正常工作。

然而就像莫非定律所说——我们的服务最终必然会在那宗百万美元的订单上垮掉。

现在的问题是:

如何让服务可靠地处理请求?

其中一个方案是把这个责任推给服务消费者。

在上面提到的场景中,这意味着如果消费者没有在步骤2.5中收到订单确认信息,那么这个订单就是失败的。

然而首先,这个方法并不健全,并会降低服务的自治性——服务无法控制消费者,也无法处理一些其它问题。

另外,这只能解决部分问题——那些与服务消费者有关的问题。

服务之间的相互作用呢?

比如在上面的订购场景中提到的——即使在步骤2.1向供应商发送订单以后,仍然可能出现问题。

第二个办法是同步处理消息。

同步操作在性能上会产生很大的问题,特别是当服务需要与外部服务、系统或资源交互的时候,因为服务在返回结果之前,整个流程都要等待第三方的回应。

更主要的是,实际上这并没有真正解决问题。

如果服务在流程中出现故障了,我们无法确认是哪里的故障。

我们只知道消息传输出现了故障,而要确定故障环节,则需要服务消费者的帮忙。

表面看来,如果服务能够使用永久性地储存介质(比如到数据库)就可以解决这个问题。

我觉得这个方向是不错;但是,这还不够。

因为如果服务在储存状态之前出现了故障,传入的消息仍然会丢失,并且服务对此一无所知。

还应该注意到,如果使用永久性存储介质,我们虽然可以追踪到在过程的哪一环节出现故障,但是我们无法确定消息是否已经发送到了其它服务。

要解决这些问题,以及整体的可靠性问题,我们需要事务处理服务。

解决方案

使用事务处理服务模式,一次性处理从读取消息到响应的整个流程。

事务处理服务模式的主要组件是消息泵(messagepump),见图7。

消息泵监听着终端或边界传入的消息。

如果接收到消息,消息泵就开始一个事务操作,读取消息,把消息发送到其它组件/类进行处理,处理后,结束事务操作(完成或失败)。

如果可以以事务处理的方式发送请求或回应,就可以把这些操作放入到事务处理中,否则你就需要为操作失败准备补偿逻辑。

使用事务处理编程模型的好处是它要么是纯语义学的,要么完全不是,因此不存在边界效应。

由于事务的四个属性(ACID),所有的操作和消息都一定会被完全处理完、或完全没有被处理,所以如果有消息离开了服务,那么触发这个行为的传入消息肯定是被完全处理过了。

ACID事务

一个事务是一个完整的任务单位。

一个任务单位如果满足ACID所定义的四个属性,那么它就是一个事务。

*原子:

事务中的所有事件都是以一个原子单位的形式发生的(atomicunit)。

这些事件要么全部发生,要么全部不发生。

*一致:

不管事务完成与否,事务的资源必需在整个过程保持一致。

*隔离:

所有外部的观察者(不参与事务处理)都不能看到内部的状态。

只能在事务处理开始前或完成后查看状态。

*持久:

事务处理过程中做出的改动储存在永久性存储介质中,因此即使系统重启后也不会丢失。

当然,你要选择事务服务模式的重要因素肯定还是性能。

由于需要准备、为了持久性而做的输入输出和锁定管理等,事务通常会比较慢。

我一般会预告确定目标场景并进行测试,以确保能够得到一个足够好的方案。

实现事务处理服务模式的一种方法是为所有服务间的消息使用事务消息传输。

事务消息传输(transactionalmessagetransport)使得模式的实现变得非常简单——只要按照前面提到的步骤来就可以了:

开始事务、读取、处理、发送、完成。

另外一种方法,也是更常见的一种方法,是在接收到消息后把消息放到事务处理资源中(比如队列或数据库),然后向服务消息者发送一条确认消息。

但是这种情况下最初的消息不包括在事务处理中,因此你要准备应付服务消费者的多次消息发送。

比如,服务消费者没有收到确认消息,于是“又”发送了一条请求消息提取100万美元。

图8是图6的事务处理服务模式。

简单重复一下,该场景是一个关于电子商务的前台订购服务。

订购服务登记订单,把订单发送到供应商,然后通知账单服务。

这些事件处理完成后,订购服务向电子商务前台应用发送一条确认信息。

在图8中,使用事务处理服务模式,步骤2.0到2.5(订购服务的行为)处于同一事务中。

这意味着如果你因为故障或其它意外没有处理下订单的消息,那么服务就不会发出任何消息。

这是个很让人开心的消息,因为我们不用再写复杂的补偿逻辑了。

这里有一个小问题,那就是如果订购服务在步骤1.0到1.2之间出现故障的话会有什么情况。

该场景不是100%安全的;它有很小的几率在我们把消息放到队列等待处理的时候出现故障,从而没有发送确认消息。

这可能导致重复接收到同样的请求。

一个在服务这边处理重复消息的办法是在服务启动时查看消息队列并对所有消息发送确认消息,这种情况下,服务消费者可能会收到多于一条的确认消息。

请注意,在这个示例中,只能在账单处理过程仅仅产生一个发货单的情况下使用单独的事件。

如果账单服务还需要处理信用卡,并且订购服务需要得到确认信息才能继续的话,就不能使用单独的事件了。

当不能使用单独的事件的时候,需要把过程分成较小的事务,这时整个过程就被称为连续操作。

需要把流程分为几个较小的事务的另一个条件是看服务是否是分布式的。

必须注意把事务的范围定到终端/边界和外部的消息发送者是不一样的。

虽然从表面看来,这个区别不是很重要;但是实际上它确实很重要——因为前者是增强服务的可靠性而后者是提高系统的耦合性并会给你带来让人头痛的问题。

如果你把事务扩展到服务之外,那将是非常大的转变,因为其它的服务是运行在自己的机器的,有它自己的服务等级协议等等。

把内部资源暴露到服务信任协议之外是很冒险的做法。

现在我们看看如何实现事务处理服务。

技术相关

这一部分我们将简单的了解一下利用当前的技术实现这个模式的意义。

如果消息传输是对事务敏感的,那么实现事务处理服务就容易得多。

大部分ESB(比如SonicESB和IonaArtix)都是事务敏感的,另外还有消息中间件(比如MSMQ)、所有JMS实现以及SQLServerServiceBroker。

如果你在使用事务消息传输,你可以通过仅仅在资源上创建一个事务来实现事务处理服务模式。

如果,比如你还要在同一任务单元中进行更新数据库的操作,你可能还需要分布式的事务处理。

然后只要从ESB或消息中间件读取消息、处理、发送响应或其它处理过程生成的消息到外部或目标队列,最后一切顺利的话结束事务。

要注意通常要在事务处理中用到多项资源,比如,一个消息队列和一个用于存储消息处理后的任何状态变化的数据库,这时你应该使用分布式事务处理。

在.NET2.0及以上版本中,如果有需要的话,你可以通过打开一个TransactionScope对象(定义于System.Transactions)显式地过渡到分布式事务处理。

如果消息传输不支持事务,只在你把消息存储到了事务存储库(比如队列或表)后有确认信息。

你仍然有在没有得到服务消费者的确认的情况下处理事务的风险,因此你得做好再次接收请求的准备,以防确认消息丢失或根本就没有发送。

如果出现故障,而处理传入的消息的事务向其它服务发送了消息,你也要准备好补偿逻辑。

毒信息

当我们以事务的方式读取信息时,需要注意分辨并处理毒信息。

毒信息是一种有缺陷的信息,它会使服务发生故障,或者使服务在处理这条消息的时候一直产生处理失败的结果。

原因在于,如果在事务中读取了一条毒信息,所产生的故障就会使信息返回队列;而毒信息则在队列中等待服务恢复,然后再被服务读取,依此循环。

某些信息技术产品可以辩认这种毒信息并将其丢弃。

你需要确认的是这些辩论机制到位,并且能够妥善地处理所有故障,或者至少把这些故障交给你亲自处理。

有一种叫WS-ReliableMessaging貌似跟此有关。

然而,虽然名字看起来很相似,

展开阅读全文
相关资源
猜你喜欢
相关搜索

当前位置:首页 > 小学教育 > 学科竞赛

copyright@ 2008-2022 冰豆网网站版权所有

经营许可证编号:鄂ICP备2022015515号-1