使用泛型和反射简化DAO

回顾之前一篇的《DAO层的设计》,DAO层的设计一般可以分为:连接层、数据操作层、代理层、工厂层。

其中数据操作层根据面向接口的设计原则,接口下的实现类之间有着一定的代码冗余性。可以根据泛型和反射来简化代码。

文章后半部分同理是关于在 Mybatis 框架下构建泛型DAO的设计示例。

泛型和反射来简化DAO

  关于DAO的具体设计模式和层次此篇不再多加赘述,详情请参考《DAO层的设计》

JDBC DAO设计示例:学生管理系统

示例整体目录结构:

.
└── website
    └── yuchen
        ├── bean
        │   ├── StudentPO.java
        │   └── TeacherPO.java
        ├── dao
        │   ├── IUserDAO.java
        │   └── UserDAO.java
        ├── factory
        │   └── DAOFactory.java
        ├── proxy
        │   └── StudentDAOProxy.java
        ├── test
        │   └── Main.java
        └── util
        ├── DBConnection.java
        └── ReflectDAOUtil.java


建立数据库

使用的是 MySQL8.0 版本

# StudentManager 数据库

create table Student (
    sid char(3) primary key,
    name varchar(11) not null,
    age int default 20
);


insert into Student values("001", "AA", 20);
insert into Student values("002", "BB", 21);
insert into Student values("003", "CC", 22);


连接工具类 DBConnection

禁止构造函数并使用静态块来预注册,从而使 DBConnection 成为一个“静态类”

package website.yuchen.util;
import java.sql.*;

public class DBConnection
{
    private static final String DRIVER = "com.mysql.cj.jdbc.Driver";
    private static final String HOST = "jdbc:mysql://localhost/StudentManager?useSSL=false";
    private static final String USR = "root";
    private static final String PASSWD = "123456";

    private DBConnection() {}  // 禁止构造函数

    static {    // 静态块预注册
        try {Class.forName(DRIVER);}
        catch(Exception e) {e.printStackTrace();}
    }

    public static Connection getConnection() {
        Connection conn = null;
        try {conn = DriverManager.getConnection(HOST, USR, PASSWD);}
        catch(SQLException e) {e.printStackTrace();}
        return conn;
    }

    public static void close(Connection conn) {
        try {if( conn != null) conn.close();}
        catch( SQLException e) {e.printStackTrace();}
    }
}


Bean对象 StudentPO

package website.yuchen.bean;

public class StudentPO {
    private String sid, name;
    private Integer age;

    public String getSid() {return sid;}
    public void setSid(String sid) {this.sid = sid;}

    public String getName() {return name;}
    public void setName(String name) {this.name = name;}

    public Integer getAge() {return age;}
    public void setAge(Integer age) {this.age = age;}
}


反射操作工具类 ReflectDAOUtil

package website.yuchen.util;

import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.sql.ResultSet;
import java.sql.SQLException;

public class ReflectDAOUtil
{
    private ReflectDAOUtil() {}

    /**
     * 获得父接口泛型的参数, 并转化为实体类对象
     * 例如继承关系: IUserDAO<StudentPO> <-- StudentDAOProxy
     *              @param cls = StudentDAOProxy.class
     *              @return StudentPO.class
     */
    public static Class<?> getParameterizedClass(Class<?> cls)
    {
        ParameterizedType type = (ParameterizedType)
                cls.getGenericInterfaces()[0];
        return (Class<?>) type.getActualTypeArguments()[0];
    }

    /**
     * 将类名进行一定处理从而转化为数据库表名
     * @param bcls 意为Bean的class实体类对象
     * 转化视具体命名规则而定,例如:
     *      @param bcls = StudentPO.class
     *      @return Student
     */
    public static String getDBTableName(Class<?> bcls) {
        String bname = bcls.getSimpleName();
        return bname.substring(0, bname.lastIndexOf("PO"));
    }

    /**
     * 根据JDBC查询的结果集实例化对象
     */
    public static Object generateEntityFromResult(ResultSet rs, Class<?> cls) throws SQLException
    {
        Field[] fields = cls.getDeclaredFields();
        Object obj = null;

        try {

            obj = cls.newInstance();
            for (int i = 0; i < fields.length; i++) {
                fields[i].setAccessible(true);
                fields[i].set(obj, rs.getObject(fields[i].getName()));
            }
        }
        catch(IllegalAccessException e) {
            e.printStackTrace();
        }
        catch(InstantiationException e) {
            e.printStackTrace();
        }
        catch(SQLException e) {
            throw new SQLException(e.getMessage());
        }
        return obj;
    }
}

在根据结果集实例化对象的函数中,因为Bean的属性根据设计规范是私有的,在反射获得Field数组时需要getDeclaredFields,表示获得全部的属性;

同时,在实例化过程中需要先设置属性访问setAccessible(true),再为field赋值。


数据操作层接口 IUserDAO

package website.yuchen.dao;

import java.sql.SQLException;
import java.util.List;

public interface IUserDAO<T> {
    public List<T> findAll() throws SQLException;
}

数据操作层接口实现类 UserDAO

package website.yuchen.dao;

import website.yuchen.util.ReflectDAOUtil;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;

public class UserDAO<T> implements IUserDAO<T>
{
    private Connection conn;
    protected Class<T> entityClass;

    protected  UserDAO() {}
    public UserDAO(Connection conn, Class<?> cls)
    {
        this.entityClass = (Class<T>) ReflectDAOUtil.
                    getParameterizedClass(cls);
        this.conn = conn;
    }

    @Override
    public List<T> findAll() throws SQLException
    {
        List<T> ls = new ArrayList<>();

        try {
            Statement sql = conn.createStatement();
            String cmd = "select * from " + ReflectDAOUtil.getDBTableName(this.entityClass);

            ResultSet rs = sql.executeQuery(cmd);


            while(rs.next()) {
                T obj = (T) ReflectDAOUtil.
                        generateEntityFromResult(rs, this.entityClass);
                ls.add(obj);
            }
        }
        catch(SQLException e) {
            throw new SQLException(e.getMessage());
        }
        return ls;
    }
}


代理层 StudentDAOProxy

package website.yuchen.proxy;

import website.yuchen.bean.StudentPO;
import website.yuchen.dao.IUserDAO;
import website.yuchen.dao.UserDAO;
import website.yuchen.util.DBConnection;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.List;


public class StudentDAOProxy implements IUserDAO<StudentPO>
{
    private Connection conn;
    private UserDAO<StudentPO> dao;

    private StudentDAOProxy() {
        conn = DBConnection.getConnection();
        dao = new UserDAO<>(conn, this.getClass());
    }

    private static StudentDAOProxy instance = null;

    public static StudentDAOProxy newInstance() {
        if( instance == null)
            instance = new StudentDAOProxy();
        return instance;
    }

    @Override
    public List<StudentPO> findAll()
    {
        List<StudentPO> ls = null;

        try {
            ls = dao.findAll();
        }
        catch(SQLException e) {
            e.printStackTrace();
        }
        finally {
            DBConnection.close(conn);
        }
        return ls;
    }
}


工厂类 DAOFactory

使用反射来生产代理对象:

package website.yuchen.factory;

import website.yuchen.dao.IUserDAO;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class DAOFactory
{
    public IUserDAO createProxy(Class<?> cls)
    {
        IUserDAO obj = null;

        try {
            Method method = cls.getMethod("newInstance");
            obj = (IUserDAO) method.invoke(null);
        }
        catch(NoSuchMethodException e) {
            e.printStackTrace();
        }
        catch(InvocationTargetException e) {
            e.printStackTrace();
        }
        catch(IllegalAccessException e) {
            e.printStackTrace();
        }

        return obj;
    }
}

tips: 因为这里反射出来的函数是静态函数,所以invoke对象部分填写的是null


测试类

package website.yuchen.test;

import website.yuchen.bean.StudentPO;
import website.yuchen.factory.DAOFactory;
import website.yuchen.proxy.StudentDAOProxy;
import java.util.List;

public class Main
{
    public static void main(String[] args)
    {
        DAOFactory factory = new DAOFactory();
        StudentDAOProxy proxy = (StudentDAOProxy) factory.
                        createProxy(StudentDAOProxy.class);

        List<StudentPO> res = proxy.findAll();
        for(StudentPO e: res) {
            System.out.println(e.getSid() + " " + e.getName() + " " + e.getAge());
        }
    }
}



Mybatis DAO设计示例

如果是在Mybatis框架下设计DAO层一般只需要设计Mapper接口类和其对应的数据操作,即框架为我们省去了连接、代理、工厂的步骤。

要构建泛型通用DAO的思路是将数据库表名,字段名另外作为字符串参数传入Mapper。所以我们仍然需要设计代理层来管理反射得到的Bean类所对应的数据库表名或字段名,并由工厂类来选择代理。

仍然沿用上个例子,目录结构如下:

.
├── java
│   └── website
│       └── yuchen
│           ├── bean
│           │   └── StudentPO.java
│           ├── factory
│           │   └── DAOFactory.java
│           ├── mapper
│           │   └── UserMapper.java
│           ├── proxy
│           │   ├── entity
│           │   │   └── StudentMapperProxy.java
│           │   ├── IUserMapperProxy.java
│           │   └── UserMapperProxy.java
│           ├── test
│           │   └── Main.java
│           └── util
│               ├── MapperUtil.java
│               └── ReflectDAOUtil.java
└── resources
    ├── mapper
    │   └── UserMapper.xml
    └── MybatisConfig.xml


MybatisConfig.xml 配置文件

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>

    <typeAliases>
        <typeAlias alias="StudentPO" type="website.yuchen.bean.StudentPO" />
    </typeAliases>

    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC" />
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.cj.jdbc.Driver" />
                <property name="url" value="jdbc:mysql://localhost:3306/StudentManager" />
                <property name="username" value="root" />
                <property name="password" value="123456" />
            </dataSource>
        </environment>
    </environments>

    <mappers>
        <mapper resource="mapper/UserMapper.xml" />
    </mappers>

</configuration>

MapperUtil 工具类

用于配置文件的读取和Mapper的获取

package website.yuchen.util;

import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import java.io.Reader;

public class MapperUtil
{
    private static SqlSessionFactory ssfactory;
    private static Reader reader;

    static {
        try {
            reader = Resources.getResourceAsReader("MybatisConfig.xml");
            ssfactory = new SqlSessionFactoryBuilder().build(reader);
        }
        catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static<T> T getMapper(Class<T> cls) {
        SqlSession ss = ssfactory.openSession();
        return ss.getMapper(cls);
    }
}


UserMapper接口

package website.yuchen.mapper;

import org.apache.ibatis.annotations.Param;

import java.util.List;
import java.util.Map;

public interface UserMapper {
    public List<Map> findAll(@Param("tbname") String tbname);
}

其中,tbname为数据库表的名字作为参数传入

tips: 因为PO模型的字段不尽相同,所以使用一个通用的Map来充当PO类型

UserMapper.xml 接口配置实现

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="website.yuchen.mapper.UserMapper">

    <select id="findAll" parameterType="String" resultType="Map">
        select * from ${tbname}
    </select>

</mapper>

tips: 这里如果使用 ‘#’ 传递表名字符串内部会被当做多一对引号

  • ‘$’ 是将传入的数据直接显示生成sql语句
  • ‘#’ 是将传入的值当做字符串的形式


ReflectDAOUtil 反射工具类

另外在反射工具类中编写两个函数,用于Map向PO对象的转换

public static<T> T convertMapEntryToBean(Map mp, Class<T> cls)
{
    Object obj = null;
    try {
        obj = cls.newInstance();
        Field[] fields = cls.getDeclaredFields();

        for( int i = 0; i < fields.length; i++ )
        {
            String prop = fields[i].getName();

            fields[i].setAccessible(true);
            fields[i].set(obj, mp.get(prop));
        }
    }
    catch (IllegalAccessException e) {
        e.printStackTrace();
    }
    catch (InstantiationException e) {
        e.printStackTrace();
    }
    return (T) obj;
}
public static<T> List<T> convertMapListToBeanList(List<Map> ls, Class<T> cls)
{
    List<T> res = new ArrayList<>();
    for(Map mp: ls) {
        res.add(convertMapEntryToBean(mp, cls));
    }
    return res;
}


IUserMapperProxy 泛型代理接口

package website.yuchen.proxy;

import java.util.List;

public interface IUserMapperProxy<T> {
    public List<T> findAll();
}

UserMapperProxy 代理接口泛型实现类

package website.yuchen.proxy;

import website.yuchen.mapper.UserMapper;
import website.yuchen.util.MapperUtil;
import website.yuchen.util.ReflectDAOUtil;

import java.util.List;
import java.util.Map;


public class UserMapperProxy<T> implements IUserMapperProxy<T>
{
    protected Class<T> entityClass;
    protected UserMapper mapper;

    protected UserMapperProxy() {}
    public UserMapperProxy(Class<?> cls)
    {
        this.entityClass = (Class<T>) ReflectDAOUtil.
                        getParameterizedClass(cls);
        mapper = MapperUtil.getMapper(UserMapper.class);
    }

    @Override
    public List<T> findAll()
    {
        String tableName = ReflectDAOUtil.getDBTableName(entityClass);
        List<Map> ls = mapper.findAll(tableName);

        return ReflectDAOUtil.convertMapListToBeanList(ls, entityClass);
    }
}


StudentMapperProxy 具体代理类

具体代理类用于具体化泛型来传递类型对象,这样才可以反射出接口中泛型的类型

package website.yuchen.proxy.entity;

import website.yuchen.bean.StudentPO;
import website.yuchen.proxy.IUserMapperProxy;
import website.yuchen.proxy.UserMapperProxy;
import java.util.List;


public class StudentMapperProxy implements IUserMapperProxy<StudentPO>
{
    private UserMapperProxy dao;
    private StudentMapperProxy() {
        dao = new UserMapperProxy(this.getClass());
    }

    private static StudentMapperProxy instance = null;

    public static StudentMapperProxy newInstance() {
        if( instance == null)
            instance = new StudentMapperProxy();
        return instance;
    }

    @Override
    public List<StudentPO> findAll() {
        return dao.findAll();
    }
}


DAOFactory 工厂类

由工厂来选择具体模型对应的代理

package website.yuchen.factory;

import website.yuchen.proxy.IUserMapperProxy;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class DAOFactory
{
    public IUserMapperProxy createEntity(Class<?> cls)
    {
        IUserMapperProxy obj = null;

        try {
            Method method = cls.getMethod("newInstance");
            obj = (IUserMapperProxy) method.invoke(null);
        }
        catch(NoSuchMethodException e) {
            e.printStackTrace();
        }
        catch(InvocationTargetException e) {
            e.printStackTrace();
        }
        catch(IllegalAccessException e) {
            e.printStackTrace();
        }

        return obj;
    }
}


测试类

package website.yuchen.test;


import website.yuchen.bean.StudentPO;
import website.yuchen.proxy.IUserMapperProxy;
import website.yuchen.proxy.entity.StudentMapperProxy;
import website.yuchen.factory.DAOFactory;

import java.util.List;

public class Main
{
    public static void main(String[] args)
    {
        DAOFactory factory = new DAOFactory();
        IUserMapperProxy dao = factory.createEntity(StudentMapperProxy.class);

        List<StudentPO> ls = dao.findAll();
        for( StudentPO e: ls) {
            System.out.println(e.getSid() + " " + e.getName() + " " + e.getAge());
        }
    }
}
-------------本文结束-------------