使用AOP实现自定义日志

发布时间:2024年01月04日

在实现自定义日志之前,我们需要了解AOP。

1.AOP

AOP(Aspect-Oriented Programming,面向切面编程)是一种编程范式,旨在通过将横切关注点(cross-cutting concerns)从主要业务逻辑中分离出来,使得代码更模块化、更易于维护。横切关注点指的是那些存在于应用程序中多个模块中、并且对整个应用程序产生影响的功能,比如日志记录、事务管理、安全性等。



AOP的核心概念包括:

切面(Aspect): 切面是一组横切关注点的模块化单元,它定义了在何处(切点)、何时(通知)以及如何执行横切关注点。

连接点(Join Point): 连接点是在应用程序执行过程中能够应用切面的点,比如方法调用、异常处理等。

切点(Pointcut): 切点是一组连接点的集合,用于定义在何处应用切面。通常,切点通过表达式或者正则表达式定义。

通知(Advice): 通知是切面在连接点上执行的动作,它定义了在连接点的何时执行什么操作,比如在方法执行前后、抛出异常时执行的操作。

引入(Introduction): 引入允许我们向现有的类添加新的方法或属性,而不需要修改它们的源代码。

目标对象(Target Object): 目标对象是一个应用程序类,它可能包含切点,也可能不包含。

织入(Weaving): 织入是将切面应用到目标对象并创建新的代理对象的过程。织入可以在编译时、类加载时、运行时进行。

AOP主要用于解决横切关注点的问题,这些关注点通常涉及多个模块并且难以在代码中统一管理。通过使用AOP,可以使得这些关注点更加集中和可维护,提高了代码的可读性和可维护性。常见的AOP框架包括Spring AOP、AspectJ等。

2.实现

**

第一步:需要在pom.xml文件中引入AOP 的依赖

**

		<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
第二步:自定义日志注解
@Target({ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Log {
    /**
     * 模块
     */
    public String title() default "";

    /**
     * 功能
     */
    public BusinessType businessType() default BusinessType.OTHER;

    /**
     * 操作人类别
     */
    public OperatorType operatorType() default OperatorType.MANAGE;

    /**
     * 是否保存请求的参数
     */
    public boolean isSaveRequestData() default true;

    /**
     * 是否保存响应的参数
     */
    public boolean isSaveResponseData() default true;
}

解释一下这里的注解的意思,没记错的话应该是java8新特性(也可能记错了)里的东西,想深入了解的可以去看看。

@Target 用于指定注解的作用范围,即注解可以被应用在哪些元素上。
在提供的例子中,@Target({ElementType.PARAMETER, ElementType.METHOD})
表示这个注解可以应用在方法的参数和方法上。

@Retention 用于指定注解的保留策略,即注解在何时生效。 RetentionPolicy.RUNTIME
表示注解会在运行时保留,因此可以通过反射机制来获取注解信息。

@Documented 用于指定注解是否应该被 javadoc 工具记录。 如果一个注解被 @Documented 标记,那么它会被
javadoc 工具提取并文档化,这使得该注解的信息可以包含在生成的文档中。

这三个元注解的使用通常是一起的,它们提供了对注解的定义、应用范围和保留策略的灵活控制。在提供的例子中,这个注解的含义是可以应用在方法参数和方法上,在运行时保留,并且会被 javadoc 工具文档化。

第三步,构建你的操作类型,或者操作人(使用枚举)
* 业务操作类型
 **/
public enum BusinessType {
    /**
     * 其它
     */
    OTHER,

    /**
     * 新增
     */
    INSERT,

    /**
     * 修改
     */
    UPDATE,

    /**
     * 删除
     */
    DELETE,

    /**
     * 授权
     */
    ASSGIN,

    /**
     * 导出
     */
    EXPORT,

    /**
     * 导入
     */
    IMPORT,

    /**
     * 强退
     */
    FORCE,

    /**
     * 更新状态
     */
    STATUS,

    /**
     * 清空数据
     */
    CLEAN,
    /**
     * 批量删除
     */
    BATCHDELETE,
}
* 操作人类别
 **/
public enum OperatorType {

    /**
     * 其它
     */
    OTHER,

    /**
     * 后台用户
     */
    MANAGE,

    /**
     * 手机端用户
     */
    MOBILE
}

第四步,

在这里我们要建立业务层,用来将生产日志保存在数据库中

public interface AsyncOperLogService {
    public void savaOperLog(BhOperLog bhOperLog);
}

通过实现类完成具体插入代码,这里使用mybatisplus,不了解的可以看我之前的笔记MyBatisPlus

@Override
    public void savaOperLog(BhOperLog bhOperLog) {
        bhOperLogMapper.insert(bhOperLog);
    }

下面是具体的实体类

@Data
@ApiModel(description = "系统操作日志")
@TableName("你的表名称")
public class OperLog extends BaseEntity {

    private static final long serialVersionUID = 1L;

    @ApiModelProperty(value = "id")
    private long id;

    @ApiModelProperty(value = "模块标题")
    @TableField("title")
    private String title;

    @ApiModelProperty(value = "业务类型(0其它 1新增 2修改 3删除)")
    @TableField("business_type")
    private String businessType;

    @ApiModelProperty(value = "方法名称")
    @TableField("method")
    private String method;

    @ApiModelProperty(value = "请求方式")
    @TableField("request_method")
    private String requestMethod;

    @ApiModelProperty(value = "操作类别(0其它 1后台用户 2手机端用户)")
    @TableField("operator_type")
    private String operatorType;

    @ApiModelProperty(value = "操作人员")
    @TableField("oper_name")
    private String operName;

    @ApiModelProperty(value = "部门名称")
    @TableField("dept_name")
    private String deptName;

    @ApiModelProperty(value = "请求URL")
    @TableField("oper_url")
    private String operUrl;

    @ApiModelProperty(value = "主机地址")
    @TableField("oper_ip")
    private String operIp;

    @ApiModelProperty(value = "请求参数")
    @TableField("oper_param")
    private String operParam;

    @ApiModelProperty(value = "返回参数")
    @TableField("json_result")
    private String jsonResult;

    @ApiModelProperty(value = "操作状态(0正常 1异常)")
    @TableField("status")
    private Integer status;

    @ApiModelProperty(value = "错误消息")
    @TableField("error_msg")
    private String errorMsg;

    @ApiModelProperty(value = "操作时间")
    @TableField("oper_time")
    private Date operTime;
}

第五步,完成具体的日志配置开发

下面是完整代码

* 操作日志记录处理
 **/
@Aspect
@Component
public class LogAspect {


    @Resource
    AsyncOperLogService asyncOperLogService;


    /**
     * 处理完请求后执行
     *
     * @param joinPoint 切点
     */
    @AfterReturning(pointcut = "@annotation(controllerLog)",returning = "jsonResult")
    public void doAfterReturnin(JoinPoint joinPoint, Log controllerLog, Object jsonResult){
        handleLog(joinPoint, controllerLog,null, jsonResult);
    }


    protected void handleLog(final JoinPoint joinPoint, Log controllerLog, final Exception e, Object jsonResult) {
        try {
            RequestAttributes ra = RequestContextHolder.getRequestAttributes();
            ServletRequestAttributes sra = (ServletRequestAttributes) ra;
            HttpServletRequest request = sra.getRequest();

            // *========数据库日志=========*//
            BhOperLog operLog = new BhOperLog();
            operLog.setStatus(1);
            // 请求的地址 IpUtil.getIpAddr(ServletUtils.getRequest());
            String ip = IpUtil.getIpAddress(request);
            operLog.setOperIp(ip);
            operLog.setOperUrl(request.getRequestURI());

            String token = request.getHeader("token");
            String userName = JwtUtils.getUsername(token);
            operLog.setOperName(userName);

            if (e != null) {
                operLog.setStatus(0);
                operLog.setErrorMsg(e.getMessage());
            }
            // 设置方法名称
            String className = joinPoint.getTarget().getClass().getName();
            String methodName = joinPoint.getSignature().getName();
            operLog.setMethod(className + "." + methodName + "()");
            // 设置请求方式
            operLog.setRequestMethod(request.getMethod());
            // 处理设置注解上的参数
            getControllerMethodDescription(joinPoint, controllerLog, operLog, jsonResult);
            // 保存数据库
            asyncOperLogService.savaOperLog(operLog);
        } catch (Exception exp) {
            // 记录本地异常日志
//            log.error("==前置通知异常==");
//            log.error("异常信息:{}", exp.getMessage());
            exp.printStackTrace();
        }

    }

    /**
     * 获取注解中对方法的描述信息 用于Controller层注解
     *
     * @param log     日志
     * @param operLog 操作日志
     * @throws Exception
     */
    public void getControllerMethodDescription(JoinPoint joinPoint, Log log, BhOperLog operLog, Object jsonResult) throws Exception {
        // 设置action动作
        operLog.setBusinessType(log.businessType().name());
        // 设置标题
        operLog.setTitle(log.title());
        // 设置操作人类别
        operLog.setOperatorType(log.operatorType().name());
        // 是否需要保存request,参数和值
        if (log.isSaveRequestData()) {
            // 获取参数的信息,传入到数据库中。
            setRequestValue(joinPoint, operLog);
        }
        // 是否需要保存response,参数和值
        if (log.isSaveResponseData() && !StringUtils.isEmpty(jsonResult)) {
            operLog.setJsonResult(JacksonUtils.obj2String(jsonResult));
        }
    }

    /**
     * 获取请求的参数,放到log中
     *
     * @param operLog 操作日志
     * @throws Exception 异常
     */
    private void setRequestValue(JoinPoint joinPoint, BhOperLog operLog) throws Exception {
        String requestMethod = operLog.getRequestMethod();
        if (HttpMethod.PUT.name().equals(requestMethod) || HttpMethod.POST.name().equals(requestMethod)) {
            String params = argsArrayToString(joinPoint.getArgs());
            operLog.setOperParam(params);
        }
    }


    /**
     * 参数拼装
     */
    private String argsArrayToString(Object[] paramsArray) {
        String params = "";
        if (paramsArray != null && paramsArray.length > 0) {
            for (Object o : paramsArray) {
                if (!StringUtils.isEmpty(o) && !isFilterObject(o)) {
                    try {
                        String jsonObj = JacksonUtils.obj2String(o);
                        params += jsonObj + " ";
                    } catch (Exception e) {
                    }
                }
            }
        }
        return params.trim();
    }

    /**
     * 判断是否需要过滤的对象。
     *
     * @param o 对象信息。
     * @return 如果是需要过滤的对象,则返回true;否则返回false。
     */
    @SuppressWarnings("rawtypes")
    public boolean isFilterObject(final Object o) {
        Class<?> clazz = o.getClass();
        if (clazz.isArray()) {
            return clazz.getComponentType().isAssignableFrom(MultipartFile.class);
        } else if (Collection.class.isAssignableFrom(clazz)) {
            Collection collection = (Collection) o;
            for (Object value : collection) {
                return value instanceof MultipartFile;
            }
        } else if (Map.class.isAssignableFrom(clazz)) {
            Map map = (Map) o;
            for (Object value : map.entrySet()) {
                Map.Entry entry = (Map.Entry) value;
                return entry.getValue() instanceof MultipartFile;
            }
        }
        return o instanceof MultipartFile || o instanceof HttpServletRequest || o instanceof HttpServletResponse
                || o instanceof BindingResult;
    }


}

那么如何使用呢?

只需要在你需要的Controller中加入
这个注解就可以了
@Log(title = "菜单管理", businessType = BusinessType.STATUS) 其中title根据具体模块书写,businessType就是你自定义的操作类型

这样就完成了自定义日志的开发,也可以将这些代码通过自定义start进行打包,加入Maven中进行使用。

文章来源:https://blog.csdn.net/qq_51365907/article/details/135389811
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。