The implementation of filters is a new mechanism being introduced into the Servlets 2.3 standard. This article explores the implications of filters to the Servlet architecture.
It has been a pet peeve of mine that one should not buy into an entire EJB solution just to gain the transparent (container-managed) transactional support for Java objects dealing with relational databases. One could inquire, what is the cost of implementing such a solution in the servlet tier itself? Because transactional support is valuable, whether the solution is distributed or not.
Typically, this transparent transactional support is accomplished by enrolling the active thread with a connection pool manager that the application relies upon for connections. The filtering mechanism is positioned to intercept the calls to an eventual servlet by registering the current thread with a connection pool manager in order to accomplish this. As one can see, the filtering mechanism is ideally suited for interposition, similar in concept to the EJB interposition of remote object calls from the EJB object to the bean instance.
Using the same interposition trick, one could also enroll a logging mechanism with the current thread as well and thereby provide applications with server-side logging that delineates at the thread level, as opposed to interlacing the log messages.
For those types of filters that do not interfere with the request or response, interposition could be adapted even in the current releases of servlets, and can provide an invaluable service to the developers that are currently bogged down by managing their own transactions programmatically. When this is combined with higher level data-access mechanisms (paralleling ADO from Microsoft or TDP(Transparent Data Pipeline) architecture that I have proposed in some previous articles), programmers can be completely oblivious to database connections altogether. This is the stage where developers can write business logic with the same ease that the database developers enjoy today with stored procedures; native and quick. I intend to demonstrate here the ease and simplicity of this process with some concrete code examples.
|
Related Reading
|
The best way to gauge the usefulness of a facility (or framework) is by looking at some sample code that actually uses that facility. As suggested, filters and transactional support are implemented by the framework, and the developer code has no reference to those objects. The servlet programming model is a request/response model, so a developer typically interprets and executes a request by dividing his work into a series of tasks. These tasks could include database queries and updates, and perhaps multiple updates requiring transactional integrity.
The ITask interface is introduced here to represent the concept of a task. Given a task name and arguments, it will perform an abstract task and return an object and throw an exception to indicate exceptional failures. Many tasks can be derived from this task, and this gives us a mechanism to treat and manage all tasks as ITasks. Please keep in mind that these concepts are introduced here just to explain transactional support. In your own program, you are free to have your own class hierarchies and methodology and to never ever refer to ITask.
Interface ITask {
Object executeTask( String taskName, Object Arguments ) throws TaskExecutionException;
}
Let us write a task that allows us to add an employee to a company:
class AddEmployeeTask implements ITask
{
Object executeTask(String taskName, Object args) throws
TaskExecutionException
{
try
{
// Get a connection pool manager
IConnectionPoolManager icpm = Factory.getInstance().getObject(IConnectionPoolManager.NAME);
// From the connection pool manager
// obtain a connection
SQLConnection con = icpm.getConnection("data_source_1");
.. use the connection to store the data in the
database
.. return any required object
}
finally
{
// return the connection back to the pool
icpm.putConnection(con);
}
}
}
Now let us write another task that would allow us to update an employee:
class UpdateEmployeeTask implements ITask
{
Object executeTask(String taskName, Object args) throws
TaskExecutionException
{
try
{
IConnectionPoolManager icpm = Factory.getInstance().getObject(IConnectionPoolManager.NAME);
SQLConnection con = icpm.getConnection("data_source_1");
.. use the connection to store the data in the database
.. return any required object
}
finally
{
icpm.putConnection(con);
}
}
}
Now assume, for some reason, we need to add a collection of employees:
class AddEmployeesTask implements ITask
{
Object executeTask(String taskName, Object args) throws TaskExecutionException
{
// No need to get the connection manager here
// We also assume the enumeration of employees is passed through the args
Enumeration e = (Enumeration)args;
AddEmployeeTask addEmployeeTask = new AddEmployeeTask();
for(;e.hasMoreElements();)
{
addEmployeeTask.executeTask(AddEmployeeTask.NAME, e.nextElement());
}
}
}
If you have noticed, the individual AddEmployeeTask that independently acquires and returns the connection can now work well even, when it is used in a compositional context. This paradigm allows developers to organize their work as independent units and test them while allowing them for future compositions. So each task, whether composed or individual, will always get executed in a transactional context. That means the transactional filter we are talking about will automatically commit or roll back these tasks, depending on the exceptions that were raised. Although I prefer exceptions as the main channel for these automatic
commit/rollbacks, one could imagine a different scheme (either via return codes or http response attributes) that could be implemented. Now it remains to be seen how the transactions are actually commited and rolled back.
|
Let us investigate where this magic is taking place, allowing us to return the same connection when executed on the same thread. Obviously, the trick lies within the implementation of the IConnectionPoolManager class. As a
matter of good practice, the implementation of this class is obtained via a factory, allowing us to migrate to an EJB instance when it is justified, while keeping the rest of the code unchanged.
interface IConnectionPoolManager
{
SQLConnection getConnection(String dataSourceName) throws SQLException;
void putConnection(SQLConnection con) throws SQLException;
}
TransactionalConnectionPool implements IConnectionPoolManager
{
// Hashtable<Thread,SQLConnection>
Hashtable threadVsConnection = new Hashtable();
String m_getConnectionLock = "lock";
SQLConnection getConnection(String datasourceName) throws SQLException
{
SQLConnection con = (SQLConnection)threadVsConnection.get(datasourceName);
if (con != null)
{
return con;
}
// no connection available
synchronized(m_getConnectionLock)
{
SQLConnection con = (SQLConnection)threadVsConnection.get(datasourceName);
if (con != null)
{
return con;
}
con = m_dataSources.get(dataSourceName).getConnection();
threadVsConnection.put(getCurrentThread,con);
}
}
void putConnection(SQLConnection con) throws SQLException
{
.. return connection to the underlying data source either by closing it or returning it
}
// called by the filter to remove any
// connections associated with this thread
void removeAssociation()
{
threadVsConnection.remove(getCurrentThread());
}
// returns the current thread
Thread getCurrentThread()
{
....
}
void commitForThisThread()
{
SQLConnection con = threadVsConnection.get(getCurrentThread());
..commit logic
}
void rollbackForThisThread()
{
SQLConnection con = threadVsConnection.get(getCurrentThread());
.. rollbacklogic
}
}
As you can see, when you ask the connection pool manager to give you a connection, it checks to see if the current thread is already associated with a connection. If it is, the connection pool manager will return the same connection. If not, it will contact the necessary data source, obtain a connection, and register with the current thread. Eventually, the filter will remove this threadVsConnection association when it is no longer required.
Now that we have seen how the connection pool manager coordinates threading with connections, it is time to see how the filters could use this property to effectively demarcate transactions.
The references included in this article are sufficient to understand filters. Unlike EJBs and like Servlets, the spec is small and can be mastered in a single sitting. To summarize the spec: the servlet container will call a series of Java objects that are registered for a given URL. These objects are called filters. Each filter will control the input and output of the downstream filter. The servlet itself is conceptually at the end of this chain. Besides the initialization and destroy methods, there is only one significant method that we need to worry about. And the code below overrides that doFilter method to intercept calls to the intended servlets requiring transactional support.
class TransactionFilter implements Filter
{
// An implementation of the IConnectionPoolManager
// and obtainedat the filter initialization
private TransactionConnectionPoolManager icpm = null;
..
void doFilter( ServletRequest request, ServletResponse response, FilterChain chain)
{
try
{
chain.doFilter(request,response,chain);
icpm = Factory.getInstance().getObject(IConnectionPoolManager.Name);
icpm.commitForThisThread();
}
catch(..)
{
.. analyze exception to see if this requires a rollback
.. for commit
icpm.rollbackForThisThread();
}
finally
{
// remove the connection from the connection pool
if (icpm != null)
{
icpm.removeAssociation();
}
}
}
}
The principle here is quite simple: at the end of the servlet request, just tell the connection pool manager that the transaction has ended (whether for good or bad) by calling its removeAssociation().
The developer is not responsible for commits or rollbacks. When there is no exception, the transaction is automatically commited
Allows a developer to write code as if a task is independent while allowing that task to be part of an another larger composed task
Filters can be selectively placed using the web.xml file in a declarative
fashion for all of the updatable content
Interposition is nicely provided because of the interception priniciples of filters
This sample code is only demonstrating one connection per data source per one thread. This can be extended for multiple data sources, provided the underlying data sources support XAConnections.
Again, this is a lightweight transaction management that is highly practical for majority of the single database solutions
Nevertheless, this can be easily extended for multiple data sources for queries
In the case of updates and multiple databases, they are restricted to one per thread. That is, one thread could be using data source 1 to update, and thread 2 could be using another data source to update. And as mentioned earlier, to update multiple data sources on the same thread, one would at the least require XA support, and possibly JTA support.
|
The same trick could be applied for thread-specific logging as well:
interface ILog
{
void log(String message);
void log(String message, Throwable);
}
class PerThreadLog implements ILog
{
// Hold messages on a thread by thread messages
Hashtable threadVsMesaageVector = new Hashtable();
void log(String message)
{
Vector messageVector =
threadVsMessageVector.get(getCurrentThread());
messageVector.add(..)
..
}
void initializeMessages()
{
threadVsMessageVector.put(getCurrentThread(),new Vector());
}
void dumpMessages()
{
Vector messageVector =
threadVsMessageVector.removet(getCurrentThread());
..lock(stream) // or you can transfer this job to a lower
priority serialized thread
..Stream the message vector to the target destination
}
}
class Log
{
public static log(String message)
{
//One would cache this object in
//production environments
ILog ilog = Factory.getInstance().getObject(ILog.NAME);
ilog.log(message);
}
}
void function1()
{
Log.log("This is a test message");
}
Filters are only going to be available in 2.3 implementations. But one would quickly realize that even in the current implementations, the majority of developers typically settle for a single base servlet solution anyway. So one could very easily incorporate this solution by making the appropriate calls at the beginning and end of the servlet and effectively acheive the same thing. Carrying this out using filters just seems natural, and also provides an additional opportunity to effect the request and response if needed.
Using the suggested architecture here you have relieved yourself of commits and rollbacks, but you are still dealing with conenctions and other JDBC-related entities. Instead, imagine the following code segment:
void printEmployees()
{
Hashtable args = new Hashtable();
args.put("employee_id", "E*");
ICollection empCollection = DataFactory.executeTransaction("GET_EMPLOYEE",args);
IIterator empItr = empCollection.getIterator();
for(empItr.begin();!empItr.atTheEnd();empItr.next())
{
Employee emp = (Employee)(empItr.getCurrent();
System.out.println(emp);
}
empCollection.close();
}
Doing it this way, the DataFactory handles all of the connection details. The only thing needed is the abstract collection.close(). Not only that, but the same interface works for non-relational data sources. The rowset API of JDBC 2.0 and the connector architecture, somewhat, have some
similarities to these ideas. But the concept and implementation behind this is so simple, one can implement all the abstractions outlined here in less than two weeks. Also, see how the collections actually return objects as opposed to simple rows and columns? Well, if I were to explain the details of this, that would be material for another article.
One truism in program optimization is that you don't do it pre-maturely. And 80% of your time is spent on 20% of the code. Obviously, one would benefit more by effecting that 20% first. The same way, in an appication server, lots of things happen locally as opposed to distributedly. I wish EJBs addressed these local issues first before addressing the distributed issues; for example, transactions, factory services, security, representing business concepts as entities, and representing business procedures as tasks. These are fundamentally local issues, if you were to make use of them effectively. All of these are of tremendous value to the servlet tier. Becasue EJBs have extended these issues to the network model, the spec is so large that it is harder to follow all the implications. Java has come to be used so effortlessly because it minimizes the complexity of C++, while preserving the OO architecture. I believe the EJB model has grown too complex trying to address multiple constituencies (distributed transactions, object databases, object to relational mapping, etc.). As a result, it is harder to get a good EJB container that is consistent and relatively cheap. My contention is that a lightweight EJB framework has a great value for the servlet tier. And such a framework would have the following features:
In systems developed with the http thin client model, clustering is always possible, either at the client machine level or user session level, providing the needed scale when the user demand increases.
|
Related Reading
|
Newer is Better by Kevin Jones in JavaPro
Lucid explanation of the new features of Servlets and JSP, including filters and tag libraries.
Servlet 2.3: New Features Exposed by Jason Hunter in Java World
An authoritative source for the new features of the Servlets 2.3 standard. Doesn't cover new JSP, though.
Filter Code with Servlet 2.3 Model by Jason Hunter in Java World
Code samples for a reusable set of filters, including file upload and
visitor monitoring.
Servlets 2.3 Spec itself from Sun
Mandatory (although somewhat terse) reading material for the new spec.
A JSP architecture for Oracle Stored Procedures by Satya Komatineni in
Java Report (print version)
Explains the TDP (Transparent Data Pipeline) architecture in a relational context.
Java/XML Programming for Relational Databases by Satya Komatineni in XML Journal (subscription required)
Explains TDP again, and also pluggable transformational services and
declarative conversion of relational data to XML.
Satya Komatineni is the CTO at Indent, Inc. and the author of Aspire, an open source web development RAD tool for J2EE/XML.
Return to ONJava.com.
Copyright © 2007 O'Reilly Media, Inc.