概述
使用JDK 11后,就sun.misc.Unsafe
的第一种方法。 其中, defineClass
方法已删除。 代码生成框架通常使用此方法在现有的类加载器中定义新的类。 尽管此方法易于使用,但它的存在也使JVM本质上不安全,正如其定义类的名称所暗示的那样。 通过允许在任何类加载器和程序包中定义一个类,就可以通过在其中定义一个类来获得对任何程序包的程序包范围访问,从而突破了原本封装的程序包或模块的边界。
为了删除sun.misc.Unsafe
,OpenJDK开始提供一种在运行时定义类的替代方法。 从版本9开始, MethodHandles.Lookup
类提供了类似于不安全版本的方法defineClass
。 但是,仅对于与查找的宿主类位于同一包中的类,才允许使用类定义。 由于模块只能解析对某个模块拥有或已打开的包的查找,因此无法再将类注入到不打算提供此类访问权限的包中。
使用方法句柄查找,可以在运行时定义类foo.Qux
,如下所示:
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodHandles.Lookup privateLookup = MethodHandles.privateLookupIn(foo.Bar.class, lookup);
byte[] fooQuxClassFile = createClassFileForFooQuxClass();
privateLookup.defineClass(fooQuxClassFile);
为了执行类定义,需要MethodHandles.Lookup
的实例,可以通过调用MethodHandles::lookup
方法来检索该MethodHandles::lookup
。 调用后一种方法对呼叫点敏感。 因此,返回的实例将代表从方法内部调用的类和包的特权。 要在另一个包中定义一个类,然后在当前包中定义一个类,则需要使用MethodHandles::privateLookupIn
对此包中的类进行解析。 仅当此目标类的程序包与原始查找类位于同一模块中,或者此包显式打开到查找类的模块时,才有可能。 如果不满足这些要求,则尝试解决私有查找将引发IllegalAccessException
,从而保护JPMS隐含的边界。
当然,代码生成库也受此限制的约束。 否则,它们可能被用来创建和注入恶意代码。 而且由于方法句柄的创建对调用站点敏感,因此在不要求用户通过提供表示其模块特权的适当查找实例的情况下,不要求用户做一些其他工作的情况下就不可能合并新的类定义机制。
使用Byte Buddy时,所需的更改很小。 该库使用ClassDefinitionStrategy
定义类,该类负责从其二进制格式加载类。 在Java 11之前,可以使用Reflection或sun.misc.Unsafe
使用ClassDefinitionStrategy.Default.INJECTION
定义一个类。 为了支持Java 11,此策略需要由ClassDefinitionStrategy.UsingLookup.of(lookup)
代替,在ClassDefinitionStrategy.UsingLookup.of(lookup)
中,提供的查找必须有权访问将驻留类的包。
将cglib代理迁移到Byte Buddy
截至目前,其他代码生成库尚未提供这种机制,并且不确定何时以及是否添加此类功能。 尤其是对于cglib,由于库的过时以及不再被更新且不会采用修改的遗留应用程序中的广泛使用,API更改在过去已被证明是有问题的。 对于希望采用Byte Buddy作为更现代且积极开发的替代产品的用户,因此以下部分将介绍可能的迁移。
例如,我们使用一个方法为以下示例类生成代理:
public class SampleClass {
public String test() {
return "foo";
}
}
为了创建代理,通常将代理类作为子类,在其中所有方法都将被覆盖以调度侦听逻辑。 为此,作为示例,我们将一个值栏附加到原始实现的返回值上。
通常使用Enhancer
类和MethodInterceptor
一起定义cglib代理。 方法拦截器提供代理实例,代理方法及其参数。 最后,它还提供了MethodProxy
的实例,该实例允许调用原始代码。
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(SampleClass.class);
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) {
return proxy.invokeSuper(obj, method, args) + "bar";
}
});
SampleClass proxy = (SampleClass) enhancer.create();
assertEquals("foobar", proxy.test());
请注意,如果在代理实例上调用了诸如hashCode
, equals
或toString
类的任何其他方法,则上述代码将引起问题。 前两个方法也将由拦截器分派,因此,当cglib尝试返回字符串类型的返回值时,将导致类强制转换异常。 相反, toString
方法可以工作,但是会返回意外的结果,因为原始实现的前缀是bar作为返回值。
在Byte Buddy中,代理不是专门的概念,但可以使用库的通用代码生成DSL进行定义。 对于与cglib最相似的方法,使用MethodDelegation
提供最简单的迁移路径。 这样的委派以用户定义的拦截器类为目标,方法调用将调度到该类:
public class SampleClassInterceptor {
public static String intercept(@SuperCall Callable<String> zuper) throws Exception {
return zuper.call() + "bar";
}
}
上面的拦截器首先通过Byte Buddy按需提供的帮助程序实例调用原始代码。 使用Byte Buddy的代码生成DSL来实现对此拦截器的委托,如下所示:
SampleClass proxy = new ByteBuddy()
.subclass(SampleClass.class)
.method(ElementMatchers.named("test"))
.intercept(MethodDelegation.to(SampleClassInterceptor.class))
.make()
.load(someClassLoader, ClassLoadingStrategy.UsingLookup.of(MethodHandles
.privateLookupIn(SampleClass.class, MethodHandles.lookup()))
.getLoaded()
.getDeclaredConstructor()
.newInstance();
assertEquals("foobar", proxy.test());
除了cglib之外,Byte Buddy还需要使用ElementMatcher
指定方法过滤器。 尽管在cglib中完全有可能进行过滤,但它非常麻烦并且没有明确要求,因此很容易被遗忘。 在Byte Buddy中,仍然可以使用ElementMatchers.any()
匹配器拦截所有方法,但是通过要求指定这样的匹配器,希望提醒用户做出有意义的选择。
使用上述匹配器,每当调用名为test的方法时,都会使用所讨论的方法委派将调用委派给指定的拦截器。
但是,引入的拦截器将无法分派不返回字符串实例的方法。 实际上,代理的创建将产生由Byte Buddy发出的异常。 但是,完全有可能定义一个更通用的拦截器,该拦截器可应用于与cglib的MethodInterceptor
提供的方法类似的任何方法:
public class SampleClassInterceptor {
@RuntimeType
public static Object intercept(
@Origin Method method,
@This Object self,
@AllArguments Object[] args,
@SuperCall Callable<String> zuper
) throws Exception {
return zuper.call() + "bar";
}
}
当然,由于在这种情况下不使用拦截器的其他参数,因此可以省略它们,从而使代理更有效。 Byte Buddy仅在需要时才按需提供参数。
由于上述代理是无状态的,因此将拦截方法定义为静态。 同样,这是一个简单的优化,因为Byte Buddy否则需要在代理类中定义一个字段,该字段保存对拦截器实例的引用。 但是,如果需要实例,则可以使用MethodDelegation.to(new SampleClassInterceptor())
将委托定向到实例的成员方法。
缓存代理类以提高性能
使用字节伙伴时,不会自动缓存代理类。 这意味着每次运行上述代码时,都会生成并加载一个新类。 由于代码生成和类定义是昂贵的操作,因此这当然效率低下,如果可以重复使用代理类,则应避免这种情况。 在cglib中,如果两次增强的输入相同,则返回先前生成的类,这通常在两次运行同一代码段时是正确的。 然而,由于通常可以更容易地计算高速缓存密钥,因此该方法相当容易出错并且通常效率低下。 使用字节伙伴,可以使用专用的缓存库(如果已有的话)。 另外,Byte Buddy还提供了TypeCache
,它通过用户定义的缓存键为类实现了简单的缓存。 例如,可以使用以下代码使用基类作为键来缓存以上类的生成:
TypeCache<Class<?>> typeCache = new TypeCache<>(TypeCache.Sort.SOFT);
Class<?> proxyType = typeCache.findOrInsert(classLoader, SampleClass.class, () -> new ByteBuddy()
.subclass(SampleClass.class)
.method(ElementMatchers.named("test"))
.intercept(MethodDelegation.to(SampleClassInterceptor.class))
.make()
.load(someClassLoader, ClassLoadingStrategy.UsingLookup.of(MethodHandles
.privateLookupIn(SampleClass.class, MethodHandles.lookup()))
.getLoaded()
});
不幸的是,Java中的缓存类带来了一些警告。 如果创建了代理,则它当然会继承它所代理的类的子类,从而使该基类不适合进行垃圾收集。 因此,如果代理类被强引用,则密钥也将被强引用。 这将使高速缓存无用,并为内存泄漏打开。 因此,必须通过构造函数参数指定的内容来轻而易举地引用代理类。 将来,如果Java引入了星历作为参考类型,则可能会解决此问题。 同时,如果不存在代理类垃圾回收的问题,则可以使用ConcurrentMap
在不存在时计算值。
扩展代理类的可用性
为了使用代理类的重用,将代理类重构为无状态并将状态隔离到实例字段中通常是有意义的。 然后可以在侦听期间使用上述依赖项注入机制来访问此字段,例如,以使后缀值可针对每个代理实例进行配置:
public class SampleClassInterceptor {
public static String intercept(@SuperCall Callable<String> zuper,
@FieldValue("qux") String suffix) throws Exception {
return zuper.call() + suffix;
}
}
上面的拦截器现在接收字段qux的值作为第二个参数,可以使用Byte Buddy的类型创建DSL声明它:
TypeCache<Class<?>> typeCache = new TypeCache<>(TypeCache.Sort.SOFT);
Class<?> proxyType = typeCache.findOrInsert(classLoader, SampleClass.class, () -> new ByteBuddy()
.subclass(SampleClass.class)
.defineField(“qux”, String.class, Visibility.PUBLIC)
.method(ElementMatchers.named("test"))
.intercept(MethodDelegation.to(SampleClassInterceptor.class))
.make()
.load(someClassLoader, ClassLoadingStrategy.UsingLookup.of(MethodHandles
.privateLookupIn(SampleClass.class, MethodHandles.lookup()))
.getLoaded()
});
现在,可以使用Java反射在每个实例创建后在每个实例上设置该字段值。 为了避免反射,DSL还可以用于实现一些接口,该接口声明用于所提及字段的设置方法,可以使用Byte Buddy的FieldAccessor
实现来实现。
加权代理运行时和创建性能
最后,在使用Byte Buddy创建代理时,需要考虑一些性能。 在生成代码时,需要在代码生成本身的性能与所生成代码的运行时性能之间进行权衡。 与cglib或其他proxing库相比,Byte Buddy通常旨在创建尽可能高效地运行的代码,这可能需要更多时间来创建此类代码。 这是基于这样的假设,即大多数应用程序运行时间很长,但是一次只能创建代理,但是代理不适用于所有类型的应用程序。
与cglib的一个重要区别是,Byte Buddy为每个方法生成一个专用的超级调用委托,该方法被拦截,而不是单个MethodProxy
。 这些附加的类需要花费更多的时间来创建和加载,但是使这些类可用可以为每个方法执行带来更好的运行时性能。 如果在循环中调用代理方法,则这种差异很快就很关键。 但是,如果运行时性能不是主要目标,并且在短时间内创建代理类更重要,则以下方法可避免完全创建其他类:
public class SampleClassInterceptor {
public static String intercept(@SuperMethod Method zuper,
@This Object target,
@AllArguments Object[] arguments) throws Exception {
return zuper.invoke(target, arguments) + "bar";
}
}
模块化环境中的代理
对拦截器使用简单形式的依赖注入,而不是依赖于特定于库的类型,例如cglib的
MethodInterceptor
,Byte Buddy在模块化环境中提供了另一个优势:由于生成的代理类将直接引用拦截器类,而不是引用特定于库的调度程序类型(例如cglib的MethodInterceptor
,因此被代理类的模块不需要读取Byte Buddy的模块。 对于cglib,代理类模块必须读取cglib的模块,该模块定义了MethodInterceptor
接口,而不是实现该接口的模块。 对于使用cglib作为传递依赖的库的用户,这很可能是不直观的,特别是如果将后者依赖视为不应公开的实现细节。
在某些情况下,代理类的模块读取提供拦截器的框架模块甚至是不可能或不希望的。 对于这种情况,Byte Buddy还提供了一种解决方案,通过使用它来完全避免这种依赖性
Advice
组件。 该组件可用于以下示例中的代码模板:
public class SampleClassAdvice {
@Advice.OnMethodExit
public static void intercept(@Advice.Returned(readOnly = false) String returned) {
returned += "bar";
}
}
上面的代码看起来似乎没有多大意义,实际上,它将永远不会执行。 该类仅用作Byte Buddy的字节代码模板,后者可读取带注释的方法的字节代码,然后将其内联到生成的代理类中。 为此,必须对上述方法的每个参数进行注释,以代表代理方法的值。 在上述情况下,注释定义了参数,以定义方法的返回值,在给定模板的情况下,将bar添加为后缀。 给定此建议类,可以如下定义代理类:
new ByteBuddy()
.subclass(SampleClass.class)
.defineField(“qux”, String.class, Visibility.PUBLIC)
.method(ElementMatchers.named(“test”))
.intercept(Advice.to(SampleClassAdvice.class).wrap(SuperMethodCall.INSTANCE))
.make()
通过将建议包装在SuperMethodCall
周围,将在对覆盖方法的调用完成后内联上述建议代码。 要在原始方法调用之前内联代码,可以使用OnMethodEnter
批注。
9和10之前的Java版本上的支持代理
在为JVM开发应用程序时,通常可以依靠在特定版本上运行的应用程序也可以在更高版本上运行。 即使使用了内部API,也已经有很长时间了。 但是,由于删除了此内部API,从Java 11开始,这种情况不再成立,在Java 11中,依赖于sun.misc.Unsafe
代码生成库将不再起作用。 同时,通过MethodHandles.Lookup
类定义MethodHandles.Lookup
用于版本9之前的JVM。
对于Byte Buddy,用户有责任使用与当前JVM兼容的类加载策略。 为了支持所有JVM,需要进行以下选择:
ClassLoadingStrategy<ClassLoader> strategy;
if (ClassInjector.UsingLookup.isAvailable()) {
Class<?> methodHandles = Class.forName("java.lang.invoke.MethodHandles");
Object lookup = methodHandles.getMethod("lookup").invoke(null);
Method privateLookupIn = methodHandles.getMethod("privateLookupIn",
Class.class,
Class.forName("java.lang.invoke.MethodHandles$Lookup"));
Object privateLookup = privateLookupIn.invoke(null, targetClass, lookup);
strategy = ClassLoadingStrategy.UsingLookup.of(privateLookup);
} else if (ClassInjector.UsingReflection.isAvailable()) {
strategy = ClassLoadingStrateg.Default.INJECTION;
} else {
throw new IllegalStateException(“No code generation strategy available”);
}
上面的代码使用反射来解析方法句柄查找并对其进行解析。 这样做,可以在Java 9之前的JDK上编译和加载代码。不幸的是,由于MethodHandles::lookup
是调用站点敏感的,因此Byte Buddy无法实现此代码,因此必须在驻留在其中的类中定义以上内容。用户的模块,而不在Byte Buddy中。
最后,值得考虑的是完全避免类注入。 代理类也可以使用ClassLoadingStrategy.Default.WRAPPER
策略在自己的类加载器中定义。 该策略不使用任何内部API,并且可以在任何JVM版本上使用。 但是,必须牢记创建专用类加载器的性能成本。 最后,即使代理类的软件包名称与代理类相同,通过在不同的类加载器中定义代理,JVM也不会将其运行时软件包视为等同,因此不允许覆盖任何软件包,私人方法。
最后的想法
最后一点,我想表达我的观点,尽管迁移成本很高,但退出sun.misc.Unsafe是朝着更安全,模块化的JVM迈出的重要一步。 在删除此非常强大的类之前,可以使用sun.misc.Unsafe
仍然提供的特权访问来绕过JPMS设置的任何边界。 如果不进行此删除,则JPMS会付出额外封装带来的所有不便,而无法依靠它。
JVM上的大多数开发人员很可能永远不会遇到这些附加限制的任何问题,但是如上所述,代码生成和代理库需要适应这些更改。 对于cglib,不幸的是,这确实意味着道路的尽头。 Cglib最初被建模为Java内置代理API的更强大版本,在该版本中,它要求代理类引用其自己的调度程序API,这与Java API要求引用其类型的方式类似。 但是,这些后一种类型驻留在java.base模块中,该模块始终由任何模块读取。 因此,Java代理API仍然可以正常运行,而cglib模型则无法修复。 过去,这已经使cglib成为OSGi环境中的难题,但是对于JPMS,作为库的cglib不再起作用。 Javassist提供的相应代理API存在类似问题。
这种变化的好处是,JVM最终提供了一个稳定的API,用于在应用程序的运行时定义类,这是一种依赖内部API二十多年的常见操作。 除了我认为仍然需要更灵活方法的Javaagents以外,这意味着在所有代理用户完成此最终迁移之后,可以保证将来的Java版本始终能够正常工作。 鉴于cglib的开发多年来一直处于休眠状态,并且该库受到许多限制,因此无论如何,今天的库用户最终迁移都是不可避免的。 Javassist代理可能也是如此,因为后者库在近半年内也没有提交。
翻译自: https://www.javacodegeeks.com/2018/04/jdk-11-and-proxies-in-a-world-past-sun-misc-unsafe.html
最后
以上就是雪白水蜜桃为你收集整理的Sun过去的世界中的JDK 11和代理的全部内容,希望文章能够帮你解决Sun过去的世界中的JDK 11和代理所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复