Spring AOP的基础使用

一、什么是AOP

  AOP:Aspect Oriented Programming 面向切面编程

    面向切面编程:基于OOP基础之上新的编程思想,OOP面向的主要对象是类,而AOP面向的主要对象是切面,在处理日志、安全管理、事务管理等方面有非常重要的作用。

    AOP是Spring中重要的核心点,虽然IOC容器没有依赖AOP,但是AOP提供了非常强大的功能,用来对IOC做补充。

    通俗点说的话就是在程序运行期间,将某段代码动态切入指定方法指定位置进行运行的这种编程方式。

  AOP的核心概念及术语
  • 切面(Aspect): 指关注点模块化,这个关注点可能会横切多个对象。事务管理是企业级Java应用中有关横切关注点的例子。 在Spring AOP中,切面可以使用通用类基于模式的方式(schema-based approach)或者在普通类中以@Aspect注解(@AspectJ 注解方式)来实现。

  • 连接点(Join point): 在程序执行过程中某个特定的点,例如某个方法调用的时间点或者处理异常的时间点。在Spring AOP中,一个连接点总是代表一个方法的执行。

  • 通知(Advice): 在切面的某个特定的连接点上执行的动作。通知有多种类型,包括“around”, “before” and “after”等等。通知的类型将在后面的章节进行讨论。 许多AOP框架,包括Spring在内,都是以拦截器做通知模型的,并维护着一个以连接点为中心的拦截器链。

  • 切点(Pointcut): 匹配连接点的断言。通知和切点表达式相关联,并在满足这个切点的连接点上运行(例如,当执行某个特定名称的方法时)。切点表达式如何和连接点匹配是AOP的核心:Spring默认使用AspectJ切点语义。

  • 引入(Introduction): 声明额外的方法或者某个类型的字段。Spring允许引入新的接口(以及一个对应的实现)到任何被通知的对象上。例如,可以使用引入来使bean实现 IsModified接口, 以便简化缓存机制(在AspectJ社区,引入也被称为内部类型声明(inter))。

  • 目标对象(Target object): 被一个或者多个切面所通知的对象。也被称作被通知(advised)对象。既然Spring AOP是通过运行时代理实现的,那么这个对象永远是一个被代理(proxied)的对象。

  • AOP代理(AOP proxy):AOP框架创建的对象,用来实现切面契约(aspect contract)(包括通知方法执行等功能)。在Spring中,AOP代理可以是JDK动态代理或CGLIB代理。

  • 织入(Weaving): 把切面连接到其它的应用程序类型或者对象上,并创建一个被被通知的对象的过程。这个过程可以在编译时(例如使用AspectJ编译器)、类加载时或运行时中完成。 Spring和其他纯Java AOP框架一样,是在运行时完成织入的。

  AOP的通知类型
  • 前置通知(Before advice): 在连接点之前运行但无法阻止执行流程进入连接点的通知(除非它引发异常)。

  • 后置返回通知(After returning advice):在连接点正常完成后执行的通知(例如,当方法没有抛出任何异常并正常返回时)。

  • 后置异常通知(After throwing advice): 在方法抛出异常退出时执行的通知。

  • 后置通知(总会执行)(After (finally) advice): 当连接点退出的时候执行的通知(无论是正常返回还是异常退出)。

  • 环绕通知(Around Advice):环绕连接点的通知,例如方法调用。这是最强大的一种通知类型,。环绕通知可以在方法调用前后完成自定义的行为。它可以选择是否继续执行连接点或直接返回自定义的返回值又或抛出异常将执行结束。

  AOP的应用场景
  • 日志管理

  • 权限认证

  • 安全检查

  • 事务控制

二、Spring AOP基于注解的配置

  1、添加pom依赖
     <!-- https://mvnrepository.com/artifact/cglib/cglib -->
        <dependency>
            <groupId>cglib</groupId>
            <artifactId>cglib</artifactId>
            <version>3.3.0</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.aspectj/aspectjweaver -->
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.9.5</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/aopalliance/aopalliance -->
        <dependency>
            <groupId>aopalliance</groupId>
            <artifactId>aopalliance</artifactId>
            <version>1.0</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.springframework/spring-aspects -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aspects</artifactId>
            <version>5.2.3.RELEASE</version>
        </dependency>
  2、编写配置

    1)将目标类和切面类加入到IOC容器中,在对应的类上添加组件注解

      给切面类添加@Component注解

      给目标类添加@Service注解

      添加自动扫描的配置:

<!--别忘了添加context命名空间-->
<context:component-scan base-package="com.llxazy"></context:component-scan>

    2)设置程序中的切面类

      在切面类中添加@Aspect注解

    3)设置切面类中的方法是什么时候在哪里执行

package com.llxazy.util;

import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.Arrays;

@Component
@Aspect
public class LogUtil {

    /*
    设置下面方法在什么时候运行
        @Before:在目标方法之前运行:前置通知
        @After:在目标方法之后运行:后置通知
        @AfterReturning:在目标方法正常返回之后:返回通知
        @AfterThrowing:在目标方法抛出异常后开始运行:异常通知
        @Around:环绕:环绕通知

        当编写完注解之后还需要设置在哪些方法上执行,使用表达式
        execution(访问修饰符  返回值类型 方法全称)
     */
    @Before("execution( public int com.llxazy.inter.MyCalculator.*(int,int))")
    public static void start(){
        System.out.println("方法开始执行,参数是:");
    }

    @AfterReturning("execution( public int com.llxazy.inter.MyCalculator.*(int,int))")
    public static void stop(){
        System.out.println("方法执行完成,结果是:");

    }

    @AfterThrowing("execution( public int com.llxazy.inter.MyCalculator.*(int,int))")
    public static void logException(){
        System.out.println("方法出现异常:");
    }

    @After("execution( public int com.llxazy.inter.MyCalculator.*(int,int))")
    public static void end(){
        System.out.println("方法执行结束了......");
    }
}

    4)开启基于注解的aop的功能

<?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:context="http://www.springframework.org/schema/context"
       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/context
       http://www.springframework.org/schema/context/spring-context.xsd
       http://www.springframework.org/schema/aop
       https://www.springframework.org/schema/aop/spring-aop.xsd
">
    <context:component-scan base-package="com.llxazy"></context:component-scan>
    <aop:aspectj-autoproxy></aop:aspectj-autoproxy>
</beans>
  3、通过cglib来创建代理对象

  spring AOP的动态代理方式是jdk自带的方式,容器中保存的组件是代理对象com.sun.proxy.$Proxy对象

  可以通过cglib的方式来创建代理对象,此时不需要实现任何接口,代理对象是class com.llxazy.inter.MyCalculator$$EnhancerBySpringCGLIB$$1f93b605类型

  综上所述:在spring容器中,如果有接口,那么会使用jdk自带的动态代理,如果没有接口,那么会使用cglib的动态代理。动态代理的实现原理,后续会详细讲。

  4、切入点表达式

  在使用表达式的时候,除了之前的写法之外,还可以使用通配符的方式:

    *:

    1、匹配一个或者多个字符

      execution( public int com.llxazy.inter.My*alculator.*(int,int))

    2、匹配任意一个参数,

      execution( public int com.llxazy.inter.MyCalculator.*(int,*))

    3、只能匹配一层路径,如果项目路径下有多层目录,那么*只能匹配一层路径

    4、权限位置不能使用*,如果想表示全部权限,那么不写即可

      execution( * com.llxazy.inter.MyCalculator.*(int,*))

    ..:

    1、匹配多个参数,任意类型参数

      execution( * com.llxazy.inter.MyCalculator.*(..))

    2、匹配任意多层路径

      execution( * com.llxazy..MyCalculator.*(..))

    在写表达式的时候,可以有N多种写法,但是有一种最偷懒和最精确的方式:

      最偷懒的方式:execution(* *(..)) 或者 execution(* *.*(..))

      最精确的方式:execution( public int com.llxazy.inter.MyCalculator.add(int,int))

    

    除此之外,在表达式中还支持 &&、||、!的方式

    &&:两个表达式同时

      execution( public int com.llxazy.inter.MyCalculator.*(..)) && execution(* *.*(int,int) )

    ||:任意满足一个表达式即可

      execution( public int com.llxazy.inter.MyCalculator.*(..)) && execution(* *.*(int,int) )

    !:只要不是这个位置都可以进行切入

  5、通知方法的执行顺序

  1、正常执行:@Before--->@After--->@AfterReturning

  2、异常执行:@Before--->@After--->@AfterThrowing

  6、获取方法的详细信息

  获取Method的详细信息,例如方法名、参数列表等信息,想要获取的话其实非常简单,只需要添加JoinPoint参数即可。

  获取结果和异常信息,还需要添加另外一个方法参数,并且告诉spring使用哪个参数来进行结果接收

package com.llxazy.util;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

import java.util.Arrays;

@Component
@Aspect
public class LogUtil {

    @Before("execution( public int com.llxazy.inter.MyCalculator.*(int,int))")
    public static void start(JoinPoint joinPoint){
        Object[] args = joinPoint.getArgs();
        String name = joinPoint.getSignature().getName();
        System.out.println(name+"方法开始执行,参数是:"+ Arrays.asList(args));
    }

    @AfterReturning(value = "execution( public int com.llxazy.inter.MyCalculator.*(int,int))",returning = "result")
    public static void stop(JoinPoint joinPoint,Object result){
        String name = joinPoint.getSignature().getName();
        System.out.println(name+"方法执行完成,结果是:"+result);

    }

    @AfterThrowing(value = "execution( public int com.llxazy.inter.MyCalculator.*(int,int))",throwing = "exception")
    public static void logException(JoinPoint joinPoint,Exception exception){
      String name = joinPoint.getSignature().getName();
      System.out.println(name+"方法出现异常:"+exception);
    }

    @After("execution( public int com.llxazy.inter.MyCalculator.*(int,int))")
    public static void end(JoinPoint joinPoint){
        String name = joinPoint.getSignature().getName();
        System.out.println(name+"方法执行结束了......");
    }
}

    7、spring对通知方法的要求

    spring对于通知方法的要求并不是很高,你可以任意改变方法的返回值和方法的访问修饰符,但是唯一不能修改的就是方法的参数,会出现参数绑定的错误,

    原因在于通知方法是spring利用反射调用的,每次方法调用得确定这个方法的参数的值。

    8、表达式的抽取

      如果在实际使用过程中,多个方法的表达式是一致的话,那么可以考虑将切入点表达式抽取出来:

        a、随便生命一个没有实现的返回void的空方法

        b、给方法上标注@Potintcut注解

  @Pointcut("execution( public int com.llxazy.inter.MyCalculator.*(int,int))")
    public void myPoint(){}
    
    @Before("myPoint()")
    public static void start(JoinPoint joinPoint){
        Object[] args = joinPoint.getArgs();
        String name = joinPoint.getSignature().getName();
        System.out.println(name+"方法开始执行,参数是:"+ Arrays.asList(args));
    }

    9、环绕通知的使用

  环绕通知的执行顺序是优于普通通知的,具体的执行顺序如下:

    环绕前置-->普通前置-->目标方法执行-->环绕正常结束/出现异常-->环绕后置-->普通后置-->普通返回或者异常。

  但是需要注意的是,如果出现了异常,那么环绕通知会处理或者捕获异常,普通异常通知是接收不到的,因此最好的方式是在环绕异常通知中向外抛出异常。

package com.llxazy.util;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

import java.util.Arrays;

@Component
@Aspect
public class LogUtil {
    @Pointcut("execution( public int com.llxazy.inter.MyCalculator.*(int,int))")
    public void myPoint(){}
    
    /**
     * 环绕通知是spring中功能最强大的通知
     * @param proceedingJoinPoint
     * @return
     */
    @Around("myPoint()")
    public Object myAround(ProceedingJoinPoint proceedingJoinPoint){
        Object[] args = proceedingJoinPoint.getArgs();
        String name = proceedingJoinPoint.getSignature().getName();
        Object proceed = null;
        try {
            System.out.println("环绕前置通知:"+name+"方法开始,参数是"+Arrays.asList(args));
            //利用反射调用目标方法,就是method.invoke()
            proceed = proceedingJoinPoint.proceed(args);
            System.out.println("环绕返回通知:"+name+"方法返回,返回值是"+proceed);
        } catch (Throwable e) {
            System.out.println("环绕异常通知"+name+"方法出现异常,异常信息是:"+e);
        }finally {
            System.out.println("环绕后置通知"+name+"方法结束");
        }
        return proceed;
    }
}

    10、多切面运行的顺序

      在spring中,默认是按照切面名称的字典顺序进行执行的,但是如果想自己改变具体的执行顺序的话,可以使用@Order注解来解决,数值越小,优先级越高。

      

      如果需要添加环绕通知呢,具体的执行顺序又会是什么顺序呢?

      因为环绕通知在进行添加的时候,是在切面层引入的,所以在哪个切面添加环绕通知,那么就会在哪个切面执行。

三、Spring AOP基于xml的配置

  添加配置文件

<?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:context="http://www.springframework.org/schema/context"
       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/context
       http://www.springframework.org/schema/context/spring-context.xsd
       http://www.springframework.org/schema/aop
       https://www.springframework.org/schema/aop/spring-aop.xsd
">

    <context:component-scan base-package="com.llxazy"></context:component-scan>
    <aop:aspectj-autoproxy></aop:aspectj-autoproxy>

    <bean id="logUtil" class="com.llxazy.util.LogUtil2"></bean>
    <bean id="securityAspect" class="com.llxazy.util.SecurityAspect"></bean>
    <bean id="myCalculator" class="com.llxazy.inter.MyCalculator"></bean>
    <aop:config>
        <aop:pointcut id="globalPoint" expression="execution(public int com.llxazy.inter.MyCalculator.*(int,int))"/>
        <aop:aspect ref="logUtil">
            <aop:pointcut id="mypoint" expression="execution(public int com.llxazy.inter.MyCalculator.*(int,int))"/>
            <aop:before method="start" pointcut-ref="mypoint"></aop:before>
            <aop:after method="end" pointcut-ref="mypoint"></aop:after>
            <aop:after-returning method="stop" pointcut-ref="mypoint" returning="result"></aop:after-returning>
            <aop:after-throwing method="logException" pointcut-ref="mypoint" throwing="exception"></aop:after-throwing>
            <aop:around method="myAround" pointcut-ref="mypoint"></aop:around>
        </aop:aspect>
        <aop:aspect ref="securityAspect">
            <aop:before method="start" pointcut-ref="globalPoint"></aop:before>
            <aop:after method="end" pointcut-ref="globalPoint"></aop:after>
            <aop:after-returning method="stop" pointcut-ref="globalPoint" returning="result"></aop:after-returning>
            <aop:after-throwing method="logException" pointcut-ref="globalPoint" throwing="exception"></aop:after-throwing>
            <aop:around method="myAround" pointcut-ref="mypoint"></aop:around>
        </aop:aspect>
    </aop:config>
</beans>

 

 

 此只是学习过程中摘抄的笔记,方便后续复习时使用。

posted @ 2021-07-06 16:15  禾野牧村  阅读(153)  评论(0)    收藏  举报