因為現今網路服務最常見的就是用 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