装载自:http://www.cnblogs.com/yiwangzhibujian/p/5845623.html
Jetty使用教程(四:21-22)—Jetty开发指南
二十一、嵌入式开发
21.1 Jetty嵌入式开发HelloWorld
本章节将提供一些教程,通过Jetty API快速开发嵌入式代码
21.1.1 下载Jetty的jar包
Jetty目前已经把所有功能分解成小的jar包,用户可以根据需要选择合适的jar包,来实现需要的功能。通常建议用户使用maven等管理工具来管理jar包,然而本教程使用一个包含所有功能的合集jar包来演示,用户可以使用curl或者浏览器下载jetty-all.jar包。
jetty-all.jar下载地址:http://central.maven.org/maven2/org/eclipse/jetty/aggregate/jetty-all/9.3.11.v20160721/jetty-all-9.3.11.v20160721-uber.jar
注意:
Maven中央仓库已经开始拒绝使用wget命令获得资源(因为一些工具的滥用),所以Maven中央仓库建议使用curl命令来获得jar包。
使用curl 命令如下(Windows用户可以将上面的地址复制到浏览器来下载):
> mkdir Demo > cd Demo > curl -o jetty-all-uber.jar http://central.maven.org/maven2/org/eclipse/jetty/aggregate/jetty-all/9.3.11.v20160721/jetty-all-9.3.11.v20160721-uber.jar
21.1.2 写一个HelloWorld例子
这个Jetty嵌入式教程章节包含很多通过Jetty API的例子,这个教程在main方法里运行一个Helloworld的handler来实现一个服务,可以自己写或者复制以下代码:
View Code
21.1.3 编译helloworld例子
使用如下命令编译生成class文件
> mkdir classes > javac -d classes -cp jetty-all-uber.jar HelloWorld.java
21.1.4 运行服务
使用如下命令运行服务,启动服务器。(建议使用开发工具进行学习,如eclipse)
> java -cp classes:jetty-all-uber.jar org.eclipse.jetty.embedded.HelloWorld
运行成功后会输出如下内容:
2016-09-02 11:44:48.851:INFO::main: Logging initialized @214ms 2016-09-02 11:44:48.915:INFO:oejs.Server:main: jetty-9.3.z-SNAPSHOT 2016-09-02 11:44:49.049:INFO:oejs.AbstractConnector:main: Started ServerConnector@5436b246{HTTP/1.1,[http/1.1]}{0.0.0.0:8080} 2016-09-02 11:44:49.050:INFO:oejs.Server:main: Started @417ms
此时可以访问http://localhost:8080 ,查看结果:
21.1.5 后续步骤
为了更好的学习Jetty,可以按如下步骤进行:
- 跟着例子学习嵌入式Jetty开发比直接看Jetty API要好
- 把Jetty javadoc完全看一遍
- 使用Maven来管理所有jar包和依赖
21.2 Jetty的嵌入式开发
Jetty有一个口号:不要把应用部署到Jetty上,要把Jetty部署到你的应用里。这句话的意思是把应用打成一个war包部署到Jetty上,不如将Jetty作为应用的一个组件,它可以实例化并像POJO一样。换种说法,在嵌入式模块中运行Jetty意味着把HTTP模块放到你的应用里,这种方法比把你的应用放到一个HTTP服务器里要好。
这个教程将一步一步的教你如何通过简单的Jetty服务运行多个web应用。大多数例子的源代码都是Jetty项目的一部分。
21.2.1 概述
本教程中嵌入一个Jetty的典型步骤如下:
- 创建一个server实例
- 新增/配置连接
- 新增/配置处理程序,或者Contexts,或者Servlets
- 启动Server
- 等待连接或者在当前线程上做一些其他的事
21.2.2 创建一个server实例
下面的代码实例化并运行了一个Jetty Server
View Code
在8080端口运行了一个HTTP服务,因为没有处理程序,所以这不是一个有效的server,所有请求将会返回404错误信息。
21.2.3 使用处理器处理请求
为了针对请求产生一个相应,Jetty要求用户为服务创建一个处理请求的处理器,一个处理器可以做的工作有:
- 检测或修改一个请求
- 一个完整的HTTP响应
- 处理转发(详见:HandlerWrapper)
- 处理转发到多个其它处理器上(详见:HandlerCollection)
21.2.3.1 处理器的HelloWorld
接下来的代码HelloHandler.java,表示一个简单的处理程序
View Code
传入到处理程序方法handle的参数有:
- target - 目标请求,可以是一个URI或者是一个转发到这的处理器的名字
- baseRequest - Jetty自己的没有被包装的请求,一个可变的Jetty请求对象
- request - 被filter或者servlet包装的请求,一个不可变的Jetty请求对象
- response - 响应,可能被filter或者servlet包装过
处理程序会设置状态码,content-type,并调用write向response输出内容。
21.2.3.2 运行HelloWorldHandler
为了让处理器处理HTTP请求,你必须将处理程序添加到server实例上,接下来的代码OneHandler.java展示了一个serve实例如何调用处理出现来处理请求:
View Code
一个或多个处理器将处理Jetty所有的请求。一些处理器会转发请求到其他处理器(例如: ContextHandlerCollection 会根据路径选择匹配的ContextHandler),另一些将根据逻辑判断来生成相应的响应(例如:ServletHandler 将把request转发到servlet),还有的处理器将处理和请求无关的操作(例如:RequestLogHandler 或者StatisticsHandler)。
后面的章节将介绍如何在切面调用一个处理器,你可以到org.eclipse.jetty.server.handler 包下看看当前可用的处理器。
21.2.3.3 处理器的集合及封装
复杂的请求可以由多个处理器来完成,你可以通过不同的方式把它们结合起来,Jetty有几个HandlerContainer接口的实现:
HandlerCollection:
一个包含多个处理器的集合,按照顺序依次处理。这在响应请求的同时进行统计和日志记录特别有用。
HandlerList:
一个包含多个处理器的集合,按照顺序处理,与HandlerCollection不同的是,当处理器出现异常或者响应被提交或者request.isHandled()方法返回true时,后续将不再被调用。一般用来匹配不同的主机,用来进行不同的处理。
HandlerWrapper:
一个处理器的基类用来进行切面编程。例如,一个标准的web应用会由context,session,安全和servlet处理器构成。
ContextHandlerCollection:
一个特殊的HandlerCollection,使用完整的URI前缀来选择匹配的ContextHandler对请求进行处理。
21.2.3.4 处理器的作用域
在Jetty中,很多标准的服务器会继承HandlerWrappers,用来进行链式处理,比如从请求从ContextHandler 到SessionHandler ,再到SecurityHandler 最后到ServletHandler。然而,因为servlet规范的性质,外部处理器不能在没有调用内部处理器的时候得知内部处理器的信息,例如:ContextHandler调用应用监听请求的context时,它必须已经知道ServletHandler将要转发请求到哪个servlet,以确保servletPath方法返回正确的值。
ScopedHandler是HandlerWrapper 一个抽象实现类,用来提供链式调用时作用域的支持。例如:一个ServletHandler内嵌在一个ContextHandler中,方法嵌套调用的顺序为:
Server.handle(...)
ContextHandler.doScope(...)
ServletHandler.doScope(...)
ContextHandler.doHandle(...)
ServletHandler.doHandle(...)
SomeServlet.service(...)
因此ContextHandler处理请求时,它内嵌的ServletHandler已经建立了。
21.2.3.5 资源处理器
下面这个FileServer例子,展示了你可以使用ResourceHandler 来处理当前工作路径下的静态资源。
View Code
请注意,例子中的HandlerList 包含ResourceHandler 和DefaultHandler,DefaultHandler 将会为不能匹配到资源的生成一个格式良好的404回应。
21.2.4 嵌入式的连接
在前面的例子中,通过在Server实例通过传入一个端口号,让Server创建一个默认的连接来监听指定的端口。然而,通常编写嵌入式代码希望显式的配置一个或者多个连接器。
21.2.4.1 一个连接
下面的例子, OneConnector.java,实例化、配置、新增一个HTTP连接到server上
import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; /** * 有一个连接的Server */ public class OneConnector { public static void main(String[] args) throws Exception { Server server = new Server(); // 创建一个HTTP的连接,配置监听主机,端口,以及超时时间 ServerConnector http = new ServerConnector(server); http.setHost("localhost"); http.setPort(8080); http.setIdleTimeout(30000); // 将此连接添加到Server server.addConnector(http); // 设置一个处理器 server.setHandler(new HelloHandler()); // 启动Server server.start(); server.join(); } }
在这个例子中,连接将处理http的请求,这个也是默认的ServerConnector连接类。
21.2.4.2 多个连接
当配置多个连接(例如:HTTP和HTTPS),它们可能是共同分享HTTP设置的参数。为了显式的配置ServerConnector 需要使用ConnectionFactory ,并提供一个常用的HTTP配置。
下面这个 ManyConnectors例子,给一个Server配置了两个ServerConnector ,http连接有一个HTTPConnectionFactory 实例,https连接有一个SslConnectionFactory 实例在HttpConnectionFactory里面。两个HttpConnectionFactory 都是基于同一个HttpConfiguration实例,然而https使用包装过的配置信息,因此SecureRequestCustomizer 可以被添加进去。
View Code
21.2.5 嵌入式的Servlets
Servlets是处理逻辑和HTTP请求的标准方式。Servlets 类似于Jetty的处理器,request对象不可变且不能被修改。在Jetty中servlet将有ServletHandler进行负责调用。它使用标准的路径匹配一个请求到servlet,设置请求的路径和请求内容,将请求传递到servlet,或者通过过滤器产生一个响应。
下面这个MinimalServlets例子,创建一个ServletHandler 实例,并配置一个简单的servlet
View Code
21.2.6 嵌入式Context
ContextHandler是一种ScopedHandler,只用来响应配匹配指定URI前缀的请求,
- 一个Classloader 当在一个请求作用域里的时候处理当前线程的请求
- 一个ServletContext有小属性的集合
- 通过ServletContext获得的初始化参数的集合
- 通过ServletContext 获得的基础资源的集合
- 一个虚拟主机名称的集合
下面这个OneContext例子,包含一个HelloHandler处理器,用来处理指定路径下的请求
import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.handler.ContextHandler; public class OneContext { public static void main(String[] args) throws Exception { Server server = new Server(8080); //在/hello路径上增加一个处理器 ContextHandler context = new ContextHandler(); context.setContextPath("/hello"); context.setHandler(new HelloHandler()); //可以通过http://localhost:8080/hello访问 server.setHandler(context); server.start(); server.join(); } }
当有很多有效的contexts 时,可以创建一个ContextHandlerCollection 集合存储这些处理器,下面的这个ManyContexts例子展示多个context的使用:
View Code
21.2.7 嵌入式ServletContexts
ServletContextHandler是一种特殊的ContextHandler,它可以支持标准的sessions 和Servlets。下面例子的OneServletContext 实例化了一个 DefaultServlet为/tmp/ 和DumpServlet 提供静态资源服务,DumpServlet 创建session并且应答请求信息
View Code
21.2.8 嵌入一个web应用
WebAppContext是ServletContextHandler 的扩展,使用标准的web应用组件和web.xml,通过web.xml和注解配置servlet,filter和其它特性。下面这个OneWebApp例子部署了一个简单的web应用。Web应用程序可以使用容器提供的资源,在这种情况下需要一个LoginService并配置:
View Code
21.2.9 使用Jetty XML配置
通常配置Jetty实例是通过配置jetty.xml和其它关联的配置文件,然而Jetty 配置信息都可以用简单的代码进行配置,下面的例子将从配置文件中获得相关信息:
- jetty.xml
- jetty-jmx.xml
- jetty-http.xml
- jetty-https.xml
- jetty-deploy.xml
- jetty-stats.xml
- jetty-requestlog.xml
- jetty-lowresources.xml
- test-realm.xml
View Code
21.3 嵌入式的例子
Jetty在被嵌入到各种应用程序方面有丰富的历史,在本节,将带着你通过几个简单的例子理解我们在git仓库的embedded-jetty-examples项目。
重要:
这是文件是直接在git仓库里面获取的,如果行号不能正确的匹配git仓库上的请通知我们。
21.3.1 文件服务器
这个例子展示了如何创建一个简单的文件服务器Jetty。是完全适合测试用于,需要一个实际的web服务器来获取文件,它可以很容易地配置到src/test/resources目录下。注意,这个没有任何形式的缓存,在服务器或设置适当的响应标头。通过简单的几行就可以提供一些文件:
View Code
21.3.1.1 运行程序
运行后通过访问 http://localhost:8080/index.html,即可查看到想要的结果。
21.3.1.2 Maven坐标
为了成功运行上面的例子,项目需要增加以下依赖:
<dependency> <groupId>org.eclipse.jetty</groupId> <artifactId>jetty-server</artifactId> <version>${project.version}</version> </dependency>
21.3.2 多重文件提供
这个例子展示了通过多重资源处理器提供资源,通过访问同一个路径会按顺兴调用ResourceHandlers 获得资源:
View Code
21.3.2.1 运行程序
启动程序后访问http://localhost:8090/index.html ,若第一个匹配路径下没有找到相应的资源,则在第二个路径下继续寻找,以此类推。
21.3.2.2 Maven坐标
为了能正确的运行需要添加以下依赖
<dependency> <groupId>org.eclipse.jetty</groupId> <artifactId>jetty-server</artifactId> <version>${project.version}</version> </dependency> <dependency> <groupId>org.eclipse.jetty.toolchain</groupId> <artifactId>jetty-test-helper</artifactId> <version>2.2</version> </dependency>
21.3.3 多重连接
这个例子展示了如何配置多重连接,主要是同时配置http和https两种,两种配置都使用了HelloHandler (此例子上面已有不再翻译):
View Code
添加maven依赖如下:
<dependency> <groupId>org.eclipse.jetty</groupId> <artifactId>jetty-server</artifactId> <version>${project.version}</version> </dependency> <dependency> <groupId>org.eclipse.jetty</groupId> <artifactId>jetty-security</artifactId> <version>${project.version}</version> </dependency>
21.3.4 登录校验示例
这个例子展示了如何通过一个安全管理器来封装一个普通管理器,我们有一个简单的Hello处理程序返回一个问候,但增加了一个限制就是得到这个问候你必须进行身份验证。另外需要注意的是这个例子使用了支持map校验的ConstraintSecurityHandler,这样比较容易展示SecurityHandler 的使用方法,如果不需要可以简单的实现SecurityHandler:
View Code
21.3.4.1 运行程序
运行程序后,输入路径http://localhost:8080/index.html ,会提示输入用户名和密码,如下图:
21.3.4.2 realm.properties文件内容
View Code
21.3.4.3 Maven坐标
<dependency> <groupId>org.eclipse.jetty</groupId> <artifactId>jetty-server</artifactId> <version>${project.version}</version> </dependency>
21.3.5 最简单Servlet
这个例子展示了如何部署一个最简单的servlet到Jetty,这是一个严格意义上的servlet而不是一个web应用的servlet。这个例子非常适合有一个用来测试一个简单的servlet。
View Code
Maven依赖:
<dependency> <groupId>org.eclipse.jetty</groupId> <artifactId>jetty-servlet</artifactId> <version>${project.version}</version> </dependency>
21.3.6 Web应用
这个例子展示如何部署一个简单的web应用到一个嵌入式的Jetty上。这种方式非常适合你用编程的方式管理一个服务的生命周期:
View Code
Maven依赖:
<dependency> <groupId>org.eclipse.jetty</groupId> <artifactId>jetty-webapp</artifactId> <version>${project.version}</version> </dependency>
21.3.7 包含JSP的web应用
这个例子和前一节的例子非常相似,尽管它使嵌入式的Jetty支持了JSP。Jetty9.2以后,使用了来自Apache的jsp引擎,它依赖servlet3.1规范以及ServletContainerInitializer 初始化。为了使其在Jetty中正常工作,你需要像下面例子一样使之支持注解:
View Code
Maven依赖
<dependency> <groupId>org.eclipse.jetty</groupId> <artifactId>jetty-annotations</artifactId> <version>${project.version}</version> </dependency> <dependency> <groupId>org.eclipse.jetty</groupId> <artifactId>apache-jsp</artifactId> <version>${project.version}</version> </dependency> <dependency> <groupId>org.eclipse.jetty</groupId> <artifactId>apache-jstl</artifactId> <version>${project.version}</version> </dependency>
二十二、HTTP客户端
22.1 简介
Jetty的HTTP模块提供简单易用的APIs和工具来处理HTTP(HTTPS)请求。Jetty的HTTP客户端是不封闭和异步的。它提供一个从不会堵塞I/O的异步的API,提高线程利用率,适合高性能场景如负载测试或并行计算。然而,如果你所有的请求都是使用GET方式来获得一个响应,Jetty的HTTP客户端还提供了一个同步API,这是一个编程接口,线程从发出请求直到请求/响应对话完成才释放。Jetty的HTTP客户端支持不同的协议,如HTTP/1.1, FastCGI 和 HTTP/2。这样HTTP请求可以以不同的方式获得资源,最常用的事HTTP/1.1。
FastCGI传输过程中大量使用Jetty FastCGI的支持,允许Jetty作为反向代理PHP(Apache或Nginx做完全一样),因此能够服务—例如WordPress网站。
简单易用的特性会让你得到这样的HTTP客户端:
- 重定向支持:像302,303重定向码是自动支持的
- Cookies支持:Cookies会自动发送到匹配的地址上
- 认证支持:HTTP的Basic和Digest认证是原始支持的,其他认证方法需要插件
- 请求代理支持:HTTP代理和SOCKS4 代理支持
22.1.1 开始使用HttpClient
主要的方法是org.eclipse.jetty.client.HttpClient,你可以把HttpClient 实例做为一个浏览器实例,像浏览器一样,它可以把请求发到不同的作用域上,管理重定向、Cookies、认证,你可以为它配置一个代理,它会提供你想要的请求和相应。
为了使用HttpClient,你必须首先实例它,然后配置它,最后启动它:
// 实例化一个HttpClient HttpClient httpClient = new HttpClient(); // 对HttpClient进行配置 httpClient.setFollowRedirects(false); // 启动HttpClient httpClient.start();
你可以启动多个HttpClient,但通常一个web应用启动一个HttpClient就足够了,启动多个HttpClient实例的原因要么是想要不同配置的HttpClient(例如:一个HttpClient使用代理,另一个不用),要么是你想有两个不同的浏览器,为了不同的cookies,不同的认证,还有就是你想有两个不同的传输。
当你使用无参构造方法实例化一个HttpClient时,你只能响应简单的HTTP请求,而不能响应HTTPS请求。
为了支持HTTPS请求,你首先应该创建一个SslContextFactory,配置它并将它传递给HttpClient的构造方法。当你创建SslContextFactory后,HttpClient即可在所有作用域里支持HTTP和HTTPS请求。
//创建并配置SslContextFactory SslContextFactory sslContextFactory = new SslContextFactory(); //通过 SslContextFactory创建HttpClient HttpClient httpClient = new HttpClient(sslContextFactory); //配置HttpClient httpClient.setFollowRedirects(false); //启动HttpClient httpClient.start();
22.1.2 终止HttpClient
当你应用停止时,建议你调用以下方法来终止一个HttpClient
httpClient.stop();
终止httpClien能确保,httpClient持有的资源(例如:认证证书,cookies等)被释放掉,线程和调度任务被适当的停止,并让被httpCLient使用的线程全部退出。
22.2 API使用
22.2.1 阻塞式APIs
解析一个http请求的最简单方法如下:
ContentResponse response = httpClient.GET("http://domain.com/path?query");
方法 HttpClient.GET(...)用来通过给定的URI来获得一个HTTP的GET请求,并且当成功响应后返回一个ContentResponse 。
ContentResponse 包含一个HTTP的相应信息:状态码、响应头、响应内容。内容大小被限制在2M内,为了获得更大的内容详见相应内容处理器。
如果你想订做请求信息,例如,使用请求头,请求方式或者浏览器版本,做法如下:
ContentResponse response = httpClient.newRequest("http://domain.com/path?query") .method(HttpMethod.HEAD) .agent("Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:17.0) Gecko/20100101 Firefox/17.0") .send();
上面代码时下面代码的简写形式:
Request request = httpClient.newRequest("http://domain.com/path?query"); request.method(HttpMethod.HEAD); request.agent("Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:17.0) Gecko/20100101 Firefox/17.0"); ContentResponse response = request.send();
你第一次通过httpClient.newRequest(...)方式创建一个Request对象,然后使用APIs来定制它(可以使用链式调用)。当Request请求被定制完成,你可以调用Request.send()方法来获得成功访问后的ContentResponse 对象。
简单的POST请求如下:
ContentResponse response = httpClient.POST("http://domain.com/entity/1") .param("p", "value") .send();
POST方式的参数通过 param() 方法,并自动进行URL编码。
Jetty的客户端会自动重定向,可以自动处理典型的WEB模式如, POST/Redirect/GET,随后的重定向可以被单独禁用或者全局禁用。
文件上传仅仅需要简单的一行,确保使用JDK7及以上版本用于支持java.nio.file类:
ContentResponse response = httpClient.newRequest("http://domain.com/upload") .method(HttpMethod.POST) .file(Paths.get("file_to_upload.txt"), "text/plain") .send();
也可以通过如下方式来规定请求超时时间:
ContentResponse response = httpClient.newRequest("http://domain.com/path?query") .timeout(5, TimeUnit.SECONDS) .send();
在上面的那个例子中,当5秒过后没有响应时,会抛出一个java.util.concurrent.TimeoutException异常。
22.2.2 非堵塞式APIs
到目前,我们已经展示了如何使用阻塞方式的Jetty HTTP客户端,线程需要等到请求/响应完成后才能结束。
这一章节,我们将着眼于非阻塞式、异步的Jetty HTTP客户端使用方法,这种方法非常适合大文件下载、并行处理,在这些情况下,性能和高效的线程性和资源利用率是一个关键因素。
异步APIs的实现依靠请求和响应各阶段的监听。这些监听器所实现的应用可以有各种类型的逻辑,监听器的实现在同一个线程中来完成请求或响应。所以如果在监听器内的应用程序代码需要较长的执行时间,那么请求或者响应将会等待直到监听器返回结果。
如果你要在监听器内执行一个需要很长时间的代码,你必须创建自己的线程,并且深度拷贝你需要的监听器的任何数据,因为当监听器返回数据后,监听器内的数据有可能会被回收、清除、销毁。
请求和响应将会被两个不同的线程处理也有可能同步进行,一个并发处理典型的例子是一个大文件上传可能与大大文件下载并发进行。符代替性下,响应的处理有可能会在请求没有结束前就已经完成了,典型的例子是一个大文件上传会立即触发一个响应回复如服务器返回的错误信息,响应完成了,但是上传文件依然在继续。
应用程序的线程调用Request.send(Response.CompleteListener)方法来对请求进行处理,直到请求被处理完或者阻塞I/O,那么它将返回(没有阻塞),如果它将阻塞I/O,那么这个线程会要求I/O系统提供一个I/O可以开始的事件,并返回,当这个事件被触发的时候,在HttpClient线程池中的线程将返回来继续处理请求。
响应会在可以有字节进行读的这一条件触发下执行,响应会一直执行知道响应完全执行完成或者将要堵塞I/O。如果它将堵塞I/O,那么线程会要求I/O系统提供一个I/O可以使用的触发事件,然后线程返回。当这个事件被触发的时候,在HttpClient线程池中的线程将返回来继续处理响应。
一个简单的异步GET方式的例子:
httpClient.newRequest("http://domain.com/path") .send(new Response.CompleteListener() { @Override public void onComplete(Result result) { // Your logic here } });
方法Request.send(Response.CompleteListener)没有返回值并且不阻塞,Response.CompleteListener作为一个参数被提供用来提醒当请求/响应会话完成的时候Result 参数会允许你调用响应体。
你也可以用JDK8的lambda表达式来书写同样的代码:
httpClient.newRequest("http://domain.com/path") .send(result -> { /* Your logic here */ });
你也可以使用同步的方式设置超时时间:
Request request = httpClient.newRequest("http://domain.com/path") .timeout(3, TimeUnit.SECONDS) .send(result -> { /* Your logic here */ });
HTTP客户端的APIs使用监听器来监听请求和响应的各种事件,使用JDK8的lambda表达式会更简单:
httpClient.newRequest("http://domain.com/path") // Add request hooks .onRequestQueued(request -> { ... }) .onRequestBegin(request -> { ... }) ... // More request hooks available // Add response hooks .onResponseBegin(response -> { ... }) .onResponseHeaders(response -> { ... }) .onResponseContent((response, buffer) -> { ... }) ... // More response hooks available .send(result -> { ... });
这让Jetty的HTTP 客户端非常适合测试,因为你可以精确的控制请求/响应的每一步的时间(从来知道请求和响应的时间的花在哪了)。
request的各种事件有:
- onBegin:请求开始
- onHeaders:Headers准备完毕可以发送
- onCommit:Headers已发送,等待进一步确认
- onContent:内容已发送
- onSuccess:成功
- onFailure:失败
response的各种事件有:
- onBegin:可以解析HTTP版本,HTTP状态码并可以解析收到的原因
- onHeader:接收到header ,不论是否被处理都要返回
- onHeaders:header 被接收并处理完成后
- onContent:org.eclipse.jetty.client.api.Response.ContentListener的方法,当响应的内容被接收到,可能会被执行多次,这个方法返回前缓冲区的内容必须被刷新。
- onContent:org.eclipse.jetty.client.api.Response.AsyncContentListener的方法
- onSuccess:异步回调方法调用时的响应内容已经收到
- onFailure:失败
- onComplete:当请求和响应都完毕吗,不管成不成功
22.2.3 内容处理器
22.2.3.1 请求的内容处理器
Jetty的http客户端提供很多工具类用来处理请求内容。
你可以把String、byte[]、ByteBuffer、java.nio.file.Path、InputStream或者你自己实现ContentProvider的类提供给request,这里有一个使用java.nio.file.Paths的例子:
ContentResponse response = httpClient.newRequest("http://domain.com/upload") .method(HttpMethod.POST) .file(Paths.get("file_to_upload.txt"), "text/plain") .send();
上面的例子等同于使用PathContentProvider 工具类:
ContentResponse response = httpClient.newRequest("http://domain.com/upload") .method(HttpMethod.POST) .content(new PathContentProvider(Paths.get("file_to_upload.txt")), "text/plain") .send();
同样,用户也可以通过InputStreamContentProvider工具类来使用FileInputStream:
ContentResponse response = httpClient.newRequest("http://domain.com/upload") .method(HttpMethod.POST) .content(new InputStreamContentProvider(new FileInputStream("file_to_upload.txt")), "text/plain") .send();
当InputStream 被阻塞了,因为输入的阻塞request也将会被堵塞,为了防止这种情况的发生可以使用HttpClient 异步的APIs。
如果你已经将内容读取内存中了,那么可以将内容做为一个byte[]提供给 BytesContentProvider工具类:
byte[] bytes = ...; ContentResponse response = httpClient.newRequest("http://domain.com/upload") .method(HttpMethod.POST) .content(new BytesContentProvider(bytes), "text/plain") .send();
如果request需要的内容不能立即准备好,那么你的应用程序会注意到什么时候会发送,DeferredContentProvider 会这么用:
DeferredContentProvider content = new DeferredContentProvider(); httpClient.newRequest("http://domain.com/upload") .method(HttpMethod.POST) .content(content) .send(new Response.CompleteListener() { @Override public void onComplete(Result result) { // 程序的逻辑 } }); // 内容还没有准备好 ... // 一个事件发生了,内容已经准备好了,提供给context byte[] bytes = ...; content.offer(ByteBuffer.wrap(bytes)); ... // 所有内容完毕后,关闭content content.close();
当一个请求因为客户端上传而等待时,服务器可以异步完成相应(至少响应头可以)。在这种情况下Response.Listener会在请求完全发送完后被调用。这样可以允许我们精确的控制请求/响应的会话:例如,服务器通过向客户端发送一个响应来拒绝客户端上传太大的文件到服务器中。
另一种方法提供请求内容的是OutputStreamContentProvider,这可以允许用户使用OutputStreamContentProvider提供的OutputStream,来等待有内容可以写入的时候:
OutputStreamContentProvider content = new OutputStreamContentProvider(); //使用try-with-resources方法在context写完后关闭OutputStream try (OutputStream output = content.getOutputStream()) { client.newRequest("localhost", 8080) .method(HttpMethod.POST) .content(content) .send(new Response.CompleteListener() { @Override public void onComplete(Result result) { // 你的逻辑 } }); ... // 写内容 writeContent(output); } //try-with-resource的结束,output.close()会在写完后自动调用
22.2.3.2 响应内容的处理
Jetty客户端允许程序通过不同的方式处理响应内容。
第一种方法是将响应内容缓存到内存中;一般用在阻塞式调用以及内容小于2M的时候。
如果你想控制响应内容的大小(例如限制响应内容的大小低于标准的2M),那么你可以使用org.eclipse.jetty.client.util.FutureResponseListener来处理:
Request request = httpClient.newRequest("http://domain.com/path"); //限制大小512KB FutureResponseListener listener = new FutureResponseListener(request, 512 * 1024); request.send(listener); ContentResponse response = listener.get(5, TimeUnit.SECONDS);
如果响应内容超过指定大小,那么响应将被中断,一个异常将从get()方法内抛出。
如果你是用异步apis(非阻塞的方式),你可以使用BufferingResponseListener 工具类:
httpClient.newRequest("http://domain.com/path") // 限制大小8M .send(new BufferingResponseListener(8 * 1024 * 1024) { @Override public void onComplete(Result result) { if (!result.isFailed()) { byte[] responseContent = getContent(); // 你自己的逻辑 } } });
第二种方法是最有效的(因为避免了内容的复制)并且允许你指定Response.ContentListener或者它的子类来处理即将到来的内容。在下面的这个例子中,Response.Listener.Adapter是一个同时实现了Response.ContentListener和Response.CompleteListener的可以传入到Request.send()中的类。Jetty的HTTP客户端将会执行onContent()方法0次或者多次(一有内容就会执行),并且最终会执行onComplete()方法。
ContentResponse response = httpClient .newRequest("http://domain.com/path") .send(new Response.Listener.Adapter() { @Override public void onContent(Response response, ByteBuffer buffer) { // 你的程序逻辑 } });
第三种方法允许你使用InputStreamResponseListener 工具类来等待response 的内容流:
InputStreamResponseListener listener = new InputStreamResponseListener(); httpClient.newRequest("http://domain.com/path") .send(listener); // 等待response头信息到来 Response response = listener.get(5, TimeUnit.SECONDS); // 核实response if (response.getStatus() == HttpStatus.OK_200) { // 使用try-with-resources来关闭资源 try (InputStream responseContent = listener.getInputStream()) { // 你自己的逻辑 } }
22.3 cookies支持
Jetty HTTP客户端很容易支持cookies。HttpClient 实例将从HTTP响应中接收cookies并把它们存在java.net.CookieStore中,这个class是JDK的一个类。当有新的请求时,如果有匹配的cookies(简单来说,cookies不能过期并要匹配请求的路径),那么cookies将会被添加到请求中。
应用程序可以以编程的方式访问cookies并进行设置:
CookieStore cookieStore = httpClient.getCookieStore(); List<HttpCookie> cookies = cookieStore.get(URI.create("http://domain.com/path"));
应用程序也可以使用编程的方式来设置cookies是否它们将从一个响应中返回:
CookieStore cookieStore = httpClient.getCookieStore(); HttpCookie cookie = new HttpCookie("foo", "bar"); cookie.setDomain("domain.com"); cookie.setPath("/"); cookie.setMaxAge(TimeUnit.DAYS.toSeconds(1)); cookieStore.add(URI.create("http://domain.com"), cookie);
cookies可以被添加到指定的请求中:
ContentResponse response = httpClient.newRequest("http://domain.com/path") .cookie(new HttpCookie("foo", "bar")) .send();
如果你不愿意在接下来的请求中使用cookies你也可以移除它:
CookieStore cookieStore = httpClient.getCookieStore(); URI uri = URI.create("http://domain.com"); List<HttpCookie> cookies = cookieStore.get(uri); for (HttpCookie cookie : cookies) cookieStore.remove(uri, cookie);
如果你想完全禁止使用cookies,那么你可以安装一个HttpCookieStore.Empty 实例这个方法来实现:
httpClient.setCookieStore(new HttpCookieStore.Empty());
你也可以实现一个cookies存储器来过滤cookies,用这种方法可以根据登录来过滤:
httpClient.setCookieStore(new GoogleOnlyCookieStore()); public class GoogleOnlyCookieStore extends HttpCookieStore { @Override public void add(URI uri, HttpCookie cookie) { if (uri.getHost().endsWith("google.com")) super.add(uri, cookie); } }
上面的例子将会只保留来自 google.com 或其子域的cookies。
22.4 证书支持
Jetty的客户端程序支持由 RFC 7235定义的 "Basic" 和"Digest"两种身份验证机制。
你可以在http客户端实例中配置身份验证如下:
URI uri = new URI("http://domain.com/secure"); String realm = "MyRealm"; String user = "username"; String pass = "password"; // 添加身份验证凭证 AuthenticationStore auth = httpClient.getAuthenticationStore(); auth.addAuthentication(new BasicAuthentication(uri, realm, user, pass)); ContentResponse response = httpClient .newRequest(uri) .send() .get(5, TimeUnit.SECONDS);
Jetty的http客户端会自动发送匹配的验证信息到服务器进行身份验证,如果验证成功后,那么将会把结果进行缓存,以后相同的域名或匹配的URI将不会再校验。
一个成功校验的HTTP会话如下:
Application HttpClient Server | | | |--- GET ---|------------ GET ----------->| | | | | |<-- 401 + WWW-Authenticate --| | | | | |--- GET + Authentication --->| | | | |<-- 200 ---|------------ 200 ------------|
当应用程序接收到401状态码时,不会响应这个事件,它将会在HttpClient 内部进行处理,会产生一个简单的携带校验信息头的请求到原地址,然后传递带有200状态码的响应到应用程序。
成功校验的结果会被缓存,但是也可以被清除,通常为了再次强制校验一遍:
httpClient.getAuthenticationStore().clearAuthenticationResults();
认证有可能会被提前,避免往返服务时间的资源的消耗:
AuthenticationStore auth = httpClient.getAuthenticationStore(); URI uri = URI.create("http://domain.com/secure"); auth.addAuthenticationResult(new BasicAuthentication.BasicResult(uri, "username", "password"));
在这种方式中,原始请求会有HttpClient 携带一个验证的头信息,并且服务端应该返回200,这种方式比返回401要更有挑战性(因为要保证验证信息的准确性)。
22.5 代理支持
Jetty的http客户端支持以代理的方式来访问目标地址。
有两种简单的方式来使用代理:一种是HTTP代理(由org.eclipse.jetty.client.HttpProxy类提供),另一种是SOCKS 4代理(由org.eclipse.jetty.client.Socks4Proxy类提供),还可以自己写一个roxyConfiguration.Proxy.的子类来实现代理。
一个典型的配置如下:
ProxyConfiguration proxyConfig = httpClient.getProxyConfiguration(); HttpProxy proxy = new HttpProxy("proxyHost", proxyPort); // 不使用代理访问 localhost:8080 proxy.getExcludedAddresses().add("localhost:8080"); // 添加新的代理 proxyConfig.getProxies().add(proxy); ContentResponse response = httpClient.GET(uri);
你可以指定代理的地址和端口,也可以有选择的指定某些地址不是用代理,并将代理配置添加到ProxyConfiguration 实例中。
如果以这种方式,httpclient建立请求到http代理(纯文本请求)或者建立一个隧道连接通过http连接(为了加密HTTPS的请求)。
22.5.1 代理认证支持
Jetty的http客户端支持代理身份认证,同样的它也支持服务端身份认证。
下面的例子,代理需要一个Basic 身份认证,但是服务器需要Digest 认证,如下:
URI proxyURI = new URI("http://proxy.net:8080"); URI serverURI = new URI("http://domain.com/secure"); AuthenticationStore auth = httpClient.getAuthenticationStore(); // 代理认证. auth.addAuthentication(new BasicAuthentication(proxyURI, "ProxyRealm", "proxyUser", "proxyPass")); // 服务端认证 auth.addAuthentication(new DigestAuthentication(serverURI, "ServerRealm", "serverUser", "serverPass")); // 代理配置 ProxyConfiguration proxyConfig = httpClient.getProxyConfiguration(); HttpProxy proxy = new HttpProxy("proxy.net", 8080); proxyConfig.getProxies().add(proxy); ContentResponse response = httpClient.newRequest(serverURI) .send() .get(5, TimeUnit.SECONDS);
若HTTP会话成功认证且代理也成功认证的会话过程如下:
Application HttpClient Proxy Server | | | | |--- GET -->|------------- GET ------------->| | | | | | | |<----- 407 + Proxy-Authn -------| | | | | | | |------ GET + Proxy-Authz ------>| | | | | | | | |---------- GET --------->| | | | | | | |<--- 401 + WWW-Authn ----| | | | | | |<------ 401 + WWW-Authn --------| | | | | | | |-- GET + Proxy-Authz + Authz -->| | | | | | | | |------ GET + Authz ----->| | | | | |<-- 200 ---|<------------ 200 --------------|<--------- 200 ----------|
应用程序不会接收到407或401状态码因为处理全部在httpclient内部完成。
同样的验证,代理和服务端的验证可能被避免,特别当407和401循环返回时。
22.6 可选择式传输
Jetty HTTP客户端可以被配置为使用不同的传输进行HTTP请求和响应。这意味着客户端使用GET方式获取资源/index.html时,可以使用不同的格式。程序并不会察觉到它们实际应用的协议。用户可以使用高级语言的API来编写逻辑隐藏具体的传输细节。
最常用的协议是HTTP/1.1,基于文件的以分割线 \r\n分割的:
GET /index.html HTTP/1.1\r\n
Host: domain.com\r\n
...
\r\n
然而,相同的请求也可以使用一种二进制协议FastCGI:
x01 x01 x00 x01 x00 x08 x00 x00 x00 x01 x01 x00 x00 x00 x00 x00 x01 x04 x00 x01 xLL xLL x00 x00 x0C x0B D O C U M E N T _ U R I / i n d e x . h t m l ...
同样,HTTP/2也是一种二进制协议,以不同的格式传输相同的信息。
22.6.1 HTTP/1.1传输协议
HTTP/1.1传输协议是默认的传输协议:
// 若没有指定,则默认使用它 HttpClient client = new HttpClient(); client.start();
如果你想定制HTTP/1.1协议,你可以配置用如下的方式配置HttpClient :
int selectors = 1; HttpClientTransportOverHTTP transport = new HttpClientTransportOverHTTP(selectors); HttpClient client = new HttpClient(transport, null); client.start();
上面的例子允许你定制一定数量的NIO选择器给HttpClient 使用。
22.6.2 HTTP/2 传输协议
HTTP/2可以用如下的方式进行配置:
HTTP2Client h2Client = new HTTP2Client(); h2Client.setSelectors(1); HttpClientTransportOverHTTP2 transport = new HttpClientTransportOverHTTP2(h2Client); HttpClient client = new HttpClient(transport, null); client.start();
HTTP2Client是低级别的客户端,它提供了一个API基于HTTP / 2,像session,strean和frames特定于HTTP / 2。HttpClientTransportOverHTTP2 使用HTTP2Client 来格式高级语义的HTTP请求("GET resource /index.html")。
22.6.3 FastCGI 传输协议
FastCGI 协议可以用如下的方式进行配置:
int selectors = 1; String scriptRoot = "/var/www/wordpress"; HttpClientTransportOverFCGI transport = new HttpClientTransportOverFCGI(selectors, false, scriptRoot); HttpClient client = new HttpClient(transport, null); client.start();
为了确保请求使用FastCGI 协议,你需要有一个像 PHP-FPM 一样的FastCGI 服务器。
FastCGI 传输协议主要用于支持提供页面的PHP服务支持(例如:WordPress)。