计算机系统应用教程网站

网站首页 > 技术文章 正文

如何自己实现一个mybatis框架?

btikc 2024-09-16 13:04:25 技术文章 25 ℃ 0 评论


简介


说到持久层框架,我们很快能想到hibernate、mybatis。Hibernate是全自动的持久层框架,而mybatis则是半自动的。那么两者有何区别?下面先简单认识一下这两个框架的异同,以便更好的理解下面阐述的自定义持久层框架的设计思想,毕竟下面的自定义框架实际就是通过阅读mybatis源码,取其核心设计,去掉细枝旁叶而设计的一个半自动持久层框架。


ORM全称Object/Relation Mapping:表示对象-关系映射的缩写。


Hibernate:全自动的持久层ORM框架。Hibernate是一个开源的对象关系映射框架,它对JDBC进行了非常轻量级的对象封装,它将POJO(Plain Ordinary Java Object)与数据库表建立映射关系,是一个全自动的ORM框架,hibernate可以自动生成SQL语句,自动执行,使得Java程序员可以随心所欲的使用对象编程思维来操纵数据库。


Mybatis:MyBatis 是一款优秀的半自动持久层框架,它支持自定义 SQL、存储过程以及高级映射。MyBatis 免除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作。MyBatis 可以通过简单的 XML 或注解来配置和映射原始类型、接口和 Java POJO为数据库中的记录。


使用 Hibernate 查询关联对象或者关联集合对象时,可以根据对象关系模型直接获取,所以它是全自动的。而 MyBatis 在查询关联对象或关联集合对象时,需要手动编写 sql 来完成,所以,称之为半自动 ORM 映射工具。


既然无论是全自动的hibernate,或是半自动的mybatis,其本质都是对JDBC进行了封装,使Java程序员可以使用对象编程思维来操纵数据库。


下面我们不妨看看使用JDBC编程是怎样的,直接使用JDBC编程会有什么问题,找到问题,解决问题,然后提出针对问题的解决方案。


jdbc操作存在的问题


一个使用原始jdbc对数据进行查询的案例


package com.kmning.wallet.jdbc;

import com.kmning.wallet.jdbc.pojo.User;

import java.sql.*;
import java.util.ArrayList;
import java.util.List;

/**
 * @author kangming.ning
 * @date 2021/5/2 9:16
 */
public class JdbcQueryDemo {
    public static void main(String[] args) {
        Connection connection=null;
        PreparedStatement preparedStatement=null;
        ResultSet resultSet =null;
        try {
            //加载数据库驱动
            //Class.forName("com.mysql.jdbc.Driver");
            //过驱动管理类获取数据库链接
            connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/yourdb?characterEncoding=UTF-8&serverTimezone=Asia/Shanghai", "root", "root");
            //编写sql语句
            String sql="select * from user where username=? or telphone=?";
            //获取预处理statement
             preparedStatement = connection.prepareStatement(sql);
            //设置参数,第一个参数为sql语句中参数的序号(从1开始),第二个参数为设置的参数值
             preparedStatement.setString(1,"aa");
             preparedStatement.setString(2,"666");
            //向数据库发出sql执行查询,查询出结果集
            resultSet = preparedStatement.executeQuery();
            //遍历结果集,封装数据
            List<User> userList=new ArrayList<>(10);
            while (resultSet.next()){
                int id = resultSet.getInt("id");
                String username = resultSet.getString("username");
                String telphone = resultSet.getString("telphone");
                //封装结果集
                User user = new User();
                user.setId(id);
                user.setUsername(username);
                user.setTelphone(telphone);
                userList.add(user);
            }
            System.out.println(userList);

        }catch (Exception e){
            e.printStackTrace();
        }finally {
            //释放资源
            if (resultSet!=null){
                try {
                    resultSet.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
            if (preparedStatement!=null){
                try {
                    preparedStatement.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
            if (connection!=null){
                try {
                    connection.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
        }

    }
}

代码非常简单且常规,JDBC操作无非都类似于上面的套路。通过驱动获取数据库连接,通过PreparedStatement预编译SQL语句,设置参数,然后向数据库发出sql执行查询,查询出结果集,遍历结果集,将结果集封装到POJO对象集合。当然,最后还得将相关资源释放。


但在实际的企业应用中,数据库的表少则几十个,多则几百个,如果使用类似于上述的方式去操作数据库,不仅前期开发工作巨大,而且后期的维护也将是一场灾难。那么上述代码存在哪些问题?让我们带着疑问对代码进行分析,然后找出问题所在,然后找到解决方案去解决它们。


以上代码存在的问题:


  • 数据库配置信息存在硬编码
  • 数据库连接创建、释放频繁造成系统资源浪费,从而影响系统性能
  • Sql语句在代码中硬编码,造成代码不易维护,实际应用中sql变化的可能较大,sql变动需要改变java代码
  • 使用preparedStatement向占位符号传参数存在硬编码,因为sql语句的where条件不一定,可能多也可能少,修改sql还要修改代码,系统不易维护
  • 对结果集解析存在硬编码(查询列名),sql变化导致解析代码变化,系统不易维护,如果能将数据库记录封装成pojo对象解析比较方便


解决思路:


  • 数据库配置信息提供配置文件,避免改动连接等配置信息需要重新编译代码
  • 使用数据库连接池初始化连接资源
  • 将sql语句、设置参数、获取结果集参数抽取到xml配置文件中
  • 使用反射、内省等底层技术,自动将实体与表进行属性与字段的自动映射


问题找到了,也整理出了解决思路,那么就可以着手设计框架了。


自定义mybatis框架


我们整理出了解决思路,接下来需要编写一个通用的解决方案,也就是框架,去解决问题。下面我们整理一下实现这样一个框架需要去做哪些工作。


首先是自定义框架肯定是独立的一个jar,然后提供给客户端使用。我们可以将开发工作分为两部分,一是框架需要做哪些东西,二是使用端(项目)需要做哪些东西。为了描述与理解方便,下面称框架端为mybatis-custom框架。


mybatis-custom框架负责对jdbc进行封装,提供基本操作接口,并对结果集和pojo实体进行映射,而客户端(项目)则需要提供数据库配置信息、sql配置信息(如UserMapper.xml)。使用端提供xxMapper.xml,意味着对sql的编写的控制权还是属于用户的(半自动映射框架)。


使用端


提供两部分配置信息:一是数据库配置信息。二是sql mapper配置信息,mapper文件定义了sql语句、参数类型、返回值类型、结果返回类型等信息。


使用配置文件来提供这两部分配置信息


  • sqlMapConfig.xml:存放数据库配置信息、同时存放mapper.xml的全路径(这样在方便在解析sqlMapConfig时直接解析mapper.xml,相关信息保存到Configuration)。样例如下
<configuration>
        <!--数据库配置信息-->
    <dataSource>
        <property name="driverClass" value="com.mysql.jdbc.Driver"></property>
        <property name="jdbcUrl" value="jdbc:mysql:///yourdb?serverTimezone=Asia/Shanghai"></property>
        <property name="username" value="root"></property>
        <property name="password" value="root"></property>
    </dataSource>

    <!--存放mapper.xml的全路径-->
    <mapper resource="UserMapper.xml"></mapper>
    <mapper resource="ProductMapper.xml"></mapper>

</configuration>


自定义持久层框架端


创建框架工程,引入相关maven依赖,根据上面的分析,框架需要解析xml,用连接池解决连接创建关闭频繁问题,那么引入相关依赖如下


<dependency>
            <groupId>dom4j</groupId>
            <artifactId>dom4j</artifactId>
            <version>1.6.1</version>
        </dependency>
        <dependency>
            <groupId>jaxen</groupId>
            <artifactId>jaxen</artifactId>
            <version>1.2.0</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.22</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.20</version>
        </dependency>


根据自定义的核心配置文件sqlMapConfig.xml和userMapper.xml文件可以看出,核心配置文件里面有连接池信息,mapper信息,那么解析成核心配置类后也应该记录这些信息,方便后面使用。而userMapper.xml记录了数据表的sql操作的配置文件肯定也要映射成某个类, 我们根据上面定义的userMapper.xml配置信息抽象成成类即可。


使用端已经使用数据库xml配置文件l和SQL xml配置文件来解决相关的硬编码问题,接下来就是框架端的工作了。框架端本质就是需要对JDBC代码进行封装。框架端解析客户端提供的数据库配置文件以创建数据库连接,当然,这里我们使用数据库连接池来解决频繁创建、释放连接问题。框架端解析xxMapper.xml配置文件,以将每个Mapper.xml文件对应的SQL标签解析成特定的配置对象(比如叫MappedStatement),最后我们把这些解析出来的配置对象保存到一个全局的配置类中(比如叫Configuration)。这个配置类在mybatis-custom中起到了基石的作用,毕竟这个框架最终是要操作数据库的,而这个配置类对象为我们提供了数据库操作的相关信息。


Configuration


package com.kmning.mybatis.config;

import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;

/**
 * @author kangming.ning
 * @date 2021/5/2 17:18
 */
public class Configuration {

    /**
     * 保存连接池信息
     * */
    private DataSource dataSource;

    /**
     * key: namespace+id,唯一标识一条sql
     * value:MappedStatement对象 通过key找到当前业务表的mapper,所以在定义dao接口时,接口的方法全限定名称
     * 和其对应的mapper中的namespace+id 必须是一致的,框架会根据当前的查询方法的全限定方法名称去找到对应的MappedStatement
     * 对象
     * 指定初始Map大小,默认16
     * */
    private Map<String,MappedStatement> mappedStatementMap=new HashMap<>(100);

    public DataSource getDataSource() {
        return dataSource;
    }

    public void setDataSource(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    public Map<String, MappedStatement> getMappedStatementMap() {
        return mappedStatementMap;
    }

}


可以看出,核心配置类主要保存连接池信息、另外用一个Map保存了所有数据库操作信息(简单的理解就是Mapper.xml里面的SQL标签对应的对象),其中key是namespace+id,唯一标识一条sql。namespace(Mapper.xml中定义的)就是我们定义的DAO接口全限定名,id则是具体的接口名称。这样一来,当我们调用DAO某个接口时,框架就能找到调用接口对应的Mapper.xml文件对应的SQL标签(或者说MappedStatement对象),从而解析出需要执行的SQL。


到此,我们已经准备好了框架执行SQL的一切条件,接下来应该要交给PreparedStatement去执行,然后得到结果,对结果进行封装等。我们定义一个执行器接口,提供查询、增删查改接口。


Executor接口


定义一个执行器接口,负责和数据库进行交互,提供查询、增删查改接口。


package com.kmning.mybatis.sqlSession;

import com.kmning.mybatis.config.Configuration;
import com.kmning.mybatis.config.MappedStatement;
?
import java.util.List;
?
/**
 * 定义数据库查询、更新接口
 * @author kangming.ning
 * @date 2021/5/2 20:33
 */
public interface Executor {
?
    /**
     * 查询接口,使用jdbc与数据通讯,封装数据映射到pojo类返回
     * @param configuration 核心配置对象
     * @param mappedStatement 封装了一条sql相关信息的对象
     * @param params sql入参对象
     * @return 已自动映射成pojo类的查询结果
     * */
    <E> List<E> query(Configuration configuration, MappedStatement mappedStatement, Object... params) throws Exception;
?
    /**
     * 更新接口,使用jdbc与数据通讯,封装数据映射到pojo类返回
     * @param configuration 核心配置对象
     * @param mappedStatement 封装了一条sql相关信息的对象
     * @param params sql入参对象
     * @return 影响行数
     * */
    int update(Configuration configuration, MappedStatement mappedStatement,Object... params)  throws Exception;
}


接口定义了一个列表查询方法,一个更新方法,然后需要传递核心配置对象与MappedStatement对象,这样才能根据配置信息解析出要执行的SQL,将参数设置到PreparedStatement。在查询接口中,要求返回一个List<E>,查询结果需要封装到泛型列表对象进行返回。从上面的Mapper.xml的定义样例中我们大概能猜到这个接口的实现需要做的事情,比如根据MappedStatement解析SQL、设置SQL参数、查询结果,通过反射或内省将数据库查询结果映射到Java对象中。更新接口也是做差不多的事情,只是不需要映射结果集到对象,只需要返回影响行数即可。


MappedStatement


将userMapper.xml抽象成MappedStatement类,用于记录相关信息,命名空间加相关查询标签的id属性可以唯一确定一个sql,另外入参类型,结果类型,标签里面的sql文本为重要信息,应该抽象成字段。


package com.kmning.mybatis.config;

/**
 * 对某个mapper的一条sql作封装,即系统有多少条sql,将会产生多少个MappedStatement对象
 * 注意下面的属性名称应该和mapper.xml定义的sql标签的属性对应起来,方便理解的同时也规范
 * @author kangming.ning
 * @date 2021/5/2 17:05
 */
public class MappedStatement {

    /**
     * 唯一标签某个mapper的某条sql,用namespace+id,方便针对不同表映射到不同的mapper.xml
     * 总不能把所有表放同一个mapper吧
     * */
    private String id;

    /**
     * 入参类型全限定类路径
     * */
    private String parameterType;

    /**
     * 结果类型全限定类路径
     * */
    private String resultType;

    /**
     * 标签里面的sql语句,此语句后续需要进一步解析(如自定义入参标签#{}等)
     * */
    private String sql;

    //get set方法
    ...
}


SimpleExecutor


package com.kmning.mybatis.sqlSession;

import com.kmning.mybatis.config.BoundSql;
import com.kmning.mybatis.config.Configuration;
import com.kmning.mybatis.config.MappedStatement;
import com.kmning.mybatis.utils.GenericTokenParser;
import com.kmning.mybatis.utils.ParameterMapping;
import com.kmning.mybatis.utils.ParameterMappingTokenHandler;

import java.beans.PropertyDescriptor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;

/**
 * @author kangming.ning
 * @date 2021/5/2 20:46
 */
public class SimpleExecutor implements Executor{
    @Override
    public <E> List<E> query(Configuration configuration, MappedStatement mappedStatement, Object... params)  throws Exception{
        PreparedStatement preparedStatement = getPreparedStatement(configuration, mappedStatement, params);
        ResultSet resultSet = preparedStatement.executeQuery();
        //封装结果集
        String resultType = mappedStatement.getResultType();
        Class<?> resultClassType = getClassType(resultType);

        //遍历结果集封装到resultType对应的pojo类对象中
        List<Object> resultList=new ArrayList<>(10);
        while (resultSet.next()){
            Object resultObj = resultClassType.newInstance();
            //通过元数据找到字段名和其对应的值 这样才能准确的设置到返回类型对象对应的属性中
            ResultSetMetaData metaData = resultSet.getMetaData();
            //注意是从第一列开始
            for (int i = 1; i <= metaData.getColumnCount(); i++) {
                String columnName = metaData.getColumnName(i);
                Object columnValue = resultSet.getObject(columnName);
                //使用反射或者内省,根据数据库表和实体的对应关系,完成封装
                //使用内省(Introspector)
                PropertyDescriptor propertyDescriptor = new PropertyDescriptor(columnName,resultClassType);
                //如setUsername
                Method writeMethod = propertyDescriptor.getWriteMethod();
                //调用setter方法设置对象当前属性的值
                writeMethod.invoke(resultObj,columnValue);
                //使用反射(反射和内省,选一种即可)
               /* Field declaredField = resultClassType.getDeclaredField(columnName);
                declaredField.setAccessible(true);
                declaredField.set(resultObj,columnValue);*/
            }
            resultList.add(resultObj);
        }
        return (List<E>) resultList;
    }

    @Override
    public int update(Configuration configuration, MappedStatement mappedStatement, Object... params)  throws Exception{
        PreparedStatement preparedStatement = getPreparedStatement(configuration, mappedStatement, params);
        preparedStatement.execute();
        int updateCount = preparedStatement.getUpdateCount();
        return updateCount;
    }

    /**
     * 通用获取PreparedStatement方法
     * */
    private PreparedStatement getPreparedStatement(Configuration configuration, MappedStatement mappedStatement, Object... params) throws SQLException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
        //通过核心配置获取连接池中的一个连接
        Connection connection = configuration.getDataSource().getConnection();
        //获取sql 获取sql语句 : select * from user where id = #{id} and username = #{username}
        String sql = mappedStatement.getSql();
        //转换sql语句: select * from user where id = ? and username = ? ,转换的过程中,还需要对#{}里面的值进行解析存储
        BoundSql boundSql = getBoundSql(sql);
        //获取预处理对象:preparedStatement
        PreparedStatement preparedStatement = connection.prepareStatement(boundSql.getSqlText());
        //设置参数
        //获取参数的全路径
        String parameterType = mappedStatement.getParameterType();
        //获取入参Class对象
        Class<?> paramClassType = getClassType(parameterType);
        //根据通过反射获取入参对象中对应了 #{id}中的id和#{username}的username字段的值,设置到preparedStatement
        //这个列表是有序的,比如示例sql中,下面列表第0个元素就是id,第1一个元素就是username
        List<ParameterMapping> parameterMappingList = boundSql.getParameterMappingList();
        for (int i = 0; i < parameterMappingList.size(); i++) {
            ParameterMapping parameterMapping = parameterMappingList.get(i);
            //其实保存的就是入参对象的字段名
            String filedName = parameterMapping.getContent();
            //反射获取入参对象当前字段的值
            Field declaredField = paramClassType.getDeclaredField(filedName);
            //忽视修饰符号 直接访问
            declaredField.setAccessible(true);
            //注意下面的params[0]其实就是入参对象 如User对象,根据这个对象直接获取到了对象的值
            Object fieldValue = declaredField.get(params[0]);

            //设置preparedStatement的入参 第一个参数为sql语句中参数的序号(从1开始),第二个参数为设置的参数值
            preparedStatement.setObject(i+1,fieldValue);
        }
        return preparedStatement;
    }

    /**
     * 完成对#{}的解析工作:1.将#{}使用?进行代替,2.解析出#{}里面的值进行存储
     * @param sql 如 select * from user where id = #{id} and username = #{username}
     * @return
     */
    private BoundSql getBoundSql(String sql) {
        //标记处理类:配置标记解析器来完成对占位符的解析处理工作
        ParameterMappingTokenHandler parameterMappingTokenHandler = new ParameterMappingTokenHandler();
        GenericTokenParser genericTokenParser = new GenericTokenParser("#{", "}", parameterMappingTokenHandler);
        //解析出来的sql select * from user where id = ? and username = ?
        String parseSql = genericTokenParser.parse(sql);
        //#{}里面解析出来的参数名称 id username 通过取入参的查询对象的id和username值分别设置为第一个?和第二个?的参数值进行查询(结果是有序的)
        List<ParameterMapping> parameterMappings = parameterMappingTokenHandler.getParameterMappings();

        BoundSql boundSql = new BoundSql(parseSql,parameterMappings);
        return boundSql;
    }

    /**
     * 根据class全路径反射获取Class对象
     * */
    private Class<?> getClassType(String clazzPath) throws ClassNotFoundException {
        if (null!=clazzPath){
            Class<?> aClass = Class.forName(clazzPath);
            return aClass;
        }
        return null;
    }
}


这个执行器是自动映射的关键。当调用查询接口时,框架使用核心配置对象,将用户传进来的参数对象,通过反射获取值,配合Mapper.xml解析出的内容给SQL设置参数,执行后将结果集自动映射到resultType对象,最终返回结果列表。这里的参数值获取和结果数据映射都使用了反射或内省技术,很好的解决了硬编码等问题。反射,程序员的快乐。


至此,我们基本上完成了框架的配置文件解析(框架初始化时解析到configuration对象)、SQL参数设置、执行结果解析封装。接下来我们需要提供一个会话对象给用户使用,提供增删查改等接口。这样用户只需要知道这个会话对象怎么使用的就行,不必理会其内部是如何实现的。


SqlSession


package com.kmning.mybatis.sqlSession;

import java.util.List;

/**
 * sql会话接口,定义sql基本接口
 * @author kangming.ning
 * @date 2021/5/2 18:58
 */
public interface SqlSession {

    /**
     * 根据statementId查询数据列表
     * @param statementId mapper.xml中唯一标识一条sql
     * @param params 查询参数
     * */
     <E> List<E> selectList(String statementId, Object... params) throws Exception;

    /**
     * 根据statementId查询一条数据
     * @param statementId mapper.xml中唯一标识一条sql
     * @param params 查询参数
     * */
     <T> T selectOne(String statementId,Object... params) throws Exception;

    /**
     * 根据statementId 更新、新增、删除数据
     * @param statementId mapper.xml中唯一标识一条sql
     * @param params 查询参数
     * */
    Integer excuteUpdate(String statementId, Object... params) throws Exception;

    /**
     * 为Dao接口生成动态代理类
     * */
     <T> T getMapper(Class<?> mapperClass);
}


从接口的声明中可以看出,用户只需要知道什么是statementId,然后传SQL参数进去即可,至于内部如何实现则完全不需要关心。


DefaultSqlSession


package com.kmning.mybatis.sqlSession;

import com.kmning.mybatis.config.Configuration;
import com.kmning.mybatis.config.MappedStatement;

import java.lang.reflect.*;
import java.util.List;

/**
 * 默认SqlSession实现
 * @author kangming.ning
 * @date 2021/5/2 19:36
 */
public class DefaultSqlSession implements SqlSession{

    private Configuration configuration;

    private Executor executor;

    public DefaultSqlSession(Configuration configuration) {
        this.configuration = configuration;
        executor=new SimpleExecutor();
    }

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

    @Override
    public <T> T selectOne(String statementId, Object... params) throws Exception {
        List<Object> results = selectList(statementId, params);
        if (results.size()==1){
            return (T) results.get(0);
        }else {
            throw new RuntimeException("查询结果为空或者返回结果过多");
        }
    }

    @Override
    public Integer excuteUpdate(String statementId, Object... params) throws Exception {
        MappedStatement mappedStatement = configuration.getMappedStatementMap().get(statementId);
        int update = executor.update(configuration, mappedStatement, params);
        return update;
    }

    @Override
    public <T> T getMapper(Class<?> mapperClass) {
        Object mapperObj = Proxy.newProxyInstance(mapperClass.getClassLoader(), new Class[]{mapperClass}, new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                // 底层都还是去执行JDBC代码 //根据不同情况,来调用selctList或者selectOne
                // 准备参数 1:statmentid :sql语句的唯一标识:namespace.id= 接口全限定名.方法名
                // 方法名:findAll
                String clazzName = method.getDeclaringClass().getName();
                String methodName = method.getName();
                String statementId=clazzName+"."+methodName;
                // 准备参数2:params:args
                // 获取被调用方法的返回值类型
                Type genericReturnType = method.getGenericReturnType();
                //判断是否进行了 泛型类型参数化
                if (genericReturnType instanceof ParameterizedType){
                    List<Object> selectList = selectList(statementId, args);
                    return selectList;
                }
                //处理添加、修改、删除的情况
                MappedStatement mappedStatement = configuration.getMappedStatementMap().get(statementId);
                String sql = mappedStatement.getSql();

                if (sql.startsWith("update")||sql.startsWith("insert")
                        ||sql.startsWith("delete")||
                sql.startsWith("UPDATE")||sql.startsWith("INSERT")
                        ||sql.startsWith("DELETE")){
                    Integer count = excuteUpdate(statementId, args);
                    return count;
                }
                return selectOne(statementId,args);
            }
        });
        return (T) mapperObj;
    }
}


可以看出,实现方法无非是根据statementId将MappedStatement对象从框架初始化时就已经存下来的Map中取出,然后调用Executor对象去执行,得到结果。这些步骤对于不同的SQL查询基本都是一样的,所以不必让用户去写,提供接口即可。当然,细心的读者可能注意到上面定义了一个getMapper接口并且给出了实现代码。其实这就是mybatis的代理模式的实现原理。mybatis的代理是使用JDK的动态代理进行实现的,使用动态代理用户就没必要对Dao接口进行实现,因为其实现代码都是大同小异,甚至可以认为是基本重复的,在文末的测试类中我会给出一个Dao实现类,读者可以观察其实现代码。


SqlSessionFactory


有了Sqlsession操作接口及实现就已经可以使用Sqlsession对象去和数据库进行交互了。但使用Sqlsession是需要在构造函数里传入configuration对象的。不可能由客户端自己去创建这个对象。所以我们需要一个生产Sqlsession对象的工厂接口,将configuration对象保存在工厂类里面。另外在工厂Builder里面提供创建工厂对象方法。通常系统只需要创建一次即可,有了工厂生产Sqlsession对象就比较方便了。


package com.kmning.mybatis.sqlSession;

/**
 * 定义生产SqlSession接口
 * @author kangming.ning
 * @date 2021/5/2 22:44
 */
public interface SqlSessionFactory {

    /**
     * 获取一个sqlsession会话对象
     * */
    SqlSession openSession();
}

DefaultSqlSessionFactory

/**
 * 默认工厂实现
 * @author kangming.ning
 * @date 2021/5/2 22:45
 */
public class DefaultSqlSessionFactory implements SqlSessionFactory{

    private Configuration configuration;

    public DefaultSqlSessionFactory(Configuration configuration) {
        this.configuration = configuration;
    }

    @Override
    public SqlSession openSession() {
        return new DefaultSqlSession(configuration);
    }
}


会话对象解决了,但核心配置类还是需要传进来。这是必然的,因为这个配置类是框架的核心,并且依靠用户的配置文件来生成。所以必须要有个接口能让用户传配置文件的流进来,然后使用解析类去解析配置文件,封装到核心配置文件里,然后就可以将核心配置对象传进工厂里面生产会话对象进行使用了。


SqlSessionFactoryBuilder


package com.kmning.mybatis.sqlSession;

import com.kmning.mybatis.config.Configuration;
import com.kmning.mybatis.parser.XMLConfigBuilder;
import org.dom4j.DocumentException;

import java.io.InputStream;

/**
 * 构建SqlSessionFactory默认实现工厂对象
 * @author kangming.ning
 * @date 2021/5/2 22:52
 */
public class SqlSessionFactoryBuilder {

    public static SqlSessionFactory build(InputStream inputStream) throws DocumentException {
        //第一:使用dom4j解析配置文件,将解析出来的内容封装到Configuration中
        XMLConfigBuilder xmlConfigBuilder=new XMLConfigBuilder();
        Configuration configuration = xmlConfigBuilder.parseConfig(inputStream);
        //第二:创建sqlSessionFactory对象:工厂类:生产sqlSession:会话对象
        DefaultSqlSessionFactory defaultSqlSessionFactory = new DefaultSqlSessionFactory(configuration);
        return defaultSqlSessionFactory;
    }
}


上面的XMLConfigBuilder就是用来解析核心配置文件以将相关信息保存到核心配置对象中的。基本这个方法就是提供给用户使用的了。


框架端开发流程整理


  1. 读取配置文件:读取完成以后以流的形式存在,我们不能将读取到的配置信息以流的形式存放在内存中,不好操作,可以创建javaBean来存储。
  2. (1)Configuration : 核心配置类(sqlMapConfig.xml解析出来的内容),存放数据库基本信息、Map<唯一标识,Mapper> 唯一标识:namespace + "." +id
  3. (2)MappedStatement:sql语句、statement类型、输入参数java类型、输出参数java类型
  4. 解析配置文件
  5. 创建sqlSessionFactoryBuilder类:
  6. 方法:sqlSessionFactory build():
  7. 第一:使用dom4j解析配置文件,将解析出来的内容封装到Configuration和MappedStatement中
  8. 第二:创建SqlSessionFactory的实现类DefaultSqlSession
  9. 创建SqlSessionFactory
  10. 方法:openSession() : 获取sqlSession接口的实现类实例对象
  11. 创建sqlSession接口及实现类:主要封装crud方法
  12. 方法:selectList(String statementId,Object param):查询所有
  13. selectOne(String statementId,Object param):查询单个
  14. 具体实现:封装JDBC完成对数据库表的查询操作


涉及到的设计模式:Builder构建者设计模式、工厂模式、代理模式。


使用端测试自定义框架


引入相关依赖


<dependency>
            <groupId>com.kmning.mybatis</groupId>
            <artifactId>mybatis-custom</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13.2</version>
        </dependency>


UserMapper.xml


namespace+id可以标识一条sql


<mapper namespace="com.kmning.mybatis.test.dao.UserDao">

    <!--sql的唯一标识:namespace.id来组成 : statementId-->
    <select id="findAll" resultType="com.kmning.mybatis.test.pojo.User" >
        select * from user
    </select>

    <select id="findByCondition" resultType="com.kmning.mybatis.test.pojo.User" parameterType="com.kmning.mybatis.test.pojo.User">
        select * from user where id = #{id} and username = #{username}
    </select>

    <update id="updateUserById" resultType="java.lang.Integer" parameterType="com.kmning.mybatis.test.pojo.User">
        UPDATE `user` SET username=#{username},telphone=#{telphone} WHERE id=#{id};
    </update>

    <insert id="insertUser" resultType="java.lang.Integer" parameterType="com.kmning.mybatis.test.pojo.User">
        INSERT INTO `user` VALUES(#{id},#{username},#{telphone});
    </insert>

    <delete id="deleteUserById" resultType="java.lang.Integer" parameterType="com.kmning.mybatis.test.pojo.User">
        DELETE FROM `user` WHERE id=#{id};
    </delete>

</mapper>


User实体


用于映射数据库查询记录


package com.kmning.mybatis.test.pojo;

/**
 * @author kangming.ning
 * @date 2021/5/2 16:43
 */
public class User {
    private Integer id;
    
    private String username;
    
    private String telphone;

   // get set
   ...
}


dao接口


package com.kmning.mybatis.test.dao;

import com.kmning.mybatis.test.pojo.User;

import java.util.List;

/**
 * @author kangming.ning
 * @date 2021/5/2 18:11
 */
public interface UserDao {

     List<User> findAll() throws Exception;

     User findByCondition(User user) throws Exception;

    Integer insertUser(User user) throws Exception;

    Integer updateUserById(User user) throws Exception;

    Integer deleteUserById(User user)throws Exception;
}


创建dao接口实现类


package com.kmning.mybatis.test.dao;
?
import com.kmning.mybatis.io.Resources;
import com.kmning.mybatis.sqlSession.SqlSession;
import com.kmning.mybatis.sqlSession.SqlSessionFactory;
import com.kmning.mybatis.sqlSession.SqlSessionFactoryBuilder;
import com.kmning.mybatis.test.pojo.User;
import org.dom4j.DocumentException;

import java.io.InputStream;
import java.util.List;

/**
 * 数据层实现类
 * @author kangming.ning
 * @date 2021/5/2 22:58
 */
public class UserDaoImpl implements UserDao{

    private SqlSessionFactory sessionFactory;

    public UserDaoImpl() {
        InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
        try {
             sessionFactory = SqlSessionFactoryBuilder.build(resourceAsStream);
        } catch (DocumentException e) {
            e.printStackTrace();
        }
    }

    public List<User> findAll() throws Exception {
        SqlSession sqlSession = sessionFactory.openSession();
        List<User> userList = sqlSession.selectList("com.kmning.mybatis.test.dao.UserDao.findAll");
        return userList;
    }

    public User findByCondition(User user) throws Exception {
        SqlSession sqlSession = sessionFactory.openSession();
        User result = sqlSession.selectOne("com.kmning.mybatis.test.dao.UserDao.findByCondition",user);
        return result;
    }

    public Integer insertUser(User user) throws Exception {
        SqlSession sqlSession = sessionFactory.openSession();
        Integer rows = sqlSession.excuteUpdate("com.kmning.mybatis.test.dao.UserDao.insertUser", user);
        return rows;
    }

    public Integer updateUserById(User user) throws Exception {
        SqlSession sqlSession = sessionFactory.openSession();
        Integer rows = sqlSession.excuteUpdate("com.kmning.mybatis.test.dao.UserDao.updateUserById", user);
        return rows;
    }

    public Integer deleteUserById(User user) throws Exception {
        SqlSession sqlSession = sessionFactory.openSession();
        Integer rows = sqlSession.excuteUpdate("com.kmning.mybatis.test.dao.UserDao.deleteUserById", user);
        return rows;
    }
}


测试类


package com.kmning.mybatis.test;

import com.kmning.mybatis.test.dao.UserDao;
import com.kmning.mybatis.test.dao.UserDaoImpl;
import com.kmning.mybatis.test.pojo.User;
import org.junit.Test;

import java.util.List;

/**
 * @author kangming.ning
 * @date 2021/5/2 23:15
 */
public class UserTest {

    private UserDao userDao=new UserDaoImpl();

    @Test
    public void findAll() throws Exception {
        List<User> list = userDao.findAll();
        for (User user : list) {
            System.out.println(user);
        }
    }

    @Test
    public void findByCondition() throws Exception {
        User user = new User();
        user.setUsername("aa");
        user.setId(1);
        User byCondition = userDao.findByCondition(user);
        System.out.println(byCondition);
    }

    @Test
    public void insertOne() throws Exception {
        User user = new User();
        user.setId(4);
        user.setUsername("cc");
        user.setTelphone("666");
        Integer count = userDao.insertUser(user);
        System.out.println("影响行数:"+count);
    }

    @Test
    public void updateOne() throws Exception {
        User user = new User();
        user.setId(4);
        user.setUsername("ccc");
        user.setTelphone("6667");
        Integer count = userDao.updateUserById(user);
        System.out.println("影响行数:"+count);
    }

    @Test
    public void deleteOne() throws Exception {
        User user = new User();
        user.setId(4);
        Integer count = userDao.deleteUserById(user);
        System.out.println("影响行数:"+count);
    }
}


使用Mapper(动态)代理模式开发


通过上述我们的自定义框架,我们解决了JDBC操作数据库带来的一些问题:例如频繁创建释放数据库连接,硬编码,手动封装返回结果集等问题,但是现在我们继续来分析刚刚完成的自定义框架代码,有没有什么问题?


问题如下 :


  • dao的实现类中存在重复的代码,整个操作的过程模板重复(创建sqlsession,调用sqlsession方法,关闭 sqlsession)
  • dao的实现类中存在硬编码,调用sqlsession的方法时,参数statement的id硬编码


解决:使用代理模式来创建接口的代理对象


在SqlSession的实现类(DefaultSqlSession)中实现获取代理对象的方法


@Override
    public <T> T getMapper(Class<?> mapperClass) {
        Object mapperObj = Proxy.newProxyInstance(mapperClass.getClassLoader(), new Class[]{mapperClass}, new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                // 底层都还是去执行JDBC代码 //根据不同情况,来调用selctList或者selectOne
                // 准备参数 1:statmentid :sql语句的唯一标识:namespace.id= 接口全限定名.方法名
                // 方法名:findAll
                String clazzName = method.getDeclaringClass().getName();
                String methodName = method.getName();
                String statementId=clazzName+"."+methodName;
                // 准备参数2:params:args
                // 获取被调用方法的返回值类型
                Type genericReturnType = method.getGenericReturnType();
                //判断是否进行了 泛型类型参数化
                if (genericReturnType instanceof ParameterizedType){
                    List<Object> selectList = selectList(statementId, args);
                    return selectList;
                }
                //处理添加、修改、删除的情况
                MappedStatement mappedStatement = configuration.getMappedStatementMap().get(statementId);
                String sql = mappedStatement.getSql();
                 if (sql.startsWith("update")||sql.startsWith("insert")
                        ||sql.startsWith("delete")||
                sql.startsWith("UPDATE")||sql.startsWith("INSERT")
                        ||sql.startsWith("DELETE")){
                    Integer count = excuteUpdate(statementId, args);
                    return count;
                }
                return selectOne(statementId,args);
            }
        });
        return (T) mapperObj;
    }


测试代理模式下的CRUD


package com.kmning.mybatis.test;
?
import com.kmning.mybatis.io.Resources;
import com.kmning.mybatis.sqlSession.SqlSession;
import com.kmning.mybatis.sqlSession.SqlSessionFactory;
import com.kmning.mybatis.sqlSession.SqlSessionFactoryBuilder;
import com.kmning.mybatis.test.dao.UserDao;
import com.kmning.mybatis.test.pojo.User;
import org.dom4j.DocumentException;
import org.junit.Before;
import org.junit.Test;

import java.io.InputStream;
import java.util.List;

/**
 * @author kangming.ning
 * @date 2021/5/2 23:15
 */
public class UserMapperTest {


    private UserDao userDao;


    @Before
    public void before() throws DocumentException {
        InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
        SqlSessionFactory sqlSessionFactory = SqlSessionFactoryBuilder.build(resourceAsStream);
        SqlSession sqlSession = sqlSessionFactory.openSession();
        userDao= sqlSession.getMapper(UserDao.class);
    }


    @Test
    public void findAll() throws Exception {
        List<User> list = userDao.findAll();
        for (User user : list) {
            System.out.println(user);
        }
    }

    @Test
    public void findByCondition() throws Exception {
        User user = new User();
        user.setUsername("aa");
        user.setId(1);
        User byCondition = userDao.findByCondition(user);
        System.out.println(byCondition);
    }

    @Test
    public void insertOne() throws Exception {
        User user = new User();
        user.setId(4);
        user.setUsername("cc");
        user.setTelphone("666");
        Integer count = userDao.insertUser(user);
        System.out.println("影响行数:"+count);
    }

    @Test
    public void updateOne() throws Exception {
        User user = new User();
        user.setId(6);
        user.setUsername("ccc");
        user.setTelphone("6667");
        Integer count = userDao.updateUserById(user);
        System.out.println("影响行数:"+count);
    }

    @Test
    public void deleteOne() throws Exception {
        User user = new User();
        user.setId(4);
        Integer count = userDao.deleteUserById(user);
        System.out.println("影响行数:"+count);
    }
}


至此,已经基本完整介绍了自定义框架的思路并对其进行实现。当然,忽略不了不少非核心代码,比如解析配置文件等代码,相对简单,避免文章过于拖沓冗长(可能已经冗长了)就没有全部贴出来。

?

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表