Bootstrap

Spring AOP编程初级

Spring AOP编程

前言

Spring框架最大的特点封装了大量的设计模式

前面的工厂部分 封装了 工厂模式

而我们要学习的AOP编程 它封装了代理设计模式

1 静态代理设计模式

1.1 为什么需要代理设计模式

1.1.1 问题

  • JavaEE 分层开发中,哪个层次对于我们来讲最重要。

    DAO---->Service---->Controller
    JavaEE分层开发中,最为重要的使Service层
    
1.1.1 Service层都包含了那些代码?
Service层中.核心功能(几十行,上百行代码)+额外功能(附加功能)
	1.核心功能
	业务运算
	DAO调用
	2.额外功能
	1.不属于业务
	2.可有可无
	3.代码量很少
	事务,日志,性能.....
----------------------------------------

核心功能
	业务,运算
	DAO的调用
	代码量 几十,上百行代码
额外功能
	事务
	日志
	性能
	不属于核心业务
	可有可无
    代码量 少

image-20200908152454102

额外功能

1.1.1.2 日志

记录用户重要操作的流水账

image-20200908154018177

例如 :用户于2020-09-08转账100给用户B success

1.1.1.3 性能

记录开始时间,记录结束时间 ,结束-开始 核心业务所用时间

image-20200908154740942

1.1.2 额外功能书写在Service层中好不好?
Service层调用者的角度:(Controller):需要在Service层书写额外功能.
	软件设计者:Service层不需要额外功能(代码入侵)
  • 现实生活中的解决方法image-20200908160526971

    • 房东贴广告 带客户看房太累了.
  • 引入一个代理(中介)类

    • 1.额外功能
    • 调用目标类(原始类)核心功能

image-20200908162751784

  • 当对中介不满意时,换一个新中介即可.

1.2 代理设计模式

1.2.1 概念
通过代理类,为原始类(目标)增加新的额外功能
好处:利于原始类(目标)的维护
1.2.2 名词解释
1. 目标类 原始类
指的是 业务类(核心功能-----> 业务运算 DAO运算)
2. 目标方法,原始方法
目标类(原始类)中的方法 就是目标方法(原始方法)
3. 额外功能(附加功能)
日志 事务 性能
1.2.3 代理开发的核心要素
代理类 = 目标类(原始类) + 额外功能 +原始类(目标类)实现相同的接口

房东----->public interface UserService{
			m1
			m2
		}
		public class UserServiceImpl implements UserService{
			m1 --->业务运算 DAO调用
			m2 
		}
		UserServiceProxy implements UserService{
			m1
			m2
		}

image-20200908164104623

image-20200908164351825

1.2.4 编码

image-20200908175640985

image-20200908175623864

1.2.5 静态代理存在的问题
1. 静态类文件数量过多,不利于项目管理
UserServiceImpl UserServiceProxy
OrderServiceImpl OrderServiceProxy
如果有100个原始类 就得有100个对应的代理类
2. 额外功能维护性差
代理类中 额外功能修改复杂(麻烦)

2 动态代理开发

2.1 Spring动态代理的概念

概念:通过代理类为原始类(目标类)增加额外功能
好处:利于原始类(目标类)的维护

2.2 搭建开发环境

<dependency>
       <groupId>org.aspectj</groupId>
       <artifactId>aspectjweaver</artifactId>
       <version>1.9.6</version>
</dependency>
<!--核心包 包括aop-->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.2.8.RELEASE</version>
</dependency>

2.3 Spring动态代理的开发步骤

  1. 创建原始对象(目标对象)

    public class OrderServiceImpl implements OrderService{
    
        @Override
        public void showOrder() {
            System.out.println("showOrder");
        }
    }
    
    
    <bean class="com.leetao.proxy.OrderServiceImpl" id="orderService"/>
    
    
  2. 额外功能

    MethodBeforeAdvice接口

    额外的功能书写在接口的实现中.运行在原始方法执行之前执行额外功能.
    
    public class Before implements MethodBeforeAdvice {
    
        @Override
        public void before(Method method, Object[] args, Object target) throws Throwable {
            System.out.println("------method before--------");
        }
    }
    
    <bean class="com.leetao.ynamic.Before" id="before" />
    
  3. 定义切入点

    切入点:额外功能加入的位置
    目的:由程序员根据自己的需要,决定额外功能加入给那个初始方法
    
    
     <aop:config>
         <aop:pointcut id="pc" expression="execution(* *(..))"/>
    </aop:config>
    

    4.组合(2,3整合)

    表达的含义:所有的方法 都加入 before的额外功能
    <aop:advisor advice-ref="before" pointcut-ref="pc"/>
    

    5.调用

    目的:获得Spring工厂创建的动态代理对象,并进行调用
    ApplicationContext ctx = new ClassPathXmlApplicationContext("/applicationContext.xml");
    注意:
    	1.Spring的工厂通过原始对象的id值获取的是代理对象
    	2.获得代理对象后,可以通过声明接口类型,进行对象的存储
            UserService userService = (UserService)ctx.getBean("userService");
    

2.4 动态代理的细节

  1. Spring创建的动态代理类在哪里?

    Spring框架在运行时,通过动态字节码技术,在JVM创建的,运行在JVM内部,等程序结束后,会和JVM一起消失
    
    什么叫动态字节码技术:通过第三方动态字节码框架,在jvm中创建对应类的字节码,进而创建对象,当虚拟机结束,动态字节码跟着消失
    	第三方的动态字节码框架
    	ASM
    	Javassist
    	cglib
    	直接在jvm生成字节码(动态字节码)
    	动态代理类 ---->动态字节码
    结论:动态代理不需要定义类文件,都是JVM运行过程中动态创建的.所以不会造成静态代理,类文件数量过多,影响项目于管理的问题
    

    image-20200908215348008

  2. 动态代理编程简化代理的开发

在额外功能不改变的前提下,创建其他目标类(原始类) 的代理对象时,只需要指定原始(目标)对象即可.

3.动态代理额外功能维护性大大提升

3 Spring动态代理详解

image-20200908225503885

3.1 额外功能详解

image-20200908225810534

1. MethodBeforeAdvice接口分析

作用:需要把运行在原始方法执行之前运行的额外功能,书写在before方法中

Method : 增加额外功能的原始方法
		login
		register
		
Object[]: 增加额外功能的原始方法的传入参数

Object:原始对象

2. before方法的三个参数,在实战中如何使用
before 方法的参数 在实战中,会根据需要进行使用,不一定都会用到,也有可能都不用.	

4 . MethodInterceptor(方法拦截器)

MethodBeforeAdvice只支持在方法执行之前

MethodInterceptor接口:额外功呢给跟据需要运行在原始方法执行的 前,后 前后

public class Arround implements MethodInterceptor {


    /**
     * invoke 方法的作用:额外功能书写在invoke
     *                  额外功能    原始方法之前
     *                              原始方法之后
     *                              原始方法之前之后
     *        确定:原始方法怎么运行
     *        参数:MethodInvocation:原始方法 相当于Method
     *
     *          try{
     *              //TODO 前置增强
     *              Object object = invocation.proceed() //执行原始方法
     *              //TODO 后置增强
     *              //返回值:原始方法执行后的返回值
     *              return object;
     *          }catch(Exception e){
     *              //TODO 异常增强
     *          }finally{
     *              //TODO 最终增强
     *          }
     *
     * 额外功能运行在原始方法之前
     */
    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
		//原始方法运行之前
        System.out.println("1234S");
        //执行原始方法
        Object proceed = invocation.proceed();

    }
}

额外功能运行在原始方法之后

       
 @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        //执行原始方法
        Object proceed = invocation.proceed();
		//原始方法之后
        System.out.println("54321");
        return proceed;
    }

额外功能运行在原始方法之前,之后

//什么样的额外功能 运行在原始方法之前,之后都要添加?
//事务,日志
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
   		//原始方法运行之前
        System.out.println("1234S");
        //执行原始方法
        Object proceed = invocation.proceed();
		//原始方法之后
        System.out.println("54321");
        return proceed;
    }

额外功能运行在原始方法抛出异常的时候

public Object invoke(MethodInvocation invocation)  {
    Object proceed = null;
    try {
        proceed = invocation.proceed();
    } catch (Throwable throwable) {
        System.out.println("----------原始方法抛出异常----------");
    }finally {
        return proceed;
    }
    
}

MethodInterceptor 影响原始方法的返回值

原始方法的返回值,直接作为invoke方法的返回值返回,MethodInterceptor不会影响原始方法的返回值


image-20200909111947822

MethodInterceptor影响原始方法的返回值
Invoke方法的返回值,不要直接返回原始方法的运行结果即可。

image-20200909112041138

5 切入点详解

切入点决定额外功能加入位置(方法)
<aop:pointcut id = "pc" expression = "execution(* *(..))"/> 

execution(* *(..)) ----->匹配所有的方法

1. execution 执行表达式
* *(..) 切入点表达式

5.1 方法切入点表达式

image-20200909114148350

* *(..) -->所有方法

* ------> 修饰符 返回值
* ------> 方法名
()------> 参数表
.. -----> 对于参数没有要求(有没有,有几个,什么类型都行)
  • 定义login方法作为切入点

    * login(..)
    
  • 定义login方法且有两个字符串类型的参数作为切入点

    * login(String,String)
    # 注意:非java.lang包的类 必须要写全限定类名
    * register(com.leetao.proxy.User)
    # 限制第一个参数必须为String,后面的参数随意
    # ..可以于具体的参数类型连用
    * login(String,..) --->login(String,String),login(String)
    
  • 定义匹配public的所有方法

    public * *(..)
    
  • 定义匹配public 返回值为void类型的register方法且形参为User的

    public void register(com.leetao.proxy.User)
    
  • 上面所讲解的方法切入点不精准

    image-20200909120352183

    • 两个包都有同名类和同名同参方法
    • 使用上面所讲的方法并不会过滤包和类
    • image-20200909121517716
* *(..)

如果中间的* 没有.只有一个参数时.表示匹配所有包和类的指定方法名 例: * login(..) -->所有包所有类的指定方法名
.. 有没有.有几个

5.2 精准方法切入点限定

image-20200909122052423

修饰符 返回值  包.类.方法(参数)
	*  		com.leetao.proxy.UserService.login(..)

5.3 类切入点

image-20200909141859224

指定特定类作为切入点(额外功能加入的位置),自然这个类中的所有方法,都会加入对应的额外功能
  • 语法1

    # 类中的所有方法加入额外功能 
    * com.leetao.proxy.UserServceImpl.*(..)
    
  • 语法2

  • # 忽略包同名类中的所有方法加入额外功能
    # 类只存在一级包
    * *.UserServiceImpl.*(..)
    # 类存在多级包
    * *..UserServiceImpl.*(..)
    # .. 没有包,任意包. 对于包没有要求
    

5.4 包切入点 (实战价值更高)

image-20200909153613468

指定包作为切入点(额外功能加入的位置),包中的所有类,所有方法都会增加额外功能
  • 语法1:
# 切入点包中的所有类,必须在proxy中,不能在proxy包的子包中
execution(* com.leetao.proxy.*.*(..))

语法2:

# 切入点当前包及其子包中的所有类,必须在proxy中
execution(* com.leetao.proxy..*.*(..))

5.5 切入点函数

切入点函数:用于执行切入点表达式
  • execution

    最为重要的切入点函数,功能最全.
    执行 方法切入点表达式 类切入点表达式 包切入点表达式
    
    弊端:execution执行切入点表达式,书写麻烦
    	execution(* com.leetao.proxy..*.*(..))
    注意:其他的切入点函数 简化的是execution书写复杂度,功能上完全一致.
    
  • args

    作用:主要用于函数(方法) 参数的匹配
    
    切入点:方法参数必须得是2个字符串类型的参数
    
    execution(* *(String,String))
    args(String,String)
    
    
  • within

    作用:主要用于进行类,包切入点表达式的匹配
    
    切入点:UserServiceImpl这个类
    execution(* *..UserServiceImpl.*.(..))
    within(*..UserServiceImpl)
    
    execution(* com.leetao.proxy..*.*(..))
    within(com.leetao.proxy..*)
    
  • @annotation

    作用:为具有特殊注解的方法加入额外功能
    <aop:pointcut id = "" expression = "@annotation(com.leetao.Log)"/>
    

5.6 切入点函数的逻辑运算

指的是 整合多个切入点函数一起配合工作.进而完成更为复杂的需求
  • and 与操作

    案例:login 同时 参数 2个字符串
    execution(* login(String,String))
    execution(* login(..)) and args(String,String)
    
    注意:与操作不能用于同种类型的切入点函数
    
    
    
  • or 或操作

     案例: register 方法 和 login方法作为切入点 
     execution(* login(..)) or execution(* register(..))
    

总结

  • 为什么需要代理

image-20200909185522733

  • 实现额外功能的接口.

    ​ MethodBeforeAdvice 不常用

    ​ MethodInterceptor(MethodInvocation)

image-20200909185652268

  • 切入表达式

image-20200909185958752

异常处理

AopInvocationException AOP实例异常

MethodInvocationException 方法实例异常

  • Null return value from advice does not match primitive return type for: 方法拦截器请设置返回值
;