大家好,今天给大家带来一篇基于AOP实现的操作日志记录的解决的方案。大家可能会说,切,操作日志记录这么简单的东西,老生常谈了。不!
网上的操作日志一般就是记录操作人,操作的描述,ip等。好一点的增加了修改的数据和执行时间。那么!我这篇有什么不同呢!今天这种不仅可以记录上方所说的一切,还增加多表记录了操作前的数据
思路介绍:
由于操作日志用注解方式的AOP记录操作日志比较便捷,所以想到了在注解中定义操作前查询数据
详情的bean,查询方法及参数,参数类型,在aop进行方法执行前,对指定的bean,方法,参数进行调用,获得修改前的参数,并进行保存.
此处需要注意:
1.在前面中调用指定bean的方法时,不可用反射进行调用,反射不能加载spring容器,无法获取指定的spring bean,下面方法中封装的获取spring bean的
工具类也需要配置为bean,而且被spring加载,才可以;
2.@Aspect注解的类一定要配置成bean,而且被spring加载,才可以,即同时配置@Component和@Aspect,或在spring的配置文件进行bean的配置
3.如果配置了bean,要检索component-scan扫描范围是否包括Aspect类;
一.pom文件
本次是基于idea创建spring boot 项目,不会的同学请自行百度,本次就不在这里赘述。
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-core</artifactId>
<version>1.3.6</version>
</dependency>
<!--spring依赖-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.46</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.9.2</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.2</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.69</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.4</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.2</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.1</version>
</dependency>
</dependencies>
二.配置文件application.properties
spring.datasource.url=jdbc:mysql://localhost:3306/test?generateSimpleParameterMetadata=true
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
mybatis.type-aliases-package=com.example.recordlog.bean
#mybatis.mapper-locations=classpath:mybatis/mapper/*.xml
#引用mybatis-plus-boot-starter写法如下
mybatis-plus.mapper-locations=classpath:mybatis/mapper/*.xml
server.port=8080
#server.servlet.context-path=/recordLog
三.定义切面执行的注解
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import java.lang.annotation.ElementType;
import java.lang.annotation.RetentionPolicy;
/**
* @author liuminglin
*/
@Target({ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface OperateLog {
/**
* 查询的bean名称
*/
String serviceClass() default "";
// Class serviceClass();
/**
* 查询单个详情的bean的方法
*/
String queryMethod() default "";
/**
* 查询详情的参数类型
*/
String parameterType() default "";
/**
* 从页面参数中解析出要查询的id,
* 如域名修改中要从参数中获取Id的值进行查询
*/
String parameterKey() default "";
/**
* 是否为批量类型操作
*/
boolean paramIsArray() default false;
}
四.切面执行的方法
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.example.recordlog.tools.SpringContextUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import org.springframework.util.ReflectionUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 操作日志切面记录操作
*/
@Component
@Aspect
@Slf4j
public class OperateLogAspect {
/**
* 切入点
*/
@Pointcut("@annotation(com.example.recordlog.anotation.OperateLog)")
private void pointcut() {
}
@Around("RecordLogAspect.pointcut()")
public Object around(ProceedingJoinPoint pjp) {
//获取request对象
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
// 拦截的实体类,
Object target = pjp.getTarget();
// 获取签名
MethodSignature signature = (MethodSignature) pjp.getSignature();
Class[] parameterTypes = signature.getMethod().getParameterTypes();
// 拦截的方法名称
String methodName = pjp.getSignature().getName();
Method method = null;
// 拦截参数
Object[] args = pjp.getArgs();
List BeforeList = null;
Object objectResult = null;
try {
// 获得被拦截的方法
method = target.getClass().getMethod(methodName, parameterTypes);
JSONArray operateParamArray = new JSONArray();
for (int i = 0; i < args.length; i++) {
Object paramsObj = args[i];
//通过该方法可查询对应的object属于什么类型:
String type = paramsObj.getClass().getName();
operateParamArray.add(paramsObj);
}
if (null != method) {
// 判断是否包含自定义的注解,说明一下这里的SystemLog就是我自己自定义的注解
if (method.isAnnotationPresent(OperateLog.class)) {
OperateLog systemlog = method.getAnnotation(OperateLog.class);
BeforeList = QueryTools.getBeforeOperateData(operateParamArray, systemlog);
objectResult = pjp.proceed();
}
} else {
//不需要拦截直接执行
objectResult = pjp.proceed();
}
} catch (Throwable throwable) {
throwable.printStackTrace();
} finally {
Map logMap = new HashMap();
logMap.put("url", request.getRequestURL().toString());
logMap.put("RequestMethod", request.getMethod());
logMap.put("preModifiedData", BeforeList);
logMap.put("modifiedData", JSON.toJSONString(args));
//保存进数据库logservice.saveLog(log);
log.info("logMap :{}", JSON.toJSONString(logMap));
}
return objectResult;
}
}
五.获取spring bean的工具类
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.util.Objects;
/**
* @author liuminglin
* @date 2021/6/22 10:48
* @description: TODO
*/
@Component
public class SpringContextUtil implements ApplicationContextAware {
/**
* 上下文对象实例
*/
private static ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) {
SpringContextUtil.applicationContext = applicationContext;
}
/**
* 获取applicationContext
*
* @return
*/
public static ApplicationContext getApplicationContext() {
return applicationContext;
}
/**
* 通过name获取 Bean.
*/
public static Object getBean(String name) {
return applicationContext.getBean(name);
}
/**
* 通过class获取Bean.
*/
public static <T> Object getBean(Class<T> cls) {
return applicationContext.getBean(cls);
}
/**
* 通过name,以及Clazz返回指定的Bean
*
* @param name
* @param clazz
* @param <T>
* @return
*/
public static <T> T getBean(String name, Class<T> clazz) {
return getApplicationContext().getBean(name, clazz);
}
/**
* 获取HttpServletRequest
*/
public static HttpServletRequest getHttpServletRequest() {
return ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
}
public static String getDomain() {
HttpServletRequest request = getHttpServletRequest();
StringBuffer url = request.getRequestURL();
return url.delete(url.length() - request.getRequestURI().length(), url.length()).toString();
}
}
六.切面工具类
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.ArrayUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
@Slf4j
public class AspectUtil {
private AspectUtil() {
}
/**
* @Desc 获取HttpServlet 请求
*/
protected static HttpServletRequest getHttpServletRequest() {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
return attributes.getRequest();
}
/**
* @Desc 获取HttpServlet 响应
*/
protected HttpServletResponse getHttpServletResponse() {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
return attributes.getResponse();
}
/**
* @Desc 获取目标类的所有参数
* @Param proceedingJoinPoint
* @Param paramClass
* @Return List<T>
*/
protected static <T> List<T> paramsOfTargetClass(ProceedingJoinPoint proceedingJoinPoint, Class<T> paramClass) {
List<T> result = new ArrayList<>();
Object[] args = proceedingJoinPoint.getArgs();
if (ArrayUtils.isNotEmpty(args)) {
for (Object object : args) {
//paramClass.isAssignableFrom 此Class对象所表示的类或接口与指定的Class 参数所表示的类或接口是否相同,或是否是其超类或超接口。如果是则返回 true;否则返回 false
if (paramClass.isAssignableFrom(object.getClass())) {
//result.add((T) object);
}
result.add((T) object);
}
}
return result;
}
/**
* @Desc 获取目标类的第一个参数
* @Param proceedingJoinPoint
* @Param paramClass 没有目标类可以为null
* @Return List<T>
*/
public static <T> T getFirstParamsOfTargetClass(ProceedingJoinPoint proceedingJoinPoint, Class<T> paramClass) {
List<T> targets = paramsOfTargetClass(proceedingJoinPoint, paramClass);
return CollectionUtils.isEmpty(targets) ? null : targets.iterator().next();
}
/**
* @Desc 获取方法名称
*/
protected static String getMethodName(ProceedingJoinPoint proceedingJoinPoint) {
return proceedingJoinPoint.getSignature().getDeclaringTypeName() + "." + proceedingJoinPoint.getSignature().getName();
}
/**
* @Desc 获取请求信息
*/
public static HttpServletRequest getRequestInfo() {
HttpServletRequest request = getHttpServletRequest();
log.info("URL : {}", request.getRequestURL().toString());
log.info("HTTP_METHOD : {}", request.getMethod());
log.info("IP : {}", request.getRemoteAddr());
return request;
}
/**
* @Desc 记录请求信息
* @Param proceedingJoinPoint
*/
public static void logRequestInfo(ProceedingJoinPoint proceedingJoinPoint) {
getRequestInfo();
getClassMethodName(proceedingJoinPoint);
}
/**
* @Desc 获取类方法名
*/
protected static void getClassMethodName(ProceedingJoinPoint proceedingJoinPoint) {
String classMethod = getMethodName(proceedingJoinPoint);
log.info("methodName : {}", classMethod);
log.info("ARGS : {}", proceedingJoinPoint.getArgs());
}
/**
* @Desc 获取注解
* @Param proceedingJoinPoint
* @Param annotationClass
* @Return T
*/
public static <T extends Annotation> T getAnnotation(ProceedingJoinPoint proceedingJoinPoint, Class<T> annotationClass) {
Signature signature = proceedingJoinPoint.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
// Class[] parameterTypes = signature.getMethod().getParameterTypes();
//method = pjp.getTarget().getClass().getMethod(methodName, parameterTypes);
Method method = methodSignature.getMethod();
// 判断是否包含自定义的注解
if (null != method && method.isAnnotationPresent(annotationClass)) {
return method.getAnnotation(annotationClass);
}
return null;
}
/**
* @Desc 获取注解
* @Param joinPoint
* @Param annotationClass
* @Return T
*/
public <T extends Annotation> T getAnnotation(JoinPoint joinPoint, Class<T> annotationClass) {
Signature signature = joinPoint.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
Method targetMethod = methodSignature.getMethod();
return targetMethod.getAnnotation(annotationClass);
}
//是源等于还是目标的子类
protected boolean isSourceEqualOrSubclassOfTarget(Class<?> srcClazz, Class<?> targetClazz) {
return targetClazz.isAssignableFrom(srcClazz);
}
protected <T> boolean isBaseResponse(Class<T> clazz) {
return isSourceEqualOrSubclassOfTarget(clazz, RestResponse.class);
}
/**
* @Desc 获取方法返回类
*/
protected Class getMethodReturnClass(ProceedingJoinPoint proceedingJoinPoint) {
return ((MethodSignature) proceedingJoinPoint.getSignature()).getReturnType();
}
/**
* @Desc 钩子方法
*/
protected RestResponse doCheck(ProceedingJoinPoint proceedingJoinPoint) {
return new RestResponse();
}
}
七.查询工具类
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.example.recordlog.anotation.OperateLog;
import org.apache.commons.lang3.StringUtils;
import org.springframework.util.ReflectionUtils;
import java.lang.reflect.Method;
import java.util.List;
public class QueryTools {
private QueryTools() {
}
/**
* @author
* @Date 2021/9/2 15:26
* @Desc Class<T> annotationClass 注解类
* @Param operateParamArray 请求参数
* @Return
*/
public static List getBeforeOperateData(JSONArray operateParamArray, OperateLog annotationClass) {
//请求查询操作前数据的spring bean
// Class serviceClass = systemlog.serviceClass();
boolean isArrayResult = annotationClass.paramIsArray();
String serviceClass = annotationClass.serviceClass();
//请求查询数据的方法
String queryMethod = annotationClass.queryMethod();
String parameterType = annotationClass.parameterType();
String parameterKey = annotationClass.parameterKey();
List beforeList = new JSONArray();
//JSONObject beforeParam =new JSONObject();
if (StringUtils.isNotBlank(serviceClass)
&& StringUtils.isNotBlank(queryMethod)
&& StringUtils.isNotBlank(parameterType)
&& StringUtils.isNotBlank(parameterKey)) {
//批量操作
if (isArrayResult) {
//从请求的参数中解析出查询key对应的value值
String value = "";
for (int i = 0; i < operateParamArray.size(); i++) {
JSONObject params = operateParamArray.getJSONObject(i);
JSONArray paramArray = (JSONArray) params.get(parameterKey);
if (paramArray != null) {
for (int j = 0; j < paramArray.size(); j++) {
String paramId = paramArray.getString(j);
//在此处判断spring bean查询的方法参数类型
Object data = getOperateBeforeData(parameterType, serviceClass, queryMethod, paramId);
//beforeParam = (JSONObject) JSON.toJSON(data);
beforeList.add(data);
}
}
}
//单量操作
} else {
//从请求的参数中解析出查询key对应的value值
String value = "";
for (int i = 0; i < operateParamArray.size(); i++) {
JSONObject params = operateParamArray.getJSONObject(i);
value = params.getString(parameterKey);
if (StringUtils.isNotBlank(value)) {
break;
}
}
//在此处获取操作前的spring bean的查询方法
Object data = getOperateBeforeData(parameterType, serviceClass, queryMethod, value);
beforeList.add(data);
}
}
return beforeList;
}
/**
* @param paramType:参数类型
* @param serviceClass:bean名称
* @param queryMethod:查询method
* @param value:查询id的value
*/
private static Object getOperateBeforeData(String paramType, String serviceClass, String queryMethod, String value) {
Object obj = new Object();
//在此处解析请求的参数类型,根据id查询数据,id类型有四种:int,Integer,long,Long
if (paramType.equals("int")) {
int id = Integer.parseInt(value);
Method mh = ReflectionUtils.findMethod(SpringContextUtil.getBean(serviceClass).getClass(), queryMethod, Long.class);
//用spring bean获取操作前的参数,此处需要注意:传入的id类型与bean里面的参数类型需要保持一致
obj = ReflectionUtils.invokeMethod(mh, SpringContextUtil.getBean(serviceClass), id);
} else if (paramType.equals("Integer")) {
Integer id = Integer.valueOf(value);
Method mh = ReflectionUtils.findMethod(SpringContextUtil.getBean(serviceClass).getClass(), queryMethod, Long.class);
//用spring bean获取操作前的参数,此处需要注意:传入的id类型与bean里面的参数类型需要保持一致
obj = ReflectionUtils.invokeMethod(mh, SpringContextUtil.getBean(serviceClass), id);
} else if (paramType.equals("long")) {
long id = Long.parseLong(value);
Method mh = ReflectionUtils.findMethod(SpringContextUtil.getBean(serviceClass).getClass(), queryMethod, Long.class);
//用spring bean获取操作前的参数,此处需要注意:传入的id类型与bean里面的参数类型需要保持一致
obj = ReflectionUtils.invokeMethod(mh, SpringContextUtil.getBean(serviceClass), id);
} else if (paramType.equals("Long")) {
Long id = Long.valueOf(value);
Method mh = ReflectionUtils.findMethod(SpringContextUtil.getBean(serviceClass).getClass(), queryMethod, Long.class);
//用spring bean获取操作前的参数,此处需要注意:传入的id类型与bean里面的参数类型需要保持一致
obj = ReflectionUtils.invokeMethod(mh, SpringContextUtil.getBean(serviceClass), id);
}
return obj;
}
}
八.调用Service
注意事项:一定要指定beanName,否则SpringContextUtil.getBean获取不到
//指定beanName
@Service(value = "userInfoService")
import com.example.recordlog.bean.UserInfo;
import com.example.recordlog.mapper.UserInfoMapper;
import com.example.recordlog.service.UserInfoService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service(value = "userInfoService")
public class UserInfoServiceImpl implements UserInfoService {
@Autowired
private UserInfoMapper userInfoMapper;
@Override
public UserInfo selectByPrimaryKey(Long id) {
return userInfoMapper.selectByPrimaryKey(id);
}
@Override
public int updateByPrimaryKeySelective(UserInfo record) {
return userInfoMapper.updateByPrimaryKeySelective(record);
}
九.调用controller
import com.example.recordlog.aspect.OperateLog;
import com.example.recordlog.bean.UserInfo;
import com.example.recordlog.mapper.GoodsMapper;
import com.example.recordlog.service.UserInfoService;
import com.example.recordlog.tools.RestResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@RequestMapping("/api")
public class OperateController {
@Autowired
private UserInfoService userInfoService;
@Autowired
private GoodsMapper goodsMapper;
@RequestMapping(value = "/updateInfo", produces = "application/json;charset=utf-8", method = {RequestMethod.POST})
@OperateLog(serviceClass = "userInfoService", queryMethod = "selectByPrimaryKey", parameterType = "Long", parameterKey = "id")
public RestResponse updateInfo(@RequestBody UserInfo info) {
int count = userInfoService.updateByPrimaryKeySelective(info);
RestResponse restResponse = RestResponse.success(count);
return restResponse;
}
}
十.数据如图
logMap :{"RequestMethod":"POST","preModifiedData":[{"address":"上海市","creatTime":1625811791000,"email":"1345555@163.com","hometown":"上海","id":"123221","modifyDate":1625811791000,"phone":"13611303411","userId":"zhangsn","userName":"杨过绝情谷"}],
"modifiedData":"[{\"id\":\"123221\",\"userName\":\"天下第一\"}]","url":"http://localhost:8080/api/updateInfo"}