aop实现日志功能

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


前言

提示:这里可以添加本文要记录的大概内容:

作为个人学习经过的记录——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,
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值