Java中的动态代码生成

jopen 5年前

通过程序来生成代码是Java平台的固有特性。当Java程序编译的时候,Java编译器生成的是字节码而不是可执行程序。字节码是Java特有的格式,它本身并没有太大的用处。为了能执行字节码,它会在运行时被JVM的just-in-time编译器翻译成本地的机器代码。

Java的导论就先讲到这吧。大多数Java开发人员应该都听说过JIT编译,但它作为这个平台最强大的功能之一,即便你不了解它的细节,也不影响你正常写你的Java程序。

然而随着POJO革命的进行,Java领域流行起了另一种代码生成的形式。许多现代的Java库和框架都在Java程序运行时通过定义自己的类来实现了很多技巧。

乍一听,这个很有点学院派的感觉。不过检查下你的业务应用的栈跟踪信息吧,你肯定会发现许多运行时生成的类。并且除了JIT编译代码以外,运行时生成的类也是你程序运行的一部分,因此它也是你需要关心的。

为什么我们需要运行时代码生成?

运行时的类定义并非意味着帮那些懒得敲代码的人省了点事。运行时代码生成解决的是Java类型系统的一个很大的短板。在深入细节之前,我们先来简单回顾下它的类型系统有哪些特性。

Java是强类型并且是静态类型的。这么说的话有的程序员会感觉迷糊。我们先跳过这个到处都在谈论的“动态及静态类型的比较”,先假设我们都喜欢强类型和静态类型。这个类型的一大好处就是它的表现力。对于每个变量而言,我们可以立马说出它有什么方法可以被调用。

只要我们不去强制进行类型转化,静态类型在编译期就可以暴露许多程序的错误,甚至都不用启动你的应用。

这个安全性对我们来说非常方便。然而,对于那些写Java框架和库的人来说,有时候就不那么方便了。静态类型意味着应用只能调用它所明确知道的方法。

但这样就和框架的初衷相悖了,它的目的是能不依赖特定的用户领域来提供功能。对于自己开发的公司内部的框架而言,直接依赖于某个特定的领域模型这么做可能还可以接受。

然而,想像下像Spring这样的框架,它得依赖于所有的用户领域类型。这在逻辑上几乎是不太可能的,框架的依赖关系图和框架实际的目的是相左的。第三方代码要去依赖框架的功能。而不是相反的方式。

Java反射来救场了

当然了,避免类似的编译期的问题正是Java反射API在教科书上的经典案例。事实上,Java的反射比它的名声其实要好得多。

Java反射的确会造成一定的运行时开销。但是,这样的开销通常都是方法查找时产生的。尽管查找的方法可以内部缓存起来,但Method类的协议约定了方法实例必须是可访问的,也就是说得是可修改的。这需要在查找方法时返回这些缓存的Method对象的浅拷贝,因此我们希望避免重复地去创建这些拷贝。

然而,一旦你获取到一个方法的实例了,如果它运行的次数足够多,Java运行时会去优化这些反射的调用。这个概念又被称为膨胀,这也是代码生成所顺带实现的一个功能。最后,其实反射调用通常并不会导致性能瓶颈,尽管流传着许多这样的谣言,但那都是老版本的Java上面的事了。

框架使用反射来和用户的代码进行交互确是一种解决方案。但是当用户程序需要织入到框架中的时候,反射就变得没那么有吸引力了。我们来考虑一个简单的案例,让这个问题更清晰一点。假设我们要实现一个非常基础的,注解驱动的安全框架。这个框架是由一个注解来管理的。

@Retention(RetentionPolicy.RUNTIME)  @interface Secured {     String requiredUser();  }

使用这个迷你框架的时候,用户可以用@Secured来注解方法以便使得这些方法只能在特定用户登录的条件下才能被调用。不过我们如何实现这个规则?简单的校验用户的方法就是读取方法的注解,将它和当前登录用户的状态进行比较。使用反射可以很容易实现仅当正确的用户登录后才调用这个方法。但我们如何才能在框架外让用户代码能访问到这个逻辑?好吧,我们可以在框架的一个对象内封装这次调用,通过传递一个被封装方法参数的数组来调用这个方法。

interface SecuredMethod {    Object invoke(Object… args);  }

POJO启示

这样做很简单,我们也已经搞定了。真是这样的吗?这个方法确实是可行的,但我们实现的这个API估计只有它老妈才会喜欢它。

首先来说,当使用这个库的实现时,用户需要显式地添加一个安全检查到方法调用上。因此,安全库会侵入到用户的代码里,如果不小心直接调用了这个方法,就会破坏掉这个注解过的方法的安全。这种东西你可不太愿意把它部署到生产环境去。更糟糕的是,选择这个实现我们会破坏了Java引以为傲的类型安全。由于这个方法的签名比较通用,我们现在可以使用任何参数来调用这个SecuredMethod接口,Java编译器也不会去警告我们。同时,后面我们还得一头扎到栈信息中去分析这里产生的运行时异常。

那么,还有什么方法?好吧,既然你读的是一篇关于代码生成的文章,你会猜到Java类的动态生成应该是一个办法吧。JVM不允许通过任何打补丁的方式来增强一个方法的实现,但你可以利用语言的特性来在子类中去重写方法。多亏了Java的动态方法分发,这使得你可以通过在运行时生成一个子类,把框架的任意功能给注入到用户代码中。并且方便的是,通过super你可以很简单就能调用到用户的方法。

有了这个方法,我们可以按需生成任意类型的子类来实现我们所说的这个安全库了。我们可以将安全校验的代码注入到用户代码中,只有当我们确保的确是合法的时候才会实际去调用那个方法。这么做的话,我们发布的安全库就只需要一个简单的接口就好了:

interface SecurityLibrary {    <T> Class<? extends T> secure(Class<T> instance);  }

通过代码生成,我们可以很容易将某个SecuredUserType继承UserType来成为它的子类,我们会去重写这个方法并且实现安全校验的逻辑。如果安全校验通过了,这个方法调用会委托到父类的方法中,那里会包含实际的处理逻辑。

最终我们实现了一个POJO框架的基础版本。没有能比这个更透明的安全库了。代码生成的最大的好处在于它可以完整地保全用户的类型。想像下这个API能让你的生活变得多轻松。

由于它不依赖于框架的类型,这使得你可以不用mock框架代码也可以写出单元测试。如果你的需求变了,把这个安全库替换掉也就和替换掉方法上的注解一样非常简单。如果你观察下周围你会发现其实许多应用框架走的都是这条路子。

未完待续。

原创文章转载请注明出处:Java中的动态代码生成

英文原文链接