摘要
Vert.x最大的特点就在于异步(底层基于Netty),通过事件循环(EventLoop)来调起存储在异步任务队列(CallBackQueue)中的任务,大大降低了传统阻塞模型中线程对于操作系统的开销。因此相比较传统的阻塞模型,异步模型能够很大层度的提高系统的并发量。
Vert.x除了异步之外,还提供了非常多的吸引人的技术,比如EventBus,通过EventBus可以非常简单的实现分布式消息,进而为分布式系统调用,微服务奠定基础。除此之外,还提供了对多种客户端的支持,比如Redis,RabbitMQ,Kafka等等。
Vert.x异步也带来了编码上的复杂性,想要编写优美的异步代码,就需要对lambda表达式、函数式编程、Reactive等技术非常熟悉才行,否则很容易导致你的代码一团糟,完全没有可读性。另外,异步模型的性能调优、异常处理与同步模型有很大差异,网络中相关资料较少,使用中遇到问题排查困难,这也是目前国内架构师不愿意选择Vert.x的原因。
Vert.x运行在Java虚拟机上,支持多种编程语言,Vert.x是高度模块化的,同一个应用,你可以选择多种编程语言同时开发。在Vert.x 2版本,也就是基于JDK7,还没有lambda的时候,一般来讲,使用JavaScript作为开发语言相对较多,到Vert.x3的时代,因为JDK8的出现,Java已经作为Vert.x主流的开发语言,而Vert.x也被更多的开发者所接受。
本篇主要从五个方面对Vert.x进行介绍
3.Vert.x的简单介绍
4.Vert.x的一些优势
5.Vert.x的技术体系
Java能做的,Vert.x都能做。主要讨论,Vert.x善于做哪些事情!
(1)Web开发,Vert.x封装了Web开发常用的组件,支持路由、Session管理、模板等,可以非常方便的进行Web开发。
==不需要容器!不需要容器!不需要容器!==
(2)TCP/UDP开发,Vert.x底层基于Netty,提供了丰富的IO类库,支持多种网络应用开发。
==不需要处理底层细节(如拆包和粘包),注重业务代码编写。==
(3)提供对WebSocket的支持,可以做网络聊天室,动态推送等。
(4)Event Bus(事件总线)是Vert.x的神经系统,通过Event Bus可以实现分布式消息,远程方法调用等等。
正是因为Event Bus的存在,Vert.x可以非常便捷的开发微服务应用。
(5)支持主流的数据和消息的访问 redis mongodb rabbitmq kafka 等
(6)分布式锁,分布式计数器,分布式map的支持
下面实现一个HttpServer,实现使用浏览器访问,给浏览器返回一个 SUCCESS的功能来快速感受下Vert.x。下面这个类可以直接运行,而不需要使用tomcat进行部署,就可以直接通过浏览器来进行访问。非常类似于nodejs,但是比nodejs的性能要高很多。
请求的地址为:http://localhost。更详细的例子可以参考Vert.x 创建HTTP服务
public class MyHttpServer extends AbstractVerticle {
public static void main(String[] args) {
// 创建服务
MyHttpServer verticle = new MyHttpServer();
Vertx vertx = Vertx.vertx();
// 部署服务,会执行MyHttpServer的start方法
vertx.deployVerticle(verticle);
}
@Override
public void start() throws Exception {
// 在这里可以通过this.vertx获取到当前的Vertx
Vertx vertx = this.vertx;
// 创建一个HttpServer
HttpServer server = vertx.createHttpServer();
server.requestHandler(request -> {
// 获取到response对象
HttpServerResponse response = request.response();
// 设置响应头
response.putHeader("Content-type", "text/html;charset=utf-8");
// 响应数据
response.end("SUCCESS");
});
// 指定监听80端口
server.listen(80);
}
}
简单说,Vert.x就是一堆的jar包,提供了一系列的编程API接口。通过这些API,可以实现异步编程。
快速理解异步编程
什么是异步编程?异步编程是Vert.x的一大特性,也是Vert.x的核心,异步编程非常好理解,写过ajax吗,看下面代码,$.ajax方法并不会阻塞,而是直接向下执行,等到远程服务器响应之后,才会回调success方法,那么这时候success方法才会执行。
ajax下面的代码不会等到success方法执行完毕之后再执行,这就是所谓的异步。
console.log("1");
$.ajax({
"url" : "/hello",
"type" : "post",
"dataType" : "json",
"success" : function(val) {
console.log("2");
}
});
console.log("3");
Vert.x可以开发Web应用,但Vert.x不仅仅是一个Web开发框架,他更像Spring,是一个技术栈(Vert.x生态可以查看https://github.com/vert-x3/vertx-awesome),或者说是一个Vert.x生态体系。在这个体系中,Vert.x只是提供了Web开发的能力。
下面对Vertx和Spring做一个对比:
| 项目 | Spring | Vertx | | -------- | ----------------- | ------------------ | | 核心框架 | spring-core | vertx-core | | Web开发 | spring-webmvc | vertx-web | | jdbc框架 | spring-jdbc | vertx-jdbc-client | | redis | spring-data-redis | vertx-redis-client | | 微服务 | spring-cloud | vertx-hazelcast |
可以说,很多的spring能做的事情,Vertx也都能实现。那么既然如此,Spring如此强大,社区如此活跃,为何还会有Vertx呢?他们之前区别的核心点就只有一个,Spring的操作是同步的,Vertx的操作是异步的。异步带来了更高的性能,但同时也带来了编码和调试的复杂度,但不得不说异步可能是未来的一个趋势,至少在Java实现高性能服务器上的一个趋势。
在Java领域,做Web开发我们一般有很多的选择,比如使用原生的Servlet,比如使用SpringMVC,再比如使用Struts等等总之你有很多的选择。在国内,目前来讲,SpringMVC作为Spring体系下的Web层架构,是深受企业青睐的,绝大部分的企业可能都在使用SpringMVC。而对于我们今天要说的Vert.x这个Web层框架,却很少有人知道,但它却是仅次于SpringMVC,排名第二的一个Web层框架。
Vert.x就像是跑在JVM之上的Nodejs,所以Vert.x的第一个优势就是这是一个异步非阻塞框架。上面也提到了异步,我们使用ajax来演示的异步,下面使用Vert.x请求远程地址一个代码,可以看到和ajax非常像!
System.out.println("1")
WebClient
.create(vertx)
.postAbs(REQUEST_URL) // 这里指定的是请求的地址
.sendBuffer(buffer, res -> { // buffer是请求的数据
if (res.succeeded()) {
// 请求远程服务成功
System.out.println("2")
} else {
// 请求失败
resultHandler.handle(Future.failedFuture("请求服务器失败..."));
}
});
System.out.println("3")
这段代码的执行效果和上面的JavaScript执行的结果是类似的,同样是先打印 1,再打印 3,最后打印 2。
异步也是Vert.x于其他的JavaWeb框架的主要区别。我们这里先不去讨论异步的优势与他的实现原理,只要先知道,Vert.x和JavaScript一样,是一个异步执行的就可以了。
Vert.x和JDK8
Vert.x必须运行在JDK8上,JDK8提供了lambda表达式,可以简化匿名内部类的编写,可以极大的挺高代码的可读性。
上面的代码中看到 在 sendBuffer 这一行里有一个 -> 这种形式。这个是Java代码吗? 是的。是JDK8提供的lambda表达式的形式。用于简化匿名内部类的开发。有兴趣的朋友可以了解一下lambda表达式,在使用Vertx进行项目开发时有大量的匿名内部类,因此很多情况会用到。
Vert.x有一个口号大概是:“我们不去评判那个编程语言更好,你只要选择你想要使用的语言就可以了”。也就是说,在Vert.x上,可以使用JavaScript,Java,Scala,Ruby等等,下面是官网的一个截图
Vert.x的底层依赖Netty,因此在使用Vert.x构建Web项目时,不依赖中间件。像Node一样,可以直接创建一个HttServer。就像我们上面第一个例子,可以直接运行main方法,启动一个Http服务,而不需要使用类似于Tomcat的中间件。不依赖中间件进行开发,相对会更灵活一些,安全性也会更高一些。
Vert.x和Spring的对比,有一种使用MacOS和Windows对比的感觉。Vert.x和庞大的Spring家族体系不同,Vert.x提供数据库操作,Redis操作,Web客户端操作,NoSQL数据库的一些操作等常用的结构,很清新,很简洁,但足够使用。下面是从官网截取的一个提供的客户端工具。
Vert .x提供了各种组件来构建基于微服务的应用程序。通过EventBus可以非常容易的进行服务之间的交互。并且提供了HAZELCAST来实现分布式。
当然了,除了一些优势以外,要在项目中选择使用Vert.x还要考虑一些问题,这里不展开说明,只是根据个人的使用经验提出一些点。
上面也提到了,Vert.x和Spring一样,也有着完善的生态,具体可以查看https://github.com/vert-x3/vertx-awesome 我们可以看到,每一块内容都提供了多种的实现,有官方支持的版本还有社区版本。下面我们具体介绍下技术体系中官方支持的版本。
Vert.x核心模块包含一些基础的功能,如HTTP,TCP,文件系统访问,EventBus、WebSocket、延时与重复执行、缓存等其他基础的功能,你可以在你自己的应用程序中直接使用。可以通过vertx-core模块引用即可。
### 1.5.2 Web模块
Vert.x Web是一个工具集,虽然核心模块提供了HTTP的支持,但是要开发复杂的Web应用,还需要路由、Session、请求数据读取、Rest支持等等还需要Web模块,这里提供了上述的这些功能的API,便于开发。
除了对Web服务的开发以外,还提供了对Web客户端请求的支持,通过vertx-web-client即可方便的访问HTTP服务。有朋友可能会有疑惑,我明明可以使用JDK提供的URL来请求HTTP服务啊。使用Vert.x一定要注意,Vert.x是一个异步框架,请求HTTP服务是一个耗时操作,所有的耗时,都会阻塞EventBus,导致整体性能被拖垮,因此,对于请求Web服务,一定要使用Vert.x提供的vertx-web-client模块
### 1.5.3 数据访问模块
Vert.x提供了对关系型数据库、NoSQL、消息中间件的支持,传统的客户端因为是阻塞的,会严重影响系统的性能,因此Vert.x提供了对以上客户端的异步支持。具体支持的数据访问如下:
MongoDB client,JDBC client,SQL common,Redis client,MySQL/PostgreSQLclient
### 1.5.4 Reactive响应式编程
复杂的异步操作,会导致异步回调地狱的产生,看下面的代码,这是我在Vert.x提供的例子中找到的,我们不去管这段代码干了啥,只是看后面的}就很惊讶了,如果操作更为复杂一些,会嵌套的层次更多,通过reactive可以最小化的简化异步回调地狱。
// create a test table
execute(conn.result(), "create table test(id int primary key, name varchar(255))", create -> {
// start a transaction
startTx(conn.result(), beginTrans -> {
// insert some test data
execute(conn.result(), "insert into test values(1, 'Hello')", insert -> {
// commit data
rollbackTx(conn.result(), rollbackTrans -> {
// query some data
query(conn.result(), "select count(*) from test", rs -> {
for (JsonArray line : rs.getResults()) {
System.out.println(line.encode());
}
// and close the connection
conn.result().close(done -> {
if (done.failed()) {
throw new RuntimeException(done.cause());
}
});
});
});
});
});
});
再看一个使用Reactive2构建的多步操作的代码,paramCheckStep,insertPayDtlStep,requestStep等等都是异步方法,但这里就很好的处理了异步回调的问题,不再有那么多层的大括号,代码结构也geng
public void scanPay(JsonObject data, Handler> resultHandler) {
paramCheckStep(data) // 参数校验
.flatMap(this::insertPayDtlStep) // 插入流水
.flatMap(x -> requestStep(x, config)) // 请求上游
.flatMap(this::cleanStep) //参数清理
.subscribe(ok -> {
logger.info("成功结束");
resultHandler.handle(Future.succeededFuture(ok));
},
err -> {
logger.error("正在结束", err);
resultHandler.handle(Future.failedFuture(err));
}
);
}
邮件客户端
Vert.x提供了一简单STMP邮件客户端,所以你可以在应用程序中发送电子邮件。
STOMP客户端与服务端
Vert.x提供了STOMP协议的实现包括客户端与服务端。
Consul Client
consul是google开源的一个使用go语言开发的服务发现、配置管理中心服务。内置了服务注册与发现框 架、分布一致性协议实现、健康检查、Key/Value存储、多数据中心方案。
RabbitMQ Client Kafka Client
消息队里的客户端支持
JCA适配器
Vert.x提供了Java连接器架构适配器,这允许同任意JavaEE应用服务器进行互操作。
Vert.x提供了简单API用于在应用中提供认证和授权。
Auth common 通用的认证API,可以通过重写AuthProvider类来实现自己的认证
JDBC auth 后台为JDBC的认证实现
JWT auth 用JSON Web tokens认证实现
Shiro auth 使用Apache Shiro认证实现
MongoDB auth MongoDB认证实现
OAuth 2 Oauth2协义认证实现
htdigest auth 这个是新增一种认证的支持
Vert.x提供多个组件构建基于微服务的应用程序。
比如服务发现(Vert.x Service Discovery)、断路器(Vert.x Circuit Breaker)、配置中心(Vert.x Config)等。
Vert.x简明介绍就说到这里,最后,技术是为业务服务的,在选择架构的时候,也要考虑用人的成本,也正是因为如此,国内使用Vert.x的企业还不是很多。但是我相信,未来,一定是异步非阻塞的天下。
Vert.x底层通信框架依赖于Netty,并封装了对Http协议的支持,因此可以非常方便的进行Web开发,且不依赖于任何中间件。笔者所在的公司老系统使用的是SSM架构的项目,部署在Weblogic上,每年花在中间件上的钱就非常多,现在全面改造为Vert.x,中间件的费用直接就省了。另外不依赖中间件,编程会变得非常灵活,定制性非常强,安全性也会得到一定层度的提高。
对于实现一个简单的web服务,有很多种选择,简单划为三种
1.这是使用最多的一种,也是很多的Java开发者可能最先想到的,就是使用Java中间件来实现,只要下载一个Tomcat,再编写个web项目就可以
对外提供Web服务。这种方式我们完全不需要考虑线程的交互,不需要考虑网络协议,只需要关注业务逻辑,可以说是一种全包的形式。
2.使用Java原生的Socket或者NIO来实现,但这种方式比较复杂,对编程功底要求最高,而且自己实现很难保证性能和稳定性。这种方式完全需要手动处理,很少会使用这种方式来实现HTTPServer,可以说这是最为原始形式。
3.介于全包和原始形式之间,就是用第三方的框架来实现,比如Vertx或者偏底层的Netty。你可以利用框架封装的API简单的创建一个HttpServer,并不需要关注底层的通信协议。这种方式更为灵活,一般来讲性能也更高,并且不依赖中间件。
下面简单的来实现一个HttpServer,通过浏览器访问这个HttpServer能够在浏览器上显示HelloWorld。
下面简单列出实现的步骤:
下面是具体的代码
io.vertx
vertx-core
3.5.2
package com.stu.vertx.hello;
import io.vertx.core.Vertx;
import io.vertx.core.http.HttpServer;
import io.vertx.core.http.HttpServerResponse;
/**
* Vertx 创建一个最简单的HttpServer,当用户请求时返回Hello World
* @author lenovo
*/
public class MyHttpServer {
public static void main(String[] args) {
Vertx vertx = Vertx.vertx();
// 创建一个HttpServer
HttpServer server = vertx.createHttpServer();
server.requestHandler(request -> {
// 获取到response对象
HttpServerResponse response = request.response();
// 设置响应头
response.putHeader("Content-type", "text/html;charset=utf-8");
// 响应数据
response.end("Hello World");
});
server.listen(8888); // 监听8888端口
}
}
代码非常简单,首先获取到Vertx对象,然后通过vertx对象创建Http服务,监听Http请求并进行处理。
这里用到了JDK8的新特性,第一个是Vertx.vertx()方法,我们知道Vertx是一个接口,按照我们以前的逻辑,方法实现是不能写在接口中的,在JDK8中增加了静态方法和默认方法。第二个是->这个符合,这也是一个新特性,可能看起来比较难懂,可以类比JavaScript来理解,JavaScript中有很多这样的写法。我们可以看到,这个方法实际上是接收一个Handle接口,Handle接口中有一个抽象方法 public void handle(HttpServerRequest request) ,这个方法有一个参数 HttpServerRequest .按照我们之前的写法应该写成如下代码:
server.requestHandler(new Handler() {
@Override
public void handle(HttpServerRequest request) {
// 获取到response对象
HttpServerResponse response = request.response();
// 设置响应头
response.putHeader("Content-type", "text/html;charset=utf-8");
// 响应数据
response.end("Hello World");
}
});
对比两者可以发现,实际上就是简化了new 子类,简化了重写方法,直接把方法的参数后跟->{方法体}来解决。这样写起来是非常方便的。
但是这种写法一个接口中只能定义一个抽象方法。这种接口一般会打上@FunctionalInterface注解。
上面那段代码是直接写到main方法中的,可以直接运行即可。监听的是8888端口,在启动的过程中,要保证8888端口不被占用。
启动成功之后可以直接通过浏览器访问。
浏览器访问 localhost:8888
到这里,一个简单的Vertx程序就写完了。
在上面演示的创建HttpServer的方式我们会发现有一个很大的问题,就是所有的代码都写在main方法中,这样显然是不好的。Vert.x推荐的写法是为每一个应用创建一个Verticle,也就是Vert.x的模块,然后通过Vertx核心接口,部署Verticle模块。多个Verticle模块之间可以通过EventBus进行相互调用。
这里提到了Verticle、EventBus等,这些都是Vert.x中一些非常重要的概念。Verticle可以简单的理解为继承了AbstractVerticle的类都是一个Verticle,每个Verticle可以单独部署或者单独运行。EventBus看不见,摸不着,可以简单的理解为,各Verticle模块之间通信的桥梁。
下面我们就创建一个HttpServer的Verticle模块,并部署到Vertx中运行,实现步骤如下:
1.创建一个类,继承AbstractVerticle类 2.重写start方法和stop方法,在start方法中处理业务逻辑,stop方法中释放资源 3.在main方法中部署Verticle模块 4.启动服务,并通过浏览器进行访问
这里我们只需要粘贴Verticle核心代码:
public class MyHttpServer2 extends AbstractVerticle {
public static void main(String[] args) {
MyHttpServer2 verticle = new MyHttpServer2();
Vertx vertx = Vertx.vertx();
// 部署verticle,这里会调用start方法
vertx.deployVerticle(verticle);
}
@Override
public void start() {
// 在这里可以通过this.vertx获取到当前的Vertx
Vertx vertx = this.vertx;
// 下面可以实现helloworld中相同的功能
// 创建一个HttpServer
HttpServer server = vertx.createHttpServer();
server.requestHandler(request -> {
// 获取到response对象
HttpServerResponse response = request.response();
// 设置响应头
response.putHeader("Content-type", "text/html;charset=utf-8");
// 响应数据
response.end("Hello World");
});
server.listen(8889);
}
@Override
public void stop() throws Exception {
super.stop();
}
}
上面代码比较简单,start方法的实现和第一个演示中的main方法代码非常相似,start方法中之所以可以直接使用vertx这个变量,是因为继承了AbstractVerticle这个类,在这个类里有一个protected类型的Vertx变量。
另外在main方法中,获取Vertx实例是通过Vertx.vertx()方法,查看代码会发现,Vertx是一个接口,按照之前的逻辑,接口中只能定义抽象方法,这是什么情况呢?这也是JDK8的新特性,接口中可以定义静态方法。也就是说我们调用的是接口中的静态方法,也就是已经实现了的方法。在Vertx中,这种方式非常常见。
这种编码方式是Vertx官方所支持的方式,后面的案例都会通过这种方式来进行编写。
到这里,我们了解了创建HttpServer的API,我们发现,Vertx的API还是非常简洁,好用的。但本节的重点还是Verticle模块的理解和编写。
当然了,如果要真正的实现一个HttpServer,还需要更多的东西,比如路由,Session,Rest接口开发,模板,WebSocket等等,后续我们会陆续整理后发布!
在Vert.x 创建HTTP服务 中我们已经创建了一个简单的HttpServer,但这个HttpServer比较低级,对于请求参数解析、Session等常用功能都需要我们通过编码实现,也就是要重复造轮子,非常不方便。
Vert.x提供了Web开发组件vertx-web,提供了一堆Web开发中常用的功能。比如参数封装,路由,国际化,认证和授权,session和cookie以及模板等,可以非常方便的进行Vert.x Web开发。
本篇主要介绍Web开发中的路由功能,路由简单说就是把用户请求交给合适的处理器处理的组件。如下图所示
路由是Web开发中最基础也是最常用的功能,Vert.x提供了强大的路由功能,包括正则匹配,二级路由等。本文从两个方面来讲述路由,分别是路由的使用和路由的实现原理(源码阅读)
1.在pom文件中,加入vertx-web的依赖包
io.vertx
vertx-web
3.5.2
2.创建一个HttpServer
/**
* 简单的路由使用
*
* @author lenovo
*
*/
public class SimpleRouter extends AbstractVerticle {
@Override
public void start() throws Exception {
// 创建HttpServer
HttpServer server = vertx.createHttpServer();
// 创建路由对象
Router router = Router.router(vertx);
// 监听/index地址
router.route("/index").handler(request -> {
request.response().end("INDEX SUCCESS");
});
// 把请求交给路由处理--------------------(1)
server.requestHandler(router::accept);
server.listen(8888);
}
public static void main(String[] args) {
Vertx.vertx().deployVerticle(new SimpleRouter());
}
}
上面这段代码还是比较好理解的,主要就是在Vert.x 创建HTTP服务的基础上增加了Router,并且最后把请求的处理交给Router来处理。这样当我们访问服务器时,就会根据匹配规则,找到对应的处理器来进行处理。
这里有一个地方,就是在代码中标(1)的部分,router::accept这个可能很多朋友不理解,这个也是JDK8的一个新特性,实际上就是一个语法糖,下面是一段不使用JDK8的代码,想必大家看了下面这段代码就都名白了。router是我们创建的router对象,然后把回调的值传给router对象的accept方法。
server.requestHandler(new Handler() {
@Override
public void handle(HttpServerRequest event) {
router.accept(event);
}
});
通过比较,我们会发现,使用JDK8 的新特性,代码上还是相对会简洁很多的。
学习都有一个2.8原则,就是说学习80%的知识可能只用20%的时间,路由也是一样,通过上面这些你实际上已经可以去进行路由了,所以也算是路由的80%的东西了,剩下的20%你可能需要花费更多的时间。
HTTP协议中定义的请求方法有GET POST PUT DELETE等,我们之前通过在浏览器地址栏输入的都是get请求,如果我们要限制,只能使用POST请求该如何处理呢?也非常简单,可以直接通过router对象提供了post方法来进行路径的匹配。
router.post("/post").handler(request -> {
request.response().end("post");
});
当我们在浏览器直接请求时,你会看到如下结果
当你使用POST发送时,结果如下:
除了post方法以外,还可以使用put,get等方法。除了直接使用这些方法以外,还有另外一种形式,也可以指定请求的方法,route方法有个重载,第一个参数是HttpMethdo,第二个参数是匹配的URL路径,如下:
router.route(HttpMethod.GET, "/method").handler(request -> {
request.response().end("method");
});
HttpMethod是一个枚举类,可用的枚举值如下
@VertxGen
public enum HttpMethod {
OPTIONS, GET, HEAD, POST, PUT, DELETE, TRACE, CONNECT, PATCH, OTHER
}
我们经常会遇到一些情形,比如要对产品做增删改查,我们可能会有如下路由规则的定义
Router restAPI = Router.router(vertx);
restAPI.get("/products/:productID").handler(rc -> {
// TODO Handle the lookup of the product....
rc.response().write(productJSON);
});
restAPI.put("/products/:productID").handler(rc -> {
// TODO Add a new product...
rc.response().end();
});
restAPI.delete("/products/:productID").handler(rc -> {
// TODO delete the product...
rc.response().end();
});
这样虽然能够满足我么的要求,但是,如果比较多的话明显的看起来比较乱,Vertx给我们提供了二级路由来解决这个问题。我们可以直接把上面的restAPI这个作为子路由,挂载到一个主路由中。也就是说,我们创建一个主路由,然后把上面的restAPI这个路由规则通过mount方法挂载就可以了。
mainRouter.mountSubRouter("/productsAPI", restAPI);
我们可能会配置很多的路由规则,而Vert.x进行路由匹配的规则非常简单,默认就是当匹配成功之后就不再继续匹配了。
比如我们有如下代码
router.route("/index/*").handler(request -> {
request.response().end("Index");
});
router.route("/index/main").handler(request -> {
request.response().end("IndexMain");
});
当我们访问:http://localhost:8080/index/main 的时候,我们希望匹配显然是下面的,我们希望返回的是 IndexMain,而实际情况是返回是Index。为了解决这个问题,我们有两种办法,第一个是把下面的代码放到上面,但这样可能也并不符合我们的习惯,还有另外一种方式就是制定order。
router.route("/index/*").order(2).handler(request -> {
request.response().end("Index");
});
router.route("/index/main").order(-1).handler(request -> {
request.response().end("IndexMain");
});
还有一种情况,如果想要所有匹配的规则都执行,可以通过requestContext 的next方法
router.route("/index/*").order(2).handler(request -> {
// request.response().end("Index");
System.out.println(1);
request.next(); // 调下一个匹配规则
});
router.route("/index/main").order(-1).handler(request -> {
// request.response().end("IndexMain");
System.out.println("2");
request.next(); // 调下一个匹配规则
});
用户经常会上传一些参数,那么我们在Vertx中如何接收用户上传的参数呢?
第一种就是get请求直接拼接在URL后的参数,比如:http://localhost:8080/method?param=hello
router.route(HttpMethod.GET, "/method").handler(request -> {
String param = request.request().getParam("param");
System.out.println("接收到用户传递的参数为:" + param);
request.response().end("method");
});
第二种是获取路径的参数,比如:http://localhost:8080/method/xiaoming/xm123
// 获取参数
router.route(HttpMethod.GET, "/method/:user/:pass").handler(request -> {
String user = request.request().getParam("user");
String pass = request.request().getParam("pass");
request.response()
.putHeader("Content-type", "text/html;charset=utf-8")
.end("接收到的用户名为:" + user + " 接收到的密码为:" + pass);
});
第三种是获取到请求体中的数据,也就是post提交的数据。这个稍微有一些繁琐,首先要指定一个BodyHandle,然后才能通过requestContext对象来获取body体中的数据。
router.route().handler(BodyHandler.create()); // 在路由的最前面,指定body的处理器
获取body体的数据的方法有很多,可以获取到字符串,可以直接转成Json对象等等,下面是直接读取到字符串
router.post("/post").handler(request -> {
String res = request.getBodyAsString(); // 获取到body体的数据
System.out.println(res);
request.response().end("post");
});
上面的代码,我们把接收用户请求和处理用户请求的逻辑放到了一个类里,这显然是不好的,会导致代码的结构不清晰,多个人在一起维护的时候变得困难。
我们可以把具体的请求处理逻辑和监听分开,使用单独的类来做业务的处理,这非常简单。我们来重写最开始处理index监听的代码
1.创建一个类,实现Handler接口
public class IndexHandle implements Handler {
@Override
public void handle(RoutingContext event) {
event.response().end("IndexController");
}
}
2.修改监听,在监听到index请求时,将处理器指定为IndexHandle
// 监听/index地址
router.route("/index").handler(new IndexHandle());
这里的Handle是不是看起来就像我们之前所写的Controller,当然,这就是Controller。
为了处理index地址的请求,我们创建了一个IndexHandler。对于不同的业务,可以创建不同的处理器来处理,但往往还有一些,比如静态资源处理,Session处理器,请求体处理器。像这些处理器几乎在所有的Web开发场景中都会使用到,而且这些处理器和业务无关,所以每次写就会重复造轮子,官方已经给我们提供了非常多的好用的处理器。如下图所示
如果你熟读源码,你会发现,Vert.x体系结构设计的非常美妙,有着非常好的扩展性。Vert.x的Web模块就可以看做是对核心模块的扩展,下面就看看Web模块中是如何实现路由功能的。
上面已经用过路由了,代码非常简单,为了便于大家的阅读,再次贴出来核心代码,如下
// 创建HttpServer
HttpServer server = vertx.createHttpServer();
// 创建路由对象
Router router = Router.router(vertx);
// 监听/index地址
router.route("/index").handler(request -> {
request.response().write(Buffer.buffer("INDEX SUCCESS")).end();
});
// 把请求交给路由处理
server.requestHandler(router);
这里先跟大家解释下,Vertx路由的使用,也就是本文上半部分的内容,是在18年7月份写的,而源码部分是在2019年4月份写的,在这个阶段里,Vert.x经历了几个版本的更新,API也有些许改变。其中,router就有了改版,当然新的版本也是兼容旧的版本的。之前把请求交给路由处理是这么写的
在新版本中已经提示过期了,新版本中的写法就是上面代码中的写法,直接把router传进入就可以了,不再需要使用方法引用的形式。
我们源码的阅读就从这里开始!
requestHandler接收的参数类型是 Handler ,方法的定义如下
HttpServer requestHandler(Handler handler);
requestHandler可以直接接收router作为实参,说明router就一定是Handler 类型,
看router类的定义,果然如我们所猜想,如下
public interface Router extends Handler {}
既然继承了Handler接口,就一定需要实现Handler接口中定义的方法,而Handler接口中只定义了一个方法,就是
void handle(E event);
当请求进来的时候,会找到requestHandler,进而调用Handler接口的handle方法,
下面就要看下Router实现类的handle方法的实现了
@Override
public void handle(HttpServerRequest request) {
if (log.isTraceEnabled()) log.trace("Router: " + System.identityHashCode(this) +
" accepting request " + request.method() + " " + request.absoluteURI());
new RoutingContextImpl(null, this, request, routes).next();
}
这里核心的代码就是创建了一个RoutingContext的实例,并调用其next方法,直接进入到next方法
@Override
public void next() {
if (!iterateNext()) {
checkHandleNoMatch();
}
}
凭我们使用路由的经验,我们可以指定多个路由,且每个路由都可以指定order,且在单个route中可以调用next方法来执行下一个匹配的路由,因此这里相当于一个路由链,通过next方法关联起来。
而iterateNext就是在执行匹配到的路由链,直到执行完毕最后一个,进入到checkHandlerNoMatch方法。
其实路由的核心就在于这个iterateNext方法的实现,代码比较多,分开来看,先看一部分,如下
while (iter.hasNext()) { // Search for more handlers
RouteImpl route = iter.next();
currentRouteNextHandlerIndex.set(0);
currentRouteNextFailureHandlerIndex.set(0);
try {
if (route.matches(this, mountPoint(), failed)) {
if (log.isTraceEnabled()) log.trace("Route matches: " + route);
try {
currentRoute = route;
if (log.isTraceEnabled()) log.trace("Calling the " + (failed ? "failure" : "") + " handler");
if (failed && currentRoute.hasNextFailureHandler(this)) {
currentRouteNextFailureHandlerIndex.incrementAndGet();
route.handleFailure(this);
} else if (currentRoute.hasNextContextHandler(this)) {
currentRouteNextHandlerIndex.incrementAndGet();
route.handleContext(this);
} else {
continue;
}
} catch (Throwable t) {
if (log.isTraceEnabled()) log.trace("Throwable thrown from handler", t);
if (!failed) {
if (log.isTraceEnabled()) log.trace("Failing the routing");
fail(t);
} else {
// Failure in handling failure!
if (log.isTraceEnabled()) log.trace("Failure in handling failure");
unhandledFailure(-1, t, route.router());
}
}
return true;
}
} catch (Throwable e) {
if (log.isTraceEnabled()) log.trace("IllegalArgumentException thrown during iteration", e);
// Failure in matches algorithm (If the exception is instanceof IllegalArgumentException probably is a QueryStringDecoder error!)
if (!this.response().ended())
unhandledFailure((e instanceof IllegalArgumentException) ? 400 : -1, e, route.router());
return true;
}
}
iter是成员变量
protected Iterator iter;
是routes的迭代器
this.iter = routes.iterator();
那么while(iter.hasNext()) 就是要迭代所有的route,然后进行匹配,进而为匹配到的route,执行route的handle方法。
如果一切条件成立,执行
进入到handleContext方法,代码如下
void handleContext(RoutingContextImplBase context) {
Handler contextHandler;
synchronized (this) {
contextHandler = contextHandlers.get(context.currentRouteNextHandlerIndex() - 1);
}
contextHandler.handle(context);
}
先获取到当前的处理器,调用处理器的handle方法,也就是第一部分中我们所写的回调方法,类似于这种
这里所写的request,就是上面传入的context,也就是RoutingContext对象。
到这里,我们已经完成了一个从请求过来,交给路由处理,并执行处理器方法这么一整个流程。
你对路由的理解是不是更深刻了呢?
对于Java开发人员,想要实现一个http服务,非常简单,写个servlet,打成war包,放到tomcat下就能运行。但如果要实现一个tcp服务就没那么简单了,因为tcp是传输层协议,并不像http那样,有类似tomcat的中间件给我们封装底层的网络协议,封装线程的交互。要实现一个tcp服务,只能自己动手处理网络和线程问题,这是非常考验编程功底的事情,而且如果团队人员素质不高,项目周期要求较短的情况下,是很难开发一个稳定的tcp服务的。
http报文格式
很多朋友可能会有疑惑,http协议挺好用的啊,为什么要用tcp服务?要知道,http是在tcp协议的基础上封装的应用层协议。这也就意味着http协议的性能一定是比tcp要低的多的(从上图可以看到,除了请求数据外有大量的头信息等)。另外http协议自身的特性,http是一个半双工且无状态的协议,因此很多的场景http协议是无法满足的。举一个最简单的例子,我们都用过打车软件,我们发个打车的请求,很多的司机端都能够快速的接收到。如果使用http协议是实现不了的(当然可以使用websocket,这里不具体讨论),即使是通过类似于ajax轮询的手段实现,性能也都会非常低,并且实时性不好,对服务器压力大等问题,这个时候tcp协议就出场了。
再考虑另外一个场景,考驾照的时候,教练车上都有一个打卡的机器(暂且称为终端机),那么打卡的机器需要和远程的服务器连接,实时传输学员练车的动态数据,如果使用http协议,不仅仅对于终端机编程(一般采用C语言,http是在tcp之上封装的协议,所以如果使用http协议会比较繁琐)要求较高,而且http协议会有很多无用的数据(http是一种通用的协议,为了实现通用,因此http头信息有很多是不用的,且http协议是一个纯文本的协议,相比较采用二进制协议,报文会比较大),浪费带宽。所以一般类似于嵌入式设备和应用服务数据交互也都会使用tcp协议。
有朋友可能会提到使用netty也可以实现tcp服务啊,当然,netty相比较使用传统的bio或者使用原生nio都要简单的多,而且解决了粘包拆包的问题,也封装了线程模型。确实,使用netty开发tcp服务器是目前很多企业的选择,但netty入门也相对比较复杂,学习成本较高。这里给大家推荐一个异步框架,Vertx,它的底层是基于netty的,使用Vertx开发tcp服务,非常的简单。
vertx开发tcp服务非常的简单,整体可以分为以下几个步骤
下面简单贴下代码,完整代码可以见GitHub,地址为:https://github.com/happy-fly/vertx
(写博客的时候,最新版本为3.6.0,写测试案例的时候最新版是3.5.4):
io.vertx
vertx-core
3.6.0
// 创建TCP服务器
NetServer server = vertx.createNetServer();
// 处理连接请求
server.connectHandler(socket -> {
socket.handler(buffer -> {
// 在这里应该解析报文,封装为协议对象,并找到响应的处理类,得到处理结果,并响应
System.out.println("接收到的数据为:" + buffer.toString());
// 按照协议响应给客户端
socket.write(Buffer.buffer("Server Received"));
});
// 监听客户端的退出连接
socket.closeHandler(close -> {
System.out.println("客户端退出连接");
});
});
// 监听端口
server.listen(5555, res -> {
if (res.succeeded()) {
System.out.println("服务器启动成功");
}
});
这里的逻辑非常简单,就是输出接收到客户端请求的数据,然后告诉客户端,Server Received。
下面是客户端核心代码:
// 创建一个TCP客户端
NetClient client = vertx.createNetClient();
// 连接服务器
client.connect(5555, "localhost", conn -> {
if (conn.succeeded()) {
NetSocket s = conn.result();
// 向服务器写数据
s.write(Buffer.buffer("hello"));
// 读取服务器的响应数据
s.handler(buffer -> {
System.out.println(buffer.toString());
});
} else {
System.out.println("连接服务器异常");
}
});
客户端的实现也很简单,向服务端发送消息:hello,等待服务端的响应。
这里实现的比较简单,真实业务场景远比此要复杂的多,根据应用场景我们一般需要定义自己的协议,服务端在接收到客户端请求的时候需要根据自己定义的协议进行拆包,并封装为请求对象,然后调用具体的代码进行业务逻辑处理,待处理完毕需要将处理器处理的结果进行封包,发送给客户端。对于客户端的实现也是同样,接收到服务端的请求之后,根据协议进行拆包,处理完毕再把响应消息封包之后发给服务器。
关于Vertx创建tcp服务就完成了,是不是比起来使用bio,nio,netty都要简单的多。
Vert.x(vertx) 连接MySQL、Oracle数据库
Vert.x提供异步访问数据库的API,可能这里有朋友会有疑惑,直接使用我们之前的熟悉的Mybatis或者Hibernate不行吗,可行,但数据库操作是一个耗时操作,使用传统的同步模型,容易阻塞线程,导致整体性能下降,因此我们对于数据库操作,需要使用Vert.x提供的异步API。
Vert.x提供的API层级非常低,可以说是仅仅在原生JDBC基础上封装了一层异步接口。所有的对数据库操作都需要通过编写SQL来完成,参数的封装和结果的获取都需要手动的来实现,对于习惯使用ORM框架的开发者可能会非常的不习惯。
先来通过一个查询数据库的案例来演示如何使用Vert.x提供的异步API
我们需要引入两个包,一个是vertx-jdbc,另一个是要真正连接数据库的驱动包,这里以MySQL为例
io.vertx
vertx-jdbc-client
3.6.0
mysql
mysql-connector-java
8.0.13
注:2019-09-25 更新,在新的版本中 3.8.1,需要引入sql-common的包,否则会有类找不到。
io.vertx
vertx-sql-common
3.8.1
抽象出一个DbUtils来方便获取数据库客户端,为了简单,直接就将配置写到代码里了
public class JdbcUtils {
// 用于操作数据库的客户端
private JDBCClient dbClient;
public JdbcUtils(Vertx vertx) {
// 构造数据库的连接信息
JsonObject dbConfig = new JsonObject();
dbConfig.put("url", "jdbc:mysql://192.168.40.66:3306/test");
dbConfig.put("driver_class", "com.mysql.jdbc.Driver");
dbConfig.put("user", "xxxx");
dbConfig.put("password", "xxxx");
// 创建客户端
dbClient = JDBCClient.createShared(vertx, dbConfig);
}
// 提供一个公共方法来获取客户端
public JDBCClient getDbClient() {
return dbClient;
}
}
通过上面的工具类,可以快速的获取到客户端,看上面的代码也很简单,通过JsonObect构建一些基本的数据库连接信息,然后通过JDBCClient的createShard方法创建一个JDBCClient实例。
进行数据库的操作,以查询年龄大于18岁的用户为例
public class JdbcTestVerticle extends AbstractVerticle {
@Override
public void start() throws Exception {
// 获取到数据库连接的客户端
JDBCClient jdbcClient = new JdbcUtils(vertx).getDbClient();
String sql = "select * from t_user where age > ?";
// 构造参数
JsonArray params = new JsonArray().add(18);
// 执行查询
jdbcClient.queryWithParams(sql, params, qryRes->{
if(qryRes.succeeded()) {
// 获取到查询的结果,Vert.x对ResultSet进行了封装
ResultSet resultSet = qryRes.result();
// 把ResultSet转为List形式
List rows = resultSet.getRows();
// 输出结果
System.out.println(rows);
} else {
System.out.println("查询数据库出错!");
}
});
}
public static void main(String[] args) {
Vertx vertx = Vertx.vertx();
vertx.deployVerticle(new JdbcTestVerticle());
}
}
JsonArray是一个数组,SQL中用到的参数可以通过构建一个JsonArray来赋值。
JsonObejct是一个Json对象,类似于阿里的fastjson中提供的JSONObject
这两个对象在Vert.x中非常常用,而且非常的好用,但一定要注意空指针的问题,这是非常让人头疼的。
通过上面的三个步骤,就可成功的对数据库进行操作了,但还有些问题需要优化,
比如数据库连接信息放到配置文件中,再比如使用数据库连接池等等。
使用配置文件
{
"default":{
"url":"jdbc:mysql://localhost:3306/my_project",
"driver_class":"com.mysql.cj.jdbc.Driver",
"user":"root",
"password":"root"
},
"prod":{
"url":"jdbc:mysql://localhost:3306/my_project",
"driver_class":"com.mysql.cj.jdbc.Driver",
"user":"root",
"password":"root"
}
}
修改DbUtils工具类
public class JdbcUtils {
private JDBCClient dbClient;
private static JsonObject config ;
static {
byte[] buff = new byte[102400];
try {
// 读取配置文件
InputStream ins = new FileInputStream("db.json");
int i = IOUtils.read(ins, buff);
config = new JsonObject(new String(buff, 0, i));
} catch (Exception e) {
System.out.println("读取配置文件失败");
}
}
public JdbcUtils(Vertx vertx, String dsName) {
JsonObject dbConfig = config.getJsonObject(dsName);
if(dbConfig == null) {
throw new RuntimeException("没有找到指定的数据源");
}
dbClient = JDBCClient.createShared(vertx, dbConfig);
}
public JdbcUtils(Vertx vertx) {
this(vertx, "default");
}
public JDBCClient getDbClient() {
return dbClient;
}
}
这样就支持了多个数据源,而且数据库连接配置都放到了配置文件中。
数据连接池默认使用的C3P0,所以可以在db.json中进行配置C3P0连接池的参数就可以了,
这里官网的地址为:https://vertx.io/docs/vertx-jdbc-client/java/
具体配置可以参考官网给出的配置,下面是一个简单的截图
遗憾的是,Vert.x给出的数据库连接池的支持并不多,如果我们想要使用比如阿里的Druid连接池,需要自己来实现DataSourceProvider。当然DataSourceProvider的实现并不复杂,但麻烦啊!后面我会给出一个关于druid的DataSourceProvider的实现。
Vert.x从比较低的层面来控制事务,不像Spring一样可以使用声明式事务管理。要想在Vert.x中开启事务,和传统的JDBC管理事务的方式非常类似。首先要获得到连接,然后调用连接的setAutoCommit方法,关闭事务的自动提交,然后再手动的提交和回滚事务。
因为开启事务、提交事务、执行SQL都需要和数据库服务进行通信,因此在Vert.x中都是异步操作,按传统方式实现一个事务代码非常痛苦,看下面的一段开启事务的代码。写了一遍以后,绝对不愿意再写第二遍。
// 获得连接
jdbcClient.getConnection(con -> {
if (con.succeeded()) {
System.out.println("获取到数据库连接");
// 获取到的连接对象
SQLConnection connection = con.result();
}
});
// 开启事务
connection.setAutoCommit(false, (v) -> {
if (v.succeeded()) {
}
});
// 执行更新操作
connection.update("sql", upRes -> {
if(upRes.succeed()){
}
});
// 提交事务
connection.commit(rx -> {
if (rx.succeeded()) {
// 事务提交成功
}
});
回滚事务
// 回滚事务
connection.rollback(rb -> {
if (rb.succeeded()) {
// 事务回滚成功
}
});
如果你觉得上面的还很简单,看看下面一个完整的例子吧,把这些嵌套在一起,你还觉得简单吗?
package stu.vertx.jdbc;
import io.vertx.core.AbstractVerticle;
import io.vertx.core.Vertx;
import io.vertx.ext.jdbc.JDBCClient;
import io.vertx.ext.sql.SQLConnection;
/**
* 获得数据库连接,执行查询,开启事务,执行更新操作
*
* @author {
if (con.succeeded()) {
System.out.println("获取到数据库连接");
// 获取到的连接对象
SQLConnection connection = con.result();
// 执行查询操作
connection.query("select * from t1", rs -> {
// 处理查询结果
if (rs.succeeded()) {
System.out.println(rs.result().getRows());
}
});
// 开启事务
connection.setAutoCommit(false, (v) -> {
if (v.succeeded()) {
// 事务开启成功 执行crud操作
connection.update("update t1 set name = '被修改了' where name = '111'", up -> {
if (up.succeeded()) {
// 再来一笔写操作
connection.update("insert into t1 values ('222','222222') ", up2 -> {
if (up2.succeeded()) {
// 提交事务
connection.commit(rx -> {
if (rx.succeeded()) {
// 事务提交成功
}
});
} else {
connection.rollback(rb -> {
if (rb.succeeded()) {
// 事务回滚成功
}
});
}
});
} else {
connection.rollback(rb -> {
if (rb.succeeded()) {
// 事务回滚成功
}
});
}
});
} else {
System.out.println("开启事务失败");
}
});
} else {
System.out.println("获取数据库连接失败");
}
});
}
public static void main(String[] args) {
Vertx.vertx().deployVerticle(new GetConnection());
}
}
上面的代码仅仅是做了两个写操作,可以说是非常的痛苦了,一层一层的嵌套,根本没法维护。那么在真实的开发环境中,该如何管理事务呢,这就需要使用rxjava了,能够有效的减少多层嵌套带来的问题。
使用rxjava首先是需要引入rxjava的依赖
io.vertx
vertx-rx-java
3.7.0
完成上面案例的同样代码如下
package stu.vertx.jdbc;
import io.vertx.core.*;
import io.vertx.core.json.JsonArray;
import io.vertx.ext.jdbc.JDBCClient;
import io.vertx.ext.sql.SQLConnection;
import rx.Single;
import java.util.UUID;
/**
* @author {
if (in.succeeded()) {
resultHandler.handle(Future.succeededFuture(connection));
} else {
resultHandler.handle(Future.failedFuture(in.cause()));
}
});
}
public void update2(SQLConnection connection, Handler> resultHandler) {
connection.update("update t1 set name = '111' where passwd = '111'", in -> {
if (in.succeeded()) {
resultHandler.handle(Future.succeededFuture(connection));
} else {
resultHandler.handle(Future.failedFuture(in.cause()));
}
});
}
public static void main(String[] args) {
Vertx.vertx().deployVerticle(new GetConnectionWithRxJava());
}
}
通过使用RxJava,没有那么深的嵌套层次,逻辑比较清晰。当然了,为了一个简单的操作,还是需要写很多的代码。
Vert.x认证和授权详解(包含认证和授权在Web系统中的使用)
每个线上系统几乎都是离不开认证和授权的,Vert.x提供了灵活、简单、便捷的认证和授权的支持。Vert.x抽象出了两个核心的认证和授权的接口,一个是AuthProvider,另一个是User。通过这两个接口,我们可以非常灵活的实现我们自定义的认证和授权方法。当然,Vert.x也给我们提供了使用 JDBC、Shiro、MongoDB、JWT等授权的实现,我们可以直接使用。
Vert.x提供的认证和授权都非常简单,多种授权方式都有一定的规律性。一般来讲不需要刻意的学习,在使用的过程中,多读下Vert.x的源码就能够非常清楚的了解到Vert.x认证和授权底层的逻辑。但不是每一位开发者都有时间或者心情去读源码的,所以,这里我简单列出关于Vert.x的认证和授权的应用。使用Vert.x认证和授权,需要经历三个阶段
1.自己实现AuthProvider和User接口实现一个简单的认证和授权。
2.使用Vert.x提供的授权方式,如JDBC
3.在Web中使用认证和授权来管理访问权限
定义授权就是自己实现AuthProvider和User这两个接口,重写这两个接口中定义的认证和授权方法,这是Vert.x认证和授权最核心的也是最为底层的,把这两个接口弄明白了,后面使用JDBC授权,jwt等等都非常简单。当然了,如果你仅仅是为了使用,那么你可以直接关注第三个阶段,认证和授权在Web中的应用。
比如我们要实现一个最简单的根据用户名和密码的认证,只要认证通过了,就可以访问系统的所有资源这么一个需求,代码如下:
io.vertx
vertx-auth-common
3.6.2
/**
* 认证与授权测试
*
* @author lenovo
*
*/
public class AuthTest extends AbstractVerticle {
@Override
public void start() throws Exception {
// 创建认证的Provider
AuthProvider provider = new UserNameAndPasswordProvider();
JsonObject authInfo = new JsonObject().put("username", "admin").put("password", "admin");
// 调用认证方法,将认证的数据传入
provider.authenticate(authInfo, res -> {
if (res.succeeded()) {
// 认证通过,可以获取到User对象,通过User对象可以进行权限校验
User user = res.result();
// 授权
user.isAuthorized("save:user", auth -> {
if (auth.succeeded()) {
System.out.println("授权成功");
} else {
System.out.println("授权失败");
}
});
} else {
System.out.println("认证失败!");
}
});
}
public static void main(String[] args) {
Vertx.vertx().deployVerticle(new AuthTest());
}
}
用法非常简单,首先创建一个AuthProvider,这里我们使用了
UserNameAndPasswordProvider
这个类是我们自己定义的一个使用用户名和密码进行认证的一个Provider,这个类需要username和password,所以我们将这两个参数放到authInfo中,传递给
authenticate
这个方法,这个方法会异步返回认证的结果。如果认证成功,会返回授权的对象User,调用User对象的
isAuthorized
可以进行验证是否授权。下面是UserNameAndPasswordProvider的一个简单实现
代码如下:
/**
* 自定义认证
*
* @author lenovo
*/
public class UserNameAndPasswordProvider implements AuthProvider {
@Override
public void authenticate(JsonObject authInfo, Handler> resultHandler) {
// authInfo中存储了认证需要的相关信息,由调用者传入
String username = authInfo.getString("username");
String password = authInfo.getString("password");
// 判断用户名和密码是否正确
if ("admin".equals(username) && "admin".equals(password)) {
// 密码验证通过,需要实例化授权对象,并在Future中响应给调用者
// 实例化授权对象,可以将认证信息传入
User user = new MyUser(authInfo);
// 所有情况均成功返回,并将授权对象响应回去
resultHandler.handle(Future.succeededFuture(user));
} else {
// 密码验证不通过,响应认证失败
resultHandler.handle(Future.failedFuture("用户名或者密码错误"));
}
}
}
看到上面的代码,AuthTest中的逻辑就更加清晰了,代码非常简单,就不多描述了。
/**
* 授权
*
* @author lenovo
*
*/
public class MyUser implements User {
private JsonObject authInfo;
public MyUser(JsonObject authInfo) {
this.authInfo = authInfo;
}
/**
* 这里依然是通过resultHandle响应授权信息,返回值为当前对象是为了Fluent调用模式
*/
@Override
public User isAuthorized(String authority, Handler> resultHandler) {
// 一直返回成功
resultHandler.handle(Future.succeededFuture(true));
return this;
}
@Override
public User clearCache() {
return null;
}
@Override
public JsonObject principal() {
return authInfo;
}
@Override
public void setAuthProvider(AuthProvider authProvider) {
}
}
这里只是重写了
isAuthorized
这个方法,这个方法里,一直异步响应授权成功,并同步返回当前类的实例,是为了级联调用起来比较方便。这里也非常简单,不多说。
通过Vert.x提供的接口我们可以自己实现认证和授权,但一般的情况下,我们可能都会选择使用数据库来保存认证和授权信息,如果每次我们都要自己实现JDBCAuthProvider会非常麻烦,重复造轮子,因此Vert.x给我们提供了JDBC授权的实现。用法非常简单。对自定义授权熟悉之后,JDBC授权就非常好理解了。 ① 引入pom依赖
io.vertx
vertx-auth-jdbc
3.6.2
②创建数据表,为了简单,我们使用5张表
-- 用户表
create table t_user (
id int primary key auto_increment,
username varchar(40) not null,
password varchar(255) not null
);
-- 角色表
create table t_role(
id int primary key auto_increment,
role_name varchar(40) not null
);
-- 权限表
create table t_permission(
id int primary key auto_increment,
prefix varchar(40) not null
);
-- 用户角色对应关系表
create table t_user_role (
id int primary key auto_increment,
user_id int not null,
role_id int not null
);
-- 角色权限对应关系表
create table t_role_permission(
id int primary key auto_increment,
role_id int not null,
per_id int not null
);
③编写测试类
public class JDBCAuthTest extends AbstractVerticle {
private JDBCClient jdbcClient;
@Override
public void start() throws Exception {
// 获取到数据库的客户端
jdbcClient = new JdbcUtils(vertx).getDbClient();
// 这个就是实现了AuthProvider接口的认证的类
JDBCAuth auth = JDBCAuth.create(vertx, jdbcClient);
// 创建用于认证的参数
JsonObject authInfo = new JsonObject();
auth.authenticate(authInfo, res -> {
if (res.succeeded()) {
// 获取到授权接口
User user = res.result();
System.out.println("认证成功");
} else {
// 认证失败
System.out.println("认证失败");
}
});
}
public static void main(String[] args) {
Vertx.vertx().deployVerticle(new JDBCAuthTest());
}
}
运行之后发现,表也找不到,字段也找不到,为啥呢,因为我们创建的表和Vert.x创建的表的表名和字段名都不一样。那么如果我们想要使用我们自己的表结构,就要给JDBCAuth设置要执行的SQL有下面的几个方法
auth.setAuthenticationQuery(""); // 指定认证的SQL
auth.setPermissionsQuery(""); // 指定获取用户权限的SQL
auth.setRolesQuery(""); // 指定获取用户角色的SQL
好了,到这里,JDBC的认证就完成了,是不是用起来还是比较简单的。不再需要我们来实现Provider和User接口了。
JWT 是 JSON Web Token 的简称,通过名字就可以知道,JWT一般是用在web开发中,一种token的认证方式。在开发中用的还是比较多的。Web开发认证的实现主要有两种方式,第一种是Session的方式,第二种是Token方式,这两种认证方式的优劣我们这里不进行比较。
JWT认证和上面提到的基于JDBC的认证以及自定义实现的认证不同,JWT认证可以认为是在JDBC认证、手机短信认证或者自定义的认证证实身份之后,给认证者的一个唯一标识,以后认证只需要带着这个标识就可以了,而不需要再带着用户名或者密码进行认证。以此来保证用户信息安全。 对于带着用户名或者密码的这种认证方式,在上送用户名和密码这些敏感信息的时候,要使用https来保证传输信息的安全。
JWT认证核心两个,一个是生成token,第二个是验证客户端上送的token是否正确。下面是具体的开发步骤
①引入pom
io.vertx
vertx-auth-jwt
3.6.2
②JDBC认证,并生成token,返回给客户端
JWTAuthOptions config = new JWTAuthOptions()
.addPubSecKey(new PubSecKeyOptions()
.setAlgorithm("HS256")
.setPublicKey("keyboard cat")
.setSymmetric(true));
JWTAuth provider = JWTAuth.create(vertx, config);
// 模拟认证通过
if("admin".equals("username") ) {
String token = provider.generateToken(new JsonObject(), new JWTOptions());
System.out.println(token);
}
③第二次客户端带着token来,服务端进行校验
JWTAuthOptions config = new JWTAuthOptions()
.addPubSecKey(
new PubSecKeyOptions().setAlgorithm("HS256")
.setPublicKey("keyboard cat")
.setSymmetric(true)
);
JWTAuth provider = JWTAuth.create(vertx, config);
provider.authenticate(new JsonObject().put("jwt", "dfasdfsadfsadfsdfs"), res -> {
System.out.println(res.result());
});
在token中带数据
jwt中可以附带一些非敏感的数据,比如用户的ID,再或者时间戳等。那么该怎么带数据呢,非常简单。注意上面生成token的代码中,传入了两个参数,一个是JsonObject,另一个是JWTOptions。其中,JsonObject就是在token中附带的数据。
JsonObject data = new JsonObject()
.put("userId","admin");
String token = provider.generateToken(data, new JWTOptions());
如上代码,就可以在token中带上userId,那么当我们解析token的时候,就可以取出userId的值了,代码如下。
// 使用jwt进行认证
provider.authenticate(new JsonObject().put("jwt", jwt), auth -> {
if (auth.succeeded()) {
JWTUser user = (JWTUser) auth.result();
JsonObject authData = user.principal(); // 这里就是token中解析的数据
String userId = authData.getString("userId");
System.out.println(userId);
request.response().end("认证成功!");
} else {
System.out.println("认证失败");
request.response().end("token无效");
}
});
使用jwt的整个认证过程如下:
package stu.vertx.auth.jwt;
import io.vertx.core.AbstractVerticle;
import io.vertx.core.Vertx;
import io.vertx.core.http.HttpServer;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.auth.PubSecKeyOptions;
import io.vertx.ext.auth.jwt.JWTAuth;
import io.vertx.ext.auth.jwt.JWTAuthOptions;
import io.vertx.ext.auth.jwt.impl.JWTUser;
import io.vertx.ext.jwt.JWTOptions;
import org.apache.commons.lang3.StringUtils;
import stu.vertx.auth.basic.UserNameAndPasswordProvider;
public class JwtAuthVerticle extends AbstractVerticle {
private JWTAuthOptions config = new JWTAuthOptions()
.addPubSecKey(new PubSecKeyOptions()
.setAlgorithm("HS256")
.setPublicKey("keyboard cat")
.setSymmetric(true));
private JWTAuth provider = JWTAuth.create(vertx, config);
@Override
public void start() throws Exception {
HttpServer server = vertx.createHttpServer();
// 处理客户端请求
server.requestHandler(request -> {
JsonObject req = JwtAuthVerticle.parseQuery(request.query());
// 判断用户是否带token来认证,如果带token,就直接通过token来认证,否则认为是第一次认证,通过用户名和密码的方式进行认证
String jwt = req.getString("jwt");
if (StringUtils.isBlank(jwt)) {
// 先使用默认的用户名密码进行认证
UserNameAndPasswordProvider p = new UserNameAndPasswordProvider();
p.authenticate(req, auth -> {
if (auth.succeeded()) {
// 认证通过之后,再生成token,以后就使用token进行认证
JsonObject data = new JsonObject()
.put("userId", "admin");
String token = provider.generateToken(data, new JWTOptions());
request.response().end(token);
} else {
System.out.println("认证失败");
request.response().end("认证失败,请输出正确的用户名和密码");
}
});
} else {
// 使用jwt进行认证
provider.authenticate(new JsonObject().put("jwt", jwt), auth -> {
if (auth.succeeded()) {
JWTUser user = (JWTUser) auth.result();
JsonObject authData = user.principal();
String userId = authData.getString("");
System.out.println(userId);
request.response().end("认证成功!");
} else {
System.out.println("认证失败");
request.response().end("token无效");
}
});
}
});
server.listen(8080);
}
/**
* 把URL后跟的查询字符串转成json对象
*
* @param query
* @return
*/
public static JsonObject parseQuery(String query) {
JsonObject data = new JsonObject();
String[] params = query.split("&");
for (String param : params) {
String[] k = param.split("=");
data.put(k[0], k[1]);
}
return data;
}
public static void main(String[] args) {
Vertx.vertx().deployVerticle(new JwtAuthVerticle());
}
}
在应用开发中,很多的应用场景都使用shiro来进行认证和授权。在Vert.x中,也提供了对Shiro的支持。对于shiro的用法这里不再详细的介绍,大家可以参考网络上关于shiro的文章,我们这里仅仅介绍shiro在Vert.x中的应用。
在Vert.x中使用shiro,首先要导入shiro的依赖,以及Vert.x-shiro的依赖包,如下
1.引入Vert.x-shiro的依赖包
io.vertx
vertx-auth-shiro
3.5.6
2.auth.properties
下面创建shiro的配置文件auth.properties
user.tim=sausages,morris_dancer,developer,vtoons
role.developer=do_actual_work
role.vtoons=place_order
user.admin=admin,manager
3.代码
在配置文件中,配置了一个admin用户,他的密码也是admin,他具有manager角色,下面是认证和授权的案例代码
/**
* 使用shiro实现认证和授权的演示案例
*
* @author lenovo
*/
public class ShiroAuthVerticle extends AbstractVerticle {
@Override
public void start() throws Exception {
JsonObject config = new JsonObject().put("properties_path", "classpath:auth.properties");
ShiroAuth provider = ShiroAuth.create(vertx, new ShiroAuthOptions().setType(ShiroAuthRealmType.PROPERTIES).setConfig(config));
JsonObject data = new JsonObject()
.put("username", "admin")
.put("password", "admin");
provider.authenticate(data, auth -> {
if (auth.succeeded()) {
System.out.println("认证成功");
User user = auth.result();
user.isAuthorized("role:manager",res->{
if(res.result()) {
System.out.println("授权成功");
} else {
System.out.println("没有权限");
}
});
} else {
System.out.println("认证失败");
}
});
}
@Override
public void stop() throws Exception {
super.stop();
}
public static void main(String[] args) {
Vertx.vertx().deployVerticle(new ShiroAuthVerticle());
}
}
关于Vert.x提供的认证和授权还有 MongoDB认证,OAuth2认证等等,这里就不再多说了,大家感兴趣的话,可以参考https://vertx.io/docs/vertx-auth-oauth2/java/这里有对oauth2的介绍,关于认证和授权,就说到这里了,下面是认证和授权在Web应用中的使用。
有了AuthProvider和User之后,再来看认证和授权在Web中的应用就非常简单了。这里我们在Web应用中使用JDBC授权的实现,这也是Web开发中最为常用的。既然使用JDBC授权,那么首先就要创建数据库,创建表,这里我们就使用Vert.x定义的表,如果你的需求比较复杂,可以定义更复杂的模型,这里为了简单,就不再扩展了。
① 建表语句如下:
CREATE TABLE `user` (
`username` VARCHAR(255) NOT NULL,
`password` VARCHAR(255) NOT NULL,
`password_salt` VARCHAR(255) NOT NULL
);
CREATE TABLE `user_roles` (
`username` VARCHAR(255) NOT NULL,
`role` VARCHAR(255) NOT NULL
);
CREATE TABLE `roles_perms` (
`role` VARCHAR(255) NOT NULL,
`perm` VARCHAR(255) NOT NULL
);
注意:MySQL8 默认对表名区分大小写,JDBCAuth的实现类中,对表名是大写的,这就会导致提示找不到表的问题。
② 在路由中使用授权
比如我们想对 /private/ 的请求需要进行认证,其他的请求不需要授权都可以访问,那么我们就可以只针对/private/实现拦截,然后进行权限的过滤。
router.route("/private/*").handler(authHandler);
路由后跟一个处理器,也就是拦截到/private/*的请求之后该如何处理,这里不需要再重复造轮子了,可以使用Vert.x提供的处理器RedirectAuthHandler,如下
AuthHandler authHandler = RedirectAuthHandler.create(authProvider,"/login.html");
create方法有两个参数,第一个就是我们上面花了大量篇幅所描述的authProvider,第二个参数很明显是一个url,表示如果认证失败,要跳转的页面。当然认证失败之后要跳转到登录页面,让用户进行登录了。下面是authProvider是如何创建的呢?
AuthProvider authProvider = JDBCAuth.create(vertx, jdbcClient);
到这里,在web应用中使用JDBC认证就完成了,是不是非常简单。但到这里,我们只是实现了一个认证的处理器,是不是还需要提供一个登录的处理器呢,不提供登录的入口,不管如何访问,都永远会跳转到登录页。对于登录的实现也非常简单,Vert.x也给我们提供了登录的处理器。
FormLoginHandler formLoginHandler = FormLoginHandler.create(authProvider)
.setDirectLoggedInOKURL("/index.html");
router.route("/login").handler(formLoginHandler);
当用户访问/login时,会使用FormLoginHandler,这个handle会读取到表单提交上来的用户名和密码,然后传递给authProvider进行认证,如果认证通过,则会跳转到setDirectLoggedInOKURL所指定的地址。当认证通过之后,再访问/private下的资源就可以了。
router.route("/private/hello").handler(re -> {
re.user().isAuthorized("role:manager", a -> {
System.out.println(a.succeeded());
re.response().end("Over");
});
});
比如有上面的私有路径,在登录之前,访问会跳转到登录页面,当登录成功之后,就可以进入到handle中。通过routeContext对象可以获取到user对象,通过user对象的isAuthorized方法可以判断是否有权限。这就完成了认证和授权的整个过程。
Event Bus(事件总线) 是Vert.x的神经系统,负责应用系统消息的传递。Vert.x各模块(Verticle)之间的相互调用就是通过Event Bus实现的,因此各Verticle之间是高度解耦的。
Event Bus提供发布订阅功能和点对点的消息服务,类似于消息队列,每条消息在Event Bus上都有一个地址(address),发布者向这个地址发送消息,接收者从这个地址接收消息。
Event Bus独立于应用系统,它使用TCP协议进行通信。因此,在任何的应用中,只要能够创建TCP连接,都可以通过Event Bus连接到Vert.x实例,如下图所示。也正是因为这个,Vert.x天生就对分布式支持非常好。
Event Bus的api使用非常简单,下面我们分别从获取Event Bus实例,发布消息,订阅消息以及服务调用来逐一说明。
获取Event Bus实例非常简单,直接通过Vert.x实例获取,代码如下
EventBus eb = vertx.eventBus();
可以看到,通过Vert.x实例获取Event Bus的实例和获取文件系统fileSystem以及创建HTTP服务createHttpServer非常类似。拿到Event Bus的实例之后,发现Event Bus和Vertx一样,都是接口,查看接口的代码可以非常清楚的看到Event Bus对外提供的API,代码如下
@Fluent
EventBus send(String address, Object message);
@Fluent
EventBus publish(String address, Object message);
MessageConsumer consumer(String address);
为了节约篇幅,去掉了部分重载的方法。主要的几个方法send,publish,consumer,sender,publisher通过方法的名字就可以猜测出方法的用途,后面根据具体的场景来分别对这些方法进行说明。
发布消息的方法有多个方法的重载,最简单的发布方法接收一个地址,和一个Object类型的消息对象,如下所示
@Fluent
EventBus publish(String address, Object message);
这种方式发布的消息,所有订阅到address的消费者都能够收到消息,这也就是发布-订阅模式。还有一种是点对点模式,这种模式发布的消息只能有一个消费者收到,方法如下
@Fluent
EventBus send(String address, Object message,
DeliveryOptions options, Handler>> replyHandler);
对于订阅消息比较简单,只有一个方法,就是consumer,方法的声明如下
MessageConsumer consumer(String address, Handler> handler);
如果想要通过其他的开发语言或者在其他应用中调用Vert.x实例模块,可以通过发起TCP请求,连接到EventBus,Event Bus使用的通信协议如下,但一定不要忘记,需要创建Event Bus Bride。
<{
type: String,
address: String,
(replyAddress: String)?,
headers: JsonObject,
body: JsonObject
}: JsonObject>
最后,附一个使用Event Bus进行远程组件调用的Demo,这里例子相对来讲可能有些复杂,但相比较真实企业项目还差的远。这个例子就是一个简单的微服务,多个服务通过Event Bus进行通信。
我们把每个Verticle单独为一个Maven模块,这个模块中核心有三个类,一个Verticle,一个对外暴露的接口Interface,一个接口的具体实现类。为了简单,这里只有两个模块。
1.第一个Verticle是对外提供服务的,暂且称之为FirstVerticle,对外提供一个sayHello服务,模块结构如下
(1)FirstVerticle中就是发布Service到总线上,代码非常简单,这里只列出start方法的代码
@Override
public void start() throws Exception {
Service service = Service.create(vertx);
new ServiceBinder(vertx).setAddress(address).register(Service.class, service);
}
发布服务,并没有调用consumer方法,而是调用了ServiceBinder类的register方法,实际上这个方法的底层就是在调用consumer方法,只是帮我们做了很多的其实事情,这里暂且不去关注。
(2)Service接口中定义sayHello方法,并且提供了获取当前实例的方法(JDK8的新特性)
@ProxyGen
@VertxGen
public interface Service {
/**
* 这个方法是提供给自己使用,方便创建服务的实现类
*
* @param vertx
* @return
*/
static Service create(Vertx vertx) {
return new ServiceImpl(vertx);
}
/**
* 这个方法给消费者使用,便于消费者创建生产者的代理类,以此来消费生产者的服务
*
* @param vertx
* @param address
* @return
*/
static Service createProxy(Vertx vertx, String address) {
return new ServiceVertxEBProxy(vertx, address);
}
/**
* 测试接口方法
*
* @param name
*/
void sayHello(String name, Handler> resultHandle);
}
这里有三个方法,create方法是为了便于获得类的实例,这个比较好理解。createProxy方法实际上在我们当前的演示案例中并没有使用到,这里读者可以先不用去关注。对于sayHello方法的resultHandle这个参数可能不是很理解,这个参数非常简单,因为Vert.x是一个异步框架,sayHello方法也是异步执行的,那么方法的返回结构就是通过这个参数封装的。
(3)ServiceImpl的实现类的代码就更简单了,就一个sayHello方法的实现,如下
@Override
public void sayHello(String name, Handler> resultHandle) {
resultHandle.handle(Future.succeededFuture(new JsonObject().put("msg", "SUCCESS")));
}
给调用者回了个Json对象,这个对象中包含一个msg,值为SUCCESS
(4)还有四,不就上面三个吗,还有一个很关键的部分,package-info
@ModuleGen(groupPackage = "stu.vertx.cluster.service.hello", name = "FirstVerticle")
package stu.vertx.cluster.service.hello;
import io.vertx.codegen.annotations.ModuleGen;
这里描述的是这个模块的包,这个是必须的,否则ServiceProxy不能生成,其他的也都是白扯。所以这里,很关键,很关键,很关键。
2.第一个组件就完成了,下面是第二个组件,第二个组件来调用第一个组件,完成一系列操作。这个组件接收客户端请求,然后接收客户端上送的name属性,传递给第二个组件,并拿到第二个组件的返回值。这个组件不对外提供服务,因此就一个Verticle就可以了。代码如下
@Override
public void start() throws Exception {
HttpServer httpServer = vertx.createHttpServer();
httpServer.requestHandler(request -> {
// 获取到response对象
HttpServerResponse response = request.response();
// 设置响应头
response.putHeader("Content-type", "text/html;charset=utf-8");
// 通过配置action参数,指定要走哪一个方法
DeliveryOptions options = new DeliveryOptions();
options.addHeader("action", "sayHello");
// 这个是给方法传入的参数
JsonObject config = new JsonObject();
config.put("name", "xiaozhang");
// 通过eventBus调用方法
vertx.eventBus().send("service.demo.firstverticle", config, options, res -> {
// 响应数据
response.end(res.result().body().getString("msg"));
});
});
httpServer.listen(1234);
}
OK,到这里,一个远程服务调用就完成了,是不是非常简单!
Vert.x 案例代码:https://github.com/happy-fly