目录
Jersey REST客户端
Jersey2.x的客户端包jersey-.client实现了JAX-RS2的客户端API,并对其进行了可插拔的(pluggable)扩展。客户端API通过HTTP协议请求Web资源,其设计宗旨是使客户端API符合统一接口和REST架构风格,同时客户端API应该易于使用,而且服务器端在概念和扩展点上保持一致。与Apache HTTP Client和HttpURLConnection相比,客户端API是可感知REST的高层API,可以与Providers集成,返回值直接对应高层的业务类实例,而不是JAXB对象或者更为低层的数据类型。
1、客户端接口
REST客户端主要包括3个接口:
- javax.ws.rs.client.Client
- javax.ws.rs.client.WebTarget
- javax.ws.rs.client.Invocation
1.1、Client接口
Client接口是REST客户端的基本接口,用于和REST服务器通信。Client被定义为一种重量级的对象
,其内部要管理客户端通信底层实现所需的各种对象,比如连接器、解析器
等。因此,不推荐在应用中产生大量的Client实例。
Jersey对JAX-RS2的Client接口的实现类是org.glassfish,jersey.client.JerseyClient
。创
建一个Client实例是通过ClientBuilder构造的,通常使用一个ClientConfig实例作为参数,该实例的构建过程是23种设计模式中构造模式的实践。在传入ClientConfig实例前,通常会注册(register)相关的Provider和Feature,也可以设定自定义属性(键值对),但这些编码都是可选的。最简单的形式是没有参数的构造,示例如下:
Client client = ClientBuilder.newClient();
通常情况下,客户端需要加载指定的资源、注册某些provider或者预定义某些属性,以支持更复杂的业务逻辑。我们在ClientConfig实例中完成了这些配置后,将其作为参数并在Client构建时传入,示例如下:
ClientConfig clientConfig = new ClientConfig();
clientConfig.register(MyProvider.class);//注册provider
clientConfig.register(MyFeature.class);//注册Feature
clientConfig.register(new AnotherClientFilter());//注册Filter
Client client = ClientBuilder.newClient(clientConfig);
client.property("myProperty", "myValue");//注册属性
在配置完毕后,可以通过Client的getConfiguration()方法获取当前Client实例的配置信息,示例如下:
//获取配置
Configuration configuration = client.getConfiguration();
//获取属性
Map<String, Object> properties = configuration.getProperties();
1.2、WebTarget接口
WebTarget接口是为REST客户端实现资源定位的接口。通过WebTarget接口,我们可以定义请求资源的具体地址、查询参数和媒体类型等信息。
Jersey对JAX-RS2的WebTarget接口的实现类是org.glassfish.jersey.client.JerseyWebTarget
。
WebTarget对象接收配置参数的方法定义有很浓厚的DSL(Domain Specific Language)味道,通过方法链即可完成对一个WebTarget实例的配置。但是,值得注意的是,如果不将方法链写成一行,而是要分开来写,需要知道WebTarget接口所采用的方法链模式是一个不变式(immutable)。也就是说,其返回值是一个新的WebTarget对象,我们无法像使用StringBuilder的append方法(该方法返回的是this)那样设置WebTarget对象,而是必须接
收设置方法的返回值,作为后续流程的句柄。
Client client = ClientBuilder.newClient();
WebTarget webTarget = client.target("uri");
//分开写需要接收返回值,因为每次调用都会返回一个新的实例
webTarget.path("books");//设置被丢弃
webTarget.queryParam("bookId", "1");
//应使用链式调用
webTarget.path("books").queryParam("bookId", "1");
1.3、Invocation接口
Invocation接口是在完成资源定位配置后,向REST服务端发起请求的接口。请求包括同步
和异步
两种方式,由Invocation接口内部的Builder接口定义,Builder接口继承了同步接口SyncInvoker。SyncInvoker中定义了标准的HTTP请求方法。
Jersey对JAX-RS2的Invocation接口的实现类是org.glassfish.jersey.client.JerseyInvocation
。
Invocation.Builder接口实例分别执行了GET请求和POST请求来提交查询和创建。默认情况下,HTTP方法调用的返回类型是Response类型,同时也支持泛型类型的返回值。
Client client = ClientBuilder.newClient();
WebTarget webTarget = client.target("uri");
WebTarget queryTarget = webTarget.path("books").queryParam("bookId", "1");
//设置请求的媒体类型
Invocation.Builder request = queryTarget.request(MediaType.APPLICATION_XML_TYPE);
//get请求
request.get();
//post请求,参数为请求体
Response response = request.post(Entity.entity("param", MediaType.TEXT_PLAIN_TYPE));
//获得返回实体,调用readEntity方法后自动释放
Book result = response.readEntity(Book.class);
如果某些异构的服务端只能返回Lst类型的POJO集合数据,我们将无法简便地使用Books这样的封装类型。此时,我们可以使用GenericType来解决,示例代码如下。
Client client = ClientBuilder.newClient();
WebTarget webTarget = client.target("uri");
WebTarget queryTarget = webTarget.path("books").queryParam("bookId", "1");
//设置请求的媒体类型
Invocation.Builder request = queryTarget.request(MediaType.APPLICATION_XML_TYPE);
//get请求
request.get();
//post请求,参数为请求体
Response response = request.post(Entity.entity("param", MediaType.TEXT_PLAIN_TYPE));
GenericType<List<Book>> genericType = new GenericType<List<Book>>(){};
List<Book> books = response.readEntity(genericType);
2、连接池
2.1、资源释放
作为REST框架,JAX-RS2不希望开发者编码实现对客户端实例的资源管理,Response实例的readEntity(在返回响应实体的同时,即完成了对客户端资源的释放。因此,开发者无须担心连接释放等资源管理细节。
也许开发者的编码没有使用Response的readEntity()而是直接返回泛型类型的对象,但是其底层还是使用了这个方法。
2.2、连接器
public interface Connector extends Inflector<ClientRequest, ClientResponse> {
@Override
ClientResponse apply(ClientRequest request);
Future<?> apply(ClientRequest request, AsyncConnectorCallback callback);
public String getName();
public void close();
}
Connector接口是REST客户端底层连接器接口,Jersey为Connector接口提供了4个实现:
- HttpUrlConnector类位于
jersey-client
包,是REST客户端的默认连接器
。 - ApacheConnector类位于
jersey-apache-connector
包。 - GrizzlyConnector类位于
jersey-grizzly-connector
包。 - InMemoryConnector类位于
jersey-test-framework-provider-inmemory
,不是一种真实的HTTP连接器,而是使用JVM调用来模拟HTTP请求访问。
1)、默认连接器
默认情况下,Jersey对JAX-RS2的Connector接口的实现类是org.glassfish.jersey.client.HttpUrlConnector
。如果开发者不显式地配置org.glassfish.jersey..client.ClientConfig
,那么在
Client初始化时会默认构造一个HttpUrlConnector类的实例作为连接器,HttpUrlConnector类底层的连接使用java.net.HttpURLConnection类。
2)、Apache连接器
<dependency>
<groupId>org.glassfish.jersey.connectors</groupId>
<artifactId>jersey-apache-connector</artifactId>
</dependency>
ApacheConnector是通过Apache提供的HTTP连接器实现,相比默认的连接器功能更完整、更强大。在Client实例中使用ApacheConnector是通过配置ClientConfig的connector来实现的,示例如下。
ClientConfig clientConfig = new ClientConfig();
clientConfig.connectorProvider(new ApacheConnectorProvider());
Client client = ClientBuilder.newClient(clientConfig);
ClientConfig实例还可以配置和Apache连接器相关的属性:
ClientConfig clientConfig = new ClientConfig();
//代理服务器配置
clientConfig.property(ClientProperties.PROXY_URI, "http:127.0.0.1");
clientConfig.property(ClientProperties.PROXY_USERNAME, "name");
clientConfig.property(ClientProperties.PROXY_PASSWORD, "password");
//连接超时设置
clientConfig.property(ClientProperties.CONNECT_TIMEOUT, 1000);
clientConfig.property(ClientProperties.READ_TIMEOUT, 2000);
- 代理服务器设置:通过属性值ClientProperties…PROXY_URI(Jersey.2.5以前是jersey-apache.connector包的ApacheClientProperties.PROXY_URI),可以在Client实例中定义HTTP代理服务器的地址。ClientProperties.PROXY USERNAME和ClientProperties.PROXY PASSWORD分别用
于配置代理服务器的用户名和口令。 - 超时设置:默认情况下,连接超时和读取超时是不设置的,就是说客户端一直尝试连接或者读取,直到成功或被服务器主动断开。通过设置ClientProperties.CONNECT TIMEOUT可以定义客户端尝试连接的最大时间,单位是毫秒(milliseconds),当超出这个时间,客户端会主动放弃连接并抛出超时异常ConnectTimeoutException。读取超时是指连接和资源定位成功后,客户端接收服务器响应消息的最大时间。通过设置ClientProperties.READ TIMEOUT可以定义客户端读取超时时间,单位是毫秒(milliseconds),当超出这个时间,客户端会主动关闭连接,抛出SocketTimeoutException异常。
3)、Grizzly连接器
GrizzlyConnector是Grizzly提供的连接器实现,其内部使用异步处理客户端com.ning.htp.client.AsyncHttpClient
类作为底层的连接,该类来自于AsyncHttpClient项目(地址是
htps:/∥github.com/AsyncHttpClient)。该项目的2.0版本将Maven的坐标信息进行了更改:groupld从com.ning.htp.client
改为org.asynchhttpclient,.com.ning.htp.client
包名改为org asynchhttpclient
。
在引用Grizzly连接器时应当注意所用版本的Maven坐标是否符合上述定义。
<dependency>
<groupId>org.glassfish.jersey.connectors</groupId>
<artifactId>jersey-grizzly-connector</artifactId>
</dependency>
AsyncHttpClient使用GrizzlyAsyncHttpProvider处理HTTP请求异步事件,GrizzlyAsyncHttpProvider是AsyncHandler接口的实现类。在Client实例中使用GrizzlyConnector是通过配置ClientConfig的connector来实现的,示例如下。
ClientConfig clientConfig = new ClientConfig();
clientConfig.connectorProvider(new GrizzlyConnectorProvider());
Client client = ClientBuilder.newClient(clientConfig);
2.3、HTTP连接池
Client的内部不只是简单地包含一个HTTP连接器的设计,而是携带了诸如资源状态等信息,因此在系统中频繁地创建Client实例会影响总体的性能。一种常见的解决方案是使用HTTP连接池来管理连接,而不是每次请求都创建一个Client实例。
这里使用ApacheConnector来实现HTTP连接池,示例如下。
ClientConfig clientConfig = new ClientConfig();
clientConfig.connectorProvider(new ApacheConnectorProvider());
//配置连接池管理实例
PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
cm.setMaxTotal(11);
cm.setDefaultMaxPerRoute(11);
//将连接池管理实例配置为ClientConfig的属性值
clientConfig.property(ApacheClientProperties.CONNECTION_MANAGER , cm);
//配置Apache连接器
clientConfig.connectorProvider(new ApacheConnectorProvider());
//根据配置创建客户端
Client client = ClientBuilder.newClient(clientConfig);
3、封装Client
通常,REST式的Web服务会按模块分别提供独立的Wb服务,而模块之间的调用通过Web服务的REST API来实现。因此,每个模块对其他模块的调用是客户端请求,不必
在每次请求时重复编写构造客户端实例的代码。为此,抽象出Client到公共模块是非常有必要的。封装Client可以减少代码冗余,也为统一Client构建策略(避免配置不统一带来的混乱)提供了可能,示例代码如下。
public class WSUtil<T> {
/**
* webservice rest请求工具类
* @param method 请求方式:GET、POST、PUT、DELETE
* @param requestUrl 请求路径
* @param headParams 请求头
* @param queryParams 查询参数
* @param requestDataType 请求的媒体类型
* @param responseDataType 响应的媒体类型
* @param returnType 返回实体类型
* @param requestData 请求体
* @param clientConfig 客户端配置
* @return
*/
public T rest(String method,
String requestUrl,
Set<Pair<String, Object>> headParams,
Set<Pair<String, Object>> queryParams,
MediaType requestDataType,
MediaType responseDataType,
Class<T> returnType,
T requestData,
ClientConfig clientConfig) {
//构造Client
Client client;
if (clientConfig != null) {
client = ClientBuilder.newClient(clientConfig);
} else {
client = ClientBuilder.newClient();
}
//构造WebTaget
WebTarget webTarget = client.target(requestUrl);
if (queryParams != null) {
for (Pair<String, Object> queryPair : queryParams) {
webTarget = webTarget.queryParam(queryPair.getKey(), queryPair.getValue());
}
}
//构造Invocation.Builder
Invocation.Builder request = webTarget.request(responseDataType);
if (headParams != null) {
for (Pair<String, Object> headParam : headParams) {
request.header(headParam.getKey(), headParam.getValue());
}
}
//发起请求和结果处理
Response response;
Entity<T> entity;
switch (method) {
case "GET":
response = request.get();
return response.readEntity(returnType);
case "DELETE":
response = request.delete();
return response.readEntity(returnType);
case "PUT":
entity = Entity.entity(requestData, requestDataType);
response = request.put(entity);
return response.readEntity(returnType);
case "POST":
entity = Entity.entity(requestData, requestDataType);
response = request.post(entity);
return response.readEntity(returnType);
default:
return null;
}
}
}
4、请求SpringBoot微服务
4.1、不同的JSON解析方式
Jersey集成Spring Boot是目前开发微服务应用的事实标准。但spring-boot-starter-jersey默认使用jackson
解析JSON,而Jersey默认使用MOXy解析JSON。当Jersey Client向Spring Boot服务请求资源时,这个差异会导致服务端和客户端对POJO的转换不同,造成反序列化错误,示例如下。
public class Book {
private Long bookId;
private String bookName;
private String publicher;
....
}
public class Books {
private List<Book> bookList;
...
}
//使用moxy解析
{
"bookList": {
"book": [
{
"bookId": 1,
"bookName": "xxx"
},
{
"bookId": 2,
"bookName": "yyy"
},
{
"bookId": 3,
"bookName": "zzz"
}
]
}
}
//采用jackson解析,默认输出全部filed,无论是否为null
{
"bookList": [
{
"bookId": 1,
"bookName": "xxx",
"publisher": null
},
{
"bookId": 2,
"bookName": "yyy",
"publisher": null
},
{
"bookId": 3,
"bookName": "zzz",
"publisher": null
}
]
}
因此,使用Jersey Client访问SpringBoot服务时,应顺应其解析方式,采用jackson解析JSON。
<dependency>
<groupId>org.glassfish.jersey.core</groupId>
<artifactId>jersey-client</artifactId>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.media</groupId>
<artifactId>jersey-media-json-jackson</artifactId>
</dependency>
然后在Client的Config实例中注册jackson特性:
clientConfig.register(JacksonFeature.class);
4.2、完整示例
服务器端使用@Produces注解提供JSON格式的媒体格式,不做其他改动。
@GET
@Path("books")
@Produces(MediaType.APPLICATION_JSON)
public List<Book> retrieveBooks(@QueryParam("book") final String value) {
return bookService.retrieve(value);
}
客户端使用JacksonFeature修改JSON解析方式为jackson。
ClientConfig clientConfig = new ClientConfig();
clientConfig.register(JacksonFeature.class);
Client client = ClientBuilder.newClient(clientConfig);
WebTarget webTarget = client.target("http://127.0.0.1:8080/ws")
.path("books")
.queryParam("book", "Java");
GenericType<List<Book>> genericType = new GenericType<List<Book>>(){};
List<Book> books = webTarget.request().get(genericType);
for (Book book : books) {
System.out.println(book);
}