ByteBuddy操纵Java字节码示例:自动移除字符串两侧空格
今天稍稍学习了 ByteBuddy 这个库。
其官方仓库地址是:https://github.com/raphw/byte-buddy。
官方对它的描述:Runtime code generation for the Java virtual machine. 即,JVM之上的运行时代码生成。
写过Java的都知道 Java 只支持基于接口的动态代理。如果你的类没有实现某个接口,而你又想代理这个类,不依靠三方库是很难做到的。
而 ByteBuddy 提供了丰富的字节码操纵接口,它既允许我们在运行时创建新的class,也支持修改已有class。像给类添加字段、添加方法或构造器、拦截方法等,在它这里都变得轻而易举。
我们今天通过一个示例,讲解如何使用 ByteBuddy 解决我们项目开发时经常遇到的一个问题:移除 Java bean 属性两边的空格。比如对于 String name = " Tom ",我们希望 getName() 方法返回的是 "Tom"。
问题背景:
在开发中,我们经常会遇到这样的场景:一个Java bean 存在很多String类型的字段,由于一些原因,这些字段的值常常两边都带有空格,而空格并不是我们想要的。怎么办?
一个繁琐的处理办法是,用到这个字段的地方,都 trim 一下:
String name = getName().trim();
类似的代码出现一两次还能忍受,多了就难免让人抓狂。我们尝试用 ByteBuddy 来处理试试。
其思路是:
对于给定 Bean,使用 ByteBuddy 创建一个该 Bean 的代理,后有对该 Bean 的后续操作,都通过代理来执行。而代理做的事也很简单,它拦截调用的方法,如果发现是 Getter 方法且返回类型是 String,则在内部 trim 一下再返回。
我们先给出使用示例,再说说具体实现。
这样使用:
@Test
public void testBuddyWrapper() {
// 这是我们封装的一个创建代理的类
BuddyWrapper wrapper = new BuddyWrapper();
// 这个是原始的bean
Bean bean = new Bean();
// 它的name 两侧有空格
bean.setName(" Hello world ");
// 这是个代理bean
Bean newBean = wrapper.trimmed(bean);
// 验证一下是否符合预期
assertEquals(bean.getName().trim(), newBean.getName());
}
使用后可以发现 newBean.getName() 返回了 "Hello world"。
有了这种效果,我们还可以扩展说一下。Bean属性拷贝是我们经常会遇到的需求,Spring框架就为我们提供了这样一个方法:
BeanUtils.copyProperties(source, target);
但它的灵活性不够好,比如我们希望在拷贝属性时,自动把字符串2边空格移除,它没提供这个选项。
好在有了上面的 BuddyWrapper 后,我们只需要这样用:
// 这个是原始的bean
Bean source = new Bean();
// 它的 name 两边有空格
source.setName(" Hello world ");
// 这个是我们需要的结果bean
Bean target = new Bean();
// 注意这里我们调用了wrapper
BeanUtils.copyProperties(wrapper.trimmed(source), target);
// 验证一下是否符合预期
assertEquals(source.getName().trim(), target.getName());
如此,代码会干净许多,避免分散在各处的trim语句。
下面是 BuddyWrapper 的源码部分:
/**
* 用于自动除属性值两边的空格
* <p>
* 该类可设置为Spring管理的单例bean
*
* @author youmoo
* @since 2020/1/14 16:06
*/
public class BuddyWrapper {
/**
* 缓存动态生成的类字节码,以提升性能
*/
private final Map<Class<?>, Class<?>> typeCache = new ConcurrentHashMap<>();
/**
* 返回一个传入的bean的代理bean
*/
public <E> E trimmed(E bean) {
if (bean == null) {
return null;
}
Class<?> clz = makeClass(bean.getClass());
if (clz == null) {
return null;
}
try {
return (E) clz.getConstructor(bean.getClass()).newInstance(bean);
} catch (Exception e) {
return null;
}
}
private Class<?> makeClass(Class<?> clz) {
return typeCache.computeIfAbsent(clz, clazz -> {
try {
return newBuddy(clazz);
} catch (Exception e) {
return null;
}
});
}
private Class<?> newBuddy(Class<?> clazz) throws Exception {
return new ByteBuddy()
.subclass(clazz)
// __target__ 字段指向被代理的bean实例
.defineField("__target__", clazz, Visibility.PRIVATE)
// 代理类有一个构造器,接收的参数是被代理bean的类型
.defineConstructor(Visibility.PUBLIC)
.withParameters(clazz)
.intercept(MethodCall.invoke(clazz.getConstructor())
.andThen(FieldAccessor.ofField("__target__").setsArgumentAt(0)))
// 拦截getter方法,统一用 TrimmingGetterInterceptor 处理
.method(nameStartsWith("get"))
.intercept(MethodDelegation.to(TrimmingGetterInterceptor.class))
.make()
.load(ClassLoader.getSystemClassLoader(), ClassLoadingStrategy.Default.INJECTION)
.getLoaded();
}
public static class TrimmingGetterInterceptor {
@RuntimeType
public static Object intercept(@AllArguments Object[] args, @Origin Method method, @FieldValue("__target__") Object delegate) throws Exception {
// 先执行getter方法
Object res = method.invoke(delegate, args);
// 如果getter方法返回的是String类型,则trim
if (args.length == 0 && res instanceof String) {
res = ((String) res).trim();
}
return res;
}
}
}
源码中重要的部分都加了注释。
留两个问题。一个给读者:
我们在拷贝Bean属性时,有时会希望若源bean的属性为null时,不再拷贝给目标bean。使用 ByteBuddy 如何实现此需求?
一个留给我自己:
我们知道Dubbo框架可以将Service暴露给外部使用,我们能否不依赖Dubbo,使用ByteBuddy将Service方法以Restful API的方式暴露出去?(提示:使用ByteBuddy动态生成Controller方法,方法的调用再分派到真实的Service中去)。
扫码关注我吧:)
如果觉得有用,还请点个「赞」或转发给你的同行。
以上是 ByteBuddy操纵Java字节码示例:自动移除字符串两侧空格 的全部内容, 来源链接: utcz.com/z/512729.html