分享

如何选择最适合自己的技术架构?

 长孫无忌 2017-08-26

概述

在新产品进入研发阶段之前,技术选型是必须要完成的一项重要工作。它是对产品非功能需求、架构设计中的各种要素及约束的综合评估以及体现。也许你的公司已经提供了统一的技术开发框架,也许公司以前产品的技术选型恰好可以满足新项目的要求,但是即便如此,了解一个产品技术选型的过程也有助于我们去从技术方案角度审视产品的各种非功能性约束,虽然这些约束有的来自客户、有的来自公司。

技术选型是各个方面各种因素的综合抉择的结果,因此这项工作格外考验选型者对产品、架构的把握以及对各项技术框架的熟悉程度。

通过下面的示意图,我们看一下技术选型涉及的各个方面:

从上图可以看到,技术选型实际上是从不同维度对产品进行分解的过程。通过分析,合理分解出各项技术需求,然后对各项技术需求进行综合评估并最终选择合适的框架。

首先,所有产品都可以从架构上大体上划分为几类,具体到每一类都有相似的架构风格,它们通常在各种架构要素的具体要求上有很大的相似性。因此确定产品类型和架构风格有助于我们参照现有的产品来做技术选型,这样可以大大节省技术选型的工作量并降低由于技术选型不合适而带来的后期的开发维护风险。

图中只是对产品做了一个最抽象的类型划分,随着后续选型内容的讲解,你就会发现实际上上述的每种类型又会细分为不同类型。如WEB应用,信息展现类和社交类选型显然是不同的。除此之外,每种产品类型的选型也会存在重叠,如RCP和RIA应用,尽管UI层的选型完全不同,但是并不妨碍两者后端选型的相似性,如两者都是数据展现及交互复杂的企业应用。

总之,产品类型就如程序设计上的设计模式一样,便于我们快速将产品分解为几个重要的架构要素并且对应到其常见的解决方案,为我们的技术选型工作发挥很大的指导作用。

其次,架构分层可以帮助我们以“分而治之”的思路来进行技术选型。这既包括“逻辑分层”,也包括“物理分层”。逻辑分层使得我们将技术选型分为展现层选型、业务层选型、持久层选型以及数据资源层选型等,然后我们再按步完成选型工作,每一步除了要考虑其对应的架构要素外,还要考虑上下层的集成方案。如方案的复杂度、健壮性、性能等。而“物理分层”则确定了各层之间的通信框架选型,同样我们需要考虑通信的性能、安全性、有效性等。

最后,无论是产品类型还是架构分层,这两者的结合都是便于我们将技术架构选型进行合理的分解,将关注点充分聚焦,从而在各框架间做有效取舍。但是除了各项技术要素及指标外,还有很重要的一方面对技术选型有非常大的影响,那就是学习成本、社区活跃度和技术成熟度。

对于两个技术框架的各项技术指标相近的情况,我们自然要选择学习成本更低、社区活跃度更高以及技术成熟度更高的一个。

对于一些新出现的框架,虽然理念非常好、社区非常活跃,但是其框架可能并不够健壮,需要更多的时间在生产环境中去完善。此时纵使其有更好的性能等的表现,我们也要审慎的来选择,或者在一些非核心的模块局部进行引入试验,或者不引入该框架,而是合理设计系统的集成方案,以便在其足够完善时能够轻易的进行框架迁移替换。

换句话说,当我们认为一款新框架有足够好的性能、可扩展性、可伸缩性时,我们更需要冷静的考虑以下它是否足够健壮,它的这些特性是否是我们所必须的。有时候你会发现,它很快、很灵活,但是却并不是你必须要拥有的,你引入它带来的系统质量的提升远远抵消不了因为维护它增加的成本。

总之,选择一款最合适你的产品的框架,而不需要对各项架构要素进行极限追求。这也是为什么各种新框架满天飞的当下,很多十几年前的框架仍保持旺盛的生命力的原因。

开发技术选型

接下来我们按照上述分解过程,对各种产品类型的各层进行简单的架构要素分析及选型说明。

展现层

可以说,展现层是最直观感受到各种产品类型差异的一层。这不只是说产品最终展现在用户面前的样子,还指展现层的技术选型工作,因为每一种产品类型选择的展现层框架是完全不同的。

RCP应用提供了桌面应用一般的数据展现和操作复杂度以及操作的流畅性(WEB应用即便网络再好、静态文件再小,其网络以及渲染开销带来的延迟仍要比RCP应用要大,尤其在加载数据较多的情况下)。但RCP应用的缺点也很明显,需要客户端安装包,而不是像WEB一样加载即用,因此它更多适用于一些内部行业应用。

针对Java语言,RCP开发主要集中于两类:Swing和SWT。Swing是JDK自带的GUI工具包,而SWT则是由Eclipse IDE开发。这只是RCP开发基于的底层工具包,而非工业化开发框架。SWT与Swing相比,使用了系统原生的窗口部件,因此拥有较好的渲染性能。但是在PC配置不断提升的当下,其带来的性能差异显然已不足以作为我们选型的重要依据,更重要的是其对应的工业化开发框架的差异。从工业化开发角度看,这两款GUI工具包都有相关的RCP开发框架,基于这些RCP开发框架,我们可以快速开发一款高质量的RCP客户端应用。基于Swing的是Spring RCP和NetBeans RCP,基于SWT的是Eclipse RCP。

Spring RCP和Eclipse RCP于基础的GUI工具包的之上,实现了MVC模式的架构封装,提供了更友好、易用的API封装以及更复杂的布局、组件、国际化、异常处理以及数据绑定支持。Spring RCP采用Spring管理各个组件的配置,因此你可以在它里面使用任何Spring的特性,而Eclipse RCP采用OSGi模块化架构,便于按照模块化管理你的应用。

从学习成本看,基于Swing的RCP开发框架显然更容易学习,毕竟Swing是JDK的一部分,熟悉相关API的人要比SWT多,而且API也更为友好。

对于WEB应用的展现层(WEB前端),想必近几年没有哪个技术细分如它一般发展迅速,堪称日新月异,尤其是与移动原生结合的混合应用开发以及移动WEB开发,催生了各种前端框架,这些框架从理念及开发模式上又极大影响了PC端WEB应用的开发。

WEB前端主要的框架如jQuery、AngularJS、Backbone.js、ReactJS、Vue.js、ember.js、Bootstrap等等。当然,现今活跃的前端框架远远超出了本文能够介绍的范围,此处只能选择几种具有代表的框架来进行选型关注点的说明。

对于WEB前端开发,它不像传统的桌面GUI应用,可基于UI组件进行操作,通过合理的封装、组合和继承,能轻易实现组件的复用。WEB应用归根结底是对HTML及DOM的操作,很长一段时间内,它并没有所谓的组件的概念(近几年前端框架的发展已经取得了巨大的进步,而且WEB组件也渐渐成为一种标准),更不用说数据绑定技术的应用。除此之外,WEB应用还有浏览器兼容性的问题。

WEB前端选型在合理评估产品适用场景之后,可能需要考虑框架浏览器兼容性、文件大小、组件丰富程度、是否支持按需加载、DOM渲染性能、开发效率及易用性等多种因素。

首先,在确定一款框架是否适合当前项目时,我们需要清楚项目规定支持的浏览器范围,并确保框架支持的浏览器已经囊括所有项目需要支持的浏览器。

其次,框架文件大小以及是否支持按需加载很大程度上会影响项目页面的加载速度。

再次,虽然在数据量较小时,各个框架的DOM渲染性能相差不大,但是如果你的应用需要在单一页面渲染大量数据,那么可以考虑针对DOM渲染性能进行测试,以确定哪一款框架的性能更出色。还有就是对于支持虚拟DOM的框架,也要关注其虚拟DOM处理性能。如Vue.js,便宣称其优势之一就是超快的虚拟DOM。

最后,框架是否提供了易用且友好的API也是我们需要考虑的一项因素。API易用且易理解便于开发人员快速上手并能够显著提升开发效率(也许一些API怪异的框架在能力出众的工程师手中是快速开发的利器,但并不代表它适合于在团队中按照工程化的方法去推广使用)。

对于上述常见的框架,其中jQuery从WEB前端开发的原始痛点入手,通过API封装,极大的简化了HTML及DOM操作、CSS操作、事件处理以及Ajax交互。同时提供了灵活的插件机制,借助各种官方及第三方插件,你可以完成各种WEB前端需求。此外,jQuery屏蔽了各浏览器的JS操作差异,提供了非常好的浏览器兼容性。

当然jQuery是轻量的、简洁的,因此它也是基础的,并不是一个完备的前端开发框架。大多数情况下,它是很多前端开发框架的基础,也可以将它与其它框架结合使用,各自完成擅长的工作。

而AngularJS、Backbone.js、ReactJS、Vue.js、ember.js,无论会MVVM、MVC还是WEB组件,都是基于工程化角度实现的WEB应用前端框架。基于这些框架,前端已不再是通过链接跳转的各种孤立的网页,而是一个应用。

当然,这几个框架只是通过JavaScript API的封装,以使WEB前端应用可以按照某种更易复用、更易维护的方式进行开发,并不包含HTML样式(CSS),因此并不算是一站式开发框架,需要你引入第三方的样式框架,如Bootstrap,或者自行编写样式文件(当然,WEB应用展现的多样性已经决定了CSS绝大多数情况下都需要进行自定义,而不是直接拿第三方的方案来使用)。

Bootstrap针对网页布局,提供了响应式布局解决方案,便于你开发适用于PC、平板以及手机等不同设备的WEB应用。通常与AngularJS等框架配合使用。

RIA(富网络应用)严格讲只是WEB应用的一类细分。它面向的是企业级的信息系统。这些系统数据结构层级多、界面展现以及操作复杂,而且风格与桌面应用或者RCP类似。此时,如果使用一些轻量的MVC或者MVVM框架显然是有难度的。而一些提供了与RCP相似风格API的框架此时便显现出了其特定优势。ExtJS以及Dojo便是这样的框架。

ExtJS与Dojo都是提供了一站式的开发方案,不仅都采用了MVC模式来进行WEB应用开发,而且还提供了足以与桌面应用媲美的UI组件。基于ExtJS或Dojo的API,我们可以开发出各种可复用的WEB 组件及界面,便捷的进行HTML事件处理。而且可以完全基于JavaScript编程,而不必操作HTML标签(当然,如果你希望操作HTML标签,它们仍支持这种开发方式,尽管这种方式的效率并不高)。

基于这两个框架,你可以构建交互复杂度等同于桌面应用的WEB应用。此外,它们还以“主题”的方式提供了统一的样式定义,使得你不必关注各种样式问题,便于为应用系统提供一致的样式外观。

当然,既然它们是一站式的开发框架,那么它们对于相对轻量级的WEB应用来说,便显得过重。如果你想基于ExtJS开发出清爽、简洁的WEB网页,你会发现CSS的修改几乎是一件不可能的事。因此,它们更适用于数据和交互复杂、样式高度统一的企业级信息系统。

还有一类WEB应用,需要特殊的UI组件支持,那就是数据分析系统。这类系统多数配合以各种图表的展现。如果你当前开发的是一款RIA应用,而且已经采用了ExtJS或者Dojo作为前端开发框架,那么它们作为一站式的解决方案,已经为你提供了强大丰富的图表组件,你可以直接使用它们来实现你的数据分析系统。相反,如果你当前开发的是一款轻量级的WEB应用,选择的框架只提供了MVC/MVVM基础API,那么你可以选择一些第三方的图表框架,如HighCharts、ECharts等(当然,如果一站式解决方案的图表功能无法满足你的需求时,你也可以引入第三方框架来实现)。

对于移动端来说,应用可以分为本地应用、WEB应用以及混合应用。本地应用技术选型相对比较固定。开发iOS应用,使用Objective-C或者Swift,开发Android应用则使用Android SDK。如果你想尝试跨平台开发,或者控制前端开发成本,则可以使用React Native。对于移动WEB应用,MVC/MVVM框架的选择上,可以参考PC WEB应用,但是需要格外关注文件加载以及渲染的性能。至于UI组件及样式,可以尝试Ionic(AngularJS)、Vux(Vue.js)、jQuery Mobile以及Frozen UI、WeUI、SUI Mobile等。对于混合应用,则可以尝试选择Apache Cordova(Phonegap)作为底层框架,并与UI框架结合进行开发。

以上技术选型集中于客户端及浏览器端,对于WEB应用展现层,还需要选择与之匹配的后端Java WEB开发框架(暂时不讨论基于其它语言的WEB开发框架)。在各种MVC/MVVM等模式的浏览器端WEB框架出现之前,后端WEB开发框架的视图层还是以JSP页面和各种模板(如freemarker、velocity等)为主,即后端渲染的方式。此时,后端的框架模式主要分为两类:MVC和基于组件的模式。前者如Spring MVC、Struts,后者如JSF、Tapestry等。

浏览器端WEB框架的快速发展,使得后端展现层的处理复杂度大大降低,也不必再考虑WEB页面组件化和复用的问题,因此,如无特殊需求,我们可以更倾向于选择MVC这种轻量的开发框架。不仅如此,现有的几款基于组件的WEB框架学习曲线都相对较高。无论从用户使用广度、资料完善程度还是社区活跃度上,都不如当下主流的MVC框架。

Spring MVC和Struts作为两款最知名的WEB MVC框架,前者与Spring框架无缝集成,可以充分利用Spring提供的各种特性,提供了更细粒度的请求处理,而后者无论是Struts 1的紧耦合还是Struts 2屡屡爆出的安全漏洞,都使得越来越多的人倾向于采用Spring MVC。当然,每种框架都有自己的特长,我们不应该轻易去否定一款框架,而是要综合自己项目的需求,合理分析当前框架的特性是否满足,从而确定对应的技术选型。

针对WEB请求的安全认证及授权,可使用的方案包括Spring Security和Apache Shiro,前者尽管基于Spring IoC,但是它的API过于复杂,对于基本的安全认证功能,我们可以尝试使用Apache Shiro。

业务层

业务层技术选型主要侧重于技术框架的可配置性、可扩展性、可伸缩性以及并发处理性能。所有这些都是为了承载业务的可拆解、可定制、可管理以及高效的处理。

当系统规模上升时,业务层与资源层最易称为性能瓶颈。而与资源层由第三方中间件负责数据的拆分、分布式以及集群而不需要额外的开发工作量不同,业务层则需要在相关框架的基础上进行合理的配置、开发以便满足上述架构要素。

业务层技术选型可以按照职责做进一步的分解(包括但不限于):

  • 业务组件的定义与管理。

  • 业务流程的组合编排。

  • 应用服务接口的导出及通信。

  • 异步任务处理及批处理。

业务组件的定义与管理,主要涉及到业务层容器的选项,当前主要的容器框架包括Spring、EJB3。

Spring框架的诞生源于作者对EJB(EJB3之前)这种重量的、紧耦合且低效的一站式架构的否定。它以其IoC容器为核心,以轻量级集成、风格一致的API来集成各种已有的第三方框架。学习使用Spring的过程中,你会发现各种技术组件的集成方案都有很大的相似性,使得你极易掌握使用。

容器(IoC)只是Spring的核心基础,通过与各种第三方框架(或Spring子项目)的集成,它可以用于应用系统架构各层的解决方案,如展现层的Spring MVC。你可以认为,Spring通过以轻量级集成第三方框架的方式,提供了J2EE应用系统的一站式解决方案。后续我们还会反复讲到与Spring集成的各种框架。

EJB3是在借鉴Spring IoC思想的基础上而形成的新一代J2EE规范,它抛弃了许多旧版本笨重的特性、降低了其复杂度,同样是轻量的基于POJO、注解以及IoC。依托于各种J2EE规范,EJB3同样可以提供常见的服务端功能的解决方案。

如果你的产品属于大型分布式企业应用,可以考虑使用EJB3,因为EJB是分布式企业应用的核心技术。如果开发的是一款轻量级的WEB应用,那么你应该选择Spring。当然,在EJB3变为轻量级架构之后,我们自然也可以对系统进行合理拆分,融合Spring和EJB3两项技术(如果确实有必要,至少理论上是可行的,虽然不推荐)。

针对业务流程的编排,首先是应用各种工作流框架。当前主要的工作流框架包括JBPM5、Activiti、OSWorkflow。JBPM5除了名字之外,与旧版本完全不同,它采用Drools作为引擎,从API上看,有些类似于知识库的概念,而且全部基于JBoss旗下的项目实现,显得过于厚重。而Activiti则是JBPM4的延续,也是目前使用最广泛的工作流框架,可以轻易与Spring等流行框架进行集成。OSWorkflow作为一款轻量灵活的工作流框架,也为你提供了另外一种选择。

除了工作流外,如果你的产品涉及到了企业应用集成或者本身就是一款企业应用集成产品,那么你还需要选择合适的企业应用集成框架。如果你的IoC框架选择的是Spring,那么你可以尝试使用Spring Integration,当然它提供的功能相对比较基础,如果需要使用更多的企业应用集成相关的功能,可以考虑Mule或者Apache Camel。Mule也默认整合了Spring,不需要额外的集成工作量。

对于应用服务的导出及通讯。如果你构建的只是一款RCP应用,可以选择采用二进制传输以取代HTTP明文传输。此时可以使用Spring HTTP Invoker或者Hessian,前者由于传输的是Java串行化对象,因此客户端也必须是Java环境,而后者客户端可以是其它语言编写(如C#)。

如果你构建的是一款基于Web Service的轻量的SOA应用,可以选择一款合适的Web Service框架。如Apache CXF或Axis2,CXF它也可以与Spring无缝集成。当然如果你使用EJB3,那么规范已经支持Web Service的发布。

如果你希望构建分布式服务系统或者微服务系统,可以考虑使用Zeroc Ice或者Dubbo(该框架由阿里开发,目前已停止维护,国内还有其它演进版本或者替代方案,总之选型时要充分考虑社区的活跃度以及框架的完善程度,尤其是对于这类会导致系统复杂度显著上升的框架,这会直接影响系统维护及定制化改造的成本)。这些框架提供了完善、高效的RPC通讯(如)以及服务分布式注册、管理及调用处理。

对于后台任务处理,如果场景相对简单,可以采用Java Timer,相对比较复杂的场景,可以采用更完善的Quartz。

同样,对于数据批处理,尤其是业务数据的事务化处理,可以采用Spring Batch。它支持多种批处理任务的调度:同步、异步以及分布式,还支持事务提交粒度、失败重试、异常任务处理以及任务状态监控等。对于大数据量的非事务批处理以及更复杂的数据分析,可以考虑大数据批处理框架,如Hadoop、Spark、Storm等。

持久层

持久层即“数据访问层”,主要是指对数据库的访问,可以分为关系型数据库访问和NoSQL数据库访问。对于数据访问框架,是否支持连接池、是否支持透明的缓存处理都会极大的影响数据操作性能。

如果采用关系型数据库存储数据,可以选择成熟的ORM框架,如Hibernate或者JPA。采用Spring Data项目,我们可以以面向接口的形式使用JPA和Hibernate。如果希望更灵活的操作SQL语句,可以采用Mybatis,配合mybatis-spring同样也可以提供面向接口的编程。

Spring Data项目不仅通过JPA提供了JDBC访问能力,而且还通过子模块以及第三方模块提供了多种数据源的访问能力,如Gemfire、LDAP、MongoDB、Redis、Apache Cassandra、Apache Solr以及Aerospike、Couchbase、DynamoDB、Elasticsearch、Hazelcast、Neo4j。不仅如此,它们的API风格基本都是一致的,因此大大降低了学习和使用成本。因此,如果采用NoSQL数据库,不妨尝试一下Spring Data。

资源层

我们说的资源层是可以供外部应用读取并写入数据、对数据进行有效管理的中间件产品。它们并不是我们应用开发的一部分,但是需要依托于它们来有效管理系统产生的数据。这些资源中间件包括数据库、缓存、文件系统、消息系统等。

资源层主要侧重于数据的一致性、完整性、存储方式、缓存、安全、访问性能等。

对于数据一致性要求高、数据集中存储的系统,通常采用关系型数据库进行管理。对于大型的企业信息系统,可以选择Oracle,对于轻量级的业务系统,可以选择更灵活的MySQL。

无论是Oracle还是MySQL,其数据最终的存储方式均为硬盘。对于实时性要求高的事务性系统,如电信、证券交易,可以采用内存数据库产品作为传统关系数据库的补充以提升事务处理性能。

当前主要的内存数据库产品如Oracle TimesTen、IBM SolidDB、VoltDB等,它们完全支持标准SQL并具备RDBMS的相关特性,提供了更高的事务处理能力和吞吐量,极低的处理延迟。尤其是TimesTen,可以轻易与Oracle集成,作为Oracle的数据前端,大大提升系统处理性能。

对于数据一致性要求不高、数据模型简单但是要求更加灵活、并发访问性能要求高、更易于弹性扩展的系统,可以考虑采用NoSQL数据库。NoSQL数据库与关系型数据库不同,并没有相似的架构,甚至两种不同的NoSQL数据库产品之间的架构差异非常大,因此不同的NoSQL产品适用于不同的应用场景。

如Redis更适合于做内容缓存,Cassandra和HBase多用于分布式文件系统,Couchbase和MongoDb用于Web应用的非结构化数据存储,Neo4j多用于社交及推荐系统。

对于数据缓存,主要用于加快数据查询速度。轻量级的应用可以考虑老牌的Ehcache,如果需要使用分布式缓存服务器,则可以选择Redis或者Memcached,两者各有所长,可根据具体的应用场景来进行取舍。

文件系统(由于操作系统本身就提供了文件系统,因此此处特指分布式文件系统)可以采用如HDFS、MogileFS、FastDFS以及TFS等。选择文件系统时,访问性能、可用性、可扩展性都是需要重点考虑的。如果是数据分析系统的数据文件存储,可以选择HDFS,方便与各种数据分析框架配合使用。如果是互联网文件的上传、下载及管理,可以选择MogileFS或者FastDFS。

消息系统通常用于企业应用集成或者业务功能异步解耦。在事务性处理中,消息系统要考虑消息的冗余、存储、可恢复、处理顺序、送达保证(丢失/重复)等。传统的消息系统,如RabbitMQ、RocketMQ 、ActiveMQ可以用于事务性的消息处理,它们有出色的性能以及可靠的消息发送及接收能力,而且可以很容易与现有的EAI框架集成(它们大多默认已支持常用的消息系统)。而对于日志消息的分布式处理(如与数据分析框架集成)则可以选择Apache Kafka,它有更好的分布式、高性能的处理能力。

部署选型

应用服务器是作为系统的最终运行环境,可以说是技术选型的最后一个环节。不仅如此,在一定程度上,系统开发技术选型的结果也影响了应用服务器选型。

如果你的应用采用了很多J2EE规范的特征,如EJB等,那么应优先选择WebSphere、Weblogic或者JBoss等成熟的企业级应用服务器产品,它们提供了更好的管理功能。

如果你的应用是一款轻量级WEB产品(不需要多余的J2EE功能),那么可以选择Tomcat、Jetty等轻量级Servlet容器。

当然,如果你开发的是微服务产品,并选择采用SpringBoot等作为微服务开发框架,那么便可以不用考虑应用服务器的选型,因为它已经内置了应用服务器(Tomcat、Jetty)以提供HTTP服务。它们中的任何一个都可以满足微服务架构的需要,而且你还可以轻易切换应用服务器方案。

总结

本篇文章成文仓促,从选型的角度讲,无论深度上还是广度上显然并不够完备。但是,我们希望阐述的是一种选型的方法。经由这种方法,你可以针对各种满足不同功能需求的技术框架进行选型。

当我们面临技术选型时,首先考虑的是要对产品架构进行合理的分解,同时对分解完的架构的每一部分,按照场景添加各种架构要素。针对备选技术框架,通过这些架构要素进行综合评估,最终选择一款最合适的框架。

当然,除了架构要素的约束,学习成本、框架成熟度、社区活跃度等也是我们需要重点关注的。

也许,经过一番评估及调查,你还是未发现那一款最适合你的框架。没关系,从胜出的几个框架中选择一个,不妨先尝试一下,只要产品架构保持良好的松耦合,将后续的框架升级及替换成本控制在尽可能小的范围内即可。在使用一段时间之后,可以根据采集的各项指标,再评估是否进行框架更换。

也许,你的顾虑在有限的时间内或者产品整个生命周期内本来就是多余的,它的瑕疵并不足以导致产品出现不可接受的缺陷或者增加超出预计的成本。更何况作为胜出者,它们本来就是各有利弊,谁也不存在所谓的压倒性优势。


GitChat 是一种全新的阅读/写作互动体验产品。一场 Chat 包含一篇文章和一场为文章的读者和作者定制的专属线上交流。本文出自 Chat 话题《如何选择最适合自己的技术架构?》

GitChat

一种全新的IT知识学习方式

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多