1. 程式人生 > 程式設計 >SpringAop實現操作日誌記錄

SpringAop實現操作日誌記錄

前言

大家好,這裡是經典雞翅,今天給大家帶來一篇基於SpringAop實現的操作日誌記錄的解決的方案。大家可能會說,切,操作日誌記錄這麼簡單的東西,老生常談了。不!

網上的操作日誌一般就是記錄操作人,操作的描述,ip等。好一點的增加了修改的資料和執行時間。那麼!我這篇有什麼不同呢!今天這種不僅可以記錄上方所說的一切,還增加記錄了操作前的資料,錯誤的資訊,堆疊資訊等。正文開始~~~~~

思路介紹

記錄操作日誌的操作前資料是需要思考的重點。我們以修改場景來作為探討。當我們要完全記錄資料的流向的時候,我們必然要記錄修改前的資料,而前臺進行提交的時候,只有修改的資料,那麼如何找到修改前的資料呢。有三個大的要素,我們需要知道修改前資料的表名,表的欄位主鍵,表主鍵的值。這樣通過這三個屬性,我們可以很容易的拼出 select * from 表名 where 主鍵欄位 = 主鍵值。我們就獲得了修改前的資料,轉換為json之後就可以存入到資料庫中了。如何獲取三個屬性就是重中之重了。我們採取的方案是通過提交的對映實體,在實體上打上註解,根據 Java 的反射取到值。再進一步拼裝獲得物件資料。那麼AOP是在哪裡用的呢,我們需要在記錄操作日誌的方法上,打上註解,再通過切面獲取到切點,一切的資料都通過反射來進行獲得。

定義操作日誌註解

既然是基於spinrg的aop實現切面。那麼必然是需要一個自定義註解的。用來作為切點。我們定義的註解,可以帶一些必要的屬性,例如操作的描述,操作的型別。操作的型別需要說一下,我們分為新增、修改、刪除、查詢。那麼只有修改和刪除的時候,我們需要查詢一下修改前的資料。其他兩種是不需要的,這個也可以用來作為判斷。

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface OperateLog {

   String operation() default "";

   String operateType() default "";

}

定義用於找到表和表主鍵的註解

表和表主鍵的註解打在實體上,內部有兩個屬性 tableName 和 idName。這兩個屬性的值獲得後,可以進行拼接 select * from 表名 where 主鍵欄位。

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SelectTable {

	String tableName() default "";

	String idName() default "";
}

定義獲取主鍵值的註解

根據上面所說的三個元素,我們還缺最後一個元素主鍵值的獲取,用於告訴我們,我們應該從提交的請求的那個欄位,拿到其中的值。

@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SelectPrimaryKey {

}

註解的總結

有了上面的三個註解,註解的準備工作已經進行完畢。我們通過反射取到資料,可以獲得一切。接下來開始實現切面,對於註解的值進行拼接處理,最終存入到我們的資料庫操作日誌表中。

切面的實現

對於切面來說,我們需要實現切點、資料庫的插入、反射的資料獲取。我們先分開進行解釋,最後給出全面的實現程式碼。方便大家的理解和學習。

切面的定義

基於spring的aspect進行宣告這是一個切面。

@Aspect
@Component
public class OperateLogAspect {
}

切點的定義

切點就是對所有的打上OperateLog的註解的請求進行攔截和加強。我們使用annotation進行攔截。

	@Pointcut("@annotation(com.jichi.aop.operateLog.OperateLog)")
	private void operateLogPointCut(){
	}

獲取請求ip的共用方法

	private String getIp(HttpServletRequest request){
		String ip = request.getHeader("X-forwarded-for");
		if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
			ip = request.getHeader("Proxy-Client-IP");
		}
		if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
			ip = request.getHeader("WL-Proxy-Client-IP");
		}
		if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
			ip = request.getHeader("HTTP_CLIENT_IP");
		}
		if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
			ip = request.getHeader("HTTP_X_FORWARDED_FOR");
		}
		if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
			ip = request.getRemoteAddr();
		}
		return ip;
	}

資料庫的日誌插入操作

我們將插入資料庫的日誌操作進行單獨的抽取。

private void insertIntoLogTable(OperateLogInfo operateLogInfo){
	operateLogInfo.setId(UUID.randomUUID().toString().replace("-",""));
	String sql="insert into log values(?,?,?)";
	jdbcTemplate.update(sql,operateLogInfo.getId(),operateLogInfo.getUserId(),operateLogInfo.getUserName(),operateLogInfo.getOperation(),operateLogInfo.getMethod(),operateLogInfo.getModifiedData(),operateLogInfo.getPreModifiedData(),operateLogInfo.getResult(),operateLogInfo.getErrorMessage(),operateLogInfo.getErrorStackTrace(),operateLogInfo.getExecuteTime(),operateLogInfo.getDuration(),operateLogInfo.getIp(),operateLogInfo.getModule(),operateLogInfo.getOperateType());
}

環繞通知的實現

日誌的實體類實現

@TableName("operate_log")
@Data
public class OperateLogInfo {

	//主鍵id
	@TableId
	private String id;
	//操作人id
	private String userId;
	//操作人名稱
	private String userName;
	//操作內容
	private String operation;
	//操作方法名稱
	private String method;
	//操作後的資料
	private String modifiedData;
	//操作前資料
	private String preModifiedData;
	//操作是否成功
	private String result;
	//報錯資訊
	private String errorMessage;
	//報錯堆疊資訊
	private String errorStackTrace;
	//開始執行時間
	private Date executeTime;
	//執行持續時間
	private Long duration;
	//ip
	private String ip;
	//操作型別
	private String operateType;

}

準備工作全部完成。接下來的重點是對環繞通知的實現。思路分為資料處理、異常捕獲、finally執行資料庫插入操作。環繞通知的重點類就是ProceedingJoinPoint ,我們通過它的getSignature方法可以獲取到打在方法上註解的值。例如下方。

MethodSignature signature = (MethodSignature) pjp.getSignature();
OperateLog declaredAnnotation = signature.getMethod().getDeclaredAnnotation(OperateLog.class);
operateLogInfo.setOperation(declaredAnnotation.operation());
operateLogInfo.setModule(declaredAnnotation.module());
operateLogInfo.setOperateType(declaredAnnotation.operateType());
//獲取執行的方法
String method = signature.getDeclaringType().getName() + "." + signature.getName();
operateLogInfo.setMethod(method);
String operateType = declaredAnnotation.operateType();

獲取請求的資料,也是通過這個類來實現,這裡有一點是需要注意的,就是我們要約定引數的傳遞必須是第一個引數。這樣才能保證我們取到的資料是提交的資料。

if(pjp.getArgs().length>0){
	Object args = pjp.getArgs()[0];
	operateLogInfo.setModifiedData(new Gson().toJson(args));
}

接下來的一步就是對修改前的資料進行拼接。之前我們提到過如果是修改和刪除,我們才會進行資料的拼接獲取,主要是通過類來判斷書否存在註解,如果存在註解,那麼就要判斷註解上的值是否是控制或者,非空才能正確的進行拼接。取field的值的時候,要注意私有的變數需要通過setAccessible(true)才可以進行訪問。

if(GlobalStaticParas.OPERATE_MOD.equals(operateType) ||
	GlobalStaticParas.OPERATE_DELETE.equals(operateType)){
	String tableName = "";
	String idName = "";
	String selectPrimaryKey = "";
	if(pjp.getArgs().length>0){
		Object args = pjp.getArgs()[0];
		//獲取操作前的資料
		boolean selectTableFlag = args.getClass().isAnnotationPresent(SelectTable.class);
		if(selectTableFlag){
			tableName = args.getClass().getAnnotation(SelectTable.class).tableName();
			idName = args.getClass().getAnnotation(SelectTable.class).idName();
		}else {
			throw new RuntimeException("操作日誌型別為修改或刪除,實體類必須指定表面和主鍵註解!");
		}
		Field[] fields = args.getClass().getDeclaredFields();
		Field[] fieldsCopy = fields;
		boolean isFindField = false;
		int fieldLength = fields.length;
		for(int i = 0; i < fieldLength; ++i) {
			Field field = fieldsCopy[i];
			boolean hasPrimaryField = field.isAnnotationPresent(SelectPrimaryKey.class);
			if (hasPrimaryField) {
				isFindField = true;
				field.setAccessible(true);
				selectPrimaryKey = (String)field.get(args);
			}
		}
		if(!isFindField){
			throw new RuntimeException("實體類必須指定主鍵屬性!");
		}
	}
	if(StringUtils.isNotEmpty(tableName) &&
		StringUtils.isNotEmpty(idName)&&
		StringUtils.isNotEmpty(selectPrimaryKey)){
		StringBuffer sb = new StringBuffer();
		sb.append(" select * from ");
		sb.append(tableName);
		sb.append(" where ");
		sb.append(idName);
		sb.append(" = ? ");
		String sql = sb.toString();
		try{
			List<Map<String,Object>> maps = jdbcTemplate.queryForList(sql,selectPrimaryKey);
			if(maps!=null){
				operateLogInfo.setPreModifiedData(new Gson().toJson(maps));
			}
		}catch (Exception e){
			e.printStackTrace();
			throw new RuntimeException("查詢操作前資料出錯!");
		}
	}else {
		throw new RuntimeException("表名、主鍵名或主鍵值 存在空值情況,請核實!");
	}
}else{
	operateLogInfo.setPreModifiedData("");
}

切面的完整實現程式碼

@Aspect
@Component
public class OperateLogAspect {

	@Autowired
	private JdbcTemplate jdbcTemplate;

	@Pointcut("@annotation(com.jichi.aop.operateLog.OperateLog)")
	private void operateLogPointCut(){
	}

	@Around("operateLogPointCut()")
	public Object around(ProceedingJoinPoint pjp) throws Throwable {
		Object responseObj = null;
		OperateLogInfo operateLogInfo = new OperateLogInfo();
		String flag = "success";
		try{
			HttpServletRequest request = SpringContextUtil.getHttpServletRequest();
			DomainUserDetails currentUser = SecurityUtils.getCurrentUser();
			if(currentUser!=null){
				operateLogInfo.setUserId(currentUser.getId());
				operateLogInfo.setUserName(currentUser.getUsername());
			}
			MethodSignature signature = (MethodSignature) pjp.getSignature();
			OperateLog declaredAnnotation = signature.getMethod().getDeclaredAnnotation(OperateLog.class);
			operateLogInfo.setOperation(declaredAnnotation.operation());
			operateLogInfo.setModule(declaredAnnotation.module());
			operateLogInfo.setOperateType(declaredAnnotation.operateType());
			//獲取執行的方法
			String method = signature.getDeclaringType().getName() + "." + signature.getName();
			operateLogInfo.setMethod(method);
			String operateType = declaredAnnotation.operateType();
			if(pjp.getArgs().length>0){
				Object args = pjp.getArgs()[0];
				operateLogInfo.setModifiedData(new Gson().toJson(args));
			}
			if(GlobalStaticParas.OPERATE_MOD.equals(operateType) ||
				GlobalStaticParas.OPERATE_DELETE.equals(operateType)){
				String tableName = "";
				String idName = "";
				String selectPrimaryKey = "";
				if(pjp.getArgs().length>0){
					Object args = pjp.getArgs()[0];
					//獲取操作前的資料
					boolean selectTableFlag = args.getClass().isAnnotationPresent(SelectTable.class);
					if(selectTableFlag){
						tableName = args.getClass().getAnnotation(SelectTable.class).tableName();
						idName = args.getClass().getAnnotation(SelectTable.class).idName();
					}else {
						throw new RuntimeException("操作日誌型別為修改或刪除,實體類必須指定表面和主鍵註解!");
					}
					Field[] fields = args.getClass().getDeclaredFields();
					Field[] fieldsCopy = fields;
					boolean isFindField = false;
					int fieldLength = fields.length;
					for(int i = 0; i < fieldLength; ++i) {
						Field field = fieldsCopy[i];
						boolean hasPrimaryField = field.isAnnotationPresent(SelectPrimaryKey.class);
						if (hasPrimaryField) {
							isFindField = true;
							field.setAccessible(true);
							selectPrimaryKey = (String)field.get(args);
						}
					}
					if(!isFindField){
						throw new RuntimeException("實體類必須指定主鍵屬性!");
					}
				}
				if(StringUtils.isNotEmpty(tableName) &&
					StringUtils.isNotEmpty(idName)&&
					StringUtils.isNotEmpty(selectPrimaryKey)){
					StringBuffer sb = new StringBuffer();
					sb.append(" select * from ");
					sb.append(tableName);
					sb.append(" where ");
					sb.append(idName);
					sb.append(" = ? ");
					String sql = sb.toString();
					try{
						List<Map<String,selectPrimaryKey);
						if(maps!=null){
							operateLogInfo.setPreModifiedData(new Gson().toJson(maps));
						}
					}catch (Exception e){
						e.printStackTrace();
						throw new RuntimeException("查詢操作前資料出錯!");
					}
				}else {
					throw new RuntimeException("表名、主鍵名或主鍵值 存在空值情況,請核實!");
				}
			}else{
				operateLogInfo.setPreModifiedData("");
			}
			//操作時間
			Date beforeDate = new Date();
			Long startTime = beforeDate.getTime();
			operateLogInfo.setExecuteTime(beforeDate);
			responseObj = pjp.proceed();
			Date afterDate = new Date();
			Long endTime = afterDate.getTime();
			Long duration = endTime - startTime;
			operateLogInfo.setDuration(duration);
			operateLogInfo.setIp(getIp(request));
			operateLogInfo.setResult(flag);
		}catch (RuntimeException e){
			throw new RuntimeException(e);
		}catch (Exception e){
			flag = "fail";
			operateLogInfo.setResult(flag);
			operateLogInfo.setErrorMessage(e.getMessage());
			operateLogInfo.setErrorStackTrace(e.getStackTrace().toString());
			e.printStackTrace();
		}finally {
			insertIntoLogTable(operateLogInfo);
		}
		return responseObj;
	}

	private void insertIntoLogTable(OperateLogInfo operateLogInfo){
		operateLogInfo.setId(UUID.randomUUID().toString().replace("-",""));
		String sql="insert into energy_log values(?,?)";
		jdbcTemplate.update(sql,operateLogInfo.getOperateType());
	}

	private String getIp(HttpServletRequest request){
		String ip = request.getHeader("X-forwarded-for");
		if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
			ip = request.getHeader("Proxy-Client-IP");
		}
		if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
			ip = request.getHeader("WL-Proxy-Client-IP");
		}
		if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
			ip = request.getHeader("HTTP_CLIENT_IP");
		}
		if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
			ip = request.getHeader("HTTP_X_FORWARDED_FOR");
		}
		if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
			ip = request.getRemoteAddr();
		}
		return ip;
	}
}

示例的使用方式

針對於示例來說我們要在controller上面打上操作日誌的註解。

  @PostMapping("/updateInfo")
  @OperateLog(operation = "修改資訊",operateType = GlobalStaticParas.OPERATE_MOD)
  public void updateInfo(@RequestBody Info info) {
    service.updateInfo(info);
  }

針對於Info的實體類,我們則要對其中的欄位和表名進行標識。

@Data
@SelectTable(tableName = "info",idName = "id")
public class Info {

  @SelectPrimaryKey
  private String id;
  
  private String name;

}

總結

文章寫到這,也就結束了,文中難免有不足,歡迎大家批評指正

以上就是SpringAop實現操作日誌記錄的詳細內容,更多關於SpringAop 操作日誌記錄的資料請關注我們其它相關文章!