AOP+注解+SpringEL表达式记录操作日志

JeecgBoot安全漏洞深度剖析:从低代码平台特性到SRC实战攻防 在软件开发领域,低代码开发平台通过可视化配置和代码生成,极大地提升了企业级应用(如OA、CRM、ERP系统)的构建效率。其核心原理在于将通用功能模块化,通过模板引擎和预置组件快速生成业务代码。然而,这种追求便捷的特性,若与薄弱的安全意识相结合,便会引入显著的安全风险,例如未授权访问和SQL注入。这些漏洞的技术价值在于,它们往往不是框架本身的致命缺陷,而是默认配置不当、依赖库漏洞与开发者对生成代码的误用共同作用的结果,最终在权限控制、数据交互等关键层面形成攻击面。在SRC(安全应急响应中心)漏洞挖掘和渗透测试 阅读详情

在后台管理系统中,日志是不可或缺的,通常,在开发的时候我们会设置日志级别为debug或info,上线后再设置为error,这些系统日志在帮助我们排查系统问题时作用非常大。但还有一类日志也是非常重要的,那就是操作日志,也就是记录用户在某个时间点干了什么事的日志。这篇文章就简单聊聊如何更好的记录操作日志。

在参与过不少项目的开发后,大概总结出了下面这么几种记录日志的方式。

第一种就是直接调用日志方法,类似下面这种,这类代码在以前的老系统中非常常见,好处是代码清晰,但太冗余了,总不可能每次要记录日志都复制一遍,如果不需要记录日志了,那又得挨个删除,特别麻烦。

void doSomething() {
    String userName = WebUtil.getUserName();
    logService.saveLog(userName, "${userName} did something at ${date}...");
}

第二种就是目前非常流行的注解加切面的方式,也是今天要介绍的。但我发现,在不少项目中,日志的功能都比较单一,记录的结果大都是“[xxx]执行了[xxx]操作”之类的,但在执行这个操作的时候,参数以及目标数据的信息等,日志中是看不出来的,这时候可能又会怀念上面的第一种方式,但实则大可不必。

用过spring缓存的应该都记得spring缓存中的非常常用的几个注解,如@Cacheable、@Cacheput等,在使用这些注解的时候,我们都会活用SpringEL表达式来操作缓存,如下面的代码,通过key = #id来动态设置key的值

@Cacheable(value = "user", key = "#id")
public UserInfo getById(Long id) {
    return userMapper.selectById(id);
}

那么在日志记录的时候,我们也可以模仿spring缓存,让日志注解支持SpringEL表达式,如此一来就可以非常方便的记录我们需要的日志了。

首先,新建日志注解LogRecord,代码如下

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface LogRecord {
    /**
     * 日志内容,支持SpEL表达式
     */
    String content();
    /**
     * 业务标识,支持SpEL表达式
     */
    String bizNo();
    /**
     * 类别
     */
    String category() default "";
    /**
     * 日志记录条件,支持SpEL表达式
     */
    String condition() default "true";
    /**
     * 是否保存入参
     */
    String saveParams() default "true";
}

然后是最重要的切面

@RequiredArgsConstructor
@Aspect
@Component
public class LogRecordAspect {

    private final ExpressionParser parser = new SpelExpressionParser();
    private final String spElFlag = "#";

    @Pointcut("@annotation(com.ygr.modules.sys.aspect.LogRecord)")
    public void pointcut() {
    }

    @Around("pointcut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        Object proceed = joinPoint.proceed();
        if (proceed instanceof ApiResult) {
            if (((ApiResult) proceed).isOk()) {
                recordLog(joinPoint, (ApiResult) proceed);
            }
        }
        return proceed;
    }


    private void recordLog(ProceedingJoinPoint joinPoint, ApiResult apiResult) {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        LogRecord logRecord = method.getAnnotation(LogRecord.class);
	    // 获取方法入参,key为参数名,value为参数值
        LinkedHashMap<String, Object> params = resolveParams(joinPoint);
        // 求值上下文
        StandardEvaluationContext context = new StandardEvaluationContext();
        if (params.size() == 1) {
            // 当参数只有一个时,设置根对象,例如入参为对象,则此时可以使用 #root.id 来获取对象的id
            params.forEach((k, v) -> context.setRootObject(v));
        }
        int i = 0;
        for (Map.Entry<String, Object> entry : params.entrySet()) {
            context.setVariable(entry.getKey(), entry.getValue());
            // 设置参数别名,按顺序,可使用 #a0 或 #p0 来获取第一个入参
            context.setVariable("a" + i, entry.getValue());
            context.setVariable("p" + i, entry.getValue());
            i++;
        }
        // ApiResult是自定义的web返回格式类,将真正执行结果也放入上下文
        // 因为有些日志是需要获取执行结果的,比如新增数据,新增成功之后才会有id
        context.setVariable("result", apiResult);

        String condition = resolveValue(logRecord.condition(), context);
        // 如果不满足记录日志条件,则返回
        if (!Boolean.parseBoolean(condition)) {
            return;
        }
        boolean saveParams = Boolean.parseBoolean(resolveValue(logRecord.saveParams(), context));
	    if (saveParams) {
            // TODO 如果需要保存日志到数据库
        }
        
    }

    private LinkedHashMap<String, Object> resolveParams(ProceedingJoinPoint joinPoint) {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        Object[] arguments = joinPoint.getArgs();
        String[] paramNames = getParameterNames(method);
        
        LinkedHashMap<String, Object> params = new LinkedHashMap<>();
        for (int i = 0; i < arguments.length; i++) {
            params.put(paramNames[i], arguments[i]);
        }
        return params;
    }

    private String[] getParameterNames(Method method) {
        ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();
        return parameterNameDiscoverer.getParameterNames(method);
    }

    private String resolveValue(String exp, EvaluationContext context) {
        String value;
        // 如果包含#字符,则使用SpringEL表达式进行解析
        if (exp.contains(spElFlag)) {
            value = resolveValueByExpression(exp, context);
        } else {
            // 否则不处理
            value = exp;
        }
        return value;
    }

    private String resolveValueByExpression(String spELString, EvaluationContext context) {
        // 构建表达式
        Expression expression = parser.parseExpression(spELString);
        // 解析
        return expression.getValue(context, String.class);
    }
}

代码很简单,注释很详细了,就不再多说了。使用也很简单,直接加注解就行,比如新增配置参数

@LogRecord(category = "SysConfigParam", bizNo = "#result.data.id", content = "'新增配置参数,code:' + #entity.code + ',name:' + #entity.name")
@PostMapping("/sys-config-param")
public ApiResult<SysConfigParam> insert(@RequestBody SysConfigParam entity) {
    this.service.save(entity);
    return ApiResult.ok(entity);
}

这其实还算比较简单的类型了,对于一些更新操作,有些时候可能希望日志记录得更详细。客户管理员在查看日志的时候,相比于[xxx]修改手机号为[15888888888],肯定是更希望看到[xxx]将手机号由[15877777777]修改为了[15888888888]。这就要求在记录日志时,不仅需要入参数据,还需要查询修改前的数据,最后才拼接出需要的日志。

这一点,SpringEL也能很好的支持,因为SpringEL是支持方法作为表达式,比如最常见的SpringSecurity中权限校验注解@PreAuthorize("hasRole('ROLE_xxx')" ),就是一个很好的例子。当然也可以借助线程上下文,将参数设置到上下文中进行参数传递。

至于自定义方法的解析,等下次有空了再做记录。

最后一种就是使用Canal监听数据库的增量日志,虽说解耦,但局限性太大,目前只支持mysql,且一旦操作涉及RPC,这种方式就几乎没用了。

[高级]SpringBoot和@Aspect实现自建Log日志功能 SpringBoot和@Aspect实现自建Log日志功能 阅读详情

相关推荐

AOP拦截EL表达式

网上很不容易找到的关于AOP拦截EL表达式的正确打开方式,比如:http://www.cnblogs.com/ph123/p/5631030.html这种根本就不准确

切面编程-记录操作日志(aop + 正则表达式)

aop+正则表达式的方式记录操作日志

lendsomething的博客 524

算力网:正在编织的“第二电网“

中国算力网:编织智能时代的"第二电网" 中国正在构建全国一体化算力网,目标让算力资源像水电一样即取即用。这一国家战略通过"东数西算"布局,将东部算力需求与西部清洁能源优势结合,形成三级时延圈(1ms城市级、5ms区域级、20ms国家级)。算力网采用三层架构:基础设施层的八大枢纽节点作为"发电厂",编排管理层的智能调度系统作为"调度中心",运营服务层提供标准化算力服务。关键技术包括算力路由、异构算力统一度量、无损网络和液冷技术等。

这只是一个博客 667

AOP + Spring-EL表达式构建优雅的系统操作日志框架

操作日志

hjg719的博客 1756

SPSS操作(五):主成分分析

1、将数据导入spss,并定义变量,将铁路运输业职工人数(人)、城市公共交通业职工人数(人)、邮政业职工人数(人)、电信和其他信息传输服务业职工人数(人)、客运量(万人)、接待入境旅游人数(万人次)、普通高校师生比(%)、星级住宿业营业额(亿元)分别定义为x1,x2,x3,x4,x5,x6,x7,x8 如图: 2、对数据进行标准化处理 ①选择【分析】—【描述统计】—【描述】 ②添加要标准化的变量,勾选【将标准化值另存为变量(Z)】,再点确定 ③返回原来的数据窗口就可以看见标准化后的变量了 3、SPS

My_daily_life的博客 8万+

UsualToolCMS-验证码绕过漏洞复现

本文以UsualToolCMS为靶场,复现了验证码绕过漏洞

小小小屿屿屿的博客 919

通过AOP+Java注解+EL表达式获取方法参数的值

Java注解通过EL表达式获取方法参数值使用场景代码实现 使用场景 在使用AOP做切面时,每个方法的参数都不一致,有时候需要取某个参数值,由于AOP增强逻辑一般都是抽象通用封装的,不能指定写死取某个值,这时候可以使用注解+EL表达式来实现。 代码实现 以实现声明式分布式锁为例子 当方法需要加锁时,加上该注解。 @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD}) @Documented public @interface Lock

Fred-X的博客 5511

隐藏技巧!用ArcGIS Pro像素编辑器实现PS级影像编辑(附地貌伪装案例)

本文深入探讨ArcGIS Pro像素编辑器的创意应用,从基础操作到高级影像处理实战,展示了其在城市规划、环境分析等领域的独特价值。通过详细的功能解析和地貌伪装等案例,帮助用户实现媲美专业图像处理软件的精细操作,提升GIS工作效率。

weixin_28520495的博客 335

【Spring EL<一>✈️ 】SL 表达式的应用

Spring表达式语言—SpringEL 的常见用法,以及结合注解使用方式

产品经理克星(狗头) 1581

kmeans聚类分析结果怎么看_【Stata与机器学习】物以类聚-KMeans算法应用案例介绍...

古语有云:物以类聚,人以群分。KMeans是无监督机器学习中的聚类算法中最简单的一种——基于特征对样本进行归为不同的类,很好地阐述了‘物以类聚’这句话的精髓。机器学习又叫统计学习。既然都统计了,所以当然包括了计量经济学。所以一般来说,机器学习的算法类型远多于计量经济学。比如线性回归,这个算是机器学习算法中比较基础的部分。当然,计量经济学在回归分析、因果推断方面的造诣,也是普通机器学算法所不能比的。...

weixin_39857480的博客 1万+

滑模控制算法——基本原理(附MATLAB程序)

选择一个滑模面和是设计参数。是滑模面函数,通常我们希望系统状态在的面上滑动。我们以一个简单的二阶系统作为示例,其状态空间方程为:是系统矩阵是输入矩阵是输出矩阵假设我们的系统如下:滑模控制算法的核心在于设计合适的滑模面和控制律,并分析系统在滑模面上的稳定性。通过上述公式和步骤,可以有效地设计滑模控制器,以处理具有非线性和不确定性的动态系统。总的来说,滑模控制算法因其鲁棒性和灵活性在处理复杂控制问题中表现出色,但在实际应用中需要精心设计以克服抖振现象和其他潜在问题。BuxuABDC。

qq_35623594的博客 2万+

使用AOP结合自定义注解实现添加日志功能【新手向】

使用Spring AOP结合自定义注解实现用户行为监控功能。讲的比较浅显易懂,都是大白话。

lyx7762的博客 1863

Python 爬虫实战:豆瓣 / IMDB 电影数据抓取(评分 + 剧情 + 演员阵容结构化处理)

在数据为王的时代,电影数据是进行市场分析、推荐系统构建和学术研究的重要资源。豆瓣和 IMDB 作为国内外最具影响力的电影评分和资讯网站,包含了海量高质量的电影数据。本文将带领你从零开始,构建一个健壮的 Python 爬虫,用于抓取豆瓣和 IMDB 的电影信息,并将其中的非结构化数据(如剧情简介、演员列表)进行清洗和结构化处理,最终存储到便于分析的结构化文件(如 CSV)中。

yansideyucsdn的博客 956

spring AOP注解@Aspect实现自定义注解日志记录@Target:注解的作用目标

Target(ElementType.TYPE)——接口、类、枚举、注解@Target(ElementType.FIELD)——字段、枚举的常量@Target(ElementType.METHOD)——方法@Target(ElementType.PARAMETER)——方法参数@Target(ElementType.CONSTRUCTOR) ——构造函数@Target(ElementType.LOCAL_VARIABLE)——局部变量。

weixin_43939708的博客 697

腾讯微搭低代码开发实战电子书:医疗挂号小程序

本书通过一个完整的“医疗挂号小程序”项目,深度讲解腾讯微搭(WeDa)低代码平台在复杂业务场景下的实战应用。从登录体系搭建、用户注册,到就诊人管理、科室排班与预约下单,全程遵循“配置+逻辑+实战”的思路,带领读者从零实现具备商业级可用性的医疗挂号小程序,掌握低代码开发的核心技巧与业务落地能力。

低代码布道师的博客 953

霍尼韦尔SoftMaster使用说明

霍尼韦尔SoftMaster使用说明

学习记录-操作日志

系统操作日志是用于记录系统中用户或系统本身所执行的各类操作的日志信息。这些日志通常包括操作的时间、操作的用户、具体操作内容、操作结果以及其他相关信息。 **安全审计**:记录用户操作以防止恶意行为,确保系统的安全性。 **问题排查**:在系统出现问题时,可以通过操作日志快速定位问题来源。

sjsjsbbsbsn的博客 1395

SIMATIC S7-1200, S7-1500 PID 控制[手册]

SIMATIC S7-1200, S7-1500 PID 控制[手册]

被忽略的 SAAS 生命线:操作日志有多重要

操作日志作为各类系统的必备功能,其记录机制的设计直接影响系统的可维护性与用户体验。与面向开发人员的系统日志不同,操作日志的核心价值在于清晰还原用户行为轨迹,因此必须具备高度的可读性

hehehaha2025的博客 160

pscad IEEE标准系统搭建说明

pscad IEEE标准系统搭建说明,包含典型的IEEE 9节点、14节点、30节点、39节点和118节点系统。内含标准测试系统的原始数据和潮流结果。

精雕软件(JDpaint) 5.5 绿色版

精雕软件(JDpaint) 5.5 绿色版精雕软件(JDpaint) 5.5 绿色版精雕软件(JDpaint) 5.5 绿色版

架构设计内容分享(一百零四):记录业务系统操作日志方案实践

说明:初始化请求Context,并解析request相关信息。4.2定义操作类型与注解,操作类型为便于搜索,注解用于标记需要追踪变更的 Controller 方法。说明:1.TrackingType 操作类型。2.moduleAlias 接口对应的模块名称,moduleClass接口对应的模块名称枚举类,如配置modelClass时,moduleAlias必为枚举类中的字段。主要为了方便模块名称的管理。3.title,标题,此次操作类型的简单描述,通常为接口功能描述。

之乎者也·的博客 3455

Spring-Boot-AOP-实现一个日志插件-应用实践

前面针对AOP的使用姿势和一些疑问进行了说明,这一篇则从应用的角度出发,看下AOP可以实现些什么样的效果。

Java程序员的知识博客 548

java 注解 spring el_怎么自定义注解,支持Spring EL表达式,可以在idea中提示出来的??...

1 新增三个注解支持类,其实是拷贝出来的spring的类package per.yan.ding.util.el;import org.aspectj.lang.JoinPoint;import org.aspectj.lang.reflect.MethodSignature;import org.springframework.cache.interceptor.SimpleKeyGene...

weixin_35950078的博客 1023

spring aop详细介绍

Spring AOP是Spring框架的一部分,主要用于在应用程序的不同部分之间进行面向切面编程(AOP)。AOP是一种编程范式,可以将应用程序的关注点分离,使应用程序更具可重用性和可维护性。 Spring AOP使用代理模式在运行时织入额外的功能,可以在方法调用前后、异常处理等方面添加额外的逻辑。使用Spring AOP可以将应用程序的关注点(如事务处理、日志记录等)分离出来,从而提高应用程序的...

weixin_42593701的博客 66

知识收集库

spring jar包作用 Spring中各个jar包的作用 spring-jar包详解整理 spring framework体系结构及内部各模块jar之间的maven依赖关系 Spring 表达式语言(Spring EL) Spring 表达式语言(Spring EL) JAVA导出EXCEL表格 JAVA导出EXCEL表格 idea idea 教程 idea快捷键 idea快捷键...

weixin_30824277的博客 239

Java项目中基于AOP注解方式实现接口日志记录

日志记录,包括接口调用时间,参数,操作类型,自定义注解的方式去实现,涵盖了AOP的基本用法及常用注解

Jnsone的博客 3639

SpringAOP实现记录日志(表达式版本)

```java @Component @Aspect @Log public class ControllerMethodParamLog { @Pointcut("execution(* cn.ccb.controller..*(..))") public void aspect() { } @Before("aspect()") public void before(JoinPoint ...

codeccb 129

使用Aop的方式实现自动日志记录

允许注解在方法和类上添加: 以下注解在接口上添加: 2.1、先编写一个pt方法,此方法结合了上面自定义的注解:2.2、然后编写log方法,对加了LogAnnotation注解的类或者方法实现环绕通知: 2.3、最后完善redcordlog方法来记录日志,并在控制台输出相关记录的日志:完整代码: redcordlog方法包含了两个工具类(用来获取当前接口ip地址的),如下: 3、使用接口,查看控制台输出信息

weixin_55076626的博客 2170

springboot整合aop,实现日志操作

springboot整合aop,实现日志操作

qq_42405688的博客 761
上一篇: springboot2集成shiro认证鉴权(下篇)
下一篇: 失败重试方案spring-retry
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值