JSF中引入jsf.js文件之后,可以拦截jsf.ajax.request请求。一直希望有一种方法可以像jquery的ajax一样,能在js中异步取得服务器端发送的数据。无奈标准JSF并没有提供这样的方法。在一些JSF框架里面提供了这样的方法,比如primefaces的onComplete方法就是返回数据到一个js方法中的。JSF能在bean里面更新视图(非ajax更新),其中的PartialViewContext类就可以做到局部更新UI,在bean里获取到这个UI就可以了。于是在网上翻看了很多开源的JSF框架,无意中发现omnifaces这个开源框架。官网:http://omnifaces.org/。
当然,一个框架的东西会有很多,以个人之力要全部参透会有一些难度。开源框架更是集思广益,不过我所需要的只是其中的一部分而已,即在js中取得bean返回的数据。
根据omnifaces的showcase http://showcase.omnifaces.org/ 看到其中的一个Ajax工具类根据PartialViewContext类做到了js取得返回数据,不论字符串或者对象数据都可以发送,非常方便。于是根据提供的源码,顺藤摸瓜,copy了必须支持的类,整理出来了一个仅Ajax发送数据的jar。
omnifaces做的步骤大概是先重写了PartialViewContextFactory这个类,然后在配置文件中配置该重写的类。
1 /** 2 * This partial view context factory takes care that the {@link OmniPartialViewContext} is properly initialized. 3 * 4 */ 5 public class CustomPartialViewContextFactory extends PartialViewContextFactory { 6 7 // Variables ------------------------------------------------------------------------------------------------------ 8 9 private PartialViewContextFactory wrapped; 10 11 // Constructors --------------------------------------------------------------------------------------------------- 12 13 /** 14 * Construct a new OmniFaces partial view context factory around the given wrapped factory. 15 * @param wrapped The wrapped factory. 16 */ 17 public CustomPartialViewContextFactory(PartialViewContextFactory wrapped) { 18 this.wrapped = wrapped; 19 } 20 21 // Actions -------------------------------------------------------------------------------------------------------- 22 23 /** 24 * Returns a new instance of {@link OmniPartialViewContext} which wraps the original partial view context. 25 */ 26 @Override 27 public PartialViewContext getPartialViewContext(FacesContext context) { 28 return new CustomPartialViewContext(wrapped.getPartialViewContext(context)); 29 } 30 31 /** 32 * Returns the wrapped factory. 33 */ 34 @Override 35 public PartialViewContextFactory getWrapped() { 36 return wrapped; 37 } 38 39 }
该类重写还需要一个PartialViewContext ,此处没有直接继承PartialViewContext ,而是继承了PartialViewContext 的子类PartialViewContextWrapper,并重写了里面的方法:
1 /** 2 * <p> 3 * This OmniFaces partial view context extends and improves the standard partial view context as follows: 4 * <ul> 5 * <li>Support for executing callback scripts by {@link PartialResponseWriter#startEval()}.</li> 6 * <li>Support for adding arguments to an ajax response.</li> 7 * <li>Any XML tags which Mojarra and MyFaces has left open after an exception in rendering of an already committed 8 * ajax response, will now be properly closed. This prevents errors about malformed XML.</li> 9 * <li>Fixes the no-feedback problem when a {@link ViewExpiredException} occurs during an ajax request on a page which 10 * is restricted by <code>web.xml</code> <code><security-constraint></code>. The enduser will now properly be 11 * redirected to the login page instead of retrieving an ajax response with only a changed view state (and effectively 12 * thus no visual feedback at all).</li> 13 * </ul> 14 * You can use the {@link Ajax} utility class to easily add callback scripts and arguments. 15 * <p> 16 * This partial view context is already registered by OmniFaces' own <code>faces-config.xml</code> and thus gets 17 * auto-initialized when the OmniFaces JAR is bundled in a web application, so end-users do not need to register this 18 * partial view context explicitly themselves. 19 * 20 * @author Bauke Scholtz 21 * @since 1.2 22 * @see OmniPartialViewContextFactory 23 */ 24 public class CustomPartialViewContext extends PartialViewContextWrapper { 25 26 // Constants ------------------------------------------------------------------------------------------------------ 27 28 private static final String AJAX_DATA = "var Faces=Faces||{};Faces.Ajax={data:%s};"; 29 private static final String ERROR_NO_OMNI_PVC = "There is no current CustomPartialViewContext instance."; 30 31 // Variables ------------------------------------------------------------------------------------------------------ 32 33 private PartialViewContext wrapped; 34 private Map<String, Object> arguments; 35 private List<String> callbackScripts; 36 private CustomPartialResponseWriter writer; 37 38 // Constructors --------------------------------------------------------------------------------------------------- 39 40 /** 41 * Construct a new OmniFaces partial view context around the given wrapped partial view context. 42 * @param wrapped The wrapped partial view context. 43 */ 44 public CustomPartialViewContext(PartialViewContext wrapped) { 45 this.wrapped = wrapped; 46 setCurrentInstance(this); 47 } 48 49 // Actions -------------------------------------------------------------------------------------------------------- 50 51 @Override 52 public PartialResponseWriter getPartialResponseWriter() { 53 if (writer == null) { 54 writer = new CustomPartialResponseWriter(this, super.getPartialResponseWriter()); 55 } 56 57 return writer; 58 } 59 60 @Override // Necessary because this is missing in PartialViewContextWrapper (will be fixed in JSF 2.2). 61 public void setPartialRequest(boolean partialRequest) { 62 getWrapped().setPartialRequest(partialRequest); 63 } 64 65 @Override 66 public PartialViewContext getWrapped() { 67 return wrapped; 68 } 69 70 /** 71 * Add an argument to the partial response. This is as JSON object available by <code>OmniFaces.Ajax.data</code>. 72 * For supported argument value types, read {@link Json#encode(Object)}. If a given argument type is not supported, 73 * then an {@link IllegalArgumentException} will be thrown during end of render response. 74 * @param name The argument name. 75 * @param value The argument value. 76 */ 77 public void addArgument(String name, Object value) { 78 if (arguments == null) { 79 arguments = new HashMap<>(); 80 } 81 82 arguments.put(name, value); 83 } 84 85 /** 86 * Add a callback script to the partial response. This script will be executed once the partial response is 87 * successfully retrieved at the client side. 88 * @param callbackScript The callback script to be added to the partial response. 89 */ 90 public void addCallbackScript(String callbackScript) { 91 if (callbackScripts == null) { 92 callbackScripts = new ArrayList<>(); 93 } 94 95 callbackScripts.add(callbackScript); 96 } 97 98 /** 99 * Reset the partial response. This clears any JavaScript arguments and callbacks set any data written to the 100 * {@link PartialResponseWriter}. 101 * @see FullAjaxExceptionHandler 102 */ 103 public void resetPartialResponse() { 104 if (writer != null) { 105 writer.reset(); 106 } 107 108 arguments = null; 109 callbackScripts = null; 110 } 111 112 /** 113 * Close the partial response. If the writer is still in update phase, then end the update and the document. This 114 * fixes the Mojarra problem of incomplete ajax responses caused by exceptions during ajax render response. 115 * @see FullAjaxExceptionHandler 116 */ 117 public void closePartialResponse() { 118 if (writer != null && writer.updating) { 119 try { 120 writer.endUpdate(); 121 writer.endDocument(); 122 } 123 catch (IOException e) { 124 throw new FacesException(e); 125 } 126 } 127 } 128 129 // Static --------------------------------------------------------------------------------------------------------- 130 131 /** 132 * Returns the current instance of the OmniFaces partial view context. 133 * @return The current instance of the OmniFaces partial view context. 134 * @throws IllegalStateException When there is no current instance of the OmniFaces partial view context. That can 135 * happen when the {@link OmniPartialViewContextFactory} is not properly registered, or when there's another 136 * {@link PartialViewContext} implementation which doesn't properly delegate through the wrapped instance. 137 */ 138 public static CustomPartialViewContext getCurrentInstance() { 139 return getCurrentInstance(getContext()); 140 } 141 142 /** 143 * Returns the current instance of the OmniFaces partial view context from the given faces context. 144 * @param context The faces context to obtain the current instance of the OmniFaces partial view context from. 145 * @return The current instance of the OmniFaces partial view context from the given faces context. 146 * @throws IllegalStateException When there is no current instance of the OmniFaces partial view context. That can 147 * happen when the {@link OmniPartialViewContextFactory} is not properly registered, or when there's another 148 * {@link PartialViewContext} implementation which doesn't properly delegate through the wrapped instance. 149 */ 150 public static CustomPartialViewContext getCurrentInstance(FacesContext context) { 151 CustomPartialViewContext instance = getContextAttribute(context, CustomPartialViewContext.class.getName()); 152 153 if (instance != null) { 154 return instance; 155 } 156 157 // Not found. Well, maybe the context attribute map was cleared for some reason. Get it once again. 158 instance = unwrap(context.getPartialViewContext()); 159 160 if (instance != null) { 161 setCurrentInstance(instance); 162 return instance; 163 } 164 165 // Still not found. Well, maybe RichFaces is installed which doesn't use PartialViewContextWrapper. 166 if (Hacks.isRichFacesInstalled()) { 167 PartialViewContext pvc = Hacks.getRichFacesWrappedPartialViewContext(); 168 169 if (pvc != null) { 170 instance = unwrap(pvc); 171 172 if (instance != null) { 173 setCurrentInstance(instance); 174 return instance; 175 } 176 } 177 } 178 179 // Still not found. Well, it's end of story. 180 throw new IllegalStateException(ERROR_NO_OMNI_PVC); 181 } 182 183 private static void setCurrentInstance(CustomPartialViewContext instance) { 184 setContextAttribute(CustomPartialViewContext.class.getName(), instance); 185 } 186 187 private static CustomPartialViewContext unwrap(PartialViewContext context) { 188 PartialViewContext unwrappedContext = context; 189 190 while (!(unwrappedContext instanceof CustomPartialViewContext) && unwrappedContext instanceof PartialViewContextWrapper) { 191 unwrappedContext = ((PartialViewContextWrapper) unwrappedContext).getWrapped(); 192 } 193 194 if (unwrappedContext instanceof CustomPartialViewContext) { 195 return (CustomPartialViewContext) unwrappedContext; 196 } 197 else { 198 return null; 199 } 200 } 201 202 // Nested classes ------------------------------------------------------------------------------------------------- 203 204 /** 205 * This OmniFaces partial response writer adds support for passing arguments to JavaScript context, executing 206 * oncomplete callback scripts, resetting the ajax response (specifically for {@link FullAjaxExceptionHandler}) and 207 * fixing incomlete XML response in case of exceptions. 208 * @author Bauke Scholtz 209 */ 210 private static class CustomPartialResponseWriter extends PartialResponseWriter { 211 212 // Variables -------------------------------------------------------------------------------------------------- 213 214 private CustomPartialViewContext context; 215 private PartialResponseWriter wrapped; 216 private boolean updating; 217 218 // Constructors ----------------------------------------------------------------------------------------------- 219 220 public CustomPartialResponseWriter(CustomPartialViewContext context, PartialResponseWriter wrapped) { 221 super(wrapped); 222 this.wrapped = wrapped; // We can't rely on getWrapped() due to MyFaces broken PartialResponseWriter. 223 this.context = context; 224 } 225 226 // Overridden actions ----------------------------------------------------------------------------------------- 227 228 /** 229 * An override which checks if the web.xml security constraint has been triggered during this ajax request 230 * (which can happen when the session has been timed out) and if so, then perform a redirect to the originally 231 * requested page. Otherwise the enduser ends up with an ajax response containing only the new view state 232 * without any form of visual feedback. 233 */ 234 @Override 235 public void startDocument() throws IOException { 236 wrapped.startDocument(); 237 String loginURL = WebXml.INSTANCE.getFormLoginPage(); 238 239 if (loginURL != null) { 240 FacesContext facesContext = FacesContext.getCurrentInstance(); 241 String loginViewId = normalizeViewId(facesContext, loginURL); 242 243 if (loginViewId.equals(getViewId(facesContext))) { 244 String originalURL = getRequestAttribute(facesContext, "javax.servlet.forward.request_uri"); 245 246 if (originalURL != null) { 247 redirect(originalURL); 248 } 249 } 250 } 251 } 252 253 /** 254 * An override which remembers if we're updating or not. 255 * @see #endDocument() 256 * @see #reset() 257 */ 258 @Override 259 public void startUpdate(String targetId) throws IOException { 260 updating = true; 261 wrapped.startUpdate(targetId); 262 } 263 264 /** 265 * An override which remembers if we're updating or not. 266 * @see #endDocument() 267 * @see #reset() 268 */ 269 @Override 270 public void endUpdate() throws IOException { 271 updating = false; 272 wrapped.endUpdate(); 273 } 274 275 /** 276 * An override which writes all {@link OmniPartialViewContext#arguments} as JSON to the extension and all 277 * {@link OmniPartialViewContext#callbackScripts} to the eval. It also checks if we're still updating, which 278 * may occur when MyFaces is used and an exception was thrown during rendering the partial response, and then 279 * gently closes the partial response which MyFaces has left open. 280 */ 281 @Override 282 public void endDocument() throws IOException { 283 if (updating) { 284 // If endDocument() method is entered with updating=true, then it means that MyFaces is used and that 285 // an exception was been thrown during ajax render response. The following calls will gently close the 286 // partial response which MyFaces has left open. 287 // Mojarra never enters endDocument() method with updating=true, this is handled in reset() method. 288 endCDATA(); 289 endUpdate(); 290 } 291 else { 292 if (context.arguments != null) { 293 startEval(); 294 write(String.format(AJAX_DATA, Json.encode(context.arguments))); 295 endEval(); 296 } 297 298 if (context.callbackScripts != null) { 299 for (String callbackScript : context.callbackScripts) { 300 startEval(); 301 write(callbackScript); 302 endEval(); 303 } 304 } 305 } 306 307 wrapped.endDocument(); 308 } 309 310 // Custom actions --------------------------------------------------------------------------------------------- 311 312 /** 313 * Reset the partial response writer. It checks if we're still updating, which may occur when Mojarra is used 314 * and an exception was thrown during rendering the partial response, and then gently closes the partial 315 * response which Mojarra has left open. This would clear the internal state of the wrapped partial response 316 * writer and thus make it ready for reuse without risking malformed XML. 317 */ 318 public void reset() { 319 try { 320 if (updating) { 321 // If reset() method is entered with updating=true, then it means that Mojarra is used and that 322 // an exception was been thrown during ajax render response. The following calls will gently close 323 // the partial response which Mojarra has left open. 324 // MyFaces never enters reset() method with updating=true, this is handled in endDocument() method. 325 endCDATA(); 326 endUpdate(); 327 wrapped.endDocument(); 328 } 329 } 330 catch (IOException e) { 331 throw new FacesException(e); 332 } 333 finally { 334 responseReset(); 335 } 336 } 337 338 // Delegate actions ------------------------------------------------------------------------------------------- 339 // Due to MyFaces broken PartialResponseWriter, which doesn't delegate to getWrapped() method, but instead to 340 // the local variable wrapped, we can't use getWrapped() in our own PartialResponseWriter implementations. 341 342 @Override 343 public void startError(String errorName) throws IOException { 344 wrapped.startError(errorName); 345 } 346 347 @Override 348 public void startEval() throws IOException { 349 wrapped.startEval(); 350 } 351 352 @Override 353 public void startExtension(Map<String, String> attributes) throws IOException { 354 wrapped.startExtension(attributes); 355 } 356 357 @Override 358 public void startInsertAfter(String targetId) throws IOException { 359 wrapped.startInsertAfter(targetId); 360 } 361 362 @Override 363 public void startInsertBefore(String targetId) throws IOException { 364 wrapped.startInsertBefore(targetId); 365 } 366 367 @Override 368 public void endError() throws IOException { 369 wrapped.endError(); 370 } 371 372 @Override 373 public void endEval() throws IOException { 374 wrapped.endEval(); 375 } 376 377 @Override 378 public void endExtension() throws IOException { 379 wrapped.endExtension(); 380 } 381 382 @Override 383 public void endInsert() throws IOException { 384 wrapped.endInsert(); 385 } 386 387 @Override 388 public void delete(String targetId) throws IOException { 389 wrapped.delete(targetId); 390 } 391 392 @Override 393 public void redirect(String url) throws IOException { 394 wrapped.redirect(url); 395 } 396 397 @Override 398 public void updateAttributes(String targetId, Map<String, String> attributes) throws IOException { 399 wrapped.updateAttributes(targetId, attributes); 400 } 401 402 } 403 }
在face-config.xml文件添加配置如下:
1 <factory> 2 <partial-view-context-factory>com.context.CustomPartialViewContextFactory</partial-view-context-factory> 3 </factory>
可以看到其中定义了两个常量,一个警告说明,一个名为AJAX_DATA,里面写的是一段js的字符串。这个常量用在了 被重写的endDocument()方法里。这个方法主要做了两件事,第一是写入要传递的数据,并且以json格式打包,第二件事是包含了一个js方法。那么可以这样认为:该方法的作用是用一个js方法取得发送的数据。
首先我们必须清楚,JSF使用f:ajax更新客户端的底层操作是怎样的。JSF更新数据是向页面发送了xml数据的,可以在firefox的 firebug下面的网络-XML面板下看到发送的xml数据,类似于以下的数据:
1 <partial-response id="j_id1"> 2 <changes> 3 <update id="j_id1:javax.faces.ViewState:0">-4426271603414575392:-5845678956333562288</update> 4 </changes> 5 </partial-response>
里面包含了需要更新的组件和组件状态。
我把omnifaces的Ajax代码整理出来以后,根据官方例子的用法,写了一个小demo测试,其中页面form表单代码如下:
1 <h:form prependId="false"> 2 <h:outputLabel value="#{test2.radom}" id="outLabel"/> 3 <h:commandButton action="#{test2.callback}" value="请求服务端js"> 4 <f:ajax/> 5 </h:commandButton> 6 <h:commandButton value="获取服务端数据到js"> 7 <f:ajax listener="#{test2.argument}"/> 8 </h:commandButton> 9 10 <h:commandButton value="showUser"> 11 <f:ajax listener="#{test2.parseUser}"/> 12 </h:commandButton> 13 </h:form>
测试bean代码如下:
1 @ManagedBean(name="test2") 2 @ViewScoped 3 public class Test2 implements Serializable { 4 5 private static final long serialVersionUID = 8686669721840131192L; 6 7 public void callback() { 8 Ajax.oncomplete("alert('Hi, I am the oncomplete callback script!')"); 9 } 10 11 public void argument() { 12 Ajax.data("foo", "bar"); 13 Ajax.data("first", "one", "second", "two"); 14 Map<String, Object> data = new HashMap<>(); 15 data.put("bool", true); 16 data.put("number", 1.2F); 17 data.put("date", new Date()); 18 data.put("array", new Integer[] { 1, 2, 3, 4, 5 }); 19 data.put("list", Arrays.asList("one", "two", "three")); 20 Ajax.data(data); 21 Ajax.oncomplete("showData()"); 22 } 23 24 public void parseUser(){ 25 Ajax.data("user", new User(1, "bigbang")); 26 Ajax.oncomplete("showUser()"); 27 } 28 29 }
其中提供了三个页面响应的方法。当点击第一个button时,页面会弹出一个alert提示框。此时查看firefox下面的数据如下:
1 <partial-response id="j_id1"> 2 <changes> 3 <update id="j_id1:javax.faces.ViewState:0">-4426271603414575392:-5845678956333562288</update> 4 <eval>alert('Hi, I am the oncomplete callback script!')</eval> 5 </changes> 6 </partial-response>
点击“获取数据到js”按钮,查看xml数据:
1 <partial-response id="j_id1"> 2 <changes> 3 <update id="j_id1:javax.faces.ViewState:0">-3364647386979820288:-1391656100755852530</update> 4 <eval>var Faces=Faces||{};Faces.Ajax={data:{"second":"two","number":1.2,"list":["one","two","three"],"foo":"bar","bool":true,"date":"Fri, 17 Jul 2015 09:17:50 GMT","first":"one","array":[1,2,3,4,5]}};</eval> 5 <eval>showData()</eval> 6 </changes> 7 </partial-response>
点击“showuser”按钮,查看xml数据:
1 <partial-response id="j_id1"> 2 <changes> 3 <update id="j_id1:javax.faces.ViewState:0">-3364647386979820288:-1391656100755852530</update> 4 <eval>var Faces=Faces||{};Faces.Ajax={data:{"user":{"id":1,"name":"bigbang"}}};</eval> 5 <eval>showUser()</eval> 6 </changes> 7 </partial-response>
可以看出刚才的那个endDocumnt方法和AJAX_DATA常量的用意了,实际是构造了一个js对象,然后传入一个js方法到客户端,客户端会自动调用这个js方法,根据对象取得json数据。那么客户端的js只需要这样写就可以了:
1 <script> 2 function showData() { 3 var data = Faces.Ajax.data; 4 $.each(data, function(key, value) { 5 console.log(key+"---"+JSON.stringify(value)); 6 }); 7 } 8 9 function showUser(){ 10 var data = Faces.Ajax.data; 11 $.each(data, function(key,value){ 12 var user = JSON.stringify(value); 13 var u = $.parseJSON(user); 14 console.log("userName----"+u.name); 15 } 16 </script>
控制台显示数据如下:
OK,大功告成了!其中的细节不用细究,无非就是掺杂了各种数据转换为json格式、数据的包装和写入。最后说明一点,omnifaces需要CDI的支持,必须导入CDI的jar包。