AOP 简介
AOP 面向切面编程,是一种编程思想,是将那些与业务无关,但却对多个对象产生影响的公共行为和逻辑,抽取出来,封装成一个切面,然后注入到对象中去。这些公共的行为和逻辑被称为横切关注点
AOP 可以对某个对象或某个对象的某个功能进行增强,也可以在某个方法执行前后做一些额外的动作
AOP 实现技术
SpringFramework 中 AOP 有静态代理和动态代理,两种实现技术。
-
静态代理:静态代理需要自己编写代理类,组合原有的目标对象,并实现原有目标对象的实现接口,以此来做到对原有对象方法的增强;
-
动态代理:动态代理只需要编写增强逻辑类,在运行时动态的将增强逻辑组合进原有的目标对象,即可生成代理对象,完成对目标对象的方法功能增强。
OOP(面向对象)的不足 & 横切关注点的思想
OOP(面向对象)有一个很大的不足,它无法即将相同、重复的逻辑分离出去,OOP只能尽可能的减少重复的代码量,却无法避免重复的代码出现
横切关注点:下面图中的四个方法都调用了一个相同的逻辑(日志记录)
图中这种框可以是一个类的几个方法,可以是多个类的不同方法。只要这些方法的开始 / 结束都有相同的逻辑,那么我们就可以把这些相同的逻辑,抽取出来视为一体,这个思想就叫做横切,抽取出来的逻辑组成的虚拟的结构,称之为横切面(上图红框框出来的就可以理解为一个横切面)
动态代理
SpringFramework AOP 使用到了两种动态代理。JDK 动态代理 和 CGLIB 动态代理;Java 在 JDK1.3 中就引入了动态代理。JDK 的动态代理默认会给原有对象中的所有方法都进行增强。如果我们只需要对某一个或某一类的方法做增强,可以做过滤方法实现,如下面的代码模板
public void init() throws ServletException {
DemoService demoService = (DemoService) BeanFactory.getBean("demoService");
Class<? extends DemoService> clazz = demoService.getClass();
this.demoService = (DemoService) Proxy
.newProxyInstance(clazz.getClassLoader(), clazz.getInterfaces(), (proxy, method, args) -> {
List<String> list = Arrays.asList("add", "subtract", "multiply", "divide");
if (list.contains(method.getName())) {
LogUtils.printLog("DemoService", method.getName(), args);
}
return method.invoke(demoService, args);
});
}
上面图中的红框我们说它称为横切面,英文表示为 Aspect ,它表示的是分布在一个 / 多个类的多个方法中的相同逻辑。利用动态代理,将这部分相同的逻辑抽取为一个独立的 Advisor 增强器,并在原始对象的初始化过程中,动态组合原始对象并产生代理对象,同样能完成一样的功能增强。在此基础上,通过指定增强的类名、方法名(甚至方法参数列表类型等),可以更细粒度的对方法增强。使用这种方式,可以在不修改原始代码的前提下,对已有任意代码的功能增强。而这种针对相同逻辑的扩展和抽取,就是所谓的面向切面编程(Aspect Oriented Programming,AOP)。
原生动态代理与Cglib动态代理回顾
动态代理场景演绎
下面将使用【游戏陪玩】 的故事来演绎动态代理。
应该是 2017 年左右吧,网络上涌上了一批游戏陪玩的平台和服务,游戏玩家可以通过在平台上付费邀请 “小姐姐” 来一起组队玩游戏。这种游戏陪玩平台的兴起,既满足了部门游戏玩家不能找到一起组排的队友的问题,也为一些游戏玩的不错 / 声音气质等很讨喜的玩家提供了收入的渠道。
下面要演绎的场景,是一个普通玩家寻找陪玩的过程。
定义一个普通玩家
public class Player {
private String name;
public Player(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
定义陪玩,已经陪玩的行为
public class Partner {
private String name;
public Partner(String name) {
this.name = name;
}
/**
* 下面定义陪玩的行为
* 收钱、陪玩
*/
public void receiveMoney(int money) {
System.out.println(name + "收到佣金:" + money + "元 ~ ");
}
public void playWith(Player player) {
System.out.println(name + "与" + player.getName() + "一起愉快地玩耍 ~ ");
}
}
定义模拟场景:假设有一个游戏玩家,名为 “郝武辽” ,他去找一个名为 “肖洁洁” 的陪玩陪他一起玩游戏。
public class Client {
public static void main(String[] args) {
Player player = new Player("郝武辽");
Partner partner = new Partner("肖洁洁");
partner.receiveMoney(2000);
partner.playWith(player);
}
}
执行结果
肖洁洁收到佣金:2000元 ~
肖洁洁与郝武辽一起愉快地玩耍 ~
jdk动态代理重构上面场景
观察上面的代码可以发现一个问题,如果实在现实场景中,这个 郝武辽 是怎么找到的 肖洁洁 呢?
我们是在Client 的 main 方法中,由 main 方法把他们俩搞到一起的吧,那如果不看 Client 的话,那就可以理解为,郝武辽 直接去 肖洁洁 的家里找的她,然后让她陪她一起玩游戏。
但是现实场景中,通常都是玩家去陪玩的平台上找陪玩,下面我们就来搞一个陪玩平台。
设计陪玩平台
陪玩平台中入驻了一些陪玩的选手,这里咱可以使用静态代码块来初始化一下:
初始化陪玩平台前,我们先将 Partner 行为抽取成一个接口
public interface Partner {
void receiveMoney(int money);
void playWith(Player player);
}
Partner 的实现类 IndividualPartner ,代表个人陪玩
public class IndividualPartner implements Partner {
private String name;
public IndividualPartner(String name) {
this.name = name;
}
public String getName() {
return name;
}
@Override
public void receiveMoney(int money) {
System.out.println(name + "收到佣金:" + money + "元 ~ ");
}
@Override
public void playWith(Player player) {
System.out.println(name + "与" + player.getName() + "一起愉快地玩耍 ~ ");
}
}
初始化陪玩平台,然后,陪玩平台要根据玩家的预算,推荐合适的陪玩
public class PartnerPlatform {
private static List<Partner> partners = new ArrayList<>();
static {
partners.add(new IndividualPartner("肖洁洁"));
partners.add(new IndividualPartner("田苟"));
partners.add(new IndividualPartner("高总裁"));
}
public static Partner getPartner(int money) {
Partner partner = partners.remove(0);
return (Partner) Proxy.newProxyInstance(partner.getClass().getClassLoader(), partner.getClass().getInterfaces(),
new InvocationHandler() {
private int budget = money;
private boolean status = false;
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (method.getName().equals("receiveMoney")) {
int money = (int) args[0];
// 平台需要运营,抽成一半
args[0] = money / 2;
// 如果在付钱时没给够,则标记budget为异常值
this.status = money >= budget;
}
if (status) {
return method.invoke(partner, args);
}
return null;
}
});
}
}
这里的代码中引入了一个 status 的标志位,来代表玩家的钱有没有给到位。
Client 的测试代码
public class Client {
public static void main(String[] args) throws Exception {
Player player = new Player("郝武辽");
Partner partner = PartnerPlatform.getPartner(50);
partner.receiveMoney(20);
partner.playWith(player);
}
}
一开始我们先不给足钱,运行 main 方法,控制台没有任何输出。。。
然后,将 20 改成 200 ,发现控制台可以打印输出了:
肖洁洁收到佣金:100元 ~
肖洁洁与郝武辽一起愉快地玩耍 ~
JDK 动态代理核心 API
JDK 的动态代理,要求被代理的类必须实现一个以上的接口,代理对象的创建使用 Proxy.newProxyInstance()
方法,该方法中有三个参数:
-
ClassLoader loader
: 被代理对象类所属的类加载器 -
Class<?> [] interfaces
: 被代理对象所属类实现的接口 -
InvocationHandler h
: 代理的具体代码实现
这三个参数中,前面两个都很容易理解,但是最后一个 InvocationHandler
是一个接口,它的核心方法 invoke()
中也有三个参数:
-
Object proxy
: 代理对象的引用(代理后的) -
Method method
: 代理对象执行的方法 -
Object[] args
: 代理对象执行方法的参数列表
具体的代理逻辑就写在 InvocationHandler.invoke()
方法中
CGLIB 动态代理重构上面场景
JDK 动态代理唯一一个让我们可能不爽的就是这个接口的抽取。不过下面的 Cglib 可以直接使用字节码增强的技术,同样实现动态代理。
引入 CGLIB 依赖
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.1</version>
</dependency>
使用 CGLIB 须知
- 被代理的类不能是 final 的(CGLIB 动态代理会创建子类,final 修饰的 Class 是无法被继承的)
- 被代理类必须有默认的 / 无参构造(底层反射创建对象时,拿不到构造方法参数)。
改造代码
Partner 新增一个无参构造
public class Partner {
public Partner() { }
}
修改 PartnerPlatform.getPartner() 方法,改用 CGLIB 实现
public static Partner getPartner(int money) {
Partner partner = partners.remove(0);
// CGLIB 使用 Enhancer 创建代理对象
return (Partner) Enhancer.create(partner.getClass(), new MethodInterceptor() {
public int budget = money;
public boolean status = false;
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
if (method.getName().equals("receiveMoney")){
int money = (int) args[0];
this.status = money >= budget;
}
if (status){
return method.invoke(partner,args);
}
return null;
}
});
}
执行测试
public class Client {
public static void main(String[] args) throws Exception {
Player player = new Player("郝武辽");
// 此处的Partner是a_basic包下的,不是接口 是类
Partner partner = PartnerPlatform.getPartner(50);
partner.receiveMoney(20);
partner.playWith(player);
partner.receiveMoney(200);
partner.playWith(player);
}
}
运行 main 方法,控制台只会打印拿到 200 之后的玩耍,证明已经成功构造了代理。
肖洁洁收到佣金:200元 ~
肖洁洁与郝武辽一起愉快地玩耍 ~
CGLIB 动态代理核心 API
使用方式与 JDK 动态代理类似,但是 CGLIB 动态代理的参数相对较少,只需要传两个东西:
-
Class type : 被代理对象所属类的类型
-
Callback callback : 增强的代码实现
由于一般情况下我们都是对类中的方法增强,所以在传入 Callback
时通常会选择这个接口的子接口 MethodIntercept
MethodIntercept.intercept()
方法中参数列表与 InvocationHandler.invoke()
方法类似,唯独多了一个 MethodProxy 参数,它是对参数列表中的 Method 又做了一层封装,利用它可以直接执行被代理对象的方法,如下:
// 执行代理对象的方法
method.invoke(proxy, args);
// 执行原始对象(被代理对象)的方法
methodProxy.invokeSuper(proxy, args);