JEP378を読んで、テキスト変換の方法をおさらいする

エキサイト株式会社エンジニアの佐々木です。Java17が昨年リリースされ、JEP378のテキストブロックがLTSとして入ったので改めてJEP378を読むとテキスト変換の方法がいくつか書いてあったので、復習がてらおさらいします。

openjdk.java.net

テキストブロックとは?

Javaには複数行のテキストをまとめて扱える仕様がありませんでした。複数行ある場合は、1行ずつ文字列を連結する必要がありました。

String s = "<h2>%s</h2>" +
                "<h3>%s</h3>" +
                "";

テキストブロックは、これをまとめて扱えるようにする仕様変更になります。""" 文字列 """ このような形で記述します。

String s = """
                <h2>%s</h2>
                <h3>%s</h3>
                """

ちょっとしたSQLやテンプレートなど複数行で書きたいことは多いと思います。ちょっと便利になりました。では、テキストブロック内に変数を入れたい場合はどうするのかを見ていきましょう。

前提

リクエストパラメータで設定したパラメータでH2H3をそれぞれ表示する簡単なWebアプリケーションを作ります。下記のようなリクエストとレスポンスになる想定です。

curl http://localhost:8080?h2=hogehoge&h3=fugafuga

<h2>hogehoge</h2>
<h3>fugafuga</h3>

String.format を使用する

JDK1.0からあるメソッドになります。String.format(String s , args...) の形になります。書いてみて結構読みづらいので、他の方法がある場合はそちらを見たほうがいいですね。

@RestController
public class DemoController {

    @GetMapping
    public String index(@RequestParam String h2, @RequestParam String h3) {

        return String.format("""
                <h2>%s</h2>
                <h3>%s</h3>
                """, h2, h3);
    }
}

String.formatted を使用する

テキストブロック内の文字列を変数で置換したいときには、formattedメソッドを使います。テキストブロック内の%sを置き換えてくれます。

@RestController
public class DemoController {

    @GetMapping
    public String index(@RequestParam String h2, @RequestParam String h3) {

        return """
                <h2>%s</h2>
                <h3>%s</h3>
                """
                .formatted(h2, h3);
    }
}

今回は引数が2つなのでこれでいいですが、引数が多くなってくると対応付けが煩雑になってきます。

String.replace を使用する

変換する変数が多くなってきた場合は、JEP378JEP 378: Text Blocksにも記載がありますが、String#replaceで代替するようです。

@RestController
public class DemoController {

    @GetMapping
    public String index(@RequestParam String h2, @RequestParam String h3){

        return """
                <h2>$h2</h2>
                <h3>$h3</h3>
                """
                .replace("$h2", h2)
                .replace("$h3", h3);
    }
}

Kotlinのようにはいかないですが、変数が多い場合の解決方法としてはいいかなと思います。

まとめ

JEP378を見てみると使い方に関することが結構書かれていて勉強になりました。Javaは半年に1度アップデートがかかりますので、今後もウォッチしていこうと思います。

最後に

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

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

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

【Figma】デザインのアクセントに!バナーにテクスチャを入れる方法

はじめに

エキサイト21卒デザイナーの山﨑です。

今回はFigmaでのバナー制作においてのちょっとした小技「バナーにテクスチャを貼り付けて質感を出す方法」について書こうと思います。

この間制作したバナーを例として解説していきます。(※諸事情で画像加工済です。)

こちらはテクスチャをつけてない場合のバナーです。少しのっぺりした印象ですね…🤔

【用意するもの】

  • バナー(背景は明るめに設定)

  • テクスチャ(紙・フェルトなど素材は何でもOK。商用利用可能なフリー画像サイトやAdobe stockなどで調達します。)

①写真をクリックしてレイヤースタイルを「multiply(乗算)」に変更します。

②そのまま写真をバナーに重ねます。

③写真をレイヤーの一番下に配置すれば完成。

完成版はこちらです。

いかがでしょうか。テクスチャを乗せる前と比べて質感が出るようになりました。

他にもフェルトっぽい質感のテクスチャを乗せて温かみのある印象を与えたり、色々な活用ができると思います。

ぜひバナー制作でご活用ください!

最後に

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

興味があれば連絡いただければと思います🙇‍♀️

それではまた!

www.wantedly.com

Nimで無理やりMeCab連携してみる

前置き

おはこんばんにちは!
Nim言語大好きな人です。

前回、無理やりPHPでMeCabを動かしました
なので今回は無理やりNimでMeCabを動かしていこうと思います!

MeCabの導入などは、以前の「PHPを使って形態素解析と文章の類似度を出してみる」をご覧くださいませ。

NimとMeCabの連携

import std/osproc

let text = "すもももももももものうち"

let result = execCmdEx("echo " & text & " | mecab")

echo result.output

以上です!!

……と、やってしまうとめっちゃ短いので、ちょっと改造していきます。

エスケープ処理

import std/osproc

let text = "すもももももももものうち"

let result = execCmdEx("echo " & text & " | mecab")

echo result.output

上記のコードだと変数textに、let text = "hoge; ls -la; exit 0"みたいな文字を指定されてしまうと、任意のコマンドが実行できてしまいます。
これはよくない!

ということでエスケープします。

import std/osproc, std/strutils

let text = escape("hoge; ls -la; exit 0")

let result = execCmdEx("echo " & text & " | mecab")

echo result.output

hoge    名詞,固有名詞,組織,*,*,*,*
;       名詞,サ変接続,*,*,*,*,*
ls      名詞,一般,*,*,*,*,*
-       名詞,サ変接続,*,*,*,*,*
la      名詞,一般,*,*,*,*,*
;       名詞,サ変接続,*,*,*,*,*
exit    名詞,一般,*,*,*,*,*
0       名詞,数,*,*,*,*,*
EOS

これでエスケープされました!

では今回はこのあたりで!

LT会【年忘れ!しくじり先生!】を開催しました 🎉

エキサイト株式会社 バックエンドエンジニアの山縣です。

本記事はアドベントカレンダー24日目の記事です。 qiita.com

12月17日に社内でLT会を開催しました!今回は40名近くの方に参加してしただき、多くの方に聞いていただくことができました。

LT会では、登壇者にこれまでにあった失敗したこととその対策について発表していただきました。

前回のLT会は下記リンクのとおりです。 tech.excite.co.jp

登壇内容

各部署1名ずつ、合計4名の方に発表していただきました。

退職者によるGoogle analytics設定 ぶっとび事件

f:id:excite-kazuki:20211224031824p:plain

一番目の方には、Google Tag Managerの管理者が退職してしまったことによるGAの設定が消えてしまったことについてお話していただきました。今後の対策として、必ず削除しない権限のあるアカウントを登録し、各種設定内容をドキュメント化していくそうです。外部ツール利用時はより注意する必要があると教えてもらいました。

リフレクションを使った話

f:id:excite-kazuki:20211224031849p:plain

二番目の方には、あるサービスでリフレクションを使ってprivateな値を取得していたことについてお話していただきました。現時点ではすでに置き換わっていますが、当時はどうしても値を取得できず、仕方なく実装してしまっていたそうです。

流れ作業で失敗してしまった

f:id:excite-kazuki:20211224031852p:plain

三番目の方には、いつもと同じような流れ作業により、誤ってデータを削除してしまったことや誤った操作をしてしまったことについてお話していただきました。

サービスの運用に関わる重要な操作を行うときは、一息ついてから行う必要があると身が引き締まる発表でした。

追加要求を誤って実装してしまった

f:id:excite-kazuki:20211224031856p:plain

四番目の方には、誤った実装をしてしまったことにより、本来であれば起こり得ないバグを引き起こしてしまったことについてお話していただきました。

対策として、エンジニア・ビジネスともに作り上げたサービスをしっかり知ること、サービスを知らないユーザーに試験的に体験してもらって想定外のパターンも考慮する必要があることなどを教えてもらいました。

おわりに

エキサイトでは毎月1回、LT会を開催しています。

普段の業務だけでは知ることができない他部署の事例や知見などを得ることができるため、今後も続けていきたい活動の1つです!

Oracle ADD_MOTHS関数とPHP DateTimeImmutableクラスのmodify関数を比べてみる

2022/02/28の1ヶ月後の日付は?
2022/02/28の1ヶ月後の日付は?

エキサイトホールディングス Advent Calendar 2021の23日目は、 エキサイト株式会社 エンジニアのあはれん がお送りします。

Oracle データベースから日付データ(例:2022/02/28)を取得する際に、ADD_MOTHS関数を利用して1ヶ月先の日付に変換して取得する処理がありました。

この取得する処理のテストを書く際に、 処理の期待値としてPHPのDateTimeImmutableクラスのmodify関数を利用してある日付のデータの1ヶ月後を生成し、 実際に取得したデータと比較を行いました。

比較した結果、

なんと......テストが落ちたのです!!!

(ADD_MOTHSを利用して算出した1ヶ月後) ≠ (PHPのDateTimeImmutableクラスのmodify関数を利用して算出した1ヶ月後)

そうです。 Oracle の ADD_MOTHS関数とPHPのDateTimeImmutableクラスのmodify関数は仕様が違うのです。 今回は、それぞれの仕様を確認しながら違いを確認したいと思います。

Oracle の ADD_MOTHS関数について

ADD_MONTHSは、日付dateに月数integerを加えて戻します。

引用元:ADD_MONTHS

ですので、例えば、2022/01/28を引数としたときは、2022/02/28になります。

ADD_MONTHS('2022/01/28, 1) => 2022/02/28

ADD_MOTHS関数で気をつけておきたい点は、以下の仕様です。

dateが月の最終日の場合、または結果の月の日数がdateの日付コンポーネントよりも少ない場合、戻される値は結果の月の最終日となります。

引用元:ADD_MONTHS

文章を読み解いていきたいと思います。

「dateが月の最終日の場合、戻される値は結果の月の最終日となります。」の意味

dateが月の最終日(2021/12/31)の場合、結果の月の最終日(2021/01/31)になるのです。

ADD_MONTHS('2021/12/31', 1) => 2021/01/31

2021/12/31から2021/01/31の算出には違和感はありませんが、月の最終日を2022/02/28とした時はどうでしょうか?

dateが月の最終日(2022/02/28)の場合、結果の月の最終日(2021/03/31)になるのです。

ADD_MONTHS('2022/02/28', 1) => 2021/03/31

2022/01/28の場合は2022/02/28になるのに、 2022/02/28の場合は2022/03/28にならないのは、 実装者が仕様を理解していないと驚いてしまうかと思います。

「結果の月の日数がdateの日付コンポーネントよりも少ない場合、戻される値は結果の月の最終日となります。」の意味

例えば、2022/1/29の1ヶ月後を単純に考えると2022/2/29だと思い付くかもしれませんが、 2022年はうるう年ではないので2022/2/29が存在せず、2022/03/01が1ヶ月後の日付になります。

このとき、結果(2022/03/01)の月の日数(01)が、date(2022/1/29)の日付コンポーネント(29)より小さいので、 2022/2/28になります。 このことより、2022/1/29 から 2022/1/31 の日付の場合は、すべて2022/2/28になります。

ADD_MONTHS('2022/1/29, 1)  => 2022/2/28
ADD_MONTHS('2022/1/30, 1)  => 2022/2/28
ADD_MONTHS('2022/1/31, 1)  => 2022/2/28

PHP DateTimeImmutableクラスのmodify関数について

modify関数の場合は、文字通り1ヶ月後の日付になっています。

ですので、2022/02/28の場合は2022/03/28になります。

(new DateTimeImmutable('2022-02-28'))->modify('next month'); → 2022/03/28

また、2022/01/29の場合は2022/03/01になります。2022/01/30以降も同様な計算になります。

(new DateTimeImmutable('2022/01/29'))->modify('next month'); → 2022/03/01
(new DateTimeImmutable('2022/01/30'))->modify('next month'); → 2022/03/02
(new DateTimeImmutable('2022/01/31'))->modify('next month'); → 2022/03/03

2022/01/31の次の日である2022/02/01の場合は2022/03/01になるので、頭が混乱してしまいますね..。

(new DateTimeImmutable('2022/02/01'))->modify('next month'); → 2022/03/01

最後に

Oracle ADD_MOTHS関数とPHP DateTimeImmutableクラスのmodify関数で1ヶ月後の日付を算出した場合の比較表を用意してみました。

Oracle ADD_MOTHS関数とPHP DateTimeImmutableクラスのmodify関数で1ヶ月後の日付を算出した場合の比較表
Oracle ADD_MOTHS関数とPHP DateTimeImmutableクラスのmodify関数で1ヶ月後の日付を算出した場合の比較表

こうして見ると結構違う結果になることがわかります。

人間でも「2022/02/28の1ヶ月後の日付は?」と聞かれると、「2022/03/28」か「2022/03/31」、人それぞれな回答をするかと思います。 関数もそれぞれに回答を決めて実装されています。 日付を操作する関数を利用する際は関数のドキュメントを読んで挙動を理解し、自身の実装目的に合っているものをお使いください。

エキサイトホールディングスのアドベントカレンダーはまだまだ続きます。

明日の執筆担当者は、@ixit_horiさんです。引き続きお楽しみください。

採用情報はこちら↓

https://www.wantedly.com/companies/excite

オンプレからAWSリソースへ安全にアクセスする

こちらは エキサイトホールディングス Advent Calendar 2021 22日目の記事です。

qiita.com

はじめに

オンプレとクラウドのハイブリッド環境でシステムを運用していると、外部アプリケーションからAWSリソースを操作することが多々あります。脳死で「IAMユーザを作って、クレデンシャル情報を生成して・・・」となりがちだったので、どのような認証手段があるのかを整理しました。

署名バージョン4

IAMユーザのアクセスキー(クレデンシャル情報)を使った認証です。AWS SDKを使えばアプリケーション側も手っ取り早く実装できるので、使っている方も多いはずです。

Signing AWS API requests - AWS Identity and Access Management

が、有効期限の長いクレデンシャル情報を使うことになるので、キー漏洩による不正利用などが問題になりがちです。そのため、クレデンシャル情報のローテーションなどを考える必要があったりします。

セキュリティは軽視できませんし、なるべくならクレデンシャル情報の保存は避けたいところです。

SAMLOpenID Connector

使い回せるような半永続的なクレデンシャル情報ではなく、一時的に都度作成されるものであればリスクを低減できそうです。そこでSAMLOpenID Connector(OIDC)の出番です。GitHub ActionsがOIDCをサポートしたのは記憶に新しいですね。おかげでIAMユーザを作成せずとも、GitHub ActionsのワークフローからAWSリソースを操作できるようになったわけです。

Configuring OpenID Connect in Amazon Web Services - GitHub Docs

大体はこれで解決できそうですが、IdPがSAMLやOIDCに対応していなかったり、そもそもIdPが存在しない場合には新たに構築が必要になります。当然のことながら、SAMLやOIDCの認証プロセスの実装や運用も必要なので、一から作っていくのは骨が折れそうです。

MutualTLS

その他にも、MutualTLS(mTLS)が使えると思います。相互TLS認証とかクライアント認証などとも呼ばれていますね。AWSではAPI GatewayAWS IoT CoreなどでmTLSを設定できます。古くからある認証手段ですので、アプリケーション側も容易に実装できます。

Configuring mutual TLS authentication for an HTTP API - Amazon API Gateway

Client authentication - AWS IoT Core

ただし、証明書のライフサイクル管理が必須、という課題は残ります。また、API GWでは証明書が失効したかどうかについては検証しない*1ため、別途Lambdaオーソライザーなどを挟む必要があります。

おわりに

どれもメリット・デメリットがあります。

手法 メリット デメリット
署名バージョン4 手軽に実装できる クレデンシャル情報が漏洩したときのリスクが大きい
SAML / OIDC 一時的なクレデンシャル情報を用いるのでセキュリティ的に安心 SAMLアサーションやアクセストークンのライフサイクル管理、IdPの管理などが必須
MutualTLS レガシーなシステムでも実装が比較的容易 証明書のライフサイクル管理が手間になる

今回に限った話ではないですが、アプリケーションとの相性やビジネス的な優先度などから使い分けていけると良いかなと思います。

エキサイトではエンジニアを募集しています。募集情報は以下から。

www.wantedly.com

JacksonのObjectMapperがかなり優秀だった話

はじめに

はじめまして。エキサイトでインターンをさせていただいている岡﨑です。 アドベントカレンダーの22日目を担当させていただいています。 今日はJacksonのObjectMapperがとてもとても便利だった話をさせていただきます。

Object Mapperとは

それでははじめに言葉の定義からです。 Object Mapperとはそもそも何なのか、この機会に調べてみました。 まずは公式ドキュメントからです。

ObjectMapperは、基本的なPOJO(Plain Old Java Objects)との間、または汎用JSONツリーモデル(JsonNode)との間でJSONを読み書きするための機能と、変換を実行するための関連機能を提供します。また、さまざまなスタイルのJSONコンテンツを処理し、ポリモーフィズムやオブジェクトIDなどのより高度なオブジェクトの概念をサポートするように高度にカスタマイズできます。 公式ドキュメントより

簡単に噛み砕くと、 JacksonのObjectMapperはJavaのObjectとJson間の変換を簡単にしてくれるクラスです。

どれくらい簡単になるの?

それでは実際にやってみましょう。 JSONだとこんな感じです。

{
    "sample_id": 1,
    "sub_list1": [
        {
            "sub_id": 11,
            "sub2_list": [
                {
                    "sub2_id": 111,
                    "answer": "test"
                },
                {
                    "sub2_id": 121
                }
            ]
        },
        {
            "sub_id": 12,
            "sub2_list": [
                {
                    "sub2_id": 211,
                    "answer": "test2"
                },
                {
                    "sub2_id": 212,
                    "answer": "test3"
                }
            ]
        }
    ]
}

これを受け取り、モデルに変換していきたいと思います。

@Slf4j
@Value
public class SampleForm {
    @NotNull
    Sample samle;

     @Value
    public static class Sub2 {
        @NotNull
        Integer subId2;

        @NotNull
        String answer;

        @JsonCreator
        public Sub2 (
            JsonProperty("sub_id2") Integer subId2; 
            @JsonProperty("answer") String answer; 
        ) {
            this.subId2 = subId2;
            this.answer = Optional.ofNullable(answer).orElse("");
        }
    }

    @Value
    public static class Sub1 {
        @NotNull
        Integer subId;

        @NotNull
        List<Sub2> sub2List;

        @JsonCreator
        public Sub1 (
            JsonProperty("sub_id") Integer subId; 
            @JsonProperty("sub2_list") List<Sub2> sub2List; 
        ) {
            this.subId = subId;
            this.sub2List = sub2List;
        }
    }

    @Value
    public static class Sample {
        @NotNull
        Integer sampleId;

        @NotNull
        List<Sub1> subList1;

        @JsonCreator
        public Sample (
            @JsonProperty("sample_id") Integer sampleId,
            @JsonProperty("sub_list1") List<Sub1> subList1
        ) {
            this.sampleId = sampleId; 
            this.subList1 = subList1;
        }
    }

    @ConstructorProperties({
        "sample"
    })
    public SampleForm(
        Sample sample
    ) {
        this.sample = sample;
    }
}

こんな感じでできたらいいなと思いますよね。 しかし、これでは動きません。JSONのデータからそのままモデルに変換することはできないから当然です。 この場合はSampleがnullになって落ちます。

それでは、次のコードを書き換えていきましょう。

    @ConstructorProperties({
        "sample"
    })
    public SampleForm(
        Sample sample
    ) {
        this.sample = sample;
    }

ここがモデルで受け取ることはできないので、Sampleクラスで受け取っているところをStringで受けとってあげます。 そして、Sampleクラスに変換します。

さて、本題です。 ここで長々とコードを書かずに、数行で全て変換してくれるObjectMapperの登場です。

    @ConstructorProperties({
        "sample"
    })
    public SampleForm(
        String sample
    ) {
        ObjectMapper mapper = new ObjectMapper();
        try {
            this.sample = mapper.readValue(sample, new TypeReference<>() {});
        } catch(Excepting exceptin) {
            new Exception("モデルに変換できませんでした");
        }
    }

ObjectMapper mapper = new ObjectMapper();

this.sample = mapper.readValue(sample, new TypeReference<>() {});

この二行を使うだけで、意図している通りにモデルを変換することができます。 とても便利ですね。

終わりに

Object Mapperがここまでできることを知らないで実装すると、とてもごちゃごちゃとしたやばいコードを書くことになります。 それを防ぐためにも、こうして使えるものは正しく使っていくことが大切だと思いました。

アドベントカレンダーも終盤ですが、最後まで楽しんでいただけると幸いです。

qiita.com

quarkusを使う(コンパイル編)

こんばんは、エキサイト株式会社の中尾です。

quarkusといえば、native compileでしょう。 まずはjvmで。

shogo.nakao@localhost:(main*) $ docker build -f src/main/docker/Dockerfile.jvm -t quarkus/hobby-jvm . 
[+] Building 100.3s (11/11) FINISHED
 => [internal] load build definition from Dockerfile.jvm                                                                                                                                                                                                                   0.0s
 => => transferring dockerfile: 2.15kB                                                                                                                                                                                                                                     0.0s
 => [internal] load .dockerignore                                                                                                                                                                                                                                          0.0s
 => => transferring context: 111B                                                                                                                                                                                                                                          0.0s
 => [internal] load metadata for registry.access.redhat.com/ubi8/ubi-minimal:8.4                                                                                                                                                                                           7.9s
 => [1/6] FROM registry.access.redhat.com/ubi8/ubi-minimal:8.4@sha256:c536d4c63253318fdfc1db499f8f4bb0881db7fbd6f3d1554b4d54c812f85cc7                                                                                                                                    14.1s
 => => resolve registry.access.redhat.com/ubi8/ubi-minimal:8.4@sha256:c536d4c63253318fdfc1db499f8f4bb0881db7fbd6f3d1554b4d54c812f85cc7                                                                                                                                     0.0s
 => => sha256:d46336f50433ab27336fad8f9b251b2f68a66d376c902dfca23a6851acae502c 39.29MB / 39.29MB                                                                                                                                                                          12.5s
 => => sha256:be961ec6866344c06fe85e53011321da508bc495513bb75a45fc41f6182921b6 1.74kB / 1.74kB                                                                                                                                                                             3.5s
 => => sha256:c536d4c63253318fdfc1db499f8f4bb0881db7fbd6f3d1554b4d54c812f85cc7 1.47kB / 1.47kB                                                                                                                                                                             0.0s
 => => sha256:4d6547bbb7c5fe0f0d37b491c0c1975ff96bf4cf1c26dd665e3a5d70918b5564 737B / 737B                                                                                                                                                                                 0.0s
 => => sha256:e7685639f55280ffe8da3724cb1d342091a1ceea79af5eb91a07ce97cd32f221 4.28kB / 4.28kB                                                                                                                                                                             0.0s
 => => extracting sha256:d46336f50433ab27336fad8f9b251b2f68a66d376c902dfca23a6851acae502c                                                                                                                                                                                  1.4s
 => => extracting sha256:be961ec6866344c06fe85e53011321da508bc495513bb75a45fc41f6182921b6                                                                                                                                                                                  0.0s
 => [internal] load build context                                                                                                                                                                                                                                          0.3s
 => => transferring context: 22.66MB                                                                                                                                                                                                                                       0.3s
 => [2/6] RUN microdnf install curl ca-certificates java-17-openjdk-headless     && microdnf update     && microdnf clean all     && mkdir /deployments     && chown 1001 /deployments     && chmod "g+rwX" /deployments     && chown 1001:root /deployments     && curl  75.7s
 => [3/6] COPY --chown=1001 build/quarkus-app/lib/ /deployments/lib/                                                                                                                                                                                                       0.1s
 => [4/6] COPY --chown=1001 build/quarkus-app/*.jar /deployments/                                                                                                                                                                                                          0.0s
 => [5/6] COPY --chown=1001 build/quarkus-app/app/ /deployments/app/                                                                                                                                                                                                       0.0s
 => [6/6] COPY --chown=1001 build/quarkus-app/quarkus/ /deployments/quarkus/                                                                                                                                                                                               0.0s
 => exporting to image                                                                                                                                                                                                                                                     2.4s
 => => exporting layers                                                                                                                                                                                                                                                    2.4s
 => => writing image sha256:7a4f731341a6d3c9b04e1ffe98783be021280fe109fea48425fe49cb983cf860                                                                                                                                                                               0.0s
 => => naming to docker.io/quarkus/hobby-jvm                                                                                                                                                                                                                               0.0s```
shogo.nakao@localhost:(main*) $ docker run -i --rm -p 8080:8080 -p 5005:5005 -e JAVA_ENABLE_DEBUG="true" quarkus/hobby-jvm
exec java -Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005 -XX:+ExitOnOutOfMemoryError -cp . -jar /deployments/quarkus-run.jar
Listening for transport dt_socket at address: 5005
__  ____  __  _____   ___  __ ____  ______
 --/ __ \/ / / / _ | / _ \/ //_/ / / / __/
 -/ /_/ / /_/ / __ |/ , _/ ,< / /_/ /\ \
--\___\_\____/_/ |_/_/|_/_/|_|\____/___/
2021-12-20 14:04:51,601 INFO  [io.quarkus] (main) hobby v1.0.0 on JVM (powered by Quarkus 2.5.1.Final) started in 1.055s. Listening on: http://0.0.0.0:8080
2021-12-20 14:04:51,602 INFO  [io.quarkus] (main) Profile prod activated.
2021-12-20 14:04:51,603 INFO  [io.quarkus] (main) Installed features: [agroal, cdi, hibernate-validator, jdbc-mysql, mybatis, narayana-jta, resteasy, resteasy-jackson, smallrye-context-propagation, vertx]
^C2021-12-20 14:05:17,653 INFO  [io.quarkus] (Shutdown thread) hobby stopped in 0.036s
shogo.nakao@localhost:(main*) $ docker run -i --rm -p 8080:8080 quarkus/hobby-jvm
exec java -Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager -XX:+ExitOnOutOfMemoryError -cp . -jar /deployments/quarkus-run.jar
__  ____  __  _____   ___  __ ____  ______
 --/ __ \/ / / / _ | / _ \/ //_/ / / / __/
 -/ /_/ / /_/ / __ |/ , _/ ,< / /_/ /\ \
--\___\_\____/_/ |_/_/|_/_/|_|\____/___/
2021-12-20 14:05:21,306 INFO  [io.quarkus] (main) hobby v1.0.0 on JVM (powered by Quarkus 2.5.1.Final) started in 0.982s. Listening on: http://0.0.0.0:8080
2021-12-20 14:05:21,307 INFO  [io.quarkus] (main) Profile prod activated.
2021-12-20 14:05:21,307 INFO  [io.quarkus] (main) Installed features: [agroal, cdi, hibernate-validator, jdbc-mysql, mybatis, narayana-jta, resteasy, resteasy-jackson, smallrye-context-propagation, vertx]

debug on で 1.055s,debug offで0.982sです。もちろんサイズが小さいこともありますが、、、早すぎじゃないですか?

続いてnativeです。初回でエラーが出る方は、sdk man でgraalVMを入れて下さい。

Cannot find the `native-image` in the GRAALVM_HOME, JAVA_HOME and System PATH. Install it using `gu install native-image`

そして待ちに待ったnative compile開始!!

...

......

.........

compileできなかった。。。

なんと、、、recordクラスに対応していなかった、mybatisも対応していなかった。。

どうやらissuesで21.3.0.r17でrecordクラスどうにかして欲しいみたいなのを見つけた。

https://github.com/quarkusio/quarkus/issues/20891

くっそう。。。悲しい、、、

できた瞬間、速攻ためそう。

最後に、弊社では採用もバシバシ実施しているので興味のあるかたがいましたらご応募ください。

www.wantedly.com



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

qiita.com

Spring Bootで、Webアクセスのパラメータをクラスで受け取るときの注意点

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

Spring BootでAPIなどを作る時は、Webからのアクセスを受け取ることになります。 そのアクセスにクエリパラメータ等でパラメータが付いている場合、そのパラメータを何かしらの方法で受け取る必要があります。

今回は、クラスを使ってパラメータを受け取るときに、予期しないデータを受け取ってしまう場合がある点について説明していきます。

クラスでパラメータを受け取る方法

例えば、以下のアクセスを受け付けるとします。

http://localhost/sample?test1=aaa&test2=bbb

その場合、以下のコードで受け取ることができます。

import lombok.Value;

import java.beans.ConstructorProperties;

@Value
public class SampleModel {
    String test1;
    String test2;

    @ConstructorProperties({"test1", "test2"})
    public SampleModel(String test1, String test2) {
        this.test1 = test1;
        this.test2 = test2;
    }
}
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping
public class SampleController {
    @GetMapping("sample")
    public String sample(
            @ModelAttribute SampleModel sampleModel
    ) {
        return "OK";
    }
}

実際にデバッグモードで実行してみると、SampleModelに以下のデータが入っていることがわかります。

SampleModel(test1=aaa, test2=bbb)

上記のコードであれば問題はありませんが、実は特定のパターンだと想定外のデータが入ってしまうことがあります。

想定外のデータが入ってしまうパターン

アクセスは同じく

http://localhost/sample?test1=aaa&test2=bbb

とし、受け取りクラスのコードを少し変えてみます。

import lombok.Data;

import java.beans.ConstructorProperties;

// ValueではなくDataで受け取る(各プロパティにSetterが付く)
@Data
public class SampleModel {
    String test1;
    String test2;

    @ConstructorProperties({"test1", "test2"})
    public SampleModel(String test1, String test2) {
        this.test1 = test1;

        // test2は、一部文字列を変更してからプロパティに代入する
        this.test2 = test2 + "ccc";
    }
}

こちらは同じコードです。

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping
public class SampleController {
    @GetMapping("sample")
    public String sample(
            @ModelAttribute SampleModel sampleModel
    ) {
        return "OK";
    }
}

本来は

SampleModel(test1=aaa, test2=bbbccc)

こうなってほしいはずですが、実際は

SampleModel(test1=aaa, test2=bbb)

こうなってしまいます。

どうやら、「 Setter が存在し、かつアクセスのパラメータと同じ名前のプロパティ」があると、コンストラクタでプロパティに値を代入した後に、直接アクセスのパラメータの値をプロパティに代入してしまい、結果として上書きされてしまうようです。

このような場合、 Setter をつけないようにすることで解決できます。

import lombok.Value;

import java.beans.ConstructorProperties;

// SetterがつかないValueを使用する
@Value
public class SampleModel {
    String test1;
    String test2;

    @ConstructorProperties({"test1", "test2"})
    public SampleModel(String test1, String test2) {
        this.test1 = test1;
        this.test2 = test2 + "ccc";
    }
}

このコードであれば、想定通り以下のデータとなります。

SampleModel(test1=aaa, test2=bbbccc)

最後に

今回はシンプルな例でしたが、複雑になってくると見つけるのが大変になっていく恐れがあります。 コンストラクタでデータを入れる場合は Setter は不要なはずなので、可能な限り外していくことをおすすめします。

TABLE定義を設計する時、カラム名に半角数値を入れるのはやめよう

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

出落ちみたいなタイトルなのですが、みなさんはテーブル設計の時、カラム名はどのようにしていますか?

基本は - 半角英字のみ - 極力正式名称をカラム名に、妙な省略はしない - package、classなど特定の言語の予約後になる可能性のある単語は避ける

だと、ほとんどの言語で互換性があり安定して使えると思います。

また、カラム名に半角数値を入れて横にカラムを伸ばすのは極力やめたほうが良いです。

そういう時は、

  • そもそも複数入るのか確認する。
  • 縦にデータを持ち、typeで分ける。

などいろいろアプローチがあるはずです。

例えば、仮に半角数値を使う時、どっちを使いますか?

  • tag_1
  • tag1

どっちを使っても、ローワーキャメルケースでは以下で現れます。

public String getTag1(){
    retrurn this->tag_1;
}

public String getTag1(){
    retrurn this->tag1;
}

キャメルケースでは半角英字をアッパーキャメルで表示しますが、数値では表現できません。これは不便ですよね。

つまり、ORMで自動でモデルを作成する場合など、キャメルケースにした場合にどっちにでも取れるカラム名にすると、大体不具合が発生することが多いです、 うまくコード変換が行われずに以下のようになる場合もあります。

public String getTag_1(){
    retrurn this->tag_1;
}

テーブル設計でそもそものデータ構造を考えるのも大事ですが、カラム名にも気をつけてあげられたら嬉しいと思います。

Rxdartを使用して値の変化を他タブなど別画面に通知する方法

これは エキサイトホールディングス Advent Calendar 2021 21日目の記事です。

エキサイト株式会社の高野です。今回はFlutterにおける別画面への値渡しや通知をする方法についてです。

実装

まずはじめに通知を受ける変数として以下を定義します。

final hoge = BehaviorSubject<void>.seeded({});

この void の部分を任意の型にすることによって通知を投げる側からその型の値を受け渡しすることができます。

受け取る際には任意の画面のinitStateなりで以下のように定義します。

hoge.listen((value) => _exFunction())
        .addTo(subscription);

これで通知の受け取りができました。
次に投げる側です

hoge.sink.add({})

こちらを任意の場所で呼んであげることによって通知を投げれます。
addのなかに定義した型の渡したい値をいれます。(ex: add('fugafuga') )

以上で受け渡しができるようになります。これを別クラスにシングルトンで作って呼び出せるようにすると使いやすいと思います。

最後に

弊社では絶賛採用強化中です。もしご興味がある方がいましたら下記リンクよりアクセスいただけると幸いです。(カジュアルからもOKです) www.wantedly.com

SpringBoot DevToolsのHot RestartとLiveReloadでサクサク開発を行う

エキサイト株式会社エンジニアの佐々木です。2021年エキサイトホールディングス アドベントカレンダー19日を担当させていただきます。

余談

エキサイトのメディア事業部では、SpringBootを使用して過去の技術的負債に立ち向かっております。私がジョインしてから1.5年以上が経過しましたが、半年以上の開発を経てメンバーも、悪い設計の見直しや改善、無駄処理の置き換え、クエリチューニングやパフォーマンス改善を通してサーバコストの圧縮を行っており成果がではじめています。サーバコストが半分以下になっていながらも、アプリケーションの負荷対策や開発速度などのパフォーマンスは上がっているところがほとんどで、技術観点や設計観点でも、ジョインしたときと比べて、成長をしているかと思います。

はじめに

SpringBootで開発する際に、Javaコンパイル言語であるのでコードを編集したらアプリケーションの再起動が必要になります。スクリプト言語のように次回実行時に動的にロードされるとかは通常ありません。SpringBootでは、Hot Reloadはないけど、Hot Restartはあるよといったものです。Hot Reloadより遅いけど、Hot Restartでも十分戦えるものになっているかと思います。バックエンドとフロントエンドの両方をご紹介します。

バックエンド

まず、バックエンド側のHot Restartの設定を紹介になります。

設定

依存関係を解消してくれるbuild.gradleに、SpringBoot devtoolsのパッケージを追加します。

dependencies {
    ...
        developmentOnly 'org.springframework.boot:spring-boot-devtools'    // この設定を追加
    ...

SpringBootの設定ファイルであるapplication.yaml(application.properties)に下記の設定を追加します。

spring.devtools.restart.enabled=true

これで準備は完了になります。

動作確認

動作確認は、

  1. IntelliJでSpringBootを起動
  2. コードを編集(コメントアウトしているコードのコメントを解除)
  3. ビルド (ファイル単体ビルド ⌘ + Shift + F9 or プロジェクトビルド ⌘ + F9 )

を実行します。これを行うと、SpringBoot DevToolsが変更を検知して、Hot Restartをしてくれます。このHot Restartは、アプリケーション全部を再起動しているわけではなく、必要な箇所だけやっているっぽいので、通常の再起動よりかなり高速です。

f:id:earu:20211219141741g:plain

さらに

ビルドのショートカットすら押したくない人もいるかと思います。IntelliJは、SpringBoot実行時には、コンパイルが走らないという設定があるので、これを解除します。

  • Preferences > Build, Execution, Deployment > Compiler > Build Project Automatically にチェックを入れます
  • Preferences > Advanced Settings > Allow auto-make to start even if developed application is currently running にチェックを入れます ※ Intellijの古いバージョンだと registly設定からauto-make で検索してください!!!

Auto Make は、ちょっとディレイがあるので、個人的には面倒でもビルドショートカットを押すのがオススメです。

フロントエンド

フロントエンドでは、LiveReloadという機構がありますので、これを使います。SpringBoot DevToolsは、このLiveReloadに依存しています。

livereload.com

設定

application.properties(application.yaml)に下記の設定を行います。

spring.devtools.livereload.enabled=true
spring.thymeleaf.cache=false

Chrome Extensionをインストール

Chromeに下記のExtensionをインストールします。

chrome.google.com

LiveReloadを動作させたいURLを開き、有効にします。

f:id:earu:20211219122336p:plain
LiveReloadのExteion

設定は以上で完了です。

動作確認

動作確認は、

  1. IntelliJでSpringBootを起動
  2. コードを編集(Hello, Spring -> Hello, LiveReload)
  3. ビルド (ファイル単体ビルド ⌘ + Shift + F9 or プロジェクトビルド ⌘ + F9 )

標準出力(テンプレートを通さない出力)もサーバサイドテンプレート出力もどちらにも対応しています。

標準出力

f:id:earu:20211219144738g:plain

サーバサイドテンプレート出力

f:id:earu:20211219143631g:plain

テンプレートのみ修正の場合

テンプレートのみ修正の場合は、再起動なしでOKです。SpringBoot DevToolsがファイル変更を検知して、LiveReloadをキックしてくれます。

f:id:earu:20211219143144g:plain

まとめ

バックエンドはスクリプト言語のようにまではいきませんが、少しはサクサク開発ができるようにはなります。設定がやや必要にはなりますが、静的型付け言語が持つ便利さを享受するのと引き換えにこのくらいの簡単な設定であれば、許容範囲ないかなと思います。フロントエンドの方は、最近ではReact.jsやVue.jsなどクライアントサイドのフレームワークが充実し、サーバサイドテンプレートは流行りませんが、SPAだとSEOは不安ですし、SSGやISRだとビルドのタイミング等に気を配る必要があります。規模によって適切なものを選択するのがいいかとおもいます。

最後に

最後まで読んでいただいてありがとうございます。引き続き、2021年エキサイトホールディングス アドベントカレンダーをお楽しみいただけると幸いです。

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

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

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

Jetpack ComposeでSwipe to Refreshのインジケータの表示位置をずらす

これは エキサイトホールディングス Advent Calendar 2021 19日目の記事です。

こんにちは。エキサイト株式会社 Androidエンジニアの克です。

今回は、AndroidJetpack ComposeでのSwipe to Refreshについてのお話です。

Swipe to Refreshとは

GoogleMaterial Designには、画面上の表示を最新の状態に更新するための仕組みとしてSwipe to Refreshというものが存在します。

Swipe-to-Refresh

AndroidのViewとしてはSwiperefreshlayoutが存在しますが、Jetpack Compose本体には相当するものが無いため今回はAccompanistSwipe Refreshを使用します。

Swipe to Refreshとリスト表示を実装する

まずはSwipe to Refresh本体と、セットになることが多いリスト表示を実装していきます。

今回はインジケータの調整が目的のため、実装内容については公式のドキュメントを参照してください。

@Composable
private fun Screen() {
    var isRefreshing by remember { mutableStateOf(false) }

    SwipeRefresh(
        state = rememberSwipeRefreshState(isRefreshing),
        onRefresh = { isRefreshing = true },
    ) {
        LazyColumn {
            items(30) { index ->
                ListItem(id = index + 1)
            }
        }
    }
}

@Composable
private fun ListItem(id: Int) {
    Box(
        modifier = Modifier
            .fillMaxWidth()
            .height(48.dp)
            .background(color = if (id % 2 == 0) Color.LightGray else Color.Gray),
        contentAlignment = Alignment.Center,
    ) {
        Text(text = "item $id")
    }
}

下記はこのコードの動作イメージです。

固定のコンテンツとリストを組み合わせる

追加の要件として、画面上部にスクロールに左右されない固定表示のコンテンツを追加します。

@Composable
private fun Screen() {
    var isRefreshing by remember { mutableStateOf(false) }

    val contentHeight = 128.dp
    val contentPadding = 16.dp

    SwipeRefresh(
        state = rememberSwipeRefreshState(isRefreshing),
        onRefresh = { isRefreshing = true },
    ) {
        Content(
            height = contentHeight,
            contentPadding = contentPadding,
            modifier = Modifier.zIndex(1F),
        )

        LazyColumn(
            contentPadding = PaddingValues(top = contentHeight),
        ) {
            items(30) { index ->
                ListItem(id = index + 1)
            }
        }
    }
}

@Composable
private fun Content(
    height: Dp,
    contentPadding: Dp,
    modifier: Modifier = Modifier,
) {
    Card(
        modifier = modifier
            .fillMaxWidth()
            .height(height = height)
            .padding(all = contentPadding),
        backgroundColor = MaterialTheme.colors.primary,
        contentColor = MaterialTheme.colors.onPrimary,
    ) {
        Box(
            contentAlignment = Alignment.Center,
        ) {
            Text(text = "Content")
        }
    }
}

リストアイテムの先頭がコンテンツの下部に位置するように、LazyColumncontentPaddingにコンテンツの高さ分を指定しています。

また、コンテンツがリストよりも上のレイヤーとなるようにコンテンツに対して zIndexを指定しています。

下記はこのコードの動作イメージです。

インジケータがコンテンツの裏側に入り込んでしまっているのがわかるでしょうか。

こちらを対応するのが今回の目的となります。

インジケータの表示位置を変更する

とはいえ、インジケータの位置を変更するのは非常に簡単です。

LazyColumnのアイテム位置をcontentPaddingで変更したのと同様に、SwipeRefreshにもindicatorPaddingというパラメータが存在するのでこれを設定するだけです。

@Composable
private fun Screen() {
    var isRefreshing by remember { mutableStateOf(false) }

    val contentHeight = 128.dp
    val contentPadding = 16.dp

    SwipeRefresh(
        state = rememberSwipeRefreshState(isRefreshing),
        onRefresh = { isRefreshing = true },
        indicatorPadding = PaddingValues(top = contentHeight - contentPadding),
    ) {
        Content(
            height = contentHeight,
            contentPadding = contentPadding,
            modifier = Modifier.zIndex(1F),
        )

        LazyColumn(
            contentPadding = PaddingValues(top = contentHeight),
        ) {
            items(30) { index ->
                ListItem(id = index + 1)
            }
        }
    }
}

コンテンツには余白が設定されているので、コンテンツの高さからコンテンツの余白分(下部のみ)を引いた値にしました。

下記はこのコードの動作イメージです。

インジケータがコンテンツの下部からきれいに現れていることが確認できますね。

まとめ

Swipe to Refreshのインジケータの表示位置は、indicatorPaddingで設定することができます。

基本的にはデフォルトの設定で問題はないかと思いますが、レイアウトによっては直感的でより指に馴染むアプリになるのでぜひお試しください。

iOSでネイティブ広告を出す際にIBOutletを繋げられない時の対処法

これは エキサイトホールディングス Advent Calendar 2021 18日目の記事です。

エキサイト株式会社の高野です。今回はFlutterにおけるネイティブ広告の話です。

はじめに

今回の記事はgoogle_mobile_ads(1.0.0)の話ですので他のライブラリを使っていたり、バージョンが上がって修正されているかもしれませんのでご容赦ください。

対処法

まず、原因としてですがSDKのIssueを除いてみると以下のようです。

xcframeworkは一般的に.xibファイルと互換性がありません。 これはAppleのバグであり、いつ修正されるかはわかりません。

このような状況ですのでいつ治るかというのは不明みたいです。これによってIBOutletを接続することができないのできません。

解決方法ですが、以下ディレクトリに存在するGADNativeAdをRunnerと同階層に一度コピーしてあげることでIBOutletの接続が可能になります。

/Pods/Google-Mobile-Ads-SDK/Frameworks/GoogleMobileAdsFramework-Current/GoogleMobileAds.xcframework/ios-arm64_armv7/GoogleMobileAds.framework/Headers/GADNativeAd.h

接続することができましたらコピーしたGADNativeAdは削除していただいて構いません。
元のGADNativeAdと連携されているので、そちらを確認してみると繋がれています。
こちらの仕組みに関しましては自分の方でも詳しく調べられていないのでご教示いただけると大変助かります。

最後に

弊社では絶賛採用強化中です。もしご興味がある方がいましたら下記リンクよりアクセスいただけると幸いです。(カジュアルからもOKです) www.wantedly.com

SolrJを使ってSolr検索をする

エキサイト株式会社の武藤です。

エキサイトホールディングス Advent Calendar 2021の18日目の記事です。

qiita.com

今回は、Spring Boot プロジェクトでSolrJを使ってSolr検索を実装する手順について説明します。

SolrJ

SolrのJava用のAPIです。 solr.apache.org

Spring Bootには、Solr用のSpring Dataがありますが、新規プロジェクトへの利用は非推奨です。

spring.io

This project is about to move to the Spring Attic and is not recommended for new projects. The last Release (4.3.0) will, as part of the spring data release 2020.0, see patch updates till mid 2022.

そういった経緯もあり、今回はSolrJを採用しました。

実装

build.gradle に下記の行を追加します。

implementation "org.apache.solr:solr-solrj:x.xx.x" 

SolrスキーマJavaで定義します。

@Fieldスキーマのフィールド名からJavaのプロパティーへの変換を設定します。

@Data
public class BookSolrModel {

    @Field("book_id")
    private String bookId;

    @Field
    private String name;

    @Field
    private String description;
}

次に、リクエストからレスポンスの取得です。Spring BootのRepository層での実装例です。

SolrQueryをインスタンス化し、クエリーをセットします。

Solrから取得したBookSolrModelをドメインモデルにマッピングしています。

HttpSolrClient.query()はThrowableなので、例外ハンドリングをしています。

@Repository
@RequiredArgsConstructor
public class BookRepositoryImpl implements BookRepository {

    private final HttpSolrClient httpSolrClient;

    @Override
    public List<BookModel> getBookList(String bookId) {
        try {
            final SolrQuery query = new SolrQuery();
            query
                    .setQuery("book_id:" + bookId)
                    .setStart(0)
                    .setRows(1);

            final QueryResponse response = httpSolrClient.query(query);

            final List<BookSolrModel> bookSolrModelList = httpSolrClient
                    .getBinder()
                    .getBeans(BookSolrModel.class, response.getResults());

            return bookSolrModelList
                    .stream()
                    .map(bookSolrModel ->
                            new BookModel()
                                    .setBookId(bookSolrModel.getBookId())
                                    .setName(bookSolrModel.getName())
                                    .setDescription(bookSolrModel.getDescription())
                    )
                    .collect(Collectors.toList());

        } catch (SolrServerException | IOException e) {
            return List.of();
        }
    }
}

HttpSolrClientは @Bean によりDIコンテナに登録することで、他のRepositoryでもインスタンスを使い回せるようにしています。

Solrのホストはapplication.yml で定義しておき、 @Value で呼び出します。これにより環境の差異をコードに記載しなくて済みます。

@Configuration
public class SolrConfig {
    @Value("${solr.host}")
    private String solrHost;

    @Bean
    public HttpSolrClient httpSolrClient() {
        return new HttpSolrClient.Builder(solrHost).build();
    }
}

類似要素検索

MoreLikeThisを使った類似検索の例です。

            final SolrQuery query = new SolrQuery();
            query
                    .setQuery("book_id:" + bookId)
                    .setStart(0)
                    .setRows(1)
                    .setMoreLikeThis(true)
                    .setMoreLikeThisFields("name")
                    .setMoreLikeThisCount(3);

            final QueryResponse response = httpSolrClient.query(query);

            final List<BookSolrModel> bookSolrModelList = httpSolrClient
                    .getBinder()
                    .getBeans(BookSolrModel.class, response.getMoreLikeThis().get(bookId));

最後に

簡単にですが、Spring BootプロジェクトでのSolrJの使い方について説明しました。 他の言語で提供されているSolr APIと大きな差はなく、使いやすいものでした。 参考になれば幸いです。

エキサイトでは、エンジニア募集を随時行っております。 www.wantedly.com

引き続きエキサイトホールディングスのアドベントカレンダーをお楽しみいただければ幸いです。 qiita.com

参考

solr.apache.org