When I was in the company, I suddenly received a personal task from the boss to develop a log annotation to record the name of every parameter in the method and record the value information of each parameter modification. I shivered when I received the assignment.

1. Select MongoDb for the database


Because MongoDb has the following characteristics

  • MongoDb is a document database. It is relatively simple and easy to operate.
  • Mongo supports rich query expressions. Query instructions use JSON-style tags to easily query objects and arrays embedded in documents.
  • MongoDb uses Bson(a binary jSON-like storage format) to store data, which is fast to update and query

Of course, there are many other advantages of MongoDb that I won’t elaborate on, but you can go to the official website to see the documentation.

2. Develop entity classes and save them in the database


@Data public class SysLogEntity implements Serializable { private static final long serialVersionUID = 1L; private Long id; Private String username; // User operation private String operation; Private String method; // Request parameter private String params; // Execution duration (ms) private Long time; // IP address private String IP; Private Date createDate; // All parameter names are separated by commas. Private String parametersName; // modifyContent private String modifyContent; // operationType private String operationType; // Key value private String logKey; }Copy the code
  • The format of the data stored in the database looks something like this

3. Think about the problem


When I was doing this, I was thinking, you don’t know what type of input the method is, it could be an entity, it could be a Map, it could be a String, and if the parameter is an entity you have to reflect and get all the fields in it, which is a time-consuming operation, So why don’t I just take it out and do it during SpringBoot initialization, so there’s the following operation.

  • Start by creating an entity class to hold the current methodParameter names.The parameter types.The field values.The position corresponding to the current field value
@data public class MethodParametersInfo {/** * parameterName */ String parameterName; /** * Parameter type */ Class<? > classType; /** * Field */ Field[] fields; /** ** field */ Integer position; }Copy the code
  • Development log annotations,
@target (ElementType.METHOD) @Retention(RetentionPolicy.runtime) @documented Public @interface ModifyLog {/** * @return */ String value() default ""; /** * Method type * @return */ LogTypeEnum type(); /** * @return whether to compare the default changes */ Boolean needDefaultCompare() default false; * @return */ String key(); }Copy the code
Public enum LogTypeEnum {/ Save * * * * / Save (" Save "), * / Update / * * * change (" Update "), / * * * * to Delete/Delete (" Delete "), /** * Save or modify */ SaveOrUpdate(" SaveOrUpdate "); LogTypeEnum(String key){ this.key = key; } public String getKey() { return key; } private final String key; }Copy the code
  • By subdividing the fields that the entity class needs to keep, an annotation was developed to determine exactly which fields in the entity class need to be kept in the log
@Retention(RetentionPolicy.RUNTIME) @Target({ElementType.FIELD}) @Documented public @interface DataName { /** * @return */ String name() default ""; }Copy the code
  • During Springboot initialization, the method’s parameters are cached. If the parameter is an entity class, all the field attributes are reflected
@Component public class ModifyLogInitialization { @Autowired private RequestMappingHandlerMapping mapping; public static Map<String,Map<String, MethodParametersInfo>> modifyLogMap = new HashMap<>(); /** * @author: Lin * @description: Initializes Controller layer methods with the @modifylog annotation cached in map * @dateTime: 2020/12/25 15:52 * @params: [event] * @ Return void * / @ EventListener public void initializationMethod (WebServerInitializedEvent event) {/ / get all the way Map<RequestMappingInfo, HandlerMethod> handlerMethods = mapping.getHandlerMethods(); Handlermethods.foreach ((k,v) -> {if(v.gete methodannotation (modifylog.class)! =null){ Class<? > beanType = v.getBeanType(); Method = v.getMethod(); // MethodParameter[] parameters = v.getmethodParameters (); // Parameter names as key cache parameter information HashMap<String, MethodParametersInfo> methodMap = new HashMap<>(); 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); // Parameter type Class<? > parameterType = parameter.getParameterType(); info.setClassType(parameterType); / / get all the Field Field [] fields = parameterType. GetDeclaredFields (); if(! parameterType.isAssignableFrom(String.class) & ! parameterType.isAssignableFrom(Map.class)){ info.setFields(fields); } // Add methodmap. put(parameterName,info) to the Map; i++; }}}); }}Copy the code

By listening to monitor WebServerInitializedEvent do cache at startup, RequestMappingHandlerMapping can get all the mark @ RequestMapping Controllec layer method

4. Use Aop to save parameter content


  • You can use SPEL expressions to add annotations so that key values can be parsed in AOP at juejin.cn/post/684490… article

  • Instead of using if and else to determine the type of arguments, I used the adapter pattern and extracted the parsing for each argument class, so that when you add parsing for different argument types you can avoid code invasiveness

  • Here is a type determination interface that can determine the type of the current parameter and call the parser for the current parameter type

Public Interface TypeAdapter {/** * @author: Lin * @description: Ap * @datetime: 2021/AP 9:16 * @params: [classType] * @Return boolean */ boolean supprot(Class<? > classType); /** * @author: Lin * @description: Ap * @datetime: 2021/AP 9:16 * @params: [sb, k, v, oldObjectList, newObjectList] * @Return void */ void getContent(StringBuilder sb, String k, MethodParametersInfo v, List<Object> oldObjectList, List<Object> newObjectList);Copy the code
  • This is map type determination and implements map type parameter resolution
@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); }}Copy the code
  • Here is a parsing interface for parameter types
Public interface ContentParse {/** * @author: Lin * @description: 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); }Copy the code
  • Parameter type resolution interface implementation
@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.goetfields (); Map<String, List<Field>> fieldMap = Arrays.stream(fields).collect(Collectors.groupingBy(Field::getName)); // Record position Integer position = v.gott position (); Object oldObject = oldobjectList. get(position); Object newObject = newObjectList.get(position); 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)){List<Field> fieldList = fieldmap. get(oldK); Field field = fieldList.get(0); / / is there a DataName annotate the if (field. IsAnnotationPresent (DataName. Class)) {sb. Append (" [parameters: "), append (k), append (" properties "). Append (field. The getName ()), append ("] from ["), append (oldV). Append ("] to ["), append (newV). Append ("];" ); }}}); }}Copy the code
  • We can then write an Util class that retrieves the value of the parameter in the database based on the current classpath and method name and compares it to the new value passed to determine that the value of that parameter has been changed
@Component public class ModifyLogUtil {/** * @author: Lin * @description: Get parameter names in database separated by commas * @dateTime: 2020/12/24 14:46 * @Params: [methodKey] * @Return java.lang.String */ @Autowired private List<TypeAdapter> typeAdapterList; Public String getParametersName(String methodKey){// Class path and method name get parameter information Map<String, Map<String, MethodParametersInfo>> modifyLogMap = ModifyLogInitialization.modifyLogMap; // Parameter information Key is the parameter name value Parameter information 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(); * @dateTime: 2020/12/24 15:05 * @params: [methodKey, parameter] * @Return java.lang.String */ public String getContentName(String methodKey, String params, List<Object> oldObjectList = parseOldObject(Params); List<Object> newObjectList = parseNewObject(joinPoint); / / class path and method of parameter information Map < String, Map < String, MethodParametersInfo > > modifyLogMap = ModifyLogInitialization. ModifyLogMap; // Parameter information Key is the parameter name value Parameter information 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 (" parameter changed to: {}", sb.tostring ()); return sb.toString(); } /** * @author: Lin * @description: Parse old parameters * @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: parse new parameters * @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)); }}Copy the code
  • The next step is to use the util tool in AOP
@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(); // Run time (ms) long time = system.currentTimemillis () -beginTime; saveUpdateSysLog(point,time,modifyLog); return result; } /** * @author: Lin * @description: @dateTime: 2020/12/22 15:51 @params: [joinPoint, time] * @Return void */ private void saveUpdateSysLog(ProceedingJoinPoint joinPoint, Long Time,ModifyLog ModifyLog){// Log entity class SysLogEntity currentSysLogEntity = New SysLogEntity(); currentSysLogEntity.setId(SnowflakeUtil.snowflakeId()); MethodSignature signature = (MethodSignature) joinPoint.getSignature(); String key = keyresolver. resolver(modifyLog, joinPoint); currentSysLogEntity.setLogKey(key); The info (" key value {} ", key); // logType String logType = modifylog.type ().getkey (); / / comment on the description of the currentSysLogEntity. SetOperation (modifyLog. Value ()); / / operation type currentSysLogEntity setOperationType (logType); // Set entity class field commonMethod(joinPoint, Time, Signature, currentSysLogEntity); / / according to the name of the class of the Canadian side had to cache lookup String parametersName = modifyLogUtil. GetParametersName (currentSysLogEntity. GetMethod ()); / / method in parameter name currentSysLogEntity. SetParametersName (parametersName); Mongotemplate. insert(currentSysLogEntity); // Save if(constant.modify_log_save.equals (logType)){mongotemplate. insert(currentSysLogEntity); return; } // The value of the deleted field if(constant.modify_log_delete.equals (logType)){Object[] args = joinPoint.getargs (); String deleteParam = JSONUtil.toJsonStr(args); CurrentSysLogEntity. SetModifyContent (" delete current parameter information is: "+ deleteParam); mongoTemplate.insert(currentSysLogEntity); return; } // The current type is saved or modified if(constant.modify_log_SAVe_or_update.equals (logType)){SysLogEntity oldSysLogEntity = getEntity(currentSysLogEntity); Mongotemplate. insert(currentSysLogEntity); // Insert information if(oldSysLogEntity == null){mongotemplate. insert(currentSysLogEntity); return; } / / a information, judge whether to modify the field to be inserted if (modifyLog. NeedDefaultCompare ()) {String content = modifyLogContent(joinPoint,oldSysLogEntity); / / set modify field properties currentSysLogEntity. SetModifyContent (content). } mongoTemplate.insert(currentSysLogEntity); }} /** * @author: Lin * @description: Set entity class attributes public method * @dateTime: 2020/12/22 16:07 * @params: [joinPoint, time, signature, sysLogEntity] * @Return void */ private void commonMethod(ProceedingJoinPoint joinPoint, long time, MethodSignature signature, String className = joinpoint.gettarget ().getclass ().getname (); SysLogEntity SysLogEntity) {// request className String className = joinpoint.gettarget ().getclass ().getname (); // methodName String methodName = signature.getname (); sysLogEntity.setMethod(className + "." + methodName + "()"); Object[] args = JoinPoint.getargs (); String argsStr = JSONUtil.toJsonStr(args); // Save sysLogEntity. SetParams (argsStr); / / get request it request = HttpContextUtils. GetHttpServletRequest (); // Set the IP address sysLogEntity. SetIp (iputils.getipaddr (request)); // Username String Username = ((SysUserEntity) securityutils.getSubject ().getPrincipal()).getUsername(); sysLogEntity.setUsername(username); sysLogEntity.setTime(time); sysLogEntity.setCreateDate(new Date()); @dateTime: 2020/12/24 14:55 * @params: @datetime: 2020/12/24 14:55 [] * @return void */ private String modifyLogContent(ProceedingJoinPoint joinPoint,SysLogEntity oldSysLogEntity){// Actual parameter  String params = oldSysLogEntity.getParams(); / / remove the modified parameter properties 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; }}Copy the code

5. Summary


  • As a beginner for half a year I still need to learn a lot of places, this method can be further optimized, I hope you can give advice. I hope we can make progress together.