AOP开发

依赖

pom.xml中添加依赖:

<dependency>
	<groupId>org.aspectj</groupId>
	<artifactId>aspectjrt</artifactId>
	<version>1.8.13</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

然后在配置文件中添加配置:

# 开启注解(默认为true)
spring.aop.auto=true
# true使用cglib代理,false使用JDK动态代理(默认为false)
spring.aop.proxy-target-class=true

该配置可以不加,并不会影响执行的结果。
若不使用cglib代理,则默认使用JDK动态代理。
在创建代理上,JDK效率更高。但在调用代理方法时,JDK使用的是反射机制,cglib使用FastClass机制,cglib效率更高。
但注意即使spring.aop.proxy-target-class设置为false,如果目标类没有生命接口,则Spring将自动使用CGLib动态代理。

开发

首先定义一个程序员类:

package com.example.test.component;

import org.springframework.stereotype.Component;

@Component
public class Programmer {

    public void develop() {
        System.out.println("开发");
    }
}

该类只有一个方法develop()
使用@Component注解将其放入了Spring容器中。
但在执行develop()之前,我们首先要打开编译器;执行develop()之后,我们要关闭编译器。
现在使用切面来开发。先定义一个环境类,增加@Component注解添加到Spring容器。然后在其下添加2个方法:

@Component
public class Auxiliary {

    public void openCompiler() {
        System.out.println("打开编译器");
    }

    public void closeCompiler() {
        System.out.println("关闭编译器");
    }
}

我们需要在Programmer.develop()前后分别调用openCompiler()closeCompiler()
接下来为Auxiliary类添加切面:

  • 添加@Aspect注解来将该类声明为切面类。
  • 添加一个代理方法work(),需要使用@Pointcut来指明该方法的切点位置。在这里,Auxiliary.work()代表了Programmer.develop()
  • openCompiler()前添加@Before("work()"),用于指明该方法需在代理方法执行前执行。
  • closeCompiler()前添加@After("work()"),用于指明该方法需在代理方法执行后执行。
package com.example.test.component;

import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class Auxiliary {

    @Pointcut("execution(* com.example.test.component.Programmer.develop(..))")
    public void work() {}

    @Before("work()")
    public void openCompiler() {
        System.out.println("打开编译器");
    }

    @After("work()")
    public void closeCompiler() {
        System.out.println("关闭编译器");
    }
}

然后将Programmer放入一个controller中进行调用:

@Autowired
private Programmer programmer;

@GetMapping("/test")
public void test() {
    programmer.develop();
}

可以看到控制台的输出:

打开编译器
开发
关闭编译器

切点语法

使用@Pointcut()来指定切点。其语法为:

@Pointcut("execution( 修饰符 返回值类型 包名.类名.方法名(参数类型..))")

其中可使用*代表任意类型。
例如:

// Programmer类
@Pointcut("execution( com.example.test.component.Programmer)");

// Programmer类下所有的方法
@Pointcut("execution( * com.example.test.component.Programmer.*(..))");

// Programmer类下所有的public方法
@Pointcut("execution( public * com.example.test.component.Programmer.*(..))");

// Programmer类下public且返回值类型为String的方法
@Pointcut("execution( public String com.example.test.component.Programmer.*(..))");

// Programmer类下public且返回值类型为String的所有参数类型的develop方法
@Pointcut("execution( public String com.example.test.component.Programmer.develop(..))");

// Programmer类下public且返回值类型为String的第一个参数为String类型的develop方法
@Pointcut("execution( public String com.example.test.component.Programmer.develop(String,..))");

// Programmer类下public且返回值类型为String的第一个参数为String,第二个参数为Long类型的develop方法
@Pointcut("execution( public String com.example.test.component.Programmer.develop(String, Long))");

各通知的调用时机:通知

针对代理被调用时的时机,有如下几种注解:

  • @before: 前置通知,在代理调用之前执行。
  • @After:后置通知,在代理调用之后执行。发生异常依然会执行。
  • @AfterReturning: 返回通知,在方法返回结果之后执行。其时机在@After之前。发生异常不会被执行。
  • @AfterThrowing:异常通知,在方法抛出异常之后执行。其时机在@After之前。
  • @Around:环绕通知,将切点方法放入自身方法体内执行,且自身方法必须返回切点方法的执行结果Object。发生异常则之后的@Around不会被执行。

其中较为特殊的是@Around
以上所有注解修饰的方法都可传入JoinPoint及其子类型参数。通过JoinPoint可获取到参数等信息。

@Around

@Around的作用是将切点方法放入自身方法体内执行。因此可以在切点方法执行前或者执行后添加各种自定义操作。
@Around需传入ProceedingJoinPoint参数(派生于JoinPoint)并调用ProceedingJoinPoint.proceed()才能实现切点方法的调用。不传参切点方法不会被调用。
对应地,ProceedingJoinPoint参数只能用于@Around
特别注意,@Around修饰的方法必须返回ProceedingJoinPoint.proceed()的结果Object,否则请求结果将不会被返回给请求者。
例如:

package com.example.test.component;

import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class Auxiliary {

    @Pointcut("execution(* com.example.test.component.Programmer.develop(..))")
    public void work() {}

    @Before("work()")
    public void openCompiler() {
        System.out.println("打开编译器");
    }

    @After("work()")
    public void closeCompiler() {
        System.out.println("关闭编译器");
    }

    @AfterReturning("work()")
    public void checkInCode() {
        System.out.println("提交代码");
    }

    @AfterThrowing("work()")
    public void throwException() {
        System.out.println("出现异常");
    }
	
    @Around("work()")
    public Object aroundCompiler(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("喝杯咖啡");
        Object result = joinPoint.proceed();
        System.out.println("喝杯水");
        return result;
    }
}

上面还加入了@AfterReturning@AfterThrowing通知。
调用接口后,可以看到控制台的输出:

喝杯咖啡
打开编译器
开发
提交代码
关闭编译器
喝杯水

@Around的时机早于@Before,晚于@After

@AfterThrowing

当逻辑出现异常时,@After依然会正常执行。而@AfterReturning以及之后的@Around不会被执行。
现在修改Programmer.develop()

@Component
public class Programmer {
    public void develop() {
        System.out.println("开发");
		
        Integer sum = null;
        sum++;
    }
}

当执行Programmer.develop()时会抛出异常。
调用接口后,可以看到控制台的输出:

喝杯咖啡
打开编译器
开发
异常
关闭编译器

在通知中获取请求的属性

所有的通知都可以传入JoinPoint参数并获取到请求的属性。

获取请求对象

@Before(value = "pointcut()")
public void pointcut(JoinPoint joinPoint) throws Throwable {
    ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
    HttpServletRequest request = attributes.getRequest();
}

获取到请求对象后即可拿到所有请求相关的属性。

修改执行参数

对于@Around,可以传入ProceedingJoinPoint参数(派生于JoinPoint)并调用ProceedingJoinPoint.proceed()才能实现切点方法的调用。
现在要在切点方法执行前对其参数进行修改,对其所有的Integer类型参数+1。可以这样操作:

@Around("work()")
public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
    System.out.println("喝杯咖啡");
	Object[] args = joinPoint.getArgs();
    for (int i = 0; i < args.length; i++) {
        if(args[i] instanceof Integer) {
            args[i] = (Integer)args[i] + 1;
        }
    }
    Object result = joinPoint.proceed(args);
    System.out.println("喝杯水");
    return result;
}

关键在于ProceedingJoinPoint.proceed()可传入Object[]型参数,其定义为:

Object proceed() throws Throwable;
Object proceed(Object[] var1) throws Throwable;

传入后将会替代原本的参数值。
注意述拦截的请求类型是get还是post,参数以?&形式还是以form-data或json形式传入,JoinPoint.getArgs()都可以获取到。json形式传入时会以String类型放在args[0]中。
执行后返回的Object结果需要由@Around所修饰的方法返回。

属性获取

对于一个POST方法,使用form-data方式传入v1v22个参数。

@PostMapping("/myTest")
public void myTest(Integer v1, Integer v2) {}

加一个切点,令其匹配测试接口:

@Aspect
@Component
public class AspectTest {
    private static final Logger log = LoggerFactory.getLogger(AspectTest.class);

    @Pointcut("execution(public * com.example.test.controller.TestController.*(..))")
    public void pointcut() {}
}

首先在@Around中修改其参数值,令其+1:

@Around(value = "pointcut()")
public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
    Object[] args = joinPoint.getArgs();
    for (int i = 0; i < args.length; i++) {
        if (args[i] instanceof Integer) {
            args[i] = (Integer) args[i] + 1;
        }
    }
    System.out.println("所有参数+1");
    Object result = joinPoint.proceed(args);
    return result;
}

然后在@Before中输出其属性值:

@Before("pointcut()")
public void beforeAdvice(JoinPoint joinPoint) throws Throwable {
    // 接收到请求,记录请求内容
    ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
    HttpServletRequest request = attributes.getRequest();
    // 方法签名
    MethodSignature signature = (MethodSignature) joinPoint.getSignature();
    Method method = signature.getMethod();

    // 记录下请求内容
    log.info("请求类型:" + request.getMethod()); // GET/POST等
    log.info("请求URL:" + request.getRequestURL());
    log.info("请求IP: " + request.getRemoteAddr());
    log.info("包名.类名: " + signature.getDeclaringTypeName());
    log.info("方法名: " + signature.getName());
    log.info("方法名: " + method.getName());

    // 参数名+参数值
    Enumeration eParameterNames = request.getParameterNames();
    while (eParameterNames.hasMoreElements()) {
        String name = (String) eParameterNames.nextElement();
        String value = request.getParameter(name);
        // 参数值为request中的原始参数值
        log.info("参数名:" + name + ", 原始参数值:" + value);
    }

    // 参数名+参数值
    LocalVariableTableParameterNameDiscoverer u = new LocalVariableTableParameterNameDiscoverer();
    String[] sParamNames = u.getParameterNames(method);
    Object[] args = joinPoint.getArgs();
    for (int i = 0; i < args.length; i++) {
        // 请求参数类型判断过滤,特殊类型不输出
        if (args[i] instanceof HttpServletRequest || args[i] instanceof HttpServletResponse || args[i] instanceof MultipartFile) {
            continue;
        }
        // 参数值为传入的args参数值,可能已经被修改过
        log.info("参数名:" + sParamNames[i] + ", 传入的args参数值 :" + JSON.toJSONString(args[i]));
    }
}

请求接口,可看到控制台输出:

所有参数+1
请求类型:POST
请求URL:http://127.0.0.1:8080/test/myTest
请求IP: 127.0.0.1
包名.类名: com.example.test.controller.TestController
方法名: myTest
方法名: myTest
参数名:v1, 原始参数值:1
参数名:v2, 原始参数值:2
参数名:v1, 传入的args参数值 :2
参数名:v2, 传入的args参数值 :3
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值