提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
前言
提示:这里可以添加本文要记录的大概内容:
作为个人学习经过的记录——aop实现日志使用记录
提示:以下是本篇文章正文内容,下面案例可供参考
一、导入依赖
导入aop的依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
二、自定义注解,作为切点
自定义注解
@Target({ElementType.METHOD,ElementType.PARAMETER})该注解表示该切点的应用范围
TYPE: 类、接口(包括注解类型自身)、枚举。
METHOD: 方法(包括实例方法、静态方法、抽象方法)。
FIELD: 字段(包括实例变量、静态变量)。
PARAMETER: 方法的参数。
CONSTRUCTOR: 构造函数。
LOCAL_VARIABLE: 局部变量(例如方法内部或代码块内的变量)。
PACKAGE: 包声明。
TYPE_PARAMETER: 类型参数(泛型参数)。
TYPE_USE: 类型使用(如泛型实例化时的类型)。
@Retention(RetentionPolicy.RUNTIME)该注解表示该切点的生命周期
SOURCE:源码级别保留。这种策略下,注解只存在于源代码中,编译器在编译期间会忽略这些注解,不会将它们写入到生成的字节码文件中。这意味着这些注解在编译后的 class 文件中不存在,也无法在运行时通过反射或其他方式获取到。
CLASS:编译期级别保留。这是默认的保留策略,如果自定义注解没有明确指定 @Retention,则采用此策略。在这种情况下,注解信息会被编译器记录在生成的 class 文件中,但不会被虚拟机加载到运行时数据区,即无法在运行时通过反射访问到。这类注解常用于编译时的处理工具,如代码生成、编译检查等。
RUNTIME:运行时级别保留。当指定为 RUNTIME 时,注解不仅会被编译器记录在 class 文件中,还会被虚拟机加载到运行时数据区。这样,在程序运行时就可以通过反射 API 等机制动态地获取到注解的信息,从而实现基于注解的高级特性,如依赖注入、切面编程、动态代理、日志记录、性能监控等。
当一个注解类型被 @Documented 标记时,任何使用了该注解的元素(如类、方法、字段等)在生成文档时,该注解及其相关属性值也将一并出现在文档输出中。
作用: @Documented 主要影响的是开发人员查阅 API 文档的体验。它指示编译器或相关的文档生成工具(如 Javadoc 工具),在生成 HTML 或其他格式的 API 文档时,将带有该注解的元素及其注解信息纳入文档内容。这样,阅读文档的人就能了解到某个类、方法、字段等是否使用了特定的注解,以及注解的参数值是什么,这对理解代码的行为、约定、约束等非常有帮助。
import java.lang.annotation.*;
@Target({ElementType.METHOD,ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Log {
/**
* 日志标题
*/
public String title() default "";
/**
* 操作类型
*/
public BusinessTypeEnum businessType() default BusinessTypeEnum.OTHER;
}
三、实现切面类
该部分表示,切面需要执行的逻辑.
日志实现的具体逻辑。
package cn.yun.bicarbon.log.aop;
import cn.yun.bicarbon.log.aop.service.SysLogService;
import com.alibaba.fastjson2.JSON;
import cn.yun.bicarbon.log.aop.entity.SysLog;
import lombok.Data;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.HandlerMapping;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.time.LocalDateTime;
import java.util.Collection;
import java.util.Map;
/**
*
* Log 注解切面配置
* @Author xiarg
* @CreateTime 2023/02/16 16:46
*/
@Data
@Aspect
@Component
public class LogConfig {
private static final Logger log = LoggerFactory.getLogger(LogConfig.class);
/**
* 引入日志Service,用于存储数据进数据库
*/
private final SysLogService sysLogService;
/**
* 配置切入点-xxx代表自定义注解的存放位置,如:com.xiarg.genius.annotation.annotation.Log
*/
@Pointcut("@annotation(cn.yun.bicarbon.log.aop.Log)")
public void logPointCut() {}
/**
* 处理完请求后执行此处代码
*
* @param joinPoint 切点
*/
@AfterReturning(pointcut = "@annotation(controllerLog)", returning = "jsonResult")
public void doAfterReturning(JoinPoint joinPoint, Log controllerLog, Object jsonResult){
handleLog(joinPoint, controllerLog, null, jsonResult);
}
/**
* 如果处理请求时出现异常,在抛出异常后执行此处代码
*
* @param joinPoint 切点
* @param e 异常
*/
@AfterThrowing(value = "@annotation(controllerLog)", throwing = "e")
public void doAfterThrowing(JoinPoint joinPoint, Log controllerLog, Exception e){
handleLog(joinPoint, controllerLog, e, null);
}
/**
* 处理日志的逻辑方法。
*
* @param joinPoint AOP切面中的连接点,表示当前被拦截的方法。
* @param controllerLog 控制器日志注解对象,用于获取控制器方法上的日志相关信息。
* @param e 异常对象,如果方法执行时发生异常,则该参数不为null。
* @param jsonResult 方法返回的JSON结果,用于记录操作结果。
*/
protected void handleLog(final JoinPoint joinPoint, Log controllerLog, final Exception e, Object jsonResult){
try {
// 获取方法签名
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
// 默认用户名
String userName = "genius";
// 构建系统日志对象
SysLog sysLog = new SysLog();
sysLog.setStatus(1); // 初始化状态为1,表示操作成功
// 获取请求相关信息
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
assert requestAttributes != null;
HttpServletRequest request = requestAttributes.getRequest();
Map<String, String[]> parameterMap = request.getParameterMap();
String ip = getIpAddr(request); // 获取IP地址
sysLog.setOperIp(ip);
sysLog.setOperLocation(request.getHeader("location")); // 获取操作地点
sysLog.setOperParam(JSON.toJSONString(parameterMap)); // 将请求参数转为JSON字符串
sysLog.setOperUrl(request.getRequestURI()); // 设置操作的URL
sysLog.setOperName(userName); // 设置操作人姓名
// 如果有异常发生,记录异常信息,并设置操作状态为失败
if (e != null) {
sysLog.setStatus(0);
int length = e.getMessage().length();
sysLog.setErrorMsg(e.getMessage().substring(0,length>2000?2000:length)); // 截取异常信息,防止过长
}
// 设置方法名称和请求方式
String className = joinPoint.getTarget().getClass().getName(); // 获取目标类名称
String methodName = joinPoint.getSignature().getName(); // 获取方法名称
sysLog.setMethod(className + "." + methodName + "()");
sysLog.setRequestMethod(request.getMethod()); // 获取请求方式
// 处理注解上的参数,并设置到sysLog中
getControllerMethodDescription(joinPoint, controllerLog, sysLog, jsonResult, request);
// 设置操作时间,并将日志保存到数据库中
sysLog.setOperTime(LocalDateTime.now()); // 设置操作时间
sysLog.setJsonResult(jsonResult==null?"":jsonResult.toString()); // 设置返回的JSON结果
sysLogService.save(sysLog); // 保存日志到数据库
} catch (Exception exp) {
// 记录异常日志
log.error("==前置通知异常==");
log.error("异常信息:{}", exp.getMessage());
exp.printStackTrace();
}
}
/**
* 获取操作ip地址
* 该方法用于从HTTP请求中获取操作者的IP地址。首先尝试从请求头中的"X-Forwarded-For"字段获取IP地址,
* 若获取不到则尝试其他几个常见的HTTP代理头字段,最后如果都获取不到,则返回请求对象自身的远程地址。
* @param request HttpServletRequest对象,用于获取请求头信息。
* @return 操作者的IP地址字符串。如果无法获取到,则返回"unknown"。
*/
public static String getIpAddr(HttpServletRequest request) {
if (request == null) {
return "unknown"; // 如果请求对象为null,直接返回"unknown"
}
// 尝试从多个常见的HTTP头中获取IP地址,按照顺序尝试
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("X-Forwarded-For");
}
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("X-Real-IP");
}
// 如果以上所有方式都获取不到IP地址,则尝试直接从请求对象中获取远程地址
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
return ip; // 返回获取到的IP地址,如果都获取不到则返回"unknown"
}
/**
* 获取控制器方法的注解信息,并设置日志的相关属性。
* @param joinPoint 切面编程的连接点,用于获取目标方法的信息。
* @param log 日志注解实例,包含日志的详细配置如业务类型和标题。
* @param sysLog 系统日志实体,用于存储日志的具体信息。
* @param jsonResult 控制器方法的返回结果,一般为JSON格式的数据。
* @param request HTTP请求对象,用于获取请求的相关信息。
* @throws Exception 抛出异常,处理过程中的任何错误都可以抛出。
*/
public void getControllerMethodDescription(JoinPoint joinPoint, Log log, SysLog sysLog, Object jsonResult, HttpServletRequest request) throws Exception {
// 根据日志注解设置系统日志的业务类型和标题
sysLog.setBusinessType(log.businessType().ordinal());
sysLog.setTitle(log.title());
}
private void setRequestValue(JoinPoint joinPoint, SysLog sysLog, HttpServletRequest request) throws Exception {
String requestMethod = sysLog.getRequestMethod();
if (RequestMethod.PUT.name().equals(requestMethod) || RequestMethod.POST.name().equals(requestMethod)) {
String params = argsArrayToString(joinPoint.getArgs());
sysLog.setOperParam(params.substring(0,2000));
} else {
Map<?, ?> paramsMap = (Map<?, ?>) request.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);
sysLog.setOperParam(paramsMap.toString().substring(0,2000));
}
}
/**
* 解析方法参数信息,并将参数以字符串形式返回。
* 该方法会将参数数组中的每个非空且不被过滤的对象转换为JSON字符串并拼接起来。
* @param paramsArray 方法参数数组,可能包含各种类型的对象。
* @return 返回拼接后的参数字符串,每个参数之间以空格分隔。如果参数数组为空或所有参数都被过滤,则返回空字符串。
*/
private String argsArrayToString(Object[] paramsArray) {
StringBuilder params = new StringBuilder();
// 遍历参数数组,将非空且不被过滤的对象转换为JSON字符串并添加到params字符串构建器中
if (paramsArray != null && paramsArray.length > 0) {
for (Object o : paramsArray) {
// 对象非空且不属于被过滤的类型时,尝试将其转换为JSON字符串
if (o != null && !isFilterObject(o)) {
try {
Object jsonObj = JSON.toJSON(o); // 将对象转换为JSON
params.append(jsonObj.toString()).append(" "); // 拼接参数字符串
} catch (Exception e) {
log.error(e.getMessage()); // 转换失败时,记录错误日志
}
}
}
}
return params.toString().trim(); // 返回处理后的参数字符串,去除首尾空格
}
/**
* 判断传入的对象是否为过滤对象。
* 该方法支持判断数组、集合和Map类型是否包含MultipartFile类型,或者判断单一对象是否为MultipartFile、HttpServletRequest、HttpServletResponse或BindingResult类型之一。
*
* @param o 待判断的对象。
* @return 如果对象是需要过滤的类型,则返回true;否则返回false。
*/
public boolean isFilterObject(final Object o) {
Class<?> clazz = o.getClass();
// 判断对象是否为数组
if (clazz.isArray()) {
// 如果是数组,判断数组的元素类型是否为MultipartFile
return clazz.getComponentType().isAssignableFrom(MultipartFile.class);
// 判断对象是否为集合
} else if (Collection.class.isAssignableFrom(clazz)) {
Collection collection = (Collection) o;
// 遍历集合,只要发现任意元素是MultipartFile类型,即返回true
for (Object value : collection) {
return value instanceof MultipartFile;
}
// 判断对象是否为Map
} else if (Map.class.isAssignableFrom(clazz)) {
Map map = (Map) o;
// 遍历Map的值,只要发现任意值是MultipartFile类型,即返回true
for (Object value : map.entrySet()) {
Map.Entry entry = (Map.Entry) value;
return entry.getValue() instanceof MultipartFile;
}
}
// 如果对象不是数组、集合或Map,那么直接判断它是否为MultipartFile、HttpServletRequest、HttpServletResponse或BindingResult类型之一
return o instanceof MultipartFile || o instanceof HttpServletRequest || o instanceof HttpServletResponse
|| o instanceof BindingResult;
}
}
附加,需要一些枚举类和mybatisplus的日志添加到数据库的操作
package cn.yun.bicarbon.log.aop;
public enum BusinessTypeEnum {
/**
* 其它
*/
OTHER,
/**
* 新增
*/
INSERT,
/**
* 修改
*/
UPDATE,
/**
* 删除
*/
DELETE,
/**
* 授权
*/
GRANT,
}

7582

被折叠的 条评论
为什么被折叠?



