一.静态代理设计模式
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接口,拿到测试类中的工厂,从而创建代理对象