Spring之AOP编程

一.静态代理设计模式

1.为什么需要代理设计模式?

在JavaEE开发中,哪个层次最为重要?

DAO层->Service层->Controller层。最重要的是Service层

Service层包含了哪些代码?

1.核心功能:业务运算+DAO调用

2.额外功能/附加功能:事务、日志、性能……

额外功能书写在Service层中好不好?

从Service层调用者(controller)来看,这是很好的,应当书写额外功能

从软件设计者的角度来看:Service层不需要额外功能

所以就要引入代理类,他要做两件事:第一,增加额外功能,第二,调用原始类(目标类)的功能

这就好像租房,房东要出租房屋,他的核心功能就是签合同收房租,而他的额外功能就是发布广告。但是房东嫌发广告看房很麻烦,不想要这样的额外功能。于是就有了中介,中介就是进行发广告,带房客看房的任务。这里面,房东就是软件设计者,房客就是controler,中介就是代理

2.代理设计模式

概念

通过代理类,为原始类增加额外功能

好处:利于原始类的维护

代理类开发的核心要素

代理类=目标类+额外功能+与原始类实现相同的接口

1.代理类中要有目标类,这是因为代理还要调用目标类的原始方法(就是核心功能)

2.为什么要实现相同的接口?

首先,原始类一般都是某个接口的实现,就比如UserServiceImpl是要实现UserService接口中的两个方法。其次,中介的方法名要和原始类的一致,这样才能迷惑调用者,就比如房客只是想买房,所以房东和中介的方法名称都是卖房。所以要实现相同的接口,从而实现相同的抽象方法

3.静态代理开发步骤

创建一个proxy包,创建UserService接口,创建UserServiceImpl原始类,创建UserServiceProxy代理类,然后原始类和代理类都implementsUserService接口,实现register和login接口。主要代理类中要有原始类对象,并在额外功能的书写前或后去调原始方法

仔细看代理类,打印的proxy log就代表了日志这个额外功能,最后还要调用原始类的核心功能(也就是业务运算和dao调用)

最后进行测试:

4.缺点

静态代理的文件数量过多,不利于项目管理,每有一个原始类,就得创建一个代理类,如果有100个原始类都需要日志功能,就需要100个代理类,但都是完成日志功能

对于额外功能的维护性差,并且有耦合

二.Spring动态代理开发

1.概念

通过代理类,为原始类添加额外功能

2.搭建开发环境

引入一下三个jar包

注意把这个runtime删除掉

3.开发步骤

创建原始对象

我们就用userservice,在配置文件中进行配置:

提供额外功能

Spring提供了接口:MethodBeforeAdvice

我们就把额外功能写到此接口中

创建一个dynamic包,创建一个Before类,实现该接口并实现其中的before抽象方法:

注意看这个befor方法的参数,第一个参数method代表额外功能所增加给的那个原始方法;第二个参数表示原始方法的参数,第三个参数表示原始类的实例。这些参数提供了但是不一定要使用,就好像Servlet中的request和response提供了但不一定使用

下面我们就书写额外功能并进行文件配置:

注意:这个类是为了提供额外功能,但我可没说要实现原始方法!!!

文件配置:

定义切入点

切入点就是额外功能增加到位置

目的:由程序员根据自己的需要,决定额外功能加到哪里

简单测试:把所有方法都作为切入点,都加额外功能。

配置如下:

<aop:config>标签是告诉Spring哪些方法需要干什么(这个哪些方法与干什么都是在中间的aop标签中定义)

<aop:pointcut>标签是说叫加切入点,其中的expression就是切入点表达式,告诉了Spring哪些方法要作为切入点

组装

将额外功能与切入点进行组装

也就是第2第3步的整合

<aop:advisor>标签,就可以将额外功能与切入点整合。advice-ref就代表了额外功能的实现类(advice表示建议,可以抽象成额外功能,ref表示一个实例对象);pointcut-ref就代表了切入点。

调用

目的:获得Spring工厂创建的动态代理对象并进行调用

如上,使用userService获得的是代理对象(为什么可以用UserService接收?因为代理和实现类都实现了同一个接口),并且在调用原始方法之前调用了Before类的before方法。

debug一下:

看,userService对象的类型是proxy是代理

4.细节分析

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

Spring框架在运行时,通过动态字节码技术,在JVM中创建的,运行在JVM内部,等程序结束后会和JVM一起销毁

什么是动态字节码技术?

以前:java运行一个类,实际上是jvm运行该类的字节码(该类的源文件.java文件编译后就生成了.class字节码文件)

现在的动态字节码技术:由于我们没有手动写代码,也就是没有写.java源文件,所以就没有编译生成.class文件的过程。而是第三方动态字节码框架直接在JVM中生成字节码(这就是生成的动态字节码,对应动态代理类)。

也就是通过第三方动态字节码框架,在jvm中创建对应的类的字节码,进而创建对象,当虚拟机结束时,动态字节码就会跟着消失。

所以说动态代理不需要定义类文件,都是JVM在运行时动态创建的,这就解决了静态代理中静态类文件过多的问题

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

比如现在又想给orderService的方法加上日志功能,你会发现好像只需要创建原始对象即可(就是在配置文件中添加一个bean组件)。而日志额外功能已经在Before类中写过了,切入点也定义好了(就是给所有方法都提供额外功能),额外功能和切入点也已经进行了组装。

所以说,只要加的额外功能一样,在创建其他类的代理对象时,只需要解决原始对象的创建即可

三.动态代理详解

AOP编程,Spring动态代理开发的四步:

1.创建原始对象 2.提供额外功能 3.定义切入点 4.组装

1.额外功能详解

MethodBeforeAdvice

这个方法的参数在上面已经讲解过。

MethodBeforeAdvice的作用:使额外功能运行在原始方法之前(只能是原始方法的前面)

MethodInterceptor(方法拦截器)

它也是为额外功能的定义进行服务的一个接口,只不过,它能使得额外功能既可以运行在原始方法之前,又能运行在原始方法之后,还能前后都运行,也能在原始方法抛出异常时运行。

我们下面就详细看看它的使用:

首先在dynamic包中创建一个Around类并实现MethodInterceptor接口

注意是第一个包

1.invoke方法的作用:书写额外功能

2.要确定原始方法怎么运行:看参数MethodInvocation,它代表的就是额外功能所增加给的那个原始方法。invocation.proceed()就是让原始方法运行

3.返回值:就是原始方法的返回值

如下:

配置文件

注意一定要把之前写的before这个额外功能类给注释掉,然后将advice-ref改成around。

额外功能在原始方法抛异常时:

methodInterceptor可影响原始方法的返回值,就是接收到ret后可进行再加工

2.切入点表达式详解

对于切入点的定义,暴扣哦了切入点函数(比如execution)和切入点表达式(比如* *(..))。我们先来介绍切入点表达式

方法切入点表达式

我们知道一个方法由五部分组成:public void修饰符、返回值、方法名、参数、异常

之前说了* *(..)代表了所有函数。因为第一个通配符*表示了对方法的修饰符和返回值没有要求,第二个*表示对方法的名称没有要求,括号里两点表示对方法的参数没有要求,所以就代表了一切方法。

那么如何以方法为切入点书写表达式?

1.若只对对方法名有要求

只要经第二个*替换成具体的方法名称即可

比如对login方法定义切入点,则* login(..)

2.若对方法名和参数有要求

再将..替换成参数类型,如下:

* login(String,String)表示参数类型为两个String的login方法

* login(String,..)表示第一个参数必须是String类型,第二个及以后有没有都可以,有几个、参数类型是啥,都没要求

* login(..,String,..)表示这个方法里面只要有String类型的参数就可以,它放在哪里有几个无所谓

类切入点表达式

若指定就给这个类的所有方法加额外功能,不指定包,则如下:

* *.UserServiceImpl.*(..)但注意,这样写的话,第二个*只代表一层包

* *..UserServiceImpl.*(..)这样写,就代表了多级包,或者一层包,或者多级,注意,不能代表没有包。

* UserServiceImpl.*(..)这样才是代表没有包

包切入点表达式

语法一:* basic.convertor.*.*(..)表示在basic包下的子包convertor中的所有类。但是,不能包含convertor包的子包中的类

语法二:* basic.convertor..*.*(..)表示在basic包的子包convertor及其子包中的所有类。这次就包含了convertor包的子包以及子包的子包……

包切入点表达式的实战价值更高,可以将像压迫添加相同额外功能的类都放到一个包下面

以上三种表达式可以搭配使用

3.切入点函数详解

切入点函数的作用:用于执行切入点表达式

execution()

最重要,功能最全面,可执行方法切入点表达式,类切入点表达式,包切入点表达式……

args()

用于函数/方法参数的匹配

例如方法参数必须是String类型,则:

execution(* *(String,String))或者args(String,String)

within()

用于进行类/包切入点表达式的匹配

例如指定类:

execution(* *..Person.*(..))或者within(*..Person)

例如指定包:

execution(* basic..*.*(..))或者within(basic..*)

@annotation

作用:为具有特殊注解的方法定义切入点

下面我们来给UserServiceImpl定义一个注解。首先要自定义注解

选择Annocation

然后加一些注解:

首先这个Target注解的可选值代表了这个注解要用在哪里,用在类上就选Type,用在方法上就选Method

这个注解代表了这个自定义注解要在什么时候用,一般都选Runtime。

然后我们在login方法上面加上这个@Log注解

最后在配置文件中进行配置@annocation()内部就是注解的全限定名

效果如上

切入点函数的逻辑运算

指的是整合多个切入点函数一起配合工作,进而完成更复杂的需求

1.and操作

示例:方法为login,并且参数是两个字符串

原来是execution(* login(String,String)),现在是execution(* login(..)) and args(String,String)

注意:与操作不能用于同种类型的切入点函数

案例:register和login都作为切入点

难道是execution(* register(..)) and execution(* login(..))吗?显然不是,这表示方法名既是register,又是login。这该怎么解决?用下面的或操作

2.or操作

案例:register和login都作为切入点

execution(* register(..)) or execution(* login(..))

四.AOP编程

1.概念

OOP(Object Oriented Programing)面向对象编程

以对象为基本单位的程序开发,通过对象间的协调和相互调用,完成程序的创建

POP(Producer Oriented Programing)面向过程编程

以过程(方法、函数)为基本单位的程序开发,通过过程间的协调和相互调用,完成程序的创建

AOP(Aspect Oriented Programing)面向切面编程

以切面为基本单位的程序开发,通过切面间的协调和相互调用,完成程序的创建

切面=切入点+额外功能;而在Spring配置文件中,就有切入点与额外功能进行整合这一步,即<aop:advisor advice-ref="before" pointcut-ref="pc">。

所以说:Spring动态代理开发就是面向切面编程,即AOP编程

或者说:AOP编程的本质就是Spring动态代理开发,通过代理类为原始类增加额外功能

2.AOP编程的开发步骤

既然AOP编程的本质就是Spring动态代理开发,那么两者的开发步骤就是相同的

1.提供原始对象

2.引入额外功能

3.定义切入点

4.组装切面

五.AOP底层实现原理

1.核心问题

1.AOP如何创建动态代理类?

2.Spring工厂如何加工创建代理对象?

2.动态代理类的创建

JDK的动态代理

创建一个包JDK,创建JDKProxy类,如下:

首先要明确代理类创建的三要素:1.提供原始对象 2.编写额外功能 3.实现相同的接口

我们还是使用UserServiceImpl作为原始对象,然后就是创建动态代理,如何创建呢?Spring提供了一个Proxy类,其中有个newProxyInstance方法,就是在创建代理对象

注意这个方法的三个参数。

先看InvocationHandler,它对应的就是额外功能。它是一个接口,需要我们实现其中的方法,这就可以用到内部类。如下:

要实现invoke方法,里面的proxy参数代表的是代理对象,这个可以忽略;method代表了额外功能所增加给的那个原始方法,args则是原始方法的参数列表。在invoke方法中,我们既要实现额外功能,又要调用原始方法。一个方法的调用必须要有对象和参数,所以在调用原始方法时,就要传入对象和参数。对象其实就是我们上面new出来的UserServiceImpl,而参数就是args。所以这个内部类可写为:

接着来解决newInstance方法中的interfaces,它对应的就是原始类所实现的所有接口,这就是要让代理类实现与原始类相同的接口,所以如下:

最后来看ClassLoader,怎么会有类加载器呢?类加载器的作用就是把对应雷达字节码文件加载到JVM中,再通过字节码文件创建class对象,进而创建这个类的对象。也就是说,要想创建对象,就要到jvm中,所以要将.class文件加载到jvm中并创建对象,这就是classLoader的工作。有了class对象才能new userServiceproxy()

那么如何获得类加载器呢?一般来说,JVM会为每一个类的.class文件自动分配一个类加载器。而对于动态代理,动态字节码技术会直接将字节码写在jvm中,所以就没有了加载对象这一步。那么门钥匙真相将对象从jvm中new出来,就必须要有一个类加载器。

这时就可以借一个类加载器,借哪个类的都可以。

分析到这里,我们就可以着手写代码了。如下:

就是借哪个类的类加载器都可以

这就创建出来UserServiceImpl的代理对象,然后调用login和register方法即可

CGlib的动态代理

与JDK创建动态代理不同,CGlib创建动态代理是为没有实现任何接口的原始类创建代理类。那没有实现共同的接口,怎么去实现共同的方法呢?可以让代理类继承原始类,通过super.login去调用原始方法。如下:

创建一个cglib包,创建一个不实现接口的orderService类

再创建CglibProxy类,继承OrderService类

如上,创建CGlib动态代理的关键类就是Enhancer类。和JDK创建动态代理相同的是都需要classLoader和额外功能,但是不同的是,CGlib不需要接口,而是需要设置父类。

现在我们类仔细看一下额外功能的提供:同样是要实现一个接口,这个接口是MethodInterceptor接口,但是和之前Spring动态代理开发实现的不是一个包中的MethodInterceptor,并且其中的抽象方法也不同。在Spring动态代理开发中,那个抽象方法是invoke方法,参数只有一个是MethodInvocation,通过调用invocation.proceed()就可以使原始方法运行。而在这里,抽象方法是intercept,参数很多,其中,method代表原始方法,objects代表参数,methodProxy就是代理对象。那么如何让原始方法被调用呢?这个调用方式就和JDK动态代理中的handler类似,也是调用method.invoke,传入对象和参数列表

3.Spring工厂如何加工原始对象从而获得代理对象?

回顾beanPostProcessor接口,他就是对对象进行加工的。

AOP编程,通过userService这个id获得的是原始对象。但要想获得代理对象,就需要BeanPostProcessor进行加工,将原始对象加工成代理对象再返回

开发步骤

创建一个Factory包,创建UserService接口以及Impl实现类,创建一个ProxyBeanPostProcessor类,实现BeanPostProcessor接口

最后进行文件配置

测试一下:

调试一下也会发现用id值获得的是代理类对象

六.基于注解的AOP编程

1.开发步骤

还是和AOP编程以及Spring动态代理开发一样

创建原始对象,提供额外功能,第一切入点,组装切面

首先,创建aspect包,并创建原始对象以及接口,进行文件配置:

然后创建Myaspect这个切面类,并且要加一些注解:

如上,@Aspect注解就告诉了Spring这是一个自定义的切面类。然后下面写一个around方法,再加一个@Around注解,此时这个around方法就等同于invoke方法了。

那么这个方法的参数是什么?是ProceedingJoinPoint这个接口,通过这个类中的proceed方法就可以调用原始方法。因为proceed方法有返回值,就是原始方法的返回值,所以我们自定义的around方法也要有返回值:

下面一步就是定义切入点,其实就是在@Around注解内部加上切入点表达式

注意要加双引号

最后进行文件配置

最后还要告诉Spring现在要基于注解进行AOP编程:

效果如下:

2.细节分析

切入点复用

现在已经给login方法加了日志功能,但还想给它加一个事务功能,就还得有一个around方法,还得重复上面的一些代码

我们会发现,这两个额外功能的切入点是相同的,所以我们可以进行如下操作从而对切入点进行复用:

也就是自定义一个函数,该函数必须是public void 无参 无实现,然后加上@Pointcut注解,里面填入切入点表达式。

动态代理的创建方式

debug看userService会发现,它是那个动态代理,但在默认情况下的AOP编程的底层是用的JDK的动态代理创建方式。那如何切换CGlib的鼎泰代理创建方式?

如上,再加一个属性即可

那再回顾传统的aop开发,就是使用了aop:config标签的Spring动态代理开发,它如何切换方式呢?也是加这样的属性,如下:

七.AOP开发中的坑

我们将Aspect类中的切入点编程UserServiceImpl这个类切入点,这样,login方法和register方法就都有额外功能了,然后我们在REgister方法内部调用login方法,如下:

然后再进行测试

发现第二个login方法没有被加上额外功能

这是因为我们register中调用的这个login方法式原始对象的,但真正的设计目的是调用代理对象的login方法。所以关键就是在于在原始类中获得代理对象。那如何获得?难道再创建工厂再getBean?

不行!!!因为Spring工厂是重量级资源,一个应用中应该只有一个工厂,所以我们要拿到测试类中的工厂。如何做到?

让UserServiceImpl事项ApplicationContextAware接口,如下:

然后修改register方法:

这次就都加上额外功能了

总结:

在同一个业务类中,进行业务方法间的相互调用,只有最外层方法是加了额外功能的,方法内部通过普通方式调用,都调用的是原始方法。如果想让内层方法也加入额外功能,就要实现ApplicationContextAware接口,拿到测试类中的工厂,从而创建代理对象

相关推荐

  1. springAOP(面向切面编程)详结

    2024-04-23 11:38:02       16 阅读
  2. Spring AOP 切面编程

    2024-04-23 11:38:02       12 阅读
  3. Spring aop切面编程

    2024-04-23 11:38:02       11 阅读

最近更新

  1. TCP协议是安全的吗?

    2024-04-23 11:38:02       16 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2024-04-23 11:38:02       16 阅读
  3. 【Python教程】压缩PDF文件大小

    2024-04-23 11:38:02       15 阅读
  4. 通过文章id递归查询所有评论(xml)

    2024-04-23 11:38:02       18 阅读

热门阅读

  1. 【领导力】削足适履与领导力

    2024-04-23 11:38:02       14 阅读
  2. 中国的微观调查数据总结

    2024-04-23 11:38:02       17 阅读
  3. vtk.vtkProcrustesAlignmentFilter()使用方法

    2024-04-23 11:38:02       12 阅读
  4. 认识线程池

    2024-04-23 11:38:02       11 阅读
  5. StarRocks用户权限管理

    2024-04-23 11:38:02       11 阅读