问题参数:
- 春天 3.1
- 甲骨文 11.2.0.3
- Glassfish 2.1 应用服务器,提供 JDBC 连接池。
问题描述:
我正在改进一组现有管理应用程序中的用户审计,以添加、编辑和删除客户用户。我需要将管理用户的 ID 存储在由与多个表关联的 Oracle 触发器创建的审计记录中。我想通过在数据库操作之前从连接池中检索到的每个连接上设置 Oracle CLIENT_IDENTIFIER 属性来使触发器可以访问管理用户 ID,然后在数据库操作之后清除该属性。我有一些有用的东西,但我真的不喜欢它的完成方式。
问题:
有没有办法访问连接,以便可以在数据库操作之前和之后设置 Oracle 上下文属性?也许某种听众响应事件?
我看过:
- 一百万个网页(好吧,也许这有点夸张,但我已经用谷歌搜索了三四个小时)。
- 使用 DataSourceUtils 获取连接。这可行,但我真的不想管理连接,我只想在进出池的途中拦截它们以设置 CLIENT_IDENTIFIER 属性值。
- 覆盖数据源的 getConnection 方法。由于这在 JdbcTemplate 内的某处被调用,因此我无法将应用程序数据获取到该方法。
我希望 Spring 和/或 Oracle 大师会说“嗯,这很明显,答案是……”,而不必破解我的实现,但无论如何都是这样。如果没有别的,如果有人正在寻找一个想法,这确实有效。
我的实现:
所有数据库操作都是使用 JdbcOperations 对注入到 Dao 中的 JdbcTemplate 对象的引用来完成的。添加、编辑和删除操作使用 JdbcOperations 查询方法,传递 PreparedStatementCreator 或 BatchPreparedStatementSetter。我在这些对象的回调方法(createPreparedStatement 或 setValues)中访问应用程序服务器连接池提供的 java.sql.Connection 对象,以设置 CLIENT_IDENTIFIER 属性。
applicationContext.xml 数据源配置:
<!-- Setup the datasource -->
<bean id="dataSource" class="org.springframework.jndi.JndiObjectFactoryBean">
<property name="jndiName" value="jdbc/IpOneDatabasePool"/>
</bean>
<!-- Setup the transaction manager -->
<bean id="txManager" class="org.springframework.transaction.jta.JtaTransactionManager" />
<tx:advice id="txAdvice" transaction-manager="txManager">
<tx:attributes>
<tx:method name="get*" read-only="true"/>
<tx:method name="*"/>
</tx:attributes>
</tx:advice>
<!-- Associate the transaction manager with objects that must be managed. -->
<aop:config>
<aop:pointcut id="userDaoOperation" expression="execution(* com.myCompany.IpOne.dao.UserDaoImpl.*(..))"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="userDaoOperation"/>
</aop:config>
<!-- Bean providing access to the various prepared statement objects -->
<bean id="daoHelperFactory" class="com.myCompany.IpOne.dao.DaoHelperFactoryImpl" />
<!-- Bean that allows setting of the client identifier for the audit trail -->
<bean id="databaseContextEditor" class="com.myCompany.IpOne.dao.OracleDatabaseContextEditor" />
<!-- Dao that manages persistence of User objects -->
<bean id="userDao" class="com.myCompany.IpOne.dao.UserDaoImpl" >
<property name="dataSource" ref="dataSource"/>
<property name="licenseDao" ref="licenseDao"/>
<property name="appPropertyManager" ref="appPropertyManager"/>
<property name="maximumLicensesPerUserKey" value="max_licences_per_user"/>
<property name="daoHelperFactory" ref="daoHelperFactory"/>
</bean>
这是用户道界面
public interface UserDao {
void addUser(User newUser,String adminUserId);
}
这是用户道类
public class UserDaoImpl implements UserDao{
private JdbcOperations jdbcOperations;
public void setDataSource(DataSource dataSource) {
this.jdbcOperations = new JdbcTemplate(dataSource);
}
public void addUser(User newUser,String adminUserId) {
PreparedStatementCreator insertUserStatement =
this.daoHelperFactory.getInsertUserStatement(newUser,adminUserId);
KeyHolder keyHolder = this.daoHelperFactory.getKeyHolder();
this.jdbcOperations.update(insertUserStatement, keyHolder);
newUser.setUserId(keyHolder.getKey().intValue());
}
}
此类提供对应用程序上下文的访问。
public class ApplicationContextProvider implements ApplicationContextAware{
private static ApplicationContext ctx = null;
public static ApplicationContext getApplicationContext() {
return ctx;
}
public void setApplicationContext(ApplicationContext ctx) throws BeansException {
this.ctx = ctx;
}
}
提供 Dao 使用的各种对象的类的接口。
public interface DaoHelperFactory {
PreparedStatementCreator getInsertUserStatement(User user,String adminUserId);
KeyHolder getKeyHolder();
}
此类只是 PreparedStatementCreator 和 BatchPreparedStatementSetter 对象以及 Dao 使用的其他对象的工厂。我已对其进行了更改,以提供将数据库上下文属性实际设置为返回的各种对象的对象。
public class DaoHelperFactoryImpl implements DaoHelperFactory{
private DatabaseContextEditor getDatabaseContextEditor(){
ApplicationContext appContext = ApplicationContextProvider.getApplicationContext();
DatabaseContextEditor databaseContextEditor = (DatabaseContextEditor) appContext.getBean("databaseContextEditor");
return databaseContextEditor;
}
public KeyHolder getKeyHolder(){
return new GeneratedKeyHolder();
}
public PreparedStatementCreator getInsertUserStatement(User user,String adminUserId){
InsertUser insertUser = new InsertUser(user,adminUserId);
insertUser.setDatabaseContextEditor(getDatabaseContextEditor());
return insertUser;
}
}
这是设置数据库上下文的类的接口
public interface DatabaseContextEditor {
public DatabaseContextEditor getInstance();
public void setClientIdentifier(Connection connection,String clientIdentifier) throws SQLException;
}
这是为 Oracle 执行此操作的类
public class OracleDatabaseContextEditor implements DatabaseContextEditor{
public void setClientIdentifier(Connection connection,String clientIdentifier) throws SQLException{
OracleJdbc4NativeJdbcExtractor extractor = new OracleJdbc4NativeJdbcExtractor();
oracle.jdbc.OracleConnection oracleConnection = null;
if(!(connection instanceof oracle.jdbc.OracleConnection))
oracleConnection = (oracle.jdbc.OracleConnection) extractor.getNativeConnection(connection);
else
oracleConnection = (oracle.jdbc.OracleConnection)connection;
String[] metrics = new String[OracleConnection.END_TO_END_STATE_INDEX_MAX];
metrics[OracleConnection.END_TO_END_CLIENTID_INDEX]=clientIdentifier;
oracleConnection.setEndToEndMetrics(metrics,(short)0);
}
public DatabaseContextEditor getInstance(){
return new OracleDatabaseContextEditor();
}
}
该类是用于添加用户的 PreparedStatementCreator
public class InsertUser implements PreparedStatementCreator {
User insertUser;
/** This is the admin user Id I need to store */
String adminUserId;
private final String SQL = "INSERT INTO SC_USR (" +
"USR_ID, USR_SSO_NAME, USR_PH_NO, USR_SIP_NAME," +
"USR_SIP_PSWD, USR_SIP_DISP_NAME, USR_SIP_DOMAIN, USR_SIP_PROXY," +
" USR_CREATED_BY, USR_CREATED_DATETIME) " +
"VALUES (SEQ_SC_USR_ID.NEXTVAL, ?, ?, ?, ?, ?, ?, ?, ?, SYSTIMESTAMP)";
private final String GENERATED_COLUMNS[] = {"USR_ID"};
/** Object that provides functionality for setting values in the database context */
private DatabaseContextEditor databaseContextEditor;
public InsertUser(User user,String adminUserId){
this.insertUser = user;
this.adminUserId = adminUserId;
}
public PreparedStatement createPreparedStatement(Connection connection) throws SQLException {
this.databaseContextEditor.setClientIdentifier(connection, adminUserId);
PreparedStatement preparedStatement = connection.prepareStatement(SQL,GENERATED_COLUMNS);
int i=1;
preparedStatement.setString(i++,this.insertUser.getSsoName());
preparedStatement.setString(i++,this.insertUser.getPhoneNumber());
preparedStatement.setString(i++,this.insertUser.getSipName());
preparedStatement.setString(i++,this.insertUser.getSipPassword());
preparedStatement.setString(i++,this.insertUser.getSipDisplayName());
preparedStatement.setString(i++,this.insertUser.getSipDomain());
preparedStatement.setString(i++,this.insertUser.getSipProxy());
preparedStatement.setString(i++,this.insertUser.getCreatedBy().name());
return preparedStatement;
}
public void setDatabaseContextEditor(DatabaseContextEditor databaseContextEditor) {
this.databaseContextEditor = databaseContextEditor;
}
}
我要审核的每个表上都有“AFTER DELETE OR INSERT OR UPDATE”触发器。每个表都有一个对应的审计表。他们从上下文中提取 CLIENT_IDENTIFIER 并在适当的审计表中插入一行。这是一个样本。
CREATE OR REPLACE TRIGGER IPONE_DEV_USER.SC_USR$AUDTRG
AFTER DELETE OR INSERT OR UPDATE
ON IPONE_DEV_USER.SC_USR
REFERENCING NEW AS NEW OLD AS OLD
FOR EACH ROW
DECLARE
v_operation VARCHAR2(10) := NULL;
v_admin_user_id VARCHAR2(30);
BEGIN
v_admin_user_id := SYS_CONTEXT('USERENV', 'CLIENT_IDENTIFIER');
IF INSERTING THEN
v_operation := 'INS';
ELSIF UPDATING THEN
v_operation := 'UPD';
ELSE
v_operation := 'DEL';
END IF;
IF INSERTING OR UPDATING THEN
INSERT INTO SC_USR$AUD (
USR_ID,
USR_SSO_NAME,
USR_PH_NO,
USR_SOME_VALUE1,
USR_SOME_VALUE2,
USR_SOME_VALUE3,
USR_SOME_VALUE4,
USR_CREATED_BY,
USR_SOME_VALUE5,
USR_SOME_VALUE6,
aud_action,aud_timestamp,aud_user) VALUES (
:new.USR_ID,
:new.USR_SSO_NAME,
:new.USR_PH_NO,
:new.USR_SOME_VALUE1,
:new.USR_SOME_VALUE2,
:new.USR_SOME_VALUE3,
:new.USR_CREATED_DATETIME,
:new.USR_CREATED_BY,
:new.USR_SOME_VALUE4,
:new.USR_SOME_VALUE5,
v_operation,SYSDATE,v_admin_user_id);
ELSE
INSERT INTO SC_USR$AUD (
USR_ID,
USR_SSO_NAME,
USR_PH_NO,
USR_SIP_NAME,
USR_SIP_PSWD,
USR_SIP_DISP_NAME,
USR_CREATED_DATETIME,
USR_CREATED_BY,
USR_SIP_DOMAIN,
USR_SIP_PROXY,
aud_action,aud_timestamp,aud_user) VALUES (
:old.USR_ID,
:old.USR_SSO_NAME,
:old.USR_PH_NO,
:old.USR_SIP_NAME,
:old.USR_SIP_PSWD,
:old.USR_SIP_DISP_NAME,
:old.USR_CREATED_DATETIME,
:old.USR_CREATED_BY,
:old.USR_SIP_DOMAIN,
:old.USR_SIP_PROXY,
v_operation,SYSDATE,v_admin_user_id);
END IF;
END;
正如我所说,这很有效,但我不喜欢它,原因如下。
- 我必须修改用于设置准备好的语句的方法中的连接。
- 我必须将此代码添加到要审核的每个 PreparedStatementCreator 或 BatchPreparedStatementSetter 对象中。
- 数据库操作后我无权访问连接,因此我可以清除该属性。
我真正想要的是一个点,我可以在连接之前和之后设置属性。
任何输入或想法将不胜感激。