java spring 记录用户增删改操作日志

在数据库中建立操作记录(方式一) 
建立操作记录(方法二) 
使用LOG4J,通过配置LOG4J来获取业务日志(Apache Log4j) 
用触发器生成SQL Server2000数据表的操作日志 
基于拦截器的操作日志保存方式 
Struts2拦截器(自定义拦截器) 
Spring结合注解和反射利用拦截器实现对日志记录 
Hibernate3拦截器 

在数据库中建立操作记录(方式一)  




这种方式主要是在每个方法中设置静态常量String 相当于对方法的注释,并建立数据库日志表,用户登陆ERP系统并进行操作时,读取用户登录信息Session,同方法中的静态常量,和时间一并写入数据库的日志表中,由此来达到建立ERP系统操作日志的目的 

缺点:此方法较为比较麻烦需要在每个方法中定义静态常量,并且影响ERP系统的工作效率,当一个操作执行多次时ERP多次读取用户信息Sessions,为TOMCAT增加较高的负荷,但思路简单,代码量大,实现方便。也可以考虑将信息写入到XML或者TXT文档中去。 

建立操作记录(方法二) 

在SSH环境下,将数据库操作事务交给Spring管理 
1、尽量使用注解声明事务; 

2、写一个类扫描使用了事务的方法。根据楼主提出的技术需求分析,只有写入动作才需要记录,同样数据库只有写入才需要事务,读取不需要,所以在不需要事务的方法上面标注@Transactional(propagation=Propagation.NOT_SUPPORTED,readOnly=true),这样可以精确过滤出需要记录日志的方法; 

3、利用AOP编程实现日志记录功能。 
时间:AOP切入点处取系统时间 
操作员和IP:控制层在session作用域里取得用户对象和request取IP地址传给切入点 

操作:可以在操作数据库的DAO组件上(方法)用自定义注解标上,例如:@Operation=INSERT|DELETE|UPDATE...只要读取到这个配置就知道操作类型。当然也可以利用Hibernate来得知,得要看Hibernate的源代码。 

结果:事务成功即成功,事务回滚即失败。 

业务数据ID:这个有两种解决方法,一种笨拙的办法是所有的数据模型层的实体对象都抽取ID到父类;二是实体映射的配置方法采用注解方式,这样可以写一个类扫描出实体的ID字段和类型,自然能记录之。 

粒度问题:首先只要应用到事务的地方都可以记录之,批量操作数据行实际是循环调用DAO组件,也就实现了批量记录。当然,如果送批量SQL语句到数据库,由DBMS来做那就无奈了。 

这种方案设计可以一次编写,终身使用,不受项目的模块增减影响。代码量小,维护容易。 

使用LOG4J,通过配置LOG4J来获取业务日志(Apache Log4j) 

用触发器生成SQL Server2000数据表的操作日志 
有时,我们想知道登录到数据库的用户做了什么,于是,记录用户执行的SQL语句就非常有必要,这将是重要的参考依据。我们先建一张日志表(DBLoger)用于保存用户执行的SQL语句: 


程序代码 
Create TABLE DBLoger( 
    LoginName nvarchar(50), 
    HostName nvarchar(50), 
    EventInfo nvarchar(500), 
    Parameters int, 
    EventType nvarchar(100) 


接着再建一个触发器,在用户对表进行增/删/改时触发,将执行的SQL语句记录到日志表中: 


程序代码 
Create TRIGGER Loger ON student 
FOR Insert, Update, Delete 
AS 
    SET NOCOUNT ON 
    
    Create TABLE #T(EventType nvarchar(100),Parameters int,EventInfo nvarchar(500)) 
    Insert #T exec('dbcc inputbuffer(' + @@spid + ')') 
    --记录到日志表 
    Insert INTO DBLoger(LoginName,HostName,EventInfo,Parameters,EventType) Select suser_sname(),host_name(),EventInfo,Parameters,EventType FROM #T 

缺点:由于dbcc inputbuffer的EventInfo最多只能保存255个字符,所以一旦执行的SQL过长,日志表中将无法看到完整的SQL语句! 

基于拦截器的操作日志保存方式 
Struts2拦截器(自定义拦截器) 
Struts2的内置拦截器只能完成一些通用功能,若要使用拦截器捕获业务信息,则需要自定义拦截器进行操作 
例:Log日志的数据库保存。即使用Struts2拦截器的intercept方法,在方法里直接把操作记录保存到数据库中,而这时计算出的查询时间则是整个查询过程的时间,即读取用户输入后,进行判断,并进行查询的时间,这种计算方法比之以前更加合理,因为查询本身就包含判断,如果仅仅只是查询数据库那一个动作,在复杂的查询里并不能体现出整个查询所花的时间,而用拦截器,则相对轻松的解决了此问题。在随后显示的时间里,查询耗时确定比以前有了很大的延长。 
如果达到与具体action无关,只与用户有关的话,那么这个拦截器很容易实扩展到在项目中的任何地方保存用户操作记录。 

Spring结合注解和反射利用拦截器实现对日志记录 
现今的框架都很灵活,对于日志的记录也有很多种方式,关键看很么样的方式更高效,更能体现系统的灵活和松散耦合。之前日志记录采用log4j的JDBCAppender,虽然配置一样很灵活,但是相对后来基于spring aop上的实现觉得还是比较复杂。比如我的表名是动态的,字段内容或则字段名字是动态的,这些配置对于log4j来说多了很多难度,而且还要管理缓存中的属性值。再则log4j没有实现jdbc的pool管理,虽然提供了相应接口。最终决定才用spring拦截器,又高效又灵活。对于数据库的操作同样还是使用自己的底层。 
源码和配置记录: 

package com.cmcc.common.controller.interceptor; 
import java.lang.reflect.Method; 
import java.util.Date; 
import org.apache.log4j.Logger; 
import org.aspectj.lang.ProceedingJoinPoint; 
import com.cmcc.common.Global; 
import com.cmcc.common.cache.PoolConfigInfoMap; 
import com.cmcc.common.controller.interceptor.annotations.GenericLogger; 
import com.cmcc.common.util.UserSessionObj; 
import com.cmcc.framework.business.interfaces.corporation.ICompanyInfoManager; 
import com.cmcc.framework.business.interfaces.log.IOperLogManager; 
import com.cmcc.framework.model.log.OperateLog; 
/** 

* 记录系统日志的拦截器 

* @author <a href="mailto:sun128837@126.com">conntsing</a> 

* @version $Revision: 1.5 $ 

* @since 2009-3-23 
*/ 
public class GenericLoggerInterceptor { 
    /** 
     * Logger for this class 
     */ 
    private static final Logger logger = Logger 
            .getLogger(GenericLoggerInterceptor.class); 
    @SuppressWarnings("unchecked") 
    public Object invoke(ProceedingJoinPoint joinPoint) throws Throwable { 
        if (logger.isDebugEnabled()) { 
            logger.debug("invoke(ProceedingJoinPoint) - start"); //$NON-NLS-1$ 
        } 
        try { 
            Object result = joinPoint.proceed(); 
            Class cl = joinPoint.getTarget().getClass(); 
            Method[] methods = cl.getMethods(); 
            GenericLogger genericLogger = null; 
            UserSessionObj userSessionObj = null; 
            String departmentNames = ""; 
            String employeeNames = ""; 
            for (Method m : methods) { 
                if (m.getName().equals(joinPoint.getSignature().getName())) { 
                    genericLogger = m.getAnnotation(GenericLogger.class); 
                } 
                if (m.getName().equalsIgnoreCase("getusersessioninfo")) { 
                    userSessionObj = (UserSessionObj) m.invoke(joinPoint 
                            .getTarget()); 
                } 
            } 
            OperateLog opLog = new OperateLog(); 
            if (null != genericLogger) { 
                if (genericLogger.isOperateDepartment()) { 
                    for (Method m : methods) { 
                        if (m.getName().equalsIgnoreCase("getdepartmentnames")) { 
                            departmentNames = (String) m.invoke(joinPoint 
                                    .getTarget()); 
                            break; 
                        } 
                    } 
                } 
                if (genericLogger.isOperateEmployee()) { 
                    for (Method m : methods) { 
                        if (m.getName().equalsIgnoreCase("getemployeenames")) { 
                            employeeNames = (String) m.invoke(joinPoint 
                                    .getTarget()); 
                            break; 
                        } 
                    } 
                } 
                ICompanyInfoManager companyservice = (ICompanyInfoManager) Global._ctx 
                        .getBean("companyservice"); 
                opLog.setEid(userSessionObj.getEid()); 
                opLog.setAdminId(userSessionObj.getId()); 
                opLog.setAdminName(userSessionObj.getLoginId()); 
                opLog.setC0(userSessionObj.getCode().toString()); 
                opLog.setOperateDesc(genericLogger.operateDescription()); 
                opLog.setOperateTime(new Date(System.currentTimeMillis())); 
                opLog.setShortName(companyservice.get( 
                        userSessionObj.getEid(), 
                        PoolConfigInfoMap.get(userSessionObj.getEid()) 
                                .getPhysical_pool_id()).getShortName()); 
                if (null != employeeNames && null != departmentNames) { 
                    if (employeeNames.equals("") && departmentNames.equals("")) { 
                        opLog.setDeptName(opLog.getShortName()); 
                    } 
                    else { 
                        opLog.setEmployeeName(employeeNames); 
                        opLog.setDeptName(departmentNames); 
                    } 
                } 
                else { 
                    opLog.setDeptName(opLog.getShortName()); 
                } 
                IOperLogManager logmanage = (IOperLogManager) Global._ctx 
                        .getBean("operLogManager"); 
                logmanage.saveOperateLog(opLog); 
            } 
            if (logger.isInfoEnabled()) { 
                logger.info("OperateLog------adminid:    " + opLog.getAdminId()); 
                logger.info("OperateLog--operatedesc:    " + opLog.getOperateDesc()); 
                logger.info("OperateLog--operatetime:    " + opLog.getOperateTime()); 
                logger.info("OperateLog----shortName:    " + opLog.getShortName()); 
            } 
            return result; 
        } 
        catch (Exception e) { 
            e.printStackTrace(); 
            logger.warn("invoke(ProceedingJoinPoint) - exception ignored", e); //$NON-NLS-1$  
        } 
        finally { 
        } 
        if (logger.isDebugEnabled()) { 
            logger.debug("invoke(ProceedingJoinPoint) - end"); //$NON-NLS-1$ 
        } 
        return null; 
    } 

<aop:config  proxy-target-class="true"> 
      <aop:pointcut id="loggerService" expression="execution(* com.cmcc.framework.controller.action..*.*(..)) and 
                    @annotation(com.cmcc.common.controller.interceptor.annotations.GenericLogger)"/> 
      <aop:aspect ref="genericLoggerInterceptor"> 
          <aop:around pointcut-ref="loggerService" method="invoke"/> 
      </aop:aspect> 
    </aop:config> 
    <bean id="genericLoggerInterceptor" class="com.cmcc.common.controller.interceptor.GenericLoggerInterceptor"/> 
像我这里会有个分表分pool区的配置处理,如果是之前我还得交给log4j去动态的传入表名参数,而且还得重新实现appender。现在我只需要拦截器拦截我匹配表达式的方法,并使用注解传入相应描述,再依赖注入反射获取相应的描述字段值,一切都很灵活配置也很简单。 
实际上,我拦截的方法只在controller层,所以同样可以通过实现struts2拦截器(同样支持注解)来拦截方法记录日志,不过使用spring拦截器更灵活易于扩展。 

    
Hibernate3拦截器 
    技术选型: 
最土的,在所有的Dao方法中显示的编写日志记录代码 
该项目以前是用.net这么干的,这种做法重复工作量太大,维护性差,并且也没实现字段级变更的记录,根本不予考虑。 
数据库触发器 - 与数据库耦合 
与数据库耦合,违背了使用hibernate的初衷,也不予考虑 
原生的Hibernate Interceptor 
优点:可以在hibernate对象操作的时候获取最为详细的运行期信息,字段名,原始值,修改后值等等。 
缺点:在JPA  API的封装下很难获取到hibernate的session,不能进行持久化操作。 
JPA callback / event-listener 
优点:JPA规范,最为优雅简单 
缺点:功能太弱,不能满足需求 
很自然地,干这种事AOP似乎比较合适 
优点:灵活,在spring容器中,可以访问所有spring bean 
缺点:不能获取详细的运行期信息(字段名,原始值,等等),无法感知hibernate的事务执行,即使dao事务rollback,仍然会插入一条操作历史记录,破坏了“操作”和“历史”的一致性。 
采用Hibernate 3的新特性 Event-listener 
可以解决以上所有问题 
能够取得运行期详细信息,除了能记录粗粒度的实体的保存删除操作外,还能精确追踪对实体字段修改、实体关联/级联关系的变更,能记录更新前的值、更新后的值,可以生成详细日志。 
灵活解耦,跨数据库,不影响原有代码。 
    Hibernate3 新特性事件处理框架是hibernate 2拦截器的一个补充或者替代,由拦截器被动拦截操作事件变成事件源的主动驱动,这是一个进步。Hibernate 事件框架官方文档. 
    Hibernate3中定义了很多的事件,涵盖了持久化过程中不同的生命周期。简单说Session的一个方法(load, flush...)分别对应一个事件,当该方法被调用时,就会触发一个相应的事件,这个事件会被我们预先定义的事件监听器收到,再进行相应的处理。这种方式来做审计日志是再适合不过。 
   但也有个缺点就是这样的Event-listener是脱离主容器(比如Spring IoC环境)单独实例化的,无法访问主容器的资源(比如要取得当前登录的用户信息就会比较麻烦)。这个暂时还没解决。 

在这里我们选取PostInsertEventListener(插入后事件),PostUpdateEventListener(更新后事件),PostDeleteEventListener(删除后事件)接口作为CRUD方法的监听接口。hibernate3中事件是分为pre和post,表示该发生事件前、后。这里我们全部用Post,因为PostEvent只有在数据实际改变后才会触发,假如CRUD事务因为异常回滚,则不会触发事件。 

首先定义一个mark接口Historiazable,实现该接口的entity类表明是需要做审计日志的。 
然后编写我们自定义的EventListener类,实现上述的事件接口。 
在事件接口实现方法里在根据不同的事件编写审计日志的代码。 
Java代码 
public class HistoryListener implements PostInsertEventListener,    
        PostUpdateEventListener, PostDeleteEventListener {    
      
    @Override   
    public void onPostInsert(PostInsertEvent event) {    
        if (event.getEntity() instanceof Historizable) {    
        //  保存 插入日志    
        }    
    }    
   
    @Override   
    public void onPostUpdate(PostUpdateEvent event) {    
        if (event.getEntity() instanceof Historizable) {    
        // 保存 修改日志    
        }    
    }    
   
    @Override   
    public void onPostDelete(PostDeleteEvent event) {    
        if (event.getEntity() instanceof Historizable) {    
        // 保存 删除日志    
        }    
    }    
}   



配置EventListener 
编辑hibernate.cfg.xml,配置监听器 
Xml代码 
<session-factory>   
    <listener type="post-insert" class="net.jeffrey.hibernate.history.HistoryListener"/>   
    <listener type="post-update" class="net.jeffrey.hibernate.history.HistoryListener"/>   
    <listener type="post-delete" class="net.jeffrey.hibernate.history.HistoryListener"/>   
</session-factory>   

配置持久化单元 
在persistence.xml中加入 
<property name="hibernate.ejb.cfgfile" value="hibernate.cfg.xml"/> 
这样JPA环境启动后,就会正确装载初始化自定义的事件监听器。 
个人认为:第一种方式有效率问题但是实现简单,第二种方式实现上和效率上都比较优秀,第三种基于触发器的操作日志实现方案由SQLServer2000触发处理不影响页面响应效率,这三种方案较为提倡,LOG4J有待了解具体没有实用过。
要读取 MySQL 的增删日志文件,可以使用 MySQL 的 binlog,binlog 是 MySQL 的二进制日志记录了 MySQL 的所有更新操作,包括增删等。 下面是使用 Spring Boot 读取 MySQL binlog 的步骤: 1. 在 MySQL 配置文件中开启 binlog,可以在 my.cnf 或 my.ini 文件中添加如下配置: ``` [mysqld] log-bin=mysql-bin binlog-format=ROW ``` 这里将 binlog 日志文件储在名为 mysql-bin 的文件中,格式为 ROW。 2. 在 Spring Boot 中添加 MySQL 驱动和 binlog 相关的依赖,例如: ``` <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.23</version> </dependency> <dependency> <groupId>com.github.shyiko</groupId> <artifactId>mysql-binlog-connector-java</artifactId> <version>0.17.0</version> </dependency> ``` 这里使用了 mysql-connector-java 和 mysql-binlog-connector-java 两个依赖。 3. 在 Spring Boot 中编写读取 binlog 日志的代码,例如: ``` @Component public class BinlogReader { private final BinaryLogClient client; public BinlogReader() { client = new BinaryLogClient("localhost", 3306, "root", "password"); client.registerEventListener(event -> { EventData data = event.getData(); if (data instanceof WriteRowsEventData) { WriteRowsEventData write = (WriteRowsEventData) data; System.out.println("inserted rows: " + write.getRows()); } else if (data instanceof UpdateRowsEventData) { UpdateRowsEventData update = (UpdateRowsEventData) data; System.out.println("updated rows: " + update.getRows()); } else if (data instanceof DeleteRowsEventData) { DeleteRowsEventData delete = (DeleteRowsEventData) data; System.out.println("deleted rows: " + delete.getRows()); } }); } @PostConstruct public void start() throws IOException { client.connect(); } @PreDestroy public void stop() throws IOException { client.disconnect(); } } ``` 这里使用了 BinaryLogClient 类来连接 MySQL,通过 registerEventListener 方法注册事件监听器来监听 binlog 日志的写入、更新、删除操作。 需要注意的是,直接读取 MySQL 的 binlog 日志文件可能会对性能和稳定性造成影响,建议在使用前先进行充分测试和评估。同时,也建议使用专业的数据库同步工具来进行 MySQL 数据库的同步,如阿里云的 DTS、腾讯云的 CDC 等。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值