通过程序来生成代码是
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上面的事了。
框架使用反射来和用户的代码进行交互确是一种解决方案。但是当用户程序需要织入到框架中的时候,反射就变得没那么有吸引力了。我们来考虑一个简单的案例,让这个问题更清晰一点。假设我们要实现一个非常基础的,
注解驱动的安全框架。这个框架是由一个注解来管理的。
class="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框架代码也可以写出单元测试。如果你的需求变了,把这个安全库替换掉也就和替换掉方法上的注解一样非常简单。如果你观察下周围你会发现其实许多应用框架走的都是这条路子。
未完待续。
原创文章转载请注明出处:http://it.deepinmind.com
英文原文链接