Here, a homemade “securized” solution (questionable ??!!!) allowing the generic calls to Spring bean on server side from the client side with Ajax requests on the format:
ajaxToSpringBeanService.do?beanName=BEANNAME&beanMethod=METHODREQUESTED ... ajaxToSpringBeanService.do?beanName=languageService&beanMethod=getAllLanguages
In fact, this solution exposes the Spring BEANs as a HTTP/AJAX SERVICE, returning a content like JSON objects in a Spring MVC application.
This solution uses several technologies:
- Spring: the HttpRequestHandler interface for the creation of coordinator servlet, the ApplicationContextAware interface for the access of coordinator servlet to the Spring application context;
- Java Reflection (see my last post on Java: Reflection, javap): used in the method’s calls on Spring bean on server side;
- JSON: JSON will be the format of response from server to client;
- SENCHA/ExtJs: used by the Ajax requests and the JSON response’s handling on client side;
I). Server Side components
First, Following the codes of coordinator servlet AjaxToSpringBeanServlet:
package com.ho.servlet; import java.io.IOException; import java.io.PrintWriter; import java.lang.reflect.Method; import java.util.HashMap; import java.util.Map; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import net.sf.json.JSONObject; import org.apache.commons.lang.exception.ExceptionUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.web.HttpRequestHandler; /** * Creation of the ajax servlet for access to spring bean in application context (Only public methods without parameter and returning a object) * <p> * This servlet is used to get the languages configured in database directly from a client side via Ajax request: * http://localhost:8080/..../ajaxToSpringBeanService.do?beanName=languageService&beanMethod=getAllLanguages * </p> * @author Huseyin OZVEREN */ public class AjaxToSpringBeanServlet implements HttpRequestHandler, ApplicationContextAware { // ------------------------------------------- CONSTANTS // -------------------------------------------- LOG4J private final static Log logger = LogFactory.getLog(AjaxToSpringBeanServlet.class); // ------------------------------------------------- PRIVATE ATTRIBUTES // Application Context automatically Injected private ApplicationContext context; // --------------------------------------------- PRIVATE METHODS private boolean isValidParameters(HttpServletRequest req, HttpServletResponse res) { boolean output = true; String beanName = req.getParameter("beanName"); String beanMethod = req.getParameter("beanMethod"); if (null == beanName || "".equals(beanName) || null == beanMethod || "".equals(beanMethod)) { output = false; // StringBuffer msg = new StringBuffer(""); msg.append("<b>Usage : </b><br/>"); msg.append("ajaxToSpringBeanService.do?beanName=BEANNAME&beanMethod=METHODREQUESTED<br/><br/>"); msg.append("The parameter 'beanName' is the name of bean requested in the Spring context;<br/>"); msg.append("The parameter 'beanMethod' is the name of method requested in the bean;<br/>"); msg.append("This remote service allows the requests to only public methods without parameter and returning a object;<br/>"); // // Data to convert to JSON Map<String, Object> model = new HashMap<String, Object>(); model.put("failure", "true"); model.put("errorMessage", msg.toString()); sendJSONReponseToClientJSON(res, model); }// end-if return output; } /** * Convert the model Map to JSON format * and send the reponse to client. * @param model */ private void sendJSONReponseToClientJSON(HttpServletResponse res, Map<String, Object> model){ try{ // // Convert POJO to JSON JSONObject jsonObj = new JSONObject(); jsonObj.putAll(model); String dataJSON = org.mortbay.util.ajax.JSON.toString(jsonObj); // // Write in RESPONSE PrintWriter out = res.getWriter(); out.println(dataJSON); } catch (IOException e) { logger.error("checkValidParameters : an exception occured" , e); } // end-try } // --------------------------------------------- PUBLIC METHODS @Override public void handleRequest(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { if (isValidParameters(req, res)) { String beanName = req.getParameter("beanName"); String beanMethod = req.getParameter("beanMethod"); // Content-type JSON res.setContentType("text/html"); // Data to convert to JSON Map<String, Object> model = new HashMap<String, Object>(); model.put("failure", "true"); try { Object result = null; // Reflection { Object obj = (Object) context.getBean(beanName); Class mClass = obj.getClass(); // Only public methods without parameter and returning a object Method m = mClass.getMethod(beanMethod, null); result = m.invoke(obj,null); } Object docModel = result; if(docModel!=null){ model = new HashMap<String, Object>(); model.put("success", "true"); model.put("data", docModel); } } catch (Throwable e) { model.put("errorMessage", ExceptionUtils.getStackTrace(e)); } sendJSONReponseToClientJSON(res, model); } } // ------------------------------------------------ GET/SET TERS @Override public void setApplicationContext(ApplicationContext applicationcontext) throws BeansException { this.context = applicationcontext; } }
Explanations:
- This class AjaxToSpringBeanServlet implements the interface HttpRequestHandler which is a plain handler interface for components that process HTTP requests, analogous to a Servlet. In this example, because we use this solution in a Web application based on Spring MVC, we will use the option recommended by the Spring (2) documentation for the way of exposing an HttpRequestHandler:
(1) The easiest way to expose an HttpRequestHandler bean in Spring style is to define it in Spring’s root web application context and define an HttpRequestHandlerServlet in web.xml, pointing at the target HttpRequestHandler bean through its servlet-name which needs to match the target bean name.(2) Supported as a handler type within Spring’s DispatcherServlet, being able to interact with the dispatcher’s advanced mapping and interception facilities. This is the recommended way of exposing an HttpRequestHandler, while keeping the handler implementations free of direct dependencies on a DispatcherServlet environment.
- More, this class AjaxToSpringBeanServlet implements also the interface setApplicationContext in order to have a direct access to the Spring Application context which is injected by the following method:
@Override public void setApplicationContext(ApplicationContext applicationcontext) throws BeansException { this.context = applicationcontext; }
- The method sendJSONReponseToClientJSON converts a model Map
to JSON format and send this result to the client. To convert a POJO to JSON, I have use the class org.mortbay.util.ajax.JSON from the library “jetty-util-6.1.9.jar”. It is possible to use another Java libraries like Google Gson that can be used to convert Java Objects into their JSON representation. JSONObject jsonObj = new JSONObject(); jsonObj.putAll(model); String dataJSON = org.mortbay.util.ajax.JSON.toString(jsonObj);
- The method isValidParameters checks the presence of “beanName” and “beanMethod” parameters; and returns to the client a error message concerning the service’s usage.
- And the last method handleRequest contains the logical business i.e.:
– retrieve the parameters “beanName” and “beanMethod” from ajax request;
– get the Spring bean due to the “beanName” parameter;
Object obj = (Object) context.getBean(beanName);
– use the reflection to invoke the bean’s method requested:
Class mClass = obj.getClass(); // Only public methods without parameter and returning a object Method m = mClass.getMethod(beanMethod, null); result = m.invoke(obj,null);
– create a Map
model.put("failure", "true");
model.put("errorMessage", ExceptionUtils.getStackTrace(e));
model.put("success", "true");
model.put("data", docModel);
So, the web.xml file will be:
[...] <servlet> <servlet-name>myHUODispatcherServlet</servlet-name> <servlet-class> org.springframework.web.servlet.DispatcherServlet </servlet-class> <load-on-startup>1</load-on-startup> <!-- Configuration file of dispatcher servlet is: myHUODispatcherServlet-servlet.xml --> </servlet> <servlet-mapping> <servlet-name>myHUODispatcherServlet</servlet-name> <url-pattern>*.do</url-pattern> </servlet-mapping> [...]
and myHUODispatcherServlet-servlet.xml file:
[...] !-- ################### SPRING MVC CONTROLLER XML ################### --> <bean class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping"> <property name="mappings"> <value> /loginSecure.do=loginSecureController /document.do=documentController /ajaxToSpringBeanService.do=ajaxToSpringBeanServlet </value> </property> </bean> <!-- ################### AJAX request to Spring bean ################### --> <bean id="ajaxToSpringBeanServlet" class="com.ho.servlet.AjaxToSpringBeanServlet"/> [...]
II). Client Side
Here, an example of a SENCHA/ExtJs interface containing a panel:
{ id: 'languagesChkBxsContainerID', xtype: 'fieldcontainer', width: 400, maintainFlex: true, defaults: { hideLabel: false, border:false, bodyStyle:'padding:0 0 0 0' }, layout: { type: 'column' }, fieldLabel: '', items: [ ] }
… and the Ajax request to our service:
// request the server to check if the filled exist Ext.Ajax.request({ async : false, url : 'ajaxToSpringBeanService.do?beanName=languageService&beanMethod=getAllLanguages', success: function(request, resp) { var result = Ext.JSON.decode(request.responseText); if (result.success) { <!-- Deleting of all buttons --> Ext.getCmp('languagesChkBxsContainerID').removeAll(); Ext.getCmp('languagesChkBxsContainerID').doLayout(); // important for(var i = 0; i < result.data.length; i++){ var languageTmp = result.data[i]; <!-- Create and add new button --> var newChkBox = new Ext.form.field.Checkbox({ xtype: 'checkboxfield', id: 'languageChkBox'+languageTmp.isoCode+'ID', name: 'languageChkBox'+languageTmp.isoCode+'ID', boxLabel: languageTmp.isoCode, inputValue: 'true', uncheckedValue: 'false', width: 50 }); Ext.getCmp('languagesChkBxsContainerID').add(newChkBox); }//end-for Ext.getCmp('languagesChkBxsContainerID').doLayout(); // important } else { NO success field in reponse => (failure=true) if(result.errorMessage === ''){ Ext.MessageBox.alert('my title', 'data not found'); }else{ Ext.MessageBox.alert('my title', result.errorMessage); } } }, failure: function() { // Timeout Ext.MessageBox.alert('my title', 'sorry, timeout!!!!!!'); } });
Explanations:
In our example, the client sends a request to server to collect all languages via the Spring bean named “languageService” and its method “getAllLanguages” (see the parameters in URL “beanName=languageService&beanMethod=getAllLanguages”).
In Sencha/ExtJs Ajax request, there are several callbacks for:
- success: if the reponse is correctly returned by the server
success: function(request, resp) {
- failure: if the reponse is not correctly returned by the server (timeout)
failure: function() { // Timeout
In the ‘failure’ case i.e. no reponse received, a timeout message is displayed to client/user.
In the ‘success’ case, the response received, is decoded via EXTJS utility, and a check is done on the ‘success’ field in JSON object corresponding to the Java code:
model.put("success", "true");
model.put("failure", "true");
var result = Ext.JSON.decode(request.responseText); if (result.success) { [...] } else { // NO success field in reponse => (failure=true) if(result.errorMessage === ''){ Ext.MessageBox.alert('my title', 'data not found'); }else{ Ext.MessageBox.alert('my title', result.errorMessage); } }
…if there is not ‘success’ field in response (failure=true), the error message recevied is displayed to client/user.
…otherwise, we range all “data” received which in our case corresponding to a list of ‘Language’ containing at least the ‘isoCode’ property. For each element receveid, we create a ‘checkboxfield’ in the previous panel with the ID ‘languagesChkBxsContainerID’.
for(var i = 0; i < result.data.length; i++){ var languageTmp = result.data[i]; <!-- Create and add new button --> var newChkBox = new Ext.form.field.Checkbox({ xtype: 'checkboxfield', id: 'languageChkBox'+languageTmp.isoCode+'ID', name: 'languageChkBox'+languageTmp.isoCode+'ID', boxLabel: languageTmp.isoCode, inputValue: 'true', uncheckedValue: 'false', width: 50 }); Ext.getCmp('languagesChkBxsContainerID').add(newChkBox); }//end-for
… the above codes will produce the below result:
III). Tests and results
Example of results with a list of following POJO, converted to JSON and sent by server to client:
package com.ho.data.common; public class Language { // ---------------------------------- ATTRIBUTS private String id = java.util.UUID.randomUUID().toString(); private Integer version = null; private String isoCode = null; private String description = null; private boolean ojLanguage = false; private boolean relayLanguage = false; private boolean sourceLanguage = false; private boolean epLanguage = false; // ----------------------------------- GET/SET TERS public String getIsoCode() { return isoCode; } public void setIsoCode(String isoCode) { this.isoCode = isoCode;} public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } public boolean isOjLanguage() { return ojLanguage; } public void setOjLanguage(boolean ojLanguage) { this.ojLanguage = ojLanguage; } public boolean isRelayLanguage() { return relayLanguage; } [...] }
Some examples:
- No parameters: ajaxToSpringBeanService.do
- The beanName parameter is missing: ajaxToSpringBeanService.do?beanName=languageService
- The beanMethod parameter is missing: ajaxToSpringBeanService.do?beanMethod=getAllLanguages
- OK: ajaxToSpringBeanService.do?beanName=languageService&beanMethod=getAllLanguages
IV). Conclusion
In this post, we have studied a “securized” homemade solution (questionable ??!!!) exposing the Spring BEANs as a HTTP/AJAX SERVICE, returning a content like JSON objects in a Spring MVC application. However, several comments:
- This solution exposes all Spring BEANs transactionnal or not.
- This solution is “securized” like a normal SPRING MVC controller.
- Currently, this solution allows the calls to public methods without parameters and returning a object of the Spring beans (so “securized”???).
- Yet, it could easily (due to Reflection) expose the private methods, fields, methods with arguments of the Spring beans.
- This solution accepts the calls from all clients which use JSON.
- There are a lot of others Java libraries like DWR that coupled with Spring, enables Java on the server and JavaScript in a browser to interact and call each other.
- More, this homemade solution is easily customized.
- …
Huseyin OZVEREN