- 投稿日:2020-11-13T21:29:34+09:00
備忘録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タグまで
引用、参考
- 投稿日:2020-11-13T21:27:30+09:00
使用したクーポンコードを指定したエンドポイントにポストバックする
広告の成果計測等のために、使用されたクーポンコード等の情報を含めて計測システムのエンドポイントへポストバックする必要がある際の成果通知の仕方です。
今回は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.javapublic 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(); }参考
- 投稿日:2020-11-13T20:43:19+09:00
湯婆婆Androidアプリを作ってみました
メリークリスマス。この記事は、湯婆婆 Advent Calendar 2020の4日目です。
賛辞と謝辞
まずは@Nemesisさんに賛辞と謝辞を送ります。
Javaで湯婆婆を実装してみるという記事で、このようなムーヴメントを興こせるだなんて、素晴らしいです。
呼んでいる胸のどこか奥でいつも心踊る夢を見たい気分にさせていただきました。私にこのような記事を投稿させてもらえる機会を与えていただき、ありがとうございます。
先日、Kotlinで湯婆婆を実装という記事を投稿させていただきました。
でも私の中では心のどこかでAndroidアプリにしたい、という想いが残っていて。
ということで作ったみた次第です。しかし今回はKotlinではなく、Javaで1。やりたいこと
まずは、こちらの名シーンをご覧ください。
GIFMAGAZINE:紙に書いた名前がふわーっと浮いてどこかへ行ってしまうGIF画像 2
荻野千尋が書いた字が、ふわーっと浮いてどこかへ行ってしまうシーンです。これを実現したいです。
完成したアプリ
名前を入力してボタンをクリックすると、1字を残してふわーっと浮いてどこかへ行ってしまうアプリです。
なお、このアニメーションGIFを作るにあたって、うっかり荻野千尋を、誤って「萩」にしてしまっていました。これはオギノさんハギノさんあるあるなのかもしれませんが、実はこれが後々に恐ろしいことにつながっていきます...詳しくは後述をご覧ください(勿体ぶってすみません)。
コード
とりあえずまずは、ずらずらずら~っと掲載しておきます。
可能な限りコメントを付けましたので、日本語と照らし合わしながら解読していただければ幸いです。
ポイント解説は後ほど。画面を実現するActivityクラスです。というか、Javaのプログラムはこれ1個だけのアプリです。
MainActivity.javapublic 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
内でLayoutInflater
にTextView
オブジェクトにしてもらうためのタネです。
この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という位置づけでお読みいただければと存じます。
そこで、
- カスタムフォント
ObjectAnimator
とAnimatorSet
を使ったアニメーション- ソフトウェアキーボードの引っ込ませ方
- 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.0f
や0.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
調べてみると、String
のsplit
メソッドはJDK1.7まではそのような挙動をし、JDK1.8以降ならば本来の私のやりたかった挙動をしてくれるようです。え~~~。build.gradleにちゃんと、
build.gradlecompileOptions { 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だからなんでしょうけど、もう何が何やら。不思議な世界に迷い込んでしまったような気分、もう私は空を見上げてこう口遊むしかないです。
繰り返すあやまちの そのたび ひとは ただ青い空の 青さを知る
クラッシュ湯婆婆
そういえば、無記入の場合はちゃんとクラッシュ湯婆婆すべきなのがこのネタの重要な仕様でしたね!
安心してください、ちゃんとクラッシュ湯婆婆します。
セリフ「java.lang.IllegalArgumentException: bound must be positive」まで一緒です。そうこなくっちゃ。
実現できなくて心残りなこと
- フォントサイズが決め打ちのままです。5字以上の名前(「ダルビッシュ有」さんとか)を考慮していないままです。端末によってはとにかくはみ出ます。ちゃんと丁寧に
<dimen>
で定義すべきなのでしょう。- ふわーっと浮いてどこかへ行ってしまう字の影も再現したかったです。
- 紙から離脱当初は浮遊されるのを抗って粘って踏みとどまろうとするのだが、いざ離脱してしまったら抵抗を諦めたという加速感。(もっといっぱい
ObjectAnimator
を作って組み合わせればいいのかな。あと、android.view.animation.AccelerateInterpolator
とか使えばいいのかな)- 映画のシーンをなぞって縦書き。(Androidで縦書きを実現することがどんだけ難儀なことか!)
- サロゲートペア対策
サロゲートペア対策ねぇ...ちょ、ちょっと待って。
サロゲートペア対策「?田さん」よりも「获野千尋」!
まずこの湯婆婆ネタで賑わっているサロゲートペア漢字「?」の対策を講じようかと思い、でも今回使用させていただいたフォントでは「?」が含まれていないのではないかと危惧したので、文字情報技術促進協議会が提供しているIPAmj明朝フォントで試してみようと...したのですが。
そんなことよりも!
しつこいのですが、前掲の紙に書いた名前がふわーっと浮いてどこかへ行ってしまうシーンを鑑賞してください。または、ビデオ持っている方はそのシーンを眼ん玉ひん剥いて視聴してみてください。
■千と千尋お得情報メモ 荻野千尋…と書いてあると思いきや、荻の字が間違っています!本来“火”と書くべきところが“犬”になっていますね。千尋さんは書き間違えただけなのか、わざと間違えたのか…気になるところですぅーー? #千と千尋の神隠し #千 #せんちひ pic.twitter.com/64i5tN9SAQ
— アンク@金曜ロードSHOW!公式 (@kinro_ntv) January 20, 2017「荻」の字が「获」となっているのです!くさかんむり「艹」+けものへん「犭」+「火」ではなく、「犬」という構成になっているのです!「萩」と間違えている場合じゃない!
ナニその漢字?!製作ミス?!監督の遊び心?!諸説紛紛!!
しかも調べてみたら、「获」という漢字は実在していて、なんと「獲」の簡体字(中国で使われている簡略化した字体の漢字)だそうです!UnicodeはU+83B7。
千尋ちゃんは「オギノ」さんじゃなく「カクノ」さんなの?!
んでもって門田さんが面倒臭がって「门」とか書いちゃうノリ?!サロゲートペア漢字もサポートしつつ、中国の簡体字もサポートしたフォント...そんなのあるのかしら?と探そうと思いましたが、とっくにありました。
私の持っているAndroidスマホの中に。標準フォントとして。最後に
なんやかんやありましたが、結果一番お気に入りなのは、ランチャーアイコンです。
スタジオジブリは作品の場面写真を常識の範囲で提供してくださっています。これもありがたいことですね。
以上です。良いお年を。
この記事投稿時点での最新バージョンのAndroid Studioには、Javaで実装したプロジェクトを一気にKotlinに変換してくれる機能があります。でもその逆はありません。ですので、Javaで書いたものはいつでもKotlinに変換できますし。 ↩
このアニメーションGIFはダウンロード可能のようなのですが、他サイト(つまりこのQiita)に貼り付けていいのかわからなかったのでリンクにしました。 ↩
そう言うわりには、Jetpackの
AppCompatActivity
を使っているというのはどういう了見なんだい?...湯婆婆様、そこだけは見逃しておくんなまし。 ↩そのおかげで何度か試行してたら「荻野千尋」の4字すべて消えてしまう事態が発生。
Random
がチョイスしたのが0だったんでしょう。「今からお前の名前はだ。いいかい、だよ。分かったら返事をするんだ、!!」って言われちゃいました。 ↩おかげでラムダ式は書けました。 ↩
- 投稿日:2020-11-13T20:24:15+09:00
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
- 投稿日:2020-11-13T18:00:08+09:00
無限くら寿司ガチャを作ってみた
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
大まかな流れ
ロジック部分の実装
- くら寿司公式サイトからメニューの読み込み部分
- データベースへの格納・摘出部分
- ガチャロジック部分
SpringBootフレームワーク部分
- build.gradle記述
- ランチャーの作成
- コントローラの作成
- html作成(view作成)
webアプリ公開(AWS)
1. ロジック部分の実装
くら寿司公式サイトからのメニュー読み込み
まずはメニューを公式サイトから読み込みます。
公式メニューサイトのhtmlを解析する必要があります。
Jsoupというライブラリを使用して解析します。Jsoup使用方法参考
https://www.codeflow.site/ja/article/java-with-jsoup解析部分の実装は以下
LoadMenu.javaprivate 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.javapublic 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/n272dc9e5c5d3build.gradleの記述
gradleでSpringBootフレームワークを使用できるようにプラグインやライブラリをbuild.gradleに追記します。
今回の実際のbuild.gradleが↓で//spring-boot追記部分とコメントしてある部分を追記しています。
build.gradleplugins { // 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化
- この記事では手順をだいぶささっと書いてしまったのでいくつかの記事に分けて一つ一つまとめてみる。
- 投稿日:2020-11-13T16:32:09+09:00
Visitor Pattern
Composite Patterの要素を取得する
以下のクラス構成で確認します
クラス 説明 interface
face型とaccept()を定義する faceImple.class faceを実装する faceList.class faceを実装する
Listを持つ
Listのiteratorを発行するメソッドを持つVisitor.class Visitor Patternを提供する user(Main.class) 動作を確認する *user 他の開発者がこのパターンを利用する、という意味合いを含みます
interface_faceinterface face{ void accept(Visitor visit); }faceImple.classclass faceImple implements face{ public void accept(Visitor visitor){ System.out.println("."+this);} }faceList.classclass 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.classclass 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()); }
- 投稿日:2020-11-13T12:11:39+09:00
【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.javamodule java.se { requires transitive java.compiler; requires transitive java.datatranfer; requires transitive java.desktop; //略 }パッケージ公開
- 自分のパッケージを公開したい場合
- 外部に公開したいパッケージ(lib)、ライブラリ内で利用するパッケージがある(internal)
- exportsディレックティブで公開したライブラリのみを外部公開できる
module-info.javamodule 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.javamodule 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.javamodule mylib { opens mylib.lib; exports mylib.lib; }特殊モジュール
- 擬似的なモジュール概念も提供されてる
- 自動モジュール
- 無名モジュール
自動モジュール
- モジュールパスに配置された.jarファイル
module-info.java
を持たないライブラリマニフェスト情報から決定
- マニフェストファイル(META-INF/MANIFEST.MF)のAutomatic-Module-Name属性で指定された名前をモジュール名にする
MANIFEST.MFManifest-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.javapublic 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ライブラリからアクセス可能にする
- 投稿日:2020-11-13T10:54:33+09:00
【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は省略可能
- @WebServlet (values="init-param")
- @WebServlet ("init-param")
- 属性がない場合、()省略可能
- マーカーアノテーション(@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; } }
- 投稿日:2020-11-13T10:01:08+09:00
【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); } }結果
アクションバーの左にあるアイコンをクリックすると、
↓
メニューが開きます。背景の透明部分をタップすると、メニューが閉じます。注意
この実装だとジェスチャー操作して、メニューがピロピロと出てこないです。
自前でメニューを色々する必要があったのでこの方法を実装しましたが、
やっぱりNavigationView 使った方がいいです。
- 投稿日:2020-11-13T01:43:52+09:00
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.0xCPU
AMD Ryzen 7 3700XMEM
G.Skill DDR4 3600GPU
RTX 3900It'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 0xffffffffI 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 >>>
- 投稿日:2020-11-13T01:07:49+09:00
【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.javaList<User> distinctUserRecords = Observable.fromStream(userRecords.stream()) .distinct(u -> u.getEmail()).toList().blockingGet();