参考文章:http://www.iteye.com/topic/477134
项目中要对数据的更新做审计,比如订单的每一项的变化,会员主要信息的变化等。
老版的程序是这样处理的,在更新前先查一次数据库,然后依次比对各列,得到修改变化的信息。但在我们新版程序中,由于用到了singleSesssion,那么在一个Session中不允许出现两个ID相同的对象,所以老路是走不通的。
随后到网上搜索,就找到了上面这篇文章,博主写得很好,按照这个思路我也把例子写出来了。但是后面还碰到了几个问题,在这里也分享一下,希望能对大家有帮助。
现有主要配置如下:
- <bean id="crudListener" class="com.liut.core.listener.CrudListener">
- </bean>
按照例子程序可以输出log日志,说明监听器是起作用的。那么接下来要把日志信息写入数据库,在监听器中引入IAuditLogService审计日志服务接口,用来保存日志信息。新的配置如下:
- <bean id="crudListener" class="com.liut.core.listener.CrudListener">
- <property name="auditLogService"><ref bean="auditLogService" /></property>
- </bean>
启动应用控制台报错,是因为这违反了spring的原则,auditLogService引用了sessionFactory,现在sessionFactory的监听器里要引用auditLogService,当然是不允许的。好吧换一种方式
- /**
- * 初始化审计日志服务类
- * @Title: init void
- * @throws
- */
- private synchronized void init(){
- if(auditLogService == null){
- WebApplicationContext context =WebApplicationContextUtils.getWebApplicationContext(SessionAgentTool.getSession().getRealHttpSession().getServletContext());
- auditLogService =(IAuditLogService)context.getBean("auditLogService");
- }
- }
(其中SessionAgentTool是工具类,可以得到当前session)
结果执行修改动作时,一点反应没有,没有记录日志,也没有错误。控制台的日志信息照常输出。这个汗。。。啊!经过数小时的修改、测试,终未果。静下心来分析整个过程,终于发现问题了,在PostUpdateEventListener中,调用保存日志的方法,这时PostUpdateEventListener还会被触发,这不成死循环了?
问题找到就好办了,现在重写保存的方法,用SQL绕过hibernate监听。OK,测试通过!
到这里还没有结束,刚才一直用码表来测试,现在来测试订单,MYGOD!Hibernate在执行flush时数据下标越界【Caused by: java.lang.IndexOutOfBoundsException: Index: 1, Size: 0
】,这怎么可能?后来发现了规律,只在在事务中间的对象记录日志就会有这个错误。难道是事务的问题???看看现在是这样配的:
- <bean id="transInterceptor" class="org.springframework.transaction.interceptor.TransactionInterceptor" >
- <property name="transactionManager">
- <ref bean="transactionManager"/>
- </property>
- <property name="transactionAttributes">
- <props>
- <prop key="query*">PROPAGATION_SUPPORTS,readOnly</prop>
- <prop key="find*">PROPAGATION_SUPPORTS,readOnly</prop>
- <prop key="get*">PROPAGATION_SUPPORTS,readOnly</prop>
- <prop key="select*">PROPAGATION_SUPPORTS,readOnly</prop>
- <prop key="load*">PROPAGATION_SUPPORTS,readOnly</prop>
- <prop key="stat*">PROPAGATION_SUPPORTS,readOnly</prop>
- <prop key="*">PROPAGATION_REQUIRED</prop>
- </props>
- </property>
- </bean>
隐约记得有个子事务的说法也没有用过,搜索一下,修改如下
- <bean id="transInterceptor" class="org.springframework.transaction.interceptor.TransactionInterceptor" >
- <property name="transactionManager">
- <ref bean="transactionManager"/>
- </property>
- <property name="transactionAttributes">
- <props>
- <prop key="query*">PROPAGATION_SUPPORTS,readOnly</prop>
- <prop key="find*">PROPAGATION_SUPPORTS,readOnly</prop>
- <prop key="get*">PROPAGATION_SUPPORTS,readOnly</prop>
- <prop key="select*">PROPAGATION_SUPPORTS,readOnly</prop>
- <prop key="load*">PROPAGATION_SUPPORTS,readOnly</prop>
- <prop key="stat*">PROPAGATION_SUPPORTS,readOnly</prop>
- <span style="color: #ff0000;"><prop key="log">PROPAGATION_REQUIRES_NEW</prop></span>
- <prop key="*">PROPAGATION_REQUIRED</prop>
- </props>
- </property>
- </bean>
注意红色部分是添加项,也就是SQL保存日志的方法名,让它启用子事务。重启测试,OK,万事大吉了!
后面针对需求做了简单配置,因为不是所有的表都需要审计,而且数据量也会超大的,那么可以做到想记哪个表就记哪个表。下面是主要代码片
监听器配置文件
- <!--
- 审计日志配置策略:
- 1.可用的关键字有:insertAllow,insertDeny,updateAllow,updateDeny,deleteAllow,deleteDeny
- 2.没有配置对象的策略,所有字段不记录
- 3.allow和deny都配置的按allow验证,并忽略deny
- 4.allow和deny都允许指定all关键字
- 5.多个字段用英文逗号隔开
- -->
- <bean id="crudListener" class="com.liut.core.listener.CrudListener">
- <property name="auditableEntitys">
- <map>
- <entry key="Order">
- <map>
- <entry key="insertAllow">
- <value>ordNo</value>
- </entry>
- <entry key="updateAllow">
- <value>all</value>
- </entry>
- <entry key="deleteAllow">
- <value>ordNo</value>
- </entry>
- </map>
- </entry>
- <entry key="OrderDetail">
- <map>
- <entry key="insertDeny">
- <value>all</value>
- </entry>
- <entry key="updateAllow">
- <value>all</value>
- </entry>
- <entry key="deleteDeny">
- <value>all</value>
- </entry>
- </map>
- </entry>
- <entry key="User">
- <map>
- <entry key="insertDeny">
- <value>userName</value>
- </entry>
- <entry key="updateAllow">
- <value>all</value>
- </entry>
- <entry key="deleteDeny">
- <value>userName</value>
- </entry>
- </map>
- </entry>
- <entry key="UserInfo">
- <map>
- <entry key="insertAllow">
- <value>email</value>
- </entry>
- <entry key="updateAllow">
- <value>all</value>
- </entry>
- <entry key="deleteAllow">
- <value>email</value>
- </entry>
- </map>
- </entry>
- </map>
- </property>
- </bean>
sessionFactory配置文件
- <bean id="sessionFactory" class="org.springframework.orm.hibernate3.LocalSessionFactoryBean">
- <property name="dataSource"><ref bean="datasource"/></property>
- 。。。(省略部分配置)
- <property name="eventListeners">
- <map>
- <entry key="post-insert">
- <ref local="crudListener"/>
- </entry>
- <entry key="post-update">
- <ref local="crudListener"/>
- </entry>
- <entry key="post-delete">
- <ref local="crudListener"/>
- </entry>
- </map>
- </property>
- </bean>
CrudListener.java
- package com.liut.core.listener;
- 。。。(省略import信息)
- /**
- * Hibernate增删改监听器,记录审记日志
- * <p>类名称:EntityCrudListener</p>
- * <p>类描述:post方法在数据更新后执行,pre方法在数据更新前执行 </p>
- * <p>创建人:LiuTong</p>
- * <p>创建时间:Sep 26, 2012 2:39:52 PM </p>
- * <p>修改人:LiuTong</p>
- * <p>修改时间:Sep 26, 2012 2:39:52 PM </p>
- * <p>修改备注: </p>
- * @version
- */
- public class CrudListener implements PostInsertEventListener,
- PostUpdateEventListener, PostDeleteEventListener {
- private static final long serialVersionUID = 1L;
- private static final String INSERT = "INSERT";
- private static final String UPDATE = "UPDATE";
- private static final String DELETE = "DELETE";
- /**
- * 允许或不允许全部时,指定all即可
- */
- public static final String ALL = "all";
- private static final Log logger = LogFactory.getLog(CrudListener.class);
- /**
- * 配置审计对象的记录策略
- */
- private Map<String,Map<String,String>> auditableEntitys;
- /**
- * 审计日志服务类
- */
- private IAuditLogService auditLogService;
- @Override
- public void onPostInsert(PostInsertEvent event) {
- if (auditableEntitys.containsKey(event.getEntity().getClass().getSimpleName())
- && event.getEntity() instanceof BaseEntity) {
- init();
- // 保存 插入日志
- AuditLog log = newAuditLog();
- log.setTableName(event.getEntity().getClass().getSimpleName());
- log.setDataId(((BaseEntity)event.getEntity()).getId().toString());
- log.setDoType(INSERT);
- {
- Object[] state = event.getState();
- String[] fields = event.getPersister().getPropertyNames();
- String content = "";
- if(state != null && fields != null
- && state.length == fields.length){
- for(int i = 0 ; i < fields.length ; i ++){
- if(isLog(event.getEntity(),fields[i],INSERT)){
- content = addStr(null, state, fields,
- content, i);
- }
- }
- }
- log.setContent("[" + content + "]");
- }
- logger.debug("插入审计日志 INSERT AuditLog ");
- insert(log);
- }
- }
- @Override
- public void onPostUpdate(PostUpdateEvent event) {
- if (auditableEntitys.containsKey(event.getEntity().getClass().getSimpleName())
- && event.getEntity() instanceof BaseEntity) {
- init();
- // 保存 修改日志
- AuditLog log = newAuditLog();
- log.setTableName(event.getEntity().getClass().getSimpleName());
- log.setDataId(((BaseEntity)event.getEntity()).getId().toString());
- log.setDoType(UPDATE);
- {
- Object[] oldState = event.getOldState();
- Object[] newState = event.getState();
- String[] fields = event.getPersister().getPropertyNames();
- String content = "";
- if(oldState != null && newState != null && fields != null
- && oldState.length == newState.length && oldState.length == fields.length){
- for(int i = 0 ; i < fields.length ; i ++){
- if(isLog(event.getEntity(),fields[i],UPDATE)){
- if((newState[i] == null && oldState[i] != null)
- || (newState[i] != null && !newState[i].equals(oldState[i]) )){
- content = addStr(oldState, newState, fields,
- content, i);
- }
- }
- }
- }
- log.setContent("[" + content + "]");
- }
- logger.debug("插入审计日志 UPDATE AuditLog ");
- insert(log);
- }
- }
- @Override
- public void onPostDelete(PostDeleteEvent event) {
- if (auditableEntitys.containsKey(event.getEntity().getClass().getSimpleName())
- && event.getEntity() instanceof BaseEntity) {
- init();
- // 保存 删除日志
- AuditLog log = newAuditLog();
- log.setTableName(event.getEntity().getClass().getSimpleName());
- log.setDataId(((BaseEntity)event.getEntity()).getId().toString());
- log.setDoType(DELETE);
- {
- Object[] state = event.getDeletedState();
- String[] fields = event.getPersister().getPropertyNames();
- String content = "";
- if(state != null && fields != null
- && state.length == fields.length){
- for(int i = 0 ; i < fields.length ; i ++){
- if(isLog(event.getEntity(),fields[i],DELETE)){
- content = addStr(null, state, fields,
- content, i);
- }
- }
- }
- log.setContent("[" + content + "]");
- }
- logger.debug("插入审计日志 DELETE AuditLog ");
- insert(log);
- }
- }
- /**
- * 记录审计日志
- * @Title: insert
- * @param log void
- * @throws
- */
- private void insert(AuditLog log) {
- auditLogService.log(log);
- }
- /**
- * 创建日志对象,同时设置操作人操作时间等信息
- * @Title: newAuditLog
- * @return AuditLog
- * @throws
- */
- private AuditLog newAuditLog() {
- Visit visit = SessionAgentTool.getCurrentVisit();
- AuditLog log = new AuditLog();
- log.setDoTime(TimeUtils.getCurrentStandardTime());
- log.setEditorId(visit.getId());
- log.setEditorName(visit.getUserOptionName());
- return log;
- }
- /**
- * 验证策略是否允许记录日志,规则如下:
- * <ol>
- * <li>可用的关键字有:insertAllow,insertDeny,updateAllow,updateDeny,deleteAllow,deleteDeny</li>
- * <li>没有配置对象的策略,所有字段不记录</li>
- * <li>allow和deny都配置的按allow验证,并忽略deny</li>
- * <li>allow和deny都允许指定all关键字</li>
- * <li>多个字段用英文逗号隔开</li>
- * </ol>
- * @Title: isLog
- * @param entity
- * @param string
- * @param string2
- * @return boolean
- * @throws
- */
- private boolean isLog(Object entity, String field, String op) {
- Map<String,String> entityConfig = auditableEntitys.get(entity.getClass().getSimpleName());
- if(entityConfig != null){
- String allowFields = entityConfig.get(op.toLowerCase() + "Allow");
- if(allowFields != null){
- if(allowFields.equals(ALL)
- || containsField(allowFields,field)){
- //配置ALL,所有允许
- return true;
- }
- }else{
- String denyFields = entityConfig.get(op.toLowerCase() + "Deny");
- if(denyFields != null){
- if(denyFields.equals(ALL)
- || containsField(denyFields,field)){
- //配置ALL,所有不允许
- return false;
- }
- }
- return true;
- }
- }else{
- }
- //缺省不记录
- return false;
- }
- /**
- * 配置中是否包含当前字段
- * @Title: containsField
- * @param fields
- * @param field
- * @return boolean
- * @throws
- */
- private boolean containsField(String fields, String field) {
- String[] fs = fields.split(",");
- for(String f : fs){
- if(f.equals(field)){
- return true;
- }
- }
- return false;
- }
- /**
- * 向content追加一个修改项
- * @Title: addStr
- * @param oldState
- * @param newState
- * @param fields
- * @param content
- * @param i
- * @return String
- * @throws
- */
- private String addStr(Object[] oldState, Object[] newState,
- String[] fields, String content, int i) {
- if(content.length() < 1000){
- if(content.length() > 0){
- content += ",";
- }
- content += "{columnName:\"" + fields[i] +
- "\",oldValue:\"" + (oldState == null ? "" : String.valueOf(oldState[i])) +
- "\",newValue:\"" + String.valueOf(newState[i]) + "\"}";
- }else{
- logger.warn("审计长度超过1000");
- }
- return content;
- }
- /**
- * 初始化审计日志服务类
- * @Title: init void
- * @throws
- */
- private synchronized void init(){
- if(auditLogService == null){
- WebApplicationContext context =WebApplicationContextUtils.getWebApplicationContext(SessionAgentTool.getSession().getRealHttpSession().getServletContext());
- auditLogService =(IAuditLogService)context.getBean("auditLogService");
- }
- }
- public void setAuditableEntitys(
- Map<String, Map<String, String>> auditableEntitys) {
- this.auditableEntitys = auditableEntitys;
- }
- }