Upgrade to PRO for Only $50/Year—Limited-Time Offer! 🔥

Spring Boot vs MicroProfile ~クラウドネイティブにおけるフレームワ...

荻原利雄
June 16, 2024
1.6k

Spring Boot vs MicroProfile ~クラウドネイティブにおけるフレームワークの比較と選択~

JJUG CCC 2024 Spring 登壇後記 - Spring Boot vs MicroProfile セッションの補足と訂正
https://developer.mamezou-tech.com/blogs/2024/06/20/after_jjug_spring-mp/
も併せて参考にしていただきたく

荻原利雄

June 16, 2024
Tweet

More Decks by 荻原利雄

Transcript

  1. 2 荻原 利雄(オギワラ トシオ) • 所属 / 職種 - 株式会社豆蔵

    - ビジネスソリューション事業部 - 主幹ソフトウェアエンジニア • プロフィール - オブジェクト指向とともにエンタープライズな Javaアプリを作りつづけて25年のアラフィフエ ンジニア - ここ数年は大規模基幹システムを支える JakartaEEフルスタックなフレームワークの開発 を行っている - 昨年度からはSpring Bootを使った共通機能の開 発も行っている extact-io 豆蔵デベロッパーサイト 豆蔵デベロッパーサイトで連載中! 逆張りのMicroProfile
  2. Agenda 1. MicroProfileとSpring Bootの超概要 2. 比較の題材 3. DI x AOPの比較

    4. RESTサーバーの比較 5. Config機能の比較 6. RestClient機能の比較 7. 結局どっちが向いている? 3
  3. お断り • 本資料はMicroProfile 6.0(+ Jakarta EE 10)をベースに作成しています • Spring Bootと比較しやすいようにMicroProfileの実装は起動形式が同じQuarkusやHelidon

    などアプリケーションサーバが不要なものを前提にしています • QuarkusやHelidonなど各MicroProfile実装はSpringと同等の独自拡張機能を持っていたりも しますが、本資料はMicroProfile標準仕様のみを扱います • MicroProfileを選択したのに独自実装を使うのは個人的に負けた気?がしますが、それぞれ豊富な機能を持っ ています。勝った気になっても工数は減らないので必要に応じて調査されるのもよいかと思います • JakartaEEの各仕様名はJavaEE時代から変更されていますが、表記が長くなるのと 発表者が呼びなれないため、JavaEE時代の旧称を使用しています <例> • Jakarta RESTful Web Services → JAX-RS(Java API for RESTful Web Services) • Jakarta Persistence → JPA(Java Persistence API) • SpringバージョンはSpring Frameworkは6.1系、Spring Bootは3.2系を ベースに作成しています • 今回紹介するRestClientが使えるようになったが6.1系からなので、このバージョン をベースにしていますが、そこそこ新しければ内容理解に問題はありません 4
  4. MicroProfileとは 6 MicroProfile®プロジェクトは、マイクロサービスアー キテクチャ向けにエンタープライズJavaを最適化するこ とを目的とし、JakartaEEかを問わずJavaのエコシステ ムを活用しながらマイクロサービス向けの新しい共通 APIと機能をオープンな環境で短いサイクルで提供して くことを目標にする JakartaEEや既存のエコシステムを活用したそんな クラウドネイティブなアプリ向けのAPIや機能を

    オープンなプロセスで策定し、短期間でリリースし てく <超訳> JakartaEEをベースとしたMSA向けAPIセット つまるところ・・・ 引用:https://microprofile.io/ 引用:https://devm.io/java/jakarta-ee-ten-release MicroProfile 6.0 JakartaEE10 CoreProfile RESTアプリで必要となる基 本セットでJPAやJSFなどは 入っていない <MicroProfileプロジェクトの公式説明>
  5. MicroProfileの仕様体系-MicroProfile固有 7 MicroProfile Spec OpenTelemetry OpenTelemetryに参加させるインテグ レーション機能(Jaeger連携) Metrics ヒープサイズやGC回数などのメトリック スの公開方法(Prometheus連携)

    Fault Tolerance サーキットブレーカやRetry、Timeoutな どのフォールトトレラント機能 Health サービスの死活監視に関するいわゆる ReadinessとLivenessに関する仕様 Config 環境変数やシステムプロパティ、設定 ファイルなど異なる設定源の統合 JWT Auth 標準Claimの定義とJWT認証 OpenAPI ソースからOAS(OpenAPI Specification) ドキュメントを生成 RestClient インタフェースを定義するだけでREST呼 び出しを可能にする 可観測性 回復性 可搬性 可用性 実装容易性 クラウドアプリは複数の要素から構成される(このため) ✓ ボトルネックや障害箇所の特定が困難となるため可観 測性がより重要となる ✓ 部分的障害が発生することを前提に機能を継続させる 回復性が重要となる ✓ またそのためにはシステムの健全性を確認できる必要 がある クラウドアプリはそれ自身の目的からスケール容易である ことが求められる ✓ スケール容易にするためコンテナ化が前提となるため、 コンテナの可搬性が求められる ✓ スケールできるようにステートレスな認証方式が求め られる クラウドアプリではRESTが多用される(このため) ✓ RESTの実装が容易であることが求められる クラウドネイティブに求められる非機能 マイクロサービスアーキテクチャで必要となる 非機能要件に対する対応 赤字:今回とりあげるSpec 今回の中心
  6. まずは用語の整理 • 細かい違いはあるがやってくれることは概ね同じ • SpringのDIコンテナはMicroProfile(JakartaEE)のCDIコンテナ • SpringのSpring BeanはMicroProfile(JakartaEE)のCDI Bean •

    SpringのAOPはMicroProfile(JakartaEE)のInterceptor • Springの@RestControllerはMicroProfile(JakartaEE)のJAX-RSのREST リソース 12 MicroProfile Spring Framework ほぼ同じ DIコンテナ Bean AOP RestController 基本こっちで呼 びます
  7. BookRepositoryをDIコンテナに登録する 13 @Component public class InMemoryBookRepository implements BookRepository { private

    Map<Integer, Book> bookMap; @PostConstruct public void init() { bookMap = new ConcurrentHashMap<>(); bookMap.put(1, new Book(1, “燃えよ剣”, “司馬遼太郎 … } @Override public Optional<Book> get(int id) { return Optional.ofNullable(bookMap.get(id)); } ・・・ } @ApplicationScoped public class InMemoryBookRepository implements BookRepository { private Map<Integer, Book> bookMap; @PostConstruct public void init() { bookMap = new ConcurrentHashMap<>(); bookMap.put(1, new Book(1, "燃えよ剣", "司馬遼太郎")); … } @Override public Optional<Book> get(int id) { return Optional.ofNullable(bookMap.get(id)); } ・・・ } point point Spring Framework MicroProfile(CDI) • 起動時にclasspathから@Componetがスキャンされ自動 で登録 • スコープは@Scope(“prototype”)など別で指定する • スコープのデフォルトはSingleton(=Application) • Springと同様に起動時にclasspathから@ApplicationScopedなど のスコープアノテーションスキャンされ自動で登録 • スコープは@RequestScopedなどSpringとほぼ同じものが用意され ている • ただし、 @Scope(“prototype”)に相当するものはない 脱線ネタ:@PostConstructの挙動は双方で違う ので注意しましょう
  8. BookRepositoryの実装を切り替える 14 // @Componentはつけない public class DatabaseRepository implements BookRepository {

    … @ConditionalOnProperty(name = "use.repository", havingValue = "jpa", matchIfMissing = true) @Component // @Repositoryが正解だけど分かりやすく@Componentにしてます public class DatabaseRepository implements BookRepository { … @ConditionalOnProperty(name = "use.repository", havingValue = “inmemory") @Component public class InMemoryRepository implements BookRepository { … // @Componentはつけない public class InMemoryRepository implements BookRepository { … @Bean BookRepository bookRepository() { return new InMemoryBookRepository(); //return new DatabaseRepository(); } ▪ Springの場合 その1:JavaConfigでプログラムで決定 その2:@ConditionalOnPropertyを使って設定で決定 コンポーネントスキャンさ れないように@Componentア ノテーションをつけないよ うにする JavaConfig(@Configuration クラス)でBean登録するイン スタンスを決定 脱線ネタ:XMLはもう説明不要でいいですよね.. 設定ファイル (application.yml)の設定値に 合致する方の@Componentを有効 にする mitchIfMissingはデフォルトの 設定
  9. BookRepositoryの実装を切り替える 15 ▪ MicroProfileの場合 MicroProfileというよりCDIにはBeanの切り替えをスマートに実現する王道 的な方法は用意されてない(荻原の知る限り) ✓ 機能本来の目的とは異なるが、CDI LiteのBuild compatible

    extensions(もしくはPortable extensions)で実現するの が個人的なベストプラクティス ✓ (どちらのextensionsも独自スコープの追加などCDI自体の機能を拡張するのが本来の目的だと思うので葛藤はある) public class BuildExtension implements BuildCompatibleExtension { // スキャン対象クラスを追加可能にするコールバックメソッド @Discovery public void discoverFrameworkClasses(ScannedClasses scan) { // 設定に登録されているクラスをスキャン対象に追加 Config config = ConfigProvider.getConfig(); String useClass = config.getValue("use.repository", String.class); scan.add(useClass); } // 指定したスキャン対象クラスのBeanメタデータを改変可能にするコールバックメソッド @Enhancement(types = BookRepository.class, withSubtypes = true) public void addInterceptorBindingToProcessors(ClassConfig clazz) { // 対象クラス(設定に登録されているクラス)に動的にアノテーションを追加 clazz.addAnnotation(ApplicationScoped.class); } } // @ApplicationScopedはつけない public class DatabaseRepository … { … // @ApplicationScopedはつけない public class InMemoryRepository … { … <Beanクラスの実装> <Build compatible extensionsの実装> 設定に登録したExtensionがビルド 時(Arc)または起動時(Weld)に実行 される
  10. BookRepositoryにトレースログを仕込む ー AOP/Interceptorの利用 16 @Component @Aspect public class TraceLogInterceptor {

    @Around("@annotation(sample.spring.book.server.advise.TraceLoggable)" + " || within(@sample.spring.book.server.advise.TraceLoggable *)") public Object obj(ProceedingJoinPoint pjp) throws Throwable { String clazz = pjp.getTarget().getClass().getSimpleName(); MethodSignature signature = (MethodSignature) pjp.getSignature(); String method = signature.getName(); log.info("%sメソッドが呼ばれました。".formatted(clazz + "#" + method)); return pjp.proceed(); } } … @TraceLoggable public class DatabaseRepository implements … { … @TraceLoggable public class InMemoryRepository implements … { @Interceptor @Priority(Interceptor.Priority.APPLICATION) @TraceLoggable public class TraceLogInterceptor { @AroundInvoke public Object obj(InvocationContext ic) throws Exception { String clazz = ic.getTarget().getClass().getSimpleName(); String method = ic.getMethod().getName(); LOGGER.info("%sメソッドが呼ばれました。".formatted(clazz + "#" + method) ); return ic.proceed(); } } Weaving Adviceからターゲット要素への依存はない PointCutが文字列定義なので Interceptorとターゲットクラスの双方で同じアノ テーションを付与することで紐づけされる Spring Framework MicroProfile(CDI) @TraceLoggableは利用者が実装するアノテーション
  11. DIxAOPはどっちがいい • Springがよいと思うところ(MicroProfileからみた) • JavaConfigや設定を使ってBeanの切り替えが柔軟かつ簡単にできる • (その引き換えとしてAutoConfigでハマることは多いが・・) • AOPのPointCutはクラス名やメソッド名のマッチングでも定義できるため、い ろいろな戦略が採れる

    • (ネーミングルールを定義して対象に一律にAOPを掛けるなど) • MicroProfileがよいと思うところ(Springからみた) • Bean登録の柔軟性は正直完敗。Springよりいいと思うところはない・・ • (アプリプログラマーにExtensionを使ってもらうのはハードルが高い) • (@Alternativeを使ってbeans.xmlで有効にするBeanを切り替えられるでしょ・・は よく紹介されるが、 jarアーカイブされたbeans.xml定義を外から変更することはで きない。なので実質的に使い物にならない) • Interceptorで出来ることはSpringでいうところのアノテーションによる AroundInvokeのみだが、これはこれでシンプルで使いやすい • (機能不足で困ったことはなく個人的にはSpringのAOPより気にいっている) 17 この勝負はやっぱりSpringが優勢
  12. 20 BookControllerを実装する @RestController @RequestMapping("/books") public class BookController { private BookRepository

    repository; public BookController(BookRepository repository) { this.repository = repository; } @GetMapping("/{id}") public Book get(@PathVariable int id) { return repository.get(id).orElse(null); } @GetMapping("/author") public List<Book> findByAuthorStartingWith( @RequestParam String prefix) { return repository.findByAuthorStartingWith(prefix); } @PostMapping public Book add(@RequestBody Book book) throws DuplicateException { return repository.save(book); } … } @ApplicationScoped @Path("books") public class BookController { private BookRepository repository; @Inject public BookController(BookRepository repository) { this.repository = repository; } @GET @Path("/{id}") @Produces(MediaType.APPLICATION_JSON) public Book get(@PathParam("id") int id) { return repository.get(id).orElse(null); } @GET @Path("/author") @Produces(MediaType.APPLICATION_JSON) public List<Book> findByAuthorStartingWith( @QueryParam("prefix") String prefix) { return repository.findByAuthorStartingWith(prefix); } @POST @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) public Book add(Book book) throws DuplicateException { return repository.save(book); } … } Spring Framework MicroProfile(JAX-RS) • MicroProfileとはアノテーションくらいの違いしかない • パス名やクエリパラメータ名が引数名と同じ場合は省略可 • リクエストをオブジェクトにバインドする場合は @RequestBodyが必要 基本同じ • コンストラクタインジェクションでも@Injectは 必要(JAX-RSではなくCDIのことだが)
  13. 21 例外に応じてレスポンスを返す-例外マッピング @Path("books") @ApplicationScoped public class BookController { // @Producesと@Consumeは省略

    … @POST public Book add(@Valid Book book) throws DuplicateException { … @PUT public Book update(@Valid @ConvertGroup(to = Update.class) Book book) throws NotFoundException { … @DELETE @Path("/{id}") public void delete(@NotNull @PathParam("id") int id) throws NotFoundException { … } 普段はつけませんが分かりように例示のため実行時例外をthrows宣言してます MicroProfileの例ですがSpringも同様になっています Controllerから例外が送出されたら例外に対応するレスポンスを一元的に返したい(ですよね!)
  14. 22 例外に応じてレスポンスを返す-例外マッピング ▪ Springの場合 紐づけ 利用者が作成したアノテーションで紐づけ られたRestControllerから送出された例外 は紐づけられた例外ハンドラで処理される // ハンドルする例外クラスごとにハンドラメソッドを定義する

    @RestControllerAdvice(annotations = ExceptionHandle.class) public class BookExceptionHandler extends ResponseEntityExceptionHandler { // NotFoundExceptionの例外ハンドルメソッド定義(404で応答) @ExceptionHandler(NotFoundException.class) public ResponseEntity<Map<String, String>> handleNotFoundException( NotFoundException e, WebRequest req) { return new ResponseEntity<>(Map.of("message", e.getMessage()), HttpStatus.NOT_FOUND); } // DuplicateExceptionの例外ハンドルメソッド定義(409で応答) @ExceptionHandler(DuplicateException.class) public ResponseEntity<Map<String, String>> handleDuplicateKeyException( DuplicateException e, WebRequest req) { return new ResponseEntity<>(Map.of("message", e.getMessage()), HttpStatus.CONFLICT); } } @RestController @ExceptionHandle @RequestMapping("/books") public class BookController { …
  15. 23 例外に応じてレスポンスを返す-例外マッピング // ハンドルする例外ごとにハンドラクラスを定義 @ConstrainedTo(RuntimeType.SERVER) public class BookExceptionMappers { //

    DuplicateException例外ハンドラ(409で応答) @Produces(MediaType.APPLICATION_JSON) public static class DuplicateExceptionMapper implements ExceptionMapper<DuplicateException> { public Response toResponse(DuplicateException exception) { // 例外をレスポンスに変換 return Response .status(Status.CONFLICT) .entity(Map.of("message", exception.getMessage())) .build(); } } // NotFoundException例外ハンドラ(404で応答) @Produces(MediaType.APPLICATION_JSON) public static class NotFoundExceptionMapper implements ExceptionMapper<NotFoundException> { public Response toResponse(NotFoundException exception) { // 例外をレスポンスに変換 return Response .status(Status.NOT_FOUND) .entity(Map.of("message", exception.getMessage())) .build(); } } … ▪ MicroProfileの場合 @ApplicationPath("") @ApplicationScoped public class BookApplication extends Application { @Override public Set<Class<?>> getClasses() { Set<Class<?>> classes = new LinkedHashSet<>(); classes.add(BookController.class); classes.add(DuplicateExceptionMapper.class); classes.add(NotFoundExceptionMapper.class); classes.add(ConstraintViolationExceptionMapper.class); return classes; } } 登録 Applicationクラスで一緒に紐づけられた RestControllerから送出された例外は登録 された例外ハンドラで処理される
  16. 24 リクエストの入力チェックを行う -BeanValidation統合 Spring Framework MicroProfile(JAX-RS) public class BookController {

    … @GetMapping("/{id}") public Book get(@NotNull @PathVariable int id) {… @GetMapping("/author") public List<Book> findByAuthorStartingWith(@NotBlank @Size(max= 10) … @PostMapping public Book add(@Validated @RequestBody Book book) … { @PutMapping public Book update(@Validated(Update.class) @RequestBody Book book) … public class BookController {// @Producesと@Consumeは省略 … @GET @Path("/{id}") public Book get(@NotNull @PathParam("id") int id) {… @GET @Path("/author") public List<Book> findByAuthorStartingWith(@NotBlank @Size(max= 10) … @POST public Book add(@Valid Book book) throws … {… @PUT public Book update(@Valid @ConvertGroup(to = Update.class) Book book) … {… public class Book { @NotNull(groups = Update.class) private Integer id; @NotBlank @Size(min = 1, max = 20) private String title; @Size(max = 20) private String author; validate • どちらもBeanValidationと統合されチェックアノテーションをつける だけでリクエスト受信時にチェックしてくれる • SpringはMessageSourceResolvableを使ってリッチなメッセージ を構築できるがUIがないRESTでは使いどころはない • エンティティオブジェクトに対してはMicroProfileは@Valid (BeanValidation標準)を使うがSpringでは独自の@Validatedを使う • group指定する場合、MicroProfileは@ConvertGroup(BeanValidation標 準)を使うがSpringでは@Validatedの属性で指定する
  17. 25 JWTを使ってユーザ認証を行う-JWT認証 JWTを発行するIDProvider側ではなくJWTを受信し検証した結果に基づきユーザを認証するリソースサーバー側の話 // 認証方式(JWT)とパスに対する認証認可設定 @Bean SecurityFilterChain securityFilterChain(HttpSecurity http) throws

    Exception { return http // /books配下は誰もでOK、/sercure以下はMEMBERロールのユーザーしか不可 .authorizeHttpRequests(authorize -> authorize .requestMatchers("/books/**").permitAll() .requestMatchers("/secure/**").hasRole("MEMBER") .anyRequest().authenticated()) // JWTで認証を行う .oauth2ResourceServer(oauth2 -> oauth2 .jwt(jwt -> jwt .jwtAuthenticationConverter(jwtAuthenticationConverter()))) // その他もろもろ設定 .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .exceptionHandling(exceptions -> exceptions .authenticationEntryPoint(new BearerTokenAuthenticationEntryPoint()) .accessDeniedHandler(new BearerTokenAccessDeniedHandler())) .csrf(csrf -> csrf.disable()) .logout(logout -> logout.disable()) .requestCache(cache -> cache.disable()) .build(); } // 受信したJWTの検証方法の設定 @Bean JwtDecoder jwtDecoder(@Value("${jwt-validator.public-key}") RSAPublicKey key, @Value("${jwt-validator.claim-issuer}") String issuer) { // ${jwt-validator.public-key}のパスの公開鍵を使う NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withPublicKey(key).build(); // JWTのisserクレーム値が${jwt-validator.claim-issuer}の値と同じかチェックする jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(issuer)); return jwtDecoder; } // 受信したJWTのユーザ情報へのマッピング方法 JwtAuthenticationConverter jwtAuthenticationConverter() { JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter(); // JWTのgroupsクレーム値をユーザの権限名として扱う grantedAuthoritiesConverter.setAuthoritiesClaimName("groups"); grantedAuthoritiesConverter.setAuthorityPrefix("ROLE_"); JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter(); jwtAuthenticationConverter .setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter); return jwtAuthenticationConverter; } ▪ Springの場合 色々柔軟に設定できることを雰囲 気で察してください
  18. 26 JWTを使ってユーザ認証を行う-JWT認証 JWTを発行するIDProvider側ではなくJWTを受信し検証した結果に基づきユーザを認証するリソースサーバー側の話 ▪ MicroProfileの場合 色々柔軟に設定できないことを雰囲気で察してください @LoginConfig(authMethod = "MP-JWT") @ApplicationPath("secure")

    @ApplicationScoped public class BookApplication extends Application { @Override public Set<Class<?>> getClasses() { Set<Class<?>> classes = new LinkedHashSet<>(); classes.add(SecureBookController.class); … return classes; } } mp.jwt.verify.publickey.location=/jwt.pub.key <検証に使う公開鍵の設定> <JWT認証を行う設定プログラム> JWKSエンドポイントを指定することも可 ←① ←② ←③ ① 登録されたクラスはJWT認証を行う ② 登録されたクラスをマッピングするトップレベルの パス。この配下は認証が掛かる ③ 登録するクラス(RestController) • JWTの検証は公開鍵、有効期限、Issuerクレーム Audienceクレームに対してのみで拡張ポイントは ない • SecurityContextにマッピングされる権限はgroups クレームから取得し、これは(標準仕様では)変更 できない
  19. RESTサーバー機能はどっちがいい • Springがよいと思うところ(MicroProfileからみた) • 認証機能が圧倒的に豊富かつ機能性十分 • MicroProfileを選択した場合、実質的にベンダー独自拡張を使うかオレオレ実装 するしかない • MicroProfileがよいと思うところ(Springからみた)

    • クラス設計が綺麗なのでFilterやInterceptorの拡張をしやすい • SpringはViewを返すMPA(Thymeleaf)のリクエストハンドルを行うSpringMVCを土 台に拡張しているので、拡張ポイントがカオス • 加えてリファレンスにもどんな拡張ポイントが用意されているのかも書かれてな いため、ソースコードを追って見つける必要がある(そしてそれが正解かもよく わからないときも・・) • 反対にMicroProfileは標準仕様なだけあり、利用者が拡張可能なことは明記され ており、明記されてないことはハックまがいと判断できる 27 とかあるが機能的にはほぼ同様で引き分け
  20. 30 デフォルトの実装の切り替え -優先度による設定上書き @ConditionalOnProperty(name = "use.repository", havingValue = "jpa", matchIfMissing

    = true) @Component // @Repositoryが正解だけど分かりやすく@Componentにしてます public class DatabaseRepository implements BookRepository { … public class BuildExtension implements BuildCompatibleExtension { @Discovery public void discoverFrameworkClasses(ScannedClasses scan) { // 設定に登録されているクラスをスキャン対象に追加 Config config = ConfigProvider.getConfig(); String useClass = config.getValue("use.repository", String.class); scan.add(useClass); } } 設定はjar内に同梱される。ではBeanの切り替えで出てきた設定をコンテナ化した後に変更するには? →環境変数による上書き! use.repository: inmemory #use.repository=sample.microprofile.book.¥ server.repository.DatabaseBookRepository use.repository=sample.microprofile.book.¥ server.repository.InMemoryBookRepository ▪ MicroProfileの場合 ▪ Springの場合 参照 参照 application.yml microprofile-config.properties $ export USE_REPOSITORY=sample.microprofile.book.server.repository.DatabaseBookRepository $ java -jar target/<jar-name>.jar 環境変数で上書きして実行 export USE_REPOSITORY=jpa $ java -jar target/<jar-name>.jar 環境変数で上書きして実行 環境変数のパターンにマッチする 設定がある場合は設定を上書きし てくれる
  21. 31 設定値を任意の型で取得する-設定値の型変換 バラバラでなくまとめてもオブジェクトにバインドしてくれる app: info: name: book-server create-date: 2024/06/16 company:

    name: mamezou url: https://www.mamezou.com/ app.info.name=book-server app.info.create-date=2024/06/16 app.info.company.name=mamezou app.info.company.url=https://www.mamezou.com ▪ MicroProfileの場合 ▪ Springの場合 application.yml microprofile-config.properties // Configオブジェクトを取得 Config config = ConfigProvider.getConfig(); // 設定値の取得 String name = config.getValue("app.info.name", String.class); String createDate = config.getValue("app.info.create-date", String.class); String company = config.getValue("app.info.company.name", String.class); String url = config.getValue("app.info.company.url", String.class); 取得 public StartupRunner(Environment env) { this.env = env; } @Override public void run(ApplicationArguments args) throws Exception { String name = env.getProperty("app.info.name"); LocalDate createDate = env.getProperty("app.info.create-date", LocalDate.class); String company = env.getProperty("app.info.company.name"); URI url = env.getProperty(“app.info.company.url”, URI.class); … 取得
  22. 32 設定情報は文字列だけでなく、受け取る型に応じて変換もしてくれる app: info: name: book-server create-date: 2024/06/16 company: name:

    mamezou url: https://www.mamezou.com/ app.info.name=book-server app.info.create-date=2024/06/16 app.info.company.name=mamezou app.info.company.url=https://www.mamezou.com ▪ MicroProfileの場合 ▪ Springの場合 application.yml microprofile-config.properties @ApplicationScoped @ToString public class AppInfo { @Inject @ConfigProperty(name = "app.info.name") private String name; @Inject @ConfigProperty(name = "app.info.create-date") private String createDate; @Inject @ConfigProperty(name = "app.info.company.name") private String company; … } @ConfigurationProperties(prefix = "app.info") @Data public class AppInfo { private String name; private String createDate; private CompanyInfo company; @Data static class CompanyInfo { private String name; private URI url; } } バインド バインド クラスのネストもOK クラスのネストはNG 設定情報をまとめて扱う -オブジェクトへのバインド
  23. Config機能はどっちがいい • Springがよいと思うところ(MicroProfileからみた) • 型変換を行うコンバーターが圧倒的に豊富 • ネストしたオブジェクトにも設定値をバインドできる • パスの指定で使えるclasspath:やoption指定はとっても便利 •

    MicroProfileがよいと思うところ(Springからみた) • config_ordinalを設定することで優先度を明示的に指定できる • Springは起点となるapplication.ymlはクラスパス上に1つしか持てない • MicroProfileはクラスパス上のすべての設定を読み込み優先度に応じてマージし てくれる • 非CDI環境で独立して利用することができる • なのでスッピンのJUnitでもConfig機能が使える • staticメソッドの取得かれでもフル機能を使うことができる 33 機能的には違いがあるが甲乙つけがたいので引き分け
  24. 37 BookClientを実装する Spring Framework • RestController側と同じインターフェースを定義する • ただし、@xxxMappingが@xxxExchangeだけは異なる @HttpExchange(url =

    "/books") public interface BookClient { @GetExchange("/{id}") BookDto get(@PathVariable int id); @GetExchange("/author") List<BookDto> findByAuthorStartingWith(@RequestParam("title") String prefix); @PostExchange BookDto add(@RequestBody BookDto book); @PutExchange BookDto update(@RequestBody BookDto book); @DeleteExchange("/{id}") void delete(@PathVariable int id); } @Component public class BookService { private BookClient bookClient; public BookService(BookClient bookClient) { this.bookClient = bookClient; } public BookDto get(int id) { return bookClient.get(id); } … @Bean BookClient bookClient(@Value("${target.url}") String url) { RestClient restClient = RestClient.builder() .baseUrl(url) .build(); RestClientAdapter adapter = RestClientAdapter.create(restClient); HttpServiceProxyFactory factory = HttpServiceProxyFactory .builderFor(adapter) .build(); return factory.createClient(BookClient.class); } RestClientインタフェース Proxy作成 利用
  25. 38 BookClientを実装する MicroProfile • RestController側と同じインターフェースを定義する • サーバー側と全く同じでProxy作成も不要 @RegisterRestClient(configKey = "book-api")

    @Path("books") public interface BookClient { @GET @Path("/{id}") @Produces(MediaType.APPLICATION_JSON) BookDto get(@PathParam("id") int id); @GET @Path("/author") @Produces(MediaType.APPLICATION_JSON) List<BookDto> findByAuthorStartingWith(@QueryParam("prefix") String prefix); @POST @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) BookDto add(BookDto book); @PUT @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) BookDto update(BookDto book); @DELETE @Path("/{id}") void delete(@PathParam("id") int id); } @ApplicationScoped public class BookService { private BookClient bookClient; @Inject public BookService(@RestClient BookClient bookClient) { this.bookClient = bookClient; } public BookDto get(int id) { return bookClient.get(id); } … RestClientインタフェース 利用
  26. どっちもどっち • 見てきた通り機能的にはSpringBootの方が多いが、 MicroProfileでも必要なものは十分そろっている • 起動速度に関してはJavaVM上で動かす場合はどれも同じくらい (早くない) • フットプリントに関してもJavaVM上で動かす場合はそもそもど れも十分大きい(その中でもQuarkusは小さい)

    • よって、クラウドネイティブだったらどれがよいとかはない • SpringBootはうざいこともあるが至れり尽くせりのメーカーパ ソコン的なのが好みの人向き • MicroProfileは自分でコツコツ好みのスタイルを組み上げる BTOや自作パソコンが好みの人向き 41