1. AOP

面向切面编程,Spring的两大核心概念之一,面试必问。

1.1 如何理解AOP

理解AOP,就要先理解面向切面编程的思想

看图:

image-20210901195828051

说明:

  • 圆代表应用程序的完整功能
  • 如果想要对原有的功能做增强,也就新增一些逻辑,那么势必会改变原有代码,表现在图上,就代表圆就要被改变,也就是圆就不是圆了,破坏了整体的结构
  • 这时候 引入了一条线,这条线就是添加的新功能
  • 和圆相交的面,就是原有的功能,也就是在不改变圆结构的情况下,对原有的功能做了增强
  • 相交的面 叫做切面
  • 这就是面向切面编程思想的来源

在不改变原有功能的基础上,对功能进行增强

1.2 对比OOP思想和AOP思想

OOP:面向对象编程

比如一个业务组件BookService,它有几个业务方法:

  • createBook:添加新的Book;
  • updateBook:修改Book;
  • deleteBook:删除Book。

每一个业务,都需要进行参数检查日志记录事务处理等操作。

代码类似这样:

public class BookService {
    public void createBook(Book book) {
        //检查参数
        checkParam(book);
        Transaction tx = startTransaction();
        try {
            // 核心业务逻辑
            tx.commit();
        } catch (RuntimeException e) {
            tx.rollback();
            throw e;
        }
        log.info("created book: " + book);
    }
     public void updateBook(Book book) {
        checkParam(book);
        Transaction tx = startTransaction();
        try {
            // 核心业务逻辑
            tx.commit();
        } catch (RuntimeException e) {
            tx.rollback();
            throw e;
        }
        log.info("update book: " + book);
    }
     public void deleteBook(Book book) {
        checkParam(book);
        Transaction tx = startTransaction();
        try {
            // 核心业务逻辑
            tx.commit();
        } catch (RuntimeException e) {
            tx.rollback();
            throw e;
        }
        log.info("delete book: " + book);
    }
}
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

对于参数检查日志记录事务处理等操作,会频繁的,重复的出现在各个方法中。

使用OOP的思想,很难将这些代码模块化。

仔细观察以上的代码,我们发现BookService不关心参数检查,日志记录,事务处理这些,它关心的自身的核心业务逻辑

那么如何让BookService只关心自己的核心业务逻辑呢?

答案就是AOP,通过Proxy的方式,将重复的公共的代码抽离出去,动态的织入BookService中,而不改变原有的Service中的代码结构。

2. AOP原理

要实现上述的功能,就需要在执行Service方法的时候,对调用的方法进行拦截,并在拦截前后进行参数检查、日志、事务等处理。

在Java平台上,对于AOP的织入,有3种方式:

  1. 编译期:在编译时,由编译器把切面调用编译进字节码,这种方式需要定义新的关键字并扩展编译器,AspectJ就扩展了Java编译器,使用关键字aspect来实现织入;
  2. 类加载器:在目标类被装载到JVM时,通过一个特殊的类加载器,对目标类的字节码重新“增强”;
  3. 运行期:目标对象和切面都是普通Java类,通过JVM的动态代理功能或者第三方库实现运行期动态织入。

Spring的AOP实现就是基于JVM的动态代理

AOP本质上就是动态代理,动态代理有两种:

  1. JDK动态代理,Spring的AOP的默认实现,要求必须实现接口
  2. CGLIBopen in new window动态代理,Spring的AOP的可选配置,类和接口都支持

2.1 JDK动态代理示例

package com.mszlu.proxy;


import com.mszlu.service.UserService;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class JDKProxy {

    public Object createBookServiceProxy(Class clazz){

        ClassLoader cl = clazz.getClassLoader();

        Class[] classes = clazz.getInterfaces();
        InvocationHandler ih = new InvocationHandler() {
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                System.out.println("检查参数");
                //jdk9以上版本
                Object ret=method.invoke(clazz.getDeclaredConstructor().newInstance(),args);
                System.out.println("事务处理");
                System.out.println("日志记录");
                return ret;
            }
        };
        Object o = Proxy.newProxyInstance(cl,classes,ih);
        return o;
    }
}

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
package com.mszlu.service;

public interface BookService {

    void createBook();
}


package com.mszlu.service;

public class BookServiceImpl implements BookService {
    @Override
    public void createBook() {
        System.out.println("create book...");
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class App {

    public static void main(String[] args) {  
//jdk动态代理
        BookService bookService = new BookServiceImpl();
        BookService jdkProxy = (BookService) new JDKProxy().createBookServiceProxy(bookService.getClass());
        jdkProxy.createBook();
    }
1
2
3
4
5
6
7
8

3. AOP概念

  • Aspect:切面,即一个横跨多个核心逻辑的功能,或者称之为系统关注点;
  • Joinpoint:连接点,即定义在应用程序流程的何处插入切面的执行;
  • Pointcut:切入点,即一组连接点的集合;
  • Advice:通知,指特定连接点上执行的动作;
  • Introduction:引介,指为一个已有的Java对象动态地增加新的接口;
  • Weaving:织入,指将切面整合到程序的执行流程中;
  • Interceptor:拦截器,是一种实现增强的方式;
  • Target Object:目标对象,即真正执行业务的核心逻辑对象;
  • AOP Proxy:AOP代理,是客户端持有的增强后的对象引用。

4. 入门案例

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
    <version>5.2.16.RELEASE</version>
</dependency>
1
2
3
4
5

上述依赖会自动引入AspectJ,使用AspectJ实现AOP比较方便。

4.1 需求

场景需求:

​ 为之前实现的注册用户服务,添加日志功能。

要求:

  1. 打印执行的类和方法
  2. 打印执行方法的参数
  3. 计算方法执行的时间
  4. 如果有异常,记录异常

4.2 实现

步骤:

  1. 定义切面类

    @Aspect
    @Component
    public class LogAspect {
        
    }
    
    1
    2
    3
    4
    5
  2. 定义切点

    @Aspect
    @Component
    public class LogAspect {
        // 定义切点 在执行UserService的每个方法前执行:
        @Pointcut("execution(public * com.mszlu.service.UserService.*(..))")
        public void pt() {}
        
    
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
  3. 定义通知

    @Aspect
    @Component
    public class LogAspect {
        // 定义切点 在执行UserService的每个方法前执行:
        @PointCut("execution(public * com.mszlu.service.UserService.*(..))")
        public void pt() {}
        
    	  //定义通知
        @Around("pt()")
        public Object doLogging(ProceedingJoinPoint pjp) throws Throwable{
            //方法调用
            Object ret = pjp.proceed();
            return ret;
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
  4. 需求实现

      <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>fastjson</artifactId>
                <version>1.2.78</version>
            </dependency>
    
    1
    2
    3
    4
    5
    <dependency>
                <groupId>ch.qos.logback</groupId>
                <artifactId>logback-classic</artifactId>
                <version>1.2.3</version>
            </dependency>
    
    1
    2
    3
    4
    5
    package com.mszlu.aop;
    
    import com.alibaba.fastjson.JSON;
    import lombok.extern.slf4j.Slf4j;
    import org.aspectj.lang.ProceedingJoinPoint;
    import org.aspectj.lang.Signature;
    import org.aspectj.lang.annotation.Around;
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Pointcut;
    import org.springframework.stereotype.Component;
    
    @Aspect
    @Component
    @Slf4j
    public class LogAspect {
    
        // 定义切点 在执行UserService的每个方法前执行:
        @Pointcut("execution(public * com.mszlu.service.UserService.*(..))")
        public void pt() {}
    
        //定义通知
        @Around("pt()")
        public Object doLogging(ProceedingJoinPoint pjp) throws Throwable{
            try {
                log.info("----------------log start--------------------");
                //1. 打印执行的类和方法
                Signature signature = pjp.getSignature();
                String methodName = signature.getName();
                String interfaceName = signature.getDeclaringTypeName();
                log.info("接口名称:{}",interfaceName);
                log.info("方法名称:{}",methodName);
                //2. 打印执行方法的参数
                log.info("方法参数:{}", JSON.toJSONString(pjp.getArgs()));
                //3. 计算方法执行的时间
                long startTime = System.currentTimeMillis();
                //方法调用
                Object ret = pjp.proceed();
                long endTime = System.currentTimeMillis();
                log.info("方法执行时间:{}ms", endTime-startTime);
                log.info("----------------log end--------------------");
                return ret;
            }catch (Exception e){
                //4. 如果有异常,记录异常
                log.error("异常信息",e);
                throw e;
            }
        }
    }
    
    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
  5. 启动

    package com.mszlu;
    
    import com.mszlu.config.SpringConfig;
    import com.mszlu.proxy.JDKProxy;
    import com.mszlu.service.BookService;
    import com.mszlu.service.BookServiceImpl;
    import com.mszlu.service.UserService;
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.annotation.AnnotationConfigApplicationContext;
    
    
    public class App {
    
        public static void main(String[] args) {
            ApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class);
            UserService bean = context.getBean(UserService.class);
            bean.registerUser("ddd@mszlu.com","123456","ddd");
        }
    }
    
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    package com.mszlu.config;
    
    import com.mszlu.domain.MyImportBeanDefinitionRegistrar;
    import com.mszlu.domain.MyImportSelector;
    import com.mszlu.domain.User;
    import com.mszlu.domain.UserRegistrar;
    import org.springframework.context.annotation.ComponentScan;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.annotation.EnableAspectJAutoProxy;
    import org.springframework.context.annotation.Import;
    
    @Configuration
    @ComponentScan("com.mszlu")
    //注意开启AOP的支持,并且代理设置为cglib动态代理,因为UserService没有接口
    @EnableAspectJAutoProxy(proxyTargetClass = true)
    public class SpringConfig {
    }
    
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18

5. 通知类型

AOP通知共分为5种类型

  1. 前置通知

  2. 后置通知

  3. 环绕通知(重点)

  4. 返回后通知(了解)

  5. 抛出异常后通知(了解)

5.1 前置通知

  • 名称:@Before
  • 类型:方法注解
  • 位置:通知方法定义上方
  • 作用:设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法前运行
  • 范例:
@Before("pt()")
public void before() {
    System.out.println("before advice ...");
}
1
2
3
4
  • 相关属性:value(默认):切点标识

5.2 后置通知

  • 名称:@After

  • 类型:方法注解

  • 位置:通知方法定义上方

  • 作用:设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法后运行

  • 范例:

    @After("pt()")
    public void after() {
        System.out.println("after advice ...");
    }
    
    1
    2
    3
    4
  • 相关属性:value(默认):切点标识

5.3 环绕通知

  • 名称:@Around(重点,常用)

  • 类型:方法注解

  • 位置:通知方法定义上方

  • 作用:设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法前后运行

  • 范例:

    @Around("pt()")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        System.out.println("around before advice ...");
        Object ret = pjp.proceed();
        System.out.println("around after advice ...");
        return ret;
    }
    
    1
    2
    3
    4
    5
    6
    7

@Around注意事项

  1. 环绕通知必须依赖形参ProceedingJoinPoint才能实现对原始方法的调用,进而实现原始方法调用前后同时添加通知

  2. 通知中如果未使用ProceedingJoinPoint对原始方法进行调用将跳过原始方法的执行

  3. 对原始方法的调用可以不接收返回值,通知方法设置成void即可,如果接收返回值,必须设定为Object类型

  4. 原始方法的返回值如果是void类型,通知方法的返回值类型可以设置成void,也可以设置成Object

  5. 由于无法预知原始方法运行后是否会抛出异常,因此环绕通知方法必须抛出Throwable对象

5.4 返回后通知

  • 名称:@AfterReturning(了解)

  • 类型:方法注解

  • 位置:通知方法定义上方

  • 作用:设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法正常执行完毕后运行

  • 范例:

    @AfterReturning("pt()")
    public void afterReturning() {
        System.out.println("afterReturning advice ...");
    }
    
    
    1
    2
    3
    4
    5

5.5 抛出异常后通知

  • 名称:@AfterThrowing(了解)

  • 类型:方法注解

  • 位置:通知方法定义上方

  • 作用:设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法运行抛出异常后执行

  • 范例:

    @AfterThrowing("pt()")
    public void afterThrowing() {
        System.out.println("afterThrowing advice ...");
    }
    
    1
    2
    3
    4

6. 切点表达式

切入点表达式标准格式:动作关键字(访问修饰符 返回值 包名.类/接口名.方法名(参数)异常名)

execution(public User com.mszlu.service.UserService.findById(int))

  • 动作关键字:描述切入点的行为动作,例如execution表示执行到指定切入点
  • 访问修饰符:public,private等,可以省略
  • 返回值
  • 包名
  • 类/接口名
  • 方法名
  • 参数
  • 异常名:方法定义中抛出指定异常,可以省略

可以使用通配符描述切入点,快速描述

  • * :单个独立的任意符号,可以独立出现,也可以作为前缀或者后缀的匹配符出现

    execution(public * com.mszlu.*.UserService.find**))
    
    1

    匹配com.mszlu包下的任意包中的UserService类或接口中所有find开头的带有一个参数的方法

  • .. :多个连续的任意符号,可以独立出现,常用于简化包名与参数的书写

    execution(public User com..UserService.findById(..))
    
    1

    匹配com包下的任意包中的UserService类或接口中所有名称为findById的方法

  • +:专用于匹配子类类型

execution(* *..*Service+.*(..))
1

6.1 execution

execution在实际工作中,很少被使用,因为匹配的打击面非常大或者非常小,不能灵活应用

execution(public * com.mszlu.service.*.*(..)) 基本能实现无差别全覆盖,即某个包下面的所有Bean的所有方法都会被拦截。

execution(public * update*(..))从方法的前缀来区分,这种误伤的概率非常大,你不可能要求所有的程序员都按照这种书写习惯来。

使用AOP,可以将指定的方法装配到指定Bean的指定方法前后,如果自动装配时,因为不恰当的范围,容易导致意想不到的结果,特别是新入职的开发人员,不懂得规则的时候,非常容易出现生产事故

6.2 annotation

我们使用AOP的时候,常常使用annotation的形式

场景:

比如我们实现一个性能监控的需求,使用AOP实现

  1. 定义注解

    package com.mszlu.aop;
    
    import java.lang.annotation.*;
    
    @Target({ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface MsMetric {
        String value() default "";
    }
    
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
  2. 在需要监控的方法上,加上注解

    @MsMetric
        public void registerUser(String mail,String password,String nickname){
            //...
        }
    
    1
    2
    3
    4
  3. 实现性能监控AOP,切点使用annotation

    package com.mszlu.aop;
    
    import lombok.extern.slf4j.Slf4j;
    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;
    
    @Aspect
    @Component
    @Slf4j
    public class MetricAspect {
    
        // 定义切点 注解所在的方法 即是切点
        @Pointcut("@annotation(MsMetric)")
        public void pt() {}
    
        //定义通知
        @Around("pt()")
        public Object doLogging(ProceedingJoinPoint pjp) throws Throwable{
            try {
                log.info("----------------metric start--------------------");
                //计算方法执行的时间
                long startTime = System.currentTimeMillis();
                //方法调用
                Object ret = pjp.proceed();
                long endTime = System.currentTimeMillis();
                log.info("方法执行时间:{}ms", endTime-startTime);
                log.info("----------------metric end--------------------");
                return ret;
            }catch (Exception e){
                log.error("异常信息",e);
                throw e;
            }
        }
    }
    
    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

7. AOP使用注意事项

  1. 访问被注入的Bean时,总是调用方法而非直接访问字段。
  2. 编写Bean时,如果可能会被代理,就不要编写public final方法。

示例:

在MailService:

package com.mszlu.service;

import com.mszlu.aop.MsMetric;
import com.mszlu.domain.User;
import org.springframework.stereotype.Component;

@Component
public class MailService {

    public final User aaa = new User();

    public User getUser(){
        return aaa;
    }

    public final User getFinalUser(){
        return aaa;
    }

    @MsMetric
    public boolean sendRegisterMail(User user){
        System.out.println(String.format("%s 正在注册码神之路,您可以点击以下的链接完成注册 %s,如果不是你注册的,请忽略本邮件",user.getNickname(),"http://www.mszlu.com"));
        return true;
    }
}

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

UserService中:

 public void registerUser(String mail,String password,String nickname){
        users.forEach(user -> {
            if(user.getMail().equalsIgnoreCase(mail)){
                throw new RuntimeException("已经注册");
            }
        });
     //输出null
        System.out.println(mailService.aaa);
     //输出null
        System.out.println(mailService.getFinalUser());
     //正常输出
        System.out.println(mailService.getUser());
        User user = new User(null,mail,password,nickname);
        mailService.sendRegisterMail(user);
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15