如何优雅的使用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)

浙公网安备 33010602011771号