JavaEE 6でお手軽AjaxChat開発
バージョンアップを重ねる毎にリッチな環境が簡単に作れるようになったJavaEE。
かつては初期の開発難度や肥大化した工数によって敬遠されがちでしたが、今はそうではありません。
最新のJavaEE 6環境で簡単にWebアプリが開発できることを、私が勉強しながらお伝えします。
【0.開発概要】
ログイン画面とチャット画面をもつシンプルなアプリケーションを作成します。
今回の開発ではデータベースにはアクセスしません。
本開発では作業者は主に以下の7ファイルを作成・編集(=コーディング)します。
・ログイン画面(index.xhtml, LoginPage.java)
・チャット画面(chat.xhtml, ChatPage.java)
・その他の管理クラス(User.java, Message.java MessagePool.java, SessionFilter.java)
【1.開発環境のインストール】
NetBeansのJava開発用(ダウンロードページの左から3つ目)をインストールする
※開発環境はEclipseでもいいけど、以下の説明はNetBeans向けになっています。
【2.まずはプロジェクトの作成】
新規プロジェクト
→Java EE
→Web アプリケーション
完了
プロジェクト名に「AjaxChat」
主プロジェクトに設定にチェック
次へ
サーバー「GlassFish Server3」
Java EE バージョン「Java EE 6 Web」
コンテキストパス「/AjaxChat」※
次へ
※
http://localhost:8080/AjaxChat/がアプリケーションのトップページとなる フレームワーク「JavaServer Faces」
完了
プロジェクトを右クリック
→実行
(メニューバー、ツールバーの「主プロジェクトを実行」でもよい)
これでHallo Worldレベルですがプロジェクトが作成できました。
【3.ログイン画面の作成】
・index.html
---------------------------------------------------------------------------
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "
http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="
http://www.w3.org/1999/xhtml"
xmlns:h="
http://java.sun.com/jsf/html">
<h:head>
<title>AjaxChat ログインページ</title>
</h:head>
<h:body>
<h1>AjaxChat ログインページ</h1>
<h:form id="loginForm" prependId="false">
Room Name: <h:inputText id="roomName" value="#{loginPage.roomName}" validator="#{loginPage.validate}" />
<h:message id="roomNameError" for="roomName" style="color: red"/><br />
User Name: <h:inputText id="userName" value="#{loginPage.userName}" validator="#{loginPage.validate}" />
<h:message id="userNameError" for="userName" style="color: red"/><br />
<h:commandButton id="login" value="Login" action="#{loginPage.login}"/>
</h:form>
</h:body>
</html>
---------------------------------------------------------------------------
見慣れないタグが付けられていますが、各々は基本的にはHTMLのinputタグに変換されます。
HTML式に書くことも可能ですが、その場合は余計なタグをいくつか記述する必要が在るので、
HTMLだけを描くデザイナと仕事をするでもなければ、JSFタグを直接打った方が補完も利き手間要らずです。
・User.java
→新規
→その他
→JavaServer Faces
→JSF管理対象Bean
→クラス名「User」
パッケージ名「ajaxchat」
スコープ「session」
完了
---------------------------------------------------------------------------
@ManagedBean
@SessionScoped
public class User implements Serializable {
private String roomName;
private String userName;
public String getRoomName() {
return roomName;
}
public void setRoomName(String roomName) {
this.roomName = roomName;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public boolean isLoginNow() {
return roomName != null && !roomName.isEmpty()
&& userName != null && !userName.isEmpty();
}
}
・LoginPage.java
→新規
→JSF管理対象Bean
→クラス名「LoginPage」
パッケージ名「ajaxchat」
スコープ「view」
完了
---------------------------------------------------------------------------
@ManagedBean
@ViewScoped
public class LoginPage implements Serializable {
private String roomName;
private String userName;
@ManagedProperty("#{user}")
private User user;
public String getRoomName() {
return roomName;
}
public void setRoomName(String roomName) {
this.roomName = roomName;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
// 入力検査
public void validate(FacesContext fc, UIComponent component, Object value) {
String id = component.getId();
String inputData = (String) value;
if ("roomName".equals(id)) {
if (inputData.isEmpty()) {
throw new ValidatorException(new FacesMessage("ルーム名は必須です。"));
}
} else if ("userName".equals(id)) {
if (inputData.isEmpty()) {
throw new ValidatorException(new FacesMessage("ユーザ名は必須です。"));
}
}
}
public String login() {
user.setRoomName(roomName);
user.setUserName(userName);
//?faces-redirect=trueを入れるとブラウザ上でもページが遷移する(リダイレクトだから)
return "chat?faces-redirect=true";
}
}
・chat.html
→新規
→その他
→JavaServer Faces
→JSF ページ
→ファイル名「chat」
完了
※今回は遷移後の確認画面としてのみ利用。
ここまで出来たら実行して確認。未入力欄があると入力欄横にエラーメッセージ、
入力要件を満たすと次画面に遷移します。
値検査、画面遷移、セッション管理が出来ました。Strutsより簡単です。
【4.さっそくAjax化しよう】
・index.xhtml
---------------------------------------------------------------------------
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "
http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="
http://www.w3.org/1999/xhtml"
xmlns:h="
http://java.sun.com/jsf/html"
xmlns:f="
http://java.sun.com/jsf/core">
<h:head>
<title>AjaxChat ログインページ</title>
</h:head>
<h:body>
<h1>AjaxChat ログインページ</h1>
<h:form id="loginForm" prependId="false">
Room Name: <h:inputText id="roomName" value="#{loginPage.roomName}" validator="#{loginPage.validate}" />
<h:message id="roomNameError" for="roomName" style="color: red"/><br />
User Name: <h:inputText id="userName" value="#{loginPage.userName}" validator="#{loginPage.validate}" />
<h:message id="userNameError" for="userName" style="color: red"/><br />
<h:commandButton id="login" value="Login" action="#{loginPage.login}">
<f:ajax execute="roomName userName" render="roomNameError userNameError"/>
</h:commandButton>
</h:form>
</h:body>
</html>
>>10 ありがとうございます。私もテンプレ揃えてから始めるべきでした。
>>4と
>>9では、ここだけ変わってます。
<html xmlns="
http://www.w3.org/1999/xhtml"
xmlns:h="
http://java.sun.com/jsf/html"
xmlns:f="
http://java.sun.com/jsf/core"> ←ココ
<h:commandButton id="login" value="Login" action="#{loginPage.login}">
<f:ajax execute="roomName userName" render="roomNameError userNameError"/> ←ココ
</h:commandButton>
このバージョンでサーバーに配備・実行すると、ログインページは既にAjax化されています。
わざと未入力状態でLoginボタンを何度か押下してみてください。
エラーメッセージは帰りますがブラウザ下部(ステータスバー)のプログレスバーは反応しません。
入力要件を満たしたときのみ画面は遷移します。(Chromeでは違いがわからないかも)
f:ajax要素のexecute属性のroomName userNameはJSFのComponentIDです。
これはHTMLタグ上のidと関連付けられていて、JavaScript側でもそのIDにアクセスできます。
Ajax対応版ではこのIDを持つComponentのみ(ある種の引数として)サーバで解釈され、
その後、エラーが無ければloginPage.loginを実行、正常ならチャット画面へと遷移します。
エラーの場合、render属性に指定したroomNameError userNameErrorのふたつのComponentを再描画します。
これは裏側ではXMLHttpRequestがroomNameとuserNameのidを持つHTMLのinput要素をサーバーに送信し、
レスポンスをごにょごにょし(ぉぃ)、エラーなら*NameErrorに描画、正常なら遷移処理を実行しています。
プログラマはf:ajaxタグに必要最低限の入出力情報を記述するだけですが、これだけのことをしてくれます。
続いてチャット機能を作成します。
【5.チャット機能を作ろう】
・Massage.java
→新規
→Java クラス
→クラス名「Message」
完了
---------------------------------------------------------------------------
public class Message implements Serializable {
private String userName;
private String comment;
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getComment() {
return comment;
}
public void setComment(String comment) {
this.comment = comment;
}
}
・MessagePool.java
→JSF管理対象Bean
→クラス名「MessagePool」
パッケージ名「ajaxchat」
スコープ「application」
---------------------------------------------------------------------------
@ManagedBean
@ApplicationScoped
public class MessagePool implements Serializable {
private static final int maxMessageSize = 15;
private Map<String, Deque<Message>> pool = new ConcurrentHashMap<String, Deque<Message>>();
@SuppressWarnings("unchecked")
public Collection<Message> getMessagesBy(String roomName) {
Deque<Message> messages = getMessages(roomName);
Collection<Message> responce;
synchronized (messages) {
responce = (Collection<Message>) ((LinkedList<Message>) messages).clone();
}
return responce;
}
public void addMessage(String roomName, String userName, String comment) {
Deque<Message> messages = getMessages(roomName);
Message msg = new Message();
msg.setUserName(userName);
msg.setComment(comment);
synchronized (messages) {
if (messages.size() >= maxMessageSize) {
messages.removeLast();
}
messages.addFirst(msg);
}
}
private Deque<Message> getMessages(String roomName) {
if (roomName == null) {
throw new NullPointerException("roomName");
}
Deque<Message> messages = pool.get(roomName);
if (messages == null) {
messages = new LinkedList<Message>();
pool.put(roomName, messages);
}
return messages;
}
}
---------------------------------------------------------------------------
・ChatPage.java
→新規
→JSF管理対象Bean
→クラス名「ChatPage」
パッケージ名「ajaxchat」
スコープ「view」
完了
---------------------------------------------------------------------------
@ManagedBean
@ViewScoped
public class ChatPage implements Serializable {
@ManagedProperty("#{messagePool}")
MessagePool messagePool;
@ManagedProperty("#{user}")
User user;
private String comment;
public MessagePool getMessagePool() { return messagePool; }
public void setMessagePool(MessagePool messagePool) { this.messagePool = messagePool; }
public User getUser() { return user; }
public void setUser(User user) { this.user = user; }
public String getComment() { return comment; }
public void setComment(String comment) { this.comment = comment; }
public void validate(FacesContext fc, UIComponent component, Object value) {
String inputData = (String) value;
if (inputData.isEmpty()) {
return;
}
if (inputData.contains("うんこ")) {
throw new ValidatorException(new FacesMessage("そういうのはあかん"));
}
}
public Collection<Message> getMessages() {
return messagePool.getMessagesBy(user.getRoomName());
}
public void sayMessage() {
if (comment == null || comment.isEmpty()) {
return;
}
messagePool.addMessage(user.getRoomName(), user.getUserName(), comment);
comment = null;
}
}
・chat.html
---------------------------------------------------------------------------
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "
http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="
http://www.w3.org/1999/xhtml" xmlns:h="
http://java.sun.com/jsf/html" xmlns:f="
http://java.sun.com/jsf/core">
<h:head>
<title>AjaxChat チャットページ</title>
<script type="text/javascript">
var tid = -1;
function doSubmitComment(evt) {
try {
if (!evt) { evt = event; }
var charCode;
if (evt.charCode) { charCode = evt.charCode;
} else if (evt.keyCode) { charCode = evt.keyCode;
} else if (evt.which) { charCode = evt.which;
} else { charCode = 0; }
if (charCode == 13) {
var button = document.getElementById('submitComment');
button.click();
return false;
}
return true;
} catch (e) { return true; }
}
function poll() {
var button = document.getElementById('pollMessages');
button.click(); }
function runPollingThread() { setInterval('poll()', 5000); }
function stopPollingThread() { if (tid != -1) { clearInterval(tid); } }
</script>
</h:head>
<h:body onload="runPollingThread();" onunload="stopPollingThread();">
<h1>ようこそ #{user.userName} さん</h1>
<h:form id="chatForm" prependId="false">
Comment: <h:inputText id="comment" value="#{chatPage.comment}" validator="#{chatPage.validate}" onkeypress="return doSubmitComment(event)" />
<h:commandButton id="submitComment" value="発言" action="#{chatPage.sayMessage}">
<f:ajax execute="comment" render="comment commentError messages"/>
</h:commandButton>
<h:message id="commentError" for="comment" style="color: red"/><br />
Room: #{user.roomName}<br />
Messages:<br />
<h:dataTable id="messages" var="msg" value="#{chatPage.messages}" border="1">
<h:column>
<f:facet name="header">
<h:outputText value="User"/>
</f:facet>
<h:outputText value="#{msg.userName}"/>
</h:column>
<h:column>
<f:facet name="header">
<h:outputText value="Message"/>
</f:facet>
<h:outputText value="#{msg.comment}"/>
</h:column>
</h:dataTable>
<h:commandButton id="pollMessages" style="visibility:hidden">
<f:ajax render="messages"/>
</h:commandButton>
</h:form>
</h:body>
</html>
無理矢理詰め込んだからソースが汚くなりました・・・
これで昔ながらのpolling式のチャットは完成です。連続投稿規制のため詳細は省略。
以上のサンプルから、JSFは独自に仕込んだJavaScriptとも相性が良いようです。
また<h:commandButton id="pollMessages">のような隠しコマンドは
邪道なやり方ですが、JavaScriptとも連携しやすく便利そうです。
(一応jsf.jsというライブラリを使うことでJavaScriptだけでコマンドは実行できるようです。)
次回はJavaEE6の目玉?であるComet(long polling, AsyncServlet)に対応したいと思います。
ネットでぱっと見た限りではJSFの正規機能としてCometが使えるかは分かりませんでした。
そこでサーバの特定のステータス変化を通知する専用のServletをCometで作り、
そこにXMLHttpRequestでアクセス、通知が返るとchat.htmlのJS関数poll()を呼び出す仕様とします。
上手くできるといいのですが。
jQueryの$.postとJSFのイベントの制御が上手くできません。
自身のチャットコメント送信後にLong Pollingをする以下のJS関数doWaitMessageが再送されないようです。
JavaEE6は使いやすいのですが、Ajaxの制御は難しいですね。。。
function doWaitMessage() {
var url = '#{facesContext.externalContext.request.contextPath}/observe'
var data = {observeId:'ajaxchat.MessagePool.received', roomName:'#{user.roomName}'}
$.post(url, data, doWaitMessageCB, 'html');
}
function doWaitMessageCB(data, status) {
if (status == 'success') {
alert('start refresh')
//$('#refresh').click();
jsf.ajax.request('refresh', null, {render:'messages'});
alert('end refresh')
} else {
alert('status: ' + status);
}
alert('start clearTimeout')
clearTimeout(tid);
alert('end clearTimeout & start setTimeout')
tid = setTimeout('doWaitMessage()', 0);
alert('end setTimeout')
}
@WebServlet(name = "ObserveServlet", urlPatterns = {"/observe"}, asyncSupported = true)
public class ObserveServlet extends HttpServlet {
@Inject
private MessagePool messagePool;
private ScheduledThreadPoolExecutor executor;
@Override
public void init() throws ServletException {
super.init();
executor = new ScheduledThreadPoolExecutor(50);
}
@Override
public void destroy() {
super.destroy();
executor.shutdownNow();
}
protected void processRequest(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
Map<String, String[]> parameterMap = request.getParameterMap();
String observeId = (String) request.getParameter("observeId");
if (observeId != null) {
if (observeId.equals("ajaxchat.MessagePool.received")) {
AsyncContext ac = request.startAsync(request, response);
executor.execute(new AsyncObserveService(ac, messagePool));
} else {
response.sendError(HttpServletResponse.SC_BAD_REQUEST);
}
} else {
response.sendError(HttpServletResponse.SC_BAD_REQUEST);
}
}
private class AsyncObserveService implements Runnable {
private AsyncContext ac;
private MessagePool messagePool;
private volatile boolean wating;
private Date lastMessageTime;
public AsyncObserveService(AsyncContext ac, MessagePool messagePool) {
this.ac = ac;
this.messagePool = messagePool;}
@Override
public void run() {
long oid = System.currentTimeMillis();
System.out.println("Observe start: id=" + oid);
HttpServletRequest req = (HttpServletRequest) ac.getRequest();
HttpServletResponse res = (HttpServletResponse) ac.getResponse();
String roomName = req.getParameter("roomName");
res.setContentType("text/plain; charset=UTF-8");
try {
synchronized (this) {
messagePool.addListener(roomName, new ReceiveListener(this));
this.wating = true;
while (this.wating) { this.wait(); } }
PrintWriter out = res.getWriter();
out.print(lastMessageTime.toString());
out.close();
} catch (Exception ex) {
Logger.getLogger(ObserveServlet.class.getName()).log(Level.SEVERE, null, ex);
throw new RuntimeException(ex);
} finally {
ac.complete();
System.out.println("Observe end: id=" + oid);
} } }
private class ReceiveListener implements MessageReceiveListener {
private AsyncObserveService observer;
public ReceiveListener(AsyncObserveService observer) {
this.observer = observer;
}
@Override
public void received(String roomName, Message msg, long timestamp) {
try {
synchronized (observer) {
observer.lastMessageTime = new Date(timestamp);
observer.wating = false;
observer.notifyAll();
}
} catch (Exception ex) {
Logger.getLogger(ObserveServlet.class.getName()).log(Level.SEVERE, null, ex);
}
}
}
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException,
IOException {
processRequest(request, response);
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException,
IOException {
processRequest(request, response);
}
}
とまあこんな感じでJavaEE6の紹介を終わります。(失敗してんじゃねーか)
今後はソースコード投稿サイト探して連投せずにコードを晒していきたいと思います。
ということで、皆さんでもっとJavaEEを活用していきましょう・・・orz
これがjsfか
エンタープライズおじゃばじゃば
値段高いからあんまり実績無いね。
javaのネットワーク関係の質問って、ここではスレ違いですよね。。。
JSF2.0は結局サンプルからして冗長で簡単とは言えないな。
Springからは人が流れてこないし、LLからくる人はplayにいくだろう。
エスパー様向け質問になってしまい申し訳ありません。
10端末から無限ループでHTTPリクエストを間髪入れず投げまくって負荷テストをしています。
apache+tomcat6+mysqlで単純なWebアプリ(Struts,Spring使用)なのですが、
freeでメモリを監視しているとusedが徐々に徐々に増えていき、
累計数万アクセスに達した時点でヒープ領域がいっぱいになってしまいます。
これはアプリのどこかでメモリリークしてしまっているということでしょうか?
それとも、激安VPSのか細いリソースのサーバでこんな負荷テストをすれば、
GCが間に合わずにこうなってしまうものでしょうか?
※DBまわりはMySQLのThreads_*を監視しているかんじだと問題なさそうです。
※ActionやDAO等はすべてSingletonです。
以上、情報が少なすぎて申し訳ありませんが、よろしくお願いします。
HttpSession使ってる?使ってた場合、無効化してる?
>>33 Sessionは仕様上必須なので使っています。
…あ、なるほど!!
curlコマンドを使用してトップ画面のurlを叩きまくっているのですが、
cookieもurlパラメータも指定していないので、
つまりアクセス毎にセッションが生成されてい…る…?
ありがとうございます、確認してみます!
wget --save-cookie $COOKIE $URL
でCookieを事前に取得してから無限ループのcurlコマンドで
curl --cookie $COOKIE $URL
とするようテストスクリプトを修正したら解決できました!
ありがとうございました!
エスパーは実在した!!!
>>33
Sessionだけで大きな問題になったってことは
Session設計がかなりでかいんじゃないの?
WEBアプリの日次集計バッチを実装しています。
SpringBatchとかiBATISとか未経験ですので別の方法で手っ取り早く済まそうとしているのですが、
WEBアプリ自体に処理を載せてバッチ用アクションURLを叩いた契機で実行、
httpリクエストをcronから飛ばす、
という横着なやり方はやはり一般的ではないですかね?
というかいくらお手軽実装優先とはいえ、こんなやり方を提案したら怪訝な目でみられちゃいますか?
いいんじゃないかな
セキュリティとか気にしていれば、別にかまわんと思うよ。
Webアプリが外部に公開されていた場合、
URLさえ分かれば誰でもバッチ処理を起動できてしまうので、
その辺は注意が必要な気がする。
struts(1.3)のFormFileが必須項目じゃなくて任意入力項目について知恵をおくれー!!!
意図しないエラーは極力メッセージ出したいからFileNotFoundExceptionとIOExceptionはエラーメッセージ表示したいからgetFileData()をtry〜catchで括ってるんだけど、
それ以前にそもそもユーザが何か入力したのかどうかの判定はgetFileName()が空文字か1文字以上あるかで判定するしかないの?
サイズだと0byteのファイルもありえることを考慮すると駄目そうな気がする
うむ、だいぶ日本語が崩壊しておる
それしかないよ
43 :
nobodyさん:2012/12/16(日) 00:02:04.31 ID:VsYzucID
Tomcatでいいじゃない
web の画面から日時を指定して、その時間にデータベースのバックアップをしたいんだけどどうするとよい?
sastruts + mysql。
一回だけバックアップして終了。バックアップ予定の取り消しあり。
java から cron つくるとよいのか、java から mysql の create event するのがよいのか。
cron から java を呼ぶとかなら情報でてくるんだけど。
java でサーバを作るのは大袈裟なきがするし。
やったことある人、どうやりましたか?
45 :
nobodyさん: