Skip to content

实战场景Coding训练:解读反射+代理+AOP 并结合业务逻辑实现

作为架构的核心工具,代理、AOP、ORM等等工具的实现,在面试当中会出现。遇到和反射相关的问题应该如何回答呢,我要准备到什么程度呢?其实,所有的准备还是回归到本质、理解原理,并且可以实现,作为学习应该达到的深度是比较合适的,也可以比较好的通过面试拿到offer。

关联题目:

  1. 什么是反射?有什么用处?
  2. 写一个关于XXX反射的程序?
  3. 解释下为什么要面向切面编程?4
  4. 为什么Proxy.newProxyInstance要传入ClassLoader?5
  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 )——把程序分成多个部分,每个部分负责独立的功能。

听上去是一件废话。我知道大家每天都在做这件事情。程序当然要分成很多个部分,让每个部分负责独立的能力。但是写着写着又忘记了。

以上节课的题目为例,是不是写着写着业务代码,打日志的代码就可业务代码混写了?是不是?写着写着支付逻辑,支付之后的通知,就在支付逻辑中实现了。

这就是关注点分离原则没有贯彻到底。所以我们不仅仅要了解原则,我们还要知道某一个原则可以如何去架构。或者说,代码怎么写?

image-20210217214528628

以订单场景为例,在这个场景中,核心逻辑当然是和订单相关的几个方法。比如说下单。支付。但是这里可能会有很多的辅助能力。比如说用户通知。写日志。触发某种奖励,比如抽奖。那像这样的程序。应该如何去架构呢?

这里其实有很多种方法,面向切面编程就是其中的一种方法。将每一种辅助能力看作是核心能力的一个切面。在下单前,下单中,或者下单之后。触发这些辅助能力。而何时触发,并不是描述在下单逻辑当中的。而是描述在一个其他的不相关的地方。这样就减少了代码的耦合。

我们之所以会用面向切面编程,是因为有两个重要的反模式需要避免。所谓反模式,就是不好的设计方法。其中一个叫做耦合,在一个业务逻辑中,比如支付,耦合了太多的可以分离的关注点,会导致主线逻辑混乱。第二种还是耦合,比如将业务逻辑用一个函数一做到底,没有分成不同的领域——比如支付领域、通知领域、任务领域等等,去看待自己的业务。

如何理解切面(Aspect)

理解了,关注点分离原则,再去理解面向切面,就容易了。面向切面是这样看待一个业务的:

image-20210217215830148

AOP的实现:代理模式

你可以思考:如何将不同的方面结合在一起工作?

image-20210218004738063

我们必须在方面和主要逻辑之间找到它们的结合点,这种结合方式必须是可以描述的,不需要,在主逻辑中去定义的。

用户调用的依然是核心方法,例如Order.pay()。而这个核心方法已经不是真正的核心方法了,他是通过配置生成的一个代理方法。

image-20210217220328085

具体来说,用户调用代理对象的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,有的是为了实现进行时修改程序(也被称作元编程)。

关于元编程,我们会在后续的课程中介绍。

文章来源于自己总结和网络转载,内容如有任何问题,请大佬斧正!联系我