如何优雅的使用aop记录带参数的复杂web接口日志
1、新建一个自定义注解
package com.ninestar.userFile.aop.annotation; import com.ninestar.userFile.constant.Constants; import java.lang.annotation.*; /** * 系统日志注解 * * @Author Tring * @Date 2024年5月7日14:31:18 */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface AutoLog { /** * 操作日志类型 * Constants.SAVE * 如果是保存操作,则prefix和field值填一个值,fiel是对应保存表的主键,如:prefix = {"员工信息保存成功"},field = {"employeeId"} * @return (1增、2删、3查、4该、5导入、6导出,7保存) */ int slOperateType(); /** * 操作日志类型 * Constants.MODULE_ALL_FILE * @return 全部文件、信息查询、待归档文件、操作日志 */ String slModule(); /** * 记录日志时,拼接的前缀(默认后面加":"),当记录多个参数的字段时,前缀一般需要和字段(field)一一对应, * 当字段(field)数量大于前缀数量时,默认取最后一个前缀,作为超出的字段的记录前缀。 */ String[] prefix() default {}; /** * 日志中记录的方法参数索引,默认记录第0个参数。如果字段(field)为空数组,则记录该参数所有信息。<br> * 如果该参数是集合(Collection),则遍历记录每一个元素。 * @return 方法参数索引 */ int argsIndex() default 0; /** * 方法参数中需要记录的属性字段名,可设置多个需要记录日志的字段 */ String[] field() default {}; /** * 对应的操作的mapper(一般只有修改的时候才填) */ String mapperCode() default Constants.MAPPER_EMPLOYEEINFO_DAO; }
2、新增切面(这里只提供部门代码)
@Around("execution(* *..*Controller.*(..)) && @annotation(com.ninestar.userFile.aop.annotation.AutoLog)") public Object sysLogs(ProceedingJoinPoint joinPoint) throws Throwable{ Class<?> targetClass = joinPoint.getTarget().getClass(); String methodName = joinPoint.getSignature().getName(); Method[] methods = targetClass.getMethods(); // 当前切中的方法 Method method = null; for (int i = 0; i < methods.length; i++){ if (methods[i].getName() == methodName){ method = methods[i]; break; } } // 执行方法前 Object proceed = null; //访问目标方法的参数: Object[] args = joinPoint.getArgs(); HttpServletRequest request = HttpUtil.getRequest(); // 获取自定义注解中的参数 add by Tring 2024/5/9 AutoLog autoLog = method.getAnnotation(AutoLog.class); // 日志的内容,下面进行拼接 StringBuilder logContents = new StringBuilder(); // 方法参数,需要记录的信息 Integer argsIndex = autoLog.argsIndex(); String[] prefixs = autoLog.prefix(); String[] fields = autoLog.field(); String opeName = autoLog.slModule(); Integer operateType = autoLog.slOperateType(); String mapperCode = autoLog.mapperCode(); switch (operateType) { case Constants.ADD: case Constants.DEL: case Constants.SELECT: case Constants.EXPORT: // 需要记录日志的参数对象,如果参数是个集合,则遍历每一个元素进行记录 Object arg = args[argsIndex]; if (arg instanceof Collection) { Collection as = (Collection) arg; for (Object a : as) { if (logContents.length() > 0) { logContents.append(";"); } logContents.append(spliceLogContents(a, fields, prefixs)); } } else { logContents.append(spliceLogContents(arg, fields, prefixs)); } break; case Constants.IMPORT: // 获取导入文件名 add by Tring 2024/5/9 List<String> fileNames = new ArrayList<>(); for (Object arg1 : args) { if (arg1 instanceof MultipartFile) { MultipartFile file = (MultipartFile) arg1; fileNames.add(file.getOriginalFilename()); } } logContents.append(prefixs[0]).append(":").append(fileNames.toString()); break; case Constants.SAVE: arg = args[argsIndex]; logContents.append(spliceLogContentsOfUpd(arg, fields, prefixs, mapperCode)); break; // ... 其他文件类型 default: } try { if(BeanUtils.isNotEmpty(request)) { request.setAttribute("enterController", true); } proceed = joinPoint.proceed(); } catch (Throwable e) { logger.error("调用接口异常参数: " + getMethodParamContent(args)); throw e; }finally{ //保存海纳云用户登陆日志时不记录 if(BeanUtils.isNotEmpty(request) && !NINESTAR_LOGIN_URL.equals(request.getRequestURI())) { // && !SEARCH_API.equals(request.getRequestURI()) this.saveLog(request.getRequestURI(), opeName, logContents.toString()); } } return proceed; } public void saveLog(String reqUrl,String opeName, String opeContent){ try { String executor = "系统[无用户登录系统]"; if(StringUtil.isNotEmpty(AuthenticationUtil.getCurrentUserFullname())){ executor = String.format("%s[%s]",AuthenticationUtil.getCurrentUserFullname(),AuthenticationUtil.getCurrentUsername()); } String logType = "操作日志"; SysLogSl log = new SysLogSl(opeName, LocalDateTime.now(), executor, WebUtil.getIpAddr(HttpUtil.getRequest()), logType, reqUrl, opeContent); log.setSlId(UniqueIdUtil.getSuid()); sysLogManager.reader(log); } catch (Exception e) { logger.error("保存操作日志失败。" + ExceptionUtil.getFullStackTrace(e)); } } /** * 通过切面参数获取内容 * @param args * @return */ private String getMethodParamContent(Object[] args) { StringBuffer sb = new StringBuffer(); if (BeanUtils.isNotEmpty(args)) { for (Object object : args) { if (object instanceof ServletRequest || object instanceof ServletResponse) { continue; } try { sb.append(JsonUtil.toJson(object)); sb.append(" "); } catch (Exception e) { sb.append(object.toString()); } } } return sb.toString(); } /** * 利用反射,从对象中,获取属性字段的值,拼接前缀。 * * @param obj 对象 * @param fields 字段名称集合 * @param prefixs 前缀集合 * @return 拼接内容 * @throws NoSuchFieldException 找不字段异常 * @throws IllegalAccessException 字段访问异常 * @Author Tring * @Date 2024年5月10日10:50:06 */ private String spliceLogContents(Object obj, String[] fields, String[] prefixs) throws IllegalAccessException { // 如果没有定义属性,则直接将对象toString后记录,如果定义了前缀,则拼接上前缀后记录 if (fields == null || fields.length == 0) { if (prefixs != null && prefixs.length > 0) { return prefixs[0] + ":" + obj.toString(); } return obj.toString(); } StringBuilder sb = new StringBuilder(); boolean hasPre = prefixs.length > 0; int prefixMaxIndex = prefixs.length - 1; int prefixIndex = 0; Class<?> aClass = obj.getClass(); // 如果该对象中找不到属性,则向上父类查找 Map<String, Field> fieldMap = new HashMap<>(); for (; aClass != Object.class; aClass = aClass.getSuperclass()) { for (Field f : aClass.getDeclaredFields()) { fieldMap.putIfAbsent(f.getName(), f); } } Field field = null; Object fieldValue = null; for (int i = 0, len = fields.length; i < len; i++) { field = fieldMap.get(fields[i]); if (field == null) { continue; } field.setAccessible(true); fieldValue = field.get(obj); if (sb.length() > 0) { sb.append(","); } if (hasPre) { prefixIndex = Math.min(i, prefixMaxIndex); sb.append(prefixs[prefixIndex]); if (!prefixs[prefixIndex].endsWith(":")) { sb.append(":"); } } sb.append(fieldValue == null ? "" : fieldValue); } return sb.toString(); } /** * 利用反射,从对象中,获取属性字段的值,拼接前缀。(用于记录update日志) * @param obj 对象 * @param fields 字段名称集合 * @param prefixs 前缀集合 * @param mapperCode mapper字符串 * @return 拼接内容 * @throws NoSuchFieldException 找不字段异常 * @throws IllegalAccessException 字段访问异常 * @Author Tring * @Date 2024年5月10日10:50:06 */ public String spliceLogContentsOfUpd(Object obj, String[] fields, String[] prefixs, String mapperCode) throws IllegalAccessException { //获取DTO类 Class<?> aClass = obj.getClass(); // 如果该对象中找不到属性,则向上父类查找 Map<String, Field> fieldMap = new HashMap<>(); for (; aClass != Object.class; aClass = aClass.getSuperclass()) { for (Field f : aClass.getDeclaredFields()) { fieldMap.putIfAbsent(f.getName(), f); } } //获取主键的值 Field field = fieldMap.get(fields[0]); if (field == null) { return ""; } field.setAccessible(true); Object fieldValue = field.get(obj); if(ObjectUtils.isEmpty(fieldValue)){ //如果fieldValue为空,则证明是新增操作,把“:”去掉 String content = spliceLogContents(obj,fields,prefixs); return content.length()>0 ? content.substring(0,content.length()-1) : ""; } //获取原数据 BaseMapper mapper = MapperFactory.getMappers(mapperCode); Object oldObj = null; if(!ObjectUtils.isEmpty(mapper)){ oldObj = mapper.selectById(fieldValue.toString()); } return prefixs[0]+ObjUtils.compareObjects(obj,oldObj); }
3、创建mapper 工厂
package com.ninestar.userFile.config; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.ninestar.userFile.constant.Constants; import com.ninestar.userFile.persistence.dao.*; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import javax.annotation.PostConstruct; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; /** * @author: Tring * @since: 2024/5/10 16:34 * @description:mapper工厂 */ @Configuration public class MapperFactory { @Autowired private EmployeeInfoDao employeeInfoDao; @Autowired private FileInfoDao fileInfoDao; @Autowired private SecretInfoDao secretInfoDao; @Autowired private SysLogSlDao sysLogSlDao; protected static Map<String, BaseMapper> mappers = new ConcurrentHashMap<>(); @PostConstruct public void init(){ mappers.put(Constants.MAPPER_EMPLOYEEINFO_DAO, employeeInfoDao); mappers.put(Constants.MAPPER_FILEINFO_DAO, fileInfoDao); mappers.put(Constants.MAPPER_SECRETINFO_DAO, secretInfoDao); mappers.put(Constants.MAPPER_SYSLOGSL_DAO, sysLogSlDao); } public static BaseMapper getMappers(String code) { return mappers.get(code); } }
4、写工具类,用于对比修改前后的日志内容显示
package com.ninestar.userFile.utils; import cn.hutool.core.util.ObjectUtil; import java.lang.reflect.Field; import java.text.SimpleDateFormat; import java.util.*; /** * @author: Tring * @since: 2024/5/10 9:57 * @description:对象工具类 */ public class ObjUtils { /** * 获取目标对象跟原对象之间属性的变化 * @param obj1 目标对象 * @param obj2 原对象 * @return 结果:属性名字:原值-->修改后的值,如account: null --> 456,fullName: 333 --> 789 * @throws IllegalAccessException */ public static String compareObjects(Object obj1, Object obj2) throws IllegalAccessException { Class<?> clazz1 = obj1.getClass(); Class<?> clazz2 = obj2.getClass(); StringBuilder str = new StringBuilder(); Field[] fields1 = clazz1.getDeclaredFields(); Field[] fields2 = clazz2.getDeclaredFields(); for (Field field1 : fields1) { field1.setAccessible(true); Object value1 = field1.get(obj1); for (Field field2 : fields2) { field2.setAccessible(true); Object value2 = field2.get(obj2); if (value1 != null && ObjectUtil.equal(field1.getName(),field2.getName()) && !ObjectUtil.equal(value1,value2)) { if(value2 instanceof Date){ SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); str.append(field1.getName() + ": " + sdf.format(value2) + " --> " + sdf.format(value1) +","); }else{ str.append(field1.getName() + ": " + value2 + " --> " + value1 +","); } } } } return str.length()>0 ?str.substring(0,str.length()-1):str.toString(); } }
5、最后在需要打印日志的Controller方法中加上一开始创建的自定义注解即可
@AutoLog(slModule= Constants.MODULE_INFO_SEL,slOperateType = Constants.SAVE,prefix = {"保存档案基础信息"},field = {"employeeId"},mapperCode = Constants.MAPPER_EMPLOYEEINFO_DAO)