基于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 }
四、在编写好了切面后,就可以通过注解配置需要保存日志的切面了
具体配置方式:
@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