Skip to content

[TOC]

前言 ​

学习目标 ​

  1. 理解AOP的设计思想
  2. 掌握切入点表达式(execution、@annotation、@target)
  3. 掌握几种通知的执行时间
  4. 熟悉AspectJ的使用

前置知识准备 ​

  • IOC和DI
  • Spring生命周期的BeanPostProcessor
  • 动态代理

介绍AOP ​

Aspect Oriented Programming

在软件业,面向切面编程,是指通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。AOP是OOP(面向对象编程)的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容。

利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

AOP和OOP ​

OOP 面向对象编程,是Object Oriented Programming的简称

OOP:通过继承来增强

AOP:通过切面来增强

AOP特点 ​

AOP采取横向抽取机制,取代了传统纵向继承体系重复性代码。

Spring AOP使用纯Java实现,不需要专门的编译过程和类加载器,在运行期通过代理方式向目标类(委托类)织入增强代码。

应用场景 ​

事务管理、性能监视、安全检查、缓存 、日志等

想象一下如果每个组件都单独去实现这些系统功能?

改变这些关注点的逻辑,修改各个模块当中的实现,方法的调用就会重复出现在各个模块中组件会因为那些与自身核心业务无关的代码而变得混乱 组件会因为那些与自身核心业务无关的代码而变得混乱

AOP编程术语 ​

  • Target 目标类 (需要被代理的类,委托类)
  • Proxy 代理类 (动态代理生成的)
  • JoinPoint 连接点 指被代理对象里那些可能会被增强的点 (方法) 如所有方法(候选的可能被增强候选点)
  • weaver 织入 (植入)是指把advice应用到目标对象来创建新的代理对象的过程

AOP实现 ​

AOP采用动态代理的方式来实现的,使用的是JDK动态代理和CGLIB动态代理。

如果有实现接口的话,使用的是JDK动态代理;如果没有实现接口的话,使用的Cglib动态代理。

AOP实战 ​

  • 动态代理
  • SpringAOP
  • AspectJ

SpringAOP ​

在容器中注册3个组件:

使用注解注册委托类组件和通知组件

java
//委托类组件
@Service
public class UserServiceImpl implements UserService{
    @Override
    public void sayHello() {
        System.out.println("hello xiaowu");
    }
}
//通知组件
@Component
public class CustomAdvice implements MethodInterceptor {
    @Override
    public Object invoke(MethodInvocation methodInvocation) throws Throwable {
        System.out.println("正道的光");
        //执行委托类的代码
        Object proceed = methodInvocation.proceed();
        //额外去做增强的业务
        System.out.println("照在了大地上");
        return proceed;
    }
}

通过代理组件注册代理组件

xml
<bean id="userServiceProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
    <!--委托类的组件是谁-->
    <property name="target" ref="userServiceImpl"/>
    <!--通知组件是谁-->
    <property name="interceptorNames" value="customAdvice"/>
</bean>

单元测试要从容器中指定代理组件取出

java
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:application.xml")
public class MyTest {

    @Autowired
    @Qualifier("userServiceProxy")
    UserService userService;

    @Test
    public void mytest1(){
        userService.sayHello();
    }
}

&#8203;

切入点表达式 &#8203;

引入Aspectjweaver依赖

xml
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.6</version>
</dependency>

execution &#8203;

  • 能否省略

  • 能否通配

  • 特殊用法

修饰符:可以省略,省略代表任意修饰符

返回值:不能省略,POJO类要写全类名;可以是用*作为通配符

包名、类名、方法名:使用..部分省略,头和尾不能省略,中间的任意一部分都可以省略;可以使用*作为通配符

形参:可以使用..或* 来进行通配,

xml

<!--execution 👉 aop:pointcut 👉 指定的是容器中的组件中的方法-->

<!--语法:execution(访问修饰符 返回值 包名.类名.方法名(参数列表))

        1、能否省略不写
        2、能否通配
        3、是否有特殊用法

        访问修饰符:可以省略、省略不写代表任意修饰符
        返回值:不可以省略、可以使用*来通配、JavaBean要写全类名(基本类型、包装类以及String可以直接写)
        包名、类名、方法名:可以部分省略,头和尾不能省略,中间的任意一部分都可以省略 👉 ..来进行省略
                         *来通配 com.*、com.cskao*,头和尾也可以使用*作为通配符                                          *.cskaoyan.service.UserService.say*
        参数列表:可以省略 👉 代表的是无参的方法
                如果想要写多个参数,写参数的全类名 👉 第一参数是String,第二个参数是int 👉 (String,int)
                可以通配:* 👉 代表的单个任意参数
                        .. 👉 任意参数 👉 任意数量的任意类型的参数
                JavaBean要写全类名(基本类型、包装类以及String可以直接写)
-->
java
@Pointcut("execution(public void com.cskaoyan.service.UserServiceImpl.sayHello(String))")
public void mypointcut1(){}

@annotation &#8203;

写上自定义注解的全类名

java
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface CountTime {
}
java
@Pointcut("@annotation(com.cskaoyan.CountTime)")
public void mypointcut2(){}

@target &#8203;

写上自定义注解的全类名

java
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.Type)
public @interface CountTimeAllMethod {
}
java
@Pointcut("@annotation(com.cskaoyan.CountTimeAllMethod)")
public void mypointcut3(){}

Aspect切面 &#8203;

在切面类中配置切面组件和通知方法

AspectJ的注解实现,首先要标记前面开关

然后在切面类中使用AspectJ相关的注解,表达切面、切入点、通知

java
/**
 * 增加对应通知的方法 : 将这样的一些方法配置为对应的通知方法
 * 方法 👉 通知
 *  before
 *  after
 *  around
 *  afterReturning
 *  afterThrowing
 */
@Component
@Aspect
public class CustomAspect {
  /**
     * pointcut方法
     * 返回值:void
     * 方法名:任意写 👉 用于作为pointcut组件的id
     * 形参:不用写
     * 方法体:不用写
     * @Pointcut的value属性写切入点表达式
     */
  @Pointcut("execution(* com.cskaoyan.service..*(..))")
  public void mypointcut1(){}
  @Pointcut("@annotation(com.cskaoyan.CountTime)")
  public void mypointcut2(){}
  @Pointcut("@annotation(com.cskaoyan.CountTimeAllMethod)")
  public void mypointcut3(){}
  /**
     * 通知注解@Before、@After、@Around、@AfterReturning、@AfterThrowing
     * value属性:
     *      1、对应的切入点表达式
     *      2、引用的切入点表达式的方法名
     */

  /**
     * before通知
     * 返回值:void
     * 方法名:任意去写
     * 形参:joinPoint连接点 (可写可不写)
     */
  //@Before("execution(* com.cskaoyan.service..*(..))")
  @Before("mypointcut()")
  public void before(JoinPoint joinPoint){
    System.out.println("before");
  }
  /**
     * after通知
     * 返回值:void
     * 方法名:任意去写
     * 形参:joinPoint连接点 (可写可不写)
     */
  @After("mypointcut()")
  public void after(){
    System.out.println("after");
  }
  /**
     * around通知
     * 返回值:Object
     * 方法名:任意去写
     * 形参:ProceedingJoinPoint连接点 (必须写) 👉 提供了proceed方法,执行的是委托类的代码
     */
  @Around("mypointcut()")
  public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
    System.out.println("around的before");

    Object proceed = proceed = joinPoint.proceed();//该方法执行的是委托类的方法

    System.out.println("around的after");
    return proceed;
  }
  /*public Object around(ProceedingJoinPoint joinPoint){
        System.out.println("around的before");
        Object proceed = null;//该方法执行的是委托类的方法
        try {
            proceed = joinPoint.proceed();
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
        System.out.println("around的after");
        return proceed;
    }*/
  /**
     * afterReturning通知
     * 返回值:void
     * 方法名:任意去写
     * 形参:Object(委托类方法的执行结果)
     */
  @AfterReturning(value = "mypointcut()",returning = "result")
  public void afterReturning(Object result){
    System.out.println("afterReturning");
  }
  /**
     * afterThrowing通知
     * 返回值:void
     * 方法名:任意去写
     * 形参:Exception/Throwable(委托类方法执行过程中抛出异常)
     */
  @AfterThrowing(value = "mypointcut()",throwing = "exception")
  public void afterThrowing(Exception exception){//Throwable
    System.out.println("afterThrowing");
    System.out.println(exception.getMessage());
  }

}

JoinPoint连接点 &#8203;

获取增强过程中的一些值

  • Signature
  • Arguments
  • This
  • Target

在通知方法的形参中,传入JoinPoint形参,通过JoinPoint获得对应的一些参数

java
@Before("mypointcut()")
public void before(JoinPoint joinPoint){
    //Signature 方法的描述
    //This 代理对象
    //Target 委托类对象
    //Arguments 参数
    Signature signature = joinPoint.getSignature();
    Object proxy = joinPoint.getThis();
    Object target = joinPoint.getTarget();
    Object[] args = joinPoint.getArgs();

    System.out.println("signature:" + signature.getName());
    System.out.println(proxy.getClass().getName());
    System.out.println(target.getClass().getName());
    System.out.println(Arrays.asList(args));
}

笔记 &#8203;

  • AOP就是把委托类变成代理类放到容器中,AOP应用场景其实就是动态代理的应用场景
  • Advice通知描述的是做什么增强具体增强代码,CutPoint切片就是从容器中选择我们要增强的方法。Aspect切面就是两者结合
  • Debug的时候输出两次增强的方法,因为动态代理中toString也被增强了
  • 切入点正则表达式跟,匹配条件去选择我们要增强的方法
  • 切入点表达式写在方法上,方法名就是id。使用方法名来区分不同的切入点表达式。是为了多个通知,用到了同一个切入点表达式
  • Around适合多个通知,场景丰富。场景单一就用@Before ,@Afte光有切入点不行。
  • @Productut写切入点,然后写通知把方法和切入点表达式绑定起来
  • @Around的value写切入点的方法名,引用对应的切入点
  • 指定切入点三种方式
  • AspectJ能做什么?计算方法执行的时间;安全检查,参数合法性校验;缓存,方法执行前看看有没有缓存对已经有的业务做调整,做缓存
  • 通知注解的value可以直接写切入点表达式,但引用方法名的办法可以复用表达式
  • 使用Before通知可以获取参数,但是修改不了获取的参数