关于技术架构:PingCode-Flow技术架构揭秘

作者:PingCode 研发 VP@徐子岩本文是PingCode Flow系列文章的第四篇,不出意外的话,也是最初一篇。从去年五月份PingCode Flow正式上线,就在脑海中构思好了四篇文档的程序和大抵内容,从介绍研发自动化的现状、痛点和解决方案,到展现如何应用PingCode Flow来实现研发的自动化。而这最初一篇,则是心愿可能从纯技术角度展现PingCode Flow外部是如何工作的,如何在每天将近4000次规定执行的压力下,保障99%的规定都能在1秒内实现,同时反对程序、并行、判断、循环等简单的运行逻辑。同时,咱们也心愿在这篇文章中分享一下咱们是如何剖析和思考,并最终得出当初的架构。因而本文不会间接展现最终的设计后果,而是会论述咱们为什么要如此的设计,优缺点的衡量,以及开发过程中的重构。PingCode Flow系列文章传送门 研发自动化,你筹备好了么 5分钟带你玩转PingCode Flow 咱们是如何应用 PingCode Flow实现研发自动化治理的 PingCode Flow的实质是什么在前几篇文章中咱们提到,PingCode Flow是一款研发自动化工具。所谓的自动化,就是指在某个事件产生后,依照预约义的流程去实现一系列的操作。所以,实质上来讲,PingCode Flow是一个TAP(Trigger Action Platform)零碎。它由一个触发器和多个动作组成一个有序的执行规定,而后依照这个规定程序执行。因而,PingCode Flow在技术架构设计的时候,就是要确保这样的流程可能顺畅的运行。 数据结构:怎么定义一个规定确定了产品的外围指标后,第一件事就是要明确数据是如何定义的。依照下面的图示,在一个团队中用户可能会定义多个规定,而每个规定都蕴含了一个触发器和多个后续的动作。基于这个简略的需要,能够将规定的数据结构定义如下。这样,一个规定就蕴含了一个触发器以及它所蕴含的动作。而动作的序号决定了他们执行的先后顺序。这样的设计看来根本满足了目前的产品需要。然而咱们晓得,当初的PingCode Flow不仅仅反对上述的复线程序执行流程,还反对条件、并行、判断、循环等负责的执行流程。而且不止于此,咱们须要将上述这些流程自由组合,譬如并行外部有判断,判断外面有循环,循环中还有并行……通过这样简直有限的组合,让一个规定实现简直任意的流转。因而,上述简略的数据结构就齐全不能满足需要。而如何设计一个可能反对各种场景的规定,是摆在咱们PingCode Flow团队背后的第一个难题。如果咱们以「一个规定就是一系列动作的汇合」这个形式去思考,那么很难设计出绝对通用的数据结构。因为规定内的动作是由用户决定的,不可能穷举出所有可能的构造进去。然而能够尝试换一种思路来思考这个问题,也就是说,咱们不再把规定看做是触发器和动作的有序列表,而是将他们定义为一个链表。那么一个规定就是 触发器及下一个动作以后动作及下一个动作的汇合。如果咱们再将「触发器」和「动作」合并为「步骤」。那么一个规定就是第一个步骤以后步骤的下一个步骤这样,咱们对于规定和步骤的定义就能够对立为即规定并不关怀它外部的动作都是什么以及先后顺序,它只关怀第一个动作是什么。而每一个动作也仅仅关怀下一个动作是什么。对于并行、循环、判断等简单的流程,咱们只须要扩大对应动作的数据结构,就能够实现不同的排列组合。譬如对于并行,它的数据结构是这样的。「并行」外部的每个分支的第一个步骤ID保留在一个数组中,示意这个分支要执行的第一个步骤是什么。「并行」自身不关怀每个分支外面具体的流程是什么样的。它只关怀当所有分支都执行结束后,下一个步骤是什么。基于这样的构造,对于上述这个简单的规定 咱们的数据大抵是这样的。对于 步骤1 ,它只设置了下一个步骤是 步骤2 。 对于 步骤2 ,它外部有两个分支 步骤3 和 步骤4 ,下一个步骤是整个分支全副执行结束的 步骤10 。因而它的数据是这样的。 对于 步骤4 ,它是一个循环步骤。进入循环体的第一个步骤是 步骤6 ,而它实现循环后就会完结以后的分支操作。因而它的数据是这样的。 下一个步骤ID 为空,示意以后分支完结。 执行逻辑:如何反对各种类型的步骤当咱们确定了数据结构之后,规定内步骤的执行形式也随之确定了。和构造相似,当一个规定被启动后(咱们先不思考规定是如何被触发的),它会首先找到第一个动作的ID。相熟PingCode Flow的读者们都晓得,咱们零碎内预置了很多的动作,譬如设置工作项负责人、创立页面、变更测试用例状态等等。那么这些动作是怎么被执行起来的呢。首先,每一个动作都会有一个全局惟一的名称。当规定执行到这个步骤的时候,咱们会通过步骤的ID找到它的动作名。通过动作的名称定位代码中对应的理论执行逻辑。譬如「设置工作项负责人」这个动作,它的连接器名称是 project ,动作名称是 set_assignee 。代码大抵如下。 @action({ name: "set_assignee", displayName: "设置工作项负责人", description: "设置以后一个或多个工作项的负责人。", isEnabled: Is.yes, allowCreation: Is.yes, allowDeletion: Is.yes})export class AgileActionSetWorkItemsAssignee extends AgileWorkItemsAction<AgileActionSetAssigneeDynamicPropertiesSchema, AgileActionSetAssigneeDirectives, AgileActionSetAssigneeRuleStepEntity> { constructor() { super(AgileActionSetAssigneeRuleStepEntity, undefined, /* ... */); } protected onGetDirectivesMetadata(): DirectivesMetadata<Omit<AgileActionSetAssigneeDirectives, keyof AgileWorkItemsActionDirectives>> { /* ... */ }; protected onGetDynamicPropertiesMetadata(): PropertiesMetadata<Omit<AgileActionSetAssigneeDynamicPropertiesSchema, keyof AgileWorkItemsDynamicPropertiesSchema>> { /* ... */ }; protected async onExecute(context: ExecuteContextWrapper<AgileActionSetAssigneeDynamicPropertiesSchema, AgileActionSetAssigneeDirectives, AgileActionSetAssigneeRuleStepEntity>): Promise<RuleStepResult<AgileActionSetAssigneeDynamicPropertiesSchema>> { /* ... */ }}其中最次要的代码是 onExecute ,它将会在执行这个步骤时候被调用。当操作执行结束后,会将数据库中保留的的 下一个步骤ID 返回,规定执行引擎会去调用后续的步骤。这就是一个最简略的动作步骤,由零碎调用,执行具体的操作,而后返回下一个步骤的ID。除了一般的动作之外,PingCode Flow还反对条件、并行、判断、循环等简单的流程管制。和方才提到的动作一样,都是通过重写 onExecute 这个办法来实现的。以「条件」为例,它须要在判断为真的时候继续执行后续的步骤,为假则进行以后步骤。那么它的 onExecute 就是这样的。 ...

May 6, 2022 · 2 min · jiezi

关于技术架构:CDO玩跨界数据驱动有戏了

随着社会的提高,专业分工呈现了。很多人被限定在本人相熟的畛域里,边界越发清晰。然而,有这样一群人渴求扭转和摸索,在数智化转型的赛道上一直跨界和冲破。他们就是“跨界人”——首席数据官,俗称CDO。 提及CDO,行内人并不生疏。作为一个新型管理者,在各大媒体、大V齐刷刷的宣传下,曾被誉为企业数智化转型的救世主。 与CIO达成IT撑持业务的职责不同,CDO须要兼顾业务倒退和数据变现,兼顾和布局企业的数据资产,让数据赋能经营决策和策略落地。显然,CDO曾经进入最高决策层,且随着数智化转型的深刻,他们极有可能成为下一任CEO的不二人选。 可事实却偏偏大失所望,CDO的倒退不孚众望。有调研显示,CDO在中大型企业内装备的比例并不算高,只有一些头部的集团型企业和创新型企业正在尝试和布局。即便“每个企业都须要一个CDO”曾经被不少管理者所认可,然而仍造成了明天这样倒退不利的困境。 到底是为什么呢? CDO,能力过人的全才之将 随着数据的利用愈演愈烈,企业对CDO的能力要求也越来越高。 首先,他们要具备数据全生命周期治理的能力,包含数据的开掘、获取、应用、存储、散发等,放弃对数据的敏感性。 其次,CDO要具备数据驱动业务的能力。不仅是联合工具开掘数据的价值,更要洞察行业发展趋势、客户生产习惯及产业链上下游的变动,让数据引领业务倒退,让业务真正匹配企业改革。 第三,CDO要具备整合资源的能力。对于不少龙头企业来说,数智化转型不止是将企业外部的数据资产积淀下来,更要鼎力补充内部数据,进而欠缺翻新利用场景,推动产业降级。 例如,在供应链金融体系中,依附内部数据判断供应商是否衰弱;在大宗贸易的过程中,依据动态数据的变动,如天气、路线、突发事件等影响,判断整个行业的开采量、生产状况,最终为产品定价。 第四,CDO要具备生态整合的能力,这是他们将来最重要的能力之一。中国企业的崛起依附的是一直拓展的商业边界,只有建设一个凋谢、协同、凋敝的生态系统,能力拉开与竞争者的差距。这样一来,CDO不仅仅要思考数据,更要聚合生态资源,以“平台+生态”的组合模式引领产业倒退。 第五,CDO要具备搭建底层数据架构的能力,善用数据中台、低代码开发平台、系统集成平台等产品,以新技术、新产品疾速拓展翻新利用,打造更多新型的商业场景,买通生态的底层架构。 用友网络助理总裁罗小江认为,只有实现数据反哺业务、驱动业务增长、进步企业生产力,能力成为一名合格的CDO。反观目前业界普遍存在的CDO,能具备如此综合实力的人少之又少,因而这也不难理解CDO的倒退为何差强人意了。 随话说,临渊羡鱼,不如退而结网。这两年企业数智化转型进入“深水区”,业务重构成为现阶段的外围工作。可仍有不少企业偏离行业趋势,疏忽企业将来倒退,全面的将原有信息化零碎以新的技术重构一遍,导致失败的案例频现。 然而,这样的后果却意外的带动了CDO的倒退,让他们有机会基于新的技术架构和产品,从数据驱动的视角落实企业数智化建设,从将来市场竞争格局和策略高度帮忙企业商业翻新,并带动产业转型降级。 CDO的时代降临了。   从新定义“数据驱动” 随着大数据和AI的衰亡,人人都在谈数据驱动。就像一百个人心中有一百一个哈姆雷特一样,每个人对数据驱动的了解都不雷同。早在2014年,用友就确定了“数据驱动”的倒退模式。那么这么多年来,用友对数据驱动又有哪些新的了解呢? 用友网络董事长兼CEO王文京,曾在公开发言中示意,业务在整个数智化过程中变成了外围,数据是根底,最初通过智能化产生价值。 用友提出,在新形势下,企业应该利用数据洞察商业机会,围绕价值发明的过程,将数据作为经济因素纳入到企业经营流动中。通过提供正当的根据,缩小企业经营流动的危险,带来企业效益的增长。 而随着数据技术的倒退,企业数据将逐渐汇总到数据中台下,通过数据采集、计算、加工、剖析等配套工具,实现数据治理,造成数据资产,进行数据共享和凋谢、全程数据安全爱护等机制。 然而,数据驱动下的基础设施建设,不仅仅是一项技术工作,更是对企业组织、文化的升华。对此,罗小江强调,企业须要在组织上配套,建设平行于IT的DT组织,并在CDO的率领下推动数据驱动的文化建设,帮忙业务成长,辅助管理决策,基于数据谈话。 有人不禁要问,如此宏大的工程,企业该如何实现数据驱动呢? 用友公司倡议,企业不仅须要构建数据连贯和采集能力、批流一体的数据架构、全生命周期的数据管理平台,而且须要基于现有数据与实在业务场景交融,依附自动化、智能化伎俩优化业务流程。不仅如此,CDO还须要让数据死记硬背,并借助自助式剖析、图谱剖析、机器学习算法等技术进行高阶的数据价值开掘,翻新商业模式。 依据多年的实践经验,罗小江认为,因为不足采集工具,很多企业并没有历史数据留存,能够说在开始就败下阵来。而大部分具备数据的企业,也只有不到35%的数据是可用的。2020年信通院公布《大数据白皮书》也验证了罗小江的推断。 报告指出,企业经营中的数据只有56%可能被及时捕捉,而这其中仅有57%的数据失去了利用。也就是说,仅有32%的企业数据价值可能被激活。 为了解决这个问题,用友推出的AIoT智能物联网平台正是连贯和买通了云端的算力与边缘端的实时处理能力,帮忙企业在获取数据的同时,通过规定引擎对数据加工、流转、可视化出现,赋能企业智慧场景利用。 正如一家创新型的医疗器械流通企业,买通了从仓储运输到生产终端的业务链条,在数据互通互联的根底上优化现有服务模式,将医生的取件过程IoT化并收集应用反馈,而后反哺到生产端,实现数据驱动下的产品个性化定制,在升高病患医疗老本的同时,拓展业务边界。 再比方中国振华,这个国内产品体系最全、综合能力最强的电子元器件产业团体,通过“工业互联网标识解析二级节点”新型基础设施,放慢“振华工业云”建设,推动社会化大数据共享和产业优化降级,并将贵州工业化能力对外输入,为工业全产业链和经济增长注入新动能。 其实,AIoT智能物联网平台只是用友在数据综合能力方面的体现之一。正是通过一直的摸索和积攒,他们帮忙企业残缺构建了从数据认知、实际到价值发明的过程。    CDO青眼的赋能工具 为了达成企业数据价值变现的需要,用友打造了全套的数据产品和工具,并人造嵌入到BIP商业翻新平台中,以数据中台的模式出现。这样做能够帮忙企业采集海量多维的数据,提供从荡涤、会集、利用、建模、剖析到监测的一系列深加工增值服务,以残缺的全生命周期治理将数据资源资产化、价值化。 细数以后数据中台赛道,玩家泛滥纷纭。前有传统数据治理厂商御数坊,后有数据中台大厂阿里巴巴,还有互联网人工智能巨头百度等,然而,用友的数据中台仍是中大型企业的首选。他们为何独受CDO和企业的青眼呢? 首先,服务企业30余载,用友BIP|YonBIP积淀了残缺的数据能力体系,通过一体化的工具和平台帮忙企业开掘数据价值。 其次,基于在各行业的Kown-How,用友具备一批批高质量的业务专家、咨询师和数据架构师资源,能够帮忙企业洞察行业趋势,把脉业务痛点,梳理数据应用流程和标准,打造数据利用的方法论,从业余角度帮忙企业构建数据能力体系。 第三,用友数据中台并不是独自存在。作为用友BIP|YonBIP的一部分,它与业务中台造成双轮驱动的中台策略,与技术平台、低代码开发平台、利用集成平台等技术平台互通互联,造成一体化、端到端的数据解决方案。 第四,将来的企业将成为整个社会化商业网络的连接器,不仅须要连贯产业链上下游数据,连贯客户的数据,还要连贯政府的数据,连贯第三方的数据。那么,用友BIP|YonBIP作为构建产业互联网和社会化商业的根底能力,能够帮忙企业整合生态资源,买通企业内外部数据连贯,疾速构建社会商业子网络。比方通过供应商互洽升高企业业务危险,通过大数据危险模型升高优质中小企业融资老本等。 罗小江认为,只有借助工具将企业底层数据价值链拉通,设计合乎业务倒退的施行门路,能力从根本上解决企业的痛点。就好比BI我的项目尽管做了很多年,然而当初仍旧很多企业在重复施行,起因就在于没有拉通企业的数据链条,管理者看到的驾驶舱并不能实在的反馈企业的经营状态。 再比方不少纯技术厂商在做数据我的项目时,因为不具备相干的行业教训而主观疏忽了主数据平台,数据标准化做不好,数据治理不到位,导致大型团体企业在施行数据策略的开始就每每失败,大失信念。 用友数据中台产品总监陈宏志说,对于头部企业来说,从优良到卓越,技术创新尤为重要,然而没有一家企业会单纯的为工具买单。尽管,用友的技术实力并不是最强的,然而用友YonBIP不只是企业的商业翻新平台,更是整合了工具、能力、资源、生态于一体的多元服务体。而只有具备全面的综合能力,能力满足企业在数据层面的多种诉求,构建数据驱动的体系框架,实现商业翻新。这也正是其余数据厂商所不可匹敌的。    从数据驱动到价值驱动 从数据大国到数据强国,海量数据空前会聚和计算力指数级增长催生出前所未有的新产业、新业态、新模式,也由此涌现出一批高素质跨界人——CDO。 他们正以数据的视角,在用友这样弱小的搭档帮忙下,驱动企业构建数智化的数据体系,盘活外部数据资产。而后通过用友BIP|YonBIP买通产业链上下游数据,搭建平安的数据基础设施平台,让数据资产化、业务化、价值化,最终赋能企业转型降级。 如此一来,从流程驱动到数据驱动,从数据驱动到价值驱动,还边远吗?

December 22, 2021 · 1 min · jiezi

关于技术架构:德荣医疗廖毅技术共建数智化平台这样选

德荣医疗科技股份有限公司创立于2010年,已倒退为以医疗供应链数智化治理为外围,集销售服务、生产研发为一体的全国性集团化公司。德荣医疗秉承“所有为人民衰弱服务”的使命,为医院提供SPD精细化治理和医疗耗材、设施供给服务,为临床医护人员提供业余医学服务,助力公立医院高质量倒退;以德荣智仓+数智化中台为外围,为厂商提供医疗器械第三方物流服务,并提供全品类医疗器械仓配解决方案;搭建数智化医疗供应链治理平台,实现医疗器械产品数据全流程可追溯,助力晋升政府监管程度。 德荣医疗业务笼罩湖南省14个地州市,服务三级医院四十多家家,根本全覆盖率,二级以上医院二百多家,湖南省二、三级医院覆盖率达70%以上。 在用友主办的“2021企业数智化转型高峰论坛-湖南站“流动上,德荣医疗副总裁廖毅发表了《平台思维助力德荣医疗产业链降级》的主题演讲,同时在承受媒体拜访时,分享了德荣医疗构建数智化平台的初衷、平台构建思路、选型过程等。 廖毅次要负责德荣医疗的经营治理和信息化建设,堪称既懂业务又懂技术。德荣医疗的数智化平台建设之路离不开他前瞻思维与求实风格的影响。 以下是局部采访内容: 01.如何了解数字化?为什么要进行数字化转型?廖毅:我了解数字化的逻辑,首先要业务在线,在此基础上通过数据赋能,给业务带来翻新。数字化并不只是一个工具,而是一种价值状态,或者说曾经成为一种生存形式。通过数字化能够取得高配置资源,以取代以后落后的生产力,带来产品、渠道、服务的重构。 德荣进行数字化转型,次要是内外因素独特驱动的。从外部环境来看,迅速增长的市场加上医院需要降级加上国家宏观政策正在深度重构这个行将逾越万亿级的行业。从国家“衰弱中国2030”策略,咱们能够看到两个趋势:一:从因素数字化到流程数字化,再到服务数字化成为演变门路。二:数据价值开掘是竞争劣势,定制化服务是要害。数字资产治理决定将来。 从外部来看,将来,德荣将全力打造覆盖全国的一流医疗供应链治理服务平台,成为国内医疗器械行业供应链治理的引领者。预计2025年实现1000家SPD单干医院的施行布局,为医院提供“业余经营团队+软件+智能硬件”的全院级医用耗材精细化治理服务。这样的业务策略,要求咱们必须以医院为核心,进行产业链整合,实现数字化供应链高效率经营。这就必须要依附数字化、智能化的力量,打造一个数字化的供应链治理平台。 02.数字化转型过程中最大的艰难是什么?廖毅:有的人会认为转型中,没有资源是最难办的。我认为,资源是能够用幻想去整合的。反而观点认知和团队文化是最难的。 数字化建设不是一个短期利益,咱们不能要求通过数字化降级或者在数字化平台上能够疾速产生价值,这是不事实的。在观点上,咱们保持长期主义。在强调求实的根底上的长期主义。同时,利用一把手的布局,来建设激励规定,扭转团队传统思维,重塑数字化的企业文化。 03.为什么抉择以中台来构建数智化供应链平台?廖毅:抉择中台并不是必然的,因为咱们选的不是技术,而是技术能带来的价值。我认为中台不是一个标准化的产品,它更多是一种方法论。能够领导咱们怎么去构建数字化体系,或者IT架构。 每个人对中台的定义都不一样。德荣抉择中台,是因为它能够满足咱们业务的多样性,以及不确定性。咱们把中台作为根底,用这种办法去搭建零碎,日后数据的标准化,数据治理、数据利用等,都会更加正当、无利,可能疾速适应前端的需要,让业务变得更加麻利。同时,中台能够为咱们的IT团队赋能,补救技术力量有余的缺憾。 04.抉择数字化供应商的规范有哪些?廖毅:首先,要有较强的综合实力。比方服务企业的教训要丰盛,产品要成熟,品牌在企业客户中的口碑要很好等; 其次是技术的先进性。德荣医疗IT部门在30人左右,开发团队8人,其余都是运维。这样的小团队去自建平台不事实,在抉择平台时,也要抉择技术实力较强的产品; 再者,共建平台。平台提供服务商的模式,必须是征询加共建,或者征询加服务的模式。肯定要是共建平台。同时第三方的技术平台能够让咱们轻松驾驭新技术,疾速获取内部能力。平台提供商也要提供整体数字化征询的服务,为咱们带来整体的转型思路与布局门路,以及可落地的计划。 05.为什么最终抉择用友iuap构建平台?廖毅:德荣与用友属于“反动友情“。过来在ERP方面单干非常欢快,给咱们也积攒了不少信息化的成绩。 第一,在数字化阶段咱们要构建平台,首先要抉择一个相熟的合作伙伴。用友通过平台和生态的理念在为企业提供服务,他们违心和企业独特构建平台独特倒退。这和咱们的观点是统一的; 第二,用友的iuap平台领有前沿技术能力,它是YonBIP和用友所有产品的撑持底座,可能撑持这么多优良产品的平台,也曾经在大量不同行业和规模的企业数字化转型过程中起到了重要的撑持作用。咱们置信它的能力; 第三,求实的团队。在今年的单干教训中,咱们认为用友和德荣一样都是求实的团队。他们和咱们一起认真调研行业、企业当下的需要,继续理解上下游状况,一直刷新认知,独特为了构建一个医疗产业的数智化供应链平台; 第四,翻新的观点和能力。在iuap平台上,能够翻新更多业务场景。而且都是具备翻新思维的团队。项目组正在通过对现有客户、业务场景的理解,剖析市场机会,寻找更多可能翻新的因素。 06.年度工作总结廖毅:咱们大略能够分为三个阶段。 在1.0阶段,次要是通过数智中台底座建设,构建数智化根底能力。比方夯实技术、数据和平安等根底,对团队进行赋能建设,并且通过中台疾速构建利用场景;2.0阶段,通过一体化服务平台优化,实现精准服务和精益智能。为经营决策提供反对,基于数据与供应链服务用户一直优化;3.0阶段,通过产业化推广翻新,最终实现数智化的德荣,为整个医疗产业服务,提供产业互联网洽购、营销、运维等整体服务,实现产业共赢。 对于YonBIP 用友商业翻新平台YonBIP(Yonyou Business Innovation Platform),是用友擦用新一代信息技术,依照云原生(含服务)、元数据驱动,中台化和数用拆散的架构设计,涵盖平台服务、应用服务、业务服务与数据服务等状态,集工具、能力和资源服务为一体,服务产业与产业商业翻新的平台型、生态型的云服务群。 YonBIP具备数字化、智能化、高弹性、平安可信、平台化、生态化、全球化和社会化八大个性,书企业通过数智化实现商业翻新倒退的使能平台。YonBIP基于最新的大数据、人工智能、云计算、物联网&5G、挪动互联网、区块链等数智化技术,采纳云原生(含微服务)、元数据驱动、中台化、数用拆散等新一代技术架构,构建技术平台、数据中台、智能中台和业务中台,并聚焦财务、人力、协同、营销、洽购、制作、供应链、金融等八大外围畛域,全面撑持企业经营治理与产业价值链,使能企业数智化转型与倒退,推动社会商业提高。

July 1, 2021 · 1 min · jiezi

线下沙龙-5月11日-百度智能云网络技术实践分享强势来袭

云计算经过了十余年的发展,逐渐成为了我们个人和商业生活中不可或缺的组成部分。早期,云技术只是一个新鲜的词汇,而如今,随着全球云计算领域的活跃创新和我国云计算发展进入应用普及阶段,简单的云资源池之间的互联已经不能满足用户的需求,在此背景下,云网融合也逐渐将由简单互联向“云+网+业务”过渡。 来自 Gartner 的分析报告显示,2019 年的全球公有云市场规模将超越 2千亿美元,并将继续保持稳定增速。“上云”将成为各类企业加快数字化转型、鼓励技术创新和促进业务增长的第一选择甚至前提条件。对于企业而言,更多的需要考虑如何迁移重构以适配云端?如何让云更好地服务生产? 5月11日,百度携手msup邀请数位百度智能云技术大咖,深入业务层面,分享百度智能云网络技术实践经验及百度的技术理念与进展,让企业上云更简单! 会议详情 主题:创新网络,为智能加速 时间:2019年5月11日(周六) 13:30-17:30 地点:(北京东城)北三环东路36号环球贸易中心D座B1 S1 Club 形式:线下交流 规模:200人 TECH DAY日程 讲师简介 周磊,百度智能云高级产品经理,百度智能云计算及网络产品线负责人,有多年的数据中心网络及云计算领域工作经验,曾从事产品技术支持、产品设计与产品管理工作。现专注于百度智能云计算与网络方向产品的设计与管理,主导了多款计算网络产品的规划、设计与发布。王耀,百度智能云主任架构师,百度智能云IaaS方向技术负责人,多年云计算领域系统研发经验,专注于分布式存储和网络虚拟化方向。2010年加入百度,一直从事基础架构相关工作,领导多个百度分布式存储系统研发,并作为核心成员领导建设了百度智能云块存储和虚拟网络方向。 董玉池,百度智能云计算部资深研发工程师。现负责虚拟网络产品的技术研发。 张杨,百度智能云行业解决方案架构师,有多年的网络规划设计、CDN加速、云计算工作经验。现隶属于百度智能云事业群互联网教育行业团队,在线教育行业方案负责人。

April 29, 2019 · 1 min · jiezi

详解服务器端的项目框架

导读我一直相信这句话,他山之石可以攻玉。在自己能力不够时,多学习别人的东西。这样,对自己只有好处,没有坏处。因而,经过将近一年的工作,研读了公司所使用的框架。我本想往架构师的方向靠近,但,自己的能力可能还不够,因而,不断地给自己充电。公司的项目是前后端分离的,前端使用HTML5,css3、jquery、vue.js、bootstrap等,以SVN做代码托管。后端采用maven构建项目,以git lab做代码托管。肯定有人会问,这两个都是版本库,但它们有什么区别?如果想要了解的话,可以参考该文档:Svn与Git的区别。现在几乎所有的公司都采用maven构建项目,很少会采用导入jar包的方式依赖第三方框架。maven介绍maven构建的项目有很多好处,首先其可以统一管理jar包,也就是说,我们在项目中不用手动导入jar包,我们只要添加依赖即可,如代码所示:<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>${jdbc.version}</version></dependency>添加依赖之后,maven就会导入该jar包,导入jar包的顺序为:首先查找本地仓库,如果本地仓库没有,进入下面步骤。maven settings profile中的repository;pom.xml中profile中定义的repository。profile激活配置文件,比如正式环境的配置文件prd.properties和开发环境的Dev.properties文件。这也是打包的依据,是打开发环境的包,还是打正式环境的包,如图所示:pom.xml中的repositorys(定义多个repository,按定义顺序找);如果经过上面的步骤,没有找到相应的jar包,最后到我们的镜像(mirror)中查找。如果mirror中存在该jar包,从mirror中拷贝下来,存储到本地仓库中,进入到最初一步。如果mirror中也没有,maven就会报相应的错误。maven报出相应的错误时,也许,是我们本地没有该jar包,远程仓库也没有该jar包,我们可以参考这篇博客:在maven的pom.xml中添加本地jar包。它会教你如何创建本地仓库,并导入创建好的本地仓库。【备注】这篇博客以架构师的角度来讲解maven,所以,不具体讲解maven各个标签的含义。如果你想了解pom的各个标签的含义,可以参考这篇文档:pom.xml详解,或者,参考这篇教程:maven教程|菜鸟教程上面解说只是配置jar文件,但是,maven的功能远不止这些,从我们创建maven项目时,就已经进入到maven的开发环境中。maven项目有人和我说过,学一项知识,什么方式最快?那就是通过做项目。你在做项目的过程中,肯定会遇到很多问题,而你不得不去查找资料,从根源上认识到这个问题。因而,我以公司所做的某个项目为例,来讲解maven的依赖、继承、聚合等关系。我们所说的maven中的关系,其实就是pom的关系,即项目对象模型(Project Object Model)的简称。maven聚合首先,我们在创建cloudCodeSale项目时,就已经创建了父pom文件,如图所示:上图就是我们的父pom文件,你很清楚的看到gav坐标。同时,你从这张图上,也能得到其他信息,其打包方式是 pom,其还关联其他module,module名称和左边的列表名称一样。这就是我们所说的maven的聚合。父类同时聚合其子类。聚合的条件有两个:修改被聚合项目的pom.xml中的packaging元素的值为pom在被聚合项目的pom.xml中的modules元素下指定它的子模块项目既然所有的子模块的pom都继承父pom,为什么父pom要聚合子模块的pom文件?这个问题很好。因为对于聚合而言,当我们在被聚合的项目上使用Maven命令时,实际上这些命令都会在它的子模块项目上使用。这就是Maven中聚合的一个非常重要的作用。在实际开发的过程中,我们只需要打包(mvn install)父pom文件。我们在父pom上使用mvn celan、mvn compile和mvn package,其会自动对子模块:platform-core、platform-core-controller、portal/member-portal、portal/platform-portal、platform-cms、platform-cms-controller、platform-custom执行mvn celan、mvn compile和mvn package。没必要一个一个地打包,这样极容易出现错误,如图所示:maven继承机制如果很多模块都需要相同的jar包,我们可以单独写在一个pom中,其他模块使用该模块的公共部分,这就是我们常说的父类。不论是java语言,还是c++语言,或者现在的pom,其都体现这个思想。我们在上文也提到了子模块,现在模块platform-core讲解。继承父类的结构一般是这样的:<parent> <groupId>parent.groupId</groupId> <artifactId>parent.artifactId</artifactId> <version>parent.version</version> <relativePath>../pom.xml</relativePath> </parent> relativePath是父pom.xml文件相对于子pom.xml文件的位置,针对被继承的父pom与继承pom的目录结构是不是父子关系。如果是父子关系,则不用该标签;如果不是,那么就用该标签。因为在当前项目中,platform-core模块的目录的pom在父目录的pom中,其和父pom的目录结构是父子关系,因而可以省略relativePath该标签,如图所示:parent标签中的groupId、artifactId、version要和父pom中的标签中一致。maven的依赖关系正如我们所知道的,maven构建项目,不仅是因为其能够管理jar包,其还使模块化开发更简单了。因而,我们在开发的过程中,一般都分模块化开发。模块与模块之间的信息是不通的,但是,我们想要模块间的之间能够通信,这时,我们就想到了java中的依赖关系。比如,我们有一个模块,这个模块封装好了微信支付、支付宝支付、处理json格式、操作文件、ResultUtil、lambdaUtil、commonUtil等工具类,还有附件、头像、用户等实体类。这些工具类在任何项目中不会轻易改变,如果为了满足某些需求而不得不得修改,需要得到架构师的同意。因而,我们可以把它拿出来,单独定义为一个模块,也就是platform-core模块。但是,我们还有一个模块,在这个模块中,根据不同的项目,其定义不同的实体类、dao层类、事务层类、枚举类、接收前端传来参数封装成的query类、从数据库中取出的数据封装成的data类,到事务层可能会调用模块plateform-core中的方法,比如调用第三方系统接口的HTTPClientUtil.doPost(String url, Map<String, String> param),判断处理lambda表达式的LambdaUtil.ifNotBlankThen(String value, Consumer<String> function) ,等等。这个自定义类的模块,我们可定义为plateform-custom。plateform-custom需要用到platform-core中的方法,因而,这时,我们就需要考虑依赖关系,怎么添加对platform-core的依赖呢?如代码所示:<dependency> <groupId>com.zfounder.platform</groupId> <artifactId>platform-core</artifactId></dependency>我们这边是前后台分离的,后台用来录入数据,前台用来展示数据,因而,我们有一个portal目录,该目录下有两个子模块。一个是member-portal模块,一个是platform-portal模块,前者接收前台的接口参数,后者接收后台的接口参数。但不论哪个模块,都需要依赖plateform-custom中的事务层方法,同时,我们传的参数,可能信息分发的platform-cms-controller中的接口,也可能是核心接口platform-core-controller中的接口。因而,我们这里以member-portal模块来举例,依赖其他模块的代码如下:<dependencies> <dependency> <groupId>com.zfounder.platform</groupId> <artifactId>platform-core-controller</artifactId> </dependency> <dependency> <groupId>com.zfounder.platform</groupId> <artifactId>platform-cms-controller</artifactId> </dependency> <dependency> <groupId>com.zfounder.platform</groupId> <artifactId>platform-custom</artifactId> <version>1.0-SNAPSHOT</version> </dependency></dependencies>这些模块你会在上面的图片找得到。同时,我们来看member-portal的pom文件继承的父pom是怎么写的:补充上面的继承关系。这里面用到了<relativePath>../../pom.xml</relativePath>你会奇怪的是,为什么这里面用到了呢?其和父pom不是父子关系,而是孙子关系。这里使用到了两次点点,这是什么意思呢? ..表示上级目录。举个例子说明:比如,在我的服务器上的www目录中,有三个文件,分别是rsa_private_key.pem, rsa_private_key_pkcs8.pem, rsa_public_key.pem,还有一个testDir目录,testDir目录中还有目录testDir,现在我们通过cd ../testDir/testDir进入到子目录中,现在,我们想返回到www的根目录中,并查看rsa_public_key.pem文件的内容,因而,我们可以用cat ../../rsa_public_key.pem命令,其首先返回两级目录,然后找到rsa_public_key.pem文件并打开该文件。“被继承的父pom与继承pom的目录结构是不是父子关系”也不是绝对的,主要是选择继承者的pom中的子目录和父目录之间的关系,其中间隔了几层目录。maven激活文件激活文件在上文也提到了,我们为什么需要激活文件?如下面的两个配置文件,一个是测试环境的配置文件,名为platform-dev.properties,一个是正式环境的配置文件,名为platform-prd.properties。两个配置文件中都存在与数据库的连接,但是呢,数据库的ip地址是不一样的。如一下的代码所示:正式服的platform-prd.properties配置文件jdbc.url=jdbc:mysql://localhost/prd_databasejdbc.username=prd_usernamejdbc.password=prd_passwordjdbc.validationQuery=select 1 from dualjdbc.removeAbandonedTimeout=180jdbc.initialSize=10jdbc.minIdle=30jdbc.maxActive=100jdbc.maxWait=30000。。。测试服的platform-dev.properties配置文件jdbc.url=jdbc:mysql://intranet_ip/dev_databasejdbc.username=dev_usernamejdbc.password=dev_passwordjdbc.validationQuery=select 1 from dualjdbc.removeAbandonedTimeout=180jdbc.initialSize=10jdbc.minIdle=30jdbc.maxActive=100jdbc.maxWait=30000。。。我们的在配置文件中配置好了数据项,但是呢,我们怎么切换不同的配置文件呢?换句话说,我们怎么想要打正式服的包放到正式服上,怎么选择platform-prd.properties的配置文件呢?反之,怎么选择platform-dev.properties配置文件?这时,我们就用到了maven当中的profile标签,如下代码所示: <profiles> <profile> <id>dev</id> <activation> <activeByDefault>true</activeByDefault> </activation> <build> <filters> <filter>../../platform-dev.properties</filter> </filters> </build> </profile> <profile> <id>prd</id> <build> <filters> <filter>../../platform-prd.properties</filter> </filters> </build> </profile></profiles>这些配置文件时写在member-portal、platform-portal、plateform-core和plateform-cms、plateform-customer模块的pom中的。但是,plateform-core和plateform-cms的配置中的filter和上面连个略有差异,其filter是这样的 <filter>../platform-dev.properties</filter>和 <filter>../platform-prd.properties</filter>,这就涉及到目录点的问题。maven依赖第三方包maven项目除了依赖本项目的,其还会依赖第三方包,比如自动生成set和get方法的lombok包,处理json格式的阿里巴巴下的fastjson包等等,我们也可以使用这种格式的依赖:<!–mysql jdbc驱动包 开始–><dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>${jdbc.version}</version></dependency><!–mysql jdbc驱动包 结束–>开发常用的jar包lombok<!– lombok驱动包 开始–><dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.16.10</version></dependency><!– lombok驱动包 开始–>我们在没有使用lombok之前,经常手动创建javabean的set个get方法,使用这个框架之后,其以注解的方式,可以自动生成set和get方法。同时,其强大的功能远不止这些,也可以生成无参构造器、全参构造器,指定参数构造器、重写toString方法、重写Hashcode、equals方法等等。如代码所示:/** * Created By zby on 17:37 2019/1/30 /@AllArgsConstructor@NoArgsConstructor@Data@ToString@EqualsAndHashCodepublic class Address { /* * 收货人 / private String consignee; /* * 手机号码 / private String phone; /* * 所在地区 / private String area; /* * 详细地址 / private String detail; /* * 标签 / private AddressTagEnum addressTag;}想要更深层次的了解这个框架,可以参考这个博客:Lombok使用详解fastjson<!– fastjson驱动包 开始–><dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.28</version></dependency><!– fastjson驱动包 结束–>fastjson是阿里巴巴开源的框架,其用来处理服务器端的json格式的数据。比如,我们需要将服务端的对象以json(JavaScript object Notation,js对象标记)格式传输到前端,但是,如果自己手动创建的话,势必会非常的麻烦,于是,我们借助这个框架,帮助我们生成json格式的对象。同时,如果我们要调用第三方接口,比如调用连连绑定银行卡的接口,其返回给我们的也是json对象的数据。但是,我们需要将其转化为我们定义的对象,调用其save方法,保存到数据库中,对账所用。对于,将对象转化为json格式的对象,如代码所示:@Testpublic void test() {// 地址 Address address = new Address(); address.setAddressTag(AddressTagEnum.ADDRESS_TAG_COMPANY); address.setArea(“杭州市….”); address.setConsignee(“zby”);// 用户 User user = new User(); user.setHobby(HobbyEnum.HOBBY_DANCING); user.setGender(“男”); user.setUserName(“蒋三”);// 订单 OrderSnapshot orderSnapshot = new OrderSnapshot(); orderSnapshot.setAddress(address); orderSnapshot.setId(1L); orderSnapshot.setName(“复读机”); orderSnapshot.setOrderNo(Long.valueOf(System.currentTimeMillis()).toString() + “1L”); orderSnapshot.setUser(user); System.out.println(JSON.toJSON(orderSnapshot));}其输出结果如图所示:但是,类似于解析json格式的数据,不只有fastjson框,还有org.json框架、Jackson框架。但经过有人验证呢,还是fastjson的效率更高一些。可以参考这篇博客:Gson、FastJson、org.JSON到底哪一个效率更高,速度更快org.json也是通过maven配置的,如代码所示:<!– json驱动包 开始–><dependency> <groupId>org.json</groupId> <artifactId>json</artifactId> <version>20140107</version></dependency><!– json驱动包 开始–>如果想要深层次了解org.json,可以参考这篇博客:Java使用org.json.jar构造和解析Json数据想要更深层次的了解fastjson,可以参考这篇博客:Fastjson 简明教程spring相关配置如果从事java-web开发,一般会用到spring框架,这方面的教程太多了,笔者就不在这介绍,但我们会用到spring的这些框架:<!–spring 相关配置开始–><dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId></dependency><dependency> <groupId>org.springframework</groupId> <artifactId>spring-context-support</artifactId></dependency><dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId></dependency><dependency> <groupId>org.springframework</groupId> <artifactId>spring-orm</artifactId></dependency><dependency> <groupId>org.springframework</groupId> <artifactId>spring-aspects</artifactId></dependency>spring会集合很多框架,比如具有拦截效果的shiro框架,持久层的hibernate和mybatis框架等等。spring通过配置文件通过注解或者配置文件的方式,实现依赖注入(dependency injection)和控制反转(inversion of control)。通过@controller遍历相应的接口,实现前后端的接口对接。spring可以实现面向切面编程,实现某个业务点的单一执行。比如,专门处理事务的业务点。spring并不难,很快就能掌握到其精髓。如果想深入了解,可以参考这篇教程:Spring教程hibernate框架hibernate框架就类似于mybatis框架,其专门处理持久层的技术。我们将瞬时状态的对象存储到数据库中,变成持久状态的对象。我们也可以从数据库中取数据,以瞬时态的对象返回到前端。这就是一存一取的框架。其可以使用注解的方式创建数据表,也可以通过配置文件创建瞬时态的对象。但就目前为止,在很多情况下,我们都是通过注解的方式,实现数据表的创建。导入hibernate相关的框架,如下所示:<!–hibernate相关配置 开始–><!–hibernateh核心框架–><dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-core</artifactId></dependency><!–hibernateh验证器–><dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-validator</artifactId></dependency><!–hibernateh缓存技术–><dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-ehcache</artifactId></dependency> <!–Java Persistence API ORM映射元数据 查询语言–><dependency> <groupId>org.hibernate.java-persistence</groupId> <artifactId>jpa-api</artifactId></dependency><!–hibernate相关配置 结束–>hibernate和mybatis具有同样的功能,如果想要了解mybatis,可以参考这篇教程:mybatis教程想要深入理解hibernate,可参考这篇教程:hibernate教程_w3cschooljbdc驱动包我们上面说了hibernate框架,但前提是,我们需要导入jdbc的框架包,如代码所示: <!– 数据库驱动包相关 开始–><dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId></dependency><!– 数据库驱动包相关 结束–>这个驱动包主要处理java与数据库的连接,实现数据的增、删、改、查。我们没有用到这个包,但是hibernate用到了这个包,因而,我们需要导入这个包,以免数据库报错。alibaba的Druid包这个包有什么用吗?我们既然通过hibernate实现与数据库的交互,那么就需要在初始化时创建连接池。现在连接池有很多种,我们为什么选择了它Druid。<!– Druid驱动包相关 开始–><dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId></dependency><!– Druid驱动包相关 结束–>它是目前最好的数据库连接池,在功能、性能、扩展性方面,都超过其他数据库连接池,包括DBCP、C3P0、BoneCP、Proxool、JBoss DataSource。Druid已经在阿里巴巴部署了超过600个应用,经过一年多生产环境大规模部署的严苛考验。Druid是阿里巴巴开发的号称为监控而生的数据库连接池!时代在变化,我们也应该应世而生,与时俱进,才不会和时代脱轨。我们使用Druid来实现数据的配置,如代码所示: <bean id=“dataSource” class=“com.alibaba.druid.pool.DruidDataSource” init-method=“init” destroy-method=“close”> <property name=“driverClassName” value=“com.mysql.jdbc.Driver”/> <property name=“url” value="${jdbc.url}"/> <property name=“username” value="${jdbc.username}"/> <property name=“password” value="${jdbc.password}"/> <property name=“maxActive” value="${jdbc.maxActive}"/> <property name=“initialSize” value="${jdbc.initialSize}"/> <property name=“removeAbandoned” value=“true”/> <property name=“removeAbandonedTimeout” value="${jdbc.removeAbandonedTimeout}"/> <property name=“testOnBorrow” value=“true”/> <property name=“minIdle” value="${jdbc.minIdle}"/> <property name=“maxWait” value="${jdbc.maxWait}"/> <property name=“validationQuery” value="${jdbc.validationQuery}"/> <property name=“connectionProperties” value=“clientEncoding=UTF-8”/></bean>你们可以看到,value值是形参,而不是具体的值。因为我们根据不同的打包方式,其传入形参对应的实参不同。这也就是我们上文提到的,platform-dev.properties和platform-prd.properties配置文件,以及maven配置的激活文件。如果想要深入了解阿里巴巴的Druid框架,可以参考这篇博客:DRUID连接池的实用 配置详解阿里云短信短信业务一般固定不变,由架构师封装好,其他人直接调用即可,因而,该框架可以写进plateform-core模块中,其配置的代码如下所示:<!– 阿里短息驱动包配置 开始 –><dependency> <groupId>com.aliyun</groupId> <artifactId>aliyun-java-sdk-dysmsapi</artifactId></dependency><dependency> <groupId>com.aliyun</groupId> <artifactId>aliyun-java-sdk-core</artifactId></dependency><!– 阿里短息驱动包配置 结束 –>日志相关配置我们在开发的过程中,经常会使用到日志,来记录相应的错误、警告、信息。比如,我在使用连连支付做提现业务时,提现成功后其会回调我们的接口,从而显示在服务端的Tomcat页面中。再比如,我们在登录时,其会在Tomcat中显示相关信息,如图所示:我们都知道日志分为几种级别。这里就不再赘述了。日志分为好多种,我们推荐使用slf4j+logback模式。因为logback自身实现了slf4j的接口,无须额外引入适配器,另外logback是log4j的升级版,具备比log4j更多的优点,我们可以通过如下配置进行集成:<!– 日志驱动包配置 开始 –><dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>1.7.21</version></dependency><dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.1.7</version></dependency><dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-core</artifactId> <version>1.1.7</version></dependency><!– 日志驱动包配置 结束 –>我们这时就用到了plateform-prd.properties文件和plateform-dev.properties文件,因为,我们需要在这里面配置日志的输出位置。然后,在logback.xml中以参数的形式,调用文件中的输出位置,如图所示:如果想要了解更多的配置文件信息,请参考这篇博客:使用 logback + slf4j 进行日志记录commons家族我们在开发的过程中,经常用到Commons家族的驱动包,比如文件操作的io包,MD5加密和解密用的codec包。当然,我们也会用到java自带的local_policy驱动包,但有时需要替换替换该驱动包,否则,就会报出Illegal Key Size的错误。文件上传下载的fileupload驱动包,操作字符串类型的lang3包,配置的驱动包如下所示:<!–comon包相关配置–><commons-io.version>2.4</commons-io.version><commons-lang3.version>3.4</commons-lang3.version><commons-codec.version>1.10</commons-codec.version><commons-fileupload.version>1.3.1</commons-fileupload.version><!– apache common 开始 –><dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>${commons-lang3.version}</version></dependency><dependency> <groupId>commons-io</groupId> <artifactId>commons-io</artifactId> <version>${commons-io.version}</version></dependency><dependency> <groupId>commons-codec</groupId> <artifactId>commons-codec</artifactId> <version>${commons-codec.version}</version></dependency><dependency> <groupId>commons-fileupload</groupId> <artifactId>commons-fileupload</artifactId> <version>${commons-fileupload.version}</version></dependency><!– apache common 结束 –>lang3包我们可以用其分割字符串,判断字符串是否为空格,判断字符串是否为空等等。如以下代码所示:public static void main(String[] args) { String keyword = “1-1-2”; if (StringUtils.isNotBlank(keyword)) { System.out.println(“keyword = " + keyword); } String[] keys = StringUtils.split(keyword, “-”); for (String key : keys) { System.out.println(“key=” + key); }}我们有时还会用其操作时间类,比如格式化时间等等,入一下代码:@Testpublic void testDate(){ String date1= FastDateFormat.getInstance(“yyyy-MM-dd”).format(System.currentTimeMillis()); System.out.println(“System.currentTimeMillis:"+date1); String date2= FastDateFormat.getInstance(“yyyy-MM-dd”).format(new Date()); System.out.println(“new Date:"+date2);}其功能远不止这些,具体可以参考这篇博客:commons-lang3工具包io包见名知意,IO即input和output的简写,即输入流和输出流。因而,我们经常使用到java自带的InputStream或FileInputStream的字节输入流,以及OutputStream或FileOutputStream的输出流。如果更高级的话,那么,就使用到了带有缓存效果的bufferReader输入流和bufferWrite输出流。这里面用到了装饰设计模式。什么是装修设计模式,可以自行学习。上面的操作比较复杂,我们就用到了apache下的io驱动包。这里就当做抛砖引玉了,想要有更深的了解,可以参考这篇博客:io包工具类codec包codec包是Commons家族中的加密和解密用的包,这里不做任何解释,具体可以参考这篇博客:Commons Codec基本使用fileupload包我们如果做java-web开发,经常会有文件上传和文件下载的功能。这时,我们就考虑到了Apache下面的 fileupload包,这可以完成文件的上传和下载。这里的文件不单单是指doc文件,也会指图片和视频文件。具体想要有更多的理解,可以参考这篇文档:commons-fileupload上传下载shiro包我们在web开发时,经常会涉及到权限问题,比如哪些页面不需要登录就能看,而哪些页面只能登录才能看。当用户在打开该页面之前,就进入到相应的过滤器中,来做相关业务的判断。如果通过,就进入到controller层;不通过,则抛出相应的异常给前端。这里就需要相应的权限控制。说到权限控制,我们不得不提到shiro框架。其有三大核心组件Subject, SecurityManager 和 Realms。这个百度百科上也说了,可以查看其解说内容:java安全框架 <!– shiro驱动包 开始 –><dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-core</artifactId></dependency><dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId></dependency><dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-web</artifactId></dependency><dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-ehcache</artifactId></dependency><!–shiro驱动包 结束 –>公司也会做相应的配置,配置如下:<?xml version=“1.0” encoding=“UTF-8”?><beans xmlns:util=“http://www.springframework.org/schema/util" xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance" xmlns=“http://www.springframework.org/schema/beans" xsi:schemaLocation=“http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd"> <!– 缓存管理器 –> <bean id=“cacheManager” class=“com..shared.framework.SpringCacheManagerWrapper”> <property name=“cacheManager” ref=“springCacheManager”/> </bean> <bean id=“springCacheManager” class=“org.springframework.cache.ehcache.EhCacheCacheManager”> <property name=“cacheManager” ref=“ehcacheManager”/> </bean> <bean id=“ehcacheManager” class=“org.springframework.cache.ehcache.EhCacheManagerFactoryBean”> <property name=“configLocation” value=“classpath:ehcache.xml”/> </bean> <!– 凭证匹配器 –> <bean id=“credentialsMatcher” class=“com.*.RetryLimitHashedCredentialsMatcher”> <constructor-arg ref=“cacheManager”/> <property name=“hashAlgorithmName” value=“md5”/> <property name=“hashIterations” value=“2”/> <property name=“storedCredentialsHexEncoded” value=“true”/> </bean> <!– Realm实现 –> <bean id=“userRealm” class=“com..shared.web.listener.MemberSecurityRealm”> <!–<property name=“credentialsMatcher” ref=“credentialsMatcher”/>–> <property name=“cachingEnabled” value=“false”/> <!–<property name=“authenticationCachingEnabled” value=“true”/>–> <!–<property name=“authenticationCacheName” value=“authenticationCache”/>–> <!–<property name=“authorizationCachingEnabled” value=“true”/>–> <!–<property name=“authorizationCacheName” value=“authorizationCache”/>–> </bean> <!– 会话ID生成器 –> <!–<bean id=“sessionIdGenerator” class=“org.apache.shiro.session.mgt.eis.JavaUuidSessionIdGenerator”/>–> <!– 会话Cookie模板 –> <bean id=“sessionIdCookie” class=“org.apache.shiro.web.servlet.SimpleCookie”> <constructor-arg value=“platform-portal-sid”/> <property name=“httpOnly” value=“true”/> <property name=“maxAge” value=“7200”/> </bean> <!– 会话管理器 –> <bean id=“sessionManager” class=“org.apache.shiro.web.session.mgt.DefaultWebSessionManager”> <property name=“globalSessionTimeout” value=“43200000”/> <property name=“deleteInvalidSessions” value=“true”/> <property name=“sessionIdCookieEnabled” value=“true”/> <property name=“sessionIdCookie” ref=“sessionIdCookie”/> </bean> <!– 安全管理器 –> <bean id=“securityManager” class=“org.apache.shiro.web.mgt.DefaultWebSecurityManager”> <property name=“realm” ref=“userRealm”/> <property name=“sessionManager” ref=“sessionManager”/> <property name=“cacheManager” ref=“cacheManager”/> </bean> <!– 相当于调用SecurityUtils.setSecurityManager(securityManager) –> <bean class=“org.springframework.beans.factory.config.MethodInvokingFactoryBean”> <property name=“staticMethod” value=“org.apache.shiro.SecurityUtils.setSecurityManager”/> <property name=“arguments” ref=“securityManager”/> </bean> <!– Shiro的Web过滤器 –> <bean id=“shiroFilter” class=“org.apache.shiro.spring.web.ShiroFilterFactoryBean” depends-on=“securityManager,memberShiroFilerChainManager”> <property name=“securityManager” ref=“securityManager”/> </bean> <!– 基于url+角色的身份验证过滤器 –> <bean id=“urlAuthFilter” class=“com.zfounder.platform.core.shared.web.filter.UrlAuthFilter”> <property name=“ignoreCheckUriList”> <list> <value>//common/enums/</value> <value>//security/</value> <value>//common/dd/</value> <value>//pictures/</value> <value>//common/sms/</value> <value>//wx/</value> </list> </property> </bean> <bean id=“memberFilterChainManager” class=“com.zfounder.platform.core.shared.web.listener.CustomDefaultFilterChainManager”> <property name=“customFilters”> <util:map> <entry key=“roles” value-ref=“urlAuthFilter”/> </util:map> </property> </bean> <bean id=“memberFilterChainResolver” class=“com..shared.web.listener.CustomPathMatchingFilterChainResolver”> <property name=“customDefaultFilterChainManager” ref=“memberFilterChainManager”/> </bean> <bean class=“org.springframework.beans.factory.config.MethodInvokingFactoryBean” depends-on=“shiroFilter”> <property name=“targetObject” ref=“shiroFilter”/> <property name=“targetMethod” value=“setFilterChainResolver”/> <property name=“arguments” ref=“memberFilterChainResolver”/> </bean> <!– Shiro生命周期处理器–> <bean id=“lifecycleBeanPostProcessor” class=“org.apache.shiro.spring.LifecycleBeanPostProcessor”/></beans>想要对其有更深的理解,请参考这篇博客:Shiro讲解工具类<!–汉字转拼音开源工具包–> <dependency> <groupId>com.github.stuxuhai</groupId> <artifactId>jpinyin</artifactId></dependency><!–网络爬虫的驱动包–><dependency> <groupId>org.jsoup</groupId> <artifactId>jsoup</artifactId></dependency><!–验证码生成工具包–><dependency> <groupId>com.github.penggle</groupId> <artifactId>kaptcha</artifactId></dependency><!–发送邮件–><dependency> <groupId>javax.mail</groupId> <artifactId>mail</artifactId></dependency>因为篇幅的限制,这里就不再细说了,如果想要更深层次的了解,可以参考以下博客:汉字转拼音开源工具包Jpinyin介绍爬虫+jsoup轻松爬知乎使用kaptcha生成验证码使用javax.mail发送邮件图片验证码的配置文件如下:<?xml version=“1.0” encoding=“UTF-8”?><beans xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance" xmlns=“http://www.springframework.org/schema/beans" xsi:schemaLocation=“http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd" default-lazy-init=“true”> <bean id=“captchaProducer” class=“com.google.code.kaptcha.impl.DefaultKaptcha”> <property name=“config”> <bean class=“com.google.code.kaptcha.util.Config”> <constructor-arg> <props> <prop key=“kaptcha.border”>${kaptcha.border}</prop> <prop key=“kaptcha.border.color”>${kaptcha.border.color}</prop> <prop key=“kaptcha.textproducer.font.color”>${kaptcha.textproducer.font.color}</prop> <prop key=“kaptcha.textproducer.char.space”>${kaptcha.textproducer.char.space}</prop> <prop key=“kaptcha.textproducer.font.size”>${kaptcha.textproducer.font.size}</prop> <prop key=“kaptcha.image.width”>${kaptcha.image.width}</prop> <prop key=“kaptcha.image.height”>${kaptcha.image.height}</prop> <prop key=“kaptcha.textproducer.char.length”>${kaptcha.textproducer.char.length}</prop> <prop key=“kaptcha.textproducer.char.string”>1234567890</prop> <prop key=“kaptcha.textproducer.font.names”>宋体,楷体,微软雅黑</prop> <prop key=“kaptcha.noise.color”>${kaptcha.noise.color}</prop> <prop key=“kaptcha.noise.impl”>com.google.code.kaptcha.impl.NoNoise</prop> <prop key=“kaptcha.background.clear.from”>${kaptcha.background.clear.from}</prop> <prop key=“kaptcha.background.clear.to”>${kaptcha.background.clear.to}</prop> <prop key=“kaptcha.word.impl”>com.google.code.kaptcha.text.impl.DefaultWordRenderer</prop> <prop key=“kaptcha.obscurificator.impl”>com.google.code.kaptcha.impl.ShadowGimpy</prop> </props> </constructor-arg> </bean> </property> </bean></beans>里面的占位符来源于plateform-dev.properties或者plateform-prd.properties,这就是我们maven激活的配置文件的作用。测试依赖包我们在开发完一个功能后,首先会想到测试它走不走得通。我们可能会在main方法中测试,一个项目类中可以写多个main方法。如果每个功能类中都写一个main方法,未免会造成代码的混乱,一点都不美观和儒雅。java为什么一直推崇面向对象,任何在现实中真实的、虚拟的事物,都可以将其封装为为java中的对象类。对象与对象之间以方法作为消息传递机制,以属性作为数据库存储的机制。如果我们在每个功能中都写一个main方法,势必会破坏这种对象的美观性。因而,我们把测试的数据以对象的方式操作,这样,将其封装为一个测试包,比如,在我写的spring框架中,就把测试类单独拿出来,如图所示:<!– 测试依赖包 开始–><!– spring test –><dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>${spring.version}</version></dependency><!– 路径检索json或设置Json –><dependency> <groupId>com.jayway.jsonpath</groupId> <artifactId>json-path</artifactId> <version>${jsonpath.version}</version> <scope>test</scope></dependency><!– testng –><dependency> <groupId>org.testng</groupId> <artifactId>testng</artifactId> <version>${testng.version}</version></dependency><!– 单元测试的powermock –><dependency> <groupId>org.powermock</groupId> <artifactId>powermock-module-testng</artifactId> <version>${powermock.version}</version></dependency><dependency> <groupId>org.powermock</groupId> <artifactId>powermock-api-mockito</artifactId> <version>${powermock.version}</version></dependency><!–测试相关 结束–>以上是几种测试包的依赖,一个是spring的测试包,这里由于篇幅的限制,就不做详细的介绍了,网上有很多这方面的教程,想要深入的了解,可参考这篇博客:Spring-Test(单元测试)我们有时也会用到TestNG框架,它是Java中的一个测试框架,类似于JUnit 和NUnit,功能都差不多,只是功能更加强大,使用也更方便。测试人员一般用TestNG来写自动化测试,开发人员一般用JUnit写单元测试。如果你是测试人员,想对其有更全面的了解,可以参考这篇教程:TestNG教程,或者这篇博客::testNG常用用法总结如果想要更深层次的了解powermock,可以参考这篇博客:PowerMock从入门到放弃再到使用如果想要更深层次的了解JsonPath,可以参考这篇博客:JsonPath教程图片处理我们在开发的过程中,会把图片存放到服务器的某个文件夹下,即某个磁盘上。如果图片过大,会占用服务器的磁盘,因而,我们需要将图片缩略,来减少对内存的占用。这时,我们如果使用java原生的图片缩略图,是非常复杂的,因而,我们可以使用以下框架对图片进行操作。<!–图片处理驱动包 开始–><dependency> <groupId>net.coobird</groupId> <artifactId>thumbnailator</artifactId></dependency><!–图片处理驱动包 结束–>这里不再细说,想要有更多的了解,可以参考这篇博客:Thumbnailator框架的使用Excel操作我们在工作的过程中,经常会将数据导出到Excel表,或将Excel表的数据导入数据库。我们以前使用poi框架,但是,超过一定量的时候,会占用大量的内存,从而降低导入的效率。阿里巴巴现在开放出操作Excel表的easyexcel框架,对百万级的导入影响不是很大。以下是maven配置两个驱动依赖:<!–阿里巴巴的easyexcel驱动包 –><dependency> <groupId>com.alibaba</groupId> <artifactId>easyexcel</artifactId> <version>{latestVersion}</version></dependency><!–poi驱动包 –><dependency> <groupId>org.apache.poi</groupId> <artifactId>poi</artifactId> <version>${poi.version}</version></dependency><dependency> <groupId>org.apache.poi</groupId> <artifactId>poi-ooxml</artifactId> <version>${poi-ooxml.version}</version></dependency>这两个就不再细说,如果想要对easyexcel更深的了解,可以参考这篇博客:alibaba/easyexcel 框架使用。如果想要对poi有更深的了解,可以参考这篇博客:Apache POI使用详解guava包我们在开发的过程中,有时会用到guava驱动包。它是为了方便编码,并减少编码错误,用于提供集合,缓存,支持原语句,并发性,常见注解,字符串处理,I/O和验证的实用方法。使用它有以下好处:标准化 - Guava库是由谷歌托管。高效 - 可靠,快速和有效的扩展JAVA标准库优化 -Guava库经过高度的优化。同时,又有增加Java功能和处理能力的函数式编程,提供了需要在应用程序中开发的许多实用程序类的,提供了标准的故障安全验证机制,强调了最佳的做法等等。它的宗旨就是:提高代码质量、简化工作,促使代码更有弹性、更加简洁的工具。我们在项目中的配置包为:<!–guava驱动包 开始–> <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId></dependency><!–guava驱动包 结束–>如果想要对其有更深的了解,可以参考这篇教程:guava入门教程freemarker包我们在开发的过程中,也许会用到这个框架。为什么要用到这个框架呢?我们有时需要动态地将xml文件转为doc文件,这个时候,就用到了freemarker包,如图所示:截图不是很全面,你会看到画红框的部分,这是一种占位符的标记,就相当于java中的形参一样。 当用户点击前端的下载按钮时,有些数据是无法直接转换成doc的,因为我们先把数据写进xml中,再将xml转化为doc。具体如何转换的可以参考该博客:Java将xml模板动态填充数据转换为word文档我们可以引用这个包: <!–freemarker驱动包 开始–><dependency> <groupId>org.freemarker</groupId> <artifactId>freemarker</artifactId> <version>${freemarker.version}</version></dependency><!–freemarker驱动包 结束–>由于篇幅限制,想要详细了解,可以参考这篇手册: freemarker在线手册servlet驱动包我记得当时在学java-web开发时,最开始用的就是servlet。接收客户端的输入,并经过一系列DB操作,将数据返回给客户端。但使用纯servlet不利于可视化界面。后来,使用了JSP开发,其是可视化界面。但是,当我们启动Tomcat后,JSP通过JSP引擎还是会转为servlet。从本质上来说,JSP和servlet是服务端语言。我最初用servlet和JSP开发的源码地址:图书馆项目后来,工作了以后。后端就用了springMVC,hibernate框架等,前端使用的是HTML、jQuery等。慢慢地脱离了JSP和servlet。但是,并没与完全与servlet分隔开,我们还时不时会用到servlet的一些类,比如HttpServletRequest,HttpServletResponse等类。既然使用了spring MVC框架,为什么还要用servlet的东西,比如,我们在导入和导出时,一个是接收前端导入的请求,一个是响应前端导出的请求。响应前端导出的代码,这里就用到了响应private static void downloadExcel(HttpServletResponse response, File newFile, String fileName) throws IOException { InputStream fis = new BufferedInputStream(new FileInputStream( newFile)); String substring = fileName.substring(fileName.indexOf(”/”) + 1); byte[] buffer = new byte[fis.available()]; fis.read(buffer); fis.close(); response.reset(); response.setContentType(“text/html;charset=UTF-8”); OutputStream toClient = new BufferedOutputStream( response.getOutputStream()); response.setContentType(“application/x-msdownload”); String newName = URLEncoder.encode( substring + System.currentTimeMillis() + “.xlsx”, “UTF-8”); response.addHeader(“Content-Disposition”, “attachment;filename="” + newName + “"”); response.addHeader(“Content-Length”, "” + newFile.length()); toClient.write(buffer); toClient.flush();}接收前端导入的请求 public static LinkedHashMap<String, List<JSONObject>> importMultiSheetExcel(HttpServletRequest request, LinkedHashMap<Integer, Integer> sheetDataStartRowMap, LinkedHashMap<Integer, String> sheetDataEndColMap) { LinkedHashMap<String, List<JSONObject>> resMap = new LinkedHashMap<>(); try { MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) request; ifNullThrow(multipartRequest, ResultCodeEnum.ILLEGAL_PARAM); MultipartFile file = multipartRequest.getFile(“file”); Workbook work = getWorkbook(file.getInputStream(), file.getOriginalFilename()); ifNullThrow(work, ResultCodeEnum.ILLEGAL_PARAM); 。。。}虽然我们现在使用了spring MVC,还是用到了servlet,而且shiro里面要使用到,以下是代码的配置:<!–servlet 开始–><!–shiro里面要使用到–><dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>${servlet.version}</version></dependency><!–servlet 结束–><dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> <version>${jstl.version}</version></dependency><!–servlet 结束–>如果想要了解servlet的话,可以参考该文档:Java Servlet API中文说明文档Lucene全文检索有时,我们在开发的过程中,需要做全文检索数据,就比如,我在Word文档中,全文检索某个词、某句话等,如图所示:这就是web端的全文检索。但是我做Java,当然,也需要全文检索。因而,我们就想到了Lucene。它是一套用于全文检索和搜寻的开源程式库,由Apache软件基金会支持和提供。提供了一个简单却强大的应用程式接口,能够做全文索引和搜寻。在Java开发环境里,它是一个成熟的免费开源工具。就其本身而言,它是当前以及最近几年最受欢迎的免费Java信息检索程序库。我们在java的maven库中的配置为: <!– lucene 开始 –><dependency> <groupId>org.apache.lucene</groupId> <artifactId>lucene-core</artifactId> <version>${lucene.version}</version></dependency><dependency> <groupId>org.apache.lucene</groupId> <artifactId>lucene-highlighter</artifactId> <version>${lucene.version}</version></dependency><dependency> <groupId>org.apache.lucene</groupId> <artifactId>lucene-analyzers-common</artifactId> <version>${lucene.version}</version></dependency><!– lucene 结束 –>想要对其有更深的了解,可以参考这篇笔记:Lucene学习笔记Quartz任务调度我们在开发的过程中,总想着要在某个时间,执行什么样的事情,于是呢,我们就相当了任务调度,比如:每天八点按时起床每年农历什么的生日每个星期都要爬一次山我们就可以用到Quartz这个框架,我们需要做一些配置,如图所示:我们可以在maven中的配置为:<!– quartz驱动包 开始–><dependency> <groupId>org.quartz-scheduler</groupId> <artifactId>quartz</artifactId> <version>${quartz.version}</version></dependency><!– quartz驱动包 结束–>想要对其有根深多的了解,可参考这篇博客:Quartz使用总结zxing二维码我们经常使用到二维码,比如,添加微信好友的二维码,支付二维码、扫一扫二维码等等,那么,这是怎么实现的呢,其实,这有一个工具包,就是zxing工具包。它是谷歌旗下的工具类,我们可以用它来生成我们想要的二维码,但是,我们先要在maven项目中配置它。如代码所示:<!– 二维码驱动包 开始–><dependency> <groupId>com.google.zxing</groupId> <artifactId>core</artifactId> <version>${zxing.version}</version></dependency><dependency> <groupId>com.google.zxing</groupId> <artifactId>javase</artifactId> <version>${zxing.se.version}</version></dependency><!– 二维码驱动包 开始–>想要对其有根深的了解,可以参考这篇博客:zxing实现二维码生成和解析WSDL包这个我也不大懂,也没有操作过,如果想要了解的话,可以参考这篇文档:WebService中的WSDL详细解析我们在maven中的怕配置为:<!– WSDL驱动包 开始–> <dependency> <groupId>wsdl4j</groupId> <artifactId>wsdl4j</artifactId> <version>${wsdl4j.version}</version></dependency><!– WSDL驱动包 结束–>配置文件配置文件来源于框架中的以下文件,如图所示:所有的配置文件都来源于资源包。这里就不再细说。总结要想成为架构师,首先学习别人的东西,他山之石,可以攻玉。 ...

March 30, 2019 · 5 min · jiezi

hostingranking.cn·基于ghost的轻量技术架构整理

本篇纯粹只讲hostingranking.cn网站的技术架构,也就是怎么做到的,达到什么效果。至于它是什么,为什么要做暂且不说,另篇会分享。技术组成首先hostingranking.cn是基于ghost博客平台而构建的,ghost最强大的部分就是可以最大限度的让你DIY网站,能力特别强。如下是技术组成图:技术讲解 运行环境用途GhostNodejs博客平台,可供主题创作的环境,博客管理,SEO等一系列可插拔的功能JQueryJavascript前端js交互handlebar-网页模板spring bootjava提供后端服务,连接第三方服务typeform 第三方问卷调查服务mailchimp 第三方邮件服务主要功能ghost主题制作,呈现网站基本内容个性化主机推荐博客ghost主题制作ghost主题制作非常简单,只要你会写js+html+css即可制作,另外最好要学会handlebar模板语言,会让你制作主题事半功倍,入门制作教程参考我另一篇文章:https://cmlanche.com/2018/08/…个性化主机推荐基本流程如下采用typeform来制作表单开启typeform的webhook,意思就是当客户提交表单的时候,会往这个webhook url发送一个post请求,来告诉你有用户提交了,以及提交的数据。spring boot是我们的后端服务,专门用来制作webhook接口的,不要把webhook想的很神圣,没什么难的,webhook接口其实就是一个普通接口,只是它被用来处理webhook发送来的数据罢了。spring boot收到typeform提交的问卷信息后,去调用mailchimp的api,创建邮件,发送给客户。活动图如下我最终通过spring Boot接收webhook传递过来的不同数据,生成不同的推荐结果。你可以在hostingranking.cn的实现效果,看自己是否能收到邮件,收到的是什么。typeform强大的问卷调查产品,体验无与伦比,生成的文件简单简洁,完整的api支撑,对刚起步的新手产品免费支持,更重要的是,支持中文!在国内访问畅通无阻!下图是我hostingranking.cn产品涉及到的问卷设计:mailchimp世界上最常用的邮箱市场营销工具,好处如下:完全的开放,完整的api支持,支持用zapper连接上百款常用产品中国访问速度还可以,用api调用无碍诚意满满的新手产品扶助计划,帮助新产品达到足够规模再收费!按照上面说的个性化功能,当你提交问卷之后,我的mailchimp后台会看到发送状态,以及打开和点击的状态,如下图:一点吐槽:看到typeform和mailchimp,我想国外的东西做的真是开放,各种api都支持,反观国内,则各种保护,就比如知乎、博客园、segmentfault等都是不开放api的,这点让我觉得国内还不够开放。博客博客功能是ghost内置的核心功能,目前我没有在hostingranking.cn中开放,因为博客页面和详情页面的功能还没做好,等做好了再贴图了。先用我的个人博客网站代替了:https://cmlanche.com (托管在github pages上的)附hostingranking.cn网站托管在Vultr主机上,买的最低配的VPS,每个月5美金如果你要买它的主机并且你的主要客户在大陆的话,你一定要买日本的主机,不要买美国和新加坡的,因为日本的平均ping值是最低的,大约一百多,新加坡的主机会绕过日本再到中国,慢一些,而美国的大概两三百。

February 24, 2019 · 1 min · jiezi