Zer0e's Blog

浅谈Spring AOP

字数统计: 2.7k阅读时长: 10 min
2020/08/10 Share

前言

上篇提到Spring AOP中使用了代理模式。其实spring aop很早我就想写文章,因为它和ioc属于spring中最重要的两个概念之一。那这篇就好好讲讲spring aop。

正文

什么是AOP

AOP(Aspect Oriented Programming),中文翻译为面向切面编程,那啥是面向切面编程?我只听过面向对象编程啊?
其实AOP是OOP的延续,并不能完全替代OOP,他们的理念不一样,OOP是对对象进行封装,而AOP则是对某个业务进行切面提取,侧重点并不一样。
我们在编程时,如果有代码的重复,我们一般会将代码封装成对象或者方法,再去调用,这种抽取被称为纵向抽取,但当某个部分的逻辑分散在各个业务中时,我们往往无法进行纵向抽取,而AOP的目的就是将各个业务中相同的代码通过切割的方式抽取到一个独立逻辑中。然后将切割出来的代码再融入到其他业务中,完成与之前相同的功能。

动态代理

按上面所讲,我们需要切割逻辑,并且完成与之前一样的功能,那我们就可以使用到之前提到的代理模式,通过增强对象,来实现对访问对象的改造或者增强,即在中间层加入相应逻辑。
要说aop原理,就不得不提代理,代理分为静态代理与动态代理,静态代理就是自己编写代理层,自己拦截需要拦截的方法并增强,可以见上一篇文章。
而spring aop的底层是动态代理。
那啥是动态代理?我们先看以下例子:
如果我们要实现一个接口,那一般情况下我们会编写实现类:

1
2
3
4
5
6
7
8
9
10
public interface Hello {
public void areYouOk();
}

public class HelloImpl implements Hello{
@Override
public void areYouOk() {
System.out.println("Are you ok?");
}
}

然后将HelloImpl实例化,调用。
那有没有可能我们不编写实现类,就直接在运行时创建接口的实例呢?答案是有,并且在Java标准库中提供了一种动态代理的机制,只需要预先知道接口,就可以在运行时动态生成实现类。而实现的方法就是reflect包中的Proxy.newProxyInstance()方法。
reflect包,诶?不就是反射操作的包吗?没错,动态代理也属于反射的一种。我们来看看怎么动态生成实现类。
首先newProxyInstance方法需要三个参数,第一个参数是类加载器ClassLoader,官方解释是loader the class loader to define the proxy class,其实就是定义由哪个classloader对生成的代理类进行加载。第二个参数是一个interface对象数组,也就是要我们需要实现哪些接口。第三个参数,一个InvocationHandler对象,代理的核心参数,当执行代理生成实现类时,会调用这个对象的invoke方法。可以在这个方法中处理逻辑。写一下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class HelloMain {
public static void main(String[] args) {
InvocationHandler handler = new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (method.getName().equals("areYouOk")){
System.out.println("Are you ok?");
}
return null;
}
};
Hello hello = (Hello) Proxy.newProxyInstance(Hello.class.getClassLoader(),
new Class[]{Hello.class},
handler);
hello.areYouOk();
}
}

其中要简单说明下,invoke方法三个参数,第一参数是代理对象,第二个是调用方法,第三个是传入的参数。要注意在invoke中不能调用proxy参数,否则会导致永久递归,栈溢出。
动态代理本质上就是JDK在运行时动态创建class字节码并加载,这和之前谈过的反射一致。它只是帮我们编写了一个实现类,仅此而已。

JDK Proxy 与 CGLib Proxy

在Java中,动态代理有两种,一种是JDK动态代理,另一种是CGlib动态代理。
JDK动态代理就是上文所讲的,需要实现一个类所使用的代理。而如果类没有实现接口,那就会使用CGLib代理。JDK动态代理本质上就是反射,但必须需要实现的接口。而CGLib动态代理是基于ASM机制,通过生成子类来作为代理类。
JDK动态代理的缺点就是只能基于接口。
CGLib是采用十分底层的字节码技术,原理是创建子类,拦截父类中的方法来实现代理,优点就是没有接口的类也能实现动态代理,而缺点则是实现相对较难。

小结

动态代理相对于静态代理,是通过动态生成代理类来实现代理技术,拦截并增强方法,将原本相同逻辑的代码抽取出来,并注入到目标对象,实现与原来一致的功能。动态代理分为JDK动态代理与CGLib动态代理,Spring AOP中默认使用JDK动态代理,当类没有接口时,才使用CGLib代理。

Spring AOP

部分概念

上文简单讲了动态代理的概念,那既然知道Spring AOP是使用动态代理了,那啥是Spring AOP呢?
首先我们需要了解几个AOP中常见的名词。

  • 连接点(Join Point): 即需要拦截的地方。
  • 切点(Poincut): 即具体定位的连接点。
  • 增强/通知(Advice): 表示需要添加到切点的逻辑,Spring AOP中有多种类型。
  • 切面(Aspect): 切面由切点和增强/通知组成,包括横切逻辑与连接点的定义。

具体实现

了解了上述的概念之后,我们继续讲怎么实现AOP。如今实现AOP已经不再使用手动实现接口的方法,而是使用更加简洁方便的@Aspect注解方式。
我们来简单实现一个记录日志的功能。来看看如何使用AOP。
我们需要先知道Spring中Advice的注解有哪些:


注解 说明
@Before 前置通知,在连接点方法前调用
@Around 环绕通知,它将覆盖原有方法,但是允许你通过反射调用原有方法
@After 后置通知,在连接点方法后调用
@AfterReturning 返回通知,在连接点方法执行并正常返回后调用,要求连接点方法在执行过程中没有发生异常
@AfterThrowing 异常通知,当连接点方法异常时调用

这里Before和AfterThrowing很好理解,一个是方法调用之前,另一个是异常之后。而After和AfterReturning的区别在于After是无论是否发生异常都会触发,而AfterReturning只在正常返回后才会触发。至于Around,可以理解成Before和After结合,可以决定在目标的什么时候执行。或者直接阻止运行,功能十分强大。
接下来我们来看看具体实现
新建一个springboot项目,并写下一个controller:

1
2
3
4
5
6
7
8
9
@RestController
public class helloController {

@RequestMapping("/hello")
public String hello(@RequestParam String name){
return "hello " + name;
}

}

接下来我们先定义切点,spring中使用execution中的正则表达式来判断具体要拦截的类和方法,可以在上面这些注解里直接定义拦截的方法,例如:

1
2
@Before("execution(public * com.example.aop_test.helloController.*(..))")

首先*表示任意返回值,接着跟类的全限定名,之后*(..)表示返回值为任意的全部方法,等价于*(*)。
但是这么写的话需要每个advice注解中都需要这么长的定义,我们可以通过对一个方法定义@Pointcut注解来避免书写多次execution。

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
@Aspect
@Component
public class WebLogAspect {
private Logger logger = Logger.getLogger(getClass());

@Pointcut("execution(public * com.example.aop_test.helloController.*(..))")
public void webLog(){}

@Before("webLog()")
public void doBefore(JoinPoint joinPoint) throws Throwable{
logger.info(joinPoint.toString());
logger.info("before");
}


@AfterReturning(returning = "ret",pointcut = "webLog()")
public void doAfterReturning(Object ret) throws Throwable{
logger.info("RESPONSE : " + ret);
logger.info("after returning");
}

@After("webLog()")
public void doAfter(JoinPoint joinPoint) throws Throwable{
logger.info("after ");
}
}

@AfterReturning注解中可以定义returning参数,来获取切点的返回值。这里没什么问题,但如果我在controller中加入一段:

1
2
3
4
5
6
7
8
@RequestMapping("/hello")
public String hello(@RequestParam String name){
if (name.equals("123")){
throw new RuntimeException("ex");
}

return "hello " + name;
}

那这时候只有before和after生效,因为没有正常返回,所以afterreturning没有执行。
再来看看环绕通知@Around,这是spring aop中最强大的通知,这个方法传入一个ProceedingJoinPoint对象,在执行joinPoint.proceed()之前就是before通知,出现异常就是afterThrowing,之后就是after通知:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Around("webLog()")
public Object doAround(ProceedingJoinPoint joinPoint){
Object res = null;
logger.info("before");
try{
res = joinPoint.proceed();
}catch (Throwable throwable){
logger.info("ex");
}

logger.info("after");
return (res != null)?res : "other";
}

当然,如果不执行proceed方法就那就相当于阻止了当前方法。所以我们可以通过这个点来做权限管理。
我们先设置切点只在hello方法执行:

1
2
@Pointcut("execution(public * com.example.aop_test.helloController.hello(..))")
public void webLog(){}

hello方法不变,创建一个新的方法来设置cookie

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@RequestMapping("/hello")
public String hello(@RequestParam String name){

return "hello " + name;
}

@RequestMapping("/set")
public String setCookie(HttpServletResponse response, @RequestParam String name){
if (name == null){
name = "";
}
Cookie cookie = new Cookie("name",name);
response.addCookie(cookie);
return "success";
}

接着是@Around方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Around("webLog()")
public Object doAround(ProceedingJoinPoint joinPoint){
Object res = null;
ServletRequestAttributes attributes = (ServletRequestAttributes)RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
Cookie[] cookies = request.getCookies();
Map<String,Cookie> map = new HashMap<>();
for (int i = 0;i<cookies.length;i++){
map.put(cookies[i].getName(),cookies[i]);
}
if (map.containsKey("name")){
String cookieName = map.get("name").getValue();
if (cookieName.equals("admin")){
try {
res = joinPoint.proceed();
return res;
}catch (Throwable throwable){
return "error";
}
}
}
return "not admin";
}

这里通过RequestContextHolder来获取request,并获取所有cookie存放在map中,通过对比cookie值是否为admin来判断是否执行controller方法,做到权限管理。当然,项目中的比较需要去redis对比token值是否失效,然后对比权限才能真正做到登录与权限管理。这里只是写个思路而已。

总结

讲到这里,差不多把spring aop基础讲完了,从代理模式一直讲到aop,再到如何使用,一路下来我也复习了好多知识,包括动态代理与spring aop,这里有一点,spring aop只是帮我们实现代理模式而已,自动添加中间层来对访问对象的控制,开发中是十分好用的。

CATALOG
  1. 1. 前言
  2. 2. 正文
    1. 2.1. 什么是AOP
    2. 2.2. 动态代理
      1. 2.2.1. JDK Proxy 与 CGLib Proxy
      2. 2.2.2. 小结
    3. 2.3. Spring AOP
      1. 2.3.1. 部分概念
      2. 2.3.2. 具体实现
  3. 3. 总结