iT邦幫忙

2023 iThome 鐵人賽

DAY 16
0
Software Development

救救我啊我救我!CRUD 工程師的惡補日記系列 第 16

【Spring Boot】RestTemplate 串接第三方服務實例

  • 分享至 

  • xImage
  •  

昨天的文章介紹了 RestTemplate 的操作方式,並存取一些測試用的 API。而本文將分享 2 個串接外部服務的實例,出處均來自於筆者工作中遇到的需求。分別是提供 IP 所在地資訊的「ipapi」,以及提供匯率的「Currencylayer」。

此篇亦轉載到個人部落格


一、用 ipapi 取得 IP 位置資訊

ipapi」這項服務,能提供 IP 所在地的相關資訊,且無需註冊。使用額度方面,目前每日有 1000 次的免費額度(以前好像比較少,筆者不記得了)。

(一)背景

之前工作時,公司產品提供了 Android 和 iOS 版的手機 App,所以網頁畫面上會出現 Play 商店跟 App Store 的下載頁面連結。然而中國是不能使用 Play 商店的,於是前端起初便呼叫 ipapi 的服務,藉此得知使用者 IP 所在地的資訊。若位於中國,則判定不顯示 Play 商店的連結。

但該服務是有每日的免費使用額度。為避免前端直接呼叫該 API 卻失敗,因此改成讓後端統一幫忙查詢。這項結果也能存入 DB,日後就優先從 DB 查。除非後端也呼叫失敗(可能額度用完了),才由前端直接呼叫 ipapi,甚至能進一步存回後端。

(二)API 規格

聽完了故事背景,讓我們來看看 ipapi 能提供什麼資訊。
https://ithelp.ithome.com.tw/upload/images/20230919/20131107l82cL4Dl2s.jpg

如圖,呼叫 GET https://ipapi.co/IP位置/json,得到的 response 內容有:城市、幣別、經緯度、時區、手機國碼等等。

(三)程式實作

接下來就開始使用 RestTemplate 串接 ipapi。按照慣例,要先決定我們想要拿取哪些欄位,才能設計出用來接收 response 的類別。

public class IpApiResponse {
    // 查詢失敗會得到的欄位
    private boolean error;
    private String reason;
    private boolean reserved; // true 代表私有 IP

    // IP 所在地資訊
    private String city;
    private String currency;
    private Double latitude;
    private Double longitude;

    @JsonProperty("utc_offset")
    private String utcOffset;

    @JsonProperty("country_calling_code")
    private String countryCallingCode;

    // getter, setter ...
}

然後再設計一個元件,將 RestTemplate 封裝起來。

@Component
public class IpApiClient {
    private RestTemplate restTemplate;

    @PostConstruct
    private void init() {
        restTemplate = new RestTemplateBuilder()
                .rootUri("https://ipapi.co")
                .setConnectTimeout(Duration.ofSeconds(20))
                .build();
    }

    public IpApiResponse getIpInfo(String ipAddress) {
        return restTemplate.getForObject(
                "/{ip}/json",
                IpApiResponse.class,
                Map.of("ip", ipAddress)
        );
    }
}

範例程式中的 init 方法,會用 builder 的方式對 RestTemplate 做客製化設定。例如 API 路徑的前綴和逾時時間。

(四)測試

最後實際測試一下剛剛實作好的 IpApiClient

@RunWith(SpringRunner.class)
@SpringBootTest
public class ApplicationTests {

    @Autowired
    private IpApiClient ipApiClient;

    @Test
    public void testIpApiClient_Public() {
        IpApiResponse ipInfo = ipApiClient.getIpInfo("208.67.222.222");

        // 確認查詢時是否有問題
        assertFalse(ipInfo.isError());
        assertNull(ipInfo.getReason());
        assertFalse(ipInfo.isReserved());

        // 確認 IP 所在地資訊
        assertEquals("San Francisco", ipInfo.getCity());
        assertEquals("USD", ipInfo.getCurrency());
        assertEquals(-122.397966, ipInfo.getLongitude(), 0);
        assertEquals(37.774778, ipInfo.getLatitude(), 0);
        assertEquals("-0700", ipInfo.getUtcOffset());
        assertEquals("+1", ipInfo.getCountryCallingCode());
    }
}

以上範例是查詢美國舊金山的一個公有 IP。而以下是試圖查詢私有 IP 的資訊。

@Test
public void testIpApiClient_Private() {
    IpApiResponse ipInfo = ipApiClient.getIpInfo("192.168.8.100");

    assertTrue(ipInfo.isError());
    assertEquals("Reserved IP Address", ipInfo.getReason());
    assertTrue(ipInfo.isReserved());
}

可以看到,私有 IP 是不能查詢的。另外,查詢「127.0.0.1」(localhost)也會得到這樣的結果。

二、用 Currencylayer 取得匯率

Currencylayer」這項服務,能提供 168 個幣別的匯率,包含歷史匯率。

使用此服務前需註冊。對於免費帳號,目前每月有 1000 次的查詢額度,且每日會更新一次匯率。若系統不要求一定要很新的即時匯率,選擇免費方案也不壞。

但免費方案只能查詢美金對其他幣別的匯率,不能查詢像是臺幣對日圓、歐元對澳幣等組合。

(一)背景

試想使用者在觀看產品列表時,可以用不同的幣別呈現價格,甚至是排序。那麼系統中就要有一個匯率表,用來將當初建立產品資料時所輸入的幣別與價格,換算成使用者想看的幣別。

原本筆者前公司是串接一個叫做「Free Forex API」的服務。但撰寫此文時,發現網站上寫說要與另一個叫做「Currencylayer」的服務合作,故本文將會以後者為串接的對象。

(二)API 規格

讓我們來看看 Currencylayer 的 API 使用方式。註冊帳號後,會得到一組 access_key。進到 dashboard 後,可實際在畫面上呼叫 API 做嘗試。
https://ithelp.ithome.com.tw/upload/images/20230919/20131107l6FPYMedbH.jpg

API 的呼叫方式為 GET http://apilayer.net/api/live?format=1&source=基礎幣別&currencies=目標幣別清單,英文逗號分隔&access_key=帳號的存取KEY

發送請求後,得到如下的 JSON 資料。
https://ithelp.ithome.com.tw/upload/images/20230919/20131107bM2raaLZKA.jpg

(三)程式實作

接下來就開始使用 RestTemplate 串接 Currencylayer。一樣要先決定想要拿取的欄位,再設計出用來接收 response 的類別。

public class CurrencyLayerResponse {
    private long timestamp;
    private String source;
    private Map<String, Double> quotes;

    // getter, setter ...
}

然後設計一個元件,將 RestTemplate 封裝起來。

@Component
public class CurrencyLayerClient {
    private static final String ACCESS_KEY = "...";
    private RestTemplate restTemplate;

    @PostConstruct
    private void init() {
        restTemplate = new RestTemplateBuilder()
                .rootUri("http://apilayer.net/api")
                .setConnectTimeout(Duration.ofSeconds(20))
                .build();
    }

    public CurrencyLayerResponse getLiveExchangeRate(String sourceCurrency, Collection<String> targetCurrencies) {
        var uriVariables = Map.of(
                "source", sourceCurrency,
                "currencies", String.join(",", targetCurrencies),
                "access_key", ACCESS_KEY
        ); 

        return restTemplate.getForObject(
                "/live?format=1&source={source}&currencies={currencies}&access_key={access_key}",
                CurrencyLayerResponse.class,
                uriVariables
        );
    }
}

實作方式與上一節大同小異。要注意的是使用 RestTemplat 時,API 路徑上的 url 至少要保留一個階層(此處保留「/live」),否則會拋出例外說「URI is not absolute」。

ACCESS_KEY 的值,可以選擇放在 application.properties 檔案中,再注入進來。

(四)測試

最後實際測試一下剛剛實作好的 CurrencyLayerClient

@RunWith(SpringRunner.class)
@SpringBootTest
public class ApplicationTests {
    // ...

    @Autowired
    private CurrencyLayerClient currencyLayerClient;

    @Test
    public void testCurrencyLayerClient() {
        var sourceCurrency = "USD";
        var targetCurrencies = List.of("TWD", "JPY", "CNY", "EUR");
        var exchangeRateRes = currencyLayerClient.getLiveExchangeRate(sourceCurrency, targetCurrencies);

        for (var target : targetCurrencies) {
            var pair = sourceCurrency + target; // USDTWD, USDJPY ...
            var rate = exchangeRateRes.getQuotes().get(pair);
            assertTrue(rate > 0);
        }
    }
}

以上範例是查詢美金對臺幣、日圓、人民幣與歐元的匯率。其他支援的幣別,有興趣的讀者請參考官方文件

三、以介面來使用 Client 元件

(一)背景

本文介紹了 IP 資訊和匯率的第三方服務。在範例程式中,client 元件與 response 物件也都根據它們的規格來實作。

但若有一天我們需要更換成新服務(例如第二節提到筆者前公司使用的匯率服務即將過時),那麼所有使用舊 client 與 response 類別的地方也都需要改動,畢竟已經無法再適用於新的第三方服務。

考慮到未來更換服務時,會遇到改動太多程式碼的不便,其實我們一開始就可以設計成透過「介面」來使用 client 元件與 response 類別。

還有另一個好處是促進「分工」。可以讓一個人先去物色適合專案的第三方服務,並實作出 client 元件。其他人也能同時依據該介面,著手開發業務邏輯。

(二)定義介面

以第一節的 IP 服務為例,我們希望能使用固定的介面,獲取需要的資訊。因此以下設計一個介面。

public interface IpInfoClientResponse {
    String getCity();
    String getCurrency();
    Double getLatitude();
    Double getLongitude();
    String getUtcOffset();
    String getCallingCode();
    String getErrorReason();
}

接下來也為 client 元件定義介面。此介面方法很簡單,傳入 IP 地址,回傳帶有 IP 資訊的介面物件。

public interface IpInfoClient {
    IpInfoClientResponse getIpInfo(String ipAddress);
}

定義好介面後,便能套用到需要的地方了。請回到第一節實作的測試程式,將注入的元件、client 元件的呼叫,以及資訊的取得,都調整成以介面來溝通。

@RunWith(SpringRunner.class)
@SpringBootTest
public class ApplicationTests {

    @Autowired
    @Qualifier("ipApiClient")
    private IpInfoClient ipInfoClient;

    @Test
    public void testIpApiClient_Public() {
        IpInfoClientResponse ipInfo = ipInfoClient.getIpInfo("208.67.222.222");

        assertNull(ipInfo.getErrorReason());
        
        // ...
    }

    @Test
    public void testIpApiClient_Private() {
        IpInfoClientResponse ipInfo = ipInfoClient.getIpInfo("192.168.8.100");
        assertEquals("Reserved IP Address", ipInfo.getErrorReason());
    }

    // ...
}

(三)實作介面

介面勢必有它的實體類別(concrete class)。定義好介面後,讓我們在 client 元件與 response 類別實作介面。

以下是讓 client 元件類別實作。

@Component
public class IpApiClient implements IpInfoClient {
    // ...

    @Override
    public IpInfoClientResponse getIpInfo(String ipAddress) {
        // ...
    }
}

以下是讓負責攜帶資料的 response 類別實作。

public class IpApiResponse implements IpInfoClientResponse {
    // ...

    @Override
    public String getErrorReason() {
        if (!error) {
            return null;
        } else if (reserved) {
            return "Reserved IP Address";
        } else {
            return reason;
        }
    }

    // getter, setter ...
}

由於已經定義好介面了,因此未來更換服務時,也能比照辦理,只要提供實作類別即可。

本文的完成專案:
https://github.com/ntub46010/SpringBootTutorial/tree/Ch22-2


今日文章到此結束!
最後推廣一下自己的部落格,我是「新手工程師的程式教室」的作者,請多指教/images/emoticon/emoticon41.gif


上一篇
【Spring Boot】使用 RestTemplate 存取外部 API
下一篇
【Spring Boot】使用 Command Line Runner 在啟動後執行動作
系列文
救救我啊我救我!CRUD 工程師的惡補日記50
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言