├── assets ├── logo-sm.png ├── jprofiler-logo.png ├── Button-Built-on-CB-1-grey.png ├── eclipse_logo_grey_small.png ├── image-20220218092424509.png ├── image-20220218092459401.png └── we-are-reactive-black-right.png ├── 认证和授权 ├── 1.assets │ ├── image-20230217134340034.png │ ├── image-20230217134407502.png │ ├── image-20230217134503420.png │ ├── image-20230217134605835.png │ └── image-20230217134651882.png ├── vx.json ├── JWT 认证与授权.md └── 使用 Keycloak 的 Vert.x 简易SSO(单点登录).md ├── Web ├── SockJS-client简介.assets │ ├── Browserstack-logo@2x.png │ └── image-20230201211424124.png ├── 使用Websockets和Vert.x进行实时竞价.assets │ └── image-20230202111820409.png ├── Vert.x 4 Web-Server Manual中文版.assets │ └── image-20230210111417229.png ├── vx.json ├── Vert.x EventBus Bridge Client中文版.md ├── 使用Websockets和Vert.x进行实时竞价.md └── SockJS-client简介.md ├── Vert.x和Reactive简介.assets ├── image-20220914142218731.png ├── image-20220914142321493.png ├── image-20220914142409910.png ├── image-20220914142456499.png ├── image-20220914142529037.png ├── image-20220914142600384.png └── image-20220914142646019.png ├── Vertx4_Core_Manual.assets ├── image-20230210111712808.png ├── image-20230210112104707.png └── image-20230227140451027.png ├── Futures_and_Promises.assets ├── image-20220225164145778.png ├── image-20220225164809902.png ├── image-20220225165322925.png ├── image-20220225165814701.png ├── image-20220225170605494.png ├── image-20220225171136849.png ├── image-20220225171553045.png ├── image-20220225171946824.png ├── image-20220225172220399.png └── vx.json ├── .gitignore ├── Sql ├── vx.json ├── Vert.x-Redis.md └── SQL Client Templates中文版.md ├── Context_API_简介.md ├── Vert.x和Reactive简介.md ├── vx.json ├── Cluster ├── Zookeeper 集群管理器.md └── Hazelcast 集群管理器.md ├── README.md ├── 监控 └── Vert.x 健康检查.md ├── Futures 和 Promises.md ├── 云原生与12要素(Cloud-Native & 12-Factor).md ├── LICENSE ├── Vert.x 4 JUnit 5 integration中文版.md └── Vert.x Service Proxy Manual中文版.md /assets/logo-sm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjw465150/Vert.x-Core-Manual/master/assets/logo-sm.png -------------------------------------------------------------------------------- /assets/jprofiler-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjw465150/Vert.x-Core-Manual/master/assets/jprofiler-logo.png -------------------------------------------------------------------------------- /assets/Button-Built-on-CB-1-grey.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjw465150/Vert.x-Core-Manual/master/assets/Button-Built-on-CB-1-grey.png -------------------------------------------------------------------------------- /assets/eclipse_logo_grey_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjw465150/Vert.x-Core-Manual/master/assets/eclipse_logo_grey_small.png -------------------------------------------------------------------------------- /assets/image-20220218092424509.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjw465150/Vert.x-Core-Manual/master/assets/image-20220218092424509.png -------------------------------------------------------------------------------- /assets/image-20220218092459401.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjw465150/Vert.x-Core-Manual/master/assets/image-20220218092459401.png -------------------------------------------------------------------------------- /assets/we-are-reactive-black-right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjw465150/Vert.x-Core-Manual/master/assets/we-are-reactive-black-right.png -------------------------------------------------------------------------------- /认证和授权/1.assets/image-20230217134340034.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjw465150/Vert.x-Core-Manual/master/认证和授权/1.assets/image-20230217134340034.png -------------------------------------------------------------------------------- /认证和授权/1.assets/image-20230217134407502.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjw465150/Vert.x-Core-Manual/master/认证和授权/1.assets/image-20230217134407502.png -------------------------------------------------------------------------------- /认证和授权/1.assets/image-20230217134503420.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjw465150/Vert.x-Core-Manual/master/认证和授权/1.assets/image-20230217134503420.png -------------------------------------------------------------------------------- /认证和授权/1.assets/image-20230217134605835.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjw465150/Vert.x-Core-Manual/master/认证和授权/1.assets/image-20230217134605835.png -------------------------------------------------------------------------------- /认证和授权/1.assets/image-20230217134651882.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjw465150/Vert.x-Core-Manual/master/认证和授权/1.assets/image-20230217134651882.png -------------------------------------------------------------------------------- /Web/SockJS-client简介.assets/Browserstack-logo@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjw465150/Vert.x-Core-Manual/master/Web/SockJS-client简介.assets/Browserstack-logo@2x.png -------------------------------------------------------------------------------- /Vert.x和Reactive简介.assets/image-20220914142218731.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjw465150/Vert.x-Core-Manual/master/Vert.x和Reactive简介.assets/image-20220914142218731.png -------------------------------------------------------------------------------- /Vert.x和Reactive简介.assets/image-20220914142321493.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjw465150/Vert.x-Core-Manual/master/Vert.x和Reactive简介.assets/image-20220914142321493.png -------------------------------------------------------------------------------- /Vert.x和Reactive简介.assets/image-20220914142409910.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjw465150/Vert.x-Core-Manual/master/Vert.x和Reactive简介.assets/image-20220914142409910.png -------------------------------------------------------------------------------- /Vert.x和Reactive简介.assets/image-20220914142456499.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjw465150/Vert.x-Core-Manual/master/Vert.x和Reactive简介.assets/image-20220914142456499.png -------------------------------------------------------------------------------- /Vert.x和Reactive简介.assets/image-20220914142529037.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjw465150/Vert.x-Core-Manual/master/Vert.x和Reactive简介.assets/image-20220914142529037.png -------------------------------------------------------------------------------- /Vert.x和Reactive简介.assets/image-20220914142600384.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjw465150/Vert.x-Core-Manual/master/Vert.x和Reactive简介.assets/image-20220914142600384.png -------------------------------------------------------------------------------- /Vert.x和Reactive简介.assets/image-20220914142646019.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjw465150/Vert.x-Core-Manual/master/Vert.x和Reactive简介.assets/image-20220914142646019.png -------------------------------------------------------------------------------- /Vertx4_Core_Manual.assets/image-20230210111712808.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjw465150/Vert.x-Core-Manual/master/Vertx4_Core_Manual.assets/image-20230210111712808.png -------------------------------------------------------------------------------- /Vertx4_Core_Manual.assets/image-20230210112104707.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjw465150/Vert.x-Core-Manual/master/Vertx4_Core_Manual.assets/image-20230210112104707.png -------------------------------------------------------------------------------- /Vertx4_Core_Manual.assets/image-20230227140451027.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjw465150/Vert.x-Core-Manual/master/Vertx4_Core_Manual.assets/image-20230227140451027.png -------------------------------------------------------------------------------- /Futures_and_Promises.assets/image-20220225164145778.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjw465150/Vert.x-Core-Manual/master/Futures_and_Promises.assets/image-20220225164145778.png -------------------------------------------------------------------------------- /Futures_and_Promises.assets/image-20220225164809902.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjw465150/Vert.x-Core-Manual/master/Futures_and_Promises.assets/image-20220225164809902.png -------------------------------------------------------------------------------- /Futures_and_Promises.assets/image-20220225165322925.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjw465150/Vert.x-Core-Manual/master/Futures_and_Promises.assets/image-20220225165322925.png -------------------------------------------------------------------------------- /Futures_and_Promises.assets/image-20220225165814701.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjw465150/Vert.x-Core-Manual/master/Futures_and_Promises.assets/image-20220225165814701.png -------------------------------------------------------------------------------- /Futures_and_Promises.assets/image-20220225170605494.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjw465150/Vert.x-Core-Manual/master/Futures_and_Promises.assets/image-20220225170605494.png -------------------------------------------------------------------------------- /Futures_and_Promises.assets/image-20220225171136849.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjw465150/Vert.x-Core-Manual/master/Futures_and_Promises.assets/image-20220225171136849.png -------------------------------------------------------------------------------- /Futures_and_Promises.assets/image-20220225171553045.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjw465150/Vert.x-Core-Manual/master/Futures_and_Promises.assets/image-20220225171553045.png -------------------------------------------------------------------------------- /Futures_and_Promises.assets/image-20220225171946824.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjw465150/Vert.x-Core-Manual/master/Futures_and_Promises.assets/image-20220225171946824.png -------------------------------------------------------------------------------- /Futures_and_Promises.assets/image-20220225172220399.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjw465150/Vert.x-Core-Manual/master/Futures_and_Promises.assets/image-20220225172220399.png -------------------------------------------------------------------------------- /Web/SockJS-client简介.assets/image-20230201211424124.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjw465150/Vert.x-Core-Manual/master/Web/SockJS-client简介.assets/image-20230201211424124.png -------------------------------------------------------------------------------- /Web/使用Websockets和Vert.x进行实时竞价.assets/image-20230202111820409.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjw465150/Vert.x-Core-Manual/master/Web/使用Websockets和Vert.x进行实时竞价.assets/image-20230202111820409.png -------------------------------------------------------------------------------- /Web/Vert.x 4 Web-Server Manual中文版.assets/image-20230210111417229.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjw465150/Vert.x-Core-Manual/master/Web/Vert.x 4 Web-Server Manual中文版.assets/image-20230210111417229.png -------------------------------------------------------------------------------- /Futures_and_Promises.assets/vx.json: -------------------------------------------------------------------------------- 1 | { 2 | "created_time": "2022-04-11T01:15:50Z", 3 | "files": [ 4 | ], 5 | "folders": [ 6 | ], 7 | "id": "120", 8 | "modified_time": "2022-04-11T01:15:50Z", 9 | "signature": "66891472839052", 10 | "version": 3 11 | } 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *.bak 3 | *.off 4 | *.old 5 | .DS_Store 6 | *.pdf 7 | 8 | /_book/ 9 | /node_modules/ 10 | 11 | /.project 12 | vx.json 13 | Sql/vx.json 14 | 15 | # IDE support files 16 | /.classpath 17 | /.launch 18 | /.project 19 | /.settings 20 | /*.launch 21 | /*.tmproj 22 | /ivy* 23 | /eclipse 24 | -------------------------------------------------------------------------------- /Sql/vx.json: -------------------------------------------------------------------------------- 1 | { 2 | "created_time": "2022-07-13T09:29:25Z", 3 | "files": [ 4 | { 5 | "attachment_folder": "", 6 | "created_time": "2022-07-13T09:29:49Z", 7 | "id": "5435", 8 | "modified_time": "2022-07-13T09:29:49Z", 9 | "name": "Reactive MySQL Client中文版.md", 10 | "signature": "79316818759821", 11 | "tags": [ 12 | ] 13 | }, 14 | { 15 | "attachment_folder": "", 16 | "created_time": "2023-02-07T02:43:27Z", 17 | "id": "5485", 18 | "modified_time": "2023-02-07T02:43:27Z", 19 | "name": "SQL Client Templates中文版.md", 20 | "signature": "49299310361295", 21 | "tags": [ 22 | ] 23 | }, 24 | { 25 | "attachment_folder": "", 26 | "created_time": "2023-02-16T09:36:13Z", 27 | "id": "5496", 28 | "modified_time": "2023-02-16T09:36:13Z", 29 | "name": "Vert.x-Redis.md", 30 | "signature": "177770199309", 31 | "tags": [ 32 | ] 33 | } 34 | ], 35 | "folders": [ 36 | ], 37 | "id": "5434", 38 | "modified_time": "2022-07-13T09:29:25Z", 39 | "signature": "177751363701", 40 | "version": 3 41 | } 42 | -------------------------------------------------------------------------------- /认证和授权/vx.json: -------------------------------------------------------------------------------- 1 | { 2 | "created_time": "2022-07-08T08:31:43Z", 3 | "files": [ 4 | { 5 | "attachment_folder": "", 6 | "created_time": "2022-07-08T08:31:51Z", 7 | "id": "5428", 8 | "modified_time": "2022-07-08T08:31:51Z", 9 | "name": "通用认证和授权.md", 10 | "signature": "27205980121975", 11 | "tags": [ 12 | ] 13 | }, 14 | { 15 | "attachment_folder": "", 16 | "created_time": "2023-02-15T07:52:57Z", 17 | "id": "5495", 18 | "modified_time": "2023-02-18T14:34:01Z", 19 | "name": "JWT 认证与授权.md", 20 | "signature": "177770106713", 21 | "tags": [ 22 | ] 23 | }, 24 | { 25 | "attachment_folder": "", 26 | "created_time": "2023-02-17T04:58:05Z", 27 | "id": "5497", 28 | "modified_time": "2023-03-03T05:34:43Z", 29 | "name": "使用 Keycloak 对 Vertx 进行 JWT 授权.md", 30 | "signature": "177770269021", 31 | "tags": [ 32 | ] 33 | }, 34 | { 35 | "attachment_folder": "", 36 | "created_time": "2023-03-03T05:22:57Z", 37 | "id": "5505", 38 | "modified_time": "2023-03-03T05:22:57Z", 39 | "name": "使用 Keycloak 的 Vert.x 简易SSO(单点登录).md", 40 | "signature": "79316838876209", 41 | "tags": [ 42 | ] 43 | } 44 | ], 45 | "folders": [ 46 | ], 47 | "id": "5427", 48 | "modified_time": "2022-07-08T08:31:43Z", 49 | "signature": "79316818324335", 50 | "version": 3 51 | } 52 | -------------------------------------------------------------------------------- /Web/vx.json: -------------------------------------------------------------------------------- 1 | { 2 | "created_time": "2022-06-08T04:51:06Z", 3 | "files": [ 4 | { 5 | "attachment_folder": "", 6 | "created_time": "2022-06-08T04:51:10Z", 7 | "id": "5420", 8 | "modified_time": "2022-06-08T07:42:12Z", 9 | "name": "Vert.x 4 Web-Client Manual中文版.md", 10 | "signature": "79316815719102", 11 | "tags": [ 12 | ] 13 | }, 14 | { 15 | "attachment_folder": "", 16 | "created_time": "2022-07-08T03:03:49Z", 17 | "id": "5426", 18 | "modified_time": "2023-02-13T01:23:31Z", 19 | "name": "Vert.x 4 Web-Server Manual中文版.md", 20 | "signature": "177750908565", 21 | "tags": [ 22 | ] 23 | }, 24 | { 25 | "attachment_folder": "", 26 | "created_time": "2023-02-03T02:51:27Z", 27 | "id": "5476", 28 | "modified_time": "2023-02-12T03:55:43Z", 29 | "name": "Vert.x EventBus Bridge Client中文版.md", 30 | "signature": "177769051823", 31 | "tags": [ 32 | ] 33 | }, 34 | { 35 | "attachment_folder": "", 36 | "created_time": "2023-02-03T02:53:45Z", 37 | "id": "5475", 38 | "modified_time": "2023-02-03T02:53:45Z", 39 | "name": "使用Websockets和Vert.x进行实时竞价.md", 40 | "signature": "79316836448057", 41 | "tags": [ 42 | ] 43 | }, 44 | { 45 | "attachment_folder": "", 46 | "created_time": "2023-02-06T07:32:23Z", 47 | "id": "5478", 48 | "modified_time": "2023-02-13T08:40:09Z", 49 | "name": "SockJS-client简介.md", 50 | "signature": "177769327879", 51 | "tags": [ 52 | ] 53 | }, 54 | { 55 | "attachment_folder": "", 56 | "created_time": "2023-02-13T03:53:27Z", 57 | "id": "5493", 58 | "modified_time": "2023-02-13T03:53:29Z", 59 | "name": "Configuring HAProxy.md", 60 | "signature": "177769919543", 61 | "tags": [ 62 | ] 63 | } 64 | ], 65 | "folders": [ 66 | ], 67 | "id": "5419", 68 | "modified_time": "2022-06-08T04:51:06Z", 69 | "signature": "177748323002", 70 | "version": 3 71 | } 72 | -------------------------------------------------------------------------------- /Context_API_简介.md: -------------------------------------------------------------------------------- 1 | # Context_API_简介 2 | 3 | ## `io.vertx.core.Context` 4 | 5 | The execution context of a [`Handler`](https://vertx.io/docs/apidocs/io/vertx/core/Handler.html) execution. 6 | 处理程序的执行上下文. 7 | 8 | When Vert.x provides an event to a handler or calls the start or stop methods of a `Verticle`, the execution is associated with a `Context`. 9 | 当Vert.x向处理程序提供事件或调用 `Verticle` 的start或stop方法时,执行与 `Context`关联。 10 | 11 | Usually a context is an *event-loop context* and is tied to a specific event loop thread. So executions for that context always occur on that exact same event loop thread. 12 | 通常,上下文是*事件循环上下文*,并绑定到特定的事件循环线程。因此,该上下文的执行始终发生在完全相同的事件循环线程上。 13 | 14 | In the case of worker verticles and running inline blocking code a worker context will be associated with the execution which will use a thread from the worker thread pool. 15 | 在工作线程和运行内联阻塞代码的情况下,工作线程上下文将与执行相关联,该执行将使用工作线程池中的线程。 16 | 17 | When a handler is set by a thread associated with a specific context, the Vert.x will guarantee that when that handler is executed, that execution will be associated with the same context. 18 | 当处理程序由与特定上下文关联的线程设置时,Vert.x将保证当执行该处理程序时,该执行将与相同的上下文关联。 19 | 20 | If a handler is set by a thread not associated with a context (i.e. a non Vert.x thread). Then a new context will be created for that handler. 21 | 如果处理程序是由与上下文不关联的线程设置的(即非Vert.x线程)。 然后将为该处理程序创建一个新的上下文。 22 | 23 | In other words, a context is propagated. 24 | 换句话说,上下文被传播。 25 | 26 | This means that when a verticle is deployed, any handlers it sets will be associated with the same context - the context of the verticle. 27 | 这意味着,当部署一个 verticle 时,其设置的任何处理程序都将与相同的上下文相关联 - verticle 的上下文。 28 | 29 | This means (in the case of a standard verticle) that the verticle code will always be executed with the exact same thread, so you don't have to worry about multi-threaded acccess to the verticle state and you can code your application as single threaded. 30 | 这意味着(在标准verticle的情况下), verticle代码将始终使用完全相同的线程执行,因此您不必担心对verticle状态的多线程访问,并且可以将应用程序编码为单线程。 31 | 32 | This class also allows arbitrary data to be `put(java.lang.Object, java.lang.Object)` and `get(java.lang.Object)` on the context so it can be shared easily amongst different handlers of, for example, a verticle instance. 33 | 这个类还允许将任意数据 `put(java.lang。Object, java.lang.Object)` 和 `get(java.lang.Object)` 放在上下文上,因此它可以在不同的处理程序之间轻松共享,例如,一个verticle实例。 34 | 35 | This class also provides `runOnContext(io.vertx.core.Handler)` which allows an action to be executed asynchronously using the same context. 36 | 这个类还提供了 `runOnContext(io. vertex .core. handler)` ,它允许使用相同的上下文异步执行操作。 37 | -------------------------------------------------------------------------------- /Vert.x和Reactive简介.md: -------------------------------------------------------------------------------- 1 | # Vert.x和Reactive简介 2 | 3 | Eclipse Vert.x 是一个用于在 JVM 上构建 **reactive** 应用程序的工具包。 **反应式**应用程序既可以随着工作负载的增长而**可扩展(scal­able)**,又可以在出现故障时**可恢复(re­silient)**。 反应式应用程序是**瞬时响应的**,因为它通过有效利用系统资源和保护自己免受错误来控制延迟。 4 | 5 | ![](Vert.x和Reactive简介.assets/image-20220914142218731.png) 6 | 7 | Vert.x 由一个庞大的响应式模块生态系统提供支持,提供了编写现代服务时所需的一切:全面的Web栈、响应式数据库驱动程序、消息传递、事件流、集群、指标、分布式跟踪等等。 8 | 9 | Vert.x 是一个工具包,而不是一个附带黑魔法的框架:**您编写的内容实际上就是您要执行的内容**,就这么简单。 10 | 11 | 那么是什么让 Vert.x 成为编写下一个 **cloud-native(云原生)** 或 **[twelve-factor(12要素应用)](https://12factor.net/)** 应用程序的绝佳选择? 12 | 13 | > "云原生与12要素(Cloud-Native & 12-Factor)", 参见我写的一个[blog](https://blog.csdn.net/wjw465150/article/details/126852385) 14 | 15 | ## 一开始,有一些线程…… 16 | 17 | 并发编程的经典方法是使用**线程**。 多个线程可以存在于一个**进程**中,执行**并发**工作,并**共享**相同的内存空间。 18 | 19 | ![](Vert.x和Reactive简介.assets/image-20220914142321493.png) 20 | 21 | 大多数应用程序和服务开发框架都基于多线程。 从表面上看,**每个连接有 1 个线程**的模型令人放心,因为开发人员可以依赖传统的**命令式(im­per­a­tive style)**编程方式。 22 | 23 | 这种看起来挺美,特别是如果你忘记了在多线程和内存访问中可能犯的那些愚蠢的错误…… 24 | 25 | ## 多线程很“简单”,但也有局限性 26 | 27 | 当工作负荷超出常规工作负荷时,会发生什么?[(参见C10k问题)](https://en.wikipedia.org/wiki/C10k_problem) 28 | 29 | > **C10k problem:** C10k 问题是优化网络套接字以同时处理大量客户端的问题。 C10k 这个名字是并发处理一万个连接的数字名称。 处理许多并发连接与每秒处理许多请求是不同的问题:后者需要高吞吐量(快速处理它们),而前者不必很快,但需要高效的连接调度。 30 | 31 | 答案很简单:你开始让你的操作系统内核**受苦**,因为有太多的**上下文切换**处理正在进行的请求。 32 | 33 | ![](Vert.x和Reactive简介.assets/image-20220914142409910.png) 34 | 35 | 你的一些线程将被**阻塞**,因为它们正在**等待** I/O 操作完成,一些将**准备好**处理 I/O 结果,还有一些正在执行CPU密集型操作任务。 36 | 37 | 现代内核有非常好的调度器,但你不能指望它们处理 5万个线程就像处理 5千个线程一样容易。而且,线程并不便宜:创建一个线程需要几毫秒,而一个新线程需要至少大约 1MB 内存。 38 | 39 | ## 异步编程: 可伸缩性和资源效率 40 | 41 | 当您使用**异步 I/O** 时,可以使用更少的线程处理更多的并发连接。 当 I/O 操作发生时,我们不会阻塞线程,而是转移到另一个准备继续执行的任务,并在初始任务就绪后继续执行 42 | 43 | Vert.x 使用**事件循环(event loops)**多路复用并发工作负载。 44 | 45 | ![](Vert.x和Reactive简介.assets/image-20220914142456499.png) 46 | 47 | 在事件循环上运行的代码不应执行阻塞 I/O 或冗长的处理。 但是如果你有这样的代码也不要担心:Vert.x 有工作线程和 API 来处理事件循环上的事件。 48 | 49 | ## 为您的问题领域选择最佳异步编程模型 50 | 51 | 我们知道,异步编程需要付出更多的努力。 Vert.x的核心,支持 **callbacks** 和 **promises/futures**,后者是用于链接异步操作的简单而优雅的模型。 52 | 53 | 使用 [RxJava](https://github.com/ReactiveX/RxJava) 可以实现高级反应式编程,如果您更喜欢更接近传统命令式编程的东西,那么我们为您提供 [Kotlin 协程 ](https://kotlinlang.org/docs/reference/coroutines-overview.html)的一流支持。 54 | 55 | ![](Vert.x和Reactive简介.assets/image-20220914142529037.png) 56 | 57 | Vert.x支持多种异步编程模型: 为你需要解决的每个问题选择最适合的模型! 58 | 59 | ## 不要让失败破坏响应能力 60 | 61 | 失败经常会发生。数据库会中断,网络会中断,或者您所依赖的某些服务会失去响应。 62 | 63 | ![](Vert.x和Reactive简介.assets/image-20220914142600384.png) 64 | 65 | Vert.x 提供了保持延迟在控制之下的工具,包括一个简单高效的**断路器**。 66 | 67 | > **断路器**的目标是防止出现对内部发生故障的系统的请求堆积起来,导致分布式服务之间的级联错误。断路器充当(网络)请求(如RPC调用、HTTP请求或数据库调用)和要调用的服务之间的代理形式。 68 | 69 | ## 丰富的生态系统 70 | 71 | *Vert.x 堆栈* 包含用于构建现代端到端反应式服务的模块。 从高效的响应式数据库客户端到事件流、消息传递和 Web 堆栈,Vert.x 工具包为您提供: 72 | 73 | ![](Vert.x和Reactive简介.assets/image-20220914142646019.png) 74 | 75 | 找不到您要查找的内容? 76 | 77 | - [The Reactiveerse](https://reactiveerse.io/) 是一个围绕响应式生态系统的更大社区,您可以在其中找到更多客户端和模块。 78 | - [Vert.x Awesome](https://github.com/vert-x3/vertx-awesome) 存储库提供了来自更大开源社区的更多有趣项目的链接! -------------------------------------------------------------------------------- /vx.json: -------------------------------------------------------------------------------- 1 | { 2 | "created_time": "2022-05-26T04:34:59Z", 3 | "files": [ 4 | { 5 | "attachment_folder": "", 6 | "created_time": "2022-05-26T04:35:35Z", 7 | "id": "5414", 8 | "modified_time": "2023-03-02T10:34:39Z", 9 | "name": "Vert.x 4 Core Manual中文版.md", 10 | "signature": "27205976392599", 11 | "tags": [ 12 | ] 13 | }, 14 | { 15 | "attachment_folder": "", 16 | "created_time": "2022-07-07T02:45:17Z", 17 | "id": "5425", 18 | "modified_time": "2022-09-14T06:10:53Z", 19 | "name": "Vert.x Service Proxy Manual中文版.md", 20 | "signature": "27205980014781", 21 | "tags": [ 22 | ] 23 | }, 24 | { 25 | "attachment_folder": "", 26 | "created_time": "2022-09-14T10:09:12Z", 27 | "id": "5442", 28 | "modified_time": "2022-09-14T10:09:12Z", 29 | "name": "Vert.x和Reactive简介.md", 30 | "signature": "82331891247176", 31 | "tags": [ 32 | ] 33 | }, 34 | { 35 | "attachment_folder": "", 36 | "created_time": "2022-09-14T10:09:16Z", 37 | "id": "5443", 38 | "modified_time": "2022-09-14T10:09:16Z", 39 | "name": "云原生与12要素(Cloud-Native & 12-Factor).md", 40 | "signature": "67535728912460", 41 | "tags": [ 42 | ] 43 | }, 44 | { 45 | "attachment_folder": "", 46 | "created_time": "2023-02-07T02:42:35Z", 47 | "id": "5479", 48 | "modified_time": "2023-02-07T02:42:35Z", 49 | "name": "Futures 和 Promises.md", 50 | "signature": "177769396891", 51 | "tags": [ 52 | ] 53 | }, 54 | { 55 | "attachment_folder": "", 56 | "created_time": "2023-02-07T02:42:58Z", 57 | "id": "5480", 58 | "modified_time": "2023-02-07T02:42:58Z", 59 | "name": "Vert.x 4 JUnit 5 integration中文版.md", 60 | "signature": "79316836793010", 61 | "tags": [ 62 | ] 63 | }, 64 | { 65 | "attachment_folder": "", 66 | "created_time": "2023-02-07T02:42:58Z", 67 | "id": "5481", 68 | "modified_time": "2023-02-07T02:42:58Z", 69 | "name": "Vert.x Config.md", 70 | "signature": "27205998590642", 71 | "tags": [ 72 | ] 73 | }, 74 | { 75 | "attachment_folder": "", 76 | "created_time": "2023-02-27T05:50:42Z", 77 | "id": "5499", 78 | "modified_time": "2023-02-27T05:50:43Z", 79 | "name": "Context_API_简介.md", 80 | "signature": "177771136178", 81 | "tags": [ 82 | ] 83 | } 84 | ], 85 | "folders": [ 86 | { 87 | "name": "Web" 88 | }, 89 | { 90 | "name": "认证和授权" 91 | }, 92 | { 93 | "name": "Sql" 94 | }, 95 | { 96 | "name": "Cluster" 97 | }, 98 | { 99 | "name": "监控" 100 | }, 101 | { 102 | "name": "消息队列" 103 | }, 104 | { 105 | "name": "微服务" 106 | } 107 | ], 108 | "id": "5413", 109 | "modified_time": "2022-05-26T04:34:59Z", 110 | "signature": "79316814594931", 111 | "version": 3 112 | } 113 | -------------------------------------------------------------------------------- /Cluster/Zookeeper 集群管理器.md: -------------------------------------------------------------------------------- 1 | # Zookeeper 集群管理器 2 | 3 | 这是使用 [Zookeeper](https://zookeeper.apache.org/) 的 Vert.x 集群管理器实现。 4 | 5 | 它完全实现了vert.x集群的接口。 因此,如果需要,您可以使用它来代替 vertx-hazelcast。 这个实现被打包在里面: 6 | 7 | ```xml 8 | 9 | io.vertx 10 | vertx-zookeeper 11 | 4.2.7 12 | 13 | ``` 14 | 15 | 在 Vert.x 中,集群管理器用于各种功能,包括: 16 | 17 | - 集群中 Vert.x 节点的发现和组成员身份 18 | - 维护集群范围的主题订阅者列表(因此我们知道哪些节点对哪些事件总线地址感兴趣) 19 | - 分布式Map支持 20 | - 分布式锁 21 | - 分布式计数器 22 | 23 | **集群管理器不处理事件总线节点间传输,这由 Vert.x 通过 TCP 连接直接完成。** 24 | 25 | ## 如何工作 26 | 27 | 我们正在使用 [Apache Curator](https://curator.apache.org/) 框架而不是直接使用 zookeeper 客户端,因此我们对 Curator 中使用的库有依赖关系,例如`guava`、`slf4j`,当然还有`zookeeper `。 28 | 29 | 由于 ZK 使用树字典来存储数据,我们可以将根路径作为命名空间,在 `default-zookeeper.json` 中默认根路径是 `io.vertx`。 在 vert.x 集群管理器中还有另外 5 个子路径用于记录功能的其他信息,您可以更改的路径是`根路径`。 30 | 31 | 你可以在`/io.vertx/cluster/nodes/`的路径中找到所有的vert.x节点信息. 32 | `/io.vertx/asyncMap/$name/`记录你用`io.vertx.core.shareddata.AsyncMap` 接口创建的所有`AsyncMap`。 33 | `/io.vertx/asyncMultiMap/$name/`记录你用`io.vertx.core.spi.cluster.AsyncMultiMap`接口创建的所有`AsyncMultiMap`。 34 | `/io.vertx/locks/`记录分布式Locks信息。 35 | `/io.vertx/counters/`记录分布式Count信息。 36 | 37 | ## 使用此集群管理器 38 | 39 | 如果您从命令行使用 Vert.x,则与该集群管理器对应的 jar(它将被命名为 `vertx-zookeeper-4.2.7.jar` )应该在 Vert.x 安装的 `lib` 目录中 . 40 | 41 | 如果你想在你的 Vert.x Maven 或 Gradle 项目中使用这个集群管理器进行集群,那么只需在你的项目中添加一个依赖项到工件:`io.vertx:vertx-zookeeper:${version}`。 42 | 43 | > **🏷注意:** 如果 jar 如上所述在您的类路径中,那么 Vert.x 将自动检测到它并将其用作集群管理器。 请确保您的类路径中没有任何其他集群管理器,否则 Vert.x 可能会选择错误的。 44 | 45 | 如果要嵌入 Vert.x,还可以通过在创建 Vert.x 实例时在选项上指定集群管理器,以编程方式指定集群管理器,例如: 46 | 47 | ```java 48 | ClusterManager mgr = new ZookeeperClusterManager(); 49 | VertxOptions options = new VertxOptions().setClusterManager(mgr); 50 | Vertx.clusteredVertx(options, res -> { 51 | if (res.succeeded()) { 52 | Vertx vertx = res.result(); 53 | } else { 54 | // failed! 55 | } 56 | }); 57 | ``` 58 | 59 | ## 配置此集群管理器 60 | 61 | 通常集群管理器由一个文件 [default-zookeeper.json](https://github.com/vert-x3/vertx-zookeeper/blob/master/src/main/resources/default-zookeeper.json) 配置,该文件包装在jar里。 62 | 63 | `default-zookeeper.json`文件内容: 64 | ```json 65 | { 66 | "zookeeperHosts":"127.0.0.1", 67 | "sessionTimeout":20000, 68 | "connectTimeout":3000, 69 | "rootPath":"io.vertx", 70 | "retry": { 71 | "initialSleepTime":100, 72 | "intervalTimes":10000, 73 | "maxTimes":5 74 | } 75 | } 76 | ``` 77 | 78 | 如果你想覆盖这个配置,你可以在你的类路径中提供一个名为`zookeeper.json`的文件,这个文件将被使用。 如果要将`zookeeper.json`文件嵌入到 fat jar 中,它必须位于 fat jar 的根目录下。 如果是外部文件,则必须将包含该文件的*目录**添加到类路径中。 例如,如果您使用 Vert.x 中的 *launcher* 类,则可以按如下方式进行类路径增强: 79 | 80 | ```bash 81 | # If the zookeeper.json is in the current directory: 82 | java -jar ... -cp . -cluster 83 | vertx run MyVerticle -cp . -cluster 84 | 85 | # If the zookeeper.json is in the conf directory 86 | java -jar ... -cp conf -cluster 87 | ``` 88 | 89 | 另一种覆盖配置的方法是为系统属性`vertx.zookeeper.conf`提供一个位置: 90 | 91 | ```bash 92 | # Use a cluster configuration located in an external file 93 | java -Dvertx.zookeeper.config=./config/my-zookeeper-conf.json -jar ... -cluster 94 | 95 | # Or use a custom configuration from the classpath 96 | java -Dvertx.zookeeper.config=classpath:my/package/config/my-cluster-config.json -jar ... -cluster 97 | ``` 98 | > **🔔重要:** 翻译者白石发现不能加`classpath:`前缀 99 | 100 | `vertx.zookeeper.config` 系统属性,如果存在,会覆盖类路径中的任何 `zookeeper.json`,但如果从该系统属性加载失败,则加载回退到 `zookeeper.json` 或 Zookeeper的 默认配置 . 101 | 102 | 配置文件在 `default-zookeeper.json 的注释中有详细描述。 103 | 104 | 如果要嵌入,也可以通过编程方式指定配置: 105 | 106 | ```java 107 | JsonObject zkConfig = new JsonObject(); 108 | zkConfig.put("zookeeperHosts", "127.0.0.1"); 109 | zkConfig.put("rootPath", "io.vertx"); 110 | zkConfig.put("retry", new JsonObject() 111 | .put("initialSleepTime", 3000) 112 | .put("maxTimes", 3)); 113 | 114 | 115 | ClusterManager mgr = new ZookeeperClusterManager(zkConfig); 116 | VertxOptions options = new VertxOptions().setClusterManager(mgr); 117 | 118 | Vertx.clusteredVertx(options, res -> { 119 | if (res.succeeded()) { 120 | Vertx vertx = res.result(); 121 | } else { 122 | // failed! 123 | } 124 | }); 125 | ``` 126 | 127 | > **⚠重要:** 您还可以使用 `vertx.zookeeper.hosts` 系统属性配置 zookeeper 主机。 128 | 129 | ### 启用日志记录 130 | 131 | 当使用 Zookeeper 解决集群问题时,从 Zookeeper 获取一些日志输出以查看它是否正确形成集群通常很有用。 您可以通过在类路径中添加一个名为 `vertx-default-jul-logging.properties` 的文件来执行此操作(使用默认的 JUL 日志记录时)。 这是一个标准的 java.util.logging (JUL) 配置文件。 里面设置: 132 | 133 | ```properties 134 | org.apache.zookeeper.level=INFO 135 | ``` 136 | 137 | 以及 138 | 139 | ```properties 140 | java.util.logging.ConsoleHandler.level=INFO 141 | java.util.logging.FileHandler.level=INFO 142 | ``` 143 | 144 | ## 关于 Zookeeper 版本 145 | 146 | 我们使用 Curator 4.3.0,因为 Zookeeper 最新的稳定版是 3.4.8,所以我们不支持 3.5.x 的任何特性. 147 | 148 | -------------------------------------------------------------------------------- /Web/Vert.x EventBus Bridge Client中文版.md: -------------------------------------------------------------------------------- 1 | # Vert.x EventBus Bridge Client中文版 2 | 3 | > 翻译: 白石(https://github.com/wjw465150/Vert.x-Core-Manual) 4 | > 5 | > 项目地址: https://github.com/vert-x3/vertx-eventbus-bridge-clients 6 | > 7 | > Maven地址: (https://jarcasting.de/artifacts/io.vertx/vertx-eventbus-bridge-client/) 8 | 9 | Vert.x EventBus Client 是一个 Java 客户端,允许应用程序通过 TCP 或 WebSocket 传输与 Vert.x EventBus 桥交互。 连接后,它允许: 10 | 11 | - 将消息发送到 EventBus 的地址。 12 | - 向 EventBus 的地址发送消息并期待回复。 13 | - 将消息发布到 EventBus 的地址。 14 | - 创建消费者并将其注册到相应地址上。 15 | - 从相应地址注销消费者。 16 | 17 | 在底层,发送到服务器的数据包遵循 [Vert.x EventBus TCP 桥](https://vertx.io/docs/vertx-tcp-eventbus-bridge/java/) 中定义的协议。 18 | 19 | > **📝注意:** 此客户端不依赖于 Vert.x,需要 Java 6 运行时,使其也可以嵌入到 Android 应用程序中。 20 | 21 | ## 使用 Vert.x Event Bus 客户端 22 | 23 | 要使用此项目,请将以下依赖项添加到构建描述符的 *dependencies* 部分: 24 | 25 | - Maven (在你的 `pom.xml`): 26 | 27 | ```xml 28 | 29 | io.vertx 30 | vertx-eventbus-bridge-client 31 | 1.0.1 32 | 33 | 34 | com.google.code.gson 35 | gson 36 | 2.2.4 37 | 38 | ``` 39 | 40 | - Gradle (在你的 `build.gradle`): 41 | 42 | ```groovy 43 | compile 'io.vertx:vertx-eventbus-bridge-client:1.0.1' 44 | compile 'com.google.code.gson:gson:2.2.4' 45 | ``` 46 | 47 | ## 创建 Vert.x EventBus 客户端 48 | 49 | 有两种创建 Vert.x EventBus 客户端的方法,具体取决于要与之交互的桥的类型: 50 | 51 | ### 创建 TCP 事件总线桥客户端 52 | 53 | ```java 54 | EventBusClient tcpEventBusClient = EventBusClient.tcp(); 55 | 56 | // Create a bus client with specified host and port and TLS enabled 57 | EventBusClientOptions options = new EventBusClientOptions() 58 | .setHost("127.0.0.1").setPort(7001) 59 | .setSsl(true) 60 | .setTrustStorePath("/path/to/store.jks") 61 | .setTrustStorePassword("change-it"); 62 | EventBusClient sslTcpEventBusClient = EventBusClient.tcp(options); 63 | ``` 64 | 65 | 此示例可用于创建连接到 [Vert.x EventBus TCP 桥](https://vertx.io/docs/vertx-tcp-eventbus-bridge/java/) 的客户端。 66 | 67 | 使用默认选项,客户端将通过纯 TCP 连接到 `localhost:7000`。还可以将客户端配置为连接到不同的主机和端口,并为安全 TCP 连接启用 TLS/SSL。 68 | 69 | ### 创建 WebSocket SockJS 桥接客户端 70 | 71 | ```java 72 | EventBusClient webSocketEventBusClient = EventBusClient.webSocket(); 73 | 74 | // Create a bus client with specified host and port, TLS enabled and WebSocket path. 75 | EventBusClientOptions options = new EventBusClientOptions() 76 | .setHost("127.0.0.1").setPort(8043) 77 | .setSsl(true) 78 | .setTrustStorePath("/path/to/store.jks") 79 | .setTrustStorePassword("change-it") 80 | .setWebSocketPath("/eventbus/message") 81 | ; 82 | EventBusClient sslWebSocketEventBusClient = EventBusClient.webSocket(options); 83 | ``` 84 | 85 | 此示例可用于创建客户端以使用 WebSocket 连接到 [SockJS EventBus Bridge](https://vertx.io/docs/vertx-web/java/#_sockjs_event_bus_bridge)。 86 | 87 | 使用默认选项,客户端将使用 WebSocket 连接到 `http://localhost/eventbus/websocket`。还可以连接到不同的主机和端口,为安全 HTTP 连接启用 TLS/SSL。 `options.setWebSocketPath("/eventbus/message")` 可用于指定与 SockJS 桥中指定的路径对应的 WebSocket 路径。 88 | 89 | ## 与 EventBus 桥通信 90 | 91 | 不管连接的是什么类型的网桥,连接后,客户端可以通过以下方式与网桥通信: 92 | 93 | > **📝注意:** 如果尚未建立连接,以下任何一种方法都会导致自动连接。 94 | 95 | ### 发送消息到 EventBus 的地址 96 | 97 | 消息可以发送到 EventBus 的地址。 98 | 99 | ```java 100 | EventBusClient busClient = EventBusClient.tcp(); 101 | 102 | // 向总线发送消息,这会将客户端连接到服务器 103 | busClient.send("newsfeed", "Breaking news: something great happened"); 104 | ``` 105 | 106 | ### 向 EventBus 的地址发送消息并期待回复 107 | 108 | 可以将消息发送到 EventBus 的地址,并指定预期的回复处理程序。 109 | 110 | ```java 111 | busClient.request("newsfeed", "Breaking news: something great happened", new Handler>>() { 112 | @Override 113 | public void handle(AsyncResult> reply) { 114 | System.out.println("We got the reply"); 115 | } 116 | }); 117 | ``` 118 | 119 | ### 发布消息到 EventBus 的地址 120 | 121 | 消息可以发布到 EventBus 的地址。 122 | 123 | ```java 124 | busClient.publish("newsfeed", "Breaking news: something great happened"); 125 | ``` 126 | 127 | ### 创建消费者并将其注册到地址 128 | 129 | 您可以创建一个消费者并将其注册到 EventBus 的地址,当有任何消息发送到该地址时将调用它。 130 | 131 | ```java 132 | busClient.consumer("newsfeed", new Handler>() { 133 | @Override 134 | public void handle(Message message) { 135 | System.out.println("Received a news " + message.body()); 136 | } 137 | }); 138 | ``` 139 | 140 | ### 从其地址注销消费者 141 | 142 | 当消费者不再需要收听时,您可以从其地址中注销消费者。 143 | 144 | ```java 145 | consumer.unregister(); 146 | ``` 147 | 148 | ## 关闭客户端 149 | 150 | 您可以关闭客户端以释放与桥接服务器的连接。 151 | 152 | ```java 153 | busClient.closeHandler(new Handler() { 154 | @Override 155 | public void handle(Void event) { 156 | System.out.println("Bus Client Closed"); 157 | } 158 | }); 159 | // Closes the connection to the bridge server if it is open 160 | busClient.close(); 161 | ``` 162 | 163 | ## JSON格式编码 164 | 165 | 客户端和桥接器以使用`JsonCodec`的实现编码的自定义 JSON 格式交换消息。 客户端提供了两个`JsonCodec`实现。 166 | 167 | 在不指定 JsonCodec 创建新的 EventBusClient 实例时,它首先尝试加载 `GsonCodec`,如果 Gson 不在类路径中,它会尝试加载 `JacksonCodec`,如果 FasterXML Jackson 数据绑定也不在类路径中 ,它无法创建客户端实例。 168 | 169 | 您也可以在创建新的`EventBusClient`实例时指定`JsonCodec`的自定义实例。 170 | 171 | ### GsonCodec 172 | 173 | 基于[Google Gson 项目](https://github.com/google/gson) 的`JsonCodec` 实现。 `com.google.code.gson:gson` 的依赖是可选的,您需要显式添加此依赖才能使用此实现。 174 | 175 | ### JacksonCodec 176 | 177 | 基于 [FasterXML Jackson 数据绑定](https://github.com/FasterXML/jackson-databind) 的 `JsonCodec` 实现。 `com.fasterxml.jackson.core:jackson-databind` 的依赖是可选的,您需要显式添加此依赖才能使用此实现。 178 | 179 | ## EventBus 客户端选项 180 | 181 | Vert.x EventBus Client 中有 2 个主要选项。 182 | 183 | ### EventBusClientOptions 184 | 185 | `EventBusClientOptions` 用于在创建期间配置 EventBusClient,它具有以下属性: 186 | 187 | - `host`: String, 要连接的网桥主机地址,默认为“localhost”。 188 | - `port`: int, 要连接的网桥端口,默认为`-1`,即 TCP 网桥为`7000`,WebSocket SockJS 网桥为`80`。 189 | - `webSocketPath`: String, WebSocket客户端连接路径,默认为`/eventbus/websocket`。 它仅供 WebSocket EventBus Client 使用。 190 | - `maxWebSocketFrameSize`: int, 最大 WebSocket 帧大小,默认为 65536。 它仅供 WebSocket EventBus Client 使用。 191 | - `ssl`: boolean, 指示是否启用 SSL,默认为 `false`,表示未启用 SSL。 192 | - `trustStorePath`: String, 信任库的路径。 它仅在 `ssl` 为true时使用。 193 | - `trustStorePassword`: String, 信任库的密码。 它仅在 `ssl` 为true时使用。 194 | - `trustStoreType`: String, 信任库类型,`jks`、`pfx`、`pem` 之一,默认为 `jks`。 它仅在 `ssl` 为true时使用。 195 | - `verifyHost`: boolean, 如果启用主机名验证(用于 SSL/TLS),则默认为`true`。 它仅在 `ssl` 为true时使用。 196 | - `trustAll`: boolean, 如果应该信任所有服务器 (SSL/TLS),则默认为`false`。 它仅在 `ssl` 为true时使用。 197 | - `pingInterval`: int, ping 间隔,以毫秒为单位,默认为`5000`毫秒。 198 | - `autoReconnectInterval`: int, 自动重新连接尝试之间的暂停长度,以毫秒为单位,默认为`3000`毫秒。 199 | - `maxAutoReconnectTries`: int, 自动重新连接尝试的最大次数,默认为`0`,表示没有限制。 200 | - `connectTimeout`: int, 连接超时,以毫秒为单位,默认为`60000`毫秒。 201 | - `idleTimeout`: int, 空闲超时,以毫秒为单位,默认为`0`,表示没有超时。 202 | - `autoReconnect`: boolean, 是否启用自动重新连接,即使客户端不尝试发送消息,默认为`true`。 203 | - `proxyHost`: String, 代理服务器地址。 204 | - `proxyPort`: int, 代理服务器端口。 205 | - `proxyUsername`: String, 如果代理需要身份验证,则为代理用户名。 206 | - `proxyPassword`: String, 如果代理需要身份验证,则为代理密码。 207 | - `proxyType`: ProxyType, `ProxyType.HTTP`、`ProxyType.SOCKS4`、`ProxyType.SOCKS5` 之一。 208 | 209 | ### DeliveryOptions 210 | 211 | `DeliveryOptions` 用于向桥发送消息时,它具有以下属性: 212 | 213 | - `timeout`: long, 发送超时,以毫秒为单位,默认为 `30 * 1000` 毫秒。 如果在超时时间内没有收到响应,则将调用处理程序失败。 214 | - `headers`: Map, 发送到桥 EventBus 的标头。 215 | 216 | ------ 217 | 218 | <<<<<< [完] >>>>>> 219 | 220 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vert.x 核心手册中文版 2 | 3 | > 翻译: 白石(https://github.com/wjw465150/Vert.x-Core-Manual) 4 | 5 | # 摘要 6 | 7 | Vert.x最大的特点就在于异步(底层基于Netty),通过事件循环(EventLoop)来调起存储在异步任务队列(CallBackQueue)中的任务,大大降低了传统阻塞模型中线程对于操作系统的开销。因此相比较传统的阻塞模型,异步模型能够很大层度的提高系统的并发量。 8 | 9 | Vert.x除了异步之外,还提供了非常多的吸引人的技术,比如EventBus,通过EventBus可以非常简单的实现分布式消息,进而为分布式系统调用,微服务奠定基础。除此之外,还提供了对多种客户端的支持,比如Redis,RabbitMQ,Kafka等等。 10 | 11 | Vert.x异步也带来了编码上的复杂性,想要编写优美的异步代码,就需要对lambda表达式、函数式编程、Reactive等技术非常熟悉才行,否则很容易导致你的代码一团糟,完全没有可读性。另外,异步模型的性能调优、异常处理与同步模型有很大差异,网络中相关资料较少,使用中遇到问题排查困难,这也是目前国内架构师不愿意选择Vert.x的原因。 12 | 13 | Vert.x运行在Java虚拟机上,支持多种编程语言,Vert.x是高度模块化的,同一个应用,你可以选择多种编程语言同时开发。在Vert.x 2版本,也就是基于JDK7,还没有lambda的时候,一般来讲,使用JavaScript作为开发语言相对较多,到Vert.x3的时代,因为JDK8的出现,Java已经作为Vert.x主流的开发语言,而Vert.x也被更多的开发者所接受。 14 | 15 | # Vert.x能干什么 16 | 17 | ***Java能做的,Vert.x都能做。主要讨论,Vert.x善于做哪些事情!*** 18 | 19 | (1)Web开发,Vert.x封装了Web开发常用的组件,支持路由、Session管理、模板等,可以非常方便的进行Web开发。**不需要容器!不需要容器!不需要容器!** 20 | 21 | (2)TCP/UDP开发,Vert.x底层基于Netty,提供了丰富的IO类库,支持多种网络应用开发。**不需要处理底层细节(如拆包和粘包),注重业务代码编写。** 22 | 23 | (3)提供对WebSocket的支持,可以做网络聊天室,动态推送等。 24 | 25 | (4)Event Bus(事件总线)是Vert.x的神经系统,通过Event Bus可以实现分布式消息,远程方法调用等等。正是因为Event Bus的存在,Vert.x可以非常便捷的开发**微服务**应用。 26 | 27 | (5)支持主流的数据和消息的访问 28 | redis mongodb rabbitmq kafka 等 29 | 30 | (6)分布式锁,分布式计数器,分布式map的支持 31 | 32 | # Vert.x的一些优势 33 | 34 | ## **(1)异步非阻塞** 35 | 36 | Vert.x就像是跑在JVM之上的Nodejs,**所以Vert.x的第一个优势就是这是一个异步非阻塞框架**。上面也提到了异步,我们使用ajax来演示的异步,下面使用Vert.x请求远程地址一个代码,可以看到和ajax非常像! 37 | 38 | ```java 39 | System.out.println("1") 40 | 41 | WebClient 42 | .create(vertx) 43 | .postAbs(REQUEST_URL) // 这里指定的是请求的地址 44 | .sendBuffer(buffer, res -> { // buffer是请求的数据 45 | 46 | if (res.succeeded()) { 47 | // 请求远程服务成功 48 | System.out.println("2") 49 | 50 | } else { 51 | // 请求失败 52 | resultHandler.handle(Future.failedFuture("请求服务器失败...")); 53 | } 54 | }); 55 | 56 | System.out.println("3") 57 | ``` 58 | 59 | 这段代码的执行效果和上面的JavaScript执行的结果是类似的,同样是先打印 1,再打印 3,最后打印 2。 60 | 61 | 异步也是Vert.x于其他的JavaWeb框架的主要区别。我们这里先不去讨论异步的优势与他的实现原理,只要先知道,Vert.x和JavaScript一样,是一个异步执行的就可以了。 62 | 63 | **Vert.x和JDK8** 64 | 65 | Vert.x必须运行在JDK8上,JDK8提供了lambda表达式,可以简化匿名内部类的编写,可以极大的挺高代码的可读性。 66 | 67 | 上面的代码中看到 在 sendBuffer 这一行里有一个`->` 这种形式。这个是Java代码吗? 是的。是JDK8提供的lambda表达式的形式。用于简化匿名内部类的开发。有兴趣的朋友可以了解一下lambda表达式,在使用Vertx进行项目开发时有大量的匿名内部类,因此很多情况会用到。 68 | 69 | ## **(2)Vertx支持多种编程语言** 70 | 71 | Vert.x有一个口号大概是:“我们不去评判那个编程语言更好,你只要选择你想要使用的语言就可以了”。也就是说,在Vert.x上,可以使用JavaScript,Java,Scala,Ruby等等,下面是官网的一个截图 72 | 73 | ![image-20220218092424509](assets/image-20220218092424509.png) 74 | 75 | ## **(3)不依赖中间件** 76 | 77 | Vert.x的底层依赖Netty,因此在使用Vert.x构建Web项目时,不依赖中间件。像Node一样,可以直接创建一个HttServer。就像我们上面第一个例子,可以直接运行main方法,启动一个Http服务,而不需要使用类似于Tomcat的中间件。不依赖中间件进行开发,相对会更灵活一些,安全性也会更高一些。 78 | 79 | ## **(4)完善的生态** 80 | 81 | Vert.x和Spring的对比,有一种使用MacOS和Windows对比的感觉。Vert.x和庞大的Spring家族体系不同,Vert.x提供数据库操作,Redis操作,Web客户端操作,NoSQL数据库的一些操作等常用的结构,很清新,很简洁,但足够使用。下面是从官网截取的一个提供的客户端工具。 82 | 83 | ![image-20220218092459401](assets/image-20220218092459401.png) 84 | 85 | ## **(5)为微服务而生** 86 | 87 | Vert .x提供了各种组件来构建基于微服务的应用程序。通过EventBus可以非常容易的进行服务之间的交互。并且提供了HAZELCAST来实现分布式。 88 | 89 | 当然了,除了一些优势以外,要在项目中选择使用Vert.x还要考虑一些问题,这里不展开说明,只是根据个人的使用经验提出一些点。 90 | 91 | \* Vert.x使用JDK8的Lambda,所以要使用Vert.x首先需要对JDK8比较熟悉。当然,对于一个开发者,现在JDK已经出到JDK11了,JDK8的特性也理应该学习一下 92 | 93 | \* 对于复杂的业务,可能会遇到Callback Hell问题(解决方案也有很多) 94 | 95 | \* 由于异步的特征、契约创建、更高级的错误处理机制使得开发过程会相对更复杂。(来自并发编程网) 96 | 97 | # **Vert.x技术体系** 98 | 99 | 上面也提到了,Vert.x和Spring一样,也有着完善的生态,具体可以查看https://github.com/vert-x3/vertx-awesome 我们可以看到,每一块内容都提供了多种的实现,有官方支持的版本还有社区版本。下面我们具体介绍下技术体系中官方支持的版本。 100 | 101 | ## **(1)核心模块** 102 | 103 | Vert.x核心模块包含一些基础的功能,如HTTP,TCP,文件系统访问,EventBus、WebSocket、延时与重复执行、缓存等其他基础的功能,你可以在你自己的应用程序中直接使用。可以通过vertx-core模块引用即可。 104 | 105 | ## **(2)Web模块** 106 | 107 | Vert.x Web是一个工具集,虽然核心模块提供了HTTP的支持,但是要开发复杂的Web应用,还需要路由、Session、请求数据读取、Rest支持等等还需要Web模块,这里提供了上述的这些功能的API,便于开发。 108 | 109 | 除了对Web服务的开发以外,还提供了对**Web客户端**请求的支持,通过vertx-web-client即可方便的访问HTTP服务。有朋友可能会有疑惑,我明明可以使用JDK提供的URL来请求HTTP服务啊。使用Vert.x一定要注意,Vert.x是一个异步框架,请求HTTP服务是一个耗时操作,所有的耗时,都会阻塞EventBus,导致整体性能被拖垮,因此,对于请求Web服务,一定要使用Vert.x提供的vertx-web-client模块 110 | 111 | ## **(3)数据访问模块** 112 | 113 | Vert.x提供了对关系型数据库、NoSQL、消息中间件的支持,传统的客户端因为是阻塞的,会严重影响系统的性能,因此Vert.x提供了对以上客户端的异步支持。具体支持的数据访问如下: 114 | 115 | MongoDB client,JDBC client,SQL common,Redis client,MySQL/PostgreSQLclient 116 | 117 | ## **(4)Reactive响应式编程** 118 | 119 | 复杂的异步操作,会导致异步回调地狱的产生,看下面的代码,这是我在Vert.x提供的例子中找到的,我们不去管这段代码干了啥,只是看后面的}就很惊讶了,如果操作更为复杂一些,会嵌套的层次更多,通过reactive可以最小化的简化异步回调地狱。 120 | 121 | ```java 122 | // create a test table 123 | execute(conn.result(), "create table test(id int primary key, name varchar(255))", create -> { 124 | // start a transaction 125 | startTx(conn.result(), beginTrans -> { 126 | // insert some test data 127 | execute(conn.result(), "insert into test values(1, 'Hello')", insert -> { 128 | // commit data 129 | rollbackTx(conn.result(), rollbackTrans -> { 130 | // query some data 131 | query(conn.result(), "select count(*) from test", rs -> { 132 | for (JsonArray line : rs.getResults()) { 133 | System.out.println(line.encode()); 134 | } 135 | 136 | // and close the connection 137 | conn.result().close(done -> { 138 | if (done.failed()) { 139 | throw new RuntimeException(done.cause()); 140 | } 141 | }); 142 | }); 143 | }); 144 | }); 145 | }); 146 | }); 147 | ``` 148 | 149 | 再看一个使用Reactive2构建的多步操作的代码,paramCheckStep,insertPayDtlStep,requestStep等等都是异步方法,但这里就很好的处理了异步回调的问题,不再有那么多层的大括号,代码结构也更清晰. 150 | 151 | ```java 152 | public void scanPay(JsonObject data, Handler> resultHandler) { 153 | paramCheckStep(data) // 参数校验 154 | .flatMap(this::insertPayDtlStep) // 插入流水 155 | .flatMap(x -> requestStep(x, config)) // 请求上游 156 | .flatMap(this::cleanStep) //参数清理 157 | .subscribe(ok -> { 158 | logger.info("成功结束"); 159 | resultHandler.handle(Future.succeededFuture(ok)); 160 | }, 161 | err -> { 162 | logger.error("正在结束", err); 163 | resultHandler.handle(Future.failedFuture(err)); 164 | } 165 | 166 | ); 167 | } 168 | ``` 169 | 170 | ## **(5)整合其他模块** 171 | 172 | 邮件客户端 173 | 174 | Vert.x提供了一简单STMP邮件客户端,所以你可以在应用程序中发送电子邮件。 175 | 176 | STOMP客户端与服务端 177 | 178 | Vert.x提供了STOMP协议的实现包括客户端与服务端。 179 | 180 | Consul Client 181 | 182 | consul是google开源的一个使用go语言开发的服务发现、配置管理中心服务。内置了服务注册与发现框 架、分布一致性协议实现、健康检查、Key/Value存储、多数据中心方案。 183 | 184 | RabbitMQ Client Kafka Client 185 | 186 | 消息队里的客户端支持 187 | 188 | JCA适配器 189 | 190 | Vert.x提供了Java连接器架构适配器,这允许同任意JavaEE应用服务器进行互操作。 191 | 192 | ## **(6)认证与授权** 193 | 194 | Vert.x提供了简单API用于在应用中提供认证和授权。 195 | 196 | Auth common 通用的认证API,可以通过重写AuthProvider类来实现自己的认证 197 | 198 | JDBC auth 后台为JDBC的认证实现 199 | 200 | JWT auth 用JSON Web tokens认证实现 201 | 202 | Shiro auth 使用Apache Shiro认证实现 203 | 204 | MongoDB auth MongoDB认证实现 205 | 206 | OAuth 2 Oauth2协义认证实现 207 | 208 | htdigest auth 这个是新增一种认证的支持 209 | 210 | ## **(7)微服务** 211 | 212 | Vert.x提供多个组件构建基于微服务的应用程序。 213 | 214 | 比如服务发现(Vert.x Service Discovery)、断路器(Vert.x Circuit Breaker)、配置中心(Vert.x Config)等。 215 | 216 | ------ 217 | 218 | 技术是为业务服务的,在选择架构的时候,也要考虑用人的成本,也正是因为如此,国内使用Vert.x的企业还不是很多。**但是我相信,未来,一定是异步非阻塞的天下!** 219 | 220 | -------------------------------------------------------------------------------- /监控/Vert.x 健康检查.md: -------------------------------------------------------------------------------- 1 | # Vert.x 健康检查 2 | 3 | 该组件提供了一个简单的检视健康状况的途径。 健康检查组件使用非常简单的措辞来表达应用程序的当前状况: *UP* 以及 *DOWN* 。 健康检查组件可以单独使用,也可以和 Vert.x Web 或者事件总线联合使用。 4 | 5 | 该组件提供一个 Vert.x Web handler 让您可以注册一些(检测)例程 用于检测应用程序的健康状况。 该 handler 计算出(健康状况的)最终状态并以 JSON 方式返回结果。 6 | 7 | ## 如何使用 Vert.x 健康检查 8 | 9 | 请注意,一般情况下您需要 Vert.x Web 模块来使用该组件。启用该组件只需要添加以下依赖: 10 | 11 | - Maven (在您的 `pom.xml` 文件中): 12 | 13 | ```xml 14 | 15 | io.vertx 16 | vertx-health-check 17 | 4.3.5 18 | 19 | ``` 20 | 21 | - Gradle (在您的 `build.gradle` 文件中): 22 | 23 | ```groovy 24 | compile 'io.vertx:vertx-health-check:4.3.5' 25 | ``` 26 | 27 | ### 创建健康检查对象。 28 | 29 | 最主要的对象是 `HealthChecks` 。 您可以通过以下方式创建该对象的实例: 30 | 31 | ```java 32 | HealthChecks hc = HealthChecks.create(vertx); 33 | 34 | hc.register( 35 | "my-procedure", 36 | promise -> promise.complete(Status.OK())); 37 | 38 | // 注册时指定超时时间参数。如果未能在超时时间之内完成,则视为故障。 39 | // timeout 参数的单位是毫秒。 40 | hc.register( 41 | "my-procedure", 42 | 2000, 43 | promise -> promise.complete(Status.OK())); 44 | ``` 45 | 46 | 一旦您创建了这个对象,您就可以注册或者注销(检测)例程。详情请见后面的章节。 47 | 48 | ### 注册 Vert.x Web handler 49 | 50 | 您可以通过以下方式创建用于健康检查的 Vert.x Web handler : 51 | 52 | - 使用已有的 `HealthChecks` 对象实例 53 | - 让 handler 为您创建一个新的对象实例。 54 | 55 | ```java 56 | HealthCheckHandler healthCheckHandler1 = HealthCheckHandler.create(vertx); 57 | 58 | HealthCheckHandler healthCheckHandler2 = HealthCheckHandler 59 | .createWithHealthChecks(HealthChecks.create(vertx)); 60 | 61 | Router router = Router.router(vertx); 62 | // 向 router 添加路由规则 63 | // 注册健康检查 handler 64 | router.get("/health*").handler(healthCheckHandler1); 65 | // 或者 66 | router.get("/ping*").handler(healthCheckHandler2); 67 | ``` 68 | 69 | 可以直接在 `HealthCheckHandler` 对象实例中注册例程。 此外,如果您已经预先创建了 `HealthChecks` 对象的实例, 您可以直接在该对象中注册例程。 在任何时刻都可以进行例程的注册和注销,即使在路由规则注册完成之后也可以: 70 | 71 | ```java 72 | HealthCheckHandler healthCheckHandler = HealthCheckHandler.create(vertx); 73 | 74 | // 注册例程 75 | // 在路由规则注册完成之后,甚至是运行时,也可以完成该操作 76 | healthCheckHandler.register("my-procedure-name", promise -> { 77 | // 进行检测 .... 78 | // 检测通过时执行 79 | promise.complete(Status.OK()); 80 | // 如果发生故障则执行: 81 | promise.complete(Status.KO()); 82 | }); 83 | 84 | // 注册另一个指定了超时时间(2秒)的例程。 如果该例程未能在 85 | // 指定的超时时间内完成,则视为故障。 86 | healthCheckHandler.register( 87 | "my-procedure-name-with-timeout", 88 | 2000, 89 | promise -> { 90 | // 进行检测 .... 91 | // 检测通过时执行 92 | promise.complete(Status.OK()); 93 | // 如果发生故障则执行: 94 | promise.complete(Status.KO()); 95 | }); 96 | 97 | router.get("/health").handler(healthCheckHandler); 98 | ``` 99 | 100 | ## 例程(procedures) 101 | 102 | 此处的例程是指一个检查系统某个表征现象的函数,用于推断当前的健康状况。 它报告一个 `Status` 对象用以指示该项检测是否通过。 该函数将检测结果报告给它所对应的 `Promise` ,并且请注意该函数不可以阻塞这个 `Promise` 。 103 | 104 | 当您注册了一个例程,您需要对其命名,并且要指定一个函数(handler)来执行该项检测。 105 | 106 | 推断健康状况的规则如下: 107 | 108 | - 如果对应的 promise 被标记为故障,则检测结果认定为 *KO* 109 | - 如果对应的 promise 成功完成但是没有包含一个 `Status` 对象, 检测结果认定为 *OK*。 110 | - 如果对应的 promise 成功完成且包含一个标记为 *OK* 的 `Status` 对象, 检测结果认定为 *OK*。 111 | - 如果对应的 promise 成功完成且包含一个标记为 *KO* 的 `Status` 对象, 检测结果认定为 *KO*。 112 | 113 | `Status` 对象可以提供额外的数据: 114 | 115 | ```java 116 | HealthCheckHandler healthCheckHandler = HealthCheckHandler.create(vertx); 117 | 118 | // Status 对象能以 JSON 形式提供额外的数据 119 | healthCheckHandler.register("my-procedure-name", promise -> { 120 | promise.complete(Status.OK(new JsonObject().put("available-memory", "2mb"))); 121 | }); 122 | 123 | healthCheckHandler.register("my-second-procedure-name", promise -> { 124 | promise.complete(Status.KO(new JsonObject().put("load", 99))); 125 | }); 126 | 127 | router.get("/health").handler(healthCheckHandler); 128 | ``` 129 | 130 | 例程可以进行分组管理。 例程的名称里可以指定分组信息。 分组的例程按照树形结构进行组织, 并且树形结构被映射到 HTTP url 之上(如下所示)。 131 | 132 | ```java 133 | HealthCheckHandler healthCheckHandler = HealthCheckHandler.create(vertx); 134 | 135 | // 注册例程 136 | // 例程可以进行分组,以例程名称中的 “/” 分隔符来判断组别 137 | // 一个分组中也可以包含另一个分组 138 | healthCheckHandler.register( 139 | "a-group/my-procedure-name", 140 | promise -> { 141 | //.... 142 | }); 143 | healthCheckHandler.register( 144 | "a-group/a-second-group/my-second-procedure-name", 145 | promise -> { 146 | //.... 147 | }); 148 | 149 | router.get("/health").handler(healthCheckHandler); 150 | ``` 151 | 152 | ## HTTP 响应和 JSON 输出 153 | 154 | 启用 Vert.x web handler 之后,可以通过对外开放的 `HealthCheckHandler` 所对应的路由规则 以 HTTP GET 或者 POST (取决于您注册的路由规则)的方式获取总体健康检查信息。 155 | 156 | 如果没有注册任何例程, 则响应信息为 `204 - NO CONTENT` , 表明系统状态为 *UP* 但是没有执行任何例程。 此时响应信息不包含任何有效数据。 157 | 158 | 如果注册了至少一个例程,该例程将被执行并计算出检测结果。 响应码包括下列几种: 159 | 160 | - `200` : 一切正常 161 | - `503` : 至少有一个例程报告了不健康状态 162 | - `500` : 某个例程抛出了错误,或者未能及时报告状态 163 | 164 | 响应的内容是一个 JSON 文档,体现的是总体结果(`outcome`)。总体结果要么是 `UP` 要么是 `DOWN` 。 此外还给出了一个 `checks` 数组用以显示每个执行过的例程的结果。 如果某个例程报告了额外的数据,这些数据也会一并给出: 165 | 166 | ```json 167 | { 168 | "checks" : [ 169 | { 170 | "id" : "A", 171 | "status" : "UP" 172 | }, 173 | { 174 | "id" : "B", 175 | "status" : "DOWN", 176 | "data" : { 177 | "some-data" : "some-value" 178 | } 179 | } 180 | ], 181 | "outcome" : "DOWN" 182 | } 183 | ``` 184 | 185 | 如果采用了分组/层级结构,则 `checks` 数组通过以下结构来描述: 186 | 187 | ```json 188 | { 189 | "checks" : [ 190 | { 191 | "id" : "my-group", 192 | "status" : "UP", 193 | "checks" : [ 194 | { 195 | "id" : "check-2", 196 | "status" : "UP", 197 | }, 198 | { 199 | "id" : "check-1", 200 | "status" : "UP" 201 | }] 202 | }], 203 | "outcome" : "UP" 204 | } 205 | ``` 206 | 207 | 如果一个例程抛出了错误,或者报告了故障(异常),该 JSON 文档会在 `data` 字段下给出 `cause` 字段。 如果某个例程未能及时上报结果,则结果将显示为 `Timeout` (超时)。 208 | 209 | ## 例程示例 210 | 211 | 此章节提供一些通用的健康检查示例 212 | 213 | ### SQL client 214 | 215 | 该例子用以报告一个数据库连接是否成功建立: 216 | 217 | ```java 218 | handler.register("database", 219 | promise -> pool.getConnection(connection -> { 220 | if (connection.failed()) { 221 | promise.fail(connection.cause()); 222 | } else { 223 | connection.result().close(); 224 | promise.complete(Status.OK()); 225 | } 226 | })); 227 | ``` 228 | 229 | ### 服务可用性 230 | 231 | 该项检测用于报告某个服务(此处是指一个HTTP endpoint)在服务发现中是否可用: 232 | 233 | ```java 234 | handler.register("my-service", 235 | promise -> 236 | HttpEndpoint.getClient(discovery, rec -> "my-service".equals(rec.getName()), 237 | client -> { 238 | if (client.failed()) { 239 | promise.fail(client.cause()); 240 | } else { 241 | client.result().close(); 242 | promise.complete(Status.OK()); 243 | } 244 | })); 245 | ``` 246 | 247 | ### 事件总线 248 | 249 | 该项检测用于报告某个事件总线上的某个消费者是否已经准备就绪。 在这个例子中,是一个简单的 ping/pong 应答协议,您也可以换成别的更为复杂的场景。 该项检测可以用于检查某个 verticle 是否已经准备就绪并且已在监听某个事件总线地址。 250 | 251 | ```java 252 | handler.register("receiver", 253 | promise -> 254 | vertx.eventBus().request("health", "ping") 255 | .onSuccess(msg -> { 256 | promise.complete(Status.OK()); 257 | }) 258 | .onFailure(err -> { 259 | promise.complete(Status.KO()); 260 | })); 261 | ``` 262 | 263 | ## 身份认证 264 | 265 | 当使用 Vert.x web handler 时, 您可以传入一个 `AuthenticationProvider` 对象用来对请求进行身份认证。 详情请查阅 `Vert.x Auth` 。 266 | 267 | Vert.x Web handler 创建一个 JSON 对象包含以下内容: 268 | 269 | - 请求头 270 | - 请求参数 271 | - 表单参数(如果存在) 272 | - JOSN 格式的内容(如果存在,并且请求的 content type 是 `application/json` ) 273 | 274 | 上述对象会被传入身份认证方式提供者来对请求进行身份认证。 如果认证失败,则会返回 `403 - FORBIDDEN` 响应。 275 | 276 | ## 在事件总线上开放健康检查功能 277 | 278 | 利用 Vert.x web handler 通过 HTTP 方式开放健康检查功能是十分便捷的,但是通过别的方式开放这些数据可以发挥更大的作用。 以下章节给出了如何在事件总线上开放健康检查数据的例子: 279 | 280 | ```java 281 | vertx.eventBus().consumer("health", 282 | message -> healthChecks.checkStatus() 283 | .onSuccess(message::reply) 284 | .onFailure(err -> message.fail(0, err.getMessage()))); 285 | ``` 286 | 287 | ------ 288 | 289 | <<<<<< [完] >>>>>> 290 | 291 | -------------------------------------------------------------------------------- /Futures 和 Promises.md: -------------------------------------------------------------------------------- 1 | # 5.3 Futures 和 Promises 2 | 3 | > 翻译: 白石(https://github.com/wjw465150/Vert.x-Core-Manual) 4 | 5 | 由于 Verticle `start` 方法的签名,你已经接触过 Vert.x 的`futures` 和 `promises`。 你可能也接触过其他语言,比如 JavaScript。 我们将进一步探索这个模型,看看它们是如何用 Vert.x 组合异步操作的有趣原语。 6 | 7 | Vert.x 实现了与 Barbara Liskov 和 Liuba Shrira 的原始研究结果一致的 Future 和 Promise 模型。他们引入了 Promise 作为组合异步远程过程调用的语言抽象。 8 | 9 | promise保存了一些现在还没有值的计算的值。承诺最终会带着一个结果值或一个错误完成。在异步I/O上下文中,promise自然适合保存异步操作的结果。反过来,future允许您读取最终将从promise中获得的值。 10 | 11 | > **⚠重要:** 总结一下:promise 用于写入最终值,future 用于在可用时读取它。 现在让我们看看它在 Vert.x 中是如何工作的。 12 | 13 | ## 5.3.1 Vert.x 中的 Futures 和 promises 14 | Promise 是由一段即将执行异步操作的代码创建的。 例如,假设您要报告异步操作已完成,不是现在,而是在 5 秒内。 在 Vert.x 中,您将为此使用计时器,并使用 promise 来保存结果,如下面的清单所示。 15 | 16 | **清单 5.13 创建一个 promise** 17 | 18 | ![image-20220225164145778](Futures_and_Promises.assets/image-20220225164145778.png) 19 | 20 | ```java 21 | Promise promise = Promise.promise(); 22 | vertx.setTimer(5000, id -> { 23 | if (System.currentTimeMillis() % 2L == 0L) { 24 | promise.complete("Ok!"); 25 | } else { 26 | promise.fail(new RuntimeException("Bad luck...")); 27 | } 28 | }); 29 | 30 | // (...) 31 | ``` 32 | 33 | 这里的异步操作是一个五秒的定时器,之后promise就完成了。 根据当前时间是奇数还是偶数,promise 以一个值完成或因异常而失败。 这很好,但我们如何真正从 Promise 中`get`值? 34 | 35 | 想要在结果可用时做出反应的代码需要一个future对象。一个Vertx future是从一个promise创建的,然后传递给想要读取该值的代码,如下一个清单所示,即清单5.13的其余部分。 36 | 37 | **清单 5.14 从一个 Promise 中创建一个future** 38 | 39 | ![image-20220225164809902](Futures_and_Promises.assets/image-20220225164809902.png) 40 | 41 | ```java 42 | Future future = promise.future(); 43 | return future; 44 | 45 | // (...) 46 | 47 | future 48 | .onSuccess(System.out::println) 49 | .onFailure(err -> System.out.println(err.getMessage())); 50 | ``` 51 | 52 | `Future` 接口定义了两种方法,`onSuccess` 和 `onFailure`,用于处理值和错误。 当我们运行相应的代码时,我们会看到“Ok!” 或“Bad lucky...”在5秒后打印。 53 | 54 | 我们可以使用Future执行更高级的异步操作,如下面的清单所示。 55 | 56 | **清单 5.15 高级future 组合操作** 57 | 58 | ![image-20220225165322925](Futures_and_Promises.assets/image-20220225165322925.png) 59 | 60 | ```java 61 | promise.future() 62 | .recover(err -> Future.succeededFuture("Let's say it's ok!")) 63 | .map(String::toUpperCase) 64 | .flatMap(str -> { 65 | Promise next = Promise.promise(); 66 | vertx.setTimer(3000, id -> next.complete(">>> " + str)); 67 | return next.future(); 68 | }) 69 | .onSuccess(System.out::println); 70 | ``` 71 | 72 | 当 promise 失败时调用 `recover` 操作,它用于将错误替换为另一个值。 您可以将 `recover` 视为 Java 中的 `catch` 块的等价物,您可以在其中处理错误。 这里,我们只是使用一个成功的future提供一个恢复值,但是在更高级的情况下,当您无法进行恢复时,您也可以使用一个失败的future。 73 | 74 | map 操作使用函数转换值,而 `flatMap` 与另一个异步操作组合。 您可以将`flatMap` 视为“然后”。 此处操作获取字符串值并在3秒后将“>>>”添加到其前面。 我们还看到了典型的 Promise/Future 模式,我们首先创建一个 Promise,然后执行一个最终完成 Promise 的异步操作,最后返回一个 Future,这样该值就可以被另一段代码使用。 75 | 76 | ## 5.3.2 Vert.x 4 中基于Future的 API 77 | 78 | Vert.x 4 将 Future与回调一起引入核心 API。 虽然回调仍然是规范模型,但大多数 API 都可以使用返回`Future`的变体。 79 | 80 | 这意味着给定一个方法,`void doThis(Handler>)`,有一个形式为`Future doThis()`的变体。 下面的清单显示了一个很好的示例,我们在其中启动了一个 HTTP 服务器。 81 | 82 | **清单 5.16 使用Future方法启动 HTTP 服务器** 83 | 84 | ![image-20220225165814701](Futures_and_Promises.assets/image-20220225165814701.png) 85 | 86 | ```java 87 | @Override 88 | public void start(Promise promise) { 89 | vertx.createHttpServer() 90 | .requestHandler(this::handleRequest) 91 | .listen(8080) 92 | .onFailure(promise::fail) 93 | .onSuccess(ok -> { 94 | System.out.println("http://localhost:8080/"); 95 | promise.complete(); 96 | }); 97 | } 98 | ``` 99 | 100 | 我们在前面的例子中看到的 listen 方法接受一个回调接口`Handler>`,但是在这里它返回一个 `Future`。 然后,我们可以链接调用 `onFailure` 和 `onSuccess` 来定义服务器启动或发生错误时要做什么。 101 | 102 | > **🏷注意:** 您可以从 Vert.x 3.8 开始使用新的 Promise/Future 接口,但基于Future的 API 仅在 Vert.x 4 中可用。 103 | 104 | ## 5.3.3 与 CompletionStage API 的互操作性 105 | 106 | Vert.x futures 还可以与 JDK 中的 `java.util.concurrent` 包的 `CompletionStage` 接口互操作。 `CompletionStage` 接口代表异步操作中的一个步骤,因此您可以将其视为future,尤其是当有一个名为 `CompletableFuture` 的类实现了 `CompletionStage` 时。 例如,Java 11 中的 HTTP 客户端 API 提供了 `sendAsync` 方法,这些方法返回 `CompletableFuture` 来发出异步 HTTP 请求。 107 | 108 | 当您需要与在 API 中使用`CompletionStage`的库进行交互时,Vert.x 的Future和`CompletionStage`之间的互操作性非常有用。 109 | 110 | > **🏷注意:** Vert.x `Future` 接口不是 `CompletionStage` 的子类型。 Vert.x 团队在为 Vert.x 4 准备路线图时考虑了这一点,但我们最终选择了我们自己的接口定义,因为`CompletionStage`与线程模型无关。实际上,以“**async**”为后缀的方法提供了变体,您可以在其中传递执行器,例如 `CompletionStage thenRunAsync(Runnable,Executor)`,而没有执行器参数的变体默认分派到 `ForkJoinPool` 实例。这些方法可以很容易地跳出 Vert.x 事件循环或工作线程池,因此我们选择提供互操作性,而不是直接在 Vert.x API 中使用 `CompletionStage`。 111 | 112 | 以下清单显示了我们如何从 Vert.x Future 迁移到 `CompletionStage`。 113 | 114 | **清单 5.17 从 Vert.x Future 到 CompletionStage** 115 | 116 | ![image-20220225170605494](Futures_and_Promises.assets/image-20220225170605494.png) 117 | 118 | ```java 119 | CompletionStage cs = promise.future().toCompletionStage(); 120 | cs 121 | .thenApply(String::toUpperCase) 122 | .thenApply(str -> "~~~ " + str) 123 | .whenComplete((str, err) -> { 124 | if (err == null) { 125 | System.out.println(str); 126 | } else { 127 | System.out.println("Oh... " + err.getMessage()); 128 | } 129 | }); 130 | ``` 131 | 132 | 这里我们将字符串结果转换为大写,在它前面加上一个字符串,最终调用了*whenComplete*。 请注意,这是一个 *BiConsumer*,需要测试哪些值或异常参数为 *null*,才能知道 promise 是否成功完成。 同样重要的是要注意,除非您调用异步的 *CompletionStage* 方法,否则调用将在 Vert.x 线程上执行。 133 | 134 | 最后但同样重要的是,您也可以将 *CompletionStage* 转换为 Vert.x `Future`,如下所示。 135 | 136 | **清单 5.18 从 CompletionStage 到 Vert.x Future** 137 | 138 | ![image-20220225171136849](Futures_and_Promises.assets/image-20220225171136849.png) 139 | 140 | ```java 141 | CompletableFuture cf = CompletableFuture.supplyAsync(() -> { 142 | try { 143 | Thread.sleep(5000); 144 | } catch (InterruptedException e) { 145 | e.printStackTrace(); 146 | } 147 | return "5 seconds have elapsed"; 148 | }); 149 | 150 | Future 151 | .fromCompletionStage(cf, vertx.getOrCreateContext()) 152 | .onSuccess(System.out::println) 153 | .onFailure(Throwable::printStackTrace); 154 | ``` 155 | 156 | `CompletableFuture` 实现了 `CompletionStage`接口,而 `supplyAsync` 调度了对默认 `ForkJoinPool` 的调用。 将使用该线程池中的一个线程,在返回一个字符串之前休眠5秒钟,该字符串将是' CompletableFuture '结果。`fromCompletionStage` 方法转换为 Vert.x `Future`。 该方法有两种变体:一种具有 Vert.x 上下文,用于在上下文上调用 `Future` 方法,如`onSuccess`,另一种调用将发生在完成提供的`CompletionStage` 实例的任何线程上。 157 | 158 | ## 5.3.4 Vert.x Future的收集器服务 159 | 160 | 回到边缘服务示例,我们可以利用使用 Future 的 Vert.x API。 我们将使用清单 5.16 中较早的 verticle `start` 方法。 161 | 162 | 首先,我们可以在下面的清单中定义 `fetchTemperature` 方法来从服务中获取温度。 163 | 164 | **清单 5.19 使用基于Future的 API 获取温度** 165 | 166 | ![image-20220225171553045](Futures_and_Promises.assets/image-20220225171553045.png) 167 | 168 | ```java 169 | private Future fetchTemperature(int port) { 170 | return webClient 171 | .get(port, "localhost", "/") 172 | .expect(ResponsePredicate.SC_SUCCESS) 173 | .as(BodyCodec.jsonObject()) 174 | .send() 175 | .map(HttpResponse::body); 176 | } 177 | ``` 178 | 179 | 该方法返回一个`JsonObject`的future,为了实现这一点,我们使用`WebClient HttpRequest send`方法返回future,然后映射结果以仅提取JSON数据。 180 | 181 | 在接下来显示的 `handleRequest` 方法中收集温度。 182 | 183 | **清单 5.20 使用基于Future的 API 收集温度** 184 | 185 | ![image-20220225171946824](Futures_and_Promises.assets/image-20220225171946824.png) 186 | 187 | ```java 188 | private void handleRequest(HttpServerRequest request) { 189 | CompositeFuture.all( 190 | fetchTemperature(3000), 191 | fetchTemperature(3001), 192 | fetchTemperature(3002)) 193 | .flatMap(this::sendToSnapshot) 194 | .onSuccess(data -> request.response() 195 | .putHeader("Content-Type", "application/json") 196 | .end(data.encode())) 197 | .onFailure(err -> { 198 | logger.error("Something went wrong", err); 199 | request.response().setStatusCode(500).end(); 200 | }); 201 | } 202 | ``` 203 | 204 | 你可以使用`CompositeFuture`将多个future组合成一个。`all`静态方法的结果是,当所有的future完成时,该future会完成,当任何future失败时,该future会失败。还有具有不同语义的`any`和`join`方法。 205 | 206 | 一旦成功接收到所有温度,对`flatMap`的调用将数据发送到快照服务,这是一个异步操作。 `sendToSnapshot` 方法的代码显示在以下清单中。 207 | 208 | **清单 5.21 使用基于Future的 API 将数据发送到快照服务** 209 | 210 | ![image-20220225172220399](Futures_and_Promises.assets/image-20220225172220399.png) 211 | 212 | ```java 213 | private Future sendToSnapshot(CompositeFuture temps) { 214 | List 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 |
31 | Current price: 32 | 33 |
34 | 35 | 36 | 37 |
38 |
39 | Feed: 40 | 41 |
42 |
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 | ![](使用Websockets和Vert.x进行实时竞价.assets/image-20230202111820409.png) 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 | > ![](SockJS-client简介.assets/image-20230201211424124.png) 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 | [![img](SockJS-client简介.assets/Browserstack-logo@2x.png)](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。“你为什么在这里?”,你可能会问...好吧,我目前有两个主要爱好,学习新事物和使用[Key­cloak](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 Con­nect / OAuth2 Sup­port](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 sim­ple web app con­sists of a sin­gle `Verticle`, runs on `http://localhost:8090` and pro­vides a few routes with pro­tected re­sources. [You can find the com­plete ex­am­ple 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 cre­ates 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 Key­cloak, since our app will try to fetch con­fig­u­ra­tion from Key­cloak. 83 | 请注意,您需要启动Keycloak,因为我们的应用程序将尝试从Keycloak获取配置。 84 | 85 | If the ap­pli­ca­tion is run­ning, just browse to: `http://localhost:8090/`. 86 | 如果应用程序正在运行,只需浏览到: `http://localhost:8090/` 。 87 | 88 | An ex­am­ple in­ter­ac­tion with the app can be seen in the fol­low­ing gif: 89 | 与应用程序的交互示例可以在以下 gif 中看到: 90 | 91 | 92 | 93 | 94 | 95 | ![](1.assets/image-20230217134340034.png) 96 | 97 | ![](1.assets/image-20230217134407502.png) 98 | 99 | ![](1.assets/image-20230217134503420.png) 100 | 101 | ![](1.assets/image-20230217134605835.png) 102 | 103 | ![](1.assets/image-20230217134651882.png) 104 | 105 | 106 | 107 | ### Router, SessionStore and CSRF Protection 路由器、会话存储和 CSRF 保护 108 | 109 | We start the con­fig­u­ra­tion of our web app by cre­at­ing a `Router` where we can add cus­tom han­dler func­tions for our routes. To prop­erly han­dle the au­then­ti­ca­tion state we need to cre­ate a `SessionStore` and at­tach it to the `Router`. The `SessionStore` is used by our OAuth2/OpenID Con­nect in­fra­struc­ture to as­so­ciate au­then­ti­ca­tion in­for­ma­tion with a ses­sion. By the way, the `SessionStore` can also be clus­tered if you need to dis­trib­ute 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 state­less but still want to sup­port clus­ter­ing, then you could pro­vide your own im­ple­men­ta­tion of a `SessionStore` which stores the ses­sion in­for­ma­tion as an en­crypted 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 pro­tected against CSRF at­tacks it is good prac­tice to pro­tect HTML forms with a CSRF token. We need this for our lo­gout form that we’ll see later. 125 | 为了防止CSRF攻击,最好使用CSRF令牌保护HTML表单。我们的注销表单需要这个,稍后会看到。 126 | 127 | To do this we con­fig­ure 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 reg­is­tered as a con­fi­den­tial OpenID Con­nect client with Au­tho­riza­tion Code Flow in Key­cloak, thus we need to con­fig­ure `client_id` and `client_secret`. Con­fi­den­tial clients are typ­i­cally used for server-side web ap­pli­ca­tions, where one can se­curely store the `client_secret`. You can find out more about[The dif­fer­ent Client Ac­cess Types](https://www.keycloak.org/docs/latest/server_admin/index.html#_access-type) in the Key­cloak doc­u­men­ta­tion. 147 | 我们的应用程序注册为机密的OpenID Connect客户端,在Keycloak中使用授权代码流,因此我们需要配置 `client_id` 和 `client_secret` 。机密客户端通常用于服务器端 Web 应用程序,其中可以安全地存储 `client_secret` 。您可以在 Keycloak 文档中找到有关不同客户端访问类型的更多信息。 148 | 149 | Since we don’t want to con­fig­ure things like OAuth2 / OpenID Con­nect End­points our­selves, we use Key­cloak’s OpenID Con­nect dis­cov­ery end­point to infer the nec­es­sary Oauth2 / OpenID Con­nect end­point 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 con­fig­ure our route han­dlers 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 han­dler ex­poses an un­pro­tected re­source: 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 ex­poses a sim­ple greet­ing page which shows some in­for­ma­tion about the user and pro­vides links to other pages. 224 | 我们的应用程序公开了一个简单的问候页面,该页面显示有关用户的一些信息,并提供指向其他页面的链接。 225 | 226 | The user greet­ing han­dler is pro­tected by the Key­cloak OAuth2 / OpenID Con­nect in­te­gra­tion. To show in­for­ma­tion about the cur­rent user, we first need to call the `ctx.user()` method to get an user ob­ject we can work with. To ac­cess the OAuth2 token in­for­ma­tion, we need to cast it to `OAuth2TokenImpl`. 227 | 用户问候处理程序受Keycloak OAuth2 / OpenID Connect集成的保护。为了显示有关当前用户的信息,我们首先需要调用 `ctx.user()` 方法来获取我们可以处理的用户对象。要访问 OAuth2 令牌信息,我们需要将其转换为 `OAuth2TokenImpl` 。 228 | 229 | We can ex­tract the user in­for­ma­tion like the user­name from the `IDToken` ex­posed by the user ob­ject via `user.idToken().getString("preferred_username")`. Note, there are many more claims like (name, email, give­nanme, fam­i­ly­name etc.) avail­able. The [OpenID Con­nect Core Spec­i­fi­ca­tion](https://openid.net/specs/openid-connect-core-1_0.html#Claims) con­tains a list of avail­able claims. 230 | 我们可以从用户对象通过 `user.idToken().getString("preferred_username")` 公开的 `IDToken` 中提取用户名等用户信息。请注意,还有更多可用的声明,例如(姓名,电子邮件,givenanme,姓氏等)。OpenID Connect Core 规范包含可用声明的列表。 231 | 232 | We also gen­er­ate a list with links to the other pages which are sup­ported: 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 han­dler shows in­for­ma­tion about the cur­rent 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 ex­poses a sim­ple admin page which shows some in­for­ma­tion for ad­mins, which should only be vis­i­ble for ad­mins. Thus we re­quire that users must have the `admin` realm role in Key­cloak to be able to ac­cess the admin page. 274 | 我们的应用程序公开了一个简单的管理页面,其中显示了一些管理员信息,这些信息应该只对管理员可见。因此,我们要求用户必须在 Keycloak 中具有 `admin` 领域角色才能访问管理页面。 275 | 276 | This is done via a call to `user.isAuthorized("realm:admin", cb)`. The han­dler func­tion `cb` ex­poses the re­sult of the au­tho­riza­tion check via the `AsyncResult res`. If the cur­rent user has the `admin` role then the re­sult is `true` oth­er­wise `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 ser­vices from our web app that are pro­tected via Bearer Au­then­ti­ca­tion. This means that we need a valid `access token` to ac­cess a re­source pro­vided on an­other server. 304 | 通常,我们需要从我们的 Web 应用调用通过持有者身份验证保护的其他服务。这意味着我们需要一个有效的 `access token` 来访问另一台服务器上提供的资源。 305 | 306 | To demon­strate this we use Key­cloak’s `/userinfo` end­point as a straw man to demon­strate back­end calls with a bearer token. 307 | 为了演示这一点,我们使用 Keycloak 的 `/userinfo` 端点作为稻草人来演示带有持有者令牌的后端调用。 308 | 309 | We can ob­tain the cur­rent valid `access token` via `user.opaqueAccessToken()`. Since we use a `WebClient` to call the pro­tected end­point, we need to pass the `access token` via the `Authorization` header by call­ing `bearerTokenAuthentication(user.opaqueAccessToken())` in the cur­rent `HttpRequest` ob­ject: 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 work­ing SSO login with au­tho­riza­tion, it would be great if we would allow users to lo­gout again. To do this we can lever­age the built-in OpenID Con­nect lo­gout func­tion­al­ity which can be called via `oAuth2Token.logout(cb)`. 342 | 现在我们获得了具有授权的工作 SSO 登录,如果我们允许用户再次注销,那就太好了。为此,我们可以利用内置的OpenID Connect注销功能,该功能可以通过 `oAuth2Token.logout(cb)` 调用。 343 | 344 | The han­dler func­tion `cb` ex­poses the re­sult of the lo­gout ac­tion via the `AsyncResult res`. If the lo­gout was suc­cess­full we destory our ses­sion via `ctx.session().destroy()` and redi­rect the user to the index page. 345 | 处理程序函数 `cb` 通过 `AsyncResult res` 公开注销操作的结果。如果注销成功,我们通过 `ctx.session().destroy()` 取消会话,并将用户重定向到索引页面。 346 | 347 | The lo­gout form is gen­er­ated via the `createLogoutForm` method. 348 | 注销表单通过 `createLogoutForm` 方法生成。 349 | 350 | As men­tioned ear­lier, we need to pro­tect our lo­gout form with a CSRF token to pre­vent [CSRF at­tacks](https://owasp.org/www-community/attacks/csrf). 351 | 如前所述,我们需要使用 CSRF 令牌保护我们的注销表单以防止 CSRF 攻击 . 352 | 353 | Note: If we had end­points that would ac­cept data sent to the server, then we’d need to guard those end­points with an CSRF token as well. 354 | 注意:如果我们的端点接受发送到服务器的数据,那么我们也需要使用 CSRF 令牌保护这些端点。 355 | 356 | We need to ob­tain the gen­er­ated `CSRFToken` and ren­der it into a hid­den form input field that’s trans­fered via HTTP POST when the lo­gout form is sub­mit­ted: 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 | + "
"; 383 | } 384 | ``` 385 | 386 | Some ad­di­tional plumb­ing: 一些额外的管道: 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 con­cludes the Key­cloak in­te­gra­tion ex­am­ple. 404 | Keycloak 集成示例到此结束。 405 | 406 | Check out the com­plete ex­am­ple in [keycloak-vertx Ex­am­ples 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 up­dates! If you want to learn more about Key­cloak, feel free to reach out to me. You can find me via [thomas­da­ri­mont on twit­ter](https://twitter.com/thomasdarimont). 410 | 感谢您抽出宝贵时间,请继续关注更多更新!如果您想了解有关Keycloak的更多信息,请随时与我联系。你可以通过twitter上的thomasdarimont找到我。 411 | 412 | Happy Hack­ing! 祝黑客愉快! --------------------------------------------------------------------------------