正在公司摸鱼的我突然接到了大佬给我的一个人任务,开发一个日志注解,来记录当方法中的每一个参数的名字,并记录每次的参数修改的值信息。接到任务的我瑟瑟发抖。
1. 数据库选择MongoDb
因为MongoDb具有以下特点
- MongoDb为文档型数据库。操作起来比较简单和容易。
- Mongo支持丰富的查询表达式。查询指令使用JSON形式的标记,可轻易查询文档中内嵌的对象及数组。
- MongoDb采用Bson(类json的一种二进制形式的存储格式)存储数据,跟新和查询的速度很快 当然了MongoDb还有很多优点我就不一一赘述了,大家可以去官网看看文档。
2. 开发实体类保存到数据库中
@Data
public class SysLogEntity implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
// 用户名
private String username;
// 用户操作
private String operation;
// 请求方法
private String method;
// 请求参数
private String params;
// 执行时长(毫秒)
private Long time;
// IP地址
private String ip;
// 创建时间
private Date createDate;
//所有参数名称 以逗号分割
private String parametersName;
//修改内容
private String modifyContent;
//操作类型
private String operationType;
//key值
private String logKey;
}
- 在数据库中保存的数据格式大体是这样的
3. 思考问题
当时我在做这一个功能的时候在想,你无法确定方法的入参到底是什么类型,有可能为实体类型,有可能为Map类型,有可能为String类型,如果参数为实体类型的需要去进行反射获取其中的所有字段,这是一个很耗时的操作,那么我为什么不拿出来在SpringBoot初始化的时候来做这个事情呢,于是就有了下面的操作。
- 首先创建一个实体类用来保存当前方法中的
参数名,参数类型,字段值,和当前字段值对应的具体位置
@Data
public class MethodParametersInfo {
/**
* 参数名称
*/
String parameterName;
/**
* 参数类型
*/
Class<?> classType;
/**
*字段
*/
Field[] fields;
/**
* 字段
*/
Integer position;
}
- 开发日志注解,
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ModifyLog {
/**
* 方法描述
* @return
*/
String value() default "";
/**
* 方法类型
* @return
*/
LogTypeEnum type();
/**
* @return 是否需要默认的改动比较
*/
boolean needDefaultCompare() default false;
/**
* key值可以根据spel表达式来填写
* @return
*/
String key();
}
public enum LogTypeEnum {
/**
* 保存
*/
Save("save"),
/**
* 修改
*/
Update("update"),
/**
* 删除
*/
Delete("delete"),
/**
* 保存或修改
*/
SaveOrUpdate("saveOrUpdate");
LogTypeEnum(String key){
this.key = key;
}
public String getKey() {
return key;
}
private final String key;
}
- 将实体类需要保存的字段进行细分,于是便又开发了一个注解,用来确定实体类中具体需要把那些字段信息保存到日志中
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD})
@Documented
public @interface DataName {
/**
* @return 字段名称
*/
String name() default "";
}
- 在Springboot进行初始化的时候把方法中的参数进行缓存,若参数为实体类,进行反射获取其中所有的字段属性
@Component
public class ModifyLogInitialization {
@Autowired
private RequestMappingHandlerMapping mapping;
public static Map<String,Map<String, MethodParametersInfo>> modifyLogMap = new HashMap<>();
/**
* @Author: lin
* @Description: 初始化Controller层上带有 @ModifyLog 注解的方法 缓存到map中
* @DateTime: 2020/12/25 15:52
* @Params: [event]
* @Return void
*/
@EventListener
public void initializationMethod(WebServerInitializedEvent event){
//获取所有方法
Map<RequestMappingInfo, HandlerMethod> handlerMethods = mapping.getHandlerMethods();
handlerMethods.forEach((k,v) -> {
//判断Controller层方法上注解
if(v.getMethodAnnotation(ModifyLog.class)!=null){
Class<?> beanType = v.getBeanType();
//获取方法对象
Method method = v.getMethod();
//方法参数
MethodParameter[] parameters = v.getMethodParameters();
//参数名作为key 缓存参数信息
HashMap<String, MethodParametersInfo> methodMap = new HashMap<>();
//类路径加方法名作为key
String methodKey = beanType.getName()+"."+method.getName()+"()";
modifyLogMap.put(methodKey,methodMap);
int i = 0;
//循环方法参数
for (MethodParameter parameter : parameters) {
MethodParametersInfo info = new MethodParametersInfo();
//参数位置
info.setPosition(i);
//参数名称
String parameterName = parameter.getParameter().getName();
info.setParameterName(parameterName);
//参数类型
Class<?> parameterType = parameter.getParameterType();
info.setClassType(parameterType);
//获取所有字段
Field[] fields = parameterType.getDeclaredFields();
if(!parameterType.isAssignableFrom(String.class) & !parameterType.isAssignableFrom(Map.class)){
info.setFields(fields);
}
//加入到Map中
methodMap.put(parameterName,info);
i++;
}
}
});
}
}
用监听的方式监听WebServerInitializedEvent在启动的时候做缓存,RequestMappingHandlerMapping可以获取所有Controllec层标注@RequestMapping的方法
4. 用Aop来保存参数内容
-
可以用spel表达式加入到注解中,这样就可以在aop中去解析获取关键值,可以参考 juejin.cn/post/684490… 篇文章
-
为了免去使用if和else来判断参数类型我使用了适配器模式,并把每一个参数类的解析抽出取来,这样当你添加不同参数类型的解析的时候可以避免代码的侵入性
-
下面是一个类型判断的接口,可以判断当前参数的类型,并且去调用当前参数类型的解析器
public interface TypeAdapter {
/**
* @Author: lin
* @Description: 判断支持类型
* @DateTime: 2021/1/4 9:16
* @Params: [classType]
* @Return boolean
*/
boolean supprot(Class<?> classType);
/**
* @Author: lin
* @Description: 获取文本
* @DateTime: 2021/1/4 9:16
* @Params: [sb, k, v, oldObjectList, newObjectList]
* @Return void
*/
void getContent(StringBuilder sb, String k, MethodParametersInfo v, List<Object> oldObjectList, List<Object> newObjectList);
- 这是map的类型判断并且实现map类型的参数解析
@Component
public class ClassTypeAdapter implements TypeAdapter {
@Autowired
ContentParse classParse;
@Override
public boolean supprot(Class<?> classType) {
return !classType.isAssignableFrom(String.class) && !classType.isAssignableFrom(Map.class);
}
@Override
public void getContent(StringBuilder sb, String k, MethodParametersInfo v, List<Object> oldObjectList, List<Object> newObjectList) {
classParse.getDifferentContent(sb,k,v,oldObjectList,newObjectList);
}
}
- 下面是一个参数类型的解析接口
public interface ContentParse {
/**
* @Author: lin
* @Description: 根据字段不同类型进行解析
* @DateTime: 2020/12/28 15:13
* @Params: [sb, k, v]
* @Return void
*/
void getDifferentContent(StringBuilder sb, String k, MethodParametersInfo v, List<Object> oldObjectList,List<Object> newObjectList);
}
- 参数类型解析接口的实现
@Component("classParse")
public class ClassParse implements ContentParse {
@Override
public void getDifferentContent(StringBuilder sb, String k, MethodParametersInfo v, List<Object> oldObjectList, List<Object> newObjectList) {
//获取所有字段
Field[] fields = v.getFields();
//根据字段名称跟字段映射成Map
Map<String, List<Field>> fieldMap = Arrays.stream(fields).collect(Collectors.groupingBy(Field::getName));
//记录位置
Integer position = v.getPosition();
//取出当前位置的 参数对应的值
Object oldObject = oldObjectList.get(position);
Object newObject = newObjectList.get(position);
//转换为Map
Map<String, Object> oldMap = JSONUtil.parseObj(oldObject);
Map<String, Object> newMap = JSONUtil.parseObj(newObject);
oldMap.forEach((oldK,oldV) -> {
Object newV = newMap.get(oldK);
if(!newV.equals(oldV)){
//值不相等,根据当前字段名(k值)取出当前字段
List<Field> fieldList = fieldMap.get(oldK);
Field field = fieldList.get(0);
//有没有DataName注解
if(field.isAnnotationPresent(DataName.class)){
sb.append("[参数: ").append(k)
.append("中属性").append(field.getName()).append("]从[")
.append(oldV).append("]改为了[").append(newV).append("];");
}
}
});
}
}
- 之后我们就可以写一个Util类用来获取数据库中根据当前类路径跟方法名所对应的参数值,并跟当前传的新值作比较,判断那个参数的值有修改
@Component
public class ModifyLogUtil {
/**
* @Author: lin
* @Description: 获取参数名称保存到数据库中以逗号分割
* @DateTime: 2020/12/24 14:46
* @Params: [methodKey]
* @Return java.lang.String
*/
@Autowired
private List<TypeAdapter> typeAdapterList;
public String getParametersName(String methodKey){
// 类路径跟方法名获取参数信息
Map<String, Map<String, MethodParametersInfo>> modifyLogMap = ModifyLogInitialization.modifyLogMap;
//参数信息 key是参数名 value参数信息
Map<String, MethodParametersInfo> parameterMap = modifyLogMap.get(methodKey);
StringBuilder sb = new StringBuilder();
if(parameterMap != null){
parameterMap.forEach((k,v) ->{
sb.append(k);
sb.append(",");
});
sb.deleteCharAt(sb.length()-1);
}
return sb.toString();
}
/**
* @Author: lin
* @Description: 根据参数名称获取参数信息进行比对
* @DateTime: 2020/12/24 15:05
* @Params: [methodKey, parameter]
* @Return java.lang.String
*/
public String getContentName(String methodKey, String params, ProceedingJoinPoint joinPoint){
//解析旧数据
List<Object> oldObjectList = parseOldObject(params);
//解析新数据
List<Object> newObjectList = parseNewObject(joinPoint);
// 类路径跟方法名获取参数信息
Map<String, Map<String, MethodParametersInfo>> modifyLogMap = ModifyLogInitialization.modifyLogMap;
//参数信息 key是参数名 value参数信息
Map<String, MethodParametersInfo> parameterMap = modifyLogMap.get(methodKey);
//记录
StringBuilder sb = new StringBuilder();
parameterMap.forEach((k,v) ->{
Class<?> classType = v.getClassType();
typeAdapterList.stream().filter(typeAdapter -> typeAdapter.supprot(classType)).findFirst()
.get().getContent(sb,k,v,oldObjectList,newObjectList);
});
log.info("参数改变的值为: {}",sb.toString());
return sb.toString();
}
/**
* @Author: lin
* @Description: 解析旧参数
* @DateTime: 2020/12/25 14:02
* @Params: [params]
* @Return java.util.List<java.lang.Object>
*/
public List<Object> parseOldObject(String params){
JSONArray array = JSONUtil.parseArray(params);
return new ArrayList<>(array);
}
/**
* @Author: lin
* @Description: 解析新参数
* @DateTime: 2020/12/25 14:02
* @Params: [joinPoint]
* @Return java.util.List<java.lang.Object>
*/
public List<Object> parseNewObject(ProceedingJoinPoint joinPoint){
Object[] args = joinPoint.getArgs();
return new ArrayList<>(Arrays.asList(args));
}
}
- 接下来就是在aop中使用这个util工具
@Slf4j
@Aspect
@Component
public class SysLogAspect {
@Autowired
private KeyResolver keyResolver;
@Autowired
private ModifyLogUtil modifyLogUtil;
@Autowired
MongoTemplate mongoTemplate;
@Around("@annotation(modifyLog)")
public Object modifyLogAround(ProceedingJoinPoint point,ModifyLog modifyLog) throws Throwable {
long beginTime = System.currentTimeMillis();
// 执行方法
Object result = point.proceed();
// 执行时长(毫秒)
long time = System.currentTimeMillis() - beginTime;
saveUpdateSysLog(point,time,modifyLog);
return result;
}
/**
* @Author: lin
* @Description: 当方法为Update类型的时候要获取其中不同项
* @DateTime: 2020/12/22 15:51
* @Params: [joinPoint, time]
* @Return void
*/
private void saveUpdateSysLog(ProceedingJoinPoint joinPoint, long time,ModifyLog modifyLog){
//日志实体类
SysLogEntity currentSysLogEntity = new SysLogEntity();
currentSysLogEntity.setId(SnowflakeUtil.snowflakeId());
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
//获取key值
String key = keyResolver.resolver(modifyLog, joinPoint);
currentSysLogEntity.setLogKey(key);
log.info("key值{}",key);
//日志类型
String logType = modifyLog.type().getKey();
// 注解上的描述
currentSysLogEntity.setOperation(modifyLog.value());
//操作类型
currentSysLogEntity.setOperationType(logType);
//设置实体类字段
commonMethod(joinPoint, time, signature, currentSysLogEntity);
//根据类名加方法名去缓存中查找
String parametersName = modifyLogUtil.getParametersName(currentSysLogEntity.getMethod());
//方法中参数名称
currentSysLogEntity.setParametersName(parametersName);
//当前类型为save则保存
if(Constant.MODIFY_LOG_SAVE.equals(logType)){
mongoTemplate.insert(currentSysLogEntity);
return;
}
//当前类型为删除记录删除的字段值
if(Constant.MODIFY_LOG_DELETE.equals(logType)){
Object[] args = joinPoint.getArgs();
String deleteParam = JSONUtil.toJsonStr(args);
currentSysLogEntity.setModifyContent("当前删除的参数信息是:" + deleteParam);
mongoTemplate.insert(currentSysLogEntity);
return;
}
//当前类型为保存或修改
if(Constant.MODIFY_LOG_SAVE_OR_UPDATE.equals(logType)){
SysLogEntity oldSysLogEntity = getEntity(currentSysLogEntity);
//没有查询到就插入信息
if(oldSysLogEntity == null){
mongoTemplate.insert(currentSysLogEntity);
return;
}
//有信息则判断是否需要插入修改字段的信息
if(modifyLog.needDefaultCompare()){
String content = modifyLogContent(joinPoint,oldSysLogEntity);
//设置修改字段属性
currentSysLogEntity.setModifyContent(content);
}
mongoTemplate.insert(currentSysLogEntity);
}
}
/**
* @Author: lin
* @Description: 设置实体类属性公用方法
* @DateTime: 2020/12/22 16:07
* @Params: [joinPoint, time, signature, sysLogEntity]
* @Return void
*/
private void commonMethod(ProceedingJoinPoint joinPoint, long time, MethodSignature signature, SysLogEntity sysLogEntity) {
//请求的类名
String className = joinPoint.getTarget().getClass().getName();
//方法名
String methodName = signature.getName();
sysLogEntity.setMethod(className + "." + methodName + "()");
//请求的参数
Object[] args = joinPoint.getArgs();
String argsStr = JSONUtil.toJsonStr(args);
//保存参数
sysLogEntity.setParams(argsStr);
// 获取request
HttpServletRequest request = HttpContextUtils.getHttpServletRequest();
// 设置IP地址
sysLogEntity.setIp(IPUtils.getIpAddr(request));
//用户名
String username = ((SysUserEntity) SecurityUtils.getSubject().getPrincipal()).getUsername();
sysLogEntity.setUsername(username);
sysLogEntity.setTime(time);
sysLogEntity.setCreateDate(new Date());
}
/**
* @Author: lin
* @Description: 是update操作则进行比对判断是那些 字段值有改变
* @DateTime: 2020/12/24 14:55
* @Params: []
* @Return void
*/
private String modifyLogContent(ProceedingJoinPoint joinPoint,SysLogEntity oldSysLogEntity){
//实际参数
String params = oldSysLogEntity.getParams();
//取出修改的参数属性
String contentName = modifyLogUtil.getContentName(oldSysLogEntity.getMethod(), params, joinPoint);
return contentName;
}
private SysLogEntity getEntity(SysLogEntity sysLogEntity){
Query query = new Query();
Criteria criteria = new Criteria();
criteria.and("logKey").is(sysLogEntity.getLogKey());
criteria.and("method").is(sysLogEntity.getMethod());
query.addCriteria(criteria);
//根据日期排序
query.with(Sort.by(Sort.Order.desc("createDate")));
query.limit(1);
SysLogEntity one = mongoTemplate.findOne(query, SysLogEntity.class);
return one;
}
}
5. 小结
- 作为一个刚入门半年的菜鸟我还是又很多地方需要学习,这个方法肯定还能在进一步的优化,希望各位大佬能给指点一下。希望大家共同进步。