您的当前位置:首页正文

自定义注解和SpEL表达式实现功能强大的无侵入式的日志功能

2024-11-20 来源:个人技术集锦

需求:日志审计

  • 用户要求系统敏感操作添加日志审计功能,方便查看哪些用户做了敏感操作
  • 日志详情样例:用户[admin]新增角色id:[111]name:[testAddRole]结果:[成功]

实现原则

因为是后加的功能,所以原实现不能大面积修改;退一步讲,就算是新开发的项目,考虑添加日志审计功能时也应该尽可能的减少代码的耦合,减少代码侵入

使用的技术

  • aspect切面(本章是基于切面功能实现,所以并不讲解关于切面的内容)
  • 自定义注解
  • SpEl表达式

代码实现

自定义注解

package com.ultra.annotation;

import java.lang.annotation.*;

/**
 * 日志审计注解
 *
 * @author admin
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface LogAudit {

    /**
     * 账号
     */
    String account() default "";

    /**
     * 模块id对应模块名称(用户,角色,资源等)
            */
    String moduleId() default "";

    /**
     * 操作id对应操作名称(新增、更新、删除等)
     */
    String operateId() default "";

    /**
     * 对象id
     */
    String id() default "";

    /**
     * 对象名称
     */
    String name() default "";

}

业务对象

  • 日志详情对象
package com.ultra.bo;

import lombok.Getter;
import lombok.Setter;

/**
 * 日志详情
 *
 * @author fan
 */
@Setter
@Getter
public class LogDetails {
    /**
     * 账号
     */
    private String account;
    /**
     * 操作
     */
    private String operate;
    /**
     * 模块
     */
    private String module;
    /**
     * id
     */
    private String id;
    /**
     * 名称
     */
    private String name;
    /**
     * 结果
     */
    private String result;

    @Override
    public String toString() {
        return "用户[" + account + "]" + operate + module + "id:[" + id + "]" + "name:[" + name + "]" + "结果:[" + result + "]";
    }
}

  • 操作枚举类
package com.ultra.constant;

/**
 * 日志操作id与名称枚举关系
 *
 * @author fan
 */
public enum LogOperateEnum {
    /**
     * id与操作对应关系
     */
    ADD("01", "新增"),
    UPDATE("02", "更新"),
    DELETE("03", "删除");

    LogOperateEnum(String id, String name) {
        this.id = id;
        this.name = name;
    }

    private String id;
    private String name;

    public static String getValue(String id) {
        for (LogOperateEnum operateEnum : LogOperateEnum.values()) {
            if (operateEnum.id.equals(id)) {
                return operateEnum.name;
            }
        }
        return null;
    }
}

  • 模块枚举类
package com.ultra.constant;

/**
 * 日志模块id与名称枚举关系
 *
 * @author fan
 */
public enum LogModuleEnum {
    /**
     * id与操作对应关系
     */
    ADD("01", "用户"),
    UPDATE("02", "角色"),
    DELETE("03", "资源");

    LogModuleEnum(String id, String name) {
        this.id = id;
        this.name = name;
    }

    private String id;
    private String name;

    public static String getValue(String id) {
        for (LogModuleEnum moduleEnum : LogModuleEnum.values()) {
            if (moduleEnum.id.equals(id)) {
                return moduleEnum.name;
            }
        }
        return null;
    }
}

  • 业务对象
package com.ultra.dao.entity;

import java.io.Serializable;
import lombok.ToString;
import lombok.Getter;
import lombok.Setter;
/**
 * 角色
 *
 * @author ${author}
 * @since 2019-09-06
 */
@Setter
@Getter
@ToString
public class Role implements Serializable {

    private static final long serialVersionUID = 1L;

    private Long id;

    private String name;

}

注解实现

package com.ultra.aspect;

import com.ultra.annotation.LogAudit;
import com.ultra.bo.LogDetails;
import com.ultra.conditional.BeanRegisterConditional;
import com.ultra.constant.LogModuleEnum;
import com.ultra.constant.LogOperateEnum;
import com.ultra.util.ArrayUtil;
import com.ultra.util.StringUtil;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;

/**
 * 日志审计切面
 *
 * @author fan
 */
@Aspect
@Component
public class LogAuditAspect {
    private static final Logger logger = LoggerFactory.getLogger(LogAuditAspect.class);

    /**
     * 获取注解参数当做方法入参
     *
     * @param joinPoint 切点方法
     * @param logAudit  注解参数
     * @return 方法执行的返回值
     * @throws Throwable 方法执行可能抛的异常
     */
    @Around("@annotation(logAudit)")
    public Object doAround(ProceedingJoinPoint joinPoint, LogAudit logAudit) throws Throwable {
        Object proceed;
        LogDetails logDetails = new LogDetails();
        try {
            // 调度之类没有账号的可以手动指定account
            String account = logAudit.account();
            if (StringUtil.isBlank(account)) {
                // 伪代码实现获取当前账号
                account = "admin";
            }
            String operateId = logAudit.operateId();
            String moduleId = logAudit.moduleId();
            String id = getElValue(logAudit.id(), joinPoint);
            String name = getElValue(logAudit.name(), joinPoint);
            logDetails.setAccount(account);
            logDetails.setOperate(LogOperateEnum.getValue(operateId));
            logDetails.setModule(LogModuleEnum.getValue(moduleId));
            logDetails.setId(id);
            logDetails.setName(name);
            proceed = joinPoint.proceed();
            // 这里假定认为没有异常是成功,有异常是失败;根据实际业务判断
            logDetails.setResult("成功");
        } catch (Throwable throwable) {
            logDetails.setResult("失败");
            throw throwable;
        } finally {
            //入库
            logger.info("logDetails:{}", logDetails);
        }
        return proceed;
    }

    /**
     * 用于SpEL表达式解析.
     */
    private SpelExpressionParser parser = new SpelExpressionParser();
    /**
     * 用于获取方法参数定义名字.
     */
    private DefaultParameterNameDiscoverer nameDiscoverer = new DefaultParameterNameDiscoverer();

    private String getElValue(String elKey, ProceedingJoinPoint joinPoint) {
        // 通过joinPoint获取被注解方法
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Method method = methodSignature.getMethod();
        // 使用spring的DefaultParameterNameDiscoverer获取方法形参名数组
        String[] paramNames = nameDiscoverer.getParameterNames(method);
        if (paramNames != null && paramNames.length > 0) {
            // spring的表达式上下文对象
            EvaluationContext context = new StandardEvaluationContext();
            // 通过joinPoint获取被注解方法的形参
            Object[] args = joinPoint.getArgs();
            // 给上下文赋值
            for (int i = 0; i < args.length; i++) {
                context.setVariable(paramNames[i], args[i]);
            }
            // 解析过后的Spring表达式对象
            Expression expression = parser.parseExpression(elKey);
            Object expressionValue = expression.getValue(context);
            if (expressionValue == null) {
                return null;
            }
            return String.valueOf(expressionValue);
        }
        return null;
    }

}

使用注解

package com.ultra.web;

import com.ultra.annotation.LogAudit;
import com.ultra.dao.entity.Role;
import com.ultra.service.RoleService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.List;


/**
 * 角色
 *
 * @author fan
 * @since 2019-09-06
 */
@RestController
@RequestMapping("/role")
public class RoleController {
 
 	@Autowired
	private RoleService roleService; 
    @PostMapping
    @LogAudit(moduleId = "02", operateId = "01", id = "#entity.id", name = "#entity.name")
    public boolean save(@RequestBody Role entity) {
        return super.save(entity);
    }

}

关键点总结

  • 怎么获取注解参数:
@Around("@annotation(logAudit)")
 public Object doAround(ProceedingJoinPoint joinPoint, LogAudit logAudit) throws Throwable {}
  • 方法参数怎么转化为日志参数:SpEl表达式,灵感来自Spring Cache中@CacheEvict注解中的key
  • SpEl实现:使用方法的入参当做上下文,使用SpEl语法解析,所以对方法没有特殊要求,任何方法都可以,也可以获取到任意参数
显示全文