手撸Mybatis源码-基础版

放假闲来无事,重新看了一遍Mybatis,有感而发,记录下来。

What is Mybatis?

按照官方文档的说法:

MyBatis is a first class persistence framework with support for custom SQL, stored procedures and advanced mappings. MyBatis eliminates almost all of the JDBC code and manual setting of parameters and retrieval of results. MyBatis can use simple XML or Annotations for configuration and map primitives, Map interfaces and Java POJOs (Plain Old Java Objects) to database records.

请说人话:Mybatis是一款持久层框架,支持自定义SQL、存储过程、和高级的映射关系等等……简单来说,它将我们从原始的jdbc操作底层数据库的繁琐操作当中解放出来,使用注解或者xml配置文件的方式形成 实体-表-sql语句的一系列有序对应,通过java运行期间动态的解析sql,操作底层的jdbc工具来实现数据的CRUD。
先看一张原理草图:
在这里插入图片描述

  1. 程序启动,输入流读取配置文件,解析xml节点,使用Confirgation对象存储数据库连接信息,并且持有所有方法语句映射集合,类型为Map对象。Key:ClassName+MethodName. Value:SqlMapper. 每一个SqlMapper代表一个映射方法,保存有动态sql语句。
  2. 创建session工厂,SqlSessionFactory。其默认实现类DefaultSqlSessionFactory,将配置对象存进对象属性。
  3. 使用工程返回一个会话SqlSession,其默认实现类DefaultSqlSession。SqlSession持有SqlMappers。
  4. 使用jdk动态代理代理我们的持久层接口,SqlSession负责生成代理类,根据反射获取运行时方法名和类名get最终的sqlMapper,将参数封装成Map,解析Mapper中的xml的spel语句,替换参数。
  5. 代理类代理接口方法,使用Excuter执行器执行具体的sql,操作jdbc创建连接、执行、控制事务、封装结果、关闭连接。
  6. 返回我们要的结果。

在初学Mybatis的时候,我记得是从最原始的配置开始操作,以至于到后面的mybatis-spring到现在的mybatis-springboot-starter和mybatis-plus等越来越多的与spring框架集成的优秀mybatis工具,发展不可谓不迅速。不过万变不离其宗,了解了其原理,不仅有利于更快理解新技术,甚至于你自己写一个出来也不是不可能。So,以下我们通过一个简单的demo,封装一个基于原生java的查询版本的mybatis框架。

冲

搭建简单maven项目,导入dom4j、mysql驱动等工具,开撸

pom.xml

 <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.8.0-beta2</version>
        </dependency>

        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-simple</artifactId>
            <version>1.8.0-beta2</version>
        </dependency>
        <!-- 数据库驱动包-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.47</version>
        </dependency>
        <!-- 代码自动生成-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.16.10</version>
        </dependency>
        <!--解析xml -->
        <dependency>
            <groupId>dom4j</groupId>
            <artifactId>dom4j</artifactId>
            <version>1.6.1</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/jaxen/jaxen xpath解析的时候需要引入 jaxen包 否则会报错 java.lang.NoClassDefFoundError: org/jaxen/JaxenException-->
        <dependency>
            <groupId>jaxen</groupId>
            <artifactId>jaxen</artifactId>
            <version>1.1.6</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/com.thoughtworks.xstream/xstream 支持xml转bean -->
        <dependency>
            <groupId>com.thoughtworks.xstream</groupId>
            <artifactId>xstream</artifactId>
            <version>1.4.11.1</version>
        </dependency>

在此之前,我们需要自己实现一个xml解析的工具类和读取路径下的静态文件的工具类。

package com.xjr.config;

import java.io.InputStream;

/**
 * @program: YourBatis
 * @description: Resource资源类
 * @author: xjr
 * @create: 2020-04-04 00:11
 **/

public class Resources {
    public static InputStream getXmlConfig(String filePath){
        return Resources.class.getClassLoader().getResourceAsStream(filePath);
    }
}

package com.xjr.util;
import com.xjr.annotations.Select;
import com.xjr.config.Resources;
import com.xjr.config.SqlMapper;
import com.xjr.config.confiruation;
import lombok.extern.slf4j.Slf4j;
import org.dom4j.Attribute;
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * @program: YourBatis
 * @description: xml配置文件解析工具
 * @author: xjr
 * @create: 2020-04-04 00:35
 **/
@Slf4j
public final class XmlUtils {
    public static confiruation parseToConfirguration(InputStream inputStream){
        try {
            //定义封装连接信息的配置对象(mybatis的配置对象)
            confiruation cfg = new confiruation();

            //1.获取SAXReader对象
            SAXReader reader = new SAXReader();
            //2.根据字节输入流获取Document对象
            Document document = reader.read(inputStream);
            //3.获取根节点
            Element root = document.getRootElement();
            //4.使用xpath中选择指定节点的方式,获取所有property节点
            List<Element> propertyElements = root.selectNodes("//property");
            //5.遍历节点
            for (Element propertyElement : propertyElements) {
                //判断节点是连接数据库的哪部分信息
                //取出name属性的值
                String name = propertyElement.attributeValue("name");
                if ("driver".equals(name)) {
                    //表示驱动
                    //获取property标签value属性的值
                    String driver = propertyElement.attributeValue("value");
                    cfg.setDriver(driver);
                }
                if ("url".equals(name)) {
                    //表示连接字符串
                    //获取property标签value属性的值
                    String url = propertyElement.attributeValue("value");
                    cfg.setUrl(url);
                }
                if ("username".equals(name)) {
                    //表示用户名
                    //获取property标签value属性的值
                    String username = propertyElement.attributeValue("value");
                    cfg.setUsername(username);
                }
                if ("password".equals(name)) {
                    //表示密码
                    //获取property标签value属性的值
                    String password = propertyElement.attributeValue("value");
                    cfg.setPassword(password);
                }
            }
            //取出mappers中的所有mapper标签,判断他们使用了resource还是class属性
            List<Element> mapperElements = root.selectNodes("//mappers/mapper");
            //遍历集合
            for (Element mapperElement : mapperElements) {
                //判断mapperElement使用的是哪个属性
                Attribute attribute = mapperElement.attribute("resource");
                if (attribute != null) {
                    log.info("正在读取xml语句信息");
                    //表示有resource属性,用的是XML
                    //取出属性的值
                    String mapperPath = attribute.getValue();
                    //把映射配置文件的内容获取出来,封装成一个map
                    Map<String, SqlMapper> sqlMappers = loadMapperConfiguration(mapperPath);
                    //给configuration中的sqlMappers赋值
                    cfg.getSqlMappers().putAll(sqlMappers);
                } else {
                    log.info("正在读取注解 sql语句");
                    //表示没有resource属性,用的是注解
                    //获取class属性的值
                    String maperClassPath = mapperElement.attributeValue("class");
                    //根据daoClassPath获取封装的必要信息
                    Map<String, SqlMapper> sqlMappers = loadMapperAnnotation(maperClassPath);
                    //给configuration中的sqlMappers赋值,这里的set方法是putAll,追加进去,不要覆盖。
                    cfg.getSqlMappers().putAll(sqlMappers);
                }
            }
            //返回Configuration
            return cfg;
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            try {
                inputStream.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 根据传入的参数,解析XML,并且封装到Map中
     *
     * @param mapperPath 映射配置文件的位置
     * @return map中包含了获取的唯一标识(key是由Mapper接口的全限定类名和方法名组成)
     * 以及执行所需的必要信息(value是一个Mapper对象,里面存放的是执行的SQL语句和要封装的实体类全限定类名)
     */
    private static Map<String, SqlMapper> loadMapperConfiguration(String mapperPath) throws IOException {
        InputStream in = null;
        try {
            //定义返回值对象
            Map<String, SqlMapper> sqlMappers = new HashMap<>();
            //1.根据路径获取字节输入流
            in = Resources.getXmlConfig(mapperPath);
            //2.根据字节输入流获取Document对象
            SAXReader reader = new SAXReader();
            Document document = reader.read(in);
            //3.获取根节点
            Element root = document.getRootElement();
            //4.获取根节点的namespace属性取值
            String namespace = root.attributeValue("namespace");
            //5.获取所有的select节点
            List<Element> selectElements = root.selectNodes("//select");
            //6.遍历select节点集合
            for (Element selectElement : selectElements) {
                //取出id属性的值      组成map中key的部分
                String id = selectElement.attributeValue("id");
                //取出resultType属性的值  组成map中value的部分
                String resultType = selectElement.attributeValue("resultType");
                //取出文本内容            组成map中value的部分
                String queryString = selectElement.getText();
                //创建Key
                String key = namespace + "." + id;
                //创建Value
                SqlMapper sqlMapper = new SqlMapper();
                sqlMapper.setQueryString(queryString);
                sqlMapper.setResultType(resultType);
                //把key和value存入sqlMappers中
                sqlMappers.put(key, sqlMapper);
            }
            return sqlMappers;
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            in.close();
        }
    }

    /**
     * 根据传入的参数,得到dao中所有被select注解标注的方法。
     * 根据方法名称和类名,以及方法上注解value属性的值,组成Mapper的必要信息
     *
     * @param mapperClassPath
     * @return
     */
    private static Map<String, SqlMapper> loadMapperAnnotation(String mapperClassPath) throws Exception {
        //定义返回值对象
        Map<String, SqlMapper> sqlMappers = new HashMap<>();
        //1.得到dao接口的字节码对象
        Class daoClass = Class.forName(mapperClassPath);
        //2.得到dao接口中的方法数组
        Method[] methods = daoClass.getMethods();
        //3.遍历Method数组
        for (Method method : methods) {
            //取出每一个方法,判断是否有select注解
            boolean isAnnotated = method.isAnnotationPresent(Select.class);
            if (isAnnotated) {
                //创建Mapper对象
                SqlMapper sqlMapper = new SqlMapper();
                //取出注解的value属性值
                Select selectAnno = method.getAnnotation(Select.class);
                String queryString = selectAnno.value();
                sqlMapper.setQueryString(queryString);
                //获取当前方法的返回值,还要求必须带有泛型信息,取出具体要封装的对象
                Type type = method.getGenericReturnType();//List<User>
                //判断type是不是参数化的类型
                if (type instanceof ParameterizedType) {
                    //强转
                    ParameterizedType ptype = (ParameterizedType) type;
                    //得到参数化类型中的实际类型参数
                    Type[] types = ptype.getActualTypeArguments();
                    //取出第一个,User
                    Class domainClass = (Class) types[0];
                    //获取domainClass的类名
                    String resultType = domainClass.getName();
                    //给Mapper赋值
                    sqlMapper.setResultType(resultType);
                }
                //组装key的信息
                //获取方法的名称
                String methodName = method.getName();
                String className = method.getDeclaringClass().getName();
                String key = className + "." + methodName;
                //给map赋值,
                sqlMappers.put(key, sqlMapper);
            }
        }
        return sqlMappers;
    }
}

根据原理图,我们先创建一个SqlSessionFactoryBuilder,实现build方法返回一个工厂类。

package com.xjr.config;

import com.xjr.util.XmlUtils;

import java.io.InputStream;

/**
 * @program: YourBatis
 * @description: 读取输入流创建SqlSessionFactory
 * @author: xjr
 * @create: 2020-04-04 00:16
 **/

public class SqlSessionFactoryBuilder {
    public SqlSessionFactory build(InputStream stream){
        return new DefaultSqlSessionFactory(XmlUtils.parseToConfirguration(stream));
    }
}

保存xml配置信息的配置类:

package com.xjr.config;

import lombok.Data;
import lombok.experimental.Accessors;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

import java.util.HashMap;
import java.util.Map;

/**
 * @program: YourBatis
 * @description: 保存xml重要配置信息
 * @author: xjr
 * @create: 2020-04-04 00:33
 **/
@Data
@Accessors(chain = true)
public class confiruation {
    //数据库驱动
    private String driver;

    //连接地址
    private String url;

    //用户名
    private String username;

    //密码
    private String password;

    //维护一个Map引用,保存所有sqlMapper的对象(初始化时完成)
    private Map<String,SqlMapper> sqlMappers=new HashMap();

    public confiruation(){

    }

}

SqlSessionFactory

package com.xjr.config;

import com.xjr.execute.SqlSession;

/**
 * @program: YourBatis
 * @description: sqlsession工厂类
 * @author: xjr
 * @create: 2020-04-04 00:17
 **/

public interface SqlSessionFactory {
    //可以看到,我们这边只提供一个打开方法
    SqlSession openSession();

}

package com.xjr.config;

import com.xjr.execute.DefaultSqlSeession;
import com.xjr.execute.SqlSession;
import com.xjr.util.DataSourceUtil;

/**
 * @program: YourBatis
 * @description: 默认工厂实现类
 * @author: xjr
 * @create: 2020-04-04 00:32
 **/
public class DefaultSqlSessionFactory implements SqlSessionFactory {
    //配置类对象引用
    private confiruation confiruation;
    @Override
    public SqlSession openSession() {
        return new DefaultSqlSeession(this.confiruation.getSqlMappers(), DataSourceUtil.getConction(this.confiruation));
    }

    //通过构造重载,注入配置类对象
    public DefaultSqlSessionFactory(confiruation confiruation){
        this.confiruation=confiruation;
    }

}

获取数据库连接的工具类:

package com.xjr.util;

import com.xjr.config.confiruation;
import lombok.extern.slf4j.Slf4j;

import java.sql.Connection;
import java.sql.DriverManager;

/**
 * @program: YourBatis
 * @description: 数据源工具类
 * @author: xjr
 * @create: 2020-04-04 00:52
 **/
@Slf4j
public final class DataSourceUtil {

    //使用threadlocal保证并发场景下保证一个线程获取一个连接
    private static final ThreadLocal<Connection> CONNECTION_THREAD_LOCAL=new ThreadLocal<>();



    public static  Connection getConction(confiruation cfg)  {
        if (CONNECTION_THREAD_LOCAL.get()!=null){
            return CONNECTION_THREAD_LOCAL.get();
        }
        try {
            Class.forName("com.mysql.jdbc.Driver");
            CONNECTION_THREAD_LOCAL.set(DriverManager.getConnection(cfg.getUrl(),cfg.getUsername(),cfg.getPassword()));
        } catch (Exception e) {
           log.info("数据库连接建立失败,错误信息:{}",e.getMessage());
        }
        return CONNECTION_THREAD_LOCAL.get();
    }
}

使用threadLocal的原因是方便在运行当中多个实例共享connection,并且可以避免在高并发场景的读写问题。

SqlSession

package com.xjr.execute;

/**
 * @program: YourBatis
 * @description: 数据库连接会话
 * @author: xjr
 * @create: 2020-04-04 00:30
 **/
public interface SqlSession {
    //获得代理
    <T> T getMapper(Class<T> clazz);

   //关闭连接
    void close();
}

package com.xjr.execute;

import com.xjr.config.SqlMapper;
import com.xjr.proxy.MapperProxy;

import java.lang.reflect.Proxy;
import java.sql.Connection;
import java.util.HashMap;
import java.util.Map;

/**
 * @program: YourBatis
 * @description: sqlsession默认实现类
 * @author: xjr
 * @create: 2020-04-04 00:31
 **/

public class DefaultSqlSeession implements SqlSession {
    //持有保存所有sqlMappers 的map对象引用
    private Map<String, SqlMapper> sqlMappers=new HashMap<>();

    //一个数据库连接信息
    private Connection connection;

    public DefaultSqlSeession(Map<String, SqlMapper> sqlMappers, Connection connection) {
        this.sqlMappers = sqlMappers;
        this.connection = connection;
    }

    @Override
    public <T> T getMapper(Class<T> clazz) {
        return (T) Proxy.newProxyInstance(clazz.getClassLoader(),new Class[]{clazz},new MapperProxy(sqlMappers));
    }

    @Override
    public void close() {
        if (connection != null) {
            try {
                connection.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }


}

执行器:

package com.xjr.execute;

import com.xjr.config.SqlMapper;
import com.xjr.util.DataSourceUtil;

import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.util.ArrayList;
import java.util.List;

/**
 * @program: YourBatis
 * @description: 执行sql并且封装结果
 * @author: xjr
 * @create: 2020-04-04 01:30
 **/

public class Executor {

    public <E> List<E> selectList(SqlMapper mapper){
        PreparedStatement pstm = null;
        ResultSet rs = null;
        try {
            //1.取出mapper中的数据
            String queryString = mapper.getQueryString();
            String resultType = mapper.getResultType();
            Class domainClass = Class.forName(resultType);
            //2.获取PreparedStatement对象
            pstm = DataSourceUtil.getConction(null).prepareStatement(queryString);
            //3.执行SQL语句,获取结果集
            rs = pstm.executeQuery();
            List<E> list = new ArrayList<E>(rs.getRow());
            while (rs.next()) {
                //实例化要封装的实体类对象
                E obj = (E) domainClass.newInstance();

                //取出结果集的元信息:ResultSetMetaData
                ResultSetMetaData rsmd = rs.getMetaData();
                //取出总列数
                int columnCount = rsmd.getColumnCount();
                //遍历总列数
                for (int i = 1; i <= columnCount; i++) {
                    //获取每列的名称,列名的序号是从1开始的
                    String columnName = rsmd.getColumnName(i);
                    //根据得到列名,获取每列的值
                    Object columnValue = rs.getObject(columnName);
                    //给obj赋值:使用Java内省机制(借助PropertyDescriptor实现属性的封装)
                    //要求:实体类的属性和数据库表的列名保持一种
                    PropertyDescriptor pd = new PropertyDescriptor(columnName, domainClass);
                    //获取它的写入方法
                    Method writeMethod = pd.getWriteMethod();
                    //把获取的列的值,给对象赋值
                    writeMethod.invoke(obj, columnValue);
                }
                //把赋好值的对象加入到集合中
                list.add(obj);
            }
            return list;
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            release(pstm, rs);
        }
    }

    private void release(PreparedStatement pstm, ResultSet rs) {
        if (rs != null) {
            try {
                rs.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        if (pstm != null) {
            try {
                pstm.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

}

记得在封装结果集的时候,arrayList需要指定初始值,避免结果集过大导致arraylist重复扩容,影响效率。

SqlMapper:

package com.xjr.config;

import lombok.Data;

/**
 * @program: YourBatis
 * @description: 接口方法的包装类
 * @author: xjr
 * @create: 2020-04-04 00:45
 **/
@Data
public class SqlMapper {

    //xml语句中保存的crud 语句
    private String queryString;

    //语句返回类型
    private String resultType;
}

使用jdk动态代理:

package com.xjr.proxy;

import com.xjr.config.SqlMapper;
import com.xjr.execute.Executor;
import lombok.extern.slf4j.Slf4j;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.Map;

/**
 * @program: YourBatis
 * @description: dao接口动态代理类
 * @author: xjr
 * @create: 2020-04-04 01:26
 **/
@Slf4j
public class MapperProxy implements InvocationHandler {

    private Map<String, SqlMapper> sqlMappers;

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        String methodName=method.getName();
        String className=method.getDeclaringClass().getName();
        SqlMapper sqlMapper=sqlMappers.get(className+"."+methodName);
        return new Executor().selectList(sqlMapper);
    }

    public MapperProxy(Map<String,SqlMapper> sqlMappers){
        this.sqlMappers=sqlMappers;
    }
}

基本工具搭建完成,为了达到跟mybatis接近的效果,再提供一个查询注解:

package com.xjr.annotations;

import java.lang.annotation.*;

/**
 * @program: YourBatis
 * @description: 查询注解
 * @author: xjr
 * @create: 2020-04-04 01:13
 **/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Select {
    String value();
}

数据库创建一张用户表,表中只有三个字段:userid、name、age。(表中有二十万条数据,为了测试性能,也就拼了)

逆向工程得到实体、xml、mapper文件.

package com.xjr.entity;

import lombok.Data;

import java.io.Serializable;

/**
 * @program: YourBatis
 * @description: 用户表
 * @author: xjr
 * @create: 2020-04-04 01:42
 **/
@Data
public class User implements Serializable {
    private Integer userid;

    private String name;

    private Integer age;
}

package com.xjr.mapper;

import com.xjr.annotations.Select;
import com.xjr.entity.User;

import java.util.List;

/**
 * @program: YourBatis
 * @description: 用户持久层接口
 * @author: xjr
 * @create: 2020-04-04 01:45
 **/
public interface UserDao {
    @Select("select * from mybatis_user")
    List<User> selectList();
}

<?xml version="1.0" encoding="UTF-8" ?>
<mapper namespace="com.xjr.mapper.UserDao">
    <select id="selectList" resultType="com.xjr.entity.User">
        select userid from mybatis_user
    </select>
</mapper>

为了更直观的体现两种方式,xml查询单个字段,注解查询所有字段。

最后,按照你使用mybatis一样,写一个简单的mybatis-config.xml文件。

<?xml version="1.0" encoding="UTF-8" ?>
<configuration>
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://localhost:3306/sophi"/>
                <property name="username" value="xxx"/>
                <property name="password" value="xxx"/>
            </dataSource>
        </environment>
    </environments>
    <!--注解形式-->
    <mappers>
<!--        <mapper class="com.xjr.mapper.UserDao"/>-->
        <mapper resource="mapping/UserMapper.xml"/>
    </mappers>

</configuration>

让我们试一下YourBatis是不是可以完成Mybatis的一个查询功能。

package com.xjr;

import com.xjr.config.Resources;
import com.xjr.config.SqlSessionFactory;
import com.xjr.config.SqlSessionFactoryBuilder;
import com.xjr.entity.User;
import com.xjr.execute.SqlSession;
import com.xjr.mapper.UserDao;
import lombok.Cleanup;
import lombok.extern.slf4j.Slf4j;

import java.io.*;
import java.time.Duration;
import java.time.LocalTime;
import java.util.List;
@Slf4j
public class Main {

    public static void main(String[] args) throws Exception {
        //1.读取配置文件,将配置文件转换为输入流
         InputStream in=Resources.getXmlConfig("mybatis-config.xml");
        //2.创建SqlSessionFactory工厂
        SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
        //返回DefaultSqlSessionFactory对象,利用XMLConfigBuilder类从输入流中读取配置信息。
        // 封装成Configuration对象传入DefaultSqlSessionFactory的构造方法
        SqlSessionFactory factory = builder.build(in);
        //3.使用工厂(DefaultSqlSessionFactory)生产SqlSession对象,
        @Cleanup SqlSession session = factory.openSession();
        //4.使用SqlSession创建Dao接口的代理对象
        UserDao dao = session.getMapper(UserDao.class);
        LocalTime start=LocalTime.now();
        //5.使用代理对象执行方法
        List<User> users = dao.selectList();
        LocalTime end=LocalTime.now();
        log.info("查询耗时:{}毫秒", Duration.between(start,end).toMillis());
        log.info("总条数:{}", users.size());
        users.forEach(d->log.info("查询结果:{}",d));

    }
    }



控制台输出:

在这里插入图片描述
在这里插入图片描述
可以看到返回了二十万条只有单个字段的数据。

再试一下@select注解是否也可以工作:
在这里插入图片描述
在这里插入图片描述

it seems that everything goes well.

大功告成。 笔者在下次会带来手动实现mybatis-spring 与手撸springboot-mybatis-starter的集成教程,觉得可以的话请点个赞哈。

猜你喜欢

转载自blog.csdn.net/qq_35323137/article/details/105317365