因為現今網路服務最常見的就是用 http 協定提供,一般資料面向的服務,也是以 JSON 資料格式作為輸入與輸出的資料格式。Retrofit 定義自己是 type-safe HTTP client for Android and Java。這邊 type-safe 的意思是,透過 Retrofit 的包裝,可以自動將 http request 與 response 資料轉換為 java 類別物件,在自動轉換的過程中,就能以類別的定義,去檢查資料是不是符合類別的定義,而不是未經定義的其他資料。因為是遠端網路服務,Retrofit 提供了同步與非同步兩種使用 http 服務的方式,非同步的呼叫方法可以解決網路延遲,甚至是故障的問題。
測試 http 服務
網路上提供 Fake HTTP API service,有以下網站可以用
GET https://reqres.in/api/users/2
{
"data": {
"id": 2,
"email": "janet.weaver@reqres.in",
"first_name": "Janet",
"last_name": "Weaver",
"avatar": "https://reqres.in/img/faces/2-image.jpg"
},
"support": {
"url": "https://reqres.in/#support-heading",
"text": "To keep ReqRes free, contributions towards server costs are appreciated!"
}
}
POST https://reqres.in/api/users
輸入參數
{
"name": "John",
"job": "leader"
}
輸出
{
"name": "morpheus",
"job": "leader",
"id": "63",
"createdAt": "2024-05-22T03:31:37.941Z"
}
Maven pom.xml
在 xml 裡面引用 retrofits 以及 converter-gson 的 library
<!--retrofit-->
<dependency>
<groupId>com.squareup.retrofit2</groupId>
<artifactId>retrofit</artifactId>
<version>2.11.0</version>
</dependency>
<dependency>
<groupId>com.squareup.retrofit2</groupId>
<artifactId>converter-gson</artifactId>
<version>2.11.0</version>
</dependency>
同步/非同步
透過 service 產生的 API Call 有 同步/非同步 兩種呼叫方式
execute()– Synchronously send the request and return its response.enqueue(retrofit2.Callback)– Asynchronously send the request and notify callback of its response or if an error occurred talking to the server, creating the request, or processing the response.
Converter
retrofit2 的 conveter 是用在 http request 與 response 的資料轉換,目前支援這些格式
- Gson:
com.squareup.retrofit2:converter-gson - Jackson:
com.squareup.retrofit2:converter-jackson - Moshi:
com.squareup.retrofit2:converter-moshi - Protobuf:
com.squareup.retrofit2:converter-protobuf - Wire:
com.squareup.retrofit2:converter-wire - Simple XML:
com.squareup.retrofit2:converter-simplexml - JAXB:
com.squareup.retrofit2:converter-jaxb - Scalars (primitives, boxed, and String):
com.squareup.retrofit2:converter-scalars
比較常見的是 json,可以使用 Gson 或 Jackson 協助轉換。
實作
data class
對應剛剛的兩個 service,分別有不同的 data class
User.java
public class User {
private long id;
private String first_name;
private String last_name;
private String email;
// getter, setter, toString
}
UserResponse.java
public class UserResponse {
private User data;
// getter, setter, toString
}
Account.java
public class Account {
private String id;
private String name;
private String job;
private Date createdAt;
// getter, setter, toString
}
service
UserService.java
import retrofit.data.Account;
import retrofit.data.UserResponse;
import retrofit2.Call;
import retrofit2.http.Body;
import retrofit2.http.GET;
import retrofit2.http.POST;
import retrofit2.http.Path;
public interface UserService {
@GET("/api/users/{id}")
public Call<UserResponse> getUser(@Path("id") long id);
@POST("/api/users")
Call<Account> createUser(@Body Account account);
}
main
RetrofitTest.java
import okhttp3.OkHttpClient;
import retrofit.data.Account;
import retrofit.data.UserResponse;
import retrofit.service.UnsafeOkHttpClient;
import retrofit.service.UserService;
import retrofit2.Call;
import retrofit2.Response;
import retrofit2.Retrofit;
import retrofit2.Callback;
import retrofit2.converter.gson.GsonConverterFactory;
public class RetrofitTest {
public static void sync() {
OkHttpClient.Builder httpClient = new OkHttpClient.Builder();
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://reqres.in/")
.addConverterFactory(GsonConverterFactory.create())
.client(httpClient.build())
// .client(UnsafeOkHttpClient.getUnsafeOkHttpClient())
.build();
UserService service = retrofit.create(UserService.class);
// Calling '/api/users/2'
Call<UserResponse> callSync = service.getUser(2);
try {
Response<UserResponse> response = callSync.execute();
UserResponse apiResponse = response.body();
System.out.println("sync: "+apiResponse);
} catch (Exception ex) {
ex.printStackTrace();
}
// Calling 'https://reqres.in/api/users'
Account account = new Account();
account.setName("John");
account.setJob("leader");
Call<Account> callSync2 = service.createUser(account);
try {
Response<Account> response2 = callSync2.execute();
Account apiResponseAccount = response2.body();
System.out.println(apiResponseAccount);
} catch (Exception ex) {
ex.printStackTrace();
}
}
public static void async() {
OkHttpClient.Builder httpClient = new OkHttpClient.Builder();
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://reqres.in/")
.addConverterFactory(GsonConverterFactory.create())
.client(httpClient.build())
// .client(UnsafeOkHttpClient.getUnsafeOkHttpClient())
.build();
UserService service = retrofit.create(UserService.class);
// Calling '/api/users/2'
Call<UserResponse> callAsync = service.getUser(2);
callAsync.enqueue(new Callback<>() {
@Override
public void onResponse(Call<UserResponse> call, Response<UserResponse> response) {
int responseCode = response.code();
UserResponse user = response.body();
System.out.println("async responseCode="+responseCode+", result=" + user);
}
@Override
public void onFailure(Call<UserResponse> call, Throwable throwable) {
System.out.println(throwable);
}
});
// Calling 'https://reqres.in/api/users'
Account account = new Account();
account.setName("John");
account.setJob("leader");
Call<Account> callAsync2 = service.createUser(account);
callAsync2.enqueue(new Callback<>() {
@Override
public void onResponse(Call<Account> call, Response<Account> response) {
int responseCode = response.code();
Account accountResponse = response.body();
System.out.println("async responseCode="+responseCode+", result=" + accountResponse);
}
@Override
public void onFailure(Call<Account> call, Throwable throwable) {
System.out.println(throwable);
}
});
}
public static void main(String[] args) {
sync();
System.exit(0);
// async();
}
}
注意:這邊特定要呼叫 System.exit(0) 來停掉程式,這是因為 Retrofit 內部使用的 OkHttp 採用了 ThreadPoolExecutor,參考這個網址的 issue 討論:Tomcat is not able to stop because of OkHttp ConnectionPool Issue #5542 · square/okhttp · GitHub ,討論寫說沒有直接停掉的方法。
裡面有說到大約 6mins 後就會 Conenction Pool 停掉。實際上實測,大約等了 5mins。
目前如果要調整這個問題,必須要覆蓋 connectionPool 的原始設定。方法是在產生 OkHttpClient.Builder() 的時候,指定一個新的 ConnectionPool,並將參數 keepAliveDurationMills 改短。
int maxIdleConnections = 10;
int keepAliveDurationMills = 1000;
OkHttpClient.Builder httpClient = new OkHttpClient.Builder()
.connectionPool(new ConnectionPool(maxIdleConnections, keepAliveDurationMills, TimeUnit.MILLISECONDS));
這樣修改,只有在同步呼叫時有用。如果是非同步呼叫,程式還是會等 3mins 才會停下來。
自訂 Http Client
有時候在開發時,https 網站會採用自己產生的 SSL 憑證,這時候需要調整 http client,不檢查 domain 來源
UnsafeOkHttpClient.java
import okhttp3.ConnectionPool;
import okhttp3.OkHttpClient;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
import javax.net.ssl.*;
import java.security.cert.CertificateException;
import java.util.concurrent.TimeUnit;
public class UnsafeOkHttpClient {
public static OkHttpClient getUnsafeOkHttpClient() {
try {
// Create a trust manager that does not validate certificate chains
final TrustManager[] trustAllCerts = new TrustManager[]{
new X509TrustManager() {
@Override
public void checkClientTrusted(java.security.cert.X509Certificate[] chain, String authType) throws CertificateException {}
@Override
public void checkServerTrusted(java.security.cert.X509Certificate[] chain, String authType) throws CertificateException {}
@Override
public java.security.cert.X509Certificate[] getAcceptedIssuers() {
return new java.security.cert.X509Certificate[]{};
}
}
};
// Install the all-trusting trust manager
final SSLContext sslContext = SSLContext.getInstance("SSL");
sslContext.init(null, trustAllCerts, new java.security.SecureRandom());
// Create an ssl socket factory with our all-trusting manager
final SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();
int maxIdleConnections = 10;
int keepAliveDurationMills = 1000;
OkHttpClient.Builder builder = new OkHttpClient.Builder()
.connectionPool(new ConnectionPool(maxIdleConnections, keepAliveDurationMills, TimeUnit.MILLISECONDS));
builder.sslSocketFactory(sslSocketFactory, (X509TrustManager)trustAllCerts[0]);
builder.hostnameVerifier(new HostnameVerifier() {
@Override
public boolean verify(String hostname, SSLSession session) {
return true;
}
});
return builder.build();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
在使用時,只需要在產生 Retrofit 時,改用這個 http client builder
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://reqres.in/")
.addConverterFactory(GsonConverterFactory.create())
// .client(httpClient.build())
.client(UnsafeOkHttpClient.getUnsafeOkHttpClient())
.build();
Service Generator
可製作 service generator,將產生 service 的程式碼再包裝起來
UserServiceGenerator.java
import okhttp3.OkHttpClient;
import okhttp3.Request;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
public class UserServiceGenerator {
private static final String BASE_URL = "https://reqres.in/";
private static Retrofit.Builder builder = new Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create());
private static Retrofit retrofit = builder.build();
private static OkHttpClient.Builder httpClient = new OkHttpClient.Builder()
.connectionPool(new ConnectionPool(10, 1000, TimeUnit.MILLISECONDS));;
public static <S> S createService(Class<S> serviceClass) {
return retrofit.create(serviceClass);
}
public static <S> S createService(Class<S> serviceClass, final String token ) {
if ( token != null ) {
httpClient.interceptors().clear();
httpClient.addInterceptor( chain -> {
Request original = chain.request();
Request request = original.newBuilder()
.header("Authorization", token)
.build();
return chain.proceed(request);
});
builder.client(httpClient.build());
retrofit = builder.build();
}
return retrofit.create(serviceClass);
}
}
使用時
UserService service = UserServiceGenerator.createService(UserService.class);
References
Introduction to Retrofit | Baeldung