今天稍稍學習了 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中去)。
掃碼關注我吧:)
若是以爲有用,還請點個「贊」或轉發給你的同行。