Javaでフレームワークを使わない場合のAjaxの使い方の情報が少なかったので、苦労しましたが、トライしてみました。
Ajax(Asynchronous JavaScript + XML)とは?
GoogleMapで利用されたことで一躍有名になったようです。非同期通信が実装できます。Ajaxは、XMLHttpRequest (XHR)が組み込みオブジェクトとして利用することで可能のようです。
同期通信と非同期通信
通常のWebサイトは同期通信が基本のようです。これは、例えば、Httpというプロトコルを使っている場合、ブラウザとサーバー間では「HttpリクエストとHttpレスポンス」のセットでやり取りが行われています。
その際、同期通信では、ブラウザからサーバー側にHttpリクエストを送り、ブラウザ側はサーバー側からHttpレスポンスが返るまで他の処理が行えないようです。
同期通信の場合
非同期通信の場合
非同期通信は、サーバー側に何かリクエストを送ってサーバー側で処理をしている間、ブラウザ側でも他の処理を進めていけるということが可能のようです。
『GoogleMap』が、位置座標などをサーバー側にリクエストしサーバー側が座標計算などを行っている間、ブラウザの表示も更新していけるのは、このような非同期通信の仕組みを利用しているためのようです。
⇩ 『Googleサジェスト』というものにもAjaxが利用されているようです。
・ゼロから始めるJavaScript講座Vol23 Ajaxとは | Tech2GO
同一生成元ポリシー(Same Origin Policy)
「スキーム、ホスト、ポート」の組み合わせをオリジンと定め、それらが同じものは同一のオリジンとして同じ保護範囲のリソースとして取り扱うということらしいです。
・Same-Origin Policy とは何なのか。 - 葉っぱ日記
クロスオリジン
2つのオリジンが同一でない場合、すなわち異なるオリジンを「クロスオリジン」と言います。クロスオリジンでのリソースでアクセスする方法については、Cross-Origin Resource Sharing(CORS)がルールとして定められています。
セキュリティ上の理由からブラウザは、スクリプトによって開始されるクロスオリジン HTTP リクエストを制限します。
要するに、なんかスクリプト(JavaScriptのようなもの)を使って他のURLとの通信(例えば、「http://hoge.com」から「http://fuga.com」のような異なるサイト間)は原則NGだよってことですかね。
jQueryのAjaxで、クロスドメインAjaxのための準備
今回、jspとJavaでAjaxを行ったのですが、もし、他のサイトととの通信を行うことが出てくることを考慮し、クロスオリジンでのAjax(クロスドメインAjaxという?)を実装してみたいと思います。
jQueryを使う場合は、jQuery本体の読み込みが必要です。
- CDNを利用する
- ファイルをインストール
の2つの方法があります。
CDNの場合は、Googleなどのサーバーにアップされているものを利用することができますが、インターネットにつながっていないと利用できません。
ファイルをインストールする場合は、インターネットにつながっていなくても利用はできますが、どちらにせよ、公開されているサイト同士で通信するにはインターネットにつながっている必要があります。
今回は、jQueryをインストールして利用したいと思います。
jQuery 3系のインストール
2017年8月24日(木)現在、jQueryのバージョンは3系まで出てるようです。jQueryは大規模な開発には向いていないので使うことがためらわれますが、 今回は、jQueryをインストールして利用します。
https://jquery.com/ にアクセスします。『Download jQuery』をクリック。
『Download the uncompressed, development jQuery 3.2.1』をクリックでインストールされます。
Eclipseで動的Webプロジェクト
Eclipse で、「パースペクティブ」が「Java EE」になっているのを確認し、「ファイル(N)」>「新規(N)」>「動的 Web プロジェクト」を選択。
「プロジェクト名(M):」を入力し、「次へ(N)>」をクリック。
「次へ(N)>」をクリック。
web.xmlを使う場合は、「web.xml デプロイメント記述子の生成(G)」にチェックを入れますが、今回はチェックなし。
プロジェクトができました。
WebContentフォルダの中にフォルダを作りインストールしたjQueryを配置
「プロジェクト・エクスプローラー」で作成されたプロジェクトの中の「WebContent」を選択し右クリックし、「新規(N)」>「フォルダ」を選択。
「フォルダー名(N):」を入力し、「完了(F)」。
作成されたフォルダにインストールしておいたjQueryのファイルを配置します。ドラッグ&ドロップで、作成したフォルダにインストールしておいたjQueryファイルを配置。
配置できました。
Ajaxの処理を記述する用のjsファイルを作成しておきます。作成したフォルダを選択した状態で右クリックし、「新規(N)」>「ファイル」を選択。
「ファイル名(M):」を入力し(拡張子は「js」にします。)、「完了(F)」をクリック。
jsファイルが準備できました。
jspファイルとServletファイルの準備
続いて、jspファイルとServletファイルを用意します。 まずはjspファイルから。今回はフォルダにjspファイルを入れるようにしてます。「WebContent」を選択した状態で右クリックし、「新規(N)」>「フォルダー」を選択。
「フォルダ名(N):」を入力し、「完了(F)」をクリック。
続いてjspファイルを作成。
「ファイル名(M):」を入力し、「次へ(N)」をクリック。
「完了(F)」をクリック。
次に、Servletファイルを作成します。「Javaリソース」>「src」を選択した状態で右クリックし、「新規(N)」>「サーブレット」を選択。
「Java パッケージ(K):」、「クラス名(M):」を入力し、「次へ(N)>」をクリック。
「次へ(N)>」をクリック。
「完了(F)」をクリック。
ファイルが準備できました。
「src」フォルダを選択した状態で、「新規(N)」>「クラス」を選択で、
「パッケージ(K):」、「名前(M):」を入力し、「インターフェイス(I):」を「追加(A)...」。
「Serializable - java.io」を選択し、「OK」をクリック。「一致する項目(M):」に表示されてない場合は、「インターフェイスを選択してください(C):」で「Seriali」とかまで入力すれば表示されると思います。
追加されたのを確認し、「完了(F)」をクリック。
続いて、DAOクラスを作成。
「パッケージ(K):」、「名前(M):」を入力し、「完了(F)」。
DAOパッケージにはもう一つクラス(データベース接続)を追加します。
それと、「feature」というパッケージに、Pager.javaとSafePassword.javaを作成しておきます。後でファイルを編集していきます。
「プロジェクト・エクスプローラー」にこんな感じで追加されていればひとまずOK。
並びがおかしくなったときは、をクリックし、「プロジェクト表示(R)」>「フラット」か「階層」どちらかをクリック。
Gsonなどの外部jarファイルの追加
今回、jsonを使っていく際に、Java SE標準のライブラリでも利用は可能らしいのですが、Gsonという外部ライブラリを利用することにします。
他にもMySQLと、jstlを利用したいので、「WebContent」>「WEB-INF」>「lib」 にそれらのjarファイルも配置します。
- Gson
- Genson
- Jackson
などいろいろな外部ライブラリがあるようです。
Gsonをインストールします。https://github.com/google/gson にアクセスします。
下の方にスクロールします。「Gson Download and Maven」というところの「Gson Download」のリンクをクリックします。
真ん中あたりの「com.google.code.gson : gson : 2.8.1」というリンクをクリック。
「gson-2.8.1.jar」をクリックするとインストールされます。
jstlのインストールについては、こちらを参考。
・ Java Eclipseで動的 Webプロジェクト ユーザー検索 JSTLも導入してます
MySQLのドライバーは、MySQLをインストールする際に一緒にインストールしていれば、『C:¥Program Files (x86)¥MySQL¥Connector.J 5.1¥mysql-connector-java-5.1.41-bin.jar』にあると思われます。
それでは、jarファイルを配置。
MySQLにデータベースとテーブルを用意
今回は、前回作成していたdaiaryデータベースのpostテーブルを利用します。
⇩ こちらを参考ください
・Windows 10 HomeにてMySQLのテーブルにcsvファイルでデータをインポート
ファイルの編集
ajaxtest.js
$(function() { $('.ajax-button a').on('click', function(event){ var url = "http://localhost:8080/AjaxTest/AjaxTestServlet?ACTION="; var action = $(this).data('action'); var page = $(this).data('page'); var data = {page: page}; url += action; // aリンクによる遷移をキャンセル event.preventDefault(); // ajaxスタート ajaxTest(url, data) .done(function(result) { console.log("ajax success!"); console.log(result); // 取得した記事に書き換え for(var index = 0; index < result.length; index++) { $('.date').eq(index).html(result[index].postDate); $('.title').eq(index).html(result[index].postTitle); $('.content').eq(index).html(result[index].postContent); } }) .fail(function(XHLHttpRequest, testStatus, errorThrown) { console.log("ajax失敗!"); }); }); // ajax用 function ajaxTest(url, data) { return $.ajax({ url: url, dataType: "jsonp", type: "POST", data: data, xhrFields: { withCredentials: true }, beforeSend: function(xhr) { xhr.setRequestHeader('X-CSRF-Token', $('meta[name=csrf-token]').attr('content')); xhr.withCredentials = true; } }); } });
ajaxTest.jsp
<%@page import="feature.SafePassword"%> <%@page import="java.security.NoSuchAlgorithmException"%> <%@page import="java.security.SecureRandom"%> <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <% SecureRandom secRandom = null; byte bytes[] = new byte[16]; try { secRandom = SecureRandom.getInstance("SHA1PRNG"); secRandom.nextBytes(bytes); } catch(NoSuchAlgorithmException e) { } String secure = String.valueOf(secRandom.nextDouble()); String csrf_token = SafePassword.getStretchedPassword(secure + secure, secure); %> <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <meta name="csrf-token" content="<%= csrf_token %>"> <title>Ajax Test</title> <script src="./js/jquery-3.2.1.js"></script> <script src="./js/ajaxtest.js"></script> </head> <body> <div id="content"> <div class="ajax"> <div class="posts"> <c:forEach var="post" items="${posts}"> <div class="post"> <div class="post-head"> <div class="date">${post.postDate}</div> <div class="title">${post.postTitle}</div> </div><!-- .post-head --> <div class="post-body"> <div class="content">${post.postContent}</div> </div><!-- .post-body --> </div><!-- .post --> </c:forEach> </div><!-- .posts --> </div> <div class="ajax-button"> <c:forEach var="page" begin="1" end="${pager}" step="1"> <a href="AjaxTest/AjaxTestServlet" data-action="pager" data-page="${page}"><c:out value="${page}"></c:out></a> </c:forEach> </div> </div><!-- #content --> </body> </html>
Post.java
package model; import java.io.Serializable; import java.sql.Date; public class Post implements Serializable { /** * フィールド */ private int postId; private String postTitle; private String postDescription; private String postContent; private Date postDate; public Post() {} public Post(int postId, String postTitle, String postDescription, String postContent, Date postDate) { this.postId = postId; this.postTitle = postTitle; this.postDescription = postDescription; this.postContent = postContent; this.postDate = postDate; } public int getPostId() { return postId; } public void setPostId(int postId) { this.postId = postId; } public String getPostTitle() { return postTitle; } public void setPostTitle(String postTitle) { this.postTitle = postTitle; } public String getPostDescription() { return postDescription; } public void setPostDescription(String postDescription) { this.postDescription = postDescription; } public String getPostContent() { return postContent; } public void setPostContent(String postContent) { this.postContent = postContent; } public Date getPostDate() { return postDate; } public void setPostDate(Date postDate) { this.postDate = postDate; } }
PostDAO.java
package dao; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.Statement; import java.util.ArrayList; import java.util.List; import model.Post; public class PostDAO { /** * 全件の記事数取得 * @return Listposts */ public int selectAllPost() { ConnectionManager cm = ConnectionManager.getInstance(); String sql = "SELECT COUNT(*) FROM post"; int count = 0; try(Connection conn = cm.getConnection(); Statement stmt = conn.createStatement()) { ResultSet res = stmt.executeQuery(sql); if(res.next()) { count = res.getInt(1); } } catch(Exception e) { System.out.println("PostDAO.javaの[selectAllPost()]でエラー" + e); } return count; } /** * 1ページ分の記事取得 */ public List selectPost(int perPage, int pageNumber) { ConnectionManager cm = ConnectionManager.getInstance(); String sql = "SELECT * FROM post LIMIT ? OFFSET ?"; List posts = new ArrayList (); Post post = null; try(Connection conn = cm.getConnection(); PreparedStatement pstmt = conn.prepareStatement(sql)) { pstmt.setInt(1, perPage); pstmt.setInt(2, pageNumber); ResultSet res = pstmt.executeQuery(); while(res.next()) { post = new Post( res.getInt("post_id"), res.getString("post_title"), res.getString("post_description"), res.getString("post_content"), res.getDate("post_date") ); posts.add(post); } } catch(Exception e) { System.out.println("PostDAO.javaの[selectPost()]でエラー" + e); } return posts; } }
ConnectionManager.java
package dao; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class ConnectionManager { /** * フィールド変数 */ // データベース接続情報 public final static String URL = "jdbc:mysql://localhost:3306/diary?useSSL=false"; public final static String USER = "owner"; public final static String PASSWORD = "owner1"; // コネクションオブジェクト private Connection connection = null; // このクラスに唯一のインスタンス private static ConnectionManager instance = new ConnectionManager(); /** * static初期化子 */ static { // JDBCドライバのロード String drv = "com.mysql.jdbc.Driver"; try { Class.forName(drv); } catch(ClassNotFoundException e) { System.out.println("ドライバがありません" + e.getMessage()); } } /** * コンストラクタ */ private ConnectionManager() { } /** * インスタンス取得メソッド */ public static ConnectionManager getInstance() { return instance; } /** * DB接続 */ public synchronized Connection getConnection() { // コネクションの確立 try { connection = DriverManager.getConnection(URL, USER, PASSWORD); } catch(SQLException e) { connection = null; System.out.println("ConnectionManager.javaの[getConnection()]でエラー " + e); } return connection; } /** * データベースの切断 */ public void closeConnection() { try { if(connection != null) { connection.close(); } } catch(SQLException e) { System.out.println("ConnectionManager.javaの[closeConnection()]でエラー " + e); } finally { connection = null; } } }
AjaxTestServlet.java
package servlet; import java.io.IOException; import java.io.PrintWriter; import java.util.List; import javax.servlet.RequestDispatcher; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import dao.PostDAO; import feature.Pager; import model.Post; /** * Servlet implementation class AjaxTestServlet */ @WebServlet("/AjaxTestServlet") public class AjaxTestServlet extends HttpServlet { private static final long serialVersionUID = 1L; PostDAO postDao = new PostDAO(); Pager pager = new Pager(); /** * @see HttpServlet#HttpServlet() */ public AjaxTestServlet() { super(); // TODO Auto-generated constructor stub } /** * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response) */ protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // String action = request.getParameter("ACTION"); String url = null; Listposts = null; HttpSession session = request.getSession(); int maxPageCount = postDao.selectAllPost(); int perPage = 10; pager.initPagination(maxPageCount, perPage); // 初期画面表示 if(action == null) { posts = postDao.selectPost(perPage, perPage * (pager.getCurrent() -1)); session.setAttribute("posts", posts); session.setAttribute("pager", maxPageCount / perPage); session.setAttribute("currentPage", pager.getCurrent()); url = "jsp/ajaxTest.jsp"; System.out.println(posts); } RequestDispatcher dispatcher = request.getRequestDispatcher(url); dispatcher.forward(request, response); } /** * @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response) */ protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // response.setContentType("text/javascript; charset=utf-8"); // 「credentials flag」を有効にした場合、「Access-Control-Allow-Origin」ヘッダーにはワイルドカード「*」は使用できない。 response.setHeader("Access-Control-Allow-Origin", "http://127.0.0.1:8080"); // 「credentials flag」を有効 response.addHeader("Access-Control-Allow-Credentials", "true"); // 許可するメソッド response.addHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE, HEAD"); response.addHeader("Access-Control-Allow-Headers", "X-PINGOTHER, Origin, X-Requested-With, Content-Type, Accept"); String action = request.getParameter("ACTION"); int page = 0; HttpSession session = request.getSession(); List posts = (List )session.getAttribute("posts"); String callback = request.getParameter("callback"); if(action == null) { } else { // Gson json = new Gson(); // Date型メンバの日付のフォーマットを指定できる Gson json = new GsonBuilder().setDateFormat("yyyy-MM-dd").create(); PrintWriter out = response.getWriter(); if(action.equals("pager")) { // 今現在選択されたページ番号 if(request.getParameter("page") != null) { page = Integer.parseInt(request.getParameter("page")); } System.out.println(page); posts = postDao.selectPost(pager.getPerPage(), (pager.getPerPage() * (page -1))); session.setAttribute("posts", posts); out.println(callback + "(" + json.toJson(posts) + ")"); } } } }
Pager.java
package feature; import java.io.Serializable; public class Pager implements Serializable { /** * */ private int allData; // データ全件 private int perPage; // ページ当たりの表示件数 private int maxPage; // 総ページ数 private int current; // 現在のページ /** * すべてのフィールドの初期化 * @param allData データの総件数 * @param perPage 1ページ当たりの表示件数 */ public void initPagination(int allData, int perPage) { this.allData = allData; this.perPage = perPage; this.maxPage = allData / perPage + (allData % perPage == 0 ? 0 : 1); this.current = 1; } /** * 表示するデータの先頭位置 * @return データの位置を示すint型の値 */ public int findTopData() { return (current -1) * perPage; } /** * 次ページを表示 */ public void moveNext() { if(maxPage > current) { ++current; } } /** * 前ページを表示 */ public void movePrev() { if(current > 1) { --current; } } /** * 末尾のページを表示 */ public void moveLast() { current = maxPage; } /** * 先頭のページを表示 */ public void moveTop() { current = 1; } /** * 表示するデータの範囲を返す */ public int[] procRange() { int[] n = new int[2]; n[0] = findTopData(); n[1] = n[0] + perPage -1; return n; } /** * DBなどから取得した総件数を取得 * @return */ public int getAllData() { return allData; } /** * 1ページ当たりの表示件数の取得 * @return */ public int getPerPage() { return perPage; } /** * 最大ページ数を取得 * @return */ public int getMaxPage() { return maxPage; } /** * 今現在のページを取得 * @return */ public int getCurrent() { return current; } }
SafePassword.java
package feature; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; public class SafePassword { private static int STRETCH_COUNT = 1000; /** * salt + ハッシュ化したパスワード */ public static String getSalttedPassword(String password, String userId) { String salt = getSha256(userId); return getSha256(salt + userId); } /** * salt + ストレッチングしたパスワード */ public static String getStretchedPassword(String password, String userId) { String salt = getSha256(userId); String hash = ""; for(int i = 0; i < STRETCH_COUNT; i++) { hash = getSha256(hash + salt + password); } return hash; } /** * 文字列から、SHA256のハッシュ値を取得 */ private static String getSha256(String target) { MessageDigest md = null; StringBuffer buf = new StringBuffer(); String algorithm = "SHA-256"; try { md = MessageDigest.getInstance(algorithm); md.update(target.getBytes()); byte[] digest = md.digest(); for(int i = 0; i < digest.length; i++) { buf.append(String.format("%02x", digest[i])); } } catch(NoSuchAlgorithmException e) { } return buf.toString(); } }
サーバー起動で確認してみます。「プロジェクト・エクスプローラー」の「servlet」>「AjaxTestServlet.java」を選択した状態で右クリックし、「実行(R)」>「サーバーで実行」を選択。
「http://localhost:8080/AjaxTest/AjaxTestServlet」にアクセスすると表示されました。
動的なページングにはなっていませんが、ページングを実装できました。
セッションを有効にしつつAjaxするには、Servlet側で、responseのHeaderにいろいろ追加するのと、HTML部分のmetaタグで、「csrf-token」を追加しておく必要があるようです。あとは、$.ajaxの中で、「csrf-token」を利用するような記述も必要になるようです。
それにしても、Javaはページングの実装参考が全然見当たらないので改良するのが厳しそうですね。MySQL のOFFSETを使う方法も宜しくないようです。
課題は山積みですね...。
今回はこのへんで。