【原创文章,转载请注明原文章地址,谢谢!】
本小节简单介绍Jersey提供的客户端API(Client API)。Jersey的客户端API能够让我们非常方便的创建出REST的Web服务客户端,不管是客户端应用,还是用于测试的代码,都是非常容易和舒服的。比较原生的使用HTTPUrlConnection或者Apache HttpClient,都更加的方便和强大。本小节的大部分示例代码来源于Jersey的文档(https://jersey.github.io/documentation/latest/client.html)。
依赖
首先我们要了解到,在JAX-RS中,提供了一系列的标准的Client API,而Jersey为了更好的实现和扩展这套API,提供了一种扩展机制,即实现了org.glassfish.jersey.client.spi.Connector接口,就可以提供不同具体实现的Client API实现。比如默认使用JDK的Http(s)URLConnection,也可以使用HttpClient的实现,还可以使用Jetty的实现,grizzly的实现等等。所以,在引入依赖的时候需要注意这点:
<dependency>
<groupId>org.glassfish.jersey.core</groupId>
<artifactId>jersey-client</artifactId>
<version>2.26</version>
</dependency>
默认的实现,使用JDK的实现;可以选择对应其他的实现方式:
<dependency>
<groupId>org.glassfish.jersey.connectors</groupId>
<artifactId>jersey-grizzly-connector</artifactId>
<version>2.26</version>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.connectors</groupId>
<artifactId>jersey-apache-connector</artifactId>
<version>2.26</version>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.connectors</groupId>
<artifactId>jersey-jetty-connector</artifactId>
<version>2.26</version>
</dependency>
比如选择使用grizzly就引入对应的jersey-grizzly-connector,选择使用apache的Httpclient,引入jersey-apache-connector即可。一般情况下,比如针对普通测试,使用默认的JDK实现即可。
Client API快速入门
既然是一种非常方便的代码,我们直接使用一段DEMO来看看Client API的基本使用方式,有一个直观的了解:
Client client = ClientBuilder.newClient();
WebTarget target = client.target("http://localhost:9998").path("resource");
Form form = new Form();
form.param("x", "foo");
form.param("y", "bar");
MyJAXBBean bean =
target.request(MediaType.APPLICATION_JSON_TYPE)
.post(Entity.entity(form,MediaType.APPLICATION_FORM_URLENCODED_TYPE),
MyJAXBBean.class);
非常直观的理解:
1,首先使用ClientBuilder的工厂方法newClient创建一个Client对象,可以理解为一个浏览器(客户端);
2,使用client对象的target方法,创建一个WebTarget,这里注意链式调用,首先使用target方法,接着再使用path方法,即WebTarget对象对应的实际资源地址为http://localhost:9998/resource;
3,创建一个Form对象,代表(或者说模拟一个表单),并为表单创建了两个值域:x,y,对应的值为foo和bar;
4,使用WebTarget对象的request方法创建一个请求,注意请求的期望响应类型为application/json;然后接着使用链式方法post,发起一个POST请求,并且在请求的过程中,传入一个实体(Entity),而这个实体就是我们上面构造的Form表单对象,并且采用的是x-www-form-urlencoded编码格式,post方法的第二个参数,就是接受的响应类型,我们需要把json响应直接转成MyJAXBBean对象。
通过这个例子,我们可以很简单的看到使用Jesey的ClientAPI创建相对复杂的请求的例子。不过,在这个例子中,其实隐藏了很多细节的代码和对象,方法,下面来更进一步的了解。
Client API简介
在上一节中我们通过一个直观的代码对Jersey的Client API有了一个了解,我相信其中的链式代码方式,应该有比较深刻的影响。我已经说到,其实在这段代码之下,隐藏了很多API细节,下面就来通过拆分代码,看看这些细节。
创建Client
一般开始一个客户端代码,都是从创建一个Client对象开始。通常情况下,都是使用ClientBuilder的newClient方法来创建一个Client:
Client client = ClientBuilder.newClient();
当我们稍微深入一点newClient方法,其实我们可以看到具体的实现其实是这样的:
public static Client newClient() {
return newBuilder().build();
}
可以看到,其实我们的Client对象是通过newBuilder()方法首先创建一个ClientBuilder实例,在通过这个实例的build方法创建的Client对象。换句话说,其实ClientBuilder内部提供了更为复杂的build模式,我们完全可以通过ClientBuilder.newBuilder()方法先创建一个ClientBuilder对象,并且对这个对象进行一系列的配置,再调用build方法来创建Client对象。
其次,我们可以发现,ClientBuilder类其实实现了javax.ws.rs.client.Configurable接口,这个接口我们在介绍ResourceConfig的时候介绍过,那么通过这个接口,我们可以为一个ClientBuilder配置更多的内容,比如配置参数,比如配置Provider(比如上面那个案例中,JSON->MyJAXBBean对象,就使用了之前介绍的Entity Provider),这也是为什么我们要在介绍拦截器和过滤器之前介绍Client API的原因,就是Client API也是能够使用各种Provider,Filter,Interceptor;
另外,ClientBuilder还提供了基于一个javax.ws.rs.client.Configurable的构造方法:
public abstract ClientBuilder withConfig(Configuration config);
public static Client newClient(final Configuration configuration) {
return newBuilder().withConfig(configuration).build();
}
更进一步,ClientBuilder本身就是实现了Configuration接口,这就意味着,我们可以使用一个ClientBuilder来配置另外一个ClientBuilder(实际开发中,我们更多使用的是ClientConfig对象,后面介绍),即我们可以完成ClientBuilder配置的复用。这有什么用?我们继续看,在ClientBuilder中还提供了HTTPS相关方法:
public abstract ClientBuilder sslContext(final SSLContext sslContext);
public abstract ClientBuilder keyStore(final KeyStore keyStore, final char[] password);
public abstract ClientBuilder trustStore(final KeyStore trustStore);
那么,针对一个SSL应用,我们就可以首先创建一个基本的SSL全局配置的ClientBuilder对象,然后再每次具体请求的时候,使用这个全局配置对象来构建新的ClientBuilder对象,然后再设置具体的特殊配置。这个技巧后面还会详细介绍。
使用ClientConfig
上一节说道,我们可以使用一个Configuration对象来创建一个Client对象,在实际的开发中,承担这个配置对象角色的更常见的是使用org.glassfish.jersey.client.ClientConfig;下面来看一个代码片段:
ClientConfig clientConfig = new ClientConfig();
clientConfig.register(MyClientResponseFilter.class);
clientConfig.register(new AnotherClientFilter());
Client client = ClientBuilder.newClient(clientConfig);
在这段代码中,我们可以明显的看到,首先创建了一个ClientConfig对象,然后使用该对象的register方法,注册了两个过滤器(下一篇文章介绍),然后再使用这个clientConfig对象,创建了一个Client对象。那么这个Client对象,就拥有了两个Filter的功能。
之所以能够通过ClientConfig对象注册Filter,根本原因还是在于ClientConfig类也实现了Configuration接口:
public class ClientConfig implements Configurable<ClientConfig>, ExtendedConfig
在ClientConfig类中,还提供了一些其他的构造方法:
public ClientConfig(final Class<?>... providerClasses)
public ClientConfig(final Object... providers)
比如上面的代码就可以重写为:
Client client = ClientBuilder.newClient(
new ClientConfig(MyClientResponseFilter.class,AnotherClientFilter.class));
ClientConfig的复制
先来看一段代码:
ClientConfig c1 = new ClientConfig();
c1.register(MyClientResponseFilter.class);
c1.register(new AnotherClientFilter());
Client client = ClientBuilder.newClient(c1);
client.register(ThirdClientFilter.class);
Configuration c2 = client.getConfiguration();
为了看的更加清楚,我把代码拆分为单行代码,可以看到:
1,创建一个ClientConfig对象c1;
2,为c1注册了两个过滤器;
3,使用c1创建了一个Client对象client;
4,注意这句代码,我们在client对象上,再次注册了一个ThirdClientFilter,之所以能这样做,不用多说,Client对象肯定也实现了Configuration接口;
5,然后调用client.getConfiguration()方法获取到了一个新的配置对象c2;
代码本身没有什么难度,但是这段代码揭示了Jersey一个很重要的概念,就是ClientConfig的复制。在这段代码中,我们先创建了一个ClientConfig对象c1,然后通过c1创建了Client对象,然后在这个Client对象上面额外添加了一个过滤器,然后再通过Client对象得到一个新的配置对象c2;那么这时候,c1,Client,c2这三个对象的过滤器情况是怎么样的呢?
1,c1的过滤器仍然只有MyClientResponseFilter和AnotherClientFilter;
2,Client对象上有MyClientResponseFilter,AnotherClientFilter和ThirdClientFilter这三个过滤器;
3,c2的过滤器有MyClientResponseFilter,AnotherClientFilter和ThirdClientFilter这三个过滤器;
换句话说,client对象的过滤器增加,并没有影响到原始的c1。这个概念对于我们上文已经提到过的统一基础配置的概念非常重要,来看这段代码:
ClientConfig baseConfig = new ClientConfig()
.register(MyClientResponseFilter.class)
.register(new AnotherClientFilter());
Client client1 = ClientBuilder.newClient(baseConfig)
.register(SomeOtherFilter.class);
//do something use client1;
Client client2 = ClientBuilder.newClient(baseConfig)
.register(CustomerFilter.class);
//do something use client2;
在这段代码中,充分利用了ClientConfig的拷贝原理,创建了一个基础的baseConfig对象,然后使用这个baseConfig创建了client1和client2,并且client1和client2各自有自己的Filter,但是他们各自注册的filter都不会影响原始的baseConfig对象,所以后面我们可以接着使用baseConfig对象去创建新的Client;
定位一个资源
创建好Client对象之后,就可以使用target方法来定位一个资源。
WebTarget webTarget = client.target("http://example.com/rest");
得到一个WebTarget对象。我们简单来认识一下WebTarget对象。该类代表一个资源URI:
public interface WebTarget extends Configurable<WebTarget>
并且可以看到,该类实际继承了Configurable接口,所以我们还可以再某一个单独的URI上面配置Filter等。
其次,该对象提供了以下几个常用的方法:
//创建一个子资源URI
public WebTarget path(String path);
//为当前URI设置矩阵参数(参考参数绑定一文)
public WebTarget matrixParam(String name, Object... values);
//为当前URI设置查询参数(参考参数绑定一文)
public WebTarget queryParam(String name, Object... values);
//发起一个请求,获取请求执行器
public Invocation.Builder request();
要注意一点的是,一般我们会使用target方法来定位WEB API的根资源,而再通过path方法来创建具体每次请求的子资源,比如:
WebTarget webTarget = client.target("http://www.wolfcode.com/api");
webTarget.register(FilterForExampleCom.class);
WebTarget resourceWebTarget = webTarget.path("resource");
WebTarget helloworldWebTarget = resourceWebTarget.path("helloworld");
WebTarget helloworldWebTargetWithQueryParam =
helloworldWebTarget.queryParam("greeting", "HiWorld!");
1,先针对应用的根路径创建一个webTarget,并设置了一个过滤器,那么这个过滤器会被由webTarget创建出来的所有的子资源所使用;
2,使用webTarget创建了第一个子资源resourceWebTarget,实际资源URI为:http://www.wolfcode.com/api/resource;
3,继续使用webTarget创建了子资源helloworldWebTarget,实际资源URI为:http://www.wolfcode.com/api/helloworld;
4,然后再使用helloworldWebTarget,添加请求参数greeting,值为hiworld,那么实际的请求URI为:http://www.wolfcode.com/api/helloworld?greeting=hiworld
执行一个HTTP请求
当获取一个资源URI之后,就可以通过WebTarget对象的request方法创建一个Invocation.Builder类,这个类代表一个HTTP请求执行器:
Invocation.Builder invocationBuilder =
helloworldWebTargetWithQueryParam.request(MediaType.TEXT_PLAIN_TYPE);
invocationBuilder.header("some-header", "true");
可以看到,我们使用request方法,传入了一个MediaType,这个MediaType就相当于请求的Accept请求头;我们还可以通过Invocation.Builder对象的header方法,为本次请求添加请求头;下面先来看看WebTarget的request方法重载:
public Invocation.Builder request();
public Invocation.Builder request(String... acceptedResponseTypes);
public Invocation.Builder request(MediaType... acceptedResponseTypes);
非常直观,第一种方法创建一个默认的Invocation.Builder,第二,三种方式可以直接配置请求的Accept类型;
下面再来看看Invocation.Builder类的常见方法:
//创建一个指定请求方法的请求
public Invocation build(String method);
//创建一个指定请求方法的请求,并添加请求实体内容
public Invocation build(String method, Entity<?> entity);
//创建一个GET请求
public Invocation buildGet();
//创建一个DELETE请求
public Invocation buildDelete();
//创建一个POST请求,并添加POST请求实体内容;
public Invocation buildPost(Entity<?> entity);
//创建一个PUT请求,并添加请求实体内容;
public Invocation buildPut(Entity<?> entity);
//设置接受请求MIME格式;
public Builder accept(MediaType... mediaTypes);
//设置接受的编码格式;
public Builder acceptEncoding(String... encodings);
//添加Cookie内容;
public Builder cookie(String name, String value);
//添加请求头信息;
public Builder header(String name, Object value);
我相信这些方法的使用都是非常直观的。而可以看到在Invocation.Builder接口中的buildXXX方法,都是返回一个Invocation对象,那么Invocation中又有哪些方法呢?
//执行请求;得到一个响应对象
public Response invoke();
//执行请求;将响应转成一个指定类型对象;
public <T> T invoke(Class<T> responseType);
//异步执行请求(关于异步请求,SSE,WebSocket会单开专题);
public Future<Response> submit();
所以,我们一个请求可以这样执行:
Invocation.Builder invocationBuilder helloworldWebTargetWithQueryParam
.request(MediaType.TEXT_PLAIN_TYPE)
.header("some-header", "true");
String responseText=invocationBuilder.buildGet().invoke(String.class);
而,又因为,Invocation.Builder接口还实现了SyncInvoker接口:
public static interface Builder extends SyncInvoker
所以,我们还可以看到类似这样的代码:
Response response = invocationBuilder.get();
而类似get的方法,就是来自于SyncInvoker接口:
Response get();
<T> T get(Class<T> responseType);
Response put(Entity<?> entity);
<T> T put(Entity<?> entity, Class<T> responseType);
Response post(Entity<?> entity);
<T> T post(Entity<?> entity, Class<T> responseType);
Response delete();
<T> T delete(Class<T> responseType);
Response head();
Response options();
Response trace();
<T> T method(String name, Class<T> responseType);
我从SyncInvoker接口中摘录了几条方法,我相信这些方法的签名已经非常明确的说明了方法的概念,比如<T> T put(Entity<?> entity, Class<T> responseType);那非常明显,就是对当前Invocation.Builder调用PUT方法请求,并传入请求实体(entity),要求的响应转化成responseType类型。
下面来看一个综合一点的代码:
//创建一个配置对象
ClientConfig clientConfig = new ClientConfig();
//注册过滤器
clientConfig.register(MyClientResponseFilter.class);
clientConfig.register(new AnotherClientFilter());
//创建一个Client对象
Client client = ClientBuilder.newClient(clientConfig);
//为这个Client对象再注册一个过滤器
client.register(ThirdClientFilter.class);
//创建根资源路径URI
WebTarget webTarget = client.target("http://wolfcode.cn/api");
//为根资源路径添加一个过滤器;
webTarget.register(FilterForExampleCom.class);
//创建一个子资源URI
WebTarget helloworldWebTarget = resourceWebTarget.path("helloworld");
//添加查询参数;
WebTarget helloworldWebTargetWithQueryParam =
helloworldWebTarget.queryParam("greeting", "Hi World!");
//获取执行器
Invocation.Builder invocationBuilder =
helloworldWebTargetWithQueryParam.request(MediaType.TEXT_PLAIN_TYPE);
//添加头信息
invocationBuilder.header("some-header", "true");
//对当前URI执行GET方法请求;获得响应对象
Response response = invocationBuilder.get();
//从响应对象中获取需要的内容
System.out.println(response.getStatus());
System.out.println(response.readEntity(String.class));
这就是一个标准的请求代码流程。但这段代码是完全拆分的代码,目的是为了让大家看清楚参与整个请求流程的每一个对象的作用;下面是这段代码的使用链式代码样式,我们做个对比:
Client client = ClientBuilder.newClient(new ClientConfig()
.register(MyClientResponseFilter.class)
.register(new AnotherClientFilter()));
String entity = client.target("http://wolfcode.cn/api")
.register(FilterForExampleCom.class)
.path("helloworld")
.queryParam("greeting", "Hi World!")
.request(MediaType.TEXT_PLAIN_TYPE)
.header("some-header", "true")
.get(String.class);
可以看到简化之后的代码的可读性是非常强的。
连接的关闭
在Jersey中,当返回的Response中的Entity被读取之后,连接自动关闭。如下代码所示:
final WebTarget target = ... some web target
Response response = target.path("resource").request().get();
//到这里,连接仍然开启,因为还没有读取response;
System.out.println("string response: " + response.readEntity(String.class));
//到这里,连接已经关闭,因为上面一句代码已经读取了response中的entity;
需要注意的一点,如果返回的response中有输入流(比如之前介绍的下载);那么在读取inputstream的过程中,连接保持开启。这种情况下,就需要在读取完毕inputstream之后,手动关闭输入流即可。
小结
在本节中,我们简单的看到了Jersey客户端API的一个基本的结构和使用方法,当然这仅仅只是一个很基础的说明,能完成常见的80%以上的测试代码情况,具体的更多的API大家可以参考文档。关于Entity,过滤器,拦截器的处理,我们在后面的章节中介绍。
WechatIMG7.jpeg
网友评论