Swagger CodegenでPHPクライアントを生成する
Swagger Codegenが生成するPHPクライアントを使う機会があったので、使い方を記事に残しておきます。
Swagger Codegenが生成するファイル
Swagger CodegenでPHPテンプレートを指定すると、以下のファイル群が生成されます。
- SwaggerClient-php/
Petstoreの例が参考になります。 swagger-codegen/samples/client/petstore/php at master · swagger-api/swagger-codegen · GitHub から参照できます。
APIクライアントの使い方
APIクライアントのインスタンスを生成し、メソッドを実行します。
require_once(__DIR__ . '/SwaggerClient-php/autoload.php');
// APIクライアントの生成
$pet_api = new Swagger\Client\Api\PetApi();
// APIの実行
$the_pet = $pet_api->getPetById(1);
この例ではID=1のPetモデルを取得しています。getPetById
メソッドはAPIクライアントクラス(PetApi.php)で定義されています。
/** * Operation getPetById * * Find pet by ID * * @param int $pet_id ID of pet to return (required) * @throws \Swagger\Client\ApiException on non-2xx response * @return \Swagger\Client\Model\Pet */ public function getPetById($pet_id)
また、getPetById
メソッドが返すモデルはモデルクラス(Pet.php)で定義されています。
/** * Pet Class Doc Comment * * @category Class * @package Swagger\Client * @author Swagger Codegen team * @link https://github.com/swagger-api/swagger-codegen */ class Pet implements ArrayAccess
モデルクラスはArrayAccessを実装しているため、配列としてアクセスできるように設計されています。また、GetterとSetterでもアクセスできるようになっています。
$the_pet->getName(); $the_pet['name']; $the_pet->setName('foo'); $the_pet['name'] = 'foo';
例外処理
APIが200番台以外のステータスコードを返した場合は例外が発生します。ステータスコードやレスポンスは例外オブジェクトから取得できます。
try { $the_pet = $pet_api->getPetById(1); } catch (Swagger\Client\ApiException $e) { echo 'Caught exception: ', $e->getMessage(), "\n"; echo 'HTTP response headers: ', $e->getResponseHeaders(), "\n"; echo 'HTTP response body: ', $e->getResponseBody(), "\n"; echo 'HTTP status code: ', $e->getCode(), "\n"; }
APIクライアントの設定とカスタマイズ
APIクライアントの getConfig
メソッドでヘッダやOAuthなどを設定できるようになっています。
$pet_api->getApiClient()->getConfig();
詳しくは swagger-codegen/Configuration.php at master · swagger-api/swagger-codegen · GitHub が参考になります。
今のところ、APIクライアントにはインターセプタの仕組みは用意されていないようです。共通処理を入れたい場合は ApiClient
クラスを継承した独自クラスを定義し、APIクライアントのコンストラクタに渡すとよいでしょう。
class MyApiClient extends Swagger\Client\ApiClient { public function callApi($resourcePath, $method, $queryParams, $postData, $headerParams, $responseType = null, $endpointPath = null) { // TODO: 事前処理 $response = parent::callApi($resourcePath, $method, $queryParams, $postData, $headerParams, $responseType, $endpointPath); // TODO: 事後処理 return $response; } }
$pet_api = new Swagger\Client\Api\PetApi(new MyApiClient());
依存関係の管理
composer.jsonとautoload.phpが生成されるので、Composerで依存関係を管理することも可能です。ここで力尽きたので詳細は割愛します。
Spring Security OAuth2のリトライを@Retryableで書く
Spring Security OAuth2のアクセストークン取得で接続失敗に対してリトライを行いたい場合、Spring Retryを使うと簡単に実現できます。
やりたいこと
- Spring BootのアプリでOAuth 2.0クライアントを利用する。
- Spring Security OAuth2でアクセストークンを取得する。(
OAuth2RestTemplate
やFeignRequestInterceptor
は内部でSpring Security OAuth2を利用している) - OAuthクライアントの接続失敗でリトライしたい。
実現方法
Spring Security OAuth2の AccessTokenProvider
にはインターセプタを設定するメソッドが用意されており、アクセストークン取得のリクエストを投げる前後で任意の処理を入れることが可能です。
インターセプタのメソッドにSpring Retryの @Retryable
アノテーションを付けることで、簡単にリトライを実現できます。
@EnableRetry @Configuration class OAuth2Configuration { @Autowired RetryableAccessTokenRequestInterceptor accessTokenRequestInterceptor @Bean AccessTokenProvider accessTokenProvider() { def accessTokenProviders = [ new AuthorizationCodeAccessTokenProvider(), new ImplicitAccessTokenProvider(), new ResourceOwnerPasswordAccessTokenProvider(), new ClientCredentialsAccessTokenProvider() ] // インターセプタを設定する accessTokenProviders*.interceptors = [accessTokenRequestInterceptor()] new AccessTokenProviderChain(accessTokenProviders) } }
@Component class RetryableAccessTokenRequestInterceptor implements ClientHttpRequestInterceptor { // 接続失敗に対するリトライを宣言する @Retryable(value = {ConnectException.class}, backoff = @Backoff(delay = 500)) @Override ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException { execution.execute(request, body) } }
あらかじめ、依存関係にSpring Retryを追加しておく必要があります。
実行結果
リトライの様子を確認するため、ログレベルを設定しておきます。
logging.level: # リトライのログ org.springframework.retry: DEBUG # Spring Security OAuth2の通信ログ org.apache.http.wire: DEBUG
アクセストークン取得を実行すると、以下のようなログが出力されます。
o.s.retry.support.RetryTemplate : Retry: count=0 org.apache.http.wire : http-outgoing-1 << "[read] I/O error: Connection reset" o.s.retry.support.RetryTemplate : Checking for rethrow: count=1 o.s.retry.support.RetryTemplate : Retry: count=1 o.s.retry.support.RetryTemplate : Checking for rethrow: count=2 o.s.retry.support.RetryTemplate : Retry: count=2 o.s.retry.support.RetryTemplate : Checking for rethrow: count=3 o.s.retry.support.RetryTemplate : Retry failed last attempt: count=3
まとめ
Spring BootアプリのテストをSpockで書く(続編)
以前にSpring BootアプリケーションのテストをSpockで書く方法を紹介しましたが、この方法ではテストの所要時間が長くなる問題がありました。本稿では他の方法を紹介します。
具体的には、インナークラスの @TestConfiguration
でMockを定義するとSpecificationクラスごとにApplication Contextが再生成されてしまうため、スローテストの原因になる問題がありました。
// Specificationのインナークラス @TestConfiguration static class MockConfig { final detachedMockFactory = new DetachedMockFactory() @Bean ExternalApiClient externalApiClient() { detachedMockFactory.Mock(ExternalApiClient) } }
特定のテストケースだけBeanをMockに置換したい場合はこの方法が必要ですが、多くの場合は必要ないはずです。その前にプロダクトコードの設計を見直す方がよいでしょう。
コンポーネントテスト
コンポーネントテストのレベルでは、コンストラクタでMockを注入する方法で十分です。
// プロダクトコード @Component class BarService { final ExternalApiClient client BarService(ExternalApiClient client) { this.client = client assert client } }
// テストコード @SpringBootTest(webEnvironment = NONE) class BarServiceSpec extends Specification { @Subject BarService service ExternalApiClient client = Mock() def setup() { service = new BarService(client) } }
Mockではなく本物のBeanが必要な場合は、Specificationクラスに @Autowired
なプロパティを追加してBeanを取得します。
@SpringBootTest(webEnvironment = NONE) class BarServiceSpec extends Specification { @Subject BarService service ExternalApiClient client = Mock() // Mock Bean @Autowired HelloProvider provider // 本物のBean def setup() { service = new BarService(client, provider) } }
E2Eテスト
冒頭で述べた方法ではSpecificationごとにMockを定義していましたが、全テストケースで共通のMockを定義するとApplication Contextが再利用されるので所要時間が大幅に短くなります。
// Mock定義 @Configuration class IntegrationTestConfiguration { private final detachedMockFactory = new DetachedMockFactory() @Bean ExternalApiClient externalApiClient() { detachedMockFactory.Mock(ExternalApiClient) } }
// テストコード @SpringBootTest(webEnvironment = RANDOM_PORT) class BarControllerSpec extends Specification { @Autowired TestRestTemplate restTemplate @Autowired ExternalApiClient client }
@Primary
を付けるなどの工夫をすれば、この方法とSpecificationごとにMockを定義する方法を組み合わせて利用できると思います。(未検証…)
まとめ
これまでに以下の方法を紹介しました。
- SpecificationクラスごとにMockを定義する方法
- Specificationクラスごとに本物とMockを使い分けられる。
- 個別にApplication Contextが生成されるので、スローテストの原因になる。
- コンストラクタでMockを注入する方法
- Application Contextが再利用されるので、テストの所要時間が短い。
- テストコードでコンストラクタ呼び出しを記述するのが面倒。
- 全テストケースで共通のMockを定義する方法
- Application Contextが再利用されるので、テストの所要時間が短い。
- Mock定義が共通なので融通が利かない。
参考までにGitHubにサンプルプロジェクトを置いています。