Spring系列文章:面向切面编程AOP

一、代理模式

1、代理模式使用场景引入

⽣活场景1:⽜村的⽜⼆看上了隔壁村⼩花,⽜⼆不好意思直接找⼩花,于是⽜⼆找来了媒婆王妈妈。这 ⾥⾯就有⼀个⾮常典型的代理模式。⽜⼆不能和⼩花直接对接,只能找⼀个中间⼈。其中王妈妈是代理 类,⽜⼆是⽬标类。王妈妈代替⽜⼆和⼩花先⻅个⾯。(现实⽣活中的婚介所)【在程序中,对象A和对 象B⽆法直接交互时。】

⽣活场景2:你刚到北京,要租房⼦,可以⾃⼰找,也可以找链家帮你找。其中链家是代理类,你是⽬标 类。你们两个都有共同的⾏为:找房⼦。不过链家除了满⾜你找房⼦,另外会收取⼀些费⽤的。(现实⽣ 活中的房产中介)【在程序中,功能需要增强时。】

⻄游记场景:⼋戒和⾼⼩姐的故事。⼋戒要强抢⺠⼥⾼翠兰。悟空得知此事之后怎么做的?悟空幻化成 ⾼⼩姐的模样。代替⾼⼩姐与⼋戒会⾯。其中⼋戒是客户端程序。悟空是代理类。⾼⼩姐是⽬标类。那 天夜⾥,在⼋戒眼⾥,眼前的就是⾼⼩姐,对于⼋戒来说,他是不知道眼前的⾼⼩姐是悟空幻化的,在 他内⼼⾥这就是⾼⼩姐。所以悟空代替⾼⼩姐和⼋戒亲了嘴⼉。这是⾮常典型的代理模式实现的保护机 制。代理模式中有⼀个⾮常重要的特点:对于客户端程序来说,使⽤代理对象时就像在使⽤⽬标对象⼀ 样。【在程序中,⽬标需要被保护时】

业务场景:系统中有A、B、C三个模块,使⽤这些模块的前提是需要⽤户登录,也就是说在A模块中要编 写判断登录的代码,B模块中也要编写,C模块中还要编写,这些判断登录的代码反复出现,显然代码没 有得到复⽤,可以为A、B、C三个模块提供⼀个代理,在代理当中写⼀次登录判断即可。代理的逻辑 是:请求来了之后,判断⽤户是否登录了,如果已经登录了,则执⾏对应的⽬标,如果没有登录则跳转 到登录⻚⾯。【在程序中,⽬标不但受到保护,并且代码也得到了复⽤。】

代理模式是GoF23种设计模式之⼀。属于结构型设计模式。

代理模式的作⽤是:为其他对象提供⼀种代理以控制对这个对象的访问。在某些情况下,⼀个客户不想 或者不能直接引⽤⼀个对象,此时可以通过⼀个称之为“代理”的第三者来实现间接引⽤。代理对象可以 在客户端和⽬标对象之间起到中介的作⽤,并且可以通过代理对象去掉客户不应该看到的内容和服务或 者添加客户需要的额外服务。 通过引⼊⼀个新的对象来实现对真实对象的操作或者将新的对象作为真实 对象的⼀个替身,这种实现机制即为代理模式,通过引⼊代理对象来间接访问⼀个对象,这就是代理模 式的模式动机。

代理模式中的⻆⾊:

  • 代理类(代理主题)
  • ⽬标类(真实主题)

代理类和⽬标类的公共接⼝(抽象主题):客户端在使⽤代理类时就像在使⽤⽬标类,不被客户端 所察觉,所以代理类和⽬标类要有共同的⾏为,也就是实现共同的接⼝。

代理模式的类图:

public interface OrderService {/*** ⽣成订单*/void generate();/*** 查看订单详情*/void detail();/*** 修改订单*/void modify();
}

代理模式在代码实现上,包括两种形式: 静态代理 动态代理 

2、静态代理

现在有这样⼀个接⼝和实现类

public interface OrderService {/*** ⽣成订单*/void generate();/*** 查看订单详情*/void detail();/*** 修改订单*/void modify();
}

 实现类如下

public class OrderServiceImpl implements OrderService {@Overridepublic void generate() {try {Thread.sleep(1234);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("订单已⽣成");}@Overridepublic void detail() {try {Thread.sleep(2541);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("订单信息如下:******");}@Overridepublic void modify() {try {Thread.sleep(1010);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("订单已修改");}
}

 其中Thread.sleep()⽅法的调⽤是为了模拟操作耗时。

项⽬已上线,并且运⾏正常,只是客户反馈系统有⼀些地⽅运⾏较慢,要求项⽬组对系统进⾏优化。于 是项⽬负责⼈就下达了这个需求。⾸先需要搞清楚是哪些业务⽅法耗时较⻓,于是让我们统计每个业务 ⽅法所耗费的时⻓。如果是你,你该怎么做呢? 第⼀种⽅案:直接修改Java源代码,在每个业务⽅法中添加统计逻辑,如下:


public class OrderServiceImpl implements OrderService {@Overridepublic void generate() {long begin = System.currentTimeMillis();try {Thread.sleep(1234);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("订单已⽣成");long end = System.currentTimeMillis();System.out.println("耗费时⻓" + (end - begin) + "毫秒");}@Overridepublic void detail() {long begin = System.currentTimeMillis();try {Thread.sleep(2541);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("订单信息如下:******");long end = System.currentTimeMillis();System.out.println("耗费时⻓" + (end - begin) + "毫秒");}@Overridepublic void modify() {long begin = System.currentTimeMillis();try {Thread.sleep(1010);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("订单已修改");long end = System.currentTimeMillis();System.out.println("耗费时⻓" + (end - begin) + "毫秒");}}

 需求可以满⾜,但显然是违背了OCP开闭原则。这种⽅案不可取。

第⼆种⽅案:使⽤代理模式(这⾥采⽤静态代理) 可以为OrderService接⼝提供⼀个代理类。

public class OrderServiceProxy implements OrderService { // 代理对象// ⽬标对象private OrderService orderService;// 通过构造⽅法将⽬标对象传递给代理对象public OrderServiceProxy(OrderService orderService) {this.orderService = orderService;}@Overridepublic void generate() {long begin = System.currentTimeMillis();// 执⾏⽬标对象的⽬标⽅法orderService.generate();long end = System.currentTimeMillis();System.out.println("耗时" + (end - begin) + "毫秒");}@Overridepublic void detail() {long begin = System.currentTimeMillis();// 执⾏⽬标对象的⽬标⽅法orderService.detail();long end = System.currentTimeMillis();System.out.println("耗时" + (end - begin) + "毫秒");}@Overridepublic void modify() {long begin = System.currentTimeMillis();// 执⾏⽬标对象的⽬标⽅法orderService.modify();long end = System.currentTimeMillis();System.out.println("耗时" + (end - begin) + "毫秒");}
}

这种⽅式的优点:符合OCP开闭原则,同时采⽤的是关联关系,所以程序的耦合度较低。所以这种⽅案 是被推荐的。

主程序

public class Client {public static void main(String[] args) {// 创建⽬标对象OrderService target = new OrderServiceImpl();// 创建代理对象OrderService proxy = new OrderServiceProxy(target);// 调⽤代理对象的代理⽅法proxy.generate();proxy.modify();proxy.detail();}
}

以上就是代理模式中的静态代理,其中OrderService接⼝是代理类和⽬标类的共同接⼝。 OrderServiceImpl是⽬标类。OrderServiceProxy是代理类。 ⼤家思考⼀下:如果系统中业务接⼝很多,⼀个接⼝对应⼀个代理类,显然也是不合理的,会导致类爆 炸。怎么解决这个问题?动态代理可以解决。因为在动态代理中可以在内存中动态的为我们⽣成代理类 的字节码。代理类不需要我们写了。类爆炸解决了,⽽且代码只需要写⼀次,代码也会得到复⽤。 

3、动态代理

在程序运⾏阶段,在内存中动态⽣成代理类,被称为动态代理,⽬的是为了减少代理类的数量。解决代 码复⽤的问题。

在内存当中动态⽣成类的技术常⻅的包括:

  • JDK动态代理技术:只能代理接⼝。
  • CGLIB动态代理技术:CGLIB(Code Generation Library)是⼀个开源项⽬。是⼀个强⼤的,⾼性 能,⾼质量的Code⽣成类库,它可以在运⾏期扩展Java类与实现Java接⼝。它既可以代理接⼝,⼜ 可以代理类,底层是通过继承的⽅式实现的。性能⽐JDK动态代理要好。(底层有⼀个⼩⽽快的字 节码处理框架ASM。)
  • Javassist动态代理技术:Javassist是⼀个开源的分析、编辑和创建Java字节码的类库。是由东京⼯ 业⼤学的数学和计算机科学系的 Shigeru Chiba (千叶 滋)所创建的。它已加⼊了开放源代码 JBoss 应⽤服务器项⽬,通过使⽤Javassist对字节码操作为JBoss实现动态"AOP"框架。

3.1、jdk动态代理

我们还是使⽤静态代理中的例⼦:⼀个接⼝和⼀个实现类。

我们在静态代理的时候,除了以上⼀个接⼝和⼀个实现类之外,还要写⼀个代理类 UserServiceProxy,在动态代理中UserServiceProxy代理类是可以动态⽣成的。这个类不需要写。我 们直接写客户端程序即可:

public class Client {public static void main(String[] args) {// 第⼀步:创建⽬标对象OrderService target = new OrderServiceImpl();// 第⼆步:创建代理对象OrderService orderServiceProxy = Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), 调⽤处理器对象);// 第三步:调⽤代理对象的代理⽅法orderServiceProxy.detail();orderServiceProxy.modify();orderServiceProxy.generate();}
}

以上第⼆步创建代理对象是需要⼤家理解的: OrderService orderServiceProxy = Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), 调⽤处理器对象);

newProxyInstance这⾏代码做了两件事:

第⼀件事:在内存中动态的生成了一个代理类的字节码class

第⼆件事:new对象了,通过内存中生成的代理类这个代码,实例化代理对象

java.lang.reflect.Proxy。这是JDK提供的⼀个类(所以称为JDK动态代理)。主要是通过 这个类在内存中⽣成代理类的字节码。

其中newProxyInstance()⽅法有三个参数:

  • 第⼀个参数:类加载器。在内存中⽣成了字节码,要想执⾏这个字节码,也是需要先把这个字节码 加载到内存当中的。所以要指定使⽤哪个类加载器加载。并且jdk要求,目标类的加载器必须和代理类的类加载器使用同一个
  • 第⼆个参数:接⼝类型。代理类和⽬标类实现相同的接⼝,所以要通过这个参数告诉JDK动态代理 ⽣成的类要实现哪些接⼝。
  • 第三个参数:调⽤处理器。这是⼀个JDK动态代理规定的接⼝,接⼝全名: java.lang.reflect.InvocationHandler。显然这是⼀个回调接⼝,也就是说调⽤这个接⼝中⽅法的程 序已经写好了,就差这个接⼝的实现类了。

所以接下来我们要写⼀下java.lang.reflect.InvocationHandler接⼝的实现类,并且实现接⼝中的⽅法, 代码如下:

public class TimerInvocationHandler implements InvocationHandler {@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {return null;}
}

InvocationHandler接⼝中有⼀个⽅法invoke,这个invoke⽅法上有三个参数:

  • 第⼀个参数:Object proxy。代理对象。设计这个参数只是为了后期的⽅便,如果想在invoke⽅法中 使⽤代理对象的话,尽管通过这个参数来使⽤。
  • 第⼆个参数:Method method。⽬标⽅法。
  • 第三个参数:Object[] args。⽬标⽅法调⽤时要传的参数。

我们将来肯定是要调⽤“⽬标⽅法”的,但要调⽤⽬标⽅法的话,需要“⽬标对象”的存在,“⽬标对象”从 哪⼉来呢?我们可以给TimerInvocationHandler提供⼀个构造⽅法,可以通过这个构造⽅法传过来“⽬标 对象”,代码如下:

public class TimerInvocationHandler implements InvocationHandler {// ⽬标对象private Object target;// 通过构造⽅法来传⽬标对象public TimerInvocationHandler(Object target) {this.target = target;}
//当代理对象调用代理方法的时候 这个invoke会被jdk调用@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {return null;}
}

有了⽬标对象我们就可以在invoke()⽅法中调⽤⽬标⽅法了。代码如下:

public class TimerInvocationHandler implements InvocationHandler {// ⽬标对象private Object target;// 通过构造⽅法来传⽬标对象public TimerInvocationHandler(Object target) {this.target = target;}@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {// ⽬标执⾏之前增强。long begin = System.currentTimeMillis();// 调⽤⽬标对象的⽬标⽅法Object retValue = method.invoke(target, args);// ⽬标执⾏之后增强。long end = System.currentTimeMillis();System.out.println("耗时"+(end - begin)+"毫秒");// ⼀定要记得返回。return retValue;}
}

到此为⽌,调⽤处理器就完成了。接下来,应该继续完善Client程序

public class Client {public static void main(String[] args) {// 创建⽬标对象OrderService target = new OrderServiceImpl();// 创建代理对象OrderService orderServiceProxy = (OrderService) Proxy.newProxyInstance(target.getClass().getClassLoader(),target.getClass().getInterfaces(),new TimerInvocationHandler(target));// 调⽤代理对象的代理⽅法orderServiceProxy.detail();orderServiceProxy.modify();orderServiceProxy.generate();}
}

⼤家可能会⽐较好奇:那个InvocationHandler接⼝中的invoke()⽅法没看⻅在哪⾥调⽤呀? 注意:当你调⽤代理对象的代理⽅法的时候,注册在InvocationHandler接⼝中的invoke()⽅法会被调 ⽤。

orderServiceProxy.detail();
orderServiceProxy.modify();
orderServiceProxy.generate();

这三⾏代码中任意⼀⾏代码执⾏,注册在InvocationHandler接⼝中 的invoke()⽅法都会被调⽤。

学到这⾥可能会感觉有点懵,折腾半天,到最后这不是还得写⼀个接⼝的实现类吗?没省劲⼉呀? 你要这样想就错了!!!! 我们可以看到,不管你有多少个Service接⼝,多少个业务类,这个TimerInvocationHandler接⼝是不是 只需要写⼀次就⾏了,代码是不是得到复⽤了!!!! ⽽且最重要的是,以后程序员只需要关注核⼼业务的编写了,像这种统计时间的代码根本不需要关注。 因为这种统计时间的代码只需要在调⽤处理器中编写⼀次即可。

到这⾥,JDK动态代理的原理就结束了。

不过我们看以下这个代码确实有点繁琐,对于客户端来说,⽤起来不⽅便:

public class ProxyUtil {public static Object newProxyInstance(Object target) {return Proxy.newProxyInstance(target.getClass().getClassLoader(),target.getClass().getInterfaces(),new TimerInvocationHandler(target));}
}

这样客户端代码就不需要写那么繁琐了:

public class Client {public static void main(String[] args) {// 创建⽬标对象OrderService target = new OrderServiceImpl();// 创建代理对象OrderService orderServiceProxy = (OrderService) ProxyUtil.newProxyInstance(target);// 调⽤代理对象的代理⽅法orderServiceProxy.detail();orderServiceProxy.modify();orderServiceProxy.generate();}
}

3.2、CGLIB动态代理

jdk只能代理接口,而CGLIB既可以代理接⼝,⼜可以代理类。底层采⽤继承的⽅式实现。所以被代理的⽬标类不能使⽤final 修饰。 使⽤CGLIB,需要引⼊它的依赖:

<dependency><groupId>cglib</groupId><artifactId>cglib</artifactId><version>3.3.0</version>
</dependency>

一个没有实现类的接口

public class UserService {public void login(){System.out.println("⽤户正在登录系统....");}public void logout(){System.out.println("⽤户正在退出系统....");}
}

使⽤CGLIB在内存中为UserService类⽣成代理类,并创建对象

public class Client {public static void main(String[] args) {// 创建字节码增强器Enhancer enhancer = new Enhancer();// 告诉cglib要继承哪个类enhancer.setSuperclass(UserService.class);// 设置回调接⼝enhancer.setCallback(⽅法拦截器对象);// ⽣成源码,编译class,加载到JVM,并创建代理对象UserService userServiceProxy = (UserService)enhancer.create();userServiceProxy.login();userServiceProxy.logout();}
}

和JDK动态代理原理差不多,在CGLIB中需要提供的不是InvocationHandler,⽽是: net.sf.cglib.proxy.MethodInterceptor 编写MethodInterceptor接⼝实现类:

public class TimerMethodInterceptor implements MethodInterceptor {@Overridepublic Object intercept(Object target, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {return null;}
}

MethodInterceptor接⼝中有⼀个⽅法intercept(),该⽅法有4个参数:

第⼀个参数:⽬标对象

第⼆个参数:⽬标⽅法

第三个参数:⽬标⽅法调⽤时的实参

第四个参数:代理⽅法

在MethodInterceptor的intercept()⽅法中调⽤⽬标以及添加增强:

public class TimerMethodInterceptor implements MethodInterceptor {@Overridepublic Object intercept(Object target, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {// 前增强long begin = System.currentTimeMillis();// 调⽤⽬标Object retValue = methodProxy.invokeSuper(target, objects);// 后增强long end = System.currentTimeMillis();System.out.println("耗时" + (end - begin) + "毫秒");// ⼀定要返回return retValue;}
}

回调已经写完了,可以修改客户端程序了:

public class Client {public static void main(String[] args) {// 创建字节码增强器Enhancer enhancer = new Enhancer();// 告诉cglib要继承哪个类enhancer.setSuperclass(UserService.class);// 设置回调接⼝enhancer.setCallback(new TimerMethodInterceptor());// ⽣成源码,编译class,加载到JVM,并创建代理对象UserService userServiceProxy = (UserService)enhancer.create();userServiceProxy.login();userServiceProxy.logout();}
}

对于⾼版本的JDK,如果使⽤CGLIB,需要在启动项中添加两个启动参数:

  • --add-opens java.base/java.lang=ALL-UNNAMED
  • --add-opens java.base/sun.net.util=ALL-UNNAMED 

二、AOP

1、aop场景介绍

⼀般⼀个系统当中都会有⼀些系统服务,例如:⽇志、事务管理、安全等。这些系统服务被称为:交叉业务 这些交叉业务⼏乎是通⽤的,不管你是做银⾏账户转账,还是删除⽤户数据。⽇志、事务管理、安全, 这些都是需要做的。

如果在每⼀个业务处理过程当中,都掺杂这些交叉业务代码进去的话,存在两⽅⾯问题:

第⼀:交叉业务代码在多个业务流程中反复出现,显然这个交叉业务代码没有得到复⽤。并且修改 这些交叉业务代码的话,需要修改多处。

第⼆:程序员⽆法专注核⼼业务代码的编写,在编写核⼼业务代码的同时还需要处理这些交叉业 务。 使⽤AOP可以很轻松的解决以上问题。

下图可以帮助快速理解AOP的思想:

⽤⼀句话总结AOP:将与核⼼业务⽆关的代码独⽴的抽取出来,形成⼀个独⽴的组件,然后以横向交叉 的⽅式应⽤到业务流程当中的过程被称为AOP。

AOP的优点:

第⼀:代码复⽤性增强。

第⼆:代码易维护。

第三:使开发者更关注业务逻辑。

2、AOP的七⼤术语

  • 连接点 Joinpoint
    在程序的整个执⾏流程中,可以织⼊切⾯的位置。比如⽅法的执⾏前后,异常抛出之后等位置(指的是位置)如下

    public class UserService {public void do1() {System.out.println("do 1");}public void do2() {System.out.println("do 2");}public void do3() {System.out.println("do 3");}public void do4() {System.out.println("do 4");}public void do5() {System.out.println("do 5");}// 核⼼业务⽅法public void service() {try {// Joinpoint 连接点do1();// Joinpoint 连接点do2();// Joinpoint 连接点do3();// Joinpoint 连接点do5();}catch (Exception e){// Joinpoint 连接点}finally {// Joinpoint 连接点}}
    }
    
  • 切点 Pointcut

        在程序执⾏流程中,真正织⼊切⾯的⽅法(指的是方法)。(⼀个切点对应多个连接点)

public void service() {try {do1();//Pointcutdo2();//Pointcutdo3();//Pointcutdo5();//Pointcut}catch (Exception e){}}
  • 通知 Advice

        通知⼜叫增强,就是具体你要织⼊的代码,通知包括:

  • 前置通知
  • 后置通知
  • 环绕通知
  • 异常通知
  • 最终通知
    // 核⼼业务⽅法public void service() {try {//前置通知(do1方法前执行的代码)do1();//后置通知(do1方法后执行的代码)do2();// 环绕通知(在do3前和结束都有执行的代码)do3();// 环绕通知do5();}catch (Exception e){// 异常通知}finally {// 最终通知}}
  • 切⾯ Aspect

切点+通知就是切面

  • 织⼊ Weaving

把通知应用到目标对象的过程

  • 代理对象 Proxy

一个对象被织入后产生的新对象

  • ⽬标对象 Target

被织入通知的对象

3、切点表达式

切点表达式用来定义通知往哪些方法切入,语法格式如下

execution([访问控制权限修饰符] 返回值类型 [全限定类名]⽅法名(形式参数列表) [异常])

3.1、访问控制权限修饰符:

  • 可选项。
  • 没写,就是4个权限都包括。
  • 写public就表示只包括公开的⽅法。

3.2、返回值类型:

  • 必填项。
  • * 表示返回值类型任意。

3.3、全限定类名:

  • 可选项。
  • 两个点“..”代表当前包以及⼦包下的所有类。
  • 省略时表示所有的类。

3.4、⽅法名:

  • 必填项。
  • *表示所有⽅法。
  • set*表示所有的set⽅法。

3.5、形式参数列表:

  • 必填项
  • () 表示没有参数的⽅法
  • (..) 参数类型和个数随意的⽅法
  • (*) 只有⼀个参数的⽅法
  • (*, String) 第⼀个参数类型随意,第⼆个参数是String的。

3.6、异常:

  • 可选项。
  • 省略时表示任意异常类型。

表达式案例

service包下所有一delete开始的所有方法

execution(public * com.demo.service.*.delete*(..))

service包下所有类的所有方法

execution(* com.demo.service..*(..))

所有类的所有方法

execution(* *(..))

三、使⽤Spring的AOP

IoC使软件组件松耦合。AOP让你能够捕捉系统中经常使⽤的功能,把它转化成组件。

AOP(Aspect Oriented Programming):⾯向切⾯编程(AOP是⼀种编程技术) AOP是对OOP的补充延伸。

AOP底层使⽤的就是动态代理来实现的。

Spring的AOP使⽤的动态代理是:JDK动态代理 + CGLIB动态代理技术。Spring在这两种动态代理中灵 活切换,如果是代理接⼝,会默认使⽤JDK动态代理,如果要代理某个类,这个类没有实现接⼝,就会 切换使⽤CGLIB。当然,你也可以强制通过⼀些配置让Spring只使⽤CGLIB。

1、简介

Spring对AOP的实现包括以下3种⽅式:

第⼀种⽅式:Spring框架结合AspectJ框架实现的AOP,基于注解⽅式。

第⼆种⽅式:Spring框架结合AspectJ框架实现的AOP,基于XML⽅式。

第三种⽅式:Spring框架⾃⼰实现的AOP,基于XML配置⽅式。

实际开发中,都是Spring+AspectJ来实现AOP。所以我们重点学习第⼀种和第⼆种⽅式。

什么是AspectJ?(Eclipse组织的⼀个⽀持AOP的框架。AspectJ框架是独⽴于Spring框架之外的⼀个框 架,Spring框架⽤了AspectJ) 。

AspectJ项⽬起源于帕洛阿尔托(Palo Alto)研究中⼼(缩写为PARC)。该中⼼由Xerox集团资助, Gregor Kiczales领导,从1997年开始致⼒于AspectJ的开发,1998年第⼀次发布给外部⽤户,2001年发 布1.0 release。为了推动AspectJ技术和社团的发展,PARC在2003年3⽉正式将AspectJ项⽬移交给了 Eclipse组织,因为AspectJ的发展和受关注程度⼤⼤超出了PARC的预期,他们已经⽆⼒继续维持它的发 展。

2、Spring结合Aspectj基于注解实现AOP

导入依赖

<dependency><groupId>org.springframework</groupId><artifactId>spring-context</artifactId><version>6.0.2</version>
</dependency>
<dependency><groupId>org.springframework</groupId><artifactId>spring-aspects</artifactId><version>6.0.2</version>
</dependency>

注意context依赖已经包含aop注解了,所以不需要再单独引入以下aop依赖

<dependency><groupId>org.springframework</groupId><artifactId>spring-aop</artifactId><version>6.0.2</version>
</dependency>

Spring配置⽂件中添加context命名空间和aop命名空间并开启bean组件扫描

<?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:context="http://www.springframework.org/schema/context"xmlns:aop="http://www.springframework.org/schema/aop"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsdhttp://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"><!--开启组件扫描--><context:component-scan base-package="com.demo.service"/>
<!--开启⾃动代理--><aop:aspectj-autoproxy proxy-target-class="true"/>
</beans>

<aop:aspectj-autoproxy proxy-target-class="true"/>开启⾃动代理之后,凡事带有@Aspect注解的 bean都会⽣成代理对象。 

proxy-target-class="true" 表示采⽤cglib动态代理。

proxy-target-class="false" 表示采⽤jdk动态代理。默认值是false。即使写成false,当没有接⼝的时 候,也会⾃动选择cglib⽣成代理类。

 业务类(目标类)

@Component
public class OrderService {// ⽬标⽅法public void generate(){System.out.println("订单已⽣成!");}
}

 编写切⾯类

通知类型包括:

  • 前置通知:@Before ⽬标⽅法执⾏之前的通知
  • 后置通知:@AfterReturning ⽬标⽅法执⾏之后的通知
  • 环绕通知:@Around ⽬标⽅法之前添加通知,同时⽬标⽅法执⾏之后添加通知。
  • 异常通知:@AfterThrowing 发⽣异常之后执⾏的通知
  • 最终通知:@After 放在finally语句块中的通知
@Component
@Aspect
public class MyAspect {@Around("execution(* com.demo.service.OrderService.* (..))")public void aroundAdvice(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {System.out.println("环绕通知开始");// 执⾏⽬标⽅法。proceedingJoinPoint.proceed();System.out.println("环绕通知结束");}@Before("execution(* com.demo.service.OrderService.* (..))")public void beforeAdvice(){System.out.println("前置通知");}@AfterReturning("execution(* com.demo.service.OrderService.*(..))")public void afterReturningAdvice(){System.out.println("后置通知");}@AfterThrowing("execution(* com.demo.service.OrderService.*(..))")public void afterThrowingAdvice(){System.out.println("异常通知");}@After("execution(* com.demo.service.OrderService.*(..))")public void afterAdvice(){System.out.println("最终通知");}
}

 测试

    @Testpublic void testAOP(){ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");OrderService orderService = applicationContext.getBean("orderService", OrderService.class);orderService.generate();}

执行结果 

 通过上⾯的执⾏结果就可以判断他们的执⾏顺序了,这⾥不再赘述。 结果中没有异常通知,这是因为⽬标程序执⾏过程中没有发⽣异常。我们尝试让⽬标⽅法发⽣异常:

@Component
public class OrderService {// ⽬标⽅法public void generate(){System.out.println("订单已⽣成!");if (1 == 1) {throw new RuntimeException("模拟异常发⽣");}}
}

测试结果

 

 通过测试得知,当发⽣异常之后,最终通知也会执⾏,因为最终通知@After会出现在finally语句块中。 出现异常之后,后置通知和环绕通知的结束部分不会执⾏。

切面的先后顺序

业务流程当中不⼀定只有⼀个切⾯,可能有的切⾯控制事务,有的记录⽇志,有的进⾏安全 控制,如果多个切⾯的话,顺序如何控制:可以使⽤@Order注解来标识切⾯类,为@Order注解的value 指定⼀个整数型的数字,数字越⼩,优先级越⾼。 再定义⼀个切⾯类

@Aspect
@Component
@Order(1) //设置优先级
public class YourAspect {@Around("execution(* com.demo.service.OrderService.*(..))")public void aroundAdvice(ProceedingJoinPoint proceedingJoinPoint) throws Throwable{System.out.println("YourAspect环绕通知开始");// 执⾏⽬标⽅法。proceedingJoinPoint.proceed();System.out.println("YourAspect环绕通知结束");}@Before("execution(* com.demo.service.OrderService.*(..))")public void beforeAdvice() {System.out.println("YourAspect前置通知");}@AfterReturning("execution(* com.demo.service.OrderService.*(..))")public void afterReturningAdvice() {System.out.println("YourAspect后置通知");}@AfterThrowing("execution(* com.demo.service.OrderService.*(..))")public void afterThrowingAdvice() {System.out.println("YourAspect异常通知");}@After("execution(* com.demo.service.OrderService.*(..))")public void afterAdvice() {System.out.println("YourAspect最终通知");}
}
@Component
@Aspect
@Order(2) //设置优先级
public class MyAspect {@Around("execution(* com.demo.service.OrderService.*(..))")public void aroundAdvice(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {System.out.println("环绕通知开始");// 执⾏⽬标⽅法。proceedingJoinPoint.proceed();System.out.println("环绕通知结束");}@Before("execution(* com.demo.service.OrderService.*(..))")public void beforeAdvice(){System.out.println("前置通知");}@AfterReturning("execution(* com.demo.service.OrderService.*(..))")public void afterReturningAdvice(){System.out.println("后置通知");}@AfterThrowing("execution(* com.demo.service.OrderService.*(..))")public void afterThrowingAdvice(){System.out.println("异常通知");}@After("execution(* com.demo.service.OrderService.*(..))")public void afterAdvice(){System.out.println("最终通知");}
}

 测试程序

 通过修改@Order注解的整数值来切换顺序,执⾏测试程序

优化使⽤切点表达式 

观察上面切面类缺点是:

第⼀:切点表达式重复写了多次,没有得到复⽤。

第⼆:如果要修改切点表达式,需要修改多处,难维护。

可以这样做:将切点表达式单独的定义出来,在需要的位置引⼊即可。如下:

// 切⾯类
@Component
@Aspect
@Order(2)
public class MyAspect {@Pointcut("execution(* com.demo.service.OrderService.*(..))")public void pointcut() {}@Around("pointcut()")public void aroundAdvice(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {System.out.println("环绕通知开始");// 执⾏⽬标⽅法。proceedingJoinPoint.proceed();System.out.println("环绕通知结束");}@Before("pointcut()")public void beforeAdvice() {System.out.println("前置通知");}@AfterReturning("pointcut()")public void afterReturningAdvice() {System.out.println("后置通知");}@AfterThrowing("pointcut()")public void afterThrowingAdvice() {System.out.println("异常通知");}@After("pointcut()")public void afterAdvice() {System.out.println("最终通知");}}

使⽤@Pointcut注解来定义独⽴的切点表达式。

注意这个@Pointcut注解标注的⽅法随意,只是起到⼀个能够让@Pointcut注解编写的位置。

执⾏测试程序:

全注解开发AOP

就是编写⼀个类,在这个类上⾯使⽤⼤量注解来代替spring的配置⽂件,spring配置⽂件消失了,如下:

@Configuration
@ComponentScan("com.demo.service")
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class Spring6Configuration {
}

 测试程序也变化了:

    @Testpublic void testAOPWithAllAnnotation(){ApplicationContext applicationContext = new AnnotationConfigApplicationContext(Spring6Configuration.class);OrderService orderService = applicationContext.getBean("orderService",OrderService.class);orderService.generate();}

3、基于XML配置⽅式的AOP(了解)

目标类

// ⽬标类
public class VipService {public void add(){System.out.println("保存vip信息。");}
}

 编写切⾯类,并且编写通知

// 负责计时的切⾯类
public class TimerAspect {public void time(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {long begin = System.currentTimeMillis();//执⾏⽬标proceedingJoinPoint.proceed();long end = System.currentTimeMillis();System.out.println("耗时"+(end - begin)+"毫秒");}
}

 编写spring配置⽂件

<?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:context="http://www.springframework.org/schema/context"xmlns:aop="http://www.springframework.org/schema/aop"xsi:schemaLocation="http://www.springframework.org/schema/beanshttp://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/contexthttp://www.springframework.org/schema/context/spring-context.xsdhttp://www.springframework.org/schema/aophttp://www.springframework.org/schema/aop/spring-aop.xsd"><!--纳⼊spring bean管理--><bean id="vipService" class="com.demo.service.VipService"/><bean id="timerAspect" class="com.demo.service.TimerAspect"/><!--aop配置--><aop:config><!--切点表达式--><aop:pointcut id="p" expression="execution(* com.demo.service.VipService.*(..))"/><!--切⾯--><aop:aspect ref="timerAspect"><!--切⾯=通知 + 切点--><aop:around method="time" pointcut-ref="p"/></aop:aspect></aop:config>
</beans>

测试

    @Testpublic void testAOPXml(){ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-aop-xml.xml");VipService vipService = applicationContext.getBean("vipService", VipService.class);vipService.add();}

4、aop实际应用-日志记录

凡事在系统中进⾏修 改操作的,删除操作的,新增操作的,都要把操作的人记录下来。因为这⼏个操作是属于危险⾏为。例如 有业务类和业务⽅法:

@Component
//⽤户业务
public class UserService {public void getUser() {System.out.println("获取⽤户信息");}public void saveUser() {System.out.println("保存⽤户");}public void deleteUser() {System.out.println("删除⽤户");}public void modifyUser() {System.out.println("修改⽤户");}
}
// 商品业务类
@Component
public class ProductService {public void getProduct(){System.out.println("获取商品信息");}public void saveProduct(){System.out.println("保存商品");}public void deleteProduct(){System.out.println("删除商品");}public void modifyProduct(){System.out.println("修改商品");}
}

接下来我们使⽤aop来解决上⾯的需求:编写⼀个负责安全的切⾯类

@Component
@Aspect
public class SecurityAspect {@Pointcut("execution(* com.demo.service..save*(..))")public void savePointcut(){}@Pointcut("execution(* com.demo.service..delete*(..))")public void deletePointcut(){}@Pointcut("execution(* com.demo.service..modify*(..))")public void modifyPointcut(){}@Before("savePointcut() || deletePointcut() || modifyPointcut()")public void beforeAdivce(JoinPoint joinpoint){System.out.println("XXX操作员正在操作"+joinpoint.getSignature().getName()+"⽅法");}
}

测试

    @Testpublic void testSecurity() {ApplicationContext applicationContext = new AnnotationConfigApplicationContext(Spring6Configuration.class);UserService userService = applicationContext.getBean("userService", UserService.class);ProductService productService = applicationContext.getBean("productService", ProductService.class);userService.getUser();userService.saveUser();userService.deleteUser();userService.modifyUser();productService.getProduct();productService.saveProduct();productService.deleteProduct();productService.modifyProduct();}

结果

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/127650.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

百度输入法全面升级,打造首个基于大模型的输入法原生应用

基于文心一言&#xff0c;百度输入法宣布全面升级&#xff0c;打造行业首个“基于大模型的输入法原生应用”&#xff0c;从“输入工具”全面转型为“AI创作工具”。 近日&#xff0c;百度文心一言正式向公众开放。基于文心一言&#xff0c;百度输入法宣布全面升级&#xff0c;打…

【JAVA】抽象类与接口

作者主页&#xff1a;paper jie_的博客 本文作者&#xff1a;大家好&#xff0c;我是paper jie&#xff0c;感谢你阅读本文&#xff0c;欢迎一建三连哦。 本文录入于《JAVASE语法系列》专栏&#xff0c;本专栏是针对于大学生&#xff0c;编程小白精心打造的。笔者用重金(时间和…

【Linux】——网络基础:http协议

目录 前言 应用层 认识协议 协议的概念 传输结构化数据 序列化和反序列化 网络版本计算器 服务器端Server 客户端Client 协议定制 其它 运行效果 HTTP协议 HTTP的简介 认识URL urlencode和urldecode HTTP协议格式 HTTP请求 HTTP响应 HTTP的方法 GET和POST…

顺序表详解

&#x1f493; 博客主页&#xff1a;江池俊的博客⏩ 收录专栏&#xff1a;数据结构探索&#x1f449;专栏推荐&#xff1a;✅C语言初阶之路 ✅C语言进阶之路&#x1f4bb;代码仓库&#xff1a;江池俊的代码仓库&#x1f525;编译环境&#xff1a;Visual Studio 2022&#x1f38…

Tampermonkey实践:安装引导及开发一个网页背景色更改插件

&#x1f3c6;作者简介&#xff0c;黑夜开发者&#xff0c;CSDN领军人物&#xff0c;全栈领域优质创作者✌&#xff0c;CSDN博客专家&#xff0c;阿里云社区专家博主&#xff0c;2023年6月csdn上海赛道top4。 &#x1f3c6;数年电商行业从业经验&#xff0c;历任核心研发工程师…

leetcode 2. 两数相加(java)

两数相加 题目描述哨兵技巧代码演示&#xff1a; 递归算法专题 题目描述 难度 - 中等 leetcode 2. 两数相加 给你两个 非空 的链表&#xff0c;表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的&#xff0c;并且每个节点只能存储 一位 数字。 请你将两个数相加&…

宇凡微发布2.4G合封芯片YE08,融合高性能MCU与射频收发功能

宇凡微在2023年推出了全新的2.4G合封芯片YE08&#xff0c;该芯片结合了32位高性能MCU和强大的2.4GHz无线通信功能&#xff0c;为各种远程遥控应用提供卓越性能和广泛应用潜力。 深入了解YE08内部构造 YE08芯片内部融合了两颗强大的芯片&#xff1a;PY32F002B MCU和G350 2.4G通…

【LeetCode-中等题】78. 子集

文章目录 组合并集问题汇总&#xff1a;题目方法一&#xff1a;动态规划方法二&#xff1a;递归加回溯(关键----startIndex) 组合并集问题汇总&#xff1a; 1、子集去重版本 2、组合非去重版本 3、组合去重版本 题目 注意&#xff1a;这里的nums数组里面的元素是各不相同的&a…

OLED透明屏触控:引领未来科技革命的创新力量

OLED透明屏触控技术作为一项颠覆性的创新&#xff0c;正在引领新一轮科技革命。它将OLED显示技术与触摸技术相结合&#xff0c;实现了透明度和触控功能的完美融合。 在这篇文章中&#xff0c;尼伽将通过引用最新的市场数据、报告和行业动态&#xff0c;详细介绍OLED透明屏触控…

《python趣味工具》——酷炫二维码(3)计算机二级考试题

昨天我们学习了如何批量制作合适的二维码&#xff0c;今天来刷几道题练练手&#xff01; 文章目录 1. 制作名单2. 年会抽奖来啦3. 精准查找 1. 制作名单 秋招来了&#xff01;hr部门需要获得简历初筛后的候选者名单&#xff0c;所有候选者简历都按照“小明_xx大学.pdf”命名放…

建站系列(五)--- 前端开发语言之HTML、CSS、JavaScript

目录 相关系列文章前言一、前端开发与后端开发二、前端语言简介&#xff08;一&#xff09;、HTML&#xff08;二&#xff09;、CSS&#xff08;三&#xff09;、JavaScript 三、学习指导&#xff08;一&#xff09;、开发环境&#xff08;二&#xff09;、第一个Hello&#xf…

咖啡店小程序:吸引顾客的创新营销手段

近日&#xff0c;“酱香拿铁”的大火让大家再次把目标聚焦在年轻人都喜欢的咖啡上。现在咖啡已经成为年轻一代的社交硬通货&#xff0c;咖啡店也遍地开花。而随着移动互联网的快速发展&#xff0c;咖啡店小程序已经成为了各大咖啡店主的选择&#xff0c;因为它提供了便捷的方式…

BUUCTF rip 1

使用linux的file命令查看基本信息 64位 使用IDA64位进行反编译 看到gets就肯定有栈溢出 能看到有一个 _system函数&#xff0c;改函数能执行系统命令 既然反编译有这个函数说明有地方调用了他 果然在一个fun函数中有调用&#xff0c;执行的命令是 /bin/sh 也就是一个后门函数&…

C# Linq源码分析之Take(五)

概要 本文在C# Linq源码分析之Take&#xff08;四&#xff09;的基础上继续从源码角度分析Take的优化方法&#xff0c;主要分析Where.Select.Take的使用案例。 Where.Select.Take的案例分析 该场景模拟我们显示中将EF中与数据库关联的对象进行过滤&#xff0c;然后转换成Web…

Spring Cloud:构建微服务的最佳实践

&#x1f337;&#x1f341; 博主猫头虎&#xff08;&#x1f405;&#x1f43e;&#xff09;带您 Go to New World✨&#x1f341; &#x1f984; 博客首页——&#x1f405;&#x1f43e;猫头虎的博客&#x1f390; &#x1f433; 《面试题大全专栏》 &#x1f995; 文章图文…

Java 基于 SpringBoot 的酒店管理系统,附源码和数据库

博主介绍&#xff1a;✌程序员徐师兄、7年大厂程序员经历。全网粉丝30W,Csdn博客专家、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ 文章目录 一、前言介绍二、系统结构三、系统详细实现3.1用户信息管理3.2会员信息管理3.3客房信息管理3.4收藏…

MySQL——DQL union合并、limit限制与DDL建表和删表

一、Union 合并 union:是实现两个查询结果的合并。 例如&#xff1a;当我们查询员工名字为manager 和 salesman的员工名字和 工作&#xff1f; select e.ename,e.job from emp e where e.jobmanager or e.job salesman; select e.ename,e.job from emp e where e.job in(man…

Notpad++常用正则表达式替换案例集锦

1、在每行的开头加上单引号 2、在每行的结尾加上单引号 3、“删除”某个关键字之前字符串 原始字符串&#xff1a; 注&#xff1a;仅保留含有"[条件日志]:"之后的内容&#xff0c;“日志:”前面的内容“删除”掉&#xff0c;即替换为“”。 4、“删除”某个关键字…

浅谈OPenGL中的纹理过滤

纹理图像中的纹理单元和屏幕上的像素几乎从来不会形成一对一的对应关系。如果程序员足够细心&#xff0c;确实可以实现这个效果&#xff0c;但这需要在对几何图形进行纹理贴图时进行精心的计划&#xff0c;使出现在屏幕上的纹理单元和像素能够对齐&#xff08;实际上在用OPenGL…

搭建HTTPS服务器

HTTPS代理服务器的作用与价值 HTTPS代理服务器可以帮助我们实现网络流量的转发和加密&#xff0c;提高网络安全性和隐私保护。本文将指导您从零开始搭建自己的HTTPS代理服务器&#xff0c;让您更自由、安全地访问互联网。 1. 准备工作&#xff1a;选择服务器与操作系统 a. 选…