きり丸の技術日記

技術検証したり、資格等をここに残していきます。

2020年の1人アドベントカレンダーを振り返る

きり丸アドベントカレンダー2020の25記事目です。


全部JavaやSpring系の記事で埋めようかと思いましたが、25記事全部1人で埋めることは今後も少ないと思うので、この振り返り記事で締めます。

完走した感想

2度と1人アドベントカレンダーをやらねぇという気持ちでいっぱいです。

理由としては大きく2つあります。

  • アクセスが思ったよりも伸びない
  • 負荷が高すぎる

アクセスが思ったよりも伸びない

投稿しても反応がイマイチなのはよくありますが、これだけ投稿していてPV10もらえないこともありました。

投稿したらPV30くらいコンスタントに貰えればうれしかったのですが…。

アドベントカレンダーの時期は他のブログも増えるし、読者としては全部の記事を読まないでしょう。自分もフォローしているブログでも、興味がなければ読みませんしね。

f:id:nainaistar:20201224225908p:plain

負荷が高すぎる

1記事を書くのに6時間くらいかかります。

  • 技術の検証
    • 3時間
  • ブログにする
    • 3時間
  • 技術ネタの選定
    • プライスレス(序盤はともかく、後半はネタ切れで1,2時間探すこともざらでした)

休日を1日丸々使って2記事、検証が簡単だったり筆が乗ったりして3記事。平日は2日で1記事完成します。

つまり、週に最大で9記事書くのが限界です。最終的にネタを確定させたのが2週間前だったので、非常に苦しかったです。有給を使ってアドベントカレンダー書いた日もありました。

会社で導入していたので大丈夫だと思っていたが、ライブラリのバージョンアップで元の設定だと動かないこともしばしば…。

…で、ここまでやったけど、反応が薄い。とても、悲しい。

それでもやってよかったこと

記事の絶対数が増える分、反応が貰いやすかったです。

Twitterで記事のいいね等も貰えたりしました。

終わりに

1人アドベントカレンダーは2度とやる気はありません。あっても、GW毎日投稿とか1週間程度の祭りには参加します。

2021年は複数人で行われているアドベントカレンダーに参加します。QAとか設計、JavaやSpring等のアドベントカレンダーに参加する予定です。

2019年ではDDDのアドベントカレンダーに参加していました。検索されやすいアドベントカレンダーQiitaに投稿した記事は、定期的にLGTM貰えて非常にうれしいです。

2019年と同じように2020年でもアクセス貰えると思いましたが…。当てが外れました。

指摘していただいたりしたので、得られることはありましたが、気ままにゆるゆると検証するのが性に合っています。

2020年のアドベントカレンダーがネガティブな締めになってしまっていることは少し残念ではありますが、これも経験だと思って来年にも活かしていきたいです。


この記事がお役に立ちましたら、各種SNSでのシェアや、今後も情報発信しますのでフォローよろしくお願いします。

類似記事

きり丸アドベントカレンダー2020 adventar.org

f:id:nainaistar:20201010234103j:plain

SpringBootで同クラス別インスタンスをインジェクションできるようにする(BeanとQualifier)

きり丸アドベントカレンダー2020の24記事目です。


23日目の記事にて、RestOperationsインターフェースを使用して別システムのAPIを実行できるようにしました。23日目の状態では他システムのAPIを実行する直前にてインスタンスを生成していましたが、別の箇所でも使えるように生成ロジック自体を共通的にできそうです。

また、各外部連携用のクライアントで生成せずに、すでに生成された状態でインジェクションできると生成ロジックが流出しないし、記載量も減るのでいいことづくめです。

ただし、AシステムとBシステムのDI対象のRestOperationsインターフェースは同じものの、使用するコンポーネント側でURL等の各システムで特化している情報をもった異なる別のインスタンスをインジェクションさせる必要があります。

特に何の設定も行わないと、どちらのRestOperationsをインジェクションするべきかがわからないので、次のエラーログが出て起動できません。

Parameter 1 of constructor in com.example.demo.external.zipCloud.ZipCloudClientImpl required a single bean, but 2 were found:
        - dummyUserRestOperations: defined by method 'dummyUserRestOperations' in class path resource [com/example/demo/external/operation/RestOperationConfig.class]
        - zipCloudRestOperations: defined by method 'zipCloudRestOperations' in class path resource [com/example/demo/external/operation/RestOperationConfig.class]

今回の記事では、共通化して同クラスになったものの、目的別のインスタンスを使用するクラスで目的通りにインジェクションさせるようにします。

ゴール

  • 同クラス別インスタンスを目的どおりにインジェクションさせる
  • RestOperationsの生成ロジックを共通化する

23日目の記事では、住所コードから住所を取得するAPIを使用しました。

今回の記事では、ダミーユーザを生成してくれるAPIを使用します。

randomuser.me

環境

  • Java
    • 15
  • org.springframework.boot:spring-boot-starter-web
    • 2.4.0
  • Lombok

手順

RestOperations生成ロジックを共通化する


23日目の記事では、生成したロジックを別クラスに移しているだけです。

解説等は上の記事から見てください。

ファイル名:RestOperationFactory.java

@RequiredArgsConstructor
@Component
public class RestOperationFactory {
  private final RestTemplateBuilder restTemplateBuilder;

  public RestOperations createRestOperations(ExternalProperties props) {
    MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
    converter.setSupportedMediaTypes(List.of(MediaType.TEXT_PLAIN));

    RestTemplate build = restTemplateBuilder
        .setConnectTimeout(Duration.ofSeconds(props.getConnectionTimeout()))
        .setReadTimeout(Duration.ofSeconds(props.getReadTimeout()))
        .build();
    build.getMessageConverters().add(converter);

    return build;
  }
}

システムごとのRestOperationsインスタンスを生成する


クラスに@Configurationアノテーションを付与します。

メソッドごとに@Beanをつけると、メソッドごとにインジェクション対象のインスタンスになります。@Beanに何も指定しないと、メソッド名がそのまま識別名になります。

ファイル名:RestOperationConfig.java

@Configuration
@RequiredArgsConstructor
public class RestOperationConfig {
  private final RestOperationFactory restOperationFactory;

  @Bean
  RestOperations dummyUserRestOperations(DummyUserClientProperties props){
    return restOperationFactory.createRestOperations(props);
  }

  @Bean
  RestOperations zipCloudRestOperations(ZipCloudClientProperties props){
    return restOperationFactory.createRestOperations(props);
  }
}

インスタンスを識別しながらQualifierを使用してインジェクションする


インジェクションする際に@Qualifierを指定します。上でも説明しましたが、@Beanのパラメータを指定していればその値を、指定しなければメソッド名を入力してください。

ファイル名:ZipCloudClientImpl.java

  public ZipCloudClientImpl(
      ZipCloudClientProperties props,
      @Qualifier("zipCloudRestOperations") RestOperations restOperations
  ) {
    this.props = props;
    this.restOperations = restOperations;
  }

ソースコード

アドベントカレンダー24日目。
github.com

終わりに

やり方がわからないと、共通化に苦労してしまいます。または、生成ロジックだけは共通化したものの、各クラスでインスタンスを何度も生成する必要があります。

私はBeanQualifierの理解に苦しみました。

インジェクション対象で同クラス別インスタンスを扱うことはユースケースとしては少ないですが、覚えておくと地味に助かると思いますので、ぜひ覚えて下さい。


この記事がお役に立ちましたら、各種SNSでのシェアや、今後も情報発信しますのでフォローよろしくお願いします。

参考記事

敬称略

paiza開発日誌:面倒な手続き不要!「Web API」の超お手軽活用術をJavaScriptコード付きで一挙大公開! paiza.hatenablog.com

この記事に対して反応してくださった内容の方が正直濃いです。
私のブログで見たうえで、さらに良い設計をする場合はこちらの記事を参考にしてください。

blog.ik.am

類似記事

きり丸アドベントカレンダー2020 adventar.org

きり丸のHerokuページ
https://kirimaru-todoapp.herokuapp.com/

25日目のアドベントカレンダーの記事。この記事は振り返りです。 https://nainaistar.hatenablog.com/entry/2020/12/25/083000nainaistar.hatenablog.com

f:id:nainaistar:20201109133010p:plain

SpringBootで別システムのAPIを実行する(RestOperations)

きり丸アドベントカレンダー2020の23記事目です。


自システムのREST APIを作成するときには、@RestControllerを付与すれば簡単に実装できます。

今回はRestOperationsを使用して、別システムのAPIを実行します。

ゴール

  • アプリケーションから別システムに対してREST通信を行う

今回は、次のサイトが提供してくれている住所コードから住所名を取得するAPIを実行しましょう。トークン発行等が不要です。

zipcloud.ibsnet.co.jp

APIの使い方


GETメソッドで次のURLを実行します。クエリパラメータで「zipcode」に住所コードを入力すると、住所名を取得できます。

https://zipcloud.ibsnet.co.jp/api/search?zipcode=2430018

レスポンスのBody:

{
  "message": null,
  "results": [
    {
      "address1": "神奈川県",
      "address2": "厚木市",
      "address3": "中町",
      "kana1": "カナガワケン",
      "kana2": "アツギシ",
      "kana3": "ナカチョウ",
      "prefcode": "14",
      "zipcode": "2430018"
    }
  ],
  "status": 200
}

環境

  • Java
    • 15
  • org.springframework.boot:spring-boot-starter-web
    • 2.4.0
  • Lombok

手順

外部連携用の設定ファイルを用意する


外部システムのURLを構築するための設定ファイルを用意します。現時点では分割する必要はありませんが、タイムアウト値等は共通的に持っておく方があとあと便利です。

YAMLファイルでの設定値を共通的にもつ方法は、こちらを参照してください。

※アンカーとエイリアスを紹介した時の記事

ファイル名:application-external.yml

external:
  commons: &external-common
    schema: https
    connectionTimeout: 3
    readTimeout: 3
  zip-cloud:
    <<: *external-common
    host: zipcloud.ibsnet.co.jp
    path: /api/search

外部連携用の設定ファイルを読み込むJavaファイルを用意する


URLの作成ロジックは共通的に作っておいた方が楽です。ですので、私は設定ファイルを読み込むJava側でロジックを持ちます。

今回はUriComponentsBuilderを使ってURLを構築します。

特殊な設定値はportくらいです。portを-1に設定した時にポート番号をURLに含めなくなります。

port 生成されるURI
80 http://localhost:80/api/v1
-1 http://localhost/api/v1

また今回はクエリパラメータに?zipcode={値}を設定したいので、クエリパラメータをURL生成時に作成します。

ファイル名:ExternalProperties.java

@Setter
public abstract class ExternalProperties {
  private String schema;
  private String host;
  private int port;
  private String path;

  @Getter
  private int connectionTimeout;
  @Getter
  private int readTimeout;

  private UriComponentsBuilder createUriBuilder() {
    return UriComponentsBuilder.newInstance()
        .scheme(schema)
        .host(host)
        .port(port != 0 ? port : -1)
        .path(path);
  }

  public String getUrl(Map<String, String> map) {
    UriComponentsBuilder builder = createUriBuilder();

    map.forEach(builder::queryParam);
    return builder.toUriString();
  }
}

実際に読み込ませるときは、作成したJavaファイルを継承しましょう。

@ConfigurationPropertiesを指定することで、設定ファイルを読み込むことができます。

ファイル名:ZipCloudClientProperties.java

@Component
@ConfigurationProperties(prefix = "external.zip-cloud")
public class ZipCloudClientProperties extends ExternalProperties {
}

APIの取得結果を受け取るDTOを作成する


レスポンスを受け取って処理をしたいので、型を作りましょう。

単純に項目名とマッピングできるDTOを作成すれば大丈夫です。

ファイル名:ZipCloudDto.java

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ZipCloudDto {
  private String message;
  private List<ZipAddressDto> results;
  private String status;
}

ファイル名:ZipAddressDto.java

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ZipAddressDto {
  private String address1;
  private String address2;
  private String address3;
  private String kana1;
  private String kana2;
  private String kana3;
  private String prefcode;
  private String zipcode;
}

APIを実行するコードを書く


RestTemplateを使用して、別システムへアクセスします。

RestTemplateBuilderからRestTemplateを生成します。タイムアウト値等やインターセプターを設定してから生成できます。

今回使用する「ZipCloud」のAPIは、レスポンスのメディアタイプがtext/plainなのでデフォルトだと受け取れません。text/plainを受け取れるようにコンバータをRestTemplateに追加します。

実際に他システムのAPIを実行するgetForEntity()の第一パラメータにURL、第二パラメータに返却の型を指定します。

@Component
@RequiredArgsConstructor
public class ZipCloudClientImpl implements ZipCloudClient {
  private final ZipCloudClientProperties props;
  private final RestTemplateBuilder restTemplateBuilder;

  @Override
  public ZipCloudDto getAddressByZipcode(String zipCode) {
    MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
    converter.setSupportedMediaTypes(List.of(MediaType.TEXT_PLAIN));

    RestTemplate restTemplate = restTemplateBuilder
        .setConnectTimeout(Duration.ofSeconds(props.getConnectionTimeout()))
        .setReadTimeout(Duration.ofSeconds(props.getReadTimeout()))
        .build();
    restTemplate.getMessageConverters().add(converter);

    HashMap<String, String> hashMap = new HashMap<>();
    hashMap.put("zipcode", zipCode);

    ResponseEntity<ZipCloudDto> responseEntity = restTemplate.getForEntity(props.getUrl(hashMap), ZipCloudDto.class);
    return responseEntity.getBody();
  }
}

テストコードを書く


RestTemplateBuilderがSpringのDI機能を使用しているので、DIできるように@SpringJUnitConfigを使用します。読み込む設定ファイルも必要になるので、RestTemplateAutoConfiguration.classも指定します。

後は基本的にはコードを見ていただければ分かるかと思います。書いてあるのはセットアップとテストだけなので。なお、設定値を見ていただけると分かるとおり、相手の本番環境にアクセスします。

相手の環境がダウンしているとアクセス失敗し、テストが正常動作しません。挙動確認が終わり次第、自前でモックサーバーを立てるとよいでしょう。モックサーバーの建て方も記事にする予定です。

@SpringJUnitConfig(classes = {
    RestTemplateAutoConfiguration.class
})
class ZipCloudClientImplTests {

  ZipCloudClient target;

  @Autowired
  private RestTemplateBuilder restTemplateBuilder;

  @BeforeEach
  void setup(){
    ZipCloudClientProperties properties = new ZipCloudClientProperties();
    properties.setSchema("https");
    properties.setHost("zipcloud.ibsnet.co.jp");
    properties.setPort(0);
    properties.setPath("/api/search");

    target = new ZipCloudClientImpl(properties, restTemplateBuilder);
  }

  @Test
  void test_01(){
    ZipCloudDto expected = ZipCloudDto.builder()
        .message(null)
        .results(List.of(
            ZipAddressDto.builder()
                .address1("神奈川県")
                .address2("厚木市")
                .address3("中町")
                .kana1("カナガワケン")
                .kana2("アツギシ")
                .kana3("ナカチョウ")
                .prefcode("14")
                .zipcode("2430018")
                .build()
        ))
        .status("200")
        .build();

    ZipCloudDto actual = target.getAddressByZipcode("2430018");

    assertThat(actual).isEqualTo(expected);
  }

  @Test
  void test_02(){
    ZipCloudDto expected = ZipCloudDto.builder()
        .message("パラメータ「郵便番号」の桁数が不正です。")
        .status("400")
        .build();

    ZipCloudDto actual = target.getAddressByZipcode("XXX");

    assertThat(actual).isEqualTo(expected);
  }

}

ソースコード

アドベントカレンダー23日目。
github.com

終わりに

自分で作ったシステムでできることには限界があります。やれることを増やすためにも、別システムを使用したり逆に情報を提供したりする必要があります。

REST通信できることで、やれることが大きく広がりますので、ぜひ試してみてください。


この記事がお役に立ちましたら、各種SNSでのシェアや、今後も情報発信しますのでフォローよろしくお願いします。

参考記事

敬称略

paiza開発日誌:面倒な手続き不要!「Web API」の超お手軽活用術をJavaScriptコード付きで一挙大公開! paiza.hatenablog.com

類似記事

きり丸アドベントカレンダー2020 adventar.org

きり丸のHerokuページ
https://kirimaru-todoapp.herokuapp.com/

24日目のアドベントカレンダーの記事 https://nainaistar.hatenablog.com/entry/2020/12/24/083000nainaistar.hatenablog.com

f:id:nainaistar:20201109133010p:plain