- 投稿日:2021-03-22T22:28:00+09:00
[Android/Java] Jetpack LiveData (Jetpack ViewModel MVVM+Repositoryパターン)
Android Studio 4.1.3(windows版) での流れとなります
ここではMutableLiveDataをRepositoryで持ち単純なデータのModelを扱うだけの
シンプルな構成で一連の流れを説明します以下の記事のプロジェクトベースでの説明となります
Android DeveloperのJetpack LiveData/MVVM+Repositoryパターンの説明は以下です
Layoutの準備
ここではViewBindingを使用して実装します
ViewBindingの設定方法を知りたい方は以下を参照してください
MainFragmentのテンプレートにあるTextViewはidが指定されていないため
ここではtextViewを指定しますデータのModelを作成
ここでは単純なString Nameだけを持つUserModelを用意します
UserModel.javapublic class UserModel { public String Name; }Repositoryを作成
ここではUserModelを扱うMutableLiveDataとそのgetterとNameをセットするメソッドを用意します
※ データの更新を行うためMutableLiveDataを使用します
setValue()/postValue()の違いを詳しく知りたい方は他の方の記事でいくつか説明がありますので探してみてくださいMainRepository.javapublic class MainRepository{ final MutableLiveData<UserModel> user = new MutableLiveData<>(); public MutableLiveData<UserModel> getUser() { return user; } public void setName(String name) { UserModel userModel = user.getValue(); if (userModel == null) { Log.d("MainRepository", "user = null"); userModel = new UserModel(); } userModel.Name = name; // setValue()はメインスレッドから呼び出す必要があります //user.setValue(userModel); user.postValue(userModel); } }ViewModelの実装
Repositoryのインスタンスを生成
Repositoryのメソッドの呼び出し用のメソッドを用意しますMainViewModel.javapublic class MainViewModel extends ViewModel { // TODO: Implement the ViewModel final MainRepository repository = new MainRepository(); public MutableLiveData<UserModel> getUser() { return repository.getUser(); } public void setName(String name) { repository.setName(name); } }Fragmentの実装
Fragmentの新規作成時に自動生成されたコードにViewModelの処理を追加します
Nameに値を設定するとobserveのonChanged()が呼び出されTextViewの内容が書き換えられますMainFragment.java@Override public void onActivityCreated(@Nullable Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); mViewModel = new ViewModelProvider(this).get(MainViewModel.class); // setを実行するとobserveのonChanged()が呼び出されます mViewModel.setName("test"); mViewModel.getUser().observe(getViewLifecycleOwner(), new Observer<UserModel>() { @Override public void onChanged(UserModel user) { Log.d("MainFragment", user.Name); binding.textView.setText(user.Name); // getValue()で値の取得もできます Log.d("MainFragment", "getValue: " + mViewModel.getUser().getValue().Name); } }); }
- 投稿日:2021-03-22T22:28:00+09:00
[Android] Jetpack LiveData (Jetpack ViewModel MVVM+Repositoryパターン)
Android Studio 4.1.3(windows版) での流れとなります
ここではMutableLiveDataをRepositoryで持ち単純なデータのModelを扱うだけの
シンプルな構成で一連の流れを説明します以下の記事のプロジェクトベースでの説明となります
Android DeveloperのJetpack LiveData/MVVM+Repositoryパターンの説明は以下です
Layoutの準備
ここではViewBindingを使用して実装します
ViewBindingの設定方法を知りたい方は以下を参照してください
MainFragmentのテンプレートにあるTextViewはidが指定されていないため
ここではtextViewを指定しますデータのModelを作成
ここでは単純なString Nameだけを持つUserModelを用意します
UserModel.javapublic class UserModel { public String Name; }Repositoryを作成
ここではUserModelを扱うMutableLiveDataとそのgetterとNameをセットするメソッドを用意します
※ データの更新を行うためMutableLiveDataを使用します
setValue()/postValue()の違いを詳しく知りたい方は他の方の記事でいくつか説明がありますので探してみてくださいMainRepository.javapublic class MainRepository{ final MutableLiveData<UserModel> user = new MutableLiveData<>(); public MutableLiveData<UserModel> getUser() { return user; } public void setName(String name) { UserModel userModel = user.getValue(); if (userModel == null) { Log.d("MainRepository", "user = null"); userModel = new UserModel(); } userModel.Name = name; // setValue()はメインスレッドから呼び出す必要があります //user.setValue(userModel); user.postValue(userModel); } }ViewModelの実装
Repositoryのインスタンスを生成
Repositoryのメソッドの呼び出し用のメソッドを用意しますMainViewModel.javapublic class MainViewModel extends ViewModel { // TODO: Implement the ViewModel final MainRepository repository = new MainRepository(); public MutableLiveData<UserModel> getUser() { return repository.getUser(); } public void setName(String name) { repository.setName(name); } }Fragmentの実装
Fragmentの新規作成時に自動生成されたコードにViewModelの処理を追加します
Nameに値を設定するとobserveのonChanged()が呼び出されTextViewの内容が書き換えられますMainFragment.java@Override public void onActivityCreated(@Nullable Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); mViewModel = new ViewModelProvider(this).get(MainViewModel.class); // setを実行するとobserveのonChanged()が呼び出されます mViewModel.setName("test"); mViewModel.getUser().observe(getViewLifecycleOwner(), new Observer<UserModel>() { @Override public void onChanged(UserModel user) { Log.d("MainFragment", user.Name); binding.textView.setText(user.Name); // getValue()で値の取得もできます Log.d("MainFragment", "getValue: " + mViewModel.getUser().getValue().Name); } }); }
- 投稿日:2021-03-22T18:14:51+09:00
美大生のためのプログラミング入門:繰り返し(ループ)
※ Qiita では、本文部分のみの印刷に苦労します。そのため、同じ内容を以下のページにも掲載しています。プリンツアウトしたり PDF 化したい人は、こちらのページを利用して下しさい:
一覧はこちら:
http://gurakura.sakura.ne.jp/series/美大生のためのプログラミング入門/Qiita 版の総合目次:
https://qiita.com/iigura/items/37180d127da93d0b8abbはじめに
Composition with Grid IX by Piet Mondrian, 1919.
https://www.wikiart.org/en/piet-mondrian/composition-with-grid-ix-1919変数の次は「繰り返し」について学びます。繰り返し処理は、プログラミング用語ではループ(loop)と呼ばれています。前の章で、変数とその使い方について学びましたが、ループにおいても変数は重要な役割を果たします。
変数を使いこなすために重要な本質的な考え方は、処理の抽象化でした。具体的な値ではなく変数を使って抽象的に考える事により、プログラムを変更することなく様々な描画に使用できることを学びました。プログラムで変更する場所は変数の値(初期値)を設定する部分だけであり、そのコード片を変更するだけで異なる絵を描くことができるようになりました。
この章では、変数の値をプログラムにて変更する方法を学びます。これにより、プログラムの効果を何十倍や何百倍・何億倍などなど、好きなだけ拡大できるようになります。
ループの必要性を体験する
本章でも変数のときと同様、まずはループの必要性を体験するところから始めていきましょう。冒頭に挙げた絵を参考に、格子状に線を描いてみましょう。例えば、400 × 300 のキャンバスに、40 × 30 の大きさの格子を書くとしましょう。その場合、以下のようなプログラムが考えられます。
// draw grid ver.0 size(400,300); background(255,255,255); strokeWeight(3); // draw vertical lines line( 0,0, 0,400); line( 40,0, 40,400); line( 80,0, 80,400); line(120,0, 120,400); line(160,0, 160,400); line(200,0, 200,400); line(240,0, 240,400); line(280,0, 280,400); line(320,0, 320,400); line(360,0, 360,400); line(400,0, 400,400); // draw horizontal lines line(0, 0, 400, 0); line(0, 30, 400, 30); line(0, 60, 400, 60); line(0, 90, 400, 90); line(0,120, 400,120); line(0,150, 400,150); line(0,180, 400,180); line(0,210, 400,210); line(0,240, 400,240); line(0,270, 400,270); line(0,300, 400,300);縦線・横線、それぞれ 11 個の line 関数にて格子のための平行線が描けました。でもこれ、似たようなコード片だらけで少々冗長な感じがします(注1)。どの line 関数も縦線か横線を書いているだけなので、変数を使って抽象化を試みてみます。例えば縦線を書く場合、描画位置の x 座 標を px という変数で表すと以下の様に書けます。
注1:プログラムの場合、似たようなコード片はあまり良い兆候ではありません。このプ ログラムをみて「面倒くさいなぁ」と思った人は、プログラマとして良い感性を持っているのかもしれません。
変数を使った line 関数部分を抽象化したプログラムの例:
// draw grid ver.1 size(400,300); background(255,255,255); strokeWeight(3); // draw vertical lines int px=0; line(px,0, px,400); px=40; line(px,0, px,400); px=80; line(px,0, px,400); px=120; line(px,0, px,400); px=160; line(px,0, px,400); px=200; line(px,0, px,400); px=240; line(px,0, px,400); px=280; line(px,0, px,400); px=320; line(px,0, px,400); px=360; line(px,0, px,400); px=400; line(px,0, px,400); // draw horizontal lines(省略)無事、縦線に関する line 関数部分は全て line(px,0, px,400); というプログラム片に統一されました。しかし、変数 px については具体的な値を毎回設定しているので、
抽象化されているとは言えません。もう、これ以上、抽象化することはできないのでしょか?...とまあ、こんな風に書いてあったら大体はまだ抽象化できるわけです。今回の場合、px に代入される数に着目してみましょう。px は格子横 幅の分(40 ピクセル)づつ増加していっています。例えば、今 px に 80 と いう値が代入されているとします。次はこの値に 40 を加えた値を px とできれば良いわけです(その場合、px は 80+40 で 120 という値になります)。ちょっと数式っぽい書き方をすると、px + 40 → px という感じでしょうか(矢印は、ある値を変数に代入することを意味しています)。
この矢印による表現(表記)を使うと、左右逆にしてこんな風にも書けます:px ← px + 40。なぜこのように左右を入れ替えて記述するかというと、多くのコンピュータ言語では左辺に右辺を代入するという形式を取るからです。そして、代入を意味する記号として = を使用するものが多い状況となっています。Processing で使用している Java というプログラミング言語でもそうです。なので、px ← px + 40 は px=px+40 と記述されます。
記号 = を数学でいうところの「等号」と解釈してしまうと、非常に混乱してしまいます。この場合の = という記号は、左辺と右辺が等しいことを示す等号ではなく、右辺を左辺に入れる(設定する)「代入」の意味であると読み替えるようにして下さい。ちなみに、等号を表す記号は Java では == と = を 2 つ連続して表します。プログラミング言語の世界では = と == は異なることにも注意して下さい。
さて、このように px の値を更新する方法を学んだので、改めてpx の値を設定する部分も含めて抽象化してみます:
// draw grid ver.2 size(400,300); background(255,255,255); strokeWeight(3); // draw vertical lines int px=0; line(px,0, px,400); px=px+40; line(px,0, px,400); px=px+40; line(px,0, px,400); px=px+40; line(px,0, px,400); px=px+40; line(px,0, px,400); px=px+40; line(px,0, px,400); px=px+40; line(px,0, px,400); px=px+40; line(px,0, px,400); px=px+40; line(px,0, px,400); px=px+40; line(px,0, px,400); px=px+40; line(px,0, px,400); // draw horizontal lines(省略)抽象化の結果、プログラムはどうなったでしょうか?px=px+40 と line(px,0, px,400) を 11 回繰り返すだけとなりました。同じプログラム片を繰り返すだけですので、全くもって冗長です。これもなんとかしたいところです。
幸いなことに、このように考える人は私だけではなく、プログラミング言語の設計者を始めとする多くの人がそのように感じるものです。そのため、プログラミング言語にはこのような冗長なプログラムを簡潔に記述するための仕組みが用意されています。
ループを使ったプログラムを書く
上のプログラムでは、同じコードが 11 回も繰り返されていました。このような冗長なプログラムとなるのを避けるために、Processing で使用している Java では、以下のような仕組みを用意しています。
do { 繰り返したい処理 } while(繰り返す条件);今回の場合、繰り返したい処理は px=px+40 と line(px,0, px,400) ですので実際のプログラム片は次のようになります:
do { line(px,0, px,400); px=px+40; } while(px が 400 になるまで);さて、「px が 400 になるまで」というのはどのように記述したら良いのでしょうか?このプログラムでは、px は 0 から始まり、40 づつ値が増えていきます。ですので「px が 400 になるまで」というのは、「px が 400 以下である間」と読み替えることができます。Processing のコードでは、px が 400 以下というのは、px<=400 と記述されます。数学記号でいうところの ≤ は、いわゆる半角英数文字では表現できないため、不等号と等号を用いて <= と表します。ちなみに、「以下」を表す表記は Java においては <= のみであり、=< と書くことは許されていません。
コメント:=< と書いても良いと思いますが、Processing で使用している Java という言語の設計者はそのようには考えなかったようです。設計した人達がそのように考えた結果ですので、そこに理由はありません。なぜ、<= は良くて =< は駄目なのか?などとは悩んではいけません(ヒトが作りしものであり、神が作りしものではないので...)。
実際のプログラムでは以下のようになります:
// draw grid ver.3 size(400,300); background(255,255,255); strokeWeight(3); // draw vertical lines by do - while loop. int px=0; do { line(px,0, px,400); px=px+40; } while(px<=400); // draw horizontal lines(省略)このプログラムでは、line(0,0,0,300); も実行されているため、厳密にはこれまでのプログラムとは異なるものです。これまでのものと全く同じ動作に変更するためには、int px=0; を int px=40; に変更すれば十分です。
しばらくの間、縦線のみを描く部分だけをプログラムとして掲載していました。ループを使うと同じような処理を大量に書かなくても良いため、実際に動作するプログラムであっても、次に示すように短いものとなります。
// draw grid ver.4 size(400,300); background(255,255,255); strokeWeight(3); // draw vertical lines by do - while loop. int px=0; do { line(px,0, px,400); px=px+40; } while(px<=400); // draw horizontal lines int py=0; do { line(0,py, 400,py); py=py+30; } while(py<=300);さらなる改良
上のリストをを見ると、例えば 400 という数値など、複数の場所に書かれているものがあります。これはどれもキャンパスの横幅を示したものであり、400 という数値を見ただけでは、
それがキャンパスの横幅を示すものであるとは分かりません。また、キャンパスの数値を 400 から 500 に変更する場合、関連する全ての数値を 400 から 500 に変更しなければならず、手間がかかります。このようにプログラム中に埋め込まれた数値のことを、「マジックナンバー」と言います。プログラムが正常に動作するための、いわば魔法の数値です。一見しただけでは理屈がよく分からない魔法の数値や数字がプログラム中に散らべられていると、プログラムの修正など保守作業の手間が著しく増えてしまいます。そのため、ソフトウェア開発の現場においては、これらマジックナンバーは無くすべきであることが知られています。
本章の最後として、上で示したプログラムよりマジックナンバーをできるだけ除去してみたいと思います。まずは、議論に上っている 400 という数値ですが、これは既に説明したとおりキャンパスの横幅を意味しています。新たに変数を定義しても良いのですが、Processing ではキャンバスの横幅は width という暗黙的に定義される変数に格納されています。なので、それを用いることにします。同様に、300 という数値はキャンバスの高さを表すものであり、それは height という暗黙的に定義されている変数に格納されていますので、それを用いるように修正します。
まだ 40 と 30 という数値が残っていますが、これは格子の横幅と高さですので、
int gridWidth =40; int gridHeight=30;とし、これらの変数を使うようにします。
本章のまとめとして、全ての修正を反映したプログラムを以下にに示します。出力される画像はこの章の初めの方に示したものとと全く同じで変化していません。ちなみに、プログラムの動作を変えずにプログラムの中身、つまりプログラムリストの書き方をより良く更新していく行為はリファクタリングと呼ばれています。
// draw grid size(400,300); background(255,255,255); strokeWeight(3); int gridWidth =40; int gridHeight=30; // draw vertical lines int px=0; do { line(px,0, px,height); px=px+gridWidth; } while(px<=width); // draw horizontal lines int py=0; do { line(0,py, width,py); py=py+gridHeight; } while(py<=height);
- 投稿日:2021-03-22T17:38:23+09:00
JavaでAtCoder青色になりました
遭難者です。ARC115でに青色になったので記事を書くことにしました。
緑色に4ヶ月程度いたのに水色には1ヶ月もいないのは春休み中毎日競プロと数学しかしていなかったからだと思います。自己紹介
現在高専1年の、情報系の学生です。
得意分野は数学とグラフ問題、苦手分野は文字列の操作とデータ構造、DP等です。
学校の授業でJavaを扱っていたということもあり、競プロをJavaで始めました。精進量
画像の通りです。
アドバイス的ななにか
青色になりたい!という人(特にJavaを使っている人向け)へのアドバイスをまとめます。
クラスを使えるようにする
ありがたいことにJavaには有志の方々が作ってくださったACL for Javaがあり、これを貼り付けるだけで様々な機能が使えます。特に高速入出力は全ての問題に使え、実行時間とデータ使用量を大幅に減らせます。私はまだACL for Javaの全てが使えるわけではありませんが、頻繁にコンテストに出るクラスは使えるようにしました。また、クラスを使えるようにするだけでなく、内部でどのようなことをしているのかも理解できるようにしました。内部を理解できるとそのクラスをより快適なものにすることができます。他人のコードを読む
Javaには様々な機能があります(何もJavaに限った話ではありませんが)。例えば任意の $0<i\le n$ を満たす $i$ について $a[i]=i$ を満たす 配列 $a$ を作りたい時、for文を使ってfor(int i = 0; i < n; i++){ a[i] = i; }という風に書けます。しかし、setAllを使うと
Arrays.setAll(a, i -> i);という風にスッキリと書けます。他人のコードが各行で何をしているのかを覗いてみると意外な成果があるかもしれません。
早解きを心がける
コンテストには崖が存在することがよくあります。例えば茶-緑-黄-橙-橙-銅のようなセットの場合、水色程度の実力では2問の勝負になります(3問目以降も解ける人は居ますが)。そういった時に大事になってくるのは如何に早く解けるか、如何にペナを出さないでACできるかになります。早解きは自分より1色以上下の問題を素早く解ける程度にするべきだと思います(1色以上下でも解けないものは解けないので、そういった問題は解説ACをすることをお勧めします)。Streakを無理に繋げない
気が向いていないときに無理に競プロをするのは苦痛になるのでやめたほうがいいです。気分ではない時は休み、気分が乗ったらたくさん問題を解き勉強する、といった感じで競プロを楽しむことが一番です。Javaのメリット
多倍長整数が使える
AtCoderでは多倍長整数が使えないと解けないような問題はまず出ません。しかし、ABC169-Bのように多倍長整数が使えると実装が楽になる・正攻法が思いつかなくてもなんとかなるケースは少なくないです。速い
速いです。C++ほどではありませんが、それでも速いです。TL2sのところを1990msで通したりするとJavaで良かったなと感じます。Javaのデメリット
JOI本選で使えない
JOI本選ではCとC++しか使えません。公平性を期すためらしいですが、なんとかなって欲しいものです。パソコン甲子園では使えるのがせめてもの救いです。今後の目標
水色になりたい!という思いと共に競技プログラミングを始めたのですが、目標を大幅に超えてしまいました。
青色になったので4つほど目標を立てます。黄色になる
ユーザー解説を書いてみたいからです。実装例にJavaを入れたいです。JOI・PCK本選に出場する
JOIは精進あるのみです。幸い同校の友人が競プロに興味を持ってくれたので、その友人と共にPCK本選を目指していきます。苦手分野を克服する
苦手分野は下手すると茶diffも解けないかもしれません。そういった問題を重点的に解いていくことにします。開発に参加する
情報系の学生とはいえ競プロのスキル単体ではあまり役にたたないと思っているので、競プロのスキルを生かせる方向に持っていきたいです。以上の4つの目標ですが、今年中には達成したいです。
最後まで読んでいただき、ありがとうございました。
- 投稿日:2021-03-22T16:29:22+09:00
Java: EasyBuggy - 30分でわかる!XMLエンティティ拡張(XEE)の脆弱性
概要
EasyBuggyまたはEasyBuggy Bootを利用して、XMLエンティティ拡張(XEE)の脆弱性をシミュレートし、その修正を試みます。
環境構築はこちらの記事から。
注意事項
本記事では脆弱性をついた攻撃手法について解説しています。
しかし、実際に第三者が運用しているWebサービスなどに対して、勝手に脆弱性の検査をおこなうのは違法行為となる可能性が非常に高いです。無害な検査用文字列を送信しているだけのつもりであっても、意図しない破壊を招いたり、監視システムによって攻撃と勘違いされる可能性があります。
絶対にやめましょう。
参考事例: 脆弱性検査について
脆弱性を確認する
EasyBuggyを起動し、
脆弱性 > XEE (XMLエンティティ拡張)を選択します。XMLファイルをアップロードし、ユーザーを一括登録する機能のようです。
しかし、アップロードされたXMLファイルを処理する実装に脆弱性があります。今回は、サーバーのCPUリソースに高負荷を与える攻撃をしてみます。
WikipediaではBillion laughs attackとも紹介されています。
プロファイリングツールの準備
サーバのCPUリソースをプロファイリングするために、下記のいずれかのツールを起動します。
Windowsであれば、
Javaのインストールフォルダ\bin内に、下記のようなファイルがあると思います。
もし存在しなければ、ダウンロードします。
プロファイリングツール名 ファイル名 特徴 ダウンロード先(例) VisualVM jvisualvm.exe または visualvm.exe 軽い、低機能 https://visualvm.github.io/download.html JDK Mission Control(JMC) jmc.exe やや重い、高機能 https://adoptopenjdk.net/jmc VisualVMの場合
- Applicationsタブにて、アプリケーションを選択します
- EasyBuggyの場合は
Tomcatを選択- EasyBuggy Bootの場合は
org.t246osslab.easybuggy4sb.Easybuggy4sbApplicationを選択- Monitorタブを選択し、CPU欄を表示します
JDK Mission Control(JMC)の場合
- JVMブラウザタブにて、アプリケーションを選択します
- EasyBuggyの場合は
org.apache.catalina.startup.Bootstrap startを選択- EasyBuggy Bootの場合は
org.t246osslab.easybuggy4sb.Easybuggy4sbApplicationを選択MBeanサーバーを選択します- 選択したサーバーのダッシュボードやプロセッサ欄を表示します
ファイルのアップロード
EasyBuggyのページ内に記載されているXMLはtypoがあるのと、ちょっと長いです。
このため、Wikipediaページに記載されていたXMLファイルを作成し、アップロードしてみます。XEEの脆弱性を攻撃するXMLファイル<?xml version="1.0"?> <!DOCTYPE lolz [ <!ENTITY lol "lol"> <!ELEMENT lolz (#PCDATA)> <!ENTITY lol1 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;"> <!ENTITY lol2 "&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;"> <!ENTITY lol3 "&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;"> <!ENTITY lol4 "&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;"> <!ENTITY lol5 "&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;"> <!ENTITY lol6 "&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;"> <!ENTITY lol7 "&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;"> <!ENTITY lol8 "&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;"> <!ENTITY lol9 "&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;"> ]> <lolz>&lol9;</lolz>アップロード操作をおこなうと、サーバの処理に時間がかかり、なかなかレスポンスが返りません。
プロファイリングツールを確認すると、アップロード前は落ち着いていたCPU使用率が上昇し続けていることがわかります。このような状態が続くと、サーバーを利用しているほかの利用者の処理が遅くなる可能性があります。
今回は、Eclipseからサーバーを終了させましょう。脆弱性を修正する
EasyBuggyでもEasyBuggy Bootでも同じ実装内容になっています。
EasyBuggyの場合は、XEEandXXEServletクラスに問題となる実装があります。
EasyBuggyの場合は、XEEandXXEControllerクラスに問題となる実装があります。脆弱性の原因
利用しているXMLパーサーのデフォルト設定で、XEEが有効になっているためです。
このため、XEEを悪用して攻撃ができてしまいます。具体的な修正方法
機能の目的から考えて、XEEを含むXMLファイルを処理する必要はありません。
このため、XEEを明示的に無効にすることで修正をおこないます。JavaではいくつかのXMLを処理するパーサーがありますが、それぞれによって修正方法が少しずつ異なります。基本的に下記のページを参考にして修正すると良いでしょう。
ページ内に
For a syntax highlighted example code snippet using SAXParserFactory, look here.とあります。EasyBuggyではSAXParserFactoryを利用しているので、指示に従います。
どうやら、下記の1行を追加してやれば良いようですね。
DOCTYPE宣言を許可しないspf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);DOCTYPE宣言内にXMLエンティティ拡張を入れることで攻撃しているわけですから、DOCTYPE宣言自体を禁止してしまえばよいようです。
修正を反映し、再度、動作確認します。
正しく修正できていれば、すぐに応答が返ってきて、処理に失敗した旨のメッセージが表示されます。
もしかしたら「もう少し詳しいエラーメッセージを用意するべきか?」とも感じるかもしれません。
しかし、相手は悪意をもって攻撃をしてきているわけです。
攻撃者に対して、わざわざ親切なエラーメッセージを表示してやる必要はないでしょう。
- 投稿日:2021-03-22T13:57:43+09:00
セッション管理
セッション
サーバ内に情報を保存し、異なるページ間で同一のクライアントを認識する仕組みのことです。
セッション管理
1.クライアントが初めてServletプログラムにアクセスした際、セッションIDが生成されます。
2.生成されたセッションIDはクライアントに返され、保管されます。
3.再びクライアントがServletプログラムにアクセスする際にセッションIDがクライアントから送信されます。
4.Servletプログラムでは送信されたセッションIDにより、同一クライアントかどうかを判断します。
これらの一連の処理はcookieにより管理されます。cookieが使えない環境では、URLリンクでセッションIDを管理することもできます。
戻り型 メソッド 説明 HttpSession getSession( ) HttpServletRequestインタフェースで定義されているメソッドです。セッションオブジェクト(セッションID)を返します。セッションオブジェクトが生成されていない場合は、セッションオブジェクトを生成して返します。 Object getAttribute(String) HttpSessionインタフェースで定義されているメソッドです。引数に指定されたデータ名に該当するセッションスコープのデータ値を返します。該当のデータ名がない場合にはnullを返します。 void void setAttribute(String, Object) HttpSessionインタフェースで定義されているメソッドです。第一引数にデータ名、第二引数にデータ値を指定し、セッションスコープを持つデータ値を登録します。すでにデータ名が存在する場合は、新しく指定されたデータ値が上書きされます。 void invalidate( ) 生成されているセッションオブジェクト(セッションID)を破棄します。 boolean isNew( ) クライアントがセッション管理されていないときにtrueを返します(初めてアクセスしたとき、クラインアントがcookieを使えないときなど)。クライアントがセッション管理されていないときにtrueを返します(初めてアクセスしたとき、クラインアントがcookieを使えないときなど)。 セッションのタイムアウト
セッションIDは一定時間経過するか、クライアントのブラウザを閉じるか、もしくはinvalidateメソッドを呼び出すと削除されます。どの程度経過するとセッションIDが削除されるかはweb.xmlのタグで指定できます。Tomcatのデフォルトの設定では30分になっています。また、HttpSessionインタフェースには、セッションごとに経過時間を設定できるsetMaxInactiveIntervalメソッド、getMaxInactiveIntervalメソッドが用意されています。
要素 内容 session-timeout セッションのタイムアウト時間を分単位で指定します。0もしくは0以下の値を指定した場合は、セッションがタイムアウトしないことを表します。 設定例
セッションのタイムアウトを180分に設定した例です。タグはタグ、タグの後に記述する必要があります。
<session-config> <session-timeout>180</session-timeout> </session-config>Javaにおける使用例
import java.io.*; import javax.servlet.*; import javax.servlet.http.*; public class SessionServlet extends HttpServlet { public void doGet(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { String id = "Java"; /** * セッションオブジェクトが生成されていない場合、getSessionメソッドでセッションオブジェクトを生成します。 * セッションオブジェクトがすでに生成されている場合は、既存のセッションオブジェクトを返します。 **/ HttpSession hs1 = req.getSession(); PrintWriter out = res.getWriter(); out.println("<HTML>"); out.println("<BODY>"); /** * クライアントがセッション管理されているかどうか(はじめてアクセスしたかどうか)で表示メッセージを変えます。 * セッション管理されていない場合、データidをsetAttributeメソッドで保存し、"id + Nice to meet you."を表示します。 * セッション管理されている場合は、データidの値をgetAttributeメソッドで取得し、"Hello! + id"を表示します。 **/ if (hs1.isNew()) { hs1.setAttribute("id", id); out.println(hs1.getAttribute("id") + " Nice to meet you."); } else { out.println("Hello! " + hs1.getAttribute("id")); } out.println("</BODY>"); out.println("</HTML>"); } }cookieが使用できない場合のセッション管理
クライアントがcookieを使用できない場合、URLリンクにセッションIDを付与することによりセッション管理ができるようになります。ServletプログラムはURLリンクから、同一クライアントかどうかを判別します。セッションIDが付与されたURLリンクを作成するためには以下のメソッドを使用します。
戻り型 メソッド 説明 String encodeURL(String) HttpServletResponseインタフェースで定義されているメソッドです。引数に指定されたURLにセッションIDを付与したURLを生成します。cookieを使用できる環境などセッションIDを付与する必要がない場合は、URLは変化しません。 String encodeRedirectURL(String) HttpServletResponseインタフェースで定義されているメソッドです。sendRedirectメソッドで使用するためにセッションIDを付与したURLを生成します。cookieを使用できる環境などセッションIDを付与する必要がない場合は、URLは変化しません。 Javaにおける使用例
String url = res.encodeURL("./SessionServlet"); out.println("<A HREF=\"" + url + "\">SessionServlet</A>");
- 投稿日:2021-03-22T12:44:21+09:00
[Android] Jetpack Navigation 画面遷移とFragment間のデータ受け渡し
Android Studio 4.1.3(windows版) での流れとなります
以下の記事のプロジェクトベースでの説明となります
Android DeveloperのJetpack Navigationの画面遷移とFragment間のデータ受け渡しの説明は以下です
SafeArgs を追加する
※ 現時点ではandroidx.navigation:navigation-safe-args-gradle-pluginをimplementationでは動作しないようです
※ androidx.navigationのバージョンに合わせて設定してくださいbuild.gradle(Project)buildscript { dependencies { def nav_version = "2.3.4" classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version" } }build.gradle(Module)apply plugin: "androidx.navigation.safeargs"画面遷移の準備
+ボタンを押してここではNextFragmentを追加します
MainFragmentのActionsの+ボタンを押して画面遷移を追加します
DestinationでnextFragmentを指定してAddボタンで追加します
Actionが追加されました
MainFragmentに画面遷移実行用のボタンとボタンイベントを追加します
ボタンイベントの設定方法を詳しく知りたい場合は以下を参考にしてください
ここではMainFragmentにonClickButtonメソッドを追加しJetpack Databindingでボタンイベントの紐づけをします
画面遷移処理の実装
MainFragment.javapublic void onClickButton() { final View view = binding.getRoot(); // MainFragmentDirectionsはsafeargsを有効にすることで自動生成されます NavDirections directions = MainFragmentDirections.actionMainFragmentToNextFragment(); Navigation.findNavController(view).navigate(directions); }Fragment間でデータを受け渡す場合
NextFragmentのArgumentsの+ボタンを押します
ここではName:Text Type:String とします
Action(やじるし)のArgument Default Values のdefault value に値を設定します
ここではnoneを設定 ※default valueを設定しないとActionクラスが自動生成されません
NextFragmentに渡したい値(ここでは"xxx")をsetします
MainFragment.javapublic void onClickButton() { final View view = binding.getRoot(); // MainFragmentDirections.ActionMainFragmentToNextFragment/setText()は自動生成されます MainFragmentDirections.ActionMainFragmentToNextFragment action = MainFragmentDirections.actionMainFragmentToNextFragment(); action.setText("xxx"); Navigation.findNavController(view).navigate(action); }値を受け取ります
※ ここでは説明を簡略化してますが、実際はJetpack View/DataBindingを使用して値を設定してくださいNextFragment.java@Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { final View view = inflater.inflate(R.layout.next_fragment, container, false); // NextFragmentArgsは自動生成されます String text = NextFragmentArgs.fromBundle(getArguments()).getText(); TextView tv = view.findViewById(R.id.textView); tv.setText(text); return view; }
- 投稿日:2021-03-22T12:44:21+09:00
[Android/Java] Jetpack Navigation 画面遷移とFragment間のデータ受け渡し
Android Studio 4.1.3(windows版) での流れとなります
以下の記事のプロジェクトベースでの説明となります
Android DeveloperのJetpack Navigationの画面遷移とFragment間のデータ受け渡しの説明は以下です
SafeArgs を追加する
※ 現時点ではandroidx.navigation:navigation-safe-args-gradle-pluginをimplementationでは動作しないようです
※ androidx.navigationのバージョンに合わせて設定してくださいbuild.gradle(Project)buildscript { dependencies { def nav_version = "2.3.4" classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version" } }build.gradle(Module)apply plugin: "androidx.navigation.safeargs"画面遷移の準備
+ボタンを押してここではNextFragmentを追加します
MainFragmentのActionsの+ボタンを押して画面遷移を追加します
DestinationでnextFragmentを指定してAddボタンで追加します
Actionが追加されました
MainFragmentに画面遷移実行用のボタンとボタンイベントを追加します
ボタンイベントの設定方法を詳しく知りたい場合は以下を参考にしてください
ここではMainFragmentにonClickButtonメソッドを追加しJetpack Databindingでボタンイベントの紐づけをします
画面遷移処理の実装
MainFragment.javapublic void onClickButton() { final View view = binding.getRoot(); // MainFragmentDirectionsはsafeargsを有効にすることで自動生成されます NavDirections directions = MainFragmentDirections.actionMainFragmentToNextFragment(); Navigation.findNavController(view).navigate(directions); }Fragment間でデータを受け渡す場合
NextFragmentのArgumentsの+ボタンを押します
ここではName:Text Type:String とします
Action(やじるし)のArgument Default Values のdefault value に値を設定します
ここではnoneを設定 ※default valueを設定しないとActionクラスが自動生成されません
NextFragmentに渡したい値(ここでは"xxx")をsetします
MainFragment.javapublic void onClickButton() { final View view = binding.getRoot(); // MainFragmentDirections.ActionMainFragmentToNextFragment/setText()は自動生成されます MainFragmentDirections.ActionMainFragmentToNextFragment action = MainFragmentDirections.actionMainFragmentToNextFragment(); action.setText("xxx"); Navigation.findNavController(view).navigate(action); }値を受け取ります
※ ここでは説明を簡略化してますが、実際はJetpack View/DataBindingを使用して値を設定してくださいNextFragment.java@Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { final View view = inflater.inflate(R.layout.next_fragment, container, false); // NextFragmentArgsは自動生成されます String text = NextFragmentArgs.fromBundle(getArguments()).getText(); TextView tv = view.findViewById(R.id.textView); tv.setText(text); return view; }
- 投稿日:2021-03-22T12:09:08+09:00
文字列を指定文字数で分解し、逆順で出力する
目次
1.はじめに
2.リトルエンディアン
3.import
4.文字列を指定文字数で分解
5.逆順に並び替える
6.splitを使う場合1.はじめに
…主に自分が見直すための記録です…
16進数の文字列データをリトルエンディアンにしたかった。
調べると split で正規表現を用いて分解する例が多かったが、今回は
0E0A0D6F0014A55F29
みたいな区切られていない文字列を
[0E,0A,0D,6F,00,14,A5,5F,29] (配列)
↓
295FA514006F0D0A0E (リトルエンディアン文字列)
にしたかった。2.リトルエンディアン
![]()
3.import
以下をimportします。
import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern;4.文字列を指定文字数で分解
import java.util.ArrayList; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; public class SortingStrings { public static void main(String[] args) { String hexString = "0E0A0D6F0014A55F29"; System.out.println("変換前:" + hexString); String result = sortingHex(hexString); System.out.println("変換後:" + result); } /** * 16進数文字列を逆順にする関数 * * @param str * @return result */ public static String sortingHex(String str) { //Matcherで分割する文字数を指定する 今回は2文字で分割 final Matcher m = Pattern.compile(".{1,2}").matcher(str); //Listを作成 List<String> strList = new ArrayList<>(); while (m.find()) { //作成したListに分割しながら挿入する strList.add(str.substring(m.start(), m.end())); } String result = strList.toString(); //結果を返す return result; } }これで
変換前:0E0A0D6F0014A55F29 変換後:[0E, 0A, 0D, 6F, 00, 14, A5, 5F, 29]と出力されます。
5.逆順に並び替える
Listを逆順に並び替えます。
sortingHexの中身のみ下記に示します。public static String sortingHex(String str) { // Matcherで分割する文字数を指定する 今回は2文字で分割 final Matcher m = Pattern.compile(".{1,2}").matcher(str); // Listを作成 List<String> strList = new ArrayList<>(); while (m.find()) { // 作成したListに分割しながら挿入する strList.add(str.substring(m.start(), m.end())); } //ListのCollectionsで逆順にする Collections.reverse(strList); String result = strList.toString(); return result; }これで
変換前:0E0A0D6F0014A55F29 変換後:[29, 5F, A5, 14, 00, 6F, 0D, 0A, 0E]と出力されます。リトルエンディアンになった!
配列形式ではなく文字列で出力したい場合は、String result = strList.toString(); の部分を ↓ String result = String.join("", strList);とすればいい。
出力結果変換前:0E0A0D6F0014A55F29 変換後:295FA514006F0D0A0E6.splitを使う場合
変更前が0E:0A:0D:6F:00:14:A5:5F:29 みたいな形なら split が使える。
@saka1029 さん からのアドバイスを基に追記。
split を使えばもっと簡単でした! 正規表現の勉強をスルーしたつけがwpublic static String sortingHex(String str) { List<String> strList = Arrays.asList(str.split("(?<=\\G..)")); Collections.reverse(strList); String result = String.join("", strList); return result; }JavaScriptだと slice とか、もっと書き方がわかるけどJavaは完全シロートでちょっと詰まってしまいました…。
- 投稿日:2021-03-22T08:48:06+09:00
EC2にJavaWebアプリ環境を構築 #4
EC2にJavaWebアプリ環境を構築 #1
EC2にJavaWebアプリ環境を構築 #2
EC2にJavaWebアプリ環境を構築 #3
EC2にJavaWebアプリ環境を構築 #4目次
1.はじめに
2.プログラム作成
3.データベース作成
4.動作確認
5.アップロードはじめに
- 前回
- インストール作業終了
プログラム作成
簡単なプログラムを動してみる
eclipse起動
新規→動的Webプロジェクト
今回はプロジェクト名をsampleにするindex.jsp
RegisterServlet.javaのみ追加
Tomcat10は9に比べてファイルが扱いやすい印象
mysql-connecterはver.8.0.22を使用
記事を書いている時点での最新は8.0.23だが、
なぜか動かなかった…MySQLに直接あーだこーだすればできるみたいな記事も見つけたが、
今回の方針とは異なるため8.0.22で行うダウンロードリンク
Operating System:Platform Independent
ZIPをダウンロード、解凍、mysql~22.jarを
libに配置改行して保存するだけで勝手に全て書き換えてくれる
プログラム全体はこんな感じ
入力した文字をデータベースに保存するだけの超簡単なプログラム
余談ですが、フォントは白源1という日本人の方が作成したものを使用してますプログラミングで有名なフォントは大体全部使いましたが、これが一番いい
Shellでも使えるので、本当におすすめ
index.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <title>サンプル</title> </head> <body> <%= request.getAttribute("word") %> <form action="register" method="post"> <dl> <dt>ワード</dt> <dd><input type="text" name="word"></dd> </dl> <button type="submit">登録</button> </form> </body> </html>RegisterServlet.java
Postの処理のみ記載request.setCharacterEncoding("UTF-8"); // パラメーター取得 String word = request.getParameter("word"); // JDBCドライバ try { Class.forName("com.mysql.cj.jdbc.Driver"); } catch (ClassNotFoundException e) { e.printStackTrace(); } try { Connection con = DriverManager.getConnection( "jdbc:mysql://localhost/sample?characterEncoding=utf8&serverTimezone=JST", "root", ""); String sql = "INSERT INTO words (word)" + " VALUES (?)"; PreparedStatement smt = con.prepareStatement(sql); smt.setString(1, word); smt.executeUpdate(); smt.close(); con.close(); } catch(Exception e) { e.printStackTrace(); } request.setAttribute("word", word); request.getRequestDispatcher("index.jsp").forward(request, response);データベース作成
CREATE DATABASE IF NOT EXISTS `sample` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE `sample`; DROP TABLE IF EXISTS `words`; CREATE TABLE IF NOT EXISTS `words` ( `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT, `created_at` timestamp NOT NULL DEFAULT current_timestamp(), `updated_at` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(), `word` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;必要最低限!!
動作確認
localhost:8080/sample
から追加できた動作確認完了
コンソールでエラーもなしアップロード
EC2にアップロードしていく
sample.warにエクスポート
sample.warは、「C:\users\ユーザー名」に置いた想定で説明
sample.pem、sample.war、パブリックIPv4
は各自のものにscp -i sample.pem sample.war ec2-user@パブリックIPv4:/home/ec2-user100%になれば完了
ssh -i sample.pem ec2-user@パブリックIPv4で接続
下記コマンド実行
sudo cp sample.war /opt/tomcat/webappsこれでアプリケーションの配置が終了
EC2でMySQLに接続し、
先程のデータベースを作成すればOK作成したアプリにアクセスする
http://パブリックIPv4 DNS/sample最後駆け気味だが、動作確認も問題なく無事終了
- 投稿日:2021-03-22T04:38:09+09:00
Java学習 基礎編 mainメソッドとは?
Java学習の備忘録になります。
内容は初心者なので基礎的な部分となります。
Javaに興味がある方やJava学習初心者の参考になれば幸いです。
mainメソッドとは?
Javaのmainメソッドとは、Javaで処理を実行するときの入り口となるもの。
いわゆるエントリーポイント。
(コンピュータプログラムを実行する際に、一番最初に実行することになっている箇所のこと)
mainメソッドに書かれた処理からJavaの実行がはじまる。
さらに詳しく見ていくと
一般的なプログラミング言語では、プログラムの実行が、どこから・どのようにはじまるかは、大きく以下の2種類にわかれる。
1.プログラミング言語の仕様で決められた、関数・プロシージャ・メソッドからはじまる
(C言語、C#、Go、kotlinなど)2.プログラムの先頭行から最後の行まで順番に実行されていく
(Perl、Ruby、Python、PHPなど)Javaは、上記1ののプログラミング言語になる。
この仕様で決められた関数・プロシージャ・メソッドをエントリーポイント(entrypoint)とも呼び、取り得る名前や引数、修飾子などが決められている。そして、Javaでは、mainメソッドがエントリーポイントとなる。
mainメソッドの形式
Javaのmainメソッドは、以下の条件を満たしているメソッドになる。
・アクセス修飾子は publicである
・static メソッドである
・メソッドの戻り値は voidである
・メソッド名は mainである ( すべて小文字 )
・メソッドの引数は Stringの配列 (あるいはStringの可変長引数 ( 引数の数が決まっていない引数のこと ) ) のみである
※引数の変数名はなんでも良いまた、mainメソッドとは別のことだが、Javaでのメソッドは、必ず何かのクラスなどに属していなければいけない。
mainメソッドを持つクラスは、普通は以下のようになる。
※変数名argsは、引数を意味する英単語argumentsを省略したものMain.javaclass Main{ // mainメソッドの基本形 public static void main(String[] args) { // 処理 } }Javaのプログラムを実行するときは、実行するクラスを指定する。
その際、指定されたクラスにmainメソッドがあるかをJavaが調べて、あればそこから実行をはじめる。
指定されたクラスにmainメソッドがなければエラーになる。
おわりに
もっと奥が深い部分なんですが、今回は基礎編なので基本的な部分のみ記載してます。
なお、ご指摘などございましたら、遠慮なくお願いいたします!!
- 投稿日:2021-03-22T03:07:52+09:00
SpringBoot2+JUnit5+DBUnitでREST API(POST)のブラックボックステスト
はじめに
REST API(POST)のテストをController,Service,Daoのレイヤ単位ではなく機能単位でJUnitでテストするために必要な準備、テストケースの作成方法を記載します。
※API単位の試験ならPostman叩いて実行すればいいじゃん!ってなるのですが、
以下の理由によりJUnitで行っています。
- レスポンスのJSON、実行後のDBの状態、機能内で呼び出している外部APIへのリクエストの内容をアサーションしたい
- テストデータを自動登録して実行後は元の状態に戻したい
- 変更が発生しても容易に回帰テストできるようにしたい
- カバレッジを取得できるようにしたい
クラス構成
今回のテスト対象機能のクラス構成は下図の通りです。
JUnitからRestControllerにリクエストを送信し、実行結果を確認します。
外部API(緑色の部分)はモック化します。
DBはローカルに構築したDBを使用しました。DBUnitでテストデータの登録/検証を行います。環境
- SpringBoot 2.3
- MyBatis 3
- JUnit 5
- Mockit 3.7.7
- DBUnit 2.7.0
- spring-ws-test(SOAPサーバをモック化するために使用)
- Oracle 19c
必要なライブラリの追加
spring-boot-starter-testにDBUnit、spring-ws-testが含まれていないため依存関係に追加します。
build.gradledependencies { testImplementation group: 'org.dbunit', name: 'dbunit', version: '2.7.0' testImplementation group: 'org.springframework.ws', name: 'spring-ws-test', version: '2.0.2.RELEASE' }pom.xml<dependencies> <dependency> <groupId>org.dbunit</groupId> <artifactId>dbunit</artifactId> <version>2.7.0</version> <scope>test</scope> </dependency> <!-- Soapサーバモック化するために使用 --> <dependency> <groupId>org.springframework.ws</groupId> <artifactId>spring-ws-test</artifactId> <scope>test</scope> </dependency> </dependencies>DBUnitを使うための設定クラス作成
以下の記事を参考にさせていただき、「テスト用DB設定クラス」「Excel用ローダークラス」を作成しました。
Spring Boot + JUnit5 + DBUnitでExcelをテストデータとしたUTを行う方法DBConfig.java@Configuration public class DBConfig { /* * テスト用のDBコネクションファクトリのセットアップ */ @Bean public DatabaseDataSourceConnectionFactoryBean dbUnitDatabaseConnection(final DataSource dataSource) { final DatabaseDataSourceConnectionFactoryBean connectionFactory = new DatabaseDataSourceConnectionFactoryBean(); connectionFactory.setDataSource(dataSource); connectionFactory.setSchema("TESTSCHEMA"); //ORACLEの場合スキーマ設定必須 return connectionFactory; } }XlsDataSetLoader.javapublic class XlsDataSetLoader extends AbstractDataSetLoader { /* * DBUnitのデータのセットアップをExcelから行うための設定 */ @Override protected IDataSet createDataSet(Resource resource) throws Exception { try (InputStream in = resource.getInputStream()) { return new XlsDataSet(in); } } }テストデータの準備
JSONファイル
テストケース毎に以下のJSONファイルを作成します。
① テスト対象のAPIに送信するリクエスト
② テスト対象のAPIのレスポンス期待値
③ 外部API(REST)に送信するリクエスト期待値 ※SOAPの場合はXMLファイル
④ 外部API(REST)のモックが返却する固定のレスポンス ※SOAPの場合はXMLファイルテストケースにJSON文字列を記述するとコード量が膨大になるので、外出ししてテストケース内で読み込む方針としました。
それぞれ以下の要領で作成します。case001req.json{ "user" : { "id" : "001", "name" : "さんぷる 太郎", "gender" : "1", "age" : 25 } }DBUnitのExcelデータシートの作成
テストデータ用のExcelデータシートを「セットアップデータ.xlsx」、
実行後の期待値のExcelデータシートを「期待値テーブルデータ.xlsx」というファイル名で今回は作成します。
データシートの作成方法はDBUnitの基本的な使い方の説明となるので省略します。テストケースの作成
以下のテストケースではMockRestServiceServerで外部API(REST)をモック化し、MockMvcでテスト対象のAPIのURIにリクエストを送信し実行結果を検証しています。
SampleControllerTest.java@SpringBootTest // ブラックボックステストのため通常のアプリケーション起動時と同様にコンポーネントスキャンし、コンフィグレーションを自動検出する @DbUnitConfiguration(dataSetLoader = XlsDataSetLoader.class) // 作成した「Excel用ローダークラス」を指定 @TestExecutionListeners({ DependencyInjectionTestExecutionListener.class, // SpringのDIをテストで利用するための指定 TransactionDbUnitTestExecutionListener.class, // DBUnitを利用するための指定 MockitoTestExecutionListener.class // @MockBeanでモック化したクラスをDIさせるための指定 }) public class SampleControllerTest{ private MockMvc mockMvc; // テスト対象のControllerにリクエストを送信するためのクラス private MockRestServiceServer restServer; // 外部APIをモック化するためのクラス private String testCaseName; // テスト対象のメソッド名を格納する変数 @Value("${externalApi.uri}") private String externalApiUri; // 外部APIのURI @BeforeEach public void setUp(TestInfo testInfo, @Autowired WebApplicationContext webApplicationContext, @Autowired RestTemplate restTemplate) { testCaseName = testInfo.getTestMethod().get().getName(); // テストケース名の取得 restServer = MockRestServiceServer.bindTo(restTemplate).build(); // MockRestServiceServerのセットアップ mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build(); // MockMVCのセットアップ。デプロイ時とほぼ同じ状態でテストするためwebAppContextSetupを指定 } @Test @DisplayName("正常ケース") // JUnitの実行モニタに表示するテストケース名 @DatabaseSetup("セットアップデータ.xlsx") // @DatabaseSetupにより、テスト開始時にDBUnitによりテストデータが登録される @ExpectedDatabase(value="期待値テーブルデータ.xlsx", assertionMode=DatabaseAssertionMode.NON_STRICT_UNORDERED) // @ExpectedDatabaseにより、実行後のテーブルがデータシートに記載したデータ通りであるかDBUnitにより検証される。 @Transactional // @Transactionalを指定することで、テスト終了時に@DatabaseSetupでセットアップしたテストデータおよびテスト対象の機能で登録したデータがロールバックされる void case0010() { // リクエスト/レスポンスのJSONロード String reqJson = StreamUtils.copyToString(new ClassPathResource(testCaseName + "req.json").getInputStream(), StandardCharsets.UTF_8); // テスト対象のAPIに送信するリクエスト String resJson = StreamUtils.copyToString(new ClassPathResource(testCaseName + "res.json").getInputStream(), StandardCharsets.UTF_8); // テスト対象のAPIのレスポンス期待値 String externalApiReq = StreamUtils.copyToString(new ClassPathResource(testCaseName + "externalApiReq.json").getInputStream(), StandardCharsets.UTF_8); // 外部API(REST)に送信するリクエスト期待値 String externalApiRes = StreamUtils.copyToString(new ClassPathResource(testCaseName + "externalApiRes.json").getInputStream(), StandardCharsets.UTF_8); // 外部API(REST)のモックが返却する固定のレスポンス // 外部APIのモック定義 restServer.expect(ExpectedCount.times(1), requestTo(externalApiUri)) // RestTemplateが実行されたタイミングで行われる検証。指定されたURIに一致すること .andExpect(MockRestRequestMatchers.method(HttpMethod.POST)) // 同様にHTTPメソッドの検証。POSTメソッドであること .andExpect(MockRestRequestMatchers.content().json(externalApiReq)) // 同様にリクエストボディの検証。期待値のJSONと一致すること .andRespond(withSuccess(externalApiRes, MediaType.APPLICATION_JSON)); // 検証がすべてOKの場合、引数に指定したレスポンスが返却される // 実行 MvcResult result= this.mockMvc.perform(MockMvcRequestBuilders .post("/example") // postで送信するためpostメソッドを使用し、URIを指定 .header("x-api-key", "QAWSEDRFTGYHUJIKOLP") // リクエストヘッダにセットする場合。キー、値の形式で設定 .content(reqJson) // リクエストボディの設定 .contentType(MediaType.APPLICATION_JSON)) // リクエストのContent-Typeの指定 .andExpect(status().is(HttpStatus.OK.value())) // ここから実行結果の検証。HTTPステータスが200であること .andExpect(content().contentType(MediaType.APPLICATION_JSON)) // Content-Typeが"Application/json"であること .andReturn(); // 後続でレスポンスボディの検証をするために、andReturnメソッドでレスポンスを取得する // レスポンスのJSON検証 String actual = result.getResponse().getContentAsString(StandardCharsets.UTF_8); // レスポンスボディをUTF-8で取得 JSONAssert.assertEquals(resJson, actual, JSONCompareMode.STRICT); // 期待値のJSONと一致しているか比較 // 定義したモックがすべて実行されたか検証 restServer.verify(); } }ソースコメントで記載しきれなかった部分について補足します。
@DatabaseSetup
テストケースのメソッドに対して付与していますが、クラスに付与することも可能です。
全部のテストケースで同じテストデータを使う場合はクラスに付与したほうが実行速度も上がります。
@Transactional
こちらもクラスに付与することも可能です。
各テストケースでテストデータが干渉しない場合はクラスに付与してもよいかと思います。
@ExpectedDatabase(... assertionMode=DatabaseAssertionMode.NON_STRICT_UNORDERED)
assertionModeは比較の厳密度の指定です。以下3種類あります。
DatabaseAssertionMode 説明 DEFAULT (未指定時) 全テーブルの全カラムの値が一致するか比較する NON_STRICT データシートに無いテーブル、カラムは検証対象外とする NON_STRICT_UNORDERED データシートに無いテーブル、カラムは検証対象外とする
行の順番も無視する
JSONAssert.assertEquals(resJson, actual, JSONCompareMode.STRICT);
JSONAssertはJSON文字列の同等性を検証するので、空白やタブ、改行、項目の定義順は無視してくれます。
JSONCompareModeで配列の比較の厳密度を指定できます。
JSONCompareMode 説明 STRICT 配列の順番を厳密に比較する LENIENT 配列の順番を無視する 他にNON_EXTENSIBLE、STRICT_ORDERがありますが、
特定のノードのみ無視する、正規表現とマッチしていればOKとするなどJSONComparatorを拡張する場合に使用します。
SOAPをモック化する場合のテストケース
SOAPの場合も要領は同じですが、SOAPクライアントの実装方法とあわせて別途記事にします。
さいごに
レイヤ単位のテストケースのサンプルは結構ネットに載っているんですが、
機能単位のサンプルはあまり載っていなかったので記録として記事を書きました。ただJUnitでテストすること自体コストが掛かるので、
複雑な機能や変更が頻繁に入りそうな機能だけJUnitでしっかりテストするなど、ポイントで採用すれば効果はあるかと思いました。
- 投稿日:2021-03-22T03:07:52+09:00
SpringBoot2 + JUnit5 + DBUnitでREST API(POST)のブラックボックステスト
はじめに
REST API(POST)のテストをController,Service,Daoのレイヤ単位ではなく機能単位でJUnitでテストするために必要な準備、テストケースの作成方法を記載します。
※API単位の試験ならPostman叩いて実行すればいいじゃん!ってなるのですが、
以下の理由によりJUnitで行っています。
- レスポンスのJSON、実行後のDBの状態、機能内で呼び出している外部APIへのリクエストの内容をアサーションしたい
- テストデータを自動登録して実行後は元の状態に戻したい
- 変更が発生しても容易に回帰テストできるようにしたい
- カバレッジを取得できるようにしたい
クラス構成
今回のテスト対象機能のクラス構成は下図の通りです。
JUnitからRestControllerにリクエストを送信し、実行結果を確認します。
外部API(緑色の部分)はモック化します。
DBはローカルに構築したDBを使用しました。DBUnitでテストデータの登録/検証を行います。環境
- SpringBoot 2.3
- MyBatis 3
- JUnit 5
- Mockit 3.7.7
- DBUnit 2.7.0
- spring-ws-test(SOAPサーバをモック化するために使用)
- Oracle 19c
必要なライブラリの追加
spring-boot-starter-testにDBUnit、spring-ws-testが含まれていないため依存関係に追加します。
build.gradledependencies { testImplementation group: 'org.dbunit', name: 'dbunit', version: '2.7.0' testImplementation group: 'org.springframework.ws', name: 'spring-ws-test', version: '2.0.2.RELEASE' }pom.xml<dependencies> <dependency> <groupId>org.dbunit</groupId> <artifactId>dbunit</artifactId> <version>2.7.0</version> <scope>test</scope> </dependency> <!-- Soapサーバモック化するために使用 --> <dependency> <groupId>org.springframework.ws</groupId> <artifactId>spring-ws-test</artifactId> <scope>test</scope> </dependency> </dependencies>DBUnitを使うための設定クラス作成
以下の記事を参考にさせていただき、「テスト用DB設定クラス」「Excel用ローダークラス」を作成しました。
Spring Boot + JUnit5 + DBUnitでExcelをテストデータとしたUTを行う方法DBConfig.java@Configuration public class DBConfig { /* * テスト用のDBコネクションファクトリのセットアップ */ @Bean public DatabaseDataSourceConnectionFactoryBean dbUnitDatabaseConnection(final DataSource dataSource) { final DatabaseDataSourceConnectionFactoryBean connectionFactory = new DatabaseDataSourceConnectionFactoryBean(); connectionFactory.setDataSource(dataSource); connectionFactory.setSchema("TESTSCHEMA"); //ORACLEの場合スキーマ設定必須 return connectionFactory; } }XlsDataSetLoader.javapublic class XlsDataSetLoader extends AbstractDataSetLoader { /* * DBUnitのデータのセットアップをExcelから行うための設定 */ @Override protected IDataSet createDataSet(Resource resource) throws Exception { try (InputStream in = resource.getInputStream()) { return new XlsDataSet(in); } } }テストデータの準備
JSONファイル
テストケース毎に以下のJSONファイルを作成します。
① テスト対象のAPIに送信するリクエスト
② テスト対象のAPIのレスポンス期待値
③ 外部API(REST)に送信するリクエスト期待値 ※SOAPの場合はXMLファイル
④ 外部API(REST)のモックが返却する固定のレスポンス ※SOAPの場合はXMLファイルテストケースにJSON文字列を記述するとコード量が膨大になるので、外出ししてテストケース内で読み込む方針としました。
それぞれ以下の要領で作成します。case001req.json{ "user" : { "id" : "001", "name" : "さんぷる 太郎", "gender" : "1", "age" : 25 } }DBUnitのExcelデータシートの作成
テストデータ用のExcelデータシートを「セットアップデータ.xlsx」、
実行後の期待値のExcelデータシートを「期待値テーブルデータ.xlsx」というファイル名で今回は作成します。
データシートの作成方法はDBUnitの基本的な使い方の説明となるので省略します。テストケースの作成
以下のテストケースではMockRestServiceServerで外部API(REST)をモック化し、MockMvcでテスト対象のAPIのURIにリクエストを送信し実行結果を検証しています。
SampleControllerTest.java@SpringBootTest // ブラックボックステストのため通常のアプリケーション起動時と同様にコンポーネントスキャンし、コンフィグレーションを自動検出する @DbUnitConfiguration(dataSetLoader = XlsDataSetLoader.class) // 作成した「Excel用ローダークラス」を指定 @TestExecutionListeners({ DependencyInjectionTestExecutionListener.class, // SpringのDIをテストで利用するための指定 TransactionDbUnitTestExecutionListener.class, // DBUnitを利用するための指定 MockitoTestExecutionListener.class // @MockBeanでモック化したクラスをDIさせるための指定 }) public class SampleControllerTest{ private MockMvc mockMvc; // テスト対象のControllerにリクエストを送信するためのクラス private MockRestServiceServer restServer; // 外部APIをモック化するためのクラス private String testCaseName; // テスト対象のメソッド名を格納する変数 @Value("${externalApi.uri}") private String externalApiUri; // 外部APIのURI @BeforeEach public void setUp(TestInfo testInfo, @Autowired WebApplicationContext webApplicationContext, @Autowired RestTemplate restTemplate) { testCaseName = testInfo.getTestMethod().get().getName(); // テストケース名の取得 restServer = MockRestServiceServer.bindTo(restTemplate).build(); // MockRestServiceServerのセットアップ mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build(); // MockMVCのセットアップ。デプロイ時とほぼ同じ状態でテストするためwebAppContextSetupを指定 } @Test @DisplayName("正常ケース") // JUnitの実行モニタに表示するテストケース名 @DatabaseSetup("セットアップデータ.xlsx") // @DatabaseSetupにより、テスト開始時にDBUnitによりテストデータが登録される @ExpectedDatabase(value="期待値テーブルデータ.xlsx", assertionMode=DatabaseAssertionMode.NON_STRICT_UNORDERED) // @ExpectedDatabaseにより、実行後のテーブルがデータシートに記載したデータ通りであるかDBUnitにより検証される。 @Transactional // @Transactionalを指定することで、テスト終了時に@DatabaseSetupでセットアップしたテストデータおよびテスト対象の機能で登録したデータがロールバックされる void case0010() { // リクエスト/レスポンスのJSONロード String reqJson = StreamUtils.copyToString(new ClassPathResource(testCaseName + "req.json").getInputStream(), StandardCharsets.UTF_8); // テスト対象のAPIに送信するリクエスト String resJson = StreamUtils.copyToString(new ClassPathResource(testCaseName + "res.json").getInputStream(), StandardCharsets.UTF_8); // テスト対象のAPIのレスポンス期待値 String externalApiReq = StreamUtils.copyToString(new ClassPathResource(testCaseName + "externalApiReq.json").getInputStream(), StandardCharsets.UTF_8); // 外部API(REST)に送信するリクエスト期待値 String externalApiRes = StreamUtils.copyToString(new ClassPathResource(testCaseName + "externalApiRes.json").getInputStream(), StandardCharsets.UTF_8); // 外部API(REST)のモックが返却する固定のレスポンス // 外部APIのモック定義 restServer.expect(ExpectedCount.times(1), requestTo(externalApiUri)) // RestTemplateが実行されたタイミングで行われる検証。指定されたURIに一致すること .andExpect(MockRestRequestMatchers.method(HttpMethod.POST)) // 同様にHTTPメソッドの検証。POSTメソッドであること .andExpect(MockRestRequestMatchers.content().json(externalApiReq)) // 同様にリクエストボディの検証。期待値のJSONと一致すること .andRespond(withSuccess(externalApiRes, MediaType.APPLICATION_JSON)); // 検証がすべてOKの場合、引数に指定したレスポンスが返却される // 実行 MvcResult result= this.mockMvc.perform(MockMvcRequestBuilders .post("/example") // postで送信するためpostメソッドを使用し、URIを指定 .header("x-api-key", "QAWSEDRFTGYHUJIKOLP") // リクエストヘッダにセットする場合。キー、値の形式で設定 .content(reqJson) // リクエストボディの設定 .contentType(MediaType.APPLICATION_JSON)) // リクエストのContent-Typeの指定 .andExpect(status().is(HttpStatus.OK.value())) // ここから実行結果の検証。HTTPステータスが200であること .andExpect(content().contentType(MediaType.APPLICATION_JSON)) // Content-Typeが"Application/json"であること .andReturn(); // 後続でレスポンスボディの検証をするために、andReturnメソッドでレスポンスを取得する // レスポンスのJSON検証 String actual = result.getResponse().getContentAsString(StandardCharsets.UTF_8); // レスポンスボディをUTF-8で取得 JSONAssert.assertEquals(resJson, actual, JSONCompareMode.STRICT); // 期待値のJSONと一致しているか比較 // 定義したモックがすべて実行されたか検証 restServer.verify(); } }ソースコメントで記載しきれなかった部分について補足します。
@DatabaseSetup
テストケースのメソッドに対して付与していますが、クラスに付与することも可能です。
全部のテストケースで同じテストデータを使う場合はクラスに付与したほうが実行速度も上がります。
@Transactional
こちらもクラスに付与することも可能です。
各テストケースでテストデータが干渉しない場合はクラスに付与してもよいかと思います。
@ExpectedDatabase(... assertionMode=DatabaseAssertionMode.NON_STRICT_UNORDERED)
assertionModeは比較の厳密度の指定です。以下3種類あります。
DatabaseAssertionMode 説明 DEFAULT (未指定時) 全テーブルの全カラムの値が一致するか比較する NON_STRICT データシートに無いテーブル、カラムは検証対象外とする NON_STRICT_UNORDERED データシートに無いテーブル、カラムは検証対象外とする
行の順番も無視する
JSONAssert.assertEquals(resJson, actual, JSONCompareMode.STRICT);
JSONAssertはJSON文字列の同等性を検証するので、空白やタブ、改行、項目の定義順は無視してくれます。
JSONCompareModeで配列の比較の厳密度を指定できます。
JSONCompareMode 説明 STRICT 配列の順番を厳密に比較する LENIENT 配列の順番を無視する 他にNON_EXTENSIBLE、STRICT_ORDERがありますが、
特定のノードのみ無視する、正規表現とマッチしていればOKとするなどJSONComparatorを拡張する場合に使用します。
SOAPをモック化する場合のテストケース
SOAPの場合も要領は同じですが、SOAPクライアントの実装方法とあわせて別途記事にします。
さいごに
レイヤ単位のテストケースのサンプルは結構ネットに載っているんですが、
機能単位のサンプルはあまり載っていなかったので記録として記事を書きました。ただJUnitでテストすること自体コストが掛かるので、
複雑な機能や変更が頻繁に入りそうな機能だけJUnitでしっかりテストするなど、ポイントで採用すれば効果はあるかと思いました。
- 投稿日:2021-03-22T01:32:58+09:00
勉強メモ17_Javaの勉強(復習用、つどつど更新)
はじめてのJava0 はじめに
Java Goldの資格持ってるが、ここ1年、Java触っていないため、
復習の意味をこめて、ドットインストールを使って、念のためJavaを学びました。
学んだ内容のURL
https://dotinstall.com/lessons/basic_java_v3はじめてのJava1 Javaを学んでみよう
0、説明
・Java にはいくつかの実装があるのですが、今回はオープンソース版の OpenJDK を使っていきます。
・Java は移植性に優れた言語で広い業界で使われているのですが、今回初めての Java ということで簡単な数当てゲームを作りながら雰囲気を掴んでいきましょう。1、実行ソース
MyApp.javaimport java.util.Scanner; import java.util.Random; //Java を実行するには class とその中にメインメソッドが必要とまずは覚えておきましょう。 class MyApp { public static void main(String[] args) { //1 から 10 までの整数値を取得できる Integer answer = new Random().nextInt(10) + 1; Integer count = 0; while (true) { System.out.print("Your guess? "); //キーボードからの入力を受け取るための Scanner をつくるには new Scanner(System.in).next(); としてあげます。 Integer guess = new Scanner(System.in).nextInt(); // count = count + 1; // count += 1; count++; if (answer == guess) { System.out.println("Bingo! It took " + count + " guesses!"); break; } else if (answer > guess ) { System.out.println("The answer is higher!"); } else { System.out.println("The answer is lower!"); } } } }2、実行結果
~ $ javac MyApp.java && java MyApp Your guess? 5 ←ここはキーボードから入力 The answer is lower! Your guess? 4 The answer is lower! Your guess? 3 The answer is lower! Your guess? 2 Bingo! It took 4 guesses! ~ $ここから先の内容は単なるメモ↓↓
★1 結果がどうなるか答えなさい
(https://tech.pjin.jp/blog/2019/01/26/java_8_gold_exercise_01/)sample問題1import java.util.stream.Stream; import java.util.ArrayList; public class Sample{ public static void main(String[] args){ ArrayList<Integer> list = new ArrayList<>(); list.add(1); list.add(2); list.stream() .forEach(i -> i+1); System.out.println(list); } } //答えは、コンパイルエラー //以下見たいに治したらうまく import java.util.ArrayList; public class Sample{ public static void main(String[] args){ ArrayList<Integer> list = new ArrayList<>(); list.add(1); list.add(2); list.stream() //.forEach(a -> a+1); .forEach(i -> System.out.println(i)); System.out.println(list); } } //実行結果 1 2 [1, 2]★2 結果がどうなるか答えなさい
(https://tech.pjin.jp/blog/2019/01/26/java_8_gold_exercise_03/)import java.util.ArrayList; public class Sample{ public static void main(String[] args){ ArrayList<Integer> list = new ArrayList<>(); list.add(1); list.add(2); list.stream() .forEach(System.out::print); } } //実行結果 12





























