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中去)。


 

掃碼關注我吧:)

若是以爲有用,還請點個「贊」或轉發給你的同行。

相關文章
相關標籤/搜索