【Spring学习笔记】AOP开发及Spring配置详解

【Spring学习笔记】AOP及Spring配置详解

什么是AOP,如何开发AOP开发

AOP就是Aspect Oriented Programming,面向切面编程,是通过预编译方式和运行期间动态代理(不修改源码情况下对目标代码进行加强,实现解耦合)实现程序功能的统一维护的一种技术。

AOP的作用及其优势

AOP是OOP(面向对象编程)的延续,是Spring框架中的重要内容,是函数式编程的一种衍生范型,可以利用配置的方式让多个类同时具有某个功能。使用AOP可以降低业务逻辑各部分的耦合度,提高代码可重用性,同时提高开发效率。

  • 总结:
    • 作用:在程序运行期间,在不修改源码的情况下对方法进行加强
    • 优势:减少重复代码,提高开发效率,降低维护成本

AOP的底层实现

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

以下是其底层实现的代码,如果不是进阶深入的研究,以下代码了解即可。

AOP的动态代理技术

在Spring中,如果目标类实现了接口,那么使用JDK动态代理,否则使用cglib动态代理,其底层的运行机制如下图。

JDK代理:基于接口的动态代理

public class ProxyTest {
    public static void main(String[] args) {
        //目标对象
        final 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();//前置增强
                        Object invoke = method.invoke(target, args);//执行目标方法
                        advice.after();//后置增强
                        return invoke;
                    }
                }
        );
        //调用代理对象的方法
        proxy.save();
    }
}

cglib代理:基于父类的动态代理技术

public class ProxyTest {
    public static void main(String[] args) {
        //目标对象
        final Target target = new Target();
        //增强对象
        Advice advice = new Advice();

        //返回值就是动态代理生成的代理对象,基于cglib
        //1. 创建增强器
        Enhancer enhancer = new Enhancer();
        //2. 设置父类(目标)
        enhancer.setSuperclass(Target.class);
        //3. 设置回调
        enhancer.setCallback(new MethodInterceptor() {
            @Override
            public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
                advice.before();//执行前置
                Object invoke = method.invoke(target, args);//执行目标
                advice.after();//执行后置
                return invoke;
            }
        });
        //4. 创建代理对象
        Target proxy = (Target) enhancer.create();
        proxy.save();
    }
}

AOP相关概念

以上的代码可能看不太懂,但没关系。我们只需要知道,Spring的AOP实现底层就是对以上动态代理的代码进行了封装,封装后我们只需要对关注的部分进行编写,并通过配置的方式完成指定目标的方法增强即可,并不需要我们自己编写以上的代码。

但AOP还有相关的一些术语需要我们去掌握理解,这里将其列出来,我们先大致认识一下,在以后的学习过程中我们会越来越深入地理解这些概念:

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

AOP开发明确的事项

需要编写的内容

  • 编写核心业务代码(即编写切点,目标类的目标方法,要被增强的方法)
  • 编写切面类,切面类中包括增强方法(即编写增强)
  • 在配置文件中配置织入关系,即将那些增强和那些连接点进行结合

AOP技术实现的内容

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


基于xml 的AOP开发

开发步骤

  1. 导入AOP相关坐标
  2. 创建目标接口和目标类(内部有切点)
  3. 创建切面类(内部有增强方法)
  4. 将目标类和切面类的对象创建权交给Spring
  5. 在applicationContext.xml中配置织入关系
  6. 测试代码

导入AOP相关坐标

Spring本身虽然有其对AOP的实现,但相对而言,第三方的AOP配置AspectJ更为优秀,Spring官方也更推荐开发者使用AspectJ进行开发,因此此处在pom.xml中导入的是AspectJ。

<dependency>
	<groupId>org.aspectj</groupId>
	<artifactId>aspectjweaver</artifactId>
	<version>1.8.4</version>
</dependency>

创建目标接口和目标类

目标接口TargetInterface.java

public interface TargetInterface {
    public void save();
}

目标类Target.java

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

创建切面类

  • 增强语法及类型

    <!-- 语法,切点表达式在文章后面有说明到 -->
    <aop:增强类型 method="切面类中的方法名" pointcut="切点表达式" />
    
    名称 标签 说明
    前置增强 <aop:before> 指定的方法在切入点方法之前执行
    后置增强 <aop:after-returning> 指定的方法在切入点方法之后执行
    环绕增强 <aop:around> 指定的方法在切入点方法之前和之后都执行
    异常抛出增强 <aop:throwing> 指定的方法在切入点方法出现异常时执行
    最终增强 <aop:after> 指定的方法无论切入点方法是否出现异常都执行

    MyAspect.java

public class MyAspect {
    public void before() {
        System.out.println("前置增强...");
    }

    public void afterReturning() {
        System.out.println("后置增强...");
    }

    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        System.out.println("环绕前增强...");
        Object proceed = pjp.proceed();
        System.out.println("环绕后增强...");
        return proceed;
    }
    
    public void afterThrowing() {
        System.out.println("异常抛出增强...");
    }
    
    public void after() {
        System.out.println("最终增强...");
    }
}

将目标类和切面类的对象创建权交给Spring

将目标类和切面类的Bean对象放入Spring容器中,此时Spring将它们视为同等的Bean对象,而并不知道它们哪一个是目标对象,哪一个是切面对象,需要等后面织入关系配置完成之后Spring才能将它们区分。

applicationContext.xml

<!--  目标对象  -->
<bean id="target" class="com.water.aop.Target"></bean>
<!--  切面对象  -->
<bean id="myAspect" class="com.water.aop.MyAspect"></bean>
<!--  配置织入:哪些方法需要进行哪些增强(前置、后置...)  -->

配置织入关系

配置织入,就是告诉Spring哪些方法(切点)有哪些增强(前置、后置...)。
在配置织入关系之前,我们需要导入aop的命名空间,这在我之前一篇IOC学习笔记中有相关介绍,这里不展开细讲,下面来看一下怎么配置织入关系。
applicationContext.xml

<aop:config>
    <!-- 声明切面 -->
    <aop:aspect ref="myAspect">
        <!-- 切面:切点 + 增强 -->
        <!-- 这里使用around增强代替before和after-returning增强
            <aop:before method="before" pointcut="execution(public void com.water.aop.*.*(..))" />
            <aop:after-returning method="afterReturning" pointcut="execution(public void com.water.aop.*.*(..))" />
            -->
        <aop:around method="around" pointcut="execution(public void com.water.aop.*.*(..))"/>
        <aop:after-throwing method="afterThrowing" pointcut="execution(public void com.water.aop.*.*(..))"/>
        <aop:after method="after" pointcut="execution(public void com.water.aop.*.*(..))"/>
    </aop:aspect>
</aop:config>

可以看到,其中的切点表达式重复出现了很多次,于是就有了相当于的解决方案:将切点表达式抽取出来,下一次要用的时候直接进行引用。这样做的好处是方便维护和简化书写。所以,以上的xml配置还可以写成下面这样。

<aop:config>
    <!-- 声明切面 -->
    <aop:aspect ref="myAspect">
        <!-- 抽取切点表达式 -->
        <aop:pointcut id="myPointcut" expression="execution(public void com.water.aop.*.*(..))"/>
        <aop:around method="around" pointcut-ref="myPointcut"/>
        <aop:after-throwing method="afterThrowing" pointcut-ref="myPointcut"/>
        <aop:after method="after" pointcut-ref="myPointcut"/>
    </aop:aspect>
</aop:config>

pointcut中的特殊写法被称为切点表达式,可以指定多个切点,其写法如下:

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

在编写切点表达式的过程有几个点需要注意:

  • 访问修饰符和异常可省略

  • 返回值类型、包名、类名、方法名可以使用星号 * 来代表任意

  • 包名与类名之间一个点,代表当前包下的类,两个点..表示当前包及其子包下的类

  • 参数列表可以使用两个点..代表任意个数,任意类型的参数列表

下面是切点表达式的几个例子:

//指定方法
execution(public void com.water.aop.Target.method())
//任意方法
execution(void com.water.aop.Target.*)
//指定类下的任意方法(任意返回类型、任意参数)
execution(* com.water.aop.Target.*(..))
//指定包下的任意类的任意方法(任意返回类型、任意参数),最常用
execution(* void com.water.aop.*.*(..))
//指定包及其子包下的任意类的任意方法(任意返回类型、任意参数)
execution(* void com.water.aop..*.*(..))
//博君一笑
execution(* void com.water.aop..*.*(..))

测试

@RunWith(SpringJUnit4ClassRunner.class)			//让测试在Spring容器环境下执行
@ContextConfiguration("classpath:applicationContext.xml")	//加载配置文件
public class AppTest {
    @Autowired		//自动注入
    private TargetInterface target;

    @Test
    public void test1() {
        target.save();
    }
}

基于注解的AOP开发

知道了如何通过XML进行AOP开发,我们过渡到注解开发就会变得相当容易了,不再需要各种复杂的配置,只需要简单记住几个注解的用法即可。

AOP的注解开发步骤

  1. 使用@Aspect标注切面类
  2. 使用@通知(增强)注解标注通知方法
  3. 在配置文件中配置组件扫描和AOP自动代理<aop:aspectj-autoproxy/>
  4. 测试

标注切面类和通知方法

为了让Spring知道哪些类是切面,我们会使用注解标明切面类:

@Component("myAspect")	//使用注解配置Bean对象,切面类一般也配置为@Controller
@Aspect
  • 注解通知(增强)的语法及类型:
@注解通知("切点表达式")
名称 注解 说明
前置通知 @Before 指定的方法在切入点方法之前执行
后置通知 @AfterReturning 指定的方法在切入点方法之后执行
环绕通知 @Around 指定的方法在切入点方法之前和之后都执行
异常抛出通知 @AfterThrowing 指定的方法在切入点方法出现异常时执行
最终通知 @After 指定的方法无论切入点方法是否出现异常都执行
  • 对切点表达式进行抽取

同XML配置AOP一样,我们也可以对切点表达式进行抽取。抽取方法是在切面内定义方法(空方法即可),在该方法上使用@Pointcut注解定义切点表达式,然后在增强注解中进行引用。

@Component("myAspect")
@Aspect
public class MyAspect {
	@Before("MyAspect.myPoint()")
    public void before() {
        System.out.println("前置增强...");
    }
    @Pointcut("execution(* com.water.anno.*.*(..))")
    public void myPoint() {}
}

配置组件扫描和AOP自动代理

<!--  组件扫描  -->
<context:component-scan base-package="com.water.anno"/>
<!--  AOP自动代理  -->
<aop:aspectj-autoproxy/>
posted @ 2021-06-03 16:58  Tuzkizki  阅读(243)  评论(0编辑  收藏  举报