如此強大的REST Client API為什么都不用?
1. 簡介
本篇文章我們將介紹JAX-RS,使用Client API實現(xiàn)REST API接口的調(diào)用。
JAX-RS(Java API for RESTful Web Services)是一個用于構(gòu)建和管理 RESTful Web 服務(wù)的標準規(guī)范。JAX-RS 提供了一系列強大的功能,使得開發(fā)者可以輕松地創(chuàng)建、消費和管理 RESTful 服務(wù)。
我們可以通過JAX-RS API編寫REST接口(Spring對應(yīng)的Controller接口),也可以使用其提供的Client API實現(xiàn) REST接口的調(diào)用。如下,我們實現(xiàn)REST API的定義:
import javax.ws.rs.*;
import javax.ws.rs.core.MediaType;
@Path("/users")
public class UserResource {
@GET
@Produces(MediaType.APPLICATION_JSON)
public List<User> getUsers() {}
@POST
@Consumes(MediaType.APPLICATION_JSON)
public void createUser(User user) {}
@PUT
@Path("/{id}")
@Consumes(MediaType.APPLICATION_JSON)
public void updateUser(@PathParam("id") int id, User user) {}
@DELETE
@Path("/{id}")
public void deleteUser(@PathParam("id") int id) {}
}
與Spring MVC的Controller接口相比,除了注解不一樣也沒有什么區(qū)別了,但是這里的JAX-RS是 Jakarta EE 的一部分是標準。
本篇文章的重點是通過JAX-RS的Client API實現(xiàn)遠程接口的調(diào)用,所以上面對服務(wù)接口的實現(xiàn)有興趣的可以查看《jaxrs-2_1-final-spec.pdf》規(guī)范文檔。
既然有了這么多的方式調(diào)用,為什么還要使用JAX-RS呢?我們這里就與Spring的RestTemplate,WebClient進行對比。
- 標準化和兼容性JAX-RS:作為 Jakarta EE 的標準規(guī)范,JAX-RS 具有廣泛的兼容性和標準化支持。這意味著無論你使用哪種 JAX-RS 實現(xiàn)(如 Jersey、RESTEasy),都具有一致的行為。RestTemplate:Spring 框架的一部分,主要用于 Spring 生態(tài)系統(tǒng)中的應(yīng)用。雖然功能強大,但其使用限于 Spring 應(yīng)用。WebClient:也是 Spring 框架的一部分,主要用于響應(yīng)式編程。同樣,其使用限于 Spring 應(yīng)用。
- 易用性和靈活性JAX-RS:提供了一套簡潔且靈活的接口,使得開發(fā)者可以輕松構(gòu)建復(fù)雜的 HTTP 請求。支持多種請求方法和響應(yīng)處理方式,且與 JAX-RS 提供者無縫集成。RestTemplate:提供了豐富的模板方法,使得發(fā)送 HTTP 請求變得簡單。WebClient:支持響應(yīng)式編程模型,提供了流式 API,使得異步和非阻塞操作更加自然和高效。但學(xué)習(xí)曲線較陡峭
- 異步支持JAX-RS:支持異步請求,對于耗時的任務(wù),提高應(yīng)用性能和響應(yīng)性。RestTemplate:主要支持同步操作,雖然可以通過 AsyncRestTemplate 實現(xiàn)異步請求,但已經(jīng)不推薦使用,過時了。WebClient:天然支持異步和響應(yīng)式編程,非常適合處理高并發(fā)和延遲敏感的應(yīng)用。
接下來,我們將詳細介紹JAX-RS 使用Client API實現(xiàn)三方接口的調(diào)用。
2. 實戰(zhàn)案例
2.1 準備三方接口
@GetMapping("/{id}")
public User queryUser(@PathVariable Long id) {
return new User(id, "姓名 - " + new Random().nextInt(100000)) ;
}
@GetMapping("/header")
public String header(@RequestHeader("x-token") String token) {
return token ;
}
@GetMapping("/exception")
public User exception(Long id, String name) {
System.out.println(1 / 0) ;
return new User(id, name) ;
}
@GetMapping("")
public List<User> queryUsers() throws Exception {
TimeUnit.SECONDS.sleep(2);
return List.of(
new User(1L, "姓名 - " + new Random().nextInt(100000)),
new User(2L, "姓名 - " + new Random().nextInt(100000)),
new User(3L, "姓名 - " + new Random().nextInt(100000))) ;
}
上面定義了4個接口,接下來,我們將圍繞這4個接口講解JAX-RS Client API的各種使用方法。
2.2 基本使用
Response response = ClientBuilder.newClient()
.target("http://localhost:9100/users/666")
.request()
.get() ;
String ret = response.readEntity(String.class) ;
System.out.println(ret) ;
輸出結(jié)果
{"id":666,"name":"姓名 - 57414"}
是不是非常的簡單?
2.3 注冊Provider(類型轉(zhuǎn)換器)
在上面的示例中,我們以String字符串的形式獲取到數(shù)據(jù),那能不能以對象如User來獲取結(jié)果呢?當然可以,如下示例:
public class JSONToObjectReader implements MessageBodyReader<Object> {
@Override
public boolean isReadable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
return !type.isPrimitive() ;
}
@Override
public Object readFrom(Class<Object> type, Type genericType, Annotation[] annotations, MediaType mediaType,
MultivaluedMap<String, String> httpHeaders, InputStream entityStream)
throws IOException, WebApplicationException {
return new ObjectMapper().readValue(entityStream, type) ;
}
}
這里,我們自定義了 MessageBodyReader 這樣我們就可以實現(xiàn)自己的邏輯如何進行數(shù)據(jù)的轉(zhuǎn)換了,我們這里直接將 Stream 通過Jackson轉(zhuǎn)換為對象。接下來就是將上面的轉(zhuǎn)換器進行注冊
Response response = ClientBuilder.newClient()
// 注冊轉(zhuǎn)換器
.register(JSONToObjectReader.class)
// ...
User ret = response.readEntity(User.class) ;
System.out.println(ret) ;
其實上面的注冊我們還可以全局注冊,如下方式:
ClientBuilder builder = ClientBuilder.newBuilder() ;
builder.register(JSONToObjectReader.class);
Client client = builder.client() ;
// ...
這樣所有三方接口調(diào)用都通過該Client即可。
2.4 注冊Filter組件
如果你想在請求發(fā)送前進行修改請求數(shù)據(jù),或者返回結(jié)果后進行相應(yīng)的處理,那么你可以自定義ClientRequestFilter,ClientResponseFilter。
public class LoggingFilter implements ClientRequestFilter, ClientResponseFilter {
private final Logger logger = LoggerFactory.getLogger(getClass()) ;
@Override
public void filter(ClientRequestContext requestContext, ClientResponseContext responseContext) throws IOException {
logger.info("已經(jīng)得到響應(yīng)結(jié)果, 響應(yīng)header: {}", responseContext.getHeaders()) ;
}
@Override
public void filter(ClientRequestContext requestContext) throws IOException {
logger.info("準備發(fā)送請求, 請求Headers: {}", requestContext.getHeaders()) ;
// 我們可以在這里進行修改請求的數(shù)據(jù)(你還可以修改請求的uri等)
requestContext.getHeaders().replace("x-token", List.of("88888888888")) ;
}
}
接下來,就是注冊該過濾器,與上面注冊轉(zhuǎn)換器是一樣的。
Response response = client
.target("http://localhost:9100/users/header")
.register(LoggingFilter.class)
// ...
控制臺輸出
圖片
2.5 定義組件順序
如上的示例,如果我們還有一個AuthFilter,希望AuthFilter先執(zhí)行,那么我們就可以通過 @Provider 注解控制他們的執(zhí)行順序。
@Priority(-1)
public class AuthFilter implements
ClientRequestFilter, ClientResponseFilter {}
@Priority(0)
public class LoggingFilter implements
ClientRequestFilter, ClientResponseFilter {}
運行結(jié)果
圖片
2.6 異步調(diào)用
要使用異步調(diào)用,我們可以使用默認的線程池,也可以自定義線程池,如下自定義線程池。
int core = Runtime.getRuntime().availableProcessors() ;
ExecutorService executorService = new ThreadPoolExecutor(core, core, 60, TimeUnit.SECONDS, new ArrayBlockingQueue<>(100)) ;
// 配置異步線程池
ClientBuilder builder = ClientBuilder.newBuilder().executorService(executorService) ;
Client client = builder.build().register(JSONToObjectReader.class) ;
Future<User> future = client
.target("http://localhost:9100/users/666")
.request(MediaType.APPLICATION_JSON)
// 設(shè)置異步調(diào)用
.async()
.get(new InvocationCallback<User>() {
@Override
public void completed(User response) {
System.out.printf("%s - 請求完成: %s%n", Thread.currentThread().getName(), response) ;
}
@Override
public void failed(Throwable throwable) {
System.err.printf("請求失敗: %s%n", throwable.getMessage()) ;
}
}) ;
User ret = future.get() ;
System.out.printf("返回結(jié)果: %s%n", ret) ;
運行結(jié)果
pool-1-thread-1 - 請求完成: User [id=666, name=姓名 - 34158]
返回結(jié)果: User [id=666, name=姓名 - 34158]
大多數(shù)情況你可以直接在回調(diào)的completed方法中執(zhí)行其它邏輯。
2.7 反應(yīng)式支持
在上面的異步請求中,我們通過回調(diào)的機制來獲取結(jié)果?;卣{(diào)適用于簡單的場景,但在多個事件同時存在時,源代碼會變得更難理解。例如,當異步調(diào)用需要組合、合并或以任何方式操作時,這些場景可能導(dǎo)致回調(diào)嵌套在其他回調(diào)中,從而使代碼的可讀性大大降低——這種情況通常被稱為“回調(diào)地獄”,因為調(diào)用的嵌套性質(zhì)。
為了提高可讀性并使程序員能夠更好地理解異步計算,Java 8 引入了一個新的接口 CompletionStage,該接口包含大量專門用于管理異步計算的方法。
JAX-RS 2.1 定義了一種新的調(diào)用者類型 RxInvoker,以及基于 Java 8 的 CompletionStage 類型的默認實現(xiàn) CompletionStageRxInvoker。
CompletionStage<User> stage = ClientBuilder.newClient()
.target("http://localhost:9100/users/666")
.register(JSONToObjectReader.class)
.request(MediaType.APPLICATION_JSON)
.rx()
.get(User.class) ;
stage.whenComplete((res, ex) -> {
if (ex != null) {
System.err.printf("發(fā)生錯誤: %s%n", ex.getMessage()) ;
} else {
System.out.println(res) ;
}
}) ;
這里我們可以利用CompletionState完成更多復(fù)雜的操作。有關(guān)CompletionState更多的用法,查看下面這篇文章:
強大的異步任務(wù)處理類CompletableFuture使用詳解
2.8 更多的配置
如果你需要進行更多的精細配置,如:超時時間,我們可以通過如下的方式進行配置:
ClientBuilder builder = ClientBuilder.newBuilder() ;
builder.connectTimeout(1000, TimeUnit.MILLISECONDS) ;
builder.readTimeout(1000, TimeUnit.MILLISECONDS) ;
builder.register(JSONToObjectReader.class) ;
Client client = builder.build() ;
client.target("http://localhost:9100/users")
.register(JSONToObjectReader.class)
.request(MediaType.APPLICATION_JSON)
.buildGet()
.submit(new InvocationCallback<List<User>>() {
@Override
public void completed(List<User> response) {
System.out.printf("返回結(jié)果: %s%n", response) ;
}
@Override
public void failed(Throwable throwable) {
System.err.printf("發(fā)生錯誤: %s%n", throwable.getMessage()) ;
}
}) ;
運行結(jié)果
發(fā)生錯誤: java.net.SocketTimeoutException: Read timed out
異常處理也是非常的便捷。