ramostear.comramostear.com 谭朝红的技术分享博客

格言 编程是一门技术,也是一门艺术 !

首席技术官(CTO)在IT创业公司的角色转变

首席技术官(CTO)在IT创业公司的角色转变

在大多数的IT创业公司中,大多是技术驱动型的公司。面对不断涌现的行业新技术,企业需要快速的掌握并采用这些创新型技术,以确保企业在激烈的竞争中获得相对的竞争优势。以技术为驱动力的公司,为了能快速的处理公司所有的技术问题,从而催生了一个新的职位——首席技术官,英文简称CTO。如果你的公司是一家以技术为驱动力的创业公司,或者你是一位准备转型做CTO的开发人员,那你应该先充分了解首席技术官在IT创业公司中所扮演的角色以及该角色需要承担的责任。

​ 本文以首席技术官的职责和技能为主题,分析首席技术官在IT创业公司的各个发展阶段所扮演的角色,帮助你快速地对首席技术官(CTO)这个职位有一个全面的了解。

一、IT创业公司的CTO在扮演什么角色?

​ 首席技术官,从字面意思上看,主要负责与公司内部技术相关的工作,但这个定义太过于模糊和笼统。在国内外的IT创业公司中,对于CTO该干什么,不该干什么没有明确的定义和统一的标准,不同的IT创业公司因其业务的不同,对CTO的职能要求也不尽相同。虽然不同IT创业公司对CTO有着不同的职能划分,但是从CTO的工作本质上看,可以将CTO划分成技术领导型和技术管理型两类。

1-1、技术领导型

​ 技术领导型的首席技术官都拥有这超高的编码水平,在软件编码、架构设计、基础设施配置等领域都拥有丰富的专业知识和实践经验。他们自身具备的丰富的专业知识和开发经验,帮助其有能力对技术团队中其他的开发人员的代码进行评审,同时能为公司的工程项目作出风险评估。

1-2、技术管理型

​ 这一类型的首席技术官主要的工作任务是监督和管理工程项目的各个项目经理。技术管理型的首席技术官也具备较高的编码水平,但他们通常不参与具体的编码工作,而是将主要的精力放在公司技术战略走向并管理技术团队内部的工程。

​ 随着IT创业公司的不断发展,首席技术官的工作重心将从技术领导过渡到技术管理。在下面的内容中,将逐步分析IT创业公司在各个阶段中,首席技术官角色的演变过程。

二、IT创业公司的CTO角色会发生什么变化?

​ 所有IT企业的首席技术官都有一个贯穿公司始末的伟大使命:确保公司的技术战略完全服务于公司的业务战略。下面是一个IT创业公司从零(0)到一(1)的过程中CTO角色发生的转变。

2-1、创业早期

​ 在创业早期,所有的工作都是围绕着一个商业创意开始的。在这个阶段,公司可能还没有成立,所有的创始人在一起思想碰撞,一场又一场的头脑风暴,目的是完善创业,起草商业计划书。此时的CTO(通常是创始人)往往是一个拥有强大技术专长的人,他们的主要工作职责是验证商业方案的技术可行性,并提出实施软件产品开发的技术解决方案。

2-2、创业初期

​ 如果创业团队的商业方案得到了投资人和消费者的关注,创业团队会注册公司并开始着手构建第一个版本的软件产品。此时的CTO是技术团队的第一个(大多数情况下也是唯一一个)开发人员,他们需要在有限的时间内开发出一个最小化的可使用的产品——MVP(MVP是Minimum Viable Product的缩写)。

​ 在这个阶段,CTO的主要工作如下:

  1. 选择并确定公司的技术栈
  2. 设计软件架构
  3. 配置可扩展的基础架构(硬件和网络)
  4. 软件源代码编写
  5. 测试应用软件

​ 这些初期的工作需要CTO具备强大编码能力(有时是全栈开发),经验和相关的开发流程知识。可能一些创业初始的公司会聘用技术人员,CTO不需要单挑所有的工作,但他们依旧需要在早期为产品原型编写大量的源代码。

2-3、创业成长期

​ 公司进入快速成长期时,软件产品开始投放市场,CTO的工作重点转向软件的升级工作。软件升级让初始版本的的产品更具功能性和可用性,以便吸引更多的用户使用软件。为了加快软件开发速度,企业通常在这个阶段加快扩充技术团队人数的速度,CTO的角色也相应发生了转变,技术团队的管理事务开始进入CTO的工作日程表:

  1. 优化应用程序的部署流程,以提高开发人员的工作效率
  2. 确保应用程序能安全、可靠的运行
  3. 提出更经济、可扩展的基础设施配置方案

​ 在这个阶段,CTO除了参与编码工作外,还要负责管理不断扩大的技术队伍,其管理工作主要体现在以下两个方向:

  1. 负责监督招聘新职员的工作,需要对招聘技术人员的工作做最后的把关
  2. 建立公司的技术文化,如开发工具集、SDK版本、编码风格、书写规范、命名规范、注释规范、文档规范等等,良好的技术文化可以确保所有的开发人员能够遵循同一个标准和最佳实践开展编码工作,从而提高生产效率。

2-4、创业成熟期

​ 公司进入创业成熟期,技术团队的人数将由原来的几人增加到几十人甚至几千人,在这个时期,CTO必须要带领大批的开发人员按照既定的开发规范和工作流程开展编码工作。此时CTO的工作重点将完全转向管理,CTO可能会同时协调并监督多个开发团队,完成不同的软件工程任务。

​ 在此阶段,CTO需要花时间和精力去构建软件产品的愿景,跟踪并分析行业中最新的技术动向,并给出在下一个软件版本中实施相关技术的可行性方案。为了做出合理的、正确的技术选择,CTO在这个时期需要进一步了解新技术的详细流程和行业竞争者的技术状态,需要采用相关的创新技术,确保相比于竞争对手有一定的技术竞争优势。

​ 在本小节中,分析了在IT创业公司中,随着公司的发展,CTO从技术领导型向技术管理型转变的原因。在下面的章节中,将介绍IT创业公司的CTO都需要具备什么样的技能。

三、IT创业公司的CTO需要哪些必杀技?

​ 前面我们分析了IT创业公司在不同阶段中CTO工作职责的转变,CTO需要在软件开发和团队管理上都有着优于常人的能力。接下来,将介绍IT创业公司在各个发展阶段CTO需要具备的一些技能。

3-1、软件架构师

​ 在创业初期,CTO是公司唯一的技术权威和技术实现者,这就要求CTO需要拥有以下的技战水平:

  1. 设计并实现软件架构
  2. 选择并确定技术栈
  3. 设计并配置基础架构(硬件和网络)
  4. 选择并调试开发工具
  5. 设计并实现数据建模
  6. 优化并升级应用程序
  7. 确保应用程序的安全性和可用性
  8. 确保基础架构的经济适用性
  9. 跟踪并分析行业新技术,并对是否采用新技术作出决策

3-2、系统管理员

​ 在创业初期,CTO大多数都兼任系统管理员一职,负责对基础架构和应用程序进行管理,需要具备以下的工作能力:

  1. 软硬件的安全防护管理
  2. 域名、SSL证书和第三方服务管理
  3. 内部开发沟通渠道管理
  4. 基础架构性能监控管理
  5. 数据备份和日志审查管理

随着公司的壮大,这一部分工作会交由首席信息官(CIO)来管理,但在早期,大多数工作还是由CTO来完成。

3-3、项目组长

​ 当技术团队开发人员数量增加时,CTO开始转向团队管理,编码工作的份额开始减少,此时的CTO开始承担一个项目组长的所有职责:

  1. 根据以往的最佳实践和新的编码方法定义和实施开发标准
  2. 制定开发流程
  3. 审查软件源代码
  4. 分配开发任务
  5. 监督招聘工作
  6. 培训新职工
  7. 与客户展开沟通
  8. 与决策层沟通
  9. 评估开发人员的KPI(Key Performance Indicatro,即关键绩效指标)
  10. 监督开发人员的工作进度
  11. 评估技术风险

​ 成为一个优秀的技术管理者不仅需要强大的专业技术知识,还需要有良好的沟通能力,需要随时与开发人员沟通,并解决开发中遇到的问题。

3-4、运维工程师

​ 除了推动公司的开发工作向前发展,管理好技术团队之外,CTO在公司的发展期,还需要成为一名不错的运维工程师,在公司进入创业发展期,CTO必须要负责建立安全、可靠、易用的软件部署通道,为开发人员提供良好的软件升级环境,此时的CTO需要建立起高效的运维流程:

  1. 构建易用、安全、稳定的软件部署通道
  2. 规划软件产品的版本
  3. 选择版本管理工具并管理软件产品版本
  4. 制定并实施软件产品版本的发布周期

3-5、企业高管

​ 在本文一开始提到,IT创业公司的CTO通常是公司的创始人之一,是公司软件工程的最高执行官。除了编码、运维等技术类工作,他们还需要参与公司的上层管理:

  1. 参与公司战略规范和重大决策工作
  2. 参与公司的高层管理工作
  3. 跟踪和分析竞争对手,提出技术创新建议
  4. 为公司的业务提供可行性的技术方案
  5. 监督并协调外部劳动力(外包团队)
  6. 跟踪行业内的技术趋势并识别出公司的技术竞争优势
  7. 评估并监督公司在技术领域的资金预算
  8. 优化基础设施配置方案,降低产品运营成本

​ 技术类的企业高管,不仅要具备强大的专业知识,还需要对企业的管理流程有深入的了解。单纯的技术达人,不是一个合格的IT企业CTO。

四、总结

​ 随着IT创业公司的发展,首席技术官所扮演的角色也在发生改变,总的来说,CTO需要处理公司内部所有和技术有关的问题,可能是微观上的,也可能是宏观上的,但其核心使命不变:确保公司的技术战略完全贴合公司的业务战略。一个成功的IT创业公司(什么样子才算成功?所有的IT创业公司都想要成为行业的领头羊或者独角兽,也许这样才算成功!),一定有一个优秀的首席技术官伴随着企业的成长,他们了解企业在不同的发展背景下应该做什么事、需要具备什么样的技术能力和需要思考什么样的问题。

​ 最后,如果需要用一句话来概括一名出色的CTO所具备的能力和特点,那可能是这样的:出色的CTO都是嚼着嘴里的,夹着碗里的,看着锅里的,想着地里长着的

总结:实施SaaS模型的11个关键步骤

总结:实施SaaS模型的11个关键步骤

总结:实施SaaS模型的11个关键步骤

前言:在SaaS模型中,应用程序是通过网络向用户提供服务。用户只需要通过网络访问应用程序便可完成自身的业务活动,而不需要安装和维护软件。任何的SaaS模型的实施,都应具备以下三个特征:

  1. 多租户架构:SaaS模型设定了多个用户共享一套应用程序源码,此源码被服务提供商统一托管
  2. 自定义可配置:不同用户共享一套应用程序源码,这就要求SaaS模型须具备一定的自定义和可配置功能,以便可以管理和维护用户的自定义业务
  3. 可访问:SaaS模型依赖于网络为用户提供服务,所以必须确保在任何的时间点都能为用户提供数据访问服务。

为了能够打造一款成功的SaaS软件,在实施SaaS模型的过程中,应该遵循以下的步骤:

1. 提前了解业务

​ 在考虑技术和架构的问题之前,清楚的了解业务非常重要。没有了解业务,任何应用程序的设计和开发工作都无从谈起。为了得到更好的SaaS模型实施效果,必须在很早的阶段就开展需求调研工作,明确软件的业务和设计目标。调研的范围应该是充足的、广泛的,以便更好的识别出SaaS模型的业务边界和核心内容。在调研时,应该包括以下的内容:

  1. 采用什么样的架构进行设计?
  2. 用户类型和用户规模分别是什么?
  3. 如何处理应用程序的可扩展性、安全性和稳定性这三个问题?

​ 业务先于设计和开发,在非常早的阶段便开展调研和需求分析工作非常重要,它有助于评估项目的成本、周期和风险。除此之外,还应该重视现有系统在改造工程中所面临的挑战。

2.选择能胜任的团队

​ 物尽其用,人尽其才,无论技术和架构如何,选择适合SaaS模型的技术团队尤为重要。在实施SaaS模型之前,决策者应该选择一支由经验丰富的开发人员组成的团队,他们必须对SaaS模型的概念有深入的了解,且该团队应该拥有多种专业技术的人员,同时对SaaS行业中应该遵循的最佳实践有充分的了解。

3.设计可扩展的基础架构

一旦技术团队了解完业务需求,下一步的重点就是构建可扩展的基础架构组件:

  1. 数据中心
  2. 网络设施:网络安全(如HTTPS加密、认证和鉴权)
  3. 硬件设施:操作系统和存储
  4. 数据备份和系统监控工具

​ 除了上述之外,内部的评审工作也应该同步进行,以便在构建基础架构是评估应用系统的总体成本,在最终决定实施SaaS模型前,还需考虑一下因素(成本驱动):

  1. SaaS软件的成熟度模型
  2. 可伸缩性和可用性
  3. 客户端兼容性
  4. 数据备份和恢复
  5. 网络带宽和存储空间
  6. 安全管理(加密级别和安全证书)

4.网络带宽和应用服务器

​ 这点非常重要,为了达到良好的用户体验效果,用户规模和类型决定了带宽的大小和应用服务的数量。因此需要综合考虑SaaS模型的带宽大小和应用服务器的数量,合适的搭配能够消除数据中心的网络瓶颈问题。如果不自行构建数据中心,选择外包托管(如阿里云,腾讯云,亚马逊)方式,则应该考虑以下因素:

  1. 数据中心是否高可用(7x24x365)?
  2. 是否具备监控和灾备能力(容灾和数据恢复)?
  3. 是否具备数据安全防护能力(防攻击,防破解)?
  4. 是否具备可测试能力(完备的API测试接口,沙盒测试环境,SDK开发包)?

5.构建基础架构组件

​ 基础设施设计完成后,我们须准备可靠的、稳定的、经过实际验证的功能组件,此过程对于确保SaaS模型的高可用性能指标尤为重要。在评估这些硬件指标时,我们还需要确保其能够在项目交付日期范围内交付所有的硬件组件并确保调试正常。

6.向基础设施中持续部署SaaS应用

​ 一旦基础设施可用,开发团队就应该持续构建和部署SaaS应用。此时应对服务器进行统一的管理和配置,根据不同的业务需要安装对应的操作系统,分配指定的IP,设置用户访问策略以及配置防火墙等。

7.容灾和数据恢复

​ 现在,SaaS应用程序已经准备就绪,下一步应该考虑的问题是如何容灾和数据恢复的问题。面对容灾和数据恢复,需要考虑一下几点:

  1. 如何应对灾难的发生(不可抗拒因素不在考虑范围)
  2. 如何缩小受灾范围以及如何减小受灾带来的影响
  3. 如何在灾难发生时,应用程序还可以局部可用
  4. 如何在有限的时间范围内完成数据恢复

8.系统监控

​ 系统监控子系统对于SaaS模型特别重要,好的应用监控子系统有助于更早的发现问题,及时的避免灾难的发生。在SaaS模型中,监控子系统应该对一下的参数进行监控:

  1. 内存和CPU的使用率
  2. 磁盘的I/O读写速率
  3. 操作系统和应用程序日志
  4. 其他应用组件(数据库、Web容器、TCP等)性能参数

9.建立客服中心

​ 一旦SaaS应用成功投放市场,它就需要有一个客户支持服务中心(面向业务或者技术)。客户支持服务中心应该具备适当的票据处理和在线沟通能力,一个优秀的SaaS模型,客户支持服务是必不可少的,它有助于提升SaaS软件的服务质量。客户支持服中心应当适时地提供邮件、短信、电话、票据等服务能力,如果SaaS模型中有需要技术团队继续关注的问题,还应该提供工单处理服务,以便在发生技术故障时,能够以最短的时间将故障信息以邮件或者短信的形式发送给相关的技术人员。

10.SaaS成熟度模型

​ 在实施SaaS模型时,选择何种等级的成熟度模型,直接决定了你需要为此付出的资金成本,同时也影响着软件的交付周期和开发风险。因此,在面对此问题时,可以先考虑第二级或者第三级SaaS软件成熟度模型,然后根据业务量和用户规模,逐步满足高性能、可配置、自定义等更高级别成熟度模型的要求,以降低实施SaaS模型的风险。SaaS软件成熟度模型评估对照表如下图所示:

11.规范化的文档

​ 在完成上述10个关键步骤后,需对整个SaaS模型做出详细的记录。如需求文档,设计文档,组件文档以及接口文档等。这些文档将帮助运维人员在运维过程中快速定位系统故障。如果SaaS模型在后期有任何的修改和升级操作,这些文档也可以帮助工作人员了解SaaS模型。

正确甄别API & REST API & RESTful API & Web Service之间的差异与联系

正确甄别API & REST API & RESTful API & Web Service之间的差异与联系

​ 看到API你会想起什么?是接口、第三方调用、还是API文档?初看你可能会觉得这太熟悉了,这不是系统开发日常系列吗?但你仔细想一想,你会发现API的概念在你脑海里是如此的模糊。如何你通过搜索引擎检索API,你会看到类似这样的信息:API——Application Programming Interface(应用程序编程接口),这太抽象了。接下来,我将结合在开发中总结的一些经验,以通俗的方式聊聊API、REST API、RESTful API以及Web Service这四者之间的联系与区别。

1、API 与 REST API

​ 什么是API?这里引述维基百科给出的定义:应用程序接口(英语:Application Programming Interface,缩写:API;又称为应用编程接口)是软件系统不同组成部分衔接的约定。这个对API的定义太过于广泛和抽象,而通俗的讲,API是一段应用程序与另一段应用程序相互“交流”的方式(协议)。在Web应用程开发中,API是我们通过网络进行数据检索的一种主要方式,API文档将告知你检索数据的URL列表、查询参数、请求方式以及响应状态,其目的是降低Web应用程序开发难度,共享两个应用程序之间的数据(文本、音频、视频、图片等),而屏蔽其内部复杂的实现细节。

​ REST是Representational State Transfer的缩写,直译过来就是:表述状态的转移。REST API是一组关于如何构建Web应用程序API的架构规则、标准或指导,或者说REST API是遵循API原则的一种架构风格。REST是专门针对Web应用程序而设计的,其目的在于降低开发的复杂度,提高系统的可伸缩性。下面是设计REST风格的系统架构时需要满足或者遵循的一些基本条件和原则:

  • 1、在REST架构中,Web中所有的事物(文本、音频、视频、图片、链接)都可以被统一的抽象为资源(resource)
  • 2、在REST架构中,每一个资源都有与之对应的唯一资源标识符(resource identifier),当资源的状态发生改变时,资源标识符不会发生改变
  • 3、在REST架构中,所有的操作都是无状态的。REST架构遵循CRUD原则,所有的资源都可以通过GET、POST、PUT和DELETE这四种行为完成对应的操作。
  • 4、可缓存(可选项),在REST架构中需要缓存来有效的处理大批量的请求
  • 5、接口一致

​ 现在,了解了API和REST API的基本概念,那这两者之间有什么异同?如果按照数学上集合的概念来解释API与REST API之间的联系与区别,API是REST API的超集,REST API 是API的子集;所有的REST API都是API,但不是所有的API都是REST API。更通俗的解释是:所有的男人都是人,但不是所有的人都是男人。

2、REST API 与RESTful API

​ 在第一小节中,了解了什么是REST API,接下来聊聊REST API与RESTful API之间的异同。很多初学者很容易将这两者等同起来,认为RESTful API就是REST API,这可能是单纯的从字面上去理解了,当你深入的去了解两者的本质后,你会发现其实不然。REST API是Web API设计的一种规范或者指导原则,而RESTful API则是这中架构设计原则或者规范的一种具体实现方式。也就是说,RESTful API是REST API的非正式实现方式,因为实现REST API的方式有很多,RESTful API只是其中一种,且没有完全满足REST API的所有设计原则,每个开发者在实现REST 架构时的则重点都会有差别。

​ 很多初学者容易将REST API与RESTful API两者的概念搞混淆,我想可能只是看字面意思,而没有关注它们本身的含义(就像认识中文字一样,有边读边,无边读中间,断章取义了)。这就好比很多人会把变性人等同于女人,变性人可能五官的表象看起来和女人一样,但变性人不能生育,它只是满足了定义一个女性的大多数条件(实现),但本质上不是女人。

​ 接下来,通过一个简单的例子以加深对REST API和RESTful API的理解。下面将给出一个执行CURD操作的RESTful API设计案例:

说明:resource代指某种资源的名称,可以时student(学生)、teacher(老师)、book(书籍)等等,通常使用名词来表示;{id}则代指某种资源的唯一标识符(resource identifier)

下面是一个具体的小例子,以学生管理为例,设计学生管理的API。学生资源包括ID,姓名和所学课程信息,学生资源信息如下:

​ 现在,我们需要将学生数据保存到数据库,然后执行查询、修改和删除学生数据的操作。学生管理API的使用者调用的API如下:

​ 前面的内容说到,API共享数据资源,而屏蔽内部实现,API的使用者(客户端)关注的是资源(读懂数据),并不需要了解API内部构造;API的提供者(服务端)只关注自己的内部实现,而不关系API使用者(客户端)的状态。为了加深对这一概念的理解,下面给出学生管理API的内部实现示例:

说明:

示例代码是基于Spring MVC进行实现的。

除了上述的内容之外,你还可以通过提供键值对的方式对查询数据进行过滤,如获取所有的学生数据时,只想获取性别为女性的学生数据,则可以通过这样的方式来完成:

[GET] http://www.example.com/students?gender=female

Tip:如果API拥有对数据过滤的功能,对应服务端的API实现代码也需要做调整。

​ 在前面的内容中,我们提到RESTful API是REST API的非正式实现方式或规范。为什么这么说呢?因为在RESTful API的设计中,我们完全可以通过GET的方式完成CURD操作,也可以通过DELETE行为来创建资源,通过POST行为来修改资源,它的实现方式并不严谨或者说并没有严格按照REST API提出的约束条件来进行。所以说RESTful API是REST API的非正式实现方式

3、REST与Web Service

3-1、什么是Web Service?

​ 如万维网联盟(W3C)所述,Web Service提供了在各种平台和/或框架上运行的不同软件应用程序之间可以进行互操作的标准方法。Web Service的特点是具有良好的互操作性和可扩展性,以及由于使用XML而可以对程序处理过程进行描述。它们可以以松散耦合的方式组合不同的服务以实现复杂的操作。提供简单服务的程序可以通过相互交互,以提供复杂的增值服务。

​ 两个Web Service之间主要通过HTTP网络协议进行通信,如我们熟知的SOA(面向服务的体系架构),主要依赖于XML-RPC和SOAP(Simple Object Access Protocol,即简单对象访问协议)。

Tip:千万不要将SOA(面向服务体系架构)和SOAP(简单对象访问协议)搞混,前者是一种架构设计形式,后者是一种数据交换协议。

​ 简单的一个示例:假设一个Web Service A提供允许其他应用程序通过URL获取用户信息的功能:[GET] http://www.abc.com/{id}。id是用户的唯一标识符,请求此URL将获得用户信息。现在假设浏览器、手机、桌面应用程序的用户都要获取服务A提供的用户信息,这三者只需要请求服务A提供的URL地址,并输入用户id信息即可。至于者三个不同客户端的实现方式(编程语言)是什么与服务A 没有任何关系,只要能够解析出服务A返回的XML文档即可。这样,应用程序之间交换数据就可以不用依赖于具体的语言和环境。这就好比不同国家不同语言的人,只要能够知晓对方语言的语法结构,两个人就可以进行交流。

3-2、Web Service的优点

​ 使用Web Service有如下的几个优点:

  • 1、互操作性:Web Service允许应用程序之间进行通信,交换数据和共享服务。
  • 2、可用性:Web Service的功能可以从简单的信息查找到复杂的算法计算。
  • 3、可重用性:Web Service之间可以相互组合,以提供更为复杂的服务,由于其互操作性的特点,可以轻松的在其他的服务中重用Web Service组件,提高了服务的重用率。
  • 4、易于部署:Web Service可以部署在基于Internet标准的容器中,如Apache、Axis2等,以提供HTTP或者WSDL(网络服务定义语言)驱动的服务。
  • 5、成本低:Web Service是通过打包成Web服务组件进行部署,从而降低了使用的成本。

3-3、Web Service的类型

​ 目前,Web Service主要有两大流派:

  • 1、基于SOAP的Web Service : SOAP(简单对象访问协议)是一种基于XML的协议,用以访问Web Service。其接口以机器可处理的格式进行描述,称为WSDL(Web服务定义语言)文档。通过使用标准的的XML文档来描述Web Service,在XML文件中,会详细记录接口的信息,如消息的格式、传输协议以及交互的位置等信息。
  • 2、基于REST的Web Service :REST(Representational State Transfer)是一种软件架构,它使用JSON来描述数据格式,最重要的是HTTP传输协议对REST来说是非必须的。

3-4、REST与SOAP的区别和联系

​ 下面,通过一张表格来对比REST与SOAP之间的异同:

总结

如上所述,我们了解了什么是API,什么是REST API,什么是RESTful API以及Web Service的相关概念。API代表应用程序编程接口,是一种较为宽泛的定义或者说是一种协议,作为软件程序之间相互通信的接口而存在。REST API是API的一个子集,所有的REST API都是API;RESTful API是对REST API架构风格的一种非正式实现方式。API与Web Service都是服务提供者和服务消费者之间的通信手段。最后,为了能够快速的识别API与Web Service之间的差异,将这两种手段的不同之处整理成对照表如下:

鱼与熊掌得兼:Hibernate与Mybatis共存

鱼与熊掌得兼:Hibernate与Mybatis共存

鱼与熊掌得兼:Hibernate与Mybatis共存

很长一段时间,网上有很多关于Hibernate与Mybatis孰优孰劣的争论,两个阵营的人谁也不能说服谁,每个人的理由都很有道理。今天,我分享的主题是:在一个项目中同时使用Hibernate和Mybatis两个ORM框架。

​ 作为一个开发者,没有必要花费过多的时间去证明技术无用论,当你开始指责某个框架垃圾,另外一个框架最好时,隐性的暴露出你对某个框架没有深入的研究,无知的指责对于技术的提升没有任何的帮助。框架本身没有对错一说,只有适合和更适合项目的选择。任何框架都有自身的能力范围,就拿Hibernate和Mybatis这两个ORM框架来说,Hibernate封装了很多有用的API给开发者,降低了操作数据库的难度和复杂度,同时也减少了模板代码的数量,但Hibernate留给开发者可操作的空间相对Mybatis少了很多;Mybatis框架使用起来很灵活,开发者可以自定义查询语句,但增加了模板代码的数量,看起来没有Hibernate那么便捷。两种框架在便捷与灵活两个指标上做出了取舍与妥协,这不能说是框架的错。对于一个框架而言,需要有自身专注的领域和设计愿景,不可能面面俱到。

​ 使用任何一种技术框架,都需要贴合现实的业务需求以及自身的技术能力。当你还没有深入的去了解一门技术或者当前业务需求无法与框架契合时,不要盲目的批判框架的好坏。今天,我不再去对比Hibernate与Mybatis两者之间的优劣,而是给出一个比较中庸的放方案,将两个ORM框架同时整合在一个项目中。

一、准备开发环境

​ 如果你想成功运行本文中的源代码,需要满足一下的几个条件:

  • 1、JDK : JDK 1.8.x及以上版本
  • 2、Maven : Maven 3.x或更高版本
  • 3、Git:版本控制工具,选择一个你喜欢的
  • 4、IDE : 选择你比较喜欢的一个代码编辑器,如STS、IntelliJ IDEA。笔者使用的是IntelliJ IDEA
  • 5、Databases : 选择一个你熟练使用的数据库系统。笔者在本文中使用的是MySQL 5.1.x版本的数据库系统

如需获取本次分享内容的源代码进调试,可以到文章末尾找到源代码仓库连接

二、搭建项目

2-1、引入依赖

​ 为了快速构建项目,笔者采用Spring Boot来构建项目,同时使用加入Spring Data JPA和Mybatis两个ORM框架的依赖包。在此需要特别说明,Hibernate是一个JPA标准的实现,尔Spring Data JPA是一个JPA数据访问抽象,通过Spring Data JPA,可以轻松使用Hibernate框架。

​ 你可以通过Spring Initializer来初始化项目,也可以通过IDEA自带的Spring Initializer功能构建项目,项目构建完成之后,pom.xml文件中的配置如下(包含但不限于文中给出的依赖项):

2-2、定义实体类-User.java

​ 为了演示同时使用Hibernate和Mybatis操作数据库,需要提供一个实体类User.java,代码如下所示:

说明:

在本次演示的项目中,使用到了Lombok插件,它可以让开发者减少模板代码的书写,提高开发速度。@Data注解可以自动生成类属性的getter、setter和toString方法。@NoArgsConstructor会自动为类生成无参构造函数,@AllArgsConstructor则会生成带全部属性的构造函数。

2-3、定义数据持久化接口

​ 在本次课程中,将使用Spring Data JPA来完成写操作,如新增、修改、删除;使用Mybatis来完成读操作,如根据用户ID查询、查询所有的用户等。Spring Data JPA和MyBatis的持久化接口都位于com.ramostear.hm.orm包下,Spring Data JPA的持久化接口相对比较简单,之间继承JpaRepository类即可,代码如下:

说明:因为JPA只负责写操作,所以直接继承并使用JpaRepository提供的API即可,不需要额外的定义其他的接口方法。

​ 下面是Mybatis的映射接口,定义了两个方法:根据ID查询用户信息和查询所有的用户信息。代码如下所示:

说明:

此接口需要注意的地方是@Component@Mapper注解,@Component注解标注此接口后,Spring会自动扫描并配置此类;@Mapper注解是把这个mapper的DAO交由Spring进行管理。

定义完Mybatis 映射接口后,需要提供一个进行数据库查询的xml配置文件。该文件位于resources/mapper文件夹中,UserMapper.xml完整代码如下:

2-4、定义UserService

​ 在UserService接口中,提供三个方法:保存用户信息、根据ID查询用户、查询所有的用户。UserService接口代码如下:

在UserService接口的实现类中,需要同时注入UserRepository和UserMapper两个依赖。我们使用构造函数的方式来注入这两个依赖。代码如下:

说明:

@Transactional注解用于设置每个方法的事务控制方式。@Service注解声明该类是一个服务提供类,且设置了该类被Spring初始化时Bean对象的名称为“userService”。

2-5、定义控制器

​ 最后,提供一个控制器,用于处理客户端的相关请求。在控制器中,提供了三个请求处理方法,分别处理客户端新增用户、根据ID查询用户和查询所有用户的请求。控制器代码如下:

说明:

在本次教程中,为了编码IDEA报警告,所有的依赖注入都采用构造函数的方式注入相关的依赖。

三、配置Hibernate和Mybatis

​ 网络上有很多关于在Spring Boot项目中配置Hibernate和Mybatis的教程,但同时配置Hibernate和Mybatis的文章很少,有一些是通过Java代码的方式对这两个ORM框架进行配置,采用的是多数据源的方法来整合两个框架。其实整合这两个框架没有想象中的那么难,只需要在application.yml或者application.properties配置文件中加入几行代码,就可以完成两个框架的整合。以application.yml配置文件为例,配置代码如下:

是不是很简单,并为没有太多复杂的配置,这是一种较为简单的整合方式。Hibernate和Mybatis共用一个数据源,如果是JPA的忠实粉丝,现在想要使用Mybatis,只需要额外加入mybatis的配置即可。

四、测试

​ 通过以上的几个步骤,整个项目已经搭建完毕,接下来将使用Postman测试工具对Controller的三个方法进行测试,验证两个ORM框架在同一个项目中是否能共存。

​ 首先测试 POST http://localhost/users ,验证Hibernate是否能够成功将用户信息持久化。打开Postman工具,在地址栏输入http://localhost/users请求地址,请求方式选择POST,在Body栏输入如下的信息:

{
    "username":"谭朝红",
    "alias":"ramostear",
    "age":28
}

点击“Send”按钮发送请求,观察服务端响应信息,测试结果如下图所示:

可以看到,服务端成功返回用户信息,且用户ID=3。接下来,我们请求 GET http://localhost/users/3 ,验证Mybatis是否能够成功查询出用户信息,测试结果如下:

通过测试,服务端成功返回了用户ID=3的用户信息:

{
  "id": 3,
  "username": "谭朝红",
  "alias": "ramostear",
  "age": 28
}

由此证明,在同一个项目中,Hibernate和Mybatis均能正常工作,整合方案有效,解决了在同一项目中Hibernate与Mybatis共存的问题。

五、总结

​ 本次课程验证了同时使用Hibernate和Mybatis两个ORM框架的方案可行,且采用了一种比较简单的方式来整合两个框架,摒弃了多数据源的复杂配置,快速实现两个框架并用的需求。

​ 在一个项目中同时使用两个ORM框架有没有实际的意义呢?我的答案是肯定的。同时使用两个ORM框架,两者之间可以相互弥补自身的不足,以达到灵活性和便捷性同时兼顾,另外一方面,在单独使用Mybatis时,开发者需要手动或者借助其他的工具生成数据库表信息,而采用本文的整合方案,Mybatis可以借助JPA自动生成数据库表的能力,从而简化使用Mybatis的步骤。最后,对于一些读多于写的系统,完全可以将这两个框架同时使用,写操作少的模块,可以使用Spring Data JPA快速完成相关功能的实现,对于读操作部分,则可以利用Mybatis来优化查询语句。两者之间的优势互补,能进一步的提升开发效率和系统性能。

本次分享内容的源代码仓库地址:

https://github.com/ramostear/orm-hm.git

5个Spring Event奇技淫巧,你GET到了吗?

5个Spring Event奇技淫巧,你GET到了吗?

5个Spring Event奇技淫巧,你GET到了吗?

前言:

谈到事件,接触过前端或GUI编程(JavaScript,Swing)的同学应该有较深刻印象。如事件源、事件的监听、回调等概念应该耳熟能详。而在Web应用程序中则很少用到事件。但在Web应用程序中,也可以轻松实现面向事件的编程。

1、为什么需要面向事件编程

​ 在本文中,将基于Spring框架来谈谈关于面向事件编程的5个奇技淫巧。在开始主要内容之前,先了解一下为什么需要面向事件编程。首先看一个生活中的一个场景来帮助我们快速的了解面向事件编程所带来的好处。以客户到银行柜台取现为例,在较早以前,银行柜台取现需要一个接一个的排长龙等待柜台业务员处理取现业务,在此期间,客户不能离开队伍去做其他的事情,否则需要重新排队。这好比常规的面向过程的编程,需要依次执行每条逻辑语句,直到所有的语句都执行完毕,方法才返回结果。现在,银行柜台取现多了一台叫号机,需要办理取现业务的客户先通过叫号机领取一张业务号,如果等待人数过多的时候,客户可以先处理自身的事情,直到柜台叫到自己的号时,才到柜台办理取现业务。这就是一个典型(不太严谨)面向事件的处理过程。首先,客户通过叫号机注册一个取现的事件,此时的叫号机,相当于事件发布器(事件注册中心),客户相当于事件源,当柜台收到有客户需要取现的消息时,就会广播提示客户办理取现业务,柜台就相当于一个事件监听器。

​ 如上所述,面向事件的编程(也称作事件驱动编程)是基于对消息信号的反应的编程。面向事件编程需要满三个条件:

  • 1、事件源:消息的源头,如点击一个按钮、访问某个页面、新增一条数据等。
  • 2、事件注册中心:事件注册中心用于存储和发布事件。
  • 3、事件监听器:用于接收特定的消息,并作出对应的反应。

下面通过一张图更为直观的了解面向事件编程的基本原理:

图 1-1 事件处理机制

2、Spring Events

​ Spring Events 是Spring Framework的一部分,但在应用程序中并不经常使用,甚至被忽略。然而,Spring的Application拥有强大的事件发布并注册事件监听器的能力,它拥有一套完整的事件发布与处理机制。Spring 4.1开始引入@EventListener注解来监听具体类型的事件,它的出现,消除了使用Spring定义一个事件监听器的复杂操作。仅仅通过一个简单的注解,就可以完成一个监听器的定义,你不需要额外的进行其他的配置;这简直太棒了。在Spring Framework中,事件的发布是由ApplicationContext提供的,如果想完成一个完整的面向事件(也称为事件驱动编程)编程,你需要遵循以下三个基本的原则:

  • 1、定义一个事件,且扩展ApplicationEvent
  • 2、事件发布者应该注入一个ApplicationEventPublisher对象
  • 3、监听器实现ApplicationListener接口或者使用@EventListener注解

在开始介绍面向事件编程的具体实施细节之前,先通过一张类图了解一下Spring的事件机制设计类图:

图 2-1 Spring Event 类图

​ 通过类图,先快速对Spring Event事件类图做一个简单的介绍:

  • 1、ApplicationEventPublisher是Spring的事件发布接口,所有的事件都是通过该接口提供的publishEvent方法发布事件的。
  • 2、ApplicationEventMulticaster是Spring事件广播器,它将提供一个SimpleApplicationEventMulticaster实现类,如果没有提供任何可用的自定义广播器,Spring将采用默认的广播规则。
  • 3、ApplicationListener是Spring事件监听接口,所有的事件监听器都需要实现此接口,并通过onApplicationEvent方法处理具体的逻辑。
  • 4、在Spring Framework中,ApplicationContext本身就具备监听器的能力,我们也可以通过ApplicationContext发布先关的事件。

​ 通过分析,可以看到:当一个事件通过ApplicationEventPublisher发布出去之后,ApplicationEventMulticaster广播器就会开始工作,它将去ApplicationContext中寻找对应的事件监听器(ApplicationListener),并执行对应监听器中的onApplicationEvent方法,从而完成事件监听的全部逻辑。

3、自定义Spring Event

​ 本次内容将使用Spring Boot 2.1.5快速搭建所需要的环境。首先将演示如何自定义一个事件,并监听该事件且对监听到的事件做出反应。

​ 我们将使用IntelliJ IDEA创建一个Web项目,并在pom.xml文件中添加如下的依赖:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.5.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.ramostear</groupId>
    <artifactId>spring-boot-event</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>spring-boot-event</name>
    <description>Demo project for Spring events</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.47</version>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

说明:

这将是一个完整的演示Spring Event事件处理机制的项目,因为期间需要演示事务管理的特性,所以加入了MySql数据库依赖。

​ 本次的内容将依据这样一个场景展开:当成功添加一条用户信息后,将发送一条邮件到用户的邮箱账户。首先,需要定义两个实体,用户实体和邮件详情实体。具体代码如下:

接下来是邮件详情实体类:

持久化类通过扩展JPA的JpaRepository类快速获得CRUD能力。用户持久化类和邮件详情持久化类分别如下:

UserRepository.java

package com.ramostear.repository;

import com.ramostear.domain.User;
import org.springframework.data.jpa.repository.JpaRepository;

/**
 * Created by Ramostear on 2019/5/31 0031.
 */
public interface UserRepository extends JpaRepository<User,Long> {
}

EmailDetailRepository.java

package com.ramostear.repository;

import com.ramostear.domain.EmailDetail;
import org.springframework.data.jpa.repository.JpaRepository;

/**
 * Created by Ramostear on 2019/5/31 0031.
 */
public interface EmailDetailRepository extends JpaRepository<EmailDetail,Long> {
}

接下来,将定义一个用户业务接口,提供创建和查找用户两个方法,代码如下:

package com.ramostear.service;

import com.ramostear.domain.User;

/**
 * Created by Ramostear on 2019/5/31 0031.
 */
public interface UserService {

    User created(User user);


    User findOne(Long id);
}

基本的环境已经准备就绪,现在开始定义定义第一个事件类:SendEmailEvent.java,其代码如下所示

如之前所述,自定义事件需要扩展ApplicationEvent类,并提供自己的实现细节。这里的事件需要为其提供一个用户ID,在接下来的监听器中,通过此ID来甄别具体的用户,并发送邮件。

接下来,我们将使用@EventListener注解实现一个事件监听器,其源码如下:

说明:

在此类中,我们注入了EmailDetailRepository和UserRepository两个依赖项,并使用@EventListener注解对sendEmail方法进行了标注。在方法中,使用了Thread.sleep(1000*3)让程序休眠3秒,以模拟发送邮件时所耗费的时间。@Slf4j注解是Lombok提供的快速使用Logger的一个快捷方式。@Component注解用于标注此类是一个普通的组件,将被Spring自动扫描并管理。

​ 准备好了事件源以及事件监听器,现在来看看如何发布一个事件。发布事件将在用户业务实现类中进行操作。UserServiceImpl.java类的实现细节如下:

​ 如前面章节所讲,如果使用Spring发布一个事件,需要在具体的类中注入一个ApplicationEventPublisher对象。在用户业务实现类中,通过构造函数的方式注入ApplicationEventPublisher对象和UserRepository持久化对象。在created(User user)方法中,当成功保存用户数据后,通过发布一个SendEmailEvent事件,将邮件发送给用户的邮箱。与此同时,我们记录了程序执行完所耗费的事件,在下一小节中我们将使用到这个数据。

​ 最后,我们定义一个控制器,提供一个处理添加用户请求的方法,验证发送邮件事件是否被监听器成功监听。控制器代码如下:

启动项目,并使用PostMan工具对POST /users API进行测试:

图 3-1 Postman测试结果

经测试,成功返回了用户信息,且耗时3530ms。我们在看看控制台的日志信息:

图 3-2 日志记录信息

​ 至此,第一个基于Spring Event的面向事件的编程案例完成。在这个案例中,虽然我们解决了事件的发送和监听的编码实现问题,但可能会有同学会想,这样子的处理方式和将发布事件的代码替换成直接调用邮件持久化类保存邮件信息没有任何区别,如同一开始说的案例,虽然有叫号机叫号,解决排队的问题,但等待的时间依旧没有改变,这和直接排队在时间耗费上没有根本的变化。那么针对这样一个问题,我们在接下来的内容中将讲解如何实现异步事件。异步事件就好比你现在从普通储蓄客户变成了VIP会员,你可以不去银行柜台叫号,而是通过电话或者其他手段预约一个时间去办理业务,银行客户经理会给你预留相应的时间,当预约时间到达之后,你才去银行柜台办理业务。

4、Spring 异步事件

​ 在该小节的基础上,将原有的事件改造成异步事件,以节省客户端等待服务端响应的时间。使用Spring实现异步事件有两种方式,第一种方式是使用@Async注解对监听器进行标注,这是最为简单的一种实现方式;第二种是通过配置类对Spring的事件广播器进行配置,设置Multicaster对异步操作的支持,此方法比第一种方法稍微复杂一些,但可以拥有对事件广播器更多的控制权。接下来,将分别介绍这两种实现方式。

4-1、使用@Async注解实现异步事件

​ 在现有的工程中,我们需要对两个类进行改造。第一处改造是在项目主类上加入@EnableAsync注解,开启对异步操作的支持。代码如下:

package com.ramostear;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;

@SpringBootApplication
@EnableAsync
public class SpringBootEventApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpringBootEventApplication.class, args);
    }
}

第二步是改造监听器,在原来的方法上加入@Async注解,其余部分保持不变,代码如下:

    @Async
    @EventListener
    public void sendEmail(SendEmailEvent event){
       ...
    }

通过上述两个步骤,我们就将Spring默认的同步事件改造为异步事件。重新启动项目,依旧使用Postman对接口POST /users进行测试,结果如下:

图 4-1 @Async异步事件

通过上图我们可以看到,服务端成功返回用户信息,注意观察,从客户端发起请求到服务端响应结束总共耗时480ms,而在第一个案例中,请求和响应的总耗时为3530ms。最后我们再来看看控制台的日志信息,确定一下数据库是否成功保存用户邮件:

图 4-2 @Async异步事件控制台日志信息

通过上述的测试结果,我们可以看到,异步事件已经生效,在相同的环境下,同样的业务请求,使用同步事件和异步事件,服务端响应时间存在巨大的差异。同步事件耗时3530ms,异步事件耗时480ms,这就是异步事件带来的巨大优势。

4-2 、使用配置文件开启对异步事件的支持

​ 在4-1中,我们学会了如何使用@Async定义异步事件,那本小节将展示另外一种方式获取对异步事件的支持。首先需要定义一个配置类,该类用于配置SimpleApplicationEventMulticaster类对于异步事件的支持。其源码如下:

package com.ramostear.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.ApplicationEventMulticaster;
import org.springframework.context.event.SimpleApplicationEventMulticaster;
import org.springframework.core.task.SimpleAsyncTaskExecutor;

/**
 * Created by Ramostear on 2019/5/31 0031.
 */
@Configuration
public class AsyncSpringEventConfig {

    @Bean(name = "applicationEventMulticaster")
    public ApplicationEventMulticaster applicationEventMulticaster(){
        SimpleApplicationEventMulticaster multicaster = new SimpleApplicationEventMulticaster();
        multicaster.setTaskExecutor(new SimpleAsyncTaskExecutor());
        return multicaster;
    }
}

​ 在此配置文件中,applicationEventMulticaster方法将返回一个SimpleApplicationEventMulticaster对象。在返回此事件发布器之前,我们为此事件发布器设置了一个任务执行器:SimpleAsyncTaskExecutor,当应用程序中有新的异步事件时,将会调用此异步任务执行器的submitListenable(Callable task)方法,发布异步事件任务,并选择相应的监听器处理事件。定义好配置类后,还需要将主类中的@EnableAsync和监听器中的@Async两个注解注释掉,其余部分保持不变。最后重启项目,并测试API接口。

图 4-3 异步事件发布器配置测试

如图4-3所示,测试成功返回用户信息,其请求和响应总耗时为492ms。通过配置的方式开启Spring对异步事件的支持已生效。

5、事件过滤

​ 如果我们想对用户事件进行限制,某一部分满足条件的用户才发送邮件,可以使用condition属性来指定限制的条件。这就好比银行柜台提供的VIP预约取现业务,它分不同等级的VIP,只有达到某个条件时,才享有预约服务。在原来的基础上,我们做如下的改动,当用户ID大于5时,才发送邮件给用户。代码如下:

    @EventListener(condition = "#event.userId > 5")
    public void sendEmail(SendEmailEvent event){
       ...
    }

现在,数据库中用户ID最大为3,我们先添加两个用数据,看邮件详情表是否会新增数据。启动项目,测试API接口:

图 5-1 条件过滤测试一

服务端成功返回结果,用户ID=4,现在看看数据库是否有用户邮件数据被添加:

图 5-2 数据库查询结果一

从查询结果可以看到,并未给ID=4的用户发送邮件。控制台也只输出了插入用户数据的SQL语句:

Hibernate: 
    insert 
    into
        t_user
        (email, first_name, last_name) 
    values
        (?, ?, ?)
2019-06-03 06:14:19.334  INFO 4652 --- [p-nio-80-exec-2] c.r.service.impl.UserServiceImpl         : total times :58

接下来我们再新增两条用户数据,观察当用户ID=6时,是否会为用户发送邮件。图5-3是当用户ID=6时,数据库的查询结果以及控制台的输出:

图 5-3 数据库查询结果二

此时,控制台的输出信息如下所示:

图 5-4 控制台日志信息

由此可以看出,我们的事件过滤条件已生效。

6、事务处理

​ 现在,Spring Event的相关使用技巧以及介绍了大部分,最后让我们来考虑这样一个问题,如果在保存用户的时候发生错误,事件将会怎么样?在监听器处理事件时,保存用户的事务还未提交会怎么样?按照常规的逻辑,当用户持久化事务未提交,或者保存过程中发生异常时,不应该给用户发送邮件。试想一下,如果本次案例的场景是发生在用户注册的业务中,如果用户尚未注册成功,就给用户发送一封邮件,那是多么尴尬的事情。这就涉及到事件的事务控制。在Spring中,可以使用@TransactionalEventListener注解对监听器进行标注,一旦使用此注解,被标注的方法将被纳入到事务管理的范围。此注解是@EventListener的扩展,它允许你将事件的监听绑定到某个事务阶段。可以通过phase属性对事务阶段进行设置。下面是phase可设置的所有事务阶段:

  • 1、AFTER_COMMIT(默认值),用于在事务成功完成后出发事件
  • 2、AFTER_ROLLBACK,用于事务回滚是出发事件
  • 3、AFTER_COMPLETION,用于事务完成后出发事件(AFTER_COMMIT和AFTER_ROLLBACK的一种模糊表述)
  • 4、BEFORE_COMMIT,用于事务提交前出发事件

最后给出一个简单的示例:

    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT,condition = "#event.userId >=5")
    public void sendEmail(SendEmailEvent event){
        Optional<User> optional = userRepository.findById(event.getUserId());
        if(optional.isPresent()){
            User user = optional.get();
            log.info("User Detail:{ id:{},firstName:{},email:{}",user.getId(),user.getFirstName(),user.getEmail());
            try {
                Thread.sleep(1000*3);
            } catch (InterruptedException e) {
                log.error(e.getLocalizedMessage());
            }
            emailDetailRepository.save(EmailDetail.builder().
                    userId(user.getId()).
                    email(user.getEmail()).
                    sendTime(new Date()).
                    build());
            log.info("User email already send.");
        }
    }

其余代码保持不变。

总结

以上就是关于Spring Event的5个实用技巧,如果你想了解更多细节内容,我已将全部源码上传至Github仓库,你可以通过此链接获取全部源码。如有问题,请在文章最后给我留言。

(七讲)Spring Boot Annotations 全家桶快速通

(七讲)Spring Boot Annotations 全家桶快速通

本次课程将对Spring Boot 中所涉及到的注解进行全部的梳理,将开发过程中常用的Annotation进行介绍并给出相应的代码示例。帮助开发者统一的、规范的、全面的学习并掌握Spring Framework Annotations的用法。

1、Spring Web MVC 与Spring Bean 注解

1-1、Spring Web MVC 注解

1-1-1、@RequestMapping

@RequestMapping注解的主要用途是将Web请求与请求处理类中的方法进行映射。Spring MVC和Spring WebFlux都通过RquestMappingHandlerMappingRequestMappingHndlerAdapter两个类来提供对@RequestMapping注解的支持。

@RequestMapping注解对请求处理类中的请求处理方法进行标注;@RequestMapping注解拥有以下的六个配置属性:

  • value:映射的请求URL或者其别名
  • method:兼容HTTP的方法名
  • params:根据HTTP参数的存在、缺省或值对请求进行过滤
  • header:根据HTTP Header的存在、缺省或值对请求进行过滤
  • consume:设定在HTTP请求正文中允许使用的媒体类型
  • product:在HTTP响应体中允许使用的媒体类型

提示:在使用@RequestMapping之前,请求处理类还需要使用@Controller@RestController进行标记

下面是使用@RequestMapping的两个示例:

@RequestMapping还可以对类进行标记,这样类中的处理方法在映射请求路径时,会自动将类上@RequestMapping设置的value拼接到方法中映射路径之前,如下:

1-1-2、@RequestBody

@RequestBody在处理请求方法的参数列表中使用,它可以将请求主体中的参数绑定到一个对象中,请求主体参数是通过HttpMessageConverter传递的,根据请求主体中的参数名与对象的属性名进行匹配并绑定值。此外,还可以通过@Valid注解对请求主体中的参数进行校验。下面是一个使用@RequestBody的示例:

1-1-3、@GetMapping

@GetMapping注解用于处理HTTP GET请求,并将请求映射到具体的处理方法中。具体来说,@GetMapping是一个组合注解,它相当于是@RequestMapping(method=RequestMethod.GET)的快捷方式。下面是@GetMapping的一个使用示例:

1-1-4、@PostMapping

@PostMapping注解用于处理HTTP POST请求,并将请求映射到具体的处理方法中。@PostMapping@GetMapping一样,也是一个组合注解,它相当于是@RequestMapping(method=HttpMethod.POST)的快捷方式。下面是使用@PostMapping的一个示例:

1-1-5、@PutMapping

@PutMapping注解用于处理HTTP PUT请求,并将请求映射到具体的处理方法中,@PutMapping是一个组合注解,相当于是@RequestMapping(method=HttpMethod.PUT)的快捷方式。下面是使用@PutMapping的一个示例:

1-1-6、@DeleteMapping

@DeleteMapping注解用于处理HTTP DELETE请求,并将请求映射到删除方法中。@DeleteMapping是一个组合注解,它相当于是@RequestMapping(method=HttpMethod.DELETE)的快捷方式。下面是使用@DeleteMapping的一个示例:

1-1-7、@PatchMapping

@PatchMapping注解用于处理HTTP PATCH请求,并将请求映射到对应的处理方法中。@PatchMapping相当于是@RequestMapping(method=HttpMethod.PATCH)的快捷方式。下面是一个简单的示例:

1-1-8、@ControllerAdvice

@ControllerAdvice@Component注解的一个延伸注解,Spring会自动扫描并检测被@ControllerAdvice所标注的类。@ControllerAdvice需要和@ExceptionHandler@InitBinder以及@ModelAttribute注解搭配使用,主要是用来处理控制器所抛出的异常信息。首先,我们需要定义一个被@ControllerAdvice所标注的类,在该类中,定义一个用于处理具体异常的方法,并使用@ExceptionHandler注解进行标记。此外,在有必要的时候,可以使用@InitBinder在类中进行全局的配置,还可以使用@ModelAttribute配置与视图相关的参数。使用@ControllerAdvice注解,就可以快速的创建统一的,自定义的异常处理类。下面是一个使用@ControllerAdvice的示例代码:

1-1-9、@ResponseBody

@ResponseBody会自动将控制器中方法的返回值写入到HTTP响应中。特别的,@ResponseBody注解只能用在被@Controller注解标记的类中。如果在被@RestController标记的类中,则方法不需要使用@ResponseBody注解进行标注。@RestController相当于是@Controller@ResponseBody的组合注解。下面是使用该注解的一个示例:

1-1-10、@ExceptionHandler

@ExceptionHander注解用于标注处理特定类型异常类所抛出异常的方法。当控制器中的方法抛出异常时,Spring会自动捕获异常,并将捕获的异常信息传递给被@ExceptionHandler标注的方法。下面是使用该注解的一个示例:

1-1-11、@ResponseStatus

@ResponseStatus注解可以标注请求处理方法。使用此注解,可以指定响应所需要的HTTP STATUS。特别地,我们可以使用HttpStauts类对该注解的value属性进行赋值。下面是使用@ResponseStatus注解的一个示例:

1-1-12、@PathVariable

@PathVariable注解是将方法中的参数绑定到请求URI中的模板变量上。可以通过@RequestMapping注解来指定URI的模板变量,然后使用@PathVariable注解将方法中的参数绑定到模板变量上。特别地,@PathVariable注解允许我们使用value或name属性来给参数取一个别名。下面是使用此注解的一个示例:

​ 模板变量名需要使用“{ }”进行包裹,如果方法的参数名与URI模板变量名一致,则在@PathVariable中就可以省略别名的定义。下面是一个简写的示例:

提示:如果参数是一个非必须的,可选的项,则可以在@PathVariable中设置require = false

1-1-13、@RequestParam

@RequestParam注解用于将方法的参数与Web请求的传递的参数进行绑定。使用@RequestParam可以轻松的访问HTTP请求参数的值。下面是使用该注解的代码示例:

该注解的其他属性配置与@PathVariable的配置相同,特别的,如果传递的参数为空,还可以通过defaultValue设置一个默认值。示例代码如下:

1-1-14、@Controller

@Controller@Component注解的一个延伸,Spring会自动扫描并配置被该注解标注的类。此注解用于标注Spring MVC的控制器。下面是使用此注解的示例代码:

1-1-15、@RestController

@RestController是在Spring 4.0开始引入的,这是一个特定的控制器注解。此注解相当于@Controller@ResponseBody的快捷方式。当使用此注解时,不需要再在方法上使用@ResponseBody注解。下面是使用此注解的示例代码:

1-1-16、@ModelAttribute

​ 通过此注解,可以通过模型索引名称来访问已经存在于控制器中的model。下面是使用此注解的一个简单示例:

@PathVariable@RequestParam注解一样,如果参数名与模型具有相同的名字,则不必指定索引名称,简写示例如下:

特别地,如果使用@ModelAttribute对方法进行标注,Spring会将方法的返回值绑定到具体的Model上。示例如下:

在Spring调用具体的处理方法之前,被@ModelAttribute注解标注的所有方法都将被执行。

1-1-17、@CrossOrigin

@CrossOrigin注解将为请求处理类或请求处理方法提供跨域调用支持。如果我们将此注解标注类,那么类中的所有方法都将获得支持跨域的能力。使用此注解的好处是可以微调跨域行为。使用此注解的示例如下:

1-1-18、@InitBinder

@InitBinder注解用于标注初始化WebDataBinider的方法,该方法用于对Http请求传递的表单数据进行处理,如时间格式化、字符串处理等。下面是使用此注解的示例:

1-2、Spring Bean 注解

​ 在本小节中,主要列举与Spring Bean相关的4个注解以及它们的使用方式。

1-2-1、@ComponentScan

@ComponentScan注解用于配置Spring需要扫描的被组件注解注释的类所在的包。可以通过配置其basePackages属性或者value属性来配置需要扫描的包路径。value属性是basePackages的别名。此注解的用法如下:

1-2-2、@Component

@Component注解用于标注一个普通的组件类,它没有明确的业务范围,只是通知Spring被此注解的类需要被纳入到Spring Bean容器中并进行管理。此注解的使用示例如下:

1-2-3、@Service

@Service注解是@Component的一个延伸(特例),它用于标注业务逻辑类。与@Component注解一样,被此注解标注的类,会自动被Spring所管理。下面是使用@Service注解的示例:

1-2-4、@Repository

@Repository注解也是@Component注解的延伸,与@Component注解一样,被此注解标注的类会被Spring自动管理起来,@Repository注解用于标注DAO层的数据持久化类。此注解的用法如下:

2、Spring Dependency Inject 与 Bean Scops注解

2-1、Spring DI注解

2-1-1、@DependsOn

@DependsOn注解可以配置Spring IoC容器在初始化一个Bean之前,先初始化其他的Bean对象。下面是此注解使用示例代码:

2-1-2、@Bean

@Bean注解主要的作用是告知Spring,被此注解所标注的类将需要纳入到Bean管理工厂中。@Bean注解的用法很简单,在这里,着重介绍@Bean注解中initMethod和destroyMethod的用法。示例如下:

2-2、Scops注解

2-2-1、@Scope

@Scope注解可以用来定义@Component标注的类的作用范围以及@Bean所标记的类的作用范围。@Scope所限定的作用范围有:singleton、prototype、request、session、globalSession或者其他的自定义范围。这里以prototype为例子进行讲解。当一个Spring Bean被声明为prototype(原型模式)时,在每次需要使用到该类的时候,Spring IoC容器都会初始化一个新的改类的实例。在定义一个Bean时,可以设置Bean的scope属性为prototype:scope=“prototype”,也可以使用@Scope注解设置,如下:

@Scope(value=ConfigurableBeanFactory.SCOPE_PROPTOTYPE)

下面将给出两种不同的方式来使用@Scope注解,示例代码如下:

2-2-2、@Scope 单例模式

​ 当@Scope的作用范围设置成Singleton时,被此注解所标注的类只会被Spring IoC容器初始化一次。在默认情况下,Spring IoC容器所初始化的类实例都为singleton。同样的原理,此情形也有两种配置方式,示例代码如下:

3、容器配置注解

3-1、@Autowired

@Autowired注解用于标记Spring将要解析和注入的依赖项。此注解可以作用在构造函数、字段和setter方法上。

3-1-1、作用于构造函数

​ 下面是@Autowired注解标注构造函数的使用示例:

3-1-2、作用于setter方法

​ 下面是@Autowired注解标注setter方法的示例代码:

3-1-3、作用于字段

@Autowired注解标注字段是最简单的,只需要在对应的字段上加入此注解即可,示例代码如下:

3-2、@Primary

​ 当系统中需要配置多个具有相同类型的bean时,@Primary可以定义这些Bean的优先级。下面将给出一个实例代码来说明这一特性:

输出结果:

this is send DingDing method message.

3-3、@PostConstruct@PreDestroy

​ 值得注意的是,这两个注解不属于Spring,它们是源于JSR-250中的两个注解,位于common-annotations.jar中。@PostConstruct注解用于标注在Bean被Spring初始化之前需要执行的方法。@PreDestroy注解用于标注Bean被销毁前需要执行的方法。下面是具体的示例代码:

3-4、@Qualifier

​ 当系统中存在同一类型的多个Bean时,@Autowired在进行依赖注入的时候就不知道该选择哪一个实现类进行注入。此时,我们可以使用@Qualifier注解来微调,帮助@Autowired选择正确的依赖项。下面是一个关于此注解的代码示例:

4、Spring Boot注解

4-1、@SpringBootApplication

@SpringBootApplication注解是一个快捷的配置注解,在被它标注的类中,可以定义一个或多个Bean,并自动触发自动配置Bean和自动扫描组件。此注解相当于@Configuration@EnableAutoConfiguration@ComponentScan的组合。在Spring Boot应用程序的主类中,就使用了此注解。示例代码如下:

@SpringBootApplication
public class Application{

    public static void main(String [] args){
        SpringApplication.run(Application.class,args);
    }

}

4-2、@EnableAutoConfiguration

@EnableAutoConfiguration注解用于通知Spring,根据当前类路径下引入的依赖包,自动配置与这些依赖包相关的配置项。

4-3、@ConditionalOnClass@ConditionalOnMissingClass

​ 这两个注解属于类条件注解,它们根据是否存在某个类作为判断依据来决定是否要执行某些配置。下面是一个简单的示例代码:

@Configuration
@ConditionalOnClass(DataSource.class)
class MySQLAutoConfiguration {
    //...
}

4-4、@ConditionalOnBean@ConditionalOnMissingBean

​ 这两个注解属于对象条件注解,根据是否存在某个对象作为依据来决定是否要执行某些配置方法。示例代码如下:

@Bean
@ConditionalOnBean(name="dataSource")
LocalContainerEntityManagerFactoryBean entityManagerFactory(){
    //...
}

@Bean
@ConditionalOnMissingBean
public MyBean myBean(){
    //...
}

4-5、@ConditionalOnProperty

@ConditionalOnProperty注解会根据Spring配置文件中的配置项是否满足配置要求,从而决定是否要执行被其标注的方法。示例代码如下:

@Bean
@ConditionalOnProperty(name="alipay",havingValue="on")
Alipay alipay(){
    return new Alipay();
}

4-6、@ConditionalOnResource

​ 此注解用于检测当某个配置文件存在使,则触发被其标注的方法,下面是使用此注解的代码示例:

@ConditionalOnResource(resources = "classpath:website.properties")
Properties addWebsiteProperties(){
    //...
}

4-7、@ConditionalOnWebApplication@ConditionalOnNotWebApplication

​ 这两个注解用于判断当前的应用程序是否是Web应用程序。如果当前应用是Web应用程序,则使用Spring WebApplicationContext,并定义其会话的生命周期。下面是一个简单的示例:

@ConditionalOnWebApplication
HealthCheckController healthCheckController(){
    //...
}

4-8、@ConditionalExpression

​ 此注解可以让我们控制更细粒度的基于表达式的配置条件限制。当表达式满足某个条件或者表达式为真的时候,将会执行被此注解标注的方法。

@Bean
@ConditionalException("${localstore} && ${local == 'true'}")
LocalFileStore store(){
    //...
}

4-9、@Conditional

@Conditional注解可以控制更为复杂的配置条件。在Spring内置的条件控制注解不满足应用需求的时候,可以使用此注解定义自定义的控制条件,以达到自定义的要求。下面是使用该注解的简单示例:

@Conditioanl(CustomConditioanl.class)
CustomProperties addCustomProperties(){
    //...
}

总结

本次课程总结了Spring Boot中常见的各类型注解的使用方式,让大家能够统一的对Spring Boot常用注解有一个全面的了解。由于篇幅的原因,关于Spring Boot不常用的一些注解,将在下一次分享中进行补充和说明。

Spring Boot 构建多租户SaaS平台核心技术指南

Spring Boot 构建多租户SaaS平台核心技术指南

本次教程所涉及到的源码已上传至Github,如果你不需要继续阅读下面的内容,你可以直接点击此链接获取源码内容。https://github.com/ramostear/una-saas-toturial

1. 概述

笔者从2014年开始接触SaaS(Software as a Service),即多租户(或多承租)软件应用平台;并一直从事相关领域的架构设计及研发工作。机缘巧合,在笔者本科毕业设计时完成了一个基于SaaS的高效财务管理平台的课题研究,从中收获颇多。最早接触SaaS时,国内相关资源匮乏,唯一有的参照资料是《互联网时代的软件革命:SaaS架构设计》(叶伟等著)一书。最后课题的实现是基于OSGI(Open Service Gateway Initiative)Java动态模块化系统规范来实现的。

时至今日,五年的时间过去了,软件开发的技术发生了巨大的改变,笔者所实现SaaS平台的技术栈也更新了好几波,真是印证了那就话:“山重水尽疑无路,柳暗花明又一村”。基于之前走过的许多弯路和踩过的坑,以及近段时间有许多网友问我如何使用Spring Boot实现多租户系统,决定写一篇文章聊一聊关于SaaS的硬核技术。

说起SaaS,它只是一种软件架构,并没有多少神秘的东西,也不是什么很难的系统,我个人的感觉,SaaS平台的难度在于商业上的运营,而非技术上的实现。就技术上来说,SaaS是这样一种架构模式:它让多个不同环境的用户使用同一套应用程序,且保证用户之间的数据相互隔离。现在想想看,这也有点共享经济的味道在里面。

笔者在这里就不再深入聊SaaS软件成熟度模型和数据隔离方案对比的事情了。今天要聊的是使用Spring Boot快速构建独立数据库/共享数据库独立Schema的多租户系统。我将提供一个SaaS系统最核心的技术实现,而其他的部分有兴趣的朋友可以在此基础上自行扩展。

2. 尝试了解多租户的应用场景

假设我们需要开发一个应用程序,并且希望将同一个应用程序销售给N家客户使用。在常规情况下,我们需要为此创建N个Web服务器(Tomcat),N个数据库(DB),并为N个客户部署相同的应用程序N次。现在,如果我们的应用程序进行了升级或者做了其他任何的改动,那么我们就需要更新N个应用程序同时还需要维护N台服务器。接下来,如果业务开始增长,客户由原来的N个变成了现在的N+M个,我们将面临N个应用程序和M个应用程序版本维护,设备维护以及成本控制的问题。运维几乎要哭死在机房了…

为了解决上述的问题,我们可以开发多租户应用程序,我们可以根据当前用户是谁,从而选择对应的数据库。例如,当请求来自A公司的用户时,应用程序就连接A公司的数据库,当请求来自B公司的用户时,自动将数据库切换到B公司数据库,以此类推。从理论上将没有什么问题,但我们如果考虑将现有的应用程序改造成SaaS模式,我们将遇到第一个问题:如果识别请求来自哪一个租户?如何自动切换数据源?

3. 维护、识别和路由租户数据源

我们可以提供一个独立的库来存放租户信息,如数据库名称、链接地址、用户名、密码等,这可以统一的解决租户信息维护的问题。租户的识别和路由有很多种方法可以解决,下面列举几个常用的方式:

  • 1.可以通过域名的方式来识别租户:我们可以为每一个租户提供一个唯一的二级域名,通过二级域名就可以达到识别租户的能力,如tenantone.example.com,tenant.example.com;tenantone和tenant就是我们识别租户的关键信息。
  • 2.可以将租户信息作为请求参数传递给服务端,为服务端识别租户提供支持,如saas.example.com?tenantId=tenant1,saas.example.com?tenantId=tenant2。其中的参数tenantId就是应用程序识别租户的关键信息。
  • 3.可以在请求头(Header)中设置租户信息,例如JWT等技术,服务端通过解析Header中相关参数以获得租户信息。
  • 4.在用户成功登录系统后,将租户信息保存在Session中,在需要的时候从Session取出租户信息。

解决了上述问题后,我们再来看看如何获取客户端传入的租户信息,以及在我们的业务代码中如何使用租户信息(最关键的是DataSources的问题)。

我们都知道,在启动Spring Boot应用程序之前,就需要为其提供有关数据源的配置信息(有使用到数据库的情况下),按照一开始的需求,有N个客户需要使用我们的应用程序,我们就需要提前配置好N个数据源(多数据源),如果N<50,我认为我还能忍受,如果更多,这样显然是无法接受的。为了解决这一问题,我们需要借助Hibernate 5提供的动态数据源特性,让我们的应用程序具备动态配置客户端数据源的能力。简单来说,当用户请求系统资源时,我们将用户提供的租户信息(tenantId)存放在ThreadLoacal中,紧接着获取TheadLocal中的租户信息,并根据此信息查询单独的租户库,获取当前租户的数据配置信息,然后借助Hibernate动态配置数据源的能力,为当前请求设置数据源,最后之前用户的请求。这样我们就只需要在应用程序中维护一份数据源配置信息(租户数据库配置库),其余的数据源动态查询配置。接下来,我们将快速的演示这一功能。

4. 项目构建

我们将使用Spring Boot 2.1.5版本来实现这一演示项目,首先你需要在Maven配置文件中加入如下的一些配置:

<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.47</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-freemarker</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
        </dependency>
    </dependencies>

然后提供一个可用的配置文件,并加入如下的内容:

spring:
  freemarker:
    cache: false
    template-loader-path:
    - classpath:/templates/
    prefix:
    suffix: .html
  resources:
    static-locations:
    - classpath:/static/
  devtools:
    restart:
      enabled: true
  jpa:
    database: mysql
    show-sql: true
    generate-ddl: false
    hibernate:
      ddl-auto: none
una:
  master:
    datasource:
      url:  jdbc:mysql://localhost:3306/master_tenant?useSSL=false
      username: root
      password: root
      driverClassName:  com.mysql.jdbc.Driver
      maxPoolSize:  10
      idleTimeout:  300000
      minIdle:  10
      poolName: master-database-connection-pool
logging:
  level:
    root: warn
    org:
      springframework:
        web:  debug
      hibernate: debug

由于采用Freemarker作为视图渲染引擎,所以需要提供Freemarker的相关技术

una:master:datasource配置项就是上面说的统一存放租户信息的数据源配置信息,你可以理解为主库。

接下来,我们需要关闭Spring Boot自动配置数据源的功能,在项目主类上添加如下的设置:

@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class UnaSaasApplication {

    public static void main(String[] args) {
        SpringApplication.run(UnaSaasApplication.class, args);
    }

}

最后,让我们看看整个项目的结构:

5. 实现租户数据源查询模块

我们将定义一个实体类存放租户数据源信息,它包含了租户名,数据库连接地址,用户名和密码等信息,其代码如下:

@Data
@Entity
@Table(name = "MASTER_TENANT")
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class MasterTenant implements Serializable{

    @Id
    @Column(name="ID")
    private String id;

    @Column(name = "TENANT")
    @NotEmpty(message = "Tenant identifier must be provided")
    private String tenant;

    @Column(name = "URL")
    @Size(max = 256)
    @NotEmpty(message = "Tenant jdbc url must be provided")
    private String url;

    @Column(name = "USERNAME")
    @Size(min = 4,max = 30,message = "db username length must between 4 and 30")
    @NotEmpty(message = "Tenant db username must be provided")
    private String username;

    @Column(name = "PASSWORD")
    @Size(min = 4,max = 30)
    @NotEmpty(message = "Tenant db password must be provided")
    private String password;

    @Version
    private int version = 0;
}

持久层我们将继承JpaRepository接口,快速实现对数据源的CURD操作,同时提供了一个通过租户名查找租户数据源的接口,其代码如下:

package com.ramostear.una.saas.master.repository;

import com.ramostear.una.saas.master.model.MasterTenant;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

/**
 * @author : Created by Tan Chaohong (alias:ramostear)
 * @create-time 2019/5/25 0025-8:22
 * @modify by :
 * @since:
 */
@Repository
public interface MasterTenantRepository extends JpaRepository<MasterTenant,String>{

    @Query("select p from MasterTenant p where p.tenant = :tenant")
    MasterTenant findByTenant(@Param("tenant") String tenant);
}

业务层提供通过租户名获取租户数据源信息的服务(其余的服务各位可自行添加):

package com.ramostear.una.saas.master.service;

import com.ramostear.una.saas.master.model.MasterTenant;

/**
 * @author : Created by Tan Chaohong (alias:ramostear)
 * @create-time 2019/5/25 0025-8:26
 * @modify by :
 * @since:
 */

public interface MasterTenantService {
    /**
     * Using custom tenant name query
     * @param tenant    tenant name
     * @return          masterTenant
     */
    MasterTenant findByTenant(String tenant);
}

最后,我们需要关注的重点是配置主数据源(Spring Boot需要为其提供一个默认的数据源)。在配置之前,我们需要获取配置项,可以通过@ConfigurationProperties(“una.master.datasource”)获取配置文件中的相关配置信息:

@Getter
@Setter
@Configuration
@ConfigurationProperties("una.master.datasource")
public class MasterDatabaseProperties {

    private String url;

    private String password;

    private String username;

    private String driverClassName;

    private long connectionTimeout;

    private int maxPoolSize;

    private long idleTimeout;

    private int minIdle;

    private String poolName;

    @Override
    public String toString(){
        StringBuilder builder = new StringBuilder();
        builder.append("MasterDatabaseProperties [ url=")
                .append(url)
                .append(", username=")
                .append(username)
                .append(", password=")
                .append(password)
                .append(", driverClassName=")
                .append(driverClassName)
                .append(", connectionTimeout=")
                .append(connectionTimeout)
                .append(", maxPoolSize=")
                .append(maxPoolSize)
                .append(", idleTimeout=")
                .append(idleTimeout)
                .append(", minIdle=")
                .append(minIdle)
                .append(", poolName=")
                .append(poolName)
                .append("]");
        return builder.toString();
    }
}

接下来是配置自定义的数据源,其源码如下:

package com.ramostear.una.saas.master.config;

import com.ramostear.una.saas.master.config.properties.MasterDatabaseProperties;
import com.ramostear.una.saas.master.model.MasterTenant;
import com.ramostear.una.saas.master.repository.MasterTenantRepository;
import com.zaxxer.hikari.HikariDataSource;
import lombok.extern.slf4j.Slf4j;
import org.hibernate.cfg.Environment;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.annotation.EnableTransactionManagement;

import javax.persistence.EntityManagerFactory;
import javax.sql.DataSource;
import java.util.Properties;

/**
 * @author : Created by Tan Chaohong (alias:ramostear)
 * @create-time 2019/5/25 0025-8:31
 * @modify by :
 * @since:
 */
@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(basePackages = {"com.ramostear.una.saas.master.model","com.ramostear.una.saas.master.repository"},
                       entityManagerFactoryRef = "masterEntityManagerFactory",
                       transactionManagerRef = "masterTransactionManager")
@Slf4j
public class MasterDatabaseConfig {

    @Autowired
    private MasterDatabaseProperties masterDatabaseProperties;

    @Bean(name = "masterDatasource")
    public DataSource masterDatasource(){
        log.info("Setting up masterDatasource with :{}",masterDatabaseProperties.toString());
        HikariDataSource datasource = new HikariDataSource();
        datasource.setUsername(masterDatabaseProperties.getUsername());
        datasource.setPassword(masterDatabaseProperties.getPassword());
        datasource.setJdbcUrl(masterDatabaseProperties.getUrl());
        datasource.setDriverClassName(masterDatabaseProperties.getDriverClassName());
        datasource.setPoolName(masterDatabaseProperties.getPoolName());
        datasource.setMaximumPoolSize(masterDatabaseProperties.getMaxPoolSize());
        datasource.setMinimumIdle(masterDatabaseProperties.getMinIdle());
        datasource.setConnectionTimeout(masterDatabaseProperties.getConnectionTimeout());
        datasource.setIdleTimeout(masterDatabaseProperties.getIdleTimeout());
        log.info("Setup of masterDatasource successfully.");
        return datasource;
    }

    @Primary
    @Bean(name = "masterEntityManagerFactory")
    public LocalContainerEntityManagerFactoryBean masterEntityManagerFactory(){
        LocalContainerEntityManagerFactoryBean lb = new LocalContainerEntityManagerFactoryBean();
        lb.setDataSource(masterDatasource());
        lb.setPackagesToScan(
           new String[]{MasterTenant.class.getPackage().getName(), MasterTenantRepository.class.getPackage().getName()}
        );

        //Setting a name for the persistence unit as Spring sets it as 'default' if not defined.
        lb.setPersistenceUnitName("master-database-persistence-unit");

        //Setting Hibernate as the JPA provider.
        JpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
        lb.setJpaVendorAdapter(vendorAdapter);

        //Setting the hibernate properties
        lb.setJpaProperties(hibernateProperties());

        log.info("Setup of masterEntityManagerFactory successfully.");
        return lb;
    }

    @Bean(name = "masterTransactionManager")
    public JpaTransactionManager masterTransactionManager(@Qualifier("masterEntityManagerFactory")EntityManagerFactory emf){
        JpaTransactionManager transactionManager = new JpaTransactionManager();
        transactionManager.setEntityManagerFactory(emf);
        log.info("Setup of masterTransactionManager successfully.");
        return transactionManager;
    }

    @Bean
    public PersistenceExceptionTranslationPostProcessor exceptionTranslationPostProcessor(){
        return new PersistenceExceptionTranslationPostProcessor();
    }

    private Properties hibernateProperties(){
        Properties properties = new Properties();
        properties.put(Environment.DIALECT,"org.hibernate.dialect.MySQL5Dialect");
        properties.put(Environment.SHOW_SQL,true);
        properties.put(Environment.FORMAT_SQL,true);
        properties.put(Environment.HBM2DDL_AUTO,"update");
        return properties;
    }
}

在改配置类中,我们主要提供包扫描路径,实体管理工程,事务管理器和数据源配置参数的配置。

6. 实现租户业务模块

在此小节中,租户业务模块我们仅提供一个用户登录的场景来演示SaaS的功能。其实体层、业务层和持久化层根普通的Spring Boot Web项目没有什么区别,你甚至感觉不到它是一个SaaS应用程序的代码。

首先,创建一个用户实体User,其源码如下:

@Entity
@Table(name = "USER")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class User implements Serializable {
    private static final long serialVersionUID = -156890917814957041L;

    @Id
    @Column(name = "ID")
    private String id;

    @Column(name = "USERNAME")
    private String username;

    @Column(name = "PASSWORD")
    @Size(min = 6,max = 22,message = "User password must be provided and length between 6 and 22.")
    private String password;

    @Column(name = "TENANT")
    private String tenant;
}

业务层提供了一个根据用户名检索用户信息的服务,它将调用持久层的方法根据用户名对租户的用户表进行检索,如果找到满足条件的用户记录,则返回用户信息,如果没有找到,则返回null;持久层和业务层的源码分别如下:

@Repository
public interface UserRepository extends JpaRepository<User,String>,JpaSpecificationExecutor<User>{

    User findByUsername(String username);
}
@Service("userService")
public class UserServiceImpl implements UserService{

    @Autowired
    private UserRepository userRepository;

    private static TwitterIdentifier identifier = new TwitterIdentifier();



    @Override
    public void save(User user) {
        user.setId(identifier.generalIdentifier());
        user.setTenant(TenantContextHolder.getTenant());
        userRepository.save(user);
    }

    @Override
    public User findById(String userId) {
        Optional<User> optional = userRepository.findById(userId);
        if(optional.isPresent()){
            return optional.get();
        }else{
            return null;
        }
    }

    @Override
    public User findByUsername(String username) {
        System.out.println(TenantContextHolder.getTenant());
        return userRepository.findByUsername(username);
    }

在这里,我们采用了Twitter的雪花算法来实现了一个ID生成器。

7. 配置拦截器

我们需要提供一个租户信息的拦截器,用以获取租户标识符,其源代码和配置拦截器的源代码如下:

/**
 * @author : Created by Tan Chaohong (alias:ramostear)
 * @create-time 2019/5/26 0026-23:17
 * @modify by :
 * @since:
 */
@Slf4j
public class TenantInterceptor implements HandlerInterceptor{

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String tenant = request.getParameter("tenant");
        if(StringUtils.isBlank(tenant)){
            response.sendRedirect("/login.html");
            return false;
        }else{
            TenantContextHolder.setTenant(tenant);
            return true;
        }
    }
}
@Configuration
public class InterceptorConfig extends WebMvcConfigurationSupport {

    @Override
    protected void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new TenantInterceptor()).addPathPatterns("/**").excludePathPatterns("/login.html");
        super.addInterceptors(registry);
    }
}

/login.html是系统的登录路径,我们需要将其排除在拦截器拦截的范围之外,否则我们永远无法进行登录

8. 维护租户标识信息

在这里,我们使用ThreadLocal来存放租户标识信息,为动态设置数据源提供数据支持,该类提供了设置租户标识、获取租户标识以及清除租户标识三个静态方法。其源码如下:

public class TenantContextHolder {

    private static final ThreadLocal<String> CONTEXT = new ThreadLocal<>();

    public static void setTenant(String tenant){
        CONTEXT.set(tenant);
    }

    public static String getTenant(){
        return CONTEXT.get();
    }

    public static void clear(){
        CONTEXT.remove();
    }
}

此类时实现动态数据源设置的关键

9. 动态数据源切换

要实现动态数据源切换,我们需要借助两个类来完成,CurrentTenantIdentifierResolver和AbstractDataSourceBasedMultiTenantConnectionProviderImpl。从它们的命名上就可以看出,一个负责解析租户标识,一个负责提供租户标识对应的租户数据源信息。

首先,我们需要实现CurrentTenantIdentifierResolver接口中的resolveCurrentTenantIdentifier()和validateExistingCurrentSessions()方法,完成租户标识的解析功能。实现类的源码如下:

package com.ramostear.una.saas.tenant.config;

import com.ramostear.una.saas.context.TenantContextHolder;
import org.apache.commons.lang3.StringUtils;
import org.hibernate.context.spi.CurrentTenantIdentifierResolver;

/**
 * @author : Created by Tan Chaohong (alias:ramostear)
 * @create-time 2019/5/26 0026-22:38
 * @modify by :
 * @since:
 */
public class CurrentTenantIdentifierResolverImpl implements CurrentTenantIdentifierResolver {

    /**
     * 默认的租户ID
     */
    private static final String DEFAULT_TENANT = "tenant_1";

    /**
     * 解析当前租户的ID
     * @return
     */
    @Override
    public String resolveCurrentTenantIdentifier() {
        //通过租户上下文获取租户ID,此ID是用户登录时在header中进行设置的
        String tenant = TenantContextHolder.getTenant();
        //如果上下文中没有找到该租户ID,则使用默认的租户ID,或者直接报异常信息
        return StringUtils.isNotBlank(tenant)?tenant:DEFAULT_TENANT;
    }

    @Override
    public boolean validateExistingCurrentSessions() {
        return true;
    }
}

此类的逻辑非常简单,就是从ThreadLocal中获取当前设置的租户标识符

有了租户标识符解析类之后,我们需要扩展租户数据源提供类,实现从数据库动态查询租户数据源信息,其源码如下:

@Slf4j
@Configuration
public class DataSourceBasedMultiTenantConnectionProviderImpl extends AbstractDataSourceBasedMultiTenantConnectionProviderImpl{

    private static final long serialVersionUID = -7522287771874314380L;

    @Autowired
    private MasterTenantRepository masterTenantRepository;

    private Map<String,DataSource> dataSources = new TreeMap<>();

    @Override
    protected DataSource selectAnyDataSource() {
        if(dataSources.isEmpty()){
            List<MasterTenant> tenants = masterTenantRepository.findAll();
            tenants.forEach(masterTenant->{
                dataSources.put(masterTenant.getTenant(), DataSourceUtils.wrapperDataSource(masterTenant));
            });
        }
        return dataSources.values().iterator().next();
    }

    @Override
    protected DataSource selectDataSource(String tenant) {
        if(!dataSources.containsKey(tenant)){
            List<MasterTenant> tenants = masterTenantRepository.findAll();
            tenants.forEach(masterTenant->{
                dataSources.put(masterTenant.getTenant(),DataSourceUtils.wrapperDataSource(masterTenant));
            });
        }
        return dataSources.get(tenant);
    }
}

在该类中,通过查询租户数据源库,动态获得租户数据源信息,为租户业务模块的数据源配置提供数据数据支持。

最后,我们还需要提供租户业务模块数据源配置,这是整个项目核心的地方,其代码如下:

@Slf4j
@Configuration
@EnableTransactionManagement
@ComponentScan(basePackages = {
        "com.ramostear.una.saas.tenant.model",
        "com.ramostear.una.saas.tenant.repository"
})
@EnableJpaRepositories(basePackages = {
        "com.ramostear.una.saas.tenant.repository",
        "com.ramostear.una.saas.tenant.service"
},entityManagerFactoryRef = "tenantEntityManagerFactory"
,transactionManagerRef = "tenantTransactionManager")
public class TenantDataSourceConfig {

    @Bean("jpaVendorAdapter")
    public JpaVendorAdapter jpaVendorAdapter(){
        return new HibernateJpaVendorAdapter();
    }

    @Bean(name = "tenantTransactionManager")
    public JpaTransactionManager transactionManager(EntityManagerFactory entityManagerFactory){
        JpaTransactionManager transactionManager = new JpaTransactionManager();
        transactionManager.setEntityManagerFactory(entityManagerFactory);
        return transactionManager;
    }

    @Bean(name = "datasourceBasedMultiTenantConnectionProvider")
    @ConditionalOnBean(name = "masterEntityManagerFactory")
    public MultiTenantConnectionProvider multiTenantConnectionProvider(){
        return new DataSourceBasedMultiTenantConnectionProviderImpl();
    }

    @Bean(name = "currentTenantIdentifierResolver")
    public CurrentTenantIdentifierResolver currentTenantIdentifierResolver(){
        return new CurrentTenantIdentifierResolverImpl();
    }

    @Bean(name = "tenantEntityManagerFactory")
    @ConditionalOnBean(name = "datasourceBasedMultiTenantConnectionProvider")
    public LocalContainerEntityManagerFactoryBean entityManagerFactory(
            @Qualifier("datasourceBasedMultiTenantConnectionProvider")MultiTenantConnectionProvider connectionProvider,
            @Qualifier("currentTenantIdentifierResolver")CurrentTenantIdentifierResolver tenantIdentifierResolver
    ){
        LocalContainerEntityManagerFactoryBean localBean = new LocalContainerEntityManagerFactoryBean();
        localBean.setPackagesToScan(
                new String[]{
                        User.class.getPackage().getName(),
                        UserRepository.class.getPackage().getName(),
                        UserService.class.getPackage().getName()

                }
        );
        localBean.setJpaVendorAdapter(jpaVendorAdapter());
        localBean.setPersistenceUnitName("tenant-database-persistence-unit");
        Map<String,Object> properties = new HashMap<>();
        properties.put(Environment.MULTI_TENANT, MultiTenancyStrategy.SCHEMA);
        properties.put(Environment.MULTI_TENANT_CONNECTION_PROVIDER,connectionProvider);
        properties.put(Environment.MULTI_TENANT_IDENTIFIER_RESOLVER,tenantIdentifierResolver);
        properties.put(Environment.DIALECT,"org.hibernate.dialect.MySQL5Dialect");
        properties.put(Environment.SHOW_SQL,true);
        properties.put(Environment.FORMAT_SQL,true);
        properties.put(Environment.HBM2DDL_AUTO,"update");
        localBean.setJpaPropertyMap(properties);
        return localBean;
    }
}

在改配置文件中,大部分内容与主数据源的配置相同,唯一的区别是租户标识解析器与租户数据源补给源的设置,它将告诉Hibernate在执行数据库操作命令前,应该设置什么样的数据库连接信息,以及用户名和密码等信息。

10. 应用测试

最后,我们通过一个简单的登录案例来测试本次课程中的SaaS应用程序,为此,需要提供一个Controller用于处理用户登录逻辑。在本案例中,没有严格的对用户密码进行加密,而是使用明文进行比对,也没有提供任何的权限认证框架,知识单纯的验证SaaS的基本特性是否具备。登录控制器代码如下:

/**
 * @author : Created by Tan Chaohong (alias:ramostear)
 * @create-time 2019/5/27 0027-0:18
 * @modify by :
 * @since:
 */
@Controller
public class LoginController {

    @Autowired
    private UserService userService;

    @GetMapping("/login.html")
    public String login(){
        return "/login";
    }

    @PostMapping("/login")
    public String login(@RequestParam(name = "username") String username, @RequestParam(name = "password")String password, ModelMap model){
        System.out.println("tenant:"+TenantContextHolder.getTenant());
        User user = userService.findByUsername(username);
        if(user != null){
            if(user.getPassword().equals(password)){
                model.put("user",user);
                return "/index";
            }else{
                return "/login";
            }
        }else{
            return "/login";
        }
    }
}

在启动项目之前,我们需要为主数据源创建对应的数据库和数据表,用于存放租户数据源信息,同时还需要提供一个租户业务模块数据库和数据表,用来存放租户业务数据。一切准备就绪后,启动项目,在浏览器中输入:http://localhost:8080/login.html

在登录窗口中输入对应的租户名,用户名和密码,测试是否能够正常到达主页。可以多增加几个租户和用户,测试用户是否正常切换到对应的租户下。

总结

在这里,我分享了使用Spring Boot+JPA快速实现多租户应用程序的方法,此方法只涉及了实现SaaS应用平台的最核心技术手段,并不是一个完整可用的项目代码,如用户的认证、授权等并未出现在本文中。额外的业务模块感兴趣的朋友可以在此设计基础上自行扩展,如对其中的代码有任何的疑问,欢迎大家在下方给我留言。

(六讲)Spring Boot REST API异常处理指南

(六讲)Spring Boot REST API异常处理指南

能够正确的处理REST API程序抛出的异常以及返回友好的异常信息是一件非常重要的事情,因为它可以帮助API客户端正确的对服务端的问题作出正确的响应。这有助于提高REST API的服务质量。Spring Boot默认返回的异常信息对于API客户端来说是晦涩难懂的,只有开发者才会关注那些堆栈异常报告。在本讲中,将对如何处理好Spring REST API异常信息做一个梳理。

最近一段时间,Spring Boot成为了Java开发圈子的网红,越来越多的开发者选择Spring Boot来构建REST API。使用Spring Boot,能够帮助开发者减少模板代码和配置文件的编写工作量。Spring Boot开箱即用的特性,受到广大开发者的热宠。在本讲中,通过一个精简的Demo项目,着重介绍一些Spring Boot REST API的异常处理技巧。

如果你不想阅读本次内容,只是想快速获得相关的源码,那你可以直接跳转到文章的结尾,找到Gihub仓库链接,通过该链接,你可以轻松的获得本次内容的全部源码。

1. 定义明确的异常信息

当程序发送错误时,不应该将晦涩的堆栈报告信息返回给API客户端,从某种意义将,这是一种不礼貌的和不负责任的行为。现在,我们将模拟这样一个需求,API客户端可以向服务端发送请求以获取一个或者多个用户信息,同时还可以发送请求创建一个新的用户信息。下面是大致的一个API信息:

API 名称 说明
GET /users/{userId} 根据用户ID检索用户信息,如果没有找到,则返回用户未找到异常信息
GET /users 根据传入的ID集合,检索用户信息,若未找到,返回未找到用户异常信息
POST /users 创建一个新的用户

Spring MVC为我们提供了一些很有用的功能,以帮助我们解决系统的异常信息,并将有用的提示信息返回给API客户端。

以 POST /users 创建一个新用户为例,当我们提供正常的用户数据并请求此接口时,REST API将返回如下的提示信息:

{
  "id": 2,
  "username": "wukong",
  "age": 52,
  "height": 170
}

现在,将用户年龄修改为200岁,身高修改为500厘米,用户名为rulai ,在此请求此REST API,观察API的返回信息:

{
  "restapierror": {
    "status": "BAD_REQUEST",
    "timestamp": "2019-05-19 06:04:47",
    "message": "Validation error",
    "subErrors": [
      {
        "object": "user",
        "field": "height",
        "rejectedValue": 500,
        "message": "用户身高不能超过250厘米"
      },
      {
        "object": "user",
        "field": "age",
        "rejectedValue": 200,
        "message": "用户年龄不能超过120岁"
      }
    ]
  }
}

如上所示,当API客户端提供不正确的数据时,REST API返回了格式良好的异常提示信息,timestamp由原来的整形时间戳格式化为一个可读的日期+时间,同时还详细列举了详细的错误报告。

2. 包装异常信息

为了能够提供一个可读的JSON格式异常信息给API客户端,我们需要在项目中引入Jackson JSR 310的依赖包,使用其提供的@JsonFormat注解将Java中的日期和时间按照我们给定的日期和时间模板进行格式化。现在,将下面的依赖包加入的Maven pom.xml文件中:

<dependency>
    <groupId>com.fasterxml.jackson.datatype</groupId>
    <artifactId>jackson-datatype-jsr310</artifactId>
    <version>2.9.8</version>
</dependency>

依赖就绪后,我们需要提供一个异常信息的包装类:RestApiError。它将负责对REST API抛出的异常信息进行封装:

public class RestApiError {

    private HttpStatus status;

    @JsonFormat(shape = JsonFormat.Shape.STRING,pattern = "yyyy-MM-dd hh:mm:ss")
    private LocalDateTime timestamp;

    private String message;

    private String debugMessage;

    private List<RestApiSubError> subErrors;

    private RestApiError(){
        timestamp = LocalDateTime.now();
    }

    RestApiError(HttpStatus status){
        this();
        this.status = status;
    }

    RestApiError(HttpStatus status,Throwable ex){
        this();
        this.status = status;
        this.message = "Unexpected error";
        this.debugMessage = ex.getLocalizedMessage();
    }

    RestApiError(HttpStatus status,String message,Throwable ex){
        this();
        this.status = status;
        this.message = message;
        this.debugMessage = ex.getLocalizedMessage();
    }
}
  • status 属性用于记录响应状态。它沿用了HttpStatus的所有状态吗,如4xx和5xx。
  • timestamp属性用于记录发送错误的时间
  • message属性用于记录自定义的异常消息,通常是对API客户端友好的提示信息
  • debugMessage属性用于记录更为详细的错误报告
  • subErrors属性用于记录异常附带的子异常信息,如用户实体中字段校验信息等

RestApiSubError类用于记录更为细致的异常信息,通常为实体类中字段校验失败的异常报告:

abstract class RestApiSubError{}
@Data
@EqualsAndHashCode(callSuper = false)
@AllArgsConstructor
class RestApiValidationError extends RestApiSubError{
    private String object;
    private String field;
    private Object rejectedValue;
    private String message;

    RestApiValidationError(String object,String message){
        this.object = object;
        this.message = message;
    }
}

RestApiSubError类是一个抽象的空类,具体的扩展将在RestApiValidationError中进行实现。RestApiValidationError类将记录实体类中(如本讲中的User对象)属性校验失败报告。

现在,我们来校验GET /users/1 API,检索用户ID为1的用户信息:

{
  "id": 1,
  "username": "ramostear",
  "age": 28,
  "height": 170
}

REST API成功的返回了用户信息,接下来我们传入一个系统不存在的用户ID,看看REST API返回什么信息:

GET /users/100

{
  "restapierror": {
    "status": "NOT_FOUND",
    "timestamp": "2019-05-19 06:31:17",
    "message": "User was not found for parameters {id=100}"
  }
}

通过上述的JSON信息我们可以看到,检索不存在的用户信息,REST API返回了友好的提示信息。在一开始的时候我们测试提供不合符规范的用户年龄和身高信息,接下来我们在来测试一下提供一个空的用户名,观察REST API返回的信息:

{
  "restapierror": {
    "status": "BAD_REQUEST",
    "timestamp": "2019-05-19 06:37:46",
    "message": "Validation error",
    "subErrors": [
      {
        "object": "user",
        "field": "username",
        "rejectedValue": "",
        "message": "不能为空"
      }
    ]
  }
}

3. Spring Boot 处理异常信息的流程

Spring Boot 处理REST API异常信息将会涉及到三个注解:

@ControllerAdivice注解是在Spring 3.2版本中新增的一个注解,它能够将单个由@ExceptionHandler注解标注的方法应用到多个控制器中。使用它的好处是我们可以在一个统一的地方同时处理多个控制器抛出的异常,当控制器有异常抛出时,ControllerAdvice会根据当前抛出的异常类型,自动匹配对应的ExceptionHandler;当没有特定的Exception可用时,将调用默认的异常信息处理类来处理控制器抛出的异常(默认的异常信息处理类)。

下面,我们通过一张流程示例图,更为直观的了解Spring Application处理控制器异常信息的全部过程:

在图中,蓝色箭头表示正常的请求和响应过程,红色箭头表示发生异常的请求和响应过程。

4. 自定义异常信息处理类

Spring Framework自带的异常信息处理类往往不能满足我们实际的业务需求,这就需要我们定义符合具体情况的异常信息处理类,在自定义异常信息处理类中,我们可以封装更为详细的异常报告。

自定义异常信息处理类,我们可以站在“巨人”的肩膀上,快速封装自己的异常信息处理类,而不必要从头开始造“轮子”。现在,为了快熟实现自定义异常信息处理类,并让其正常工作,我们可以直接扩展Spring 提供的ResponseEntityExceptionHandler类来定义用户异常信息处理类。ResponseEntityExceptionHandler已经提供了很多可用的功能,我们只需要扩展该类或者覆盖其提供的方法即可。

打开ResponseEntityExceptionHandler类,我们可以看到如下的源码:

public abstract class ResponseEntityExceptionHandler {

 //不支持的HTTP请求方法异常信息处理方法
 protected ResponseEntity<Object> handleHttpRequestMethodNotSupported(...){...}
//不支持的HTTP媒体类型异常处理方法
 protected ResponseEntity<Object> handleHttpMediaTypeNotSupported(...){...}   
//不接受的HTTP媒体类型异常处方法
 protected ResponseEntity<Object> handleHttpMediaTypeNotAcceptable(...){...}
 //请求路径参数缺失异常处方法
 protected ResponseEntity<Object> handleMissingPathVariable(...){...}   
 //缺少servlet请求参数异常处理方法   
 protected ResponseEntity<Object> handleMissingServletRequestParameter(...){...}
 //servlet请求绑定异常
 protected ResponseEntity<Object> handleServletRequestBindingException(...){...}   
//不支持转换
 protected ResponseEntity<Object> handleConversionNotSupported(...){...}
 //类型不匹配
 protected ResponseEntity<Object> handleTypeMismatch(...){...}
 //消息无法检索
 protected ResponseEntity<Object> handleHttpMessageNotReadable(...){...}
 //HTTP消息不可写
 protected ResponseEntity<Object> handleHttpMessageNotWritable(...){...}
 //方法参数无效
 protected ResponseEntity<Object> handleMethodArgumentNotValid(...){...}
 //缺少servlet请求部分
 protected ResponseEntity<Object> handleMissingServletRequestPart(...){...}
 //绑定异常
 protected ResponseEntity<Object> handleBindException(...){...}
 //没有发现处理程序异常
 protected ResponseEntity<Object> handleNoHandlerFoundException(...){...}   
 //异步请求超时异常   
 @Nullable
 protected ResponseEntity<Object> handleAsyncRequestTimeoutException(...){...}
 //内部异常
 protected ResponseEntity<Object> handleExceptionInternal(...){...}       
}

我们选择性的覆盖几个常用的异常处理方法,并添加我们自定义异常处理方法:

public class RestExceptionHandler extends ResponseEntityExceptionHandler {

    @ExceptionHandler(UserNotFoundException.class)
    protected ResponseEntity<Object> handleUserNotFound(UserNotFoundException ex){
        RestApiError apiError = new RestApiError(HttpStatus.NOT_FOUND);
        apiError.setMessage(ex.getMessage());
        return buildResponseEntity(apiError);
    }


    @Override
    protected ResponseEntity<Object> handleMissingServletRequestParameter(
            MissingServletRequestParameterException ex, HttpHeaders headers,
            HttpStatus status, WebRequest request) {
        String error = ex.getParameterName() + " parameter is missing";
        return buildResponseEntity(new RestApiError(BAD_REQUEST, error, ex));
    }



    @Override
    protected ResponseEntity<Object> handleHttpMediaTypeNotSupported(
            HttpMediaTypeNotSupportedException ex,
            HttpHeaders headers,
            HttpStatus status,
            WebRequest request) {
        StringBuilder builder = new StringBuilder();
        builder.append(ex.getContentType());
        builder.append(" media type is not supported. Supported media types are ");
        ex.getSupportedMediaTypes().forEach(t -> builder.append(t).append(", "));
        return buildResponseEntity(new RestApiError(HttpStatus.UNSUPPORTED_MEDIA_TYPE, builder.substring(0, builder.length() - 2), ex));
    }

    ...

}

UserNotFoundException类为我们自定义异常信息类,在执行GET /users/{userIds}或 GET /users请求时,如果数据库中不存在该ID的记录信息,将抛出UserNotFoundException异常信息,且将响应状态码设置为NOT_FOUND。UserNotFoundException源码如下:

public class UserNotFoundException extends Exception {

    public UserNotFoundException(Class clz,String...searchParams){
        super(UserNotFoundException.generateMessage(clz.getSimpleName(),toMap(String.class,String.class,searchParams)));
    }

    private static String generateMessage(String entity, Map<String,String> searchParams){
        return StringUtils.capitalize(entity)+
                " was not found for parameters "+
                searchParams;
    }

    private static <K,V> Map<K,V> toMap(Class<K> key,Class<V> value,Object...entries){
        if(entries.length % 2 == 1){
            throw new IllegalArgumentException("Invalid entries");
        }
        return IntStream.range(0,entries.length/2).map(i->i*2)
                .collect(HashMap::new,
                        (m,i)->m.put(key.cast(entries[i]),value.cast(entries[i+1])),Map::putAll);
    }
}

下图将更为直观的说明自定义异常处理的整个流程:

当UserService发生异常时,异常信息将向上传递到UserController,此时的异常信息被Spring所捕获,并将其跳转到UserNotFoundException处理方法中。UserNotFoundException将异常报告封装到RestApiError对象中,并回传给API Client。通过此方法,API客户端将获得一份逻辑清晰的响应报告。

本次课程的全部源码已经上传到 Github 仓库,你可以点击此链接获取源码:https://github.com/ramostear/Spring_Boot_2.X_Tutorial/tree/master/spring-boot-exception-handing

(第五讲)自定义Spring Boot Starter

(第五讲)自定义Spring Boot Starter

(第五讲)自定义Spring Boot Starter

第四讲中我们已经了解了Spring Boot自动配置的基本原理,了解其原理之后,你会觉得Spring Boot的自动配置能力其实也没有那么的神秘。在本讲中,我们将尝试实现一个自定义的Spring Boot Starter,以巩固上一件中的内容。

1. 主要内容

  • 1.实现自定义的spring boot starter
  • 2.验证自定义的spring boot starter是否生效
  • 3.验证自动配置是否生效

本讲所涉及的源码已经上传到Github仓库,你可以通过下面的链接获取全部的源码:

https://github.com/ramostear/Spring_Boot_2.X_Tutorial/tree/master/spring-boot-starter-site

2. 实现自定义的spring boot starter

现在,我们来实现这一一个功能,当SiteInformation类在classpath下的时候,系统自动配置SiteInformation类的Bean,并且SiteInformation类中的属性可以在application.properties文件中进行配置。

首先,使用IntelliJ IDEA创建一个Maven工程,在当前Project上选择新建Module,在弹出面板中填写相关信息,如下图所示:

点击下一步,选择module存放路径,如下图所示:

点击完成按钮,生成新的Module,然后修改module中的pom.xml,添加spring-boot-autoconfigure依赖,完整的配置代码如下所示:

添加spring-boot-autoconfigure依赖的目的是让自定义的starter具有自动配置的能力

然后,创建一个名为:com.ramostear.spring.boot.starter.site的包,并在包下创建SiteInformation.java文件,这个类提供了站点的一些基本属性,源码如下:

@ConfigurationProperties(prefix = “site”)注解的作用是当我们需要覆盖站点信息时,在application.properties文件中需要使用“site”作为属性的前缀,如site.name = ramostear。如果不做任何配置,站点信息将使用默认值:unknown

接下来,创建一个SiteInformationProvider.java文件,用于返回站点信息,其源码如下所示:

SiteInformationProvider.java是自动配置的依据类,当引用此自定义Starter时,会依据SiteInformationProvider是否存在类创建这个类的Bean。

准备好上述两个类之后,需要创建一个自动配置类,如SiteInformationAutoConfiguration.java,其源码如下所示:

@ConditionalOnWebApplication注解表名此自动配置可以在Web应用中进行使用,@EnableConfigurationProperties(SiteInformation.class)注解指明提供自动配置的属性对象@ConditionalOnMissingBean(SiteInformationProvider.class)指明当容器中没有SiteInformationProvider这个类的Bean时,自动配置这个类的Bean

最后,需要将此配置类进行注册,Spring Boot才能完成自动配置工作。在resources资源文件夹下创建名为META-INF的文件夹,并添加名为spring.factories的文件,修改其内容为:

若有多个自动配置项,请用“,”进行分割,此处的“\”是为了换行后任然能够读取到属性。

完成上述的所有工作后,点击IDEA右侧Maven工具栏,选择当前项目,找到Plugins中的install选项并点击执行安装,如下图所示:

上述操作运行成功后,你可以在控制台看到如下所示的提示信息:

提示,请使用maven自带的插件对项目进行打包和安装,切勿使用spring-boot-maven-plugin进行打包安装,否则打包后的jar无法使用,请将下面的代码从你的pom文件中剔除:

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>

至此,自定义的spring boot starter制作完成。接下来创建一个web工程,引入spring-boot-starter-site,测试此starter是否能够正常使用。

3. 创建Web工程

新建一个测试用的Module,并使用IDEA自带的Spring Initializr创建一个Web工程,如下图所示:

选择Module SDK的版本,选择下一步,填写Module相关信息,如下图所示:

然后点击下一步,将Web依赖添加到项目中,然后点击保存按钮,生成新的Module。如下如所示:

接下来,将spring-boot-starter-site依赖添加到测试用的Web工程中,pom.xml源码如下:

4. 验证自定义Starter

修改CustomStarterTestApplication.java中的源码,添加@RestController注解,并创建一个获取站点信息的方法,源码如下:

接下来在application.properties文件中添加如下的类容:

site.name = 谭朝红的技术分享博客
site.domain = www.ramostear.com
site.keywords = blog
site.description = 谭朝红的技术分享博客
site.copyright = Copyright 2019-Ramostear

logging.level.org.springframework = debug

配置logging.level.org.springframework= debug属性的作用是能够在控制台日志中查看自定义的starter是否生效。

5. 运行并测试

点击运行CustomStarterTestApplication主类,并观察控制台输出,当自定义的starter生效后,你可以在控制台看到如下的信息:

============================
CONDITIONS EVALUATION REPORT
============================


Positive matches:
-----------------
...
   SiteInformationAutoConfiguration matched:
      - @ConditionalOnWebApplication (required) found 'session' scope (OnWebApplicationCondition)

   SiteInformationAutoConfiguration#provider matched:
      - @ConditionalOnMissingBean (types: com.ramostear.spring.boot.starter.site.SiteInformationProvider; SearchStrategy: all) did not find any beans (OnBeanCondition)
...

最后,使用Postman工具请求http://localhost:8080/site/info 并观察返回的信息:

请求成功、并返回了站点的配置信息,详细信息如下所示:

{
  "name": "谭朝红的技术分享博客",
  "domain": "www.ramostear.com",
  "copyright": "Copyright 2019-Ramostear",
  "keywords": "blog",
  "description": "谭朝红的技术分享博客"
}

6. 总结

本小节演示了如何创建一个自定义的Spring Boot Starter,并进行测试。进一步加深和强化了对Spring Boot自动配置原理的理解。本次分享类容到此结束,谢谢~~

(译)使用Spring Boot和Axon实现CQRS&Event Sourcing

(译)使用Spring Boot和Axon实现CQRS&Event Sourcing

上一篇中,我们讲述了CQRS和Event Sourcing的相关概念以及他们能解决什么问题。尽管可以在不适用任何其他框架或库的情况下实现CQRS/ES,但我们还是建议使用已有的一些工具。这些工具可以简化开发过程,同时运行开发人员专注于业务逻辑的处理,避免重复的造轮子。在本节中,我们将选择Axon框架来实现CQRS/ES。

什么是Axon?

Axon是一个轻量级的Java开源框架,可以帮助构建你构建基于CQRS模式的可伸缩、可扩展和库维护的Java应用程序。它还可以帮助你准备Event Sourcing所需要的环境。Axon提供了所有重要构建模块的实现,如聚合、存储库,命令和时间总线。Axon可以让开发人员的工作更为轻松。

为何选择Axon?

Axon让我们避免了负载的配置和对数据流的操作,我们可以专注于应用程序业务规则的定制,而不是创建样板代码。使用Axon可以获得如下的一些优势:

  • 优化事件处理:我们应该注意到,事件的发布具有先后顺序。Axon框架保证了事件被执行的先后顺序。
  • 内置测试环境:Axon提供了一个测试工具集,允许在特定的时间上对系统进行测试,这使得单元测试更为容易。
  • Spring Boot AutoConfiguration:在Spring Boot应用程序中配置Axon是一件非常简单的事情。只需要提供必要的依赖项,Axon将会自动配置一些基本组件。
  • 支持注解:Axon提供了注解支持,这使得我们的代码更清晰可读,我们可以使用这些注解轻松构建聚合和事件处理程序,而无需关心Axon特定的处理逻辑

Spring Boot应用程序中快速配置Axon

在默认情况下,Axon以经提供了对Spring Boot的集成支持。我们只需要通过一些简单的配置步骤就可以将Axon与Spring Boot整合在一起。

步骤1

第一步是使用合适的项目构建工具在项目中配置Axon依赖项。以下是使用Gradle来配置Axon的方法:

dependencies{
    compile("org.axonframework:axon-spring-boot-starter:3.2")
    compile("org.axonframework:axon-mongo:3.2")
    testCompile("org.axonframework:axon-test:32.")
}

第一个依赖项为我们提供了与Spring Boot集成的最基本的Axon所有必要组件,如命令中线,事件总线和聚合。第二个依赖项是为我们的聚合或事件配置库提供所需的基本环境。最后一个依赖项用于构建先关的测试环境。

步骤2

根据需要配置Axon所需要的一些Spring Bean。比如EventHandlerConfiguration(负责控制事件处理程序行为的组件),如果其中有一个事件执行失败,则终止处理后续的所有事件。当然这是非必须的,但是还是值得在应用程序中进行此配置,以防止系统中数据的不一致。配置代码如下:

@Configuration
public class AxonConfig {

private final EventHandlingConfiguration eventHandlingConfiguration;

@Autowired
public AxonConfig(EventHandlingConfiguration eventHandlingConfiguration) {
   this.eventHandlingConfiguration = eventHandlingConfiguration;
}

@PostConstruct
public void registerErrorHandling() {
   eventHandlingConfiguration.configureListenerInvocationErrorHandler(configuration -> (exception, event, listener) -> {
       String msg = String.format(
               "[EventHandling] Event handler failed when processing event with id %s. Aborting all further event handlers.",
               event.getIdentifier());
       log.error(msg, exception);
       throw exception;
   });
}}

这里的主要思想是创建一个额外的配置文件(使用@Configuration注释的类)。该类的构造函数注入由Spring自身所管理的EventHandlingConfiguration依赖项。由于绑定依赖,我们可以在此对象上调用configureListenerInvocationErrorHandler()并通过记录异常将异常传播到上层去处理错误。

步骤3

我们使用MongoDB来存储Event Store中所发生的所有事件。要实现此功能,可以通过如下的方法来实现:

@Bean
public EventStorageEngine eventStore(MongoTemplate mongoTemplate) {
   return new MongoEventStorageEngine(
           new JacksonSerializer(), null, mongoTemplate, new DocumentPerEventStorageStrategy());
}

这样,在事件总线上发布的所有事件都将自动保存到MongoDB数据库中。通过这种简单的配置,我们就可以在应用程序中使用MongoDB的数据源。

在Axon配置方面就是这样的简单。当然,我们还有其他很多的配置方式。但我们可以通过上述简单的配置,就可以使用Axon的功能了。

使用Axon实现CQRS

根据上图,创建命令,将命令传递给命令总线然后创建事件并将事件放在事件总线上还不是CQRS。我们必须记住改变写入存储库的状态并从读取数据库中读取当前状态,这才是CQRS模式的关键点。

配置此流程也并不复杂。在将命令传递给命令网关时,Spring将名利类型作为参数以搜索带有@CommandHandler 注释的方法。

@Value
class SubmitApplicationCommand {
   private String appId;
   private String category;
}

@AllArgsConstructor
public class ApplicationService {
   private final CommandGateway commandGateway;

   public CompletableFuture<Void> createForm(String appId) {
       return CompletableFuture.supplyAsync(() -> new SubmitExpertsFormCommand(appId, "Android"))
               .thenCompose(commandGateway::send);
   }
}

除其他事项外,命令处理程序负责将创建的事件发送到事件总线。它将事件对象放置到AggregateLifecycle静态导入的apply()方法中。然后调度该事件以查找预期的处理程序,并且由于我们配置了事件存储库,所有事件都自动保存在数据库中。

@Value
class ApplicationSubmittedEvent {
   private String appId;
   private String category;
}

@Aggregate
@NoArgsConstructor
public class ApplicationAggregate {
   @AggregateIdentifier
   private String id;

   @CommandHandler
   public ApplicationAggregate(SubmitApplicationCommand command) {
      //some validation
       this.id = command.getAppId;
       apply(new ApplicationSubmittedEvent(command.getAppId(), command.getCategory()));
   }
}

要更改写库的状态,我们需要提供一个使用@EventHandler注释的方法。该应用程序可以包含多个事件处理程序。他们每个人都应该执行一个特定的任务,如发送电子邮件,记录或保存在数据库中。

@RequiredArgsConstructor
@Order(1)
public class ProjectingEventHandler {
   private final IApplicationSubmittedProjection projection;

   @EventHandler
   public CompletableFuture<Void> onApplicationSubmitted(ExpertsFormSubmittedEvent event) {
       return projection.submitApplication(event.getApplicationId(), event.getCategory());
   }

如果我们想确定所有事件处理程序的处理顺序,我们可以用@Order注释一个类并设置一个序列号。submitApplication()方法负责进行所有必要的更改并将新的数据存储到写库中。

这些都是使我们的应用程序采用CQRS模式原则的关键点。当然,这些原则只能应用于我们应用程序的某些部分,具体取决于业务需求。Event Sourcing不适合我们正在构建的每个应用程序或模块。在实现此模式时也要谨慎,因为更复杂的应用程序可能难以维护。

总结

使用Axon框架,CQRS和Event Sourcing的实现变得简单化。有关高级配置的更多详细信息,请访问Axon的网站https://docs.axonframework.org/

原文作者:ŁukaszKucik

原文地址:https://www.nexocode.com/blog/posts/smooth-implementation-cqrs-es-with-sping-boot-and-axon/

译 者:谭朝红

译文地址:https://www.ramostear.com/articles/impl_cqrs_es_by_spring_boot_and_axon.html