java spring 記錄使用者增刪改操作日誌
阿新 • • 發佈:2019-01-14
在資料庫中建立操作記錄(方式一)
建立操作記錄(方法二)
使用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:[email protected]">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
可以解決以上所有問題
能夠取得執行期詳細資訊,除了能記錄粗粒度的實體的儲存刪
建立操作記錄(方法二)
使用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:[email protected]">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
可以解決以上所有問題
能夠取得執行期詳細資訊,除了能記錄粗粒度的實體的儲存刪