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