システム開発で思うところ

Javaで主にシステム開発をしながら思うところをツラツラを綴る。主に自分向けのメモ。EE関連の情報が少なく自分自身がそういう情報があったら良いなぁということで他の人の参考になれば幸い

Lombokのメモ

使い方

Stable

Lombokを使った開発ひと巡り - 覚えたら書く

Lombok - アノテーション一覧 - ぺんぎんらぼ

Lombok Experimental features - abcdefg.....

lombok こう使ってます! #Java - Qiita

Lombokの@BuilderがCSVファイル生成に役立った話 - STORES Product Blog

JavaDoc生成の際、Lombokの@Builderを使うと「エラー:シンボルを見つけられません」が発生する #javadoc - Qiita

SpotBugsが可変オブジェクトでないものを可変オブジェクトと判定してしまう場合の対処法 - エキサイト TechBlog.

Lombokを使っているときにJacocoのカバレッジから自動生成分を除外する方法 #Java - Qiita

lombok.config

lombok.extern.findbugs.addSuppressFBWarnings = true
lombok.addLombokGeneratedAnnotation = true 

OpenAPI(MicroProfile)を拡張してみる

はじめに

vermeer.hatenablog.jp

でOpenAPI(MicroProfile)の実装をしてみましたが、もう少し楽ができるようにしたいと思いました。
かといって実装そのものを拡張したり大がかりなものをつくるほどでも無い「程よい程度に」拡張をしてみました。

拡張ポイント

プロジェクト全体の定義

OpenApiModelReader(OASModelReaderの実装)に

  • Lisence
  • ドキュメント
  • セキュリティ(トークンの設定)

などのプロジェクトの基本情報を設定します。

オブジェクトのSchemaを追加

OpenApiSchemaに変換をしたいクラスと refで使用するキー値を登録します。

  public static final String Gender = prifixPath + "Gender";

キー値のペアとなる、Schema生成関数も登録します.

  // openApiのUtilは実装依存となるため実行時に決定するように関数で指定します.
  private static final Map<String, Supplier<Schema>> schemaMap =
      Map.ofEntries(
          entry(Gender, () -> OpenApiSchemaUtil.createEnumSchema(Gender.class)),
          entry(Genders, () -> OpenApiSchemaUtil.createEnumListSchema(Gender.class)),
          entry(UploadFile, () -> OpenApiSchemaUtil.createUploadFileSchema()),
          entry(UploadFiles, () -> OpenApiSchemaUtil.createUploadFileListSchema()));

実際に生成するのは OpenApiModelReader#buildModel内でOpenApiSchema.appendSchema(components);で行います。

登録したキー値は@Schemarefとして指定をします。

  @Parameters({
    @Parameter(name = "gender", description = "性別", schema = @Schema(ref = OpenApiSchema.Gender)),
    @Parameter(name = "name", description = "ユーザー名", example = "user name")
  })
  public Response getUsersByQuery(
      @QueryParam("gender") Gender gender, @QueryParam("name") String name) {

なにがうれしいか

スキーマの指定をクラスで指定できるので、書き間違いによる実行時エラーが回避できます。

実装

refの指定としてメソッドを使用できないため、実装としては「疑似 Enumなストラテジー」を作って対応をしました。

/** OpenApiSchemaの変換およびSchemaにrefに指定する定数を管理します. */
public class OpenApiSchema {

  private static final String prifixPath = "#/components/schemas/";

  public static final String Gender = prifixPath + "Gender";
  public static final String Genders = prifixPath + "Genders";
  public static final String UploadFile = prifixPath + "UploadFile";
  public static final String UploadFiles = prifixPath + "UploadFiles";

  // openApiのUtilは実装依存となるため実行時に決定するように関数で指定します.
  private static final Map<String, Supplier<Schema>> schemaMap =
      Map.ofEntries(
          entry(Gender, () -> OpenApiSchemaUtil.createEnumSchema(Gender.class)),
          entry(Genders, () -> OpenApiSchemaUtil.createEnumListSchema(Gender.class)),
          entry(UploadFile, () -> OpenApiSchemaUtil.createUploadFileSchema()),
          entry(UploadFiles, () -> OpenApiSchemaUtil.createUploadFileListSchema()));

  /**
   * プロパティで指定したクラスをSchemaへ変換してOpenAPIのコンポーネントへ追記します.
   *
   * @param components OpenAPIのcomponents
   */
  public static void appendSchema(Components components) {

    validate();
    int startIndex = prifixPath.length();

    schemaMap.entrySet().stream()
        .forEach(
            entrySet -> {
              var key = entrySet.getKey().substring(startIndex);
              components.addSchema(key, entrySet.getValue().get());
            });
  }

  /** Publicフィールドとスキーマの設定をするMapの整合性が取れていることを検証します. */
  private static void validate() {

    var fieldList =
        Stream.of(OpenApiSchema.class.getFields())
            .filter(f -> f.getType().isPrimitive() == false)
            .filter(f -> f.getType().isInstance(""))
            .map(f -> f.getName())
            .collect(Collectors.toSet());

    if (schemaMap.entrySet().size() != fieldList.size()) {
      throw new IllegalArgumentException(
          "public static field is not match schemaMap. append Schema must match.");
    }
  }
}

@ExampleObjectにjsonを指定

@ExampleObjectvaluejsonを記述をすることができますが、resource配下のjsonファイルを指定することはできません。
本来、externalValuehttpを使用して外部リソースを参照するものですが、あまり使用することは無いと考え、externalValueにリソースパスを指定して読み込めるようにしました。

externalValue = "openapi/user/get_response_default.json"
src/main/resources/openapi/user/get_response_default.json

内部的には、externalValuejsonを展開した結果をvalueへ転記して、OpenAPI.yamlとして、そのまま使えるようにしています。

なにがうれしいか

同じレスポンスの型を使ったExampleの記述が完結に記載できます。
またエスケープの無いjsonの記述ができるので見やすいです。

実装

OASFilterの実装であるOpenApiFilterでリクエストボディとレスポンスボディの@ExampleObjectの中身を書き換えます.

/** OpenAPIのOASFilterの実装. */
public class OpenApiFilter implements OASFilter {

  @Override
  public RequestBody filterRequestBody(RequestBody requestBody) {
    OpenApiExampleObjectUtil.convertExternalValueToValue(requestBody.getContent());
    return OASFilter.super.filterRequestBody(requestBody);
  }

  @Override
  @SuppressWarnings("checkstyle:AbbreviationAsWordInName")
  public APIResponse filterAPIResponse(APIResponse apiResponse) {
    OpenApiExampleObjectUtil.convertExternalValueToValue(apiResponse.getContent());
    return OASFilter.super.filterAPIResponse(apiResponse);
  }
}
/** OpenApiExampleObjectUtil. */
public class OpenApiExampleObjectUtil {

  /**
   * ExampleObjectのExternalValueで指定したJsonをValueとして展開します.
   *
   * <p>Contentを直接上書きします.
   *
   * <p>上書きに使用した externalValue は消去します.
   *
   * <p>valueに記述がある場合はvalueの記述を優先します.
   *
   * @param content openApiの@Content
   * @throws UncheckedIOException IOExceptionが発生したら処理を中断します.
   */
  public static void convertExternalValueToValue(Content content) {

    content.getMediaTypes().entrySet().stream()
        .filter(e -> Objects.nonNull(e))
        .forEach(
            e1 -> {
              e1.getValue().getExamples().entrySet().stream()
                  .filter(
                      e2 ->
                          Objects.isNull(e2.getValue().getValue())
                              || e2.getValue().getValue().equals(""))
                  .filter(e2 -> Objects.nonNull(e2.getValue().getExternalValue()))
                  .forEach(
                      example -> {
                        var externalValue = example.getValue().getExternalValue();

                        ClassLoader loader = OpenApiExampleObjectUtil.class.getClassLoader();

                        try (var inputStream = loader.getResourceAsStream(externalValue)) {
                          if (Objects.isNull(inputStream)) {
                            throw new FileNotFoundException(
                                "externalValue =["
                                    + externalValue
                                    + "] cloud not find resource path.");
                          }
                          String json =
                              new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);
                          example.getValue().setValue(json);
                          example.getValue().setExternalValue("");
                        } catch (IOException ex) {
                          throw new UncheckedIOException(
                              "externalValue =["
                                  + externalValue
                                  + "] could not find resource path or could not read resource file.",
                              ex);
                        }
                      });
            });
  }
}

モックとしてjsonを使用する

OpenAPIというよりも、RESTfulAPIを便利にするものです。
開発初期では、インターフェースとしてのOpenAPIの定義設定にあわせて アプリケーションサーバーをモックサーバとして使用したいケースがあります。
メソッドの戻り値の型はResponseではなく、直接レスポンスを返却しています。
(ただし、実行結果のHttpが200が固定になります)

  public UserResponse.UserResponseBody getJsonUserById(@PathParam("id") String id) {
    var response =
        JsonUtil.readFromResource(
            "openapi/user/get_response_default.json", UserResponse.UserResponseBody.class);
    return response.get();
  }

なにがうれしいか

すでに作成済みの@ExampleObjectjsonをそのまま使用することができるので楽ができます。
入力値で返却値を変えたい場合は、複数のjsonを作成して引数を元に返却を切り替えるようにするだけで対応できます。

実装

jsonの読み込みでjacksonを使用するので、pom.xmlに依存を追加します。

<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.16.1</version>
    <type>jar</type>
</dependency>

モックでの使用を前提としているので、Jsonの操作時のエラーハンドリングを簡略化して*1実行時例外はあえて塗りつぶしています。

/** JsonUtil. */
public class JsonUtil {

  private static final Logger logger = Logger.getLogger(JsonUtil.class.getName());

  /**
   * Json文字列をクラスにマッピングします.
   *
   * @param <T> マッピングするクラスの型
   * @param json json文字列
   * @param classType マッピングするクラスの型
   * @return マッピングしたインスタンス.例外があった場合は {@code Optional.empty()}
   */
  public static <T> Optional<T> read(String json, Class<T> classType) {

    ObjectMapper mapper = new ObjectMapper();
    try {
      T object = mapper.readValue(json, classType);
      return Optional.of(object);
    } catch (JsonProcessingException ex) {
      logger.log(Level.SEVERE, "Json could not parse.", ex);
      return Optional.empty();
    }
  }

  /**
   * Jsonリソースをクラスにマッピングします.
   *
   * @param <T> マッピングするクラスの型
   * @param resourcePath リソースパス
   * @param classType マッピングするクラスの型
   * @return マッピングしたインスタンス.例外があった場合は {@code Optional.empty()}
   */
  public static <T> Optional<T> readFromResource(String resourcePath, Class<T> classType) {

    ClassLoader loader = Thread.currentThread().getContextClassLoader();

    try (var inputStream = loader.getResourceAsStream(resourcePath)) {
      if (Objects.isNull(inputStream)) {
        throw new FileNotFoundException(
            "resourcePath =[" + resourcePath + "] cloud not find resource path.");
      }

      String json = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);
      return read(json, classType);
    } catch (IOException ex) {
      logger.log(Level.SEVERE, "resourcePath =[" + resourcePath + "] ioexception.", ex);
    }

    return Optional.empty();
  }
}

Code

experimentation/ee10-02-openapi at openapi-extend · vermeerlab/experimentation · GitHub

さいごに

色々と自動生成的なことをしようとも思ったのですが、標準機能による拡張ポイントで出来ることにあえて限定しておくほうが、EE系の場合には良いかな?と思って このくらいにしましたが、それでも随分と冗長な記述が減るのではないかな?と思っています。
ここではPayaraを使っているため、MicroProfileの実装としては正直貧弱だと思いますが、QuarkusやHelidonといったMicroProfileの実装を使えば(importにQuarkusのライブラリを使うような拡張を使えば)、もっと便利なものがすでに提供されている可能性は大いにあります。

*1:Eitherの代わりにOptionalで簡易的に処置

KarateのfeatureをOpenAPIから作成(ZenWave Karate IDE)

はじめに

OpenAPIの定義をDSL的に使って、Karateのfeatureを自動生成するVSCodeの拡張です。

基本的な使い方は拡張機能のサイトにある動画を見るのが分かりやすいので割愛します。

marketplace.visualstudio.com

ここでは自動生成された資産についての補足です。

認可トークンのファイルの出力場所

OpenAPIで認可トークンありにすると、その辺りの資産も自動生成してくれます。
karate-auth.js に認可トークンに関する共通の設定を記述できます。

ただ、このファイルの生成先のディレクトリがプロジェクトのルートでした。
これだとfeatureからのパス指定位置としてふさわしくないため、テストコードのルートとなるディレクトリにファイルを移動させました。
デフォルトはベーシック認証になっているみたいです。
今回は認可トークンをつかったロジックなどは実装していないのでコメントアウトしました。

自動生成で出来なかったところの手直し

通常のRESTfulな仕様の範囲は特にエラーにならなかったです。
ただファイルのアップロードだけは multi-part で指定してくれなかったので修正をしました*1

  And multipart file file = {read:'test-data/FileToUpload1.txt', filename:'FileToUpload1.txt',Content-type:'mulitpart/form-data'}

Code

vermeer.hatenablog.jp

で作成したEEのアプリをCargoで起動して、karateのテストを動かします。
手順は、下記のリポジトリのREADMEに記載しているので、そちらを参考にしてください。

experimentation/karate-01-openapi at openapi · vermeerlab/experimentation · GitHub

さいごに

OpenAPIをちゃんとつくると、それをDSLとして色々とできるというのは非常に良いですね。
テストコードの構造化しているところとか、よく見るサンプルよりも一歩踏み込んだ感じなので色々と学びも多かったように思います。

*1:通常のリクエストボディと同じ扱いをされる

PayaraでOpenAPI(MicroProfile)

はじめに

PayaraでOpenAPI-UIをつかって、WebAPIの仕様と実行インターフェースを準備する実装です。
軽く触ってみた感じの実装例や、Quarkus(MicroProfileの実装)を使ったものはあったのですが標準仕様だけの範疇だけでできるものはなかなか見つからなかったので色々と試しながらやってやってみました。

メインをOpenAPIにするのでJAX-RSJakarta RESTful Web Services)についての説明は割愛します。

実行環境

やってみて思ったこと・わかったこと

実際にやってみて思ったことなどを先に。
どうしてこうなっているの?というのが分かっていると以下のコードの理由が分かりやすいように思ったので。

OpenAPIの記述は実装よりも優先

実装からある程度自動生成はされますが、 OpenAPIの記述の方が優先されるのでOpenAPI-UIでつけるインターフェースの作り込みは自分でコツコツ書き込む方と割り切った方が良さそうです。
例えばEnumクラスはEnumの要素を enumeration で列挙するのを基本となるみたいです。ちょっと面倒ですね…。
ちなみに @BeanParamで定義したものは、そのままではどうやってもOpenAPI-UIで使えないのですが、OpenAPIの記述が実装よりも優先されることで作り込むことができます*1

アノテーションで出来ないときはコードで作り込み

リストのクエリパラメータはアノテーションでは実現出来ませんでした。
そんなときは OASModelReader の実装の中で OASFactory.createComponents() をつかってコードで作り込むと実現できることもあります。
アノテーションで頑張って上手くいかないときは割り切ってコードを書くのも一案です。
ちなみに、上記の「Enumenumeration の列挙」ですが、コードでスキーマを定義すれば共通化できます。

OpenAPIの記述と実行記述は分けておきたい

メソッドの引数に@Parameterを付ける方法もありますが、個人的にはメソッドアノテーションに寄せた方がコードが読みやすいかなと思います。
イメージとしてはRESTfulの実装をした上で、そのコード部分には手を加えずにメソッドアノテーションで情報を付与するという流れで書き足した方が好みです。
仮引数の中に複雑なアノテーションを記述していくと、コードの引数がどこにあるのか視認性が悪くなるというのが理由です。
ただ、あんまりそういう書き方を推奨するという情報も無かったので王道ではないかもしれないです。

tagをつかってコントローラー単位で集約

tagを使うのURLをまとめることが出来ます。
以前、SpringBoot & springdoc-openapi を使った時はtagを使わなかったので全部がフラットになってドキュメントとしての視認性が悪かったように思います。
なお、tagを複数付与するとtagの分だけ同じURLが同じOpenAPI-UIに表示されます。
OpenAPIの定義をDSLとしてさらに自動生成をすると、不具合に繋がるかもしれないので複数tagを付与するときは、その先も含めて事前に技術検証をしておくことをおすすめします。

レスポンスの型はGenericを使わないように細かく作成

@Schema(implementation = Hoge.class)

みたいに記述することでレスポンスの型の指定ができるのですがGenericが使えません。
なので、レスポンスクラスを代表として、その派生クラスをインナークラスとして定義しました。

public class UserResponse extends UserRequest {
 (略)
  static class UserResponseBody extends ResponseBody<UserResponse> {}
}

@ExampleObjectをちゃんと書く

Genericを使わなければ型情報からある程度はOpenAPI-UIで表示可能な定義を実装から自動生成してくれます。
ただ今回は ResponseBodyという共通定義にレスポンスをGenericで定義して入れ子構造な感じにしたので、そのあたりは自動ではできず*2
リクエストは入れ子構造にしなかったのですが、Object型(例えばEnum)を使ったりすると、exampleが思ったように出力されなかったので、こちらもコツコツ定義しました。
レスポンスはとりあえず条件を指定して実際に出力をすればexampleが無くても最悪なんとかできなくはないのですが、リクエストはPOSTやPUTをするときにOpenAPI-UIで使いたいので手間がかかったとしても、きちんと記述することをお勧めします。

@ExampleObjectを記述するときの留意事項として

  • MediaTypeを揃える
  • nameがexampleのキーになるので記述を忘れない

というのがハマったところでした。

パラメータの記載は @Parameters を使う

引数ではなくメソッドにパラメータを指定する場合、パラメータが1つの場合でも、@Parametersを使わないとOpenAPIの出力として情報が出力されませんでした。

OpenAPI yamlでの出力から逆算して考える

例えば、ファイルアップロードの書き方についてアノテーションやコードでの定義方法を探してすぐに見つからない場合は、最終的なOpenAPIのyamlでの書き方を調べて、それをそれをどうやったらアノテーションもしくはコードで定義できるのか?というのを逆引きで組み立てるとスムーズに試行が進められました。
今回の実装でいうと

  • Enumを検索用クエリとしてリストにしたい
  • 複数ファイルのアップロードの設定のやり方

が、逆引きから見つけられた定義でした。

pom

OpenAPI-UIを使うためのライブラリ。
これがないとOpenAPI-UIのページが作られないです。

<dependency>
    <groupId>org.microprofile-ext.openapi-ext</groupId>
    <artifactId>openapi-ui</artifactId>
    <version>2.1.0</version>
</dependency>

そして

http://localhost:8080/ee10-01-openapi/api/openapi-ui/index.html

みたいな感じで、アプリのルートから「openapi-ui/index.html」へアクセスをするとOpenAPI-UIの画面が表示されます。

また、以下にアクセスをすると OpenAPIの定義(こちらはPayaraの実装が作成しているもの)を確認することができます。
なので、自分が意図した画面表示ができていないときは、こちらを確認して どういう定義になれば良いのか確認をしながら作業を進めることになります。

[http://localhost:8080/openapi]

Responseの構造体

まずはResponseの構造体から。
共通の構造体をラップしたものをレスポンスするようにしたいと思います。
フロントでHTTPステータスで判定をするのも良いですが、正常異常をif文で分けたいとか、そういう付加情報を設けたいケースを想定したものです。
もちろん、共通クラスをextendするやり方でも良いと思います。

@Schema
public class BaseResponseBody {
  @Schema(title = "レスポンス成否", description = "正常の場合はtrue", readOnly = true)
  boolean ok;
@Schema
public class ResponseBody<T> extends BaseResponseBody {

  @Schema(title = "レスポンスボディ", description = "レスポンス毎の独自の型のオブジェクト", readOnly = true)
  private T body;

(getter setter 他は省略)

リスト構造の場合はこちらを使います。

@Schema
public class ResponseListBody<T> extends BaseResponseBody {

  @Schema(title = "レスポンスボディ", description = "レスポンス毎の独自の型のオブジェクト", readOnly = true)
  private List<T> body;

  public ResponseListBody() {}

(getter setter 他は省略)

これを付与するためのFactoryを使ってResponseをつくるという流れです。
ちなみに GenericEntityを使っていますが、多分使わなくても結果は同じじゃないかな?と思います*3

public static <T> Response success(T body) {
  var entity = new ResponseBody<T>(true, body);
  var responseBuilder = Response.ok(new GenericEntity<ResponseBody<T>>(entity) {});
  return responseBuilder.build();
}

GET

シンプルなパスアクセス

一番シンプルな記載はこんな感じです。
ここでポイントになるのはレスポンスの型の指定とその実装です。

static class UserResponseBody extends ResponseBody<UserResponse> {}

という感じで、OpenAPI用に型クラスを作って、それを指定します。
勿論、スキーマを個別に定義する方法もありますが、OASModelReader に記載する必要があるのと文字列での指定になるのでクラス記述で対応できるものは極力そちらで対応をしておくのが良いと思います。

@ExampleObject ですが、externalValue にクラスパスを指定しても、そこにあるJSONファイルを読み込んではくれませんでした*4
なのでコツコツとベタで書き込むことになります*5

  @GET
  @Path("{id}")
  @Produces(MediaType.APPLICATION_JSON)
  @Operation(summary = "ユーザー情報を検索します")
  @APIResponse(
      content = {
        @Content(
            mediaType = MediaType.APPLICATION_JSON,
            schema = @Schema(implementation = UserResponse.UserResponseBody.class),
            examples = {
              @ExampleObject(
                  name = "default",
                  value =
                      "{\n"
                          + "  \"body\": {\n"
                          + "    \"gender\": \"OTHER\",\n"
                          + "    \"name\": \"Name:example\",\n"
                          + "    \"id\": \"100\"\n"
                          + "  },\n"
                          + "  \"ok\": true\n"
                          + "}")
            })
      },
      responseCode = "200")
  @Parameters({@Parameter(name = "id", description = "ユーザーID")})
  public Response getUserById(@PathParam("id") String id) {

レスポンスの定義もちゃんと出ていますね。

1点、解決が出来なかったことがあります。
リクエストとレスポンスを継承で実装したのですが、おそらくその影響でEnum部分の要素が重複して登録されてしまいました。
まぁ、、レスポンスは受け手なので実害は少ないということで、ここは目をつぶることにしました。

クエリを使った問い合わせ

クエリを使った場合は複数の結果を返却することもあるので配列が戻り値の型になります。
定義のコード実装はこんな感じです。

  static class UserResponseListBody extends BaseResponseBody {
    @SuppressFBWarnings("UUF_UNUSED_FIELD")
    private List<UserResponse> body;
  }

本来、ResponseListBodyのプロパティを記述しなくて良いのですが、OpenAPIで定義する型情報としては、プロパティを上書きするようなコードを書くことになります。

感覚的には以下なのですが、これだとGenericが List入れ子になっていて出力されたOpenAPIの定義にクラス型のスキーマが設定されません*6

static class UserResponseListNgBody extends ResponseListBody<UserResponse> {}

いずれにしても、このインナークラスは「OpenAPIのスキーマを出力するためのクラス」なので、こうやったらできるよ くらいの気持ちで割り切った方が良いと思います*7

上記に加えて、複数の@ExampleObject を使う実装例も示します。
例えば、exampleの例示として2パターンを示したいことがあるかもしれません。
nameをキーとして定義したパターンの表示を切り替えることができます。
Enumを使った選択肢の定義もできます。
こうすることで定数型をつかった定義ができるので利用側も宣言的な実装が可能になります*8

  @GET
  @Produces(MediaType.APPLICATION_JSON)
  @Operation(
      summary = "ユーザー情報を検索します",
      operationId = "getUsersByQuery",
      description = "クエリーで指定した条件を絞り込み条件として使用します。<br>" + "条件を指定しない場合は全レコードが取得対象となります")
  @APIResponse(
      content = {
        @Content(
            mediaType = MediaType.APPLICATION_JSON,
            schema = @Schema(implementation = UserResponse.UserResponseListBody.class),
            examples = {
              @ExampleObject(
                  name = "default",
                  value =
                      ""
                          + "{\n"
                          + "  \"body\": [\n"
                          + "    {\n"
                          + "      \"gender\": \"OTHER\",\n"
                          + "      \"name\": \"Name:example\",\n"
                          + "      \"id\": \"100\"\n"
                          + "    }\n"
                          + "  ],\n"
                          + "  \"ok\": true\n"
                          + "}"),
              @ExampleObject(
                  name = "return 2 record",
                  value =
                      ""
                          + "{\n"
                          + "  \"body\": [\n"
                          + "    {\n"
                          + "      \"gender\": \"MALE\",\n"
                          + "      \"name\": \"Name:user name1\",\n"
                          + "      \"id\": \"1\"\n"
                          + "    },\n"
                          + "    {\n"
                          + "      \"gender\": \"OTHER\",\n"
                          + "      \"name\": \"Name:user name2\",\n"
                          + "      \"id\": \"2\"\n"
                          + "    }\n"
                          + "  ],\n"
                          + "  \"ok\": true\n"
                          + "}")
            })
      },
      responseCode = "200")
  @Parameters({
    @Parameter(
        name = "gender",
        description = "性別",
        schema =
            @Schema(
                enumeration = {"MALE", "FEMALE", "OTHER"},
                implementation = String.class)),
    @Parameter(name = "name", description = "ユーザー名", example = "user name")
  })
  public Response getUsersByQuery(
      @QueryParam("gender") Gender gender, @QueryParam("name") String name) {

ちょっと分かりにくいですが、配列になっていますね([ ] がありますね)。

クエリをつかった問い合わせ(@BeanParam)

クエリで1つ1つの変数をメソッドの引数として定義するよりも、@BeanParamで1つにまとめて使いやすくしたいものです。
できれば BeanParam先のクラスでパラメータの設定ができるとOpenAPIの記述としても共通化できるので良かったのですが、残念ながら @Parametersがメソッドにしか適用できないので冗長ではありますが、メソッド単位で同じことを記述する必要があります。

基本はベタでアノテーションで定義するのと違いはありません。
@BeanParamの要素をすべて記述するだけです。
ポイントは「in = ParameterIn.QUERY」というように、@BeanParamアノテーションで指定したフィールドに付与したアノテーション(例えば @QueryParam)と合った定義を追記するところです*9

  @GET
  @Path("beamparam")
  @Produces(MediaType.APPLICATION_JSON)
  @Operation(
      summary = "ユーザー情報を検索します(BeanParamを使用)",
      description =
          "クエリーで指定した条件を絞り込み条件として使用します。<br>"
              + "条件を指定しない場合は全レコードが取得対象となります。<br>"
              + "*BeanParamによるクエリー指定をするとOpenAPIでは指定ができません。<br>")
  @APIResponse(
      content = {
        @Content(
            mediaType = MediaType.APPLICATION_JSON,
            schema = @Schema(implementation = UserResponse.UserResponseListBody.class),
            examples = {
              @ExampleObject(
                  name = "default",
                  value =
                      ""
                          + "{\n"
                          + "  \"body\": [\n"
                          + "    {\n"
                          + "      \"gender\": \"OTHER\",\n"
                          + "      \"name\": \"Name:example\",\n"
                          + "      \"id\": \"100\"\n"
                          + "    }\n"
                          + "  ],\n"
                          + "  \"ok\": true\n"
                          + "}")
            })
      },
      responseCode = "200")
  @Parameters({
    @Parameter(
        name = "gender",
        in = ParameterIn.QUERY,
        description = "性別",
        schema = @Schema(ref = "#/components/schemas/Gender")),
    @Parameter(
        name = "name",
        in = ParameterIn.QUERY,
        description = "ユーザー名",
        schema = @Schema(example = "User Name", implementation = String.class))
  })
  public Response getUsersByBeanParam(@BeanParam UserQueryParam userQueryParam) {

@BeanParamJAX-RSの実装だけでOpen-APIアノテーションはありません。

@lombok.Data
public class UserQueryParam {
  @QueryParam("gender")
  private Gender gender;

  @QueryParam("name")
  private String name;
}

POST

ポイントはリクエストボディの @ExampleObject を定義するところです。
これが OpenAPI-UIのリクエストボディの雛形になります。
例えば、複数パターンを準備したい時は複数記述をすると良いでしょう。

  @POST
  @Consumes(MediaType.APPLICATION_JSON)
  @Produces(MediaType.APPLICATION_JSON)
  @Operation(summary = "ユーザー情報を登録します")
  @APIResponse(
      content =
          @Content(
              mediaType = MediaType.APPLICATION_JSON,
              schema = @Schema(implementation = UserResourceId.UserResponseIdBody.class),
              examples = {
                @ExampleObject(
                    name = "default",
                    value =
                        ""
                            + "{\n"
                            + "  \"body\": {\n"
                            + "    \"id\": \"57d1a3b9-bb09-42f4-9913-941de0a7d4cb\"\n"
                            + "  },\n"
                            + "  \"ok\": true\n"
                            + "}")
              }),
      responseCode = "201")
  @RequestBody(
      content =
          @Content(
              mediaType = MediaType.APPLICATION_JSON,
              examples = {
                @ExampleObject(
                    name = "default",
                    value =
                        ""
                            + "{\n"
                            + "    \"gender\": \"MALE\",\n"
                            + "    \"name\": \"Name:name-1\"\n"
                            + "}")
              }))
  public Response postUser(UserRequest userRequest) {

リクエストボディのクラスに OpenAPIの定義を記述しています。
こちらのEnum(3つの要素)は、拡張して作成したレスポンスクラスの結果と違って、定義通り、3つです。 とりあえず、クライアントとしては仕様として妥当なものが提示されているということで及第点かな?と。

@lombok.Data
@lombok.NoArgsConstructor
@Schema
public class UserRequest {

  @Schema(
      title = "性別",
      example = "OTHER",
      enumeration = {"MALE", "FEMALE", "OTHER"},
      required = true)
  private String gender;

  @Schema(title = "ユーザー名", example = "User Name", required = true)
  private String name;

  User toModel() {
    return this.toModel(null);
  }

  User toModel(String userId) {
    var model =
        User.builder()
            .userId(Objects.isNull(userId) ? null : UserId.of(userId))
            .gender(Gender.valueOf(this.gender))
            .name(Text.of(name))
            .build();
    return model;
  }
}

PUT

特にいうことは無く…

 @PUT
  @Path("{id}")
  @Consumes(MediaType.APPLICATION_JSON)
  @Operation(summary = "ユーザー情報を更新します")
  @APIResponse(responseCode = "204")
  @Parameters({
    @Parameter(
        name = "id",
        description = "ユーザーID",
        example = "57d1a3b9-bb09-42f4-9913-941de0a7d4cb")
  })
  @RequestBody(
      content =
          @Content(
              mediaType = MediaType.APPLICATION_JSON,
              examples = {
                @ExampleObject(
                    name = "default",
                    value =
                        ""
                            + "{\n"
                            + "    \"gender\": \"MALE\",\n"
                            + "    \"name\": \"Name:name-1\"\n"
                            + "}")
              }))
  public void putUser(@PathParam("id") String id, UserRequest userRequest) {

DELETE

こちらも特筆することはなく…

  @DELETE
  @Path("{id}")
  @Consumes(MediaType.APPLICATION_JSON)
  @Operation(summary = "ユーザー情報を削除します")
  @APIResponse(responseCode = "204")
  @Parameters({
    @Parameter(
        name = "id",
        description = "ユーザーID",
        example = "57d1a3b9-bb09-42f4-9913-941de0a7d4cb")
  })
  public void deleteUser(@PathParam("id") String id) {

ファイルアップロード

WebAPIとしてファイルをアップロードをするところもやっておきたいところです。

単一ファイル

例示のようにアノテーションで定義もできますが、アップロードする場合は常に同じ記述をするのでコードでスキーマ定義を作成して利用するというやり方もできます。

  @POST
  @Path("{id}/file")
  @Consumes(MediaType.MULTIPART_FORM_DATA)
  @Produces(MediaType.APPLICATION_JSON)
  @Operation(summary = "ユーザーに関連するファイルをアップロードします")
  @APIResponse(
      content = {
        @Content(
            mediaType = MediaType.APPLICATION_JSON,
            schema = @Schema(implementation = BaseResponseBody.class),
            examples = @ExampleObject(name = "default", value = "{\"ok\": true}"))
      },
      responseCode = "200")
  @Parameters(@Parameter(name = "id", description = "ユーザーID"))
  @RequestBody(
      description = "アップロードファイルを選択してください",
      content =
          @Content(
              mediaType = MediaType.MULTIPART_FORM_DATA,
              schema =
                  @Schema(
                      type = SchemaType.OBJECT,
                      properties = {
                        @SchemaProperty(name = "file", type = SchemaType.STRING, format = "binary"),
                      })))
  public Response postUploadUserFile(@PathParam("id") String id, EntityPart file) {

スキーマの定義版

  @POST
  @Path("{id}/file")
  @Consumes(MediaType.MULTIPART_FORM_DATA)
  @Produces(MediaType.APPLICATION_JSON)
  @Operation(summary = "ユーザーに関連するファイルをアップロードします")
  @APIResponse(
      content = {
        @Content(
            mediaType = MediaType.APPLICATION_JSON,
            schema = @Schema(implementation = BaseResponseBody.class),
            examples = @ExampleObject(name = "default", value = "{\"ok\": true}"))
      },
      responseCode = "200")
  @Parameters(@Parameter(name = "id", description = "ユーザーID"))
  @RequestBody(
      name = "file",
      description = "アップロードファイルを選択してください",
      content =
          @Content(
              mediaType = MediaType.MULTIPART_FORM_DATA,
              schema = @Schema(ref = "#/components/schemas/UploadFile")))
  public Response postCustomUploadUserFile(@PathParam("id") String id, EntityPart file) {

スキーマを定義して読み込む

定義の読み込みは OASModelReader の実装で行います。

まずはスキーマを生成するコード。
他のプロジェクトでも同じことをするのでUtilにしています。

  /**
   * 複数ファイルのアップロードを定義したスキーマを作成します.
   *
   * @param propertyName スキーマのプロパティ名
   * @return 複数ファイルのアップロードするスキーマ
   */
  public static Schema createUploadFileSchema(String propertyName) {

    return OASFactory.createSchema()
        .description("アップロードを指定するためのスキーマです.")
        .type(Schema.SchemaType.OBJECT)
        .properties(
            Map.of(
                propertyName,
                OASFactory.createSchema().type(Schema.SchemaType.STRING).format("binary")));
  }

これを OASModelReader.buildModel() の中で定義します。

var components =
    OASFactory.createComponents()
        .addSecurityScheme("access_token", securityScheme)
        .addSchema("Gender", OpenApiSchemaUtil.createEnumSchema(Gender.class))
        .addSchema("Genders", OpenApiSchemaUtil.createEnumListSchema(Gender.class))
        .addSchema("UploadFile", OpenApiSchemaUtil.createUploadFileSchema())
        .addSchema("UploadFiles", OpenApiSchemaUtil.createUploadFileListSchema());

こんな感じで独自のスキーマは1つ1つ追加します*10

OpenAPI-UIとしては以下のようにファイルを指定できます。

複数ファイル

アノテーションでオブジェクトのリスト構造を定義することは出来ませんでした。
ここはコードでの生成一択です。

  @POST
  @Path("{id}/files")
  @Consumes(MediaType.MULTIPART_FORM_DATA)
  @Produces(MediaType.APPLICATION_JSON)
  @Operation(summary = "ユーザーに関連するファイルを複数アップロードします")
  @Parameters(@Parameter(name = "id", description = "ユーザーID"))
  @RequestBody(
      name = "files",
      description = "アップロードファイルを選択してください",
      content =
          @Content(
              mediaType = MediaType.MULTIPART_FORM_DATA,
              schema = @Schema(ref = "#/components/schemas/UploadFiles")))
  @APIResponse(
      content = {
        @Content(
            mediaType = MediaType.APPLICATION_JSON,
            schema = @Schema(implementation = BaseResponseBody.class),
            examples = @ExampleObject(name = "default", value = "{\"ok\": true}"))
      },
      responseCode = "200")
  public Response postCustomUploadUserFiles(@PathParam("id") String id, List<EntityPart> files) {

ちなみにネット調べではイイ感じのものが見つからなかったので、OpenAPIの定義を見つつ「こういう感じになったらいいんじゃないかな?」と当て推量込みで試していって見つけた感じです。

  public static Schema createUploadFileListSchema(String propertyName) {

    return OASFactory.createSchema()
        .description("アップロードを複数指定するためのスキーマです.")
        .type(Schema.SchemaType.OBJECT)
        .properties(
            Map.of(
                propertyName,
                OASFactory.createSchema()
                    .type(Schema.SchemaType.ARRAY)
                    .items(
                        OASFactory.createSchema()
                            .type(Schema.SchemaType.STRING)
                            .format("binary"))));
  }

OpenAPI-UIで、複数ファイルを指定できるようになっていますね。

Enumのリストをクエリパラメータ

Enumとかオブジェクトをリストととしてクエリパラメータに定義する場合は、アノテーションでは定義できませんでした。
なので、コードで対応&OASModelReader で設定で対応しました。
Enumname をそのまま使用する場合であればこれで良いと思います。
もしコード値を変換するようなケースがあればインターフェースをEnumにつけて(たとえば getCd みたいな)読み込むようにすれば実現可能です。

  public static Schema createEnumListSchema(Class<? extends Enum<?>> enumClass) {
    var enums = enumClass.getEnumConstants();
    return OASFactory.createSchema()
        .description(enumClass.getSimpleName() + "を複数指定するためのスキーマです.")
        .example(enums[0].name())
        .type(Schema.SchemaType.ARRAY)
        .items(
            OASFactory.createSchema()
                .type(Schema.SchemaType.STRING)
                .enumeration(Stream.of(enums).map(Enum::name).collect(Collectors.toList())));
  }

ちなみにリストではないEnumもついでにつくりました。
これがあれば enumerationを列挙しなくて良くなります*11

  public static Schema createEnumSchema(Class<? extends Enum<?>> enumClass) {
    var enums = enumClass.getEnumConstants();
    return OASFactory.createSchema()
        .description(enumClass.getSimpleName() + "のスキーマです.")
        .example(enums[0].name())
        .type(Schema.SchemaType.STRING)
        .enumeration(Stream.of(enums).map(Enum::name).collect(Collectors.toList()));
  }

こうやってつくったスキーマ定義を refで指定します。

  @GET
  @Path("enumlist")
  @Produces(MediaType.APPLICATION_JSON)
  @Operation(
      summary = "ユーザー情報を検索します",
      description = "クエリーで指定した条件を絞り込み条件として使用します。<br>" + "条件を指定しない場合は全レコードが取得対象となります")
  @APIResponse(
      content = {
        @Content(
            mediaType = MediaType.APPLICATION_JSON,
            schema = @Schema(implementation = UserResponse.UserResponseListBody.class),
            examples = {
              @ExampleObject(
                  name = "default",
                  value =
                      ""
                          + "{\n"
                          + "  \"body\": [\n"
                          + "    {\n"
                          + "      \"gender\": \"OTHER\",\n"
                          + "      \"name\": \"Name:example\",\n"
                          + "      \"id\": \"100\"\n"
                          + "    }\n"
                          + "  ],\n"
                          + "  \"ok\": true\n"
                          + "}"),
              @ExampleObject(
                  name = "return 2 record",
                  value =
                      ""
                          + "{\n"
                          + "  \"body\": [\n"
                          + "    {\n"
                          + "      \"gender\": \"MALE\",\n"
                          + "      \"name\": \"Name:user name1\",\n"
                          + "      \"id\": \"1\"\n"
                          + "    },\n"
                          + "    {\n"
                          + "      \"gender\": \"OTHER\",\n"
                          + "      \"name\": \"Name:user name2\",\n"
                          + "      \"id\": \"2\"\n"
                          + "    }\n"
                          + "  ],\n"
                          + "  \"ok\": true\n"
                          + "}")
            })
      },
      responseCode = "200")
  @Parameters({
    @Parameter(
        name = "genders",
        description = "性別(複数指定)",
        schema = @Schema(ref = "#/components/schemas/Genders")),
    @Parameter(
        name = "gender",
        description = "性別",
        schema = @Schema(ref = "#/components/schemas/Gender")),
    @Parameter(
        name = "name",
        description = "ユーザー名",
        example = "user name",
        schema = @Schema(implementation = String.class))
  })
  public Response getUsersByQueryWithListEnum(

「性別(複数指定)」の選択肢を ctrlを押しながら選択したら複数のEnum属性値を検索条件として指定できます。

Exception(2xx系以外のステータス)

やり方は色々とあると思います。

  • メソッドに1つ1つ記述する
  • OASModelReaderを使って全メソッドにつける
  • その説明用のURLを作る(今回はコレ)

一般的には上2つのいずれかだと思うのですが

  • 4xx、5xx は多くの場合、ほぼ同じ事書くだけ
  • メソッドのレスポンスとして想定できるHTTPステータスだけを書くやり方もあるけど、そうすると結局 5xxなどのシステム基盤として定義したものの扱いをどうする?というのは残ってしまう
  • 想定できる(設計する)以外のパターンの有無はサービスより深い所次第で変わってくる。全部をController単位で把握できるものでも実際のところないのでは?

と考えている中で、次に「利用側として何が欲しい?」と考えたときに

  • 4xx、5xxは共通してフロントとしてもハンドリングすることが多い
  • 共通部品で対応をするときに接続テストをしやすいものがあると嬉しい

というように考え至り、ExceptionControllerを作るという設計に落ち着きました。

全量は長いので一部抜粋です。

@Path("/openapi-http-ng-status")
@Tag(ref = "ExceptionController")
@EntryPoint
@SuppressWarnings("checkstyle:MissingJavadocMethod")
public class ExceptionController {

  @SuppressWarnings("resource")
  @GET
  @Path("{httpStatus}")
  @Produces(MediaType.APPLICATION_JSON)
  @Operation(
      summary = "HttpのNGステータスのレスポンス仕様",
      operationId = "getHttpNgStatus",
      description =
          ""
              + "多くのケースでは2xx以外のステータスはExceptionHandlerなどで一括して操作を行います.<br>\n"
              + "各API毎に4xxのステータス仕様を記載したり、共通して出力するような実装を追加することもできますが<br>\n"
              + "冗長な記載を減らすため本URLに4xxおよび5xxのAPI仕様は本URLへ集約します.\n"
              + "クライアントにて4xx,5xxのHttpステータス毎の制御確認での利用も想定しています.")
  @APIResponse(
      content = {
        @Content(
            mediaType = MediaType.APPLICATION_JSON,
            schema = @Schema(implementation = ErrorResponse.ErrorResponseBody.class),
            examples = {
              @ExampleObject(
                  name = "default",
                  value =
                      "{\n"
                          + "  \"ok\": false,\n"
                          + "  \"body\": {\n"
                          + "    \"errors\": [\n"
                          + "      {\n"
                          + "        \"message\": \"構文が無効です\",\n"
                          + "        \"messageCode\": \"HTTP_STATUS_BAD_REQUEST\"\n"
                          + "      }\n"
                          + "    ]\n"
                          + "  }\n"
                          + "}")
            })
      },
      responseCode = "400")

Code

experimentation/ee10-01-openapi at openapi · vermeerlab/experimentation · GitHub

ルートのプロジェクトで

./mvnw package

をして、全プロジェクトをビルドしてから

ee10-01-openapi プロジェクトで

./mvnw cargo:run -Ppayara

をしてから

http://localhost:8081/ee10-01-openapi/api/openapi-ui/index.html

参考

こちらにOpenAPIのメモは列挙

vermeer.hatenablog.jp

さいごに

Quarkus用のOpenAPI-UIライブラリだったり、Springだったらspringdoc-openapi だったりだと色々と「やってみて思ったこと」をライブラリが解消してくれます。
標準仕様の範疇だとその辺りは自分で作り込まないといけないので「あっちだとできるみたいなんだけどなぁ」とモヤモヤすることが多かったです。
とはいえ、実現方法が分かってくると面倒なことは面倒ですがJavaDocを書いている代わりだと割り切ってからは少し肩の力が抜けた気がします。 とにかくコツコツ試してみてやってみたいことは一応実現するところまでたどり着けたのは良かったです。 それにしてもEE系でOpenAPIの記事が本当に少ないなぁと思いました。
「ちょっとやってみた」はあるのですが、実用レベルのものが少ないというか…
個人的にはWebAPIのテスト用IFとしてOpenAPI-UIは使いやすいので、もうちょっと普及していても良さそうに思っているのですが、テスト系は請負開発だとエクセルへのコピペで納品物を作る系のWF開発としては余計な作業&結果やったことのある人が少ない、みたいな感じなんでしょうかね?*12

*1:これを見つけられたのが、今回のアレコレやってみて個人的には一番の収穫

*2:springdoc-openapi はやってくれた

*3:おまじない(?)みたいな感じで使っています

*4:少なくともPayaraでは。このあたりはQuarkusだと出来たりするみたいなので実装依存なようです

*5:この辺りはちょっと自前で頑張っても良いかもしれないと思いますが、まずは オレオレFW的なことをしないベタなやり方で出来ることを整理します

*6:これもまた、色々やってみて良かったものの1つ

*7:ゆえにライブラリのアップデートで不要になるかもしれないし、ならないかもしれない。ベストプラクティスというよりも「なんとか見つけたTIPS」です

*8:ここでは、あえてベタで定義する書き方の例としてスキーマを作成して設定するというやり方をしていません

*9:はじめ「in」属性は何に使うのか分かっていなかったのですが、べた書きで生成したものと見比べて「in」属性が足りていないことが分かって対応方法が分かりました。これが見つかって本当に良かった

*10:逆にいうと追加しないといけないのが、ちょっと残念とも言えます

*11:#/components/schemas/ を文字列で書かないといけないというのは残ってしまいますが、まぁそれでも共通化できるだけマシということで

*12:ユニットテストも作らないのと似ている理由。納品物を作る工数として積みにくい

JAX-RS(Jakarta RESTful Web Services)のメモ

JAX-RS入門および実践

Jakarta EE 10 - Jakarta RESTful Web Services 3.1 変更内容まとめ - A Memorandum

JAX-RS(Jakarta RESTful Web Services) 3.1.0で、Contextアノテーションの代わりにCDIが推奨されるようになっていたという話 - CLOVER🍀

JAX-RSを使って動画ファイルをダウンロードする #JAX-RS - Qiita

JAX-RSによるExcel/CSV/PDFファイルダウンロード #Java - Qiita

JAX-RSでファイルダウンロードし、終了したらファイル削除 | GWT Center

JAX-RSで複数ファイルをアップロードするには | KATSUMI KOKUZAWA'S BLOG

JAX-RSを利用して大量データを効率的に配信する方法 - エンタープライズギークス (Enterprise Geeks)

JJUGナイトセミナー Jakarta EE特集

はじめに

こちらに参加してきました

speakerdeck.com

メモ

さっととったメモというか感想みたいなものを放流

豆知識

Spring Bootの成果物をOpen Libertyの上で実行すると高いスループットを出すこともあるよ。

Jakarta EE 11

Java SE 17が仕様の前提。

Jakarta Data

Repositoryの戻り値がJPAのEntityになるみたい?
となると3層+ドメインだと、Repositoryからインフラ層で、その中で更にRepositoryという名前のインターフェースでデータ取得をするっていう、名前付け的な意味で結構わかりにくくなりそう。
というか、サービスから直接 Jakarta DataのRepositoryを呼ぶイメージなんだろうなって思った。
理由は、以前、JJUG CCCだったと思うんだけど、MSのJavaの方が「EEでDDD」的なセッションでは、JPA(のEntity)をドメインオブジェクトの中核として扱っていたので。
この辺りは流派が違うというか、EEを軸にしている人は「EEパッケージの依存が入ることは問題ない」という流派なんだと思う。
僕はドメインオブジェクトは可能な限りPOJOでEEパッケージの依存も入れたくない流派 。 抽象化をどこにどういう感じで仕込むのか、みたいなのは好みが分かれるよなぁなどと思う。

メジャーとマイナーのバージョンアップの違い

互換性が無くなったらメジャー、、なんだけどPersistanceは非推奨削除だけどマイナーバージョンアップだったり、必ずしも統一的ではないみたい。

TCKの修正もバージョンアップの対象

仕様の策定も含めてEEだから、TCKの修正もその対象になるというのは確かにだけど気が付かなかった発見。

EE11で変更が一番多かったのはJPA

日付の型はLocalDate系以外は非推奨。

javaxを単純にjakartaに変換するのはNG

Java SE の javaxは置換するとNG。
Eclipse Transformerが有力。
バイトコードも変換対象にする。
ソースコードが入手できない場合でも対応ができるということ。

クラスタリングのベースの遷移

昔はミドルウェアによる実現であった。
今はK8sのようにインフラ自体がその主体になっているというのが違い。 EEの思想はJVM自体がクラスタリングの主体だったところが、もっと広いところで実現するようになったかんじかな?
このあたりは、むかーし Glassfish勉強会で 寺田さんがK8sだったかな?を扱った話をしていた時にも思ったところと同じかな?

Glassfishを扱わなくなったのでOracleにはEEの参照実装がない

そうか、、そういうことになるのか、、

WebSphereLibertyは全部カバー

JavaEEもJakartaEEとMicroprofile全部乗せ。

Spring Boot3からjakartaパッケージ

同梱のTomcatに引っ張られる感じ。

EEの良くないところ

アプリケーションサーバーとwarのデプロイがいるということ。
Spring Bootとの違いともいえる。
確かになぁ…
実際、改めてやってみると管理コンソールでDB接続情報の設定とか、Tomcatで同梱みたいなのと比べると敷居は高い。
情報量も年々少なくなってきているし…

OpenLibertyはBootに近いのかな?

導入可能zipに加えて、実行可能jarも作成できる。
ただ、実行可能jarはログとか異常終了したときの振る舞いとか設定が色々必要なので実際はちょっと面倒とのこと。
なのでおすすめは導入可能zipをunzipして実行みたい。

OpenLibertyを使ってみようかな?

と、2年前も思って、そのまま今(Netbeans + Payara)に至る…
あくまで素振りな実装をしているだけだから、それで事足りてしまっているんだよなぁ…
クラウド利用を真剣に(?)考えるんだったら、OpenLibertyは真剣に導入を考えたいと思いつつ(そして月日が過ぎていく)。

EJBが非推奨はどうなりそう?

今のところはCDIを推奨で、EJBは消えはしないけど放置したままにするという扱いというのが現時点。
なので「非推奨」にもなっていない、ただし放置。

セキュリティマネージャー

脆弱性につながっているケースが少なからずあるみたい?
(このあたりは良く分からない)

VSCodeJavaの開発はどのくらいできるもの?

GUIによる設定とかを期待すると弱いとは思うけど、開発するためのIDEとしては随分と使えるようになっているみたい。
3年くらい前に VSCode + Payara をやろうとして、ちょっと躓いて、即 Netbeansに戻った情弱だけれど、OpenLibertyの仕組みとか聞いているといけそうな気もしてくる。
理由:VSCode + Payara で躓いた理由は、IDEとEEサーバの連携するところがイマイチ イメージできなくて、何をしたら良いのか調べるのも億劫になって考えるのを止めたというのが経緯だったと思うから(目の前に動かせるNetbeansがあるから…)

OpenLibertyは逆にアプケーションサーバーへのデプロイをする、というのとは違うアプローチなので、そういう意味だと Bootに近くて VSCodeでやりやすい印象をもったというのが感想。

さいごに

見知らぬ人との会話の訓練を、、と考えて懇親会にも参加しました。
人数が少ないこともあって、探り探りではありましたが、色々と話を伺う事ができて楽しい時間でした。
前々回は参加したけど懇親会は顔出しだけして気分すぐれず即撤退、前回のJJUG CCCは不参加、という感じで人と交わることに強いストレスを感じることが多くなってしまっていたけれど、今回の懇親会は良いリハビリ(?)になったように思います。

Interfaceのdefaultで多重継承(ファーストクラスコレクション編)

はじめに

コードの全量のリンクをこちらの記事に書いているので、先読みで全量を見たい方はこちらを参照してください。

vermeer.hatenablog.jp

こちらでファーストクラスコレクションを扱う実装例を考えてみました。

ファーストクラスコレクション - システム開発で思うところ

少し工夫をすればInterfaceのdefaultで実装をするパターンが作れそうだと思ってやってみました。

何が嬉しいの?

ファーストクラスコレクションを統一して扱う操作の定義をしておくと類似実装をする時のブレがなくなります。
副作用の伴う操作は独立したInterfaceにすることで、対象のファーストクラスコレクションが副作用を扱うか、扱わないかということを宣言を調べることで判別できるようになります。

基底の型

リストの基本情報の取得と、変換用の操作を持たせました。
getValues()で内部リストを公開しているので、それを使ってしまえば何でも出来てしまうのですが…。
アクセッサを非公開にすることも考えたのですが、それを実現するためにはファーストクラスコレクションを抽象クラスにすれば実現できそうではあったのですが、それをすると「副作用を持たないファーストクラスコレクション」をつくることができないため断念しました。
なので、アクセッサの利用抑止をさせたい場合は ArchUnitを使えば…と思ったりします*1

public interface FirstClassCollectionType<T> {

  /**
   * 保持しているリストを返却します.
   * <p>
   * ファーストクラスコレクション内で使用するようにしてください.
   *
   *
   * @return
   */
  List<T> getValues();

  /**
   * リストが空であることを判定します.
   *
   * @return 空リストの場合はtrue
   */
  default boolean isEmpty() {
    return this.getValues().isEmpty();
  }

  /**
   * 保持しているリストの件数を返却します.
   *
   * @return 保持しているリストの件数
   */
  default int size() {
    return this.getValues().size();
  }

  /**
   * 要素の変換をしたリストを返却します.
   * <p>
   * ドメインオブジェクトからDTOへの変換に使用することを想定したもののため、更にファーストクラスコレクションにしたい場合は 変換後のリストをコンストラクタ(またはFactoryメソッド)の引数として指定してください.
   * </p>
   *
   * @param <R> 変換後のリスト要素の型
   * @param function リストの要素を変換する関数
   * @return 要素を変換したリスト
   */
  default <R> List<R> apply(Function<T, R> function) {
    return this.getValues().stream().map(item -> {
      return function.apply(item);
    }).collect(Collectors.toUnmodifiableList());
  }
}
  • テスト
public class FirstClassCollectionTypeTest {

  @Test
  public void testGetValues() {

    var fstClassCollection = FirstClassCollectionTypeImpl.of(List.of(new Item("1"), new Item("2")));
    List<Item> items = fstClassCollection.getValues();

    assertEquals(new Item("1"), items.get(0));
    assertEquals(new Item("2"), items.get(1));
    assertEquals(2, items.size());

  }

  @Test
  public void testIsEmpty() {
    var fstClassCollection = FirstClassCollectionTypeImpl.of(null);
    assertTrue(fstClassCollection.isEmpty());
  }

  @Test
  public void testSize() {
    var fstClassCollection = FirstClassCollectionTypeImpl.of(List.of(new Item("1"), new Item("2")));
    assertEquals(2, fstClassCollection.size());
  }

  @Test
  public void testApply() {
    var fstClass = FirstClassCollectionTypeImpl.of(List.of(new Item("1"), new Item("2")));
    List<Item2> convertedItemList = fstClass.apply(item -> new Item2(item.getValue()));
    assertEquals(2, convertedItemList.size());
    assertEquals("1", convertedItemList.get(0).getValue());
    assertEquals("2", convertedItemList.get(1).getValue());
  }

  static class FirstClassCollectionTypeImpl implements FirstClassCollectionType<Item> {

    private final List<Item> values;

    private FirstClassCollectionTypeImpl(List<Item> values) {
      this.values = List.copyOf(values);
    }

    static FirstClassCollectionTypeImpl of(List<Item> values) {
      List<Item> items = Objects.nonNull(values) ? List.copyOf(values) : Collections.emptyList();
      return new FirstClassCollectionTypeImpl(items);
    }

    @Override
    public List<Item> getValues() {
      return this.values;
    }
  }

副作用

リストの要素に対して副作用を伴う操作をしたい場合はこちらを使用します。
こちらもgetValues()使えば良いじゃないというのは、ごもっともなんですが、こちらを使えば「副作用に使用されている」ということが宣言的に調査がしやすくなるので、こちらを使って実装をしてもらいたいところです。

public interface FirstClassCollectionConsumer<T> extends FirstClassCollectionType<T> {

  /**
   * リスト要素毎に副作用の伴う操作を行います.
   *
   * @param consumer 副作用を行う関数
   */
  default void accept(Consumer<T> consumer) {
    this.getValues().forEach(item -> {
      consumer.accept(item);
    });
  }

}
  • テスト
public class FirstClassCollectionConsumerTest {

  @Test
  public void testAccept() {
    var fstClass = FirstClassCollectionConsumerImpl.of(List.of(new Item("1"), new Item("2")));

    List<Item> items = new ArrayList<>();

    // 本来は標準出力などの副作用のあるロジックの実行を想定
    fstClass.accept(item -> items.add(item));
    assertEquals(2, items.size());
    assertEquals("1", items.get(0).getValue());
    assertEquals("2", items.get(1).getValue());

  }

  public static class FirstClassCollectionConsumerImpl implements FirstClassCollectionConsumer<Item> {

    private List<Item> values;

    private FirstClassCollectionConsumerImpl(List<Item> values) {
      this.values = values;
    }

    static FirstClassCollectionConsumerImpl of(List<Item> values) {
      List<Item> items = Objects.nonNull(values) ? List.copyOf(values) : Collections.emptyList();
      return new FirstClassCollectionConsumerImpl(items);
    }

    @Override
    public List<Item> getValues() {
      return this.values;
    }
  }

さいごに

filterなど「新たなファーストクラスコレクションを生成する」というパターンもやろうと思って

Interfaceのdefaultで多重継承(下準備編) - システム開発で思うところ

で作った「InstanceCreator」を使う事を考えたのですが、リフレクションをベースにせず関数をベースにした生成をした方が黒魔術感の無い実装になるのでは?と思い直して一旦この段階で止めました。
気が向いたら、過去のものも含めて、ちょっと考え直してみようかな?と思っています。

*1:あとはInterfaceのメソッドがprotectedが使えれば良いのですが無いものは仕方ないので…