Sunday, March 04, 2012

Calling Any Spring Service from JSTL

Update

There is actually a cleaner way to expose some (or all in one shot even not recommended) spring injected beans through the use of InternalResourceViewResolver#setExposedContextBeanNames:
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
   <property name="exposedContextBeanNames">
      <list>
         <value>employeeService</value>
         <value>clientService</value>
      </list>
   </property>
</bean>
In JSP you then just need to refer to the service:
<c:set var="clients" scope="request" value="${clientService.findAll()}"/>

Original post

There are times when you want to expose Spring Services through JSTL.

It is annoying though to have to create a new taglib definition plus a new Java method whenever you are in needs to expose certain functionality in JSP.

Did I say I like separation of concerns :-) You will say, but wait a minute exposing anything to JSP will be dangerous. Then I would respond, forcing to touch java for exposing functionality to the front end is not Agile.

So there is no silver bullet nor unique answer and the pragmatic architect knows how to respond to design questions: It depends.

Yes, you can go evil with the approach I am explaining here so you will need to limit the packages you expose to the front end, do visual code review and even come up with ingenious automated code review but I do believe this approach results in faster response to business needs at least for some "authorized" JSPs.

For example I have a team where people building reports and ETLs are not the same as those programming the back end or the front end application. I need them to build cool forms to accepts parameters from users and use those parameters to render reports and trigger ETL processes. I cannot afford the data analysts to be waiting for Java back end developers in order to inject from Controllers the objects they need in each page.

So first of all you need a class that will use reflection to invoke any Spring Service:
package com.nestorurquiza.utils;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

import org.springframework.context.ApplicationContext;

public final class SpringUtils {
    
    private SpringUtils(){}
    
    public static Object runService( ApplicationContext ctx, String service, String method, Object... params ) throws ClassNotFoundException, SecurityException, NoSuchMethodException, IllegalArgumentException, IllegalAccessException, InvocationTargetException {
        Class c = Class.forName(service);
        Object springService = ctx.getBean(c);
        Class[] classes = null;
        if( params != null ) {
            classes = new Class[params.length];
            int i = 0;
            for( Object o : params ) {
                classes[i++] = o.getClass();
            }
        }
        
        Method m = c.getDeclaredMethod(method, classes);
        Object o = m.invoke(springService, params);
        return o;
    }

}

You need a class that will be able to interact with a Servlet Container to get the Spring Application Context:
package com.nestorurquiza.utils;

import java.lang.reflect.InvocationTargetException;

import org.springframework.context.ApplicationContext;

import com.nestorurquiza.utils.web.ApplicationServletContextListener;

public final class SpringWebUtils {
    
    private SpringWebUtils(){}
    
    public static Object runService( String service, String method, Object[] params ) throws ClassNotFoundException, SecurityException, NoSuchMethodException, IllegalArgumentException, IllegalAccessException, InvocationTargetException {
        //ApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(request.getSession().getServletContext()); 
        ApplicationContext ctx = ApplicationServletContextListener.getWebApplicationContext();
        return SpringUtils.runService(ctx, service, method, params);
    }
}
A taglib (SpringUtils.tld):
<?xml version="1.0" encoding="UTF-8"?>
<taglib xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemalocation="http://java.sun.com/xml/ns/j2ee/web-jsptaglibrary_2_0.xsd"
version="2.0">
<tlib-version>2.1</tlib-version>
<uri>SpringUtils</uri>

<function>
<name>runService</name>
<function-class>com.nestorurquiza.utils.SpringWebUtils</function-class>
<function-signature>
java.lang.Object runService( java.lang.String, java.lang.String, java.lang.Object[] )
</function-signature>
</function>
</taglib>
In JSP:
...
<%@ taglib prefix="spu" uri="/WEB-INF/tld/SpringUtils.tld"%>
<c:set var="clients" scope="request" value="${spu:runService('com.nestorurquiza.service.ClientService', 'findAll', null)}"/>
<tr>
<td ><spring:message code="label.clientName"/>:</td>
<td>
<form:select path="clients" >
<form:options items="${clients}" itemValue="id" itemLabel="name" />
</form:select>
</td>
<td><!-- errors for client or fund here --></td>
</tr>
...
<% String[] aParams = { "USA" };
request.setAttribute("aParams", aParams); %>
<c:set var="someClients" scope="request" value="${spu:runService('com.nestorurquiza.service.ClientService', 'findByName',aParams)}"/>
<tr>
<td ><spring:message code="label.clientName"/>:</td>
<td>
<form:select path="someClients" >
<form:options items="${someClients}" itemValue="id" itemLabel="name" />
</form:select>
</td>
<td><!-- errors for client or fund here --></td>
</tr>
As you can see you can call any service wether they need parameters or not. I have used some scriptlet to get an array because for some reason the below won't work in my current environment:
<c:set var="params" value="'USA'"/>
<c:set var="aParams" value="${fn:split(params, ',')}"/>

No comments:

Followers