echo

任生命穿梭 时间的角落

0%

Aspect Spring AOP

基于注解的 Spring AOP 开发

基本概念

img

定义切入点函数

1
2
3
4
5
//在执行完 UserDao.add() 方法后执行
@After(value="execution(* com.zejian.spring.springAop.dao.UserDao.addUser(..))")
public void after(){
System.out.println("最终通知....");
}

还可采用 @PointCut 关键字定义切入点表达式:

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 使用Pointcut定义切点
*/
@Pointcut("execution(* com.zejian.spring.springAop.dao.UserDao.addUser(..))")
private void myPointcut(){}

/**
* 应用切入点函数
*/
@After(value="myPointcut()")
public void afterDemo(){
System.out.println("最终通知....");
}

切入点指示符

通配符

在定义匹配表达式时,通配符几乎随处可见如 * , .. , + ,它们的含义如下:

  • ..:匹配方法定义中任意数量的参数,此外还匹配类定义中任意数量包
1
2
3
4
//任意返回值,任意名称,任意参数的公共方法
execution(public * *(..))
//匹配com.zejian.dao包及其子包中所有类中的所有方法
within(com.zejian.dao..*)
  • +:匹配给定类的任意子类
1
2
//匹配实现了DaoUser接口的所有子类的方法
within(com.zejian.dao.DaoUser+)
  • *:匹配任意数量的字符
1
2
3
4
//匹配com.zejian.service包及其子包中所有类的所有方法
within(com.zejian.service..*)
//匹配以set开头,参数为int类型,任意返回值的方法
execution(* set*(int))
类型签名表达式

为了方便类型(如接口、类名、包名)过滤方法,Spring AOP 提供了within关键字。其语法格式如下:

1
within(<type name>)

type name 则使用包名或者类名替换即可。

1
2
3
4
5
6
7
8
9
10
11
//匹配com.zejian.dao包及其子包中所有类中的所有方法
@Pointcut("within(com.zejian.dao..*)")

//匹配UserDaoImpl类中所有方法
@Pointcut("within(com.zejian.dao.UserDaoImpl)")

//匹配UserDaoImpl类及其子类中所有方法
@Pointcut("within(com.zejian.dao.UserDaoImpl+)")

//匹配所有实现UserDao接口的类的所有方法
@Pointcut("within(com.zejian.dao.UserDao+)")
方法签名表达式

如果想根据方法签名进行过滤,关键字 execution 可以帮到我们,语法表达式如下

1
2
3
4
5
//scope :方法作用域,如public,private,protect
//returnt-type:方法返回值类型
//fully-qualified-class-name:方法所在类的完全限定名称
//parameters 方法参数
execution(<scope> <return-type> <fully-qualified-class-name>.*(parameters))

对于给定的作用域、返回值类型、完全限定类名以及参数匹配的方法将会应用切点函数指定的通知,这里给出模型案例:

1
2
3
4
5
6
7
8
9
10
11
//匹配UserDaoImpl类中的所有方法
@Pointcut("execution(* com.zejian.dao.UserDaoImpl.*(..))")

//匹配UserDaoImpl类中的所有公共的方法
@Pointcut("execution(public * com.zejian.dao.UserDaoImpl.*(..))")

//匹配UserDaoImpl类中的所有公共方法并且返回值为int类型
@Pointcut("execution(public int com.zejian.dao.UserDaoImpl.*(..))")

//匹配UserDaoImpl类中第一个参数为int类型的所有公共的方法
@Pointcut("execution(public * com.zejian.dao.UserDaoImpl.*(int , ..))")
其他指示符
  • this:用于匹配当前 AOP 代理对象类型的执行方法;请注意是 AOP代理对象的类型匹配,这样就可能包括引入接口也类型匹配

    1
    2
    3
    //匹配了任意实现了UserDao接口的代理对象的方法进行过滤
    @Pointcut("this(com.zejian.spring.springAop.dao.UserDao)")
    private void myPointcut2(){}
  • bean:Spring AOP 扩展的,AspectJ没有对应指示符,用于匹配特定名称的 Bean 对象的执行方法

    1
    2
    3
    //匹配名称中带有后缀Service的Bean。
    @Pointcut("bean(*Service)")
    private void myPointcut1(){}
  • target:用于匹配当前目标对象类型的执行方法

    1
    2
    3
    //匹配了任意实现了UserDao接口的目标对象的方法进行过滤
    @Pointcut("target(com.zejian.spring.springAop.dao.UserDao)")
    private void myPointcut3(){}
  • @within:用于匹配所有持有指定注解类型内的方法;请注意与 within 是有区别的,within是用于匹配指定类型内的方法执行;

    1
    2
    3
    //匹配使用了MarkerAnnotation注解的类(注意是类)
    @Pointcut("@within(com.zejian.spring.annotation.MarkerAnnotation)")
    private void myPointcut4(){}
  • @annotation(com.zejian.spring.MarkerMethodAnnotation) : 根据所应用的注解进行方法过滤

    1
    2
    3
    //匹配使用了MarkerAnnotation注解的方法(注意是方法)
    @Pointcut("@annotation(com.zejian.spring.annotation.MarkerAnnotation)")
    private void myPointcut5(){}

切点指示符可以使用运算符语法进行表达式的混编,如and、or、not(或者&&、||、!),如下一个简单例子:

1
2
3
4
5
6
7
//匹配了任意实现了UserDao接口的目标对象的方法并且该接口不在com.zejian.dao包及其子包下
@Pointcut("target(com.zejian.spring.springAop.dao.UserDao) !within(com.zejian.dao..*)")
private void myPointcut6(){}

//匹配了任意实现了UserDao接口的目标对象的方法并且该方法名称为addUser
@Pointcut("target(com.zejian.spring.springAop.dao.UserDao)&&execution(* com.zejian.spring.springAop.dao.UserDao.addUser(..))")
private void myPointcut7(){}
5 种通知函数

通知主要分5种类型,分别是前置通知、后置通知、异常通知、最终通知以及环绕通知,下面分别介绍。

  • 前置通知 @Before

    前置通知通过 @Before 注解进行标注,并可直接传入切点表达式的值,该通知在目标函数执行前执行,JoinPoint 是 Spring 提供的静态变量,通过它,可以获取目标对象的信息,如类名称,方法参数,方法名等,该参数可选。

    1
    2
    3
    4
    5
    6
    7
    8
    /**
    * 前置通知
    * @param joinPoint 该参数可以获取目标对象的信息,如类名称,方法参数,方法名称等
    */
    @Before("execution(* com.zejian.spring.springAop.dao.UserDao.addUser(..))")
    public void before(JoinPoint joinPoint){
    System.out.println("我是前置通知");
    }
  • 后置通知 @AfterReturning

    通过 @AfterReturning 标注,该函数在目标函数执行完成后执行,并可以获取到目标函数最终的返回值 returnVal,当目标函数没有返回值时,returnVal 将返回 null。

    必须通过returning = “returnVal”注明参数的名称而且必须与通知函数的参数名称相同。

    1
    2
    3
    4
    5
    6
    7
    /**
    * 后置通知,不需要参数时可以不提供
    */
    @AfterReturning(value="execution(* com.zejian.spring.springAop.dao.UserDao.*User(..))")
    public void AfterReturning(){
    System.out.println("我是后置通知...");
    }
    1
    2
    3
    4
    5
    6
    7
    8
    /**
    * 后置通知
    * returnVal,切点方法执行后的返回值
    */
    @AfterReturning(value="execution(* com.zejian.spring.springAop.dao.UserDao.*User(..))",returning = "returnVal")
    public void AfterReturning(JoinPoint joinPoint,Object returnVal){
    System.out.println("我是后置通知...returnVal+"+returnVal);
    }
  • 异常通知 @AfterThrowing

    该通知只有在异常时才会被出发,并由 throwing 来声明一个接受异常信息的变量,同样异常通知也用于 Joinpoing 参数,需要时可以加上。

    1
    2
    3
    4
    5
    6
    7
    8
    /**
    * 抛出通知
    * @param e 抛出异常的信息
    */
    @AfterThrowing(value="execution(* com.zejian.spring.springAop.dao.UserDao.addUser(..))",throwing = "e")
    public void afterThrowable(Throwable e){
    System.out.println("出现异常:msg="+e.getMessage());
    }
  • 最终通知 @After

    该通知有点类似于 finally 代码块,只要应用了,无论什么情况下都会执行。

    1
    2
    3
    4
    5
    6
    7
    8
    /**
    * 无论什么情况下都会执行的方法
    * joinPoint 参数
    */
    @After("execution(* com.zejian.spring.springAop.dao.UserDao.*User(..))")
    public void after(JoinPoint joinPoint) {
    System.out.println("最终通知....");
    }
  • 环绕通知 @Around

    环绕通知既可在目标方法前执行也可以在目标方法后执行,更重要的是环绕通知可以控制目标方法是否指向执行,但即使如此,我们应该尽量以最简单的方式满足需求,在仅需目标方法前执行时,使用前置通知而非环绕通知。

    第一个参数必须是 ProceedingJoinPoint,通过该对象的 proceed() 方法来执行目标函数,proceed() 的返回值就是环绕通知的返回值。

    1
    2
    3
    4
    5
    6
    7
    8
    @Around("execution(* com.zejian.spring.springAop.dao.UserDao.*User(..))")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
    System.out.println("我是环绕通知前....");
    //执行目标函数
    Object obj= (Object) joinPoint.proceed();
    System.out.println("我是环绕通知后....");
    return obj;
    }
通知传递参数

在 Spring AOP 中,除了 execution 和 bean 指示符不能传递参数给通知方法,其他指示符都可以将匹配的方法相应参数或对象自动传递给通知方法。获取到匹配的方法参数后通过 argNames属性指定参数名。如下,args(param)、argNames=”param”、before(int param)这三个参数命名必须保持一致

1
2
3
4
@Before(value="args(param)", argNames="param") //明确指定了    
public void before(int param) {
System.out.println("param:" + param);
}

也可直接使用 args 指示符不带 argNames 声明参数,如下:

1
2
3
4
5
@Before("execution(public * com.zejian..*.addUser(..)) && args(userId,..)")  
public void before(int userId) {
//调用addUser的方法时如果与addUser的参数匹配则会传递进来会传递进来
System.out.println("userId:" + userId);
}

args(userId,..)表达式表示:只匹配那些至少接收一个参数而且传入的类型必须与userId一致的方法,传递的参数可以简单类型或者对象,只有参数和目标方法也匹配时才有会有值传递进来。

Aspect 优先级

如果有多个通知需要在同一个切点函数指定的过滤目标方法上执行,在所有前置通知函数中,优先级最高的通知函数将会被先执行,在所有后置通知函数中,优先级最高的通知函数将会最后执行。

新建名为 aspectdemo 的工程,首先引入核心依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>

<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
</dependency>

添加启动类

1
2
3
4
5
6
7
8
@SpringBootApplication
public class AspectdemoApplication {

public static void main(String[] args) {
SpringApplication.run(AspectdemoApplication.class, args);
}

}

开启 AspectJAutoProxy ,添加配置类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.hoo.aspectdemo.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

/**
* 在配置文件中启动@Aspect支持后,
* Spring容器只会尝试自动识别带@Aspect的Bean,
* 前提是任何定义的切面类都必须已在配置文件以Bean的形式声明。
* */
@Configuration
@EnableAspectJAutoProxy
public class AspectJConfig {
}

添加测试用 Controller

1
2
3
4
5
6
7
@RestController
public class TestController {
@GetMapping("/order")
public String testOrder(){
return "success";
}
}

首先添加优先级较高的切面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package com.hoo.aspectdemo.aspect;

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.springframework.core.Ordered;
import org.springframework.stereotype.Component;
/*
切面实现 org.springframework.core.Ordered 接口,该接口用于控制切面类的优先级,同时重写getOrder方法,定制返回值,返回值(int 类型)越小优先级越大
*/
@Aspect
@Component
public class AspectOrderZero implements Ordered {
@Override
public int getOrder() {
return 0;
}

@Pointcut("execution(* com.hoo.aspectdemo.controller.TestController.testOrder())")
public void pointCut(){

}

@Before("pointCut()")
public void beforeOne(){
System.out.println("前置通知 ..优先级 0 .. 执行顺序 1");
}

@Before("pointCut()")
public void beforeTwo(){
System.out.println("前置通知 ..优先级 0 .. 执行顺序 2");
}

@AfterReturning("pointCut()")
public void afterReturningThree(){
System.out.println("后置通知 ..优先级 0 .. 执行顺序 3");
}

@AfterReturning("pointCut()")
public void afterReturningFour(){
System.out.println("后置通知 ..优先级 0 .. 执行顺序 4");
}
}

然后添加优先级较低的切面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package com.hoo.aspectdemo.aspect;

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.springframework.core.Ordered;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class AspectOrderTwo implements Ordered {
@Override
public int getOrder() {
return 2;
}

@Pointcut("execution(* com.hoo.aspectdemo.controller.TestController.testOrder())")
public void pointCut(){

}

@Before("pointCut()")
public void beforeOne(){
System.out.println("前置通知 ..优先级 2 .. 执行顺序 1");
}

@Before("pointCut()")
public void beforeTwo(){
System.out.println("前置通知 ..优先级 2 .. 执行顺序 2");
}

@AfterReturning("pointCut()")
public void afterReturningThree(){
System.out.println("后置通知 ..优先级 2 .. 执行顺序 3");
}

@AfterReturning("pointCut()")
public void afterReturningFour(){
System.out.println("后置通知 ..优先级 2 .. 执行顺序 4");
}
}

运行,即可在控制台看到如下信息:

1
2
3
4
5
6
7
8
前置通知 ..优先级 0 .. 执行顺序 1
前置通知 ..优先级 0 .. 执行顺序 2
前置通知 ..优先级 2 .. 执行顺序 1
前置通知 ..优先级 2 .. 执行顺序 2
后置通知 ..优先级 2 .. 执行顺序 3
后置通知 ..优先级 2 .. 执行顺序 4
后置通知 ..优先级 0 .. 执行顺序 3
后置通知 ..优先级 0 .. 执行顺序 4

总结:在同一个切面中定义多个通知响应同一个切点函数,执行顺序为声明顺序;如果在不同的切面中定义多个通知响应同一个切点,进入时则优先级高的切面类中的通知函数优先执行,退出时则最后执行。

Spring AOP 简单应用场景

  1. 性能监控

首先我们定义用于测试的controller,并模拟这个接口需要 5s 来执行操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.hoo.aspectdemo.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class TestController2 {

@GetMapping("/monitor")
public String testMonitor() throws InterruptedException {
Thread.sleep(5);
return "success";
}
}

然后定义性能监控信息类 MonitorTime

1
2
3
4
5
6
7
8
9
10
11
12
package com.hoo.aspectdemo.aspect;

import java.util.Date;

public class MonitorTime {

private String className;
private String methodName;
private Date logTime;
private long consumeTime;
// 省略 getter 和 setter
}

然后定义一个监控的切面类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package com.hoo.aspectdemo.aspect;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

import java.util.Date;

@Aspect
@Component
public class TimerAspect {

@Pointcut("within(com.hoo.aspectdemo.controller..*)")
public void pointCut(){

}

@Around("pointCut()")
public Object logTimer(ProceedingJoinPoint joinPoint) throws Throwable {
MonitorTime monitor = new MonitorTime();
//获取目标类名称
String className = joinPoint.getTarget().getClass().getName();
//获取目标类方法名称
String methodName = joinPoint.getSignature().getName();

monitor.setClassName(className);
monitor.setMethodName(methodName);
monitor.setLogTime(new Date());

//计时并调用目标函数
long start = System.currentTimeMillis();
Object result = joinPoint.proceed();
long time = System.currentTimeMillis() - start;
monitor.setConsumeTime(time);

//把 MonitorTime 记录的信息上传给监控系统,这里没有实现

System.out.println(monitor);

return result;
}
}

启动程序,访问http://localhost:8080/monitor,查看控制台,打印出如下信息:

1
MonitorTime{className='com.hoo.aspectdemo.controller.TestController2', methodName='testMonitor', logTime=Mon Apr 12 17:08:53 CST 2021, consumeTime=11}
  1. 异常监控

首先定义异常信息类

1
2
3
4
5
6
7
8
9
10
11
12
package com.hoo.aspectdemo.aspect;

import java.util.Date;

public class ExceptionInfo {

private String className;
private String methodName;
private Date logTime;//异常记录时间
private String message;//异常信息
// 省略 getter 和 setter
}

定义自己的异常类

1
2
3
4
5
6
7
8
package com.hoo.aspectdemo.exception;

public class MyException extends Exception{

public MyException(String message) {
super(message);
}
}

在 TestController2 中添加测试函数

1
2
3
4
5
6
7
8
@GetMapping("/monitor/{mode}")
public String testMonitor2(@PathVariable Integer mode) throws MyException {
if(mode == 0){
return "success";
}else{
throw new MyException(" testMonitor() 方法出错了!");
}
}

然后定义异常处理的切面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package com.hoo.aspectdemo.aspect;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import java.util.Date;

@Aspect
@Component
public class ExceptionMonitorAspect {

@Pointcut("within(com.hoo.aspectdemo.controller..*)")
void exceptionMethod(){
}

@Around("exceptionMethod()")
public Object monitorMethods(ProceedingJoinPoint joinPoint){
try {
return joinPoint.proceed();
} catch (Throwable e) {
ExceptionInfo info = new ExceptionInfo();
info.setClassName(joinPoint.getTarget().getClass().getName());
info.setMethodName(joinPoint.getSignature().getName());
info.setLogTime(new Date());
info.setMessage(e.getMessage());

System.out.println(info);
//上传日志系统,这里省略
//返回失败信息
return e.getMessage();
}
}
}

浏览器访问 http://localhost:8080/monitor/0 ,可以看到返回 ”success“信息,并在控制台可以看到访问时间日志:

1
MonitorTime{className='com.hoo.aspectdemo.controller.TestController2', methodName='testMonitor2', logTime=Mon Apr 12 21:46:30 CST 2021, consumeTime=11}

浏览器访问 http://localhost:8080/monitor/1 ,可以看到返回 ”testMonitor() 方法出错了!“信息,并在控制台可以看到访问时间日志:

1
ExceptionInfo{className='com.hoo.aspectdemo.controller.TestController2', methodName='testMonitor2', logTime=Mon Apr 12 21:47:15 CST 2021, message=' testMonitor() 方法出错了!'}

Spring AOP 实现原理概要

对于织入过程,一般分为动态织入和静态织入,动态织入在运行时动态地将要增强的代码织入到目标类中,这往往是通过动态代理技术完成的,如 Java JDK 的动态代理(底层通过反射实现)或者 CGLIB 的动态代理(底层通过继承实现)。

AspectJ 采用静态织入的方式。它在编译器使用 acj 编译器把 aspect 类编译成 class 字节码后,在 java 目标类编译时织入,先编译 aspect 类再编译目标类。

image-20210414190852904

JDK 动态代理

首先看一个简单的例子,声明一个 ExInterface 接口,利用 JDK 动态代理技术在 execute() 方法前后加入权限验证和日志记录。

1
2
3
4
//自定义的接口类,JDK 动态代理的实现必须要有对应的接口类
public interface ExInterface {
void execute();
}
1
2
3
4
5
6
7
//A类,实现了ExInterface接口类
public class A implements ExInterface {
@Override
public void execute() {
System.out.println("---- A.execute() ----");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
//代理类的实现
public class JDKProxy implements InvocationHandler {
/**
* 要被代理的目标对象
*/
private A target;

public JDKProxy(A target) {
this.target = target;
}

/*
* 创建代理类
*/
public ExInterface createProxy(){
return (ExInterface) Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(),this);
}

/**
* 调用被代理类(目标对象)的任意方法都会触发invoke方法
* @param proxy 代理类
* @param method 被代理类的方法
* @param args 被代理类的方法参数
* @return
* @throws Throwable
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//过滤不需要该业务的方法
if("execute".equals(method.getName())) {
//调用前验证权限
//AuthCheck.authCheck();
System.out.println("---- 鉴权 ----");
//调用目标对象的方法
Object result = method.invoke(target, args);
//记录日志数据
//Report.recordLog();
System.out.println("---- 记录日志 ----");
return result;
}else if("delete".equals(method.getName())){
//.....
}
//如果不需要增强直接执行原方法
return method.invoke(target,args);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
//测试类
class Test{
public static void main(String[] args) {
A a = new A();
//创建 JDK 代理
JDKProxy jdkProxy = new JDKProxy(a);
//创建代理对象
ExInterface proxy = jdkProxy.createProxy();
//执行代理对象方法
proxy.execute();
}
}
1
2
3
4
//测试结果
---- 鉴权 ----
---- A.execute() ----
---- 记录日志 ----

在 A 的 execute() 方法中没有调用任何权限和日志的 代码,也没有直接操作 a 对象,只是调用了 proxy 代理对象的方法,最终的结果却是预期的,这就是动态代理技术。动态代理的底层通过反射来实现,只要拿到 A 类的 class 文件和 A 类的实现接口,很自然就可以生成相同接口的代理类并调用 a 对象的方法。

实现 Java 动态代理是先决条件为:目标对象必须带接口,如果类的接口是 ExInterface,通过该接口,动态代理可以创建与 A 类类型相同的代理对象。

用JDK动态代理,被代理类(目标对象,如A类),必须已有实现接口如(ExInterface),因为JDK提供的Proxy类将通过目标对象的类加载器ClassLoader和Interface,以及句柄(Callback)创建与A类拥有相同接口的代理对象proxy,该代理对象将拥有接口ExInterface中的所有方法。同时,代理类必须实现一个类似回调函数的InvocationHandler接口并重写该接口中的invoke方法,当调用proxy的每个方法(如案例中的proxy#execute())时,invoke方法将被调用,利用该特性,可以在invoke方法中对目标对象(被代理对象如A)方法执行的前后动态添加其他外围业务操作,此时无需触及目标对象的任何代码,也就实现了外围业务的操作与目标对象(被代理对象如A)完全解耦合的目的。当然缺点也很明显需要拥有接口,这也就有了后来的CGLIB动态代理。

CGLIB 动态代理

通过CGLIB动态代理实现上述功能并不要求目标对象拥有接口类,实际上CGLIB动态代理是通过继承的方式实现的,因此可以减少没必要的接口,下面直接通过简单案例协助理解。

首先引入 asm-8.0.1.jarcglib-3.3.0.jar,也可使用 maven 下载。

1
2
3
4
5
6
public class A {
//被代理的类(目标对象)
public void execute(){
System.out.println("---- A.execute() ----");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62

import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;

public class CGLibProxy implements MethodInterceptor {
/**
* 被代理的目标类
* */
private A target;

public CGLibProxy(A target) {
super();
this.target = target;
}

/**
* 创建代理对象
* */
public A createProxy(){
//使用 CGLIB 生成代理
//1.生成增强类实例,用于生产代理类
Enhancer enhancer = new Enhancer();
//2.设置被代理类字节码,CGLIB 根据字节码生成被代理类的子类
enhancer.setSuperclass(target.getClass());
//3. 设置回调函数,即一个方法拦截
enhancer.setCallback(this);
//4.创建代理
return (A) enhancer.create();
}
/**
* 回调函数
* @param obj 代理对象
* @param method 委托类方法
* @param args 方法参数
* @param proxy 每个被代理的方法都对应一个MethodProxy对象,
* methodProxy.invokeSuper方法最终调用委托类(目标类)的原始方法
* @return
* @throws Throwable
*/
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
//过滤不需要该业务的方法
if("execute".equals(method.getName())){
//调用前验证权限
System.out.println("---- 鉴权 ----");

//调用目标对象的方法(执行 A 独享即被代理对象的 execute 方法)
Object result = proxy.invokeSuper(obj,args);

//记录日志数据(动态添加其他要执行业务)
System.out.println("---- 日志 ----");

return result;
}else if("delete".equals(method.getName())){
return proxy.invokeSuper(obj, args);
}
//不需要增强直接执行原方法
return proxy.invokeSuper(obj,args);
}
}
1
2
3
4
5
6
7
8
9
10
11
public class CGLibTest {
public static void main(String[] args) {
A a = new A();

CGLibProxy cgLibProxy = new CGLibProxy(a);

A proxy = cgLibProxy.createProxy();

proxy.execute();
}
}
1
2
3
4
//测试结果
---- 鉴权 ----
---- A.execute() ----
---- 日志 ----

被代理的类无需接口即可实现动态代理,而CGLibProxy代理类需要实现一个方法拦截器接口MethodInterceptor并重写intercept方法,类似JDK动态代理的InvocationHandler接口,也是理解为回调函数,同理每次调用代理对象的方法时,intercept方法都会被调用,利用该方法便可以在运行时对方法执行前后进行动态增强。关于代理对象创建则通过Enhancer类来设置的,Enhancer是一个用于产生代理对象的类,作用类似JDK的Proxy类,因为CGLib底层是通过继承实现的动态代理,因此需要传递目标对象(如A)的Class,同时需要设置一个回调函数对调用方法进行拦截并进行相应处理,最后通过create()创建目标对象(如A)的代理对象,运行结果与前面的JDK动态代理效果相同。

总结

Spring AOP内部已都实现了这两种技术,Spring AOP 在使用时机上也进行自动化调整,当有接口时会自动选择JDK动态代理技术,如果没有则选择CGLIB技术,当然Spring AOP的底层实现并没有这么简单,为更简便生成代理对象,Spring AOP 内部实现了一个专注于生成代理对象的工厂类,这样就避免了大量的手动编码,这点也是十分人性化的,但最核心的还是动态代理技术。从性能上来说,Spring AOP 虽然无需特殊编译器协助,但性能上并不优于AspectJ的静态织入。img

参考