外部 (Web) APIへのリクエストは基本的にはHTTPリクエストとなるため、 HTTPクライアントを使用する。
HTTPクライアントという言葉がわかりづらいが、要はHTTPリクエストを送るときの定番の設定や処理を一手にやってくれるインターフェイスのこと。
Java (Spring)のメジャーなHTTPクライアントには以下の2つがある。大抵は前者のRestTemplateを使用してリクエストを送信する。
- RestTemplate (同期)
- WebClient (非同期)
参考までに、Javaの純粋なライブラリで使用できるHTTPクライアントには以下2つのインターフェイスがある。
- HttpURLConnection
- HttpClient
外部リクエスト送信の基本
@Service
public class SampleService {
private RestTemplate restTemplate;
SampleService() {
this.restTemplate = restTemplate;
}
public SampleResponse sampleMethod() {
URI apiUli = URI.create("http://sample.com/method");
SapmleRequest request = new SampleRequest();
request.setParam = "sampla";
HttpEntity requestEntity = new HttpEntity<>(request, [header]);
ResponseEntity responseEntity = this.restTemplate.exchange(apiUri, HttpMethod.POST, requestEntity, SampleResponse.class); …※
return response;
}
}
以下の部分でリクエストを送信している。
ResponseEntity responseEntity = this.restTemplate.exchange(apiUri, HttpMethod.POST, requestEntity, SampleResponse.class);
restTemplate.exchange()の引数は以下。
- 第一引数:URI
- 第二引数:GETかPOSTか
- 第三引数:HTTPリクエスト
- 第四引数:レスポンスの型 ※戻り値無しの場合は、Void.classとする
リクエストボディではなく、クエリパラメータを使用する場合
public SampleResponse sampleMethod() {
MultiValueMap<String, String> queryParam = new LinkedMultiValueMap<>();
queryParam.add("samplKey", "sampleVal");
URI apiUri = UriComponentsBuilder.fromUriString("http://sample.com/method").queryParam(queryParam).build.toUri();
ResponseEntity responseEntity = this.restTemplate.exchange(apiUri, HttpMethod.GET, null, SampleResponse.class);
return response;
}
204(NoContent)を受け取る場合
Bodyの型部分に、空であることを示すVoidを使用する。
ResponseEntity<Void> responseEntity = this.restTemplate.exchange(apiUrl, HttpMethod.POST, requestEntity, Void.class);
Controllerでのリクエストの受け取り
@GetMapping("init")
public SapmleInitResponse init(@Validated SapmleInitParam param) {
return this.service.init(param);
}
@PostMapping("update")
public SampleUpdateResponse update(@RequestBody @Valid SapmleUpdateRequest request, Errors errors) {
return this.service.update(request);
}
ヘッダー
ヘッダーはサーブレットに含まれる情報。
ボディ部と比べ再利用性が高いが、リクエストの詳細を送るためのものではなく、以下のような、通信に関する概要が記載される。
- ユーザーエージェント:リクエスト送信元端末情報
- リクエスト形式:基本はJSON
- IP情報:送信元IP
- 認証情報:AuthorizationやCookie情報
- etc.
ヘッダーのライフサイクル
本来リクエストレスポンス単位で生成すべきだが、ヘッダー自体はインスタンス単位で作成されるため、シングルトンで作ることの多いJavaアプリケーションでは、何も設定しなければ、同じヘッダーが使われ続ける。
アーキのAOPなどで、リクエスト送信時の挙動としてインターセプトし、値のチェックや設定を付与することになる。
リクエスト形式 Content-Type
Javaのリクエスト形式について普段意識することは無いが、基本的にはJSON形式で送信されている。
POSTMANなどでリクエストを送ると、ヘッダーにContent-Typeという項目があり、ここでJSONを指定していることがわかる。
このContent-Typeや、文字コードは、HttpMessageConverterという機能でコントロールされており、Spring Javaでは標準で搭載されている。
特殊な仕様でリクエストを送りたい場合は、この辺りをカスタマイズすることになる。
参考:Springが用意するWEBクライアント(RestTemplate)の使い方
URLENCODED
リクエストパラメータに、URLなどの特殊記号を含む文字列を使用している場合、『&』や『=』などが不正検知され、JSON形式でリクエストが送信できない場合がある。
その場合は、MediaTypeを、デフォルトのJSONからURLENCODEDに変更し、MultivalueMapでリクエストを作成する。
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.setContentType(MediaType.APPLICATION=FORM=URLENCODED); // メディアタイプを変更
MultiValueMap<String, String> map = new LinkedMultiValueMap<>(); // Mapでリクエスト作成
map.add("paramName", "https://sample.com%param=sample¶m2=sample2");
HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(map, httpHeaders);
XFF X-Forwarded-For
HTTPヘッダに挿入する、送信元IPアドレス (アプリケーション層)を特定するための
情報。
多段プロキシを使用している場合、複数のIPが記載されることがあるが、その場合一番
左がクライアントのIPとなる。
他にもiv-remote-address(ネットワーク層のIP)や、x-real-ip (クライアントIP)があ
るが、これらは、Webサーバーで設定しないと取得されない。
Appendix
RestTemplateのヘッダーをカスタマイズする
RestTemplateにヘッダーを設定する場合、通常はリクエスト送信時に設定する。
しかし、アーキ部品やフレームワークをカスタマイズしたい場合、送信処理時ではなくRestTemplateインスタンスのヘッダーを改変する実装が求められることがある。
その場合はintercepterを使用する。
Rest Template restTemplate = restTemplateBuilder.additionalInterceptors((request, body, execution) -> { // restTemplateのフィールドをインターセプト
request.getHeaders().set("Authorization", "Basic xxxxx"); // 既存のヘッダーをgetして新たにパラメータを追加
return execution.execute(request, body);
}).build();
参考:RestTemplateBuilder/RestTemplateでheaderを追加
任意の型を許容するメソッドを作成する
例えば、RestTemplateを使用してリクエストを送信する場合、レスポンスの型を指定する必要がある。
ResponseEntity<DtoName> responseEntity this.restTemplate.exchange (apiUrl, HttpMethod.POST, requestEntity, DtoName.class);
このとき、レスポンスヘッダーは使用するが、レスポンスボディは必要としない場合、わざわざBodyのDTO(上記の<DtoName>部分)を作成するのは面倒である。
そんな時は、任意の型を表現するジェネリクスを使用する。
public <T> void myMethod (ResponseEntity<T> responseEntity) {
ResponseEntity<T> responseEntity this.restTemplate.exchange (apiUrl, HttpMethod.POST, requestEntity,
new Parameterized TypeReference<T>() {});
}
サービスのスタブ
外部のAPIと通信する場合、開発環境からは接続しない作りにするケースは多い。
この時、テスト用のプロファイルの場合には外部APIとは通信しない、という分岐処理が必要となるが、そのためにif文で分岐するのは、本来の処理ロジックが何なのかわかりづらくなり保守性に乏しい。
このようなケースでは、@Profileによって呼び出すクラスを分岐する。(Profileの設定はサーバー側となるため割愛)
実装
Serviceインターフェースクラスを継承し、ServiceImpl.java (実装クラス)と、ServiceStub.java(スタブクラス)で実装する。
スタブは開発環境で使用するプロファイルで呼び出すクラスである。
■Sapmle.java ※インターフェイス
public interface SampleService {
void sampleFnc();
}
実装
■SampleImpl.java ※実装クラス
@Service
@Profile({"prod", "it"})
public class SampleServiceImpl implements SampleService {
@Override
public void sampleFnc() {
// 外部通信あり
}
}
■SampleStub.java ※スタブクラス
@Service
@Profile({"!prod & !it"})
public class SampleServiceStub implements SampleService {
@Override
public void sampleFnc() {
// 外部通信無し
}
}
■SampleController.java
コントローラーでは、サービスのインターフェイスを呼び出すことで、実装とスタブ、どちらが呼び出されるかはプロファイルによって動的に判断される。
@Controller
public class SampleController {
private SampleService service;
public SampleController() {
this.service = service;
}
@GetMapping("init")
public void init() {
this.service.sampleFnc(); // インターフェイスを呼び出す
}
}
どうしても1つのクラス内でプロファイルを判別して分岐したい場合は、org.springframework.core.env.Environmentの、getActiveProfiles () メソッドを使用する。
参考:SpringBootで現在実行中のプロファイル(Profile)を取得する
リダイレクト
通常レスポンスは、リクエスト送信元のクライアントに返却されるが、場合によってはの送信元ではない別のクライアントに向けてレスポンスを返却したい場合がある。
その時、リクエストを受け取ったシステムは、リダイレクト先のURLを載せて、ステータスコード301や302で送信元クライアントにレスポンスを返却する。
このステータスコードを送信元のクライアントが受け取ると、ブラウザが判断してリダイレクトURLに向けてレスポンスを送信し直す。
GETとPOSTの使い分け
GETはクエリパラメータのみでリクエストボディを送信しない。(物理的に送信はできるが、受信側でリクエストボディを受け取れないツールも多く非推奨)
クエリパラメータで指定するため、個人情報等はGETでは送信しない。
一方のPOSTは、更新処理が主たる用途。しかし、上記の通り、リクエストボディを必要とする場合は、その用途を超えて使用される。
更新処理を行うため、個人情報を含むあらゆるデータを送信する。
そのため、POSTメソッドに限定して、AOPやSpringSecurityなどで、特定の制限や個人情報チェックなどのフィルタを実装しているケースも多い。
GETは、これらのフィルタを通過しなくてよいシンプルな通信に向いている。
インシデント
リクエスト送信時エラー
■エラーメッセージ
java.lang.IllegalStateException: An Errors/BindingResult argument is expected to be declared immediately after the model attribute, the @RequestBody or the @RequestPart arguments to which they apply: public xxx.SampleResponse xxx.sampleMethod(java.lang.String, org.springframework.validation. Errors)
ErrorsやBinding Resultでパリデーション検証結果を確認したい場合、 @RequestBodyか@RequestPartが期待されるけど、 記述されていないよ。というエラー。
クエリパラメータなら、@Validatedを使用して、Modelの各パラメータにに@NotBlankなどのバリデーションを設定する。
リクエストボディなら、@ValidとErrorsを使用する。
このケースは、クエリパラメータなのに@Validを使用していたためのエラー。
Resource not found
リクエストのURLが誤っている。
405 METHOD_NOT_ALLOWED
[o.s.w.c.RestTemplate.debug] Response 405 METHOD_NOT_ALLOWED [http-nio-8080-exec-6]
GETとPOSTが間違っている。
response has already closed.
@Transactionalを設定したサービスAPIで、例外が発生した際に特定のメッセージを設定して、画面側にレスポンスを返却しようとしてエラーとなった。
response has already closed.
java.lang.IllegalStateException: getOutputStream() has already been called for this response
at org.apache.catalina.connector.Response.getWriter(Response.java:584)
…
このAPIでは、ControllerでHttpServletResponseを使用していた。
例外が発生すると、その時点、もしくはTransactionalの機能によって、APIのレスポンスがクローズするが、そのレスポンスをHttpServletResponseで改変しようとしたため、すでにクローズ済みである、とのエラーが発生したものと思われる。
HttpServletResponseを使用しない実装に変更して解消した。
UnknownHostException
DNSの名前解決に失敗している。 URLあっているかログから確認する。
OracleDriver was not found
APIからDB資源を呼び出す際の接続情報(APIのapplication.env.yml)が誤っている。
もしくは、DB資源がビルド(gradlew.bat)されていない。
No HttpMessageConverter
リクエスト BODYに、 URLで使用されるようなエスケープが必要な文字の考慮が必要な場合、 メディアタイプをx-www-form-urlencoded形式に指定してエンコードを有効にする必要がある。
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED)
Modelからフラグがget取得できない
フラグはgetFIgName()ではなくisFlgName()で取得する。
コメント