SpringFramework — AOP 动态代理回顾

小龙 470 2022-06-06

AOP 简介

AOP 面向切面编程,是一种编程思想,是将那些与业务无关,但却对多个对象产生影响的公共行为和逻辑,抽取出来,封装成一个切面,然后注入到对象中去。这些公共的行为和逻辑被称为横切关注点

AOP 可以对某个对象或某个对象的某个功能进行增强,也可以在某个方法执行前后做一些额外的动作

AOP 实现技术

SpringFramework 中 AOP 有静态代理和动态代理,两种实现技术。

  • 静态代理:静态代理需要自己编写代理类,组合原有的目标对象,并实现原有目标对象的实现接口,以此来做到对原有对象方法的增强;

  • 动态代理:动态代理只需要编写增强逻辑类,在运行时动态的将增强逻辑组合进原有的目标对象,即可生成代理对象,完成对目标对象的方法功能增强。

OOP(面向对象)的不足 & 横切关注点的思想

OOP(面向对象)有一个很大的不足,它无法即将相同、重复的逻辑分离出去,OOP只能尽可能的减少重复的代码量,却无法避免重复的代码出现

横切关注点:下面图中的四个方法都调用了一个相同的逻辑(日志记录)

image

图中这种框可以是一个类的几个方法,可以是多个类的不同方法。只要这些方法的开始 / 结束都有相同的逻辑,那么我们就可以把这些相同的逻辑,抽取出来视为一体,这个思想就叫做横切,抽取出来的逻辑组成的虚拟的结构,称之为横切面(上图红框框出来的就可以理解为一个横切面)

动态代理

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);

# AOP # CGLIB # JDK Proxy