|
|
 |
November 2008
Discuss this Article
Suppose you have an existing J2EE application with EJB's, RMI objects, JMS destinations
and other objects bound into a JNDI registry. During the course of the project
schedule, you need to make significant changes to the underlying architecture,
re-define business processes and/or need to identify transactional/performance
problems. Without a proper framework in place, it can be difficult to make "non-intrusive" changes
to an existing system without rippling side effects.
This article presents a simple filtering framework to "intercept" JNDI
operations and objects in a non-intrusive way (without code changes or the
overhead of AOP systems). You can "peek-into the JNDI subsystem" and
fully control the behavior of an application to support:
- Interception of JNDI operations (such as lookup, bind, rebind, etc).
- Interception
of JNDI-bound object invocations before the object is called.
- Examination
of the parameters before a JNDI-bound object is called.
- Modification of
the parameters.
- Modification of the return values.
- Interception of JNDI-bound object invocations
after the object is called.
- Chaining of requests and responses.
The J2EE specification provides a filtering
API for Servlets. Unfortunately, a similar API does not exist for other J2EE
components such as:
- EJBs,
- RMI objects,
- JMS destinations
- Other JNDI-bound objects (i.e., javax.sql.DataSource, transaction
managers, foreign JNDI objects from other providers, etc).
Benefits and Usage
In general, JNDI filters can be used as an advanced J2EE feature primarily
intended for situations where the developer cannot change the coding of an
existing resource and needs to modify the code to change the behavior of the
resource. Generally, it is more efficient to modify the code to change the
behavior of the resource itself rather than using filters to modify the resource.
But, in most cases, that is not possible, and changes need to be "loosely-coupled" around
an existing architecture.
The following benefits are gained in JNDI filtering:
- Adaptability - Allows the re-configuration of business processes and the
ability to 're-route' requests to other services based
on service-level agreements (SLAs).
- Extensibility - Allows pre/post processing
for logging, auditing, security and custom logic without breaking existing
interface contracts.
- Flexibility - Easily reconfigure and chain filters on
a dynamic basis.
- Modularization - Encapsulation of application-processing
logic into Java code can define modular units easily and can be added to
and removed from a request/response chain.
Here are some common filter usage patterns:
Filter Type |
Description |
Logging and Auditing |
Messages can be logged before and after the
execution of the target object. Auditing and statistics can be performed
to record invocation time, response time, etc. |
JDBC and DataSource |
Print out SQL queries and timings on javax.sql.DataSource
objects and java.sql.Connection objects to get a detailed view of what
is going on with database access. Furthermore, this can be used in conjunction
with the Caching filter to have a proxy between the app server and database
for performance improvements. |
Security |
Provides an 'app-server neutral' approach
for authentication and authorization. |
Caching |
Cache data without invoking the service. |
Transaction Management |
Display detailed information about the current
transaction (such as XID, TX statuses, etc). Fully control the demarcation
of transactions such as selectively suspending or starting a new transaction. |
Course Grained |
Redefine business processes dynamically.
Perform dynamic rerouting and change the behavior of the service invocation
by breaking down logical steps across multiple services based on service
level agreements ( SLA). |
HA (High Availability) |
Check for the existence and availability
of an object invocation service. Asynchronous communication can be performed
if a service is unavailable. |
Request/Response Modification |
Manipulating the request/response. |
Real-World Example
Let's start with a real-world example. Suppose you are building a J2EE
banking system and have the following code embedded within your application:
InitialContext ic = new
InitialContext();
AccountHome home = (AccountHome)
ic.lookup("ejb20-beanManaged-AccountHome");
Account account = home.findByPrimaryKey("12345");
account.withdraw(200);
This code simply looks up an 'Account' EJB from JNDI and performs
a withdraw operation.
Now, assume that the code behind the bank account EJB is very complicated
and fragile. The following new features need to be added to the system (because
the current code doesn't support them):
- Check the account balance before withdrawing funds.
- Perform a security fraud check when withdrawing funds (auditing and logging).
- Measure how long the withdraw operation takes to execute and capture
statistical information on user input.
- Measure SQL performance bottlenecks and audit SQL statements and timings.
- Perform asynchronous, high-availability persistence if the Account EJB
is unavailable due to system outages.
JNDI filters allow the code above to be untouched while performing the new
requirements in a highly modular, decoupled manner.
How does this work? Well, the same code:
InitialContext ic = new
InitialContext();
AccountHome home = (AccountHome)
ic.lookup("ejb20-beanManaged-AccountHome");
Account account = home.findByPrimaryKey("12345");
account.withdraw(200);
is left untouched. The following happens under the covers with JNDI filters:
Figure 1 – Client Interactions
- When the new InitialContext() operation is invoked, an instance
of a limaye.filter.impl.DefaultInitialContextFactory is used. This
is set asA system property in the JVM.
- Dynamic proxies are created when JNDI
lookup operations are performed. Hence, JNDI filters are invoked (potentially
in a chain) for pre/post processing.
Introducing JNDI Filters
JNDI filters are Java classes that can be invoked in response to a request
for a resource in an intercepted JNDI object. They are very similar to Servlet
filters but are used on the server-side.
Resources can include any JNDI-bound object such as EJBs, RMI objects, etc.
A filter intercepts the request and can examine and modify the response and
request objects or execute other tasks.
A filter intercepts a request for a specific named resource or a group of
resources (based on a JNDI name, target class and target return value classes)
and executes the code in the filter. For each resource or group of resources,
you can specify a single filter or multiple filters that are invoked in a specific
order, called a chain.
When a filter intercepts a request, it has access to the limaye.filter.api.Request
and limaye.filter.api.Response objects that provide access to the proxy object,
its method, arguments, naming context. and a limaye.filter.api.FilterChain
object. The FilterChain object contains a list of filters that can be invoked
sequentially. When a filter has completed its work, the filter can call the
next filter in the chain, block the request, throw an exception, or invoke
the originally requested resource.
After the original resource is invoked, control is passed back to the filter
at the bottom of the list in the chain. This filter can then examine and modify
the request parameters and return value, block the request, throw an exception,
or invoke the next filter up from the bottom of the chain. This process continues
in reverse order up through the chain of filters.

Figure 2 – High Level API Class Diagrams
Writing a Filter Class
To write a filter class, implement the limaye.filter.api.Filter interface.
You must implement the following methods of this interface:
You use the doFilter() method to examine and modify the request and response
objects, perform other tasks such as logging, invoke the next filter in the
chain, or block further processing.
Several other methods are available on the FilterConfig object for accessing
the name of the filter, the java.naming.Context and the filter's initialization
attributes. To access the next item in the chain (either another filter or
the original resource, if that is the next item in the chain), call the FilterChain.doFilter()
method.
Configuring Filters
You can configure filters in the filter-config.xml file that is part of the
system classpath. You specify the filter and then map the filter to a JNDI-Name,
target and target-return value classes.
To configure a filter:
- Specify one or more initialization attributes inside a <filter> element.
For example:
<filter>
<filter-name>LogFilter</filter-name>
<filter-class>
examples.LogFilter
</filter-class>
<description>
Performs simple logging of the class,
methods and interfaces
</description>
<display-name>Log Filter</display-name>
<init-param>
<param-name>TestKey1</param-name>
<param-value>TestValue</param-value>
</init-param>
</filter>
Your Filter class can read the initialization attributes using the FilterConfig.getInitParameter()
or FilterConfig.getInitParameters() methods.
- Add filter mappings. The <filter-mapping> element specifies which
filter to execute based on a JNDI name, target interface name and target
return value interface names.
The <filter-mapping> element must immediately
follow the <filter> element(s).
NOTE : Wildcards can be used for the
jndi-name, target-class, methods or target-return-class-list for pattern
matching. If a match fails, a Class.isAssignable check is performed.
Example:
Suppose you have the following code in your application:
InitialContext ic = new
InitialContext();
Object obj =
ic.lookup("ejb20-beanManaged-AccountHome");
Account account = (Account)
PortableRemoteObject.narrow(obj
Account.class);
account.withdraw(200);
You would define the filter-config.xml as the following:
<filter-mapping>
<filter-name>LogFilter</filter-name>
<jndi-name>ejb20-beanManaged-AccountHome</jndi-name>
<target-class>
examples.ejb20.basic.beanManaged.AccountHome
</target-class>
<target-return-value-class-list>
<target-return-value-class>
examples.ejb20.basic.beanManaged.Account
</target-return-value-class>
</target-return-value-class-list>
<description>
test description
</description>
</filter-mapping>
The LogFilter class will look like the following:
public class LogFilter implements Filter {
public void doFilter(Request request,
Response response, FilterChain chain)
throws FilterException {
System.out.println("*** Start Log " +
Filter on method:"+
request.getMethod());
chain.doFilter(request, response);
System.out.println("*** End Log Filter **");
}
}
Here are some combinations that you can use in the filter-config.xml:
Jndi-name |
Target-class |
methods |
Target-return-value-class-list |
description |
* |
java.rmi.Remote |
* |
* |
Proxies any remote object and all return
values |
* |
javax.ejb.EJBHome |
|
javax.ejb.EJBObject |
Proxies all EJBs (home objects for the target-class
and remote objects for the return values). |
* |
examples.ejb20.basic.beanManaged.*
|
* |
examples.ejb20.basic.beanManaged.*
|
Proxies any object that falls in this package. |
ejb20* |
examples.ejb20.basic.beanManaged.AccountHome |
* |
examples.ejb20.basic.beanManaged.Account |
Proxies any jndi-name that starts with ejb20
and matches the target and return value interfaces |
- To create a chain of filters, specify multiple filter mappings. For more
information, see the next section, Configuring a chain of filters.
Configuring a Chain of Filters
This framework creates a chain of filters by creating a list of all the filter
mappings that match an incoming request's JNDI name and/or target-class
and target-return value interface combinations. The ordering of the list is
determined by the following sequence:
- Filters where the filter-mapping element contains a jndi-name that matches
the request are added to the chain in the order they appear in the filter-config.xml
file.
- Filters where the filter-mapping element contains a filter-name that
matches the request are added to the chain after the filters that match
a pattern.
- The last item in the chain is always the originally requested resource.
In your filter class, use the FilterChain.doFilter() method to invoke the
next item in the chain.
Specifying JNDI parameters
A delegate initial context factory must be specified for the framework to
dynamically proxy requests. This is defined in the <initial-context-factory-class> element
and is required.
In the case of Weblogic, it would be something like: weblogic.jndi.WLInitialContextFactory or
for JBoss something like:
org.jnp.interfaces.NamingContextFactory
<jndi>
<initial-context-factory-class>weblogic.jndi.WLInitialContextFactory</initial-context-factory-class>
</jndi>
Environment Prerequisites
- JDK 1.3 or above
- JNDI-capable JVM
- The jndi_filter.jar and filter-config.xml files must be in the system classpath.
- A system property must be set to use the limaye.filter.impl.InitialContextFactory as
the default IntitialContext context factory or the Context.INITIAL_CONTEXT_FACTORY
property must be set to the limaye.filter.impl.InitialContextFactory when
instantiating a new InitialContext.
How It Works
Sun Microsystems's JNDI specification allows for different InitialContextFactory
implementations to be specified at runtime. The technique that I will present
to you utilizes a custom JNDI factory with dynamic proxies.
Listing 1: DefaultInitialContextFactory.java
Some key points to note about this class:
- Implements the javax.naming.spi.InitialContextFactory interface.
- Uses the
AbstractFactory pattern to "plug-in" a JNDI provider
that delegates to a concrete JNDI provider.
- "Bootstraps" using the
following techniques:
- A dynamic proxy of the concrete Context implementation is created.
- A helper
class called Loader is used to:
- Ensure that the delegate InitialContextFactory
class and Context objects are properly initialized (due to a Weblogic
timing issue at startup).
- Retrieve the list of Filter objects from the XML configuration and store
them into memory.
Listing 2: Creates dynamic proxies on the JNDI bound objects,
its return values and invokes the filters.
Some key points to note about this class:
- Intercepts "lookup" operations and creates dynamic proxies
on the JNDI objects.
- Invokes the matched filter and chains.
- Creates dynamic proxies on the matched
return values.
J2EE Servlet Filters vs. JNDI Filters
First and foremost, the major difference between J2EE Servlet filters and
JNDI filters is that Servlet filters are used for web resources and JNDI filters
are not. Here are some important points to note about these two technologies:
- Servlet filters are part of the J2EE specification and are configurable
via the web application container's deployment descriptors. JNDI filters
are decoupled from a J2EE container.
- Servlet filters and JNDI filters share
a very similar API that supports chaining, regular expressions, etc for
pre/post processing.
- Interception occurs on the "client" side.
Servlet filters and JNDI filters can be used hand-in-hand by acting as a "traffic" copy
to perform simple interceptions such as:
- Server-side validation.
- Server-side authentication/authorization.
- Server-side logging, auditing
and statistics.
Alternative Technologies
Aspect-oriented programming (AOP) offers several techniques to allow reusable
code snippets, or "aspects" to be injected within "cross-cuts" of
application code. Several AOP frameworks are available ranging from heavy-weight
implementations to simple, reflection based systems.
This article demonstrates how a custom JNDI provider and dynamic proxies can
be used to provide "non-intrusive" changes to applications. It
can be categorized as a 'simple', lightweight AOP system that uses
Java reflection and dynamic proxies without the use of complicated and proprietary
AOP compiler techniques.
Biography
Bahar Limaye is a System Architect at The College Board. He has extensive
experience is building distributed O-O systems. He can be reached at BLimaye@collegeboard.org.
Resources
http://java.sun.com/products/jndi/docs.html
http://java.sun.com/j2se/1.3/docs/guide/reflection/proxy.html
http://java.sun.com/products/servlet/Filters.html
http://java.sun.com/j2ee/
http://java.sun.com/blueprints/patterns/InterceptingFilter.html
Listings
Listing 1 DefaultInitialContextFactory.java
package limaye.filter.impl;
import java.lang.reflect.Proxy;
import java.util.Hashtable;
import javax.naming.Context;
import javax.naming.NamingException;
import javax.naming.spi.InitialContextFactory;
import limaye.filter.impl.config.DefaultFilterSet;
public class DefaultInitialContextFactory implements InitialContextFactory {
public final static String DELEGATE_INITIAL_CONTEXT_FACTORY =
"JNDI_FILTER.DELEGATE_INITIAL_CONTEXT_FACTORY";
public DefaultInitialContextFactory() {
Loader.initialize(this);
}
public Context getInitialContext(Hashtable environment) throws NamingException {
try
{
DefaultFilterSet filterSet = Loader.getFilterSet();
String initialContextFactoryDelegate = filterSet.getInitialContextFactoryClassName();
if (environment.get(DELEGATE_INITIAL_CONTEXT_FACTORY) != null) {
initialContextFactoryDelegate =
(String) environment.get(DELEGATE_INITIAL_CONTEXT_FACTORY);
}
Class clazz = Class.forName(initialContextFactoryDelegate);
javax.naming.spi.InitialContextFactory icf =
(javax.naming.spi.InitialContextFactory) clazz.newInstance();
Context context = icf.getInitialContext(environment);
if (Loader.getFilterSet() == null) {
return context;
}
else
{
Context ctx = (Context)Proxy.newProxyInstance(context.getClass().getClassLoader(),
new Class[] { Context.class },
new ProxyInvocationHandler(context));
return ctx;
}
} catch (Throwable ignored) {}
return null;
}
}
Listing 2 ProxyInvocationHandler.java
package limaye.filter.impl;
import java.io.Serializable;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import javax.naming.Context;
import limaye.filter.api.FilterException;
import limaye.filter.api.Response;
import limaye.filter.impl.config.DefaultFilterSet;
import limaye.filter.impl.config.DefaultFilterSetEntry;
class ProxyInvocationHandler implements InvocationHandler, Serializable {
private final static String LOOKUP_METHOD_NAME = "lookup";
private final DefaultFilterSetEntry filterSetEntry;
private final Context context;
private final Object targetObject;
ProxyInvocationHandler(Context context) {
this(null, context, context);
}
ProxyInvocationHandler(DefaultFilterSetEntry filterSetEntry,
Context context,
Object targetObject) {
if (context == null) {
throw new IllegalArgumentException("Context is null.");
}
if (targetObject == null) {
throw new IllegalArgumentException("Object is null.");
}
this.filterSetEntry = filterSetEntry;
this.context = context;
this.targetObject = targetObject;
}
public Object invoke(Object proxy,
Method method,
Object[] args)
throws Throwable {
if(Context.class.isAssignableFrom(targetObject.getClass())) {
return createContextProxy(method, args);
}
Response response = null;
try
{
response = doFilter(targetObject, method, args);
return createProxyOnResponse(response);
}
finally
{
}
}
private Response doFilter(Object object, Method method, Object[] args) throws Throwable,
FilterException {
ClassLoader classLoader = getClassLoader(object.getClass());
DefaultRequest request = createRequest(object, method, args);
DefaultResponse response = createResponse();
DefaultFilterChain chain = createFilterChain(object,
classLoader,
filterSetEntry,
context);
try
{
String[] methodNames = filterSetEntry.getKey().getMethodNames();
System.out.println("Method names are: " + methodNames);
System.out.println("Real Method name is: " + method.getName());
for (int i=0; i < methodNames.length; i++) {
if (WildcardPatternMatcher.matches(methodNames[i], method.getName(), true)) {
System.out.println("*** Matches!");
chain.doFilter(request, response);
break;
}
}
} catch (FilterException fe) {
if (fe.getCause() instanceof InvocationTargetException) {
throw ((InvocationTargetException)fe.getCause()).getTargetException();
}
else
{
throw fe;
}
}
finally
{
}
return response;
}
/**
* @param response
* @return
* @throws IllegalArgumentException
*/
private Object createProxyOnResponse(Response response) throws IllegalArgumentException,
ClassNotFoundException {
if (response == null) {
throw new IllegalArgumentException("Response is null.");
}
Object returnValue = response.getReturnValue();
if(returnValue != null) {
System.out.println("Return Value Class Name is: " + returnValue.getClass().getName());
String[] targetReturnValueClassNames = filterSetEntry.getTargetReturnValueClassNames();
Class[] returnValueInterfaces = returnValue.getClass().getInterfaces();
boolean doInterfacesMatch = doInterfacesMatch(targetReturnValueClassNames, returnValue);
if (doInterfacesMatch) {
returnValue = createNewProxyInstance(context, returnValue);
}
}
return returnValue;
}
private Object createContextProxy(Method method, Object[] arguments) throws Throwable {
Object returnValue = null;
DefaultFilterSet filterSet = getFilterSet();
if (filterSet == null) {
return null;
}
try
{
returnValue = method.invoke(targetObject, arguments);
if (returnValue == null) { return null; }
} catch(InvocationTargetException ite) {
throw ite.getTargetException();
}
if(method.getName().equals(LOOKUP_METHOD_NAME) && arguments[0] instanceof String) {
String jndiName = (String)arguments[0];
DefaultFilterSetEntry filterSetEntry = getFilterSetEntry(jndiName, returnValue);
if(filterSetEntry != null) {
returnValue = createNewProxyInstance(context, returnValue, filterSetEntry);
}
}
return returnValue;
}
private Object createNewProxyInstance(Context context, Object returnValue) throws
IllegalArgumentException, ClassNotFoundException {
return createNewProxyInstance(context, returnValue, this.filterSetEntry);
}
private Object createNewProxyInstance(Context context, Object returnValue,
DefaultFilterSetEntry entry) throws IllegalArgumentException, ClassNotFoundException {
ClassLoader classLoader = getClassLoader(returnValue.getClass());
returnValue = Proxy.newProxyInstance(returnValue.getClass().getClassLoader(),
returnValue.getClass().getInterfaces(),
new ProxyInvocationHandler(entry,
context,
returnValue));
return returnValue;
}
private ClassLoader getClassLoader(Class clazz) {
ClassLoader classLoader = null;
if (clazz == null) {
classLoader = Thread.currentThread().getContextClassLoader();
}
else
{
classLoader = clazz.getClassLoader();
if (classLoader == null) {
classLoader = Thread.currentThread().getContextClassLoader();
}
}
return classLoader;
}
private DefaultFilterSetEntry getFilterSetEntry(String jndiName, Object targetObject) {
DefaultFilterSetEntry entry = null;
// try jndi named first
System.out.println("** JNDI Name is: " + jndiName);
DefaultFilterSet filterSet = getFilterSet();
if (filterSet != null) {
DefaultFilterSetEntry[] entries = filterSet.getFilterSetEntries();
if (entries != null) {
for (int i=0; i < entries.length; i++) {
DefaultFilterSetEntry currentEntry = entries[i];
String currentEntryJNDIName = currentEntry.getKey().getJndiName();
String targetInterfaceName = currentEntry.getKey().getTargetClassName();
String[] targetInterfaceNames = new String[] { targetInterfaceName };
boolean isJNDINameMatched = WildcardPatternMatcher.matches(currentEntryJNDIName,
jndiName, true);
System.out.println("Is JNDI Name Matched is: " + isJNDINameMatched);
if (isJNDINameMatched) {
boolean doInterfacesMatch = doInterfacesMatch(targetInterfaceNames,
targetObject);
if (doInterfacesMatch) {
entry = currentEntry;
break;
}
}
}
}
}
return entry;
}
private boolean doInterfacesMatch(String[] targetInterfaceNames, Object targetObject) {
if ( (targetInterfaceNames != null) && (targetObject != null) ) {
Class[] interfaces = targetObject.getClass().getInterfaces();
if (interfaces != null) {
// first check if the names match
for (int i=0; i < interfaces.length; i++) {
String interfaceName = interfaces[i].getName();
for (int j=0; j < targetInterfaceNames.length; j++) {
if (WildcardPatternMatcher.matches(targetInterfaceNames[j], interfaceName, true)) {
System.out.println("*********** Matched Name..");
return true;
}
}
}
// next check if the target interfacenames are assignable from the interface
for (int i=0; i < interfaces.length; i++) {
for (int j=0; j < targetInterfaceNames.length; j++) {
try
{
Class clazz = getClassLoader(interfaces[i]).loadClass(targetInterfaceNames[j]);
if (clazz.isAssignableFrom(interfaces[i])) {
System.out.println("*************** Matched Assignable..");
return true;
}
} catch (ClassNotFoundException ignored) {}
}
}
}
}
return false;
}
private DefaultFilterSet getFilterSet() {
DefaultFilterSet filterSet = Loader.getFilterSet();
return filterSet;
}
private DefaultRequest createRequest(Object object, Method method, Object[] args) {
return new DefaultRequest(object, method, args);
}
private DefaultResponse createResponse() {
return new DefaultResponse();
}
private DefaultFilterChain createFilterChain(Object object,
ClassLoader classLoader,
DefaultFilterSetEntry filterSetEntry,
Context context) {
return new DefaultFilterChain(object, classLoader, filterSetEntry, context);
}
}
PRINTER FRIENDLY VERSION
|