WebService——JAX-RS2(Jersey)客户端详解

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);
}
  • 20
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值