リモートワークを快適に行うためにデスクまわりを整備した

はじめに

エキサイト株式会社 21卒 バックエンドエンジニアの山縣です。 普段は既存サービスのリビルド(PHP / BEAR.Sunday → Java / SpringBoot)を担当しています。

エキサイトホールディングス Advent Calendar 2021の3日目の記事です! qiita.com

現在エキサイトでは、月8回は出社する必要があり、その他の曜日は自宅から作業をしています。 本記事では、自分がリモートワークを快適に行うために購入してよかったものを紹介します。

LG 4Kモニター

f:id:excite-kazuki:20211202190018j:plain

このLGの4Kモニター(32UN500-W)を購入する前は、DellのWQHDモニター(P2720DC)を使っていました。 Dellのモニターでも十分に満足していましたが、不慮の事故によりディスプレイを壊してしまい、新しく購入することになりました。 下記条件を満たすモニターがないか調べていたところ、LGの4Kモニター(32UN500-W)がこれらを満たしていました。

  • 4Kの解像度
  • 枠が細い
  • スピーカー内臓
  • モニターアームが取り付けられる(VESA規格対応)
  • 5万円以内

現状では大画面で作業することができてとても満足していますが、チームメンバーから「EIZOのモニターはいいぞ!」とおすすめされることも多く、今後買い換えるかもしれないです🤣

Anker PowerExpand 9-in-1

f:id:excite-kazuki:20211202191001j:plain

このドッキングステーションは「MacBookとモニターをケーブル1本で繋いで配線を整理したいな」と考えて、購入しました。 現在使用しているMacBookにはType-Cのポートしかないため、給電、ディスプレイ出力、Webカメラ接続などをハブで行うと配線がゴチャゴチャしてしまい、スッキリしていませんでした。PowerExpand 9-in-1を購入してからは、配線がスッキリしたことや、私用のMacBookと社用のMacBookを簡単に切り替えることができることなど、とても満足しています。

エルゴトロン LXデスクマウントアーム

f:id:excite-kazuki:20211202185627j:plain

このモニターアームは「モニターアームがカッコイイ!欲しい!」という単純な理由で購入しました💪 実際に使ってみると、モニターを最大まで低く配置することができたり、モニターの下にキーボードを置くことができたりしてよかったです。あとは見た目がカッコイイのが最高!

AfterShokz OpenMove

f:id:excite-kazuki:20211202185543j:plain

この骨伝導イヤホンは、AirPods Proを使ってミーティングをしていたときに、耳の中を傷つけてしまったため、耳をふさがないイヤホンが欲しいと考えて、購入しました。 「骨伝導イヤホンってしっかり音が聞こえるのかな...?」と懐疑的でしたが、ミーティングは問題なく行えています。 OpenMoveは外部の音をあまり拾わないことや、Type-Cで充電できることなど魅力がたくさんあります。 たまに、大きな音が流れてくると、イヤホンが震えるため、ゾワッ としてしまうのがつらいところ...😵 好きな音楽を流しながら集中して作業したい場合は、ヘッドホンに切り替えるとよさそうです。

オカムラ コンテッサセコンダ

f:id:excite-kazuki:20211202193621j:plain

このオフィスチェアを購入するまでは、IKEAの3000円ほどで購入した椅子を使っていました。 ほぼ毎日がリモートワークということもあり、「腰は大事にしたほうがよい」「よい椅子を買うと幸せになれる」といった記事を多く見たため、「そこまで言うなら買うぞ!」と思いオフィスチェアを購入しました。

長時間作業しても腰が痛くなることはなく、座り心地も最高なため、ずっと座って作業できます! ただ、座りっぱなしは身体によくなたいめ、定期的に立ったり身体を動かすように気をつけていきたいです。

IKEA デスク

f:id:excite-kazuki:20211202184723j:plain

このデスクは天板と2本の脚と引き出しユニットを組み合わせて構築しています。 当初、昇降式デスクも視野に入れていましたが、「結局使うことないかな〜」と思い、それなら引き出しを置こうと思ってこの構成にしています。 横幅が186cmもあるため、一般的な横幅120cmほどのデスク比べてもかなり広いため、余裕をもって色々なものを置くことができたり、引き出しに収納できたりして満足しています。

おわりに

リモートワークを快適に行うために、色々なものを購入してきました。 購入金額の合計を出すのが怖いですね...💸 自宅では職場と違って色々とカスタマイズできるのが魅力的かなと思います。

ここまで読んでいただきありがとうございました!

AWS SDK for PHP を利用してSESで添付ファイル付きのメールを送信する

こんにちは 👋

エキサイトホールディングス Advent Calendar 2021の3日目は、エキサイト株式会社の伊藤(🐦 @motokiito2)が担当させていただきます!

今回は、AWS SDK for PHP を利用して、SESで添付ファイル付きのメールを送信する方法について書かせていただきたいと思います!

なお、SDKの導入や SesClient() の利用方法については本記事では割愛させていただきます。

添付ファイルつきメールを送信する方法

AWS公式のチュートリアルで利用されている sendEmail() は宛先、送信元、本文、件名、文字コード等を渡すだけでメールを送信することができます。

しかし、 sendEmail() には添付ファイルを指定するパラメータが存在しないため、メールのheaderとbodyを自分で成形して送信する sendRawEmail() を利用する必要があります。

docs.aws.amazon.com

sendRawEmail() の利用

sendRawEmail()sendEmail() とは異なり、メールヘッダおよびコンテンツを自前で整形して渡す必要があります。

この例では、送信元 $from , 宛先 $to を渡していますが、必須パラメータは RawMessageのみなので、RawMessageのヘッダ内に含めても問題ないです。

<?php

$this->sesClient->sendRawEmail([
    'Destinations' => [
        $values['to'],
    ],
    'Source'     => $values['from'],
    'RawMessage' => [
        'Data' => $rawMessage,
    ],
]);

sendRawEmail() の詳細は下記ドキュメントを参照してください。

https://docs.aws.amazon.com/aws-sdk-php/v3/api/api-email-2010-12-01.html#sendrawemail

RawMessage

RawMessageを用意する方法はいろいろありますが、この例ではシンプルに文字列で用意します。

SESの設定セットを利用している場合は、ヘッダー部に X-SES-CONFIGURATION-SET: を含める必要があります。

また、sendEmail() がよしなにやってくれている部分を自前で用意する必要があるので、注意してください。

  • SubjectのMIMEエンコードを忘れない

  • 添付ファイルを入れる場合にはメッセージの Content-type を multipart/mixed にしておく必要がある

など..

<?php

private function generateRawMessage(
    string $from,
    string $to,
    string $subject,
    string $body,
    string $fileName,
    string $fileStringBase64Encoded
) : string {
    $boundaryString = '__BOUNDARY__';

    return 'To: ' . $to . "\n"
         . 'From: ' . $from . "\n"
         . 'X-SES-CONFIGURATION-SET: ' . $this->config['aws']['ses']['configuration_set'] . "\n"
         . 'Subject: ' . mb_encode_mimeheader($subject, 'UTF-8') . "\n"
         . "MIME-Version: 1.0\n"
         . 'Content-Type: multipart/mixed; boundary="' . $boundaryString . '"' . "\n\n"
         . '--' . $boundaryString . "\n"
         . "Content-Type: text/html; charset=\"UTF-8\"\n\n"
         . nl2br($body) . "\n\n"
         . '--' . $boundaryString . "\n"
         . 'Content-Type: application/octet-stream; name="' . $fileName . '"' . "\n"
         . "Content-Transfer-Encoding: base64\n"
         . 'Content-Disposition: attachment; filename="' . $fileName . '"' . "\n\n"
         . $fileStringBase64Encoded
         . '--' . $boundaryString . "\n";
}

添付ファイル

添付ファイルは、ファイルをbase64エンコードした文字列を用意します。

<?php

private function generateFileStringBase64Encoded(
    string $fileAbsolutePath
) : string {
    if (!file_exists($fileAbsolutePath)) {
        return false;
    }

    return chunk_split(base64_encode(file_get_contents($fileAbsolutePath)));
}

メール送信メソッドサンプル

これまでの実装を利用した送信メソッドのサンプルコードをのせます。 (SesClientとLoggerはクラス内でコンストラクタインジェクションしています)

<?php

private function sendEmailWithAttachment(
    string $from,
    string $to,
    string $subject,
    string $messageBody,
    string $fileAbsolutePath,
    string $fileName
) : void {
    $fileStringBase64Encoded = $this->generateFileStringBase64Encoded($fileAbsolutePath);

    if (!$fileStringBase64Encoded) {
        throw MailSendFailedException::factoryMailSendFailedException('attachment file is invalid');
    }

    $rawMessage = $this->generateRawMessage(
        $from,
        $to,
        $subject,
        $messageBody,
        $fileName,
        $fileStringBase64Encoded
    );

    try {
        $result = $this->sesClient->sendRawEmail([
            'Destinations' => [
                $to,
            ],
            'Source'     => $from,
            'RawMessage' => [
                'Data' => $rawMessage,
            ],
        ]);
        $messageId = $result['MessageId'];
        $this->logger->info("Completed to sent, messageId: $messageId");
    } catch (AwsException $e) {
        $this->logger->error('[MailSendError] ErrorMessage: ' . $e->getMessage());

        throw MailSendFailedException::factoryMailSendFailedException('mail send error');
    }
}

最後に

AWS SDK for PHPの SesClientは、シンプルなメールを送信する場合は実装が楽なのですが、メールヘッダを弄る必要があったり、今回のようにファイルを添付したい場合などはRawMessageを扱う必要があり、実装が少し複雑になります。

同じようなケースで sendRawEmail() を利用しなければいけない場合に、この記事が参考になってくれればとても嬉しいです!


弊社アドベントカレンダーの他の記事も是非!

qiita.com

WebViewの高さをローディング時に動的に変える方法

はじめに

2021年度アドベントカレンダーの2日目の担当の高野です。メディア事業部のアプリではWebViewを使うことが多く、その備忘録として残します。

動作バージョン

Flutter 2.5.1
Dart 2.14.0
webview_flutter 2.1.1

実装

今回はwebview_flutterを使用していますが、他のWebViewライブラリでもおそらく同じだと思います。
まず初めに動的にWebViewの高さを変更するためにはSizedBoxまたはContainerでWebViewを包みます。

Container(
  height: controller.webViewHeight.value,
  child: WebView(
    navigationDelegate: (request) => controller.onRequestNavigation(request: request),
    javascriptMode: JavascriptMode.unrestricted,
    onPageFinished: (_) => controller.onPageFinished(),
    onWebViewCreated: (webViewController) => controller.onWebViewCreated(controller: webViewController),
  ),
);

ここのheightにWebViewの高さを取得した後に入れることで動的に高さを変えることができます。
このWebViewのonPageFinishedの発火時に以下コードを発火させることによって取得できます。Javascriptを使用するので JavascriptMode.unrestrictedjavascriptMode に指定しておいてください。

Future<double> _getWebViewHeight() async {
  try {
    final rawWebviewHeight = await controller!.evaluateJavascript('document.body.offsetHeight;');
    final webviewHeight = double.parse(rawWebviewHeight);
    return webviewHeight;
  } catch (e) {
    Logger.error(e);
    return 1000;
  }
}

これであとはこの関数の返り値をContainerやSizedBoxのheightに指定してあげれば取得時にbuild()が走るので更新が行われます。

まとめ

どこが一番重要かと言うと

await controller!.evaluateJavascript('document.body.offsetHeight;');

ここですね、これによってロードしたページ(body)の高さを取得することができます。

最後に、弊社では採用もバシバシ実施しているので興味のあるかたがいましたらご応募ください。
https://www.wantedly.com/companies/excite

Advent Calendar 2021を引き続き楽しんでいただけると嬉しいです。

SpringBoot x Gradleマルチプロジェクトで環境構築をする

エキサイト株式会社佐々木です。アドベントカレンダー2021の2ページ目の1日目になります。メディア事業部では、SpringBoot x Gradleマルチプロジェクトを使用して、モジュラーモノリスのような構成で各メディアのリビルドの開発を進めています。既存のリポジトリは1メディアで30〜50リポジトリが存在し、リポジトリの行き来で見通しが悪くなっているので、リビルドではモノリポ x マルチプロジェクト形式にしています。

Gradleマルチプロジェクトとは

Gradleマルチプロジェクトは、1つのメインプロジェクト内にいくつものサブプロジェクトが存在する構成になります。

設定

設定には、build.gradleとsettings.gradleを使用します。

settings.gradle

マルチプロジェクトで運用するには、settings.gradleにプロジェクト名を追記します。

仮に、web,batch,api,usecase,service,repositoryのように各層ごとに分けるようにすると下記のようになります。

rootProject.name = 'demo'

include "web"
include "batch"
include "api"
include "service"
include "repository"
include "domain"

build.gradle

settings.gradleで分割したモジュールをbuild.gradleに反映していきます。

plugins {
    id 'org.springframework.boot' version '2.6.1'
    id 'io.spring.dependency-management' version '1.0.11.RELEASE'
    id 'java'
}

subprojects {   // ★1

    apply plugins: "java"    // ★2
    apply plugin: "org.springframework.boot"
    apply plugin: "io.spring.dependency-management"

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

    repositories {
        mavenCentral()
    }

    configurations {
        compileOnly {
            extendsFrom annotationProcessor
        }
    }

    dependencies {
        compileOnly 'org.projectlombok:lombok'
        developmentOnly 'org.springframework.boot:spring-boot-devtools'
        annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
        annotationProcessor 'org.projectlombok:lombok'
        testImplementation 'org.springframework.boot:spring-boot-starter-test'
    }

    test {
        useJUnitPlatform()
    }

}


project(":web") {    // ★3

    bootJar {    // ★4
        enabled = true
    }

    dependencies {    // ★5
        implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
        implementation 'org.springframework.boot:spring-boot-starter-web'
        implementation 'org.springframework.boot:spring-boot-starter-webflux'
        implementation project(":service")   // ★6
        implementation project(":domain")   // ★6
        testImplementation 'io.projectreactor:reactor-test'
    }
}

project(":api") {

    bootJar {
        enabled = true
    }

    dependencies {
        implementation 'org.springframework.boot:spring-boot-starter-web'
        implementation 'org.springframework.boot:spring-boot-starter-webflux'
        implementation project(":service")
        testImplementation 'io.projectreactor:reactor-test'
    }
}

project(":service") {

    bootJar {
        enabled = false
    }

    dependencies {
        implementation project(":repository")
    }
}

project(":repository") {

    bootJar {
        enabled = false
    }

    dependencies {
        implementation project(":domain")
    }
}

project(":domain") {
    bootJar {
        enabled = false
    }
}

上記のような設定になります。★ごとに解説します。

★1

Gradleマルチプロジェクト構成にすると、プロジェクト直下以外は、すべてサプブロジェクトとなります。この時に、サブプロジェクト共通で読み込みたいようなライブラリや設定があるときに、 subprojects {} の中に書いておくと全てのサブプロジェクトに適用できます。ほぼすべてのプロジェクトで必要そうなものを記述しておきます。(LombokやSpringBootのAnnotationまわりの設定等)

★2

Gradleのプラグインの設定はサブプロジェクトごとにプラグインを使うか使わないかを決められます。逆に設定しないと使えないです。ここでよくハマっているのをみます。

★3

各サブプロジェクトごとの設定は、 project(":プロジェクト名") {} の内側に記述します。ここには、プロジェクト固有のものを設定します。例では、project(":web"){...} では、テンプレートエンジンのThymeleafとSpringBootのWeb系のライブラリを読み込んでいます。

★4

SpringBootはコンパイルすると実行可能なjarの生成が可能です。ですが、serviceプロジェクトやrepositoryプロジェクトでは、実行形式にする必要がありません(エンドポイントがないので)。そういうときには、bootJar{} を使用して、実行可能なjarを生成する・しないの設定を追加できます。webapiプロジェクトは、エンドポイントがそれぞれあるので、bootJar{enabled=true}を指定して、実行可能なjarを生成するようにします。

★5

サブプロジェクトごとに依存関係を定義できます。ここで定義したものは、そのサブプロジェクト内でしか使えないことに注意してください。

★6

ここで、サブプロジェクト内で別のサブプロジェクトを依存関係に含めています。これを行うことで、サブプロジェクト内のコードを使えるようになります。例ではwebプロジェクトは、serviceサブプロジェクトとdomainサブプロジェクトのコードにアクセスができますが、repositoryサブプロジェクトのコードにはアクセスできないことになります。

Gradleマルチプロジェクトのメリット

Gradleマルチプロジェクトのメリットは、アクセス制御が可能になことです。依存関係をサブプロジェクト単位で定義できることによって、モジュラーモノリスやミニサービスの構成を簡単に実現できます。依存関係に定義していないサブプロジェクトやライブラリを使おうとしてもコンパイルエラーになりますので、管理が楽になります。

Gradleマルチプロジェクトのような機能がない状態で、厳密に依存関係を制御しようとすると、コードレビューをするまたはチェック用のコードを書く等のことをしなければならないですが、Gradleの依存関係の設定のみでそれが可能になります。JavaにもArchUnitというライブラリはありますが、テストコードを記述する必要があります。

最後に

アドベントカレンダー1日目を書かせていただきました。他社同様エキサイトHDでもアドベントカレンダーをやっていますので、ご覧いただけると幸いです。(https://qiita.com/advent-calendar/2021/excite-hd)

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

カジュアル面談はこちらになります! meety.net

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

Spring Bootで、AWSパラメータストアから簡単にDBのパスワードを取得する方法

こんにちは。 エキサイト株式会社の三浦です。

いよいよ今年も12月になりました。 「師走」の文字通り、忙しくしている方も多いのではないでしょうか。

この「12月」ですが、我々エンジニアにとっては別の意味も持っています。 すなわち、アドベントカレンダーの時期です!

というわけで、エキサイトホールディングスの今年のアドベントカレンダーの第一回目のブログを作成させていただくことになりました!

第一回目となる今回は、Spring Bootにおける、DBのパスワード管理問題のAWSパラメータストアを使用した簡単な解決方法について書かせていただきます。

DBのパスワード管理問題

アプリケーションでは、特に一定以上の規模になってくれば、多くの場合DBからデータを取得したり書き込んだりする必要が出てくるでしょう。 この時問題になってくるのが、「DBのパスワードをどうやって管理するか」です。

方法自体は以下のようにいくつか考えられますが、DBのデータの改竄はアプリケーションにとって致命的な問題となりうるので、パスワードが漏れてしまうことは絶対に避ける必要があります。

Github等で、その他のコードと一緒にバージョン管理する

Github等でバージョン管理してしまえば管理自体は簡単ですが、何かしらの事情でパスワードが漏れてしまうリスクが増加してしまいます。 可能であれば避けるべきでしょう。

デプロイサーバ等で管理し、デプロイのたびに書き込む形式にする

例えばデプロイサーバの環境変数やどこかのファイルに保存しておき、デプロイ時にアプリケーションコードにそのパスワードを書き込む方法です。 もちろんこれでも可能ですが、何かしらの理由でデプロイサーバのデータが失われたときに、パスワードのデータが消えてしまう可能性があります。 また、デプロイサーバやデプロイ方法が変更となった場合に、移行しなければならないでしょう。

AWS RDSの場合)IAMを使用した認証方法にする

AWS RDSでは、パスワードではなくIAMを使って接続をすることができます。 これができればそもそもパスワード自体が不要ではありますが、RDSでしか使用できないことや、IAMを使用した認証方法だと接続数によっては追加オーバーヘッドが発生してしまう場合があるという問題があります。

AWSパラメータストアを使用する

AWSには、パラメータストアというサービスがあります。 これは、AWS上で様々なデータを保存しておくことが出来るものですが、

  1. パラメータストアにパスワードを置いておく
  2. それをデプロイ時に取得し、アプリケーションコードに書き込む

とすることで、安全にパスワードを管理することができます。

問題点として、パラメータストアからデータを取得するのが面倒なこと、取得したデータをアプリケーションコードに書き込むのが面倒なことが上げられますが、実はSpring Bootであれば、それらを劇的に簡単にする方法があります。

そこで今回は、最後の「AWSパラメータストアを使用」してDBのパスワードを管理する方法について、Spring Bootで簡単に行うやり方について説明します。

Spring Cloudを使ったパラメータストアとの統合

Spring Bootには、Spring Cloudというプロジェクトが存在します。 ここではAWSとの統合を容易にする様々なライブラリが提供されているのですが、その中にパラメータストアとの統合を容易にするものも存在しているのです。

以下の設定だけで、パラメータストアからパスワードを取得出来るようになります。

アプリケーションコードの設定

アプリケーションコードの設定を行います。 必要なのは、 build.gradleapplication.yml のみです。

build.gradle

バージョンは適宜変更してください。

implementation 'io.awspring.cloud:spring-cloud-starter-aws-parameter-store-config:2.3.2'

application.yml

以下以外の設定は通常通りで大丈夫です。

spring:
  application:
    name: sample.application # パラメータストアのキーに使用します
  config:
    import: 'aws-parameterstore:'
  datasource:
    password: xxxx # ここにパスワードが入ります。上書きされるので、何を書いていても大丈夫です。

aws:
  paramstore:
    region: ap-northeast-1 # 使用したいパラメータストアのregionを設定してください
    prefix: /config # パラメータストアのキーのprefixに使用します

コードとしては、なんと実質これだけで「パラメータストアからのパスワードの取得」と「取得したパスワードのアプリケーションコードへの書き込み」ができたことになります!

AWSと接続するための認証

コードとしては上記で完成ですが、パラメータストアからデータを取得するためには、AWSと接続するための認証の設定が必要となります。 方法例として、以下のものが挙げられます。

環境変数で設定する

AWS_ACCESS_KEY_IDAWS_SECRET_ACCESS_KEY という名前の環境変数を作成し、それぞれにパラメータストアにアクセスできる権限を持つIAMユーザのアクセストークン・シークレットトークンを入力します。

クレデンシャルファイルを使用する

AWS CLIaws configure コマンド等を使って、パラメータストアにアクセスできる権限を持つIAMユーザのクレデンシャルファイルを作成します。

ECSやEC2インスタンスプロファイルのクレデンシャルを使用する

実行環境のECSやEC2インスタンスプロファイルに、パラメータストアからデータを取得できるポリシーを追加します。

詳しくはこちらを御覧ください。

とりあえずローカルで確認するだけなら、環境変数かクレデンシャルファイルを使用すると良いでしょう。

パスワードの保存

ここまででパラメータストアからデータを取得する準備は整いました! 最後に、パラメータストアにデータを保存します。

キー名は、

{$aws.paramstore.prefix}/{$spring.application.name}_{$プロファイル}/spring.datasource.password

となります。 もし実行時に local というプロファイルを使用するのであれば、今回の設定の場合は

/config/sample.application_local/spring.datasource.password

というキーの値にDBのパスワードを入れれば大丈夫です。 念の為、「安全な文字列」として保存しておくと良いでしょう。

最後に

DBのパスワードは、バージョン管理する管理方法が他の方法と比べてあまりに簡単すぎるので、まだそうしてしまっているアプリケーションも多いのではないでしょうか。 保存しているGithubリポジトリをprivateにしておけばとりあえずは安全にも見えますが、例えば何かしらのヒューマンエラーでpublicにしてしまって、その間に見られてしまう可能性も考えられます。 そして、それによってDBのデータが改竄されてしまった場合、損害は計り知れないものとなってしまう場合もありうるでしょう。

幸いSpring Bootであれば、上記のように比較的簡単にセキュアな方法で管理ができるので、ぜひやってみてはいかがでしょうか。


さて、私の今回のブログは以上ですが、アドベントカレンダーはまだまだ続きます! ぜひ明日以降も御覧ください!

qiita.com

PHPを使って形態素解析と文章の類似度を出してみる

ご無沙汰しております。
taanatsuです。

今回は珍しくPHPの記事を書いていこうと思います。
ExciteといえばPHPですからね!しらんけど。

形態素解析

皆さんは「形態素解析」という言葉を耳にしたことがありますでしょうか?
機械学習だ!AIだ!と騒がれる昨今、文章の解析で使われる手法の一つがこの形態素解析です。
私は漢字が4つ以上並ぶと読めなくなるので、形態素解析という言葉が苦手ではあります。

形態素解析とは、文章を「形態素」、いわゆる名詞・動詞・形容詞・副詞のような、日本語の最小単位の単語に分割する処理のことを言います。

形態素解析器「MeCab

形態素解析を行ってくれるツールです。
今回はよく使われる「MeCab」を利用していきたいと思います。

で、Windowsの方はすいません。。。
会社のPCがMacなので、この記事はMac用になります。
私個人はWindows機を利用していて、できることは知っていますので、Windowsの方は頑張ってください!
(確かバイナリをダウンロードして、ダブルクリックしてインストールするだけだったはず…!)

MeCabのインストール

Homebrewを使えば一発です。

$ brew install mecab

以上!
……と言いたいところですが、文章を単語に分割するための「辞書」が必要になります。
辞書も入れましょう。

$ brew install mecab-ipadic

以上です。
ターミナル上でmecabとタイプしてEnterを押してみてください。
入力待ちになります。

この状態で「すもももももももものうち」と入力してEnterを押してみましょう。

$ mecab            
すもももももももものうち
すもも   名詞,一般,*,*,*,*,すもも,スモモ,スモモ
も 助詞,係助詞,*,*,*,*,も,モ,モ
もも  名詞,一般,*,*,*,*,もも,モモ,モモ
も 助詞,係助詞,*,*,*,*,も,モ,モ
もも  名詞,一般,*,*,*,*,もも,モモ,モモ
の 助詞,連体化,*,*,*,*,の,ノ,ノ
うち  名詞,非自立,副詞可能,*,*,*,うち,ウチ,ウチ

きれいに分割されていますね!
これが形態素解析です。

PHPからMeCabを使う

さて、こういう分野ではPythonブイブイ言わせていますが、弊社はPHPが盛んな会社ですので、PHPで使っていきましょう。 PHP7.1くらいまでは php-mecabというPHPMeCabを使えるようにするバインディングツールがあったのですが、どうやらPHP8には対応していなさそう…
なので、ちょっと強引ですが、PHPexecを使っていきます。
参考

<?php
$result = [];

$text = 'すもももももももものうち';
exec('echo ' . $text . ' | mecab', $result);

var_dump($result);

array(8) {
  [0] =>
  string(61) "すもも    名詞,一般,*,*,*,*,すもも,スモモ,スモモ"
  [1] =>
  string(40) "も        助詞,係助詞,*,*,*,*,も,モ,モ"
  [2] =>
  string(49) "もも      名詞,一般,*,*,*,*,もも,モモ,モモ"
  [3] =>
  string(40) "も        助詞,係助詞,*,*,*,*,も,モ,モ"
  [4] =>
  string(49) "もも      名詞,一般,*,*,*,*,もも,モモ,モモ"
  [5] =>
  string(40) "の        助詞,連体化,*,*,*,*,の,ノ,ノ"
  [6] =>
  string(63) "うち      名詞,非自立,副詞可能,*,*,*,うち,ウチ,ウチ"
  [7] =>
  string(3) "EOS"
}

形態素解析を使った文章の類似度

それでは形態素解析を使って文章の類似度を出してみましょう。
2つの文章に登場した形態素に対して、文章がその形態素を持っていれば1、持っていなければ0として計算してみましょう。

<?php
$text1 = '新宿は豪雨';
$text2 = '渋谷は豪雨';

echo cosineSimilarity($text1, $text2);

/**
 * 分かち書きのリストを作成します
 * 
 * @param string $text 分かち書きを作成したい文章
 */
function getWakachiList(string $text): array {
    $result = '';
    exec('echo ' . $text . ' | mecab -Owakati', $result);

    if (!is_array($result) || count($result) !== 1) {
        return [];
    }

    return explode(' ', $result[0]);
}


/**
 * 文章の類似度をコサイン類似度を用いて求めます
 * 
 * @param string $text1 文章1つ目
 * @param string $text2 文章2つ目
 */
function cosineSimilarity(string $text1, string $text2): float {
    // 文章を形態素に分解
    $text1Corpus = getWakachiList($text1);
    $text2Corpus = getWakachiList($text2);

    // 2つの文章の形態素を抽出
    $allCorpus = array_unique(array_merge($text1Corpus, $text2Corpus));

    // コサイン類似度の計算に必要な分子分母の変数
    $c = 0;
    $m1 = 0;
    $m2 = 0;

    foreach ($allCorpus as $word) {
        // 文章1に対象の形態素があるかどうか(あれば1、なければ0)
        $n1 = (array_search($word, $text1Corpus) !== false) ? 1 : 0;
        // 文章2に対象の形態素があるかどうか(あれば1、なければ0)
        $n2 = (array_search($word, $text2Corpus) !== false) ? 1 : 0;

        // コサイン類似度に利用する分子分母の数値を計算
        $c += ($n1 * $n2);
        $m1 +=  $n1 * $n1;
        $m2 += $n2 * $n2;
    }

    // コサイン類似度の計算
    if ($m1 === 0 || $m2 === 0) {
        return 0;
    }

    return $c / (sqrt($m1) * sqrt($m2));
}

新宿は豪雨
渋谷は豪雨

以上のテキストから形態素を抽出します。
また各文章にその形態素があるかないかも確認します。

新宿 豪雨 渋谷
テキスト1 1(形態素を持っている) 1 1 0(形態素を持っていない)
テキスト2 0 1 1 1

上記の1と0を使って、コサイン類似度を算出します。

このように形態素解析を使うと、単語単位で分解できるため、精度良く文章の類似度を出すことができます。
それでは今回はこのあたりで!

業種交流LT会【広告チーム編】を開催しました📃

iXITの小長谷です。

11月19日に、2回目の開催となる業種交流LT会を行いました🎉

今回は広告チーム編というテーマで、普段広告に関わる業務を行っている方々に登壇していただきました!

今年8月には業種交流LT会【クリエイティブ編】も開催しています。そちらも以下からぜひご覧ください。 

tech.excite.co.jp

発表内容

メディア事業部から2名、ヘルスケア事業部から2名の、計4名に普段の業務内容などを発表していただきました。

メディア事業部の広告について

メディア事業部の2名からは、それぞれ編集担当、営業担当として行っていること、その中で工夫している点・苦労している点などを発表していただきました。

営業では、案件獲得のために代理店・広告主となる企業へのアプローチを行っていること、編集部では、営業の方と一緒に案件獲得から始まり、記事制作、公開、継続提案までを行っていることをお話しいただきました。

苦労している点として

  • コロナ禍でオフラインによる新規担当者との出会いが減ってしまったこと

  • 確定案件を制作しながら、自主提案企画も併行して進めていること

  • 並行して担当している案件が多く、スケジュール管理が大変であること

などがあり、工夫している点として

  • クライアントと、社内の編集部の方との齟齬が無いように連携を大切にすること

  • 自身の経験や周囲の体験を企画に落とし込むこと

  • 営業と編集で常に情報共有、意見交換を活発に行うこと

などがあるとのことでした。

ヘルスケア事業部の広告について

ヘルスケア事業部の2名からは、広告計測のこと、担当サービスの新規課金を促すための広告運用について発表していただきました。

広告の計測については、間接コンバージョンと直接コンバージョンの説明や、異なる広告媒体でも平等に計測することができる「アドコード」の設定などについてお話しいただきました。

広告運用では、リスティング広告についての説明から、広告の表示順位に関わる「広告ランク」に関してのことと、広告ランクを上げるために取り組んでいる「広告とランディングページの品質」などについてお話しいただきました。

まとめ

他業種のことを知り、交流するきっかけになることを目的とした業種交流LT会の第2回目として、今回は「広告」というテーマで開催しました。

普段なかなか知る機会がない業務内容や、そこで働く方々の知見を得ることができ、とても良い機会になったと感じています!

今後のLT会レポートもお楽しみに!

ECSで複数のターゲットグループを紐付けるときの注意点

こんにちは。 エキサイト株式会社の三浦です。

AWSをインフラとしている場合、アプリケーションを動かす環境としてECSは選択肢の大きな一つです。 それがWebアプリケーションであれば、ECSとALBのターゲットグループを紐付けることが多いでしょう。

実はECSは、2つ以上のターゲットグループと紐付けることができます。 ですが、1つのみ紐付ける場合と比べると注意点があります。

今回は、その注意点について説明していきます。

なおこちら、2021/11/22現在の情報であり、将来的には変わっている可能性があるのでご注意ください。

ECSとターゲットグループ

ECSは、AWSが提供しているコンテナ稼働のマネージドサービスです。 公式ページには、以下のように説明されています。

Amazon ECS は、フルマネージドコンテナオーケストレーションサービスであり、コンテナ化されたアプリケーションを簡単にデプロイ、管理、およびスケーリングできます。

マネージドであり管理が非常に容易なため、AWSをインフラとしていてアプリケーションをコンテナでデプロイしたい場合は、ECSを使うことが選択肢の大きな一つとなります。 また、提供したいアプリケーションがWebサービスである場合、ECSとALBのターゲットグループを紐付けることによりALB経由でアクセスを流すことができます。

多くの場合、ECSとターゲットグループは 1 : 1 で紐付けると思います。 ただ、例えばinternal(VPC内のみ)とexternal(internet経由)の両方のALBを使用する必要があるなど、2つ以上のターゲットグループをECSに紐付けたいこともありえます。

実はそうした場合、1つのECSに対して2つ以上のターゲットグループを紐付けることができます。

f:id:excite-takayuki-miura:20211122113324p:plain
ターゲットグループが1つの場合

f:id:excite-takayuki-miura:20211122114121p:plain
ターゲットグループが2つの場合

非常に便利な機能なのですが、1つ注意点があります。

2つ以上のターゲットグループを紐付ける注意点

ECSには「オートスケーリング」という機能があります。 これは文字通り、コンテナへのアクセス量や負荷に応じて自動的にコンテナ数を増減してくれる機能であり、例えば時間や情勢によってアクセス量が変わりうるWebサービスなどで非常に有用な機能です。

このオートスケーリングでは、基本的に3つの指標をもとにコンテナの増減数を決定します。

ECSServiceAverageCPUUtilization

平均のCPU使用率が指定した値を超えたらコンテナ数を増やす

ECSServiceAverageMemoryUtilization

平均のメモリ使用率が指定した値を超えたらコンテナ数を増やす

ALBRequestCountPerTarget

ALBからの1コンテナ平均リクエスト数が指定した値を超えたらコンテナ数を増やす

これらはそれぞれサービスの特性に応じて使い分ければよいのですが、実は2つ以上のターゲットグループに紐付けたコンテナでは、最後の ALBRequestCountPerTarget を使用するのは危険です。

f:id:excite-takayuki-miura:20211122114906p:plain
ターゲットグループが1つの場合

f:id:excite-takayuki-miura:20211122125321p:plain
ターゲットグループが2つの場合

選択自体はできますが、実はこちら、片方のターゲットグループしか計算されていません。

計算されている方のターゲットグループにアクセスがあった場合

f:id:excite-takayuki-miura:20211122131831p:plain
計算されている方のターゲットグループにアクセスがあった場合のグラフ

f:id:excite-takayuki-miura:20211122131924p:plain
計算されている方のターゲットグループにアクセスがあった場合のコンテナ数

計算されていない方のターゲットグループにアクセスがあった場合

f:id:excite-takayuki-miura:20211122150139p:plain
計算されていない方のターゲットグループにアクセスがあった場合のグラフ

f:id:excite-takayuki-miura:20211122150210p:plain
計算されていない方のターゲットグループにアクセスがあった場合のコンテナ数

このように、片方のターゲットグループしか計算されないので、基本的に2つ以上のターゲットグループを紐付けたECSでは、 ALBRequestCountPerTarget は使わないほうが良いでしょう。

最後に

こちら、実は昔はそもそも2つ以上のターゲットグループを紐付けたECSでは ALBRequestCountPerTarget を選択すること自体が不可能でした。 それが選べるようになったということは、徐々に改修がされていっているのかもしれません。

こちらは2021/11/22現在の情報ですが、将来的には適切に処理がなされるようになるかもしれないので、気になる方は注意してみると良いでしょう。

SpringBootで対話的インターフェースをSpring Shellで実装する

エキサイト株式会社エンジニアの佐々木です。SpringBootは、Webアプリケーションフレームワークというのが一般的な認識可と思いますが、結構なんでもできたります。今回はマイナーな、インタラクティブCUIアプリケーションを作る、SpringShellの機能についてご紹介します。(https://docs.spring.io/spring-shell/docs/2.0.1.RELEASE/reference/htmlsingle/)

はじめに

運用フェーズで極稀に操作することとかあるとおもいます(あまりしたくはないですが)。そういうときに、Shellが役に立つことがあります。

設定

build.gradle

依存関係を解消するのに、下記のように修正します。

依存関係の解決と、 ./gradlew bootRun で実行したときに標準入力を有効にするオプションを追加します。

dependencies {
    ....
    implementation 'org.springframework.shell:spring-shell-starter:2.0.1.RELEASE'   // これを追加する
    ....
}

bootRun {
    standardInput = System.in   // ./gradlew bootRun で実行したときに、標準入力を有効にする為
}

application.properties

下記を入れないと循環参照エラーで起動しないのを回避するのに追加します。

....
spring.main.allow-circular-references=true
....

コード

ここからはWebアプリケーションと同じようなエンドポイントのようなものを書いていきます。

package com.example.batch.controller;

import org.springframework.shell.standard.ShellComponent;
import org.springframework.shell.standard.ShellMethod;

@ShellComponent
public class DemoShellController {

    @ShellMethod(value = "足し算をする", key = "sampleAdd" , group = "calc")
    public Integer add(int a, @Max(10) int b, @ShellOption(value = "--optional_c" , defaultValue = "1") int c){
        return a + b + c;
    }
}

このような感じで記述できます。@ShellMedthodでShellのエンドポイントを定義できます。valueアトリビュートは必須設定ですが、その他は任意になります。コマンド名はデフォルトメソッド名ですが、keyをつけることにより変更できます。@MaxでValidationも付与でき、@ShellOptionで引数名変更やデフォルト値を入れることも可能です。デフォルト値を入れないと、必須パラメータ になるので、注意してください。

実行

動作結果としてはこのような感じになります。 補完が効くのも便利です。

shell:>sampleAdd 1 10
12
shell:>sampleAdd 1 11
The following constraints were not met:
        --b int : 10 以下の値にしてください (You passed '11')
shell:>sampleAdd 1 11 10
The following constraints were not met:
        --b int : 10 以下の値にしてください (You passed '11')
shell:>sampleAdd 1 10 10
21
shell:>

f:id:earu:20211122100917g:plain

まとめ

Shellアプリケーションが簡単に実装できたかとおもいます。Java + SpringBoot + Gradleではマルチプロジェクト、モジュラーモノリス構成が簡単に構築できますので、内部の実装はWebアプリケーションで開発した実装を使用することが可能です。SpringBoot依存にはなってしまいますが、サービスが大きくなってきたらマイクロサービスに切り出すでいいかと思っております。

今回は、インタラクティブ(対話的)なShellアプリケーションでしたが、同じソースコードバッチ処理も可能ですので、次回ご紹介いたします。

おわりに

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

カジュアル面談はこちらになります! meety.net

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

以上となります。

DNSの名前衝突に気を付けよう

こんにちは。インフラストラクチャーグループの宝田です。

DNS 運用で気を付けるべき名前衝突問題に出くわしたので、事象の発生経緯と今後の対策を記しておきます。DNS の名前衝突問題とは、組織が内部的に使う Top Level Domain(TLD)と インターネットで利用できる TLD が重複してしまうことにより、DNSの動作が期待するものとは違った動作になることを指します。

経緯

サービスで使用中のとある AWS アカウントで AWS ClientVPN(以降 VPN と記載します) を構築し利用していたところ、VPN を接続している時に Slack の通話ができないという現象が発生しました。他の通話アプリ、例えば Google Meet や Zoom 等は問題なく使用できるが Slack だけ通話機能が一切使えないという状態でした。
とりあえず VPN を接続した状態で Slack チャンネルで /call --test と打って通話機能のテストを試したところ、以下のように音声接続がうまくいっていないことが分かりました。

f:id:excite-takarada:20211118101710p:plain
コールテスト画面

Slack 通話のどのフェーズで失敗しているか把握したいので Slack のサポートに問い合わせてみると下記の方法で通話ログを取得できることを教えてもらいました。

  1. Slack アプリの上部のメニューバーで、「ヘルプ」>「トラブルシューティング」>「再起動してネットワークログを取得」を選択します(Windows 10 の場合は、左上の 3 本の横線をクリックして、「ヘルプ」>「トラブルシューティング」>「再起動してネットワークログを取得」を選択します)

  2. 再起動を求めるポップアップが表示されます。「Slack を再起動する」 をクリックします

  3. デスクトップアプリが自動的に再起動し、「ログの進行中です」というポップアップが表示されます

  4. 調査したい内容を再現します

  5. ロギングを 2 分間実行して、問題がキャプチャされたことを確認してから、ポップアップにある「ロギングを停止」をクリックします

  6. Slack のダウンロード先のフォルダに表示される .zip ファイルを解凍した中にある net.log がネットログになります

ただし、通話ログの見方は公開されていないようなので自ら解析するなら雰囲気で理解する必要があります。今回はサポートにお願いして通話ログの中身を解析してもらいました。
解析してもらったところ、Slack 通話で発生する signal.m1.an1.app.chime.awsnearest-media-region.l.chime.aws の名前解決が失敗していることが分かりました。余談にはなりますが、上記の URL からも分かる通り Slack は Amazon Chime SDK を使っているようです。

aws.amazon.com

ここまでくると、aws ドメインの名前解決が失敗する理由はすぐ分かりました。原因は、VPNDNS 設定でプライベートホストゾーンの名前解決を可能にしており、そのプライベートホストゾーンに aws というホストゾーンが存在しているからでした。つまり、 VPN を繋いだ状態で aws ドメインを名前解決するとプライベートホストゾーンを参照してしまい、そのプライベートホストゾーンには signal.m1.an1.app.chime.awsnearest-media-region.l.chime.aws はレコードとして当然登録されていないので名前解決が失敗していたということです。

上記の事象は、AWS が管理している aws ドメインを内部向けドメインに使用してしまったという名前衝突が根本的な原因となります。

対策

今回は弊社の内部向けで使っているドメインが他組織で管理しているドメインと重複してしまい、名前解決に不具合が起きた結果、外部のサービスが利用できなくなる現象がおきました。

名前衝突の対策としては JPNIC の記事にある通り、ドメインを取得してそのドメインを内部向け DNSドメインとして利用することにしました。これで他組織で管理されているドメインと重複することが原理的になくなります。

内部利用向けのTLDに関しては、インターネットで利用できるグローバルなドメイン名を使用する1

まとめ

DNS の名前衝突の事例について紹介しました。内部向けドメインは組織で取得したものを利用しましょう。
最後に補足として、DNS のサーチリスト2と呼ばれるドメイン名の補完機能を使うと、ローカルネットワークのドメイン名を名前解決するつもりがインターネット上の TLD に対して名前解決してしまうという今回の事例とは反対の現象も起こり得ますので、サーチリストも極力使わないということも合わせて押さえておくといいと思います。

JJUG CCC 2021 Fallに弊社社員が登壇します!

弊社社員がJJUG CCC 2021 Fallで登壇いたします。 お時間のある方はぜひご覧ください!

登壇情報

登壇者: s-nakao@エキサイト株式会社 (@openjdk17) | Twitter

日時: 2021/11/21 10:00〜

場所:Track D

タイトル:エキサイトブログ刷新に向けて序章 - APIを一つに

プロポーザルURL: fortee.jp

JJUG CCC 2021 Fall

開催日時:2021/11/21

開催場所:オンライン開催

公式サイト:

jjug.doorkeeper.jp

公式TwitterJJUG (@JJUG) | Twitter

JavaのRestTemplateでタイムアウトを設定する方法

こんにちは。 エキサイト株式会社の三浦です。

APIにアクセスする際、一定時間までにレスポンスが返ってこなかったらエラーとして処理したい、というのはよくある要望かと思います。 そのために使用するタイムアウト設定について、JavaのRestTemplateで設定する方法を説明します。

タイムアウト

APIなどにネットワーク経由でアクセスする場合、必ずしもレスポンスがすぐに返ってくるとは限りません。 API自体に不具合があったり、アクセスが急増していたり、ネットワークに問題があったりなど、様々な理由でレスポンスが返ってくるまでに時間がかかってしまう可能性があります。

バッチなど、多少時間がかかっても問題がないアプリケーションであれば大丈夫かもしれませんが、Webページ用のサーバであればそうも言っていられないこともあるでしょう。 そのような場合、「指定した一定時間でレスポンスが返ってこなかったらエラーとして処理する」というタイムアウト設定をするのが一般的です。

RestTemplateにおけるタイムアウト設定

RestTemplateでは、以下のようにすればタイムアウト設定ができます。

build.gradle

// バージョンは適宜変更してください
implementation 'org.apache.httpcomponents:httpclient:4.5.13'

タイムアウト設定

import org.apache.http.impl.client.HttpClientBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;

import java.util.Arrays;

@Configuration
public class RestConfig {

    @Bean
    public RestTemplate restTemplate() {
        var clientHttpRequestFactory = new HttpComponentsClientHttpRequestFactory(HttpClientBuilder.create().build());

        // ここでタイムアウトを設定

        // 接続が確立するまでのタイムアウト
        clientHttpRequestFactory.setConnectTimeout(500);

        // コネクションマネージャーからの接続要求のタイムアウト
        clientHttpRequestFactory.setConnectionRequestTimeout(500);

        // ソケットのタイムアウト(パケット間のタイムアウト)
        clientHttpRequestFactory.setReadTimeout(1000);

        return new RestTemplate(clientHttpRequestFactory);
    }
}

終わりに

タイムアウトを設定していないと、思わぬところでWebページの応答速度が大きくなってしまう可能性があります。 Webページの要件ごとにタイムアウト時間は異なってくると思うので、適切な設定をしましょう。

外部公開勉強会を開催しました!

技術的な知見を共有しあうことを目的に、外部公開勉強会を開催しました。

第1回のテーマは「前へ進むための新しい取り組み」でした。

長年サービス運営をしてきたエキサイト社員とiXIT社員が、どのようにして技術的負債に向き合いサービスを成長させていったのか、その成功と失敗について話していただきました。

今後も外部公開勉強会を開催していきますので、見に来ていただけると幸いです。

excite.connpass.com

発表資料

クラウドネイティブ化への第一歩

speakerdeck.com

僕たちがすべきことはリファクタリングなのか

www.slideshare.net

約10年続くスマホゲームアプリのDB負荷と向き合う話

www.slideshare.net

エキサイトブログ刷新に向けて 序章 - APIを一つに

www.slideshare.net

JacksonでXMLをJavaオブジェクトに変換する

はじめに

エキサイト株式会社 21卒 バックエンドエンジニアの山縣です。 既存サービスのリビルドするにあたり、外部APIを呼び出してXMLを取得してJavaオブジェトに変換する処理を書きました。 Jacksonを使用したXMLJavaオブジェクト変換についての記事は少なかったため本記事にて紹介します。

導入

本記事で使用するサンプルのXMLとそれに対応するJavaクラスです。

サンプルで使用したXML

<Response>
    <ItemList>
        <Item id="a123" name="orange"/>
        <Item id="b234" name="lemon"/>
        <Item id="c345" name="apple"/>
    </ItemList>
    
    <ResultList>
        <Result num="1">オレンジ</Result>
        <Result num="2">レモン</Result>
        <Result num="3">リンゴ</Result>
    </ResultList>
</Response>

XMLに対応するJavaのクラス

package com.example.demo.model;

import java.util.List;

import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlText;
import lombok.Data;

@Data
@JacksonXmlRootElement(localName = "Response")
public class Basket {
    @JacksonXmlProperty(localName = "ItemList")
    @JacksonXmlElementWrapper(useWrapping = false)
    private List<Item> items;

    @JacksonXmlProperty(localName = "ResultList")
    @JacksonXmlElementWrapper(useWrapping = false)
    private List<Result> results;

    @Data
    public static class Item {
        @JacksonXmlProperty(isAttribute = true)
        private String id;
        @JacksonXmlProperty(isAttribute = true)
        private String name;
    }

    @Data
    public static class Result {
        @JacksonXmlProperty(isAttribute = true)
        private Integer num;
        @JacksonXmlText
        private String text;
    }
}

XMLのルートの下からマッピングする

XMLのルートの下からマッピングするには、@JacksonXmlRootElementを付与する必要があります。

@JacksonXmlRootElement(localName = "Response")
public class Basket { ... }

XMLのリストをマッピングする

XMLのリストをマッピングするには、下記2つのアノテーションが必要です。

@JacksonXmlProperty(localName = "ResultList")
@JacksonXmlElementWrapper(useWrapping = false)
private List<Result> results;

@JacksonXmlElementWrapperは、useWrappingの他にlocalNameも設定できます。 このとき、@JacksonXmlPropertyを消して、@JacksonXmlElementWrapper(useWrapping = false, localName = "ResultList") と書くと動作するように見えますが、UnrecognizedPropertyExceptionとなってしまいマッピングすることができませんでした。 そのため、上記のように2つのアノテーションを記述しなくてはなりません。

RestTemplateでXMLJavaオブジェクトの変換をする

最後に、下記のように記述することで、外部APIから取得したXMLJavaオブジェクトの変換を実現することができます。

final ResponseEntity<Basket> response = restTemplate
    .exchange(
        "http://example.com",
        HttpMethod.GET,
        httpEntity,
        Basket.class
);

おわりに

Jacksonを使用したXMLJavaオブジェクト変換についてまとめました。 JSONJavaオブジェクトの記事は数多くありますが、XMLJavaオブジェクトの記事はあまり見当たらなかったので、 XMLの変換で困っている人の助けになれば幸いです!

エキサイトは「DroidKaigi2021」に協賛しました!

エキサイトは「DroidKaigi2021」にSUPPORTERSとして協賛しました。今回は惜しくも弊社から登壇者はいませんでしたが、グループ会社であるRadiotalkから牧山さんが登壇しました。 来年は弊社から登壇者が出るように技術力を切磋琢磨し、向上させていきたいです。

登壇内容

登壇者:牧山

タイトル:Androidエンジニアが1人という不安と向き合う

内容:Androidエンジニア不足が叫ばれる昨今、1つのAndroidアプリに対して1人のエンジニアという状況が増えてきているように感じています。このような状況下では、スキル面・キャリア面での不安を持つ方も多いのではないでしょうか。私もそうでした。私のAndroidエンジニアキャリアもほとんどがAndroidエンジニアが1人での開発を経験しています。  本セッションでは、私の経験を踏まえ、どのようにAndroidエンジニアとしての技術を学び、どのように不安と向き合っているのかを紹介します。

  • Androidエンジニアとしての技術の学び方

 ・キャッチアップの方法

 ・ 優れたソースコードを読む

  • 不安と向き合うために

 ・スコープを広げてソフトウェア開発を学ぶ

 ・サービスへの理解を深めソフトウェア開発に還元する

 ・サービスの未来を見据えた設計/開発

 ・エンジニアリングでサービスを前にすすめる

アーカイブURL:https://www.youtube.com/watch?v=5M0xJLdAcAE

DroidKaigi2021

公式サイト:https://droidkaigi.jp/2021/

公式チャンネル:https://www.youtube.com/c/DroidKaigi