03Spring AOP

0、传统开发中的问题–为什么引入AOP

在学习AOP之前,我们先看一个问题,就是数据库事务的控制问题:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hD0IQ5DG-1579010054247)(02.png)]
**在转账业务中,由于要考虑各种各样出现的问题,所以必须加事务的控制,而对业务的各种方法必须加上重复的代码,造成代码的冗余,因此考虑是否能把重复的代码抽取出来,并在恰当的时候插入到待执行业务代码中呢?把上述的描述可以抽象为以下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jn8S090E-1579010054249)(05.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VZLRivr1-1579010054250)(03.png)]

1、什么是AOP

AOP的全程是Aspect-Oriented Programmming,即面向切面编程(也称面向方面编程),是面向对象编程(OOP)的一种补充,目前已经成为一种比较成熟的编程方式。


在传统业务处理代码中,通常都会进行事务处理,日志记录等操作。虽然使用OOP可以通过组合或者继承的方式来达到代码的重用吗,但如果要实现某个功能(如记录日志),相同的代码仍然会分散到各个方法中。这样,如果想要关闭某个功能,或者对其进行修改,就必须修改所有相关方法。这不但增加了开发人员的工作量,而且提高了代码的出错率


为了解决这一问题,AOP思想随之产生。AOP采取横向抽取机制,将分散在各个方法中的重复代码抽取出来,然后再程序编译或运行时再讲这些抽取出来的代码应用到需要执行的地方,这种采用横向抽取机制的方式,采用传统的OOP思想显然是无法办到的,因为OOP只能实现父子关系的纵向重用。虽然AOP是一种新的编程思想,但却不是OOP的替代品,它只是OOP的延伸和补充。


在AOP思想中,通过Aspect(切面)可以分别在不同类的方法中加入事务,日志,权限和异常等功能。

AOP的使用使开发人员在编写业务逻辑时可以专心于核心业务,而不用过多的关注其他业务逻辑的实现,这不但提高了开发效率,而且增强了代码的可维护性**


目前流行的AOP框架有两个,分别是Spring AOP和AspectJ.Spring AOP使用纯Java实现,不需要专门的编译过程和类加载器,在运行期间通过代理方式向目标类植入增强的代码。AspectJ是一个基于Java语言的AOP框架。


[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xsjlE0a0-1579010054250)(06.png)]


1.1、AOP的作用和优势

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zfJacOqm-1579010054251)(07.png)]



DI能够让相互协作的软件组件保持松散耦合,而面向切面编程(aspect-oriented programming,AOP)允许你把遍布应用各处的功能分离出来形成可重用的组件。

面向切面编程往往被定义为促使软件系统实现关注点的分离的一项技术。系统由许多不同的组件组成,每一个组件各负责一块特定的功能。除了实现自身核心的功能之外,这些组件还经常承担着额外的职责。诸如日志,事务管理和安全这样的系统服务经常融入到自身具有核心业务逻辑组件中去,这些系统服务通常称为横切关注点,因为它们会跨越系统的多个组件。

如果将这些关注点分散到多个组件中去,你的代码将会带来多重的复杂性:

  • 实现系统关注点功能的代码将会重复出现在多个组件中。这意味着如果要修改这些关注点的逻辑,必须修改各个模块中的相关实现。即使把这些关注点抽象为一个独立的模块,其他模块只是调用它的方法,但方法调用还是会重复出现在各个模块中。
  • 组件会因为那些与自身核心业务无关的代码而变得混乱。一个向地址簿增加地址条目的方法应该只关注如歌添加地址,而不应该关注它是不是安全的或者是否需要支持事务。

AOP能够使这些服务模块化,并以声明的方式将他们应用到它们需要影响的组件中去。所造成的结果就是这些组件会具有更高的内聚性并且会更加关注自身的业务,完全不需要了解系统服务所带来的复杂性。总之,AOP能够确保POJO的简单性,

2、AOP原理:基于动态代理

在了解AOP是怎么来的,以及AOP的作用和优势之后我们再来看看AOP是怎么实现的,也就是AOP的底层原理是什么么?

查看官方的文档,我们看看AOP是怎么实现的?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-82r9Twk9-1579010054252)(08.png)]

通过以上分析,可以看出AOP的底层原理就是基于动态代理的,那么结合前面的IOC容器,可以看出 IOC容器使用了一种工厂模式,而AOP使用了代理模式。
AOP的代理模式分为两种,第一种是基于JDK动态代理,如果代理接口实现类使用这种,而如果是没有实现接口的类要实现代理采用CGLIB代理的方式。

2.1JDK代理

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DwXfxMnS-1579010054252)(09.png)]


[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uk0uW9in-1579010054253)(10.png)]

public class Client {

public static void main(String[] args) {

    //被代理对象
    final Producer producer = new Producer();

    /**
     * 动态代理特点:
     * 特点:随用随创建,随用随加载
     * 作用:不修该源码基础上对方法增强
     * 分类:
     *          基于接口动态代理
     *           基于子类动态代理
     * 基于接口动态代理:
     *          涉及的类:Proxy
     *          提供者:JDK官方
     *
     *          如何创建代理对象:
     *                  使用Proxy类中的newProxyInstance方法
     *
     *           创建代理对象的要求:
     *                  被代理类至少实现一个接口,如果没有则不能调用
     *
     *
     *
     *                  public static Object newProxyInstance(ClassLoader loader,
                                    Class<?>[] interfaces,
                                    InvocationHandler h)
     *  newProxyInstance 方法的参数:
     *              classLoader:类加载器,它是用于加载代理对象字节码的,和被代理对象使用相同的类加载器:固定写法
     *              Class[]:字节码数组,它是用于让代理对象和被代理对象有相同的方法(有相同的接口即可):固定写法
     *              InvocationHandler:用于提供增强的代码;它是让我们写如何代理。
     *                                我们一般都是写一个该接口的实现类,通常情况下都是匿名内部类,但不是必须的。
     *                                此接口的实现类,一般谁用谁写
     *
     */
    //被代理对象
    Iproducer iproducer =

            (Iproducer) Proxy.newProxyInstance(producer.getClass().getClassLoader(), producer.getClass().getInterfaces(), new InvocationHandler() {
        /**
         * 作用执行被代理对象的任何接口方法都会经过该方法,该方法有拦截的功能
         * @param proxy   代理对象的引用 (一般不用)
         * @param method  当前执行的方法
         * @param args    当前执行方法所需要的参数
         * @return     和被代理对象有相同的返回值
         * @throws Throwable
         */

        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            //提供增强的代码
            Object returnValue = null;
            //1、获取方法执行的参数
            Float money = (Float)args[0];
            //2、判断当前方法是不是销售
            if("saleProduct".equals(method.getName()))
            returnValue = method.invoke(producer,money*0.8f);
            return returnValue;
        }
    });
       iproducer.saleProduct(10000f);

}

2.2CGLIB代理

import com.iyheima.Iproducer;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

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

/**
 * @Author Zhou  jian
 * @Date 2019 ${month}  2019/11/21 0021  16:30
 * 模拟一个消费者
 */
public class Client {

public static void main(String[] args) {
    final Producer producer = new Producer();

    /**
     * 动态代理特点:
     * 特点:随用随创建,随用随加载
     * 作用:不修该源码基础上对方法增强
     * 分类:
     *          基于接口动态代理
     *           基于子类动态代理
     * 基于子类动态代理:
     *          涉及的类:Enhancer
     *          提供者:第三方 Cglib
     *
     *          如何创建代理对象:
     *                  使用Enhancer类中的create方法
     *
     *           创建代理对象的要求:
     *                被代理类不能是最终类
     *
     *      create方法的参数:
     *                  class:字节码
     *                          它是指被代理对象的字节码
     *
     *                   callBack:用于提供增强的方法,我们一般写的都是该接口的子接口的实现类:MethodInterCeptor
     *                      特点:执行被代理对象的任何方法都会经过该方法
     *
     */

   Producer cglibProducer =(Producer) Enhancer.create(producer.getClass(), new MethodInterceptor() {
        /**
         *特点:执行被代理对象的任何方法都会经过该方法
         * @param proxy:代理对象的引用 (一般不用)
         * @param method 当前执行的方法
         * @param args   当前执行方法所需要的参数
         * @param methodProxy  当前执行方法的代理对象 (用不上)
         * @return
         * @throws Throwable
         */
        public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
            //提供增强的代码
            Object returnValue = null;
            //1、获取方法执行的参数
            Float money = (Float)args[0];
            //2、判断当前方法是不是销售
            if("saleProduct".equals(method.getName()))
                returnValue = method.invoke(producer,money*0.8f);
            return returnValue;
        }
    });
    cglibProducer.saleProduct(10000f);
}
	

3、AOP使用

在使用AOP之前声明一些术语:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ymiEThD5-1579010054254)(04.png)]


使用AspectJ实现AOP有两种方式:一种是基于XML的生明式;另一种是基于注解式的声明。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NY3BvXED-1579010054254)(13.png)]
在使用前导入相关的jar包:

 <dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.0.2.RELEASE</version>
    </dependency>

    <!--  解析切入点表达式-->
    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjweaver</artifactId>
        <version>1.8.7</version>
    </dependency>
	</dependencies>

在配置文件中,引入配置声明头文件:

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

3.1、基于XML的AOP使用

配置步骤总结:

	1、把通知Bean也交给spring来管理
    2、使用 aop:config标签 表明开始aop的配置
    3、使用 aop:aspect表明开始配置切面
            id:是给切面提供唯一标志
            ref:指定通知类的bean的id
    4、在  aop:aspect标签的内部使用对应的标签来配置通知的类型
            现在的示例是让logger方法printLogg在切入点之前执行,所以前置通知aop:before
            aop:before:表示配置前置通知
            method属性:用于指定logger类中哪个方法时前置通知

​ pointcut:用于指定切入点表达式,该表达式的含义是对业务层哪些方法增强

基于XML的声明式AspectJ是指通过XML文件来定义切面、切入点及通知,所有的切面、切入点和通知都必须定义在aop:config元素内。Spring配置文件中的元素下可以包含多个aop:config元素,一个aop:config元素中又可以包含属性和子元素,其子元素包括aop:pointcutaop:advisoraop:aspect.

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uU0f2OMP-1579010054255)(11.png)]
(1)配置切面
在Spring的配置文件中,配置切面使用的是aop:aspect元素,该元素会将一个已定义好的Spring Bean转换成切面Bean,所以要在配置文件中先定义一个普通的SpringBean,定义完成之后,通过aop:aspecct元素的ref属性即可饮用该Bean.
(2)配置切入点
在Spring的配置文件中,切入点是通过aop:pointcut元素来定义的。当aop:pointcut元素作为aop:config元素的子元素定义时,表示该切入点是全局切入点,可以被多个切面所共享;当aop:pointcut元素作为aop:aspect元素的子元素时,表示该切入点只能对当前切面有效。

属性名称 描述
id 用于定义切入点的唯一标志名称
expression 用于指定切入点关联的切入点表达式
 切入点表达式的写法:
                    关键字: execution(表达式)
                    表达式:
                            访问修饰符 返回值 包名。包名.类名.方法名.(参数列表)
                            public     void   com.itheima.service.Impl.AccountServiceImpl. saveAccount()
                                    ①访问修饰符可以省略
                                            void   com.itheima.service.Impl.AccountServiceImpl. saveAccount()
                                    ②返回值可以使用通配符表示任意返回值
                                            *  com.itheima.service.Impl.AccountServiceImpl. saveAccount()
                                    ③包名可以使用通配符表示任意包,但是有几级包就需要些几个*
                                            * *.*.*.*.AccountServiceImpl.saveAccount()
                                    ③可以使用..表示当前包及其自爆
                                            * *..AccountServiceImpl.SaveAccount()
                                    ⑤类名和方法名都可以使用*实现通配
                                            * *..* *()
                                     ⑥参数列表可以直接书序类型:
                                                    基本类性可以直接写名称
                                                    引用类型可以写包名.类名的方式
                                      类型可以使用通配符,可以使用任意类型但必须有参数:*
                                      可以使用 ..表示有误参数均可
                            全通配写法:
                                        * *..*.*(..)
    实际开发中切入点表达式,切到业务层实现类下所有方法
      * com.itheima.Service.Impl.*.*(..)

(3)配置通知
在配置代码中,分别使用aop:aspect的子元素配置了5种常用通知,这些子元素不支持再使用子元素。但在使用时,可以指定一些属性:

属性名称 描述
pointcut 用于指定一个切入点表达式,Spring将在匹配该表达式的连接点时植入该通知
pointcut-ref 指定一个已经存在的切入点名称,通常poincut和point-ref两个属性只需要使用其中之一
method 指定一个方法名,指将切面Bean中的该方法转换为增强处理
throwing

通知的种类:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PM0pTniP-1579010054256)(12.png)]
配置环绕通知的注意事项:

		/**
     * 环绕通知
     *
     * 问题:当我们配置了环绕通知之后,切入点方法没有执行,而通知方法执行了。
     * 分析:
     *          通过对比动态代理中的环绕通知代码,发现动态代理放入环绕通知有明确的切入点方法调用,而我们的代码中没有
     *
     *  Spring框架提供了一个接口:  ProceedingJoinPoint,该接口有一个方法proceed(),此方法就相当于明确调用切入点方法。
     *                              该接口可以作为环绕通知的方法参数,在程序执行时,Spring框架为我们提供改接口的实现类,为我们使用。

            Spring中的环绕通知:
                        它是Spring框架为我们提供的一种方式:可以在代码中手动控制增强方法何时执行的方式。
                            对应配置的方式;
 */
public Object aroundPrintLog(ProceedingJoinPoint pjp){
    Object rtValue = null;
    try {
        Object[] args = pjp.getArgs();//得到方法执行所需要的参数
        System.out.println("Logger类中的aroundPrintLog方法开始记录日志...前置");
        rtValue = pjp.proceed(args);//明确调用业务层方法(切入点方法)
        System.out.println("Logger类中的aroundPrintLog方法开始记录日志..后置");
        return rtValue;
    } catch (Throwable throwable) {
        throwable.printStackTrace();
        System.out.println("Logger类中的aroundPrintLog方法开始记录日志..异常");
        throw new RuntimeException("");
    }finally {
        System.out.println("Logger类中的aroundPrintLog方法开始记录日志..最终");
    }
}

3.2、基于注解的AOP使用

基于XML声明式ASPECTJ实现AOP编程虽然便捷,但存在一些缺点,那就是要在Spring文件中配置大量的代码信息,为了解决这个问题,AspectJ框架为AOP的实现提供了一套注解,用以取代Spring配置文件中为实现AOP功能所配置的臃肿代码:

注解名称 描述
@Aspect 用于定义一个切面
@Pointcut 用于定义点表达式,在使用时还需定义一个包含名字和任意参数的方法签名来表示切入点名称。实际上,这个方法签名就是一个返回值为void且方法体为空的普通方法
@Before 用于定义前置通知,相当于BeforeAdvice,在使用时,通常需要制定一个value属性值,该属性值用于指定一个切入点表达式(可以是已有的切入点,也可以直接定义切入点表达式)
@AfterReturing 用于定义后置通知,相当于AfterReturingAdvice。在使用时可以指定pointcut.
@Around 用于定义环绕通知,相当于MethodInterceptor,在使用时需要指定一个Value属性,该属性用于指定通知被植入的切入点
@AfterThrowing 用于定义异常通知来处理城西中未处理的异常
@After 用于定义最终final通知,无论是否有异常,该通知都会执行。

在使用前需要在xml配置文件中开启以下:

  <!--配置Spring创建容器时要扫描的包-->
    <context:component-scan base-package="com.itheima"></context:component-scan>

    <!--配置spring开启注解Aop的支持-->
    <aop:aspectj-autoproxy></aop:aspectj-autoproxy>

	package com.itheima.utils;

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

/**
 * @Author Zhou  jian
 * @Date 2019 ${month}  2019/11/21 0021  19:45
    用于记录日志的工具类,它里面提供了公共的代码
 */
@Component("logger")
@Aspect  //表述当前类是一个切面类
public class Logger {

@Pointcut("execution(* com.itheima.service.Impl.*.*(..))")
private void pt1(){}

    /**
     * 前置通知用于打印日志,计划让其在切入点方法之前执行(切入点方法就是业务层方法
     */
    @Before("pt1()")
    public void beforeprintLog(){
        System.out.println("前置通知Logger类中的printLog方法开始记录日志");
    }
    
        /**
     * 后置通知
     */
    @AfterReturning("pt1()")
    public void afterReturingprintLog(){
        System.out.println("后置通知Logger类中的afterprintLog方法开始记录日志");
    }
    
	    /**
     * 异常通知
     */
    @AfterThrowing("pt1()")
    public void afterThrowingprintLog(){
        System.out.println("异常通知Logger类中的afterReturingprintLog方法开始记录日志");
    }
    
    
        /**
     * 最终通知
     */
    @After("pt1()")
    public void afterprintLog(){
        System.out.println("最终通知Logger类中的afterprintLog方法开始记录日志");
    }

    /**
     * 环绕通知
     *
     * 问题:当我们配置了环绕通知之后,切入点方法没有执行,而通知方法执行了。
     * 分析:
     *          通过对比动态代理中的环绕通知代码,发现动态代理放入环绕通知有明确的切入点方法调用,而我们的代码中没有
     *
     *  Spring框架提供了一个接口:  ProceedingJoinPoint,该接口有一个方法proceed(),此方法就相当于明确调用切入点方法。
     *                              该接口可以作为环绕通知的方法参数,在程序执行时,Spring框架为我们提供改接口的实现类,为我们使用。

                Spring中的环绕通知:
                            它是Spring框架为我们提供的一种方式:可以在代码中手动控制增强方法何时执行的方式。
                                对应配置的方式;
     */
   // @Around("pt1()")
    public Object aroundPrintLog(ProceedingJoinPoint pjp){
        Object rtValue = null;
        try {
            Object[] args = pjp.getArgs();//得到方法执行所需要的参数
            System.out.println("Logger类中的aroundPrintLog方法开始记录日志...前置");
            rtValue = pjp.proceed(args);//明确调用业务层方法(切入点方法)
            System.out.println("Logger类中的aroundPrintLog方法开始记录日志..后置");
            return rtValue;
        } catch (Throwable throwable) {
            throwable.printStackTrace();
            System.out.println("Logger类中的aroundPrintLog方法开始记录日志..异常");
            throw new RuntimeException("");
        }finally {
            System.out.println("Logger类中的aroundPrintLog方法开始记录日志..最终");
        }
    }

4、官方文档对应部分的阅读

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-42lkOc7D-1579010054257)(14.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-x8sVskuj-1579010054257)(15.png)]

发布了101 篇原创文章 · 获赞 17 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/ZHOUJIAN_TANK/article/details/103980603