- 投稿日:2020-10-11T21:50:02+09:00
【Java】Calendarクラスで10日後の日付を取得
TenDaysCalendar.javaimport java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Date; public class TenDaysCalendar { public static void main(String[] args) { // 現在の日付情報を取得 Date now = new Date(); // Calendarにセット Calendar c = Calendar.getInstance(); // Calendarはnewを使わない c.setTime(now); // 引数にはDate型 // 日付を表示してみる System.out.println("***********現在日時***********"); System.out.println("Calendar.DATE: " + Calendar.DATE); // 5 System.out.println("c.get(Calendar.DATE): " + c.get(Calendar.DATE)); // 今日の日付 // 表示形式を指定して出力 SimpleDateFormat f = new SimpleDateFormat("yyyy年MM月dd日"); // 表示形式を指定 // 日付に10加算して10日後の日付を取得したい c.add(Calendar.DATE, 10); System.out.println(); System.out.println("***********10日後***********"); System.out.println("10日後の年月日: " + f.format(c.getTime())); System.out.println("Calendar.DATE: " + Calendar.DATE); // 5 System.out.println("c.get(Calendar.DATE): " + c.get(Calendar.DATE)); // さらに10日後の日付をセットして取得してみる System.out.println(); System.out.println("***********さらに10日後***********"); System.out.println("c.set(Calendar.DATE, (Calendar.DATE + 10))として10日後の日付にしようと試みる"); c.set(Calendar.DATE, (Calendar.DATE + 10)); System.out.println("10日後の年月日: " + f.format(c.getTime())); System.out.println("Calendar.DATEは定数で5なので、10加算すると15日になる"); System.out.println(); System.out.println("c.set(Calendar.DATE, (c.get(Calendar.DATE) + 10))として10日後の日付にしようと試みる"); c.set(Calendar.DATE, (c.get(5) + 10)); System.out.println("10日後の年月日: " + f.format(c.getTime())); System.out.println("上でDATEが15日にセットされたので10を加算して25日になる"); } }実行結果***********現在日時*********** Calendar.DATE: 5 c.get(Calendar.DATE): 11 ***********10日後*********** 10日後の年月日: 2020年10月21日 Calendar.DATE: 5 c.get(Calendar.DATE): 21 ***********さらに10日後*********** c.set(Calendar.DATE, (Calendar.DATE + 10))として10日後の日付にしようと試みる 10日後の年月日: 2020年10月15日 Calendar.DATEは定数で5なので、10加算すると15日になる c.set(Calendar.DATE, (c.get(Calendar.DATE) + 10))として10日後の日付にしようと試みる 10日後の年月日: 2020年10月13日 上でDATEが15日にセットされたので10を加算して25日になるCalendar.DATEはfinal定数だから変更できない
finalなのになんでsetとかできるの?
これが全然理解できませんでした。というか今もよく理解できていません。とりあえずいろいろ検証してみた結果、定数フィールド値というものがあって
Calendar.DATEは定数5
が格納されているよう。getをすると日付が取得できる
c.get(Calendar.DATE);とすると日付がちゃんと取得できる。どういうこと?って感じでした。
とりあえず、Calendar.DATEは定数5なので、
c.get(5);として日付が取得できました。
日付情報はDATEじゃなくて他のところに格納されている?!
ちょっと調べてみましたが私にはわかりませんでしたが、getの引数に5を渡すと、
DATEの定数値が来たぞ!日付を返さなきゃ!
ということで、別のところに入っている日付を取ってきてくれるというイメージで良いかと。基礎でやるような
int name = "tanaka"; getName(){ return name; }のイメージとは違うのかな。
またわかり次第記事を更新しますが、とりあえず上記のような理解ですっきりしましたし、扱いとして問題ありませんでした。
- 投稿日:2020-10-11T21:03:59+09:00
【Java】Dateクラスでミリ秒を使って10日後の日付情報を取得する
Dateクラスの扱いの練習
Dateについて勉強していましたが、
* エポックミリ秒についての理解
* 形式を指定して表示する方法この2点においてあんまりちゃんと理解できていない部分があった感じがするのでまとめます。
本当はCalendarクラスのフィールドがよくわかっていないことで沼ってたというのもあるし、Java8からは新しい日付APIがありもう少し直感に近い扱いができるらしいですが、それについてもしっかり原始的なDateクラスの理解を始めた方がいいと感じたので、自分なりにいろいろ検証しつつ描いてみました。
Calendarクラスを使ったバージョンも参考になれば
Dateクラスを使って10日後の日付情報を表示してみよう
TenDaysDate.javaimport java.text.SimpleDateFormat; import java.util.Date; public class TenDaysDate { public static void main(String[] args) { // Date型のlong値を変更して10日後の日付を取得 Date d = new Date(); // 現在の日時を取得 long ts = System.currentTimeMillis(); // 現在のエポックミリ秒を取得 // Date型のgetTimeメソッドの返り値とcurrentTimeMillisの値を比較 System.out.println("d.getTime:\t\t\t" + d.getTime()); // Date.getTimeの値 System.out.println("currentTimeMillis:\t" + ts); // currentTimeMillisの値 if(ts == d.getTime()) System.out.println("いっしょやん"); // long値は一致します // Date型の日付情報を形式を指定して表示 SimpleDateFormat f = new SimpleDateFormat("yyyy年MM月dd日"); // 表示形式を指定 System.out.println("今日の日付:\t" + f.format(d)); // SimpleDateFormat型にDate型の引数を渡す // Date型のsetTimeメソッドで10日分のミリ秒を加算して10日後の日付情報に変更する d.setTime(d.getTime() + (long)(24 * 60 * 60 * 1000) * 10); // ()が1日のミリ秒 System.out.println("10日後の日付:\t" + f.format(d)); // SimpleDateFormat型にDate型の引数に渡す } }出力結果d.getTime: 1602415769204 currentTimeMillis: 1602415769204 いっしょやん 今日の日付: 2020年10月11日 10日後の日付: 2020年10月21日getとset
d.getTime(); // エポックミリ秒が返ってくる d.setTime(エポックミリ秒); // エポックミリ秒を引数に渡して日付情報をセットするエポックミリ秒
1970年1月1日0時0分0秒UTCからの経過ミリ秒
Unix Timeともいう
1日分のミリ秒は24 * 60 * 60 * 1000
。これに10をかければ10日分のミリ秒になる。また、
getTime() + 24 * 60 * 60 * 1000 * 10
とすることで、現在時刻よりさらに10日分のミリ秒経過後の日付情報を取得できる。場合によっては
(long)24 * 60 * 60 * 1000
としてlong型にキャストする必要あり?うるい年などによって思った日付にならないこともある
10年後とかそういう中に閏年が出てくると、1年後のミリ秒として
24 * 60 * 60 * 1000 * 365 * 10
という風にしても、10年後の同じ月同じ日にならない。長いスパンで計算するときは年フィールドなどに加算して計算しないといけませんね。ややこしい。
- 投稿日:2020-10-11T17:25:51+09:00
指定した文字列が、対象の文字コードにおいてサポートされているかを調べたい
指定した文字列が、対象の文字コードにおいてサポートされているかどうかを調べたい場合、
CharsetEncoder.canEncode
を利用すると便利です。これは引数として与えられたメソッドが対象の文字コードでサポートされている場合true、それ以外はfalseを返すメソッドで、たとえば文字列s
がShift_JISにおいてサポートされているかを調べたい場合は次のように書けばよいです。String s = "..."; Charset.forName("Shift_JIS").newEncoder().canEncode(s);実際に動いているかを検証する例として、「髙」という漢字を利用します。いわゆる「はしごだか」ですね。この漢字はShift_JISではサポートされていないのですが、その拡張であるWindows-31Jではサポートされています。これを確かめてみましょう。
Charset.forName("Shift_JIS").newEncoder().canEncode("髙"); //=> false Charset.forName("Windows-31J").newEncoder().canEncode("髙"); //=> true想定通り動作していそうですね(´・ω・`)
環境情報:
C:\>javac -version javac 11.0.3 C:\>java -version openjdk version "11.0.3" 2019-04-16 OpenJDK Runtime Environment AdoptOpenJDK (build 11.0.3+7) OpenJDK 64-Bit Server VM AdoptOpenJDK (build 11.0.3+7, mixed mode)
- 投稿日:2020-10-11T17:09:07+09:00
Java・ツイッタークローン・タスク管理システム④タスク一覧表示をする
はじめに
Javaを使って初めてアプリケーションを作成する人にむけて記事を書いてみようと思います。
ポートフォリオや、会社の研修課題作成の参考にしていただければ幸いです。
今回は、タスクマネージャーを作成します。
これを応用することで、ツイッタークローンの作成にも活かすことができます。アプリケーションの機能ごとに記事を投稿していきます。
1.データベース作成
2.ログイン機能
3.タスクの登録機能
4.一覧表示
-ソート機能
-検索機能
5.編集機能
6.削除機能
7.ログイン機能
8.排他制御について*詳しい説明はコード内に書いてありますので、コピペする人は消して使ってください
実行環境
eclipse4.16.0
Tomcat9
Java11目的
1.viewの作成
2.DAOの作成(一覧表示・ソート)
3.サーブレットの作成(一覧表示・ソート)
4.DAOの作成(検索機能)
5.サーブレットの作成(検索機能)
6.次回予告viewの作成
検索フォームとタスク一覧テーブルを作っていく
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" import="java.util.List, model.entity.TaskBean, model.entity.UserBean, model.dao.UserDAO, model.dao.CategoryDAO, model.dao.StatusDAO"%> <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>タスク一覧</title> <link rel="stylesheet" href="${pageContext.request.contextPath}/task-list.css"> </head> <body> <% List<TaskBean> tasklist = (List<TaskBean>) request.getAttribute("tasklist"); UserDAO userdao = new UserDAO(); CategoryDAO categorydao = new CategoryDAO(); StatusDAO statusdao = new StatusDAO(); String error = (String)request.getAttribute("error"); %> <!-- *****************検索フォーム**********************--> <jsp:include page="header.jsp"/> <div class="contain"> <h3>しぼりこみ検索</h3> <form action="task-search-servlet" method="post"> <table class="form-table search-table"> <tbody> <tr> <th>タスク名で絞り込み</th> <td> <div class="search-key"> <input type="text" class="form-control" name="task_name"> </div> </td> </tr> <tr> <th>カテゴリーで絞り込み</th> <td> <div class="search-key"> <select class="form-control" name="category_id"> <option selected>Choose...</option> <option value="1">新商品A:開発プロジェクト</option> <option value="2">既存商品B:改良プロジェクト</option> </select> </div> </td> </tr> <tr> <th>ステータスで絞り込み</th> <td> <div class="search-key"> <select class="form-control" name="status_code"> <option selected>Choose...</option> <option value="00">未着手</option> <option value="50">着手</option> <option value="99">完了</option> </select> </div> </td> </tr> <tr> <th><input type="submit" value="検索" class="search-button"></th> <td> </td> </tr> </tbody> </table> </form> <!-- *****************タスク一覧テーブル(テーブル無いの▼ボタンでソートする)*********--> <h3>タスク一覧</h3> <%if(error != null){ %> <p class="error"><%=error %></p> <%} %> <div class="table-wrapper"> <table class="list-table" border="1"> <thead> <tr class="table-header"> <th class="fixed01"> <div class="title">タスクID</div> <div class="sort-box" > <form action="task-list-servlet" method="post" class="sort-form"> <input type="submit" value="▲" class="sort"> </form> <form action="task-order-by-id-desc" method="post" class="sort-form"> <input type="submit" value="▼" class="sort"> </form> </div> </th> <th class="fixed01"><div class="title">タスク名</div></th> <th class="fixed01"> <div class="title">カテゴリー</div> <form action="task-order-by-category-servlet" method="post" class="sort-form"> <input type="submit" value="▲" class="sort"> </form> <form action="task-order-by-category-desc" method="post" class="sort-form"> <input type="submit" value="▼" class="sort"> </form> </th> <th class="fixed01"> <div class="title">期限</div> <form action="task-order-by-limit-servlet" method="post" class="sort-form"> <input type="submit" value="▲" class="sort"> </form> <form action="task-order-by-limit-desc" method="post" class="sort-form"> <input type="submit" value="▼" class="sort"> </form> </th> <th class="fixed01"> <div class="title">ユーザ</div> <form action="task-order-by-user-servlet" method="post" class="sort-form"> <input type="submit" value="▲" class="sort"> </form> <form action="task-order-by-user-desc" method="post" class="sort-form"> <input type="submit" value="▼" class="sort"> </form> </th> <th class="fixed01"> <div class="title">ステータス</div> <form action="task-order-by-status-servlet" method="post" class="sort-form"> <input type="submit" value="▲" class="sort"> </form> <form action="task-order-by-status-desc" method="post" class="sort-form"> <input type="submit" value="▼" class="sort"> </form> </th> <th class="title fixed01">メモ</th> <th class="title fixed01">登録日時</th> <th class="title fixed01">更新日時</th> <th colspan="3" class="title fixed01">タスク管理</th> </tr> </thead> <tbody> <% for(TaskBean task : tasklist){ String user_name = userdao.getUserName(task.getUser_id()); String category_name = categorydao.getCategoryName(task.getCategory_id()); String status_name = statusdao.getStatusName(task.getStatus_code()); %> <tr > <td class="parameter"><%=task.getTask_id() %></td> <td class="parameter"><%=task.getTask_name() %></td> <td class="parameter"><%=task.getCategory_id() %>:<%=category_name %></td> <td class="parameter"><%=task.getLimit_date() %></td> <td class="parameter"><%=user_name %></td> <td class="parameter"><%=task.getStatus_code() %>:<%=status_name %></td> <td class="parameter"><%=task.getMemo() %></td> <td class="parameter"><%=task.getCreate_datetime() %></td> <td class="parameter"><%=task.getUpdate_datetime() %></td> <td class="parameter"> <form action="task-update-detail-servlet" method="post"> <input type="hidden" name="task_id" value="<%=task.getTask_id() %>"> <button type="submit" class="task-button">更新</button> </form> </td> <td class="parameter"> <form action="task-delete-detail-servlet" method="post"> <input type="hidden" name="task_id" value="<%=task.getTask_id() %>"> <button type="submit" class="task-button">削除</button> </form> </td> <td class="parameter"> <form action="comment-detail-servlet" method="post"> <input type="hidden" name="task_id" value="<%=task.getTask_id() %>"> <button type="submit" class="task-button">コメント</button> </form> </td> </tr> <% } %> </tbody> </table> </div> </div> </body> </html>DAOの作成(一覧表示・ソート)
このSQL文のt_taskとなっている部分をcategory_idやlimit_dateなどに変えることで簡単にソート用のSQL文はできます。
今回は、task_id, limit_date, category_id, status_code, user_id順でソートできるようにしました。model.dao.TaskDAO.java/** * タスクの一覧表示 * @return tasklist * @throws SQLException * @throws ClassNotFoundException */ //タスクID順 public List<TaskBean> getTaskList() throws SQLException, ClassNotFoundException{ String sql = "select * from t_task"; List<TaskBean> tasklist = new ArrayList<TaskBean>(); try(Connection con = ConnectionManager.getConnection(); Statement stmt = con.createStatement(); ResultSet res = stmt.executeQuery(sql)){ //取得したSQLのカラムを順番にタスクオブジェクトへ格納していく。最後にtasklistへ追加する while(res.next()) { TaskBean task = new TaskBean(); task.setTask_id(res.getInt("task_id")); task.setTask_name(res.getString("task_name")); task.setCategory_id(res.getInt("category_id")); task.setLimit_date(res.getDate("limit_date")); task.setUser_id(res.getString("user_id")); task.setStatus_code(res.getString("status_code")); task.setMemo(res.getString("memo")); task.setCreate_datetime(res.getTimestamp("create_datetime")); task.setUpdate_datetime(res.getTimestamp("update_datetime")); tasklist.add(task); } } return tasklist; } //逆 public List<TaskBean> getTaskListOrderByIdDesc() throws SQLException, ClassNotFoundException{ String sql = "select * from t_task order by task_id desc"; List<TaskBean> tasklist = new ArrayList<TaskBean>(); try(Connection con = ConnectionManager.getConnection(); Statement stmt = con.createStatement(); ResultSet res = stmt.executeQuery(sql)){ //取得したSQLのカラムを順番にタスクオブジェクトへ格納していく。最後にtasklistへ追加する while(res.next()) { TaskBean task = new TaskBean(); task.setTask_id(res.getInt("task_id")); task.setTask_name(res.getString("task_name")); task.setCategory_id(res.getInt("category_id")); task.setLimit_date(res.getDate("limit_date")); task.setUser_id(res.getString("user_id")); task.setStatus_code(res.getString("status_code")); task.setMemo(res.getString("memo")); task.setCreate_datetime(res.getTimestamp("create_datetime")); task.setUpdate_datetime(res.getTimestamp("update_datetime")); tasklist.add(task); } } return tasklist; }サーブレットの作成(一覧表示・ソート)
まず、menu画面から遷移した時に表示されるものです。
ログインしていないと一覧が表示されないようになっています。servlet.TaskListServlet.javaprotected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // TODO Auto-generated method stub TaskDAO dao = new TaskDAO(); try { HttpSession session = request.getSession(); boolean logincheck = (Boolean)session.getAttribute("login"); if(logincheck) { //取得した全タスクをリクエストスコープへ保存する List<TaskBean> tasklist = dao.getTaskList(); request.setAttribute("tasklist", tasklist); //転送する RequestDispatcher rd = request.getRequestDispatcher("task-list.jsp"); rd.forward(request, response); } } catch(SQLException | ClassNotFoundException | NullPointerException e) { e.printStackTrace(); RequestDispatcher rd = request.getRequestDispatcher("login.html"); rd.forward(request, response); }次に、各ソートボタンを押した時に送られるサーブレットの例です
TaskDAO.javaで定義したソート用のメソッド達ごとに、サーブレットを作っていきます。
例えばこれは、task_idの降順にタスクを取得するサーブレットです。TaskOrderByIdDesc.javaprotected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // TODO Auto-generated method stub TaskDAO dao = new TaskDAO(); List<TaskBean> tasklist = null; try { //ここの部分で使用するメソッドを変えるだけです。 tasklist = dao.getTaskListOrderByIdDesc(); } catch(SQLException | ClassNotFoundException e) { e.printStackTrace(); } request.setAttribute("tasklist", tasklist); RequestDispatcher rd = request.getRequestDispatcher("task-list.jsp"); rd.forward(request, response); }以上で、タスクの一覧表示とソート機能は完成です。
次に、検索機能を実装していきます。
DAOの作成(検索機能)
検索機能は、task_name, category_id, status_codeで絞り込みができるようにしていきます。
/** * 検索メソッド * @param task_name * @param category_id * @param status_code * @return * @throws SQLException * @throws ClassNotFoundException */ public List<TaskBean> searchTask(String task_name, int category_id, String status_code) throws SQLException, ClassNotFoundException{ //SQL文(全ての検索フォームが埋められた時に使うもの) String sql = "select * from t_task where task_name like ? and category_id = ? and status_code = ?"; //それぞれの検索項目について、選ばれたものだけを使うSQL文を作成 if(category_id == 0 && status_code == "0") { sql = "select * from t_task where task_name like ?"; }else if(category_id ==0) { sql = "select * from t_task where task_name like ? and status_code = ?"; }else if(status_code == "0") { sql = "select * from t_task where task_name like ? and category_id = ?"; } List<TaskBean> tasklist = new ArrayList<TaskBean>(); try(Connection con = ConnectionManager.getConnection(); PreparedStatement pstmt = con.prepareStatement(sql)){ //先ほどと同じように、それぞれの検索項目について、選ばれた物だけを使うSQL文にそぐう形で引数を代入指定いく if(category_id == 0 && status_code == "0") { pstmt.setString(1, task_name); }else if(category_id ==0) { pstmt.setString(1, task_name); pstmt.setString(2, status_code); }else if(status_code == "0") { pstmt.setString(1, task_name); pstmt.setInt(2, category_id); }else { pstmt.setString(1, task_name); pstmt.setInt(2, category_id); pstmt.setString(3, status_code); } ResultSet res = pstmt.executeQuery(); //取得したSQLのカラムを順番にタスクオブジェクトへ格納していく。最後にtasklistへ追加する while(res.next()) { TaskBean task = new TaskBean(); task.setTask_id(res.getInt("task_id")); task.setTask_name(res.getString("task_name")); task.setCategory_id(res.getInt("category_id")); task.setLimit_date(res.getDate("limit_date")); task.setUser_id(res.getString("user_id")); task.setStatus_code(res.getString("status_code")); task.setMemo(res.getString("memo")); task.setCreate_datetime(res.getTimestamp("create_datetime")); task.setUpdate_datetime(res.getTimestamp("update_datetime")); tasklist.add(task); } } return tasklist; }サーブレットの作成(検索機能)
コメントをみながらやってみてください。
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // TODO Auto-generated method stub TaskDAO taskdao = new TaskDAO(); List<TaskBean> tasklist = new ArrayList<TaskBean>(); //リクエストパラメータ取得 request.setCharacterEncoding("UTF-8"); String task_name = request.getParameter("task_name"); String category_id_str = request.getParameter("category_id"); String status_code = request.getParameter("status_code"); //入力漏れをチェック if(category_id_str.equals("Choose...")) { category_id_str = "0"; } if(status_code.equals("Choose...")) { status_code = "0"; } task_name = "%" + task_name + "%"; //category_idをint型へ戻す int category_id = Integer.parseInt(category_id_str); try { //絞り込み内容から検索 tasklist = taskdao.searchTask(task_name, category_id, status_code); //もしtasklistが0件ならエラー画面を表示 if(tasklist.size() == 0) { String error = "検索結果は0件です"; request.setAttribute("error", error); } }catch(SQLException | ClassNotFoundException e) { e.printStackTrace(); RequestDispatcher rd = request.getRequestDispatcher("task-list-servlet"); rd.forward(request, response); } request.setAttribute("tasklist", tasklist); RequestDispatcher rd = request.getRequestDispatcher("task-list.jsp"); rd.forward(request, response); }6.次回予告
今回は、タスク一覧の表示とその処理についてやってまいりました。
次回は、そのタスクリストから遷移することができるタスクの編集機能についてまとめてまいります。
- 投稿日:2020-10-11T15:46:07+09:00
Spring CacheでキャッシュをRedisに保存する際の有効期間の指定について
経緯
Redisの有効期間指定を調べたが、記載が古くそのまま使える情報が少なかったため記事を作成する※20200827時点で調査
Spring CacheでRedisにデータごとの有効期間指定ができることを確認する
build.gradle
implementation 'org.springframework.boot:spring-boot-starter-cache' implementation 'org.springframework.boot:spring-boot-starter-data-redis'設定クラスを作成
@Configuration @EnableCaching public class RedisConfig { @Bean public CacheManager cacheManager(RedisConnectionFactory fac) { // 1.キャッシュビルダーからキャッシュを作成する RedisCacheManagerBuilder builder = RedisCacheManager.builder(fac); builder .cacheDefaults( // 2.デフォルトのキャッシュ有効期限を設定する RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofDays(1))) // 3.cache1という名称のキャッシュに対し、有効期限を設定する .withCacheConfiguration("cache1", RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofMinutes(1))) // 3.cache2という名称のキャッシュに対し、有効期限を設定する .withCacheConfiguration("cache2", RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofHours(1))); return builder.build(); } }補足
1.
- RedisCacheManagerはスタティックな内部クラスであるビルダーを介して作成します
- 引数にFactoryでなくRedisCacheWriterも指定できる(内部的にはFacroty⇒RedisCacheWriterと変換しており、入口はどちらでもよい)
- 以下のようにするとビルダーを使わなくても作ることができる。この場合デフォルト有効期限(期間無限)となります。
// facは、RedisConnectionFactory型 RedisCacheManager rcm = RedisCacheManager.create(fac);2.
- デフォルトの有効期限と、データ毎の有効期限は設定するメソッドが異なる
3.
- 有効期間はDurationを利用して日付や時間など柔軟に設定できる
- データごとにの設定は内部にMAPをもっている
- withInitialCacheConfigurationsメソッドをつかうと、設定内容をあらかじめ作成したMAPでまとめて登録もできる
動作確認用のコントローラ(cache1とcache2を作成する)
@RestController @RequestMapping("/api/sample") public class SampleRedisCacheController { @Autowired CacheManager cm; @GetMapping("redisCache") public String redisCache() { Cache c1 = cm.getCache("cache1"); c1.put("k1", 123456); c1.put("k2", 234567); Cache c2 = cm.getCache("cache2"); c2.put("k1", 123456); c2.put("k2", 234567); return "redis cache test"; } }確認結果
以下にアクセスし、Redisにキャッシュが作成されること
http://localhost:8080/api/sample/redisCacheコメント
CacheManagerクラスの中身は難しそうに思えたが、作り方や色々な設定について理解できた。
ビルダーはEffectiveJavaに記載があった。コンストラクタの引数が多くなる場合に使うと便利ということで紹介されていた気がする。今回設定値が多いので導入されているのだと思う。
- 投稿日:2020-10-11T15:40:30+09:00
Java・ツイッタークローン・タスク管理システム③タスク登録機能をつける
はじめに
Javaを使って初めてアプリケーションを作成する人にむけて記事を書いてみようと思います。
ポートフォリオや、会社の研修課題作成の参考にしていただければ幸いです。
今回は、タスクマネージャーを作成します。
これを応用することで、ツイッタークローンの作成にも活かすことができます。アプリケーションの機能ごとに記事を投稿していきます。
1.データベース作成
2.ログイン機能
3.タスクの登録機能
4.一覧表示
-ソート機能
-検索機能
5.編集機能
6.削除機能
7.ログイン機能
8.排他制御について*詳しい説明はコード内に書いてありますので、コピペする人は消して使ってください
実行環境
eclipse4.16.0
Tomcat9
Java11目次
1.viewの作成
2.DAOの作成
3.サーブレットの作成
4.次回予告Viewの作成
こちらは、よくある入力フォームです。
task-insert.jsp<body> <!-- header.jspは最後に作ります--> <jsp:include page="header.jsp"/> <div class="contain"> <h3>タスク登録</h3> <!-- task-insert-servletへそれぞれの入力パラメータを渡す--> <form action="task-insert-servlet" method="post"> <table class="form-table" border="1"> <tbody> <tr> <th>タスク名</th> <td> <div class="input-key"> <input type="text" class="form-control" name="task_name" size="60" placeholder="入力してください"> </div> </td> </tr> <tr> <th>カテゴリー</th> <td> <div class="input-key"> <select class="form-control" name="category_id"> <option selected>Choose...</option> <option value="1">新商品A:開発プロジェクト</option> <option value="2">既存商品B:改良プロジェクト</option> </select> </div> </td> </tr> <tr> <th>期限</th> <td> <div class="input-key"> <input type="date" name="limit_date" class="form-control" size="60"> </div> </td> </tr> <tr> <th>ステータス</th> <td> <div class="input-key"> <select class="form-control" name="status_code" > <option selected>Choose...</option> <option value="00">未着手</option> <option value="50">着手</option> <option value="99">完了</option> </select> </div> </td> </tr> <tr> <th>メモ</th> <td> <div class="input-key"> <input type="text" class="form-control" name="memo" size="60"> </div> </td> </tr> <tr> <th><input type="submit" value="登録" class="input-submit"></th> <td></td> </tr> </tbody> </table> </form> </div> </body>投稿が完了した時の確認画面です。ここで投稿内容を把握することができます。
task-insert-comp.jsp<body> <% String task_name = (String)request.getAttribute("task_name"); int category_id = (int)request.getAttribute("category_id"); String category_name = (String)request.getAttribute("category_name"); Date limit_date = (Date)request.getAttribute("limit_date"); String status_code = (String)request.getAttribute("status_code"); String status_name = (String)request.getAttribute("status_name"); String memo = (String)request.getAttribute("memo"); %> <jsp:include page="header.jsp"/> <div class="contain"> <div class="box"> <h3>タスクの登録が完了しました</h3> <p>以下の内容で登録しました</p> <hr> <p> タスク名:<%=task_name %><br> カテゴリー:<%=category_id %>:<%=category_name %><br> 期限:<%=limit_date %><br> ステータス:<%=status_code %>:<%=status_name %><br> メモ:<%=memo %><br> </p> <a href="menu.jsp">メニューへ戻る</a> </div> </div> </body>投稿に失敗した時の表示です。
特に入力エラーではどの箇所に誤りがあったのかを表示します。task-insert-failure.jsp<body> <!-- リクエストスコープのerrorには入力に誤りがあった理由がリスト化されているので、これを受け取る--> <% List<String> error = (List<String>)request.getAttribute("error"); %> <jsp:include page="header.jsp"/> <div class="contain"> <div class="box"> <h3>タスクの登録に失敗しました</h3> <hr> <p>*下記の原因が考えられます</p> <ul> <!-- errorを表示させていく--> <%for(String er : error){ %> <li><%=er %></li> <%} %> </ul> <a href="task-insert.jsp">登録画面へ戻る</a> </div> </div> </body>DAOの作成
まずは、タスク登録用のメソッドを作成します。
model.dao.TaskDAO.java/** * タスク登録用メソッド * @param task_name * @param category_id * @param limit_date * @param user_id * @param status_code * @param memo * @return 実行件数 * @throws SQLException * @throws ClassNotFoundException */ public int insertTask(String task_name, int category_id, Date limit_date, String user_id, String status_code, String memo) throws SQLException, ClassNotFoundException { //SQL文 入力パラメータとuser_idを指定してレコードを作成 String sql = "insert into t_task (task_name, category_id, limit_date, user_id, status_code, memo) values(?, ?, ?, ?, ?, ?)"; int sum = 0; //データベースへ接続 try(Connection con = ConnectionManager.getConnection(); PreparedStatement pstmt = con.prepareStatement(sql)){ pstmt.setString(1, task_name); pstmt.setInt(2, category_id); pstmt.setDate(3, limit_date); pstmt.setString(4, user_id); pstmt.setString(5, status_code); pstmt.setString(6, memo); //更新系SQL文の実行 sum = pstmt.executeUpdate(); } return sum; }詳しい解説は、第2回のUserDAO.javaと同じです。
ステータスidだけではわかり伝いのでステータス名を表示させたいです。
そこでメソッドを使ってstatus_idを引数にstatus_nameを取得しましょうmodel.dao.StatusDAO.javapublic class StatusDAO { /** * ステータス名を取得するメソッド * @param status_code * @return * @throws SQLException * @throws ClassNotFoundException */ public String getStatusName(String status_code) throws SQLException, ClassNotFoundException { String sql = "select status_name from m_status where status_code = ?"; String status_name ="不明"; try(Connection con = ConnectionManager.getConnection(); PreparedStatement pstmt = con.prepareStatement(sql)){ pstmt.setString(1, status_code); ResultSet res = pstmt.executeQuery(); while(res.next()) { status_name = res.getNString("status_name"); } } return status_name; } }詳しい解説は、第2回のUserDAO.javaと同じです。
カテゴリーCodeだけではわかり伝いのでステータス名を表示させたいです。
そこでメソッドを使ってcategory_codeを引数にcategory_nameを取得しましょうmodel.dao.CategoryDAO.javapublic class CategoryDAO { /** * カテゴリー名を取得するメソッド * @param category_id * @return * @throws SQLException * @throws ClassNotFoundException */ public String getCategoryName(int category_id) throws SQLException, ClassNotFoundException { String sql = "select category_name from m_category where category_id = ?"; String category_name ="不明"; try(Connection con = ConnectionManager.getConnection(); PreparedStatement pstmt = con.prepareStatement(sql)){ pstmt.setInt(1, category_id); ResultSet res = pstmt.executeQuery(); while(res.next()) { category_name = res.getNString("category_name"); } } return category_name; }サーブレットの作成
servlet.TaskInsertServlet.javaprotected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // TODO Auto-generated method stub //DAOの生成 TaskDAO taskdao = new TaskDAO(); CategoryDAO categorydao = new CategoryDAO(); StatusDAO statusdao = new StatusDAO(); //パラメータの取得 request.setCharacterEncoding("UTF-8"); String task_name = request.getParameter("task_name"); String category_id_str = request.getParameter("category_id"); String limit_date_check = request.getParameter("limit_date"); String status_code = request.getParameter("status_code"); String memo = request.getParameter("memo"); //sessionスコープからログイン中のユーザー情報を取得してuser_idを取得 HttpSession session = request.getSession(); String user_id = (String)session.getAttribute("current_user_id"); //error表示を保存するリスト生成 List<String> error = new ArrayList<String>(); //sessionチェック boolean sessioncheck = (boolean)session.getAttribute("login"); if(!sessioncheck) { error.add("ログインしてからタスク登録をしてください"); } //入力漏れをチェック if(task_name.equals("")) { error.add("タスク名が空欄です"); } if(category_id_str.equals("Choose...")) { error.add("カテゴリーが選択されていません"); } if(status_code.equals("Choose...")) { error.add( "ステータスが選択されていません"); } //期限(limit_date)についてはDate型なので特別 boolean limit_check; limit_date_check = limit_date_check.replace('-', '/'); DateFormat format = DateFormat.getDateInstance(); format.setLenient(false); try { format.parse(limit_date_check); limit_check = true; } catch(Exception e) { limit_check = false; } if(!limit_check) { error.add("期限には日付を入力してください"); } //リクエストスコープへエラーを保存 request.setAttribute("error", error); if(task_name != "") { try { //パラメータ通りに受け取れないもの、つまりカテゴリーと期限(String型でないもの)を変換 int category_id = Integer.parseInt( category_id_str); Date limit_date = Date.valueOf(request.getParameter("limit_date")); //insertメソッドによりデータベース処理 taskdao.insertTask(task_name, category_id, limit_date, user_id, status_code, memo); //カテゴリー名・ステータス名を取得 String category_name = categorydao.getCategoryName(category_id); String status_name = statusdao.getStatusName(status_code); //リクエストスコープへ登録情報を保存 request.setAttribute("task_name", task_name); request.setAttribute("category_id", category_id); request.setAttribute("category_name", category_name); request.setAttribute("limit_date", limit_date); request.setAttribute("status_code", status_code); request.setAttribute("status_name", status_name); request.setAttribute("memo", memo); RequestDispatcher rd = request.getRequestDispatcher("task-insert-comp.jsp"); rd.forward(request, response); } catch(SQLException | ClassNotFoundException | IllegalArgumentException e) { RequestDispatcher rd = request.getRequestDispatcher("task-insert-failure.jsp"); rd.forward(request, response); } }else { RequestDispatcher rd = request.getRequestDispatcher("task-insert-failure.jsp"); rd.forward(request, response); } }次回予告
今回はタスク登録機能を実装しました
次回は、登録したタスクを一覧表示させたりソート機能をつけたり検索機能をつけてみます。
- 投稿日:2020-10-11T15:39:21+09:00
Java・ツイッタークローン・タスク管理システム②ログイン機能をつける
はじめに
Javaを使って初めてアプリケーションを作成する人にむけて記事を書いてみようと思います。
ポートフォリオや、会社の研修課題作成の参考にしていただければ幸いです。
今回は、タスクマネージャーを作成します。
これを応用することで、ツイッタークローンの作成にも活かすことができます。アプリケーションの機能ごとに記事を投稿していきます。
1.データベース作成
2.ログイン機能
3.タスクの登録機能
4.一覧表示
-ソート機能
-検索機能
5.編集機能
6.削除機能
7.ログイン機能
8.排他制御について*詳しい説明はコード内に書いてありますので、コピペする人は消して使ってください
実行環境
eclipse4.16.0
Tomcat9
Java11目次
1.viewの作成
2.DAOの作成
3.サーブレットの作成
4.次回予告viewの作成
*今回だけスタイリングにBootStrapを使用しています。
login.jsp<body> <!-- 入力エラーがあった場合には再度このページが開かれるのでその際になぜエラーになったのかをお知らせします。リクエストスコープデータ名errorの値を受け取る--> <% List<String> error = (List<String>)request.getAttribute("error"); %> <div class="container mt-5"> <div class="row justify-content-center"> <h3>ログイン画面</h3> </div> <!-- errorを赤色で表示させる。--> <%for(String er : error){ %> <div class="row justify-content-center"> <p style="color:red"><%=er %></p> </div> <%} %> <div class="row justify-content-center"> <!-- login-servletを指定して、入力データ(user_id, password)を送る--> <form action="login-servlet" method="post"> <div class="form-group"> <label>ユーザID</label> <input type="text" name="user_id" class="form-control" placeholder="User-ID"> </div> <div class="form-group"> <label>パスワード</label> <input type="password" name="password" class="form-control" id="exampleInputPassword1" placeholder="Password"> <small class="form-text text-muted">We'll never share your Password with anyone else.</small> </div> <button type="submit" class="btn btn-outline-info">ログイン</button> </form> </div> </div>DAOの作成
サーブレットをすっきりさせたり、あとで変更をしやすくするためにDAOにてメソッドを定義してきます。
model.dao.UserDAO.java/** *このメソッドでは入力されたユーザIDからユーザの情報全部を取得するときなどに使います。 * ユーザIDを引数にユーザ情報を取得するメソッド * @param user_id * @return ユーザ情報 * @throws SQLException * @throws ClassNotFoundException */ public UserBean getUser(String user_id) throws SQLException, ClassNotFoundException { UserBean user = new UserBean(); //SQL文user_idを指定して、レコードを取得 String sql = "select * from m_user where user_id = ?"; //データベースへ接続 try(Connection con = ConnectionManager.getConnection(); PreparedStatement pstmt = con.prepareStatement(sql)){ //受け取ったuser_idをSQL文へ代入 pstmt.setString(1, user_id); //SQL文を実行して実行結果を取得 ResultSet res = pstmt.executeQuery(); while(res.next()) { //実行結果よりそれぞれのカラムの値を取得 String password = res.getString("password"); String user_name = res.getString("user_name"); //userオブジェクトへセット user.setPassword(password); user.setUser_id(user_id); user.setUser_name(user_name); } return user; } } /** * ユーザIDを引数にユーザ名を取得するメソッド * @param user_id * @return ユーザ名 * @throws SQLException * @throws ClassNotFoundException */ public String getUserName(String user_id) throws SQLException, ClassNotFoundException { //SQL文User_idよりuser_nameを取得 String sql = "select user_name from m_user where user_id = ?"; //usre_nameの初期値をセット String user_name ="不明"; //データベースへ接続 try(Connection con = ConnectionManager.getConnection(); PreparedStatement pstmt = con.prepareStatement(sql)){ //受け取ったuser_idをSQL文へ代入 pstmt.setString(1, user_id); //SQL文を実行して実行結果を取得 ResultSet res = pstmt.executeQuery(); while(res.next()) { //実行結果よりそれぞれのカラムの値を取得 user_name = res.getNString("user_name"); } } return user_name; } /** * ログイン認証用メソッド * @param user_id * @param password * @return boolean型でtrueならログイン可能 falseならログイン不可能 * @throws SQLException * @throws ClassNotFoundException */ public boolean logincheck(String user_id, String password) throws SQLException, ClassNotFoundException { //そもそも入力欄が空欄だった場合を除外する if (user_id == null || user_id.length() == 0 || password == null || password.length() == 0){ return false; } //SQL文 入力されたuser_id, passwordを指定してレコードを取得 String sql = "select * from m_user where user_id = ? && password = ?"; //データベースへ接続 try (Connection con = ConnectionManager.getConnection(); PreparedStatement pstmt = con.prepareStatement(sql)){ pstmt.setString(1, user_id); pstmt.setString(2, password); ResultSet res = pstmt.executeQuery(); //SQL文の実行結果があれば、ユーザが存在するのでtrue。結果が無いということはそのユーザは登録されていないのでfalse if(res.next()) { return true; } else { return false; } } }サーブレットの作成
DAOで作成したメソッドを利用して、サーブレットを作成していきます。
Servlet.Login.javaprotected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // TODO Auto-generated method stub //リクエストパラメータの受け取り request.setCharacterEncoding("UTF-8"); String user_id = request.getParameter("user_id"); String password = request.getParameter("password"); //DAOオブジェクト生成 UserDAO userdao = new UserDAO(); //sessionスコープ生成・ HttpSession session = request.getSession(); try { boolean logincheck = userdao.logincheck(user_id, password); if(logincheck) { //-----------入力が正しかった時の処理--------------------------------------------------- //sessionスコープへログイン状況をセット session.setAttribute("login", true); //ログインユーザ名と情報を取得してsessionスコープへセット String current_user_name = userdao.getUserName(user_id); UserBean current_user = userdao.getUser(user_id); session.setAttribute("current_user_name", current_user_name); session.setAttribute("current_user", current_user); session.setAttribute("current_user_id", user_id); //メニューセッションへ転送 RequestDispatcher rd = request.getRequestDispatcher("menu-servlet"); rd.forward(request, response); } else { //--------入力が違った時の処理----------------------------------------------------------- //空欄をチェック List<String> error = new ArrayList<String>(); if(password.equals("")) { error.add("パスワードが空欄です"); } if(user_id.equals("")) { error.add("ユーザIDが空欄です"); } error.add("ユーザIDかパスワードに誤りがあります"); request.setAttribute("error", error); //sessionスコープへログイン状況をセット(認証不可) session.setAttribute("login", false); //ログイン画面へ転送 RequestDispatcher rd = request.getRequestDispatcher("login.jsp"); rd.forward(request, response); } //---------その他SQLなどのエラー-------------------------------------------------------------- }catch(SQLException | ClassNotFoundException e) { e.printStackTrace(); RequestDispatcher rd = request.getRequestDispatcher("login.jsp"); rd.forward(request, response); } }次回予告
今回は、ログイン機能の実装をしてみました。
次回は、タスクの登録機能の実装をしていきます。
- 投稿日:2020-10-11T15:38:48+09:00
Java・ツイッタークローン・タスク管理システム①データベースを作成する
はじめに
Javaを使って初めてアプリケーションを作成する人にむけて記事を書いてみようと思います。
ポートフォリオや、会社の研修課題作成の参考にしていただければ幸いです。
今回は、タスクマネージャーを作成します。
これを応用することで、ツイッタークローンの作成にも活かすことができます。アプリケーションの機能ごとに記事を投稿していきます。
1.データベース作成
2.ログイン機能
3.タスクの登録機能
4.一覧表示
-ソート機能
-検索機能
5.編集機能
6.削除機能
7.ログイン機能
8.排他制御について*詳しい説明はコード内に書いてありますので、コピペする人は消して使ってください
実行環境
eclipse4.16.0
Tomcat9
Java11
Mysql5.7目次
1.データベース概要
2.SQL文
3.Bean作成
4.データベースとの接続
5.次回予告データベース概要
データベース名task_db
権利ユーザ名testuser
パスワードpassword
テーブル ユーザのマスターテーブル
カテゴリーのマスターテーブル
ステータスのマスターテーブル
タスクテーブル(ユーザID、カテゴリーID、ステータスIDのForeignKeyを持ちます)
コメントテーブル(ユーザID、タスクIDのForeignKeyを持ちます)SQL文
①データベース作成
create databasetask_db②ユーザ作成
まずrootユーザになってパスワードのバリデーションを変更しますSet global validate_password.length=6; パスワードの長さを6に set global validate_password.policy=LOW; ポリシーを変更次にパスワード付きでユーザ作成
create user testuser identified by 'password'③ユーザへtask_dbの全権限を与える
grant all on task_db.* to testuser以上で準備は完了です。
④それぞれのテーブルを作成していきます。
--------ユーザのマスターテーブル create table m_user ( user_id varchar(24) primary key, password varchar(32) not null, user_name varchar(20) not null unique, update_datetime timestamp not null default current_timestamp on update current_timestamp ); --------カテゴリのマスターテーブル create table m_category ( category_id int primary key auto_increment, category_name varchar(20) not null unique, update_datetime timestamp not null default current_timestamp on update current_timestamp ); --------ステータスのマスターテーブル create table m_status ( status_code char(2) primary key, status_name varchar(20) unique not null, update_datetime timestamp not null default current_timestamp on update current_timestamp ); --------タスクテーブル create table t_task ( task_id int auto_increment primary key, task_name varchar(50) not null, category_id int not null, limit_date date, user_id varchar(24) not null, status_code char(2) not null, memo varchar(100), create_datetime timestamp default current_timestamp not null, update_datetime timestamp default current_timestamp not null on update current_timestamp, foreign key(category_id) references m_category(category_id), foreign key(user_id) references m_user(user_id), foreign key(status_code) references m_status(status_code) ); --------コメントテーブル create table t_comment( comment_id int auto_increment primary key, task_id int, user_id varchar(100) not null, comment varchar(100) not null, update_datetime timestamp default current_timestamp not null on update current_timestamp, foreign key(task_id) references t_task(task_id) on delete set null on update cascade, foreign key(user_id) references m_user(user_id) );次に初期データを入れておきます。
-------カテゴリーレコード作成 insert into m_category (category_name) value("新商品A:開発プロジェクト"); insert into m_category (category_name) value("既存商品B:改良プロジェクト"); select * from m_category; --------ステータスレコード作成 insert into m_status ( status_code, status_name) values("00", "未着手"); insert into m_status ( status_code, status_name) values("50", "着手"); insert into m_status ( status_code, status_name) values("99", "完了"); select * from m_status; --------ユーザーレコード作成 insert into m_user(user_id, password, user_name) values("testid", "password", "testuser"); insert into m_user(user_id, password, user_name) values("testid2", "password", "testuser2"); --------タスクレコード作成 insert into t_task(task_name, category_id, limit_date, user_id, status_code, memo) values("サンプルタスク1", 1, "2022-10-01", "testid", "99", "テスト入力"); insert into t_task(task_name, category_id, limit_date, user_id, status_code, memo) values("サンプルタスク2", 2, "2020-07-05", "testid", "50", "テスト入力"); insert into t_task(task_name, category_id, limit_date, user_id, status_code, memo) values("サンプルタスク2", 1, "2020-09-30", "testid", "00", "テスト入力"); insert into t_task(task_name, category_id, limit_date, user_id, status_code, memo) values("サンプルタスク3", 2, "2002-08-30", "testid", "99", "テスト入力"); insert into t_task(task_name, category_id, limit_date, user_id, status_code, memo) values("サンプルタスク4", 1, "2000-09-30", "testid", "00", "テスト入力"); insert into t_task(task_name, category_id, limit_date, user_id, status_code, memo) values("サンプルタスク5", 2, "2025-09-30", "testid", "50", "テスト入力"); insert into t_task(task_name, category_id, limit_date, user_id, status_code, memo) values("サンプルタスク6", 1, "1998-09-30", "testid", "00", "テスト入力"); insert into t_task(task_name, category_id, limit_date, user_id, status_code, memo) values("サンプルタスク7", 2, "2020-09-30", "testid", "99", "テスト入力"); insert into t_task(task_name, category_id, limit_date, user_id, status_code, memo) values("サンプルタスク8", 1, "2020-10-30", "testid", "00", "テスト入力");Bean作成
ここでは、それぞれのテーブルのBeanを作成していきます。
フィールド、コンストラクタ、getterとsetterを記述しているだけです。
確認したらコピペでオッケーですmodel.entity.TaskBean.javapublic class TaskBean { /** * フィールド */ private int task_id; private String task_name; private int category_id; private Date limit_date; private String user_id; private String status_code; private String memo; private Timestamp create_datetime; private Timestamp update_datetime; private int version; /** * コンストラクタ */ public TaskBean(){ } /** * メソッド */ public int getTask_id() { return task_id; } public void setTask_id(int task_id) { this.task_id = task_id; } public String getTask_name() { return task_name; } public void setTask_name(String task_name) { this.task_name = task_name; } public int getCategory_id() { return category_id; } public void setCategory_id(int category_id) { this.category_id = category_id; } public Date getLimit_date() { return limit_date; } public void setLimit_date(Date limit_date) { this.limit_date = limit_date; } public String getUser_id() { return user_id; } public void setUser_id(String user_id) { this.user_id = user_id; } public String getStatus_code() { return status_code; } public void setStatus_code(String status_code) { this.status_code = status_code; } public String getMemo() { return memo; } public void setMemo(String memo) { this.memo = memo; } public Timestamp getCreate_datetime() { return create_datetime; } public void setCreate_datetime(Timestamp create_datetime) { this.create_datetime = create_datetime; } public Timestamp getUpdate_datetime() { return update_datetime; } public void setUpdate_datetime(Timestamp update_datetime) { this.update_datetime = update_datetime; } public int getVersion() { return version; } public void setVersion(int version) { this.version = version; } }model.entity.UserBean.javapublic class UserBean { /** * フィールド */ private String user_id; private String password; private String user_name; /** * コンストラクタ */ public UserBean(){ } /** * メソッド */ public String getUser_id() { return user_id; } public void setUser_id(String user_id) { this.user_id = user_id; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public String getUser_name() { return user_name; } public void setUser_name(String user_name) { this.user_name = user_name; } }model.entity.StatusBean.javapublic class StatusBean { /** * フィールド */ private String status_code; private String status_name; /** * コンストラクタ */ public StatusBean(){ } /** * メソッド */ public String getStatus_code() { return status_code; } public void setStatus_code(String status_code) { this.status_code = status_code; } public String getStatus_name() { return status_name; } public void setStatus_name(String status_name) { this.status_name = status_name; } }model.entity.CategoryBean.javapublic class CategoryBean { /** * フィールド */ private int category_id; private String category_name; /** * コンストラクタ */ public CategoryBean(){ } /** * メソッド */ public int getCategory_id() { return category_id; } public void setCategory_id(int category_id) { this.category_id = category_id; } public String getCategory_name() { return category_name; } public void setCategory_name(String category_name) { this.category_name = category_name; } }model.entity.CommentBean.javapublic class CommentBean { /** * フィールド */ private int comment_id; private int task_id; private String user_id; private String comment; private Timestamp update_datetime; /** * コンストラクタ */ public CommentBean() { } /** * メソッド */ public int getComment_id() { return comment_id; } public void setComment_id(int comment_id) { this.comment_id = comment_id; } public int getTask_id(){ return task_id; } public void setTask_id(int task_id) { this.task_id = task_id; } public String getUser_id() { return user_id; } public void setUser_id(String user_id) { this.user_id = user_id; } public String getComment() { return comment; } public void setComment(String comment) { this.comment = comment; } public Timestamp getUpdate_datetime() { return update_datetime; } public void setUpdate_datetime(Timestamp update_datetime) { this.update_datetime = update_datetime; } }データベースとの接続
①JDBCをプロジェクトのWebContent-Web-INF-libの中に入れる
②ビルドパスの構成から追加をしておきます
③コネクションマネージーを作成
変数USERと変数PASSWORDはパスワードとユーザ名はそれぞれtestuserとpasswordでやっております。model.dao.ConnectionManager.javapublic class ConnectionManager { //データベースの情報 private final static String URL = "jdbc:mysql://localhost:3306/task_db?useSSL=false&serverTimezone=JST"; private final static String USER = "testuser"; private final static String PASSWORD = "password"; //データベースへの接続メソッド public static Connection getConnection() throws SQLException, ClassNotFoundException{ Class.forName("com.mysql.cj.jdbc.Driver"); return DriverManager.getConnection(URL, USER, PASSWORD); } }次回予告
今回は、データベースの作成からBean作成とmysqlとの接続までやってまいりました。
作成いしたm_userテーブルを使って次回は、ログイン機能をつけていきます。
- 投稿日:2020-10-11T15:18:56+09:00
Spring Bootのテストで@WebMvcTestを付けたテストでSpring Securityのデフォルトセキュリティが有効化される
Spring Boot 2.3.4.RELEASEでWebFluxとSecurityを使用している際、
コントローラーなどのテストのために@WebMvcTest
を付与しているテストで
WebTestClient
でコントローラーにリクエストを行うと、何故かBasic認証を求められた。確かにSpring Securityを利用しているプロジェクトではあるが、Basic認証などは利用していない。
原因に検討が付かなかったが、GitHubにissueが存在した。
要約すると、
@WebFluxTest
では@Conroller
や@ControllerAdvice
,WebFluxConfigurer
など、コントローラー関係の@Bean
は構成されるが、@Service
の@Bean
は構成されない。ただし、
@Configuration
が付与されたクラスは@Bean
として生成はされるようだ。
そして、その中から条件に一致する@Bean
のみが生成されるらしい。Spring SecurityのWebFlux向けのセキュリティ構成をしている場合、多くの場合ドキュメントに記載されている以下のような構成を用意することになる。
@Configuration @EnableWebFluxSecurity public class SecurityConfiguration { @Bean public SecurityWebFilterChain springWebFilterChain(ServerHttpSecurity http) { // do stuff } }このとき
SecurityWebFilterChain
は生成されないが、SecurityConfiguration
と一緒に@EnableWebFluxSecurity
がスキャンされる。
@EnableWebFluxSecurity
の定義を見ると@Import(WebFluxSecurityConfiguration.class)が付与されている。
WebFluxSecurityConfiguration
はデフォルトのSecurityWebFilterChainを生成する。こういった流れで
@WebFluxTest
が付与されたテストでSpring Securityのデフォルトセキュリティが適用されるらしい。対応策としてはプロジェクトで用意している
SecurityWebFilterChain
を生成する@Configuration
を明示的に@Import
するしかないらしい。
- 投稿日:2020-10-11T15:18:56+09:00
Spring Bootの@WebMvcTestテストでSpring Securityのデフォルトセキュリティが有効化される
Spring Boot 2.3.4.RELEASEでWebFluxとSecurityを使用している際、
コントローラーなどのテストのために@WebMvcTest
を付与しているテストで
WebTestClient
でコントローラーにリクエストを行うと、何故かBasic認証を求められた。確かにSpring Securityを利用しているプロジェクトではあるが、Basic認証などは利用していない。
原因に検討が付かなかったが、GitHubにissueが存在した。
要約すると、
@WebFluxTest
では@Conroller
や@ControllerAdvice
,WebFluxConfigurer
など、コントローラー関係の@Bean
は構成されるが、@Service
の@Bean
は構成されない。ただし、
@Configuration
が付与されたクラスは@Bean
として生成はされるようだ。
そして、その中から条件に一致する@Bean
のみが生成されるらしい。Spring SecurityのWebFlux向けのセキュリティ構成をしている場合、多くの場合ドキュメントに記載されている以下のような構成を用意することになる。
@Configuration @EnableWebFluxSecurity public class SecurityConfiguration { @Bean public SecurityWebFilterChain springWebFilterChain(ServerHttpSecurity http) { // do stuff } }このとき
SecurityWebFilterChain
は生成されないが、SecurityConfiguration
と一緒に@EnableWebFluxSecurity
がスキャンされる。
@EnableWebFluxSecurity
の定義を見ると@Import(WebFluxSecurityConfiguration.class)が付与されている。
WebFluxSecurityConfiguration
はデフォルトのSecurityWebFilterChainを生成する。こういった流れで
@WebFluxTest
が付与されたテストでSpring Securityのデフォルトセキュリティが適用されるらしい。対応策としてはプロジェクトで用意している
SecurityWebFilterChain
を生成する@Configuration
を明示的に@Import
するしかないらしい。
- 投稿日:2020-10-11T12:21:37+09:00
JPAのEntityクラスファイルを自動生成するソースを作ってみた
はじめに
タイトルにもある通り、今回はEntityクラスのJavaファイルを生成するアプリを作ってみました!
OracleDB11g2R,MySQLでは動作確認済みです。
※下図の通り、結構簡単な物しか生成できませんが。。
(諸事情で@columnと@Id、@TableはJavaEEのではなく、自作したものを使用しました)Test.javaimport java.util.ArrayList; import annotation.Entity; import annotation.Table; import annotation.column; import annotation.id; @Entity @Table("test")//DBに登録したテーブル名が表示されます public class Test { @id//主キー @column//カラム private String num; @column//カラム private String namename; public void setNum(String num) { this.num = num; } public String getNum() { return this.num; } public void setNamename(String namename) { this.namename = namename; } public String getNamename() { return this.namename; } }環境(適当)
- 実行環境: macOS
- IDE: Eclipse
構成(超概要)
クラス名 説明 EntityGenerator 下記クラスを呼び出しながら、JavaFileの生成及びファイルの中身をWriteしていくクラス DBReader DBに接続し、メタデータを取得してくるクラス(テーブル一覧、カラム名、主キーetc..) DataTypeMapper カラムのデータ型をDBのデータ型 → javaのデータ型に変換するクラス EntityInfo DBReaderで取得してきた情報を格納するクラス annotation群 @Table, @Id, @column
ソース
EntityGenerator
interface
EntityGenerator.javapackage entityCreater.generator; public interface EntityGenerator { void generateEntity(); }実装クラス
EntityGeneratorImplements.javapackage entityCreater.generator; import java.io.BufferedWriter; import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import entityCreater.info.EntityInfo; import entityCreater.reader.DBReaderImplements; public class EntityGeneratorImplements implements EntityGenerator{ //Fileを保存する場所 String filePlace; /** * コンストラクタ */ public EntityGeneratorImplements(String filePlace) { this.filePlace = filePlace; } /** * Entity作成メソッド */ @Override public void generateEntity() { System.out.print(Thread.currentThread().getStackTrace()[1].getClassName() + ":"); System.out.println("EntityFile作成メソッドを開始します"); ArrayList<EntityInfo> entityInfos = new DBReaderImplements().read(); for(EntityInfo ei : entityInfos) { createFile(ei); } } /** * EntityFile作成メソッド * @param entityInfo */ private void createFile(EntityInfo entityInfo) { //DBのテーブル情報を取得 String tableName = entityInfo.getTableName(); String id = entityInfo.getId(); List<String[]> columns = entityInfo.getColumns(); //クラス名を生成 String className = tableName.substring(0, 1).toUpperCase() + tableName.substring(1).toLowerCase(); //EntityFileクラス File entityFile = new File(filePlace + "/" + className + ".java"); try{ if (entityFile.createNewFile()){ System.out.print(Thread.currentThread().getStackTrace()[1].getClassName() + ":"); System.out.println("ファイルの作成に成功しました。"); }else{ System.out.print(Thread.currentThread().getStackTrace()[1].getClassName() + ":"); System.out.println("ファイルの作成に失敗しました"); } }catch(IOException e){ System.out.println(e); } //Fileの中身を作成する System.out.print(Thread.currentThread().getStackTrace()[1].getClassName() + ":"); System.out.println("ファイルの中身を作成します。"); try(PrintWriter pw = new PrintWriter(new BufferedWriter(new FileWriter(entityFile, true)));) { //Annotationのインポート文 pw.println("import annotation.Entity;"); pw.println("import annotation.Table;"); pw.println("import annotation.id;"); pw.println("import annotation.column;"); pw.println(""); //Class宣言(開始) pw.println("@Entity"); pw.println("@Table(\"" + tableName + "\")"); pw.println("public class " + className + " {"); pw.println(""); //カラム宣言 Iterator<String[]> it = columns.iterator(); while(it.hasNext()) { String[] colum = it.next(); //Columnの宣言 if(colum[1].equals(id)) { pw.println(" @id"); } pw.println(" @column"); pw.println(" private " + colum[0] + " " + colum[1] + ";"); pw.println(""); } //Setter.Getter宣言 //Iteratorを先頭に戻す it = columns.iterator(); while(it.hasNext()) { String[] colum = it.next(); //Setter宣言 pw.println(" public void set" + colum[1].substring(0, 1).toUpperCase() + colum[1].substring(1).toLowerCase() + "(" + colum[0] + " " + colum[1] + ") {"); pw.println(" this." + colum[1] + " = " + colum[1] + ";"); pw.println(" }"); pw.println(""); //Getter宣言 pw.println(" public " + colum[0] + " get" + colum[1].substring(0, 1).toUpperCase() + colum[1].substring(1).toLowerCase() + "() {"); pw.println(" return this." + colum[1] + ";"); pw.println(" }"); pw.println(""); } //Class宣言(終了) pw.println("}"); //終了表示 System.out.println(Thread.currentThread().getStackTrace()[1].getClassName() + ":"); System.out.println("*************************************************************"); System.out.println("*** SUCCESS パッケージ宣言、多重度の宣言を追記してください。 ***"); System.out.println("*************************************************************"); } catch (IOException e) { e.printStackTrace(); }finally { } } }DBReader
interface
DBReader.javapackage entityCreater.reader; import java.util.ArrayList; import entityCreater.info.EntityInfo; public interface DBReader { ArrayList<EntityInfo> read(); }実装クラス
DBReaderImplements.javapackage entityCreater.reader; import java.sql.Connection; import java.sql.DatabaseMetaData; import java.sql.ResultSet; import java.util.ArrayList; import java.util.Iterator; import container.DBConfig; import container.EnvironmentConfigReader; import db_access.DBAccess; import entityCreater.dataTypeMapper.DataTypeMapper; import entityCreater.info.EntityInfo; public class DBReaderImplements implements DBReader{ @Override public ArrayList<EntityInfo> read() { //metadata格納用 ResultSet rs = null; //Table情報格納用 ArrayList<String> tableNames = new ArrayList<>(); //Entity情報格納用 ArrayList<EntityInfo> entityInfos = new ArrayList<>(); //DB設定の取得 /* * 以下の範囲はあまり気にしないでください。 * 自作のyamlファイルからDBの接続情報(URLとかPASSWORDとか)を取得してきて、 * DBAccessクラス内部でDBとのConnectionを取得しているだけです。 * EnvironmentConfigReaderとDBAccessのソースは参考に記載しておきます */ /* *ここから */ EnvironmentConfigReader ecr = new EnvironmentConfigReader(); DBConfig dbc = ecr.read(); String dbName = dbc.getDbName(); //コネクションの確立 DBAccess dba = new DBAccess(dbc); /* * ここまで */ try { // データベースとの接続 Connection con = dba.getConnection(); //データベースメタデータの取得 DatabaseMetaData dbmd = con.getMetaData(); //Table名の取得 System.out.print(Thread.currentThread().getStackTrace()[1].getClassName() + ":"); System.out.println("Table一覧を取得します"); String[] types = { "TABLE", "VIEW", "SYSTEM TABLE" }; rs = dbmd.getTables(dbName, null, "%", types); while (rs.next()) { String tableName = rs.getString("TABLE_NAME"); //Table名の格納 tableNames.add(tableName); } //Table情報の格納用クラス群 Iterator<String> tableNameList = tableNames.iterator(); String tableName; String id = null; ArrayList<String[]> columns; while (tableNameList.hasNext()) { //TableNameを取得 tableName = tableNameList.next(); System.out.print(Thread.currentThread().getStackTrace()[1].getClassName() + ":"); System.out.println("Table名:" + tableName + "のEntityクラスを生成します"); //Tableの全column情報の取得 System.out.print(Thread.currentThread().getStackTrace()[1].getClassName() + ":"); System.out.println("Table名:" + tableName + "の全column情報を取得します"); rs = dbmd.getColumns(dbName, null, tableName, "%"); //column格納用 columns = new ArrayList<>(); while (rs.next()) { //SQLのデータタイプからJavaのデータタイプに変換 String dataType = DataTypeMapper.dataTypeChange(rs.getString("TYPE_NAME")); //変数名を小文字に変換 String columnName = rs.getString("COLUMN_NAME").toLowerCase(); String[] column = { dataType, columnName }; columns.add(column); } //PrimaryKeyを取得 System.out.print(Thread.currentThread().getStackTrace()[1].getClassName() + ":"); System.out.println("Table名:" + tableName + "のPrimaryKeyを取得します"); ResultSet primaryKeys = dbmd.getPrimaryKeys(dbName, null, tableName); if (primaryKeys.next()) { System.out.print(Thread.currentThread().getStackTrace()[1].getClassName() + ":"); System.out.println("Table名:" + tableName + "のPrimaryKeyは " + primaryKeys.getString("COLUMN_NAME") + "です"); id = primaryKeys.getString("COLUMN_NAME").toLowerCase(); } System.out.print(Thread.currentThread().getStackTrace()[1].getClassName() + ":"); System.out.println("EntityInfoクラスにTable情報を格納しています"); //Table情報格納用クラス EntityInfo ei = new EntityInfo(); //Table情報格納用クラスにTableNameを設定 ei.setTableName(tableName); //Table情報格納用クラスにidを設定 ei.setId(id); //Table情報格納クラスにカラム情報を設定 ei.setColumns(columns); entityInfos.add(ei); } rs.close(); // データベースのクローズ System.out.print(Thread.currentThread().getStackTrace()[1].getClassName() + ":"); System.out.println("DBとのコネクションを破棄します"); con.close(); } catch (Exception e) { System.out.println("Exception発生"); e.printStackTrace(); } return entityInfos; } }DataTypeMapper
DataTypeMapper.javapackage entityCreater.dataTypeMapper; public class DataTypeMapper { /** * SQLのデータタイプからJavaのデータタイプに変換するクラス * @param sqlDataType * @return */ public static String dataTypeChange(String sqlDataType) { String javaDataType = null; switch (sqlDataType) { case "CHAR": case "VARCHAR": case "LONGVARCHAR": javaDataType = "String"; break; case "NUMERIC": case "DECIMAL": javaDataType = "java.math.BigDecimal"; break; case "BIT": javaDataType = "boolean"; break; case "TINYINT": javaDataType = "byte"; break; case "SMALLINT": javaDataType = "short"; break; case "INTEGER": case "INT": javaDataType = "Integer"; break; case "BIGINT": javaDataType = "long"; break; case "REAL": javaDataType = "float"; break; case "FLOAT": case "DOUBLE": javaDataType = "double"; break; case "BINARY": case "VARBINARY": case "LONGVARBINARY": javaDataType = "byte[]"; break; case "DATE": javaDataType = "java.sql.Date"; break; case "TIME": javaDataType = "java.sql.Time"; break; case "TIMESTAMP": javaDataType = "java.sql.Timestamp"; break; default: break; } return javaDataType; } }EntityInfo
EntityInfo.javapackage entityCreater.info; import java.util.List; /** * * Entity情報格納クラス * @author okamotoyuuma * */ public class EntityInfo { //テーブル名 private String tableName; //Primary Key名 private String id; //すべてのカラム名と型 private List<String[]> columns; /** * (non-javadoc) * getter setter * @return */ public String getTableName() { return tableName; } public void setTableName(String tableName) { this.tableName = tableName; } public String getId() { return id; } public void setId(String id) { this.id = id; } public List<String[]> getColumns() { return columns; } public void setColumns(List<String[]> columns) { this.columns = columns; } }アノテーション
@Tableアノテーション(属性にテーブル名を格納するクラス)
Table.javapackage annotation; import static java.lang.annotation.ElementType.*; import static java.lang.annotation.RetentionPolicy.*; import java.lang.annotation.Retention; import java.lang.annotation.Target; @Target(TYPE) @Retention(RUNTIME) public @interface Table { String value(); }@idアノテーション(主キーのフィールドに付与する)
id.javapackage annotation; import static java.lang.annotation.ElementType.*; import static java.lang.annotation.RetentionPolicy.*; import java.lang.annotation.Retention; import java.lang.annotation.Target; @Retention(RUNTIME) @Target(FIELD) public @interface id { }@columnnアノテーション(テーブルのカラム値に付与する)
column.javapackage annotation; import static java.lang.annotation.ElementType.*; import static java.lang.annotation.RetentionPolicy.*; import java.lang.annotation.Retention; import java.lang.annotation.Target; @Retention(RUNTIME) @Target(FIELD) public @interface column { }使い方
適当にMainメソッドを作って、下記のように生成するEntityクラスを格納する場所を指定すればOKです!
main.javapublic class main { public static void main(String[] args) { EntityGenerator eg = new EntityGeneratorImplements("保存先のパス"); eg.generateEntity(); } }最後に
今回はEntityクラスの自動生成をしてみました。
ソースやロジック等はまだまだ未熟で稚拙ですが、File生成はしたことがなかったのでとても良い経験になりました!
考慮できていない点や、ソースの改善点などご指摘あればコメントでいただけますと幸いです。以下の参考項に上記ソースに載せていないDB設定ファイルや読み込むクラス等を載せていますので
興味ある方はみていただけたらと思います。参考(DB設定YAMLファイルと設定ファイルを読み込むクラスなど)
DB設定ファイル
DBProfile.yaml#DBの設定 !!container.DBConfig #DB定義設定クラスのパス driver: com.mysql.cj.jdbc.Driver #driver名 url: jdbc:mysql://localhost:3306/DB名 #DBのURL user: DBのユーザ名 #DBのユーザ password: password #DBのパスワード numberOfAccess: 10 #コネクションの数 dbName: データベース名 #DB名 dbType: MySQL #DBの種類(今回対応するのはMySQLとOracleDBのみ) schema: スキーマ名※DB設定ファイルは別のアプリでも使っているため、余計な情報も入ってます。。
設定情報を格納するためのクラス
DBConfig.javapackage container; //データベースの設定ファイル public class DBConfig { //ドライバ名 String driver; //DBのURL String url; //DBのユーザ String user; //パスワード String password; //スキーマ名 String schema; //コネクションの確立数 int numberOfAccess; //DBName String dbName; //DBType String dbType; public String getDriver() { return driver; } public void setDriver(String driver) { this.driver = driver; } public String getUrl() { return url; } public void setUrl(String url) { this.url = url; } public String getUser() { return user; } public void setUser(String user) { this.user = user; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public String getSchema() { return schema; } public void setSchema(String schema) { this.schema = schema; } public int getNumberOfAccess() { return numberOfAccess; } public void setNumberOfAccess(int numberOfAccess) { this.numberOfAccess = numberOfAccess; } public String getDbName() { return dbName; } public void setDbName(String dbName) { this.dbName = dbName; } public String getDbType() { return dbType; } public void setDbType(String dbType) { this.dbType = dbType; } }上記設定ファイルを読み込むEnvironmentConfigReaderクラス
EnvironmentConfigReader.javapackage container; import org.yaml.snakeyaml.Yaml; public class EnvironmentConfigReader implements Reader<DBConfig>{ //DBの設定ファイル static String configFileName = "DBProfile.yaml"; //yamlファイルからDB設定を取得するメソッド(引数なし) @Override public DBConfig read() { //ログ発生箇所 System.out.print(Thread.currentThread().getStackTrace()[1].getClassName() + ":"); //処理内容 System.out.println(configFileName + "の読み込みを開始します。"); Yaml yaml = new Yaml(); DBConfig dbc = (DBConfig) yaml.load(getClass().getResourceAsStream(configFileName)); //ログ発生箇所 System.out.print(Thread.currentThread().getStackTrace()[1].getClassName() + ":"); //処理内容 System.out.println(configFileName + "の読み込みが完了しました。"); return dbc; } //yamlファイルからDB設定を取得するメソッド(引数あり) @Override public DBConfig read(String setConfigFileName) { //ログ発生箇所 System.out.print(Thread.currentThread().getStackTrace()[1].getClassName() + ":"); //処理内容 System.out.println(configFileName + "の読み込みを開始します。"); //指定されたファイル名をセット if (configFileName != null) { configFileName = setConfigFileName; } Yaml yaml = new Yaml(); DBConfig dbc = (DBConfig) yaml.load(getClass().getResourceAsStream(configFileName)); //ログ発生箇所 System.out.print(Thread.currentThread().getStackTrace()[1].getClassName() + ":"); //処理内容 System.out.println(configFileName + "の読み込みが完了しました。"); return dbc; } }DBとのコネクションを格納するクラス
DBAccess.javapackage db_access; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; import container.DBConfig; public class DBAccess { //メンバ private Connection conn = null; public DBAccess(DBConfig dbc) { try { Class.forName(dbc.getDriver()); conn = DriverManager.getConnection(dbc.getUrl(), dbc.getUser(), dbc.getPassword()); System.out.print(Thread.currentThread().getStackTrace()[1].getClassName() + ":"); System.out.println("DBに接続しました"); //自動コミットOFF conn.setAutoCommit(false); } catch (SQLException | ClassNotFoundException e) {//コネクションの確立に失敗 System.out.print(Thread.currentThread().getStackTrace()[1].getClassName() + ":"); System.out.println("DBとの接続に失敗しました"); e.printStackTrace(); } } //コネクションを配布する public Connection getConnection() { return conn; } //コネクションの破棄 public void closeConnection() { try { conn.close(); System.out.print(Thread.currentThread().getStackTrace()[1].getClassName() + ":"); System.out.println("DBから切断しました"); } catch (SQLException e) { // TODO 自動生成された catch ブロック System.out.print(Thread.currentThread().getStackTrace()[1].getClassName() + ":"); System.out.println("DBから切断できませんでした"); e.printStackTrace(); } } }
- 投稿日:2020-10-11T10:58:44+09:00
Spring WebFluxで自作WebFilterなどでServerHttpRequestからJSONボディを読み込むショートハンド
Spring Boot 2.3.4.RELEASE時点の環境の話です。
Spring WebFluxではHTTPリクエストを処理するための方法を幾つか提供してくれている。
この時にHTTPレスポンスデータを生成するために、
多くの場合はRouterFunction
や@Controller
を使用することが多いと思う。
RouterFunction
にはServerRequest
が渡されるので、ServerRequest#bodyToMono(Class<? extends T>)
を呼び出しすれば
リクエストボディのJSONデータをパース出来る。Spring Bootでは
@Controller
の場合は@RequestBody
アノテーションが付与されていれば
フレームワーク側でリクエストボディをパースしてくれる。ほとんどの場合はこの方法でリクエストボディのJSONをアプリケーション内で利用するオブジェクトに変換できる。
しかし、Spring Securityのカスタム認証はWebFilterの段階で処理を行うため、
RouterFunction
などが呼び出される。このとき
WebFilter
にはServerWebExchange
が渡されるが、ServerWebExchange#getRequest()
メソッドは
ServerHttpRequest
を返す。
ServerHttpRequest
はServerRequest
との間に継承関係なども持たないため、
ServerRequest#bodyToMono(Class<? extends T>)
のような便利な関数を持たない。
ServerWebExchange#getFormData()
でフォームデータの取得などはできるが、リクエストボディのJSONのパースなどは自分で行わなければいけない。このパース処理をSpring Bootと同じようにするためには、
ServerCodecConfigurer
をAutowired
で取得し、
ServerCodecConfigurer#getReaders()
でListSE<HttpMessageReader<?>>
を取得、
HTTPリクエストボディをパースするためのHttpMessageReader
はStream#filter()
で取得して...といった面倒な作業が必要になる。これをSpring Securityの
org.springframework.security.web.server.authentication.ServerAuthenticationConverter
で書くと大体以下のようになる。import java.io.Serializable; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Objects; import javax.naming.AuthenticationException; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import org.springframework.core.ResolvableType; import org.springframework.http.MediaType; import org.springframework.http.codec.HttpMessageReader; import org.springframework.http.codec.ServerCodecConfigurer; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.web.server.authentication.ServerAuthenticationConverter; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; public class JsonLoginServerAuthenticationConverter implements ServerAuthenticationConverter { private final List<HttpMessageReader<?>> httpMessageReaders; public JsonLoginServerAuthenticationConverter(ServerCodecConfigurer serverCodecConfigurer) { this(serverCodecConfigurer.getReaders()); } public JsonLoginServerAuthenticationConverter(List<HttpMessageReader<?>> httpMessageReaders) { this.httpMessageReaders = Objects.requireNonNull(httpMessageReaders); } @Override public Mono<Authentication> convert(ServerWebExchange exchange) { ServerHttpRequest request = exchange.getRequest(); MediaType contentType = request.getHeaders().getContentType(); MediaType acceptType = MediaType.APPLICATION_JSON; // Content-Type のチェック。 if (contentType == null || acceptType.isCompatibleWith(contentType)) { return Mono.error(new AuthenticationException("Invalid Content-Type")); } ResolvableType resolvableType = ResolvableType.forClass(JsonParameter.class); // JsonParameter は username, password をフィールドに格納するだけのクラス。 // JsonAuthentication は JsonParameter を保存するだけの Authentication のサブクラス。 // どちらも定義は省略。 return this.httpMessageReaders.stream() // Content-Type: application/json のリクエストデータを JsonParameter.class に変換可能な // HttpMessageReader 実装を探す。 .filter(httpMessageReader -> httpMessageReader.canRead(resolvableType, acceptType)) .findFirst() .orElseThrow(() -> new IllegalStateException("Could not read JSON data.")) .readMono(resolvableType, request, Collections.emptyMap()) .cast(JsonParameter.class) .map(JsonAuthentication::new); } @JsonIgnoreProperties(ignoreUnknown = true) static class JsonParameter { private final String username; private final String password; @JsonCreator public JsonParameter( @JsonProperty("username") String username, @JsonProperty("password") String password) { this.username = username; this.password = password; } public String getUsername() { return this.username; } public String getPassword() { return this.password; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } JsonParameter that = (JsonParameter) o; return Objects.equals(username, that.username) && Objects.equals(password, that.password); } @Override public int hashCode() { return Objects.hash(username, password); } @Override public String toString() { return "JsonParameter(" + "username=" + this.username + ", password=" + this.password + ")"; } } }正直、この実装はかなり面倒だと思ってしまう。
非同期I/Oで何時送られれ来るか分からないリクエストボディを処理する事と、
Spring Bootでメッセージボディを処理するオブジェクトに何が使用されるか実行時まで確定しないという都合から
こうなるのは理解できる。しかし、Servert APIとは異なる独自のHTTPメッセージ処理であることを差し引いても
この手順を毎回書くのは非常に厄介に思えた。
Mono
が使用されている都合上、便利なユーティリティー関数を用意するのも少し面倒になる。などと思っていたが、ある日
ServerRequest#create(ServerWebExchange, List<HttpMessageReader<?>>)
関数が存在することに気が付いた。
この関数で生成されたServerRequest
はメッセージボディの処理に与えられたList<HttpMessageReader<?>>
の中から
適切なHttpMessageReader
を取り出して使用してくれる。メッセージボディを何に変換したいのかは
BodyExtractors
に用意されている関数からBodyExtractor
を生成するだけでよい。先ほどのコードでもコード量を大きく減らすことが出来る。
Content-Type
のチェックは念のため残している。import java.io.Serializable; import java.util.Collection; import java.util.List; import java.util.Objects; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import org.springframework.http.codec.HttpMessageReader; import org.springframework.http.codec.ServerCodecConfigurer; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.web.server.authentication.ServerAuthenticationConverter; import org.springframework.web.reactive.function.BodyExtractors; import org.springframework.web.reactive.function.server.ServerRequest; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; public class JsonLoginServerAuthenticationConverter implements ServerAuthenticationConverter { private final List<HttpMessageReader<?>> httpMessageReaders; public JsonLoginServerAuthenticationConverter(ServerCodecConfigurer serverCodecConfigurer) { this(serverCodecConfigurer.getReaders()); } public JsonLoginServerAuthenticationConverter(List<HttpMessageReader<?>> httpMessageReaders) { this.httpMessageReaders = Objects.requireNonNull(httpMessageReaders); } @Override public Mono<Authentication> convert(ServerWebExchange exchange) { ServerHttpRequest request = exchange.getRequest(); MediaType contentType = request.getHeaders().getContentType(); if (contentType == null || MediaType.APPLICATION_JSON.isCompatibleWith(contentType)) { return Mono.error(new AuthenticationException("Invalid Content-Type")); } return ServerRequest.create(exchange, this.httpMessageReaders) .body(BodyExtractors.toMono(JsonParameter.class)) .map(JsonAuthentication::new); } }
ServerRequest#create(ServerWebExchange, List<HttpMessageReader<?>>)
はWebFilter
で有用な機能なのに、
あまり紹介されていない。
- 投稿日:2020-10-11T07:12:01+09:00
javaのラムダ式[メモ書き]
java8から使用できるラムダ式を試してみました。
demojava/demo9/Demo9.javapackage demojava.demo9; interface Rtest{ public abstract String Res(Integer x,Integer y ); } public class Demo9 { public static void main(String [] args) { Rtest rtest = (x,y)->{ return "(" + x + ","+ y + ")\n\n"; }; System.out.println(rtest.Res(10,20)); } }