美文网首页
天哪!手动编写mybatis雏形竟然这么简单

天哪!手动编写mybatis雏形竟然这么简单

作者: 程序员爱酸奶 | 来源:发表于2020-06-06 15:54 被阅读0次

    前言

    mybaits 在ORM 框架中,可算是半壁江山了,由于它是轻量级,半自动加载,灵活性和易拓展性。深受广大公司的喜爱,所以我们程序开发也离不开mybatis 。但是我们有对mabtis 源码进行研究吗?或者想看但是不知道怎么看的苦恼吗?


    在这里插入图片描述

    归根结底,我们还是需要知道为什么会有mybatis ,mybatis 解决了什么问题?
    想要知道mybatis 解决了什么问题,就要知道传统的JDBC 操作存在哪些痛点才促使mybatis 的诞生。
    我们带着这些疑问,再来一步步学习吧。

    原始JDBC 存在的问题

    所以我们先来来看下原始JDBC 的操作:
    我们知道最原始的数据库操作。分为以下几步:
    1、获取connection 连接
    2、获取preparedStatement
    3、参数替代占位符
    4、获取执行结果resultSet
    5、解析封装resultSet 到对象中返回。

    如下是原始JDBC 的查询代码,存在哪些问题?

    public static void main(String[] args) {
            String dirver="com.mysql.jdbc.Driver";
            String url="jdbc:mysql://localhost:3306/mybatis?characterEncoding=utf-8";
            String userName="root";
            String password="123456";
    
            Connection connection=null;
            List<User> userList=new ArrayList<>();
            try {
                Class.forName(dirver);
                connection= DriverManager.getConnection(url,userName,password);
    
                String sql="select * from user where username=?";
                PreparedStatement preparedStatement=connection.prepareStatement(sql);
                preparedStatement.setString(1,"张三");
                System.out.println(sql);
                ResultSet resultSet=preparedStatement.executeQuery();
    
                User user=null;
                while(resultSet.next()){
                    user=new User();
                    user.setId(resultSet.getInt("id"));
                    user.setUsername(resultSet.getString("username"));
                    user.setPassword(resultSet.getString("password"));
                    userList.add(user);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }finally {
                try {
                    connection.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
    
            if (!userList.isEmpty()) {
                for (User user : userList) {
                    System.out.println(user.toString());
                }
            }
    
        }
    

    小伙伴们发现了上面有哪些不友好的地方?
    我这里总结了以下几点:
    1、数据库的连接信息存在硬编码,即是写死在代码中的。
    2、每次操作都会建立和释放connection 连接,操作资源的不必要的浪费。
    3、sql 和参数存在硬编码。
    4、将返回结果集封装成实体类麻烦,要创建不同的实体类,并通过set方法一个个的注入。

    存在上面的问题,所以mybatis 就对上述问题进行了改进。
    对于硬编码,我们很容易就想到配置文件来解决。mybatis 也是这么解决的。
    对于资源浪费,我们想到是用连接池,mybatis 也是这个解决的。
    对于封装结果集麻烦,我们想到是用JDK的反射机制,好巧,mybatis 也是这么解决的。


    在这里插入图片描述

    设计思路

    既然如此,我们就来写一个自定义吃持久层框架,来解决上述问题,当然是参照mybatis 的设计思路,这样我们在写完之后,再来看mybatis 的源码就恍然大悟,这个地方这样配置原来是因为这样啊。
    我们分为使用端和框架端两部分。

    使用端

    我们在使用mybatis 的时候是不是需要使用SqlMapConfig.xml 配置文件,用来存放数据库的连接信息,以及mapper.xml 的指向信息。mapper.xml 配置文件用来存放sql 信息。
    所以我们在使用端来创建两个文件SqlMapConfig.xml 和mapper.xml。

    框架端

    框架端要做哪些事情呢?如下:
    1、获取配置文件。也就是获取到使用端的SqlMapConfig.xml 以及mapper.xml的 文件
    2、解析配置文件。对获取到的文件进行解析,获取到连接信息,sql,参数,返回类型等等。这些信息都会保存在configuration 这个对象中。
    3、创建SqlSessionFactory,目的是创建SqlSession的一个实例。
    4、创建SqlSession ,用来完成上面原始JDBC 的那些操作。

    那在SqlSession 中 进行了哪些操作呢?
    1、获取数据库连接
    2、获取sql,并对sql 进行解析
    3、通过内省,将参数注入到preparedStatement 中
    4、执行sql
    5、通过反射将结果集封装成对象

    使用端实现

    好了,上面说了一下,大概的设计思路,主要也是仿照mybatis 主要的类实现的,保证类名一致,方便我们后面阅读源码。我们先来配置好使用端吧,我们创建一个maven 项目。
    在项目中,我们创建一个User实体类

    public class User {
        private Integer id;
        private String username;
        private String password;
        private String birthday;
        //getter()和setter()方法
    }
    

    创建SqlMapConfig.xml 和Mapper.xml
    SqlMapConfig.xml

    <?xml version="1.0" encoding="UTF-8" ?>
    <configuration>
        <property name="driverClass" value="com.mysql.jdbc.Driver"></property>
        <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC&amp;characterEncoding=utf8&amp;useUnicode=true&amp;useSSL=false"></property>
        <property name="userName" value="root"></property>
        <property name="password" value="123456"></property>
        
        <mapper resource="UserMapper.xml">
        </mapper>
    </configuration>
    

    可以看到我们xml 中就配置了数据库的连接信息,以及mapper 一个索引。mybatis中的SqlMapConfig.xml 中还包含其他的标签,只是丰富了功能而已,所以我们只用最主要的。

    mapper.xml
    是每个类的sql 都会生成一个对应的mapper.xml 。我们这里就用User 类来说吧,所以我们就创建一个UserMapper.xml

    <?xml version="1.0" encoding="UTF-8" ?>
    <mapper namespace="cn.quellanan.dao.UserDao">
        <select id="selectAll" resultType="cn.quellanan.pojo.User">
            select * from user
        </select>
        <select id="selectByName" resultType="cn.quellanan.pojo.User" paramType="cn.quellanan.pojo.User">
            select * from user where username=#{username}
        </select>
    </mapper>
    

    可以看到有点mybatis 里面文件的味道,有namespace表示命名空间,id 唯一标识,resultType 返回结果集的类型,paramType 参数的类型。
    我们使用端先创建到这,主要是两个配置文件,我们接下来看看框架端是怎么实现的。

    加油哈哈。


    在这里插入图片描述

    框架端实现

    框架端,我们按照上面的设计思路一步一步来。

    获取配置

    怎么样获取配置文件呢?我们可以使用JDK自带自带的类Resources加载器来获取文件。我们创建一个自定义Resource类来封装一下:

    import java.io.InputStream;
    public class Resources {
        public  static InputStream getResources(String path){
            //使用系统自带的类Resources加载器来获取文件。
            return Resources.class.getClassLoader().getResourceAsStream(path);
        }
    }
    

    这样通过传入路径,就可以获取到对应的文件流啦。

    解析配置文件

    上面获取到了SqlMapConfig.xml 配置文件,我们现在来解析它。
    不过在此之前,我们需要做一点准备工作,就是解析的内存放到什么地方?
    所以我们来创建两个实体类Mapper 和Configuration。

    Mapper
    Mapper 实体类用来存放使用端写的mapper.xml 文件的内容,我们前面说了里面有.id、sql、resultType 和paramType .所以我们创建的Mapper实体如下:

    public class Mapper {
        private String id;
        private Class<?> resultType;
        private Class<?> parmType;
        private String sql;
        //getter()和setter()方法
    }
    

    这里我们为什么不添加namespace 的值呢?
    聪明的你肯定发现了,因为mapper里面这些属性表明每个sql 都对应一个mapper,而namespace 是一个命名空间,算是sql 的上一层,所以在mapper中暂时使用不到,就没有添加了。

    Configuration
    Configuration 实体用来保存SqlMapConfig 中的信息。所以需要保存数据库连接,我们这里直接用JDK提供的 DataSource。还有一个就是mapper 的信息。每个mapper 有自己的标识,所以这里采用hashMap来存储。如下:

    public class Configuration {
    
        private DataSource dataSource;
        HashMap <String,Mapper> mapperMap=new HashMap<>();
        //getter()和setter方法
        }
    

    XmlMapperBuilder

    做好了上面的准备工作,我们先来解析mapper 吧。我们创建一个XmlMapperBuilder 类来解析。通过dom4j 的工具类来解析XML 文件。我这里用的dom4j 依赖为:

            <dependency>
                <groupId>org.dom4j</groupId>
                <artifactId>dom4j</artifactId>
                <version>2.1.3</version>
            </dependency>
    

    思路:
    1、获取文件流,转成document。
    2、获取根节点,也就是mapper。获取根节点的namespace属性值
    3、获取select 节点,获取其id,sql,resultType,paramType
    4、将select 节点的属性封装到Mapper 实体类中。
    5、同理获取update/insert/delete 节点的属性值封装到Mapper 中
    6、通过namespace.id 生成key 值将mapper对象保存到Configuration实体中的HashMap 中。
    7、返回 Configuration实体
    代码如下:

    
    public class XmlMapperBuilder {
        private Configuration configuration;
        public XmlMapperBuilder(Configuration configuration){
            this.configuration=configuration;
        }
    
        public Configuration loadXmlMapper(InputStream in) throws DocumentException, ClassNotFoundException {
            Document document=new SAXReader().read(in);
    
            Element rootElement=document.getRootElement();
            String namespace=rootElement.attributeValue("namespace");
    
            List<Node> list=rootElement.selectNodes("//select");
    
            for (int i = 0; i < list.size(); i++) {
                Mapper mapper=new Mapper();
                Element element= (Element) list.get(i);
                String id=element.attributeValue("id");
                mapper.setId(id);
                String paramType = element.attributeValue("paramType");
                if(paramType!=null && !paramType.isEmpty()){
                    mapper.setParmType(Class.forName(paramType));
                }
                String resultType = element.attributeValue("resultType");
                if (resultType != null && !resultType.isEmpty()) {
                    mapper.setResultType(Class.forName(resultType));
                }
                mapper.setSql(element.getTextTrim());
                String key=namespace+"."+id;
                configuration.getMapperMap().put(key,mapper);
            }
            return configuration;
        }
    
    }
    

    上面我只解析了select 标签。大家可以解析对应insert/delete/uupdate 标签,操作都是一样的。

    XmlConfigBuilder

    我们再来解析一下SqlMapConfig.xml 配置信息思路是一样的,
    1、获取文件流,转成document。
    2、获取根节点,也就是configuration。
    3、获取根节点中所有的property 节点,并获取值,也就是获取数据库连接信息
    4、创建一个dataSource 连接池
    5、将连接池信息保存到Configuration实体中
    6、获取根节点的所有mapper 节点
    7、调用XmlMapperBuilder 类解析对应mapper 并封装到Configuration实体中
    8、完
    代码如下:

    public class XmlConfigBuilder {
        private Configuration configuration;
        public XmlConfigBuilder(Configuration configuration){
            this.configuration=configuration;
        }
    
        public Configuration loadXmlConfig(InputStream in) throws DocumentException, PropertyVetoException, ClassNotFoundException {
    
            Document document=new SAXReader().read(in);
    
            Element rootElement=document.getRootElement();
    
            //获取连接信息
            List<Node> propertyList=rootElement.selectNodes("//property");
            Properties properties=new Properties();
    
            for (int i = 0; i < propertyList.size(); i++) {
                Element element = (Element) propertyList.get(i);
                properties.setProperty(element.attributeValue("name"),element.attributeValue("value"));
            }
            //是用连接池
            ComboPooledDataSource dataSource = new ComboPooledDataSource();
            dataSource.setDriverClass(properties.getProperty("driverClass"));
            dataSource.setJdbcUrl(properties.getProperty("jdbcUrl"));
            dataSource.setUser(properties.getProperty("userName"));
            dataSource.setPassword(properties.getProperty("password"));
            configuration.setDataSource(dataSource);
    
            //获取mapper 信息
            List<Node> mapperList=rootElement.selectNodes("//mapper");
            for (int i = 0; i < mapperList.size(); i++) {
                Element element= (Element) mapperList.get(i);
                String mapperPath=element.attributeValue("resource");
                XmlMapperBuilder xmlMapperBuilder = new XmlMapperBuilder(configuration);
                configuration=xmlMapperBuilder.loadXmlMapper(Resources.getResources(mapperPath));
            }
            return configuration;
        }
    }
    

    创建SqlSessionFactory

    完成解析后我们创建SqlSessionFactory 用来创建Sqlseesion 的实体,这里为了尽量还原mybatis 设计思路,也也采用的工厂设计模式。
    SqlSessionFactory 是一个接口,里面就一个用来创建SqlSessionf的方法。
    如下:

    public interface SqlSessionFactory {
        public SqlSession openSqlSession();
    }
    

    单单这个接口是不够的,我们还得写一个接口的实现类,所以我们创建一个DefaultSqlSessionFactory。
    如下:

    public class DefaultSqlSessionFactory implements SqlSessionFactory {
    
        private Configuration configuration;
    
        public DefaultSqlSessionFactory(Configuration configuration) {
            this.configuration = configuration;
        }
        public SqlSession openSqlSession() {
            return new DefaultSqlSeeion(configuration);
        }
    }
    

    可以看到就是创建一个DefaultSqlSeeion并将包含配置信息的configuration 传递下去。DefaultSqlSeeion 就是SqlSession 的一个实现类。

    创建SqlSession

    在SqlSession 中我们就要来处理各种操作了,比如selectList,selectOne,insert.update,delete 等等。
    我们这里SqlSession 就先写一个selectList 方法。
    如下:

    public interface SqlSession {
    
        /**
         * 条件查找
         * @param statementid  唯一标识,namespace.selectid
         * @param parm  传参,可以不传也可以一个,也可以多个
         * @param <E>
         * @return
         */
        public <E> List<E> selectList(String statementid,Object...parm) throws Exception;
    
    

    然后我们创建DefaultSqlSeeion 来实现SqlSeesion 。

    public class DefaultSqlSeeion implements SqlSession {
        private Configuration configuration;
        private Executer executer=new SimpleExecuter();
        
        public DefaultSqlSeeion(Configuration configuration) {
            this.configuration = configuration;
        }
    
        @Override
        public <E> List<E> selectList(String statementid, Object... parm) throws Exception {
            Mapper mapper=configuration.getMapperMap().get(statementid);
            List<E> query = executer.query(configuration, mapper, parm);
            return query;
        }
    
    }
    

    我们可以看到DefaultSqlSeeion 获取到了configuration,并通过statementid 从configuration 中获取mapper。 然后具体实现交给了Executer 类来实现。我们这里先不管Executer 是怎么实现的,就假装已经实现了。那么整个框架端就完成了。通过调用Sqlsession.selectList() 方法,来获取结果。


    在这里插入图片描述

    感觉我们都还没有处理,就框架搭建好了?骗鬼呢,确实前面我们从获取文件解析文件,然后创建工厂。都是做好准备工作。下面开始我们JDBC的实现。

    SqlSession 具体实现

    我们前面说SqlSeesion 的具体实现有下面5步
    1、获取数据库连接
    2、获取sql,并对sql 进行解析
    3、通过内省,将参数注入到preparedStatement 中
    4、执行sql
    5、通过反射将结果集封装成对象

    但是我们在DefaultSqlSeeion 中将实现交给了Executer来执行。所以我们就要在Executer中来实现这些操作。

    我们首先来创建一个Executer 接口,并写一个DefaultSqlSeeion中调用的query 方法。

    public interface Executer {
    
        <E> List<E> query(Configuration configuration,Mapper mapper,Object...parm) throws Exception;
    
    }
    

    接着我们写一个SimpleExecuter 类来实现Executer 。
    然后SimpleExecuter.query()方法中,我们一步一步的实现。

    获取数据库连接

    因为数据库连接信息保存在configuration,所以直接获取就好了。

    //获取连接
            connection=configuration.getDataSource().getConnection();
    

    获取sql,并对sql 进行解析

    我们这里想一下,我们在Usermapper.xml写的sql 是什么样子?

    select * from user where username=#{username}
    

    {username} 这样的sql 我们改怎么解析呢?

    分两步
    1、将sql 找到#{***},并将这部分替换成 ?号

    2、对 #{***} 进行解析获取到里面的参数对应的paramType 中的值。

    具体实现用到下面几个类。
    GenericTokenParser类,可以看到有三个参数,开始标记,就是我们的“#{” ,结束标记就是 “}”, 标记处理器就是处理标记里面的内容也就是username。

    public class GenericTokenParser {
    
      private final String openToken; //开始标记
      private final String closeToken; //结束标记
      private final TokenHandler handler; //标记处理器
    
      public GenericTokenParser(String openToken, String closeToken, TokenHandler handler) {
        this.openToken = openToken;
        this.closeToken = closeToken;
        this.handler = handler;
      }
    
      /**
       * 解析${}和#{}
       * @param text
       * @return
       * 该方法主要实现了配置文件、脚本等片段中占位符的解析、处理工作,并返回最终需要的数据。
       * 其中,解析工作由该方法完成,处理工作是由处理器handler的handleToken()方法来实现
       */
      public String parse(String text) {
         //具体实现
         }
      }
    

    主要的就是parse() 方法,用来获取操作1 的sql。获取结果例如:

    select * from user where username=?
    

    那上面用到TokenHandler 来处理参数。
    ParameterMappingTokenHandler实现TokenHandler的类

    
    public class ParameterMappingTokenHandler implements TokenHandler {
        private List<ParameterMapping> parameterMappings = new ArrayList<ParameterMapping>();
    
        // context是参数名称 #{id} #{username}
    
        @Override
        public String handleToken(String content) {
            parameterMappings.add(buildParameterMapping(content));
            return "?";
        }
    
        private ParameterMapping buildParameterMapping(String content) {
            ParameterMapping parameterMapping = new ParameterMapping(content);
            return parameterMapping;
        }
    
        public List<ParameterMapping> getParameterMappings() {
            return parameterMappings;
        }
    
        public void setParameterMappings(List<ParameterMapping> parameterMappings) {
            this.parameterMappings = parameterMappings;
        }
    
    }
    
    

    可以看到将参数名称存放 ParameterMapping 的集合中了。
    ParameterMapping 类就是一个实体,用来保存参数名称的。

    public class ParameterMapping {
    
        private String content;
    
        public ParameterMapping(String content) {
            this.content = content;
        }
        //getter()和setter() 方法。
    }
    

    所以我们在我们通过GenericTokenParser类,就可以获取到解析后的sql,以及参数名称。我们将这些信息封装到BoundSql实体类中。

    public class BoundSql {
    
        private String sqlText;
        private List<ParameterMapping> parameterMappingList=new ArrayList<>();
        public BoundSql(String sqlText, List<ParameterMapping> parameterMappingList) {
            this.sqlText = sqlText;
            this.parameterMappingList = parameterMappingList;
        }
        ////getter()和setter() 方法。
      }
    

    好了,那么分两步走,先获取,后解析
    获取
    获取原始sql 很简单,sql 信息就存在mapper 对象中,直接获取就好了。

    String sql=mapper.getSql()
    

    解析
    1、创建一个ParameterMappingTokenHandler 处理器
    2、创建一个GenericTokenParser 类,并初始化开始标记,结束标记,处理器
    3、执行genericTokenParser.parse(sql);获取解析后的sql‘’,以及在parameterMappingTokenHandler 中存放了参数名称的集合。
    4、将解析后的sql 和参数封装到BoundSql 实体类中。

    /**
         * 解析自定义占位符
         * @param sql
         * @return
         */
        private BoundSql getBoundSql(String sql){
            ParameterMappingTokenHandler parameterMappingTokenHandler = new ParameterMappingTokenHandler();
            GenericTokenParser genericTokenParser = new GenericTokenParser("#{","}",parameterMappingTokenHandler);
            String parse = genericTokenParser.parse(sql);
            return new BoundSql(parse,parameterMappingTokenHandler.getParameterMappings());
    
        }
    

    将参数注入到preparedStatement 中

    上面的就完成了sql,的解析,但是我们知道上面得到的sql 还是包含 JDBC的 占位符,所以我们需要将参数注入到preparedStatement 中。
    1、通过boundSql.getSqlText()获取带有占位符的sql.
    2、接收参数名称集合 parameterMappingList
    3、通过mapper.getParmType() 获取到参数的类。
    4、通过getDeclaredField(content)方法获取到参数类的Field。
    5、通过Field.get() 从参数类中获取对应的值
    6、注入到preparedStatement 中

            BoundSql boundSql=getBoundSql(mapper.getSql());
            String sql=boundSql.getSqlText();
            List<ParameterMapping> parameterMappingList = boundSql.getParameterMappingList();
    
            //获取preparedStatement,并传递参数值
            PreparedStatement preparedStatement=connection.prepareStatement(sql);
            Class<?> parmType = mapper.getParmType();
    
            for (int i = 0; i < parameterMappingList.size(); i++) {
                ParameterMapping parameterMapping = parameterMappingList.get(i);
                String content = parameterMapping.getContent();
                Field declaredField = parmType.getDeclaredField(content);
                declaredField.setAccessible(true);
                Object o = declaredField.get(parm[0]);
                preparedStatement.setObject(i+1,o);
            }
            System.out.println(sql);
            return preparedStatement;
    

    执行sql

    其实还是调用JDBC 的executeQuery()方法或者execute()方法

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

    通过反射将结果集封装成对象

    在获取到resultSet 后,我们进行封装处理,和参数处理是类似的。
    1、创建一个ArrayList
    2、获取返回类型的类
    3、循环从resultSet中取数据
    4、获取属性名和属性值
    5、创建属性生成器
    6、为属性生成写方法,并将属性值写入到属性中
    7、将这条记录添加到list 中
    8、返回list

    /**
         * 封装结果集
         * @param mapper
         * @param resultSet
         * @param <E>
         * @return
         * @throws Exception
         */
        private <E> List<E> resultHandle(Mapper mapper,ResultSet resultSet) throws Exception{
            ArrayList<E> list=new ArrayList<>();
            //封装结果集
            Class<?> resultType = mapper.getResultType();
            while (resultSet.next()) {
                ResultSetMetaData metaData = resultSet.getMetaData();
                Object o = resultType.newInstance();
                int columnCount = metaData.getColumnCount();
                for (int i = 1; i <= columnCount; i++) {
                    //属性名
                    String columnName = metaData.getColumnName(i);
                    //属性值
                    Object value = resultSet.getObject(columnName);
                    //创建属性描述器,为属性生成读写方法
                    PropertyDescriptor propertyDescriptor = new PropertyDescriptor(columnName,resultType);
                    Method writeMethod = propertyDescriptor.getWriteMethod();
                    writeMethod.invoke(o,value);
                }
                list.add((E) o);
            }
            return list;
        }
    

    创建SqlSessionFactoryBuilder

    我们现在来创建一个SqlSessionFactoryBuilder 类,来为使用端提供一个人口。

    public class SqlSessionFactoryBuilder {
    
        private Configuration configuration;
    
        public SqlSessionFactoryBuilder(){
            configuration=new Configuration();
        }
    
        public SqlSessionFactory build(InputStream in) throws DocumentException, PropertyVetoException, ClassNotFoundException {
            XmlConfigBuilder xmlConfigBuilder = new XmlConfigBuilder(configuration);
            configuration=xmlConfigBuilder.loadXmlConfig(in);
    
            SqlSessionFactory sqlSessionFactory = new DefaultSqlSessionFactory(configuration);
            return sqlSessionFactory;
        }
    }
    

    可以看到就一个build 方法,通过SqlMapConfig的文件流将信息解析到configuration,创建并返回一个sqlSessionFactory 。

    到此,整个框架端已经搭建完成了,但是我们可以看到,只实现了select 的操作,update、inster、delete 的操作我们在我后面提供的源码中会有实现,这里只是将整体的设计思路和流程。

    在这里插入图片描述

    测试

    终于到了测试的环节啦。我们前面写了自定义的持久层,我们现在来测试一下能不能正常的使用吧。
    见证奇迹的时刻到啦


    在这里插入图片描述

    我们先引入我们自定义的框架依赖。以及数据库和单元测试

    <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>8.0.11</version>
            </dependency>
            <dependency>
                <groupId>cn.quellanan</groupId>
                <artifactId>myself-mybatis</artifactId>
                <version>1.0.0</version>
            </dependency>
    
            <dependency>
                <groupId>junit</groupId>
                <artifactId>junit</artifactId>
                <version>4.10</version>
            </dependency>
    

    然后我们写一个测试类
    1、获取SqlMapperConfig.xml的文件流
    2、获取Sqlsession
    3、执行查找操作

    @org.junit.Test
        public void test() throws Exception{
            InputStream inputStream= Resources.getResources("SqlMapperConfig.xml");
            SqlSession sqlSession = new SqlSessionFactoryBuilder().build(inputStream).openSqlSession();
            List<User> list = sqlSession.selectList("cn.quellanan.dao.UserDao.selectAll");
    
            for (User parm : list) {
                System.out.println(parm.toString());
            }
            System.out.println();
    
            User user=new User();
            user.setUsername("张三");
            List<User> list1 = sqlSession.selectList("cn.quellanan.dao.UserDao.selectByName", user);
            for (User user1 : list1) {
                System.out.println(user1);
            }
    
        }
    
    在这里插入图片描述

    可以看到已经可以了,看来我们自定义的持久层框架生效啦。


    在这里插入图片描述

    优化

    但是不要高兴的太早哈哈,我们看上面的测试方法,是不是感觉和平时用的不一样,每次都都写死statementId ,这样不太友好,所以我们接下来来点骚操作,通用mapper 配置。
    我们在SqlSession中增加一个getMapper方法,接收的参数是一个类。我们通过这个类就可以知道statementId .

    /**
         * 使用代理模式来创建接口的代理对象
         * @param mapperClass
         * @param <T>
         * @return
         */
        public <T> T getMapper(Class<T> mapperClass);
    

    具体实现就是利用JDK 的动态代理机制。
    1、通过Proxy.newProxyInstance() 获取一个代理对象
    2、返回代理对象
    那代理对象执行了哪些操作呢?
    创建代理对象的时候,会实现一个InvocationHandler接口,重写invoke() 方法,让所有走这个代理的方法都会执行这个invoke() 方法。那这个方法做了什么操作?
    这个方法就是通过传入的类对象,获取到对象的类名和方法名。用来生成statementid 。所以我们在mapper.xml 配置文件中的namespace 就需要制定为类路径,以及id 为方法名。
    实现方法:

    @Override
        public <T> T getMapper(Class<T> mapperClass) {
    
            Object proxyInstance = Proxy.newProxyInstance(DefaultSqlSeeion.class.getClassLoader(), new Class[]{mapperClass}, new InvocationHandler() {
                @Override
                public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    
                    //获取到方法名
                    String name = method.getName();
                    //类型
                    String className = method.getDeclaringClass().getName();
                    String statementid=className+"."+name;
    
                    return selectList(statementid,args);
                }
            });
    
    
            return (T) proxyInstance;
        }
    

    我们写一个UserDao

    public interface UserDao {
        List<User> selectAll();
    
        List<User> selectByName(User user);
    }
    

    这个是不是我们熟悉的味道哈哈,就是mapper层的接口。
    然后我们在mapper.xml 中指定namespace 和id


    在这里插入图片描述

    接下来我们在写一个测试方法

    @org.junit.Test
        public void test2() throws Exception{
            InputStream inputStream= Resources.getResources("SqlMapperConfig.xml");
            SqlSession sqlSession = new SqlSessionFactoryBuilder().build(inputStream).openSqlSession();
    
            UserDao mapper = sqlSession.getMapper(UserDao.class);
            List<User> users = mapper.selectAll();
            for (User user1 : users) {
                System.out.println(user1);
            }
    
            User user=new User();
            user.setUsername("张三");
            List<User> users1 = mapper.selectByName(user);
            for (User user1 : users1) {
                System.out.println(user1);
            }
    
        }
    
    在这里插入图片描述

    番外

    自定义的持久层框架,我们就写完了。这个实际上就是mybatis 的雏形,我们通过自己手动写一个持久层框架,然后在来看mybatis 的源码,就会清晰很多。下面这些类名在mybatis 中都有体现。


    在这里插入图片描述

    这里抛砖引玉,祝君阅读源码愉快。
    觉得有用的兄弟们记得收藏啊。

    厚颜无耻的求波点赞!!!

    在这里插入图片描述

    本文由博客一文多发平台 OpenWrite 发布!

    相关文章

      网友评论

          本文标题:天哪!手动编写mybatis雏形竟然这么简单

          本文链接:https://www.haomeiwen.com/subject/ijaxtktx.html