1. 概述
在一般系統中,當我們做了一些重要的操作時,如登陸系統,新增使用者,刪除使用者等操作時,我們需要將這些行為持久化。本文我們通過Spring AOP和Java的自定義註解來實現日誌的插入。此方案對原有業務入侵較低,實現較靈活
2. 日誌的相關類定義
我們將日誌抽象為以下兩個類:功能模組和操作型別 使用列舉類定義功能模組型別ModuleType,如學生、使用者模組
public enum ModuleType {
DEFAULT("1"), // 預設值
STUDENT("2"),// 學生模組
TEACHER("3"); // 使用者模組
private ModuleType(String index){
this.module = index;
}
private String module;
public String getModule(){
return this.module;
}
}
複製程式碼
使用列舉類定義操作的型別:EventType。如登陸、新增、刪除、更新、刪除等
public enum EventType {
DEFAULT("1", "default"), ADD("2", "add"), UPDATE("3", "update"), DELETE_SINGLE("4", "delete-single"),
LOGIN("10","login"),LOGIN_OUT("11","login_out");
private EventType(String index, String name){
this.name = name;
this.event = index;
}
private String event;
private String name;
public String getEvent(){
return this.event;
}
public String getName() {
return name;
}
}
複製程式碼
3. 定義日誌相關的註解
3.1. @LogEnable
這裡我們定義日誌的開關量,類上只有這個值為true,這個類中日誌功能才開啟
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface LogEnable {
/**
* 如果為true,則類下面的LogEvent啟作用,否則忽略
* @return
*/
boolean logEnable() default true;
}
複製程式碼
3.2. @LogEvent
這裡定義日誌的詳細內容。如果此註解註解在類上,則這個引數做為類全部方法的預設值。如果註解在方法上,則只對這個方法啟作用
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({java.lang.annotation.ElementType.METHOD, ElementType.TYPE})
public @interface LogEvent {
ModuleType module() default ModuleType.DEFAULT; // 日誌所屬的模組
EventType event() default EventType.DEFAULT; // 日誌事件型別
String desc() default ""; // 描述資訊
}
複製程式碼
3.3. @LogKey
此註解如果註解在方法上,則整個方法的引數以json的格式儲存到日誌中。如果此註解同時註解在方法和類上,則方法上的註解會覆蓋類上的值。
@Target({ElementType.FIELD,ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface LogKey {
String keyName() default ""; // key的名稱
boolean isUserId() default false; // 此欄位是否是本次操作的userId,這裡略
boolean isLog() default true; // 是否加入到日誌中
}
複製程式碼
4. 定義日誌處理類
4.1. LogAdmModel
定義儲存日誌資訊的類
public class LogAdmModel {
private Long id;
private String userId; // 操作使用者
private String userName;
private String admModel; // 模組
private String admEvent; // 操作
private Date createDate; // 操作內容
private String admOptContent; // 操作內容
private String desc; // 備註
set/get略
}
複製程式碼
4.2. ILogManager
定義日誌處理的介面類ILogManager 我們可以將日誌存入資料庫,也可以將日誌傳送到開中介軟體,如果redis, mq等等。每一種日誌處理類都是此介面的實現類
public interface ILogManager {
/**
* 日誌處理模組
* @param paramLogAdmBean
*/
void dealLog(LogAdmModel paramLogAdmBean);
}
複製程式碼
4.3. DBLogManager
ILogManager實現類,將日誌入庫。這裡只模擬入庫
@Service
public class DBLogManager implements ILogManager {
@Override
public void dealLog(LogAdmModel paramLogAdmBean) {
System.out.println("將日誌存入資料庫,日誌內容如下: " + JSON.toJSONString(paramLogAdmBean));
}
}
複製程式碼
5. AOP的配置
5.1. LogAspect定義AOP類
- 使用@Aspect註解此類
- 使用@Pointcut定義要攔截的包及類方法
- 我們使用@Around定義方法
@Component
@Aspect
public class LogAspect {
@Autowired
private LogInfoGeneration logInfoGeneration;
@Autowired
private ILogManager logManager;
@Pointcut("execution(* com.hry.spring.mvc.aop.log.service..*.*(..))")
public void managerLogPoint() {
}
@Around("managerLogPoint()")
public Object aroundManagerLogPoint(ProceedingJoinPoint jp) throws Throwable {
….
}
}
複製程式碼
aroundManagerLogPoint:主方法的主要業務流程 1. 檢查攔截方法的類是否被@LogEnable註解,如果是,則走日誌邏輯,否則執行正常的邏輯 2. 檢查攔截方法是否被@LogEvent,如果是,則走日誌邏輯,否則執行正常的邏輯 3. 根據獲取方法上獲取@LogEvent 中值,生成日誌的部分引數。其中定義在類上@LogEvent 的值做為預設值 4. 呼叫logInfoGeneration的processingManagerLogMessage填充日誌中其它的引數,做個方法我們後面再講 5. 執行正常的業務呼叫 6. 如果執行成功,則logManager執行日誌的處理(我們這裡只記錄執行成功的日誌,你也可以定義記錄失敗的日誌)
@Around("managerLogPoint()")
public Object aroundManagerLogPoint(ProceedingJoinPoint jp) throws Throwable {
Class target = jp.getTarget().getClass();
// 獲取LogEnable
LogEnable logEnable = (LogEnable) target.getAnnotation(LogEnable.class);
if(logEnable == null || !logEnable.logEnable()){
return jp.proceed();
}
// 獲取類上的LogEvent做為預設值
LogEvent logEventClass = (LogEvent) target.getAnnotation(LogEvent.class);
Method method = getInvokedMethod(jp);
if(method == null){
return jp.proceed();
}
// 獲取方法上的LogEvent
LogEvent logEventMethod = method.getAnnotation(LogEvent.class);
if(logEventMethod == null){
return jp.proceed();
}
String optEvent = logEventMethod.event().getEvent();
String optModel = logEventMethod.module().getModule();
String desc = logEventMethod.desc();
if(logEventClass != null){
// 如果方法上的值為預設值,則使用全域性的值進行替換
optEvent = optEvent.equals(EventType.DEFAULT) ? logEventClass.event().getEvent() : optEvent;
optModel = optModel.equals(ModuleType.DEFAULT) ? logEventClass.module().getModule() : optModel;
}
LogAdmModel logBean = new LogAdmModel();
logBean.setAdmModel(optModel);
logBean.setAdmEvent(optEvent);
logBean.setDesc(desc);
logBean.setCreateDate(new Date());
logInfoGeneration.processingManagerLogMessage(jp,
logBean, method);
Object returnObj = jp.proceed();
if(optEvent.equals(EventType.LOGIN)){
//TODO 如果是登入,還需要根據返回值進行判斷是不是成功了,如果成功了,則執行新增日誌。這裡判斷比較簡單
if(returnObj != null) {
this.logManager.dealLog(logBean);
}
}else {
this.logManager.dealLog(logBean);
}
return returnObj;
}
/**
* 獲取請求方法
*
* @param jp
* @return
*/
public Method getInvokedMethod(JoinPoint jp) {
// 呼叫方法的引數
List classList = new ArrayList();
for (Object obj : jp.getArgs()) {
classList.add(obj.getClass());
}
Class[] argsCls = (Class[]) classList.toArray(new Class[0]);
// 被呼叫方法名稱
String methodName = jp.getSignature().getName();
Method method = null;
try {
method = jp.getTarget().getClass().getMethod(methodName, argsCls);
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
return method;
}
}
複製程式碼
6. 將以上的方案在實際中應用的方案
這裡我們模擬學生操作的業務,並使用上文註解應用到上面並攔截日誌
6.1. IStudentService
業務介面類,執行一般的CRUD
public interface IStudentService {
void deleteById(String id, String a);
int save(StudentModel studentModel);
void update(StudentModel studentModel);
void queryById(String id);
}
複製程式碼
6.2. StudentServiceImpl:
- @LogEnable : 啟動日誌攔截
- 類上@LogEvent定義所有的模組
- 方法上@LogEven定義日誌的其它的資訊
@Service
@LogEnable // 啟動日誌攔截
@LogEvent(module = ModuleType.STUDENT)
public class StudentServiceImpl implements IStudentService {
@Override
@LogEvent(event = EventType.DELETE_SINGLE, desc = "刪除記錄") // 新增日誌標識
public void deleteById(@LogKey(keyName = "id") String id, String a) {
System.out.printf(this.getClass() + "deleteById id = " + id);
}
@Override
@LogEvent(event = EventType.ADD, desc = "儲存記錄") // 新增日誌標識
public int save(StudentModel studentModel) {
System.out.printf(this.getClass() + "save save = " + JSON.toJSONString(studentModel));
return 1;
}
@Override
@LogEvent(event = EventType.UPDATE, desc = "更新記錄") // 新增日誌標識
public void update(StudentModel studentModel) {
System.out.printf(this.getClass() + "save update = " + JSON.toJSONString(studentModel));
}
// 沒有日誌標識
@Override
public void queryById(String id) {
System.out.printf(this.getClass() + "queryById id = " + id);
}
}
複製程式碼
執行測試類,列印如下資訊,說明我們日誌註解配置啟作用了:
將日誌存入資料庫,日誌內容如下: {"admEvent":"4","admModel":"1","admOptContent":"{\"id\":\"1\"}","createDate":1525779738111,"desc":"刪除記錄"}
複製程式碼
7. 程式碼
以上的詳細的程式碼見下面 github程式碼,請儘量使用tag v0.21,不要使用master,因為我不能保證master程式碼一直不變