1.什么是AOP?

AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。


1.1面试题

什么是AOP? AOP就是面向切面编程。把公共的模块提取出来,作为切面,在程序运行期织入到对应的业务中。 切面:web层级设计中,web层->网关层->服务层->数据层,每一层之间也是一个切面。编程中,对象与对象之间,方法与方法之间,模块与模块之间都是一个个切面。 AOP是通过动态代理实现的。
spring的AOP是使用哪种代理方式? spring默认会判断被代理的类是否实现类接口,如果实现了接口,就使用JDK的代理方式,如果没有 实现接口,就使用CGLib代理方式,当然我们也可以通过配置强制要求spring全部使用CGLib代理方式。


2.springAOP-XML-Helloword

2.1添加依赖

  1. <!-- spring的依赖 -->
  2. <dependency>
  3. <groupId>org.springframework</groupId>
  4. <artifactId>spring-context</artifactId>
  5. <version>5.2.9.RELEASE</version>
  6. </dependency>
  7. <dependency>
  8. <groupId>org.springframework</groupId>
  9. <artifactId>spring-aspects</artifactId>
  10. <version>5.2.9.RELEASE</version>
  11. </dependency>

2.2添加业务

/**
 * @author:刘倩云  接口
 * @createTime:2021-03-17
 */
public interface UserDAO {
    void save(String username);
    void delete();
}

/**
 * @author:刘倩云  实现类
 * @createTime:2021-03-17
 */
public class UserDAOImpl implements UserDAO {

   public void save(String username) {
        System.out.println("添加用户:"+username);
    }

    public void delete() {
        System.out.println("删除用户");
    }
}

2.3添加切面类

/**
 * @author:刘倩云
 * @createTime:2021-03-18
 */
public class UserAspacts {
    //通知的前置方法
    public void before(){
        System.out.println("----前置的通知方法----");
    }
}

2.4添加spring的配置文件

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop
        http://www.springframework.org/schema/aop/spring-aop.xsd">

    <!--业务类-->
    <bean  id="userDAO" class="com.aop.dao.impl.UserDAOImpl"></bean>
    <!--切面类-->
    <bean id="aspacts" class="com.aop.aspects.UserAspacts"></bean>
    <!--添加aop的配置-->
    <!--- proxy-target-class:默认是false,如果修改为true,则表示要求spring所有的 动态代理都使用CGLib -->
    <aop:config>
        <!--配置切面-->
        <aop:aspect id="userAspacts" ref="aspacts">
            <!-- 配置一个前置通知:被拦截方法的之前执行的方法 -->
            <!-- method:配置切面类中的对应的要执行的方法-->
            <!-- pointcut:切入点。 描述这个增强的方法,应该执行的位置-->
            <aop:before method="before" pointcut="execution(*
            com.aop.dao.impl.UserDAOImpl.*(..))"/>
        </aop:aspect>
    </aop:config>

</beans>

2.5测试

@Test
public void testUserAspacts(){
    ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
    UserDAO bean = context.getBean(UserDAO.class);
    //方法会被增强
    bean.save("Cherry");
}

——前置的通知方法—— 添加用户:Cherry

3.springAOP的五种通知

3.1spring的AOP分为

前置通知(before) 后置通知(after-returning) 环绕通知(round) 被拦截方法的前后都会有通知 异常通知(after-throwing) 出现异常就执行 最终通知(after) 无论是否出现异常都会执行的通知。

tips:所有的通知都是可以多次添加的。

/**
 * @author:刘倩云
 * @createTime:2021-03-18
 */
public class UserAspacts {
    //通知的前置方法
    public void before(){
        System.out.println("----前置的通知方法----");
    }

    //通知的后置方法
    public void afterReturning(){
        System.out.println("----后置的通知方法----");
    }

    //环绕通知
    public Object round(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("----环绕通知前----");
        //执行目标方法
        Object proceed = null;
        proceed = joinPoint.proceed();
        System.out.println("{----环绕通知的后----}");
        return proceed;
    }

    //异常通知 - 出现异常就通知
    public void afterThrowing(){
        System.out.println("----出现异常----");
    }

    //最终通知
    public void after(){
        System.out.println("----最终通知,必须执行----");
    }
}
<aop:config>
  <!--配置切面-->
  <aop:aspect id="userAspacts" ref="aspacts">
    <!-- 配置一个前置通知:被拦截方法的之前执行的方法 -->
    <!-- method:配置切面类中的对应的要执行的方法-->
    <!-- pointcut:切入点。 描述这个增强的方法,应该执行的位置-->
    <aop:before method="before" pointcut="execution(*
                                          com.aop.dao.impl.UserDAOImpl.*(..))"/>
    <!--后置通知-->
    <aop:after-returning method="afterReturning" pointcut="execution(*
                                                           com.aop.dao.impl.UserDAOImpl.*(..))"/>
    <!--环绕通知-->
    <aop:around method="round" pointcut="execution(*
                                         com.aop.dao.impl.UserDAOImpl.*(..))"/>
    <!--异常通知-->
    <aop:after-throwing method="afterThrowing" pointcut="execution(*
                                                         com.aop.dao.impl.UserDAOImpl.*(..))"/>
    <!--最终-->
    <aop:after method="after" pointcut="execution(*
                                        com.aop.dao.impl.UserDAOImpl.*(..))"/>
  </aop:aspect>
</aop:config>

——前置的通知方法—— ——环绕通知前—— 添加用户:Cherry ——最终通知,必须执行—— {——环绕通知的后——} ——后置的通知方法——

注:如果出现异常,那么异常在最终通知后面会显示出现异常,且后面的环绕通知后面不执行
也可以重复配置,比如有个before方法,还可以再写一个before1方法。
tips:同种类型的通知,执行的顺序和配置顺序一致。

**

4.spring通知方法中的参数

所有的通知方法都可以设置参数JoinPoint。 这个参数叫连接点。包含了连接点的所有信息。
tips:一个通知的方法是可以传入多个参数的,如果传入多个参数,JoinPoint必须写第一个

4.1源代码如下

package org.aspectj.lang; 
import org.aspectj.lang.reflect.SourceLocation;

public interface JoinPoint {
    //..
    //连接点的字符串表达形式 其中包含的方法的名字
    String toString(); 
    String toShortString();
    String toLongString(); 
    //目标对象 
    Object getThis();
    //获取被代理的对象
    Object getTarget(); 
    //获取被增强的方法的参数数组 
    Object[] getArgs();
    //....
}

JoinPoint这个参数可以在每一个通知方法中编写。spring会自动传入对应的参数.
使用JoinPoint可以获取连接点的信息。

4.2before

//前置通知的方法
public void before(JoinPoint point){
    System.out.println("---前置通知方法-start--");   
    System.out.println("getThis:"+point.getThis());
    System.out.println("getArgs:"+ Arrays.toString(point.getArgs()));
    System.out.println("getTarget:"+point.getTarget());
    System.out.println("toString:"+point.toString());
    System.out.println("toShortString:"+point.toShortString()); 
    System.out.println("---前置通知方法-end--");
}


4.3round

Tips:环绕通知传入的参数是JoinPoint的子接口,所以也可以获取连接点的信息

//环绕通知
public Object round(ProceedingJoinPoint joinPoint) throws Throwable {
    System.out.println("----环绕通知前----");
    //获取连接点的所有信息
    System.out.println("getThis:"+joinPoint.getThis());
    System.out.println("getArgs:"+ Arrays.toString(joinPoint.getArgs()));
    System.out.println("getTarget:"+joinPoint.getTarget());
    System.out.println("toString:"+joinPoint.toString());
    System.out.println("toShortString:"+joinPoint.toShortString());
    System.out.println("toLongString:"+joinPoint.toLongString());
    //执行目标方法
    Object proceed = null;
    proceed = joinPoint.proceed();
    System.out.println("{----环绕通知的后----}");
    return proceed;
}

4.4增强方法中获取指定参数

我们想在增强的方法中获取指定的参数可以使用args指定
在增强方法中写好参数

public void before1(String username) {
    System.out.println("----前置的通知方法----");
    System.out.println("传入请求的参数:"+username);
}
<aop:before method="before1" arg-names="username" pointcut="args(username) and execution(*
            com.aop.dao.impl.UserDAOImpl.*(..))"/>

5.springAOP切点的配置方法

5.1切点信息描述

pointcut=”args(username) and execution( com.aop.dao.impl.UserDAOImpl.(..))”

由于上面的切点信息都一样,所以可以像定义一个变量一样,定义一个切点信息。













5.2切点描述的配置方式

下面是一个官方给出的案例:

  • 表示所有的public的方法

    execution(public (..))

  • 所有的set开头的方法

    execution( set(..))

  • AccountService接口中的所有方法

    execution( com.xyz.service.AccountService.(..))

  • 在service包下的所有类的所有方法(不包含子孙包)

    execution( com.xyz.service..*(..))

  • service包以及其子孙包下的所有类的所有方法

    execution( com.xyz.service...*(..))

  • service包中的任何连接点(仅在 Spring AOP 中执行方法)

    within(com.xyz.service.*)


  • service包中的任何连接点(仅在 Spring AOP 中执行方法)或其中一个 sub-packages

    within(com.xyz.service..*)

  • 就是说在this中我们配置的都是具体的类型,也就是全限定类名。注意This中不支持通配符。

    this(com.xyz.service.AccountService)


  • target和this的主要不同点是:target是按照目标类型进行匹配的。this是按照调用类型进行匹配的。

    target(com.xyz.service.AccountService)


  • 匹配有指定参数的方法

    args(java.io.Serializable)


  • 按照指定额bean的beanName匹配

    bean(userService)

  • 匹配所有的beanName以service结尾的bean

    bean(*Service)

6.Advisor方式配置AOP

6.1前置通知

定义一个类实现MethodBeforAdvice接口

/**
 * @author:刘倩云
 * @createTime:2021-03-18
 */
public class CyBeforeAdvice implements MethodBeforeAdvice {
    /**
     * @param method 被增强的方法
     * @param args 被增强方法的参数
     * @param target 被增强的对象
     * @throws Throwable
     */
    public void before(Method method, Object[] args, Object target) throws Throwable {
        System.out.println("--前置通知开始--");
        System.out.println("被增强对象:"+target.toString()); 
        System.out.println("被增强的方法:"+method.getName());
        System.out.println("被增强的方法的参数列表:"+ Arrays.toString(args));
        System.out.println("--前置通知结束--");
    }
}

将这个通知注册到spring中,并且配置切面:

<!--业务类-->
<bean  id="userDAO" class="com.aop.dao.impl.UserDAOImpl"></bean>
<!--切面类-->
<bean id="beforeAdvice" class="com.aop.aspects.CyBeforeAdvice"></bean>
<!--添加aop的配置-->
<aop:config>
  <!-- pointcut:切入点。 描述这个增强的方法,应该执行的位置-->
  <aop:pointcut id="allMethod" expression="execution(* com.aop.dao.impl.UserDAOImpl.*(..))"/>
  <aop:advisor advice-ref="beforeAdvice" pointcut-ref="allMethod"/>
</aop:config>

测试

@Test
public void testUserAdvisor(){
    ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext1.xml");
    UserDAO bean = context.getBean(UserDAO.class);
    //方法会被增强
    bean.save("Cherry");
}

—前置通知开始— 被增强对象:com.aop.dao.impl.UserDAOImpl@3ba9ad43 被增强的方法:save 被增强的方法的参数列表:[Cherry] —前置通知结束— 添加用户:Cherry


6.2后置通知(最终通知)

在advisor的方式中,AfterReturningAdvice是AfterAdvice的子类, 而AfterAdvice是没有方法的。所有以只有AfterReturningAdvice。
自己写一个类实现AfterReturningAdvice,当然我们也可以直接使用上面得StBeforeAdvice去实现,但是为了程序更加清楚,我们单独写一个类。

/**
 * @author:刘倩云
 * @createTime:2021-03-18
 */
public class CyAfterAdvice implements AfterReturningAdvice {
    /**
     * @param method 被增强的方法
     * @param args 被增强方法的参数
     * @param target 被增强的对象
     * @throws Throwable
     */
    public void afterReturning(Object o, Method method, Object[] args, Object target) throws Throwable {
        System.out.println("--后置通知开始--");
        System.out.println("被增强对象:"+target.toString());
        System.out.println("被增强的方法:"+method.getName());
        System.out.println("被增强的方法的参数列表:"+ Arrays.toString(args));
        System.out.println("--后置通知结束--");
    }
}

配置xml

<!--业务类-->
<bean  id="userDAO" class="com.aop.dao.impl.UserDAOImpl"></bean>
<!--切面类-->
<bean id="beforeAdvice" class="com.aop.aspects.CyBeforeAdvice"></bean>
<bean id="afterAdvice" class="com.aop.aspects.CyAfterAdvice"></bean>
<!--添加aop的配置-->
<aop:config>
  <!-- pointcut:切入点。 描述这个增强的方法,应该执行的位置-->
  <aop:pointcut id="allMethod" expression="execution(* com.aop.dao.impl.UserDAOImpl.*(..))"/>
  <aop:advisor advice-ref="beforeAdvice" pointcut-ref="allMethod"/>
  <aop:advisor advice-ref="afterAdvice" pointcut-ref="allMethod"/>
</aop:config>

6.3异常通知

实现 ThrowsAdvice接口
其他配置和前面一样

6.4环绕通知

/**
 * @author:刘倩云
 * @createTime:2021-03-18
 */
public class CyAfroundAdvice implements MethodInterceptor {

    /**
     * @param invocation  方法调用对象,继承了接口:JoinPoint
     * @return
     * @throws Throwable
     */
    public Object invoke(MethodInvocation invocation) throws Throwable {
        System.out.println("{环绕通知开始}");
        System.out.println("被拦截对象:"+invocation.getThis());
        System.out.println("被拦截的方法:"+invocation.getMethod().getName());
        System.out.println("被拦截的方法的参数:"+ Arrays.toString(invocation.getArguments()));
        //执行目标方法
        Object returnVal = invocation.proceed();
        System.out.println("环绕通知方法的返回值:"+returnVal); 
        System.out.println("{环绕通知结束}");
        return returnVal;
    }
}

配置和前面一样

6.5advisor方式配置的同种类型的通知执行顺序


默认按照oreder的数字从小到大执行,如果没有指定order,则按照配置顺序,从上向下。

7.AOP术语

切面(Aspect) 切面由切点和增强组成,它既包括了横切逻辑的定义,也包括了连接点的定义,SpringAOP就是将切面所定义的横切逻辑织入到切面所制定的连接点中。
比如上文讨论的数据库事务,这个数据库事务代码贯穿了我们的整个代码,我们就可以这个叫做切面。 SpringAOP将切面定义的内容织入到我们的代码中,从而实现前后的控制逻辑。 比如我们常写的拦截器Interceptor,这就是一个切面类

连接点(Joinpoint) 程序执行的某个特定位置,如某个方法调用前,调用后,方法抛出异常后,这些代码中的特定点称为连接点。简单来说,就是在哪加入你的逻辑增强
连接点表示具体要拦截的方法,上面切点是定义一个范围,而连接点是具体到某个方法

切点(PointCut) 每个程序的连接点有多个,如何定位到某个感兴趣的连接点,就需要通过切点来定位。比如,连接点—数据库的记录,切点—查询条件
切点用于来限定Spring-AOP启动的范围,通常我们采用表达式的方式来设置,所以关键词是范围

增强(Advice) 增强是织入到目标类连接点上的一段程序代码。在Spring中,像BeforeAdvice等还带 有方位信息
通知是直译过来的结果,我个人感觉叫做“业务增强”更合适 对照代码就是拦截器定义的相关方法,通知分为如下几种:
前置通知(before):在执行业务代码前做些操作,比如获取连接对象
后置通知(after):在执行业务代码后做些操作,无论是否发生异常,它都会执行,比如关闭连接对象
异常通知(afterThrowing):在执行业务代码后出现异常,需要做的操作,比如回滚事务
返回通知(afterReturning),在执行业务代码后无异常,会执行的操作
环绕通知(around),这个目前跟我们谈论的事务没有对应的操作,所以暂时不谈

目标对象(Target) 需要被加强的业务对象

织入(Weaving) 织入就是将增强添加到对目标类具体连接点上的过程。
织入是一个形象的说法,具体来说,就是生成代理对象并将切面内容融入到业务流程的过程。

代理类(Proxy) 一个类被AOP织入增强后,就产生了一个代理类。