基於spring aop的操作日志功能


公司有一個項目需要加一個操作日志的功能。領導明確說明不要用觸發器,所以想到了aop,並在網上找到了一些例子進行學習。我根據業務需要增加了一些功能,在這里做一下記錄。

一、開啟aop。在web.xml中contextConfigLocation對應的配置文件內加入<aop:aspectj-autoproxy proxy-target-class="false"/>。因為我需要記錄的是mapper層,所以將proxy-target-class設為false,使用jdk代理。
二、自定義一個注解。該注解用於配置需要記錄的操作接口。
@Target({ ElementType.PARAMETER, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SystemMapperLog {

/** 要執行的具體操作比如:添加用戶 **/
//操作詳情
String operation() default "";
//操作表
String table() default "";
//操作列
//查詢新舊值使用,非更新不需要填寫
String columns() default "";
//操作列名
//查詢新舊值使用,非更新不需要填寫
String columnsName() default "";
//操作模塊
String operateObject() default "";
//參數-拼接操作詳情使用
String param() default "";
//類型-
String type() default "";
//條件-新增時會用到
String condition() default "";
}

 

三、聲明一個切面,定義一個切點,創建切入之后進行的操作方法
1、切面聲明
//切面聲明

@Component
@Aspect
public class LogAopAction {

}

 

2、切點定義

1 private final String MAPPER_POINT = "execution(public * com.seeyoui.kensite..persistence.*.*(..))";
2 
3 //mapper層切點
4 @Pointcut(MAPPER_POINT)
5 private void mapperAspect() {
6 }

 

這個切點切入的是所有的mapper層方法,但這顯然是不對的,不對在切入后會根據注解進行判斷,在存在對應注解的方法處才行進行日志的保存操作。
3、創建切入之后執行的方法

 

//mapper層切入后的處理方法-環繞
	@SuppressWarnings("unchecked")
	@Around("mapperAspect()")
	public Object doAroundMapper(ProceedingJoinPoint pjp) throws Throwable {
		try {
			Class[] parameterTypes = ((MethodSignature) pjp.getSignature()).getMethod().getParameterTypes();
			MethodSignature signature = (MethodSignature) pjp.getSignature();
			Method method = signature.getMethod();
			//如果有注解,說明是需要監聽的方法
			if(method.isAnnotationPresent(SystemMapperLog.class)){
				SysUser sysUser = UserUtils.getUser();
				Object[] args = pjp.getArgs();
				//從注解中獲取需要的信息
				HashMap<String,String> map = getMapperMthodDescription(pjp);
				String operation = map.get("operation");
				String table = map.get("table");
				String columns = map.get("columns");
				String columnsName = map.get("columnsName");
				String param = map.get("param");
				String operateObject = map.get("operateObject");
				String type = map.get("type");
				String condition = map.get("condition");
				String[] columnNameArr = null;//獲取操作行名稱
				Map map1= new HashMap();//儲存方法內參數
				List<ChangeList> changeList = new ArrayList();//儲存新舊值變化
				if(args != null && args.length != 0 && !("deleteA").equals(type)){
					map1 = BeanUtils.describe(args[0]);
				}
				if(("save").equals(type)){
					String[] paramArr = null;
					//如果是保存
					//判斷數據是否滿足條件,不滿足不需要保存日志
					if(StringUtils.isNoneBlank(condition)){
						//第一步拆分,拆分出每個條件
						String[] conArr = condition.split(",");
						for (int i = 0; i < conArr.length; i++) {
							//第二部拆分,拆分出每個條件的key和value
							String[] conditionArr = conArr[i].split("\\|");
							if(!(conditionArr[1].equals((String)map1.get(conditionArr[0])))){
								//如果不滿足條件,直接跳出方法
								Object result = pjp.proceed();
								return result;
							}
						}
					}
					//如果有param從參數中取出
					if(StringUtils.isNoneBlank(param)){
						paramArr = param.split(",");
						//如果填寫了自定義拼接操作需要的信息,開始拼接操作信息
					}
					if(operation.indexOf("param") != -1){
						String[] operationArr = operation.split("param");
						operation = "";
						for (int i = 0; i < operationArr.length; i++) {
							//根據|分割
							String[] pa = paramArr[i].split("\\|");
							String str = "";
							for (int j = 0; j < pa.length; j++) {
								str = (String)map1.get(StringUtils.toCamelCase(pa[j]));
								if(StringUtils.isBlank(str)){
									continue;
								}else{
									break;
								}
							}
							operation += operationArr[i] + str;
						}
					}
				}else if(("update").equals(type)){
					if(StringUtils.isNoneBlank(table)){
						columnNameArr = columnsName.split(",");
					}
					//根據所獲取的信息拼接出日志對象
					if(StringUtils.isNoneBlank(table)&&StringUtils.isNoneBlank(columns)){
						String paramStr = "";
						String[] paramArr = null;
						//若傳入的參數中存在delFlag且他的值為0,則認定此次操作為假刪除,不處理各個字段的變化,保存一條刪除記錄
						boolean isDel = false;//假刪除標識
						String[] operationArr = operation.split("param");
						if(("0").equals((String)map1.get("delFlag"))){
							isDel = true;
							operationArr[0] = "刪除記錄";
						}
						if(StringUtils.isNoneBlank(param)){
							paramStr = ","+param;
							paramArr = param.split(",");
						}
						//如果填寫了表信息和字段信息
						String sql = "select " + columns + paramStr + " from " + table + " where id='"+map1.get("id")+"'";
						if((columns.indexOf(",") != -1) && !isDel){
							//如果列中存在逗號,說明是多列
							String[] columnArr = columns.split(",");
							for (int i = 0; i < columnArr.length; i++) {
								ChangeList cl = new ChangeList();
								String oldValue = DBUtils.getString(sql, columnArr[i]);
								String newValue = (String)map1.get(StringUtils.toCamelCase(columnArr[i]));
								try {
									//根據北京時間格式轉換新值。有異常說明不是時間格式
									String DATE_FORMAT = "EEE MMM dd HH:mm:ss z yyyy";
									Date date = new SimpleDateFormat(DATE_FORMAT, Locale.US).parse(newValue);
									SimpleDateFormat format0 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
									newValue = format0.format(date);
									//如果新值是時間格式。將舊值的.0處理掉
									oldValue = oldValue.substring(0,oldValue.length()-2);
								} catch (Exception e) {
									// TODO: handle exception
								}
								//如果要修改的新值為空。不予記錄
								if(StringUtils.isBlank(newValue)){
									continue;
								}
								cl.setColumn(columnArr[i]);
								cl.setOldValue(oldValue);
								cl.setNewValue(newValue);
								changeList.add(cl);
							}
						}else if(columns.indexOf(",") == -1){
							//單列
							ChangeList cl = new ChangeList();
							cl.setColumn(columns);
							cl.setOldValue(DBUtils.getString(sql, columns));
							cl.setNewValue((String)map1.get(StringUtils.toCamelCase(columns)));
							changeList.add(cl);
						}
						//如果填寫了自定義拼接操作需要的信息,開始拼接操作信息
						if(operation.indexOf("param") != -1){
							operation = "";
							for (int i = 0; i < operationArr.length; i++) {
								operation += operationArr[i] + DBUtils.getString(sql, paramArr[i]);
							}
						}
					}
				}else if(type.indexOf("delete") != -1){
					//如果是刪除
					String[] paramArr = null;
					if(StringUtils.isNoneBlank(param)){
						paramArr = param.split(",");
					}
					String sql = "";
					if(("deleteA").equals(type)){
						//如果delete的傳入值是list
						sql = "select " + param + " from " + table + " where id='"+args[0].toString()+"'";
					}else{
						//如果delete的穿入值是對象
						sql = "select " + param + " from " + table + " where id='"+map1.get("id")+"'";
					}
					//如果填寫了表信息和字段信息
					if(operation.indexOf("param") != -1){
						String[] operationArr = operation.split("param");
						operation = "";
						for (int i = 0; i < operationArr.length; i++) {
							operation += operationArr[i] + DBUtils.getString(sql, paramArr[i]);
						}
					}
				}
				SystemLog systemLog = new SystemLog();
				systemLog.setUserAccount(sysUser.getUserName());
				systemLog.setDelFlag("1");
				systemLog.setType("9");
				systemLog.setOperationCode("1");
				systemLog.setCampId(sysUser.getCampId());
				systemLog.setGroupId(sysUser.getGroupId());
				systemLog.setOperateObject(operateObject);
				systemLog.setOperation(operation);
				if(changeList.size() != 0){
					for (int i = 0; i < changeList.size(); i++) {
						if(!changeList.get(i).getOldValue().equals(changeList.get(i).getNewValue())){
							systemLog.setId(GeneratorUUID.getId());
							systemLog.setOldValue(changeList.get(i).getOldValue());
							systemLog.setNewValue(changeList.get(i).getNewValue());
							//獲取操作對象名
							systemLog.setFeeItem(columnNameArr[i]);
							systemLog.preInsert();
							systemLogMapper.save(systemLog);
						}
					}
				}else{
					systemLog.preInsert();
					systemLogMapper.save(systemLog);
				}
			}
		} catch (Exception e) {
			// TODO: handle exception
			System.out.println(e.getMessage());
		}
		Object result = pjp.proceed();
		return result;
	}

  

這個方法是這個日志功能的核心所在,因為需要記錄操作的新舊值,所以進行了一系列的判斷。在這個方法中,用到了從注解中取值的操作,具體方法如下:

 

 1 /**
 2      * 獲取注解中對方法的描述信息 用於mapper層注解
 3      * 
 4      * @param joinPoint
 5      *            切點
 6      * @return 方法描述
 7      * @throws Exception
 8      */
 9     public static HashMap getMapperMthodDescription(JoinPoint joinPoint)
10             throws Exception {
11         MethodSignature signature = (MethodSignature) joinPoint.getSignature();
12         Method method = signature.getMethod();
13         String methodName = signature.getName();
14         Object[] arguments = joinPoint.getArgs();
15         HashMap<String,String> map = new HashMap<String,String>();
16         if (method.getName().equals(methodName)) {
17             Class[] clazzs = method.getParameterTypes();
18             if (clazzs.length == arguments.length) {
19                 map.put("operation", method.getAnnotation(SystemMapperLog.class).operation());
20                 map.put("table", method.getAnnotation(SystemMapperLog.class).table());
21                 map.put("columns", method.getAnnotation(SystemMapperLog.class).columns());
22                 map.put("columnsName", method.getAnnotation(SystemMapperLog.class).columnsName());
23                 map.put("operateObject", method.getAnnotation(SystemMapperLog.class).operateObject());
24                 map.put("param", method.getAnnotation(SystemMapperLog.class).param());
25                 map.put("type", method.getAnnotation(SystemMapperLog.class).type());
26                 map.put("condition", method.getAnnotation(SystemMapperLog.class).condition());
27             }
28         }
29         return map;
30     }

 

 

 

在最初編寫好切面后,又想到了一個新的需求:很多狀態都是用的數字,在保存時需要將其轉化為對應的字符串。所以想到了一種解決方法:將所有狀態保存為對應的常量,在注解中的傳入字段對應的常量。
因為並沒有寫完,所以單獨列出這個功能
 1 private final static Map<String,String> CATER_ORDER_STATE = new HashMap<String,String>();
 2 
 3 static{
 4     CATER_ORDER_STATE.put("6", "預訂");
 5 }
 6 
 7 /**
 8      * 獲取對應常量對應值的名稱
 9      * @param field
10      * @param key
11      * @return
12      */
13     public static String getFinalValue(String field,String key){
14         try {
15             Class<LogAopAction> clazz = LogAopAction.class;
16             Map map = (Map) clazz.getDeclaredField(field).get(null);
17             return (String)map.get(key);
18         } catch (Exception e) {
19             // TODO Auto-generated catch block
20             e.printStackTrace();
21             return null;
22         }
23     }

 

這樣在保存新舊值時調用getFinalValue方法即可將狀態值轉化為字符串。
四、在編寫好了切面后,就可以通過注解配置需要保存日志的切面了
具體配置方式:
@SystemMapperLog(operation="修改餐飲訂單param",operateObject="餐飲訂單管理",param="name",table="",columns="name,price",columnsName="菜品名稱,菜品價格",type="update")

 

1、operation是操作詳情,可自定義操作詳情的內容,其中的參數全部使用param,在解析注解保存日志時對其進行了處理。注意:operation內的param一定要和后面的param對應,param內的字段名使用逗號分隔即可
2、operateObject是模塊名稱,根據對應的操作模塊填寫
3、param上文說過,不再詳細介紹
4、table中寫入需要查詢的表名,在不需要儲存新舊值變化時不需要填寫此字段
5、columns中寫入需要記錄新舊值變化的字段,在不需要儲存新舊值變化時不需要填寫此字段,該字段內的數據用逗號分隔
6、columnsName中寫入需要記錄新舊值變化的字段名稱,在不需要儲存新舊值變化時不需要填寫此字段,該字段內的數據用逗號分隔
7、type為該接口的類型,類型分為save(保存)、update(修改)、deleteA(a類刪除)、deleteB(b類刪除)
因為不用的人寫代碼的習慣不同,所以刪除類型分成了兩種(也可以繼續添加)。
A類刪除適用於方法傳入對象是list的情況,會取到list中第一個值作為id進行查詢記錄操作。
B類刪除適用於方法傳入對象是object的情況,去從中取到id進行查詢記錄操作。在實際應用過程中,B類刪除不止可以適用於刪除方法。
 
至此。整個日志操作模塊基本完成,在這里做一個記錄。
 
2018-07-20更新:
1、格式化新值中的日期,使其可以正常對比新舊值
2、修改了操作列拼接的方法,之前多參數的拼接有bug


免責聲明!

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



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