【アプリ開発】データレイヤにおけるリポジトリ層とデータソース層について

こんにちは。エキサイト株式会社でエンジニアをしている新卒の岡島と申します。 普段業務ではFlutterを用いたアプリ開発を行っています。 今回は、既存APIからリビルド後のAPIへの繋ぎ込みを行った際に、アーキテクチャの重要性を痛感したので、アーキテクチャについて勉強したことを共有していこうと思います。

アーキテクチャの重要性を感じた業務中のケース

アーキテクチャの理解が乏しかったため、リビルド後のAPIへの繋ぎ込みを以下のように行っていました。

繋ぎ込み前
繋ぎ込み後

しかし、コードレビュー時に以下のようにすると良いとの指摘を受けました。

このようにすることで、Repositoryはデータソースの向き先だけ変更するだけで済み、下記のようなメリットがありました。

  • ViewModelが参照するRepositoryの変更が不要
  • 移行後は既存APIのDataSourceを削除するだけで済む

要約すると影響範囲が少なくて済むということです。

今までアーキテクチャはなんとなく責務を分離できるためメンテナンスが容易である程度の理解でしたが、このことをきっかけにアーキテクチャへの理解が進みました。 そこで勉強したリポジトリ層とデータソース層についてAndroid Developersで紹介されているデータレイヤに基づいてまとめたいと思います。

Android アプリアーキテクチャにおけるデータレイヤ

ここではAndroid Developersのアプリアーキテクチャガイドで紹介されているデータレイヤについて紹介します。 データレイヤとはデータの取得、操作に関する層であります。

データレイヤにはアプリデータとビジネス ロジックが含まれています。ビジネス ロジックは、アプリデータの作成、保存、変更方法を決定する実際のビジネスルールで構成されており、アプリに価値を提供するものです。

データレイヤでこのように関心の分離を行うことで、複数画面での使用、アプリの各要素間での情報共有ができるほか、単体テスト用に UI の外部でビジネス ロジックを再現することも可能になります。

下記より引用 https://developer.android.com/topic/architecture/data-layer?hl=ja

リポジトリ層の役割

リポジトリ層や以下の役割を担います。

複数のデータソース(リモートAPI、ローカルDBなど)からデータを収集し、必要なビジネスロジックを実行する層がリポジトリ層です。リポジトリ層はデータソース層からデータを収集し、アプリケーションの他の部分に提供しますが、データの取得や保存そのものの操作はデータソース層が行います。

データソース層の役割

データソース層は実際のデータ取得や保存を担当する層であり、各データソース(リモートAPI、ローカルDBなど)ごとに個別に実装されます。この層の主な役割は、外部のデータリソースと直接やり取りを行い、必要なデータを取得したり保存したりすることです。

リポジトリ層との違いは、実際にデータの取得保存などの操作を行う点です。

まとめ

今回は業務中の事例からデータレイヤのリポジトリ層とデータソース層についてまとめました。一言にデータの取得といっても、データソース層からデータを取得するのか、実際にDBやAPIからデータを取得するのかといった違いがあり、リポジトリ層とデータソース層の役割を明確に理解することができました。

【JetBrains製エディタ対応】PrettierでTailwind CSSのclass名を規則的に自動ソーティングさせる

こんにちは。エキサイトでデザイナーをしている齋藤です。

今回は、Prettierを使用してTailwind CSSのclass名を自動でソーティングさせる方法についてお話をします。

VS Codeを使用した環境での情報は多く出回っていますが、IntelliJ IDEAやWebStormなどのJetBrains製エディタを使用している場合についての情報はあまりなかったので、IntelliJ IDEAでの設定方法をご紹介したいと思います。

なぜclass名を規則的に記述したいのか

冒頭、前提としてなぜTailwind CSSのclass名を規則的に記述したいかについて、理由をご説明します。

Tailwind CSSCSSを記述することなく、HTMLタグのclass属性にCSSプロパティに対応したclass名を記述することでスタイリングができるCSSフレームワークです。

BEMなどのCSS設計が不要になることや、スタイルが膨れ上がる心配がないなどの利点がある一方で、複雑なスタイリングをする場合にclass属性が長くなり可読性が落ちるという欠点もあります。

<!-- 不規則に記述したコード -->
<div class="py-1 sm:flex-col px-2 items-center justify-center flex">...</div>

そのため、ルールに則って記述することで自分以外のメンバーがコードを読み解く場合の認知負荷を下げることができます。

<!-- レイアウト→修飾→疑似要素・ブレークポイントの順に規則的に記述したコード -->
<div class="flex items-center justify-center px-2 py-1 sm:flex-col">...</div>

しかしながら、手動で並び替えをすると、メンバー間で認識のズレが発生しやすくなったり、新しいメンバーが手を入れる場合にルールを覚えるのに時間を要したりしてしまいます。

そこで、コードフォーマッターのPrettierを使用することで、規則的なコード整形を自動化することができます。

また、Tailwind CSSが公式プラグインを公開しているため、整形ルールを細かく定義する必要なく簡単に設定することができます。

導入方法

前置きが長くなりましたが、PrettierとTailwind CSS公式プラグインの導入方法についてご説明します。

開発環境

今回はSpring Boot + Thymeleafなプロジェクトに、それぞれnpm版のパッケージをインストールします。今回導入する各パッケージのバージョンは以下の通りです。

├── prettier-plugin-tailwindcss@0.6.1
├── prettier@3.3.1
└── tailwindcss@3.4.3

Prettier本体と公式プラグインをインストールする

npm install -D prettier prettier-plugin-tailwindcss

Prettierの設定ファイルでプラグインの使用を宣言する

Prettierの設定ファイルである.prettierrcに、先程インストールしたprettier-plugin-tailwindcssを使用することを宣言します。

(※.prettierrcが存在しない場合にはnode_modulesと同じ階層に作成してください)

{
    "plugins": [
        "prettier-plugin-tailwindcss"
    ]
}

以上でプロジェクト上の設定は完了です。

エディタの設定

自動整形をを可能にするためにはIntelliJ IDEAの環境設定を変更する必要があります。

環境設定の言語&フレームワークJavaScript→Prettierから以下の通り設定してください。

  1. Manual Prettier configurationをオンにする
  2. Prettierパッケージのパスを指定する(node_modules内のprettierディレクトリを指定)
  3. 「次のファイルに実行」にTailwind CSSを使用するファイルの拡張子を指定する(今回はThymeleafなので.htmlのみ)
  4. 「保存時に実行」をオンにする

設定が完了した環境設定

以上の手順で設定することで、ファイル保存時に自動でclass名がソーティングされます。

並び順は、flexabsoluteなどのレイアウトに影響の大きいものを筆頭に、paddingプロパティなどの修飾的なもの、最後にfocus:などの疑似要素やsm:などのブレークポイントとなります。*1

さいごに

今回はPrettierのみを使用してTailwind CSSのclass名を自動でソーティングさせる方法をご紹介しました。

Tailwind CSSの弱点である可読性をPrettierを使用することで改善することができ、さらにはチーム内の認知負荷を下げる工夫にも繋がります。

これからTailwind CSSを使用される方の一助となれれば幸いです。

ご精読ありがとうございました。

SpringBoot Adminを利用して簡易的なアプリケーションモニタリングツールを導入する

エキサイト株式会社エンジニア佐々木です。メディア事業部ではSpringBootとAWSを使用してサービスを展開しています。簡易的な監視にSpringBootAdminを利用していますので、その紹介になります。

環境

環境は下記になります。

$ java --version 
openjdk 21.0.3 2024-04-16 LTS
OpenJDK Runtime Environment Corretto-21.0.3.9.1 (build 21.0.3+9-LTS)
OpenJDK 64-Bit Server VM Corretto-21.0.3.9.1 (build 21.0.3+9-LTS, mixed mode, sharing)

$ ./gradlew --version
------------------------------------------------------------
Gradle 8.5
------------------------------------------------------------

Build time:   2023-11-29 14:08:57 UTC
Revision:     28aca86a7180baa17117e0e5ba01d8ea9feca598

Kotlin:       1.9.20
Groovy:       3.0.17
Ant:          Apache Ant(TM) version 1.10.13 compiled on January 4 2023
JVM:          21.0.3 (Amazon.com Inc. 21.0.3+9-LTS)
OS:           Mac OS X 14.4.1 aarch64

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v3.2.5)

なぜ簡易モニタリングが欲しかったのか?

メディア事業部ではSpringBootをAWS ECSに載せてサービスを提供していますが、コストの関係でエラーログはCloudWatch Logs、正常系はFireLensでS3に入れてAWS Athena、メトリクスはContainer InsightCloudWatchで見るようなこと切替が発生していました。簡易的でも統合的にモニタリングしたい欲求があるので、SpringBoot系に限定はされてしまいますがSpringBoot Adminというモニタリングツールがあるので、導入してみようと思います。

結果から書きますと、SpringBootAdminを入れることで下記のことを実現することができました。

  • ログがリアルタイムに流れてくる
  • メトリクスはほぼ設定不要で取得できる(yamlで数行)
  • アプリケーション内の設定情報を見ることが可能
  • ログレベルを再起動なしで変更可能
  • スレッドダンプ・ヒープダンプの取得が容易
  • キャッシュ削除をSpringBootAdminから実行可能
  • スケジューラーの設定確認が可能

パッと思いつくだけでもこれくらいあります。ただし、過去データを蓄積する機能はありませんので、その場合は他のモニタリングサービスを使用しないといけません。そしてサーバ費のみとなっております。

SpringBoot Adminの構成について

SpringBoot Adminは、サーバとクライアントでライブラリが分かれています。それぞれで設定が必要になります。

SpringBootAdminのServerとClientの構成

SpringBoot Admin Serverの設定

build.gradle

サーバ側の依存関係は下記を追加するだけです。

dependencies {
    ....
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation "de.codecentric:spring-boot-admin-starter-server:3.2.3"
    ...
}

application.yml

サーバー側のアプリケーション側の設定ファイルもほとんど設定がありません。

spring:
  application:
    admin:
      enabled: true
  boot:
    admin:
      monitor:
        default-retries: 1
      ui:
        # 公開用のURL
        public-url: http://localhost:8080/

以上でサーバ側の設定は完了です。

SpringBoot Admin Clientの設定

クライアント側の設定は、各API、Webフロント、管理面、バッチなどの既存のアプリケーションに下記の設定を追加します。

build.gradle

クライアントの方は下記の依存関係を追加します。

dependencies {
        implementation 'org.springframework.boot:spring-boot-starter-actuator'   // メトリクス収集するためのエンドポイントライブラリ
        implementation "de.codecentric:spring-boot-admin-starter-client:$springBootAdminClientVersion"   // SpringBootAdmin Client
}

application.yml

クライアントのアプリケーション側の設定ファイルは、下記となります。少し多めです。

## アプリケーション名
spring:
  application:
    name: sample-application   # SpringBootAdminServerで表示する
  boot:
    admin:
      client:
        enabled: true
        url: http://localhost:8080  # SpringBootAdminServerのURLを記載します
        auto-registration: true  # SpringBootAdminServerに自身のサーバ自動登録する
        auto-deregistration: true # サーバが落ちたときに自動で削除処理を行う。この設定がfalseだと、サーバが落ちた時にSpringBootAdminServer上で異常として検知されます

## spring-boot-starter-actuator のメトリクス用のエンドポイントでどこまで公開するかを設定する
## ほぼ全公開の設定になります
management:
  info:
    env:
      enabled: true
  endpoint:
    health:
      enabled: true
      probes:
        enabled: true
      show-components: always
    logfile:
      enabled: true
  endpoints:
    web:
      exposure:
        include: '*'

## アプリケーションログの出力設定になります。
logging:
  file:
    name: application.log
  logback:
    rollingpolicy:
      max-history: 7
      max-file-size: 10MB

上記で設定が完了となります。

Server/Clientをそれぞれ起動する

それぞれを起動して、通信ができると下記のような画面が出てくると思います。

SpringBootAdminServerのUI

サーバの状態

サーバの状態は画像の通りになります。gitのコミットIDやビルドバージョン、メモリやCPUが取得できているかなど基本情報は取得できています。

サーバの状態

ログファイルの確認

サーバから出力されているログファイルも取得することができます。

ログレベルの変更

ログレベルの変更も、メソッド単位で可能です。

まとめ

SpringBootAdminは、内部でWebflux/Nettyを使用しているようで、結構な接続数を処理できるようです。SpringBootAdminServer1台で、クライアント50〜100台は処理できるようです。データ蓄積はできませんが、簡易的にみるには十分かなと思います。また、Slack通知などや認証まわりなどのカスタマイズも設定を書けば可能なようです。これから使用していこうと思います。

最後に

エキサイトではフロントエンジニア、バックエンドエンジニア、アプリエンジニアを随時募集しております。長期インターンも歓迎していますので、興味があれば連絡いただければと思います。

募集職種一覧はこちらになります!(カジュアルからもOK) www.wantedly.com

[Java]GoogleのOAuth2.0で受け取ったトークンを取り消す方法[Spring Boot]

はじめに

こんにちは、新卒2年目の岡崎です。今回は、GoogleのOAuth2.0で受け取ったトークンを取り消す方法を紹介します。

環境

openjdk version "21.0.2" 2024-01-16 LTS
OpenJDK Runtime Environment Corretto-21.0.2.13.1 (build 21.0.2+13-LTS)
OpenJDK 64-Bit Server VM Corretto-21.0.2.13.1 (build 21.0.2+13-LTS, mixed mode, sharing)
  • Spring Boot
  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v3.2.1)
  • build.gradle

build.grafleに以下の設定を加えます。

dependencies {
 implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
}

実装例

GoogleのOAuth2.0で受け取ったトークンを取り消すには、ユーザーがアカウント設定にアクセスして取り消す方法やアプリケーション側で取り消す方法があります。今回は、アプリケーション側で取り消す方法を考えます。

アクセストークンを取り消すためのリクエストは以下です。

curl -d -X -POST --header "Content-type:application/x-www-form-urlencoded" \
        https://oauth2.googleapis.com/revoke?token={token}

詳細を知りたい方は、公式ドキュメントをご覧ください。

developers.google.com

このリクエストをJavaで実装します。

アクセストークンの取得

ますは、アクセストークンを取得する必要があります。

@RequiredArgsConstructor
public class  Oauth2Service {
    private final OAuth2AuthorizedClientService authorizedClientService;
    private final ModelMapper modelMapper;
    
    public String getToken(Authentication authentication) {
            final OAuth2AuthenticationToken token = modelMapper.map(authentication, OAuth2AuthenticationToken.class);
            final OAuth2AuthorizedClient currentAuthorizedClient = authorizedClientService.loadAuthorizedClient(
                auth2AuthenticationToken.getAuthorizedClientRegistrationId(),
                auth2AuthenticationToken.getName()
        );
        
        return currentAuthorizedClient.getAccessToken().getTokenValue();
    }
}

これでアクセストークンを取得することができます。

アクセストークンの取り消し

取得したアクセストークンを使い、取り消しを行います。

以下は、GoogleのOAuth2.0で受け取ったアクセストークンを取り消す実装例です。

@RequiredArgsConstructor
public class OAtuh2TokenComponent {
    private final QueryParams queryParams;
    private final RestTemplate restTemplate;
    private final static String GOOGLE_LOGIN_REVOKE_URI = "https://accounts.google.com/o/oauth2/revoke";

    @Override
    public void revokeOatuh2Token(String token) {
        final HttpHeaders httpHeaders = HttpHeadersBuilder.build();
        final HttpEntity<String> entity = new HttpEntity<>(httpHeaders);

        final MultiValueMap<String, String> queryParams = this.queryParams
                .builder()
                .add("token", token)
                .build();

        final URI uri = UriComponentsBuilder
                .fromUriString(GOOGLE_LOGIN_REVOKE_URI)
                .queryParams(queryParams)
                .build()
                .encode()
                .toUri();

        try {
            restTemplate.exchange(uri, HttpMethod.POST, entity, Void.class);
        } catch (Exception exception) {
            throw new InternalServerErrorException("API request exception: " + exception.getMessage(), exception);
        }
    }
}

QueryParamsの実装例です。

@Component
@RequiredArgsConstructor
public class QueryParams {
    private final SpringProfile springProfile;

    /**
     * クエリパラメータを取得する
     *
     * @return クエリパラメータ
     */
    public QueryParamsBuilder builder() {
        return new QueryParamsBuilder("domainパス");
    }

    public static class QueryParamsBuilder {
        private final LinkedMultiValueMap<String, String> queryParams;

        /**
         * コンストラクタ
         *
         * @param selfDomain 自ドメイン
         */
        public QueryParamsBuilder(String selfDomain) {
            this.queryParams = new LinkedMultiValueMap<>();

            this.queryParams.add("domain", selfDomain);
        }

        /**
         * クエリパラメータを追加する
         *
         * @param key キー
         * @param value
         * @return クエリパラメータビルダー
         */
        public QueryParamsBuilder add(String key, String value) {
            this.queryParams.add(key, value);
            return this;
        }

        /**
         * クエリパラメータを取得する
         *
         * @return クエリパラメータ
         */
        public MultiValueMap<String, String> build() {
            return this.queryParams.deepCopy();
        }
    }
}

上記で実装したメソッドを呼び出し、アクセストークンが取り消されたことが確認できれば実装完了です。

最後に

今回は、JavaGoogleのOAuth2.0で受け取ったトークンを取り消す方法について紹介しました。誰かの参考になれば幸いです。

エキサイトではフロントエンジニア、バックエンドエンジニア、アプリエンジニアを募集しております。
興味があれば是非ご連絡いただければと思います!

募集職種一覧はこちら www.wantedly.com

【Flutter】UIのイベントをデバウンスする

こんにちは。エキサイト株式会社 モバイルアプリエンジニアの克です。

今回は、FlutterにおいてUIのイベントをデバウンスする手法についてです。

デバウンスについて

デバウンスとは、短時間に複数回のイベントが発生することを防ぐための手法です。
例えばイベントに対して100msのデバウンスを設定する場合、最後のイベントが発火してから100msが経過したタイミングで初めてそのイベントが通知されます。
100msが経過する前に次のイベントが発火した場合は、そのイベントからさらに100msが経過するまで通知は延期されていきます。

似たものとしてスロットリングがありますが、こちらはまず最初に発火したイベントを通知し、そこから一定時間が経過するまでに発火したイベントは無視するものとなります。

用途としては最新のイベントを重視する場合にはデバウンス、時間あたりのイベント数を制限したい場合にはスロットリングというように使い分けます。

まずはUIを実装する

デバウンスを適用するUIですが、今回はPageViewとSliderを連動させ、Sliderの操作時に選択されたページをPageViewに表示するというものにします。
これは画像のビューアーなどでよく使われる構成です。

まずはこれらのUIを実装していきます。
動作イメージとコードは下記の通りです。

class SliderPager extends HookWidget {
  const SliderPager({super.key});

  @override
  Widget build(BuildContext context) {
    final selected = useState(0.0);
    final pageController = usePageController();
    return Scaffold(
      body: Column(
        children: [
          Expanded(
            child: PageView.builder(
              controller: pageController,
              itemBuilder: (_, index) => Center(
                child: Text(index.toString()),
              ),
              itemCount: 100,
            ),
          ),
          Slider(
            value: selected.value,
            onChanged: (value) {
              pageController.jumpToPage(value.ceil());
              selected.value = value;
            },
            max: 100,
          ),
        ],
      ),
    );
  }
}

値の管理をシンプルにするためにflutter_hooksを使用しています。
選択中の位置はhooksを使いselected 変数で管理しています。

UIイベントにデバウンスを設定する

要件が単純な場合はこのままでも何も問題はないかと思います。
ただし、下記のようなケースではデバウンスを設定したほうがいい場合があります。

・アイテムの表示を計測しており、スライダーのシーク中は計測のイベントを送りたくない
・アイテムが表示された際に重い処理を実行しており、連続で実行されると困る

これらのケースを想定して、スライダーをシークした際のイベントにデバウンスを設定するようにします。

今回は公開されているライブラリのeasy_debounceを使用します。
内部的にはTimerを使用してデバウンスを実現しています。

コードを下記のように変更します。

EasyDebounce.debounce(
  'slider-debounce',
  const Duration(milliseconds: 100),
  () => pageController.jumpToPage(value.ceil()),
);

第1引数はデバウンスのタグで、この値を変えることで複数同時にデバウンスを設定することもできます。
第2引数はデバウンスの待機時間です。長くしすぎると操作時に違和感が出てしまうため調整したほうがいいでしょう。
第3引数が実際に実行する処理となります。

注意点として、selectedの更新にはデバウンスを設定しないようにします。
この値はSliderの表示に使用しているため、リアルタイムで更新する必要があります。

この変更による動作イメージは下記となります。

まとめ

デバウンスは便利な場面もありますが、多用するとUXに悪影響を及ぼします。
そのため、要件との折り合いを考えつつ手段の一つとしてスポットで活用するのがよさそうかなと思います。
様々な実装方法を組み合わせて使いやすいアプリを作っていきましょう。

採用情報

エキサイトではフロントエンジニア、バックエンドエンジニア、アプリエンジニアを募集しております。
興味があれば是非ご連絡いただければと思います!

募集職種一覧はこちら www.wantedly.com

Docker 運用の Terraform に TFLint を導入する

エキサイト株式会社の@mthiroshiです。

運用している Terraform に TFLint を導入してみましたので、設定の方法や lint ルールの一例について紹介します。

TFLint とは

TFLint は、Terraform の lintです。 github.com

HashiCorp 非公式のサードパーティツールですが、Terraform の公式 Style Guide の中で紹介されています。 developer.hashicorp.com

Use a linter such as TFLint to enforce your organization's own coding best practices.

TFLint の設定

運用中の Terraform プロジェクトは、Docker で Terraform を実行していました。 TFLint の導入も Docker で行いました。

Docker の設定

まず、compose.yaml を説明します。

services:
  terraform:
    image: hashicorp/terraform:1.7.5
    volumes:
      - .:/terraform-demo
    working_dir: /terraform-demo
    entrypoint: ["/bin/ash"]
    tty: true

  tflint:
    image: ghcr.io/terraform-linters/tflint
    volumes:
      - .:/terraform-demo
    working_dir: /terraform-demo
    entrypoint: ["/bin/sh"]
    tty: true

ディレクトterraform-demo/ 以下で Terraform コードを管理します。 Terraform を実行する際は、下記のコマンドで Docker 環境にログインします。

docker compose up -d terraform
docker compose exec terraform /bin/ash

TFLint の設定

続いて、TFLint を実行するために .tflint.hcl に設定を記述していきます。 公式の設定を参考にしています。

github.com

plugin "terraform" {
  enabled = true
  preset  = "recommended"
}

plugin "aws" {
  enabled = true
  version = "0.31.0"
  source  = "github.com/terraform-linters/tflint-ruleset-aws"
}

AWS を使っているプロジェクトなので、AWS 用のプラグインを有効にします。

TFLint を実行する

下記で TFLint を実行します。

docker compose run --rm tflint -c "\
    tflint --init;\
    tflint --recursive;\
"

実行結果の例です。

docker compose run --rm tflint -c "\
                tflint --init;\
                tflint --recursive;\
        "
Installing "aws" plugin...
Installed "aws" (source: github.com/terraform-linters/tflint-ruleset-aws, version: 0.31.0)

....

Warning: Missing version constraint for provider "aws" in `required_providers` (terraform_required_providers)

  on main.tf line 107:
 107: resource "aws_route53_record" "record" {

Reference: https://github.com/terraform-linters/tflint-ruleset-terraform/blob/v0.7.0/docs/rules/terraform_required_providers.md

terraform_required_providers のルールについて警告されました。 このルールは、Provider にはバージョンを必須で指定することを求めています。 内容の詳細は、表示されたリンクから確認できます。

次に、--fix オプションをつけて実行します。 --fix オプションでは、いくつかのルールを自動で修正してくれます。

docker compose run --rm tflint -c "\
    tflint --init;\
    tflint --recursive --fix;\
"

--fix オプションで適用されたルールの例です。

  • terraform_deprecated_index
  • terraform_unused_declarations

terraform_deprecated_index のルールは、配列参照のレガシーな記法を指摘するものです。 --fix オプションでは、自動で角括弧の記法に修正します。

github.com

terraform_unused_declarations のルールは、使用されていない変数の宣言を指摘するものです。 --fix オプションでは、自動で変数の宣言を削除します。

github.com

注意点として、自動修正の内容によっては構文エラーが起きます。 例えば、 terraform_unused_declarations の場合、module で定義していた使われていない引数が削除されることがあります。module を参照していた側では、削除された引数を残したままになるため、構文エラーになります。 自動適用後の terraform plan の確認を行いましょう。

最後に

Docker 運用の Terraform プロジェクトに対する TFLint の設定と lint ルールの一例を紹介しました。 運用中のコードに対して lint を実行すると、様々な警告が指摘されると思います。 修正によってはエラーが起きていることがあるので、確認をしながら修正しましょう。

参考になれば幸いです。

採用アナウンス

エキサイトではフロントエンジニア、バックエンドエンジニア、アプリエンジニアを随時募集しております。長期インターンも歓迎していますので、興味があれば連絡いただければと思います。

募集職種一覧はこちらになります!(カジュアル面談もぜひ!) www.wantedly.com

ModelMapperの使い方

はじめに

こんにちは、新卒2年目の岡崎です。今回は、ModelMapperの使い方について紹介します。

環境

  • Spring boot
  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v3.2.2)
openjdk version "21.0.2" 2024-01-16 LTS
OpenJDK Runtime Environment Corretto-21.0.2.13.1 (build 21.0.2+13-LTS)
OpenJDK 64-Bit Server VM Corretto-21.0.2.13.1 (build 21.0.2+13-LTS, mixed mode, sharing)

ModelMapperについて

知っている人も多いと思いますが、改めておさらいしていきたいと思います。ModelMapperでは、違う型同士の値をコピーし、反映することができます。

例えばAオブジェクトとBオブジェクトがあり、AオブジェクトからBオブジェクトへ値を反映させたいケースがあったとします。この時、ModelMapperを使って行うこともでき、とても便利です。

それでは、実際にコードで見てみましょう!

準備

build.gradleに以下の実装をしてください。

dependencies {
  // model mapper
  implementation 'org.modelmapper:modelmapper:{モデルマッパーの最新バージョン}'

  // lombok
  compileOnly 'org.projectlombok:lombok'
  annotationProcessor 'org.projectlombok:lombok'
}

実装

以下のように、NovelDtoオブジェクトとNovelオブジェクトが存在していると仮定します。

@Data
@Accessors(chain = true)
public class NovelDto {
    /**
     * ID
     */
    private Long id;

    /**
     * ユーザーID
     */
    private Long userId;

    /**
     * 高評価数
     */
    private Long likeTotal;

    /**
     * 低評価数
     */
    private Long dislikeTotal;

    /**
     * 高評価したかどうか
     */
    private Boolean isLiked = false;

    /**
     * 低評価したかどうか
     */
    private Boolean isDisliked = false;

    /**
     * 自分の小説かどうか
     */
    private Boolean isMyNovel = false;

    /**
     * 作成日時
     */
    private LocalDateTime createdAt;
}
@Data
@Accessors(chain = true)
public class Novel {
    /**
     * ID
     */
    private Long id;

    /**
     * ユーザーID
     */
    private Long userId;

    /**
     * 高評価数
     */
    private Long likeTotal;

    /**
     * 低評価数
     */
    private Long dislikeTotal;

    /**
     * 高評価したかどうか
     */
    private Boolean isLiked = false;

    /**
     * 低評価したかどうか
     */
    private Boolean isDisliked = false;

    /**
     * 自分の小説かどうか
     */
    private Boolean isMyNovel = false;

    /**
     * 作成日時
     */
    private LocalDateTime createdAt;

    /**
     * オブジェクトが空かどうか判定する.
     *
     * @return オブジェクトが空かどうか
     */
    public Boolean isEmpty() {
        return Objects.isNull(this.commentId);
    }
}

ほとんどのプロジェクトでは層によって使用するオブジェクトが決まっていると思います。例えば、Service層ではRepository層から受け取ったNovelDtoオブジェクトを、Novelオブジェクトへ反映します。

この時の実装例を以下に示します。

@Service
@RequiredArgsConstructor
public class NovelService {
    private final NovelRepository novelRepository;

    public Novel getNovel(Long id) {
        final NovelDto dto = novelRepository.findNovel(id);

        // 中略

        final Novel novel = new Novel()
            .setid(dto.getId())
            .setUserId(dto.getUserId())
            .setLikeTotal(dto.getLikeTotal())
            .setDislikeTotal(dto.getDislikeTotal())
            .setIsLiked(dto.getIsLiked())
            .setIsDisliked(dto.getIsDisliked())
            .setIsMyNovel(dto.getIsMyNovel());

        return novel;
    }
}

このコードの問題点は、setし忘れが起きてバグの原因になる可能性があることです。(もしかすると、このようなコードが煩雑に感じる人もいるかもしれません。)

ModelMapperを使えば、以下のようにコードを書けます。

@Service
@RequiredArgsConstructor
public class NovelService {
    private final NovelRepository novelRepository;
    private final ModelMapper modelMapper;

    public Novel getNovel(Long id) {
        final NovelDto dto = novelRepository.findNovel(id);
        
        return modelMapper.map(dto, Novel.class);
    }
}

一行で書くことができるので、コードはシンプルになり、setのし忘れは起きないようになります。

ちなみにListのオブジェクトの反映は、以下のように行います。

List.of(modelMapper.map(dtoList, 反映したいclass[].class));

補足

ModelMapperを使って開発していると、UnrecognizedPropertyExceptionが発生することがあります。

これはオブジェクト同士を比較した時、存在しない属性があることが原因です。

様々な解決方法があると思いますが、今回は@JsonIgnoreを使う方法を紹介します。

まずは、build.gradleに依存関係を追加します。

dependencies {
        implementation "org.mybatis.spring.boot:mybatis-spring-boot-starter:${springMybatisStarterVersion}"
}

そして、オブジェクト同士を比較して存在しない属性に対し、@JsonIgnoreを追加します。

@Data
@Accessors(chain = true)
public class Novel {
    /**
     * ID
     */
    private Long id;

   // 中略

    /**
     * オブジェクトが空かどうか判定する.
     *
     * @return オブジェクトが空かどうか
     */
    @JsonIgnore
    public Boolean isEmpty() {
        return Objects.isNull(this.commentId);
    }
}

これでエラーが発生しなくなりました。

最後に

今回は、ModelMapperの使い方を紹介しました。ModelMapperはとても便利なので、使ってみてください。

最後に、エキサイトではデザイナー、フロントエンジニア、バックエンドエンジニア、アプリエンジニアを絶賛募集しております!

興味があればぜひぜひ連絡ください!

www.wantedly.com

PHPカンファレンス小田原で初スタッフに挑戦しました!

こんにちは!エキサイト株式会社のまさきちです。

先日、PHPカンファレンス小田原でスタッフしてきました。 今までカンファレンススタッフの経験は無くドキドキでしたが振り返っていきます。

PHPカンファレンス小田原とは

2024年4月13日(土)におだわら市民交流センターUMECOにて開催されたPHPカンファレンスです。(初開催)

PHP関連のイベントは色々な場所で開催されておりますが、小田原の魅力がいっぱい詰まったカンファレンスでした! phpcon-odawara.jp 私は会場&前夜祭お手伝いと、当日スタッフとして参加しました。

会場準備お手伝い

開催日の前日に小田原の会場に行き準備のお手伝いをしました。(実はこの時に初めて小田原に来た)

会場に荷物を運んだり、机と椅子を並び替えたり、配信機材の準備を行いました。 思ったよりやる事が多くて大変でしたが、これから始まるカンファレンスをみんなで作り上げている感じがとても楽しかったです。

前夜祭

前夜祭では受付と写真撮影を担当しました。

受付時にチケットの種類がいくつかあり、条件分岐に戸惑ってしまう場面もありましたが、スタッフで協力し合って乗り切りました。 参加者の皆様にもご配慮いただき助かりました。

受付後にPHPer同士でIRTを行い、写真撮影を担当したのですが、 皆さん盛り上がっていて楽しそうな写真がたくさん撮れて嬉しかったです。

カンファレンス当日

当日は準備や片付けに加え、主に「かま」会場スタッフ、LTタイムキーパーの役割を担当しました。

会場スタッフ

カンファレンスは主に、「かま」と「ぼこ」の二つの会場に分かれて行いました。 「かま」会場では、配信機材や照明の管理、トーク司会、誘導係の3つの役割分担を決めてローテーションしていました。

配信機材を触ったことが無いのでうまく出来るか不安でしたが、操作マニュアルを作っていただけたり、詳しい方が使い方を丁寧に説明してくださっていたので安心して取り組む事ができました。

トーク司会では、登壇者の準備のお手伝いや、タイトル読み上げ、タイムキーパーなど行いました。 こちらも初めての経験で不安でしたが、トークスクリプトを用意してくださっていたので、迷う事なくこなす事ができました。

誘導係は、トークを終えたスピーカーの方を別会場へ案内したり、会場の扉を開け閉めしたり、サブ的な役割をこなしていました。

初めての事ばかりでしたが、コアスタッフの方々が念入りに準備してくださっていたので問題なく対応できました。

LTタイムキーパー

カンファレンス終盤のLTではタイムキーパーを担当しました。 LTには小田原のゆるキャラ「梅丸」が来てくれて大盛り上がり! 私はLT終了時間が来たら梅丸が持っているドラを叩きを担当。(梅丸、ドラ持つの大変だったと思うけどお疲れ様でした) 途中機材トラブルなどもありましたが、周りの方々の助けもあり、無事乗り切る事ができました。

おだわランチ

昼休憩はスタッフの方たちと小田原のランチを楽しみました。 スタッフがお勧めのお店をnoteに書いてくださったので、そちらのお店に行きました。美味しかったし、お店の人優しくて感動。

ふりかえり

カンファレンス終了後にスタッフでふりかえりを行いました。 360°Thanksとして自分を含めたメンバーそれぞれに感謝したり、GoodとMoreを出す作業をMiroを使って付箋に書き出しながら行いました。 振り返ってみると課題もたくさん見つかったので、Nextアクションに繋げていければいいなと思います。

まとめ

初めてのカンファレンススタッフで大変だったけど挑戦してよかった。 カンファレンスの裏側を知る事ができたし、機材や進行などを実体験できて学びが多かった。 PHPerの温かさに再度気づくことができました。 スタッフに興味がある方はぜひトライしてみてください。

Figmaのローカルバリアブルを使ってテキストにスタイルを適用してみる

こんにちは!SaaS・DX事業部デザイナーの鍜治本です!
担当しているサービス「KUROTEN」のUI設計をする際にFigmaを使っており、デザインシステムの構築を日々進めています。
そんな中、スタイルの定義を行う際に気がついた機能があるので、TIPSとして記事にしてみました。

Figmaのローカルバリアブル機能の概要

Figmaにはファイル内で色やフォントなどの情報をライブラリとして扱うことができる、「スタイル」「バリアブル」という機能があります。どちらの機能も他のファイルで引用でき、UI制作においては一貫した管理がしやすいメリットがあります。
細かい機能については、Figma公式のサイトからどうぞ🙆
バリアブルとスタイルの違い

文字列変数「String variable」がフォントのプロパティに対応できるように

これまでバリアブルの変数は、色・変数・文字列・ブーリアンの4種類があり、フォント以外のプロパティには概ね設定できていました。
アップデートによってテキストプロパティ(フォントサイズ・ウェイト・行間・文字間隔・段落間隔など)にも、変数を指定することができるようになり、スタイル機能だけでなく、バリアブル機能にも同様な管理ができるようになりました。

ローカルバリアブルを設定してみる

担当しているサービス「KUROTEN」でもスタイル機能を用いてフォントを管理しているのですが、日本語と英数字とでフォントを分けているためスタイル機能を解除することが起こります。
例えば「2024年5月10日」のような文字列の場合、数字部分はRoboto・漢字はNoto Sans JPとしたいため、Figmaで再現する場合はスタイル解除し直接プロパティを編集する必要があります。

合成フォントを想定しているが、Figma上はフォントごとに設定する手間が生じる
タイポグラフィとして定義はしているものの、常に覚えていられるものでもなく、毎度正しい値を確認して作業するのは手間になります。さらに、複数人で作業をするのであれば尚更、どれが正解かわからなくなってしまいますよね。
今回は、ローカルバリアブルにもタイポグラフィを管理できるように設定していきます。
かなり自己流で手探りなので、知見がある方いらっしゃればぜひアドバイスをお願いします🙇

①数値・文字列の登録をする

色やタイポグラフィを定義している「Foundations」とボタンやフォームなど管理する「Components」
前提として、KUROTENのデザインシステムをFigmaに反映させるために、2つのファイルをそれぞれ用意しています。色やタイポグラフィなどを定義している「Foundations」と、ボタンなどパーツ化されたものを保管している「Components」です。
今回設定したいフォントのスタイルは文字列に関する変数であるため、Foundationsのローカルバリアブルに値を設定していきます。
ローカルバリアブルへの登録手順
1. はじめにローカルバリアブルを開き、モーダル一番下にある「バリアブルを作成」から『文字列』のメニューを選択します。
2. Tアイコンの入った行が追加されるので、[名前]の列には管理する上でわかりやすい名前を、[値]の列にはフォント名を入力します。フォント名はFigma上にあるものと同じでないと反応しないため、間違えないよう確認してください🙆
今回は[名前]に「Noto Sans」、[値]に「Noto Sans JP」と入力しました。

ちょっとした余談ですが、[名前]列に入力する際、『Typograhpy/Noto Sans』のように「/(スラッシュ)」を入れることで、Typography部分をグループ名とした階層構造を作成できます。フォント以外にフォントサイズや行間、HeadingやBodyなど種類違いで管理する場合は、グループごとに設定することで管理がしやすいです。

②テキストスタイルにバリアブルを設定する

テキスト箇所へバリアブルを設定していく手順
①で設定したバリアブルを、テキストのスタイルに適用していきます。
3. 適用させたいテキストを選択し、プロパティのフォント名をクリックして、メニューを表示します。
4. フォント選択メニューの右上に「バリアブル」アイコンが表示されているので、クリックしてバリアブル適用メニューを表示させます。
5. バリアブルメニューの中には、先のローカルバリアブル登録手順で登録した文字列変数(今回はNoto Sans)が表示されているので、クリックします。
6. バリアブルを設定したテキストのフォント箇所を見ると、フォント名が表示されry箇所がチップのように表示されます。
この手順で、テキストにバリアブルを設定できました!

またフォント名だけでなく、数値など駆使することで、ウェイト・サイズ・行間・文字間隔・段落間隔までバリアブルの指定ができるようになります。

フォント以外の項目もバリアブルを設定してみた図(文字間隔・段落間隔以外)
現状全てに対応する必要性はそこまで見出せていないものの、バリアブルを設定すると作業効率があがるUI(Webサイト制作や一括で切り替えたい場面など)においては、かなり有力な機能ではないでしょうか。

バリアブルの表示範囲が指定できるように

バリアブルの設定では、変数を表示させられる範囲が指定できます。

適用範囲を指定するとプロパティ内で候補の制限ができる
例えば色であれば、塗りだけに適用させたいものがあったり、テキストに適用させてシェイプ・フレームには適用させないルールを設けている場合がありますね。
数値のバリアブルでの適用範囲設定
ローカルバリアブルでは、こういった適用範囲も細かく指定できます。

行間・文字間隔・段落間隔のローカルバリアブルが出てこない原因

テキスト関連のプロパティを設定していた際に遭遇した落とし穴について共有します。
先ほど数値の変数も適用範囲が指定できる話をしましたが、「行間」「文字間隔」「段落間隔」の項目に『テキストコンテンツ』の適用範囲を指定しても、候補に表示がされない事象に遭遇しました。

行間にも「テキストコンテンツ」の適用範囲を指定すると、プロパティの候補に表示されない
サイズやウェイトでは問題なく候補に表示されるのですが、「行間」「文字間隔」「段落間隔」のみ適用範囲を制限すると候補として表示されなくなってしまいます。

解決方法

解決方法は、適用範囲を設けず「サポートされているプロパティの全てで表示」を選択するだけで回避できます。

バリアブルの適用範囲を修正し、テキストプロパティの候補に表示を確認
候補件数が増えてしまうので探しにくくなってしまいますが、「①数値・文字列の登録をする」の中でお伝えしたグループ化や、変数名の検索などで改善できそうです。

まとめ

今回はローカルバリアブル機能を活用して、テキストへのスタイル適用についてまとめさせていただきました。
最初の設定がやや手間にはなりつつも、ある程度ルールを設けることでアウトプットのしやすさ改善につながったり、複数人で作業する場面での齟齬を未然に防げると思います。
またテキストのプロパティにバリアブルを設定する箇所の思わぬ落とし穴についても、回避術共有になれば嬉しいです🙌

おわりに

エキサイトといえばtoC向けサービスが印象的ですが、新規事業としてtoB領域のプロダクトも展開しています🙆
エンジニアはもちろん、デザイナーも新卒・中途を問わず採用募集していますので、カジュアル面談からでもお話ししませんか?
ご興味があればぜひお気軽にご連絡ください🙌

www.wantedly.com

Gradleマルチプロジェクトでは、Mavenリポジトリは絶対パスで指定すべきだった話

こんにちは、エキサイト株式会社の平石です。

今回は、Gradleでマルチプロジェクトを構成している状態で、MavenのLocalRepositoryを指定する際にハマった内容を紹介します。

デフォルト以外のLocalRepository

今回は、repositoriesの設定で独自のRepositoryを指定することを目的としていました。

Maven Repositoryに上げられているようなグローバルに公開されているライブラリを利用する際には、

repositories {
    mavenCentral()
}

のように記述します。

ローカルにあるRepositoryを設定したい場合には

repositories {
    mavenLocal()
}

のように記述します。
デフォルトでは、LocalRepositoryは$USER/.m2/repositoriesにあります。

しかし、時にはプロジェクトごとに分けたいという理由で別のローカルリポジトリを指定したい場合もあります。

今回は、local_repositoryというディレクトリをプロジェクトルート直下に作成して、そこにライブラリを入れているとします。

この場合、build.gradleに以下のように記述することで、指定することができます。

repositories {
        maven {
            url "./local_repository"
        }
    }

urlには、サーバー上のリポジトリやS3のようなオブジェクトストレージにあるライブラリへのパスを記述できますが、ローカル環境にあるリポジトリの場所を指定することもできます。

以下のように、プロジェクトに依存関係を追加することができます。

dependencies {
    implementation "com.example.rh:sample:0.0.1-SNAPSHOT"
}

このように一つのプロジェクト内のみでの設定であれば問題ないのですが、allprojectssubprojectsを利用して複数のプロジェクトに対して設定を行おうとしたときにハマってしまいました。

以下が、ハマったときのプロジェクトルート直下のbuild.gradleの全体です。

plugins {
    id 'java'
    id 'org.springframework.boot' version '3.2.1'
    id 'io.spring.dependency-management' version '1.1.4'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'

java {
    sourceCompatibility = '21'
}

allprojects {
    repositories {
        mavenCentral()

        maven {
            url "./local_repository"
        }
    }
}

subprojects {
    apply plugin: 'java'
    apply plugin: 'org.springframework.boot'
    apply plugin: 'io.spring.dependency-management'

    dependencies {
        implementation 'org.springframework.boot:spring-boot-starter'
        implementation 'org.springframework.boot:spring-boot-starter-web'
    }
}

project(':sub_project') {
    dependencies {
        implementation "com.example.rh:sample:0.0.1-SNAPSHOT"
    }
}

allprojectsmavenCentralとプロジェクトルート直下の./local_repositoryからライブラリを取得することを指定しています。
また、:subprojectというサブプロジェクトに./local_repositoryから"com.example.rh:sample:0.0.1-SNAPSHOT"という依存関係を追加しています。

この状態で、gradleプロジェクトをビルドすると、以下のようなエラーが発生します。

:sub_project:main: Could not find com.example.rh:sample:0.0.1-SNAPSHOT.
Required by:
    project :sub_project

com.example.rh:sample:0.0.1-SNAPSHOTが見つからないというエラーのようですが、allprojectsブロックでrepositoriesは指定しているはずです。

これはどういうことでしょうか。
解決法はシンプルで、LocalRepositoryの指定を絶対パスにします。

allprojects {
    repositories {
        mavenCentral()

        maven {
            url "$rootDir/local_repository"
        }
    }
}

allprojectsでLocalRepositoryを指定した際に、その指定したプロジェクトから見たパスで全体に対して取り込まれると勘違いしていました。
しかし、実際には各プロジェクトから見たパスで参照しているようです。

したがって、subprojectというサブプロジェクトはdependency_testの配下にあり、dependency_test/subprojectにはlocal_repositoryというrepositoryが存在しないため、依存関係を解決できなかったようです。

仕様を理解していれば当然の結果ではありました。

終わりに

今回は、マルチプロジェクトにおけるLocalRepositoryを指定する際にハマった内容を紹介しました。

では、また次回。

SpringBootで複数のapplication.ymlの読み込んで、環境ごとの起動をラクにする

エキサイト株式会社エンジニアの佐々木です。SpringBootではapplication.ymlなどの設定情報を読み込む方法がいくつかあるのでまとめます。

前提

$ java --version
openjdk 17.0.10 2024-01-16 LTS
OpenJDK Runtime Environment Corretto-17.0.10.7.1 (build 17.0.10+7-LTS)
OpenJDK 64-Bit Server VM Corretto-17.0.10.7.1 (build 17.0.10+7-LTS, mixed mode, sharing)

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v3.2.3)

設定ファイル一覧

設定

下記の3ファイルになります。

- application.yml
- application-a.yml
- application-b.yml
- application-c.yml

コード

リクエストされたら、application.ymlで設定されてデータが返却されます。

@RestController
@RequestMapping("yml")
@RequiredArgsConstructor
@Slf4j
public class ApplicationYmlController {

    @Value("${jp.co.excite.name}")
    private String value;

    @GetMapping("")
    public String applicationYml() {
        return value;
    }

}

起動引数で設定される値を変える

  • 引数なし
java -jar yml-test.jar

curl http://localhost:8080/yml
default

引数がない場合は、application.ymlが使用されます。

  • 引数 --spring.profiles.active=a
java -jar yml-test.jar --spring.profiles.active=a

curl http://localhost:8080/yml
a

引数がある場合は、デフォルトのプロファイルと指定されたプロファイルが使用されます。 この場合は、application.ymlapplication-a.ymlが読み込まれます。

  • 引数 --spring.profiles.active=b
java -jar yml-test.jar --spring.profiles.active=b

curl http://localhost:8080/yml
b

別のプロファイルを指定してもちゃんと読み込まれています。

  • 引数 --spring.profiles.active=a,b
java -jar yml-test.jar --spring.profiles.active=a,b

curl http://localhost:8080/yml
b

java -jar yml-test.jar --spring.profiles.active=b,a

curl http://localhost:8080/yml
a

複数指定する場合は、後勝ちになります。指定された順番で上書きしていくイメージです。

補足./gradlew bootRun の場合

gradle

 ./gradlew bootRun --args='--spring.profiles.active=a'

設定ファイルの中で指定する場合

設定ファイルの中でも、他の設定ファイルを読み込むことが可能です。spring.config.importを使用します。

spring:
  config:
     import: classpath:${プロファイルのファイル}

上記で指定できます。こちらも後勝ちなので、設定には注意が必要です。

まとめ

SpringBootでは、環境によって設定ファイルをリネーム、設定の書き換えなどの操作は、基本的に行わずに起動引数によって解決するようになっています。これはとても便利なので、多用していきたいところです。

さいごに

エキサイトではフロントエンジニア、バックエンドエンジニア、アプリエンジニアを随時募集しております。長期インターンも歓迎していますので、興味があれば連絡いただければと思います。

募集職種一覧はこちらになります!(カジュアル面談もぜひ!) www.wantedly.com

Spring AI に入門する

エキサイト株式会社エンジニアの佐々木です。詳細は話せませんがAI系の業務が発生したので、技術選定の一環でSpring AIを触ってみました。

前提

$ java --version
openjdk 17.0.10 2024-01-16 LTS
OpenJDK Runtime Environment Corretto-17.0.10.7.1 (build 17.0.10+7-LTS)
OpenJDK 64-Bit Server VM Corretto-17.0.10.7.1 (build 17.0.10+7-LTS, mixed mode, sharing)

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v3.2.3)

手順

Spring AI 公式サイトの手順に従って進めてみます。

1. Spring CLI をインストールする

Spring CLIをインストールします。ここではMacのコマンドになります。

brew tap spring-cli-projects/spring-cli
brew install spring-cli

2. Spring CLIでAI用のプロジェクトを作成する

spring-ai-sampleプロジェクトを作成します。

spring boot new --from ai --name spring-ai-sample

3. OpenAIのAPIキーを取得します

OpenAIのapi-keysにいき、APIキーを取得します。

4. 環境変数APIキーをセットします

export SPRING_AI_OPENAI_API_KEY=${取得したAPIキー}

5. SpringBootを起動します

下記コマンドを投入し、SpringBootを起動します。

cd spring-ai-sample
./mvnw spring-boot:run

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v3.2.3)

2024-04-17T13:27:14.877+09:00  INFO 80604 --- [           main] o.s.a.o.samples.helloworld.Application   : Starting Application using Java 17.0.1 with PID 80604

6. リクエストしてみる

8080ポートでSpringBootが起動していますので、curlでリクエストしてみます。

curl http://localhost:8080/ai/simple --get --data-urlencode 'message=What is Java?'

{"generation":"Java is a high-level programming language developed by Sun Microsystems (now owned by Oracle) in 1995. It is known for its platform independence, meaning that Java programs can run on any device or operating system that has a Java Virtual Machine (JVM) installed. Java is widely used for developing a variety of applications, including desktop, web, mobile, and enterprise software. It is also popular for developing server-side applications and Android mobile apps."}

サクッと応答がしてもらえました。

内部実装

簡単にですが、内部実装をみていきます。

ライブラリ

ライブラリとしては、AI用としては下記の2つが入っています。

org.springframework.ai:spring-ai-bom
org.springframework.ai:spring-ai-openai-spring-boot-starter

コード

生成されていたコードとしては、下記のようなコードになっていました。

@RestController
public class SimpleAiController {

    private final ChatClient chatClient;  

    @Autowired     // ChatClientを使えるようにしている
    public SimpleAiController(ChatClient chatClient) {  
        this.chatClient = chatClient;
    }

    @GetMapping("/ai/simple")
    public Map<String, String> completion(@RequestParam(value = "message", defaultValue = "Tell me a joke") String message) {
        return Map.of("generation", chatClient.call(message));   // リクエストをなげている
    }
}

SpringBootを使用されている方には見慣れたコードですね。ChatClient、EmbeddedClientなどいくつかあるので、これから試してみようと思います。

Spring AIで使用できる一覧

Spring AIで使用できる各API一覧です。Claude3とかはないですが、一通りある感じです。

Chat Models

    OpenAI
    Azure Open AI
    Amazon Bedrock
    Google Vertex AI Palm
    Google Gemini
    HuggingFace - access thousands of models, including those from Meta such as Llama2
    Ollama - run AI models on your local machine
    MistralAI

----
Text-to-image Models
    OpenAI with DALL-E
    StabilityAI

----
Transcription (audio to text) Models
    OpenAI

----
Embedding Models

    OpenAI
    Azure OpenAI
    Ollama
    ONNX
    PostgresML
    Bedrock Cohere
    Bedrock Titan
    Google VertexAI
    Mistal AI

まとめ

Spring AIを触ってみましたが、かなり簡単にAPIに接続できました。手軽ですし、既存のサービスへのアドオンとしてはさっとできそうなのでよさそうです。。今後、試していこうと思います。

さいごに

エキサイトではフロントエンジニア、バックエンドエンジニア、アプリエンジニアを随時募集しております。長期インターンも歓迎していますので、興味があれば連絡いただければと思います。

募集職種一覧はこちらになります!(カジュアルからでもぜひ!) www.wantedly.com

Thymeleafでハイパーリンクを実装する

はじめに

こんにちは、新卒2年目の岡崎です。今回は、Thymeleafでハイパーリンクを実装する方法を紹介します。

環境

  • Thymeleaf 3.3.0

前提

以下のようなオブジェクトが存在することを仮定します。

@Data
public class Test {
    private Long id;
    
    private String name;
    
    // 以下略
}

オブジェクトのリストを表示する場合、以下のコントローラーを使います。

    @GetMapping("list/test")
    public String getTestList(
            Model model
    ) {
            final List<Test> testList = testUseCase.getTestList();

            model.addAttribute("testList", testList);

            return "test/list/index";
    }

また、オブジェクトを取得する場合は、idから表示します。

    @GetMapping("test")
    public String getTest(
            @ModelAttribute Model model,
            TestRequestDto request,
    ) {
            final Test test = testUseCase.getTestById(request.id(), request.type());

            model.addAttribute("test", test);

            return "test/index";
    }
public record TestRequestDto(
        @NotNull @Positive
        Long id,

        @Nullable 
        String type
) {
}

test/list/index.htmlを用意します。

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4">
<head>
    <title>Test List</title>
    <meta charset="utf-8"/>
</head>

<body>

</body>
</html>

test/index.htmlを用意します。

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4">
<head>
    <title>Test</title>
    <meta charset="utf-8"/>
</head>

<body>
   <div>sample</div>
</body>
</html>

実装

前提で紹介したリストの表示を行います。

test/list/index.htmlを変更します。

<body>

   <th:block th:each="test : ${testList}">
       <tr>
           <td th:text="${test.getId()}"></td>
           <td th:text="${test.getName()}"></td>
                // 以下略
       </tr>
   </th:block>

</body>
</html>

idをタップすると、詳細画面を見ることができるようにするため、ハイパーリンクを作成します。

<th:block th:each="test : ${testList}">
    <tr>
        <td>
          <a th:href="@{/test(id=${test.getId()})}" th:text="${test.getId()}"></a>
        </td>
        <td th:text="${test.getName()}"></td>
                // 以下略
    </tr>
</th:block>

th:href=@url(パラメーター名=${変数名})と書くことで、変数の値をクエリーパラメーターとして指定することができます。

ハイパーリンクに、パスのクエリーパラメーターを使う方法

実際のケースで、ハイパーリンクに、パスのクエリーパラメーターを使いたい場合がありました。この時の実装例を紹介します。

下記のようなパスが存在していると仮定します。

http://localhost8080/test/list?type=TEST

<div th:with="type=${param.type}">
  <a th:href="@{/test(id=${test.getId(), type=${type})}" th:text="${test.getId()}"></a>
</div>

この場合、<div th:with="クエリーパラメーター=${param.クエリーパラメーター}"> 〜 </div> と実装すれば、そのdivタグの中でクエリーパラメーターを使うことができます。

最後に

今回は、Thymeleafでハイパーリンクを実装する方法について紹介しました。

最後に、エキサイトではデザイナー、フロントエンジニア、バックエンドエンジニア、アプリエンジニアを絶賛募集しております!

興味があればぜひぜひ連絡ください!

www.wantedly.com

JavaでURIを作る方法

はじめに

こんにちは、新卒2年目の岡崎です。今回は、JavaURIを作る方法について紹介します。

前提

openjdk version "21.0.2" 2024-01-16 LTS
OpenJDK Runtime Environment Corretto-21.0.2.13.1 (build 21.0.2+13-LTS)
OpenJDK 64-Bit Server VM Corretto-21.0.2.13.1 (build 21.0.2+13-LTS, mixed mode, sharing)
  • Spring Boot
  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v3.2.1)

実装

色々な方法があると思いますが、今回はUriComponentsBuilderを使って実装する方法を紹介します。

UriComponentsBuilderは、Spring Bootで用意されているコンポーネントです。これにより、文字列結合をしなくても、URIを作ることができます。

final String uri = UriComponentsBuilder
          .fromUriString("http://localhost:8080/test")
          .queryParam("service", service)
          .fragment("test1")
          .encode()
          .build()
          .toUriString();

作られるURI

http://localhost:8080/test?service=service#test1

fromUriString

fromUriString(”パス名”)で、URIのパスを指定することができます。

queryParam

ここでは、クエリーパラメーターを指定することができます。queryParam(”パラメーター名”, パラメーター)と実装します。

例えば、

         .fromUriString("http://localhost:8080/test")
          .queryParam("service", "DEMO")

と実装した場合、http://localhost:8080/test?service=DEMOというURIが作られます。

fragment

URIのフラグメントを指定することができます。fragment(文字列)と実装することで、指定した文字列のフラグメントが作られます。

例えば、

         .fromUriString("http://localhost:8080/test")
          .queryParam("service", "DEMO")
          .fragment("demo1")

と実装した場合、http://localhost:8080/test?service=DEMO#demo1 というURIが作られます。

最後に

今回は、JavaURIを作る方法について紹介しました。

最後に、エキサイトではデザイナー、フロントエンジニア、バックエンドエンジニア、アプリエンジニアを絶賛募集しております!

興味があればぜひぜひ連絡ください!

www.wantedly.com

SpringBootの階層の深いapplication.ymlをrecordで取り出す

エキサイト株式会社メディア事業部エンジニアの佐々木です。Javaにrecord型が登場し、SpringBootでも結構いろいろなところで使用できるようになりました。今回は、application.ymlからrecordを使用して取り出す方法になります。

前提

環境は下記になります。

$ java --version
openjdk 21.0.2 2024-01-16 LTS
OpenJDK Runtime Environment Corretto-21.0.2.13.1 (build 21.0.2+13-LTS)
OpenJDK 64-Bit Server VM Corretto-21.0.2.13.1 (build 21.0.2+13-LTS, mixed mode, sharing)


$ ./gradlew --version

------------------------------------------------------------
Gradle 8.6
------------------------------------------------------------

Build time:   2024-02-02 16:47:16 UTC
Revision:     d55c486870a0dc6f6278f53d21381396d0741c6e

Kotlin:       1.9.20
Groovy:       3.0.17
Ant:          Apache Ant(TM) version 1.10.13 compiled on January 4 2023
JVM:          21.0.2 (Amazon.com Inc. 21.0.2+13-LTS)
OS:           Mac OS X 12.5 aarch64

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v3.2.3)

application.yml

取り出したいapplication.ymlの情報は下記になります。アプリケーション内で使用する国情報みたいなデータです。

jp:
  co:
    excite:
      country:
        - name: japan
          lang:
            - ja
          data:
            population: 126000000
            capital: tokyo
            currency: yen
            timezone: Asia/Tokyo
        - name: united states
          lang:
            -  en
          data:
            population: 328000000
            capital: washington
            currency: dollar
            timezone: America/New_York

コード

コードとしては、下記になります。

@ConfigurationProperties(prefix = "jp.co.excite")
public record LangListConfig (List<CountryType> country) {
    record CountryType(String name, List<String> lang, CountryData data) {
        record CountryData(String population, String capital, String currency, String timezone) {}
    }
}

// 出力
LangListConfig[
  country=[
    CountryType[name=japan, lang=[ja], data=CountryData[population=126000000, capital=tokyo, currency=yen, timezone=Asia/Tokyo]],
    CountryType[name=united states, lang=[en], data=CountryData[population=328000000, capital=washington, currency=dollar, timezone=America/New_York]]
  ]
]

SpringBoot3系だと、この行数でタイプセーフに記述することが可能です。設定ファイルがいくらネストしていても、同じようにrecordをネストすれば取り出せます。

まとめ

SpringBootで利用しているライブラリは、SnakeYAMLで結構いろいろできます。どんどんタイプセーフにしていきましょう。

最後に

エキサイトではフロントエンジニア、バックエンドエンジニア、アプリエンジニアを随時募集しております。長期インターンも歓迎していますので、興味があれば連絡いただければと思います。

募集職種一覧はこちらになります!(カジュアルからもOK) www.wantedly.com