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 一下再返回。

 

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

 

这样使用:

@Testpublic 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 后,我们只需要这样用:

// 这个是原始的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 {        @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

回到顶部