內容:
- 實現目標的行動計劃
- 探索 Skyscanner API
- 創建基於Spring Boot的應用程式框架
- 我們正在為專案中的 Skyscanner Flight Search API 編寫一個客戶端請求
- 為本地化請求建立用戶端服務
- REST 以及 REST 端點的建構方式;
- 關係資料庫;
- maven的工作(特別是什麼是依賴);
- JSON 物件;
- 日誌記錄原則。
- 您可以選擇特定日期的航班並追蹤其價格。使用者透過電子郵件地址進行識別。一旦訂閱價格變更,用戶就會收到電子郵件通知。
- 每 30 分鐘(此間隔透過 application.properties 配置),將為所有訂閱重新計算一次航班的最低價格。如果其中一個值變低,用戶將收到電子郵件通知。
- 所有航班日期已過期的訂閱都將被刪除。
- 透過 REST API,您可以:
- 建立訂閱;
- 編輯;
- 透過電子郵件接收所有訂閱;
- 刪除訂閱。
實現目標的行動計劃
您需要從以下事實開始:需要從某個地方獲取有關航班的資訊。通常,網站提供開放的 REST API,透過該 API 可以檢索資訊。
API(應用程式介面)是您可以與應用程式互動的介面。由此我們可以建造一座通往 REST API 的橋樑。 REST API 是 REST 請求的接口,可用於與 Web 應用程式進行通訊。 |
探索 Skyscanner API
讓我們點擊rakuten api的連結。首先您需要註冊。 所有這些都需要接收唯一金鑰才能使用其網站並向發佈在其上的公共 API 發出請求。我們需要的Skyscanner Flight Search就是這些 API 之一。現在讓我們弄清楚它是如何工作的。讓我們找到 GET List Places 請求。如圖所示,您需要填寫資料並啟動Test Endpoint,結果我們會收到右側 JSON 物件形式的回應: 並且將像這樣建立請求:
https://skyscanner-skyscanner-flight-search-v1.p.rapidapi.com/apiservices/autosuggest/v1.0/{country}/{currency}/{locale}/?query={query}
將所有參數代入此公式,得:
https://skyscanner-skyscanner-flight-search-v1.p.rapidapi.com/apiservices/autosuggest/v1.0/UK/GBP/en-GB/?query=Stockholm
並且兩個標頭將傳遞給這些請求:
.header("x-rapidapi-host", "skyscanner-skyscanner-flight-search-v1.p.rapidapi.com")
.header("x-rapidapi-key", "sing-up-for-key"),
sign-up-for-key
註冊後頒發的 地方。為了追蹤價格下跌,我們需要瀏覽報價端點。自己找吧:)
創建基於Spring Boot的應用程式框架
要使用 Spring Boot 快速輕鬆地建立項目,您可以使用Spring Initializr。選擇以下選項:- Maven專案
- 爪哇
- 2.1.10
- 群組 - 無論您認為有必要,例如 ru.javarush
- 工件 - 完全相同,例如航班監控
- 在搜尋依賴項時,我們會尋找以下內容:
- 春季網
- Java郵件發送器
- Spring數據Jpa
- H2資料庫
- 控制器 - 登入應用程式。這裡將描述 REST API
- SERVICE是業務邏輯層。這裡將描述應用程式的整個邏輯。
- REPOSITORY - 用於處理資料庫的層。
我們正在為專案中的 Skyscanner Flight Search API 編寫一個客戶端請求
Skyscanner 提供了一篇有關如何使用其 API 的文章(我們不會建立帶有活動請求的會話)。「寫客戶」是什麼意思?我們需要使用某些參數來建立對特定 URL 的請求,並為傳回給我們的資料準備一個 DTO(資料傳輸物件)。網站上有四組請求:- 即時航班搜尋 - 目前我們認為沒有必要。
- 地方——讓我們寫一下。
- 瀏覽航班價格 - 我們將使用一個請求,您可以在其中獲取所有資訊。
- 本地化 - 讓我們添加它以便我們知道支援哪些數據。
為本地化請求建立客戶端服務:
該計劃就像蒸蘿蔔一樣簡單:創建請求,查看參數,查看回應。有兩個查詢:清單標記和貨幣。讓我們從貨幣開始。從圖中可以看出,這是一個沒有附加欄位的請求:需要取得支援的貨幣資訊: 回應是JSON物件的形式,其中包含相同物件的集合,例如:{
"Code":"LYD"
"Symbol":"د.ل."
"ThousandsSeparator":","
"DecimalSeparator":"."
"SymbolOnLeft":true
"SpaceBetweenAmountAndSymbol":false
"RoundingCoefficient":0
"DecimalDigits":3
}
讓我們為此物件建立一個CurrencyDto:
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
/**
* Data transfer object for Currency.
*/
@Data
public class CurrencyDto {
@JsonProperty("Code")
private String code;
@JsonProperty("Symbol")
private String symbol;
@JsonProperty("ThousandsSeparator")
private String thousandsSeparator;
@JsonProperty("DecimalSeparator")
private String decimalSeparator;
@JsonProperty("SymbolOnLeft")
private boolean symbolOnLeft;
@JsonProperty("SpaceBetweenAmountAndSymbol")
private boolean spaceBetweenAmountAndSymbol;
@JsonProperty("RoundingCoefficient")
private int roundingCoefficient;
@JsonProperty("DecimalDigits")
private int decimalDigits;
}
在哪裡:
- @Data 是來自Lombok 專案的註釋,可產生所有 getter、setter,並覆寫 toString()、equals() 和 hashCode() 方法。什麼提高了程式碼可讀性並加快了編寫POJO物件的時間;
- @JsonProperty("Code") 是來自 Jackson Project 的註釋,它告訴將哪個欄位指派給該變數。也就是說,JSON 中等於 Code 的欄位將會被指派給code變數。
<dependency>
<groupId>com.mashape.unirest</groupId>
<artifactId>unirest-java</artifactId>
<version>1.4.9</version>
</dependency>
接下來,我們將撰寫一個執行 REST 請求的服務。當然,對於每個客戶端/服務,我們將建立一個介面及其實作:
import com.mashape.unirest.http.HttpResponse;
import com.mashape.unirest.http.JsonNode;
/**
* Service, which is manipulating with Rest calls.
*/
public interface UniRestService {
/**
* Create GET request based on provided {@param path} with needed headers.
*
* @param path provided path with all the needed data
* @return {@link HttpResponse<jsonnode>} response object.
*/
HttpResponse<jsonnode> get(String path);
}
及其實作:
import com.github.romankh3.flightsmonitoring.exception.FlightClientException;
import com.github.romankh3.flightsmonitoring.client.service.UniRestService;
import com.mashape.unirest.http.HttpResponse;
import com.mashape.unirest.http.JsonNode;
import com.mashape.unirest.http.Unirest;
import com.mashape.unirest.http.exceptions.UnirestException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
/**
* {@inheritDoc}
*/
@Slf4j
@Service
public class UniRestServiceImpl implements UniRestService {
public static final String HOST = "https://skyscanner-skyscanner-flight-search-v1.p.rapidapi.com";
public static final String PLACES_FORMAT = "/apiservices/autosuggest/v1.0/%s/%s/%s/?query=%s";
public static final String CURRENCIES_FORMAT = "/apiservices/reference/v1.0/currencies";
public static final String COUNTRIES_FORMAT = "/apiservices/reference/v1.0/countries/%s";
public static final String PLACES_KEY = "Places";
public static final String CURRENCIES_KEY = "Currencies";
public static final String COUNTRIES_KEY = "Countries";
@Value("${x.rapid.api.key}")
private String xRapidApiKey;
/**
* {@inheritDoc}
*/
@Override
public HttpResponse<jsonnode> get(String path) {
HttpResponse<jsonnode> response = null;
try {
response = Unirest.get(HOST + path)
.header("x-rapidapi-host", "skyscanner-skyscanner-flight-search-v1.p.rapidapi.com")
.header("x-rapidapi-key", xRapidApiKey)
.asJson();
} catch (UnirestException e) {
throw new FlightClientException(String.format("Request failed, path=%s", HOST + path), e);
}
log.info("Response from Get request, on path={}, statusCode={}, response={}", path, response.getStatus(), response.getBody().toString());
return response;
}
}
它的本質是我們感興趣的所有請求都是為 GET 請求創建的,並且該服務接受現成的請求並添加必要的標頭,例如:
.header("x-rapidapi-host", "skyscanner-skyscanner-flight-search-v1.p.rapidapi.com")
.header("x-rapidapi-key", xRapidApiKey)
若要從屬性中取得數據,請使用 @Value 註釋,如下所示:
@Value("${x.rapid.api.key}")
private String xRapidApiKey;
它說在application.properties中會有一個名為x.rapid.api.key的屬性,需要將其註入到該變數中。我們擺脫了硬編碼值,並從程式碼中導出該變數的定義。此外,當我在 GitHub 上發布此應用程式時,我沒有添加此屬性的值。這樣做是出於安全原因。我們已經寫了一個可以處理 REST 請求的服務,現在是時候提供在地化服務了。我們正在建立一個基於 OOP 的應用程序,因此我們建立LocalizationClient介面及其實作LocalizationClientImpl:
import com.github.romankh3.flightsmonitoring.client.dto.CountryDto;
import com.github.romankh3.flightsmonitoring.client.dto.CurrencyDto;
import java.io.IOException;
import java.util.List;
/**
* Client for SkyScanner localisation.
*/
public interface LocalisationClient {
/**
* Retrieve the market countries that SkyScanner flight search API support. Most suppliers (airlines,
* travel agents and car hire dealers) set their fares based on the market (or country of purchase).
* It is therefore necessary to specify the market country in every query.
*
* @param locale locale of the response.
*
* @return the collection of the {@link CountryDto} objects.
*
* @throws IOException
*/
List<CountryDto> retrieveCountries(String locale);
/**
* Retrieve the currencies that we ScyScanner flight search API.
*
* @return the collection of the {@link CurrencyDto} objects.
*
* @throws IOException
*/
List<CurrencyDto> retrieveCurrencies();
}
和 LocalizationClientImpl 的實現
import static com.github.romankh3.flightsmonitoring.client.service.impl.UniRestServiceImpl.COUNTRIES_FORMAT;
import static com.github.romankh3.flightsmonitoring.client.service.impl.UniRestServiceImpl.COUNTRIES_KEY;
import static com.github.romankh3.flightsmonitoring.client.service.impl.UniRestServiceImpl.CURRENCIES_FORMAT;
import static com.github.romankh3.flightsmonitoring.client.service.impl.UniRestServiceImpl.CURRENCIES_KEY;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.romankh3.flightsmonitoring.client.dto.CountryDto;
import com.github.romankh3.flightsmonitoring.client.dto.CurrencyDto;
import com.github.romankh3.flightsmonitoring.client.service.LocalisationClient;
import com.github.romankh3.flightsmonitoring.client.service.UniRestService;
import com.mashape.unirest.http.HttpResponse;
import com.mashape.unirest.http.JsonNode;
import com.mashape.unirest.http.exceptions.UnirestException;
import java.io.IOException;
import java.util.List;
import org.apache.http.HttpStatus;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* {@inheritDoc}
*/
@Component
public class LocalisationClientImpl implements LocalisationClient {
@Autowired
private UniRestService uniRestService;
@Autowired
private ObjectMapper objectMapper;
/**
* {@inheritDoc}
*/
@Override
public List<CountryDto> retrieveCountries(String locale) throws IOException {
HttpResponse<JsonNode> response = uniRestService.get(String.format(COUNTRIES_FORMAT, locale));
if (response.getStatus() != HttpStatus.SC_OK) {
return null;
}
String jsonList = response.getBody().getObject().get(COUNTRIES_KEY).toString();
return objectMapper.readValue(jsonList, new TypeReference<List<CountryDto>>() {
});
}
/**
* {@inheritDoc}
*/
@Override
public List<CurrencyDto> retrieveCurrencies() throws IOException, UnirestException {
HttpResponse<JsonNode> response = uniRestService.get(CURRENCIES_FORMAT);
if (response.getStatus() != HttpStatus.SC_OK) {
return null;
}
String jsonList = response.getBody().getObject().get(CURRENCIES_KEY).toString();
return objectMapper.readValue(jsonList, new TypeReference<List<CurrencyDto>>() {
});
}
}
在哪裡
- @Autowired是一個註解,表示需要在這個類別中註入一個物件並使用它,而不需要建立它,即不需要new Object操作;
- @Component 是一個註解,表示必須將此物件加入應用程式上下文中,以便稍後可以使用 @Autowired 註解進行注入;
- ObjectMapper objectMapper 是 Jackson Project 中的一個對象,它將所有這些轉換為 Java 物件。
- 貨幣DTO和國家DTO:
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
/**
* Data transfer object for Currency.
*/
@Data
public class CurrencyDto {
@JsonProperty("Code")
private String code;
@JsonProperty("Symbol")
private String symbol;
@JsonProperty("ThousandsSeparator")
private String thousandsSeparator;
@JsonProperty("DecimalSeparator")
private String decimalSeparator;
@JsonProperty("SymbolOnLeft")
private boolean symbolOnLeft;
@JsonProperty("SpaceBetweenAmountAndSymbol")
private boolean spaceBetweenAmountAndSymbol;
@JsonProperty("RoundingCoefficient")
private int roundingCoefficient;
@JsonProperty("DecimalDigits")
private int decimalDigits;
}
и
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
/**
* Data transfer object for Country.
*/
@Data
public class CountryDto {
@JsonProperty("Code")
private String code;
@JsonProperty("Name")
private String name;
}
為了將 ObjectMapper 注入專案的任何部分,我新增了透過配置類別建立並將其新增到 ApplicationContext 中。
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* {@link Configuration} class.
*/
@Configuration
public class Config {
@Bean
public ObjectMapper objectMapper() {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(new JavaTimeModule());
return objectMapper;
}
}
@Configuration註解告訴Spring這個類別中會有一些配置。為此我添加了 ObjectMapper。同樣,我們加入 PlacesClient 和 PlacesClientImpl:
import com.github.romankh3.flightsmonitoring.client.dto.PlaceDto;
import com.github.romankh3.flightsmonitoring.client.dto.PlacesDto;
import com.mashape.unirest.http.exceptions.UnirestException;
import java.io.IOException;
import java.util.List;
/**
* SkyScanner client.
*/
public interface PlacesClient {
/**
* Get a list of places that match a query string based on arguments.
*
* @param query the code of the city.
* @param country the code of the country.
* @param currency the code of the currency.
* @param locale the code of the locale.
* @return the collection of the {@link PlaceDto} objects.
*/
List<PlacesDto> retrieveListPlaces(String query, String country, String currency, String locale)
throws IOException, UnirestException;
}
和
import static com.github.romankh3.flightsmonitoring.client.service.impl.UniRestServiceImpl.PLACES_FORMAT;
import static com.github.romankh3.flightsmonitoring.client.service.impl.UniRestServiceImpl.PLACES_KEY;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.romankh3.flightsmonitoring.client.dto.PlacesDto;
import com.github.romankh3.flightsmonitoring.client.service.PlacesClient;
import com.github.romankh3.flightsmonitoring.client.service.UniRestService;
import com.mashape.unirest.http.HttpResponse;
import com.mashape.unirest.http.JsonNode;
import com.mashape.unirest.http.exceptions.UnirestException;
import java.io.IOException;
import java.util.List;
import org.apache.http.HttpStatus;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* {@inheritDoc}
*/
@Service
public class PlacesClientImpl implements PlacesClient {
@Autowired
private UniRestService uniRestService;
@Autowired
private ObjectMapper objectMapper;
/**
* {@inheritDoc}
*/
@Override
public List<PlacesDto> retrieveListPlaces(String query, String country, String currency, String locale)
throws IOException, UnirestException {
HttpResponse<JsonNode> response = uniRestService
.get(String.format(PLACES_FORMAT, country, currency, locale, query));
if (response.getStatus() != HttpStatus.SC_OK) {
return null;
}
String jsonList = response.getBody().getObject().get(PLACES_KEY).toString();
return objectMapper.readValue(jsonList, new TypeReference<List<PlacesDto>>() {
});
}
}
其中 PlacesDto 的形式為:
import com.fasterxml.jackson.annotation.JsonProperty;
import com.github.romankh3.flightsmonitoring.client.service.PlacesClient;
import lombok.Data;
/**
* Using for {@link PlacesClient}.
*/
@Data
public class PlacesDto {
@JsonProperty("PlaceId")
private String placeId;
@JsonProperty("PlaceName")
private String placeName;
@JsonProperty("CountryId")
private String countryId;
@JsonProperty("RegionId")
private String regionId;
@JsonProperty("CityId")
private String cityId;
@JsonProperty("CountryName")
private String countryName;
}
最後,用戶端服務將根據必要的數據返回航班的最低價格和所有必要的資訊:FlightPriceClient 和 FlightPriceClientImpl。我們將只實作一個請求,browseQuotes。航班價格客戶:
import com.github.romankh3.flightsmonitoring.client.dto.FlightPricesDto;
/**
* Browse flight prices.
*/
public interface FlightPricesClient {
/**
* Browse quotes for current flight based on provided arguments. One-way ticket.
*
* @param country the country from
* @param currency the currency to get price
* @param locale locale for the response
* @param originPlace origin place
* @param destinationPlace destination place
* @param outboundPartialDate outbound date
* @return {@link FlightPricesDto} object.
*/
FlightPricesDto browseQuotes(String country, String currency, String locale, String originPlace,
String destinationPlace, String outboundPartialDate);
/**
* Browse quotes for current flight based on provided arguments. Round trip ticket.
*
* @param country the country from
* @param currency the currency to get price
* @param locale locale for the response
* @param originPlace origin place
* @param destinationPlace destination place
* @param outboundPartialDate outbound date
* @param inboundPartialDate inbound date
* @return {@link FlightPricesDto} object.
*/
FlightPricesDto browseQuotes(String country, String currency, String locale, String originPlace,
String destinationPlace, String outboundPartialDate, String inboundPartialDate);
}
FlightPriceClientImpl
import static com.github.romankh3.flightsmonitoring.client.service.impl.UniRestServiceImpl.CURRENCIES_KEY;
import static com.github.romankh3.flightsmonitoring.client.service.impl.UniRestServiceImpl.PLACES_KEY;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.romankh3.flightsmonitoring.client.dto.CarrierDto;
import com.github.romankh3.flightsmonitoring.client.dto.CurrencyDto;
import com.github.romankh3.flightsmonitoring.client.dto.FlightPricesDto;
import com.github.romankh3.flightsmonitoring.client.dto.PlaceDto;
import com.github.romankh3.flightsmonitoring.client.dto.QuoteDto;
import com.github.romankh3.flightsmonitoring.client.dto.ValidationErrorDto;
import com.github.romankh3.flightsmonitoring.client.service.FlightPricesClient;
import com.github.romankh3.flightsmonitoring.client.service.UniRestService;
import com.github.romankh3.flightsmonitoring.exception.FlightClientException;
import com.mashape.unirest.http.HttpResponse;
import com.mashape.unirest.http.JsonNode;
import java.io.IOException;
import java.util.List;
import org.apache.http.HttpStatus;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* {@inheritDoc}
*/
@Service
public class FlightPricesClientImpl implements FlightPricesClient {
public static final String BROWSE_QUOTES_FORMAT = "/apiservices/browsequotes/v1.0/%s/%s/%s/%s/%s/%s";
public static final String OPTIONAL_BROWSE_QUOTES_FORMAT = BROWSE_QUOTES_FORMAT + "?inboundpartialdate=%s";
public static final String QUOTES_KEY = "Quotes";
public static final String ROUTES_KEY = "Routes";
public static final String DATES_KEY = "Dates";
public static final String CARRIERS_KEY = "Carriers";
public static final String VALIDATIONS_KEY = "ValidationErrors";
@Autowired
private UniRestService uniRestService;
@Autowired
private ObjectMapper objectMapper;
/**
* {@inheritDoc}
*/
@Override
public FlightPricesDto browseQuotes(String country, String currency, String locale, String originPlace,
String destinationPlace, String outboundPartialDate) {
HttpResponse<JsonNode> response = uniRestService.get(String
.format(BROWSE_QUOTES_FORMAT, country, currency, locale, originPlace, destinationPlace,
outboundPartialDate));
return mapToObject(response);
}
public FlightPricesDto browseQuotes(String country, String currency, String locale, String originPlace,
String destinationPlace, String outboundPartialDate, String inboundPartialDate) {
HttpResponse<JsonNode> response = uniRestService.get(String
.format(OPTIONAL_BROWSE_QUOTES_FORMAT, country, currency, locale, originPlace, destinationPlace,
outboundPartialDate, inboundPartialDate));
return mapToObject(response);
}
private FlightPricesDto mapToObject(HttpResponse<JsonNode> response) {
if (response.getStatus() == HttpStatus.SC_OK) {
FlightPricesDto flightPricesDto = new FlightPricesDto();
flightPricesDto.setQuotas(readValue(response.getBody().getObject().get(QUOTES_KEY).toString(),
new TypeReference<List<QuoteDto>>() {
}));
flightPricesDto.setCarriers(readValue(response.getBody().getObject().get(CARRIERS_KEY).toString(),
new TypeReference<List<CarrierDto>>() {
}));
flightPricesDto.setCurrencies(readValue(response.getBody().getObject().get(CURRENCIES_KEY).toString(),
new TypeReference<List<CurrencyDto>>() {
}));
flightPricesDto.setPlaces(readValue(response.getBody().getObject().get(PLACES_KEY).toString(),
new TypeReference<List<PlaceDto>>() {
}));
return flightPricesDto;
}
throw new FlightClientException(String.format("There are validation errors. statusCode = %s", response.getStatus()),
readValue(response.getBody().getObject().get(VALIDATIONS_KEY).toString(),
new TypeReference<List<ValidationErrorDto>>() {
}));
}
private <T> List<T> readValue(String resultAsString, TypeReference<List<T>> valueTypeRef) {
List<T> list;
try {
list = objectMapper.readValue(resultAsString, valueTypeRef);
} catch (IOException e) {
throw new FlightClientException("Object Mapping failure.", e);
}
return list;
}
}
其中 FlightClientException 如下所示:
import com.github.romankh3.flightsmonitoring.client.dto.ValidationErrorDto;
import java.util.List;
/**
* A {@link RuntimeException} that is thrown in case of an flight monitoring failures.
*/
public final class FlightClientException extends RuntimeException {
public FlightClientException(String message) {
super(message);
}
public FlightClientException(String message, Throwable throwable) {
super(message, throwable);
}
public FlightClientException(String message, List<ValidationErrorDto> errors) {
super(message);
this.validationErrorDtos = errors;
}
private List<ValidationErrorDto> validationErrorDtos;
}
結果,根據 PlacesCl 的數據
GO TO FULL VERSION