参考文章: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>
<prop key="log">PROPAGATION_REQUIRES_NEW</prop>
<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;
}
}