上周我们探索了下
Java的强类型及静态类型系统。我承认这样的类型让我们的代码表达性更强,但是同时也限制了第三方库提供面向POJO的API的能力。
我们明白了Java的反射的确是一种和用户代码交互的不错方式,但是这样损失了类型安全的好处。为了实现类型安全,_或许更好的方式就是在运行时通过代码生成来创建指定用户类的子类_?对于这些类,我们会重写它们的方法以实现我们的框架逻辑,而不需要用户依赖于框架的类型。
我建议你读下这篇文章的第一部分,如果你还没读过的话。我希望它能让你明白代码生成想要解决的是哪类问题,以及什么时候用反射的话会更合适一些 。
可以开始了吗
我们都知道如何去定义一个Java类(我希望如此),因此这里也没有什么新的东西可讲了。然而一般我们都是通过某种JVM语言来表述这些类的定义,然后才请求编译器去把我们的东西翻译成字节码。
Java字节码是一个二进制格式,但是,不止如此,Java编程语言和它对应的字节码非常相似。这也不奇怪,因为Java语言某种程度上也驱动着字节码指令集的演进。如果你想了解更多关于字节码的知识,可以参考下RebelLabs上的这篇_
掌握JVM字节码_的不错的分享,它会告诉你里面的很多技巧。
虽然直接操作字节码可以让你完全掌控类的生成过程,不过它的确很难搞定。字节码并没有给你提供像自动装箱或者高层面的控制流程之类的便利性。同样的,生成字节码需要你做许多繁琐的工作比如计算操作数栈的大小以及计算栈帧。如果可以跳过这些,只关注于你要生成的运行时代码不是更好吗?
当然如此。也正由于这个原因,即便是Java类库也自带了一些有限的支持。这些类又被称为JDK代理,它可以在运行时生成实现了某组接口的某个类的定义。
调用里面任何一个接口的方法都会被重定向到一个调用handler里面,你得去实现并提供这个handler。说起JDK代理,它依赖于接口的实现。你不能用这些Java代理来扩展现有的类。
对于我们上篇博客中的那个安全框架而言,这意味着我们只能确保接口方法的安全,而不能是别的类型的方法。这个限制真是弱爆了。正是因为这个,在JDK代理出现了之后,很快cglib就作为一个独立的库发布了。
cglib的工作原理与JDK代理类似,但是它可以动态生成子类而不止是实现接口,因此它可以代理任何方法。cglib至今仍非常流行,像Spring这样的框架也依赖于它来实现某些功能。
然而,尽管它非常流行,cglib这些年的开发已经不那么活跃了,甚至bug修复也只是偶尔才发布一次。由于CGLIB不再支持Java 8的默认方法等新特性,这个问题就变得相当严重了。
由于cglib这些未解决的问题,这些年越来越多的工程开始选择移除这个库。至少在Hiberate将javassist作为首选之后,这个_Java辅助_(javassist: java assistant)库明显成为了主流的选择。
javassist有一个和cglib功能非常类似的一个代码库。除此之外,它还提供了一个运行时的Java编译器,这使得你可以通过提供文本的Java代码来进行任何Java类的定义及重定义。
这听起来非常酷,在一个字符串中写代码就可以快速地解决问题,尤其是你需要将一个已编译的功能和一个动态功能进行混入的时候。下图中的代码是一个使用javassist的真实例子。
更重要的是,尽管Java源码是表达Java类的一个不错的方式,不幸的是,尽管Red Hat已经开始赞助这个库了,但迄今为止javassist仍然只能算是一个个人的项目。随着Java语言的不断改进,javassist的编译器慢慢也开始落后于JDK的版本了。
因此,尽管可以用javassist来写Java代码,你还是得考虑一下字节码。而且这也是必须的,比如说,进行基础类型的显式装箱。猜猜为什么?这是因为由于Java 8新的语言特性的引入,两个编译器间的差距变得越来越大了。
少抱怨一点好吧?
你很容易就能指出别人写的库中存在的问题,但是我们可以改善这种情况吗?cglib和javassist都 是本世纪初期的时候建立的,它们提供的API是围绕着Java在这段时期提供的语言特性来设计 的。
这些库的介入之后一个显著的创新就是注解。这多少有点可笑,代码生成主要就是用于实现基于注解的API,但是居然没有一个库是内建了这个功能的。由于这个原因,我接受了这个挑战,自己写了另一个库来实现这个功能。
这个库叫做
byte buddy,它使用注解和一个领域特定的语言来实现它的目标。你可以像下面这段代码那样创建一个运行时类:
new ByteBuddy()
.subclass(Object.class)
.method(named(“toString”))
.intercept(MethodDelegation.to(ToStringInterception.class))
.make()
如果你能读懂这段代码的含义,那么这个库就已经达到我的预期了。还有一个比较神秘的一点或许就是方法拦截了,它接受一个方法代理作为参数。当接受到这样一个方法代理的时候,Byte Buddy会将方法调用重定向到指定类中匹配给定名字的一个静态方法里。目标类和方法可以实现如下:
class ToStringInterception {
public static String intercept(@Context Class<?> type) {
return type.getSimpleName();
}
}
有了上述的这个拦截器,这个动态类上任何toString方法的调用都可以委托给这个静态方法。通过使用了@Context注解,这个方法接受一个类引用作为它的唯一参数。因此,运行时toString的返回值就是这个被拦截类的名字。
这只是Byte Buddy API的一只皮毛功夫,这里我们并不想深入这个库的细节。如果你感兴趣的话,可以参数下它的
官网文档,上面有详细的说明。
不管怎么说,为了演示下这个库非常适用于日常的编程工作,我们来用Byte Buddy实现我们在文章前面所提到的那个安全库。正如你所看到的,这个实现只是简单的将登录用户存储在了一个静态字段中,它只需几行代码就能完成:
class ByteBuddySecurityLibrary implements SecurityLibrary {
public static String currentUser = “admin”;
@Override
public Class<? extends T> secure(Class type) {
return new ByteBuddy()
.method(isAnnotatedBy(Secured.class))
.intercept(MethodDelegation.to(ByteBuddySecurityLibrary.class))
.make()
.load(type.getClassLoader(), ClassLoadingStrategy.Default.INJECTION)
.getLoaded();
}
@RuntimeType
public static Object intercept(@SuperCall Callable<?> superMethod,
@Origin Method method) throws Exception {
if (!method.getAnnotation(Secured.class).requiredUser().equals(currentUser)) {
throw new IllegalStateException(method + " requires appropriate login”);
}
return superMethod.call();
}
}
和第一个例子类似,我们使用Byte Buddy来拦截方法以便将它们的调用委派给框架方法。这次我们是拦截了被Secured所注解的方法。
对这些方法而言,我们将注解的值和当前登录用户进行比较,如果用户不是注解所需的用户的话,就抛出异常。否则,我们调用原始的那个方法。
在Java中,在实例外部调用父方法一般是不可能的。为了解决这个问题,Byte Buddy自动创建了一个代理类,它的定义有点类似一个非静态的内部类。这么做的话,即便从拦截方法的外部也可以调用super方法了。
未完待续。
原创文章转载请注明出处:
http://it.deepinmind.com
英文原文链接