Java反射應用:基於Redis實現一個通用的Dao類

前言

   ​ Redis是開源的,內存數據結構存儲的健值數據庫。即Key: Value形式來存儲每一個數據,以Redis String、Hash、List類型爲例:

  • String 相當於 Map<String, String>
  • Hash 相當於 Map<String, Map<String, String>>
  • List 相當於 Map<String, List<String>>

每一個記錄都相當於一個Map鍵值對,並根據不同的值類型,使用不同類型的Map。

如何用Redis存儲一個對象實體

那麼如何使用這種結構來存儲一個對象記錄呢,例如User {id: "user1", name: "sundial dreams", age: 21, sex: "man"},其實存儲方式有很多,我們挑一種簡單的存儲方式,即直接用Hash類型來存儲,以user:id當成一個鍵,如下:

user:user1=> {name: "sundial dreams", age: 21, sex: "man"}

但是我們存儲了一堆的User,然後我們需要獲取所有的User咋辦呢,最暴力的方式是遍歷所用的鍵,然匹配出user:爲前綴的鍵,但是效率上肯定不高,因此我們可以使用一個List來存儲當前的用戶Id,並以user:list爲鍵,如下

user:list => ["user1"]

那如果我們需要根據用戶名來查詢一條用戶的信息呢,最暴力的方式還是枚舉出所有的用戶,然後再匹配姓名,考慮到效率問題,我們使用反向索引的方式來實現,即以user:username爲鍵值爲userid的List類型,比如

user:sundial dreams => ["user1"]

如果還想根據年齡來查找用戶,也可以爲年齡構建一個反向索引,即

user:21 => ["user1"]

有了這個思路,其他的都是同樣的道理。

基於Java反射的Dao類

可以看到,User實體的存儲其實也是有固定模式的(其實其他實體也可以用類似的存儲模式),因此可以寫一個UserDao類來存儲每一個User實體,基本方法包括增刪改查,但是,如果我們的實體類一換,即不是User,而是其他類,如Article,那麼我們不得不寫一個ArticleDao類,並重新實現跟UserDao類似的方法,這不符合代碼複用原則,即同一份代碼,在UserDao的實現類裏要寫一遍,而在ArticleDao的實現類裏也得寫一遍,所以我使用Java的反射和泛型來實現了一個公共的Dao類,並且以Dao<User> userDao = new Dao()的方式來產生操作User實體類的Dao對象,同理Dao<Article> articleDao = new Dao()來產生操作Article實體類的Dao對象,這樣同一個方法只需實現一次即可,提高代碼複用率。

IDao接口的定義

基於上面的思路我們定義如下接口:

/**
 * Dao 接口
 *
 * @param <E>
 */
interface IDao<E> {
    //查詢所有 返回一個Map類型的值,其中鍵爲Id,值爲泛型類對象(實體類對象,如User類的對象)
    Map<String, E> queryAll() throws Exception;

    //基於域查詢 返回值與queryAll一致, 根據對應屬性的值來查詢,以User類爲例,queryByField("name", "sundial dreams") => 查詢叫做sundial dreams的用戶ID
    Map<String, E> queryByField(String field, String value) throws Exception;

    //基於Id查詢
    Tuple2<String, E> queryById(String id) throws Exception;

    //插入操作
    String insert(E ele) throws Exception;

    //更新操作
    String update(String id, E ele) throws Exception;

    //刪除操作
    void delete(String id) throws Exception;
}

定義實體,以User實體爲例

/**
 * User實體類
 * 因爲我實現的Dao是通用的Dao,而且在構建反向索引時不可能爲類每一個屬性都構建,
 * 比如可以通過name查詢對應用戶,但沒有根據密碼來查詢用戶的說法。
 * 因此,我只會爲設置爲public的屬性構建反向索引,並且定義的實體類名,最後會對應上Redis數據庫上存儲的名字
 */
class User {
    public String name; // 用戶名  會構建反向索引
    public String birthday; // 生日 會構建反向索引
    public String area; // 所在地區 會構建反向索引
    private String sex; // 性別 不會構建反向索引
    private String password; // 密碼 不會構建反向索引

    public User() {
    }

    public User(String name, String birthday, String password, String area, String sex) {
        this.name = name;
        this.birthday = birthday;
        this.password = password;
        this.area = area;
        this.sex = sex;
    }

    static public User onlyPassword(String password) {
        return new User(null, null, password, null, null);
    }

    @Override
    public String toString() {
        return String.format("{ name: %s, birthday: %s, password: %s, sex: %s, area: %s}", name, birthday, password, sex, area);
    }
}

對於上面的User實體,存儲到Redis中的結構如下

IDao接口的實現

接下來我們基於lettuce(Redis數據庫驅動,官網:https://lettuce.io/)來實現IDao接口,每一個方法的實現思路如下

  • insert(T ele) :

     根據上面的redis存儲模式,我們除了要存儲一個Hash類型外,還要存儲Set, Zset類型來做輔助查詢(反向索引),上面設計實體類(User)的時候提到,對public屬性設置反向索引,對private的屬性不設置反向索引,所以我們就需要用反射來獲取一個對象的public屬性,和private 屬性,另一方面,使用incr函數來構建一個自增的Id,並由此產生一個用來存儲實體類的屬性的鍵(pKey),使用hmset方法來存儲對象的屬性和值,遍歷每個public屬性,獲取屬性名和值,由此拼接一個用來做反向索引的鍵,使用sadd方法存儲,其值就是上面提到的pKey,最後在使用zadd方法來存儲當前的pKey。需要注意的是,由於我使用的是lettuce的異步命令,每次操作都返回一個RedisFuture對象(hmset, sadd, zadd),所以等使用LettuceFutures.awaitAll來等待所有的異步操作完成。

  • queryById(String id):

      這個方法實現比較簡單,利用id構建一個鍵,首先判斷鍵是否存在,存在就使用hgetall來獲取對於的值,然後利用反射構建一個泛型對象併爲屬性賦值,然後返回一個二元組對象,即(id, object)。

  • queryByField(String field, String value):

     通過域來查詢,首先判斷域是否爲公有域(只爲公有域構建了反向索引),然後通過field, value來構建sKey,再使用smembers方法來獲取對應的存儲對象屬性和值的id,調用queryById方法獲取特定的對象。

  • queryAll():

      從存儲主鍵的有序集合裏獲取所有值,對獲取的值使用並行流進行map處理,map函數的返回值爲queryById的結果。

  • update(String id, T obj):

      由於之前設計時使用了反向索引,所以在對實體對象更新時,需要對反向索引進行更新。步驟就是,先根據id從數據庫中查找出對應的hash值,依據hash構建對象即爲newObj,讓他和obj相對比,找出需要更新的字段,然後更新該字段在hash裏面的值,以及更新該字段的反向索引。

  • delete(String id):

     刪除操作也是,不僅僅是刪除數據庫hash類型的實體,還要刪除對應的反向索引。根據id找出對應hash,然後根據hash裏面的 key找對應的反向索引,由於反向索引都是集合類型,所以刪除對應id的值即可。

(注:使用的是lettuce,並且使用異步命令)

完整的Java代碼如下:

/**
 * Dao類
 * JDK 1.8
 * 使用lettuce 5.1.3 且使用異步命令
 * 數據庫存儲結構
 * 以User爲例
 * User: 類型String 記錄user id
 * User:id 類型Hash 記錄User信息
 * User:fields:attr:value 類型SET 反向索引 屬性與鍵
 * User:index 記錄所有user 的key 類型ZSET
 *
 * @param <T>
 */
public class Dao<T> implements IDao<T> {
    private Class<T> _class;
    protected String dbName;
    protected final String SEP = ":";
    private final String NS_FIELDS = "fields";
    private final String NS_INDEX = "index";
    // 使用lettuce異步命令
    private RedisClient redisClient = RedisClient.create(Utils.createRedisURI());
    protected RedisClusterAsyncCommands<String, String> async = redisClient.connect().async();

    public Dao<T> withAsync(RedisClusterAsyncCommands<String, String> async) {
        this.async = async;
        return this;
    }

    public Dao(Class<T> oClass) {
        _class = oClass;
        dbName = _class.getName().toLowerCase();
    }

    private String makePrimaryKey(String id) {
        return (dbName + SEP + id).trim();
    }

    private String makeIndexKey() {
        return (dbName + SEP + NS_INDEX).trim();
    }

    private String makeSetKey(String field, String value) {
        return (dbName + SEP + NS_FIELDS + SEP + field + SEP + value).trim();
    }


    /**
     * @return Map ,{id, {...}}
     * @throws Exception
     */
    public Map<String, T> queryAll() throws Exception {
        String indexKey = makeIndexKey();
        if (async.exists(indexKey).get(1, TimeUnit.MINUTES) == null) throw new Exception("key is not exist");
        return async.zrange(indexKey, 0, -1).get(1, TimeUnit.MINUTES)// user:1 user:2 ........
                .parallelStream()   // 使用Jdk1.8的並行流
                .map(key -> {
                    String[] k = key.split(SEP);
                    try {
                        return queryById(k[1]);
                    } catch (Exception e) {
                        return null;
                    }
                })
                .filter(Objects::nonNull)
                .collect(Collectors.toMap(Tuple2::getT1, Tuple2::getT2));
    }

    /**
     * 基於Field查詢,即queryByField("name", "sundial dreams")
     *
     * @param field
     * @param value
     * @return Map, {id, {...}}
     * @throws Exception
     */
    public Map<String, T> queryByField(String field, String value) throws Exception {
        if (_class.getField(field) == null) throw new Exception("field is not exist");
        String sKey = makeSetKey(field, value);
        if (async.exists(sKey).get(1, TimeUnit.MINUTES) == null) throw new Exception("key is not exist");
        return async.smembers(sKey).get(1, TimeUnit.MINUTES) // user:1 ,user:2 ....
                .parallelStream()
                .map(key -> {
                    String id = key.split(SEP)[1];
                    try {
                        return queryById(id);
                    } catch (Exception e) {
                        return null;
                    }
                })
                .filter(Objects::nonNull)
                .collect(Collectors.toMap(Tuple2::getT1, Tuple2::getT2));
    }

    /**
     * Id查詢
     *
     * @param id
     * @return Tuple2, 即(id, {...})
     * @throws Exception
     */
    public Tuple2<String, T> queryById(String id) throws Exception {
        String queryKey = makePrimaryKey(id);
        if (async.exists(queryKey).get(1, TimeUnit.MINUTES) == null) throw new Exception("key is not exist");
        Map<String, String> map = async.hgetall(queryKey).get(1, TimeUnit.MINUTES);
        T object = _class.getDeclaredConstructor().newInstance();
        for (Map.Entry<String, String> entry : map.entrySet()) {
            String k = entry.getKey(), v = entry.getValue();
            Field field = _class.getDeclaredField(k);
            if (!field.isAccessible()) field.setAccessible(true);
            field.set(object, v);
        }
        return new Tuple2<>(id, object);
    }

    /**
     * 插入
     * 這裏利用反射,對實體類的公有屬性設置反向索引
     *
     * @param ele
     * @throws Exception
     */
    public String insert(T ele) throws Exception {
        Field[] publicFields = _class.getFields(),
                fields = _class.getDeclaredFields();
        Map<String, String> map = new TreeMap<>();
        List<RedisFuture> futures = new LinkedList<>();
        Long id = async.incr(dbName + SEP).get(1, TimeUnit.MINUTES);
        String queryKey = makePrimaryKey(id + "");
        String indexKey = makeIndexKey();
        for (Field field : publicFields) {
            String fieldName = field.getName(),
                    value = String.valueOf(field.get(ele)),
                    sKey = makeSetKey(fieldName, value);
            futures.add(async.sadd(sKey, queryKey));
        }
        for (Field field : fields) {
            if (!field.isAccessible()) field.setAccessible(true);
            String fieldName = field.getName(),
                    value = String.valueOf(field.get(ele));
            map.put(fieldName, value);
        }
        futures.add(async.hmset(queryKey, map));
        futures.add(async.zadd(indexKey, id, queryKey));
        // 等待所有Future對象完成
        LettuceFutures.awaitAll(Duration.ofSeconds(5), futures.toArray(new RedisFuture[0]));
        return queryKey;
    }

    /**
     * 更新
     * 主要維護兩種鍵
     * 以User爲例
     * User:1 => 類型Hash,存儲實體類User的屬性,也是更新操作的主要目標
     * User:fields:name:sundial dreams => 類型Set,存儲姓名的反向索引,用於快速查詢對應名字的鍵
     * 以更新名字爲例,我們不僅需要改Hash類型的名字,而且還需要對應名字的更新反向索引
     *
     * @param id
     * @param obj
     * @throws Exception
     */
    public String update(String id, T obj) throws Exception {
        String queryKey = makePrimaryKey(id);
        if (async.exists(queryKey).get(1, TimeUnit.MINUTES) == null) throw new Exception("error");
        T object = queryById(id).getT2();
        Field[] fields = _class.getDeclaredFields(),
                publicFields = _class.getFields();
        for (Field field : fields) if (!field.isAccessible()) field.setAccessible(true);
        List<RedisFuture> futures = new LinkedList<>();
        for (Field field : publicFields) {
            if (field.get(obj) != null && !(field.get(obj)).equals("")) {
                String oldKey = makeSetKey(field.getName(), String.valueOf(field.get(object))),
                        newKey = makeSetKey(field.getName(), String.valueOf(field.get(obj)));
                if (!field.get(object).equals(field.get(obj))) {
                    futures.add(async.srem(oldKey, queryKey));
                    futures.add(async.sadd(newKey, queryKey));
                }
            }
        }
        for (Field field : fields)
            if (field.get(obj) != null && !(field.get(obj).equals("")))
                futures.add(async.hset(queryKey, field.getName(), String.valueOf(field.get(obj))));

        for (Field field : publicFields) {
            if (field.get(obj) != null && !(field.get(obj).equals(""))) {
                String sKey = makeSetKey(field.getName(), String.valueOf(field.get(obj)));
                futures.add(async.sadd(sKey, queryKey));
            }
        }
        // 等待所有Future對象完成
        LettuceFutures.awaitAll(Duration.ofSeconds(5), futures.toArray(new RedisFuture[0]));
        return queryKey;
    }

    /**
     * 刪除對應的鍵
     *
     * @param id
     * @throws Exception
     */
    public void delete(String id) throws Exception {
        String queryKey = makePrimaryKey(id);
        if (async.exists(queryKey).get(1, TimeUnit.MINUTES) == null) throw new Exception("has error");
        T object = queryById(id).getT2();
        List<RedisFuture> futures = new LinkedList<>();
        futures.add(async.del(queryKey));
        futures.add(async.zrem(makeIndexKey(), queryKey));
        Field[] publicFields = _class.getFields();
        for (Field field : publicFields) {
            String sKey = makeSetKey(field.getName(), String.valueOf(field.get(object)));
            futures.add(async.srem(sKey, queryKey));
        }
        // 等待所有Future對象完成
        LettuceFutures.awaitAll(Duration.ofSeconds(5), futures.toArray(new RedisFuture[0]));
    }
}

輔助類

/**
 * 輔助類
 * 提供數據庫連接URI
 * Redis版本 5.1
 *
 */
public class Utils {
    static private final String HOST = "localhost"; // 主機
    static private final int PORT = 6379; // 端口
    static private final String AUTH = ""; //redis 數據庫密碼
    static private final int DB = 2; // 選擇的數據庫

    static public RedisURI createRedisURI() {
        return RedisURI.Builder.redis(HOST, PORT)
                .withPassword(AUTH).withDatabase(DB).withTimeout(Duration.ofSeconds(5)).build();
    }
}

/**
 * 工具類 二元組
 * @param <T1>
 * @param <T2>
 */
class Tuple2<T1, T2> {
    private final T1 t1;
    private final T2 t2;

    public Tuple2(T1 t1, T2 t2) {
        this.t1 = t1;
        this.t2 = t2;
    }

    public T1 getT1() {
        return t1;
    }

    public T2 getT2() {
        return t2;
    }

    @Override
    public String toString() {
        return "(" + t1 + ", " + t2 + ")";
    }
}

Dao類的基本使用

insert方法

List<User> users = new ArrayList<>();
users.add(new User("sundial dreams", "1998-11-04", "12345678", "US", "women"));
users.add(new User("daydream", "1998-12-04", "12345678", "US", "women"));
Dao<User> userDao = new Dao<>(User.class);
for (User user: users) {
    userDao.insert(user);
}

query方法

System.out.println(userDao.queryAll());
System.out.println(userDao.queryById("1"));
System.out.println(userDao.queryByField("name", "sundial dreams"));

update方法

userDao.update("1", User.onlyPassword("new password"));

delete方法

userDao.delete("1");

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章