Java 语言带有一套比较严格的类型系统。Java 要求所有变量和对象都有一个确定的类型,并且任何向不兼容类型赋值都会造成一个错误。这些错误通常都会被编译器检查出来,极少情况下会被 Java 运行时检查到,然后抛一个非法类型的错误。如此严格的类型在大多数情况下是比较令人满意的,比如在编写业务应用时。通常,可以以任何模型元素表示其自己的类型这种明确的方式来描述业务域。通过这种方式,我们可以用 Java 构建具有非常强可读性和稳定性的应用,应用中的错误也非常贴近源码。除此之外,Java 严格的类型系统造就 Java 在企业编程中的普及。
然而,通过强制其严格的类型系统,Java 强加一些限制,在其他领域限制了语言应用范围。 比如,当写一个通用的库时,这个库将被其他 Java 应用使用,我们通常不能引用任何在用户应用中定义的类型,因为当这个库被编译时,我们还不知道这些类型。为了调用用户为知代码的方法或者访问其属性,Java 类库提供了一套反射 API。使用这套反射 API,我们就可以反省为知类型,进而调用方法或者访问属性。不幸的是,这套反射 API 的用法有两个明显的缺点:
- 相比硬编码的方法调用,使用 反射 API 非常慢:首先,需要执行一个相当昂贵的方法查找来获取描述特定方法的对象。同时,当一个方法被调用时,这要求 Java 虚拟机去运行本地代码,相比直接调用,这需要一个很长的运行时间。然而,现代 Java 虚拟机知道一个被称为“类型膨胀”的概念:基于 JNI 的方法调用会被动态生成的字节码给替换掉,而这些方法调用的字节码被注入到一个动态生成的类中。(即使 Java 虚拟机自身也使用代码生成!)毕竟,Java 的类型膨胀系统仍存在生成非常一般的代码的缺点,例如,仅能使用基本类型的装箱类型以至于性能缺陷不能完全解决。
- 反射 API 能绕过类型安全检查:即使 Java 虚拟机支持通过反射进行代码调用,但反射 API 自身并不是类型安全的。当编写一个类库时,只要我们不需要把反射 API 暴露给库的用户,就不会有什么大问题。毕竟,当我们编译类库时,我们不知道用户代码,而且也不能校验我们的库与用户类型是否匹配。 有时,需要通过让一个库为我们自己调用我们自己的方法之一来向用户显示反射 API 示例。 这是使用反射 API 变得有问题的地方,因为 Java 编译器将具有所有信息来验证我们的程序的类型安全性。例如,当实现方法级安全库时,这个库的用户将希望这个库做到强制执行安全限制才能调用方法。为此,在用户传递过来方法所需的参数后,这个库将反射性地调用方法。这样,就没有编译时类型检查这些方法参数是否与方法的反射调用相匹配。方法调用依然会校验,只是被推迟到了运行时。这样做,我们就错失了 Java 编程语言的一大特性。
这正是运行时代码生成能帮助我们的地方。它允许我们模拟一些只有使用动态编程语言编程才有的特性,而且不丢失 Java 的静态类型检查。这样,我们就可以两全其美并且还可以提高运行时性能。为了更好地理解这个问题,让我们实现一个方法级安全库。
编写一个安全的库
业务应用程序可能会增长,有时很难在我们的应用程序中概述调用堆栈。当我们在应用程序中使用至关重要的方法时,而这些方法只能在特定条件下调用,这可能会变得有问题。 设想一下,实现重置功能的业务应用程序可以从应用程序的数据库中删除所有内容。
class Service {
void deleteEverything() {
// delete everything ...
}
}
这样的复位操作当然只能由管理员执行,而不是由应用程序的普通用户执行。通过分析源代码,我们当然可以确保这将永远不会发生。但是,我们期望我们的应用能够在未来发展壮大。因此,我们希望实现更紧密的安全模型,其中通过对应用程序的当前用户的显式检查来保护方法调用。我们通常会使用一个安全框架来确保该方法从不被除管理员外的任何人调用。
为此,假设我们使用具有公共 API 如下的安全框架:
@Retention(RetentionPolicy.RUNTIME)
@interface Secured {
String user();
}
class UserHolder {
static String user;
}
interface Framework {
<T> T secure(Class<T> type);
}
在此框架中,Secured
注解应用于标记只能由给定用户访问的方法。UserHolder
用于在全局范围内定义当前登录到应用程序的用户。Framework
接口允许通过调用给定类型的默认构造函数来创建安全实例。当然,这个框架过于简单,但是,从本质上来说,即使流行的安全框架,例如 Spring Security,也是这样实现的。这个安全框架的一个特点是我们过滤用户的类型。通过调用我们框架的接口,我们承诺返回给用户任何类型 T
的实例。幸亏这样,用户能够透明地他自己的类型进行交互,就像安全框架根本不存在一样。在测试环境中,用户甚至可以创建其类型的不安全实例,使用这些实例来代替安全实例。你会同意这真的很方便!已知这种框架使用 POJO,普通的旧 Java 对象进行交互,这是一种用于描述不侵入框架的术语,这些框架不会将自己的类型强加给用户。
现在,想象一下,假如我们知道传递给 Framework
的类型只能是 T = Service
,而且 deleteEverything
方法用 @Secured("ADMIN")
注解。这样,我们可以通过简单的子类化来轻松实现这种特定类型的安全版本:
class SecuredService extends Service {
@Override
void deleteEverything() {
if(UserHolder.user.equals("ADMIN")) {
super.deleteEverything();
} else {
throw new IllegalStateException("Not authorized");
}
}
}
通过这个额外的类,我们可以实现框架如下:
class HardcodedFrameworkImpl implements Framework {
@Override
public <T> T secure(Class<T> type) {
if(type == Service.class) {
return (T) new SecuredService();
} else {
throw new IllegalArgumentException("Unknown: " + type);
}
}
}
当然这个实现并没有太多的用处。通过标注 secure
方法签名,我们建议该方法可以为任何类型提供安全性,但实际上,一旦遇到其他事情,我们将抛出一个异常,然后是已知的 Service
。此外,当编译库时,这将需要我们的安全库知道有关此特定 Service
类型的信息。显然,这不是实现框架的可行解决方案。那么我们如何解决这个问题呢?好吧,由于这是一个关于代码生成库的教程,你可能已经猜到答案:当通过调用 secure
方法, Service
类第一次被我们安全框架知道时,我们会在运行时后台地创建一个子类。通过使用代码生成,我们可以使用任何给定的类型,在运行时将其子类化,并覆盖我们要保护的方法。在我们的例子中,我们覆盖所有被 @Secured
注解标注的方法,并从注解的 user
属性中读取所需的用户。许多流行的 Java 框架都使用类似的方法实现。