20201113のJavaに関する記事は11件です。

備忘録11/13

・macでスクリーンショットのショートカットキー
 command + shift + 3 を同時押し → 画面全体を画像としてデスクトップに保存
 command + shift + 4 を同時押し → 画面の一部の範囲を選択して画像としてデスクトップに保存

・HTMLファイルのひな形(テンプレート)の自動入力
 html:5 → tab

・MacでVScodeの文字を大きくしたり小さくしたりする(JISキーボード)
 command(⌘) + シフト(⇧) + プラス(+)
 command(⌘) + シフト(⇧) + マイナス(-)

・ ul>*6 → tab で、ulタグの中に(子要素として)liタグを6個いれるという意味

・ 崩れたコードがあったら、bodyタグ全体をマウスのドラッグで選択し、右クリックのメニューから選択範囲のフォーマットを選択する(フォーマットとは、崩れたコードを読みやすく整形すること)すると自動的にソースコードを整形できる。

進捗 04.さまざまなHTMLタグまで   

引用、参考

N予備校

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

使用したクーポンコードを指定したエンドポイントにポストバックする

広告の成果計測等のために、使用されたクーポンコード等の情報を含めて計測システムのエンドポイントへポストバックする必要がある際の成果通知の仕方です。

今回はHttpURLConnectionを利用しました。

security configの設定とmanifestの設定

設定するポストバック先のURLがhttps://〜であればこの設定は不要です。
Android 9(Pie)では暗号化されていない通信はできない、つまりhttp://ではじまるURLへの通信はデフォルトでは不可能です。
※設定するURLがhttps://で始まればこの手順は必要ありません。

security config設定

app/xmlディレクトリに以下のファイルを作成します。

network_security_config.xml
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <domain-config cleartextTrafficPermitted="true">
        <domain includeSubdomains="true">XXXX.net</domain>
    </domain-config>
</network-security-config>

指定したドメインに暗号化されていない通信(http://)を許可(cleartextTrafficPremittedをtrue)しています。

manifestの設定

その後AndroidManifestのapplicationタグの属性部分に

android:networkSecurityConfig="@xml/network_security_config

を追加します。
(例)

AndroidManifest.xml
<application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme"
        android:networkSecurityConfig="@xml/network_security_config">

ポストバック

ポストバックを発生させたいイベント等が発生したときに以下のメソッドを実行します。
クーポンコード等、イベント毎に変わる可能性があるユーザ入力値は引数として受け取る設定をしています。

Activity.java
public void postBack(final String cp){
        new Thread(new Runnable() {
            @Override
            public void run() {
                try{
                    URL url = new URL("https:/XXXX.net?coupon="+cp);
                    HttpURLConnection con = (HttpURLConnection)url.openConnection();
                    String str = InputStreamToString(con.getInputStream());
                    Log.d("HTTP",str);
                }catch(Exception e){
                    System.out.println(e);
                }
            }
        }).start();
    }

    static String InputStreamToString(InputStream is) throws IOException{
        BufferedReader br = new BufferedReader(new InputStreamReader(is));
        StringBuilder sb = new StringBuilder();
        String line;
        while( (line=br.readLine()) != null ){
            sb.append(line);
        }
        br.close();
        return sb.toString();
    }

参考

HttpURLConnection (Java Platform SE 8)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

湯婆婆Androidアプリを作ってみました

メリークリスマス。この記事は、湯婆婆 Advent Calendar 2020の4日目です。

賛辞と謝辞

まずは@Nemesisさんに賛辞と謝辞を送ります。
Javaで湯婆婆を実装してみるという記事で、このようなムーヴメントを興こせるだなんて、素晴らしいです。
呼んでいる胸のどこか奥でいつも心踊る夢を見たい気分にさせていただきました。

私にこのような記事を投稿させてもらえる機会を与えていただき、ありがとうございます。

先日、Kotlinで湯婆婆を実装という記事を投稿させていただきました。
でも私の中では心のどこかでAndroidアプリにしたい、という想いが残っていて。
ということで作ったみた次第です。しかし今回はKotlinではなく、Javaで1

やりたいこと

まずは、こちらの名シーンをご覧ください。

GIFMAGAZINE:紙に書いた名前がふわーっと浮いてどこかへ行ってしまうGIF画像 2

荻野千尋おぎのちひろが書いた字が、ふわーっと浮いてどこかへ行ってしまうシーンです。これを実現したいです。

完成したアプリ

OLD_sentochihiro.gif

名前を入力してボタンをクリックすると、1字を残してふわーっと浮いてどこかへ行ってしまうアプリです。

なお、このアニメーションGIFを作るにあたって、うっかりおぎ野千尋を、誤って「はぎ」にしてしまっていました。これはオギノさんハギノさんあるあるなのかもしれませんが、実はこれが後々に恐ろしいことにつながっていきます...詳しくは後述をご覧ください(勿体もったいぶってすみません)。

コード

とりあえずまずは、ずらずらずら~っと掲載しておきます。
可能な限りコメントを付けましたので、日本語と照らし合わしながら解読していただければ幸いです。
ポイント解説は後ほど。

画面を実現するActivityクラスです。というか、Javaのプログラムはこれ1個だけのアプリです。

MainActivity.java
public class MainActivity extends AppCompatActivity {

    // もはやこのRandomオブジェクト自体が湯婆婆なんじゃないのかと思ふ
    private Random random = new Random();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // ここに入力した名前の画像(ImageViewそしてBitmap)を埋め込みます
        LinearLayout nameArea = findViewById(R.id.name_area);

        // 「契約する」ボタンクリック
        Button keiyakuBtn = findViewById(R.id.keiyaku_button);
        keiyakuBtn.setOnClickListener(v -> {
            EditText editText = findViewById(R.id.name);
            // EditTextクリックで出てきたソフトウェアキーボードを引っ込めさせる
            InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE);
            inputMethodManager.hideSoftInputFromWindow(editText.getWindowToken(), 0);
            editText.clearFocus();
            // 2度目以降を考慮してこの名前表示領域から全子ビューを削除
            nameArea.removeAllViews();
            // 入力欄(EditText)から入力文字列(String)を取得
            String name = editText.getText().toString();
            // 配列としてバラバラにします("荻野千尋"なら要素数4の配列)
            String[] names = new String[name.length()];
            for (int i = 0; i < name.length(); i++) {
                names[i] = String.valueOf(name.charAt(i));
            }
            // 運命の分かれ道!さあ、どの字だけが残るのか?!
            int num = random.nextInt(names.length);
            String newName = names[num];
            // String配列の要素数と同数のTextView配列オブジェクトを生成
            TextView[] texts = new TextView[names.length];
            // LayoutInflaterは、XMLで定義した<TextView>を、JavaのTextViewオブジェクトにしてくれます
            LayoutInflater layoutInflater = LayoutInflater.from(this);
            for (int i = 0; i < names.length; i++) {
                texts[i] = (TextView) layoutInflater.inflate(R.layout.name_entity, null);
                // 各TextView1個ずつに名前1字を設定
                texts[i].setText(names[i]);

                if (i != num) {
                    // 選択されなかった字以外は、ふわーっと浮いてどこかへ行ってしまうアニメーションをセット
                    huwa_xtutouiteAnimation(texts[i]);
                }
                // 名前エリアに埋め込みます
                nameArea.addView(texts[i]);
            }
            // 「フン~」セリフ出現
            TextView hun = findViewById(R.id.hun);
            hun.setText(String.format(getResources().getString(R.string.hun), name));
            hun.setVisibility(View.VISIBLE);

            // 「いいかい~」セリフ出現
            TextView iikai = findViewById(R.id.iikai);
            iikai.setText(String.format(getResources().getString(R.string.iikai), newName, newName, newName));
            iikai.setVisibility(View.VISIBLE);
        });
    }

    /**
     * ふわーっと浮いてどこかへ行ってしまうアニメーション
     *
     * @param view ふわーっと浮いてどこかへ行かせてしまいたいView
     */
    private void huwa_xtutouiteAnimation(final View view) {
        // 様々な挙動をするアニメーションを複合化するには、ObjectAnimatorが便利のようだ
        ObjectAnimator transY, transX, scaleX, alpha;
        List<Animator> animatorList = new ArrayList<>();

        // アニメーション時間もランダムに5~10秒に演出
        int durationTY = (random.nextInt(6) + 5) * 1000;
        int durationTX = (random.nextInt(6) + 5) * 1000;
        int durationSX = (random.nextInt(6) + 5) * 1000;

        // 上昇"translationY"
        transY = ObjectAnimator.ofFloat(view, View.TRANSLATION_Y.getName(), 0.0f, -1000.0f);
        transY.setDuration(durationTY);
        animatorList.add(transY);

        // 左から右へ移動"translationX"
        transX = ObjectAnimator.ofFloat(view, View.TRANSLATION_X.getName(), view.getX(), view.getX() - 100.0f, view.getX() + 100.0f);
        transX.setDuration(durationTX);
        animatorList.add(transX);

        // 幅の伸び縮み"scaleX"
        scaleX = ObjectAnimator.ofFloat(view, View.SCALE_X.getName(), 1.0f, 0.1f, 1.0f, 0.1f, 1.0f, 0.1f, 1.0f);
        scaleX.setDuration(durationSX);
        animatorList.add(scaleX);

        // 透明化"alpha"
        alpha = ObjectAnimator.ofFloat(view, View.ALPHA.getName(), 1.0f, 0.0f);
        alpha.setDuration(Math.max(Math.max(durationTY, durationTX), durationSX) / 2);
        animatorList.add(alpha);

        // アニメーションの複合化、再生
        AnimatorSet set = new AnimatorSet();
        set.playTogether(animatorList);
        set.start();
    }
}

このMainActivityのためのレイアウトリソースファイルです。

activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <!-- 契約書だよ。そこに名前を書きな。 -->
    <TextView
        android:id="@+id/keiyakushodayo"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentTop="true"
        android:fontFamily="@font/sanarifont"
        android:gravity="center_horizontal"
        android:text="@string/keiyakushodayo"
        android:textColor="@android:color/black"
        android:textSize="30sp" />

    <!-- 名前入力欄 -->
    <EditText
        android:id="@+id/name"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@+id/keiyakushodayo"
        android:fontFamily="@font/sanarifont"
        android:textSize="40sp" />

    <!-- 書き終わったらクリックしてください -->
    <Button
        android:id="@+id/keiyaku_button"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@+id/name"
        android:fontFamily="@font/sanarifont"
        android:text="@string/keiyakusuru"
        android:textSize="30sp" />

    <!-- 「フン~」セリフ -->
    <TextView
        android:id="@+id/hun"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@+id/keiyaku_button"
        android:fontFamily="@font/sanarifont"
        android:gravity="center_horizontal"
        android:text="@string/hun"
        android:textColor="@android:color/black"
        android:textSize="30sp"
        android:visibility="invisible" />

    <!-- ここに入力した名前のTextViewを埋め込みます -->
    <LinearLayout
        android:id="@+id/name_area"
        android:layout_width="match_parent"
        android:layout_height="200dp"
        android:layout_centerVertical="true"
        android:gravity="bottom|center_horizontal"
        android:orientation="horizontal" />

    <!-- 「いいかい~」セリフ -->
    <TextView
        android:id="@+id/iikai"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@+id/name_area"
        android:fontFamily="@font/sanarifont"
        android:gravity="center_horizontal"
        android:text="@string/iikai"
        android:textColor="@android:color/black"
        android:textSize="30sp"
        android:visibility="invisible" />

</RelativeLayout>

変数名やid名が英語やローマ字が混在していますが、そこらへんの命名規約の破綻はご容赦ください。

レイアウトリソースファイルはもう1枚あります。
MainActivity内でLayoutInflaterTextViewオブジェクトにしてもらうためのタネです。
このTextViewは、プログラムの方で1字ずつ分解した「野」や「千」を表示するためのものです。
プログラムの方でふわーっと浮いてどこかへ行くアニメーションを設定します。

name_entity.xml
<?xml version="1.0" encoding="utf-8"?>
<!-- 名前1字分 -->
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:fontFamily="@font/sanarifont"
    android:foregroundGravity="bottom"
    android:textColor="@android:color/black"
    android:textSize="80sp" />

最後に文字列リソースファイルです。

strings.xml
<resources>
    <string name="app_name">湯婆婆</string>
    <string name="keiyakushodayo">契約書だよ。\nそこに名前を書きな。</string>
    <string name="keiyakusuru">契約する</string>
    <string name="hun">フン。\n%sというのかい。\n贅沢な名だねぇ。</string>
    <string name="iikai">今からお前の名前は%sだ。\nいいかい、%sだよ。\n分かったら返事をするんだ、%s!!</string>
</resources>

%sの箇所が、新しい名前を埋め込む箇所です。

ポイント

そもそも元ネタ「Javaで湯婆婆を実装してみる」がJavaで実装し、私のこのAndroidアプリもJavaで実装しているわけですから、入出力のUIは違えども、アルゴリズム的には同じすぎて、この私の記事の存在意義が疑われるところではあります。

ですのでこの記事はむしろAndroidアプリ開発におけるTipsという位置づけでお読みいただければと存じます。
そこで、

  • カスタムフォント
  • ObjectAnimatorAnimatorSetを使ったアニメーション
  • ソフトウェアキーボードの引っ込ませ方
  • AndroidのJavaはJavaではない不思議な世界

について以下説明します。

フォントにこだわってみました

今一度、前掲の紙に書いた名前がふわーっと浮いてどこかへ行ってしまうシーンを鑑賞していただきたいのですが、なんとも味のある字体です。
そこで似たようなフリーフォントを探しました。
それが細鳴りフォント(Sanarifont)です。ありがとうございます。

APIレベル26から、フォントリソースが導入されました。これが導入される前は難儀を強いられましたが、導入後はラクになったものです。

フォントファイル(.ttf、.ttc、.otf、.xml)を/res/fontフォルダに格納します。ファイル名はリソースIDとして使用されます。

使用箇所は、

プログラム上なら
Typeface typeface = getResources().getFont(R.font.sanarifont);

や、

レイアウトXML上なら
<TextView
    android:fontFamily="@font/sanarifont" />

といったところです。

字がふわーっと浮いてどこかへ行ってしまうアニメーション

「荻」や「千」の字を埋め込んだTextViewを移動させ透明化させます。

というわけで、参考にしたのがPieceX:AndroidStudioでアニメーション アニメーションで紙吹雪を降らせる方法を考えてみましたです。ありがとうございます。
MainActivityに定義したprivateメソッドのコードは、このサイトを参考にさせていただきました。

ポイントは、様々な挙動をするアニメーションを複合化して同時に再生することです。今回は、

  • 上昇
  • 左右にゆらゆら
  • 透明化

といった挙動をまとめてアニメーションしたかったわけです。

そこでandroid.animation.ObjectAnimatorを利用するのが適しているようです。
それぞれのObjectAnimatorオブジェクトに、プロパティ("translationY""scaleX"など)と値(1.0f0.1fなど)を設定し、Listにまとめてandroid.animation.AnimatorSetに俺とプレイトゥギャザーしてスタートしようぜ!(ルー大柴の声でお楽しみください)で済みます。

普段あまりアニメーションなんかやったことがない私は、ここにいちばん時間をかけてしまいました。でも楽しかったです。

このアニメーション、もっともっと凝ることもできるかと思いますし、そういうライブラリーも世に有るかとは思いますが、私は一つ自分へのかせとして標準APIのみで実装することに挑戦してみたかった3ので、この程度のアニメーションで「もういいか」とさせてください。

EditTextをクリックしたら出てきちゃうソフトウェアキーボードを引っ込めさせる

「契約する」ボタンをクリックしたら、見せ所はその下方のアニメーションと湯婆婆のキメ台詞なのに、依然としてソフトウェアキーボードがのさばって見えねーじゃねーか!ということで、

もう、引っ込んでくれて構わないのだよ。お疲れ。
InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE);
inputMethodManager.hideSoftInputFromWindow(editText.getWindowToken(), 0);
editText.clearFocus();

InputMethodManagerさんにhideしてくれるように頼みました。これがホントの神隠し。なんちって。

Stringを1字ずつの配列にしたくて、String#splitしてみたら...

なにげにこんなことでつまずきました。

当初はこんなコードで済ませていたのです。

String#splitでいいや
String[] names = name.split("");

ところがこれだと配列の0番目に空文字(長さ0の文字列)が入ってしまっていました。4
調べてみると、StringsplitメソッドはJDK1.7まではそのような挙動をし、JDK1.8以降ならば本来の私のやりたかった挙動をしてくれるようです。

え~~~。build.gradleにちゃんと、

build.gradle
compileOptions {
    sourceCompatibility = 1.8
    targetCompatibility = 1.8
}

とJDK1.8でお願いしますと設定してある5のにー?!...こういったあたりで「AndroidのJavaは、Javaに非ず」というダークな面が垣間見えますね。

ということで、なんかスマートじゃないなぁと後ろめたさもありながらも、String#charAtをグルグル回してやりました。

Try KotlinでString#splitしてみたら...

ちなみに、Kotlinに書き換えてみても、同じでした(配列の0番目が空文字だった)。
「AndroidのKotlinだからかな?」と思って、Try Kotlinにて以下のKotlinプログラムを実行してみました。

KotlinでString#splitしてみた
fun main(args : Array<String>) {
    println("Hello, Kobaba!")
    val name = "荻野千尋"
    val names = name.split("")
    for (aStr in names) {
        println(aStr)
    }
    println(names.size)
}
実行結果
Hello, Kobaba!

荻
野
千
尋

6

え。うそ。6個?!
このTry Kotlinで試せるすべてのバージョンで実行しても、同じ結果に。

EclipseでString#splitしてみたら...

急いでEclipse(Version: 2020-06)を起動して、以下のJavaを書きました。

EclipseでString#splitしてみたくて
public static void main(String[] args) {
    System.out.println("Hello, jababa!");
    String name = "荻野千尋";
    String[] names = name.split("");
    for (String aStr : names) {
        System.out.println(aStr);
    }
    System.out.println(names.length);
}
実行結果
Hello, jababa!
荻
野
千
尋
4

まあ、私のEclipseがJDK11だからなんでしょうけど、もう何が何やら。不思議な世界に迷い込んでしまったような気分、もう私は空を見上げてこう口遊くちずさむしかないです。

繰り返すあやまちの そのたび ひとは ただ青い空の 青さを知る

クラッシュ湯婆婆

そういえば、無記入の場合はちゃんとクラッシュ湯婆婆すべきなのがこのネタの重要な仕様でしたね!

安心してください、ちゃんとクラッシュ湯婆婆します。

crashyubaba.png

セリフ「java.lang.IllegalArgumentException: bound must be positive」まで一緒です。そうこなくっちゃ。

実現できなくて心残りなこと

  • フォントサイズが決め打ちのままです。5字以上の名前(「ダルビッシュ有」さんとか)を考慮していないままです。端末によってはとにかくはみ出ます。ちゃんと丁寧に<dimen>で定義すべきなのでしょう。
  • ふわーっと浮いてどこかへ行ってしまう字のも再現したかったです。
  • 紙から離脱当初は浮遊されるのをあらがって粘って踏みとどまろうとするのだが、いざ離脱してしまったら抵抗を諦めたという加速感。(もっといっぱいObjectAnimatorを作って組み合わせればいいのかな。あと、android.view.animation.AccelerateInterpolatorとか使えばいいのかな)
  • 映画のシーンをなぞって縦書き。(Androidで縦書きを実現することがどんだけ難儀なことか!)
  • サロゲートペア対策

サロゲートペア対策ねぇ...ちょ、ちょっと待って。

サロゲートペア対策「?田さん」よりも「获野千尋」!

まずこの湯婆婆ネタで賑わっているサロゲートペア漢字「?」の対策を講じようかと思い、でも今回使用させていただいたフォントでは「?」が含まれていないのではないかと危惧したので、文字情報技術促進協議会が提供しているIPAmj明朝フォントで試してみようと...したのですが。

そんなことよりも!

しつこいのですが、前掲の紙に書いた名前がふわーっと浮いてどこかへ行ってしまうシーンを鑑賞してください。または、ビデオ持っている方はそのシーンを眼ん玉ひん剥いて視聴してみてください。

おぎ」の字が「」となっているのです!くさかんむり「艹」+けものへん「犭」+「」ではなく、「」という構成になっているのです!「はぎ」と間違えている場合じゃない!

ナニその漢字?!製作ミス?!監督の遊び心?!諸説紛紛!!

しかも調べてみたら、「获」という漢字は実在していて、なんと「獲」の簡体字(中国で使われている簡略化した字体の漢字)だそうです!UnicodeはU+83B7。

千尋ちゃんは「オギノ」さんじゃなく「カクノ」さんなの?!
んでもって門田さんが面倒臭がって「门」とか書いちゃうノリ?!

サロゲートペア漢字もサポートしつつ、中国の簡体字もサポートしたフォント...そんなのあるのかしら?と探そうと思いましたが、とっくにありました。
私の持っているAndroidスマホの中に。標準フォントとして。

最後に

なんやかんやありましたが、結果一番お気に入りなのは、ランチャーアイコンです。

ic_launcher.png

スタジオジブリは作品の場面写真を常識の範囲で提供してくださっています。これもありがたいことですね。

以上です。良いお年を。


  1. この記事投稿時点での最新バージョンのAndroid Studioには、Javaで実装したプロジェクトを一気にKotlinに変換してくれる機能があります。でもその逆はありません。ですので、Javaで書いたものはいつでもKotlinに変換できますし。 

  2. このアニメーションGIFはダウンロード可能のようなのですが、他サイト(つまりこのQiita)に貼り付けていいのかわからなかったのでリンクにしました。 

  3. そう言うわりには、JetpackのAppCompatActivityを使っているというのはどういう了見なんだい?...湯婆婆様、そこだけは見逃しておくんなまし。 

  4. そのおかげで何度か試行してたら「荻野千尋」の4字すべて消えてしまう事態が発生。Randomがチョイスしたのが0だったんでしょう。「今からお前の名前はだ。いいかい、だよ。分かったら返事をするんだ、!!」って言われちゃいました。 

  5. おかげでラムダ式は書けました。 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

SpringBootのWebアプリケーションにMicrosoft IDプラットフォームでサインインさせる

概要

Microsoftの公式ドキュメントが混乱していたり、ライブラリの更新が追いついていなかったりで最新のSpringBootからMicrosoftアカウントでサインインさせることが難しくなっています。
現状でサインインを実装するためのあれこれをまとめました。

  • Azure AD用のSpringBootStarterはそのままでは起動できない
  • Microsoftのドキュメントも大体更新が追いついておらず、古い
  • 認証のみならSpring SecurityのOAuth2だけで可能
  • ほとんど日本語資料がない
  • 認証後にMicrosoftGraphを通じて各機能にアクセスするならMicrosoft Graph SDK for Javaを使用する1

前提条件

  • Java11
  • SpringBoot2.3.5(最新は2.4)
  • 基本の依存性はSpring Initialzrで書き出す
  • Gredle6.7
  • 筆者のOSはWindows10

Azure AD Spring Boot Starter client library for Java(2.3.5)で実装する

まず最初に当たるのはこのライブラリ2だと思うのですが、現状ではReadMe通りの設定では動きません→報告をもとに修正されましたがmavenへの反映をお待ちください

理由についても突き止めてGitHubで報告してありますが、SpringBoot2.3.5の依存性によりAAD-Starterが依存するnimbus-jose-jwtのバージョンが上がってしまい、起動できなくなっています。
これはnimbus-jose-jwtの8.10以降DefaultJWKSetCacheのコンストラクタにリフレッシュサイクルを指定する引数が追加されたために起こっています。
まずはこれに対応します。

AADAuthenticationFilterAutoConfigurationの設定を上書きする

幸いにこのバグの原因がAutoConfigの中で起こっているので、実行時にこれを解決することができます。(記事を書いている間にこの問題自体は修正されました)
バージョンを下げて対応するのは良しとせず、それを修正して使用します。
AAD-Starterの利用には基本的にConfigを書くことになりますので、そこでBean定義を上書きします。

AADOAuth2LoginConfig.java
/*ReadMeのサンプルにコメント部分を追加*/

@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class AADOAuth2LoginConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private OAuth2UserService<OidcUserRequest, OidcUser> oidcUserService;

    //設定プロパティをインジェクション
    @Autowired
    private AADAuthenticationProperties aadAuthProps;


    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
            .anyRequest().authenticated()
            .and()
            .oauth2Login()
            .userInfoEndpoint()
            .oidcUserService(oidcUserService);
    }

    //問題の設定を上書きするBean定義
    @Bean
    public JWKSetCache getJWKSetCache() {
        long lifespan = aadAuthenticationProperties.getJwkSetCacheLifespan();
        return new DefaultJWKSetCache(lifespan, lifespan, TimeUnit.MILLISECONDS);
    }
}

ただし、このBean定義には問題があります。キャッシュの寿命とリフレッシュサイクルが同じになるため、実行時に不規則なエラーが発生する可能性があります3
これを解決するにはDefaultJWKSetCacheの二つ目の引数を一つ目の引数よりも小さい数値に設定する必要があり、プロパティを自作するかコード内で指定する必要があります。
また、引数なしのコンストラクタは寿命を15分、リフレッシュサイクルを5分に設定されているようですので、そのサイクルに問題がなさそうであれば引数を渡さないという選択もあります。

アプリをArueに登録

これは基本的にドキュメント通りの設定で問題なく動きます。
注意しなくてはいけないのが、リダイレクトURLはSpringSecurityのデフォルトとなる{baseURL}/login/oauth2/code/azureでなくてはならないことです。
SpringSecurityのプロパティで変更できそうな気がしますが、私は成功しませんでした。

そしてもう一つ重要なのが、アプリに与えるAPIへのアクセス権限です。
これは完全に罠なのですが、ドキュメントの内容で必要なのはAzureADGraphの権限であって、MicroSoftGraphの権限ではありません。現在のAzureのWeb画面ではAzureADGraphの表示は最下部にあり、目立っているMicroSoftGraphの権限を付与してしまっていくら頑張ってもAADSTS90008を拝むことになります。これについてはうっかりミスですが、気付くのにはかなり時間がかかりました(コードのバグを疑っていたので…)。
これに気づいた後はすんなり部署やセキュリティーグループを利用したアクセス許可のコントロールも成功しました。

記述途中→ログイン後にGraphの機能でいろいろする

ライブラリの問題回避と修正で力尽きたので一旦保留とします…

まとめ

このライブラリの問題回避とアクセス権限の調査で、SpringSecurityでのOAuth2認証とAzureAD(Microsoft IDプラットフォーム)の連携の仕組みが良くわかりました。
Azure AD Spring Boot Starter client library for Javaは次バージョンで大きくアップデートされるようですが、現状は問題が多く含まれているようです。

皆さんもライブラリの調査でちょっとでも問題に気づいたら報告してみるといいかもしれません。

参考資料

Azure AD Spring Boot Starter client library for Java4
OAuth 2.0 Sample for Azure AD Spring Boot Starter client library for Java
A Java web application using Spring security which signs in users with the Microsoft identity platform
【Microsoft ID プラットフォーム】(Azure AD) Java Web アプリ (WAR file)-設定編-
【spring-projects/spring-security】OAuth 2.0 Migration Guide
Spring Security Reference12.1. OAuth 2.0 Login


  1. ただしこのSDKのアクセスポイントは旧バージョン(v1.0)であるため、v2.0を使用するときはbetaを使用する必要がある 

  2. 以下AAD-Starerと省略 

  3. nimbus-jose-jwtの修正もこれが原因だと思われ、この修正はその場しのぎであることはGitHubで共有されました 

  4. 多分一番新しいStarterのドキュメント 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

無限くら寿司ガチャを作ってみた

10月からスタートしたGoToEatキャンペーン皆さんは使用していますか?
農林水産省 GoToEatキャンペーン

私はちょくちょく使用していたのですが、なんでも巷で「無限くら寿司」というものが流行っているらしい。
GoToEatでもらえるポイントで食事をして、その食事でまたポイントが貰えるので、くら寿司を何回もお得に食べられるといったものです。(モラル的な話はおいておきます)
くら寿司公式GoToEatキャンペーンページ

GoToEatでポイントをもらうためには、くら寿司で一人あたりランチは税込み500円、ディナーは税込み1000円以上食事をする必要があります。
そこで、くら寿司のメニューを該当価格を超えるくらいでランダムで表示される「無限くら寿司ガチャ」を作ってみました。

作ったwebアプリはこちら↓
無限くら寿司ガチャ

今回は「SpringBoot」フレームワークを使用しました。
コード全体はgithubで公開しています↓
https://github.com/yutwoking/eternalKurazushi

今後こういったアプリを作成する際の手順としてこの記事を残しておきます。

アプリ作成環境や使用フレームワークなどの前提

  • MacBook Pro (13-inch, 2018, Four Thunderbolt 3 Ports)
  • macOS Catalina 10.15.6
  • Eclipse_2020-09
  • Java11
  • gradle
  • SpringBoot
  • Doma2

大まかな流れ

  1. ロジック部分の実装

    • くら寿司公式サイトからメニューの読み込み部分
    • データベースへの格納・摘出部分
    • ガチャロジック部分
  2. SpringBootフレームワーク部分

    • build.gradle記述
    • ランチャーの作成
    • コントローラの作成
    • html作成(view作成)
  3. webアプリ公開(AWS)

1. ロジック部分の実装

くら寿司公式サイトからのメニュー読み込み

まずはメニューを公式サイトから読み込みます。
公式メニューサイトのhtmlを解析する必要があります。
Jsoupというライブラリを使用して解析します。

Jsoup使用方法参考
https://www.codeflow.site/ja/article/java-with-jsoup

解析部分の実装は以下

LoadMenu.java
private static List<MenuModel>  loadMenuFromSite(String url) throws IOException{
        List<MenuModel> models = new LinkedList<>();

        Document doc = Jsoup.connect(url).get();
        Elements menusBySection = doc.select(".section-body");
        for (Element section : menusBySection) {
            String sectionName  = section.select(".menu-section-header h3").text();
            if (!StringUtils.isEmpty(sectionName)) {
                Elements menus  = section.select(".menu-list").get(0).select(".menu-item");
                for (Element menu : menus) {
                    String name = menu.select(".menu-name").text();
                    Elements summary = menu.select(".menu-summary li");

                    if (summary.size() >2) {
                        int price  = stringToInt(summary.get(0).select("p").get(0).text());
                        int kcal  = stringToInt(summary.get(0).select("p").get(1).text());
                        String area = summary.get(1).select("p").get(1).text();
                        boolean takeout = toBoolean(summary.get(2).select("p").get(1).text());
                        models.add(new MenuModel(name, price, kcal, area, takeout, sectionName));
                    } else if (summary.size() == 2) {
                        int price  = stringToInt(summary.get(0).select("p").get(0).text());
                        int kcal  = stringToInt(summary.get(0).select("p").get(1).text());
                        String area = "";
                        boolean takeout = toBoolean(summary.get(1).select("p").get(1).text());
                        models.add(new MenuModel(name, price, kcal, area, takeout, sectionName));
                    }
                }
            }
        }
        return models;
    }

基本的なJSoupの使用方法としては、

Document doc = Jsoup.connect(url).get();

でurlのhtmlを読み込んで、

Elements elements = doc.select(".section-body");

のようにselectメソッドを使用して、該当要素を抜き出していきます。

ちょっと実装コードは、if文やfor文がネストしちゃっていて見にくいのは反省。。。

データベースへの格納・摘出部分

次にデータベース周りの実装です。
Doma2 というjavaのDBアクセスフレームワークを使用しています。
Doma2公式はこちら

Domaには以下の特徴があります。

・注釈処理を使用して コンパイル時 にコードの生成やコードの検証を行う
・データベース上のカラムの値を振る舞いを持った Java オブジェクトにマッピングできる
・2-way SQL と呼ばれる SQL テンプレートを利用できる
・Java 8 の java.time.LocalDate や java.util.Optional や java.util.stream.Stream を利用できる
・JRE 以外のライブラリへの依存が一切ない

個人的にsqlファイルで管理できるところが好きで良く使用しているフレームワークです。
使用方法は公式がまとまっている&日本語なので参考にすると良いです。
実装コードはここでは割愛。
コード全体はgithubで公開しています↓
https://github.com/yutwoking/eternalKurazushi

ガチャロジック部分

Gacha.java
public static List<MenuModelForSearch> getResult(Areas area, boolean isLunch){
        List<MenuModelForSearch> result  = new ArrayList<>();
//thresholdにランチなら500,ディナーなら1000を格納
        int threshold = getThreshold(isLunch);
//candidatesに全メニューを格納
        List<MenuModelForSearch> candidates = MenuCaches.getSingleton().getMenuList(area, isLunch);

//取得したメニューの合計金額がthresholdを超えているかチェックし、超えるまでランダムでcandidatesからメニューを加える。
        while (isOverThreshold(threshold, result) == false) {
            addElement(result, candidates);
        }
//最後にランチメニューが含まれているかチェック。ランチメニューが含まれている場合、結果をランチメニューのみにする。
        checkIncludeLunchMenu(result);

        return result;
    }

各メソッドはコード全体を参照してください。

2. SpringBootフレームワーク部分

SpringBootに関しては、以下サイトを参考にしました。
https://qiita.com/gosutesu/items/961b71a95daf3a2bce96
https://qiita.com/opengl-8080/items/eb3bf3b5301bae398cc2
https://note.com/ymzk_jp/n/n272dc9e5c5d3

build.gradleの記述

gradleでSpringBootフレームワークを使用できるようにプラグインやライブラリをbuild.gradleに追記します。

今回の実際のbuild.gradleが↓で//spring-boot追記部分とコメントしてある部分を追記しています。

build.gradle
plugins {
    // Apply the java plugin to add support for Java
    id 'java'

    // Apply the application plugin to add support for building a CLI application
    id 'application'
    id 'eclipse'
    id 'com.diffplug.eclipse.apt' version '3.25.0'

    id 'org.springframework.boot' version '2.3.5.RELEASE' //spring-boot追記部分
    id 'io.spring.dependency-management' version '1.0.10.RELEASE'//spring-boot追記部分
}

version = '2.26.0-SNAPSHOT'
ext.dependentVersion = '2.24.0'

task copyDomaResources(type: Sync)  {
    from sourceSets.main.resources.srcDirs
    into compileJava.destinationDir
    include 'doma.compile.config'
    include 'META-INF/**/*.sql'
    include 'META-INF/**/*.script'
}

compileJava {
    dependsOn copyDomaResources
    options.encoding = 'UTF-8'
}

compileTestJava {
    options.encoding = 'UTF-8'
    options.compilerArgs = ['-proc:none']
}

repositories {
    mavenCentral()
    mavenLocal()
    maven {url 'https://oss.sonatype.org/content/repositories/snapshots/'}
}

dependencies {
    // Use JUnit test framework
    testImplementation 'junit:junit:4.12'

    // https://mvnrepository.com/artifact/org.jsoup/jsoup
    compile group: 'org.jsoup', name: 'jsoup', version: '1.13.1'

    // https://mvnrepository.com/artifact/org.apache.commons/commons-lang3
    compile group: 'org.apache.commons', name: 'commons-lang3', version: '3.11'

    annotationProcessor "org.seasar.doma:doma:${dependentVersion}"
    implementation "org.seasar.doma:doma:${dependentVersion}"
    runtimeOnly 'com.h2database:h2:1.3.175'
    // https://mvnrepository.com/artifact/org.postgresql/postgresql
    compile group: 'org.postgresql', name: 'postgresql', version: '42.2.8'

    // https://mvnrepository.com/artifact/com.zaxxer/HikariCP
    compile  group: 'com.zaxxer', name: 'HikariCP', version: '3.4.1'

    // https://mvnrepository.com/artifact/javax.inject/javax.inject
    compile group: 'javax.inject', name: 'javax.inject', version: '1'

    // https://mvnrepository.com/artifact/io.vavr/vavr
    compile group: 'io.vavr', name: 'vavr', version: '0.10.2'

    // https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-thymeleaf
    compile group: 'org.springframework.boot', name: 'spring-boot-starter-thymeleaf', version: '2.3.5.RELEASE'//spring-boot追記部分

    // https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-web
    compile group: 'org.springframework.boot', name: 'spring-boot-starter-web', version: '2.3.5.RELEASE'//spring-boot追記部分

    // https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter
    compile group: 'org.springframework.boot', name: 'spring-boot-starter', version: '2.3.5.RELEASE'//spring-boot追記部分

}


//spring-bootプロジェクトをサービス化するためにbootJarタスクを追記
bootJar { 
    launchScript()
}

application {
    // Define the main class for the application
    mainClassName = 'eternalKurazushi.ServerLuncher'
}

eclipse {
    classpath {
        file {
            whenMerged { classpath ->
                classpath.entries.removeAll { it.path == '.apt_generated' }
            }
            withXml { provider ->
                def node = provider.asNode()
                // specify output path for .apt_generated
                node.appendNode( 'classpathentry', [ kind: 'src', output: 'bin/main', path: '.apt_generated'])
            }
        }
    }
    jdt {
        javaRuntimeName = 'JavaSE-11'
    }
}

ランチャーの作成

ServerLuncher.java
@SpringBootApplication
public class ServerLuncher {

    public static void main(String[] args) throws Exception {
        SpringApplication.run(ServerLuncher.class, args);
        LoadMenu.init();
        MenuCaches.getSingleton().load();
    }

}

@SpringBootApplicationアノテーションを付けて、SpringApplication.runを実装するだけでOKです。

LoadMenu.init();
MenuCaches.getSingleton().load();

この部分はサーバー起動時にメニューを読み込んでDBに格納し、メモリにもメニューを持つようにしています。
実は今回の構成ならば、DBはいらなかったりもしますが、一応今後拡張した場合(おそらく拡張なんてしないけど)も考えてDBも使用しています。

コントローラの作成

FrontController.java
@Controller
public class FrontController {
    @RequestMapping("/")
    public String index() {
        return "index";
    }

    @RequestMapping(value = "/result", method = RequestMethod.POST)
    public String getResult(@RequestParam("radio_1") String eatTime, @RequestParam("radio_2") String areaString, Model model) {
        if (StringUtils.isEmpty(eatTime) || StringUtils.isEmpty(eatTime)) {
            return "error";
        }

        boolean isLunch = eatTime.equals("ランチ") ? true : false;
        Areas area = Areas.東日本;
        if (areaString.equals("西日本")) {
            area = Areas.西日本;
        } else if (areaString.equals("九州・沖縄")) {
            area = Areas.九州;
        }

        List<MenuModelForSearch> gachaResult = Gacha.getResult(area, isLunch);
        model.addAttribute("list", gachaResult);
        model.addAttribute("sum", getSumString(gachaResult));
        model.addAttribute("time", eatTime);
        model.addAttribute("area", areaString);
        return "result";
    }

@Controller アノテーションを使用し、コントローラを実装する。
@RequestMapping アノテーションで対応させるpathを指定する。このへんはjax-rsと似ている。
@RequestParam アノテーションを使用することでhtmlから値を受け取る。

model.addAttribute("list", gachaResult);

addAttributeによって、htmlに値を渡すことができる。
この例だと、listという変数名でgachaResultの値をhtmlに渡している。

return "result";

によって、/resources/templates/【Controller の戻り値】.htmlのテンプレートhtmlが返却される。
この例だと、/resources/templates/result.html が読み込まれて返却される。

html作成(view作成)

result.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>無限くら寿司ガチャ</title>
</head>
<body bgcolor=#99FFFF>
    <div style="text-align: center">
        <main>
            <h1>無限くら寿司ガチャ</h1>
            <p th:text="${time} + '  /  ' + ${area}" class="description"></p>
            <table border="1" align="center">
                <tr>
                    <th>種類</th>
                    <th>商品名</th>
                    <th>価格(税抜き)</th>
                    <th>カロリー</th>
                    <!--
                    <th>提供エリア</th>
                    <th>お持ち帰り</th>
                    -->
                </tr>
                <tr th:each="menu : ${list}">
                    <td th:text="${menu.type}"></td>
                    <td th:text="${menu.name}"></td>
                    <td th:text="${menu.price} + '円'"></td>
                    <td th:text="${menu.kcal} + 'kcal'"></td>
                    <!--
                    <td th:text="${menu.area}"></td>
                    <td th:text="${menu.takeout} ? '可' : '不可'"></td>
                    -->
                </tr>
            </table>
            <h3>
                <p th:text="${sum}" class="sum"></p>
            </h3>
            <br>

            <form method="POST" action="/result">
                <input type="hidden" name="radio_1" th:value="${time}"> <input
                    type="hidden" name="radio_2" th:value="${area}">
                <div style="margin: 2rem 0rem">
                    <input type="submit" value="同じ条件でもう一度ガチャを回す"
                        style="width: 250px; height: 50px">
                </div>
            </form>
            <form method="GET" action="/">
                <div style="margin: 2rem 0rem">
                    <input type="submit" value="戻る">
                </div>
            </form>
        </main>
    </div>
</body>
</html>

th:value="${変数名}"
とすることで、コントローラから受け取った値を使用することができる。
formタグを使用して、inputを作成しnameを指定すれば、コントローラに値を渡すことができる。

3. webアプリ公開(AWS)

今回はAWSを使用してwebアプリを公開しました。
シンプルなアプリなので使用したのは、

  • VPC
  • EC2
  • RDS

くらいです。
初心者はこの書籍がとても丁寧&練習もできるのでおすすめ。
Amazon Web Services 基礎からのネットワーク&サーバー構築 改訂版

最後に

仕事外で何か作成するのは久々でした。
仕事のプログラミングも楽しいけど、プライベートでのプログラミングはまた違った楽しさ(フレームワークの選定や環境作成など)もあるので定期的にスキルアップのためにも行っていきたい。

時間があれば以下に取り組みたい。

  • 独自ドメインを取得して設定
  • コードのリファクタリング
  • webアプリの接続プロトコルのhttps化
  • この記事では手順をだいぶささっと書いてしまったのでいくつかの記事に分けて一つ一つまとめてみる。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Visitor Pattern

Composite Patterの要素を取得する

Design Pattarm MENU

以下のクラス構成で確認します

クラス 説明
interface
face
型とaccept()を定義する
faceImple.class faceを実装する
faceList.class faceを実装する
Listを持つ
Listのiteratorを発行するメソッドを持つ
Visitor.class Visitor Patternを提供する
user(Main.class) 動作を確認する

*user 他の開発者がこのパターンを利用する、という意味合いを含みます

interface_face
interface face{
  void accept(Visitor visit);
}
faceImple.class
class faceImple implements face{
  public void accept(Visitor visitor){
         System.out.println("."+this);}
}
faceList.class
class faceList implements face{
  List ls = new ArrayList();
  void      add( face fc){ls.add(fc);}
  public    void accept(Visitor visit){
            visit.visitor(this);}
  Iterator  iterate(){return ls.iterator();}
}
Visitor.class
class Visitor{
      void visitor( faceImple imple ){
           imple.accept(this);}
      void visitor( faceList list ){
           Iterator it = list.iterate();
           while(it.hasNext()){
                 face fc = (face) it.next();
                 System.out.println("/"+fc);
                 fc.accept(this);
           }
      }
}
user(Main.class)
public static void main(String[] args){
       faceList f1 = new faceList();
       faceList f2 = new faceList();
       faceList f3 = new faceList();
       f3.add(new  faceImple());
       f3.add(new  faceImple());
       f2.add(new  faceImple());
       f2.add(f3);
       f1.add(f2);
       f1.accept(new Visitor());
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Java】モジュール

モジュールとは

  • パッケージの上位概念
  • 構成
    • パッケージ群と関連するリソース
    • 自身の構成情報を規定するモジュール定義ファイル

従来のパッケージ問題点

パッケージ単位でアクセス権限を設定できない

  • ライブラリ内部での利用を想定したパッケージの場合、ライブラリ外部からアクセスできないようにしたい
  • しかし内部用途のpublicクラスを不可視にする手段はない

.jarファイルは依存関係を表現できない

  • 複数のパッケージを.jar形式の圧縮ファイルとしてまとめ、ライブラリとして提供
  • しかし.jarではライブラリ間の依存関係は表現できない
  • .jarの中でどの型のAPIが外部からの利用を想定しているのか、動作のためにどのライブラリが必要かわからない

モジュール利用で解決!

  • 特定のライブラリ、フレームワークをグループにして束ねる
  • モジュール化されたライブラリではpublicクラスもより細かな管理が可能
    • 現在のモジュールの中だけでpublic
    • 特定のモジュールに対してのみpublic
    • 全モジュールに対してpublic (従来と同じpublic)

モジュール基本

  • /sec直下のmodule-info.javaでモジュール定義ファイル記述
  • モジュールが依存するパッケージ、外に公開するパッケージ情報を列挙
  • 必要ライブラリをrequiresで宣言してないとThe type java.net.http.HttpRequest is not accessible
module-info.java
//モジュール名foo
//fooモジュールではjava.net.httpを必要
module foo {
  requires java.net.http;
  requires mylib;
  requires gson;
}

標準ライブラリのモジュール

  • 標準ライブラリもモジュール化されている

基本モジュール

  • java.base
  • 標準ライブラリを一個一個requires追加するのは面倒なので、よく利用するパッケージをjava.baseモジュールにまとめられている
  • java.baseモジュールは暗黙的にロードされるので明示的にrequires宣言不要
  • Java標準ライブラリ全体を定義したjava.seモジュールもあるが不要なモジュールもロードしてしまう

推移的な依存

  • 推移的:ロードしたモジュールの先でさらに他のモジュールに依存していた場合を依存先もロード
  • requires transitiveで推移的な依存をいい感じに解決
module-info.java
module java.se { 
  requires transitive java.compiler;
  requires transitive java.datatranfer;
  requires transitive java.desktop;
//略
}

パッケージ公開

  • 自分のパッケージを公開したい場合
    • 外部に公開したいパッケージ(lib)、ライブラリ内で利用するパッケージがある(internal)
  • exportsディレックティブで公開したライブラリのみを外部公開できる
module-info.java
module mylib { 
  exports mylib.lib;
}
//OK
import mylib.lib.MainLib;
//NG! interna;パッケージは不可視
//import mylib.internal.Sublib;

public class ModuleClient {

  public static void main(String[] args) {
    var main = new MainLib();
    main.run();
  }
}
module foo {
  requires java.net.http;
  requires mylib;
  requires gson;
}

特定のモジュールに対してのみパッケージ公開

  • exports パッケージ to モジュール
  • 以下の場合、libパッケージはfooモジュールにのみ公開される
  • 公開先は,で複数指定可能
module-info.java
module mylib {
  exports mylib.lib to foo;
}

privateメソッドをopenにする

  • ディープリフレクション:リフレクションを利用し強制的にアクセス
    • モジュールexportsのみではディープリフレクションは不可能
import java.lang.reflect.InvocationTargetException;
import mylib.lib.MainLib;

public class ModuleClient2 {

  public static void main(String[] args) {
    try {
      var clazz = MainLib.class;
      var con = clazz.getConstructor();
      var m = con.newInstance();
      var name = clazz.getDeclaredField("name"); 
      name.setAccessible(true); //エラー
      System.out.println(name.get(m));

    } catch (InstantiationException | IllegalAccessException |
        IllegalArgumentException | InvocationTargetException
        | NoSuchMethodException | SecurityException | NoSuchFieldException e) {
      e.printStackTrace();
    }
  }
}

opensディレクティブでパッケージ宣言して解決!

  • あくまでも実行時のみパッケージ公開する
  • リフレクション以外で型を参照する場合はexportsディレクティブも併記
  • open module mylib {にするとモジュール配下のパッケージを全部open扱いにする
module-info.java
module mylib {
  opens mylib.lib;
  exports mylib.lib;
}

特殊モジュール

  • 擬似的なモジュール概念も提供されてる
    • 自動モジュール
    • 無名モジュール

自動モジュール

  • モジュールパスに配置された.jarファイル
  • module-info.javaを持たないライブラリ

マニフェスト情報から決定

  • マニフェストファイル(META-INF/MANIFEST.MF)のAutomatic-Module-Name属性で指定された名前をモジュール名にする
MANIFEST.MF
Manifest-Version: 1.0
Automatic-Module-Name: hoge.bar

.jarファイル名で決定

  • 命名規則
    • 拡張子.jarは削除
    • ハイフン以降の文字が数値/ドットのみの場合、ハイフン以降削除
    • 英数字以外は.に変換
    • 繰り返しドットは単一ドットに、先頭/末尾のドットは除去
      • ex: hoge-bar-1.0.5.jarはhoge.bar
  • 動作ルール
    • 配下の全パッケージをexports/opens
    • モジュールパスに登録された全モジュール、無名モジュールをrequires
    • 自動モジュールでexports/opensされたパッケージを他モジュールから利用するにはrequires必要

無名モジュール

  • クラスパスに配置された.jarファイル
  • モジュール名持たない
  • コンパイル時にアプリケーションモジュールからの参照は不可能
  • 自動モジュールからの参照は可能
    • アプリケーションモジュール:module-info.javaをもるモジュール
    • プラットフォームモジュール:標準ライブラリを構成するモジュール

非モジュールライブラリとの共存

  • モジュール化されたアプリから非モジュールライブラリを利用する
  • 以下のGsonライブラリはJavaオブジェクトをJSON形式の文字列に変換する
    • java.sqlパッケージ依存
    • 変換対象のJavaオブジェクトにディープリフレクション
  • Gsonが内部的に利用するjava.sqlモジュールへの参照がないとjava.sql.Timeクラスにもアクセスできない
//NG例 実行時エラー
import com.google.gson.Gson;

public class NoModuleLib {

  public static void main(String[] args) {
    var g = new Gson();
    var a = new Article("Java 11の変更点", "https://codezine.jp/article/corner/751");

    //オブジェクト内容をJSON化した結果を出力
    System.out.println(g.toJson(a));
  }
}
Article.java
public class Article {
  private String title;
  private String url;

  public Article(String title, String url) {
    this.title = title;
    this.url = url;
  }

  @Override
  public String toString() {
    return String.format("タイトル:%s(%s)",
        this.title, this.url);
  }
}

実行オプションで明示的にモジュール追加

  • 実行構成のVM引数で
  • --add-opens=モジュール名/パッケージ名=アクセス許可するモジュール
    • --add-modules=java.sql --add-opens=foo/example=gson
      • fooモジュールのexampleパッケージをgsonライブラリからアクセス可能にする
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Java】アノテーション

アノテーション

  • クラス・インターフェースなどの型、メンバーに付与できるマーク
  • アプリ本来のロジックとは直接関係しない付随情報をコードに追加できる
  • 対象となる宣言の先頭に記述

@Override

  • 標準ライブラリで提供されるアノテーション
  • 対象メソッドが基底クラスのメソッドをオーバーライドしていることを宣言
  • 引数のミスでオーバーライドできてない時もコンパイラが警告してくれる
public class Person {
  private String firstName;
  private String lastName;

  public Person(String firstName, String lastName) {
    this.firstName = firstName;
    this.lastName = lastName;
  }

  @Override
  public String toString() {
    return String.format("名前は、%s %s です。",
        this.lastName, this.firstName);
  }

  public String getLastName() {
    return this.lastName;
  }

  public String getFirstName() {
    return this.firstName;
  }
}

@WebServlet

  • サーブレットとはサーバー環境でJavaアプリを実行するための基本技術の一種
  • サーブレットの基本的な構成情報
    • urlPatterns:サーブレットを呼ぶためのURL
    • initParams:サーブレットで利用できるパラメータ情報

アノテーションのポイント

  • アノテーション入れ子が可能
  • 要素1個なら配列の{...}省略可能
    • urlPatterns = "/init-param"
  • 属性名valuesは省略可能
  • 属性がない場合、()省略可能
    • マーカーアノテーション(@Overrideなど)

標準アノテーション

  • @Override:基底クラスのメソッドを上書きしていることを宣言
  • @Deprecated:クラスやメンバーなどが非推奨であることを宣言
  • @SuppressWarnings:コンパイラー警告を抑制
  • @SafeVarargs:可変長引数の型安全を宣言
  • @FunctionalInterface:インターフェースを関数型インターフェースとして定義することを宣言

@Deprecated

  • クラス、メソッドなどが非推奨であることを宣言
  • IntegerコンストラクタはJava9以降で非推奨(since="9"
    • ドキュメンテーションでも記述
  • コンパイルは通るが警告がでる
Integer.java
/**
 * 中略
 * @deprecated
 * It is rarely appropriate to use this constructor. The static factory
 * {@link #valueOf(int)} is generally a better choice, as it is
 * likely to yield significantly better space and time performance.
 */

@Deprecated(since="9")
public Integer(int value) {
    this.value = value;
}

@SuppressWarnings

  • 特定範囲/種類のコンパイラー警告を非表示
    • 他の本来確認すべき警告の見落としを防げる
    • 意図して警告対象のコードを利用しているという意思表示
public class AnnotationBasic {

  public static Integer process() {
   @SuppressWarnings("deprecation")
    var i = new Integer(108);
   return i;
  }

  public static void main(String[] args) {
    System.out.println(process());
  }
}

@SafeVarargs

  • 可変長引数の型安全を宣言し、警告が表示されない
    • ジェネリック型を可変長引数とすると警告が出る
  • あくまでも型安全であることを開発者が認識してるという宣言

自作アノテーション

  • アノテーションは修飾子と違って自作できる
  • アノテーションは@interface命令で定義
  • @interface配下でアノテーションで利用できる属性を宣言
    • 以下の@ClassInfoではバージョン属性をversion属性、value属性で指定する
      • @ClassInfo(version="2.1", description="アノテーションテスト")
      • @ClassInfo(version="2.1")
  • アノテーション構成情報宣言するメタアノテーションを記述
    • @Documented:javadocにアノテーション情報反映
    • @Target:アノテーションをどの要素に適用するか決める
    • @Inherited:アノテーションが派生クラスにも継承
    • @Retention:アノテーション情報をどのタイミングまで保持するか
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

//構成情報宣言
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
//@interface命令でアノテーション定義
public @interface ClassInfo {
  //アノテーションで利用できる属性を宣言
  String value() default "";
  String version() default "";
  String description() default "";
}

リフレクションでアノテーション利用

  • リフレクション(Reflection):コードの実行中に型情報取得、操作する
  • 以下ではClass.forNameメソッドで指定した型情報をClassクラスで取得
  • マーカーアノテーション:アノテーションが保持する情報(属性)ではなくアノテーション存在そのものに関心がある
    • isAnnotationPresentメソッドでアノテーション有無確認
package mypackage;

//ClassInfoアノテーション指定
@ClassInfo(version = "2.1", description = "アノテーションの動作テスト")
// @ClassInfo("2.1")
public class AnnotationClient {
  public static void main(String[] args) throws ClassNotFoundException {
    //Classオブジェクトを経由して配下のメンバー取得
    var clazz = Class.forName("mypackage.AnnotationClient");
    //getAnnotationメソッドでクラスに付与されたアノテーション取得
    var info = (ClassInfo)
 clazz.getAnnotation(ClassInfo.class);
    System.out.println("バージョン:" +
        (info.value().equals("") ? info.version() : info.value()));
    System.out.println("説明:" + info.description());
  }
}

リフレクションのメソッド

オブジェクト生成

  • コンストラクタ経由でインスタンス生成
    • 呼び出すコンストラクタをClassクラスのgetConstructorメソッドで生成
    • newInstanceメソッドで呼び出すことでオブジェクト生成
    • Array.newInstanceメソッドで配列生成
    • setメソッドで配列要素設定
import java.io.File;
import java.lang.reflect.Array;
import java.lang.reflect.InvocationTargetException;
import java.util.Arrays;

public class ReflectInstance {

  public static void main(String[] args) {
    try {
      //Fileクラス取得
      var clazz = File.class;

      //コンストラクタ経由でFileオブジェクト生成
      var c = clazz.getConstructor(String.class);
      var fl = c.newInstance("./data/data.txt");
      System.out.println(fl);

      //サイズ2の配列生成
      var list = (File[]) Array.newInstance(File.class, 2);
      Array.set(list, 0, fl);
      System.out.println(Arrays.toString(list));
    } catch (InstantiationException | IllegalAccessException |
        IllegalArgumentException | InvocationTargetException
        | NoSuchMethodException | SecurityException e) {
      e.printStackTrace();
    }
  }
}

全メソッド取得

  • getMethodsメソッドでFileクラスのPublicメソッド列挙
    • publicメソッドをMethodオブジェクトの配列として返す
  • public以外も取りたいときはgetDeclaredMethodsメソッドで全メソッド取得
import java.io.File;

public class ReflectMethods {

  public static void main(String[] args) {
    //Fileクラス取得
    var str = File.class;
    //Fileクラス配下のpublicメソッドをgetNameでメソッド名列挙
    for (var m : str.getMethods()) {
      System.out.println(m.getName());
    }
  }
}

メソッド実行

  • Methodオブジェクトを介して実行
  • getMethodメソッドで個別のメソッドを取得し、invokeメソッドで実行
import java.io.File;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class ReflectInvoke {

  public static void main(String[] args) {
    try {
      //Fileクラス取得
      var clazz = File.class;
      //Fileオブジェクト生成
      var f1 = clazz.getConstructor(String.class).newInstance("./data/data.txt");
      var f2 = clazz.getConstructor(String.class).newInstance("./data/sample.txt");
      //renameToメソッド取得、実行
      Method m = clazz.getMethod("renameTo", File.class);
      System.out.println(m.invoke(f1, f2));
    } catch (NoSuchMethodException | SecurityException |
        InstantiationException | IllegalAccessException
        | IllegalArgumentException | InvocationTargetException e) {
      e.printStackTrace();
    }
  }
}

フィールド取得、設定

  • publicフィールドはgetFieldメソッドで取得
  • その他フィールドはgetDeclaredFiledメソッドで取得
  • 引数は取得したいフィールドの名前を指定
  • Fieldオブジェクト取得したらget/setメソッドでフィールド値を取得/設定
  • privateフィールドはそのままアクセスできないので、setAccessibleメソッドでアクセスを明示的に許可
  • 注意:リフレクションは低速
//PersonクラスのlastNameフィールド取得

import java.lang.reflect.InvocationTargetException;
import example.Person;

public class ReflectField {

  public static void main(String[] args) {
    try {
      var clazz = Person.class;
      var con = clazz.getConstructor(String.class, String.class);
      var p = con.newInstance("太郎", "山田");
      //フィールド取得
      var last = clazz.getDeclaredField("lastName");
      //privateフィールドへのアクセスを明示的に許可
      last.setAccessible(true);
      //get/setメソッドでフィールド値取得/設定
      last.set(p, "鈴木");
      System.out.println(last.get(p)); //鈴木
    } catch (NoSuchMethodException | SecurityException |
        InstantiationException | IllegalAccessException
        | IllegalArgumentException | InvocationTargetException |
        NoSuchFieldException e) {
      e.printStackTrace();
    }
  }
}

package example;
public class Person {
  private String firstName;
  private String lastName;
  public Person(String firstName, String lastName) {
    this.firstName = firstName;
    this.lastName = lastName;
  }
  @Override
  public String toString() {
    return String.format("名前は、%s %s です。",
        this.lastName, this.firstName);
  }
  public String getLastName() {
    return this.lastName;
  }
  public String getFirstName() {
    return this.firstName;
  }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Android】NavigationViewを使わないで、スライドするメニューをつくる

スライドするメニューはDialogFragmentで作成しました。
Activityの上に、DialogFragmentが被さるイメージです。

アニメーションの作成

左から右へスライドするアニメーション(res/anim/slide_in.xml)

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate
        android:duration="250"
        android:fromXDelta="-100%"
        android:toXDelta="0%" />

    <alpha
        android:duration="250"
        android:fromAlpha="0.8"
        android:toAlpha="1.0" />
</set>

右から左へスライドするアニメーション(res/anim/slide_out.xml)

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate
        android:duration="250"
        android:fromXDelta="0%"
        android:toXDelta="-100%" />

    <alpha
        android:duration="250"
        android:fromAlpha="1.0"
        android:toAlpha="0.2" />
</set>

themes.xmlに、作成したアニメーションのstyleを定義

<resources xmlns:tools="http://schemas.android.com/tools">
    ....
    <style name="SlideInMenuAnimation">
        <item name="android:windowEnterAnimation">@anim/slide_in</item>
        <item name="android:windowExitAnimation">@anim/slide_out</item>
    </style>
    ....
</resources>

スライドするメニューの作成

レイアウト

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/slide_menu"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/white"
    tools:context=".SlideInMenuFragment">

    <LinearLayout
        android:id="@+id/menu_area"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <!-- ActionBarと同じ高さのスペースの余白 -->
        <FrameLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="@color/design_default_color_primary"
            android:minHeight="?android:attr/actionBarSize" />

        <!-- メニュー部分 -->
        <ListView
            android:id="@+id/menu_list"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />
    </LinearLayout>

</FrameLayout>

DialogFragmentの実装

public class SlideInMenuFragment extends DialogFragment {

    public SlideInMenuFragment() {
        // Required empty public constructor
    }

    public static SlideInMenuFragment newInstance() {
        SlideInMenuFragment fragment = new SlideInMenuFragment();
        Bundle args = new Bundle();
        fragment.setArguments(args);
        return fragment;
    }

    @NonNull
    @Override
    public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
        Dialog dialog = new Dialog(getActivity());
        dialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
        dialog.setContentView(R.layout.fragment_slide_in_menu);

        // 全画面表示のDialogで、左寄せに配置する
        Window window = dialog.getWindow();
        window.getAttributes().windowAnimations = R.style.SlideInMenuAnimation;
        window.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
        window.setGravity(Gravity.TOP | Gravity.START);
        window.setLayout(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT);

        // LayoutのルートViewを設定
        FrameLayout rootView = dialog.findViewById(R.id.slide_menu);
        onViewCreated(rootView, savedInstanceState);
        return dialog;
    }

    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);

        String[] menuArray = {"メニュー", "メニュー", "メニュー"};
        // メニューの内容を設定する
        ListView menu = view.findViewById(R.id.menu_list);
        ArrayAdapter<String> adapter = new ArrayAdapter<String>(getContext(), android.R.layout.simple_list_item_1, menuArray);
        menu.setAdapter(adapter);
    }
}

MainActivityで呼ぶ

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // ActionBarの左端にボタンを設定する
        ActionBar actionBar = getSupportActionBar();
        if (actionBar != null) {
            actionBar.setDisplayHomeAsUpEnabled(true);
            // ↓アイコンは標準のソートアイコン使ってますが、≡ に変えたらいい感じになります。
            actionBar.setHomeAsUpIndicator(android.R.drawable.ic_menu_sort_by_size);
        }
    }

    @Override
    public boolean onOptionsItemSelected(@NonNull MenuItem item) {
        int id = item.getItemId();
        if (id == android.R.id.home) {
            // メニュー表示
            FragmentManager fragmentManager = getSupportFragmentManager();
            SlideInMenuFragment fragment = SlideInMenuFragment.newInstance();
            fragment.show(fragmentManager, fragment.getClass().getSimpleName());
            return true;
        }
        return super.onOptionsItemSelected(item);
    }

}

結果

アクションバーの左にあるアイコンをクリックすると、
image.png
 ↓
image.png
メニューが開きます。背景の透明部分をタップすると、メニューが閉じます。

注意

この実装だとジェスチャー操作して、メニューがピロピロと出てこないです。
自前でメニューを色々する必要があったのでこの方法を実装しましたが、
やっぱりNavigationView 使った方がいいです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Don't work Python with OpenCV on AMD Ryzen CPU on WSL2 Ubuntu 18.04 And 20.04

MAIN BOARD
ASUSTek COMPUTER INC.
PRIME X570-PRO
Rev X.0x

CPU
AMD Ryzen 7 3700X

MEM
G.Skill DDR4 3600

GPU
RTX 3900

It's looks like this error.
Don't work.

$ python
>>>import cv2
WARNING: CPU random generator seem to be failing, disable hardware random number generation
WARNING: RDRND generated: 0xffffffff 0xffffffff 0xffffffff 0xffffffff

Maybe it.
https://askubuntu.com/questions/1264439/warning-cpu-random-generator-seem-to-be-failing-disable-hardware-random-number

I update bios. It's fatal.
It's working update after.
My computer use PRIME X570-PRO BIOS 0602. I set up PRIME X570-PRO BIOS 2802.

Type "help", "copyright", "credits" or "license" for more information.
>>> import cv2
>>>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Java】オブジェクトの中のキーでdistinctを行いたい

概要

JavaもStreamの機能が実装されてから、Listから重複削除(distinct)が行えるようになりました。ただ、【Java8】Streamで重複削除(& 重複チェック)の記事にある通り、標準の機能ではListの値のみで重複削除が可能であり、Listがオブジェクトの場合にオブジェクトの中の指定したキーで重複削除が行えません。
ということで、今回はどのようにしたらオブジェクトの中の指定したキーで重複削除を実現できるか、というのを書いてみます。

対応候補

Stream.distinctをフィールド・プロパティなどで行うにはの記事にある通り、distinctを使わずにfilterとHashSetを組み合わせて重複削除を実現する方法が紹介されています。また、Java 8 Distinct by propertyの記事には、groupingByを使う方法やクラスをWrapperする方法、ライブラリを使う方法が紹介されています。
方法は色々あって、どれでも良いかなとは思うのですが、今回はRxJavaのライブラリを使う方法を紹介します。

<2020/11/13追記>
@Rui_K さんのコメントで紹介頂いた記事Java8のStream distinctを使用する方法では、標準Streamのdistinctの内部処理ではequalsが使われているようです。なので、equalsメソッドに同一判定のロジックを実装すれば、標準のメソッドでも対応できそう。

RxJavaを使った重複削除

RxJavaとは今さらながらのRxJava - 概要編の記事に紹介されている通り、リアクティブプログラミングを実現するために開発されたライブラリです。RxJavaで用意されているObservableクラスにおいてdistinctメソッドが用意されていて、このメソッドでは重複対象とするキーを指定することができます。こちらのドキュメントを参照ください。
サンプルコードは以下の通りです。ここではUserクラスのemailの値で重複削除を行っています。

sample.java
List<User> distinctUserRecords = Observable.fromStream(userRecords.stream())
    .distinct(u -> u.getEmail()).toList().blockingGet();
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む