一、AOP简介
1、AOP基本介绍
Spring中的另一个重点是AOP。AOP 的全称是“Aspect Oriented Programming”,即面向切面编程,它将业务逻辑的各个部分进行隔离,使开发人员在编写业务逻辑时可以专心于核心业务,从而提高了开发效率。简单来说就是不通过修改源代码方式,在主干功能里面添加新功能。
在传统的业务处理代码中,我们一般都会进行事务处理、日志记录等操作。以往通过使用OOP的思想可以以组合或者继承的方式实现代码的重用,但如果要实现某个功能(如日志记录),此时同样的代码就会分散到各个方法中。当想要关闭某个功能或者对功能进行修改,就必须要修改所有的相关方法。这不但增加了开发人员的工作量,而且提高了代码的出错率。
AOP 采取横向抽取机制,取代了传统纵向继承体系的重复性代码。这种横向抽取机制能够将影响到多个类的公共行为封装到一个可重用的模块,并将其命名为“Aspect”,也就是我们所说的切面。切面指的是那些与业务无关,但被业务模块所共同调用的逻辑或责任,将它们封装起来,从而减少系统中的重复代码,降低了模块之间的耦合度,有利于后续的可操作和可维护。
举个例子:在某个系统中,日志记录是基础功能,系统中的业务流程都需要进行日志记录。如果将日志记录功能写到每一个业务流程中,就会造成代码冗余,也增加了维护的难度。假设此时日志记录逻辑发生变化,那每个业务流程中的日志记录代码都需要进行修改,这种方式显然不可取。相反,如果将日志记录功能抽取出来,作为独立的模块,当业务流程需要的时候,系统自动将日志记录功能切入到业务流程中,这样维护起来就方便多了。而这正式AOP所能实现的。如下图:
2、AOP的相关术语
术语 | 描述 |
---|---|
横切关注点 |
对哪些方法进行拦截,拦截后怎么处理,这些关注点称之为横切关注点 |
Aspect(切面) |
通常是一个类,里面可以定义切入点和通知。一个应用程序可以拥有任意数量的切面 |
JointPoint(连接点) |
程序执行过程中明确的点,即:业务流程在运行过程中需要插入切面的具体位置,一般是方法的调用。因为Spring只支持方法类型的连接点,所以在Spring中连接点指的就是被拦截到的方法,实际上连接点还可以是字段或者构造器 |
Advice(通知) |
切面的具体实现方法,在拦截到 Joinpoint 之后要做的事情,即对切入点增强的内容 |
Pointcut(切入点) |
就是带有通知的连接点,用于定义通知应该切入到哪些连接点上,不同的通知通常需要切入到不同的连接点上。在程序中主要体现为书写切入点表达式。 |
Weave(织入) |
表示切入,也称为织入。将切面应用到目标对象从而创建一个新的代理对象的过程。这个过程可以发生在编译期、类装载期及运行期 |
Introduction(引入) |
在不修改代码的前提下,引入可以在运行期为类动态地添加一些方法或字段 |
代理(Proxy) |
表示代理对象。将通知应用到目标对象之后被动态创建的对象。可以简单地理解为,代理对象为目标对象的业务逻辑功能加上被切入的切面所形成的对象。Spring中的AOP代理可以使JDK动态代理,也可以是CGLIB代理,前者基于接口,后者基于子类 |
目标对象(Target Object) |
包含连接点的对象,即被一个或者多个切面所通知的对象。也被称作被通知或被代理对象 |
3、AOP中的通知类型
通知类型 | 描述 |
---|---|
前置通知(Before) |
在目标方法被调用之前做增强处理,@Before只需要指定切入点表达式即可 |
返回后通知(AfterReturning) |
在目标方法正常完成后做增强,@AfterReturning除了指定切入点表达式后,还可以指定一个返回值形参名returning,代表目标方法的返回值 |
抛出异常后通知(AfterThrowing) |
主要用来处理程序中未处理的异常,@AfterThrowing除了指定切入点表达式后,还可以指定一个throwing的返回值形参名,可以通过该形参名来访问目标方法中所抛出的异常对象 |
后置通知(After) |
在目标方法完成之后做增强,无论目标方法时候成功完成。@After可以指定一个切入点表达式 |
环绕通知(Around) |
在目标方法完成前后做增强处理,环绕通知是最重要的通知类型,像事务、日志等都是环绕通知 |
4、切入点表达式
切入点指示符用来指示切入点表达式目的,在Spring AOP中目前只有执行方法这一个连接点 。切入点表达式的格式如下:
execution([可见性] 返回类型 [声明类型].方法名(参数) [异常]),其中[]中的为可选,其他的还支持通配符的使用:
*:匹配所有字符
..:一般用于匹配多个包,多个参数
+:表示类及其子类
切入点表达式中还可以使用运算符,如:&&、||、!
下面举个例子,来说明切入点表达式的使用:
(1)标准的表达式写法:public void com.yht.controller.UserController.findUser()
(2)全通配写法:*..*.*(..)
(3)省略访问修饰符:void com.yht.controller.UserController.findUser()
(4)返回值使用通配符*:* com.yht.controller.UserController.findUser()
(5)包名使用通配符*:* *.*.*.UserController.findUser() //有几级包就写几个*
(6)类名和方法名使用通配符*:*..*.*()
(7)参数列表
如果有参数,在方法的()加入*——(*)
有无参数均可:在方法的()加入..——(..)
(8)通常写法:* com.yht.controller.UserController.*(..)
关于切入点表达式的其他介绍可参考一下博客:
https://blog.51cto.com/lavasoft/172292
https://www.cnblogs.com/sjqq/p/10241781.html
二、Spring中的代理模式
前面介绍了:Spring中的AOP代理可以使JDK动态代理,也可以是CGLIB代理,前者基于接口,后者基于子类 。接下来就对这两种代理方式进行实现。
1、JDK动态代理
(1)创建IOderDao接口及其子类OrderDaoImpl。
public interface IOrderDao {
void addOrder();
void deleteOrder();
void updateOrder();
void searchOrder();
}
public class OrderDaoImpl implements IOrderDao {
@Override
public void addOrder() {
System.out.println("添加订单");
}
@Override
public void deleteOrder() {
System.out.println("删除订单");
}
@Override
public void updateOrder() {
System.out.println("更新订单");
}
@Override
public void searchOrder() {
System.out.println("查询订单");
}
}
(2)创建切面类 MyAspect
public class MyAspect {
public void before(){
System.out.println("方法执行之前");
}
public void after(){
System.out.println("方法执行之后");
}
}
(3) 创建代理类 MyBeanFactory
public class MyBeanFactory {
public static IOrderDao getBean() {
// 准备目标类
final IOrderDao customerDao = new OrderDaoImpl();
// 创建切面类实例
final MyAspect myAspect = new MyAspect();
// 使用代理类,进行增强
//Proxy 的 newProxyInstance() 方法的第一个参数是当前类的类加载器,第二参数是所创建实例的实现类的接口,第三个参数就是需要增强的方法
return (IOrderDao) Proxy.newProxyInstance(
MyBeanFactory.class.getClassLoader(),
new Class[] { IOrderDao.class }, new InvocationHandler() {
public Object invoke(Object proxy, Method method,
Object[] args) throws Throwable {
myAspect.before(); // 前增强
Object obj = method.invoke(customerDao, args);
myAspect.after(); // 后增强
return obj;
}
});
}
}
(4)进行测试。
@Test
public void test() {
// 从工厂获得指定的内容(相当于spring获得,但此内容时代理对象)
IOrderDao orderDao = MyBeanFactory.getBean();
// 执行方法
orderDao.updateOrder();
System.out.println("=====================");
orderDao.searchOrder();
}
结果如下:
2、cglib动态代理
(1)引入jar包
<!-- https://mvnrepository.com/artifact/cglib/cglib-nodep -->
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib-nodep</artifactId>
<version>3.1</version>
</dependency>
(2)创建目标类 Goods
public class Goods{
public void addGoods() {
System.out.println("添加货物");
}
public void deleteGoods() {
System.out.println("删除货物");
}
public void updateGoods() {
System.out.println("更新货物");
}
public void searchGoods() {
System.out.println("查询货物");
}
}
(3) 创建代理类 GoodsBeanFactory
public class GoodsBeanFactory {
public static Goods getBean() {
// 准备目标类
final Goods goodsDao = new Goods();
// 创建切面类实例
final MyAspect myAspect = new MyAspect();
// 生成代理类,CGLIB在运行时,生成指定对象的子类,增强
Enhancer enhancer = new Enhancer();
// 确定需要增强的类
enhancer.setSuperclass(goodsDao.getClass());
// 添加回调函数
enhancer.setCallback(new MethodInterceptor() {
// intercept 相当于 jdk invoke,前三个参数与 jdk invoke—致
@Override
public Object intercept(Object proxy, Method method, Object[] args,
MethodProxy methodProxy) throws Throwable {
myAspect.before(); // 前增强
Object obj = method.invoke(goodsDao, args); // 目标方法执行
myAspect.after(); // 后增强
return obj;
}
});
// 创建代理类
Goods goodsDaoProxy = (Goods) enhancer.create();
return goodsDaoProxy;
}
}
(4)进行测试
@Test
public void testGoods() {
// 从工厂获得指定的内容(相当于spring获得,但此内容时代理对象)
Goods goods = GoodsBeanFactory.getBean();
// 执行方法
goods.deleteGoods();
System.out.println("=====================");
goods.searchGoods();
}
结果如下: