继续浏览精彩内容
慕课网APP
程序员的梦工厂
打开
继续
感谢您的支持,我会继续努力的
赞赏金额会直接到老师账户
将二维码发送给自己后长按识别
微信支付
支付宝支付

SpringBoot 中使用 AOP 打印接口日志

张伟30
关注TA
已关注
手记 1
粉丝 0
获赞 0

前言

AOP 是 Aspect Oriented Program (面向切面)的编程的缩写。他是和面向对象编程相对的一个概念。在面向对象的编程中,我们倾向于采用封装、继承、多态等概念,将一个个的功能在对象中来实现。但是,我们在实际情况中也发现,会有另外一种需求就是一类功能在很多对象的很多方法中都有需要。例如有一些对数据库访问的方法有事务管理的需求,有很多方法中要求打印日志。按照面向对象的方式,那么这些相同的功能要在很多地方来实现或者在很多地方来调用。这就非常繁琐并且和这些和业务不相关的需求耦合太紧密了。所以后来就出现了面向切面的编程来解决这一类问题,并对面向对象的编程做了很好的补充

概念

要很好的理解面向切面的编程,先要理解 AOP 的一些概念。在 Java 中 AspectJ 比较完整的实现了 AOP 的功能,但是使用起来也比较复,所以这里主要是讨论 Spring 的 AOP 。Spring AOP 采用简单够用的原则,实现了 AOP 的核心功能。下面先说说 AOP 中的具体概念

Aspect:方面。一个可以切入多个类的关注点。这个关注点实现了我们前面说的具体的业务功能。例如打印日志,进行数据库的事务管理等。
Joint point:被切入点。是指具体要实现前面所说的例如打印日志,数据库事务管理的被切入的点。也就是通过 AOP 将切面功能动态加入进去的程序位置。在 Spring AOP 里面这个指的都是某个方法
Pointcut:切点。用来指明如何通过规则匹配 Joint point。这个规则是一个表达式。在 Spring 中,默认使用的是 AspectJ 的 pointcut 表达式语言
Advice:指明在一个切入点的不同位置上采取的动作。例如对于一个数据库访问事务管理来说,在进入方法后要开启事务,在方法结束前要提交事务,在发生错误的时候要回滚事务。这属于三个不同的 Advice,要分别进行实现。Advice 通常和具体的 Pointcut 关联在一起。
AOP proxy:AOP 代理。用来实现将 Advice 功能动态加入到 Pointcut 的方法。在 Spring 的 AOP 中采用动态代理和 CGLIB 代理的方式来实现。而 AspectJ 则采用了特定编译器侵入字节码的方式来实现。

SprinBoot AOP 实现

前面我们已经用好几章讲述了 SpringBoot 的基本使用。那么这里我们就用 SpringBoot 和 AOP 结合来实现一个输出所有 Rest 接口输入参数和返回参数的日志的功能。
实现 rest 服务功能。
根据前面的文章,我们先建立一个 SpingBoot 的工程如下图所示

图片描述

SpringBoot 项目配置

我们对 SpringBoot 项目配置如下

server:
port: 3030
servlet:
context-path: /aop-demo
spring:
jackson:
date-format: yyyy-MM-dd HH:mm:ss
serialization:
indent-output: true
logging:
level:
com.yanggch: debug

其中 jackson 相关配置是为了将对象输出成 json 字符串后能够格式化输出
实现一个 rest 接口的 Controller 类
在这里,我们实现两个 rest 接口。一个是返回 hello 信息。一个是根据输入返回登录信息。

图片描述
先在我们要通过 AOP 功能将所有 Rest 接口的输入参数和返回结果输出到日志中。

实现 Web Aop 功能。

package com.ronglikj.cloud.sms.middleware.aop;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**

  • web 接口日志

  • @since : 2018-05-27
    */
    @Aspect
    @Component
    public class WebLogAspect {
    private static Logger log = LoggerFactory.getLogger(WebLogAspect.class);

    private final ObjectMapper mapper;

    @Autowired
    public WebLogAspect(ObjectMapper mapper) {
    this.mapper = mapper;
    }

    @Pointcut("@annotation(org.springframework.web.bind.annotation.RequestMapping)")
    public void webLog() {
    }

    @Before(“webLog()”)
    public void doBefore(JoinPoint joinPoint) {
    String request=“接口:”+joinPoint.getTarget().getClass().getName() + “.” + joinPoint.getSignature().getName()+“参数”;
    for (Object object : joinPoint.getArgs()) {
    if (
    object instanceof MultipartFile
    || object instanceof HttpServletRequest
    || object instanceof HttpServletResponse
    ) {
    continue;
    }
    try {
    request=request+mapper.writeValueAsString(object)+" ";
    /* log.debug(
    joinPoint.getTarget().getClass().getName() + “.” + joinPoint.getSignature().getName()
    + " : request parameter : " + mapper.writeValueAsString(object)
    );*/

         } catch (Exception e) {
             e.printStackTrace();
         }
     }
     log.debug(request);
    

    }

    @AfterReturning(returning = “response”, pointcut = “webLog()”)
    public void doAfterReturning(Object response) throws Throwable {
    if (response != null) {
    log.debug("接口响应:response parameter : " + mapper.writeValueAsString(response));
    }
    }
    }

测试

在前台通过 postman 发起请求,后台日志输入结果如下

接口:com.ronglikj.cloud.sms.middleware.controller.SmsSendVerifyCodeController.sendVerifyCodeApp参数"18721709643" “2.3.8” null “7853” null null “android” “_360” null

接口响应:response parameter : {
“code” : “0”,
“data” : {
“oldUserFlag” : true
},
“desc” : “发送成功”
}

此可见,我们虽然没有在 rest 接口方法中写输出日志的代码,但是通过 AOP 的方式可以自动的给各个 rest 入口方法中添加上输出入口参数和返回参数的代码并正确执行。

其他说明

前面提到了 Advice 的类型和 Pointcut 的 AOP 表达式语言。具体参考如下。

Advice 类型

before advice 在方法执行前执行。
after returning advice 在方法执行后返回一个结果后执行。
after throwing advice 在方法执行过程中抛出异常的时候执行。
Around advice 在方法执行前后和抛出异常时执行,相当于综合了以上三种通知。

AOP 表达式语言

方法参数匹配

@args()

方法描述匹配

execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern)throws-pattern?)
其中 returning type pattern,name pattern, and parameters pattern是必须的.

ret-type-pattern:可以为表示任何返回值,全路径的类名等.
name-pattern:指定方法名, 代表所有
set
代表以set开头的所有方法.
parameters pattern:指定方法参数(声明的类型),(…)代表所有参数,(
)代表一个参数
(*,String)代表第一个参数为任何值,第二个为String类型.

当前AOP代理对象类型匹配

目标类匹配
@target()
@within()
标有此注解的方法匹配
@annotation()

打开App,阅读手记
0人推荐
发表评论
随时随地看视频慕课网APP