一文帶你學會基於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, 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("");
			}
			//操作時間
			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.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());
	}

	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;

}

總結

文章寫到這,也就結束了,文中難免有不足,歡迎大家批評指正,另外可以關注我的公眾號,進群交流哦。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM