实战场景Coding训练:解读反射+代理+AOP 并结合业务逻辑实现
作为架构的核心工具,代理、AOP、ORM等等工具的实现,在面试当中会出现。遇到和反射相关的问题应该如何回答呢,我要准备到什么程度呢?其实,所有的准备还是回归到本质、理解原理,并且可以实现,作为学习应该达到的深度是比较合适的,也可以比较好的通过面试拿到offer。
关联题目:
- 什么是反射?有什么用处?
- 写一个关于XXX反射的程序?
- 解释下为什么要面向切面编程?4
- 为什么Proxy.newProxyInstance要传入ClassLoader?5
- 写一个程序实现AOP?(关联上一节课的订单切面框架)
反射(reflection)的概念
定义
反射(reflection),就是运行时查看、反观程序内部结构,甚至修改。
为了实现反射,编程语言在运行时(runtime),必须了解正在执行的程序都由哪些部分组成。这样的了解,我们通常称之为元数据(Meta Data)。Java中,模块、类、函数、注解、源代码……都是元数据。通过反射,我们可以在运行时拿到这些数据。
有时候我们反射的目的是为了反观自身。比如查找元数据,通过字符串找到某个类并且调用它的方法。比如说去查看一个类有哪些属性和方法。比如去查看一个类有哪些注解,注解有哪些数据?这些都是反射的能力。
另一个反射提供的能力,就是让程序用自己动态的修改自己。这部分知识牵扯到直接修改Java的字节码,我会在讲ClassLoader的时候,为大家演示Javasist工具的使用方法。 Javasisit支持我们在运行时再去定义类、接口、方法,将这些定义转化成字节码,再通过ClassLoader加载。
面向切面编程(Aspect Oriented Programming, AOP)
Java编程中经常会用到反射。其中比较常见的场景包括: ORM工具、注解和AOP。 接下来我们说说AOP。
AOP最核心的指导思想就是关注点分离原则(Seperation of Concern )——把程序分成多个部分,每个部分负责独立的功能。
听上去是一件废话。我知道大家每天都在做这件事情。程序当然要分成很多个部分,让每个部分负责独立的能力。但是写着写着又忘记了。
以上节课的题目为例,是不是写着写着业务代码,打日志的代码就可业务代码混写了?是不是?写着写着支付逻辑,支付之后的通知,就在支付逻辑中实现了。
这就是关注点分离原则没有贯彻到底。所以我们不仅仅要了解原则,我们还要知道某一个原则可以如何去架构。或者说,代码怎么写?
以订单场景为例,在这个场景中,核心逻辑当然是和订单相关的几个方法。比如说下单。支付。但是这里可能会有很多的辅助能力。比如说用户通知。写日志。触发某种奖励,比如抽奖。那像这样的程序。应该如何去架构呢?
这里其实有很多种方法,面向切面编程就是其中的一种方法。将每一种辅助能力看作是核心能力的一个切面。在下单前,下单中,或者下单之后。触发这些辅助能力。而何时触发,并不是描述在下单逻辑当中的。而是描述在一个其他的不相关的地方。这样就减少了代码的耦合。
我们之所以会用面向切面编程,是因为有两个重要的反模式需要避免。所谓反模式,就是不好的设计方法。其中一个叫做耦合,在一个业务逻辑中,比如支付,耦合了太多的可以分离的关注点,会导致主线逻辑混乱。第二种还是耦合,比如将业务逻辑用一个函数一做到底,没有分成不同的领域——比如支付领域、通知领域、任务领域等等,去看待自己的业务。
如何理解切面(Aspect)?
理解了,关注点分离原则,再去理解面向切面,就容易了。面向切面是这样看待一个业务的:
AOP的实现:代理模式
你可以思考:如何将不同的方面结合在一起工作?
我们必须在方面和主要逻辑之间找到它们的结合点,这种结合方式必须是可以描述的,不需要,在主逻辑中去定义的。
用户调用的依然是核心方法,例如Order.pay()。而这个核心方法已经不是真正的核心方法了,他是通过配置生成的一个代理方法。
具体来说,用户调用代理对象的pay方法,但实际触发的不是Order.pay,而是一个中间人(代理人)InvocationHander的invoke方法。invoke方法执行的时候,会调用order的pay方法,也会帮助执行结合点的程序(Aspect程序)。
Java的代理能力,是通过Proxy类实现的。 我们可以通过Proxy.newInstance创建一个代理对象。
Proxy.newInstance(classloader, interfaces, InvocationHandler);
- classloader:用于创建InvocationHander和代理对象的ClassLoader
- interfaces: 代理哪些接口
- InvocationHandler: 代理方法触发后的实际执行者
下面是通过代理对象访问pay方法的示例:
@Test
public void test_proxy() throws InterruptedException {
var order = new Order();
var proxy = (IOrder)Proxy.newProxyInstance(
Order.class.getClassLoader(),
new Class[]{IOrder.class},
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("before invoke method:" + method);
return method.invoke(order);
}
}
);
proxy.pay();
}
建议看下视频的解读。
这里有3个思考:
- 思考:为什么需要InvocationHandler?
- 思考:为什么需要传入Interface?
- 思考:为什么需要ClassLoader?
如果感兴趣答案,看下视频。
题目的答案
在Aspect中实现一个static
方法用于实现代理:
public interface Aspect {
void before();
void after();
static <T> T getProxy(Class<T> cls, String ... aspects) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
var inst = cls.getConstructor().newInstance();
var aspectInsts= Arrays.stream(aspects).map(name -> Try.ofFailable(() -> {
System.out.println("Find class");
var clazz = Class.forName(name);
System.out.println(clazz);
var aspectInst = (Aspect)clazz.getConstructor().newInstance();
return aspectInst;
}))
.filter(aspectTry -> aspectTry.isSuccess())
.collect(Collectors.toList());
return (T) Proxy.newProxyInstance(
cls.getClassLoader(),
cls.getInterfaces(),
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
for(var aspect : aspectInsts) {
aspect.get().before();
}
// var result = method.invoke(inst);
for(var aspect : aspectInsts) {
aspect.get().after();
}
return null;
//return result;
}
}
);
}
}
Line by Line的Coding见视频讲解。
小彩蛋
在上面程序中,读取方面对象出来的时候用了Try Monad。这个Monad非常重要,但是Java没有提供,如果感兴趣,看我视频的解读和在慕课网gitlab上的源代码。
总结
对编程语言来说,反射是一个非常重要的能力,是一个节省代码量(避免样板代码,就是代码重复)的大杀器。 ORM框架、Web框架中会大量使用反射——有的是使用注解、有的是为了实现AOP,有的是为了实现进行时修改程序(也被称作元编程)。
关于元编程,我们会在后续的课程中介绍。