Spring 3.1和Oracle Audit Trail:向触发器提供应用程序数据

时间:2012-07-04 13:33:14

标签: spring oracle11g spring-jdbc

问题参数

  1. Spring 3.1
  2. Oracle 11.2.0.3
  3. Glassfish 2.1 Application Server,提供JDBC连接池。
  4. 问题说明

    我正在现有的一组管理应用程序中改进用户审核,以添加,编辑和删除客户用户。我需要将管理用户的ID存储在与许多表关联的Oracle触发器创建的审计记录中。我希望通过在数据库操作之前在从连接池检索的每个连接上设置Oracle CLIENT_IDENTIFIER属性,然后在数据库操作之后清除该属性,使管理员可以访问管理用户ID。我有一些有用的东西,但我不喜欢它的完成方式。

    问题:

    是否有办法访问连接,以便在数据库操作之前和之后设置Oracle上下文属性?也许某种倾听者会回应一个事件?

    我看过:

    1. 一百万个网页(好吧可能有点夸张,但我用Google搜索了三四个小时)。
    2. 使用DataSourceUtils获取连接。这可以工作,但我真的不想管理连接,我只是想在进出池的方式拦截它们来设置CLIENT_IDENTIFIER属性值。
    3. 覆盖数据源的getConnection方法。由于这在JdbcTemplate中被调用,我无法将应用程序数据传递给该方法。
    4. 我希望春天和/或甲骨文大师会说“好吧很明显,答案是......”而不必破解我的实施,但无论如何它都在这里。如果没有别的,这确实有效,万一有人正在寻找一个想法。

      我的实施:

      所有数据库操作都是使用对注入Dao的JdbcTemplate对象的JdbcOperations引用完成的。添加,编辑和删除操作使用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>
      

      这是用户Dao界面

      public interface UserDao {
      
          void addUser(User newUser,String adminUserId);
      
      }
      

      这是用户Dao类

      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;
      

      正如我所说的那样有效,但由于以下原因,我不喜欢它。

      1. 我必须修改用于设置预准备语句的方法中的连接。
      2. 我必须将此代码添加到我想要审核的每个PreparedStatementCreator或BatchPreparedStatementSetter对象。
      3. 我无法在数据库操作后访问连接,因此我可以清除该属性。
      4. 我真正想要的是一个单点,我可以在连接之前和之后设置属性。

        任何意见或想法都会受到赞赏。

2 个答案:

答案 0 :(得分:3)

Spring有一种优雅的方式来做到这一点。这个例子几乎是你想要的: Spring Data docs

当从数据源获得连接时,使用AOP设置CLIENT_IDENTIFIER。

关闭连接时可以使用另一个切入点。但问题不在于您的应用程序使用了连接池。

答案 1 :(得分:0)

更好的方法是使用连接标签。看一下 oracle.ucp.jdbc.LabelableConnection 界面。