使用AOP记录接口访问日志

SpringBoot应用中使用AOP记录接口访问日志

本文主要讲述AOP在项目中的应用,通过在controller层建一个切面来实现接口访问的统一日志记录。

AOP

AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。利用AOP可以对业务逻辑的各个部分进行隔离,将那些与业务无关的、却被业务模块共同调用的功能抽取出来,形成一个独立的模块,从而在需要的时候动态地将这些功能"织入"到业务模块中,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

AOP的相关术语

通知(Advice)

通知描述了切面要完成的工作以及何时执行。比如我们的日志切面需要记录每个接口调用时长,就需要在接口调用前后分别记录当前时间,再取差值。

  • 前置通知(Before):在目标方法调用前调用通知功能;通常用于执行一些预处理操作,如权限检查、参数验证等。
  • 后置通知(After):无论目标方法是否正常返回,都会执行的通知;通常用于资源清理和释放等操作。
  • 返回通知(AfterReturning):在目标方法正常返回后调用通知功能;通常用于获取方法的返回值,并做一些后续处理。
  • 异常通知(AfterThrowing):在目标方法抛出异常后调用通知功能;通常用于进行异常处理和日志记录。
  • 环绕通知(Around):通知包裹了目标方法,在目标方法调用之前和之后执行自定义的行为;通过 ProceedingJoinPoint 参数,可以选择是否执行目标方法。
连接点(JoinPoint)

指程序执行的某个特定位置,比如方法调用、异常抛出等。这些位置就是切面能够插入自己的逻辑的地方。连接点是程序执行过程中的一个精确的点。比如方法调用、方法执行、异常抛出等。

切点(Pointcut)

切点(Pointcut)是一个表达式,用于匹配连接点。切点定义了通知功能被应用的范围。比如日志切面的应用范围就是所有接口,即所有controller层的接口方法。

切面(Aspect)

切面(Aspect)是一个模块化的横切关注点,定义了在何处以及在何时执行特定的行为。切面由切点(Pointcut)和通知(Advice)组成。(通常定义一个切面类)

引入(Introduction)

在无需修改现有类的情况下,向现有的类添加新方法或属性,实现接口,甚至可以将横切关注点的逻辑添加到现有的类中。

指的是在切面中声明一些新的成员(方法或属性)或为现有的类添加实现新的接口。使得某个类在不直接修改其代码的情况下,获得新的行为或特性。

引入通常通过使用@DeclareParents注解来实现。有两个关键参数:valuedefaultImpl

@DeclareParents(value = "com.example.service.*+", defaultImpl = AdditionalFunctionalityImpl.class)

  • value 可以使用通配符来匹配一组类,比如
    • "com.example.service.*" 匹配 com.example.service 包下的所有类。
    • "com.example.service..*" 匹配 com.example.service 包及其子包下的所有类。
    • "com.example.service.SomeService+" 匹配 SomeService 类及其所有子类。
  • defaultImpl:这个参数用于指定要引入的接口的默认实现类。当value中目标类被引入该接口时,如果目标类没有实现该接口,将使用这个默认实现类的方法。
织入(Weaving)

把切面应用到目标对象并创建新的代理对象的过程。织入可以在不同的阶段进行,具体包括编译时、类加载时和运行时。

AOP 的工作原理

Spring 容器在启动时,会扫描所有 Bean,识别出其中的切面。

程序在执行到切点所描述的连接点时,AOP 框架会动态地将切面的通知"织入"到目标对象中,从而实现横切性功能的整合。
这种动态织入的方式,使得开发人员可以专注于业务逻辑的开发,而不必过多地关注这些横切性需求。

Spring中使用注解创建切面

相关注解

  • @Aspect:用于定义切面
  • @Before:通知方法会在目标方法调用之前执行
  • @After:通知方法会在目标方法返回或抛出异常后执行
  • @AfterReturning:通知方法会在目标方法返回后执行
  • @AfterThrowing:通知方法会在目标方法抛出异常后执行
  • @Around:通知方法会将目标方法封装起来
  • @Pointcut:定义切点表达式

切点表达式

指定了通知被应用的范围,表达式格式:

execution(方法修饰符 返回类型 方法所属的包.类名.方法名称(方法参数)
//com.tiny.controller包中所有类的public方法都应用切面里的通知
execution(public * com.tiny.controller.*.*(..))
//com.tiny.service包及其子包下所有类中的所有方法都应用切面里的通知
execution(* com.tiny.service..*.*(..))
//com.tiny.service.PmsBrandService类中的所有方法都应用切面里的通知
execution(* com.tiny.service.PmsBrandService.*(..))

添加AOP切面实现接口日志记录

添加切面类WebLogAspect

定义了一个日志切面,在环绕通知中获取日志需要的信息,并应用到controller层中所有的public方法中去。

package com....;

/**
 * 请求日志及访问限制
 */
@Aspect
@Component
@Slf4j
public class RequestAspect {
    @Autowired
    private HttpServletRequest request;
    /**
     * 是否记录所有日志到数据库中
     */
    private static final Boolean IS_ADD_MYSQL = false;

    /**
     * 定义切点
     */
    @Pointcut("execution(* com.master.chat..controller..*.*(..))")
    public void requestApi() {
    }

    @Before("requestApi()")
    public void doBefore(JoinPoint joinPoint) throws Throwable {
        addOperaterInfo(joinPoint.getArgs());
    }

    @After("requestApi()")
    public void doAfter() throws Throwable {
    }

    @Around("requestApi()")
    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
        long beginTime = System.currentTimeMillis();
        Object result = joinPoint.proceed();
        long time = System.currentTimeMillis() - beginTime;
        Object[] args = joinPoint.getArgs();
        String parameter = getParameter(args);
        saveSyslog(joinPoint, parameter, result, time);
        log.info(getRequestParams(), getRequestArgs(parameter, time));
        return result;
    }

    /**
     * 添加操作人信息
     *
     * @param args
     */
    private void addOperaterInfo(Object[] args) {
        if (ValidatorUtil.isNullIncludeArray(args)) {
            return;
        }
        Stream<?> stream = ArrayUtils.isEmpty(args) ? Stream.empty() : Arrays.stream(args);
        CommonCommand command = (CommonCommand) stream.filter(arg -> arg instanceof CommonCommand).findFirst().orElse(null);
        if (ValidatorUtil.isNull(command)) {
            return;
        }
        UserDetail userDetail = JwtTokenUtils.getLoginUser();
        if (ValidatorUtil.isNotNull(userDetail)) {
            command.setOperaterId(userDetail.getId());
            command.setOperater(userDetail.getUsername());
        }
    }

    /**
     * 构建系统请求日志信息
     *
     * @param parameter 请求参数
     * @param result    返回结果
     * @param time      请求耗时
     */
    private void saveSyslog(ProceedingJoinPoint joinPoint, String parameter, Object result, long time) {
        if (!IS_ADD_MYSQL) {
            return;
        }
        try {
            MethodSignature signature = (MethodSignature) joinPoint.getSignature();
            UserDetail userDetail = JwtTokenUtils.getLoginUser();
            UserAgent userAgent = UserAgentUtil.parse(request.getHeader(Header.USER_AGENT.getValue()));
            String ip = IPUtil.getIpAddr(request);
            String urlStr = request.getRequestURL().toString();
            String domain = StrUtil.removeSuffix(urlStr, URLUtil.url(urlStr).getPath());
            String className = joinPoint.getTarget().getClass().getName();
            String methodName = signature.getName();
            methodName = className + StringPoolConstant.DOT + methodName + StringPoolConstant.LEFT_BRACKET + StringPoolConstant.RIGHT_BRACKET;
            SysLog sysLog = SysLog.builder()
                    .sysUserId(ValidatorUtil.isNotNull(userDetail) ? userDetail.getId() : 0L)
                    .username(ValidatorUtil.isNotNull(userDetail) ? userDetail.getUsername() : "游客")
                    .ip(ip).domain(domain)
                    .browser(userAgent.getBrowser().getName()).os(userAgent.getOs().getName())
                    .method(methodName).requestMethod(request.getMethod()).uri(request.getRequestURI())
                    .build();
            AsyncManager.me().execute(AsyncFactory.addSysLog(sysLog, parameter, result, time));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 获取请求日志参数
     *
     * @param args 请求参数
     * @return
     */
    private String getRequestParams() {
        // 构建成一条长 日志,避免并发下日志错乱
        StringBuilder requestLog = new StringBuilder(300);
        requestLog.append("\n");
        requestLog.append("请求IP:{}\n");
        requestLog.append("请求路径:{}\n");
        requestLog.append("请求方式:{}\n");
        requestLog.append("请求参数:{}\n");
        requestLog.append("请求耗时:{}ms");
        return requestLog.toString();
    }

    /**
     * 获取请求日志参数
     *
     * @param args 请求参数
     * @return
     */
    private Object[] getRequestArgs(String parameter, Long time) {
        List<Object> responseArgs = new ArrayList<>();
        responseArgs.add(IPUtil.getIpAddr(request));
        responseArgs.add(request.getRequestURI());
        responseArgs.add(request.getMethod());
        responseArgs.add(parameter);
        responseArgs.add(time);
        return responseArgs.toArray();
    }

    /**
     * 根据方法和传入的参数获取请求参数
     *
     * @param args 请求参数
     * @return
     */
    private String getParameter(Object[] args) {
        Stream<?> stream = ArrayUtils.isEmpty(args) ? Stream.empty() : Arrays.stream(args);
        List<Object> logArgs = stream
                .filter(arg -> (!(arg instanceof HttpServletRequest)
                        && !(arg instanceof HttpServletResponse)
                        && !(arg instanceof MultipartFile[])
                        && !(arg instanceof MultipartFile)
                        && !(arg instanceof Principal)))
                .collect(Collectors.toList());
        return logArgs.size() == 0 ? StringPoolConstant.EMPTY : JSON.toJSONString(logArgs);
    }

}

进行接口测试

运行项目并访问:

可以看到控制住台中会打印如下日志信息:

2024-05-18 21:24:56.367 [http-nio-8088-exec-17] INFO  com.chat.framework.aspect.RequestAspect - 
请求IP127.0.0.1
请求路径:/chat/app/chat/message
请求方式:POST
请求参数:[{"chatNumber":"1791817611953176576","model":"SPARK","needsOperator":true,"operater":"177********","operaterId":1,"prompt":"怎么快速上手一个Java项目","systemPrompt":"怎么快速上手一个Java项目","userId":1}]
请求耗时:22ms
  • 19
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Spring AOP(Aspect-Oriented Programming)是Spring框架中的一个模块,用于实现面向切面编程。Spring AOP可以在运行时动态地将代码织入到目标对象的方法中,从而实现一些横切关注点的功能,如日志记录、性能统计、事务管理等。 以下是Spring AOP使用步骤: 1.在Spring配置文件中启用AOP。 ```xml <beans xmlns:aop="http://www.springframework.org/schema/aop"> <aop:aspectj-autoproxy/> </beans> ``` 2.定义切面类,即实现横切功能的类。 ```java @Aspect public class LoggingAspect { @Before("execution(* com.example.demo.service.*.*(..))") public void logBefore(JoinPoint joinPoint) { System.out.println("Method " + joinPoint.getSignature().getName() + " is called."); } } ``` 3.在切面类中定义通知,即横切功能的具体实现。 ```java @Before("execution(* com.example.demo.service.*.*(..))") public void logBefore(JoinPoint joinPoint) { System.out.println("Method " + joinPoint.getSignature().getName() + " is called."); } ``` 4.在目标对象中使用切面。 ```java @Service public class UserService { public void addUser(User user) { //... } } ``` 5.在目标对象中使用切面。 ```xml <bean id="userService" class="com.example.demo.service.UserService"/> <bean id="loggingAspect" class="com.example.demo.aspect.LoggingAspect"/> <aop:config> <aop:aspect ref="loggingAspect"> <aop:before pointcut="execution(* com.example.demo.service.*.*(..))" method="logBefore"/> </aop:aspect> </aop:config> ``` 以上就是Spring AOP的基本使用方法。需要注意的是,Spring AOP只支持基于代理的AOP实现,因此需要在目标对象上使用接口。另外,Spring AOP只能拦截方法调用,无法拦截属性访问等其他操作。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值