自定义持久层框架

为什么要自定义持久层框架?

为什么要自定义持久层框架?Mybatis,hibernate这些开源的持久层框架,它不香吗?是的,这些开源框架都非常优秀,所以我们自定义实现一个持久层框架并不是为了在生产环境中去使用,而是在梳理,编写自定义持久层框架的过程中,加深对持久层框架原理的理解,帮助我们在日常开发工作中,更好的去使用这些持久层框架。我这边常用的是Mybatis框架,本文模仿Mybatis实现一个简易版本的持久层框架。
【注意】,这样的一个自定义框架,并没有和Spring进行整合。完整的代码,请参考:https://gitee.com/jiancongchen/stage1-module1-mybatis/blob/master/jiancongchen.zip,下载后,IDEA导入即可

思路分析

我们日常开发过程中,使用Mybatis的时候,一般需要:

  1. 配置数据源信息,也就是:sqlMapConfig.xml。(Springboot+Mybatis框架,当然可以在appliaction.yml中进行配置)
  2. 编写Mapper接口,提供Mapper接口对应的XML文件,或者是在Mapper中使用注解,目的都是提供可执行的sql语句,也就是:mapper.xml,本文只实现了xml的解析,注解形式未完成。
    那么,我们自定义的持久层框架,首先需要知道sqlMapConfig.xml文件的路径,以及mapper.xml文件的路径,至于mapper.xml文件的路径,我们可以配置在sqlMapConfig.xml中,这样我们就只需要知道sqlMapConfig.xml文件的路径就可以。
    那么解析sqlMapConfig.xml,我们可以拿到数据源的配置信息,构建一个DataSource,以及通过解析mapper.xml的文件,获得mapper.xml中配置的sql语句信息,那么一个Configuration的配置类,就不难想象:
public class Configuration {

    private DataSource dataSource;
    
    /**
     *  key: statementid 
     *  value: 封装好的mappedStatement对象
     */
    Map<String,MappedStatement> mappedStatementMap = new HashMap<>();
	...
}

数据源的配置信息,大家应该不会陌生,我们可以使用C3p0的ComboPooledDataSource构建一个数据源DataSource。

    <dataSource>
        <property name="driverClass" value="com.mysql.jdbc.Driver"></property>
        <property name="jdbcUrl" value="jdbc:mysql:///zdy_mybatis"></property>
        <property name="username" value="root"></property>
        <property name="password" value="root"></property>
    </dataSource>

那么,mappedStatementMap 中的id是什么,使用过Mybatis的同学肯定都能猜到是,namespace+id的形式,namespace是mapper接口的类路径,id是方法名,那么为什么这个id必须是这个形式呢?后续我们再探讨。我们先分析一下MappedStatement这个类中,应该有些什么属性?这个类封装了sql的信息,我们在编写mapper.xml的时候,需要些什么呢?
sql语句总是需要的吧,那么这个sql语句如果需要参数,参数是什么类型,以及执行sql语句的返回结果是什么类型也是需要的。这句sql语句总是需要一个id来唯一标识区分。所以MappedStatement属性也就比较清楚了:

public class MappedStatement {

    //id标识
    private String id;
    //返回值类型
    private String resultType;
    //参数值类型
    private String paramterType;
    //sql语句
    private String sql;
    ...
   }

整体就是通过dom4j解析sqlMapConfig.xml,封装Configuration对象。

DataSource有了,SQL语句也有了,如何执行sql语句呢?sql语句无非增删改查,那么我们定义一个Executor的接口:

public interface Executor {

    public <E> List<E> query(Configuration configuration, MappedStatement mappedStatement, Object... params) throws Exception;

    public void insert(Configuration configuration, MappedStatement mappedStatement, Object... params) throws Exception;

    public void update(Configuration configuration, MappedStatement mappedStatement, Object... params) throws Exception;

    public void delete(Configuration configuration, MappedStatement mappedStatement, Object... params) throws Exception;

}

这里的Configuration参数主要是用来获取DataSource属性,params自然就是Sql语句的参数。在query方法的调用过程中,可以分为三个步骤,第一步是参数封装,Mybatis底层的查询实现还是依赖于JDBC的那一套机制,我们需要封装一个PreparedStatement,然后就是执行sql查询,最后是将sql返回的结果集封装为我们设置的返回类型。

public class SimpleExecutor implements Executor {

    @Override
    public <E> List<E> query(Configuration configuration, MappedStatement mappedStatement, Object... params) throws Exception {

        //参数处理转换
        PreparedStatementHandler preparedStatementHandler = new PreparedStatementHandler(configuration, mappedStatement, params);
        PreparedStatement preparedStatement = preparedStatementHandler.getPreparedStatement();

        //执行sql
        ResultSet resultSet = preparedStatement.executeQuery();

        //查询结果集封装
        ResultSetHandler resultSetHandler = new ResultSetHandler(mappedStatement,resultSet);
        return resultSetHandler.getResultSet();
    }
	...
}
    public  PreparedStatement getPreparedStatement() throws Exception {
        //1.注册驱动,获取链接
        Connection connection = configuration.getDataSource().getConnection();

        //2.获取sql语句  ->  转换sql语句
        String sql = mappedStatement.getSql();
        BoundSql bondSql = getBoundSql(sql);

        //3.获取预处理对象
        PreparedStatement preparedStatement = connection.prepareStatement(bondSql.getSqlText());

        //4.设置参数
        String paramterType = mappedStatement.getParamterType();
        Class<?> paramterClass = paramterType != null ? Class.forName(paramterType) : null;
        List<ParameterMapping> parameterMappingList = bondSql.getParameterMappingList();
        for (int i = 0; i < parameterMappingList.size(); i++) {
            ParameterMapping parameterMapping = parameterMappingList.get(i);
            String content = parameterMapping.getContent();
            //反射
            Field declaredField = paramterClass.getDeclaredField(content);
            //暴力访问
            declaredField.setAccessible(true);
            Object o = declaredField.get(params[0]);
            preparedStatement.setObject(i+1,o);
        }
        return preparedStatement;
    }
    public <E> List<E> getResultSet() throws Exception {
        String resultType = mappedStatement.getResultType();
        Class<?> resultTypeClass = resultType != null ? Class.forName(resultType) : null;
        ArrayList<Object> objects = new ArrayList<Object>();
        //6.封装返回结果
        while (resultSet.next()){
            Object o = resultTypeClass.newInstance();
            ResultSetMetaData metaData = resultSet.getMetaData();
            for(int i = 1; i <= metaData.getColumnCount(); i++){
                String columnName = metaData.getColumnName(i);
                Object value = resultSet.getObject(columnName);
                PropertyDescriptor propertyDescriptor = new PropertyDescriptor(columnName, resultTypeClass);
                Method writeMethod = propertyDescriptor.getWriteMethod();
                writeMethod.invoke(o,value);
            }
            objects.add(o);
        }
        return (List<E>) objects;
    }

那么问题来了,Executor的mappedStatement参数哪里来的?它应该怎么知道要执行的是哪一个mappedStatement?我们在使用sql语句的时候,就需要提供namespace+id,来从Configuration的mappedStatementMap中获取到对应的mappedStatement。我们可以再封装一下:

public interface SqlSession {

    public <E> List<E> selectList(String statementId, Object... params) throws Exception;

    public <T> T selectOne(String statementId, Object... params) throws Exception;

    public void insertOne(String statementId, Object... params) throws Exception;

    public void updateOne(String statementId, Object... params) throws Exception;

    public void deleteOne(String statementId, Object... params) throws Exception;

}
    @Override
    public <E> List<E> selectList(String statementId, Object... params) throws Exception {
        SimpleExecutor simpleExecutor = new SimpleExecutor();
        MappedStatement mappedStatement = configuration.getMappedStatementMap().get(statementId);
        List<Object> list = simpleExecutor.query(configuration, mappedStatement, params);
        return (List<E>) list;
    }

到这里,整个Sql的解析,执行调用过程就很清晰了,那么作为框架的使用者是如何使用的呢?假设我们有这么一个接口,UserMapper中已经编写好了对应的sql语句:

public interface IUserDao {

    /**
     * 查询所有用户
     * @return
     */
    List<User> findAll() throws Exception;

    /**
     * 根据条件进行查询
     * @param user
     * @return
     */
    User findByCondition(User user) throws Exception;
	...
}
public class UserDaoImpl implements IUserDao {

    public List<User> findAll() throws Exception {
        InputStream resourceAsSteam = Resources.getResourceAsStream("SqlMapConfig.xml");
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsSteam);
        SqlSession sqlSession = sqlSessionFactory.openSession();

        List<User> userList = sqlSession.selectList("com.jiancong.dao.IUserDao.findAll");
        for (User user1 : userList) {
            System.out.println(user1);
        }
        return userList;
    }
   
    public User findByCondition(User user) throws Exception {
        InputStream resourceAsSteam = Resources.getResourceAsStream("SqlMapConfig.xml");
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsSteam);
        SqlSession sqlSession = sqlSessionFactory.openSession();
        User user2 = sqlSession.selectOne("com.jiancong.dao.IUserDao.findByCondition", user);
        System.out.println(user2);
        return user2;
    }
    ...
 }

那么以上的代码就能够运行了,但是有一个问题,在UserDaoImpl 存在一些冗余代码,sqlSession的获取过程是一致,不应该这样重复,还有一个问题就是我们在使用Mybatis的时候,并不需要显示的这样提供一个namespace+id的参数,我们只需要调用方法就好,那么Mybatis是怎么知道我们调用的是那一句SQL语句的呢?这里我们就要解释一下前面说到的,为什么namespace+id是有要求的,namespace是mapper接口的类路径,id是方法名,因为Mybatis使用JDK动态代理来完成方法的调用,简易的实现如下。在动态代理中,我们不能知道XML文件中的id,但是我们可以获取到类的路径,以及方法名,只要我们保持这样的一致性,动态代理就可以通过【类路径+方法名】作为namespace+id,获取到对应的sql语句。

  • 那么问题来了,Mybatis的Mapper接口中能否使用方法重载?
  • 显然是不能的,方法重载Mybatis通过JDK动态代理的方式,无法找到正确的SQL语句,在启动解析XML文件的过程中就会报错。
@Override
    public <T> T getMapper(Class<?> mapperClass) {
        //使用JDK动态代理模式为DAO接口生成对象
        Object proxyInstance = Proxy.newProxyInstance(DefaultSqlSession.class.getClassLoader(), new Class[]{mapperClass}, new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                String methodName = method.getName();
                String className = method.getDeclaringClass().getName();
                String statementId  = className + "." + methodName;
                Type genericReturnType = method.getGenericReturnType();
                //是否为参数化类型
                if(genericReturnType instanceof ParameterizedType){
                    List<Object> objects = selectList(statementId, args);
                    return objects;
                }
                //是否为基础类型,且为void
                if(genericReturnType instanceof Class && VOID.equals(genericReturnType.getTypeName())){
                    updateOne(statementId, args);
                    return null;
                }
                return selectOne(statementId,args);
            }
        });
        return (T) proxyInstance;
    }

完整的代码,请参考:https://gitee.com/jiancongchen/stage1-module1-mybatis/blob/master/jiancongchen.zip,下载后,IDEA导入即可

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值