ByteBuddy manipulating Java bytes example: automatically removing spaces on both sides of a string

I learned a little bit about ByteBuddy today.


Its official warehouse address is: https://github.com/raphw/byte-buddy.

Official description: Runtime code generation for the Java virtual machine.

 

Anyone who has written Java knows that Java only supports interface based dynamic proxies. If your class does not implement an interface, and you want to proxy this class, it is difficult to do without relying on the tripartite library.

ByteBuddy provides a rich bytecode manipulation interface, which not only allows us to create new classes at runtime, but also supports modifying existing classes. Like adding fields to a class, adding methods or constructors, intercepting methods, etc., it's easy here.

Today, we will use an example to show how to use ByteBuddy to solve one of the problems we often encounter in project development: remove the spaces on both sides of Java bean properties. For example, for String name = "Tom", we want the getName() method to return "Tom".

 

Background:

In the development, we often encounter the scenario that a Java bean has many fields of String type. For some reasons, the values of these fields often have spaces on both sides, and the spaces are not what we want. What should I do?

A cumbersome way to deal with this is to use the following fields:

String name = getName().trim();

Similar code can be tolerated once or twice, so it's hard to avoid madness. Let's try ByteBuddy.

The idea is:

For a given Bean, ByteBuddy is used to create a proxy for the Bean, and subsequent operations on the Bean are performed through the proxy. What the agent does is very simple. It intercepts the called method. If it is found to be a Getter method and the return type is String, it will return in the internal trim.

 

Let's give an example first, and then talk about the specific implementation.

 

Use as follows:

@Testpublic void testBuddyWrapper() {    // This is a class that we encapsulate to create a proxy    BuddyWrapper wrapper = new BuddyWrapper();    // This is the original bean    Bean bean = new Bean();    //Its name has spaces on both sides    bean.setName(" Hello world  ");    // This is a proxy bean    Bean newBean = wrapper.trimmed(bean);    // Verify that it meets expectations    assertEquals(bean.getName().trim(), newBean.getName());}

 

After use, you can find that newBean.getName() returns "Hello world".

With this effect, we can also expand it. Bean attribute copying is a common requirement. Spring framework provides us with such a method:

BeanUtils.copyProperties(source, target);

But its flexibility is not good enough. For example, we want to automatically remove the spaces on the two sides of the string when copying properties. It does not provide this option.

Fortunately, with the BuddyWrapper above, we only need to use it like this:

//This is the original beanBean source = new Bean(); / / there is a space source.setname ("Hello world") on both sides of its name; / / this is the result we need beanBean target = new Bean(); / / note that here we call wrapperBeanUtils.copyProperties(wrapper.trimmed(source), target); / / verify whether it meets the expected assertEquals(source.getName().trim(), target.getName());

In this way, the code will be cleaner and avoid the scattered trim statements.

 

Here is the source code of BuddyWrapper:

/** * Used to automatically divide spaces around property values * <p> * This class can be set as a Spring managed singleton bean * * @author youmoo * @since 2020/1/14 16:06 */public class BuddyWrapper {    /**     * Cache dynamically generated bytecodes to improve performance     */    private final Map<Class<?>, Class<?>> typeCache = new ConcurrentHashMap<>();    /**     * Returns the proxy bean of an incoming 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)                // __The target field points to the bean instance being proxied                .defineField("__target__", clazz, Visibility.PRIVATE)                                // The proxy class has a constructor that receives the type of the bean being proxied                .defineConstructor(Visibility.PUBLIC)                .withParameters(clazz)                .intercept(MethodCall.invoke(clazz.getConstructor())                        .andThen(FieldAccessor.ofField("__target__").setsArgumentAt(0)))                                // Intercepting getter methods and handling them uniformly with 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 {            // Execute getter method first            Object res = method.invoke(delegate, args);            // If the getter method returns a String type, trim            if (args.length == 0 && res instanceof String) {                res = ((String) res).trim();            }            return res;        }    }}

 

Important parts of the source code are annotated.

 

Leave two questions. One for the reader:

When copying bean properties, we sometimes hope that if the properties of the source bean are null, they will not be copied to the target bean. How can I achieve this with ByteBuddy?

One for myself:

We know that Dubbo framework can expose services to external use. Can we use ByteBuddy to expose Service methods as Restful API without relying on Dubbo? (hint: use ByteBuddy to dynamically generate the Controller method, and then dispatch the method call to the real Service.).

 

Scan and follow me:)

If you find it useful, please click "like" or forward it to your peers.

Tags: Programming Java Spring Dubbo github

Posted on Tue, 14 Jan 2020 06:07:37 -0500 by FidelGonzales