blog
原创

Spring学习笔记——AOP

AOP

AOP是Aspect Oriented Programming 的缩写,意为面向切面编程,是通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术

AOP可以在程序运行期间,在不修改源码的情况下对方法进行功能增强,可以减少代码重复性,提高开发效率,便于维护

AOP的底层是通过Spring提供的动态代理技术实现的,在运行期间,Spring通过动态代理技术动态生成代理对象,代理对象方法执行时进行增强功能的介入,再去调用目标对象的方法,从而完成功能的增强

常用的动态代理技术

JDK代理

基于接口的动态代理技术

创建目标对象接口

public interface TargetInterface {
    public void save();
}

创建目标对象

public class Target implements TargetInterface {
    @Override
    public void save() {
        System.out.println("Save running...");
    }
}

创建增强方法

public class Advice {
    // 前置增强
    public void before() {
        System.out.println("Before....");
    }
    // 后置增强
    public void after(){
        System.out.println("After....");
    }
}

编写测试方法

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

public class ProxyTest {
    public static void main(String[] args) {
        // 创建目标对象
        Target target = new Target();
        // 获得增强对象
        Advice advice = new Advice();
        // 返回值为动态生成的代理对象
        TargetInterface proxy = (TargetInterface) Proxy.newProxyInstance(
                target.getClass().getClassLoader(), // 目标对象类加载器
                target.getClass().getInterfaces(), // 目标对象相同的接口字节码对象数组
                new InvocationHandler() {
                    // 调用代理对象的任何方法,实质上执行的都是 invoke 方法
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        // 前置增强
                        advice.before();
                        // 执行目标方法  目标对象 参数
                        method.invoke(target,args);
                        // 后置增强
                        advice.after();
                        return null;
                    }
                }
        );
        // 调用代理对象方法
        proxy.save();
    }
}

cglib代理

基于父类的动态代理技术

创建目标对象

public class Target {
    public void save() {
        System.out.println("Save running...");
    }
}

创建增强方法

public class Advice {
    public void before() {
        System.out.println("Before....");
    }
    public void after(){
        System.out.println("After....");
    }
}
实现步骤
  1. 创建增强器Enhancer (org.springframework.cglib.proxy)
Enhancer enhancer = new Enhancer();
  1. 设置父类

public void setSuperclass(Class superclass)

enhancer.setSuperclass(Target.class);
  1. 设置回调函数

public void setCallback(org.springframework.cglib.proxy.Callback callback)

enhancer.setCallback(new MethodInterceptor() {
    @Override
    // 相当于基于jdk动态代理中的 invoke 方法
    public Object intercept(Object proxy, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        // 执行前置增强
        advice.before();
        // 执行目标方法
        Object invoke = method.invoke(target, args);
        // 执行后置增强
        advice.after();
        return invoke;
    }
});
  1. 生成代理对象

public Object create()

Target proxy = (Target) enhancer.create();

完整代码

import cn.hhxy.prox.jdk.TargetInterface;
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;

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

public class ProxyTest {
    public static void main(String[] args) {
        // 创建目标对象
        Target target = new Target();
        // 获得增强对象
        Advice advice = new Advice();
        // 创建增强器
        Enhancer enhancer = new Enhancer();
        // 设置父类
        enhancer.setSuperclass(Target.class);
        // 设置回调函数
        enhancer.setCallback(new MethodInterceptor() {
            @Override
            // 相当于基于jdk动态代理中的 invoke 方法
            public Object intercept(Object proxy, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
                // 执行前置增强
                advice.before();
                // 执行目标方法
                Object invoke = method.invoke(target, args);
                // 执行后置增强
                advice.after();
                return invoke;
            }
        });
        // 生成代理对象
        Target proxy = (Target) enhancer.create();
        // 执行方法测试
        proxy.save();
    }
}

AOP相关概念

  • Target 目标对象:代理的目标对象
  • Proxy 代理:一个类被AOP织入后,就产生一个结果代理类
  • JoinPoint 连接点:连接点就是被拦截到的点,在Spring中,这些点指的是方法,在Spring中只支持方法类型的连接点(就是==可以被增强的方法==)
  • PointCut 切入点:切入点就是指我们要对哪些连接点进行拦截的定义
  • Advice 通知/增强:通知是指拦截到连接点之后要做的事情
  • Aspect 切面:是切入点通知的结合
  • Weaving 织入:是指把增强应用到目标对象来创建新的代理对象的过程。Spring采用动态代理织入,而AspectJ采用编译期织入类装载期织入

AOP开发需要明确的事项

需要编写的内容

  1. 编写核心业务代码(目标类的目标方法)
  2. 编写切面类,切面类中有通知
  3. 在配置文件中配置织入关系,即将哪些通知和哪些连接点进行结合

AOP技术实现的内容

Spring框架会监控切入点方法的执行,一旦监控到切入点方法被运行,使用代理机制,动态创建目标对象的代理对象,根据通知类别,在代理对象的对应位置,将通知的对应功能织入,完成完整的代码逻辑运行

AOP底层使用哪种代理方式

在Spring中,框架会根据目标类是否实现了接口来决定采用哪种动态代理方式

AOP要点

  • AOP意为面向切面编程
  • AOP的底层是基于JDK动态代理Cglib动态代理
  • AOP的重点概念
    • PointCut 切入点 —— 被增强的方法
    • Advice 通知/增强 —— 封装增强业务逻辑的方法
    • Aspect 切面 —— 切点 + 通知
    • Weaving 织入 —— 将切点与通知结合的过程
  • 开发明确事项:编写切点,编写通知,将切点和通知织入配置

基于XML的AOP开发

基本步骤

  1. 导入AOP相关坐标

    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.2.9.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjweaver</artifactId>
        <version>1.9.6</version>
    </dependency>
    
  2. 创建目标接口和目标类

    public interface TargetInterface {
        public void save();
    }
    
    public class Target implements TargetInterface {
        @Override
        public void save() {
            System.out.println("Save running...");
        }
    }
    
  3. 创建切面类(内有增强方法)

    public class MyAspect {
        public void before(){
            System.out.println("Before....");
        }
    }
    
  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"
           xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    
        <!-- 目标对象 -->
        <bean id="target" class="cn.hhxy.aop.Target"></bean>
    
        <!-- 切面对象 -->
        <bean id="myAspect" class="cn.hhxy.aop.MyAspect"></bean>
    
    </beans>
    
  5. applicationContext.xml中配置织入关系

    导入命名空间

    xmlns:aop="http://www.springframework.org/schema/aop"
    xsi:schemaLocation="http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"
    

    配置织入

    <!-- 配置织入 -->
    <!-- 告知Spring哪些方法需要哪些增强 -->
    <aop:config>
        <!-- 声明切面 -->
        <aop:aspect ref="myAspect">
            <!-- 切面 = 切点 + 通知 -->
            <aop:before method="before" pointcut="execution(public void cn.hhxy.aop.Target.save())"></aop:before>
        </aop:aspect>
    </aop:config>
    
  6. 代码测试

    import cn.hhxy.aop.TargetInterface;
    
    import org.junit.Test;
    import org.junit.runner.RunWith;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.test.context.ContextConfiguration;
    import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
    
    @RunWith(SpringJUnit4ClassRunner.class)
    @ContextConfiguration("classpath:applicationContext.xml")
    public class AopTest {
        @Autowired
        private TargetInterface targetInterface;
        @Test
        public void Test1(){
            targetInterface.save();
        }
    }
    

XML配置详解

<aop:config>:AOP配置

<aop:aspect>:声明一个切面,属性ref为Bean的引用

<aop:before>:配置通知

切点表达式的写法

execution( [修饰符] 返回值类型 包名.类名.方法名(参数) )

  • 访问修饰符可以省略
  • 返回值类型、包名、类名、方法名可以使用*代表任意
  • 包名与类名之间的.代表当前包下的类,两个点..表示当前包及其子包下的类
  • 参数列表可以使用两个点..表示任意个数、任意类型的参数列表
通知的种类

配置语法:<aop:通知类型 method = "切面中类方法名" pointcut = "切点表达式"></aop:通知类型>

名称 标签 描述
前置通知 <aop:before> 指定的增强方法在切入点方法之前执行
后置通知 <aop:after-returning> 指定的增强方法在切入点方法之后执行
环绕通知 <aop:around> 指定的增强方法在切入点方法之前和之后都执行
异常抛出通知 <aop:after-throwing> 指定的增强方法在出现异常时执行
最终通知 <aop:after> 无论增强方法执行是否有异常都会执行

切面类

import org.aspectj.lang.ProceedingJoinPoint;

public class MyAspect {

    // 前置通知
    public void before() {
        System.out.println("Before....");
    }

    // 后置通知
    public void afterReturning(){
        System.out.println("After Returning....");
    }

    // 环绕通知
    // ProceedingJoinPoint 切点
    public void around(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("Around Before....");
        Object proceed = joinPoint.proceed(); // 切点方法
        System.out.println("Around After....");
    }

    // 抛出异常通知
    public void afterThrowing(){
        System.out.println("Throwing....");
    }

    // 最终通知
    public void after(){
        System.out.println("After....");
    }
}

xml切面配置

<aop:config>
    <!-- 声明切面 -->
    <aop:aspect ref="myAspect">
        <!-- 切面 = 切点 + 通知 -->
        <aop:before method="before" pointcut="execution(* cn.hhxy.aop.*.*(..))"/>
        <aop:after-returning method="afterReturning" pointcut="execution(* cn.hhxy.aop.*.*(..))"/>
        <aop:around method="around" pointcut="execution(* cn.hhxy.aop.*.*(..))"/>
        <aop:after-throwing method="afterThrowing" pointcut="execution(* cn.hhxy.aop.*.*(..))"/>
        <aop:after method="after" pointcut="execution(* cn.hhxy.aop.*.*(..))"/>
    </aop:aspect>
</aop:config>

输出结果

Before....              // 前置通知
Around Before....       // 环绕前通知
Save running...         // 切点方法
After....               // 最终通知
Around After....        // 环绕后通知
After Returning....     // 后置通知

当在目标类中创造异常时输出

public class Target implements TargetInterface {
    @Override
    public void save() {
        System.out.println("Save running...");
        int i = 1 / 0;
    }
}
Before....                                            // 前置通知
Around Before....                                     // 环绕前通知
Save running...                                       // 目标方法
After....                                             // 最终通知
Throwing....                                          // 异常抛出通知

java.lang.ArithmeticException: / by zero              // 异常

切点表达式的抽取

当多个切点表达式相同时,可以将切点表达式进行抽取,在通知中使用pointcut-ref来代替pointcut属性来引用抽取后的切点表达式

使用<aop:pointcut>标签来声明一个切点表达式

<aop:pointcut id="myPointcut" expression="execution(* cn.hhxy.aop.*.*(..))"/>

使用抽取的切点表达式

<aop:before method="before" pointcut-ref="myPointcut"/>
<aop:after-returning method="afterReturning" pointcut-ref="myPointcut"/>
<aop:around method="around" pointcut-ref="myPointcut"/>
<aop:after-throwing method="afterThrowing" pointcut-ref="myPointcut"/>
<aop:after method="after" pointcut-ref="myPointcut"/>

基于XML的AOP开发总结

  • AOP织入配置
<aop:config>
	<aop:aspect ref="切面类">
    	<aop:通知类型 method="通知方法名" pointcut="切点表达式"></aop:通知类型>
    </aop:aspect>
</aop:config>
  • 通知类型:前置通知、后置通知、环绕通知、异常抛出通知、最终通知
  • 切点表达式写法:execution( [修饰符] 返回值类型 包名.类名.方法名(参数) )

基于注解的AOP开发

基本步骤

  1. 创建目标接口和目标类
public interface TargetInterface {
    public void save();
}
public class Target implements TargetInterface {
    @Override
    public void save() {
        System.out.println("Save running...");
    }
}
  1. 创建切面类
public class MyAspect {
    public void before() {
        System.out.println("Before....");
    }
}
  1. 将目标类和切面类的对象创建权交给Spring
import org.springframework.stereotype.Component;

@Component("target") // 目标类
public class Target implements TargetInterface {
    @Override
    public void save() {
        System.out.println("Save running...");
    }
}
import org.springframework.stereotype.Component;

@Component("myAspect") // 切面类
public class MyAspect {
    public void before() {
        System.out.println("Before....");
    }
}
  1. 在切面类中使用注解配置织入关系
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;

@Component("myAspect")
@Aspect // 标注当前类是一个切面类
public class MyAspect {
    @Before("execution(* cn.hhxy.anno.*.*(..))") // 配置前置通知
    public void before() {
        System.out.println("Before....");
    }
}
  1. 在配置文件中开启组件扫描和AOP的自动代理
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       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
       http://www.springframework.org/schema/context
       http://www.springframework.org/schema/context/spring-context.xsd">

    <!-- 开启组件扫描 -->
    <context:component-scan base-package="cn.hhxy.anno"/>

    <!-- 开启AOP自动代理 -->
    <aop:aspectj-autoproxy/>

</beans>
  1. 编写代码测试
import cn.hhxy.anno.TargetInterface;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext-anno.xml")
public class AnnoTest {
    @Autowired
    private TargetInterface targetInterface;
    @Test
    public void Test1(){
        targetInterface.save();
    }
}

注解通知的类型

配置语法:@通知注解("切点表达式")

名称 注解 描述
前置通知 @Before 指定的增强方法在切入点方法之前执行
后置通知 @AfterReturning 指定的增强方法在切入点方法之后执行
环绕通知 @Around 指定的增强方法在切入点方法之前和之后都执行
异常抛出通知 @AfterThrowing 指定的增强方法在出现异常时执行
最终通知 @After 无论增强方法执行是否有异常都会执行
注解通知切点表达式的抽取

同xml配置aop相同,我们可以将切点表达式抽取,方式是在切面内定义方法,在该方法上使用@Pointcut注解定义切点表达式,然后再通知注解内进行引用

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

@Component("myAspect")
@Aspect // 标注当前类是一个切面类
public class MyAspect {
    @Before("MyAspect.pointcut()") // 使用切点表达式
    public void before() {
        System.out.println("Before....");
    }
    @Pointcut("execution(* cn.hhxy.anno.*.*(..))") // 抽取切点表达式
    public void pointcut(){}
}
Spring
  • 作者:Melonico
  • 发表时间:2021-03-15 17:56
  • 更新时间:2021-03-15 17:56

评论

暂无评论,快来发表第一个评论吧!
留言
TOP