preface

Hello everyone, here is the classic chicken wing, today to bring you a SpringAop based on the implementation of operation logging solution. You might say, gee, something as simple as operation logging is a cliche. No!



Operation logs on the Internet generally record the operator, operation description, and IP address. It’s nice to add modified data and execution time. So! What’s different about this one?! Today it not only logs everything above, but also pre-operation data, error messages, stack information, etc. Start of the text ~~~~~

Thinking is introduced

Pre-operation data is important for recording operation logs. We explore this by modifying the scene. When we want to fully record the flow of data, we must record the data before modification, and when the front desk submits, there is only the modified data, so how to find the data before modification? We need to know the table name of the data before modification, the primary key of the table field, and the value of the primary key of the table. Select * from table name where primary key = primary key. We get the data before we modify it, convert it to JSON and store it in the database. How to get the three attributes is the most important thing. The solution we took was to annotate the submitted mapping entities and fetch values based on Java reflection. Further assembly to obtain object data. So where is AOP used? We need to annotate the method of logging operations, and then get to the pointcut through the aspect, all the data is obtained through reflection.

Define operation log annotations

Since it is based on SPINRG AOP implementation aspect. Of course, a custom annotation is required. It’s used as a pointcut. We can define annotations with necessary attributes, such as the description of the operation and the type of the operation. The types of operations need to be mentioned, we can divide into new, modify, delete, query. So only when we modify and delete, we need to query the data before modification. The other two are not needed, and this can also be used as a judgment.

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface OperateLog {

      String operation() default "";

      String operateType() default "";

}
Copy the code

Defines annotations for finding tables and table primary keys

Table and table primary keys are annotated on entities with two internal properties, tableName and idName. After the values of these two attributes are obtained, we can concatenate the select * from table name where primary key field.

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SelectTable {

	String tableName() default "";

	String idName() default  "";
}
Copy the code

Defines an annotation to get a primary key value

With the above three elements, we are missing the primary key fetch of the last element, which tells us that we should get the value from the field of the submitted request.

@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SelectPrimaryKey {

}
Copy the code

Summary of notes

With the above three annotations, the preparation of annotations is complete. We get the data through reflection, we get everything. Next we implement the facets, concatenate the values of the annotations, and finally store them in our database operation log table.

Implementation of section

For the aspect, we need to implement the pointcut, database insertion, reflected data acquisition. We will explain it separately and finally give the comprehensive implementation code. Easy for everyone to understand and learn.

Definition of section

This is a facet declared based on spring’s aspect.

@Aspect
@Component
public class OperateLogAspect {
}
Copy the code

Definition of tangent point

The cutting point is to intercept and enforce all OperateLog annotated requests. We intercept using annotations.

	@Pointcut("@annotation(com.jichi.aop.operateLog.OperateLog)")
	private void operateLogPointCut(){
	}
Copy the code

A common method for getting the requested IP

	private String getIp(HttpServletRequest request){
		String ip = request.getHeader("X-forwarded-for");
		if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
			ip = request.getHeader("Proxy-Client-IP");
		}
		if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
			ip = request.getHeader("WL-Proxy-Client-IP");
		}
		if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
			ip = request.getHeader("HTTP_CLIENT_IP");
		}
		if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
			ip = request.getHeader("HTTP_X_FORWARDED_FOR");
		}
		if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
			ip = request.getRemoteAddr();
		}
		return ip;
	}
Copy the code

Log insertion operations for the database

We will do a separate extraction of the log operations that are inserted into the database.

private void insertIntoLogTable(OperateLogInfo operateLogInfo){ operateLogInfo.setId(UUID.randomUUID().toString().replace("-","")); String sql="insert into log values(? ,? ,? ,? ,? ,? ,? ,? ,? ,? ,? ,? ,? ,? ,?) "; jdbcTemplate.update(sql,operateLogInfo.getId(),operateLogInfo.getUserId(), operateLogInfo.getUserName(),operateLogInfo.getOperation(),operateLogInfo.getMethod(), operateLogInfo.getModifiedData(),operateLogInfo.getPreModifiedData(), operateLogInfo.getResult(),operateLogInfo.getErrorMessage(),operateLogInfo.getErrorStackTrace(), operateLogInfo.getExecuteTime(),operateLogInfo.getDuration(),operateLogInfo.getIp(), operateLogInfo.getModule(),operateLogInfo.getOperateType()); }Copy the code

Implementation of surround notification

Entity class implementation of logging

@tablename ("operate_log") @data public class OperateLogInfo {// private String id @tableId; // Operator ID private String userId; Private String userName; // Private String operation; Private String method; // Data after operation private String modifiedData; // Data before operation private String preModifiedData; Private String result; // errorMessage private String errorMessage; Private String errorStackTrace; // Start time private Date executeTime; Private Long duration; //ip private String ip; // Operation type private String operateType; }Copy the code

All the preparations have been completed. The next focus is on the implementation of surround notification. The idea is divided into data processing, exception capture, finally database insertion operation. The focus class surrounding the notification is ProceedingJoinPoint, whose getSignature method allows us to get the value of the annotation typed on the method. For example, below.

MethodSignature signature = (MethodSignature) pjp.getSignature(); OperateLog declaredAnnotation = signature.getMethod().getDeclaredAnnotation(OperateLog.class); operateLogInfo.setOperation(declaredAnnotation.operation()); operateLogInfo.setModule(declaredAnnotation.module()); operateLogInfo.setOperateType(declaredAnnotation.operateType()); / / obtain the execution method String method = signature. GetDeclaringType (). The getName () + "" + signature. The getName (); operateLogInfo.setMethod(method); String operateType = declaredAnnotation.operateType();Copy the code

This class is also used to retrieve the requested data, but it is important to note that we have a convention that the parameter must be passed as the first parameter. This ensures that the data we fetch is the submitted data.

if(pjp.getArgs().length>0){
	Object args = pjp.getArgs()[0];
	operateLogInfo.setModifiedData(new Gson().toJson(args));
}
Copy the code

The next step is to concatenate the data before modification. As mentioned earlier, we only do concatenation of data for modification and deletion, mainly to determine whether the book has annotations by class. If there are annotations, then we need to determine whether the value on the annotation is control or not empty to correctly concatenate the data. When fetching the value of field, note that private variables need to be accessed by setAccessible(true).

if(GlobalStaticParas.OPERATE_MOD.equals(operateType) || GlobalStaticParas.OPERATE_DELETE.equals(operateType)){ String tableName = ""; String idName = ""; String selectPrimaryKey = ""; if(pjp.getArgs().length>0){ Object args = pjp.getArgs()[0]; Boolean selectTableFlag = args.getClass().isAnnotationPresent(SelectTable); if(selectTableFlag){ tableName = args.getClass().getAnnotation(SelectTable.class).tableName(); idName = args.getClass().getAnnotation(SelectTable.class).idName(); }else {throw new RuntimeException(" Operation log type is modify or delete, entity class must specify surface and primary key annotation!" ); } Field[] fields = args.getClass().getDeclaredFields(); Field[] fieldsCopy = fields; boolean isFindField = false; int fieldLength = fields.length; for(int i = 0; i < fieldLength; ++i) { Field field = fieldsCopy[i]; boolean hasPrimaryField = field.isAnnotationPresent(SelectPrimaryKey.class); if (hasPrimaryField) { isFindField = true; field.setAccessible(true); selectPrimaryKey = (String)field.get(args); } } if(! IsFindField){throw new RuntimeException(" Entity class must specify primary key attributes!" ); } } if(StringUtils.isNotEmpty(tableName) && StringUtils.isNotEmpty(idName)&& StringUtils.isNotEmpty(selectPrimaryKey)){ StringBuffer sb = new StringBuffer(); sb.append(" select * from "); sb.append(tableName); sb.append(" where "); sb.append(idName); sb.append(" = ? "); String sql = sb.toString(); try{ List<Map<String, Object>> maps = jdbcTemplate.queryForList(sql, selectPrimaryKey); if(maps! =null){ operateLogInfo.setPreModifiedData(new Gson().toJson(maps)); } }catch (Exception e){ e.printStackTrace(); Throw new RuntimeException(" Data error before query operation!" ); }}else {throw new RuntimeException(" null for table name, primary key or primary key value, please verify! ); } }else{ operateLogInfo.setPreModifiedData(""); }Copy the code

Section complete implementation code

@Aspect
@Component
public class OperateLogAspect {

	@Autowired
	private JdbcTemplate jdbcTemplate;

	@Pointcut("@annotation(com.jichi.aop.operateLog.OperateLog)")
	private void operateLogPointCut(){
	}

	@Around("operateLogPointCut()")
	public Object around(ProceedingJoinPoint pjp) throws Throwable {
		Object responseObj = null;
		OperateLogInfo operateLogInfo = new OperateLogInfo();
		String flag = "success";
		try{
			HttpServletRequest request = SpringContextUtil.getHttpServletRequest();
			DomainUserDetails currentUser = SecurityUtils.getCurrentUser();
			if(currentUser!=null){
				operateLogInfo.setUserId(currentUser.getId());
				operateLogInfo.setUserName(currentUser.getUsername());
			}
			MethodSignature signature = (MethodSignature) pjp.getSignature();
			OperateLog declaredAnnotation = signature.getMethod().getDeclaredAnnotation(OperateLog.class);
			operateLogInfo.setOperation(declaredAnnotation.operation());
			operateLogInfo.setModule(declaredAnnotation.module());
			operateLogInfo.setOperateType(declaredAnnotation.operateType());
			//获取执行的方法
			String method = signature.getDeclaringType().getName() + "."  + signature.getName();
			operateLogInfo.setMethod(method);
			String operateType = declaredAnnotation.operateType();
			if(pjp.getArgs().length>0){
				Object args = pjp.getArgs()[0];
				operateLogInfo.setModifiedData(new Gson().toJson(args));
			}
			if(GlobalStaticParas.OPERATE_MOD.equals(operateType) ||
				GlobalStaticParas.OPERATE_DELETE.equals(operateType)){
				String tableName = "";
				String idName = "";
				String selectPrimaryKey = "";
				if(pjp.getArgs().length>0){
					Object args = pjp.getArgs()[0];
					//获取操作前的数据
					boolean selectTableFlag = args.getClass().isAnnotationPresent(SelectTable.class);
					if(selectTableFlag){
						tableName = args.getClass().getAnnotation(SelectTable.class).tableName();
						idName = args.getClass().getAnnotation(SelectTable.class).idName();
					}else {
						throw new RuntimeException("操作日志类型为修改或删除,实体类必须指定表面和主键注解!");
					}
					Field[] fields = args.getClass().getDeclaredFields();
					Field[] fieldsCopy = fields;
					boolean isFindField = false;
					int fieldLength = fields.length;
					for(int i = 0; i < fieldLength; ++i) {
						Field field = fieldsCopy[i];
						boolean hasPrimaryField = field.isAnnotationPresent(SelectPrimaryKey.class);
						if (hasPrimaryField) {
							isFindField = true;
							field.setAccessible(true);
							selectPrimaryKey = (String)field.get(args);
						}
					}
					if(!isFindField){
						throw new RuntimeException("实体类必须指定主键属性!");
					}
				}
				if(StringUtils.isNotEmpty(tableName) &&
					StringUtils.isNotEmpty(idName)&&
					StringUtils.isNotEmpty(selectPrimaryKey)){
					StringBuffer sb = new StringBuffer();
					sb.append(" select * from  ");
					sb.append(tableName);
					sb.append(" where ");
					sb.append(idName);
					sb.append(" = ? ");
					String sql = sb.toString();
					try{
						List<Map<String, Object>> maps = jdbcTemplate.queryForList(sql, selectPrimaryKey);
						if(maps!=null){
							operateLogInfo.setPreModifiedData(new Gson().toJson(maps));
						}
					}catch (Exception e){
						e.printStackTrace();
						throw new RuntimeException("查询操作前数据出错!");
					}
				}else {
					throw new RuntimeException("表名、主键名或主键值 存在空值情况,请核实!");
				}
			}else{
				operateLogInfo.setPreModifiedData("");
			}
			//操作时间
			Date beforeDate = new Date();
			Long startTime = beforeDate.getTime();
			operateLogInfo.setExecuteTime(beforeDate);
			responseObj = pjp.proceed();
			Date afterDate = new Date();
			Long endTime = afterDate.getTime();
			Long duration = endTime - startTime;
			operateLogInfo.setDuration(duration);
			operateLogInfo.setIp(getIp(request));
			operateLogInfo.setResult(flag);
		}catch (RuntimeException e){
			throw new RuntimeException(e);
		}catch (Exception e){
			flag = "fail";
			operateLogInfo.setResult(flag);
			operateLogInfo.setErrorMessage(e.getMessage());
			operateLogInfo.setErrorStackTrace(e.getStackTrace().toString());
			e.printStackTrace();
		}finally {
			insertIntoLogTable(operateLogInfo);
		}
		return responseObj;
	}

	private void insertIntoLogTable(OperateLogInfo operateLogInfo){
		operateLogInfo.setId(UUID.randomUUID().toString().replace("-",""));
		String sql="insert into energy_log values(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)";
		jdbcTemplate.update(sql,operateLogInfo.getId(),operateLogInfo.getUserId(),
			operateLogInfo.getUserName(),operateLogInfo.getOperation(),operateLogInfo.getMethod(),
			operateLogInfo.getModifiedData(),operateLogInfo.getPreModifiedData(),
			operateLogInfo.getResult(),operateLogInfo.getErrorMessage(),operateLogInfo.getErrorStackTrace(),
			operateLogInfo.getExecuteTime(),operateLogInfo.getDuration(),operateLogInfo.getIp(),
			operateLogInfo.getModule(),operateLogInfo.getOperateType());
	}

	private String getIp(HttpServletRequest request){
		String ip = request.getHeader("X-forwarded-for");
		if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
			ip = request.getHeader("Proxy-Client-IP");
		}
		if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
			ip = request.getHeader("WL-Proxy-Client-IP");
		}
		if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
			ip = request.getHeader("HTTP_CLIENT_IP");
		}
		if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
			ip = request.getHeader("HTTP_X_FORWARDED_FOR");
		}
		if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
			ip = request.getRemoteAddr();
		}
		return ip;
	}
}
Copy the code

How to use the example

For the example we will annotate the operation log on the controller.

@ PostMapping ("/updateInfo ") @ OperateLog (operation = "modify information," operateType = GlobalStaticParas. OPERATE_MOD) public void updateInfo(@RequestBody Info info) { service.updateInfo(info); }Copy the code

For the entity class of Info, we identify the fields and table names.

@Data
@SelectTable(tableName = "info",idName = "id")
public class Info  {

    @SelectPrimaryKey
    private String id;
    
    private String name;

}
Copy the code