Spring学习 AOP
Spring视频教程
https://www.bilibili.com/video/BV1Sb411s7vP?p=65&spm_id_from=pageDriver
AOP:全称是 Aspect Oriented Programming 即:面向切面编程
面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。其实AOP就是Spring中实现动态代理的技术框架,让使用者在写代码的时候可以省掉很多不必要的麻烦事和代码。
所以先来看看动态代理是什么,然后再看看AOP是怎么实现动态代理的,其实也是怎么用AOP的一些参数和代码
静态代理和动态代理
这里代理(Proxy)和现实中的意思是一样的,厂家生产产品,但是厂家只想生产产品不想管销售的事,所以想找人去卖自己的产品,其实这就是代理的意思,在java中的代理也是一样。
静态代理
产品是一个接口,厂家是继承了这个接口的一个实现类,代理也是继承了这个接口的一个实现类,厂家是代理中类中的一个对象,代理类中代理厂家产品的同时可以加一些自己的东西进去,比如电影放映的前后,电影院放映自己的广告,这里电影导演就是被代理的一方,电影是产品,电影院是代理。
(例子及代码参考自https://www.cnblogs.com/cC-Zhou/p/9525638.html)
定义一个接口,也就是电影
public interface Movie1 {
void play1();
}
在电影院放映的肯定不止一部电影,所以我们假设他取得了两部电影的放映权。
public interface Movie2 {
void play2();
}
定义一个拍电影的类(我们称它为导演类吧),也就是电影的生产者
public class Director1 implements Movie1{
@Override
public void play1() {
System.out.println("本部电影为肖申克的救赎");
}
}
public class Director2 implements Movie2{
@Override
public void play2() {
System.out.println("本部电影为当幸福来敲门");
}
}
定义电影院,也是一个实现接口Movie的类,如果我这家电影院取得了两部电影的放映权,那我就要实现两部电影的接口,并且电影院在放映电影的同时,也就是代理的同时,不仅要放电影,取得电影票房,还要插入自己的一些小广告,赚些广告费,这些就相当于代理在原来类上的增强
public class StaticProxy implements Movie2 , Movie1{
Director1 d1 = new Director1();
Director2 d2 = new Director2();
public StaticProxy(Director1 d1, Director2 d2) {
this.d1 = d1;
this.d2 = d2;
}
@Override
public void play1() {
guanggao1();
d1.play1();
guanggao2();
}
@Override
public void play2(){
guanggao1();
d2.play2();
guanggao2();
}
public void guanggao1(){
System.out.println(" 观影前广告 ");
}
public void guanggao2(){
System.out.println(" 观影后广告 ");
}
}
最后我们在电影院分不同场次放两部电影,并且同时加入电影院的广告
import java.util.Stack;
public class TestProxy {
public static void main(String[] args) {
private Director1 d1;
private Director2 d2;
StaticProxy cinema = new StaticProxy(d1, d2);
System.out.println("三点场,当幸福来敲门");
cinema.play1();
System.out.println("---------------------------------------------");
System.out.println("五点场,肖申克的救赎");
cinema.play2();
}
}
输出为
静态代理的特点很明显:
- 自己写代码实现代理类
- 可以在不修改被代理对象的基础上,通过拓展代理类,进行一些功能的附加与增强。
- 代理类与被代理类应该实现同一个接口,或者共同继承某个类
-----------------------------静态代理的弊端-----------------------------
我们仔细想想,这是我们已经知道,在这个时间段,我这个电影院代理了两部电影,我现在就实现两个接口,但是,如果我电影院过一段时间又接到了,速度与激情9的放映权,这个时候我是不是又得去改源代码,实现速9的这个接口?
所以这种方法被称为静态代理,就是因为他一旦写好,就被写死了,不能动态的改变,所以Java中给我们提供了实现动态代理的类Proxy。
电影和导演的类我们不变,就相当于是两部电影。
我们把电影院的类改成动态类。
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
public class DynamicProxy implements InvocationHandler {
private Object director;
public DynamicProxy(Object director) {
this.director = director;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("----------欢迎来到Proxy电影院----------");
System.out.println(" 观影前广告 ");
method.invoke(director, args);
System.out.println(" 观影后广告 ");
return null;
}
}
然后我们直接写放电影的代码就可以了
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.Stack;
public class TestProxy {
public static void main(String[] args) {
Director1 director1 = new Director1();
Director2 director2 = new Director2();
InvocationHandler cinema1 = new DynamicProxy(director1);
InvocationHandler cinema2 = new DynamicProxy(director2);
Movie1 dyP = (Movie1) Proxy.newProxyInstance(director1.getClass().getClassLoader(),
director1.getClass().getInterfaces(), cinema1);
Movie2 dyP1 = (Movie2) Proxy.newProxyInstance(director2.getClass().getClassLoader(),
director2.getClass().getInterfaces(), cinema2);
dyP.play1();
dyP1.play2();
}
}
输出结果为
因为我们的DynamicProxy类只实现了InvocationHandler这个接口,所以就算我多接很多部电影,不过是在放映电影的main函数中多写几行代码而已,这样我们的代理类就没被写死,可以动态的代理多个实现了某个接口的类。
Proxy的实现和代理方式基本是固定的,直接背下来就行,来多少部电影,我们直接创建多少个cinema实例对象,然后直接放映就行了。
这里要注意的是,这些实例对象是同一家电影院,
也就是说,我们是针对同一个类,对多个接口或者说是类进行代理,其实真正的代理是一个类,而不是某个实例化的对象,这个类可以动态的加入多个实现了某个接口的类,去成为该类的代理。
AOP实现动态代理
动态代理就是,在程序运行期,创建目标对象的代理对象,并对目标对象中的方法进行功能性增强的一种技术。在生成代理对象的过程中,目标对象不变,代理对象中的方法是目标对象方法的增强方法。可以理解为运行期间,对象中方法的动态拦截,在拦截方法的前后执行功能操作。
AOP是Aspect Oriented Programming面向切面编程,最少我们得知道切面是个什么东西。
我们上面的例子可以知道,我电影院作为代理,放映一部电影,主要的目标是放映这部电影,所以这个是我的关键点,而其他的广告什么的都是我对这个点的扩张,或者说是对被代理类的一个增强。
所以放电影这个方法,我们称之为切入点,而通过这个点扩张的其他方法与这个切入点加起来,称为切入面。
所以切入面其实就是围绕切入点做文章,动态代理对被代理类的增强其实也是在类实现前后加入增强的东西。
对于Java中存在的异常体系,我们其实可以把代理类中的invoke方法写成
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try{
System.out.println("----------欢迎来到Proxy电影院----------");
System.out.println(" 观影前广告 ");
method.invoke(director, args);
System.out.println(" 观影后广告 ");
}catch (Exception e){
System.out.println("异常通知");
}finally {
System.out.println("最终通知");
}
return null;
}
在Spring中, 把在切入点实现前,实现后,异常时,最终实现的方法,分别称为四种通知类型:前置,后置,异常,最终
而AOP实现动态代理的配置,我们也只需要根据方法,把这四个方法配置好,就相当于实现了整个切面。
AOP同样提供了两种方法配置AOP
XML配置AOP
我们先创建一个用于通知的类
public class Logger {
//前置通知
public void beforePrintLog(){
System.out.println("前置通知Logger类中的beforePrintLog方法开始记录日志了。。。");
}
//后置通知
public void afterReturningPrintLog(){
System.out.println("后置通知Logger类中的afterReturningPrintLog方法开始记录日志了。。。");
}
//异常通知
public void afterThrowingPrintLog(){
System.out.println("异常通知Logger类中的afterThrowingPrintLog方法开始记录日志了。。。");
}
//最终通知
public void afterPrintLog(){
System.out.println("最终通知Logger类中的afterPrintLog方法开始记录日志了。。。");
}
}
因为Spring中所有类都放于容器中,所以xml容器中还是要配置bean容器
<!-- 把要实现的功能所对应的类配置进来,该例子取自课程
在该账户中,我们要实现的保存账信息为要代理的类,也就是我们说的切入点
-->
<bean id="accountService" class="com.itheima.service.impl.AccountServiceImpl"></bean>
<!-- 配置通知的方法类,也是就是Logger类 -->
<bean id="logger" class="com.itheima.utils.Logger"></bean>
<!--配置AOP-->
<aop:config>
<!-- 配置切入点表达式 id属性用于指定表达式的唯一标识。expression属性用于指定表达式内容,就是指定切入点,
execution中的格式可写为:
访问修饰符 返回值 包名.包名.包名...类名.方法名(参数列表)
此标签写在aop:aspect标签内部只能当前切面使用。
它还可以写在aop:aspect外面,此时就变成了所有切面可用
-->
<aop:pointcut id="pt1" expression="execution(* com.itheima.service.impl.*.*(..))"></aop:pointcut>
<!--配置切面 -->
<aop:aspect id="logAdvice" ref="logger">
<!-- 配置前置通知:在切入点方法执行之前执行-->
<aop:before method="beforePrintLog" pointcut-ref="pt1" ></aop:before>
<!-- 配置后置通知:在切入点方法正常执行之后值。它和异常通知永远只能执行一个-->
<aop:after-returning method="afterReturningPrintLog" pointcut-ref="pt1"></aop:after-returning>
<!-- 配置异常通知:在切入点方法执行产生异常之后执行。它和后置通知永远只能执行一个-->
<aop:after-throwing method="afterThrowingPrintLog" pointcut-ref="pt1"></aop:after-throwing>
<!-- 配置最终通知:无论切入点方法是否正常执行它都会在其后面执行-->
<aop:after method="afterPrintLog" pointcut-ref="pt1"></aop:after>
</aop:aspect>
</aop:config>
除此之外,AOP还提供了一种环绕通知。
之前说的前置和后置通知是不是有点像将切入点包围起来的样子,而这个环绕通知就是将前置,后置,异常,最终可以放入同一个方法类中。
<!--配置AOP-->
<aop:config>
<!-- 配置切入点表达式 id属性用于指定表达式的唯一标识。expression属性用于指定表达式内容
此标签写在aop:aspect标签内部只能当前切面使用。
它还可以写在aop:aspect外面,此时就变成了所有切面可用
-->
<aop:pointcut id="pt1" expression="execution(* com.itheima.service.impl.*.*(..))"></aop:pointcut>
<!--配置切面 -->
<aop:aspect id="logAdvice" ref="logger">
<!-- 配置环绕通知 详细的注释请看Logger类中-->
<aop:around method="aroundPringLog" pointcut-ref="pt1"></aop:around>
</aop:aspect>
</aop:config>
这样一来Logger类可以改成
public class Logger {
/**
* 环绕通知
* 问题:
* 当我们配置了环绕通知之后,切入点方法没有执行,而通知方法执行了。
* 分析:
* 通过对比动态代理中的环绕通知代码,发现动态代理的环绕通知有明确的切入点方法调用,而我们的代码中没有。
* 解决:
* Spring框架为我们提供了一个接口:ProceedingJoinPoint。该接口有一个方法proceed(),此方法就相当于明确调用切入点方法。
* 该接口可以作为环绕通知的方法参数,在程序执行时,spring框架会为我们提供该接口的实现类供我们使用。
*
* spring中的环绕通知:
* 它是spring框架为我们提供的一种可以在代码中手动控制增强方法何时执行的方式。
*/
public Object aroundPringLog(ProceedingJoinPoint pjp){
Object rtValue = null;
try{
Object[] args = pjp.getArgs();//得到方法执行所需的参数
System.out.println("Logger类中的aroundPringLog方法开始记录日志了。。。前置");
rtValue = pjp.proceed(args);//明确调用业务层方法(切入点方法)
System.out.println("Logger类中的aroundPringLog方法开始记录日志了。。。后置");
return rtValue;
}catch (Throwable t){
System.out.println("Logger类中的aroundPringLog方法开始记录日志了。。。异常");
throw new RuntimeException(t);
}finally {
System.out.println("Logger类中的aroundPringLog方法开始记录日志了。。。最终");
}
}
}
其中的实现方式是固定的,想听详细解读的可以看开头的视频教程或者去百度搜索,看看源码。
这个环绕通知,其实就相当于SpringAOP给程序员一个手动添加四种通知的方法,跟用Proxy类实现动态代理有些类似。
注解配置AOP
Spring中的IOC有XML和注解实现,那么AOP同样也有两种。
但是对于注解来说,实现的顺序,本来是前置,切入点,后置或异常,最终
但是在AOP注解中,最终比后置或异常先一步执行。当然,因为环绕通知是程序员手动添加顺序,所以顺序不变,所以用注解实现AOP要注意顺序。
用注解实现AOP,那么就只需要添加Spring创建容器要扫描的包和开始AOP的配置
<!-- 配置spring创建容器时要扫描的包-->
<context:component-scan base-package="com.itheima"></context:component-scan>
<!-- 配置spring开启注解AOP的支持 -->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
如果用四种通知实现AOP,Logger类写成,
/**
* 用于记录日志的工具类,它里面提供了公共的代码,
*/
@Component("logger")//将Logger类用logger放入IOC容器
@Aspect//表示当前类是一个切面类
public class Logger {
//切入点,该表达式为impl下面的所有类的方法都为切入点,也就是这些配置对于所有的方法和类都适用
@Pointcut("execution(* com.itheima.service.impl.*.*(..))")
private void pt1(){}
/**
* 前置通知
*/
@Before("pt1()")
public void beforePrintLog(){
System.out.println("前置通知Logger类中的beforePrintLog方法开始记录日志了。。。");
}
/**
* 后置通知
*/
@AfterReturning("pt1()")
public void afterReturningPrintLog(){
System.out.println("后置通知Logger类中的afterReturningPrintLog方法开始记录日志了。。。");
}
/**
* 异常通知
*/
@AfterThrowing("pt1()")
public void afterThrowingPrintLog(){
System.out.println("异常通知Logger类中的afterThrowingPrintLog方法开始记录日志了。。。");
}
/**
* 最终通知
*/
@After("pt1()")
public void afterPrintLog(){
System.out.println("最终通知Logger类中的afterPrintLog方法开始记录日志了。。。");
}
}
同样我们也可以用注解实现环绕通知
@Component("logger")
@Aspect//表示当前类是一个切面类
public class Logger {
@Pointcut("execution(* com.itheima.service.impl.*.*(..))")
private void pt1(){}
@Around("pt1()")
public Object aroundPringLog(ProceedingJoinPoint pjp){
Object rtValue = null;
try{
Object[] args = pjp.getArgs();//得到方法执行所需的参数
System.out.println("Logger类中的aroundPringLog方法开始记录日志了。。。前置");
rtValue = pjp.proceed(args);//明确调用业务层方法(切入点方法)
System.out.println("Logger类中的aroundPringLog方法开始记录日志了。。。后置");
return rtValue;
}catch (Throwable t){
System.out.println("Logger类中的aroundPringLog方法开始记录日志了。。。异常");
throw new RuntimeException(t);
}finally {
System.out.println("Logger类中的aroundPringLog方法开始记录日志了。。。最终");
}
}
}