JSF2.0のSystem Eventsを使うとLifeCycleがリアルに確認できていいですね。カスタムコンポーネントと組み合わせて、いろいろ覗いてみました。
使用するサンプル
「Core JavaServer Faces(Third Edition)」にあったUISpinnerをカスタムコンポーネントとして使います。画面キャプチャを貼るのが面倒なので、Lynxの画面で説明します。
[Down][Up]ボタンで数値を増減して、[Submit]すると結果が表示されます。
What is your number? 3___________________ Down Up Submit
Your number: 3 Return
ソース
・カスタムタグの指定
web.xml
<?xml version="1.0"?> <web-app version="3.0" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"> ・・・ <context-param> <param-name>javax.faces.FACELETS_LIBRARIES</param-name> <param-value>/WEB-INF/sandbox.taglib.xml</param-value> </context-param> </web-app>
sandbox.taglib.xml
<?xml version="1.0" encoding="UTF-8"?> <facelet-taglib version="2.0" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-facelettaglibary_2_0.xsd"> <namespace>http://sandbox.com</namespace> <tag> <tag-name>spinner</tag-name> <component> <component-type>sandbox.Spinner</component-type> </component> </tag> </facelet-taglib>
・Facelets
index.xhtml
<!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" xmlns:ui="http://java.sun.com/jsf/facelets" xmlns:sandbox="http://sandbox.com"> <h:head> <title>HOGE</title> </h:head> <h:body> What is your number?<br/> <h:form> <sandbox:spinner value="#{myBean.value}" id="mySpinner" /> <br/> <h:commandButton value="Submit" action="output" /> </h:form> <h:messages /> </h:body> </html>
output.xhtml
<!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" xmlns:ui="http://java.sun.com/jsf/facelets" xmlns:sandbox="http://sandbox.com"> <h:head> <title>HOGE</title> </h:head> <h:body> <h:form> Your number: <h:outputText value="#{myBean.value}" /> <br /> <h:commandButton value="Return" action="index" /> </h:form> </h:body> </html>
・カスタムコンポーネント
sandbox/UISpinner.java
package sandbox; import java.io.IOException; import java.util.Map; import javax.faces.component.FacesComponent; import javax.faces.component.UIInput; import javax.faces.context.FacesContext; import javax.faces.context.ResponseWriter; import javax.faces.convert.IntegerConverter; @FacesComponent("sandbox.Spinner") public class UISpinner extends UIInput { private static final String MORE = ".more"; private static final String LESS = ".less"; public UISpinner() { System.out.println("Constructing UISpinner" + this); setConverter(new IntegerConverter()); setRendererType(null); } @Override public void encodeBegin(FacesContext context) throws IOException { System.out.println("Encoding UISpinner " + this); ResponseWriter writer = context.getResponseWriter(); String clientId = getClientId(context); encodeInputField(writer, clientId); encodeDecrementButton(writer, clientId); encodeIncrementButton(writer, clientId); } @Override public void decode(FacesContext context) { System.out.println("Decoding UISpinner" + this); Map<String, String> requestMap = context.getExternalContext().getRequestParameterMap(); String clientId = getClientId(context); int increment; if (requestMap.containsKey(clientId + MORE)) increment = 1; else if (requestMap.containsKey(clientId + LESS)) increment = -1; else increment = 0; try { int submittedValue = Integer.valueOf((String) requestMap.get(clientId)); int newValue = submittedValue + increment; setSubmittedValue("" + newValue); } catch (NumberFormatException ex) { setSubmittedValue((String) requestMap.get(clientId)); } } // private private void encodeInputField(ResponseWriter writer, String clientId) throws IOException { writer.startElement("input", this); writer.writeAttribute("name", clientId, null); Object v = getValue(); if (v != null) writer.writeAttribute("value", v, "value"); writer.endElement("input"); } private void encodeIncrementButton(ResponseWriter writer, String clientId) throws IOException { writer.startElement("input", this); writer.writeAttribute("type", "submit", null); writer.writeAttribute("name", clientId + MORE, null); writer.writeAttribute("value", "Up", "value"); writer.endElement("input"); } private void encodeDecrementButton(ResponseWriter writer, String clientId) throws IOException { writer.startElement("input", this); writer.writeAttribute("type", "submit", null); writer.writeAttribute("name", clientId + LESS, null); writer.writeAttribute("value", "Down", "value"); writer.endElement("input"); } }
コンストラクタ、encodeBegin、decodeのタイミングでログを吐くようにしてあります。Backing Beanのソースは省略。
・Event Listenerの仕込み
faces-config.xml
<?xml version="1.0" encoding="UTF-8"?> <faces-config xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-facesconfig_2_0.xsd" version="2.0"> <lifecycle> <phase-listener>lib.PhaseTracker</phase-listener> </lifecycle> <application> <system-event-listener> <system-event-listener-class>lib.ViewEventListener</system-event-listener-class> <system-event-class>javax.faces.event.PostAddToViewEvent</system-event-class> </system-event-listener> <system-event-listener> <system-event-listener-class>lib.ViewEventListener</system-event-listener-class> <system-event-class>javax.faces.event.PreRemoveFromViewEvent</system-event-class> </system-event-listener> </application> </faces-config>
PhaseEventに加えて、PostAddToViewEvent、PreRemoveFromViewEventのリスナーを追加しています。リスナーの実装は次の通り。
lib/PhaseTracker.java
package lib; import javax.faces.event.PhaseEvent; import javax.faces.event.PhaseId; import javax.faces.event.PhaseListener; public class PhaseTracker implements PhaseListener { private static final long serialVersionUID = -2917841296997807166L; @Override public PhaseId getPhaseId() { return PhaseId.ANY_PHASE; } @Override public void beforePhase(PhaseEvent event) { System.out.println( "--> Before: " + event.getPhaseId().toString()); } @Override public void afterPhase(PhaseEvent event) { System.out.println("<-- After: " + event.getPhaseId().toString()); } }
ViewEventListener.java
package lib; import javax.faces.event.AbortProcessingException; import javax.faces.event.SystemEvent; import javax.faces.event.SystemEventListener; public class ViewEventListener implements SystemEventListener { @Override public boolean isListenerForSource(Object obj) { if (obj instanceof sandbox.UISpinner) return true; return false; } @Override public void processEvent(SystemEvent event) throws AbortProcessingException { String sourceClass = event.getSource().getClass().getCanonicalName(); String eventName = event.toString(); System.out.println(sourceClass + ": " + eventName + " is Called"); } }
全てのPhaseのBefore/AfterとUISpinnerのAdd/Remove Viewについてログを吐きます。
実行結果
・初回画面表示
10:37:00,902 INFO [stdout] (http--127.0.0.1-8080-1) --> Before: RESTORE_VIEW 1 10:37:00,939 INFO [stdout] (http--127.0.0.1-8080-1) <-- After: RESTORE_VIEW 1 10:37:00,940 INFO [stdout] (http--127.0.0.1-8080-1) --> Before: RENDER_RESPONSE 6 10:37:00,989 INFO [stdout] (http--127.0.0.1-8080-1) Constructing UISpinnersandbox.UISpinner@68b57582 10:37:01,032 INFO [stdout] (http--127.0.0.1-8080-1) sandbox.UISpinner: javax.faces.event.PostAddToViewEvent[source=sandbox.UISpinner@68b57582] is Called 10:37:01,046 INFO [stdout] (http--127.0.0.1-8080-1) Encoding UISpinner sandbox.UISpinner@68b57582 10:37:01,066 INFO [stdout] (http--127.0.0.1-8080-1) <-- After: RENDER_RESPONSE 6
RENDER_RESPONSEフェーズで、UISpinnerオブジェクトが生成されて、対応するHTMLが吐き出されています。
・[Up]ボタン実行
:38:29,891 INFO [stdout] (http--127.0.0.1-8080-1) --> Before: RESTORE_VIEW 1 10:38:29,893 INFO [stdout] (http--127.0.0.1-8080-1) Constructing UISpinnersandbox.UISpinner@2b3a9ca5 10:38:29,894 INFO [stdout] (http--127.0.0.1-8080-1) sandbox.UISpinner: javax.faces.event.PostAddToViewEvent[source=sandbox.UISpinner@2b3a9ca5] is Called 10:38:29,895 INFO [stdout] (http--127.0.0.1-8080-1) <-- After: RESTORE_VIEW 1 10:38:29,895 INFO [stdout] (http--127.0.0.1-8080-1) --> Before: APPLY_REQUEST_VALUES 2 10:38:29,896 INFO [stdout] (http--127.0.0.1-8080-1) Decoding UISpinnersandbox.UISpinner@2b3a9ca5 10:38:29,896 INFO [stdout] (http--127.0.0.1-8080-1) <-- After: APPLY_REQUEST_VALUES 2 10:38:29,896 INFO [stdout] (http--127.0.0.1-8080-1) --> Before: PROCESS_VALIDATIONS 3 10:38:29,915 INFO [stdout] (http--127.0.0.1-8080-1) <-- After: PROCESS_VALIDATIONS 3 10:38:29,915 INFO [stdout] (http--127.0.0.1-8080-1) --> Before: UPDATE_MODEL_VALUES 4 10:38:29,916 INFO [stdout] (http--127.0.0.1-8080-1) <-- After: UPDATE_MODEL_VALUES 4 10:38:29,916 INFO [stdout] (http--127.0.0.1-8080-1) --> Before: INVOKE_APPLICATION 5 10:38:29,916 INFO [stdout] (http--127.0.0.1-8080-1) <-- After: INVOKE_APPLICATION 5 10:38:29,917 INFO [stdout] (http--127.0.0.1-8080-1) --> Before: RENDER_RESPONSE 6 10:38:29,919 INFO [stdout] (http--127.0.0.1-8080-1) Encoding UISpinner sandbox.UISpinner@2b3a9ca5 10:38:29,919 INFO [stdout] (http--127.0.0.1-8080-1) <-- After: RENDER_RESPONSE 6
私には予想外だったのですが、RESTORE_VIEWフェーズで、UISpinnerオブジェクトが再度生成されています。Viewのツリー構造はどこかに保存されているはずですが、付随するコンポーネントのオブジェクトは毎回生成しているようです。RENDER_RESPONSEでは、先のフェーズで生成済みのオブジェクトを使って、HTMLを吐きます。
・[Submit]実行
10:40:16,960 INFO [stdout] (http--127.0.0.1-8080-1) --> Before: RESTORE_VIEW 1 10:40:16,962 INFO [stdout] (http--127.0.0.1-8080-1) Constructing UISpinnersandbox.UISpinner@5721d4eb 10:40:16,962 INFO [stdout] (http--127.0.0.1-8080-1) sandbox.UISpinner: javax.faces.event.PostAddToViewEvent[source=sandbox.UISpinner@5721d4eb] is Called 10:40:16,963 INFO [stdout] (http--127.0.0.1-8080-1) <-- After: RESTORE_VIEW 1 10:40:16,964 INFO [stdout] (http--127.0.0.1-8080-1) --> Before: APPLY_REQUEST_VALUES 2 10:40:16,964 INFO [stdout] (http--127.0.0.1-8080-1) Decoding UISpinnersandbox.UISpinner@5721d4eb 10:40:16,964 INFO [stdout] (http--127.0.0.1-8080-1) <-- After: APPLY_REQUEST_VALUES 2 10:40:16,965 INFO [stdout] (http--127.0.0.1-8080-1) --> Before: PROCESS_VALIDATIONS 3 10:40:16,966 INFO [stdout] (http--127.0.0.1-8080-1) <-- After: PROCESS_VALIDATIONS 3 10:40:16,966 INFO [stdout] (http--127.0.0.1-8080-1) --> Before: UPDATE_MODEL_VALUES 4 10:40:16,967 INFO [stdout] (http--127.0.0.1-8080-1) <-- After: UPDATE_MODEL_VALUES 4 10:40:16,967 INFO [stdout] (http--127.0.0.1-8080-1) --> Before: INVOKE_APPLICATION 5 10:40:16,968 INFO [stdout] (http--127.0.0.1-8080-1) <-- After: INVOKE_APPLICATION 5 10:40:16,969 INFO [stdout] (http--127.0.0.1-8080-1) --> Before: RENDER_RESPONSE 6 10:40:16,981 INFO [stdout] (http--127.0.0.1-8080-1) <-- After: RENDER_RESPONSE 6
やはり、RESTORE_VIEWフェーズで、UISpinnerオブジェクトが再度生成されています。今回は、新しいページに遷移するので、RENDER_RESPONSEフェーズでは、もはやUISpinnerは使われません。