跳过正文
  1. 文章/
  2. Java/
  3. SpringFramework/
  4. Spring/

3、AOP

·5370 字·11 分钟· loading · loading · ·
Java SpringFramework Spring
GradyYoung
作者
GradyYoung
Spring - 点击查看当前系列文章
§ 3、AOP 「 当前文章 」

AOP
#

AOP的作用
#

AOP的最大作用:采用代码模式,将核心业务,与非核心业务进行分离关注

核心业务,我们采用纵向关注,而非核心业务,我们采用横向关注

交叉业务:不同的功能模块中,都拥有的业务(几乎都是非核心功能)

总结:AOP最终目的就是作用在方法上,并且对这个方法进行增强(添加逻辑代码)

AOP在项目的应用(面试题)
#

  1. 事务控制
  2. 日志记录
  3. 异常处理
  4. 敏感词过滤

AOP的名词
#

  • Joinpoint(连接点):切面中需要加入公共程序的地方!(某一个具体的要增强的方法)
  • Pointcut(切入点):符合某种规则的连接点,好多个连接点用对象化表示出来!(多个要增强的方法的组合)
  • Advice(通知):需要加入的公共程序!(要增强的逻辑代码),根据通知增强在方法上的位置不同分为不同的类型
    • 环绕通知:目标方法前/后调用,阻止方法调用
    • 前置通知:在目标方法调用
    • 后置通知:在目标方法调用
    • 异常通知:当目标方法抛出异常时调用
    • 返回通知:在我们的目标方法正常返回值后运行
  • Aspect(切面),在连接点上做的一系列行为!(多个要增强的方法添加增强逻辑后的结果)

切入点表达式
#

用来匹配需要增强的切入点(多个方法)

execution():用于匹配方法声明符合格式的方法 args():用于匹配方法参数为指定类型的执行方法 this():用于匹配当前AOP代理对象类型的执行方法;注意是AOP代理对象的类型匹配,这样就可能包括引入接口也类型匹配; target():用于匹配当前目标对象类型的执行方法;注意是目标对象的类型匹配,这样就不包括引入接口也类型匹配; within():用于匹配指定类型内的方法执行; @args():于匹配当前执行的方法传入的参数持有指定注解的执行; @target():用于匹配当前目标对象类型的执行方法,其中目标对象持有指定的注解; @within():用于匹配所以持有指定注解的类的方法; @annotation():用于匹配当前执行方法持有指定注解的方法

execution()
#

通过权限修饰符、方法返回值类型、方法所在类路径、方法名称、参数匹配方法

  • 固定格式:execution([权限修饰符][返回类型][类全路径][方法名称][参数列表])
  • execution(modifiers-pattern ret-type-pattern declaring-type-pattern name-pattern(param-pattern) throws-pattern)
  • modifiers-pattern:访问修饰符,可以省略
  • ret-type-pattern:返回类型,不能省略
  • declaring-type-pattern:类的类路径,可以省略
  • name-pattern:方法的名称,不能省略
  • param-pattern:参数列表,不能省略
  • throws-pattern:异常列表,可以省略
  • *:通配符
// 匹配top.ygang.service包下所有类的所有方法
execution(* top.ygang.service.*.*(..)) 
// 匹配所有public方法
execution(public * *(..))
// 匹配save开头的方法
execution(* save*(..))
// 匹配指定类的指定方法, 拦截时候一定要定位到方法
execution(public top.ygang.g_pointcut.OrderDao.save(..))
// 匹配指定类的所有方法
execution(* top.ygang.g_pointcut.UserDao.*(..))
// 匹配指定包,以及其子包下所有类的所有方法
execution(* top..*.*(..))
// || 和 or表示两种满足其一即可,取两个表达式的并集
execution(* top.ygang.g_pointcut.UserDao.save()) || execution(* top.ygang.g_pointcut.OrderDao.save())
execution(* top.ygang.g_pointcut.UserDao.save()) or execution(* top.ygang.g_pointcut.OrderDao.save())
// && 和 and表示两种都同时满足才行,取交集
execution(* top.ygang.g_pointcut.UserDao.save()) &&execution(* com.ygang.g_pointcut.OrderDao.save())
execution(* top.ygang.g_pointcut.UserDao.save()) and execution(* top.ygang.g_pointcut.OrderDao.save())
// 取非值, !和not表示不在该范围内的作为切点
!execution(* top.ygang.g_pointcut.OrderDao.save())
not execution(* top.ygang.g_pointcut.OrderDao.save())

args()
#

通过方法参数匹配

// 匹配方法第一个参数为User类型,剩余参数无限制
args(top.ygang.User,...)
// 匹配方法第一个参数为User类型,且仅有这一个参数
args(top.ygang.User)

Spring AOP
#

依赖
#

需要引入spring-aspects,才可以使用Spring AOP功能

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
    <version>5.2.9.RELEASE</version>
</dependency>

Spring AOP的底层原理(动态代理)
#

Spring AOP的底层原理主要依赖于动态代理技术。它使用了两种主要的代理方式:JDK动态代理和CGLIB代理。

需要注意的是,Spring AOP是基于代理的方式实现的,只能拦截被代理对象的外部方法调用。如果在目标对象内部方法中进行自我调用,那么切面和通知将不会被触发。

JDK动态代理
#

  • 当目标对象实现了接口时,Spring AOP使用JDK动态代理来生成代理对象。
  • JDK动态代理是通过java.lang.reflect.Proxy类和java.lang.reflect.InvocationHandler接口实现的。
  • 在运行时,通过Proxy.newProxyInstance()方法创建代理对象,并指定InvocationHandler来处理方法调用。
  • InvocationHandler中的invoke()方法在方法调用前后织入横切逻辑。

CGlib
#

  • 当目标对象没有实现接口时,Spring AOP使用CGLIB代理来生成代理对象。
  • CGLIB(Code Generation Library)是一个开源的字节码生成库,可以在运行时生成子类来代理目标对象。
  • CGLIB通过继承的方式创建代理类,重写目标类中的方法,并在方法调用前后织入横切逻辑。

Spring AOP的实现
#

基于XML配置文件
#

使用Spring提供的通知类型接口
#

创建Bean,以及需要被增强的方法

public class MyService {

    public void doSomeThing(String name){
        System.out.println("my service: doSomeThing");
        System.out.println(name);
    }
}

创建通知类,根据需求实现通知类型的接口即可

  • 环绕通知:org.aopalliance.intercepter.MethodInterceptor
  • 前置通知:org.springframework.aop.MethodBeforeAdvice
  • 后置通知:org.springframework.aop.AfterAdvice
  • 异常通知:org.springframework.aop.ThrowsAdvice
  • 返回通知:org.springframework.aop.AfterReturningAdvice
// 前置通知
public class TestAdvice implements MethodBeforeAdvice {

    @Override
    public void before(Method method, Object[] objects, Object o) throws Throwable {
        System.out.println("被代理的对象执行的方法:"+ method);
        System.out.println("被代理的对象执行的方法的参数是:"+ Arrays.toString(objects));
        System.out.println("被代理的对象是:"+ o);
    }
}

Spring配置文件中管理Bean以及定义切面

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<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 https://www.springframework.org/schema/aop/spring-aop.xsd"
>
    <bean id="myService" class="top.ygang.springdemo.MyService"/>

    <bean id="testAdvice" class="top.ygang.springdemo.TestAdvice"/>

    <aop:config>
        <aop:advisor advice-ref="testAdvice" pointcut="execution(* top.ygang.springdemo.MyService.*(..))"/>
    </aop:config>
</beans>

main方法测试

public static void main(String[] args) {
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
    MyService bean = applicationContext.getBean(MyService.class);
    bean.doSomeThing("grady");
}

输出结果

被代理的对象执行的方法:public void top.ygang.springdemo.MyService.doSomeThing(java.lang.String)
被代理的对象执行的方法的参数是:[grady]
被代理的对象是:top.ygang.springdemo.MyService@34b7ac2f
my service: doSomeThing
grady
自定义通知类
#

创建Bean,以及需要被增强的方法

public class MyService {

    public String doSomeThing(String name){
        System.out.println("my service: doSomeThing");
        System.out.println(name);
        return "returnMsg";
    }
}

编写自定义通知类,注意方法的形参

public class TestAdvice {

    public void beforeMethod(JoinPoint jp) {
        //获取被代理方法对象
        MethodSignature signature = (MethodSignature) jp.getSignature();
        Method method = signature.getMethod();

        System.out.println("前置通知");
        System.out.println("被代理的对象是:" + jp.getTarget());
        System.out.println("被代理的方法是:" + method);
        System.out.println("被代理的方法形参是:" + Arrays.toString(jp.getArgs()));

    }

    public void afterMethod(JoinPoint jp) {
        System.out.println("后置通知");
    }

    public void afterReturning(JoinPoint jp, Object c) {
        System.out.println("后置返回通知");
        System.out.println("被代理的方法返回值是:" + c);
        System.out.println(c);
    }

    public void afterThrowing(JoinPoint jp,Exception e) {
        System.out.println("异常通知");
        System.out.println("异常为:" + e);
    }

    public void around(ProceedingJoinPoint pjp) {
        try {
            System.out.println("环绕通知开始");
            Object a = pjp.proceed();
            System.out.println("环绕通知结束");
            System.out.println("被代理的方法返回值是:" + a);
        } catch (Throwable e) {
            e.printStackTrace();
        }
    }
}

Spring配置文件中管理Bean以及定义切面

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<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 https://www.springframework.org/schema/aop/spring-aop.xsd"
>
    <bean id="myService" class="top.ygang.springdemo.MyService"/>

    <bean id="testAdvice" class="top.ygang.springdemo.TestAdvice"/>

    <aop:config>
        <aop:pointcut id="pc" expression="execution(* top.ygang.springdemo.MyService.*(..))"/>
        <aop:aspect ref="testAdvice">
            <aop:before method="beforeMethod" pointcut-ref="pc"/>
            <aop:before method="afterMethod" pointcut-ref="pc"/>
            <aop:after-returning method="afterReturning" returning="c" pointcut-ref="pc"/>
            <aop:after-throwing method="afterThrowing" throwing="e" pointcut-ref="pc"/>
            <aop:around method="around" pointcut-ref="pc"/>
        </aop:aspect>
    </aop:config>
</beans>

main方法测试

public static void main(String[] args) {
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
    MyService bean = applicationContext.getBean(MyService.class);
    bean.doSomeThing("grady");
}

输出结果

前置通知
被代理的对象是:top.ygang.springdemo.MyService@a514af7
被代理的方法是:public java.lang.String top.ygang.springdemo.MyService.doSomeThing(java.lang.String)
被代理的方法形参是:[grady]
后置通知
环绕通知开始
my service: doSomeThing
grady
环绕通知结束
被代理的方法返回值是:returnMsg
后置返回通知
被代理的方法返回值是:null
null

基于注解
#

常用注解
#
注解 说明
@Aspect 把当前类声明为切面类(自定义通知类),该类需要再加@Component注解,这样才可以被spring容器管理
@Before 把当前方法看成是前置通知,value:用于指定切入点表达式,还可以指定切入点表达式的引用
@AfterReturning 把当前方法看成是后置返回通知,value:用于指定切入点表达式,还可以指定切入点表达式的引用
@AfterThrowing 把当前方法看成是异常通知,value:用于指定切入点表达式,还可以指定切入点表达式的引用
@After 把当前方法看成是后置通知,value:用于指定切入点表达式,还可以指定切入点表达式的引用
@Around 把当前方法看成是环绕通知,value:用于指定切入点表达式,还可以指定切入点表达式的引用
@Pointcut 指定切入点表达式,value:指定表达式的内容
@Order 用于多个通知增强同一个方法时,在通知类上设置优先级,value:数值类型,越小优先级越高

注意:要使用以上注解,必须在Spring配置文件中开启自动代理,对Spring AOP相关注解的支持<aop:aspectj-autoproxy/>

<!--  启动自动代理,开启对AOP注解的支持  -->
<aop:aspectj-autoproxy/>

该标签有几个属性可以选择

  • proxy-target-class(默认值为false):用于指定是否使用基于类的代理,默认情况下使用基于接口的代理。如果设置为true,则将使用CGLIB库创建基于类的代理。
  • expose-proxy(默认值为false):用于指定是否将代理对象公开给AopContext,以便在切面内部通过AopContext.currentProxy()方法访问。如果设置为true,则可以在切面内部访问代理对象。
代码示例
#

创建Bean,以及需要被增强的方法

@Component
public class MyService {

    public String doSomeThing(String name){
        System.out.println("my service: doSomeThing");
        System.out.println(name);
        return "returnMsg";
    }
}

创建切面类

@Aspect
@Component
public class TestAdvice {

    @Pointcut("execution(* top.ygang.springdemo.MyService.*(..))")
    public void pc(){}

    @Before("pc()")
    public void before(JoinPoint jp){
        //获取被代理方法对象
        MethodSignature signature = (MethodSignature) jp.getSignature();
        Method method = signature.getMethod();

        System.out.println("前置通知");
        System.out.println("被代理的对象是:" + jp.getTarget());
        System.out.println("被代理的方法是:" + method);
        System.out.println("被代理的方法形参是:" + Arrays.toString(jp.getArgs()));
    }
}

配置文件开启Spring各种注解的支持

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<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"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
      http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd"
>
    <context:component-scan base-package="top.ygang.*"/>
    <context:annotation-config/>

    <!--  启动自动代理,开启对AOP注解的支持  -->
    <aop:aspectj-autoproxy/>
</beans>

main方法测试

public static void main(String[] args) {
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
    MyService bean = applicationContext.getBean(MyService.class);
    bean.doSomeThing("grady");
}

输出结果

前置通知
被代理的对象是:top.ygang.springdemo.MyService@f2ff811
被代理的方法是:public java.lang.String top.ygang.springdemo.MyService.doSomeThing(java.lang.String)
被代理的方法形参是:[grady]
my service: doSomeThing
grady

AspectJ
#

AspectJ是一个面向切面的框架,不是Spring框架的一部分,可以单独使用,是目前最好用,最方便的AOP框架,和spring中的aop可以集成在一起使用,通过Aspectj提供的一些功能实现aop代理变得非常方便。

Spring AOP和AspectJ的区别
#

Spring AOP
#

  • 基于动态代理来实现,默认如果实现接口的,用JDK提供的动态代理实现,如果没有实现接口则使用CGLIB实现
  • Spring AOP需要依赖IOC容器来管理,并且只能作用于Spring容器,使用纯Java代码实现
  • 在性能上,由于Spring AOP是基于动态代理来实现的,在容器启动时需要生成代理实例,在方法调用上也会增加栈的深度,使得Spring AOP的性能不如AspectJ的那么好
  • 注意:Spring AOP归根结底也是动态代理,所以和AspectJ的关系并不大,只是引入了Aspect、advice、joinpoint等等概念

AspectJ
#

  • AspectJ来自于Eclipse基金会

  • AspectJ属于静态织入,通过修改代码来实现,有如下几个织入的时机:

    • 编译期织入(Compile-time weaving): 如类 A 使用 AspectJ 添加了一个属性,类 B 引用了它,这个场景就需要编译期的时候就进行织入,否则没法编译类 B。
    • 编译后织入(Post-compile weaving): 也就是已经生成了 .class 文件,或已经打成 jar 包了,这种情况我们需要增强处理的话,就要用到编译后织入。
    • 类加载后织入(Load-time weaving): 指的是在加载类的时候进行织入,要实现这个时期的织入,有几种常见的方法。1、自定义类加载器来干这个,这个应该是最容易想到的办法,在被织入类加载到 JVM 前去对它进行加载,这样就可以在加载的时候定义行为了。2、在 JVM 启动的时候指定 AspectJ 提供的 agent:-javaagent:xxx/xxx/aspectjweaver.jar。
  • AspectJ可以做Spring AOP干不了的事情,它是AOP编程的完全解决方案,Spring AOP则致力于解决企业级开发中最普遍的AOP(方法织入)。而不是成为像AspectJ一样的AOP方案

  • 因为AspectJ在实际运行之前就完成了织入,所以说它生成的类是没有额外运行时开销的

Spring - 点击查看当前系列文章
§ 3、AOP 「 当前文章 」