使用cglib實現資料庫框架的級聯查詢

何白白發表於2019-02-27

寫在前面的

這一章是之前寫的 《手把手教你寫一個Java的orm框架》 的追加內容。因為之前寫的資料庫框架不支援級聯查詢這個操作,對於有關聯關係的表用起來還是比較麻煩,於是就準備把這個功能給加上。這個功能是在我之前寫的資料庫框架基礎上做的,有興趣的同學可以看一看。

資料庫框架

github:JdbcPlus

關於這個框架

手把手教你寫個java的orm框架(1)

手把手教你寫個java的orm框架(2)

手把手教你寫個java的orm框架(3)

手把手教你寫個java的orm框架(4)

手把手教你寫個java的orm框架(5)

大致的思路

對於級聯查詢這個操作,他用起來大致是這樣的,比如:

//在這裡使用資料庫框架查詢一個物件出來
User user = jdbcPuls.selectById(User.class, 1);
//user物件中有一個parent屬性,關聯的是另外一個表,在資料庫中是一個外來鍵
//這時候我們直接使用getParent(),就可以將關聯物件查出來
Parent parent = user.getParent();複製程式碼

要實現這個功能,首要條件是需要在第一次查詢的時候返回一個代理物件,這個代理物件會攔截到所有的get方法,並且需要在方法中判斷:如果這個方法對應的屬性是一個外來鍵的話,就通過資料庫將這個物件查出來(還是個代理物件),然後返回出去。

需要用到的技能

根據上面的思路來說,主要使用到的就是動態代理。動態代理有很多種實現方式,比如java的動態代理cglib的動態代理等等。這裡我使用cglib的動態代理,原因嗎...是因為java的動態代理必須要基於一個介面,我又不想修改資料庫對應的實體類,那就只能用cglib咯。

關於cglib呢,它是一個Java的位元組碼框架,官方描述是這樣的:

 Byte Code Generation Library is high level API to generate and transform JAVA byte code. It is used by AOP, testing, data access frameworks to generate dynamic proxy objects and intercept field access. github.com/cglib/cglib…

是不是很厲害的樣子。cglib的動態代理是基於子類實現的(它是直接生成了一個子類的class檔案,通過一定的配置可以得到生成的class檔案),然後在子類中呼叫父類的方法這樣的。具體原理這裡就不多說了,我們只需要知道怎麼用就好了。

用cglb建立代理物件

使用cglib建立代理物件的方法大致是這樣的:

Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(User.class);
//設定方法回撥
enhancer.setCallback(new MethodInterceptor() {
    /**
     * 被代理的物件方法執行前,會呼叫這個方法,用於方法的攔截
     * @param o
     * @param method
     * @param objects
     * @param methodProxy
     * @return
     * @throws Throwable
     */
    @Override
    public Object intercept(
            Object o,
            Method method,
            Object[] objects,
            MethodProxy methodProxy
    ) throws Throwable {
        //原方法的執行結果
        Object invokeResult = methodProxy.invokeSuper(o, objects);
        //這裡可以寫一些別的東西
        return invokeResult;
    }
});
//建立代理物件
User proxyUser = (User) enhancer.create();複製程式碼

這樣建立代理物件的這一部分就完成啦,接下來就是要想一下怎樣實現級聯查詢這個功能了。

實現級聯查詢

寫一個註解描述關聯關係

之前我寫的資料庫框架:JdbcPlus 是通過註解來實現的,我要新增一個註解用來描述一個實體類的屬性另一個實體類之間的關聯關係。這裡我自定義一個註解 @FK,程式碼如下

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 新增在外來鍵字屬性上,
 * 屬性型別是關聯物件的Entity
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface FK {

    /**
     * 被關聯的欄位名稱
     *
     * @return
     */
    String value();

}複製程式碼

註解中的value表示被關聯物件的欄位名稱。下面建立一個實體類來說明一下:

/**
 * 使用者表
 *
 */
@Getter
@Setter
@Table("user")
public class User {

    /**
     * 使用者名稱
     */
    @Column("name")
    private String name;

    /**
     * 使用者id
     */
    @Id
    @Column("id")
    private int id;

    /**
     * parent_id
     */
    @Column("parent_id")
    @FK("id")
    private User parentId;

}
複製程式碼

1:@Table("user")說明User物件對應資料庫中的user表。

2:@Column("parent_id") 表示這個屬性對應的是sql中的欄位parent_id

3:@FK("id")說明這個欄位是一個外來鍵,關聯到user表中的欄位id上。

4:@Getter和@Setter是lombok中的註解,用於生成對應的get/set方法,不重要。

修改框架查詢方法,返回代理物件

這裡要將之前寫的資料庫框架返回的查詢結果修改為代理物件:

/**
 * 把資料庫查詢的結果與物件進行轉換
 *
 * @param resultSet
 * @param rowNum
 * @return
 * @throws SQLException
 */
@Override
@SneakyThrows(SQLException.class)
public T mapRow(ResultSet resultSet, int rowNum) {
    Map<String, Object> resultMap = columnMapRowMapper.mapRow(resultSet, rowNum);
    //建立cglib代理物件
    EntityProxy entityProxy = EntityProxy.entityProxy(tableClass, jdbcPlus);
    Object proxy = entityProxy.getProxy();
    for (Map.Entry<String, Object> entry : resultMap.entrySet()) {
        //資料庫欄位名
        String key = entry.getKey();
        if (!columnFieldMapper.containsKey(key)) {
            continue;
        }
        Field declaredField = columnFieldMapper.get(key);
        if (declaredField == null) {
            continue;
        }
        Object value = entry.getValue();
        //如果屬性新增了@FK註解,新建一個空物件佔位
        if (EntityUtils.isFK(declaredField)) {
            Object fkObject = getJoinFieldObject(declaredField, value);
            ClassUtils.setValue(proxy, declaredField, fkObject);
        } else {
            ClassUtils.setValue(proxy, declaredField, value);
        }
    }
    return (T) proxy;
}

/**
 * 用於填充查詢物件,使其toString中外來鍵值不顯示null
 *
 * @param fkField  外來鍵屬性
 * @param sqlValue sql中的結果
 * @return
 */
Object getJoinFieldObject(Field fkField, Object sqlValue) {
    if (sqlValue == null) {
        return null;
    }
    Class fieldType = fkField.getType();
    //找到對應的Class
    EntityTableRowMapper mapper = EntityMapperFactory.getMapper(fieldType);
    Map<String, Field> mapperColumnFieldMapper = mapper.getColumnFieldMapper();
    FK FK = EntityUtils.getAnnotation(fkField, FK.class);
    String fieldName = FK.value();
    //例項化原始物件,與之後的代理物件做區分
    Object entityValue = ClassUtils.getInstance(fieldType);
    Field field = mapperColumnFieldMapper.get(fieldName);
    ClassUtils.setValue(entityValue, field, sqlValue);
    return entityValue;
}複製程式碼

這裡是將資料庫查詢結果轉換成一個實體類的方法,是SpringJDBC中介面RowMapper的一個實現類。這主要修改的部分是:

1:在遍歷屬性並賦值的部分新增了屬性上有沒有註解@FK的判斷。

2:如果屬性上新增了@FK,就根據屬性的型別,例項化一個原始物件,並把查詢結果的值放進這個原始物件的對應屬性中。

3:將查詢的返回結果修改成代理物件。這裡建立代理物件的類是EntityProxy,這個在後面會說明。

方法攔截器

這裡主要就是在說EntityProxy這個類,它主要做的事情就是建立代理物件,並且攔截物件中的方法。程式碼是這樣的:

package com.hebaibai.jdbcplus;

import com.hebaibai.jdbcplus.util.ClassUtils;
import com.hebaibai.jdbcplus.util.EntityUtils;
import com.hebaibai.jdbcplus.util.StringUtils;
import lombok.Getter;
import lombok.SneakyThrows;
import lombok.extern.apachecommons.CommonsLog;
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;
import org.springframework.util.Assert;

import java.lang.reflect.Field;
import java.lang.reflect.Method;

/**
 * entity的代理物件
 *
 * @author hjx
 */
@CommonsLog
public class EntityProxy implements MethodInterceptor {

    /**
     * 代理物件
     */
    @Getter
    private Object proxy;

    /**
     * 資料庫操作工具
     */
    private JdbcPlus jdbcPlus;

    /**
     * 建立代理物件
     *
     * @param entityClass
     * @return
     */
    public static EntityProxy entityProxy(Class entityClass, JdbcPlus jdbcPlus) {
        log.debug("建立代理物件:" + entityClass.getName());
        EntityProxy entityProxy = new EntityProxy();
        Assert.isTrue(EntityUtils.isTable(entityClass), "代理物件不是一個@Table!");
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(entityClass);
        //設定方法回撥
        enhancer.setCallback(entityProxy);
        //建立代理物件
        entityProxy.proxy = enhancer.create();
        entityProxy.jdbcPlus = jdbcPlus;
        return entityProxy;
    }


    /**
     * 代理物件方法攔截器,用於實現幾聯查詢
     *
     * @param entity
     * @param method
     * @param values
     * @param methodProxy
     * @return
     */
    @Override
    @SneakyThrows(Throwable.class)
    public Object intercept(Object entity, Method method, Object[] values, MethodProxy methodProxy) {
        //執行原本的方法
        Object invokeResult = methodProxy.invokeSuper(proxy, values);
        Class fkEntityClass = method.getReturnType();
        String name = method.getName();
        //返回值位null,直接返回
        if (invokeResult == null) {
            return invokeResult;
        }
        //如果是get方法, 或者 boolean 型別的is 開頭
        else if (name.startsWith("get") || name.startsWith("is")) {
            Class invokeResultClass = invokeResult.getClass();
            Class superclass = invokeResultClass.getSuperclass();
            //如果父類等於欄位型別並且新增了@Table註解,
            // 說明是cglib生成的子類並且已經查詢出來了結果,直接返回
            if (fkEntityClass == superclass || !EntityUtils.isTable(fkEntityClass)) {
                return invokeResult;
            }
            //通過方法名找到Entity的屬性,之後找到該屬性關聯的Entity中的屬性。
            Field fkField = getFieldBy(method);
            Field fkTargetField = EntityUtils.getEntityFkTargetField(fkField);
            if (fkTargetField == null) {
                return invokeResult;
            }
            Column column = EntityUtils.getAnnotation(fkTargetField, Column.class);
            Object value = ClassUtils.getValue(invokeResult, fkTargetField);
            //執行查詢
            log.debug("對外來鍵屬性進行資料庫查詢。。。");
            Object fkEntityProxy = jdbcPlus.selectOneBy(fkEntityClass, column.value(), value);
            //將查詢結果賦值給原物件
            ClassUtils.setValue(this.proxy, fkField, fkEntityProxy);
            return fkEntityProxy;
        } else {
            return invokeResult;
        }
    }

    /**
     * 通過方法找到對應的屬性
     *
     * @param method
     * @return
     */
    private Field getFieldBy(Method method) {
        String fieldName = method.getName();
        if (fieldName.startsWith("get")) {
            fieldName = fieldName.substring(3);
            fieldName = StringUtils.lowCase(fieldName, 0);
        } else if (fieldName.startsWith("is")) {
            fieldName = fieldName.substring(2);
            fieldName = StringUtils.lowCase(fieldName, 0);
        } else {
            //沒有以get 或者 is 開頭的方法,直接返回null
            return null;
        }
        //通過屬性名找到class中對應的屬性
        Class<?> declaringClass = method.getDeclaringClass();
        try {
            Field field = declaringClass.getDeclaredField(fieldName);
            //如果找到的屬性欄位型別與方法返回值不同,返回null
            if (field.getType() != method.getReturnType()) {
                return null;
            }
            return field;
        } catch (NoSuchFieldException e) {
            return null;
        }
    }


    /**
     * 私有化構造器
     */
    private EntityProxy() {
    }
}複製程式碼

說一下其中的幾個屬性和方法。

1:private Object proxy; 建立出來的代理物件。

2:private JdbcPlus jdbcPlus; 運算元據庫的類,就是這個框架本身主要的類。

3:public static EntityProxy entityProxy(Class entityClass, JdbcPlus jdbcPlus); 建立代理物件的方法,建立一個entityClass的代理物件。

4:public Object intercept(Object entity, Method method, Object[] values, MethodProxy methodProxy); 實現了cglib的介面MethodInterceptor後需要重寫的方法,也是實現級聯查詢功能主要要寫的方法。這裡傳入了五個引數:

entity:物件本身。

method:代理物件所攔截的方法本身。

values:方法中傳入的引數。

methodProxy:攔截的方法的代理。

在進入這個方法的時候先做了一些校驗,比如方法是不是一個get方法,返回值的型別是不是新增了@Table註解,返回值是不是null等等,在這些條件滿足的情況下,取出這個方法的返回值invokeResultClass(此時這個物件是一個原始物件,是在上面的getJoinFieldObject中建立出來的)中儲存的sql中查詢出來的值,最後通過方法名找到對應的新增了@FK註解的屬性找到所關聯的表,執行查詢並得到查詢結果(這時結果是一個代理物件),最後吧查詢結果設定到對應的屬性上,並返回就好了。

最後

這裡部分寫的比較混亂(吃了沒文化的虧/(ㄒoㄒ)/~~),但是程式碼上還是比較清楚的,可以結合程式碼看一下。主要的部分就是EntityProxy.interceptEntityTableRowMapper.mapRow兩個方法,大家可以上github看一下。

閱讀原文


相關文章