- 投稿日:2021-03-06T22:56:38+09:00
【Java】AtCoderのABC-194に参加しました(レート:197→230)。
こんばんは。
2021/3/6に、AtCoderのABC-194に参加しました。
レートは以下の通りとなっています。
時間ぎりぎりでC問題まで何とか解けました。
ただ、時間がかかったのと、C問題はトライ&エラーを繰り返したため低くなっています。
開き直ってもしょうがないですが、トライ&エラーは今の自分には必要なので、まあ良いかなと。順位は4443位でした。レートは197→230に更新!よし200達成だ、おめ(^▽^)/!
A問題
問題文がややこしくて時間を取ってしまいました。
無脂乳固形分(A)と乳脂肪分(B)を入力。
乳固形分(A+B)、乳脂肪分(B)の割合に応じて、1~4の値を出力。import java.util.Scanner; public class Main{ public static void main(String args[]){ Scanner sc = new Scanner(System.in); int a = sc.nextInt(); int b = sc.nextInt(); int nk = a+b; int ns = b; int flg = 0; //System.out.println(nk); if(nk>=15&&ns>=8){ flg = 1; }else{ if(nk>=10&&ns>=3){ flg = 2; }else{ if(nk>=3){ flg=3; }else{ flg=4; } } } System.out.println(flg); } }B問題
N人の従業員、それぞれAの仕事時間とBの仕事時間がある。
両方片付けるのに最短の時間はいくらか。
(同じ従業員の場合はAi+Bi、違う従業員の場合は時間のかかる方)import java.util.Scanner; public class Main{ public static void main(String args[]){ Scanner sc = new Scanner(System.in); int n = sc.nextInt(); int a[] = new int[n]; int b[] = new int[n]; for(int i=0;i<n;i++){ a[i] = sc.nextInt(); b[i] = sc.nextInt(); } int min = 100001; int lg = 0; for(int i=0;i<n;i++){ for(int j=0;j<n;j++){ lg = 0; if(a[i]>b[j]){ lg = a[i]; }else{ lg = b[j]; } if(i==j){ if((a[i]+b[j])<min){ min = a[i]+b[j]; }else{ } }else{ if(lg<min){ min = lg; }else{ } } } } System.out.println(min); } }C問題
とのことです。
処理時間がかかるので、入力時に計算可能な値を計算しながら、ループをせずに合計値を求めるロジックに変えていきました。試行錯誤して時間ギリギリで何とかなりました。import java.util.Scanner; public class Main{ public static void main(String args[]){ Scanner sc = new Scanner(System.in); int n = sc.nextInt(); long a[] = new long[n]; long ttl =0; long ttl2 =0; long r2 = 0; long r2j = 0; for(int i=0;i<n;i++){ a[i] = sc.nextLong(); if(i>0){ r2 = r2 + (-1)*a[i-1]; r2j = r2j + a[i-1]*a[i-1]; ttl2 = ttl2 + i*(a[i])*(a[i]) + (2)*a[i]*(r2) + r2j; }else{ } //System.out.println(" i*(a[i])*(a[i]) " + i*(a[i])*(a[i]) + " r2 " + r2 + " r2j " + r2j + " ttl2 " + ttl2); // for(int j=0;j<i;j++){ // ttl = ttl + (a[i]-a[j])*(a[i]-a[j]); // } } //int total = 0; //for(int i=0;i<n;i++){ // for(int j=i+1;j<n;j++){ // total = total + (a[j]-a[i])*(a[j]-a[i]); // } //} System.out.println(ttl2); //System.out.println(ttl); //System.out.println(total); } }感想
今年以内に茶色を取得する目標ですが、意外と近づいてきた気がします。
プログラミングって書いているときは苦しく、終わると楽しいんですよね。
開発現場の気持ちを保っていく意味でも、競技プログラミングは良いかなと思いました。これからもコツコツ取り組んで、茶色を目指したいと思います!
同じく灰色レベルで頑張っている方、お互い頑張りましょう!
- 投稿日:2021-03-06T19:23:02+09:00
Android Studio ストップウォッチアプリ制作
今回はストップウォッチアプリを制作しました。
YouTubeに作っている動画をアップしています。
https://www.youtube.com/watch?v=1c0LvmCW5EU&list=PLhg2PHSq8bjjjBpOPll39ZAUgtbnlDOvUstrings.xml<resources> <string name="app_name">StopWatchApp</string> <string name="btn_start">START</string> <string name="btn_stop">STOP</string> <string name="btn_reset">RESET</string> <string name="btn_lap">LAP</string> </resources>list_items.xml<?xml version="1.0" encoding="utf-8"?> <TextView xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="center" android:textSize="24sp"></TextView>activity_main.xml<?xml version="1.0" encoding="utf-8"?> <LinearLayout 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" android:weightSum="5" tools:context=".MainActivity"> <TextView android:id="@+id/text_timer" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" android:text="00:00.000" android:gravity="center" android:textSize="36sp"></TextView> <LinearLayout android:layout_width="match_parent" android:layout_height="0dp" android:gravity="center" android:layout_weight="1"> <Button android:id="@+id/btn_start" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="2dp" android:onClick="startTimer" android:text="@string/btn_start"></Button> <Button android:id="@+id/btn_stop" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="2dp" android:onClick="stopTimer" android:text="@string/btn_stop"></Button> <Button android:id="@+id/btn_reset" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="2dp" android:onClick="resetTimer" android:text="@string/btn_reset"></Button> </LinearLayout> <ListView android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="2.5" android:id="@+id/list"></ListView> <LinearLayout android:layout_width="match_parent" android:layout_height="0dp" android:gravity="center" android:layout_weight="0.5"> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/btn_lap" android:id="@+id/btn_lap" android:onClick="tapLap"></Button> </LinearLayout> </LinearLayout>MainActivity.xmlpackage com.example.stopwatchapp; import androidx.appcompat.app.AppCompatActivity; import android.os.Bundle; import android.os.Handler; import android.view.View; import android.widget.ArrayAdapter; import android.widget.Button; import android.widget.ListView; import android.widget.TextView; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Locale; public class MainActivity extends AppCompatActivity { Handler handler = new Handler(); Runnable runnable; Button btn_start, btn_stop, btn_reset; long startTime; TextView text_timer; long t, elapsedTime; ArrayList<String> lapTime; ListView listView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); btn_start = (Button) findViewById(R.id.btn_start); btn_stop = (Button) findViewById(R.id.btn_stop); btn_reset = (Button) findViewById(R.id.btn_reset); text_timer = (TextView) findViewById(R.id.text_timer); listView = findViewById(R.id.list); lapTime = new ArrayList<String>(); btnState(true, false, false); } public void btnState(boolean start, boolean stop, boolean reset) { btn_start.setEnabled(start); btn_stop.setEnabled(stop); btn_reset.setEnabled(reset); } public void startTimer(View view) { startTime = System.currentTimeMillis(); runnable = new Runnable() { @Override public void run() { t = System.currentTimeMillis() - startTime + elapsedTime; SimpleDateFormat sdf = new SimpleDateFormat("mm:ss.SSS", Locale.US); text_timer.setText(sdf.format(t)); handler.removeCallbacks(runnable); handler.postDelayed(runnable, 10); } }; handler.postDelayed(runnable, 10); btnState(false, true, false); } public void stopTimer(View view) { elapsedTime += System.currentTimeMillis() - startTime; handler.removeCallbacks(runnable); btnState(true, false, true); } public void resetTimer(View view) { elapsedTime = 0; t = 0; text_timer.setText("00:00.000"); btnState(true, false, false); lapTime.clear(); listView.setAdapter(new ArrayAdapter<String>(MainActivity.this,R.layout.list_items,lapTime)); } public void tapLap(View view){ lapTime.add(text_timer.getText().toString()); ArrayAdapter<String> adapter = new ArrayAdapter<String>(MainActivity.this,R.layout.list_items,lapTime); listView.setAdapter(adapter); } }YouTubeに作っている動画をアップしています。
https://www.youtube.com/watch?v=1c0LvmCW5EU&list=PLhg2PHSq8bjjjBpOPll39ZAUgtbnlDOvU
以上
- 投稿日:2021-03-06T19:02:05+09:00
(リスペクト作品) カフェでハッキングしてる風(ちょっとやっちゃう)Java(クソ)コード ver.2.0.0
リスペクト: @3S_Laboo様. 「カフェでプログラミングしてる風(でも何もやってない)Java(クソ)コード」
「ハッキング風」は何番煎じかわからないネタですが
リスペクト記事が好きなのでさらなるリアリティを付け加えたいと思い、改造しちゃいました。
スパイ映画などの「暗号化されているわ。少し時間はかかっちゃうけど、楽勝よ。」みたいなシーンが好きなので、テーマは暗号です。
機能1 〜怪しげな「ハッキングコード」〜
以下「ターゲット」にアクセスし、"接続成功"、"ダウンロード中"、"暗号解読中"を示すログを出します。
- amazon
- fujiwaratatsuya
- apple
(俗にいうGAFAですね。F社さんすみません。やりたいことがあったのでfujiwaratatsuyaに変更させていただきました)
なにやら怪しげな「ハッキングコード」(意味不明)を使っている感、極秘情報を取得しちゃってる感のリアリティーを出しました。
各ログのステータス表示のプログラムにつきましてはJava(クソ)コードの方を関数化させていただきました。感謝いたします。改めて記事の方を共有させていただきます。
「カフェでプログラミングしてる風(でも何もやってない)Java(クソ)コード」
/** * ステータスを更新してる風コード */ public class DungStatusChange { /** * * @param symbol ステータスの進捗を示すシンボル * @param symbolCount シンボルの数 * @param lineCount ステータスの行数 */ public static void run(String symbol, int symbolCount, int lineCount) { // カウントアップする変数 int y = 0; int n = 0; while(n<lineCount) { if (symbolCount != y) { // 上限までシンボルを足していく System.out.print(symbol); } else { // 50文字になったら「 done!! + 改行」を出力し // 何かが終わった感を演出 System.out.println(" done!!"); // カウントアップをリセット y = -1; n++; } // カウントアップしていく y++; // 高速で出力すると素敵感が出ないので // 出力待ち時間を作成する int sleepTime = 10; // ランダム値にマッチすると待ち時間をさらに長くする。 // 何か大きな処理をしているように見えること請け合い int osooso = (int)(Math.random() * 10); if (4 < osooso && osooso <= 6) { sleepTime = 100; } try { // 待つ Thread.sleep(sleepTime); } catch (InterruptedException e) { e.printStackTrace(); } } } }以下がメイン関数です。
for(;;)
による無限ループがオタクっぽさを出すためのポイントです。
Target
とHacker
は独自定義のものですので後述します。public class SuperHacker { public static void main(String[] args) { System.out.println("I AM A SUPER HACKER."); for(;;) { for (Target target: Target.values()) { Hacker.hackServer(target); } } } }続いて以下が
Target
と称された怪しげな「ハッキングコード」の列挙型です。実際は後で紹介する変換処理後の文字列です。それぞれ元は
"google.com"
,"amazom.com"
,"fujiwaratatsuya"
,"apple.com"
です。public enum Target { GOOGLE(".\u000E\u0002&?\u0010^\u0006\u001D%"), AMAZON("(\f\f;<\u001B^\u0006\u001D%"), FUJITATSU("/\u0014\u0007($\u0014\u0002\u0004\u0006)\u0015\u0010\u001E\u001C\u0013"), APPLE("(\u0011\u001D-6[\u0013\n\u001F"); private String hackingCode; private Target(String hackingCode) { this.hackingCode = hackingCode; } public String getHackingCode() { return this.hackingCode; } }
fujiwaratatsuya
の「ハッキングコード」が一番長いのが地味に好きです。そして以下が上記の「ハッキングコード」を生み出すためのクラスです。この記事でリアリティを出すための申し訳程度の暗号技術要素です。
/** * XOR演算による基本的な暗号 */ public class BasicEncrypt { /** * 共通鍵(普通は非公開。定数にしてはいけない。) */ private static final String key = ""; /** * 文字列のXOR演算 * @param s1 文字列1 * @param s2 文字列2 * @return s1 xor s2 */ public static String xor(String s1, String s2) { // バイト列 final byte[] keyBytes = s1.getBytes(); final byte[] sBytes = s2.getBytes(); StringBuilder sb = new StringBuilder(); final int n = Math.min(keyBytes.length, sBytes.length); for (int i=0; i < n; i++) { // 各バイト値をXOR演算し文字として保持 sb.append((char) (keyBytes[i] ^ sBytes[i])); } return sb.toString(); } /** * 暗号化 * @param s 平文 * @return sをkeyで暗号化した文字列(暗号文) */ public static String encrypt(String s) { return xor(key, s); } /** * 復号化 共通鍵暗号方式では暗号化と同じ。 * @param s 暗号文 * @return sをkeyで復号化した文字列(平文) */ public static String decrypt(String s) { return xor(key, s); } }このクラスの主となる処理は、二つの文字列に対し1バイト(≒ 1文字)ずつ XOR演算(排他的論理和) をおこなうメソッド
xor
です。XOR演算子(^
)はビット演算子の一つですが、以下のように興味深い性質を持ちます。共通鍵暗号方式に応用される演算の一つです。A ^ Key == B B ^ Key == A A ^ B == Key※共通鍵は普通公開しませんのでこの記事では鍵を空文字にしてみました。ただしこの記事の例の場合、とても簡単に鍵が判明しますので興味がありましたら解読に挑戦してみてください(突然のエンタメ)。ワンタイムパッド方式など共通鍵を使い捨てすることによって安全性を確保する方式もあります。
余談ですが暗号がテーマということで、せっかくですのでワンタイムパッド方式に関連する興味深い世界的な事件でも紹介しておきます
- ベノナ: https://ja.wikipedia.org/wiki/%E3%83%99%E3%83%8E%E3%83%8A
- ローゼンバーグ事件: https://ja.wikipedia.org/wiki/%E3%83%AD%E3%83%BC%E3%82%BC%E3%83%B3%E3%83%90%E3%83%BC%E3%82%B0%E4%BA%8B%E4%BB%B6
そして以下が大きなロジック部分です。リスペクト記事様の関数がうまい具合にハマっています。
import org.apache.commons.text.StringEscapeUtils; public class Hacker { private static final String HACKING = "Hacking Code: %s\n"; private static final String ACCESSING = "Accessing to: %s\n"; private static final String CONNECTING = "Connecting: %s\n"; private static final String DOWNLOAD = "Download:"; private static final String BREAKING = "Breaking code:"; public static void hackServer(Target target) { hackingLog(target); switch (target) { case GOOGLE: case AMAZON: case APPLE: breakingLog(5); break; case FUJITATSU: F.getInfo().ifPresent(info -> { breakingLog(info.size()); System.out.println(String.join("\n", info)); }); break; } } private static void hackingLog(Target target) { // 怪しげな「ハッキングコード」 final String hackingCode = target.getHackingCode(); System.out.printf(HACKING, StringEscapeUtils.escapeJava(hackingCode)); // 実際はただのドメイン名 final String justDomainName = BasicEncrypt.decrypt(hackingCode); System.out.printf(ACCESSING, justDomainName); DungStatusChange.run(".", 50, 3); // 接続中 System.out.printf(CONNECTING, justDomainName); } private static void breakingLog(int downloadCount) { // ダウンロード System.out.println(DOWNLOAD); DungStatusChange.run("#", 50, downloadCount); // 暗号解読 System.out.println(BREAKING); for (int i=0; i<downloadCount; i++) { // 「ターゲット」が怪しげな文字列なのでテキトーに並べる。 for (Target target : Target.values()) { final String accessKey = target.getHackingCode(); DungStatusChange.run(StringEscapeUtils.escapeJava(accessKey), 3, 1); } } System.out.println("BREAKING CODE COMPLETED."); } }そしてこうなります。
機能2 〜マジでデータ取っちゃう〜
上記ロジック、なにやら
FUJITATSU
のときだけなんかやってますね。case FUJITATSU: F.getInfo().ifPresent(info -> { breakingLog(info.size()); System.out.println(String.join("\n", info)); }); break;fujiwaratatsuyaへのアクセスについてはマジで公式ホームページを開いて、タレントについての最新情報を取得および出力する機能を用意しました。
以下が
jsoup
というスクレイピングに使えるJavaライブラリを使って最新のタレント情報を取得する処理です。import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; import org.jsoup.select.Elements; import java.io.IOException; import java.util.List; import java.util.Optional; import java.util.stream.Collectors; public class F { /** * 事務所のURL */ private static final String OFFICE_URL = "https://www.horipro.co.jp/"; /** * fujiwaratatsuya のタレント情報を取得する。 * @return タレント情報 */ public static Optional<List<String>> getInfo() { // 事務所のURL + fujirwaratatsuya final String url = OFFICE_URL + BasicEncrypt.decrypt(Target.FUJITATSU.getHackingCode()); // ホームページからタレント情報をとってきてリストに変換 Optional<List<String>> titles = getPage(url).map(document -> { final Elements info = document.select(".talent-info .inner-ttl"); return info.stream() .map(Element::text) .collect(Collectors.toList()); }); return titles; } /** * エラーハンドリングめんどいのでホームページの検索結果をOptionalで包む * @param url * @return ホームページ検索結果 */ private static Optional<Document> getPage(String url) { try { final Document document = Jsoup.connect(url).get(); return Optional.of(document); } catch (IOException e) { e.printStackTrace(); return Optional.empty(); } } }2021年3月5日(金)より、藤原竜也主演映画『太陽は動かない』が上映中です。ぜひ劇場に足をお運びください。
(暗号は全く関係ありません)
Javaでのスクレイピングについていろいろ調べていたら関係ないところでおもしろい記事に出会い、閃いてしまったので勢いで作ってしまいました。ちょうど藤原竜也さんの映画の上映も開始し、Fに組み込めることに気がついたときには運命を感じざるを得ませんでした。
どなたかバージョン 3.0 お待ちしております。
- 投稿日:2021-03-06T18:46:13+09:00
Ruby で解けず Java と Crystal で解く AtCoder ABC 189 C
はじめに
AtCoder Problems の Recommendation を利用して、過去の問題を解いています。
AtCoder さん、AtCoder Problems さん、ありがとうございます。今回のお題
AtCoder Beginner Contest C - Mandarin Orange
Difficulty: 565今回のテーマ、トランスパイラ
コンテスト中にRubyのコンテスタントが7名しか解けなかった問題です。
実行時間制限: 1.5 sec と厳しいのですが、Difficultyは茶色でした。Java
java.javaimport java.util.*; class Main { public static void main(String[] args) { Scanner sc = new Scanner(System.in); int n = Integer.parseInt(sc.next()); int a[] = new int[n]; for (int i = 0; i < n; i++) { a[i] = Integer.parseInt(sc.next()); } sc.close(); long ans = 0; long max = 0; for (int i = 0; i < n; i++) { long min = 1000000000; for (int j = i; j < n; j++) { if (min > a[j]) { min = a[j]; } if (max < min * (j - i + 1)) { max = min * (j - i + 1); } } if (ans < max) { ans = max; } } System.out.println(ans); } }コンテスト中に
Ruby
からJava
に切り替えて解答しました。
コードとして捻りも何もないのですが、短時間では脳が付いていかないです。Crystal
Crystal.crn = gets.to_s.to_i a = gets.to_s.split.map { |c| c.to_i } cnt = 0 max = 0 n.times do |i| min = 10**9 i.upto(n - 1) do |j| min = a[j] if min > a[j] max = min * (j - i + 1) if max < min * (j - i + 1) end cnt = max if cnt < max end puts cntこのコードは
Ruby
でもそのまま動作します。
Windows10
でCrystal
が動作すれば代替してもいいのですが。
Ruby Java Crystal コード長 (Byte) 265 831 265 実行時間 (ms) TLE 268 76 メモリ (KB) 14816 43072 3860 まとめ
- ABC 189 C を解いた
- Crystal に詳しくなった
- 投稿日:2021-03-06T16:23:45+09:00
2週間の予定
前回の記事から2週間が立ちましたのでまた新しい予定立てます.
前回の予定であった「Javaで作って学ぶ暗号技術」の読破
は完了し,マルウェアのリバースエンジニアリング教本を読見始めました.
私にとって新しい分野ですが思いの外ハマってしまいました.
ただアセンブリが読めない...勉強します..-C++でのAtCoder AtCoderBeginnerContestのA,B,C問題
これは前回の予定に引き続き取り組みます.-WannaCryのようなランサムウェアを解析し,どれほどの暗号が使われているか知る
- 投稿日:2021-03-06T15:13:18+09:00
Apache Tikaでファイルの内容からMIMEタイプを推測する
動作環境
Java openjdk 11.0.7
Spring boot 2.4.2
Gradle 6.5実装方法
依存関係
デフォルトではTikaを読み込めないので、依存関係に入れます。最新バージョンはこちらから確認してください。
build.gradledependencies{ implementation 'org.apache.tika:tika-parsers:1.25' }MIMEタイプを読み取るコード
今回はMultipartFileのインスタンスから中身をバイト配列として取り出し、そのバイト配列からMIMEタイプを判断しようと思います。以下のコードでは自作のgetMimeTypeメソッドとして定義していますが、メソッドとして定義する必要は(当然ですが)ありません。
private String getMimeType(MultipartFile multipartFile) throws IOException { Tika tika = new Tika(); // getBytes()はMultipartFileのインスタンスの中身をバイト配列として取り出すというメソッド // getBytes()はIOExceptionを起こす可能性があるため、getMimeTypeメソッドにthrows IOExceptionを付けている // 以下3行は「return tika.detect(multipartFile.getBytes());」と1行で書くこともできる byte[] multipartFileContent = multipartFile.getBytes(); // detect(byte[] prefix)メソッドで引数の中身からMIMEタイプを判断する String mimeType = tika.detect(multipartFileContent); return mimeType; }detectメソッドはバイト配列、Fileのインスタンス、文字列などいろいろなものを入れて判断することができます。その場に応じて一番入れやすいものを入れましょう。
- 投稿日:2021-03-06T15:13:18+09:00
【Java】Apache Tikaでファイルの内容からMIMEタイプを推測する
動作環境
Java openjdk 11.0.7
Spring boot 2.4.2
Gradle 6.5実装方法
依存関係
デフォルトではTikaを読み込めないので、依存関係に入れます。最新バージョンはこちらから確認してください。
build.gradledependencies{ implementation 'org.apache.tika:tika-parsers:1.25' }MIMEタイプを読み取るコード
今回はMultipartFileのインスタンスから中身をバイト配列として取り出し、そのバイト配列からMIMEタイプを判断しようと思います。以下のコードでは自作のgetMimeTypeメソッドとして定義していますが、メソッドとして定義する必要は(当然ですが)ありません。
private String getMimeType(MultipartFile multipartFile) throws IOException { Tika tika = new Tika(); // getBytes()はMultipartFileのインスタンスの中身をバイト配列として取り出すというメソッド // getBytes()はIOExceptionを起こす可能性があるため、getMimeTypeメソッドにthrows IOExceptionを付けている // 以下3行は「return tika.detect(multipartFile.getBytes());」と1行で書くこともできる byte[] multipartFileContent = multipartFile.getBytes(); // detect(byte[] prefix)メソッドで引数の中身からMIMEタイプを判断する String mimeType = tika.detect(multipartFileContent); return mimeType; }detectメソッドはバイト配列、Fileのインスタンス、文字列などいろいろなものを入れて判断することができます。その場に応じて一番入れやすいものを入れましょう。
- 投稿日:2021-03-06T08:57:59+09:00
【SpringBoot】リクエスト/セッション単位で一意な値を設定・参照する
やること
現在時刻をリクエスト内の各所で
LocalDateTime.now()
するような形で取得してしまうと、処理の最初と最後で値がズレてしまい、バグにつながる場合があります。
このような値は1度のみ生成して使い回すことが望ましいですが、素直に書いてしまうと各関数に値を渡す必要が出たり、値を使い回すことに設計が引っ張られてしまうことにもなり得ます。そこで、リクエスト単位で一意な値を設定・参照できるようにすることでこの問題を解決します。
やり方
この記事では
HandlerInterceptor
でRequestContextHolder
に値を設定し、それを参照する方法を紹介します。値を設定・参照する
リクエスト単位での値の保持は以下のように行うことができます。
例ではリクエスト単位の指定としていますが、setAttribute
の第三引数(scope
)にRequestAttributes.SCOPE_SESSION
を指定すればセッション単位で保持することもできます。import org.springframework.web.context.request.RequestAttributes import org.springframework.web.context.request.RequestContextHolder import java.time.LocalDateTime val now: LocalDateTime = LocalDateTime.now() val currentAttributes: RequestAttributes = RequestContextHolder.currentRequestAttributes() currentAttributes.setAttribute("now", now, RequestAttributes.SCOPE_REQUEST)参照は以下のように行います。
取れるのはObject
になるため、型にするためにはキャストが必要です。
また、リクエストに対する一連の処理内で当該のname
・scope
への登録がされていなければnull
が返ってきます。import org.springframework.web.context.request.RequestContextHolder import java.time.LocalDateTime val currentAttributes: RequestAttributes = RequestContextHolder.currentRequestAttributes() val now: LocalDateTime = currentAttributes.getAttribute("now", RequestAttributes.SCOPE_REQUEST) as LocalDateTimeリクエストごとに値を設定する
リクエストごとに値を設定する方法はいくつか考えられますが、ここでは
HandlerInterceptor
を作成し、コンフィグで全パスに適用する方法を紹介します。まず以下のように現在日時をセットするインターセプターを定義します。
サンプルではnow
という名前でLocalDateTime.now()
を登録しています。import org.springframework.stereotype.Component import org.springframework.web.context.request.RequestAttributes import org.springframework.web.context.request.RequestContextHolder import org.springframework.web.servlet.HandlerInterceptor import java.time.LocalDateTime import javax.servlet.http.HttpServletRequest import javax.servlet.http.HttpServletResponse @Component class SetCurrentTimeInterceptor : HandlerInterceptor { override fun preHandle(request: HttpServletRequest, response: HttpServletResponse, handler: Any): Boolean { // リクエスト毎に一意な日時をセット RequestContextHolder .currentRequestAttributes() .setAttribute("now", LocalDateTime.now(), RequestAttributes.SCOPE_REQUEST) return true } }次に、以下のようにインターセプターを登録するコンフィグを追加します。
import org.springframework.context.annotation.Configuration import org.springframework.web.servlet.config.annotation.InterceptorRegistry import org.springframework.web.servlet.config.annotation.WebMvcConfigurer @Configuration class SetCurrentTimeInterceptorConfig( private val setCurrentTimeInterceptor: SetCurrentTimeInterceptor ) : WebMvcConfigurer { override fun addInterceptors(registry: InterceptorRegistry) { registry.addInterceptor(setCurrentTimeInterceptor) .addPathPatterns("/**") // 適用対象となるパス、ここでは全指定 } }これによって、リクエストの全てで
name
とscope
から一意な日時の読み出しが行えるようになります。実際のアプリケーションでは、既存のインターセプター/コンフィグに設定を追加する形でも構わないでしょう。
注意点
今回紹介したやり方では、リクエストを受けたスレッドと異なるスレッド(
pallarelStream
の内部や、@Async
を付与したメソッド)から呼び出した場合、以下のようにIllegalStateException
が発生します。java.lang.IllegalStateException: No thread-bound request found: Are you referring to request attributes outside of an actual web request, or processing a request outside of the originally receiving thread? If you are actually operating within a web request and still receive this message, your code is probably running outside of DispatcherServlet: In this case, use RequestContextListener or RequestContextFilter to expose the current request.この状況に対しては、マルチスレッド処理のスコープは限られていて値を渡すのは容易だと考えられるため、以下のように何故エラーが出ているかだけ分かるようにラップだけしておくのが良いと思っています1。
import org.springframework.web.context.request.RequestAttributes import org.springframework.web.context.request.RequestContextHolder import java.time.LocalDateTime /** * @return リクエストごとに一意な現在日時 */ val currentTime: LocalDateTime get() = try { RequestContextHolder .currentRequestAttributes() .getAttribute("now", RequestAttributes.SCOPE_REQUEST) as LocalDateTime } catch (e: IllegalStateException) { throw IllegalStateException("リクエストを処理するスレッド以外から呼び出さないで下さい。", e) }終わりに
この記事では、
RequestContextHolder
経由で値を設定・参照することで、リクエスト単位で値を一意に扱う方法を紹介しました。
グローバル変数同様乱用は禁物ですが、層を横断して一意な値を扱いたいような場面では非常に強力なツールになりうると思います。参考にさせて頂いた内容
- RequestContextHolder (Spring Framework 5.3.4 API)
- RequestAttributes (Spring Framework 5.3.4 API)
- SpringのContextHolderいろいろ - SIerだけど技術やりたいブログ
エラーメッセージの通り、
RequestContextListener
またはRequestContextFilter
で対処できるかもしれませんが、本文で説明した通りの理由で検証する気が起きなかったため、この記事では紹介しません。 ↩