基于Spring Aop+SpEL优雅地记录操作日志

image.png

在我们工作的日常开发中,我们经常会遇到需要记录用户的操作日志(如上图所示)的需求,这个需求本身不是很难,但是如何让操作日志不和业务逻辑耦合?:❓ 如何优雅的记录操作日志?这就是本文分享的主要内容:优雅的记录操作日志。

与业务系统解耦,相信很多小伙伴已经想到了Spring AOP,没错,本文分享的方案就是使用 AOP 生成动态的操作日志。(ps:如果有小伙伴对Spring AOP有疑问,可以先去看看这篇文章 彻底搞懂Spring AOP

基于Spring AOP+SpEL 生成动态的操作日志

这里先给出本文实现的一个效果,然后再看是怎么实现这样功能的
image.png
从图上看,通过 SpEL(Spring Expression Language,Spring表达式语言)表达式实现了动态模板,SpEL表达式引用方法上的参数,可以让变量填充到模板中达到动态的操作日志文本内容。

这里说明一下,正常情况下像业务单号这类数据应该是从入参或者其它地方获取,写固定是为了演示方便,操作人也是不应该从入参中获取,而是从登陆信息里获取,这里作者就偷懒了…

代码实现

AOP拦截
  • 定义注解@LogRecordAnnotation
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface LogRecordAnnotation {

    //操作是否成功
    boolean succeed() default true;

    //操作人
    String operator() default "";

    //业务单号
    String bizNo() default "";

    //操作日志的种类
    String category() default "";

    //扩展参数,记录操作日志的修改详情
    String detail() default "";

    //记录日志的条件
    String condition() default "";
}

注解中的参数,应该满足大部分的使用场景,在需要记录操作日志的方法上加上该注解,以便后续进行拦截处理。

  • 切面增强逻辑LogRecordAspect
@Slf4j
@Aspect
@Component
public class LogRecordAspect {

    @Pointcut("@annotation(com.tx.operating.annotation.LogRecordAnnotation)")
    private void method() {
    }

    @Around("method()")
    public Object divAround(ProceedingJoinPoint joinPoint) throws Throwable {
        Map<String, Object> resultMap = new HashMap<>();
        Object proceed = null;

        try {
            //获取注解信息
            LogRecordAnnotation annotation = LogRecordOperationSource.getAnnotation(joinPoint);
            //获取SPEL表达式
            Map<String, Object> spelMap = LogRecordOperationSource.getBeforeExecuteFunctionTemplate(annotation);
            //执行SPEL表达式和执行自定义函数
            AnnotatedElementKey methodKey=new AnnotatedElementKey(((MethodSignature) joinPoint.getSignature()).getMethod(),joinPoint.getTarget().getClass());
            resultMap = LogRecordOperationSource.processBeforeExecuteFunctionTemplate(spelMap,methodKey,joinPoint.getArgs()[0]);
        } catch (Exception e) {
           log.info("/// log record exec error",e);
        }

        try {
            proceed = joinPoint.proceed();
        } catch (Exception e) {
            //目标方法执行异常,设置操作状态为失败
            resultMap.put(CommonConstants.SUCCEED,false);
            throw new OrsRuntimeException(" 目标方法执行异常,"+ e.getMessage());
        }finally {
            //通过SPI加载自定义持久化日志的方法进行日志持久化
            ServiceLoader<KeepOperatingRecordSpi> loader = ServiceLoader.load(KeepOperatingRecordSpi.class);
            Iterator<KeepOperatingRecordSpi> it = loader.iterator();
            if (it.hasNext()) {
                KeepOperatingRecordSpi keepOperatingRecordSpi = it.next();
                keepOperatingRecordSpi.keepRecord(resultMap);
            }
        }

        return proceed;
    }
    
}

核心逻辑:在业务的方法执行之前,会提前解析的自定义函数和SpEL表达式求值,操作日志的记录持久化是在方法执行完之后执行的,当方法抛出异常时,继续将操作日志持久化完成。

解析模板

LogRecordOperationSource里面封装了自定义函数和 SpEL 解析OperateRecordExpressionParse

public class OperateRecordExpressionParse extends CachedExpressionEvaluator {

    private final static OperateRecordExpressionParse operateRecordExpressionParse=new OperateRecordExpressionParse();

    public OperateRecordExpressionParse(){}

    private Map<ExpressionKey, Expression> expressionCache = new ConcurrentHashMap<>(64);

    public String parseExpression(String conditionExpression, AnnotatedElementKey methodKey, EvaluationContext evalContext) {
        return getExpression(this.expressionCache, methodKey, conditionExpression).getValue(evalContext, String.class);
    }

    public static OperateRecordExpressionParse getInstance(){
        return operateRecordExpressionParse;
    }
}

OperateRecordExpressionParse 继承自 CachedExpressionEvaluator 类,这个类里面有一个Map是 expressionCache。在上面的例子中可以看到,SpEL 会解析成一个 Expression 表达式,然后根据传入的 Object 获取到对应的值,所以 expressionCache 是为了缓存方法、表达式和 SpEL 的 Expression 的对应关系,让方法注解上添加的 SpEL 表达式只解析一次。

getExpression(this.expressionCache, methodKey, conditionExpression).getValue(evalContext, String.class);

getExpression 方法会从 expressionCache 中获取到 @LogRecordAnnotation 注解上的表达式的解析 Expression 的实例,然后调用 getValue 方法,getValue 传入一个 evalContext 上下文对象。在上面的例子中方法的入参就是上下文。

自定义函数

为什么会有自定义函数呢?

在上面的例子中可以看到有这样一条操作日志记录:用户A将地址从云南修改为深圳,新地址深圳从入参中很容易就获取到了,旧地址云南似乎是也可以,但是在我之前所想到的方法都没那么优雅。而通过自定义函数,我们可以根据数据Id转化为我们需要的对象信息,当然通过自定义函数我们还能做很多事,这里就不一一列举了。

定义IParseFunction 接口

public interface IParseFunction {
    
    //自定义函数名 用于@LogRecordAnnotation中进行模板配置
    String functionName();

    // 执行逻辑
    String apply(Object value);
}

ParseFunctionFactory代码比较简单,它的功能是把所有的 IParseFunction 注入到函数工厂中。

public class ParseFunctionFactory {

    private Map<String, IParseFunction> allFunctionMap;

    public ParseFunctionFactory() {
    // 通过反射获取所有IParseFunction接口的实现类
        List<Object> parseFunctions = FindImplementationsUtil.findImplementations(IParseFunction.class);
        if (CollectionUtils.isEmpty(parseFunctions)) {
            return;
        }
        allFunctionMap = new HashMap<>();
        for (Object obj : parseFunctions) {
            if (!(obj instanceof IParseFunction)) {
                continue;
            }
            IParseFunction parseFunction = (IParseFunction) obj;
            if (StringUtils.isEmpty(parseFunction.functionName())) {
                continue;
            }
            allFunctionMap.put(parseFunction.functionName(), parseFunction);
        }
    }

    public IParseFunction getFunction(String functionName) {
        return allFunctionMap.get(functionName);
    }
}
日志持久化

KeepOperatingRecordSpiSPI接口,业务可以实现这个保存接口,然后把日志保存在任何存储介质上(mysql、redis、mongoDB等等)

public interface KeepOperatingRecordSpi {

   <T> void keepRecord(T t);
}

对于java SPI机制不了解的小伙伴,可以看看这篇文章彻底搞懂JAVA SPI,这里就不做太多介绍了。

至此,本文实现操作日志记录的人核心方法与逻辑就介绍完啦,如果有小伙伴需要源码的可以去GitHub上获取opreating-record,也可以私信我。

总结

这篇文章主要分享了基于Spring Aop+SpEL实现操作日志记录,通过AOP与业务逻辑解耦,通过自定义函数、SpEL表达式实现日志的动态模板,使得我们可以优雅的记录操作日志。当然作者在实现的时候,也有瑕疵,也有一些东西没考虑到,欢迎小伙伴们留言,让其更加完善。

参考资料

  • 3
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
首先,我们需要定义一个自定义注解 `@RequiresPermissions`,用于标识需要授权访问的方法,例如: ```java @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface RequiresPermissions { String[] value(); // 权限值 } ``` 然后,我们需要实现一个切面,用于拦截被 `@RequiresPermissions` 标识的方法,并进行权限校验,例如: ```java @Component @Aspect public class PermissionCheckAspect { @Autowired private AuthService authService; @Around("@annotation(requiresPermissions)") public Object checkPermission(ProceedingJoinPoint joinPoint, RequiresPermissions requiresPermissions) throws Throwable { // 获取当前用户 User user = authService.getCurrentUser(); if (user == null) { throw new UnauthorizedException("用户未登录"); } // 获取当前用户的权限列表 List<String> permissions = authService.getUserPermissions(user); // 校验权限 for (String permission : requiresPermissions.value()) { if (!permissions.contains(permission)) { throw new ForbiddenException("没有访问权限:" + permission); } } // 执行目标方法 return joinPoint.proceed(); } } ``` 在切面中,我们首先通过 `AuthService` 获取当前用户及其权限列表,然后校验当前用户是否拥有被 `@RequiresPermissions` 标识的方法所需的所有权限,如果没有则抛出 `ForbiddenException` 异常,如果有则继续执行目标方法。 最后,我们需要在 Spring 配置文件中启用 AOP 自动代理,并扫描切面所在的包,例如: ```xml <aop:aspectj-autoproxy /> <context:component-scan base-package="com.example.aspect" /> ``` 这样,我们就通过 Spring AOP 和自定义注解模拟实现了类似 Shiro 权限校验的功能。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值