由项目踩坑引发的思考 - 动态代理的原理与手动实现

前言

最近项目中,由于明文传输user_id有一定的风险,所以采用上一片文章中的方法,将user_id以及一些数据加密为token并保存在客户端。

每次客户端请求时,携带上token,服务器解析该token获取相关数据。

不过由于该项目由我们从0开始搭建的,之前没有其他项目踩坑的经历,所以在很多框架结构的设计上,埋下了不少的坑。

其中之一,就是为了测试方便,一开始写controller时,传user_id等数据时,直接用的是表单形式。

后来修改为使用token传递后,就要对整个controller进行修改,这工作量就打起来了啊。

众所周知,我是个懒人(gou),能用代码(或者说使用pc能实现自动化的手段)实现的,除非代码也比较难写(还是懒),否则我坚决不用手改。

在思考如何解决这个问题时,突然想到之前学习spring(那都是一年以前的故事了)的时候,记得视频里面讲过AOP,即 Aspect Oriented Programming,面向切面编程。

其就是解决有大量方法需要进行同样操作时,节省代码量以及便利性的一种方法。

印象里以前JVM及字节码的时候,里面讲到过有些应用,就是使用动态代理,来为所有方法加上一个日志记录的操作,同时mybatis也是通过动态代理,来为mapper接口生成动态实现。

什么是AOP?

以下摘自博客园的文章

AOP(Aspect Oriented Programming),即面向切面编程,可以说是OOP(Object Oriented Programming,面向对象编程)的补充和完善。OOP引入封装、继承、多态等概念来建立一种对象层次结构,用于模拟公共行为的一个集合。不过OOP允许开发者定义纵向的关系,但并不适合定义横向的关系,例如日志功能。日志代码往往横向地散布在所有对象层次中,而与它对应的对象的核心功能毫无关系对于其他类型的代码,如安全性、异常处理和透明的持续性也都是如此,这种散布在各处的无关的代码被称为横切(cross cutting),在OOP设计中,它导致了大量代码的重复,而不利于各个模块的重用。

AOP技术恰恰相反,它利用一种称为"横切"的技术,剖解开封装的对象内部,并将那些影响了多个类的公共行为封装到一个可重用模块,并将其命名为"Aspect",即切面。所谓"切面",简单说就是那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块之间的耦合度,并有利于未来的可操作性和可维护性。

使用"横切"技术,AOP把软件系统分为两个部分:核心关注点横切关注点。业务处理的主要流程是核心关注点,与之关系不大的部分是横切关注点。横切关注点的一个特点是,他们经常发生在核心关注点的多处,而各处基本相似,比如权限认证、日志、事物。AOP的作用在于分离系统中的各种关注点,将核心关注点和横切关注点分离开来。

AOP的使用

切面(pointcut)

首先需要定义切面。具体的文档以及使用方法我在此就不赘述了,Spring文档写的非常清楚。并且由于使用注解相对于xml配置来说,方便很多,所以我在这里用注解的方式来实现。

因为我们会在小程序与后台交互的过程中使用token,所以首先配置好切面,即pointcut。

由于项目不公开,所以一部分我打码了,不过应该可以理解我的意思,spring提供的API已经非常全面且好用,可以支持各种不同的需求。切面其实也就是确定AOP要作用于的方法。

通知(advice)

这里我是用的是 @Around 通知。

我们在切面所在方法执行前,对header中的token进行解析,并将其装入到当前请求所在环境的RequestAttributes中,这样我们就不用在切面方法执行时,传入参数。直接将这些参数放到请求的环境中,让controller中方法按需求获取,操作空间更大,也不用强制满足方法参数的约束。

同时为了获取参数更加方便,我封装了一个工具类,省去了重复写获取参数的代码。

1
RequestContextHolder.getRequestAttributes().getAttribute(key, RequestAttributes.SCOPE_REQUEST)

并且我们知道,每当一个请求过来时,实际上前端控制器是会为每一个请求创建一个线程(简要来说,Spring的实现在此基础上肯定进行了更复杂的封装以满足高吞吐量的需求)。

而我将参数保存时, RequestContextHolder.setRequestAttributes() 中范围设置为了SCOPE_REQUEST,也就是说当前请求所在的线程,都是可以获取到的。也避免了多个请求来时,导致参数被覆盖而出现问题。

由AOP引发的思考

AOP的实现原理,完全使用java语言自身特性来实现。

我查了下相关的资料与博客,AOP使用了JDK与CGLIB的动态代理。

JDK的动态代理有一个问题,就是他代理的类,必须是接口的实现类,否则无法生成代理类。

那么既然我们知道了JDK动态代理是基于生成动态代理类,我们能不能自己尝试写一个呢?

手动实现动态代理

静态代理

在开始写动态代理前,我们先看看静态代理。

静态代理

实际上如果是静态代理,其实就是代理对象(Proxy对象)将实际的实现类对象(Impl对象)包装在自己内部,当主程序通过多态调用接口类(Interface对象)方法时,代理类可以在实际的实现类方法调用的前、后进行自己的操作。

不过既然它是静态代理,因此调用的方式、先后顺序以及其代理的接口类都是写死的,极其不通用,所以我们才需要实现“动态”,来满足各种需求。

动态代理

我实现的动态代理,其实和静态代理很相似,只不过对于代理的对象,可以进行自定义,同时代理对象的操作也可以定制。

比如这个项目我想在实现类方法执行前打印一句“Let’s roll”,那个项目我想在实现类方法执行后打印一句“nothing is true, every thing is permitted”,一切都可以自己定制。这样通用性就更好。

JDK动态代理的实现

我们先来看一下JDK动态代理中,InvocationHandler类。

1
2
3
4
public interface InvocationHandler {
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable;
}

其提供了proxy、method、args三个变量。

proxy代表了动态生成代理类的实例对象,method代表了当前执行的方法,args则是该方法的参数。

这样就可以在执行method.invoke( ),也就是执行真正实现类方法前或者后,进行各种操作。

其他的我就不赘述了,读者可以自己去尝试使用JDK动态代理,然后使用如下语句,来设置保存最后的动态代理类。然后看一下生成的动态代理类,就能明白其大致原理。

1
System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles","true");

自己的实现

我自己的实现就比较简陋了,没有自定义代理方法和具体实现类方法之间的顺序,只是简单地设置为先运行代理的方法,然后运行实体类方法。

我的实现分为了5步

  1. 实现InvocationHandler类,将需要运行的代码放入到接口invoke( )方法中
  2. 传入实现类与接口类的Class对象,以及上一步创建好的InvocationHandler类对象
  3. 通过预先写好的模板,通过拼接生成字符串,并将其保存为 $Proxy0.java 文件
  4. 对文件进行编译,生成 $Proxy0.class 字节码文件
  5. 通过 java 的反射机制,动态加载 $Proxy0.class 字节码文件,并在jvm中生成对应的Class类对象,并实例化生成对象,返回方法调用

Test.java 用于测试

完成后创建一个Test类,进行测试。

首先创建一个实现了invoke方法的 InvocationHandler 对象实例,然后通过 UserPersistenceProxy 类的 newInstance 静态方法,生成代理后的对象。

调用接口类中的方法,根据多态,其会自动判别调用的方法。

其中 UserInterface 如下图。

接口类

而接口的实现类如下图。

实现类

此时我们运行该测试,最终 terminal 输出如下:

最终运行结果

确实实现了动态生成字节码,并代理的效果。并且代理类确实是在运行时生成并加载的,相比于静态代理,更加的灵活。

总结

在自己实现了简单的动态代理后,对于动态代理的实现方式,就有了一些自己的理解。

对于我自己个人来讲,我不太喜欢死记硬背。相反,我更喜欢自己尝试去理解,通过探索、理解的过程,来让大脑自己记住,而不是强迫大脑去记忆。

这一次的手动实现,虽然实现了基本的效果,但是任然有几个地方,因为时间和精力的问题,没有做好:

  1. 生成的 java 文件需要持久化保存在硬盘中,以文件的形式后,才能被编译。查了一些相关资料之后,发现有专门的一个库,可以直接将拼接好的字符串,在内存中直接编译,并直接动态加载,减少了因为持久化,导致文件IO过程中占用的时间。同时也不用从硬盘中手动读取比特流。
  2. 没有自定义代理方法和实际实现类方法之间调用顺序的逻辑,我为了省事,直接写死了,后续可以借鉴JDK中 InvocationHandler 的形式。
  3. 没有具体去看CGLIB的实现方式。

附录

参考

  1. 设计模式之代理,手动实现动态代理,揭秘原理实现