tempData = temps.list();
215 | JsonObject data = new JsonObject()
216 | .put("data", new JsonArray()
217 | .add(tempData.get(0))
218 | .add(tempData.get(1))
219 | .add(tempData.get(2)));
220 | return webClient
221 | .post(4000, "localhost", "/")
222 | .expect(ResponsePredicate.SC_SUCCESS)
223 | .sendJson(data)
224 | .map(response -> data);
225 | }
226 | ```
227 |
228 | 这段代码与`fetchTemperature`类似,因为我们使用了一个返回Future的WebClient方法。
229 |
230 |
--------------------------------------------------------------------------------
/云原生与12要素(Cloud-Native & 12-Factor).md:
--------------------------------------------------------------------------------
1 | # 云原生与12要素(Cloud-Native & 12-Factor)
2 |
3 | ## 前言
4 |
5 | **Cloud-Native:**
6 |
7 | Java并不是为了Web而诞生,但似乎B/S架构让Java生机无限,Spring全家桶的助推也使得Java在Web更为强大,微服务体系Spring Cloud更是顺风顺水,不得不说的Spring应用的痛点就是:
8 |
9 | - 启动过慢
10 | - 内存占用偏高
11 | - 对服务器资源占用较大
12 |
13 | 而且JVM的本身就难逃离内存的过度依赖.
14 |
15 | 随着容器化技术Docker、Kubernetes,让云原生似乎成为了未来的发展方向,**云原生(Cloud-Native)**这个概念最早由Pivotal公司的Matt Stine于2013年首次提出,提到云原生首先想到的关键词可能就是 **容器化**、**微服务**、**Lambda**,**服务网格** 等...
16 |
17 | 当然这些是必要元素,但是不代表拥有这些元素就是云原生应用,很多应用的部署只能是基于云来完成,比如私有云、公有云,这也是未来的趋势。
18 |
19 | **云原生本质上不是部署,而是以什么方式来构建应用**,云原生的最终目的是为了提高开发效率,提升业务敏捷度、扩容性、可用性、资源利用率,降低成本。
20 |
21 | Go语言作为一种云原生语言也体现出了强大的生命力,Java也在变化,2018年Java出现了大量轻量级微服务框架,来面对未来的云原生趋势,Eclipse基金会的Vert.x, Red Hat推出的Quarkus、Oracle的Helidon以及Spring Native都在快速发展,拥抱云原生。
22 |
23 | **12-Factor:**
24 |
25 | 也称为“12要素”,是一套流行的应用程序开发原则。云原生架构中使用12-Factor作为设计准则。
26 |
27 | 12-Factor 的目标在于:
28 |
29 | - 使用标准化流程自动配置,从而使新的开发者花费最少的学习成本加入项目中。
30 | - 和底层操作系统之间尽可能的划清界限,在各个系统中提供最大的可移植性。
31 | - 适合部署在现代的云计算平台,从而在服务器和系统管理方面节省资源。
32 | - 将开发环境和生产环境的差异降至最低,并使用持续交付实施敏捷开发。
33 | - 可以在工具、架构和开发流程不发生明显变化的前提下实现扩展。
34 |
35 | 12-Factor 可以适用于任意语言和后端服务(数据库、消息队列、缓存等)开发的应用程序,自然也适用于云原生。在构建云原生应用时,也需要考虑这十二个方面的内容。
36 |
37 | ## 1 基准代码
38 |
39 | 代码是程序的根本,有什么样的代码最终会表现为怎么样的程序软件。从源码到产品发布中间会经历多个环节,比如开发、编译、测试、构建、部署等,这些环节可能都有自己的不同的部署环境,而不同的环境相应的责任人关注于产品的不同阶段。比如,测试人员主要关注于测试的结果,而业务人员可能关注于生产环境的最终的部署结果。但不管是哪个环节,部署到怎么的环境中,他们所依赖的代码是一致的,即所谓的“一份基准代码(Codebase),多份部署(Deploy)”。
40 |
41 | 现代的代码管理,往往需要进行版本的管理。即便是个人的工作,采用版本管理工具进行管理,对于方便查找特定版本的内容,或者是回溯历史的修改内容都是极其必要。版本控制系统就是帮助人们协调工作的工具,它能够帮助我们和其他小组成员监测同一组文件,比如说软件源代码,升级过程中所做的变更,也就是说,它可以帮助我们轻松地将工作进行融合。
42 |
43 | 版本控制工具发展到现在已经有几十年了,简单地可以将其分为四代:
44 |
45 | - 文件式版本控制系统,比如 SCCS、RCS;
46 | - 树状版本控制系统—服务器模式,比如 CVS;
47 | - 树状版本控制系统—双服务器模式,比如 Subversion;
48 | - 树状版本控制系统—分布式模式,比如 Bazaar、Mercurial、Git。
49 |
50 | 目前,在企业中广泛采用服务器模式的版本控制系统,但越来越多的企业开始倾向于采用分布式模式版本控制系统。
51 |
52 | 读者如果对版本控制系统感兴趣,可以参阅笔者所著的《分布式系统常用技术及案例分析》中的“第7章分布式版本控制系统”内容。本书“10.3 代码管理”章节部分,还会继续深入探讨 Git 的使用。
53 |
54 | ## 2 依赖
55 |
56 | 应该明确声明应用程序依赖关系(Dpendency),这样,所有的依赖关系都可以从工件的存储库中获得,并且可以使用依赖管理器(例如 Apache Maven、Gradle)进行下载。
57 |
58 | 显式声明依赖的优点之一是为新进开发者简化了环境配置流程。新进开发者可以检出应用程序的基准代码,安装编程语言环境和它对应的依赖管理工具,只需通过一个构建命令来安装所有的依赖项,即可开始工作。
59 |
60 | 比如,项目组统一采用 Gradle 来进行依赖管理。那么可以使用 Gradle Wrapper。Gradle Wrapper 免去了用户在使用 Gradle 进行项目构建时需要安装 Gradle 的繁琐步骤。每个 Gradle Wrapper 都绑定到一个特定版本的 Gradle,所以当你第一次在给定 Gradle 版本下运行上面的命令之一时,它将下载相应的 Gradle 发布包,并使用它来执行构建。默认,Gradle Wrapper 的发布包是指向的官网的 Web 服务地址,相关配置记录在了 gradle-wrapper.properties 文件中。我们查看下 Sring Boot 提供的这个 Gradle Wrapper 的配置,参数“distributionUrl”就是用于指定发布包的位置。
61 |
62 | ```properties
63 | distributionBase=GRADLE_USER_HOME
64 | distributionPath=wrapper/dists
65 | zipStoreBase=GRADLE_USER_HOME
66 | zipStorePath=wrapper/dists
67 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.5.1-bin.zip
68 | ```
69 |
70 | 而这个 gradle-wrapper.properties 文件是作为依赖项,而纳入代码存储库中的。
71 |
72 | ## 3 配置
73 |
74 | 相同的应用,在不同的部署环境(如预发布、生产环境、开发环境等等)下,可能有不同的配置内容。这其中包括:
75 |
76 | - 数据库、Redis 以及其他后端服务的配置;
77 | - 第三方服务的证书;
78 | - 每份部署特有的配置,如域名等。
79 |
80 | 这些配置项不可能硬编码在代码中,因为我们必须要保证同一份基准代码(Codebase)能够多份部署。一种解决方法是使用配置文件,但不把它们纳入版本控制系统,就像 Rails 的 config/database.yml。这相对于在代码中硬编码常量已经是长足进步,但仍然有缺点:
81 |
82 | - 不小心将配置文件签入了代码库;
83 | - 配置文件的可能会分散在不同的目录,并有着不同的格式,不方便统一管理;
84 | - 这些格式通常是语言或框架特定的,不具备通用性。
85 |
86 | 所以,推荐的做法是将应用的配置存储于环境变量中。好处在于:
87 |
88 | - 环境变量可以非常方便地在不同的部署间做修改,却不动一行代码;
89 | - 与配置文件不同,不小心把它们签入代码库的概率微乎其微;
90 | - 与一些传统的解决配置问题的机制(比如 Java 的属性配置文件)相比,环境变量与语言和系统无关。
91 |
92 | 本书介绍了另外一种解决方案——集中化配置中心。通过配置中心来集中化管理各个环境的配置变量。配置中心的实现也是于具体语言和系统无关的。欲了解有关配置中心的内容,可以参阅本书“10.5 配置管理”章节的内容。
93 |
94 | ## 4 后端服务
95 |
96 | 后端服务(Backing Services)是指程序运行所需要的通过网络调用的各种服务,如数据库(MySQL,CouchDB),消息/队列系统(RabbitMQ,Beanstalkd),SMTP 邮件发送服务(Postfix),以及缓存系统(Memcached,Redis)。
97 |
98 | 这些后端服务,通常由部署应用程序的系统管理员一起管理。除了本地服务之外,应用程序有可能使用了第三方发布和管理的服务。示例包括 SMTP(例如 Postmark),数据收集服务(例如 New Relic 或 Loggly),数据存储服务(如 Amazon S3),以及使用 API 访问的服务(例如 Twitter、Google Maps 等等)。
99 |
100 | 12-Factor 应用不会区别对待本地或第三方服务。对应用程序而言,本地或第三方服务都是附加资源,都可以通过一个 URI 或是其他存储在配置中的服务定位或服务证书来获取数据。12-Factor 应用的任意部署,都应该可以在不进行任何代码改动的情况下,将本地 MySQL 数据库换成第三方服务(例如 Amazon RDS)。类似的,本地 SMTP 服务应该也可以和第三方 SMTP 服务(例如 Postmark )互换。比如,在上述两个例子中,仅需修改配置中的资源地址。
101 |
102 | 每个不同的后端服务都是一份资源 。例如,一个 MySQL 数据库是一个资源,两个 MySQL 数据库(用来数据分区)就被当作是两个不同的资源。12-Factor 应用将这些数据库都视作附加资源,这些资源和它们附属的部署保持松耦合。
103 |
104 | 使用后端服务的好处在于,部署可以按需加载或卸载资源。例如,如果应用的数据库服务由于硬件问题出现异常,管理员可以从最近的备份中恢复一个数据库,卸载当前的数据库,然后加载新的数据库,整个过程都不需要修改代码。
105 |
106 | ## 5 构建、发布、运行
107 |
108 | 基准代码进行部署需要以下三个阶段:
109 |
110 | - 构建阶段:是指将代码仓库转化为可执行包的过程。构建时会使用指定版本的代码,获取和打包依赖项,编译成二进制文件和资源文件。
111 | - 发布阶段:会将构建的结果和当前部署所需配置相结合,并能够立刻在运行环境中投入使用。
112 | - 运行阶段:是指针对选定的发布版本,在执行环境中启动一系列应用程序进程。
113 |
114 | 应用严格区分构建、发布、运行这三个步骤。举例来说,直接修改处于运行状态的代码是非常不可取的做法,因为这些修改很难再同步回构建步骤。
115 |
116 | 部署工具通常都提供了发布管理工具,在必要的时候还是可以退回至较旧的发布版本。
117 |
118 | 每一个发布版本必须对应一个唯一的发布 ID,例如可以使用发布时的时间戳(2011-04-06-20:32:17),亦或是一个增长的数字(v100)。发布的版本就像一本只能追加的账本,一旦发布就不可修改,任何的变动都应该产生一个新的发布版本。
119 |
120 | 新的代码在部署之前,需要开发人员触发构建操作。但是,运行阶段不一定需要人为触发,而是可以自动进行。如服务器重启,或是进程管理器重启了一个崩溃的进程。因此,运行阶段应该保持尽可能少的模块,这样假设半夜发生系统故障而开发人员又捉襟见肘也不会引起太大问题。构建阶段是可以相对复杂一些的,因为错误信息能够立刻展示在开发人员面前,从而得到妥善处理。
121 |
122 | ## 6 进程
123 |
124 | 12-Factor 应用推荐以一个或多个无状态进程运行应用。这里的“无状态”是与 REST 中的无状态是一个意思,即进程的执行不依赖于上一个进程的执行。
125 |
126 | 举例来说,内存区域或磁盘空间可以作为进程在做某种事务型操作时的缓存,例如下载一个很大的文件,对其操作并将结果写入数据库的过程。12-Factor 应用根本不用考虑这些缓存的内容是不是可以保留给之后的请求来使用,这是因为应用启动了多种类型的进程,将来的请求多半会由其他进程来服务。即使在只有一个进程的情形下,先前保存的数据(内存或文件系统中)也会因为重启(如代码部署、配置更改、或运行环境将进程调度至另一个物理区域执行)而丢失。
127 |
128 | 一些互联网应用依赖于“粘性 session”, 这是指将用户 session 中的数据缓存至某进程的内存中,并将同一用户的后续请求路由到同一个进程。粘性 session 是 12-Factor 极力反对的。Session 中的数据应该保存在诸如 Memcached 或 Redis 这样的带有过期时间的缓存中。
129 |
130 | 相比于有状态的应用而言,无状态具有更好的可扩展性。
131 |
132 | ## 7 端口绑定
133 |
134 | 传统的互联网应用有时会运行于服务器的容器之中。例如 PHP 经常作为 Apache HTTPD 的一个模块来运行,而 Java 应用往往会运行于 Tomcat 中。
135 |
136 | 12-Factor 应用完全具备自我加载的能力,而不依赖于任何网络服务器就可以创建一个面向网络的服务。互联网应用通过端口绑定(Port binding)来提供服务,并监听发送至该端口的请求。
137 |
138 | 举例来说,Java 程序完全能够内嵌一个 Tomcat 在程序中,从而自己就能启动并提供服务,省去了将 Java 应用部署到 Tomcat 中的繁琐过程。在这方面,Spring Boot 框架的布道者 Josh Long 有句名言“Make JAR not WAR”,即 Java 应用程序应该被打包为可以独立运行的 JAR 文件,而不是传统的 WAR 包。
139 |
140 | 以 Spring Boot 为例,构建一个具有内嵌容器的 Java 应用是非常简单的,只需要引入以下依赖:
141 |
142 | ```scss
143 | // 依赖关系
144 | dependencies {
145 |
146 | // 该依赖用于编译阶段
147 | compile('org.springframework.boot:spring-boot-starter-web')
148 |
149 | }
150 | ```
151 |
152 | 这样,该 Spring Boot 应用就包含了内嵌 Tomcat 容器。
153 |
154 | 如果想使用其他容器,比如 Jetty、Undertow 等,只需要在依赖中加入相应 Servlet 容器的 Starter 就能实现默认容器的替换,比如:
155 |
156 | - spring-boot-starter-jetty:使用 Jetty 作为内嵌容器,可以替换 spring-boot-starter-tomcat;
157 | - spring-boot-starter-undertow:使用 Undertow 作为内嵌容器,可以替换 spring-boot-starter-tomcat。
158 |
159 | 可以使用 Spring Environment 属性配置常见的 Servlet 容器的相关设置。通常您将在 application.properties 文件中来定义属性。
160 |
161 | 常见的 Servlet 容器设置包括:
162 |
163 | - 网络设置:监听 HTTP 请求的端口(server.port)、绑定到 server.address 的接口地址等;
164 | - 会话设置:会话是否持久(server.session.persistence)、会话超时(server.session.timeout)、会话数据的位置(server.session.store-dir)和会话 cookie 配置(server.session.cookie.*);
165 | - 错误管理:错误页面的位置(server.error.path)等;
166 | - SSL;
167 | - HTTP 压缩。
168 |
169 | Spring Boot 尽可能地尝试公开这些常见公用设置,但也会有一些特殊的配置。对于这些例外的情况,Spring Boot 提供了专用命名空间来对应特定于服务器的配置(比如 server.tomcat 和 server.undertow)。
170 |
171 | ## 8 并发
172 |
173 | 在 12-factor 应用中,进程是一等公民。由于进程之间不会共享状态,这意味着应用可以通过进程的扩展来实现并发。
174 |
175 | 类似于 unix 守护进程模型,开发人员可以运用这个模型去设计应用[架构](https://so.csdn.net/so/search?q=架构&spm=1001.2101.3001.7020),将不同的工作分配给不同的进程。例如,HTTP 请求可以交给 web 进程来处理,而常驻的后台工作则交由 worker 进程负责。
176 |
177 | 在 Java 语言中,往往通过多线程的方式来实现程序的并发。线程允许在同一个进程中同时存在多个线程控制流。线程会共享进程范围内的资源,例如内存句柄和文件句柄,但每个线程都有各自的程序计数器、栈以及局部变量。线程还提供了一种直观的分解模式来充分利用操作系统中的硬件并行性,而在同一个程序中的多个线程也可以被同时调度到多个CPU上运行。
178 |
179 | 毫无疑问,多线程编程使得程序任务并发成为了可能。而并发控制主要是为了解决多个线程之间资源争夺等问题。并发一般发生在数据聚合的地方,只要有聚合,就有争夺发生,传统解决争夺的方式采取线程锁机制,这是强行对CPU管理线程进行人为干预,线程唤醒成本高,新的无锁并发策略来源于异步编程、非阻塞I/O等编程模型。
180 |
181 | 并发的使用并非没有风险。多线程并发会带来如下的问题:
182 |
183 | - 安全性问题。在没有充足同步的情况下,多个线程中的操作执行顺序是不可预测的,甚至会产生奇怪的结果。线程间的通信主要是通过共享访问字段及其字段所引用的对象来实现的。这种形式的通信是非常有效的,但可能导致两种错误:线程干扰(thread interference)和内存一致性错误(memory consistency errors)。
184 | - 活跃度问题。一个并行应用程序的及时执行能力被称为它的活跃度(liveness)。安全性的含义是“永远不发生糟糕的事情”,而活跃度则关注于另外一个目标,即“某件正确的事情最终会发生”。当某个操作无法继续执行下去,就会发生活跃度问题。在串行程序中,活跃度问题形式之一就是无意中造成的无限循环(死循环)。而在多线程程序中,常见的活跃度问题主要有死锁、饥饿以及活锁。
185 | - 性能问题。在设计良好的并发应用程序中,线程能提升程序的性能,但无论如何,线程总是带来某种程度的运行时开销。而这种开销主要是在线程调度器临时关起活跃线程并转而运行另外一个线程的上下文切换操作(Context Switch)上,因为执行上下文切换,需要保存和恢复执行上下文,丢失局部性,并且CPU时间将更多地花在线程调度而不线程运行上。当线程共享数据时,必须使用同步机制,而这些机制往往会抑制某些编译器优化,使内存缓存区中的数据无效,以及增加贡献内存总线的同步流量。所以这些因素都会带来额外的性能开销。
186 |
187 | ## 9 易处理
188 |
189 | 12-Factor 应用的进程是易处理(Disposable)的,意味着它们可以瞬间启动或停止。比如,Spring Boot 应用,它可以无需依赖容器,而采用内嵌容器的方式来实现自启动。这有利于迅速部署变化的代码或配置,保障系统的可用性,并在系统负荷到来前,快速实现扩展。
190 |
191 | 进程应当追求最小启动时间。 理想状态下,进程从敲下命令到真正启动并等待请求的时间应该只需很短的时间。更少的启动时间提供了更敏捷的发布以及扩展过程,此外还增加了健壮性,因为进程管理器可以在授权情形下容易的将进程搬到新的物理机器上。
192 |
193 | 进程一旦接收终止信号(SIGTERM)就会优雅的终止。就网络进程而言,优雅终止是指停止监听服务的端口,即拒绝所有新的请求,并继续执行当前已接收的请求,然后退出。
194 |
195 | 对于 worker 进程来说,优雅终止是指将当前任务退回队列。例如,RabbitMQ 中,worker 可以发送一个 NACK 信号。Beanstalkd 中,任务终止并退回队列会在 worker 断开时自动触发。有锁机制的系统诸如 Delayed Job 则需要确定释放了系统资源。
196 |
197 | ## 10 开发环境与线上环境等价
198 |
199 | 我们期望一份基准代码可以部署到多个环境,但如果环境不一致,最终也可能导致运行程序的结果不一致。
200 |
201 | 比如,在开发环境,我们是采用了 MySQL 作为测试数据库,而在线上生产环境,则是采用了 Oracle。虽然,MySQL 和 Oracle 都遵循相同的 SQL 标准,但两者在很多语法上还是存在细微的差异。这些差异非常有可能导致两者的执行结果不一致,甚至某些 SQL 语句在开发环境能够正常执行,而在线上环境根本无法执行。这都给调试增加了复杂性,同时,也无法保障最终的测试效果。
202 |
203 | 所以,一个好的指导意见是,不同的环境尽量保持一样。开发环境、测试环境与线上环境设置成一样,更早发现测试问题,而不至于在生产环境才暴露出问题。
204 |
205 | ## 11 日志
206 |
207 | 在应用程序中打日志是一个好习惯。日志使得应用程序运行的动作变得透明。日志是在系统出现故障时,排查问题的有力帮手。
208 |
209 | 日志应该是事件流的汇总,将所有运行中进程和后端服务的输出流按照时间顺序收集起来。尽管在回溯问题时可能需要看很多行,日志最原始的格式确实是一个事件一行。日志没有确定开始和结束,但随着应用在运行会持续的增加。对于传统的 Java EE 应用程序而言,有许多框架和库可用于日志记录。Java Logging (JUL) 是 Java 自身所提供的现成选项。除此之外 Log4j、Logback 和 SLF4J 是其他一些流行的日志框架。
210 |
211 | 对于传统的单块架构而言,日志管理本身并不存在难点,毕竟所有的日志文件,都存储在应用所部属的主机上,获取日志文件或者搜索日志内容都比较简单。但在 Cloud Native 应用中,
212 | 情况则有非常大的不同。分布式系统,特别是微服务架构所带来的部署应用方式的重大转变,都使得微服务的日志管理面临很多新的挑战。一方面随着微服务实例的数量的增长,伴随而来的就是日志文件的递增。另一方面,日志被散落在各自的实例所部署的主机上,不方面整合和回溯。
213 |
214 | 在这种情况下,将日志进行集中化的管理变得意义重大。本书的“10.4 日志管理”章节内容,会对 Cloud Native 的日志集中化管理进行详细的探讨。
215 |
216 | ## 12 管理进程
217 |
218 | 开发人员经常希望执行一些管理或维护应用的一次性任务,例如:
219 |
220 | - 运行数据移植(Django 中的 manage.py migrate, Rails 中的 rake db:migrate)。
221 | - 运行一个控制台(也被称为 REPL shell),来执行一些代码或是针对线上数据库做一些检查。大多数语言都通过解释器提供了一个 REPL 工具(python 或 perl),或是其他命令(Ruby 使用 irb, Rails 使用 rails console)。
222 | - 运行一些提交到代码仓库的一次性脚本。
223 |
224 | 一次性管理进程应该和正常的常驻进程使用同样的环境。这些管理进程和任何其他的进程一样使用相同的代码和配置,基于某个发布版本运行。后台管理代码应该随其他应用程序代码一起发布,从而避免同步问题。
225 |
226 | 所有进程类型应该使用同样的依赖隔离技术。例如,如果 Rub y的 web 进程使用了命令 bundle exec thin start,那么数据库移植应使用 bundle exec rake db:migrate。同样的,如果一个 Python 程序使用了 Virtualenv,则需要在运行 Tornado Web 服务器和任何 manage.py 管理进程时引入 bin/python。
227 |
--------------------------------------------------------------------------------
/Web/使用Websockets和Vert.x进行实时竞价.md:
--------------------------------------------------------------------------------
1 | # 使用Websockets和Vert.x进行实时竞价
2 |
3 | > 翻译: 白石(https://github.com/wjw465150/Vert.x-Core-Manual)
4 | >
5 | > 原文地址: https://vertx.io/blog/real-time-bidding-with-websockets-and-vert-x/
6 |
7 | 在过去的几年中,用户对网络应用程序的期望发生了变化。在拍卖竞价过程中,用户不再需要按下刷新按钮来检查价格是否变化或拍卖是否结束。这使得竞标变得困难且不那么有趣。相反,他们希望在应用程序中实时看到更新。
8 |
9 | 在本文中,我想展示如何创建一个提供实时出价的简单应用程序。 我们将使用 WebSockets、[SockJS](https://github.com/sockjs/sockjs-client) 和 Vert.x。
10 |
11 | 我们将创建一个用于快速出价的前端,它与用 Java 编写并基于 Vert.x 的微服务进行通信。
12 |
13 | ## Websocket 是什么?
14 |
15 | WebSocket 是异步、双向、全双工协议,它通过单个 TCP 连接提供通信通道。 通过 [WebSocket API](http://www.w3.org/TR/websockets/),它提供了网站和远程服务器之间的双向通信。
16 |
17 | WebSockets 解决了许多阻止 HTTP 协议适用于现代实时应用程序的问题。 不再需要像轮询这样的解决方法,这简化了应用程序架构。 WebSockets 不需要打开多个 HTTP 连接,它们减少了不必要的网络流量并减少了延迟。
18 |
19 | ## Websocket API 与 SockJS
20 |
21 | 遗憾的是,并非所有 Web 浏览器都支持 WebSocket。 但是,当 WebSockets 不可用时,有些库会提供回退。一个这样的库是 [SockJS](https://github.com/sockjs/sockjs-client)。 SockJS 从尝试使用 WebSocket 协议开始。但是,如果这不可能,它会使用[各种特定于浏览器的传输协议](https://github.com/sockjs/sockjs-client#supported-transports-by-browser-html-served-from-http-or-https)。 SockJS 是一个库,旨在在所有现代浏览器和不支持 WebSocket 协议的环境中工作,例如在限制性公司代理后面。 SockJS 提供了一个类似于标准 WebSocket API 的 API。
22 |
23 | ## 快速出价的前端
24 |
25 | 拍卖网页包含投标表格和一些简单的 JavaScript,它从服务中加载当前价格,打开到 SockJS 服务器的事件总线连接并提供投标。 我们出价的示例网页的 HTML 源代码可能如下所示:
26 |
27 | ```html
28 | Auction 1
29 |
30 |
43 | ```
44 |
45 | 我们使用 `vertx-eventbus.js` 库来创建到事件总线的连接。 `vertx-eventbus.js` 库是 Vert.x 发行版的一部分。 `vertx-eventbus.js` 在内部使用 SockJS 库将数据发送到 SockJS 服务器。在下面的代码片段中,我们创建了一个事件总线实例。构造函数的参数是连接到事件总线的 URI。然后我们注册监听地址 `auction.` 的处理程序。每个客户端都可以在多个地址注册,例如 在拍卖 1234 中出价时,他们会在地址`auction.1234`等上注册。当数据到达处理程序时,我们会更改当前价格和拍卖网页上的出价提要。
46 |
47 | ```javascript
48 | function registerHandlerForUpdateCurrentPriceAndFeed() {
49 | var eventBus = new EventBus('http://localhost:8080/eventbus');
50 | eventBus.onopen = function () {
51 | eventBus.registerHandler('auction.' + auction_id, function (error, message) {
52 | document.getElementById('current_price').innerHTML = JSON.parse(message.body).price;
53 | document.getElementById('feed').value += 'New offer: ' + JSON.parse(message.body).price + '\n';
54 | });
55 | }
56 | };
57 | ```
58 |
59 | 任何尝试出价的用户都会向服务生成一个 PATCH Ajax 请求,其中包含有关拍卖中新出价的信息(请参阅下面的`bid()`函数)。在服务器端,我们将事件总线上的此信息发布给注册到某个地址的所有客户端。如果您收到`200 (OK)`以外的 HTTP 响应状态代码,则会在网页上显示一条错误消息。
60 |
61 | ```javascript
62 | function bid() {
63 | var newPrice = document.getElementById('my_bid_value').value;
64 |
65 | var xmlhttp = (window.XMLHttpRequest) ? new XMLHttpRequest() : new ActiveXObject("Microsoft.XMLHTTP");
66 | xmlhttp.onreadystatechange = function () {
67 | if (xmlhttp.readyState == 4) {
68 | if (xmlhttp.status != 200) {
69 | document.getElementById('error_message').innerHTML = 'Sorry, something went wrong.';
70 | }
71 | }
72 | };
73 | xmlhttp.open("PATCH", "http://localhost:8080/api/auctions/" + auction_id);
74 | xmlhttp.setRequestHeader("Content-Type", "application/json");
75 | xmlhttp.send(JSON.stringify({price: newPrice}));
76 | };
77 | ```
78 |
79 | ## 拍卖 服务
80 |
81 | SockJS 客户端需要服务器端部分。现在我们要创建一个轻量级的 RESTful 拍卖服务。我们将以 JSON 格式发送和检索数据。让我们从创建一个 Verticle 开始。首先我们需要继承自 [`AbstractVerticle`](https://vertx.io/docs/apidocs/io/vertx/core/AbstractVerticle.html) 类并覆盖 `start` 方法。每个 Verticle 实例都有一个名为`vertx`的成员变量。这提供了对 Vert.x 核心 API 的访问。例如,要创建一个 HTTP 服务器,您可以在 `vertx` 实例上调用 `createHttpServer` 方法。要告诉服务器在端口 8080 上侦听传入请求,您可以使用 `listen` 方法。
82 |
83 | 我们需要一个带有路由的router。 router 接受 HTTP 请求并找到第一个匹配的路由。 路由可以有一个与之关联的处理程序,它接收请求(例如,匹配路径 `/eventbus/*` 的路由与 `eventBusHandler` 相关联)。
84 |
85 | 我们可以对请求做一些事情,然后结束它或将它传递给下一个匹配的处理程序。
86 |
87 | 如果您有很多处理程序,则将它们拆分为多个路由器是有意义的。
88 |
89 | 您可以通过在另一个路由器的挂载点挂载一个路由器来完成此操作(参见下面代码片段中与 `/api` 挂载点相对应的 `auctionApiRouter`)。
90 |
91 | 这是一个示例verticle:
92 |
93 | ```java
94 | public class AuctionServiceVerticle extends AbstractVerticle {
95 |
96 | @Override
97 | public void start() {
98 | Router router = Router.router(vertx);
99 |
100 | router.route("/eventbus/*").handler(eventBusHandler());
101 | router.mountSubRouter("/api", auctionApiRouter());
102 | router.route().failureHandler(errorHandler());
103 | router.route().handler(staticHandler());
104 |
105 | vertx.createHttpServer().requestHandler(router::accept).listen(8080);
106 | }
107 |
108 | //…
109 | }
110 | ```
111 |
112 | 现在我们将更详细地看一下。 我们将讨论 Verticle 中使用的 Vert.x 功能:错误处理程序、SockJS 处理程序、Body处理程序、共享数据、静态处理程序和基于方法、路径等的路由。
113 |
114 | ### 错误处理器
115 |
116 | 除了设置处理程序来处理请求外,您还可以为路由中的失败设置处理程序。 如果处理程序抛出异常,或者如果处理程序调用 [`fail`](https://vertx.io/docs/apidocs/io/vertx/ext/web/RoutingContext.html#fail-int-)方法。 为了呈现错误页面,我们使用 Vert.x 提供的错误处理程序:
117 |
118 | ```java
119 | private ErrorHandler errorHandler() {
120 | return ErrorHandler.create();
121 | }
122 | ```
123 |
124 | ### SockJS 处理程序
125 |
126 | Vert.x 为 SockJS 处理程序提供了事件总线桥,它将服务器端 Vert.x 事件总线扩展到客户端 JavaScript。
127 |
128 | 配置网桥以告诉它哪些消息应该通过很容易。您可以使用 [`BridgeOptions`](https://vertx.io/docs/apidocs/io/vertx/ext/web/handler/sockjs/BridgeOptions.html) 指定允许哪些匹配项用于入站和出站流量 .如果消息是出站的,在将其从服务器发送到客户端 JavaScript 之前,Vert.x 将查看所有出站允许的匹配项。在下面的代码片段中,我们允许来自以“拍卖”开头的地址的任何消息。 并以数字结尾(例如 `auction.1`、`auction.100` 等)。
129 |
130 | 如果你想在桥上发生事件时得到通知,你可以在调用桥时提供一个处理程序。例如,创建新的 SockJS 套接字时将发生 SOCKET_CREATED 事件。该事件是 [`Future`](https://vertx.io/docs/apidocs/io/vertx/core/Future.html) 的实例。完成事件处理后,您可以使用“true”完成未来以启用进一步处理。
131 |
132 | 要启动桥,只需在 SockJS 处理程序上调用 `bridge` 方法:
133 |
134 | ```java
135 | private SockJSHandler eventBusHandler() {
136 | BridgeOptions options = new BridgeOptions()
137 | .addOutboundPermitted(new PermittedOptions().setAddressRegex("auction\\.[0-9]+"));
138 | return SockJSHandler.create(vertx).bridge(options, event -> {
139 | if (event.type() == BridgeEventType.SOCKET_CREATED) {
140 | logger.info("A socket was created");
141 | }
142 | event.complete(true);
143 | });
144 | }
145 | ```
146 |
147 | ### Body 处理器
148 |
149 | BodyHandler 允许您检索请求正文、限制正文大小并处理文件上传。对于需要此功能的任何请求,Body处理程序应该在匹配的路由上。我们在投标过程中需要 BodyHandler(PATCH 方法请求 `/auctions/` 包含请求正文,其中包含有关拍卖中新报价的信息)。创建一个新的Body处理程序很简单:
150 |
151 | ```java
152 | BodyHandler.create();
153 | ```
154 |
155 | 如果请求体是JSON格式,可以通过[`getBodyAsJson`](https://vertx.io/docs/apidocs/io/vertx/ext/web/RoutingContext.html#getBodyAsJson--)方法获取。
156 |
157 | ### 共享数据
158 |
159 | 共享数据包含允许您在同一 Vert.x 实例中或跨 Vert.x 实例集群的不同应用程序之间安全地共享数据的功能。 共享数据包括本地共享Map、分布式集群范围Map、异步集群范围锁和异步集群范围计数器。
160 |
161 | 为了简化应用程序,我们使用本地共享Map来保存有关拍卖的信息。 本地共享Map允许您在同一 Vert.x 实例中的不同 Verticle 之间共享数据。 以下是在拍卖服务中使用共享本地Map的示例:
162 |
163 | ```java
164 | public class AuctionRepository {
165 |
166 | //…
167 |
168 | public Optional getById(String auctionId) {
169 | LocalMap auctionSharedData = this.sharedData.getLocalMap(auctionId);
170 |
171 | return Optional.of(auctionSharedData)
172 | .filter(m -> !m.isEmpty())
173 | .map(this::convertToAuction);
174 | }
175 |
176 | public void save(Auction auction) {
177 | LocalMap auctionSharedData = this.sharedData.getLocalMap(auction.getId());
178 |
179 | auctionSharedData.put("id", auction.getId());
180 | auctionSharedData.put("price", auction.getPrice());
181 | }
182 |
183 | //…
184 | }
185 | ```
186 |
187 | 如果您想将拍卖数据存储在数据库中,Vert.x 提供了一些不同的异步客户端来访问各种数据存储(MongoDB、Redis 或 JDBC 客户端)。
188 |
189 | ### 拍卖 API
190 |
191 | Vert.x 允许您根据请求路径上的模式匹配将 HTTP 请求路由到不同的处理程序。 它还使您能够从路径中提取值并将它们用作请求中的参数。 每个 HTTP 方法都存在相应的方法。 第一个匹配的将收到请求。 此功能在开发 REST 样式的 Web 应用程序时特别有用。
192 |
193 | 要从路径中提取参数,您可以使用冒号字符来表示参数的名称。 正则表达式也可用于提取更复杂的匹配项。 通过模式匹配提取的任何参数都将添加到请求参数映射中。
194 |
195 | [`Consumes`](https://vertx.io/docs/apidocs/io/vertx/ext/web/Route.html#consumes-java.lang.String-) 描述了处理程序可以使用哪些 MIME 类型。通过使用 [`produces`](https://vertx.io/docs/apidocs/io/vertx/ext/web/Route.html#produces-java.lang.String-) 您可以定义路由生成的 MIME 类型。在下面的代码中,路由将匹配任何带有匹配` application/json` 的 `content-type` 标头和 `accept` 标头的请求。
196 |
197 | 让我们看一个挂载在主路由器上的子路由器的例子,它是在 Verticle 的 `start` 方法中创建的:
198 |
199 | ```java
200 | private Router auctionApiRouter() {
201 | AuctionRepository repository = new AuctionRepository(vertx.sharedData());
202 | AuctionValidator validator = new AuctionValidator(repository);
203 | AuctionHandler handler = new AuctionHandler(repository, validator);
204 |
205 | Router router = Router.router(vertx);
206 | router.route().handler(BodyHandler.create());
207 |
208 | router.route().consumes("application/json");
209 | router.route().produces("application/json");
210 |
211 | router.get("/auctions/:id").handler(handler::handleGetAuction);
212 | router.patch("/auctions/:id").handler(handler::handleChangeAuctionPrice);
213 |
214 | return router;
215 | }
216 | ```
217 |
218 | GET 请求返回拍卖数据,而 PATCH 方法请求允许您在拍卖中出价。让我们关注更有趣的方法,即 `handleChangeAuctionPrice`。用最简单的术语来说,该方法可能看起来像这样:
219 |
220 | ```java
221 | public void handleChangeAuctionPrice(RoutingContext context) {
222 | String auctionId = context.request().getParam("id");
223 | Auction auction = new Auction(
224 | auctionId,
225 | new BigDecimal(context.getBodyAsJson().getString("price"))
226 | );
227 |
228 | this.repository.save(auction);
229 | context.vertx().eventBus().publish("auction." + auctionId, context.getBodyAsString());
230 |
231 | context.response()
232 | .setStatusCode(200)
233 | .end();
234 | }
235 | ```
236 |
237 | 对 `/auctions/1` 的 `PATCH` 请求将导致变量 `auctionId` 获得值 1。我们在拍卖中保存一个新的报价,然后在事件总线上将这个信息发布给所有在客户端JavaScript地址上注册的客户端。完成 HTTP 响应后,您必须对其调用 `end` 函数。
238 |
239 | ### 静态 处理器
240 |
241 | Vert.x 提供了处理静态网络资源的处理程序。提供静态文件的默认目录是 `webroot`,但可以对其进行配置。默认情况下,静态处理程序将设置缓存标头以使浏览器能够缓存文件。可以使用 [`setCachingEnabled`](https://vertx.io/docs/apidocs/io/vertx/ext/web/handler/StaticHandler.html#setCachingEnabled-boolean-) 方法禁用设置缓存标头。要从拍卖服务提供拍卖 HTML 页面、JS 文件(和其他静态文件),您可以创建一个静态处理程序,如下所示:
242 |
243 | ```java
244 | private StaticHandler staticHandler() {
245 | return StaticHandler.create()
246 | .setCachingEnabled(false);
247 | }
248 | ```
249 |
250 | ## 我们来Run一下!
251 |
252 | [github](https://github.com/mwarc/simple-realtime-auctions-vertx3-example) 上提供了完整的应用程序代码。
253 |
254 | 克隆存储库并运行 `./gradlew run` 。
255 |
256 | 打开一个或多个浏览器并将它们指向“http://localhost:8080”。现在您可以在拍卖中出价:
257 |
258 | 
259 |
260 | ## 总结
261 |
262 | 本文概述了一个允许实时出价的简单应用程序。 我们创建了一个用 Java 编写并基于 Vert.x 的轻量级、高性能和可扩展的微服务。 我们讨论了 Vert.x 提供的内容,其中包括分布式事件总线和优雅的 API,可让您立即创建应用程序。
263 |
264 |
265 |
266 | ------------
267 |
268 | <<<<<< [完] >>>>>>
269 |
--------------------------------------------------------------------------------
/Sql/Vert.x-Redis.md:
--------------------------------------------------------------------------------
1 | # Vert.x-Redis
2 |
3 | > 翻译: 白石(https://github.com/wjw465150/Vert.x-Core-Manual)
4 |
5 | Vert.x-redis 是与 Vert.x 一起使用的 Redis 客户端。
6 |
7 | 该模块允许在 Redis 中保存、查询、搜索和删除数据。Redis 是一个开源的、先进的键值存储数据库。 它通常被称为数据结构服务器,因为 Redis 的键可以存储字符串、散列、列表、集合和排序集合。 要使用此模块,您的网络上必须运行一个 Redis 服务器实例。
8 |
9 | Redis 有着丰富的 API,可以分成以下几组:
10 |
11 | - 集群 Cluster - 与集群管理相关的命令, 使用这些命令需要注意 redis server 版本 >=3.0.0 。
12 | - 连接 Connection - 切换数据库,连接,断开连接以及身份认证的命令。
13 | - 哈希 Hashes - 对哈希进行操作的命令。
14 | - 基数统计 HyperLogLog - 对可重复集合中统计不重复元素的命令。
15 | - 键 Keys - 使用 key 相关的命令。
16 | - 列表 List - 使用 list 相关的命令。
17 | - 发布/订阅 Pub/Sub - 创建队列和发布/订阅客户端的命令。
18 | - 脚本 Scripting - 在 Redis 中运行 Lua 脚本的命令。
19 | - 服务器 Server - 管理和获取服务器配置的命令。
20 | - 集合 Sets - 处理无序集合的命令。
21 | - 有序集合 Sorted Sets - 处理有序集合的命令。
22 | - 字符串 Strings - 处理字符串的命令。
23 | - 事务 Transactions - 处理事务生命周期的命令。
24 | - 流 Streams - 处理流的命令。
25 |
26 | ## 使用 Vert.x-Redis
27 |
28 | 要使用 Vert.x Redis 客户端,请将以下依赖项添加到项目的 *dependencies* 中:
29 |
30 | - Maven(在您的 `pom.xml` 中):
31 |
32 | ```xml
33 |
34 | io.vertx
35 | vertx-redis-client
36 | 4.3.5
37 |
38 | ```
39 |
40 | - Gradle(在您的 `build.gradle` 文件中):
41 |
42 | ```groovy
43 | compile 'io.vertx:vertx-redis-client:4.3.5'
44 | ```
45 |
46 | ## 连接到 Redis
47 |
48 | Redis 客户端可以在四种模式下操作:
49 |
50 | - 简易客户端 (大多数用户需要的)。
51 | - 哨兵 (在高可用模式下使用 Redis)。
52 | - 集群 (在集群模式下使用 Redis)。
53 | - 分片 (单节点共享, 单节点写入,多节点读取)。
54 |
55 | 连接方式由 Redis 接口的工厂方法决定。无论客户端是哪种模式, 都可以通过 `RedisOptions` 数据对象来配置。 默认情况下,一些配置项按下面的值初始化:
56 |
57 | - `netClientOptions`: 默认为 `TcpKeepAlive: true`, `TcpNoDelay: true`
58 | - `endpoint`: 默认为 `redis://localhost:6379`
59 | - `masterName`: 默认为 `mymaster`
60 | - `role`: 默认为 `MASTER`
61 | - `useReplicas`: 默认为 `NEVER`
62 |
63 | 用以下代码获得连接:
64 |
65 | ```java
66 | Redis.createClient(vertx)
67 | .connect()
68 | .onSuccess(conn -> {
69 | // 使用 connection
70 | });
71 | ```
72 |
73 | 配置中包含 `password` 且/或 `select` 数据库, 在成功建立连接后,这两个命令会自动执行。
74 |
75 | ```java
76 | Redis.createClient(
77 | vertx,
78 | // 客户端处理 REDIS URLs。
79 | // 规范的数据库URL,密码是 URL 授权的密码字段。
80 | "redis://:abracadabra@localhost:6379/1")
81 | .connect()
82 | .onSuccess(conn -> {
83 | // use the connection
84 | });
85 | ```
86 |
87 | ## 连接字符串
88 |
89 | 客户端会识别表达式上的地址:
90 |
91 | ```
92 | redis://[:password@]host[:port][/db-number]
93 | ```
94 |
95 | 或
96 |
97 | ```
98 | unix://[:password@]/domain/docker.sock[?select=db-number]
99 | ```
100 |
101 | 当指定密码或数据库时,这些命令会在连接启动时执行。
102 |
103 | ## 执行命令
104 |
105 | Redis 客户端已连接到服务器,现在可以使用此模块执行所有命令。 例如,该模块提供了一个简洁的 API 来执行命令,而不需要自己手写命令。 如果想要获取键的值,可以这样做:
106 |
107 | ```java
108 | RedisAPI redis = RedisAPI.api(client);
109 |
110 | redis
111 | .get("mykey")
112 | .onSuccess(value -> {
113 | // do something...
114 | });
115 | ```
116 |
117 | 返回的对象是泛型类型,它允许从基本的 redis 类型转换为您的编程语言类型。 例如,如果返回对象类型为 `INTEGER` ,则可以通过任意数值基本类型获取该值,如 `int`、`long` 等等。
118 |
119 | 或者,可以执行更复杂的任务,例如将返回的值作为迭代器处理:
120 |
121 | ```java
122 | if (response.type() == ResponseType.MULTI) {
123 | for (Response item : response) {
124 | // do something with item...
125 | }
126 | }
127 | ```
128 |
129 | ## 高可用模式
130 |
131 | 在高可用性模式下使用,创建连接的过程非常相似:
132 |
133 | ```java
134 | Redis.createClient(
135 | vertx,
136 | new RedisOptions()
137 | .setType(RedisClientType.SENTINEL)
138 | .addConnectionString("redis://127.0.0.1:5000")
139 | .addConnectionString("redis://127.0.0.1:5001")
140 | .addConnectionString("redis://127.0.0.1:5002")
141 | .setMasterName("sentinel7000")
142 | .setRole(RedisRole.MASTER))
143 | .connect()
144 | .onSuccess(conn -> {
145 | conn.send(Request.cmd(Command.INFO))
146 | .onSuccess(info -> {
147 | // do something...
148 | });
149 | });
150 | ```
151 |
152 | 需要注意的是,在此模式下,将建立额外连接到服务器。 客户端将在后台监听哨兵的事件。当哨兵通知我们切换了主机时, 就会向客户端发送一个异常,您可以决定下一步做什么。
153 |
154 | ## 集群模式
155 |
156 | 在集群模式下使用,创建连接的过程也非常相似:
157 |
158 | ```java
159 | final RedisOptions options = new RedisOptions()
160 | .addConnectionString("redis://127.0.0.1:7000")
161 | .addConnectionString("redis://127.0.0.1:7001")
162 | .addConnectionString("redis://127.0.0.1:7002")
163 | .addConnectionString("redis://127.0.0.1:7003")
164 | .addConnectionString("redis://127.0.0.1:7004")
165 | .addConnectionString("redis://127.0.0.1:7005");
166 | ```
167 |
168 | 在这种情况下,需要配置一个或多个集群成员。 此成员列表用于向集群请求当前配置,这意味着列表中不可用的成员将被跳过。
169 |
170 | 在集群模式下将建立到每个节点的连接。 在执行命令时需要特别小心,建议阅读Redis手册以了解集群如何工作。 在此模式下操作的客户端会尽量识别执行的命令使用哪个槽(slot),以便在正确的节点上执行它。 如果出现无法识别的情况,最好在随机节点上运行该命令。
171 |
172 | ## 分片模式
173 |
174 | 服务器是否使用分片模式运行对客户端来说是透明的。获取一个连接是耗费很大的操作。客户端会遍历所有的节点,直到找到主节点。 一旦找到主节点 (所有的写命令都可以在主节点上执行),那么客户端会尽力去连接到所有的分片节点 (读取节点)。
175 |
176 | 当获取到所有的节点后,客户端现在会过滤所有的操作,并在恰当的节点上执行读操作或写操作。注意,由 `useReplica` 配置项控制节点的选择。就像集群模式一样,当分片节点的配置项状态是 `ALWAYS` 时,所有的读操作都会在该节点上执行,当状态是 `SHARED` 时,读操作会在主节点和分片节点上随机执行,而当状态是 `NEVER` 时,读操作不会在该节点上执行。
177 |
178 | 考虑到获取连接的开销是很大的,因此如果您需要使用分片模式,您的应用需要尽可能地考虑重用数据库连接。
179 |
180 | ```java
181 | Redis.createClient(
182 | vertx,
183 | new RedisOptions()
184 | .setType(RedisClientType.REPLICATION)
185 | .addConnectionString("redis://localhost:7000")
186 | .setMaxPoolSize(4)
187 | .setMaxPoolWaiting(16))
188 | .connect()
189 | .onSuccess(conn -> {
190 | // this is a replication client.
191 | // write operations will end up in the master node
192 | conn.send(Request.cmd(Command.SET).arg("key").arg("value"));
193 | // and read operations will end up in the replica nodes if available
194 | conn.send(Request.cmd(Command.GET).arg("key"));
195 | });
196 | ```
197 |
198 | ## 发布/订阅模式
199 |
200 | Redis 支持队列和发布/订阅模式。 在此模式下操作时,当一连接调用订阅模式,则它不能用于运行除退出该模式之外的其他命令。
201 |
202 | 要启动订阅者,需要执行以下操作:
203 |
204 | ```java
205 | Redis.createClient(vertx, new RedisOptions())
206 | .connect()
207 | .onSuccess(conn -> {
208 | conn.handler(message -> {
209 | // do whatever you need to do with your message
210 | });
211 | });
212 | ```
213 |
214 | 其他位置的代码将消息发布到队列:
215 |
216 | ```java
217 | redis.send(Request.cmd(Command.PUBLISH).arg("channel1").arg("Hello World!"))
218 | .onSuccess(res -> {
219 | // published!
220 | });
221 | ```
222 |
223 | 注意: `SUBSCRIBE`, `UNSUBSCRIBE`, `PSUBSCRIBE`, `PUNSUBSCRIBE` 这些命令返回值是 `void`。 这意味着成功的结果是 `null`,而不是响应的实例。所有消息都通过客户端上的 handler 进行路由。
224 |
225 | ## 域套接字
226 |
227 | 大部分例子展示连接到 TCP 套接字,但也可以用 Redis 连接到 UNIX 域套接字。
228 |
229 | ```java
230 | Redis.createClient(vertx, "unix:///tmp/redis.sock")
231 | .connect()
232 | .onSuccess(conn -> {
233 | // so something...
234 | });
235 | ```
236 |
237 | 请注意,高可用模式和集群模式报告的服务器地址始终位于 TCP 地址上,而不是域套接字上。 这是 Redis 的原因而不是客户端的原因,因此混合使用是不行的。
238 |
239 | ## 连接池
240 |
241 | 所有的客户端都有一个连接池。默认配置连接池大小为 1,这意味着操作和单个连接一样。连接池有四个可调项:
242 |
243 | - `maxPoolSize` 最大连接数 (默认为 `6`)
244 | - `maxPoolWaiting` 在队列上获取连接的最大等待处理程序数 (默认值为 `24`)
245 | - `poolCleanerInterval` 清除连接的时间间隔 默认为 `-1` (禁用)
246 | - `poolRecycleTimeout` 连接池中打开的连接保持等待到关闭的超时时间 (默认 `15_000`)
247 |
248 | 连接池非常有用,无需自己管理连接,例如,您只需要:
249 |
250 | ```java
251 | Redis.createClient(vertx, "redis://localhost:7006")
252 | .send(Request.cmd(Command.PING))
253 | .onSuccess(res -> {
254 | // Should have received a pong...
255 | });
256 | ```
257 |
258 | 需要注意的是,连接不需要手动获取或者归还,所有连接都由连接池处理。 但是超过 1 个尝试从连接池中获取连接的并发请求可能会出现一些可伸缩性问题。 为了克服这个问题,我们需要对连接池进行调优。 常见的配置是将连接池的最大大小设置为可用CPU核心数,并允许排队从连接池里面获取连接。
259 |
260 | ```java
261 | Redis.createClient(
262 | vertx,
263 | new RedisOptions()
264 | .setConnectionString("redis://localhost:7006")
265 | // 允许最多有 8 个连接到 redis
266 | .setMaxPoolSize(8)
267 | // 允许 32 个连接请求排队等待连接可用
268 | .setMaxWaitingHandlers(32))
269 | .send(Request.cmd(Command.PING))
270 | .onSuccess(res -> {
271 | // Should have received a pong...
272 | });
273 | ```
274 |
275 | 注意:连接池不支持 `SUBSCRIBE`, `UNSUBSCRIBE`, `PSUBSCRIBE`, `PUNSUBSCRIBE` 这些命令。 因为这些命令将修改连接的操作方式,而且连接不能重复使用。
276 |
277 | ## 出错时重连
278 |
279 | 虽然连接池非常有用,但为了提高性能,连接不应自动管理,而应该由您控制。 因此您需要处理连接恢复、错误处理和重新连接。
280 |
281 | 典型的情况是,每当发生错误时,用户都希望重新连接到服务器。 自动重新连接不是 Redis 客户端的一部分,因为它将强制执行可能不符合用户预期的行为,例如:
282 |
283 | 1. 如何处理当前执行的请求?
284 | 2. 是否调用异常处理程序?
285 | 3. 如果重试也将失败,该怎么办?
286 | 4. 是否应恢复以前的状态(数据库、身份验证、订阅)?
287 | 5. 等等等等。
288 |
289 | 为了给用户充分的灵活性,我们决定不应由客户端执行。 但是,对于超时的简单重新连接可以按如下方式实现:
290 |
291 | ```java
292 | class RedisVerticle extends AbstractVerticle {
293 |
294 | private static final int MAX_RECONNECT_RETRIES = 16;
295 |
296 | private final RedisOptions options = new RedisOptions();
297 | private RedisConnection client;
298 | private final AtomicBoolean CONNECTING = new AtomicBoolean();
299 |
300 | @Override
301 | public void start() {
302 | createRedisClient()
303 | .onSuccess(conn -> {
304 | // 连接到 redis!
305 | });
306 | }
307 |
308 | /**
309 | * 当连接中出现异常时,将创建一个 Redis客 户端并设置重新连接处理程序。
310 | */
311 | private Future createRedisClient() {
312 | Promise promise = Promise.promise();
313 |
314 | if (CONNECTING.compareAndSet(false, true)) {
315 | Redis.createClient(vertx, options)
316 | .connect()
317 | .onSuccess(conn -> {
318 |
319 | // 关闭旧的连接
320 | if (client != null) {
321 | client.close();
322 | }
323 |
324 | // 确保客户端在报错时重连
325 | conn.exceptionHandler(e -> {
326 | // 有无法恢复错误时
327 | // 尝试重连
328 | attemptReconnect(0);
329 | });
330 |
331 | // 进一步处理
332 | promise.complete(conn);
333 | CONNECTING.set(false);
334 | }).onFailure(t -> {
335 | promise.fail(t);
336 | CONNECTING.set(false);
337 | });
338 | } else {
339 | promise.complete();
340 | }
341 |
342 | return promise.future();
343 | }
344 |
345 | /**
346 | * 尝试重新连接次数最多到 MAX_RECONNECT_RETRIES 次
347 | */
348 | private void attemptReconnect(int retry) {
349 | if (retry > MAX_RECONNECT_RETRIES) {
350 | // 现在应该停下来,因为我们无能为力。
351 | CONNECTING.set(false);
352 | } else {
353 | // 最长回退重试 10240 ms
354 | long backoff = (long) (Math.pow(2, Math.min(retry, 10)) * 10);
355 |
356 | vertx.setTimer(backoff, timer -> {
357 | createRedisClient()
358 | .onFailure(t -> attemptReconnect(retry + 1));
359 | });
360 | }
361 | }
362 | }
363 | ```
364 |
365 | 在本例中,客户端对象将在重新连接时被替换,应用程序将重试最多 16 次,回退时间最长可达 1280 ms。 通过弃用旧客户端,我们可以确保所有没有处理的响应都被抛弃。
366 |
367 | 需要注意,重新连接将创建一个新的连接对象,因此不会每次都缓存和执行这些对象的引用。
368 |
369 | ## 协议解析器
370 |
371 | 这个客户端同时支持 `RESP2` 和 `RESP3` 协议,在连接握手阶段, 客户端会自动检测服务器支持的版本,并使用之。
372 |
373 | 解析器隐式地为从服务器接收到的数据块创建"无限"可读缓冲区, 考虑到内存容量,为了避免产生过多的内存垃圾,在JVM启动的时候,可以配置一个可调优的watermark值。 系统参数 `io.vertx.redis.parser.watermark` 定义了一个缓冲区被废弃之前,可以存储可读数据的数量。 默认情况下,这个大小是512Kb。这意味着每个服务器的连接都会消耗至少512Kb的内存。 客户端以 pipeline 模式运行,他会保持较低的连接数同时提供最佳效果, 这意味着会消耗 `512Kb * n连接数` 大小的内存。 如果应用需要大量连接,那么我们建议将watermark值调小或者直接禁用之。
374 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/Vert.x 4 JUnit 5 integration中文版.md:
--------------------------------------------------------------------------------
1 | # Vert.x 4 JUnit 5 integration中文版
2 |
3 | 该模块为使用 JUnit 5 编写 Vert.x 测试提供集成和支持。
4 |
5 | ## 在你的构建中使用它
6 |
7 | - `groupId`: `io.vertx`
8 | - `artifactId`: `vertx-junit5`
9 | - `version`: (当前 Vert.x 版本或快照)
10 |
11 | ## 为什么测试异步代码是不同的
12 |
13 | 测试异步操作需要比 JUnit 等测试工具提供的更多工具。 让我们考虑一个典型的 Vert.x 创建 HTTP 服务器,并将其放入 JUnit 测试中:
14 |
15 | ```java
16 | @ExtendWith(VertxExtension.class)
17 | class ATest {
18 | Vertx vertx = Vertx.vertx();
19 |
20 | @Test
21 | void start_server() {
22 | vertx.createHttpServer()
23 | .requestHandler(req -> req.response().end("Ok"))
24 | .listen(16969, ar -> {
25 | // (we can check here if the server started or not)
26 | });
27 | }
28 | }
29 | ```
30 |
31 | 这里有一些问题,因为 `listen` 在尝试异步启动 HTTP 服务器时不会阻塞。 我们不能简单地假设服务器在 `listen` 调用返回时已正确启动。 还:
32 |
33 | 1. 传递给`listen`的回调将从 Vert.x 事件循环线程执行,该线程与运行 JUnit 测试的线程不同,并且
34 | 2. 在调用 `listen` 之后,测试退出并被认为通过,而 HTTP 服务器甚至可能还没有完成启动,并且
35 | 3. 由于 `listen` 回调在与执行测试的线程不同的线程上执行,因此 JUnit 运行程序无法捕获任何异常,例如由失败的断言引发的异常。
36 |
37 | ## 异步执行的测试上下文
38 |
39 | 这个模块的第一个贡献是一个 `VertxTestContext` 对象:
40 |
41 | 1. 允许等待其他线程中的操作以通知完成,并且
42 | 2. 支持接收断言失败以将测试标记为失败。
43 |
44 | 这是一个非常基本的用法:
45 |
46 | ```java
47 | @ExtendWith(VertxExtension.class)
48 | class BTest {
49 | Vertx vertx = Vertx.vertx();
50 |
51 | @Test
52 | void start_http_server() throws Throwable {
53 | VertxTestContext testContext = new VertxTestContext();
54 |
55 | vertx.createHttpServer()
56 | .requestHandler(req -> req.response().end())
57 | .listen(16969)
58 | .onComplete(testContext.succeedingThenComplete()); //(1)
59 |
60 | assertThat(testContext.awaitCompletion(5, TimeUnit.SECONDS)).isTrue(); //(2)
61 | if (testContext.failed()) { //(3)
62 | throw testContext.causeOfFailure();
63 | }
64 | }
65 | }
66 | ```
67 |
68 | 1. `succeedingThenComplete` 返回一个异步结果处理程序,预期会成功,然后使测试上下文通过。
69 | 2. `awaitCompletion` 具有`java.util.concurrent.CountDownLatch` 的语义,如果在测试通过之前等待延迟到期,则返回`false`。
70 | 3. 如果上下文捕获了一个(可能是异步的)错误,那么在完成后我们必须抛出失败异常以使测试失败。
71 |
72 | ## 使用任何断言库
73 |
74 | 该模块不对您应该使用的断言库做出任何假设。 您可以使用普通的 JUnit 断言、[AssertJ](http://joel-costigliola.github.io/assertj/) 等。
75 |
76 | 要在异步代码中进行断言并确保 `VertxTestContext` 被通知潜在的失败,您需要通过调用 `verify`、`succeeding` 或 `failing` 来包装它们:
77 |
78 | ```java
79 | HttpClient client = vertx.createHttpClient();
80 |
81 | client.request(HttpMethod.GET, 8080, "localhost", "/")
82 | .compose(req -> req.send().compose(HttpClientResponse::body))
83 | .onComplete(testContext.succeeding(buffer -> testContext.verify(() -> {
84 | assertThat(buffer.toString()).isEqualTo("Plop");
85 | testContext.completeNow();
86 | })));
87 | ```
88 |
89 | `VertxTestContext` 中有用的方法如下:
90 |
91 | - `completeNow` 和 `failNow` 通知成功或失败
92 | - `succeedingThenComplete` 提供 `Handler>` 处理程序,期望成功然后完成测试上下文
93 | - `failingThenComplete` 提供 `Handler>` 处理程序,该处理程序预期失败,然后完成测试上下文
94 | - `succeeding` 提供 `Handler>` 处理程序,期望成功并将结果传递给另一个回调,回调抛出的任何异常都被视为测试失败
95 | - `failing` 提供预期失败并将异常传递给另一个回调的 `Handler>` 处理程序,回调抛出的任何异常都被视为测试失败
96 | - `verify` 来执行断言,从代码块抛出的任何异常都被认为是测试失败。
97 |
98 | > **☢警告:** 与 `succeedingThenComplete` 和 `failingThenComplete` 不同,调用 `succeeding` 和 `failing` 方法只能使测试失败(例如,`succeeding` 得到失败的异步结果)。 要使测试通过,您仍然需要调用 `completeNow`,或使用如下所述的检查点。
99 |
100 | ## 有多个成功条件时的检查点
101 |
102 | 许多测试可以通过在执行的某个时间点调用`completeNow`来标记为通过。 话虽如此,在许多情况下,测试的成功取决于要验证的不同异步部分。
103 |
104 | 您可以使用检查点来标记一些要通过的执行点。 一个`Checkpoint`可能需要一个标记或多个标记。 当所有检查点都被标记后,相应的 `VertxTestContext` 使测试通过。
105 |
106 | 这是一个示例,其中 HTTP 服务器上的检查点正在启动,10 个 HTTP 请求得到响应,10 个 HTTP 客户端请求已经发出:
107 |
108 | ```java
109 | Checkpoint serverStarted = testContext.checkpoint();
110 | Checkpoint requestsServed = testContext.checkpoint(10);
111 | Checkpoint responsesReceived = testContext.checkpoint(10);
112 |
113 | vertx.createHttpServer()
114 | .requestHandler(req -> {
115 | req.response().end("Ok");
116 | requestsServed.flag();
117 | })
118 | .listen(8888)
119 | .onComplete(testContext.succeeding(httpServer -> {
120 | serverStarted.flag();
121 |
122 | HttpClient client = vertx.createHttpClient();
123 | for (int i = 0; i < 10; i++) {
124 | client.request(HttpMethod.GET, 8888, "localhost", "/")
125 | .compose(req -> req.send().compose(HttpClientResponse::body))
126 | .onComplete(testContext.succeeding(buffer -> testContext.verify(() -> {
127 | assertThat(buffer.toString()).isEqualTo("Ok");
128 | responsesReceived.flag();
129 | })));
130 | }
131 | }));
132 | ```
133 |
134 | > **💡提示:** 检查点应该只从测试用例主线程创建,而不是从 Vert.x 异步事件回调。
135 |
136 | ## 与 JUnit 5 集成
137 |
138 | 与以前的版本相比,JUnit 5 提供了不同的模型。
139 |
140 | ### Test 方法
141 |
142 | Vert.x 集成主要使用 `VertxExtension` 类,并使用 `Vertx` 和 `VertxTestContext` 实例的测试参数注入:
143 |
144 | ```java
145 | @ExtendWith(VertxExtension.class)
146 | class SomeTest {
147 |
148 | @Test
149 | void some_test(Vertx vertx, VertxTestContext testContext) {
150 | // (...)
151 | }
152 | }
153 | ```
154 |
155 | > **🏷注意:** `Vertx` 实例没有集群并且具有默认配置。 如果您需要其他东西,那么不要在该参数上使用注入并自己准备一个`Vertx`对象。
156 |
157 | 测试会自动包装在 `VertxTestContext` 实例生命周期中,因此您不需要自己插入 `awaitCompletion` 调用:
158 |
159 | ```java
160 | @ExtendWith(VertxExtension.class)
161 | class SomeTest {
162 |
163 | @Test
164 | void http_server_check_response(Vertx vertx, VertxTestContext testContext) {
165 | vertx.deployVerticle(new HttpServerVerticle(), testContext.succeeding(id -> {
166 | HttpClient client = vertx.createHttpClient();
167 | client.request(HttpMethod.GET, 8080, "localhost", "/")
168 | .compose(req -> req.send().compose(HttpClientResponse::body))
169 | .onComplete(testContext.succeeding(buffer -> testContext.verify(() -> {
170 | assertThat(buffer.toString()).isEqualTo("Plop");
171 | testContext.completeNow();
172 | })));
173 | }));
174 | }
175 | }
176 | ```
177 |
178 | 您可以将它与标准的 JUnit 注解一起使用,例如 `@RepeatedTest` 或生命周期回调注解:
179 |
180 | ```java
181 | @ExtendWith(VertxExtension.class)
182 | class SomeTest {
183 |
184 | // Deploy the verticle and execute the test methods when the verticle
185 | // is successfully deployed
186 | @BeforeEach
187 | void deploy_verticle(Vertx vertx, VertxTestContext testContext) {
188 | vertx.deployVerticle(new HttpServerVerticle(), testContext.succeedingThenComplete());
189 | }
190 |
191 | // Repeat this test 3 times
192 | @RepeatedTest(3)
193 | void http_server_check_response(Vertx vertx, VertxTestContext testContext) {
194 | HttpClient client = vertx.createHttpClient();
195 | client.request(HttpMethod.GET, 8080, "localhost", "/")
196 | .compose(req -> req.send().compose(HttpClientResponse::body))
197 | .onComplete(testContext.succeeding(buffer -> testContext.verify(() -> {
198 | assertThat(buffer.toString()).isEqualTo("Plop");
199 | testContext.completeNow();
200 | })));
201 | }
202 | }
203 | ```
204 |
205 | 也可以在测试类或方法上使用 `@Timeout` 注解自定义默认的 `VertxTestContext` 超时:
206 |
207 | ```java
208 | @ExtendWith(VertxExtension.class)
209 | class SomeTest {
210 |
211 | @Test
212 | @Timeout(value = 10, timeUnit = TimeUnit.SECONDS)
213 | void some_test(Vertx vertx, VertxTestContext context) {
214 | // (...)
215 | }
216 | }
217 | ```
218 |
219 | ### 生命周期方法
220 |
221 | JUnit 5 提供了几个用户定义的生命周期方法,用 `@BeforeAll`、`@BeforeEach`、`@AfterEach` 和 `@AfterAll` 注解。
222 |
223 | 这些方法可以请求注入 `Vertx` 实例。 通过这样做,他们很可能对 `Vertx` 实例执行异步操作,因此他们可以请求注入 `VertxTestContext` 实例以确保 JUnit 运行程序等待它们完成,并报告可能的错误。
224 |
225 | 这是一个例子:
226 |
227 | ```java
228 | @ExtendWith(VertxExtension.class)
229 | class LifecycleExampleTest {
230 |
231 | @BeforeEach
232 | @DisplayName("Deploy a verticle")
233 | void prepare(Vertx vertx, VertxTestContext testContext) {
234 | vertx.deployVerticle(new SomeVerticle(), testContext.succeedingThenComplete());
235 | }
236 |
237 | @Test
238 | @DisplayName("A first test")
239 | void foo(Vertx vertx, VertxTestContext testContext) {
240 | // (...)
241 | testContext.completeNow();
242 | }
243 |
244 | @Test
245 | @DisplayName("A second test")
246 | void bar(Vertx vertx, VertxTestContext testContext) {
247 | // (...)
248 | testContext.completeNow();
249 | }
250 |
251 | @AfterEach
252 | @DisplayName("Check that the verticle is still there")
253 | void lastChecks(Vertx vertx) {
254 | assertThat(vertx.deploymentIDs())
255 | .isNotEmpty()
256 | .hasSize(1);
257 | }
258 | }
259 | ```
260 |
261 | #### `VertxTestContext` 对象的作用域
262 |
263 | 由于这些对象有助于等待异步操作完成,因此会为任何 `@Test`、`@BeforeAll`、`@BeforeEach`、`@AfterEach` 和 `@AfterAll` 方法创建一个新实例。
264 |
265 | #### `Vertx` 对象的作用域
266 |
267 | `Vertx` 对象的范围取决于 [JUnit 相对执行顺序](http://junit.org/junit5/docs/current/user-guide/#extensions-execution-order) 中的哪个生命周期方法首先需要一个 要创建的新实例。 一般来说,我们尊重 JUnit 扩展范围规则,但这里是规范。
268 |
269 | 1. 如果父测试上下文已经有一个 `Vertx` 实例,它会在子扩展测试上下文中被重用。
270 | 2. 注入 `@BeforeAll` 方法会创建一个新实例,该实例将在所有后续测试和生命周期方法中共享以供注入。
271 | 3. 注入没有父上下文的 `@BeforeEach` 或先前的 `@BeforeAll` 注入会创建一个与相应测试和 `AfterEach` 方法共享的新实例。
272 | 4. 如果在运行测试方法之前不存在任何实例,则会为该测试创建一个实例(并且仅针对该测试)。
273 |
274 | #### 配置 `Vertx` 实例
275 |
276 | 默认情况下,使用 `Vertx` 的默认设置使用 `Vertx.vertx()` 创建 `Vertx` 对象。 但是,您可以配置 `VertxOptions` 以满足您的需求。 一个典型的用例是“为调试延长阻塞超时警告”。 要配置 `Vertx` 对象,您必须:
277 |
278 | 1. 使用 [json 格式](https://vertx.io/docs/apidocs/io/vertx/core/VertxOptions.html#VertxOptions-io.vertx.core.json.JsonObject-) 创建一个带有 `VertxOptions` 的 json 文件
279 | 2. 创建一个指向该文件的环境变量`vertx.parameter.filename`
280 |
281 | 延长超时的示例文件内容:
282 |
283 | ```json
284 | {
285 | "blockedThreadCheckInterval" : 5,
286 | "blockedThreadCheckIntervalUnit" : "MINUTES",
287 | "maxEventLoopExecuteTime" : 360,
288 | "maxEventLoopExecuteTimeUnit" : "SECONDS"
289 | }
290 | ```
291 |
292 | 满足这些条件后,将使用配置的选项创建 `Vertx` 对象
293 |
294 | #### 关闭和移除 `Vertx` 对象
295 |
296 | 注入的 `Vertx` 对象会自动关闭并从其相应的范围中删除。
297 |
298 | 例如,如果为测试方法的范围创建了一个 `Vertx` 对象,它会在测试完成后关闭。 类似地,当它被`@BeforeEach` 方法创建时,它会在可能的`@AfterEach` 方法完成后被关闭。
299 |
300 | #### 对相同生命周期事件的多个方法发出警告
301 |
302 | JUnit 5允许为相同的生命周期事件存在多个方法。
303 |
304 | 例如,可以在同一个测试中定义 3 个 `@BeforeEach` 方法。 由于异步操作,这些方法的效果可能同时发生而不是顺序发生,这可能导致状态不一致。
305 |
306 | 这是 JUnit 5 而不是这个模块的问题。 如有疑问,您可能总是想知道为什么单一方法不能比许多方法更好。
307 |
308 | ## 支持其他参数类型
309 |
310 | Vert.x JUnit 5 扩展是可扩展的:您可以通过 `VertxExtensionParameterProvider` 服务提供者接口添加更多类型。
311 |
312 | 如果你使用 RxJava,而不是`io.vertx.core.Vertx`,你可以注入:
313 |
314 | - `io.vertx.rxjava3.core.Vertx`, 或者
315 | - `io.vertx.reactivex.core.Vertx`, 或者
316 | - `io.vertx.rxjava.core.Vertx`.
317 |
318 | 为此,请将相应的库添加到您的项目中:
319 |
320 | - `io.vertx:vertx-junit5-rx-java3`, 或者
321 | - `io.vertx:vertx-junit5-rx-java2`, 或者
322 | - `io.vertx:vertx-junit5-rx-java`.
323 |
324 | 在 Reactiveerse 上,您可以在 `reactiverse-junit5-extensions` 项目中找到越来越多的 `vertx-junit5` 扩展集合,这些扩展与 Vert.x 堆栈集成:`https://github.com/reactiverse/reactiverse-junit5-extensions`。
325 |
326 | ## 参数排序
327 |
328 | 可能是一个参数类型必须放在另一个参数之前。 例如,`vertx-junit5-extensions` 项目中的 Web 客户端支持要求 `Vertx` 参数位于 `WebClient` 参数之前。 这是因为 `Vertx` 实例需要存在才能创建 `WebClient`。
329 |
330 | 期望参数提供者抛出有意义的异常,让用户知道可能的排序约束。
331 |
332 | 在任何情况下,最好先使用 `Vertx` 参数,然后按照手动创建它们的顺序排列下一个参数。
333 |
334 | ## 使用 `@MethodSource` 的参数化测试
335 |
336 | 您可以使用带有 vertx-junit5 的 `@MethodSource` 的参数化测试。 因此,您需要在方法定义中的 vertx 测试参数之前声明方法源参数。
337 |
338 | ```java
339 | @ExtendWith(VertxExtension.class)
340 | static class SomeTest {
341 |
342 | static Stream testData() {
343 | return Stream.of(
344 | Arguments.of("complex object1", 4),
345 | Arguments.of("complex object2", 0)
346 | );
347 | }
348 |
349 | @ParameterizedTest
350 | @MethodSource("testData")
351 | void test2(String obj, int count, Vertx vertx, VertxTestContext testContext) {
352 | // your test code
353 | testContext.completeNow();
354 | }
355 | }
356 | ```
357 |
358 | 其他 `ArgumentSources` 也是如此。 参见[ParameterizedTest](https://junit.org/junit5/docs/current/api/org.junit.jupiter.params/org/junit/jupiter/params/ParameterizedTest.html)的API文档中的`Formal Parameter List`部分.
359 |
360 | ## 在 Vert.x 上下文中运行测试
361 |
362 | 默认情况下,调用测试方法的线程是 JUnit 线程。 `RunTestOnContext` 扩展可用于通过在 Vert.x 事件循环线程上运行测试方法来改变此行为。
363 |
364 | > **⚠小心:** 请记住,在使用此扩展程序时,您不能阻止事件循环。
365 |
366 | 为此,扩展需要一个 `Vertx` 实例。 默认情况下,它会自动创建一个,但您可以提供配置选项或 supplier 方法。
367 |
368 | 测试运行时可以检索 `Vertx` 实例。
369 |
370 | ```java
371 | @ExtendWith(VertxExtension.class)
372 | class RunTestOnContextExampleTest {
373 |
374 | @RegisterExtension
375 | RunTestOnContext rtoc = new RunTestOnContext();
376 |
377 | Vertx vertx;
378 |
379 | @BeforeEach
380 | void prepare(VertxTestContext testContext) {
381 | vertx = rtoc.vertx();
382 | // Prepare something on a Vert.x event-loop thread
383 | // The thread changes with each test instance
384 | testContext.completeNow();
385 | }
386 |
387 | @Test
388 | void foo(VertxTestContext testContext) {
389 | // Test something on the same Vert.x event-loop thread
390 | // that called prepare
391 | testContext.completeNow();
392 | }
393 |
394 | @AfterEach
395 | void cleanUp(VertxTestContext testContext) {
396 | // Clean things up on the same Vert.x event-loop thread
397 | // that called prepare and foo
398 | testContext.completeNow();
399 | }
400 | }
401 | ```
402 |
403 | 当用作`@RegisterExtension` 实例字段时,会为每个测试方法创建一个新的`Vertx` 对象和`Context`。 `@BeforeEach` 和 `@AfterEach` 方法在此上下文中执行。
404 |
405 | 当用作`@RegisterExtension` 静态字段时,会为所有测试方法创建一个`Vertx` 对象和`Context`。 `@BeforeAll` 和 `@AfterAll` 方法也在这个上下文中执行。
406 |
407 |
--------------------------------------------------------------------------------
/Sql/SQL Client Templates中文版.md:
--------------------------------------------------------------------------------
1 | # SQL Client Templates中文版
2 |
3 | > 翻译: 白石(https://github.com/wjw465150/Vert.x-Core-Manual)
4 |
5 | SQL Client Templates 是一个小型库,旨在促进 SQL 查询的执行。
6 |
7 | ## 用法
8 |
9 | 要使用 SQL 客户端模板,请将以下依赖项添加到构建描述符的 *dependencies* 部分:
10 |
11 | - Maven (在你的 `pom.xml`):
12 |
13 | ```xml
14 |
15 | io.vertx
16 | vertx-sql-client-templates
17 | 4.2.6
18 |
19 | ```
20 |
21 | - Gradle (在你的 `build.gradle`):
22 |
23 | ```groovy
24 | dependencies {
25 | implementation 'io.vertx:vertx-sql-client-templates:4.2.6'
26 | }
27 | ```
28 |
29 | ## 入门
30 |
31 | 这是使用 SQL 模板的最简单方法。
32 |
33 | SQL 模板使用 *named* 参数,因此(默认情况下)将map作为参数源而不是元组。
34 |
35 | SQL 模板生成(默认情况下)一个 `RowSet`,就像客户端`PreparedQuery`。 事实上,模板是 `PreparedQuery` 的一个瘦包装器。
36 |
37 | ```java
38 | Map parameters = Collections.singletonMap("id", 1);
39 |
40 | SqlTemplate
41 | .forQuery(client, "SELECT * FROM users WHERE id=#{id}")
42 | .execute(parameters)
43 | .onSuccess(users -> {
44 | users.forEach(row -> {
45 | System.out.println(row.getString("first_name") + " " + row.getString("last_name"));
46 | });
47 | });
48 | ```
49 |
50 | 当您需要执行插入或更新操作并且您不关心结果时,可以使用 `SqlTemplate.forUpdate` 代替:
51 |
52 | ```java
53 | Map parameters = new HashMap<>();
54 | parameters.put("id", 1);
55 | parameters.put("firstName", "Dale");
56 | parameters.put("lastName", "Cooper");
57 |
58 | SqlTemplate
59 | .forUpdate(client, "INSERT INTO users VALUES (#{id},#{firstName},#{lastName})")
60 | .execute(parameters)
61 | .onSuccess(v -> {
62 | System.out.println("Successful update");
63 | });
64 | ```
65 |
66 | ## 模板语法
67 |
68 | 模板语法使用 `#{XXX}` 语法,其中 `XXX` 是有效的 [java 标识符](https://docs.oracle.com/javase/specs/jls/se8/html/jls-3.html#jls-3.8) 字符串(没有关键字限制)。
69 |
70 | 您可以使用反斜杠字符 `\` 转义任何 ` 字符,即 `\{foo}` 将被解释为没有 `foo` 参数的 `#{foo}` 字符串。
71 |
72 | ## 行映射
73 |
74 | 默认情况下,模板生成 `Row` 作为结果类型。
75 |
76 | 您可以提供自定义的 `RowMapper` 来实现行级映射:
77 |
78 | ```java
79 | RowMapper ROW_USER_MAPPER = row -> {
80 | User user = new User();
81 | user.id = row.getInteger("id");
82 | user.firstName = row.getString("firstName");
83 | user.lastName = row.getString("lastName");
84 | return user;
85 | };
86 | ```
87 |
88 | 改为实现行级映射:
89 |
90 | ```java
91 | SqlTemplate
92 | .forQuery(client, "SELECT * FROM users WHERE id=#{id}")
93 | .mapTo(ROW_USER_MAPPER)
94 | .execute(Collections.singletonMap("id", 1))
95 | .onSuccess(users -> {
96 | users.forEach(user -> {
97 | System.out.println(user.firstName + " " + user.lastName);
98 | });
99 | });
100 | ```
101 |
102 | ## 贫血的 JSON 行映射
103 |
104 | 贫血的 JSON 行映射是模板行列和 JSON 对象之间的简单映射,使用 `toJson`
105 |
106 | ```java
107 | SqlTemplate
108 | .forQuery(client, "SELECT * FROM users WHERE id=#{id}")
109 | .mapTo(Row::toJson)
110 | .execute(Collections.singletonMap("id", 1))
111 | .onSuccess(users -> {
112 | users.forEach(user -> {
113 | System.out.println(user.encode());
114 | });
115 | });
116 | ```
117 |
118 | ## 参数映射
119 |
120 | 模板使用 `Map` 作为默认输入。
121 |
122 | 您可以提供自定义映射器:
123 |
124 | ```java
125 | TupleMapper PARAMETERS_USER_MAPPER = TupleMapper.mapper(user -> {
126 | Map parameters = new HashMap<>();
127 | parameters.put("id", user.id);
128 | parameters.put("firstName", user.firstName);
129 | parameters.put("lastName", user.lastName);
130 | return parameters;
131 | });
132 | ```
133 |
134 | 来实现参数映射:
135 |
136 | ```java
137 | User user = new User();
138 | user.id = 1;
139 | user.firstName = "Dale";
140 | user.firstName = "Cooper";
141 |
142 | SqlTemplate
143 | .forUpdate(client, "INSERT INTO users VALUES (#{id},#{firstName},#{lastName})")
144 | .mapFrom(PARAMETERS_USER_MAPPER)
145 | .execute(user)
146 | .onSuccess(res -> {
147 | System.out.println("User inserted");
148 | });
149 | ```
150 |
151 | 您还可以轻松地执行批处理:
152 |
153 | ```java
154 | SqlTemplate
155 | .forUpdate(client, "INSERT INTO users VALUES (#{id},#{firstName},#{lastName})")
156 | .mapFrom(PARAMETERS_USER_MAPPER)
157 | .executeBatch(users)
158 | .onSuccess(res -> {
159 | System.out.println("Users inserted");
160 | });
161 | ```
162 |
163 | ## 贫血的JSON参数映射
164 |
165 | 贫血的JSON参数映射是模板参数和JSON对象之间的简单映射:
166 |
167 | ```java
168 | JsonObject user = new JsonObject();
169 | user.put("id", 1);
170 | user.put("firstName", "Dale");
171 | user.put("lastName", "Cooper");
172 |
173 | SqlTemplate
174 | .forUpdate(client, "INSERT INTO users VALUES (#{id},#{firstName},#{lastName})")
175 | .mapFrom(TupleMapper.jsonObject())
176 | .execute(user)
177 | .onSuccess(res -> {
178 | System.out.println("User inserted");
179 | });
180 | ```
181 |
182 | ## 使用 Jackson 数据绑定进行映射
183 |
184 | 您可以使用 Jackson 数据绑定功能进行映射。
185 |
186 | 您需要将 Jackson 数据绑定依赖项添加到构建描述符的 *dependencies* 部分:
187 |
188 | - Maven (在你的 `pom.xml`):
189 |
190 | ```xml
191 |
192 | com.fasterxml.jackson.core
193 | jackson-databind
194 | ${jackson.version}
195 |
196 | ```
197 |
198 | - Gradle (在你的 `build.gradle`):
199 |
200 | ```groovy
201 | dependencies {
202 | compile 'com.fasterxml.jackson.core:jackson-databind:${jackson.version}'
203 | }
204 | ```
205 |
206 | 行映射是通过使用行键/值对创建一个`JsonObject`,然后调用`mapTo`将其映射到任何带有 Jackson 数据绑定的 Java 类来实现的。
207 |
208 | ```java
209 | SqlTemplate
210 | .forQuery(client, "SELECT * FROM users WHERE id=#{id}")
211 | .mapTo(User.class)
212 | .execute(Collections.singletonMap("id", 1))
213 | .onSuccess(users -> {
214 | users.forEach(user -> {
215 | System.out.println(user.firstName + " " + user.lastName);
216 | });
217 | });
218 | ```
219 |
220 | 同样,参数映射是通过使用`JsonObject.mapFrom`将对象映射到`JsonObject`,然后使用键/值对生成模板参数来实现的。
221 |
222 | ```java
223 | User u = new User();
224 | u.id = 1;
225 |
226 | SqlTemplate
227 | .forUpdate(client, "INSERT INTO users VALUES (#{id},#{firstName},#{lastName})")
228 | .mapFrom(User.class)
229 | .execute(u)
230 | .onSuccess(res -> {
231 | System.out.println("User inserted");
232 | });
233 | ```
234 |
235 | ### Java Date/Time API 映射
236 |
237 | 您可以使用 *jackson-modules-java8* Jackson 扩展映射 `java.time` 类型。
238 |
239 | 您需要将 Jackson JSR 310 数据类型依赖项添加到构建描述符的 *dependencies* 部分:
240 |
241 | - Maven (在你的 `pom.xml`):
242 |
243 | ```xml
244 |
245 | com.fasterxml.jackson.datatype
246 | jackson-datatype-jsr310
247 | ${jackson.version}
248 |
249 | ```
250 |
251 | - Gradle (在你的 `build.gradle`):
252 |
253 | ```groovy
254 | dependencies {
255 | compile 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:${jackson.version}'
256 | }
257 | ```
258 |
259 | 然后你需要将时间模块注册到 Jackson `ObjectMapper`:
260 |
261 | ```java
262 | ObjectMapper mapper = io.vertx.core.json.jackson.DatabindCodec.mapper();
263 |
264 | mapper.registerModule(new JavaTimeModule());
265 | ```
266 |
267 | 您现在可以使用 `java.time`包下的类型,例如 `LocalDateTime`:
268 |
269 | ```java
270 | public class LocalDateTimePojo {
271 |
272 | public LocalDateTime localDateTime;
273 |
274 | }
275 | ```
276 |
277 | ## 使用 Vert.x 数据对象进行映射
278 |
279 | SQL 客户端模板组件可以为 Vert.x 数据对象生成映射函数。
280 |
281 | Vert.x 数据对象是一个简单的 Java bean 类,使用 `@DataObject` 进行注解。
282 |
283 | ```java
284 | @DataObject
285 | class UserDataObject {
286 |
287 | private long id;
288 | private String firstName;
289 | private String lastName;
290 |
291 | public long getId() {
292 | return id;
293 | }
294 |
295 | public void setId(long id) {
296 | this.id = id;
297 | }
298 |
299 | public String getFirstName() {
300 | return firstName;
301 | }
302 |
303 | public void setFirstName(String firstName) {
304 | this.firstName = firstName;
305 | }
306 |
307 | public String getLastName() {
308 | return lastName;
309 | }
310 |
311 | public void setLastName(String lastName) {
312 | this.lastName = lastName;
313 | }
314 | }
315 | ```
316 |
317 | ### 代码生成
318 |
319 | 由`@RowMapped` 或`@ParametersMapped` 注释的任何数据对象都将触发相应映射器类的生成。
320 |
321 | *codegen* 注解处理器在编译时生成这些类。 它是 Java 编译器的一项功能,因此*不需要额外的步骤*,只需正确配置您的构建:
322 |
323 | 只需将 `io.vertx:vertx-codegen:processor` 和 `io.vertx:vertx-sql-client-templates` 依赖项添加到您的构建中。
324 |
325 | Here a configuration example for Maven:
326 |
327 | ```xml
328 |
329 | io.vertx
330 | vertx-codegen
331 | 4.2.6
332 | processor
333 |
334 |
335 | io.vertx
336 | vertx-sql-client-templates
337 | 4.2.6
338 |
339 | ```
340 |
341 | 这个特性也可以在 Gradle 中使用:
342 |
343 | ```
344 | annotationProcessor "io.vertx:vertx-codegen:4.2.6:processor"
345 | compile "io.vertx:vertx-sql-client-templates:4.2.6"
346 | ```
347 |
348 | IDE 通常为注解处理器提供支持。
349 |
350 | 代码生成 `processor` 分类器通过 `META-INF/services` 插件机制将服务代理注解处理器的自动配置添加到 jar 中。
351 |
352 | 如果您愿意,您也可以将它与常规 jar 一起使用,但您需要显式声明注解处理器,例如在 Maven 中:
353 |
354 | ```xml
355 |
356 | maven-compiler-plugin
357 |
358 |
359 | io.vertx.codegen.CodeGenProcessor
360 |
361 |
362 |
363 | ```
364 |
365 | ### 行映射
366 |
367 | 您可以通过使用`@RowMapped`注解数据对象来生成行映射器。
368 |
369 | ```java
370 | @DataObject
371 | @RowMapped
372 | class UserDataObject {
373 |
374 | private long id;
375 | private String firstName;
376 | private String lastName;
377 |
378 | public long getId() {
379 | return id;
380 | }
381 |
382 | public void setId(long id) {
383 | this.id = id;
384 | }
385 |
386 | public String getFirstName() {
387 | return firstName;
388 | }
389 |
390 | public void setFirstName(String firstName) {
391 | this.firstName = firstName;
392 | }
393 |
394 | public String getLastName() {
395 | return lastName;
396 | }
397 |
398 | public void setLastName(String lastName) {
399 | this.lastName = lastName;
400 | }
401 | }
402 | ```
403 |
404 | 默认情况下,每个列名都绑定在数据对象属性之后,例如 `userName` 属性绑定到 `userName` 列。
405 |
406 | 借助 `@Column` 注解,您可以使用自定义名称。
407 |
408 | ```java
409 | @DataObject
410 | @RowMapped
411 | class UserDataObject {
412 |
413 | private long id;
414 | @Column(name = "first_name")
415 | private String firstName;
416 | @Column(name = "last_name")
417 | private String lastName;
418 |
419 | public long getId() {
420 | return id;
421 | }
422 |
423 | public void setId(long id) {
424 | this.id = id;
425 | }
426 |
427 | public String getFirstName() {
428 | return firstName;
429 | }
430 |
431 | public void setFirstName(String firstName) {
432 | this.firstName = firstName;
433 | }
434 |
435 | public String getLastName() {
436 | return lastName;
437 | }
438 |
439 | public void setLastName(String lastName) {
440 | this.lastName = lastName;
441 | }
442 | }
443 | ```
444 |
445 | 您可以对字段、getter 或 setter 进行注解。
446 |
447 | 生成的映射器可用于执行行映射,如 [行映射章节](https://vertx.io/docs/vertx-sql-client-templates/java/#row_mapping_with_custom_mapper) 中所述。
448 |
449 | ```java
450 | SqlTemplate
451 | .forQuery(client, "SELECT * FROM users WHERE id=#{id}")
452 | .mapTo(UserDataObjectRowMapper.INSTANCE)
453 | .execute(Collections.singletonMap("id", 1))
454 | .onSuccess(users -> {
455 | users.forEach(user -> {
456 | System.out.println(user.getFirstName() + " " + user.getLastName());
457 | });
458 | });
459 | ```
460 |
461 | ### 参数映射
462 |
463 | 您可以通过使用`@ParametersMapped`注解您的数据对象来生成参数映射器。
464 |
465 | ```java
466 | @DataObject
467 | @ParametersMapped
468 | class UserDataObject {
469 |
470 | private long id;
471 | private String firstName;
472 | private String lastName;
473 |
474 | public long getId() {
475 | return id;
476 | }
477 |
478 | public void setId(long id) {
479 | this.id = id;
480 | }
481 |
482 | public String getFirstName() {
483 | return firstName;
484 | }
485 |
486 | public void setFirstName(String firstName) {
487 | this.firstName = firstName;
488 | }
489 |
490 | public String getLastName() {
491 | return lastName;
492 | }
493 |
494 | public void setLastName(String lastName) {
495 | this.lastName = lastName;
496 | }
497 | }
498 | ```
499 |
500 | 默认情况下,每个参数都绑定在数据对象属性之后,例如 `userName` 属性绑定到 `userName` 参数。
501 |
502 | 借助 `@TemplateParameter` 注解,您可以使用自定义名称。
503 |
504 | ```java
505 | @DataObject
506 | @ParametersMapped
507 | class UserDataObject {
508 |
509 | private long id;
510 | @TemplateParameter(name = "first_name")
511 | private String firstName;
512 | @TemplateParameter(name = "last_name")
513 | private String lastName;
514 |
515 | public long getId() {
516 | return id;
517 | }
518 |
519 | public void setId(long id) {
520 | this.id = id;
521 | }
522 |
523 | public String getFirstName() {
524 | return firstName;
525 | }
526 |
527 | public void setFirstName(String firstName) {
528 | this.firstName = firstName;
529 | }
530 |
531 | public String getLastName() {
532 | return lastName;
533 | }
534 |
535 | public void setLastName(String lastName) {
536 | this.lastName = lastName;
537 | }
538 | }
539 | ```
540 |
541 | 您可以对字段、getter 或 setter 进行注解。
542 |
543 | 生成的映射器可用于执行参数映射,如 [参数映射章节](https://vertx.io/docs/vertx-sql-client-templates/java/#params_mapping_with_custom_function) 中所述。
544 |
545 | ```java
546 | UserDataObject user = new UserDataObject().setId(1);
547 |
548 | SqlTemplate
549 | .forQuery(client, "SELECT * FROM users WHERE id=#{id}")
550 | .mapFrom(UserDataObjectParamMapper.INSTANCE)
551 | .execute(user)
552 | .onSuccess(users -> {
553 | users.forEach(row -> {
554 | System.out.println(row.getString("firstName") + " " + row.getString("lastName"));
555 | });
556 | });
557 | ```
558 |
559 | ### Java 枚举类型映射
560 |
561 | 您可以在客户端支持时映射 Java 枚举类型(例如 Reactive PostgreSQL 客户端)。
562 |
563 | 通常 Java 枚举类型映射到字符串 / 数字和可能的自定义数据库枚举类型。
564 |
565 | ### 命名格式
566 |
567 | 默认模板对参数和列使用相同的大小写。 您可以覆盖 `Column` 和 `TemplateParameter` 注解中的默认名称,并使用您喜欢的格式。
568 |
569 | 你也可以在 `RowMapped` 和 `ParametersMapped` 注解中配置映射器的特定格式化情况:
570 |
571 | ```java
572 | @DataObject
573 | @RowMapped(formatter = SnakeCase.class)
574 | @ParametersMapped(formatter = QualifiedCase.class)
575 | class UserDataObject {
576 | // ...
577 | }
578 | ```
579 |
580 | 可以使用以下情况:
581 |
582 | - `CamelCase` : `FirstName`
583 | - `LowerCamelCase` : `firstName` - 像骆驼大小写,但以小写字母开头,这是默认大小写
584 | - `SnakeCase` : `first_name`
585 | - `KebabCase` : `first-name`
586 | - `QualifiedCase` : `first.name`
587 |
--------------------------------------------------------------------------------
/Cluster/Hazelcast 集群管理器.md:
--------------------------------------------------------------------------------
1 | # Hazelcast 集群管理器
2 |
3 | Vert.x 基于 [Hazelcast](https://hazelcast.com/) 实现了一个集群管理器。
4 |
5 | 这是 Vert.x CLI默认的集群管理器。由于 Vert.x 集群管理器的可插拔性,可轻易切换至其它的集群管理器。
6 |
7 | 这个集群管理器由以下依赖引入:
8 |
9 | ```xml
10 |
11 | io.vertx
12 | vertx-hazelcast
13 | 4.3.5
14 |
15 | ```
16 |
17 | Vert.x 集群管理器包含以下几项功能:
18 |
19 | - 发现并管理集群中的节点
20 | - 管理集群的 EventBus 地址订阅清单(这样就可以轻松得知集群中的哪些节点订阅了哪些 EventBus 地址)
21 | - 分布式 Map 支持
22 | - 分布式锁
23 | - 分布式计数器
24 |
25 | **Vert.x 集群器 **并不** 处理节点之间的通信。在 Vert.x 中,集群节点间通信是直接由 TCP 连接处理的。**
26 |
27 | ## 使用集群管理器
28 |
29 | 如果通过命令行来使用 Vert.x,对应集群管理器的 `jar` 包(名为 `vertx-hazelcast-4.3.5.jar` ) 应该在 Vert.x 中安装路径的 `lib` 目录中。
30 |
31 | 如果在 Maven 或者 Gradle 工程中使用 Vert.x, 只需要在工程依赖中加上依赖: `io.vertx:vertx-hazelcast:4.3.5`。
32 |
33 | 如果(集群管理器的)jar 包在 classpath 中,Vert.x将自动检测到并将其作为集群管理器。 **需要注意的是,要确保 Vert.x 的 classpath 中没有其它的集群管理器实现, 否则会使用错误的集群管理器。**
34 |
35 | 编程内嵌 Vert.x 时,可以在创建 Vert.x 实例时, 通过编程的方式显式配置 Vert.x 集群管理器,例如:
36 |
37 | ```java
38 | ClusterManager mgr = new HazelcastClusterManager();
39 |
40 | VertxOptions options = new VertxOptions().setClusterManager(mgr);
41 |
42 | Vertx.clusteredVertx(options, res -> {
43 | if (res.succeeded()) {
44 | Vertx vertx = res.result();
45 | } else {
46 | // 失败
47 | }
48 | });
49 | ```
50 |
51 | ## 配置集群管理器
52 |
53 | ### 使用XML文件配置
54 |
55 | 通常情况下,集群管理器的相关配置默认是通过打包在jar中的配置文件 [`default-cluster.xml`](https://github.com/vert-x3/vertx-hazelcast/blob/master/src/main/resources/default-cluster.xml) 配置的。
56 |
57 | ```xml
58 |
59 |
74 |
75 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 | 1
88 | SET
89 |
90 |
91 |
92 | 1
93 |
94 |
95 |
96 | 1
97 |
98 |
99 |
100 | 0
101 |
102 |
103 | __vertx.*
104 | false
105 | 1
106 |
107 |
108 |
109 |
110 |
111 | ```
112 |
113 | 如果要覆盖此配置,可以在 classpath 中添加一个 `cluster.xml` 文件。如果想在 fat jar 中内嵌 `cluster.xml` ,此文件必须在 fat jar 的根目录中。如果此文件是一个外部文件,则必须将其所在的 **目录** 添加至 classpath 中。举个例子,如果使用 Vert.x 的 *launcher* 启动应用,则 classpath 应该设置为:
114 |
115 | ```sh
116 | # 如果 cluster.xml 在当前目录:
117 | java -jar ... -cp . -cluster
118 | vertx run MyVerticle -cp . -cluster
119 |
120 | # 如果 cluster.xml 在 conf 目录:
121 | java -jar ... -cp conf -cluster
122 | ```
123 |
124 | 还可以通过配置系统属性 `vertx.hazelcast.config` 来覆盖默认的配置文件:
125 |
126 | ```sh
127 | # 指定一个外部文件为自定义配置文件
128 | java -Dvertx.hazelcast.config=./config/my-cluster-config.xml -jar ... -cluster
129 |
130 | # 从 classpath 中加载一个文件为自定义配置文件
131 | java -Dvertx.hazelcast.config=classpath:my/package/config/my-cluster-config.xml -jar ... -cluster
132 | ```
133 |
134 | 如果 `vertx.hazelcast.config` 值不为空时,将用其覆盖 classpath 中所有的 `cluster.xml` 文件; 但是如果加载 `vertx.hazelcast.config` 系统配置失败时, 系统将选取 classpath 任意一个 `cluster.xml` ,甚至直接使用默认配置。
135 |
136 | > **📌小心:** Vert.x 并不支持 `-Dhazelcast.config` 设置方式, 请不要使用
137 |
138 | 这里的 xml 是 Hazelcast 的配置文件, 可以在 Hazelcast 官网找到详细的配置文档。
139 |
140 | ### 通过编程配置
141 |
142 | 您也可以通过编程的形式配置集群管理器:
143 |
144 | ```java
145 | Config hazelcastConfig = new Config();
146 |
147 | // 设置相关的hazlcast配置,在这里省略掉,不再赘述
148 |
149 | ClusterManager mgr = new HazelcastClusterManager(hazelcastConfig);
150 |
151 | VertxOptions options = new VertxOptions().setClusterManager(mgr);
152 |
153 | Vertx.clusteredVertx(options, res -> {
154 | if (res.succeeded()) {
155 | Vertx vertx = res.result();
156 | } else {
157 | // 失败!
158 | }
159 | });
160 | ```
161 |
162 | 您也可以对已存在的XML配置进行修改。 比如修改集群名:
163 |
164 | ```java
165 | Config hazelcastConfig = ConfigUtil.loadConfig();
166 |
167 | hazelcastConfig.setClusterName("my-cluster-name");
168 |
169 | ClusterManager mgr = new HazelcastClusterManager(hazelcastConfig);
170 |
171 | VertxOptions options = new VertxOptions().setClusterManager(mgr);
172 |
173 | Vertx.clusteredVertx(options, res -> {
174 | if (res.succeeded()) {
175 | Vertx vertx = res.result();
176 | } else {
177 | // 失败!
178 | }
179 | });
180 | ```
181 |
182 | `ConfigUtil#loadConfig` 方法会加载 Hazelcast 的XML配置文件,并将其转换为 `Config` 对象。 读取的XML配置文件来自:
183 |
184 | 1. `vertx.hazelcast.config` 系统配置指定的文件,若不存在则
185 | 2. classpath 内的 `cluster.xml` 文件,若不存在则
186 | 3. 默认的配置文件
187 |
188 | ### 发现配置
189 |
190 | Hazelcast 支持几种不同的发现配置。 Hazelcast 默认配置使用多播,因此您必须在网络上启用多播才能正常工作。
191 |
192 | 关于如何配置不同的发现方式,请查阅 Hazelcast 文档。
193 |
194 | ### 通过系统配置改变本地地址及公共地址
195 |
196 | 有时,集群节点必须绑定到其他集群成员无法访问的地址。 例如,节点不在同一网络区域中,或在某些具有特定防火墙配置的云服务中时,可能会发生这种情况。
197 |
198 | 可以使用以下系统属性设置绑定的本地地址和公共地址(向其他成员发布的地址):
199 |
200 | ```sh
201 | -Dhazelcast.local.localAddress=172.16.5.131 -Dhazelcast.local.publicAddress=104.198.78.81
202 | ```
203 |
204 | ## 使用已存在的 Hazelcast 集群
205 |
206 | 可以向集群管理器传入 `HazelcastInstance` 来复用现有集群:
207 |
208 | ```java
209 | ClusterManager mgr = new HazelcastClusterManager(hazelcastInstance);
210 | VertxOptions options = new VertxOptions().setClusterManager(mgr);
211 | Vertx.clusteredVertx(options, res -> {
212 | if (res.succeeded()) {
213 | Vertx vertx = res.result();
214 | } else {
215 | // 失败!
216 | }
217 | });
218 | ```
219 |
220 | 在这种情况下,Vert.x不是 Hazelcast 集群的所有者,所以不要关闭 Vert.x 时关闭 Hazlecast 集群。
221 |
222 | 请注意,自定义 Hazelcast 实例需要以下配置:
223 |
224 | ```xml
225 |
226 | 1
227 | SET
228 |
229 |
230 |
231 | 1
232 |
233 |
234 |
235 | 1
236 |
237 |
238 |
239 | 0
240 |
241 |
242 | __vertx.*
243 | false
244 | 1
245 |
246 |
247 |
248 | ```
249 |
250 | > **🔔重要:** 不支持 Hazelcast 客户端及智能客户端。
251 |
252 | > **🔔重要:** 要确保 Hazelcast 集群 先于 Vert.x 集群启动,后于 Vert.x 集群关闭。 同时需要禁用 `shutdown hook` (参考上述的 xml 配置,或通过系统变量来实现)。
253 |
254 | ## 修改故障节点的超时配置
255 |
256 | 缺省情况下,Hazelcast 会移除集群中超过300秒没收到心跳的节点。 通过系统配置 `hazelcast.max.no.heartbeat.seconds` 可以修改这个超时时间,如:
257 |
258 | ```
259 | -Dhazelcast.max.no.heartbeat.seconds=5
260 | ```
261 |
262 | 修改后,超过5秒没发出心跳的节点会被移出集群。
263 |
264 | 请参考 [Hazelcast 系统配置](https://docs.hazelcast.org/docs/latest/manual/html-single/#system-properties) 。
265 |
266 | ## 集群故障排除
267 |
268 | 如果默认的组播配置不能正常运行,通常有以下原因:
269 |
270 | ### 机器禁用组播
271 |
272 | 通常来说,OSX 默认禁用组播。 请自行Google一下如何启用组播。
273 |
274 | ### 使用错误的网络接口
275 |
276 | 如果机器上有多个网络接口(也有可能是在运行 VPN 的情况下), 那么 Hazelcast 很有可能使用错误的网络接口。
277 |
278 | 为了确保 Hazelcast 使用正确的网络接口,在配置文件中将 `interface` 设置为指定IP地址。 同时确保 `enabled` 属性设置为 `true` 。例如:
279 |
280 | ```xml
281 |
282 | 192.168.1.20
283 |
284 | ```
285 |
286 | ### 使用VPN
287 |
288 | VPN 软件工作时通常会创建虚拟网络接口,但往往不支持组播。 在 VPN 环境中,如果 Hazelcast 与 Vert.x 没有配置正确的话, 将会选择 VPN 创建的网络接口,而不是正确的网络接口。
289 |
290 | 所以,如果您的应用运行在 VPN 环境中,请参考上述章节, 设置正确的网络接口。
291 |
292 | ### 组播不可用
293 |
294 | 在某些情况下,因为特殊的运行环境,可能无法使用组播。 在这种情况下,应该配置其他网络传输,例如使用 TCP 套接字,或在亚马逊云 EC2 上使用AWS。
295 |
296 | 有关 Hazelcast 更多传输方式,以及如何配置它们, 请查询 Hazelcast 文档。
297 |
298 | ### 开启日志
299 |
300 | 在排除故障时,开启 Hazelcast 日志很有帮助,可以观察是否组成了集群。 使用默认的 JUL 日志时,在 classpath 中添加 `vertx-default-jul-logging.properties` 文件可开启 Hazelcast 日志。 这是一个标准 java.util.logging(JUL) 配置文件。 具体配置如下:
301 |
302 | ```properties
303 | com.hazelcast.level=INFO
304 | ```
305 |
306 | 以及
307 |
308 | ```properties
309 | java.util.logging.ConsoleHandler.level=INFO
310 | java.util.logging.FileHandler.level=INFO
311 | ```
312 |
313 | ## Hazelcast 日志配置
314 |
315 | Hazelcast 的日志默认采用 `JDK` 的实现(即 JUL)。 如果想切换至其他日志库,通过设置系统配置 `hazelcast.logging.type` 即可:
316 |
317 | ```properties
318 | -Dhazelcast.logging.type=slf4j
319 | ```
320 |
321 | 详细文档请参考 [hazelcast 文档](http://docs.hazelcast.org/docs/3.6.1/manual/html-single/index.html#logging-configuration) 。
322 |
323 | ## 使用其他 Hazelcast 版本
324 |
325 | 当前的 Vert.x HazelcastClusterManager 使用的 Hazelcast 版本为 `4.2.5` 。 如果开发者想使用其他版本的 Hazelcast,需要做以下工作:
326 |
327 | - 将目标版本的 Hazelcast 依赖添加至 classpath 中
328 | - 如果是 fat jar 的形式,在构建工具中使用正确的版本
329 |
330 | 使用 Maven 时可参考下面代码:
331 |
332 | ```xml
333 |
334 | com.hazelcast
335 | hazelcast
336 | ENTER_YOUR_VERSION_HERE
337 |
338 |
339 | io.vertx
340 | vertx-hazelcast
341 | 4.3.5
342 |
343 | ```
344 |
345 | 对于某些版本,您可能需要排除掉一些(冲突的)依赖。
346 |
347 | 对于 Gradle 可以使用下面代码:
348 |
349 | ```groovy
350 | dependencies {
351 | compile ("io.vertx:vertx-hazelcast:4.3.5"){
352 | exclude group: 'com.hazelcast', module: 'hazelcast'
353 | }
354 | compile "com.hazelcast:hazelcast:ENTER_YOUR_VERSION_HERE"
355 | }
356 | ```
357 |
358 | ## 配置 Kubernetes
359 |
360 | Kubernetes 上的 Hazelcast 要配置为使用 [Hazelcast Kubernetes](https://github.com/hazelcast/hazelcast-kubernetes) 插件。
361 |
362 | 首先在项目中增加依赖: `io.vertx:vertx-hazelcast:${vertx.version}` 和 `com.hazelcast:hazelcast-kubernetes:${hazelcast-kubernetes.version}` 。 对于 Maven,参考下面代码:
363 |
364 | ```xml
365 |
366 | io.vertx
367 | vertx-hazelcast
368 | ${vertx.version}
369 |
370 |
371 | com.hazelcast
372 | hazelcast-kubernetes
373 | ${hazelcast-kubernetes.version}
374 |
375 | ```
376 |
377 | > **📝注意:** 如果您使用了其他版本的 Hazelcast core 依赖,请确保兼容 Kubernetes discovery 插件。
378 |
379 | 然后在 Hazelcast 配置中配置 Kubernetes discovery 插件,可以通过自定义的 `cluster.xml` 文件进行配置,或通过编程方式配置(参考 [配置集群管理器](https://vertx-china.github.io/docs/vertx-hazelcast/java/#configcluster))。
380 |
381 | Kubernetes discovery 插件提供了两种可选的 [发现模式](https://github.com/hazelcast/hazelcast-kubernetes#understanding-discovery-modes): *Kubernetes API* 和 *DNS Lookup* 。 关于这两种模式的利弊,请参阅该插件的项目网站。
382 |
383 | 在本文中,我们使用 *DNS Lookup* 发现模式。请修改/增加以下的配置:
384 |
385 | ```xml
386 |
387 |
388 | true (1)
389 |
390 |
391 |
392 |
393 | (2)
394 |
395 |
396 |
397 | (3)
398 | class="com.hazelcast.kubernetes.HazelcastKubernetesDiscoveryStrategy">
399 |
400 | MY-SERVICE-DNS-NAME (4)
401 |
402 |
403 |
404 |
405 |
406 |
407 | ```
408 |
409 | 1. 启用 Discovery SPI
410 | 2. 停用其他发现模式
411 | 3. 启用 Kubernetes 插件
412 | 4. 服务DNS,通常以 `MY-SERVICE-NAME.MY-NAMESPACE.svc.cluster.local` 的形式出现,视乎 Kubernetes 的分布配置
413 |
414 | `MY-SERVICE-DNS-NAME` 的取值必须是 Kubernetes 的一个 **无头** 服务(Headless Services),Hazelcast 将用其识别所有集群成员节点。 无头服务的创建配置可参考下面代码:
415 |
416 | ```yaml
417 | apiVersion: v1
418 | kind: Service
419 | metadata:
420 | namespace: MY-NAMESPACE
421 | name: MY-SERVICE-NAME
422 | spec:
423 | selector:
424 | component: MY-SERVICE-NAME (1)
425 | clusterIP: None
426 | ports:
427 | - name: hz-port-name
428 | port: 5701
429 | protocol: TCP
430 | ```
431 |
432 | 1. 按标签选择的集群成员
433 |
434 | 最终,属于集群的所有 Kubernetes 部署需要追加 `component` 标签:
435 |
436 | ```yaml
437 | apiVersion: extensions/v1beta1
438 | kind: Deployment
439 | metadata:
440 | namespace: MY-NAMESPACE
441 | spec:
442 | template:
443 | metadata:
444 | labels:
445 | component: MY-SERVICE-NAME
446 | ```
447 |
448 | 更多关于配置的详情请参考 [Hazelcast Kubernetes 插件页面](https://github.com/hazelcast/hazelcast-kubernetes)。
449 |
450 | ### 滚动更新
451 |
452 | 在滚动更新期间,建议逐一更换 Pod。
453 |
454 | 为此,我们必须将 Kubernetes 配置为:
455 |
456 | - 不要同时启动多个新 Pod
457 | - 在滚动更新过程中,不可用的 Pod 不能多于一个
458 |
459 | ```yaml
460 | spec:
461 | strategy:
462 | type: Rolling
463 | rollingParams:
464 | updatePeriodSeconds: 10
465 | intervalSeconds: 20
466 | timeoutSeconds: 600
467 | maxUnavailable: 1 (1)
468 | maxSurge: 1 (2)
469 | ```
470 |
471 | 1. 在升级过程中允许 不可用的最大 Pod 数
472 | 2. 允许超过预期创建数量的最大 Pod 数(译者注:即,实际创建的 Pod 数量 ≤ 预期 Pod 数量 + maxSurge)
473 |
474 | 同样地,Pod 的准备情况探针(readiness probe)必须考虑集群状态。 请参阅 [集群管理](https://vertx-china.github.io/docs/vertx-hazelcast/java/#one-by-one) 章节,了解如何使用 [Vert.x 健康检查](https://vertx-china.github.io/docs/vertx-health-check/java/) 实现准备情况探针。
475 |
476 | ## 集群管理
477 |
478 | Hazelcast 集群管理器的工作原理是将 Vert.x 节点作为 Hazelcast 集群的成员。 因此,Vert.x 使用 Hazelcast 集群管理器时,应遵循 Hazelcast 的管理准则。
479 |
480 | 首先介绍下分区数据和脑裂。
481 |
482 | ### 分区数据
483 |
484 | 每个 Vert.x 节点都包含部分集群数据,包括:EventBus 订阅,异步 Map,分布式计数器等等。
485 |
486 | 当有节点加入或离开集群时,Hazelcast 会迁移分区数据。 换句话说,它可以移动数据以适应新的集群拓扑。 此过程可能需要一些时间,具体取决于集群数据量和节点数量。
487 |
488 | ### 脑裂
489 |
490 | 在理想环境中,不会出现网络设备故障。 实际上,集群早晚会被分成多个小组,彼此之间不可见。
491 |
492 | Hazelcast 能够将节点合并回单个集群。 但是,就像数据分区迁移一样,此过程可能需要一些时间。 在集群变回可用之前,某些 EventBus 的消费者可能无法获取到消息。 否则,重新部署故障的 Verticle 过程中无法保证高可用。
493 |
494 | > **📝注意:** 很难(或者说基本不可能)区分脑裂和:长时间的GC暂停 (导致错过了心跳检查),部署新版本应用时,同时强制关闭了很多节点
495 |
496 | ### 建议
497 |
498 | 考虑到上面讨论的常见集群问题,建议遵循下述的最佳实践。
499 |
500 | #### 优雅地关闭
501 |
502 | 应该避免强行停止集群成员节点(例如,对节点进程使用 `kill -9` )。
503 |
504 | 当然,进程崩溃是不可避免的,但是优雅地关闭进程有助于其余节点更快地恢复稳定状态。
505 |
506 | #### 逐个添加或移除节点
507 |
508 | 滚动更新新版本应用时,或扩大/缩小集群时,应该一个接一个地添加或移除节点。
509 |
510 | 逐个停止节点可避免集群误以为发生了脑裂。 逐个添加节点可以进行干净的增量数据分区迁移。
511 |
512 | 可以使用 [Vert.x 运行状况检查](https://vertx-china.github.io/docs/vertx-health-check/java/) 来验证集群安全性:
513 |
514 | ```java
515 | Handler> procedure = ClusterHealthCheck.createProcedure(vertx);
516 | HealthChecks checks = HealthChecks.create(vertx).register("cluster-health", procedure);
517 | ```
518 |
519 | 完成集群创建后,可以通过 [Vert.x Web](https://vertx-china.github.io/docs/vertx-web/java/) 路由 Handler 编写的HTTP程序进行健康检查:
520 |
521 | ```java
522 | Router router = Router.router(vertx);
523 | router.get("/readiness").handler(HealthCheckHandler.createWithHealthChecks(checks));
524 | ```
525 |
526 | #### 使用轻量级成员(Lite Members)
527 |
528 | 为了尽量减少 Vert.x 集群适应新拓扑的时间,您可以使用外部数据节点,并将 Vert.x 节点标记为 [*轻量级成员*](https://docs.hazelcast.org/docs/latest/manual/html-single/#enabling-lite-members)。
529 |
530 | *轻量级成员* 像普通成员一样加入 Hazelcast 集群,但是他们不拥有任何数据分区。 因此,添加或删除此类成员时,Hazelcast 不需要迁移数据分区。
531 |
532 | > **🔔重要:** 您必须事先启动外部数据节点,因为 Hazelcast 不会只使用 *轻量级成员* 节点创建集群。
533 |
534 | 启动外部数据节点可以使用 Hazelcast 分发启动脚本,或以编程方式进行。
535 |
536 | 可以在XML配置中将Vert.x节点标记为 *轻量级成员* 节点:
537 |
538 | ```xml
539 |
540 | ```
541 |
542 | 还可以通过编程实现:
543 |
544 | ```java
545 | Config hazelcastConfig = ConfigUtil.loadConfig()
546 | .setLiteMember(true);
547 |
548 | ClusterManager mgr = new HazelcastClusterManager(hazelcastConfig);
549 |
550 | VertxOptions options = new VertxOptions().setClusterManager(mgr);
551 |
552 | Vertx.clusteredVertx(options, res -> {
553 | if (res.succeeded()) {
554 | Vertx vertx = res.result();
555 | } else {
556 | // failed!
557 | }
558 | });
559 | ```
560 |
561 | ------
562 |
563 | <<<<<< [完] >>>>>>
564 |
565 |
--------------------------------------------------------------------------------
/Vert.x Service Proxy Manual中文版.md:
--------------------------------------------------------------------------------
1 | # Vert.x Service Proxy Manual中文版
2 |
3 | > 翻译: 白石(https://github.com/wjw465150/Vert.x-Core-Manual)
4 |
5 | 当您编写一个 Vert.x 应用程序时,您可能希望在某个地方隔离一个功能,并使其可供应用程序的其余部分使用。 这是服务代理的主要目的。 它允许您在事件总线上公开一个 *service*,因此,任何其他 Vert.x 组件只要知道发布服务的 *address* 就可以使用它。
6 |
7 | *service* 使用包含遵循 *async 模式* 的方法的 Java 接口来描述。 在幕后,消息在事件总线上发送以调用服务并获取响应。 但为了便于使用,它会生成一个*代理*,您可以直接调用(使用服务接口中的 API)。
8 |
9 | ## 使用 Vert.x 服务代理
10 |
11 | 要 **使用** Vert.x 服务代理,请将以下依赖项添加到构建描述符的 *dependencies* 部分:
12 |
13 | - Maven (在你的 `pom.xml`):
14 |
15 | ```xml
16 |
17 | io.vertx
18 | vertx-service-proxy
19 | 4.3.1
20 |
21 | ```
22 |
23 | - Gradle (在你的 `build.gradle` ):
24 |
25 | ```groovy
26 | implementation 'io.vertx:vertx-service-proxy:4.3.1'
27 | ```
28 |
29 | 要 **实现** 服务代理,还要添加:
30 |
31 | - Maven (在你的 `pom.xml`):
32 |
33 | ```xml
34 |
35 | io.vertx
36 | vertx-codegen
37 | 4.3.1
38 | provided
39 |
40 | ```
41 |
42 | - Gradle < 5 (在你的 `build.gradle` file):
43 |
44 | ```groovy
45 | compileOnly 'io.vertx:vertx-codegen:4.3.1'
46 | ```
47 |
48 | - Gradle >= 5 (在你的 `build.gradle` file):
49 |
50 | ```groovy
51 | implementation 'io.vertx:vertx-codegen:4.3.1'
52 | implementation 'io.vertx:vertx-service-proxy:4.3.1'
53 |
54 | annotationProcessor 'io.vertx:vertx-codegen:4.3.1:processor'
55 | annotationProcessor 'io.vertx:vertx-service-proxy:4.3.1'
56 | ```
57 |
58 | > **🏷注意:** 请注意,由于服务代理机制依赖于代码生成,因此对*服务接口*的修改需要重新编译源代码以重新生成代码。
59 |
60 | 要生成不同语言的代理,您需要为 Groovy 添加 *language* 依赖项,例如 `vertx-lang-groovy`。
61 |
62 | ## 服务代理简介
63 |
64 | 让我们看一下服务代理以及它们为何有用。 假设您在事件总线上公开了一个*数据库服务*,您应该执行以下操作:
65 |
66 | ```java
67 | JsonObject message = new JsonObject();
68 |
69 | message
70 | .put("collection", "mycollection")
71 | .put("document", new JsonObject().put("name", "tim"));
72 |
73 | DeliveryOptions options = new DeliveryOptions().addHeader("action", "save");
74 |
75 | vertx.eventBus()
76 | .request("database-service-address", message, options)
77 | .onSuccess(msg -> {
78 | // done
79 | }).onFailure(err -> {
80 | // failure
81 | });
82 | ```
83 |
84 | 创建服务时,有一定数量的样板代码用于在事件总线上侦听传入消息,将它们路由到适当的方法并在事件总线上返回结果。
85 |
86 | 使用 Vert.x 服务代理,您可以避免编写所有样板代码并专注于编写服务。
87 |
88 | 您将服务编写为 Java 接口并使用 `@ProxyGen` 对其进行注解,例如:
89 |
90 | ```java
91 | @ProxyGen
92 | public interface SomeDatabaseService {
93 |
94 | // 几个工厂方法来创建实例和代理
95 | static SomeDatabaseService create(Vertx vertx) {
96 | return new SomeDatabaseServiceImpl(vertx);
97 | }
98 |
99 | static SomeDatabaseService createProxy(Vertx vertx, String address) {
100 | return new SomeDatabaseServiceVertxEBProxy(vertx, address);
101 | }
102 |
103 | // 实际服务操作在这里...
104 | void save(String collection, JsonObject document, Handler> resultHandler);
105 | }
106 | ```
107 |
108 | 您还需要在定义接口的包中(或上面)的某个位置有一个`package-info.java`文件。该包需要用`@ModuleGen`注释,以便 Vert.x CodeGen 可以识别您的接口并生成 适当的 EventBus 代理代码。
109 |
110 | `package-info.java`文件内容
111 |
112 | ```java
113 | @io.vertx.codegen.annotations.ModuleGen(groupPackage = "io.vertx.example", name = "services", useFutures = true)
114 | package io.vertx.example;
115 | ```
116 | > **💡提示:** `ModuleGen` 声明一个Codegen模块,则注解包或其子包中包含的所有处理过的元素都将是同一模块的一部分。模块的标识扮演着重要的角色,因为运行时可以使用它来加载模块。
117 | >
118 | > `name`属性声明模块的名称:非分层名称(不能是个空字符串)。 JavaScript 或 Ruby 语言使用此类名称为其运行时生成模块。 Java 或 Groovy 运行时不使用此信息。
119 | >
120 | > `groupPackage`属性声明了模块的组名(可以是个空字符串):用于生成**生成包名的组的包**(用于 Groovy 或 RxJava的生成). 如果定义了此属性,那么组包必须是注解的模块包的前缀,它定义了属于同一组的模块的生成包的命名.
121 | > 例如:`@ModuleGen(name = "acme", groupPackage="com.acme")` 对于Groovy API生成包名`com.acme.groovy`,对于RxJava API生成包名`com.acme.rxjava`.
122 |
123 | 有了这个接口,Vert.x将生成通过事件总线访问您的服务所需的所有样板代码,它还将为您的服务生成一个**客户端代理**,因此您的客户端可以为您的服务使用一个丰富的惯用API,而不必手动编写事件总线消息来发送。无论您的服务在事件总线的哪个位置(可能在另一台机器上),客户端代理都可以工作。
124 |
125 | 这意味着您可以像这样与您的服务进行交互:
126 |
127 | ```java
128 | SomeDatabaseService service = SomeDatabaseService.createProxy(vertx, "database-service-address");
129 |
130 | // Save some data in the database - this time using the proxy
131 | service.save(
132 | "mycollection",
133 | new JsonObject().put("name", "tim"),
134 | res2 -> {
135 | if (res2.succeeded()) {
136 | // done
137 | }
138 | });
139 | ```
140 |
141 | 你也可以将`@ProxyGen` 和语言API代码生成(`@VertxGen`)结合起来,以Vert.x支持的任何语言来创建服务存根——这意味着你可以只在Java中编写一次服务,然后通过一个习惯的其他语言API与它交互,而不管服务是在本地还是完全在事件总线的其他地方。为此,不要忘记在构建描述符中添加对其它语言的依赖:
142 |
143 | ```java
144 | @ProxyGen // 生成服务代理
145 | @VertxGen // 生成客户端
146 | public interface SomeDatabaseService {
147 | // ...
148 | }
149 | ```
150 |
151 | > **💡提示:** 当`@VertxGen`注解存在时,Vert.x Java 注解处理器的代码生成将在构建时启用所有合适的其它语言绑定的代码生成器。要生成 其它语言的 绑定,我们需要添加对其它语言的依赖项。
152 |
153 | ## 异步接口
154 |
155 | 要由服务代理生成使用,*服务接口*必须遵守一些规则。 首先它应该遵循异步模式。 要返回结果,该方法应声明一个 `Future` 返回类型。 `ResultType` 可以是另一个代理(因此代理可以是其他代理的工厂)。
156 |
157 | 让我们看一个例子:
158 |
159 | ```java
160 | @ProxyGen
161 | public interface SomeDatabaseService {
162 |
163 | // 几个工厂方法来创建实例和代理
164 | static SomeDatabaseService create(Vertx vertx) {
165 | return new SomeDatabaseServiceImpl(vertx);
166 | }
167 |
168 | static SomeDatabaseService createProxy(Vertx vertx, String address) {
169 | return new SomeDatabaseServiceVertxEBProxy(vertx, address);
170 | }
171 |
172 | // 通知完成但没有结果的方法(void)
173 | Future save(String collection, JsonObject document);
174 |
175 | // 提供结果的方法(一个 json 对象)
176 | Future findOne(String collection, JsonObject query);
177 |
178 | // 创建连接
179 | Future createConnection(String shoeSize);
180 |
181 | }
182 | ```
183 |
184 | 和:
185 |
186 | ```java
187 | @ProxyGen
188 | @VertxGen
189 | public interface MyDatabaseConnection {
190 |
191 | void insert(JsonObject someData);
192 |
193 | Future commit();
194 |
195 | @ProxyClose
196 | void close();
197 | }
198 | ```
199 |
200 | 您还可以通过使用 `@ProxyClose` 注解来声明特定方法取消注册代理。 调用此方法时会释放代理实例。
201 |
202 | 下面描述了对*服务接口*的更多限制。
203 |
204 | ## 带有回调的异步接口
205 |
206 | 在 Vert.x 4.1 之前,服务异步接口是由回调定义的。
207 |
208 | 您仍然可以使用回调创建服务异步接口,使用此模块声明:
209 |
210 | `package-info.java`文件内容
211 |
212 | ```java
213 | @io.vertx.codegen.annotations.ModuleGen(groupPackage = "io.vertx.example", name = "services", useFutures = false)
214 | package io.vertx.example;
215 | ```
216 |
217 | > **🏷注意:** 为了向后兼容,`useFutures` 的默认值为 `false`,所以你也可以省略声明
218 |
219 | 带有回调的服务异步接口如下所示:
220 |
221 | ```java
222 | @ProxyGen
223 | public interface SomeDatabaseService {
224 |
225 | // 通知完成但没有结果的方法(void)
226 | void save(String collection, JsonObject document, Handler> result);
227 |
228 | // 提供结果的方法(一个 json 对象)
229 | void findOne(String collection, JsonObject query, Handler> result);
230 |
231 | // 创建连接
232 | void createConnection(String shoeSize, Handler> resultHandler);
233 |
234 | }
235 | ```
236 |
237 | 返回类型必须是以下之一:
238 |
239 | - `void`
240 | - `@Fluent` 并返回对服务的引用(`this`):
241 |
242 | ```java
243 | @Fluent
244 | SomeDatabaseService doSomething();
245 | ```
246 |
247 | 这是因为方法不能阻塞,如果服务是远程的,不可能立即返回结果而不阻塞。
248 |
249 | ## 安全
250 |
251 | 服务代理可以使用简单的拦截器执行基本的安全性。 必须提供身份验证提供程序,可以选择添加`Authorization`,在这种情况下,还必须存在`AuthorizationProvider`。 请注意,身份验证基于从 `auth-token` 标头中提取的令牌。
252 |
253 | ```java
254 | SomeDatabaseService service = new SomeDatabaseServiceImpl();
255 | // 注册处理程序
256 | new ServiceBinder(vertx)
257 | .setAddress("database-service-address")
258 | // 保护传输中的消息
259 | .addInterceptor(
260 | new ServiceAuthInterceptor()
261 | // 令牌将使用 JWT 身份验证进行验证
262 | .setAuthenticationProvider(JWTAuth.create(vertx, new JWTAuthOptions()))
263 | // 可选地,我们也可以保护权限:
264 |
265 | // 一个 admin
266 | .addAuthorization(RoleBasedAuthorization.create("admin"))
267 | // 可以打印的
268 | .addAuthorization(PermissionBasedAuthorization.create("print"))
269 |
270 | // 授权被加载的地方,让我们从令牌中假设
271 | // 但如果需要,它们可以从数据库或文件中加载
272 | .setAuthorizationProvider(
273 | JWTAuthorization.create("permissions")))
274 |
275 | .register(SomeDatabaseService.class, service);
276 | ```
277 |
278 | ## 代码生成
279 |
280 | 带有`@ProxyGen`注解的服务会触发服务助手类的生成:
281 |
282 | - 服务代理:编译时生成的代理,它使用 `EventBus` 通过消息与服务进行交互
283 | - 服务处理程序:编译时生成的`EventBus`处理程序,它对代理发送的事件做出反应
284 |
285 | 生成的代理和处理程序以服务类命名,例如,如果服务名为`MyService`,则 **服务处理程序** 称为`MyServiceVertxProxyHandler`,**服务代理** 称为`MyServiceVertxEBProxy`。
286 |
287 | 此外,Vert.x Core 提供了一个生成器,用于创建数据对象转换器,以简化服务代理中数据对象的使用。 这种转换器为在服务代理中使用数据对象所必需的`JsonObject`构造函数和`toJson()`方法提供了基础。
288 |
289 | *codegen* 注解处理器在编译时生成这些类。 它是 Java 编译器的一项功能,因此*不需要额外的步骤*,只需正确配置您的构建:
290 |
291 | 只需将 `io.vertx:vertx-codegen:processor` 和 `io.vertx:vertx-service-proxy` 依赖项添加到您的构建中。
292 |
293 | 这里是 Maven 的配置示例:
294 |
295 | ```xml
296 |
297 | io.vertx
298 | vertx-codegen
299 | 4.3.1
300 | processor
301 |
302 |
303 | io.vertx
304 | vertx-service-proxy
305 | 4.3.1
306 |
307 | ```
308 |
309 | 这个特性也可以在 Gradle 中使用:
310 |
311 | ```groovy
312 | implementation 'io.vertx:vertx-codegen:4.3.1'
313 | implementation 'io.vertx:vertx-service-proxy:4.3.1'
314 |
315 | annotationProcessor 'io.vertx:vertx-codegen:4.3.1:processor'
316 | annotationProcessor 'io.vertx:vertx-service-proxy:4.3.1'
317 | ```
318 |
319 | IDE 也通常为注释处理器提供支持。
320 |
321 | codegen `processor` 分类器通过 `META-INF/services` 插件机制将服务代理注解处理器的自动配置添加到 jar 中。
322 |
323 | 如果您愿意,您也可以将它与常规 jar 一起使用,但您需要显式声明注解处理器,例如在 Maven 中:
324 |
325 | ```xml
326 |
327 | maven-compiler-plugin
328 |
329 |
330 | io.vertx.codegen.CodeGenProcessor
331 |
332 |
333 |
334 | ```
335 |
336 | ## 公开您的服务
337 |
338 | 一旦你有了你的*服务接口*,编译源代码来生成存根和代理。然后,你需要一些代码在事件总线上“注册”你的服务:
339 |
340 | ```java
341 | SomeDatabaseService service = new SomeDatabaseServiceImpl();
342 | // Register the handler
343 | new ServiceBinder(vertx)
344 | .setAddress("database-service-address")
345 | .register(SomeDatabaseService.class, service);
346 | ```
347 | > **💡提示:** 译者注: 为了提高处理速度,可以在同一个地址上重复注册异步服务.其实内部就是在相同的EvenBus地址上添加了新的consumer!
348 |
349 | 这可以在verticle里完成,也可以在代码中的任何地方完成。
350 |
351 | 一旦注册,服务就可以访问。如果您在集群上运行应用程序,那么任何主机都可以提供该服务。
352 |
353 | 要撤销您的服务,请使用 `unregister` 方法:
354 |
355 | ```java
356 | ServiceBinder binder = new ServiceBinder(vertx);
357 |
358 | // 创建服务实现的实例
359 | SomeDatabaseService service = new SomeDatabaseServiceImpl();
360 | // Register the handler
361 | MessageConsumer consumer = binder
362 | .setAddress("database-service-address")
363 | .register(SomeDatabaseService.class, service);
364 |
365 | // ....
366 |
367 | // 取消注册您的服务。
368 | binder.unregister(consumer);
369 | ```
370 |
371 | ## 代理创建
372 |
373 | 现在服务已公开,您可能想要使用它。 为此,您需要创建一个代理。 可以使用 `ServiceProxyBuilder` 类创建代理:
374 |
375 | ```java
376 | ServiceProxyBuilder builder = new ServiceProxyBuilder(vertx)
377 | .setAddress("database-service-address");
378 |
379 | SomeDatabaseService service = builder.build(SomeDatabaseService.class);
380 | // 或有 delivery 选项:
381 | SomeDatabaseService service2 = builder.setOptions(options)
382 | .build(SomeDatabaseService.class);
383 | ```
384 |
385 | 第二种方法采用 `DeliveryOptions` 的实例,您可以在其中配置消息传递(例如超时)。
386 |
387 | 或者,您可以使用生成的代理类。 代理类名是 *service interface* 类名,后跟 `VertxEBProxy`。 例如,如果您的 *service interface* 命名为 `SomeDatabaseService`,则代理类命名为 `SomeDatabaseServiceVertxEBProxy`。
388 |
389 | 通常,*service interface* 包含一个`createProxy` 静态方法来创建代理。
390 |
391 | ```java
392 | @ProxyGen
393 | public interface SomeDatabaseService {
394 |
395 | // 创建代理的方法。
396 | static SomeDatabaseService createProxy(Vertx vertx, String address) {
397 | return new SomeDatabaseServiceVertxEBProxy(vertx, address);
398 | }
399 |
400 | // ...
401 | }
402 | ```
403 |
404 | ## 错误处理
405 |
406 | 服务方法可能会通过将包含 `ServiceException` 实例的失败 `Future` 传递给方法的 `Handler` 来向客户端返回错误。 `ServiceException` 包含一个 `int` 失败代码、一条消息和一个可选的 `JsonObject`,其中包含任何被认为对返回调用者很重要的额外信息。 为方便起见,`ServiceException.fail` 工厂方法可用于创建已包装在失败的`Future` 中的`ServiceException` 实例。 例如:
407 |
408 | ```java
409 | public class SomeDatabaseServiceImpl implements SomeDatabaseService {
410 |
411 | private static final BAD_SHOE_SIZE = 42;
412 | private static final CONNECTION_FAILED = 43;
413 |
414 | // Create a connection
415 | public Future createConnection(String shoeSize) {
416 | if (!shoeSize.equals("9")) {
417 | return Future.failedFuture(ServiceException.fail(BAD_SHOE_SIZE, "The shoe size must be 9!",
418 | new JsonObject().put("shoeSize", shoeSize)));
419 | } else {
420 | return doDbConnection().recover(err -> Future.failedFuture(ServiceException.fail(CONNECTION_FAILED, result.cause().getMessage())));
421 | }
422 | }
423 | }
424 | ```
425 |
426 | 然后,客户端可以检查它从失败的 `Future` 接收到的 `Throwable` 是否是 `ServiceException`,如果是,请检查内部的特定错误代码。 它可以使用此信息来区分业务逻辑错误和系统错误(例如未向事件总线注册的服务),并准确确定发生了哪个业务逻辑错误。
427 |
428 | ```java
429 | public Future foo(String shoeSize) {
430 | SomeDatabaseService service = SomeDatabaseService.createProxy(vertx, SERVICE_ADDRESS);
431 | server.createConnection("8")
432 | .compose(connection -> {
433 | // 做成功的事。
434 | return doSuccessStuff(connection);
435 | })
436 | .recover(err -> {
437 | if (err instanceof ServiceException) {
438 | ServiceException exc = (ServiceException) err;
439 | if (exc.failureCode() == SomeDatabaseServiceImpl.BAD_SHOE_SIZE) {
440 | return Future.failedFuture(
441 | new InvalidInputError("You provided a bad shoe size: " +
442 | exc.getDebugInfo().getString("shoeSize")));
443 | } else if (exc.failureCode() == SomeDatabaseServiceImpl.CONNECTION) {
444 | return Future.failedFuture(new ConnectionError("Failed to connect to the DB"));
445 | }
446 | } else {
447 | // 必须是系统错误(例如,没有为代理注册服务)
448 | return Future.failedFuture(new SystemError("An unexpected error occurred: + " result.cause().getMessage()));
449 | }
450 | });
451 | }
452 | ```
453 |
454 | 如果需要,服务实现也可以返回 `ServiceException` 的子类,只要为其注册了默认的 `MessageCodec`。 例如,给定以下 `ServiceException` 子类:
455 |
456 | ```java
457 | class ShoeSizeException extends ServiceException {
458 | public static final BAD_SHOE_SIZE_ERROR = 42;
459 |
460 | private final String shoeSize;
461 |
462 | public ShoeSizeException(String shoeSize) {
463 | super(BAD_SHOE_SIZE_ERROR, "In invalid shoe size was received: " + shoeSize);
464 | this.shoeSize = shoeSize;
465 | }
466 |
467 | public String getShoeSize() {
468 | return extra;
469 | }
470 |
471 | public static Future fail(int failureCode, String message, String shoeSize) {
472 | return Future.failedFuture(new MyServiceException(failureCode, message, shoeSize));
473 | }
474 | }
475 | ```
476 |
477 | 只要注册了一个默认的 `MessageCodec`,Service 实现就可以直接将自定义异常返回给调用者:
478 |
479 | ```java
480 | public class SomeDatabaseServiceImpl implements SomeDatabaseService {
481 | public SomeDataBaseServiceImpl(Vertx vertx) {
482 | // 注册服务端。 如果使用本地事件总线,这就是所有需要的,因为代理端将共享同一个 Vertx 实例。
483 | SomeDatabaseService service = SomeDatabaseService.createProxy(vertx, SERVICE_ADDRESS);
484 | vertx.eventBus().registerDefaultCodec(ShoeSizeException.class, new ShoeSizeExceptionMessageCodec());
485 | }
486 |
487 | // 创建连接
488 | Future createConnection(String shoeSize) {
489 | if (!shoeSize.equals("9")) {
490 | return ShoeSizeException.fail(shoeSize);
491 | } else {
492 | // 在此处创建连接
493 | return Future.succeededFuture(myDbConnection);
494 | }
495 | }
496 | }
497 | ```
498 |
499 | 最后,客户端现在可以检查自定义异常:
500 |
501 | ```java
502 | public Future foo(String shoeSize) {
503 | // 如果此代码在集群中的不同节点上运行,则 ShoeSizeExceptionMessageCodec 也需要在该节点上的 Vertx 实例中注册。
504 | SomeDatabaseService service = SomeDatabaseService.createProxy(vertx, SERVICE_ADDRESS);
505 | service.createConnection("8")
506 | .compose(connection -> {
507 | // 做成功的事。
508 | return doSuccessStuff(connection);
509 | })
510 | .recover(err -> {
511 | if (result.cause() instanceof ShoeSizeException) {
512 | ShoeSizeException exc = (ShoeSizeException) result.cause();
513 | return Future.failedFuture(
514 | new InvalidInputError("You provided a bad shoe size: " + exc.getShoeSize()));
515 | } else {
516 | // 必须是系统错误(例如,没有为代理注册服务)
517 | return Future.failedFuture(
518 | new SystemError("An unexpected error occurred: + " result.cause().getMessage())
519 | );
520 | }
521 | });
522 | }
523 | ```
524 |
525 | 请注意,如果您正在集群 `Vertx` 实例,则需要将自定义异常的 `MessageCodec` 注册到集群中的每个 `Vertx` 实例。
526 |
527 | ## 服务接口限制
528 |
529 | 可以在服务方法中使用的类型和返回值有一些限制,因此它们很容易在事件总线消息上编组,因此它们可以异步使用。 他们是:
530 |
531 | ### 数据类型
532 |
533 | 让`JSON` = `JsonObject | JsonArray` 让 `PRIMITIVE` = 任何原始类型或包装原始类型
534 |
535 | 参数可以是以下任何一种:
536 |
537 | - `JSON`
538 | - `PRIMITIVE`
539 | - `List`
540 | - `List`
541 | - `Set`
542 | - `Set`
543 | - `Map`
544 | - `Map`
545 | - 任何 *Enum* 类型
546 | - 任何使用 `@DataObject` 注解的类
547 |
548 | 异步结果模型化为:
549 |
550 | - `Future`
551 | - `Handler>` 用于回调样式
552 |
553 | `R` 可以是以下任何一种:
554 |
555 | - `JSON`
556 | - `PRIMITIVE`
557 | - `List`
558 | - `List`
559 | - `Set`
560 | - `Set`
561 | - 任何 *Enum* 类型
562 | - 任何使用 `@DataObject` 注解的类
563 | - 其他代理
564 |
565 | ### 重载方法
566 |
567 | 不能有重载的服务方法。 (*即*多个同名,无论签名)。
568 |
569 | ## 通过事件总线调用服务的约定(无代理)
570 |
571 | 服务代理假定事件总线消息遵循某种格式,因此可以使用它们来调用服务。
572 |
573 | 当然,如果您不想这样做,您不必**必须**使用客户端代理来访问远程服务。 仅通过事件总线发送消息来与它们交互是完全可以接受的。
574 |
575 | 为了使服务以一致的方式进行交互,以下消息格式**必须用于**任何 Vert.x 服务。
576 |
577 | 格式非常简单:
578 |
579 | - 应该有一个名为`action`的标题,它给出了要执行的操作的名称。
580 | - 消息的主体应该是一个`JsonObject`,在对象中应该有一个字段用于操作所需的每个参数。
581 |
582 | 例如,调用一个名为 `save` 的操作,它需要一个字符串集合和一个 JsonObject 文档:
583 |
584 | ```
585 | Headers:
586 | "action": "save"
587 | Body:
588 | {
589 | "collection", "mycollection",
590 | "document", {
591 | "name": "tim"
592 | }
593 | }
594 | ```
595 |
596 | 无论是否使用服务代理来创建服务,都应使用上述约定,因为它允许与服务进行一致的交互。
597 |
598 | 在使用服务代理的情况下,`action`值应该映射到服务接口中的操作方法的名称,并且正文中的每个 `[key, value]` 应该映射到 `[arg_name, arg_value]` 动作方法。
599 |
600 | 对于返回值,服务应该使用 `message.reply(...)` 方法来发回一个返回值 - 这可以是事件总线支持的任何类型。 要发出失败信号,应该使用方法 `message.fail(...)`。
601 |
602 | 如果您使用服务代理,生成的代码将自动为您处理。
603 |
--------------------------------------------------------------------------------
/Web/SockJS-client简介.md:
--------------------------------------------------------------------------------
1 | # SockJS-client简介
2 |
3 | # SockJS企业版
4 |
5 | 可作为Tidelift订阅的一部分。
6 |
7 | SockJS和数千个其他包的维护者正在与Tidelift合作,为您用于构建应用程序的开源依赖关系提供商业支持和维护。节省时间、降低风险并改善代码运行状况,同时为您使用的确切依赖项支付维护者费用。[了解更多](https://tidelift.com/subscription/pkg/npm-sockjs-client?utm_source=npm-sockjs-client&utm_medium=referral&utm_campaign=enterprise&utm_term=repo)
8 |
9 | # 概述
10 |
11 | SockJS是一个浏览器JavaScript库,提供了一个类似websocket的对象。SockJS为您提供了一个连贯的,跨浏览器的Javascript API,它在浏览器和web服务器之间创建了一个低延迟,全双工,跨域通信通道。
12 |
13 | 实际上,SockJS首先尝试使用本地WebSockets。如果失败了,它可以使用各种特定于浏览器的传输协议,并通过类似websocket的抽象来表示它们。
14 |
15 | SockJS旨在适用于所有现代浏览器和不支持WebSocket协议的环境——例如,在限制性的公司代理之后。
16 |
17 | SockJS-client确实需要一个对应的服务器:
18 |
19 | - [SockJS-node](https://github.com/sockjs/sockjs-node) 是Node.js的SockJS服务器。
20 |
21 | 原则:
22 |
23 | - API应该尽可能地遵循 [HTML5 Websockets API](https://www.w3.org/TR/websockets/)。
24 | - 所有传输必须支持开箱即用的跨域连接。可以并且建议将SockJS服务器托管在与主网站不同的服务器上。
25 | - 每个主流浏览器都至少支持一种流协议。
26 | - 流传输应该跨域工作,并且应该支持cookie(用于基于cookie的粘滞会话)。
27 | - 轮询传输用作旧浏览器和受限制代理后面的主机的回退。
28 | - 连接建立应快速、轻便。
29 | - 内部没有Flash(不需要打开端口843 -不需要通过代理工作,不需要托管'crossdomain.xml',不需要[等待3秒](https://github.com/gimite/web-socket-js/issues/49)以检测问题)
30 |
31 | 订阅[SockJS邮件列表](https://groups.google.com/forum/#!forum/sockjs)进行讨论和支持。
32 |
33 | # SockJS 家族
34 |
35 | - [SockJS-client](https://github.com/sockjs/sockjs-client) JavaScript client library
36 | - [SockJS-node](https://github.com/sockjs/sockjs-node) Node.js server
37 | - [SockJS-erlang](https://github.com/sockjs/sockjs-erlang) Erlang server
38 | - [SockJS-cyclone](https://github.com/flaviogrossi/sockjs-cyclone) Python/Cyclone/Twisted server
39 | - [SockJS-tornado](https://github.com/MrJoes/sockjs-tornado) Python/Tornado server
40 | - [SockJS-twisted](https://github.com/DesertBus/sockjs-twisted/) Python/Twisted server
41 | - [SockJS-aiohttp](https://github.com/aio-libs/sockjs/) Python/Aiohttp server
42 | - [Spring Framework](https://projects.spring.io/spring-framework) Java [client](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/web.html#websocket-fallback-sockjs-client) & server
43 | - [vert.x](https://github.com/vert-x/vert.x) Java/vert.x server
44 | - [Xitrum](https://xitrum-framework.github.io/) Scala server
45 | - [Atmosphere Framework](https://github.com/Atmosphere/atmosphere) JavaEE Server, Play Framework, Netty, Vert.x
46 | - [Actix SockJS](https://github.com/fafhrd91/actix-sockjs) Rust Server, Actix Framework
47 |
48 | 进行中的工作:
49 |
50 | - [SockJS-ruby](https://github.com/nyarly/sockjs-ruby)
51 | - [SockJS-netty](https://github.com/cgbystrom/sockjs-netty)
52 | - [SockJS-gevent](https://github.com/ksava/sockjs-gevent) ([SockJS-gevent fork](https://github.com/njoyce/sockjs-gevent))
53 | - [pyramid-SockJS](https://github.com/fafhrd91/pyramid_sockjs)
54 | - [wildcloud-websockets](https://github.com/wildcloud/wildcloud-websockets)
55 | - [wai-SockJS](https://github.com/Palmik/wai-sockjs)
56 | - [SockJS-perl](https://github.com/vti/sockjs-perl)
57 | - [SockJS-go](https://github.com/igm/sockjs-go/)
58 | - [syp.biz.SockJS.NET](https://github.com/sypbiz/SockJS.NET) - .NET port of the SockJS client
59 |
60 | # 开始
61 |
62 | SockJS 模仿 [WebSockets API](https://www.w3.org/TR/websockets/),但不是 `WebSocket`,而是 `SockJS` Javascript 对象。
63 |
64 | 首先,您需要加载 SockJS JavaScript 库。 例如,你可以把它放在你的 HTML 头部:
65 |
66 | ```javascript
67 |
68 | ```
69 |
70 | After the script is loaded you can establish a connection with the SockJS server. Here's a simple example:
71 |
72 | ```javascript
73 | var sock = new SockJS('https://mydomain.com/my_prefix');
74 | sock.onopen = function() {
75 | console.log('open');
76 | sock.send('test');
77 | };
78 |
79 | sock.onmessage = function(e) {
80 | console.log('message', e.data);
81 | sock.close();
82 | };
83 |
84 | sock.onclose = function() {
85 | console.log('close');
86 | };
87 | ```
88 |
89 | # SockJS-client API
90 |
91 | ## SockJS 类
92 |
93 | 与`WebSocket` API 类似,`SockJS`构造函数采用一个或多个参数:
94 |
95 | ```javascript
96 | var sockjs = new SockJS(url, _reserved, options);
97 | ```
98 |
99 | `url` 可以包含一个查询字符串,如果需要的话。
100 |
101 | 其中 `options` 是一个散列,它可以包含:
102 |
103 | - **server (字符串)**
104 |
105 | 要附加到实际数据连接的 url 的字符串。 默认为随机的 4 位数字。
106 |
107 | - **transports (字符串 或者 字符串数组)**
108 |
109 | 有时禁用一些后备传输很有用。 此选项允许您提供 SockJS 可能使用的传输列表。 默认情况下,将使用所有可用的传输。
110 |
111 | - **sessionId (数字 或者 函数)**
112 |
113 | 客户端和服务器都使用会话标识符来区分连接。如果您将此选项指定为数字,SockJS 将使用其随机字符串生成器函数生成 N 个字符长的会话 ID(其中 N 对应于 **sessionId** 指定的数字)。当您将此选项指定为一个函数时,该函数必须返回一个随机生成的字符串。每次 SockJS 需要生成会话 ID 时,它都会调用此函数并直接使用返回的字符串。如果不指定此选项,则默认使用默认的随机字符串生成器生成 8 个字符的长会话 ID。
114 |
115 | - **timeout (数字)**
116 |
117 | 指定用于传输连接的最小超时时间(以毫秒为单位, 默认是 5 毫秒)。默认情况下,这是根据测量的 RTT 和预期往返次数动态计算的。此设置将建立一个最小值,但如果计算出的超时值更高,则会使用该值。
118 |
119 | > **译者白石注** 例如: A WebSocket was created,uri: `/eventbus/843/zsqys1m5/websocket`,其中:
120 | >
121 | > - `843` 是 server
122 | > - `zsqys1m5` 是 sessionId
123 | > - `websocket` 是 transports
124 |
125 | 虽然 `SockJS` 对象试图模拟 `WebSocket` 行为,但不可能支持它的所有功能。SockJS 的一个重要限制是您不允许一次打开一个以上的 SockJS 连接到一个域。此限制是由传出连接的浏览器内限制引起的 - 通常[浏览器不允许打开两个以上的传出连接到单个域](https://stackoverflow.com/questions/985431/max-parallel-http-connections-in-a-browser)。一个 SockJS 会话需要这两个连接 - 一个用于下载数据,另一个用于发送消息。同时打开第二个 SockJS 会话很可能会阻塞,并可能导致两个会话超时。
126 |
127 | 一次打开多个 SockJS 连接通常是一种不好的做法。如果你绝对必须这样做,你可以使用多个子域,为每个SockJS连接使用不同的子域。
128 | > **译者白石注:** 现代的浏览器早已支持同一个域可以打开多个连接了.
129 | >
130 | > 
131 |
132 | # 支持的传输,按浏览器(从 http:// 或 https:// 提供的 html)
133 |
134 | | *Browser* | *Websockets* | *Streaming* | *Polling* |
135 | | --------------------- | ----------------- | ------------------ | ------------------ |
136 | | IE 6, 7 | no | no | jsonp-polling |
137 | | IE 8, 9 (cookies=no) | no | xdr-streaming † | xdr-polling † |
138 | | IE 8, 9 (cookies=yes) | no | iframe-htmlfile | iframe-xhr-polling |
139 | | IE 10 | rfc6455 | xhr-streaming | xhr-polling |
140 | | Chrome 6-13 | hixie-76 | xhr-streaming | xhr-polling |
141 | | Chrome 14+ | hybi-10 / rfc6455 | xhr-streaming | xhr-polling |
142 | | Firefox <10 | no ‡ | xhr-streaming | xhr-polling |
143 | | Firefox 10+ | hybi-10 / rfc6455 | xhr-streaming | xhr-polling |
144 | | Safari 5.x | hixie-76 | xhr-streaming | xhr-polling |
145 | | Safari 6+ | rfc6455 | xhr-streaming | xhr-polling |
146 | | Opera 10.70+ | no ‡ | iframe-eventsource | iframe-xhr-polling |
147 | | Opera 12.10+ | rfc6455 | xhr-streaming | xhr-polling |
148 | | Konqueror | no | no | jsonp-polling |
149 |
150 | - **†**: IE 8+ 支持[XDomainRequest]^[1]^(https://github.com/sockjs/sockjs-client#user-content-fn-9-a585ebdccb2f8f0e0f2f1a448ce422c1), 它本质上是一个修改后的 AJAX/XHR,可以跨域进行请求。 但不幸的是,它不发送任何 cookie,这使得它不适合在负载均衡器使用 JSESSIONID cookie 进行粘性会话时进行部署。
151 | - **‡**: Firefox 4.0和Opera 11.00,并附带禁用Websockets“hixie-76”。 它们仍然可以通过手动更改浏览器设置来启用。
152 |
153 | # 支持的传输,由浏览器(从 file:// 提供的 html)
154 |
155 | 有时您可能希望从“file://”地址提供您的 html - 用于开发或者如果您正在使用 PhoneGap 或类似技术。但是由于跨源策略,从“file://”提供的文件没有源,这意味着某些 SockJS 传输将无法工作。由于这个原因,SockJS 传输表与通常不同,主要区别是:
156 |
157 | | *Browser* | *Websockets* | *Streaming* | *Polling* |
158 | | --------- | ------------- | ------------------ | ------------------ |
159 | | IE 8, 9 | no | iframe-htmlfile | iframe-xhr-polling |
160 | | Other | same as above | iframe-eventsource | iframe-xhr-polling |
161 |
162 | # 支持的传输,按名称
163 |
164 | | *Transport* | *References* |
165 | | -------------------- | ------------------------------------------------------------ |
166 | | websocket (rfc6455) | [rfc 6455][2](https://github.com/sockjs/sockjs-client#user-content-fn-10-a585ebdccb2f8f0e0f2f1a448ce422c1) |
167 | | websocket (hixie-76) | [draft-hixie-thewebsocketprotocol-76][3](https://github.com/sockjs/sockjs-client#user-content-fn-1-a585ebdccb2f8f0e0f2f1a448ce422c1) |
168 | | websocket (hybi-10) | [draft-ietf-hybi-thewebsocketprotocol-10][4](https://github.com/sockjs/sockjs-client#user-content-fn-2-a585ebdccb2f8f0e0f2f1a448ce422c1) |
169 | | xhr-streaming | Transport using [Cross domain XHR][5](https://github.com/sockjs/sockjs-client#user-content-fn-5-a585ebdccb2f8f0e0f2f1a448ce422c1) [streaming][6](https://github.com/sockjs/sockjs-client#user-content-fn-7-a585ebdccb2f8f0e0f2f1a448ce422c1) capability (readyState=3). |
170 | | xdr-streaming | Transport using [XDomainRequest][1](https://github.com/sockjs/sockjs-client#user-content-fn-9-a585ebdccb2f8f0e0f2f1a448ce422c1) [streaming][6](https://github.com/sockjs/sockjs-client#user-content-fn-7-a585ebdccb2f8f0e0f2f1a448ce422c1) capability (readyState=3). |
171 | | eventsource | [EventSource/Server-sent events][7](https://github.com/sockjs/sockjs-client#user-content-fn-4-a585ebdccb2f8f0e0f2f1a448ce422c1). |
172 | | iframe-eventsource | [EventSource/Server-sent events][7](https://github.com/sockjs/sockjs-client#user-content-fn-4-a585ebdccb2f8f0e0f2f1a448ce422c1) used from an [iframe via postMessage][8](https://github.com/sockjs/sockjs-client#user-content-fn-3-a585ebdccb2f8f0e0f2f1a448ce422c1). |
173 | | htmlfile | [HtmlFile][9](https://github.com/sockjs/sockjs-client#user-content-fn-8-a585ebdccb2f8f0e0f2f1a448ce422c1). |
174 | | iframe-htmlfile | [HtmlFile][9](https://github.com/sockjs/sockjs-client#user-content-fn-8-a585ebdccb2f8f0e0f2f1a448ce422c1) used from an [iframe via postMessage][8](https://github.com/sockjs/sockjs-client#user-content-fn-3-a585ebdccb2f8f0e0f2f1a448ce422c1). |
175 | | xhr-polling | Long-polling using [cross domain XHR][5](https://github.com/sockjs/sockjs-client#user-content-fn-5-a585ebdccb2f8f0e0f2f1a448ce422c1). |
176 | | xdr-polling | Long-polling using [XDomainRequest][1](https://github.com/sockjs/sockjs-client#user-content-fn-9-a585ebdccb2f8f0e0f2f1a448ce422c1). |
177 | | iframe-xhr-polling | Long-polling using normal AJAX from an [iframe via postMessage][8](https://github.com/sockjs/sockjs-client#user-content-fn-3-a585ebdccb2f8f0e0f2f1a448ce422c1). |
178 | | jsonp-polling | Slow and old fashioned [JSONP polling][10](https://github.com/sockjs/sockjs-client#user-content-fn-6-a585ebdccb2f8f0e0f2f1a448ce422c1). This transport will show "busy indicator" (aka: "spinning wheel") when sending data. |
179 |
180 | # 在没有客户端的情况下连接到 SockJS
181 |
182 | 虽然 SockJS 的主要目的是启用浏览器到服务器的连接,但也可以从外部应用程序连接到 SockJS。任何符合 0.3 协议的 SockJS 服务器都支持原始 WebSocket url。测试服务器的原始WebSocket url如下所示:
183 |
184 | - ws://localhost:8081/echo/websocket
185 |
186 | 您可以将任何符合 WebSocket RFC 6455 标准的 WebSocket 客户端连接到此 url。这可以是命令行客户端、外部应用程序、第三方代码甚至是浏览器(尽管我不知道您为什么要这样做)。
187 |
188 | # 部署
189 |
190 | 您应该使用支持服务器使用的协议的 sockjs-client 版本。 例如:
191 |
192 | ```javascript
193 |
194 | ```
195 |
196 | 关于服务器端部署技巧,特别是关于负载平衡和会话粘连的技巧,请参阅[SockJS-node readme](https://github.com/sockjs/sockjs-node#readme)。
197 |
198 | # 开发 和 测试
199 |
200 | SockJS-client需要[node.js](https://nodejs.org/)来运行测试服务器和简化JavaScript。如果你想使用SockJS-client源代码,签出git repo并遵循以下步骤:
201 |
202 | ```bash
203 | cd sockjs-client
204 | npm install
205 | ```
206 |
207 | 要生成JavaScript,运行:
208 |
209 | ```bash
210 | gulp browserify
211 | ```
212 |
213 | 要生成最小化的JavaScript,运行:
214 |
215 | ```bash
216 | gulp browserify:min
217 | ```
218 |
219 | 这两个命令都输出到 `build` 目录。
220 |
221 | ## 测试
222 |
223 | 由以下机构提供的自动化测试:
224 |
225 | [](https://browserstack.com/)
226 |
227 | 编译 SockJS-client 后,您可能想检查您的更改是否通过了所有测试。
228 |
229 | ```bash
230 | npm run test:browser_local
231 | ```
232 |
233 | 这将启动 [karma](https://karma-runner.github.io/) 和测试支持服务器。
234 |
235 | # 浏览器怪癖
236 |
237 | 我们不打算解决各种浏览器怪癖:
238 |
239 | - 在 Firefox 20 之前,在 Firefox 中按 ESC 会关闭 SockJS 连接。有关解决方法和讨论,请参阅 [#18](https://github.com/sockjs/sockjs-client/issues/18)。
240 | - `jsonp-polling` 传输在发送数据时会显示一个“纺车”(又名“忙指示器”)。
241 | - 由于[浏览器的并发连接限制](https://stackoverflow.com/questions/985431/max-parallel-http-connections-in-a-browser),您不能同时打开多个SockJS连接到一个域(此限制不包括本机WebSocket连接)。
242 | - 虽然SockJS正在尝试转义任何奇怪的Unicode字符(甚至是无效字符-[像替身\xD800-\xDBFF ](https://en.wikipedia.org/wiki/Mapping_of_Unicode_characters#Surrogates)或 [\xFFFE和\xFFFF](https://en.wikipedia.org/wiki/Unicode#Character_General_Category)),但建议只使用有效字符。使用无效字符有点慢,并且可能不适用于具有适当 Unicode 支持的 SockJS 服务器。
243 | - 拥有一个名为 `onmessage` 的全局函数可能不是一个好主意,因为它可以由内置的 `postMessage` API 调用。
244 | - 从 SockJS 的角度来看,SSL/HTTPS 没有什么特别之处。未加密和加密站点之间的连接应该可以正常工作。
245 | - 尽管 SockJS 尽最大努力支持基于前缀和基于 cookie 的粘性会话,但后者可能无法与默认情况下不接受第三方 cookie 的浏览器 (Safari) 跨域良好地工作。为了解决这个问题,请确保您从与主站点相同的父域连接到 SockJS。例如,如果您从“[www.a.com](http://www.a.com/)”或“a.com”连接,“sockjs.a.com”能够设置 cookie。
246 | - 尝试从安全的“https://”连接到不安全的“http://”不是一个好主意。反过来应该没问题。
247 | - 众所周知,长轮询会导致 Heroku 出现问题,但 [SockJS 的解决方法可用](https://github.com/sockjs/sockjs-node/issues/57#issuecomment-5242187)。
248 | - SockJS [websocket传输在SSL上更稳定](https://github.com/sockjs/sockjs-client/issues/94)。如果你是一个严肃的SockJS用户,那么考虑使用SSL([更多信息](https://www.ietf.org/mail-archive/web/hybi/current/msg01605.html))。
249 |
250 | ## 脚注
251 |
252 | 1. https://blogs.msdn.microsoft.com/ieinternals/2010/05/13/xdomainrequest-restrictions-limitations-and-workarounds/ [↩](https://github.com/sockjs/sockjs-client#user-content-fnref-9-a585ebdccb2f8f0e0f2f1a448ce422c1) [↩2](https://github.com/sockjs/sockjs-client#user-content-fnref-9-2-a585ebdccb2f8f0e0f2f1a448ce422c1) [↩3](https://github.com/sockjs/sockjs-client#user-content-fnref-9-3-a585ebdccb2f8f0e0f2f1a448ce422c1)
253 | 2. https://www.rfc-editor.org/rfc/rfc6455.txt [↩](https://github.com/sockjs/sockjs-client#user-content-fnref-10-a585ebdccb2f8f0e0f2f1a448ce422c1)
254 | 3. https://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76 [↩](https://github.com/sockjs/sockjs-client#user-content-fnref-1-a585ebdccb2f8f0e0f2f1a448ce422c1)
255 | 4. https://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-10 [↩](https://github.com/sockjs/sockjs-client#user-content-fnref-2-a585ebdccb2f8f0e0f2f1a448ce422c1)
256 | 5. https://secure.wikimedia.org/wikipedia/en/wiki/XMLHttpRequest#Cross-domain_requests [↩](https://github.com/sockjs/sockjs-client#user-content-fnref-5-a585ebdccb2f8f0e0f2f1a448ce422c1) [↩2](https://github.com/sockjs/sockjs-client#user-content-fnref-5-2-a585ebdccb2f8f0e0f2f1a448ce422c1)
257 | 6. http://www.debugtheweb.com/test/teststreaming.aspx [↩](https://github.com/sockjs/sockjs-client#user-content-fnref-7-a585ebdccb2f8f0e0f2f1a448ce422c1) [↩2](https://github.com/sockjs/sockjs-client#user-content-fnref-7-2-a585ebdccb2f8f0e0f2f1a448ce422c1)
258 | 7. https://html.spec.whatwg.org/multipage/comms.html#server-sent-events [↩](https://github.com/sockjs/sockjs-client#user-content-fnref-4-a585ebdccb2f8f0e0f2f1a448ce422c1) [↩2](https://github.com/sockjs/sockjs-client#user-content-fnref-4-2-a585ebdccb2f8f0e0f2f1a448ce422c1)
259 | 8. https://developer.mozilla.org/en/DOM/window.postMessage [↩](https://github.com/sockjs/sockjs-client#user-content-fnref-3-a585ebdccb2f8f0e0f2f1a448ce422c1) [↩2](https://github.com/sockjs/sockjs-client#user-content-fnref-3-2-a585ebdccb2f8f0e0f2f1a448ce422c1) [↩3](https://github.com/sockjs/sockjs-client#user-content-fnref-3-3-a585ebdccb2f8f0e0f2f1a448ce422c1)
260 | 9. http://cometdaily.com/2007/11/18/ie-activexhtmlfile-transport-part-ii/ [↩](https://github.com/sockjs/sockjs-client#user-content-fnref-8-a585ebdccb2f8f0e0f2f1a448ce422c1) [↩2](https://github.com/sockjs/sockjs-client#user-content-fnref-8-2-a585ebdccb2f8f0e0f2f1a448ce422c1)
261 | 10. https://secure.wikimedia.org/wikipedia/en/wiki/JSONP [↩](https://github.com/sockjs/sockjs-client#user-content-fnref-6-a585ebdccb2f8f0e0f2f1a448ce422c1)
262 |
263 |
264 |
265 | # 各种问题和设计注意事项
266 |
267 | ## WebSocket兼容的负载均衡器
268 |
269 | https://openbase.com/js/sockjs/documentation
270 |
271 | 通常 WebSockets 不能很好地与代理和负载均衡器一起使用。 在 Nginx 或 Apache 后面部署 SockJS 服务器可能会很痛苦。
272 |
273 | 幸运的是,一个优秀的负载均衡器[HAProxy](http://haproxy.1wt.eu/)的最新版本能够代理WebSocket连接。我们建议将HAProxy作为前端负载均衡器,并使用它将 SockJS 流量与普通 HTTP 数据分开。查看示例 [SockJS HAProxy 配置](https://github.com/sockjs/sockjs-node/blob/master/examples/haproxy.cfg)。
274 |
275 | `haproxy.cfg`文件:
276 |
277 | ```ini
278 | # Requires recent Haproxy to work with websockets (for example 1.4.16).
279 | defaults
280 | mode http
281 | # Set timeouts to your needs
282 | timeout client 5s
283 | timeout connect 5s
284 | timeout server 5s
285 |
286 | frontend all 0.0.0.0:8888
287 | mode http
288 | timeout client 120s
289 |
290 | option forwardfor
291 | # Fake connection:close, required in this setup.
292 | option http-server-close
293 | option http-pretend-keepalive
294 |
295 | acl is_sockjs path_beg /echo /broadcast /close
296 | acl is_stats path_beg /stats
297 |
298 | use_backend sockjs if is_sockjs
299 | use_backend stats if is_stats
300 | default_backend static
301 |
302 |
303 | backend sockjs
304 | # 根据从 url 路径中的前两个目录创建的哈希进行负载平衡。
305 | # 例如,去往 `/1/` 的请求应该由单个服务器处理(假设资源前缀是一级深度,如“/eventbus”)。
306 | # 例如SockJS的URL `/eventbus/843/zsqys1m5/websocket`
307 | balance uri depth 2
308 | timeout server 120s
309 | server srv_sockjs1 127.0.0.1:9999
310 | # server srv_sockjs2 127.0.0.1:9998
311 |
312 | backend static
313 | balance roundrobin
314 | server srv_static 127.0.0.1:8000
315 |
316 | backend stats
317 | stats uri /stats
318 | stats enable
319 | ```
320 |
321 | 该配置还展示了如何使用 HAproxy 平衡在多个 Node.js 服务器之间拆分流量。 您还可以使用 DNS 名称进行平衡。
322 |
323 | ## 粘性会话
324 |
325 | 如果您计划部署多个SockJS服务器,则必须确保单个会话的所有HTTP请求都将命中同一个服务器。SockJS有两种机制可以实现这一点:
326 |
327 | - URL 以服务器和会话 ID 编号为前缀,例如:`/resource///transport`。 这对于支持基于前缀的关联(HAProxy 支持)的负载均衡器很有用。
328 | - `JSESSIONID` cookie 由 SockJS 节点设置。 如果设置了该 cookie,许多负载平衡器会打开粘性会话。 此技术源自 Java 应用程序,其中通常需要粘性会话。 HAProxy 以及一些托管服务提供商(例如 CloudFoundry)确实支持这种方法。 为了在客户端启用此方法,请向 SockJS 构造函数提供 `cookie:true` 选项。
329 |
330 | ## 授权
331 |
332 | SockJS 节点不会向应用程序公开 cookie。 这是故意这样做的,因为在 SockJS 中使用基于 cookie 的授权根本没有意义,并且会导致安全问题。
333 |
334 | cookie是浏览器和http服务器之间的契约,由域名标识。如果浏览器为特定的域设置了cookie,它将把它作为所有http请求的一部分传递给主机。但是为了让各种传输工作,SockJS使用了一个中间人
335 |
336 | - 一个来自目标SockJS域的iframe。这意味着服务器将从iframe接收请求,而不是从真实域接收请求。iframe的域和SockJS的域是一样的。问题是任何网站都可以嵌入iframe并与之通信-并请求建立SockJS连接。在这种情况下,使用cookie进行授权将导致从任何网站授予SockJS与您的网站通信的完全访问权。这是典型的CSRF攻击。
337 |
338 | 基本上- cookie不适合SockJS模型。如果你想授权一个会话-在一个页面上提供一个唯一的令牌,首先通过SockJS连接发送它,并在服务器端验证它。本质上,这就是cookie的工作原理。
--------------------------------------------------------------------------------
/认证和授权/JWT 认证与授权.md:
--------------------------------------------------------------------------------
1 | # JWT 认证与授权
2 |
3 | 该组件包含了一个现成的 JWT 实现,要使用这个项目, 将下面的依赖添加到构建描述符里的 *dependencies* 部分
4 |
5 | - Maven(在您的 `pom.xml`):
6 |
7 | ```xml
8 |
9 | io.vertx
10 | vertx-auth-jwt
11 | 4.3.5
12 |
13 | ```
14 |
15 | - Gradle(在您的 `build.gradle` 文件):
16 |
17 | ```groovy
18 | compile 'io.vertx:vertx-auth-jwt:4.3.5'
19 | ```
20 |
21 | JSON Web 令牌是一种简单的以明文(通常是URL)发送信息的方法, 其内容可被验证为是可信的。 像下面的这些场景 JWT 是非常适用的:
22 |
23 | - 在单点登录方案中,需要一个单独的身份验证服务器, 然后该服务器可以以被信任的方式发送用户信息。
24 | - 无状态的API 服务,非常适合单页应用。
25 | - 等等…
26 |
27 | 在决定使用 JWT 之前, 需要重点注意的是 JWT 并不加密 payload, 而是对它签名。 您不应该使用 JWT 发送任何机密信息,相反应该发送的是非私密的,但需要被验证的信息。 举个例子,使用 JWT 发送一个签名过的用户 id 来表明这个用户已经登录了的做法非常棒! 相反发送一个用户的密码的做法则是非常非常错误的。
28 |
29 | JWT 主要的优点有:
30 |
31 | - 它允许您验证令牌的真实性。
32 | - 它有一个 JSON 结构,可以包含任何您所需的任意数量的数据。
33 | - 它是无状态的。
34 |
35 | ------
36 |
37 | **作者注解:** JWT令牌有三部分:
38 |
39 | - Header
40 | 包含用于对令牌签名的算法,用于声明类型`typ`和加密算法`alg`,该内容使用base64加密
41 |
42 | - Body
43 |
44 | 主要为json数据,该数据经过Base64URl编码,包含声明
45 |
46 | - 标准声明(建议使用)
47 | jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。
48 | iss(issuer):JWT 签发方。
49 | iat(issued at time):JWT 签发时间。
50 | sub(subject):JWT 主题。
51 | aud(audience):JWT 接收方。
52 | exp(expiration time):JWT 的过期时间。是一个数字的组合,这是因为这个字符串使用的是 Unix 的时间
53 | nbf(not before time):JWT 生效时间,早于该定义的时间的 JWT 不能被接受处理。
54 | jti(JWT ID):JWT 唯一标识。
55 |
56 | - 公有声明
57 | 该部分可添加任何信息
58 | - 私有声明
59 | 客户端与服务端共同定义的声明
60 |
61 | - Signature
62 | 根据算法,签名包含使用私钥签名的正文签名
63 |
64 | ------
65 |
66 | 您可使用 `JWTAuth` 创建一个验证器的实例。 并指定一个格式为 JSON 的配置文件。
67 |
68 | 这是创建一个 JWT 身份验证器的示例代码:
69 |
70 | ```java
71 | JWTAuthOptions config = new JWTAuthOptions()
72 | .setKeyStore(new KeyStoreOptions()
73 | .setPath("keystore.jceks")
74 | .setPassword("secret"));
75 |
76 | AuthenticationProvider provider = JWTAuth.create(vertx, config);
77 | ```
78 |
79 | JWT 用法的典型流程是,您的应用程序中有一个接口负责颁发令牌,这个接口应在 SSL 模式下运行, 接口在通过用户名和密码验证完请求用户之后, 您应当这样做:
80 |
81 | ```java
82 | JWTAuthOptions config = new JWTAuthOptions()
83 | .setKeyStore(new KeyStoreOptions()
84 | .setPath("keystore.jceks")
85 | .setPassword("secret"));
86 |
87 | JWTAuth provider = JWTAuth.create(vertx, config);
88 |
89 | // 验证用户的用户名和密码之后
90 | // 通过端点生成签名令牌
91 | if ("paulo".equals(username) && "super_secret".equals(password)) {
92 | String token = provider.generateToken(
93 | new JsonObject().put("sub", "paulo"), new JWTOptions());
94 |
95 | // 现在,对于任何对受保护资源的请求,您应该
96 | // 检查他们的HTTP头中Authorization字符串:
97 | // Authorization: Bearer
98 | }
99 | ```
100 |
101 | ### 加载秘钥
102 |
103 | 秘钥可以通过三种不同的方式载入:
104 |
105 | - 使用 secrets(对称秘钥)
106 | - 使用 OpenSSL 生成的 `pem` 格式文件(公钥)
107 | - 使用 Java Keystore 文件(对称加密公钥)
108 |
109 | #### 使用对称秘钥
110 |
111 | JWT的默认签名方法称为 `HS256`。 `HS` 默认表示为 `HMAC 加密 使用 SHA256`。
112 |
113 | 这便是最简单的加载秘钥方式。您只需要将 secret 与第三方共享,举个例子 假设 secret 是:`keyboard cat` 那么您可将 Auth 配置为:
114 |
115 | ```java
116 | JWTAuth provider = JWTAuth.create(vertx, new JWTAuthOptions()
117 | .addPubSecKey(new PubSecKeyOptions()
118 | .setAlgorithm("HS256")
119 | .setBuffer("keyboard cat")));
120 |
121 | String token = provider.generateToken(new JsonObject());
122 | ```
123 |
124 | 在这种情况下 secret 将被设置为公钥,因为这是双方都知道的令牌, 您可配置 PubSec 密钥为对称的。
125 |
126 | #### 使用 RSA 秘钥
127 |
128 | 这部分不是 OpenSSL 文档,建议阅读 OpenSSL 文档了解命令的使用。 我们将介绍如何生成最通用的密钥以及如何与 JWT auth 一起使用。
129 |
130 | 想象一下,您想使用非常常见的 `RS256` 加密算法来保护您的资源。 与您想象的相反, 256 不是秘钥长度而是哈希算法的签名长度。 任何 RSA 秘钥都可以和 JWT 加密算法一期使用。 这是信息表:
131 |
132 | | "alg" 参数值 | 数字签名算法 |
133 | | ------------ | ----------------------------------- |
134 | | *RS256* | **RSASSA-PKCS1-v1_5 using SHA-256** |
135 | | *RS384* | **RSASSA-PKCS1-v1_5 using SHA-384** |
136 | | *RS512* | **RSASSA-PKCS1-v1_5 using SHA-512** |
137 |
138 | 如果您想生成一个2048位的 RSA 密钥对,那么您应该 (请记住 **不要** 添加密码, 否则 JWT auth 将无法载入秘钥文件):
139 |
140 | ```shell
141 | openssl genrsa -out private.pem 2048
142 | ```
143 |
144 | 如您看到类似的文件内容,那么恭喜您,秘钥文件正确的生成了:
145 |
146 | ```
147 | -----BEGIN RSA PRIVATE KEY-----
148 | MIIEowIBAAKCAQEAxPSbCQY5mBKFDIn1kggvWb4ChjrctqD4nFnJOJk4mpuZ/u3h
149 | ...
150 | e4k0yN3F1J1DVlqYWJxaIMzxavQsi9Hz4p2JgyaZMDGB6kGixkMo
151 | -----END RSA PRIVATE KEY-----
152 | ```
153 |
154 | 标准的 JDK 是无法读取该文件的,所以我们 **必须** 将其转换成 PKCS8 标准格式:
155 |
156 | ```
157 | openssl pkcs8 -topk8 -inform PEM -in private.pem -out private_key.pem -nocrypt
158 | ```
159 |
160 | 现在类似原始文件的新文件 `private_key.pem` 里包含了:
161 |
162 | ```
163 | -----BEGIN PRIVATE KEY-----
164 | MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDE9JsJBjmYEoUM
165 | ...
166 | 0fPinYmDJpkwMYHqQaLGQyg=
167 | -----END PRIVATE KEY-----
168 | ```
169 |
170 | 如您只验证令牌(只需要 private_key.pem 文件)那么您需要签发令牌, 故而您需要一个公钥。在这种情况下您需要从私钥文件中提取公钥文件:
171 |
172 | ```
173 | openssl rsa -in private.pem -outform PEM -pubout -out public.pem
174 | ```
175 |
176 | 您会见到类似以下内容的文件:
177 |
178 | ```
179 | -----BEGIN PUBLIC KEY-----
180 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxPSbCQY5mBKFDIn1kggv
181 | ...
182 | qwIDAQAB
183 | -----END PUBLIC KEY-----
184 | ```
185 |
186 | 现在可以校验令牌有效性了:
187 |
188 | ```java
189 | JWTAuth provider = JWTAuth.create(vertx, new JWTAuthOptions()
190 | .addPubSecKey(new PubSecKeyOptions()
191 | .setAlgorithm("RS256")
192 | .setBuffer(
193 | "-----BEGIN PUBLIC KEY-----\n" +
194 | "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxPSbCQY5mBKFDIn1kggv\n" +
195 | "Wb4ChjrctqD4nFnJOJk4mpuZ/u3h2ZgeKJJkJv8+5oFO6vsEwF7/TqKXp0XDp6IH\n" +
196 | "byaOSWdkl535rCYR5AxDSjwnuSXsSp54pvB+fEEFDPFF81GHixepIbqXCB+BnCTg\n" +
197 | "N65BqwNn/1Vgqv6+H3nweNlbTv8e/scEgbg6ZYcsnBBB9kYLp69FSwNWpvPmd60e\n" +
198 | "3DWyIo3WCUmKlQgjHL4PHLKYwwKgOHG/aNl4hN4/wqTixCAHe6KdLnehLn71x+Z0\n" +
199 | "SyXbWooftefpJP1wMbwlCpH3ikBzVIfHKLWT9QIOVoRgchPU3WAsZv/ePgl5i8Co\n" +
200 | "qwIDAQAB\n" +
201 | "-----END PUBLIC KEY-----"))
202 | .addPubSecKey(new PubSecKeyOptions()
203 | .setAlgorithm("RS256")
204 | .setBuffer(
205 | "-----BEGIN PRIVATE KEY-----\n" +
206 | "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDE9JsJBjmYEoUM\n" +
207 | "ifWSCC9ZvgKGOty2oPicWck4mTiam5n+7eHZmB4okmQm/z7mgU7q+wTAXv9Oopen\n" +
208 | "RcOnogdvJo5JZ2SXnfmsJhHkDENKPCe5JexKnnim8H58QQUM8UXzUYeLF6khupcI\n" +
209 | "H4GcJOA3rkGrA2f/VWCq/r4fefB42VtO/x7+xwSBuDplhyycEEH2Rgunr0VLA1am\n" +
210 | "8+Z3rR7cNbIijdYJSYqVCCMcvg8cspjDAqA4cb9o2XiE3j/CpOLEIAd7op0ud6Eu\n" +
211 | "fvXH5nRLJdtaih+15+kk/XAxvCUKkfeKQHNUh8cotZP1Ag5WhGByE9TdYCxm/94+\n" +
212 | "CXmLwKirAgMBAAECggEAeQ+M+BgOcK35gAKQoklLqZLEhHNL1SnOhnQd3h84DrhU\n" +
213 | "CMF5UEFTUEbjLqE3rYGP25mdiw0ZSuFf7B5SrAhJH4YIcZAO4a7ll23zE0SCW+/r\n" +
214 | "zr9DpX4Q1TP/2yowC4uGHpBfixxpBmVljkWnai20cCU5Ef/O/cAh4hkhDcHrEKwb\n" +
215 | "m9nymKQt06YnvpCMKoHDdqzfB3eByoAKuGxo/sbi5LDpWalCabcg7w+WKIEU1PHb\n" +
216 | "Qi+RiDf3TzbQ6TYhAEH2rKM9JHbp02TO/r3QOoqHMITW6FKYvfiVFN+voS5zzAO3\n" +
217 | "c5X4I+ICNzm+mnt8wElV1B6nO2hFg2PE9uVnlgB2GQKBgQD8xkjNhERaT7f78gBl\n" +
218 | "ch15DRDH0m1rz84PKRznoPrSEY/HlWddlGkn0sTnbVYKXVTvNytKSmznRZ7fSTJB\n" +
219 | "2IhQV7+I0jeb7pyLllF5PdSQqKTk6oCeL8h8eDPN7awZ731zff1AGgJ3DJXlRTh/\n" +
220 | "O6zj9nI8llvGzP30274I2/+cdwKBgQDHd/twbiHZZTDexYewP0ufQDtZP1Nk54fj\n" +
221 | "EpkEuoTdEPymRoq7xo+Lqj5ewhAtVKQuz6aH4BeEtSCHhxy8OFLDBdoGCEd/WBpD\n" +
222 | "f+82sfmGk+FxLyYkLxHCxsZdOb93zkUXPCoCrvNRaUFO1qq5Dk8eftGCdC3iETHE\n" +
223 | "6h5avxHGbQKBgQCLHQVMNhL4MQ9slU8qhZc627n0fxbBUuhw54uE3s+rdQbQLKVq\n" +
224 | "lxcYV6MOStojciIgVRh6FmPBFEvPTxVdr7G1pdU/k5IPO07kc6H7O9AUnPvDEFwg\n" +
225 | "suN/vRelqbwhufAs85XBBY99vWtxdpsVSt5nx2YvegCgdIj/jUAU2B7hGQKBgEgV\n" +
226 | "sCRdaJYr35FiSTsEZMvUZp5GKFka4xzIp8vxq/pIHUXp0FEz3MRYbdnIwBfhssPH\n" +
227 | "/yKzdUxcOLlBtry+jgo0nyn26/+1Uyh5n3VgtBBSePJyW5JQAFcnhqBCMlOVk5pl\n" +
228 | "/7igiQYux486PNBLv4QByK0gV0SPejDzeqzIyB+xAoGAe5if7DAAKhH0r2M8vTkm\n" +
229 | "JvbCFjwuvhjuI+A8AuS8zw634BHne2a1Fkvc8c3d9VDbqsHCtv2tVkxkKXPjVvtB\n" +
230 | "DtzuwUbp6ebF+jOfPK0LDuJoTdTdiNjIcXJ7iTTI3cXUnUNWWphYnFogzPFq9CyL\n" +
231 | "0fPinYmDJpkwMYHqQaLGQyg=\n" +
232 | "-----END PRIVATE KEY-----")
233 | ));
234 |
235 | String token = provider.generateToken(
236 | new JsonObject().put("some", "token-data"),
237 | new JWTOptions().setAlgorithm("RS256"));
238 | ```
239 |
240 | #### 使用 EC 秘钥
241 |
242 | 我们还支持椭圆曲线加密算法,但是在默认 JDK 上使用有一定限制
243 |
244 | 用法和 RSA 加密算法极其相似,首先您需要创建一个公钥:
245 |
246 | ```shell
247 | openssl ecparam -name secp256r1 -genkey -out private.pem
248 | ```
249 |
250 | 然后您会看到类似以下内容的文件了:
251 |
252 | ```
253 | -----BEGIN EC PARAMETERS-----
254 | BggqhkjOPQMBBw==
255 | -----END EC PARAMETERS-----
256 | -----BEGIN EC PRIVATE KEY-----
257 | MHcCAQEEIMZGaqZDTHL+IzFYEWLIYITXpGzOJuiQxR2VNGheq7ShoAoGCCqGSM49
258 | AwEHoUQDQgAEG1O9LCrP6hg3Y9q68+LF0q48UcOkwVKE1ax0b56wjVusf3qnuFO2
259 | /+XHKKhtzEavvFMeXRQ+ZVEqM0yGNb04qw==
260 | -----END EC PRIVATE KEY-----
261 | ```
262 |
263 | 但是 JDK 更倾向于使用PKCS8格式,我们必须将其转换:
264 |
265 | ```shell
266 | openssl pkcs8 -topk8 -nocrypt -in private.pem -out private_key.pem
267 | ```
268 |
269 | 然后会看到类似内容的文件:
270 |
271 | ```
272 | -----BEGIN PRIVATE KEY-----
273 | MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgxkZqpkNMcv4jMVgR
274 | YshghNekbM4m6JDFHZU0aF6rtKGhRANCAAQbU70sKs/qGDdj2rrz4sXSrjxRw6TB
275 | UoTVrHRvnrCNW6x/eqe4U7b/5ccoqG3MRq+8Ux5dFD5lUSozTIY1vTir
276 | -----END PRIVATE KEY-----
277 | ```
278 |
279 | 使用私钥您可生成令牌:
280 |
281 | ```java
282 | JWTAuth provider = JWTAuth.create(vertx, new JWTAuthOptions()
283 | .addPubSecKey(new PubSecKeyOptions()
284 | .setAlgorithm("ES256")
285 | .setBuffer(
286 | "-----BEGIN PRIVATE KEY-----\n" +
287 | "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgeRyEfU1NSHPTCuC9\n" +
288 | "rwLZMukaWCH2Fk6q5w+XBYrKtLihRANCAAStpUnwKmSvBM9EI+W5QN3ALpvz6bh0\n" +
289 | "SPCXyz5KfQZQuSj4f3l+xNERDUDaygIUdLjBXf/bc15ur2iZjcq4r0Mr\n" +
290 | "-----END PRIVATE KEY-----\n")
291 | ));
292 |
293 | String token = provider.generateToken(
294 | new JsonObject(),
295 | new JWTOptions().setAlgorithm("ES256"));
296 | ```
297 |
298 | 为了验证令牌您还需要一个公钥:
299 |
300 | ```shell
301 | openssl ec -in private.pem -pubout -out public.pem
302 | ```
303 |
304 | 现在您可用它进行全部操作了:
305 |
306 | ```java
307 | JWTAuth provider = JWTAuth.create(vertx, new JWTAuthOptions()
308 | .addPubSecKey(new PubSecKeyOptions()
309 | .setAlgorithm("ES256")
310 | .setBuffer(
311 | "-----BEGIN PUBLIC KEY-----\n" +
312 | "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEraVJ8CpkrwTPRCPluUDdwC6b8+m4\n" +
313 | "dEjwl8s+Sn0GULko+H95fsTREQ1A2soCFHS4wV3/23Nebq9omY3KuK9DKw==\n" +
314 | "-----END PUBLIC KEY-----"))
315 | .addPubSecKey(new PubSecKeyOptions()
316 | .setAlgorithm("ES256")
317 | .setBuffer(
318 | "-----BEGIN PRIVATE KEY-----\n" +
319 | "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgeRyEfU1NSHPTCuC9\n" +
320 | "rwLZMukaWCH2Fk6q5w+XBYrKtLihRANCAAStpUnwKmSvBM9EI+W5QN3ALpvz6bh0\n" +
321 | "SPCXyz5KfQZQuSj4f3l+xNERDUDaygIUdLjBXf/bc15ur2iZjcq4r0Mr")
322 | ));
323 |
324 | String token = provider.generateToken(
325 | new JsonObject(),
326 | new JWTOptions().setAlgorithm("ES256"));
327 | ```
328 |
329 | #### JWT keystore 文件
330 |
331 | 如果您更倾向于使用 Java Keystores 格式的秘钥文件,那么您也可如此做。
332 |
333 | 身份认证器需要在 classpath 或文件路径上加载一个秘钥库,以供 `javax.crypto.Mac` 或 `java.security.Signature` 生成或认证令牌。
334 |
335 | 默认情况下,该实现将查找以下别名,并不是所有加密算法都有别名。 就比如 `HS256` 是存在的:
336 |
337 | ```
338 | `HS256`:: HMAC 使用SHA-256哈希算法
339 | `HS384`:: HMAC 使用SHA-384哈希算法
340 | `HS512`:: HMAC 使用SHA-512哈希算法
341 | `RS256`:: RSASSA 使用SHA-256哈希算法
342 | `RS384`:: RSASSA 使用SHA-384哈希算法
343 | `RS512`:: RSASSA 使用SHA-512哈希算法
344 | `ES256`:: ECDSA 使用P-256曲线和SHA-256哈希算法
345 | `ES384`:: ECDSA 使用P-384曲线和SHA-384哈希算法
346 | `ES512`:: ECDSA 使用P-521曲线和SHA-512哈希算法
347 | ```
348 |
349 | 如果未提供密钥库,则实现将回退到不安全模式,并且不会验证签名, 这对于通过外部手段对 payload 签名会很有用。
350 |
351 | 存储于 keystore 里的密钥对始终包含证书。 证书的有效性在加载时就进行了测试, 如果证书已过期或无效, 则不会加载证书。
352 |
353 | 给定别名将在所有的密钥算法中匹配最合适的。 例如 `RS256` 算法是不允许的,`EC` 算法或 `RSA` 算法是允许的, 注意 `RSA` 具体为 `SHA1WithRSA` 而不是 `SHA256WithRSA`。
354 |
355 | ##### 生成新的Keystore格式秘钥
356 |
357 | 生成秘钥文件需要唯一的工具是 `keytool`, 您通过以下方式指定算法:
358 |
359 | ```
360 | keytool -genseckey -keystore keystore.jceks -storetype jceks -storepass secret -keyalg HMacSHA256 -keysize 2048 -alias HS256 -keypass secret
361 | keytool -genseckey -keystore keystore.jceks -storetype jceks -storepass secret -keyalg HMacSHA384 -keysize 2048 -alias HS384 -keypass secret
362 | keytool -genseckey -keystore keystore.jceks -storetype jceks -storepass secret -keyalg HMacSHA512 -keysize 2048 -alias HS512 -keypass secret
363 | keytool -genkey -keystore keystore.jceks -storetype jceks -storepass secret -keyalg RSA -keysize 2048 -alias RS256 -keypass secret -sigalg SHA256withRSA -dname "CN=,OU=,O=,L=,ST=,C=" -validity 360
364 | keytool -genkey -keystore keystore.jceks -storetype jceks -storepass secret -keyalg RSA -keysize 2048 -alias RS384 -keypass secret -sigalg SHA384withRSA -dname "CN=,OU=,O=,L=,ST=,C=" -validity 360
365 | keytool -genkey -keystore keystore.jceks -storetype jceks -storepass secret -keyalg RSA -keysize 2048 -alias RS512 -keypass secret -sigalg SHA512withRSA -dname "CN=,OU=,O=,L=,ST=,C=" -validity 360
366 | keytool -genkeypair -keystore keystore.jceks -storetype jceks -storepass secret -keyalg EC -keysize 256 -alias ES256 -keypass secret -sigalg SHA256withECDSA -dname "CN=,OU=,O=,L=,ST=,C=" -validity 360
367 | keytool -genkeypair -keystore keystore.jceks -storetype jceks -storepass secret -keyalg EC -keysize 384 -alias ES384 -keypass secret -sigalg SHA384withECDSA -dname "CN=,OU=,O=,L=,ST=,C=" -validity 360
368 | keytool -genkeypair -keystore keystore.jceks -storetype jceks -storepass secret -keyalg EC -keysize 521 -alias ES512 -keypass secret -sigalg SHA512withECDSA -dname "CN=,OU=,O=,L=,ST=,C=" -validity 360
369 | ```
370 |
371 | 更多有关 keystores 的信息以及如何使用 `PKCS12` 格式秘钥 (默认:Java版本 >=9) 请参阅通用模块的文档。
372 |
373 | ### 读取令牌
374 |
375 | 如果由第三方发布 JWT 令牌,而您没有私钥, 在这种情况下您的公钥必须是 PEM 格式的。
376 |
377 | ```java
378 | JWTAuthOptions config = new JWTAuthOptions()
379 | .addPubSecKey(new PubSecKeyOptions()
380 | .setAlgorithm("RS256")
381 | .setBuffer("BASE64-ENCODED-PUBLIC_KEY"));
382 |
383 | AuthenticationProvider provider = JWTAuth.create(vertx, config);
384 | ```
385 |
386 | ## AuthN/AuthZ 和 JWT
387 |
388 | 通常在开发微服务时,您希望应用程序能够调用一些 API 。 而这些 API 并不打算暴露给一般用户, 因而我们应该在架构中移除所有对 API 调用方进行身份验证的交互部分的内容。
389 |
390 | 在这种情况下,可以使用 HTTP 作为调用 API 的协议, 并且 HTTP 协议已经定义了应该用于传递授权信息的标头 `Authorization` 。在大多数情况下令牌将以承载令牌(bearer tokens)的形式发送, 例如:`Authorization: Bearer some+base64+string`。
391 |
392 | ### 鉴权/身份验证 (AuthN)
393 |
394 | 对于此验证器,如果令牌通过签名检查并且令牌未过期, 则对用户进行身份验证。因此,必须保证私钥安全不被泄露,并且不要在项目中复制粘贴, 因为这将是一个安全漏洞。
395 |
396 | ```java
397 | jwtAuth.authenticate(new JsonObject().put("token", "BASE64-ENCODED-STRING"))
398 | .onSuccess(user -> System.out.println("User: " + user.principal()))
399 | .onFailure(err -> {
400 | // 失败!
401 | });
402 | ```
403 |
404 | 简而言之,验证服务正在检查以下几件事:
405 |
406 | - 令牌签名是否有效
407 | - `exp`, `iat`, `nbf`, `audience`, `issuer` 等字段是否满足配置要求
408 | > iss(issuer):JWT 签发方。
409 | > iat(issued at time):JWT 签发时间。
410 | > sub(subject):JWT 主题。
411 | > aud(audience):JWT 接收方。
412 | > exp(expiration time):JWT 的过期时间。是一个数字的组合,这是因为这个字符串使用的是 Unix 的时间
413 | > nbf(not before time):JWT 生效时间,早于该定义的时间的 JWT 不能被接受处理。
414 | > jti(JWT ID):JWT 唯一标识。
415 |
416 | 如果所有这些都有效,则令牌被认为是正确的,并返回一个用户对象。
417 |
418 | 尽管字段 `exp`,`iat`、`nbf` 是简单的时间戳校验,但只有 `exp` 可以被配置成忽略:
419 |
420 | ```java
421 | jwtAuth.authenticate(
422 | new JsonObject()
423 | .put("token", "BASE64-ENCODED-STRING")
424 | .put("options", new JsonObject()
425 | .put("ignoreExpiration", true)))
426 | .onSuccess(user -> System.out.println("User: " + user.principal()))
427 | .onFailure(err -> {
428 | // 失败!
429 | });
430 | ```
431 |
432 | 为了验证 `aud` 字段需要像以上用例一样传递选项:
433 |
434 | ```java
435 | jwtAuth.authenticate(
436 | new JsonObject()
437 | .put("token", "BASE64-ENCODED-STRING")
438 | .put("options", new JsonObject()
439 | .put("audience", new JsonArray().add("paulo@server.com"))))
440 | .onSuccess(user -> System.out.println("User: " + user.principal()))
441 | .onFailure(err -> {
442 | // 失败!
443 | });
444 | ```
445 |
446 | 验证 issuer 字段:
447 |
448 | ```java
449 | jwtAuth.authenticate(
450 | new JsonObject()
451 | .put("token", "BASE64-ENCODED-STRING")
452 | .put("options", new JsonObject()
453 | .put("issuer", "mycorp.com")))
454 | .onSuccess(user -> System.out.println("User: " + user.principal()))
455 | .onFailure(err -> {
456 | // Failed!
457 | });
458 | ```
459 |
460 | ### 授权 (AuthZ)
461 |
462 | 一旦令牌被解析并且有效,我们就可以使用它来执行授权任务。最简单的方法是验证用户是否具有特定权限。 授权将遵循通用的 `AuthorizationProvider` API。 选择相应的验证服务 API 来产生、验证令牌。
463 |
464 | 目前有两个工厂类:
465 |
466 | - `JWTAuthorization` 根据权限声明确定权限。
467 | - `MicroProfileAuthorization` 令牌根据 MP JWT spec .
468 |
469 | 典型的用法是使用验证器从用户对象中提取权限并执行证明:
470 |
471 | ```java
472 | AuthorizationProvider authz = MicroProfileAuthorization.create();
473 |
474 | authz.getAuthorizations(user)
475 | .onSuccess(v -> {
476 | // 现在我们可以根据需要执行检查
477 | if (PermissionBasedAuthorization.create("create-report").match(user)) {
478 | // 是的,用户可以创建报告
479 | }
480 | });
481 | ```
482 |
483 | 默认情况下验证器会检查 `permissions` 键,但是和其他验证器一样, 可以通过使用 `:` 分隔符将概念拓展到角色,因此可以用 `role:authority` 查找令牌。
484 |
485 | JWT 是个相当自由的格式,并没有强制规定,所以可以将 `permissions` 配置成其他内容, 例如,甚至可以在这样的路径下查找:
486 |
487 | ```java
488 | JsonObject config = new JsonObject()
489 | .put("public-key", "BASE64-ENCODED-PUBLIC_KEY")
490 | // 因为我们正在使用 keycloak JWT 因此我们需要
491 | // 在令牌中设置许可声明
492 | .put("permissionsClaimKey", "realm_access/roles");
493 |
494 | AuthenticationProvider provider =
495 | JWTAuth.create(vertx, new JWTAuthOptions(config));
496 | ```
497 |
498 | 所以在此示例中,我们将 JWT 配置为使用 Keycloak 令牌格式。在这种情况下 `realm_access/roles` 路径下的 claims 会被检查 而不是 `permissions`。
499 |
500 | ### 校验令牌
501 |
502 | 方法 `authenticate` 被调用时,令牌将根据初始化期间提供的 `JWTOptions` 进行 验证。验证步骤如下:
503 |
504 | 1. `ignoreExpiration` (默认关闭) 是关闭的的情况下, 校验令牌的有效期, 将检查字段: `exp`, `iat` 和 `nbf`。 由于各端时间存在一定偏差, 可以配置 `leeway`宽限日期, 应对时间超出而失效的情况。
505 | 2. 如果配置了 `audience`, 那么根据配置检查令牌中的 `aud` 属性, 所以令牌中必须有属性。
506 | 3. 如果配置了 `issuer` ,那么令牌的 `iss` 属性会被检查。
507 |
508 | 这些验证完成后,将返回 JWTUser 对象,该对象包含了配置中对权限声明密文的引用, 这个值在后面验证时会用到。 该值对应于权限检查会用的 json 路径。
509 |
510 | ### 自定义 Token 生成
511 |
512 | 以相同的方式验证令牌,生成是在初始化期间进行初始配置的。
513 |
514 | 生成令牌时,可以提供一个可选的额外参数来控制令牌的生成, 这是一个 `JWTOptions` 对象。 可以使用 `algorithm` 属性来配置令牌签名算法(默认为:HS256)。 在这种情况下,将执行与该算法相对应的密钥的查找并将其用于签名。
515 |
516 | 令牌的 `headers` 属性可以添加额外的信息或者与默认选项合并。
517 |
518 | 有时我们发行的令牌会没有时间戳(例如:在测试、开发过程中),在这种情况下 `noTimestamp` 属性应该被设置成 ture (默认为 false)。 这将表示着令牌中没有 `iat` 字段。
519 |
520 | 令牌的过期时间由 `expiresInSeconds` 属性控制,默认情况下不会过期。 然后可以配置其他控制字段 `audience`,`issuer` 以及 `subject` 并将其加入到令牌元数据中。
521 |
522 | 最后对令牌以正确的格式进行编码并签名。
523 |
--------------------------------------------------------------------------------
/认证和授权/使用 Keycloak 的 Vert.x 简易SSO(单点登录).md:
--------------------------------------------------------------------------------
1 | # 使用 Keycloak 的 Vert.x 简易SSO(单点登录)
2 |
3 | > 原文: https://vertx.io/blog/easy-sso-for-vert-x-with-keycloak/
4 |
5 | 在这篇博文中,您将了解:
6 |
7 | - 如何使用 OpenID Connect 实现单点登录
8 | - 如何使用Keycloak的OpenID Discovery来推断OpenID提供程序配置
9 | - 如何获取用户信息
10 | - 如何检查授权
11 | - 如何使用访问令牌调用受持有者保护的服务
12 | - 如何实现基于表单的注销
13 |
14 | ## 你好博客
15 |
16 | 这是我在 Vert.x 博客上的第一篇文章,我必须承认,到目前为止,我从未在实际项目中使用 Vert.x。“你为什么在这里?”,你可能会问...好吧,我目前有两个主要爱好,学习新事物和使用[Keycloak](https://www.keycloak.org/)保护应用程序。所以几天前,我在YouTube上偶然发现了Deven Phillips的[Vert.x简介视频系列](https://www.youtube.com/watch?v=LsaXy7SRXMY&list=PLkeCJDaCC2ZsnySdg04Aq9D9FpAZY6K5D),我立即被迷住了。Vert.x对我来说是一件新鲜事,所以下一个合乎逻辑的步骤是弄清楚如何使用Keycloak保护Vert.x应用程序。
17 |
18 | 在本例中,我使用 Vert.x 构建了一个小型 Web 应用程序,该应用程序演示如何使用 Keycloak 和 OpenID Connect 实现单点登录 (SSO)、获取有关当前用户的信息、检查角色、调用承载保护服务和正确处理注销。
19 |
20 | ## Keycloak
21 |
22 | Keycloak是一个开源的身份和访问管理解决方案,它为基于OpenID Connect的Singe-Signon提供了支持,等等。我简要地寻找了使用 Keycloak 保护 Vert.x 应用程序的方法,并很快在这篇博客中找到了一个较旧的 Vert.x Keycloak 集成示例。虽然这对初学者来说是一个良好的开端,但该示例包含一些问题,例如:
23 |
24 | - 它使用硬编码的 OpenID 提供程序配置
25 | - 具有非常简单的集成(为了简单起见)
26 | - 未使用用户信息
27 | - 不显示注销功能
28 |
29 | 这不知何故让我有点厌恶,因此,经过一整天的咨询工作后,我坐下来创建了一个基于 [Vert.x OpenID Connect / OAuth2 Support](https://vertx.io/docs/vertx-auth-oauth2/java/) 支持的完整 Keycloak 集成示例。
30 |
31 | 所以让我们开始吧!
32 |
33 | ### Keycloak 设置
34 |
35 | 要使用Keycloak保护Vert.x应用程序,我们当然需要一个Keycloak实例。虽然Keycloak有一个很棒的入门指南,但我想让把所有东西放在一起更容易一些,因此我准备了一个本地的Keycloak docker容器,你可以轻松上手,它带有所有必需的配置。
36 |
37 | 名为 `vertx` 的预配置 Keycloak 领域包含一个用于我们的 Vert.x 网络应用程序的 `demo-client` 和一组用于测试的用户。
38 |
39 | ```bash
40 | docker run \
41 | -it \
42 | --name vertx-keycloak \
43 | --rm \
44 | -e KEYCLOAK_USER=admin \
45 | -e KEYCLOAK_PASSWORD=admin \
46 | -e KEYCLOAK_IMPORT=/tmp/vertx-realm.json \
47 | -v $PWD/vertx-realm.json:/tmp/vertx-realm.json \
48 | -p 8080:8080 \
49 | quay.io/keycloak/keycloak:9.0.0
50 | ```
51 |
52 | ## Vert.x Web App Vert.x 网页应用
53 |
54 | The simple web app consists of a single `Verticle`, runs on `http://localhost:8090` and provides a few routes with protected resources. [You can find the complete example here](https://github.com/thomasdarimont/vertx-playground/blob/master/keycloak-vertx/src/main/java/demo/MainVerticle.java).
55 | 简单的 Web 应用程序由单个 `Verticle` 组成,在 `http://localhost:8090` 上运行,并提供一些具有受保护资源的路由。 您可以在[此处](https://github.com/thomasdarimont/vertx-playground/blob/master/keycloak-vertx/src/main/java/demo/MainVerticle.java))找到完整的示例。
56 |
57 | Web 应用包含以下带有处理程序的路由:
58 |
59 | - `/` - 未受保护的索引页
60 | - `/protected` - 显示问候消息的受保护页面,用户需要登录才能访问此路径下的页面。
61 | - `/protected/user` - 受保护的用户页面,显示有关用户的一些信息。
62 | - `/protected/admin` - 受保护的管理员页面,其中显示有关管理员的一些信息,只有角色为 `admin` 的用户才能访问此页面。
63 | - `/protected/userinfo` - 受保护的用户信息页面,从 Keycloak 中的持有者令牌保护的用户信息端点获取用户信息。
64 | - `/logout` - 受保护的注销资源,用于触发用户注销。
65 |
66 | ### 运行应用
67 |
68 | 要运行应用程序,我们需要通过以下方式构建应用程序:
69 |
70 | ```bash
71 | cd keycloak-vertx
72 | mvn clean package
73 | ```
74 |
75 | This creates a runnable jar, which we can run via:
76 | 这将创建一个可运行的 jar,我们可以通过以下方式运行它:
77 |
78 | ```bash
79 | java -jar target/*.jar
80 | ```
81 |
82 | Note, that you need to start Keycloak, since our app will try to fetch configuration from Keycloak.
83 | 请注意,您需要启动Keycloak,因为我们的应用程序将尝试从Keycloak获取配置。
84 |
85 | If the application is running, just browse to: `http://localhost:8090/`.
86 | 如果应用程序正在运行,只需浏览到: `http://localhost:8090/` 。
87 |
88 | An example interaction with the app can be seen in the following gif:
89 | 与应用程序的交互示例可以在以下 gif 中看到:
90 |
91 |
92 |
93 |
94 |
95 | 
96 |
97 | 
98 |
99 | 
100 |
101 | 
102 |
103 | 
104 |
105 |
106 |
107 | ### Router, SessionStore and CSRF Protection 路由器、会话存储和 CSRF 保护
108 |
109 | We start the configuration of our web app by creating a `Router` where we can add custom handler functions for our routes. To properly handle the authentication state we need to create a `SessionStore` and attach it to the `Router`. The `SessionStore` is used by our OAuth2/OpenID Connect infrastructure to associate authentication information with a session. By the way, the `SessionStore` can also be clustered if you need to distribute the server-side state.
110 | 我们通过创建一个 `Router` 开始配置 Web 应用程序,我们可以在其中为路由添加自定义处理程序函数。要正确处理身份验证状态,我们需要创建一个 `SessionStore` 并将其附加到 `Router` .我们的 OAuth2/OpenID Connect 基础架构使用 `SessionStore` 将身份验证信息与会话相关联。顺便说一下,如果需要分发服务器端状态, `SessionStore` 也可以群集。
111 |
112 | Note that if you want to keep your server stateless but still want to support clustering, then you could provide your own implementation of a `SessionStore` which stores the session information as an encrypted cookie on the Client.
113 | 请注意,如果要使服务器保持无状态但仍希望支持群集,则可以提供自己的 `SessionStore` 实现,该实现将会话信息作为加密 cookie 存储在客户端上。
114 |
115 | ```java
116 | Router router = Router.router(vertx);
117 |
118 | // Store session information on the server side
119 | SessionStore sessionStore = LocalSessionStore.create(vertx);
120 | SessionHandler sessionHandler = SessionHandler.create(sessionStore);
121 | router.route().handler(sessionHandler);
122 | ```
123 |
124 | In order to protected against CSRF attacks it is good practice to protect HTML forms with a CSRF token. We need this for our logout form that we’ll see later.
125 | 为了防止CSRF攻击,最好使用CSRF令牌保护HTML表单。我们的注销表单需要这个,稍后会看到。
126 |
127 | To do this we configure a `CSRFHandler` and add it to our `Router`:
128 | 为此,我们配置了一个 `CSRFHandler` 并将其添加到我们的 `Router` 中:
129 |
130 | ```java
131 | // CSRF handler setup required for logout form
132 | String csrfSecret = "zwiebelfische";
133 | CSRFHandler csrfHandler = CSRFHandler.create(csrfSecret);
134 | router.route().handler(ctx -> {
135 | // Ensures that the csrf token request parameter is available for the CsrfHandler
136 | // after the logout form was submitted.
137 | // See "Handling HTML forms" https://vertx.io/docs/vertx-core/java/#_handling_requests
138 | ctx.request().setExpectMultipart(true);
139 | ctx.request().endHandler(v -> csrfHandler.handle(ctx));
140 | }
141 | );
142 | ```
143 |
144 | ### Keycloak Setup via OpenID Connect Discovery 通过 OpenID 连接发现设置密钥斗篷
145 |
146 | Our app is registered as a confidential OpenID Connect client with Authorization Code Flow in Keycloak, thus we need to configure `client_id` and `client_secret`. Confidential clients are typically used for server-side web applications, where one can securely store the `client_secret`. You can find out more about[The different Client Access Types](https://www.keycloak.org/docs/latest/server_admin/index.html#_access-type) in the Keycloak documentation.
147 | 我们的应用程序注册为机密的OpenID Connect客户端,在Keycloak中使用授权代码流,因此我们需要配置 `client_id` 和 `client_secret` 。机密客户端通常用于服务器端 Web 应用程序,其中可以安全地存储 `client_secret` 。您可以在 Keycloak 文档中找到有关不同客户端访问类型的更多信息。
148 |
149 | Since we don’t want to configure things like OAuth2 / OpenID Connect Endpoints ourselves, we use Keycloak’s OpenID Connect discovery endpoint to infer the necessary Oauth2 / OpenID Connect endpoint URLs.
150 | 由于我们不想自己配置OAuth2 / OpenID Connect Endpoint之类的东西,我们使用Keycloak的OpenID Connect发现端点来推断必要的Oauth2 / OpenID Connect端点URL。
151 |
152 | ```java
153 | String hostname = System.getProperty("http.host", "localhost");
154 | int port = Integer.getInteger("http.port", 8090);
155 | String baseUrl = String.format("http://%s:%d", hostname, port);
156 | String oauthCallbackPath = "/callback";
157 |
158 | OAuth2ClientOptions clientOptions = new OAuth2ClientOptions()
159 | .setFlow(OAuth2FlowType.AUTH_CODE)
160 | .setSite(System.getProperty("oauth2.issuer", "http://localhost:8080/auth/realms/vertx"))
161 | .setClientID(System.getProperty("oauth2.client_id", "demo-client"))
162 | .setClientSecret(System.getProperty("oauth2.client_secret", "1f88bd14-7e7f-45e7-be27-d680da6e48d8"));
163 |
164 | KeycloakAuth.discover(vertx, clientOptions, asyncResult -> {
165 |
166 | OAuth2Auth oauth2Auth = asyncResult.result();
167 |
168 | if (oauth2Auth == null) {
169 | throw new RuntimeException("Could not configure Keycloak integration via OpenID Connect Discovery Endpoint. Is Keycloak running?");
170 | }
171 |
172 | AuthHandler oauth2 = OAuth2AuthHandler.create(oauth2Auth, baseUrl + oauthCallbackPath)
173 | .setupCallback(router.get(oauthCallbackPath))
174 | // Additional scopes: openid for OpenID Connect
175 | .addAuthority("openid");
176 |
177 | // session handler needs access to the authenticated user, otherwise we get an infinite redirect loop
178 | sessionHandler.setAuthProvider(oauth2Auth);
179 |
180 | // protect resources beneath /protected/* with oauth2 handler
181 | router.route("/protected/*").handler(oauth2);
182 |
183 | // configure route handlers
184 | configureRoutes(router, webClient, oauth2Auth);
185 | });
186 |
187 | getVertx().createHttpServer().requestHandler(router).listen(port);
188 | ```
189 |
190 | ### Route handlers 路由处理程序
191 |
192 | We configure our route handlers via `configureRoutes`:
193 | 我们通过 `configureRoutes` 配置路由处理程序:
194 |
195 | ```java
196 | private void configureRoutes(Router router, WebClient webClient, OAuth2Auth oauth2Auth) {
197 |
198 | router.get("/").handler(this::handleIndex);
199 |
200 | router.get("/protected").handler(this::handleGreet);
201 | router.get("/protected/user").handler(this::handleUserPage);
202 | router.get("/protected/admin").handler(this::handleAdminPage);
203 |
204 | // extract discovered userinfo endpoint url
205 | String userInfoUrl = ((OAuth2AuthProviderImpl)oauth2Auth).getConfig().getUserInfoPath();
206 | router.get("/protected/userinfo").handler(createUserInfoHandler(webClient, userInfoUrl));
207 |
208 | router.post("/logout").handler(this::handleLogout);
209 | }
210 | ```
211 |
212 | The index handler exposes an unprotected resource:
213 | 索引处理程序公开未受保护的资源:
214 |
215 | ```java
216 | private void handleIndex(RoutingContext ctx) {
217 | respondWithOk(ctx, "text/html", "Welcome to Vert.x Keycloak Example Protected ");
218 | }
219 | ```
220 |
221 | ### Extract User Information from the OpenID Connect ID Token 从 OpenID 连接 ID 令牌中提取用户信息
222 |
223 | Our app exposes a simple greeting page which shows some information about the user and provides links to other pages.
224 | 我们的应用程序公开了一个简单的问候页面,该页面显示有关用户的一些信息,并提供指向其他页面的链接。
225 |
226 | The user greeting handler is protected by the Keycloak OAuth2 / OpenID Connect integration. To show information about the current user, we first need to call the `ctx.user()` method to get an user object we can work with. To access the OAuth2 token information, we need to cast it to `OAuth2TokenImpl`.
227 | 用户问候处理程序受Keycloak OAuth2 / OpenID Connect集成的保护。为了显示有关当前用户的信息,我们首先需要调用 `ctx.user()` 方法来获取我们可以处理的用户对象。要访问 OAuth2 令牌信息,我们需要将其转换为 `OAuth2TokenImpl` 。
228 |
229 | We can extract the user information like the username from the `IDToken` exposed by the user object via `user.idToken().getString("preferred_username")`. Note, there are many more claims like (name, email, givenanme, familyname etc.) available. The [OpenID Connect Core Specification](https://openid.net/specs/openid-connect-core-1_0.html#Claims) contains a list of available claims.
230 | 我们可以从用户对象通过 `user.idToken().getString("preferred_username")` 公开的 `IDToken` 中提取用户名等用户信息。请注意,还有更多可用的声明,例如(姓名,电子邮件,givenanme,姓氏等)。OpenID Connect Core 规范包含可用声明的列表。
231 |
232 | We also generate a list with links to the other pages which are supported:
233 | 我们还生成一个列表,其中包含指向支持的其他页面的链接:
234 |
235 | ```java
236 | private void handleGreet(RoutingContext ctx) {
237 |
238 | OAuth2TokenImpl oAuth2Token = (OAuth2TokenImpl) ctx.user();
239 |
240 | String username = oAuth2Token.idToken().getString("preferred_username");
241 |
242 | String greeting = String.format("Hi %s @%s ", username, Instant.now());
247 |
248 | String logoutForm = createLogoutForm(ctx);
249 |
250 | respondWithOk(ctx, "text/html", greeting + logoutForm);
251 | }
252 | ```
253 |
254 | The user page handler shows information about the current user:
255 | 用户页面处理程序显示有关当前用户的信息:
256 |
257 | ```java
258 | private void handleUserPage(RoutingContext ctx) {
259 |
260 | OAuth2TokenImpl user = (OAuth2TokenImpl) ctx.user();
261 |
262 | String username = user.idToken().getString("preferred_username");
263 | String displayName = oAuth2Token.idToken().getString("name");
264 |
265 | String content = String.format("User Page: %s (%s) @%s Protected Area ",
266 | username, displayName, Instant.now());
267 | respondWithOk(ctx, "text/html", content);
268 | }
269 | ```
270 |
271 | ### Authorization: Checking for Required Roles 授权:检查所需角色
272 |
273 | Our app exposes a simple admin page which shows some information for admins, which should only be visible for admins. Thus we require that users must have the `admin` realm role in Keycloak to be able to access the admin page.
274 | 我们的应用程序公开了一个简单的管理页面,其中显示了一些管理员信息,这些信息应该只对管理员可见。因此,我们要求用户必须在 Keycloak 中具有 `admin` 领域角色才能访问管理页面。
275 |
276 | This is done via a call to `user.isAuthorized("realm:admin", cb)`. The handler function `cb` exposes the result of the authorization check via the `AsyncResult res`. If the current user has the `admin` role then the result is `true` otherwise `false`:
277 | 这是通过调用 `user.isAuthorized("realm:admin", cb)` 来完成的。处理程序函数 `cb` 通过 `AsyncResult res` 公开授权检查的结果。如果当前用户具有 `admin` 角色,则结果为 `true` ,否则为 `false` :
278 |
279 | ```java
280 | private void handleAdminPage(RoutingContext ctx) {
281 |
282 | OAuth2TokenImpl user = (OAuth2TokenImpl) ctx.user();
283 |
284 | // check for realm-role "admin"
285 | user.isAuthorized("realm:admin", res -> {
286 |
287 | if (!res.succeeded() || !res.result()) {
288 | respondWith(ctx, 403, "text/html", "Forbidden ");
289 | return;
290 | }
291 |
292 | String username = user.idToken().getString("preferred_username");
293 |
294 | String content = String.format("Admin Page: %s @%s Protected Area ",
295 | username, Instant.now());
296 | respondWithOk(ctx, "text/html", content);
297 | });
298 | }
299 | ```
300 |
301 | #### Call Services protected with Bearer Token 使用持有者令牌保护的呼叫服务
302 |
303 | Often we need to call other services from our web app that are protected via Bearer Authentication. This means that we need a valid `access token` to access a resource provided on another server.
304 | 通常,我们需要从我们的 Web 应用调用通过持有者身份验证保护的其他服务。这意味着我们需要一个有效的 `access token` 来访问另一台服务器上提供的资源。
305 |
306 | To demonstrate this we use Keycloak’s `/userinfo` endpoint as a straw man to demonstrate backend calls with a bearer token.
307 | 为了演示这一点,我们使用 Keycloak 的 `/userinfo` 端点作为稻草人来演示带有持有者令牌的后端调用。
308 |
309 | We can obtain the current valid `access token` via `user.opaqueAccessToken()`. Since we use a `WebClient` to call the protected endpoint, we need to pass the `access token` via the `Authorization` header by calling `bearerTokenAuthentication(user.opaqueAccessToken())` in the current `HttpRequest` object:
310 | 我们可以通过 `user.opaqueAccessToken()` 获取当前有效的 `access token` 。由于我们使用 `WebClient` 调用受保护的终结点,因此我们需要通过在当前 `HttpRequest` 对象中调用 `bearerTokenAuthentication(user.opaqueAccessToken())` 来通过 `Authorization` 标头传递 `access token` :
311 |
312 | ```java
313 | private Handler createUserInfoHandler(WebClient webClient, String userInfoUrl) {
314 |
315 | return (RoutingContext ctx) -> {
316 |
317 | OAuth2TokenImpl user = (OAuth2TokenImpl) ctx.user();
318 |
319 | URI userInfoEndpointUri = URI.create(userInfoUrl);
320 | webClient
321 | .get(userInfoEndpointUri.getPort(), userInfoEndpointUri.getHost(), userInfoEndpointUri.getPath())
322 | // use the access token for calls to other services protected via JWT Bearer authentication
323 | .bearerTokenAuthentication(user.opaqueAccessToken())
324 | .as(BodyCodec.jsonObject())
325 | .send(ar -> {
326 |
327 | if (!ar.succeeded()) {
328 | respondWith(ctx, 500, "application/json", "{}");
329 | return;
330 | }
331 |
332 | JsonObject body = ar.result().body();
333 | respondWithOk(ctx, "application/json", body.encode());
334 | });
335 | };
336 | }
337 | ```
338 |
339 | ### Handle logout 处理注销
340 |
341 | Now that we got a working SSO login with authorization, it would be great if we would allow users to logout again. To do this we can leverage the built-in OpenID Connect logout functionality which can be called via `oAuth2Token.logout(cb)`.
342 | 现在我们获得了具有授权的工作 SSO 登录,如果我们允许用户再次注销,那就太好了。为此,我们可以利用内置的OpenID Connect注销功能,该功能可以通过 `oAuth2Token.logout(cb)` 调用。
343 |
344 | The handler function `cb` exposes the result of the logout action via the `AsyncResult res`. If the logout was successfull we destory our session via `ctx.session().destroy()` and redirect the user to the index page.
345 | 处理程序函数 `cb` 通过 `AsyncResult res` 公开注销操作的结果。如果注销成功,我们通过 `ctx.session().destroy()` 取消会话,并将用户重定向到索引页面。
346 |
347 | The logout form is generated via the `createLogoutForm` method.
348 | 注销表单通过 `createLogoutForm` 方法生成。
349 |
350 | As mentioned earlier, we need to protect our logout form with a CSRF token to prevent [CSRF attacks](https://owasp.org/www-community/attacks/csrf).
351 | 如前所述,我们需要使用 CSRF 令牌保护我们的注销表单以防止 CSRF 攻击 .
352 |
353 | Note: If we had endpoints that would accept data sent to the server, then we’d need to guard those endpoints with an CSRF token as well.
354 | 注意:如果我们的端点接受发送到服务器的数据,那么我们也需要使用 CSRF 令牌保护这些端点。
355 |
356 | We need to obtain the generated `CSRFToken` and render it into a hidden form input field that’s transfered via HTTP POST when the logout form is submitted:
357 | 我们需要获取生成的 `CSRFToken` 并将其呈现为一个隐藏的表单输入字段,该字段在提交注销表单时通过 HTTP POST 传输:
358 |
359 | ```java
360 | private void handleLogout(RoutingContext ctx) {
361 |
362 | OAuth2TokenImpl oAuth2Token = (OAuth2TokenImpl) ctx.user();
363 | oAuth2Token.logout(res -> {
364 |
365 | if (!res.succeeded()) {
366 | // the user might not have been logged out, to know why:
367 | respondWith(ctx, 500, "text/html", String.format("Logout failed %s ", res.cause()));
368 | return;
369 | }
370 |
371 | ctx.session().destroy();
372 | ctx.response().putHeader("location", "/?logout=true").setStatusCode(302).end();
373 | });
374 | }
375 |
376 | private String createLogoutForm(RoutingContext ctx) {
377 |
378 | String csrfToken = ctx.get(CSRFHandler.DEFAULT_HEADER_NAME);
379 |
380 | return ""
381 | + String.format(" ", CSRFHandler.DEFAULT_HEADER_NAME, csrfToken)
382 | + "Logout ";
383 | }
384 | ```
385 |
386 | Some additional plumbing: 一些额外的管道:
387 |
388 | ```java
389 | private void respondWithOk(RoutingContext ctx, String contentType, String content) {
390 | respondWith(ctx, 200, contentType, content);
391 | }
392 |
393 | private void respondWith(RoutingContext ctx, int statusCode, String contentType, String content) {
394 | ctx.request().response() //
395 | .putHeader("content-type", contentType) //
396 | .setStatusCode(statusCode)
397 | .end(content);
398 | }
399 | ```
400 |
401 | ## More examples 更多例子
402 |
403 | This concludes the Keycloak integration example.
404 | Keycloak 集成示例到此结束。
405 |
406 | Check out the complete example in [keycloak-vertx Examples Repo](https://github.com/thomasdarimont/vertx-playground/tree/master/keycloak-vertx).
407 | 查看 keycloak-vertx 示例存储库中的完整示例。
408 |
409 | Thank you for your time, stay tuned for more updates! If you want to learn more about Keycloak, feel free to reach out to me. You can find me via [thomasdarimont on twitter](https://twitter.com/thomasdarimont).
410 | 感谢您抽出宝贵时间,请继续关注更多更新!如果您想了解有关Keycloak的更多信息,请随时与我联系。你可以通过twitter上的thomasdarimont找到我。
411 |
412 | Happy Hacking! 祝黑客愉快!
--------------------------------------------------------------------------------