ByteBuddy操纵Java字节码示例:自动移除字符串两侧空格

今天稍稍学习了 ByteBuddy 这个库。java


其官方仓库地址是:https://github.com/raphw/byte-buddy。git

官方对它的描述:Runtime code generation for the Java virtual machine. 即,JVM之上的运行时代码生成。github


 

写过Java的都知道 Java 只支持基于接口的动态代理。若是你的类没有实现某个接口,而你又想代理这个类,不依靠三方库是很难作到的。缓存

而 ByteBuddy 提供了丰富的字节码操纵接口,它既容许咱们在运行时建立新的class,也支持修改已有class。像给类添加字段、添加方法或构造器、拦截方法等,在它这里都变得垂手可得。app

咱们今天经过一个示例,讲解如何使用 ByteBuddy 解决咱们项目开发时常常遇到的一个问题:移除 Java bean 属性两边的空格。好比对于 String name = "  Tom  ",咱们但愿 getName() 方法返回的是 "Tom"框架


 

问题背景:性能

在开发中,咱们常常会遇到这样的场景:一个Java bean 存在不少String类型的字段,因为一些缘由,这些字段的值经常两边都带有空格,而空格并非咱们想要的。怎么办?学习

一个繁琐的处理办法是,用到这个字段的地方,都 trim 一下:spa

String name = getName().trim();

相似的代码出现一两次还能忍受,多了就不免让人抓狂。咱们尝试用 ByteBuddy 来处理试试。代理

其思路是:

对于给定 Bean,使用 ByteBuddy 建立一个该 Bean 的代理,后有对该 Bean 的后续操做,都经过代理来执行。而代理作的事也很简单,它拦截调用的方法,若是发现是 Getter 方法且返回类型是 String,则在内部 trim 一下再返回。


 

咱们先给出使用示例,再说说具体实现。

 

这样使用:

@Testpublic void testBuddyWrapper() {// 这是咱们封装的一个建立代理的类BuddyWrapper wrapper = new BuddyWrapper();​// 这个是原始的beanBean bean = new Bean();    // 它的name 两侧有空格bean.setName(" Hello world  ");​// 这是个代理beanBean newBean = wrapper.trimmed(bean);​// 验证一下是否符合预期assertEquals(bean.getName().trim(), newBean.getName());}

 

使用后能够发现 newBean.getName() 返回了 "Hello world"

有了这种效果,咱们还能够扩展说一下。Bean属性拷贝是咱们常常会遇到的需求,Spring框架就为咱们提供了这样一个方法:

BeanUtils.copyProperties(source, target);

但它的灵活性不够好,好比咱们但愿在拷贝属性时,自动把字符串2边空格移除,它没提供这个选项。

好在有了上面的 BuddyWrapper 后,咱们只须要这样用:

// 这个是原始的beanBean source = new Bean();// 它的 name 两边有空格source.setName(" Hello world  ");​// 这个是咱们须要的结果beanBean target = new Bean();// 注意这里咱们调用了wrapperBeanUtils.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 {​@RuntimeTypepublic static Object intercept(@AllArguments Object[] args, @Origin Method method, @FieldValue("__target__") Object delegate) throws Exception {​// 先执行getter方法Object res = method.invoke(delegate, args);​// 若是getter方法返回的是String类型,则trimif (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中去)。


 

扫码关注我吧:)

若是以为有用,还请点个「赞」或转发给你的同行。

相关文章
相关标签/搜索