使用cglib實現數據庫框架的級聯查詢

寫在前面的

這一章是以前寫的 《手把手教你寫一個Java的orm框架》 的追加內容。由於以前寫的數據庫框架不支持級聯查詢這個操做,對於有關聯關係的表用起來仍是比較麻煩,因而就準備把這個功能給加上。這個功能是在我以前寫的數據庫框架基礎上作的,有興趣的同窗能夠看一看。java

數據庫框架

github:JdbcPlusgit

關於這個框架

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

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

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

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

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

大體的思路

對於級聯查詢這個操做,他用起來大體是這樣的,好比:bash

//在這裏使用數據庫框架查詢一個對象出來
User user = jdbcPuls.selectById(User.class, 1);
//user對象中有一個parent屬性,關聯的是另一個表,在數據庫中是一個外鍵
//這時候咱們直接使用getParent(),就能夠將關聯對象查出來
Parent parent = user.getParent();複製代碼

要實現這個功能,首要條件是須要在第一次查詢的時候返回一個代理對象,這個代理對象會攔截到全部的get方法,而且須要在方法中判斷:若是這個方法對應的屬性是一個外鍵的話,就經過數據庫將這個對象查出來(仍是個代理對象),而後返回出去。app

須要用到的技能

根據上面的思路來講,主要使用到的就是動態代理。動態代理有不少種實現方式,好比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看一下。

閱讀原文

相關文章
相關標籤/搜索