Sun 10 Jun 2007
Propagating Middle-Tier and Application Users to the DBMS (Part 2 of 3)
Posted by Slavik under Oracle, Uncategorized, technical tips, user identity
As promised, this is the second of a three part blog entry discussing the propagation of middle-tier users to the database. This post will mainly concentrate on the Java side of things. I will show how to use Spring-framework’s excellent transactional support using AOP to add an additional advice, relying on ThreadLocal to pass application user identities from the web tier all the way to the database, and using annotations on the service layer to specify the module and action.
I strongly advise downloading the linked zipped source code at the end of the post if you’re planning on seriously reviewing the code.
OK, let us get down to the gory details:
Our domain object will be the Context that will represent the propagated data.
package com.slaviks_blog.domain;
/**
* The <code>Context</code> class contains context data retrieved from the database.
* @author slavik
*
*/
public class Context
{
private final String action;
private final String module;
private final String clientIdentifier;
public Context(String action, String module, String clientIdentifier)
{
this.action = action;
this.module = module;
this.clientIdentifier = clientIdentifier;
}
public String getAction()
{
return action;
}
public String getClientIdentifier()
{
return clientIdentifier;
}
public String getModule()
{
return module;
}
public String toString()
{
StringBuilder sb = new StringBuilder(”Context - “);
sb.append(”{Action [").append(action).append("], Module [").append(
module).append("]“).append(”Client Identifier [").append(
clientIdentifier).append("]}”);
return sb.toString();
}
}
Next, let?s define the data layer we are going to use in our application. For simplicity, we?ll keep the transactions on this layer.
package com.slaviks_blog.dao;
import com.slaviks_blog.auth.ModuleAndAction;
import com.slaviks_blog.domain.Context;
public interface TestDao
{
@ModuleAndAction(action = “getContext”, module = “Test”)
Context getContext();
}
package com.slaviks_blog.dao.jdbc;
import java.sql.ResultSet;
import java.sql.SQLException;
import org.springframework.jdbc.core.simple.ParameterizedRowMapper;
import org.springframework.jdbc.core.support.JdbcDaoSupport;
import org.springframework.transaction.annotation.Transactional;
import com.slaviks_blog.dao.TestDao;
import com.slaviks_blog.domain.Context;
public class TestDaoJdbc extends JdbcDaoSupport implements TestDao
{
private static final String GET_CONTEXT_SQL = “SELECT sys_context(’USERENV’,'ACTION’) action, ”
+ “sys_context(’USERENV’,'MODULE’) module, ”
+ “sys_context(’USERENV’,'CLIENT_IDENTIFIER’) client_identifier ”
+ “FROM dual”;
static class ContextRowMapper implements ParameterizedRowMapper<Context>
{
public Context mapRow(ResultSet rs, int rowNum) throws SQLException
{
String action = rs.getString(”action”);
String module = rs.getString(”module”);
String clientIdentifier = rs.getString(”client_identifier”);
return new Context(action, module, clientIdentifier);
}
}
@Transactional(readOnly = true)
public Context getContext()
{
return (Context) getJdbcTemplate().queryForObject(GET_CONTEXT_SQL,
new ContextRowMapper());
}
}
Aside from the standard Spring database plumbing stuff, notice that the implementation method is annotated with the Transactional annotation and the interface is annotated with our own ModuleAndAction annotation. I believe that this is the correct way, as transactions are an implementation detail while module and action are not.
Next, let?s create the annotation, the container for client identifiers and the actual class that will do our work ? the advisor to intercept our annotation and set the correct propagation on the connection.
package com.slaviks_blog.auth;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* The annotation to add metadata of action and module to methods inside
* transactions.
* @author slavik
*
*/
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ModuleAndAction
{
public String action();
public String module();
}
package com.slaviks_blog.auth;
/**
* The <code>AuthContainer</code> will contain the actual user details
* coming from various application tiers. It will be used in the transactional
* database tier to set the client identifier in the database.
* @author slavik
*
*/
public class AuthContainer
{
/**
* The currently thread bound user - for simplicity, only a string
*/
private static ThreadLocal<String> currentUser = new ThreadLocal<String>();
public static String getCurrentUser()
{
return currentUser.get();
}
public static void setCurrentUser(String user)
{
if (user == null)
{
currentUser.remove();
}
else
{
currentUser.set(user);
}
}
}
package com.slaviks_blog.auth;
import java.sql.Connection;
import javax.sql.DataSource;
import oracle.jdbc.driver.OracleConnection;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.jdbc.datasource.DataSourceUtils;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;
/**
* This ‘around’ advice will add the details from the annotation such as action and module as well
* as the user details from the ThreadLocal
* @author slavik
*
*/
public class ModuleAndActionAdvice implements MethodInterceptor
{
private DataSource dataSource;
private PlatformTransactionManager transactionManager;
public Object invoke(MethodInvocation mi) throws Throwable
{
// Make sure we are in a transaction, else throw exception
TransactionDefinition definition =
new DefaultTransactionDefinition(TransactionDefinition.PROPAGATION_MANDATORY);
TransactionStatus status = transactionManager.getTransaction(definition);
if (status.isCompleted())
{
throw new IllegalStateException(”Transaction is already completed”);
}
// Get the current connection bound to this transaction.
Connection conn = DataSourceUtils.getConnection(dataSource);
// We can always use a connection extractor if using commons-dbcp, etc.
if (conn instanceof OracleConnection)
{
// A minor hack to get the annotation on the interface.
ModuleAndAction ma = mi.getThis().getClass().getInterfaces()[0]
.getMethod(mi.getMethod().getName(),
mi.getMethod().getParameterTypes()).getAnnotation(
ModuleAndAction.class);
if (ma != null)
{
String metrics[] = new String[OracleConnection.END_TO_END_STATE_INDEX_MAX];
// The details from the annotation
metrics[OracleConnection.END_TO_END_ACTION_INDEX] = ma.action();
metrics[OracleConnection.END_TO_END_MODULE_INDEX] = ma.module();
// The client identifier from the external tier (be that web or something else)
metrics[OracleConnection.END_TO_END_CLIENTID_INDEX] = AuthContainer.getCurrentUser();
// Set these metrics
((OracleConnection) conn).setEndToEndMetrics(metrics, (short) 0);
}
}
Object rc = null;
try
{
rc = mi.proceed();
}
finally
{
// Clear the metrics
if (conn instanceof OracleConnection)
{
String metrics[] = new String[OracleConnection.END_TO_END_STATE_INDEX_MAX];
metrics[OracleConnection.END_TO_END_ACTION_INDEX] = null;
metrics[OracleConnection.END_TO_END_MODULE_INDEX] = null;
metrics[OracleConnection.END_TO_END_CLIENTID_INDEX] = null;
((OracleConnection) conn).setEndToEndMetrics(metrics,
Short.MIN_VALUE);
}
}
return rc;
}
public void setTransactionManager(
PlatformTransactionManager transactionManager)
{
this.transactionManager = transactionManager;
}
public void setDataSource(
DataSource dataSource)
{
this.dataSource = dataSource;
}
}
And finally, let’s tie everything with a Spring configuration file:
<?xml version=”1.0″ encoding=”UTF-8″?>
<beans xmlns=”http://www.springframework.org/schema/beans”
xmlns:xsi=”http://www.w3.org/2001/XMLSchema-instance”
xmlns:aop=”http://www.springframework.org/schema/aop”
xmlns:tx=”http://www.springframework.org/schema/tx”
xsi:schemaLocation=”http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd”>
<!–================================================================–>
<!– Datasource –>
<!–================================================================–>
<bean id=”dataSource” class=”oracle.jdbc.pool.OracleDataSource”
destroy-method=”close”>
<property name=”URL”
value=”jdbc:oracle:thin:@192.168.17.128:1521:slavdev” />
<property name=”user” value=”scott” />
<property name=”password” value=”tiger” />
<property name=”connectionCachingEnabled” value=”true” />
</bean>
<!–================================================================–>
<!– Transaction Manager for a datasource –>
<!–================================================================–>
<bean id=”transactionManager”
class=”org.springframework.jdbc.datasource.DataSourceTransactionManager”>
<property name=”dataSource” ref=”dataSource” />
</bean>
<tx:annotation-driven transaction-manager=”transactionManager” />
<bean id=”contextAdvice”
class=”com.slaviks_blog.auth.ModuleAndActionAdvice”>
<property name=”dataSource” ref=”dataSource” />
<property name=”transactionManager” ref=”transactionManager” />
</bean>
<bean id=”contextPointcut”
class=”org.springframework.aop.support.annotation.AnnotationMatchingPointcut”>
<constructor-arg type=”java.lang.Class” value=”" />
<constructor-arg type=”java.lang.Class” value=”com.slaviks_blog.auth.ModuleAndAction” />
</bean>
<bean id=”contextAdvisor”
class=”org.springframework.aop.support.DefaultPointcutAdvisor”>
<property name=”advice” ref=”contextAdvice” />
<property name=”pointcut” ref=”contextPointcut” />
</bean>
<!–================================================================–>
<!– Data Access Objects (DAOs) –>
<!–================================================================–>
<bean id=”testDao” class=”com.slaviks_blog.dao.jdbc.TestDaoJdbc”>
<property name=”dataSource”>
<ref local=”dataSource”></ref>
</property>
</bean>
</beans>
The last thing we should do is to provide a simple unit test to test everything we’ve done so far.
package com.slaviks_blog.dao.jdbc;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.support.FileSystemXmlApplicationContext;
import org.springframework.test.AbstractTransactionalDataSourceSpringContextTests;
import com.slaviks_blog.auth.AuthContainer;
import com.slaviks_blog.dao.TestDao;
import com.slaviks_blog.domain.Context;
public class TestDaoJdbcTest extends
AbstractTransactionalDataSourceSpringContextTests
{
private TestDao testDao;
/**
* Specify all Spring config files needed for the test.
*
* @return
*/
protected String[] getConfigLocations()
{
return new String[]
{
“/war/WEB-INF/config/spring/data-access.xml”
};
}
/**
* Override the default implementation to use FileSystem path instead of
* Classpath, because the lookup of the spring config xml files is different
* when running as a web-app, as opposed to running as a test-case.
*
* @param locations
* @return
*/
protected ConfigurableApplicationContext loadContextLocations(
String[] locations)
{
return new FileSystemXmlApplicationContext(locations);
}
public void testContextIsSet()
{
AuthContainer.setCurrentUser(”Slavik”);
Context ctx = testDao.getContext();
assertEquals(”Action was not set”, “getContext”, ctx.getAction());
assertEquals(”Module was not set”, “Test”, ctx.getModule());
assertEquals(”ClientID was not set”, “Slavik”, ctx.getClientIdentifier());
}
public void setTestDao(TestDao testDao)
{
this.testDao = testDao;
}
}
Source files for the example project
In the next blog entry, I will describe a way to set the action, module and client identifier in a Java application for which we have no source code. We will make this magic happen using AspectJ.
August 31st, 2007 at 6:10 am
Hi.
Excellent article. This is exactly something I have been looking for. Do you know when you will be publishing part 3?
August 31st, 2007 at 6:40 am
Hi Paul,
Thanks for the comment. I planned to publish part 3 a while ago but was totally swamped with work and travels. I’ll be publishing part 3 next week after finally freeing some weekend time to finish it.
September 3rd, 2007 at 5:42 am
OK. Excellent.
I have another question for you. We have tried to use your code with our database, but we can not get it to work. The test runs fine without any exceptions, but the assertions fails. We have tried both set/clearClientIdentifier, and the End-to-end metrics API. Is there any special settings we need to enable on the database side? The only thing we have done is to set audit=DB in ora.ini file.
September 5th, 2007 at 7:08 am
Hi Paul,
The database requires no special settings (not even for the audit to be enabled). What database version are you running?
September 10th, 2007 at 11:52 pm
We are using Oracle 9i R2.
September 11th, 2007 at 6:40 am
I was able to make it work using another method though… If I disregard everything that has to do with action and module, and only care about clientid, I can make it work if I do a PL/SQL call in the advice instead of using the jdbc driver API.
So by changing connection.setEndToEndMetrics to a PL/SQL call: “begin dbms_session.set_identifier(’Slavik’);end;”
This is per suggestion in the article http://www.oracle.com/technology/tech/java/oc4j/pdf/j2ee-cmp-with-vpd.pdf
I have no idea why the jdbc driver API does not work for us, but I guess using the PL/SQL call works just as fine.