手动实现 MyBatis 底层机制

  |   0 评论   |   0 浏览

手动实现 MyBatis 底层机制

https://www.bilibili.com/video/BV1h64y1q7az?share_source=copy_web

1.MyBatis 整体架构分析

1.Mybatis 核心框架示意图

image.png

  1. mybatis 的核心配置文件 mybatis-config.xml: 进行全局配置,全局只能有一个这样的配置文件 XxxMapper.xml 配置多个 SQL,可以有多个 XxxMappe.xml 配置文件
  2. 通过 mybatis-config.xml 配置文件得到 SqlSessionFactory
  3. 通过 SqlSessionFactory 得到 SqlSession,用 SqlSession 就可以操作数据了
  4. SqlSession 底层是 Executor(执行器), 有2个重要的实现类, 有很多方法

image-20220626214312956

​ 5.MappedStatement 是通过 XxxMapper.xml 中定义, 生成的 statement 对象

​ 6.参数输入执行并输出结果集, 无需手动判断参数类型和参数下标位置, 且自动将结果集 映射为 Java 对象

2.搭建MyBatis底层机制开发环境

1、创建 Maven 项目 llp-mybatis

2.修改pom.xml 引入相关依赖

<!--定义编译器 / source / target 版本即可-->
<properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
    <java.version>1.8</java.version>
</properties>
<!--引入必要的依赖-->
<dependencies>
    <!--引入dom4j-->
    <dependency>
        <groupId>dom4j</groupId>
        <artifactId>dom4j</artifactId>
        <version>1.6.1</version>
    </dependency>
    <!--引入mysql依赖-->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>5.1.49</version>
    </dependency>
    <!--lombok-简化entity/javabean/pojo开发 -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.4</version>
    </dependency>
    <!--junit依赖-->
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.12</version>
    </dependency>
</dependencies>

3、创建数据库和表

CREATE DATABASE `llp_mybatis`;

USE `llp_mybatis`;

CREATE TABLE `monster` (
	`id` INT NOT NULL AUTO_INCREMENT,
	`age` INT NOT NULL,
	`birthday` DATE DEFAULT NULL,
	`email` VARCHAR ( 255 ) NOT NULL,
	`gender` TINYINT NOT NULL,
	`name` VARCHAR ( 255 ) NOT NULL,
	`salary` DOUBLE NOT NULL,
	PRIMARY KEY ( `id` ) 
) CHARSET = utf8; 

INSERT INTO `monster`
VALUES
	(
		NULL,
		200,
		'2000-11-11',
		'nmw@sohu.com',
		1,
	'牛魔王',
	8888.88)

4、到此: 项目开发环境搭建 OK

3.MyBatis的设计思路

1.示意图

image-20220626221932457

4.自己实现 MyBatis 底层机制 【封装 Sqlsession 到执行器 + Mapper 接口和 Mapper.xml + MapperBean + 动态代理代理 Mapper 的方法

1.实现任务阶段 1- 完成读取配置文件,得到数据库连

1.说明: 通过配置文件,获取数据库连接

2.分析+代码实现

● 分析示意图

image-20220626222052112

1.resource目录下创建llp_mybatis.xml

<?xml version="1.0" encoding="UTF-8" ?>
<database>
    <property name="driverName" value="com.mysql.jdbc.Driver"></property>
    <property name="url" value="jdbc:mysql://127.0.0.1:3306/llp_mybatis?useSSL=true&amp;useUnicode=true&amp;characterEncoding=UTF-8"></property>
    <property name="userName" value="root"></property>
    <property name="password" value="root"></property>
</database>

2.创建src\main\java\com\llp\llpmybatis\sqlsession\LLPConfiguration.java

/**
 * 读取xml文件,建立连接
 */
public class LLPConfiguration {

    //属性-类的加载器
    private static ClassLoader loader = ClassLoader.getSystemClassLoader();

    //读取xml文件信息并处理
    public Connection build(String resource) {
        Connection connection = null;
        try {
            //1.加载配置文件 llP_mybatis.xml 获取到对应的InputStream
            InputStream stream = loader.getResourceAsStream(resource);
            //2.解析llp_mybatis.xml =>dom4j
            SAXReader saxReader = new SAXReader();
            Document document = saxReader.read(stream);
            Element rootElement = document.getRootElement();
            System.out.println("root=" + rootElement);
            //3.解析root元素,返回Connection
            connection = evalDataSource(rootElement);

        } catch (Exception e) {
            e.printStackTrace();
        }
        return connection;
    }

    /**
     * 这个方法会解析我们的llp_mybatis.xml信息,并返回连接
     *
     * @param node
     * @return
     */
    private Connection evalDataSource(Element node) throws ClassNotFoundException, SQLException {
        if (!"database".equals(node.getName())) {
            throw new RuntimeException("root 节点应该是<database>");
        }
        //连接DB的必要参数
        String driverName = null;
        String url = null;
        String userName = null;
        String password = null;
        List propertys = node.elements("property");
        for (Object property : propertys) {
            Element element = (Element) property;
            String name = element.attributeValue("name");
            String value = element.attributeValue("value");
            if (name == null || value == null) {
                throw new RuntimeException("property 节点没有设置name或者value属性");
            }
            switch (name) {
                case "driverName":
                    driverName = value;
                    break;
                case "url":
                    url = value;
                    break;
                case "userName":
                    userName = value;
                    break;
                case "password":
                    password = value;
                    break;
            }

        }
        Class.forName(driverName);//建议写上
        Connection connection = DriverManager.getConnection(url, userName, password);
        return connection;
    }

}

3.完成测试

public class LLPMyBatisTest {

    @Test
    public void build(){
        LLPConfiguration llpConfiguration = new LLPConfiguration();
        Connection connection = llpConfiguration.build("llp_mybatis.xml");
        System.out.println(connection);
    }
}

image-20220626230716990

2.实现任务阶段 2- 编写执行器,输入 SQL 语句,完成操作

1.说明:通过实现执行器机制,对数据表操作

● 分析示意图

image-20220627231746943

说明:我们把对数据库的操作,会封装到一套 Executor 机制中,程序具有更好的扩展性, 结构更加清晰

image-20220627222353874

2.分析+代码实现

1.创建Monster.java

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Monster {

    private Integer id;
    private Integer age;
    private String name;
    private String email;
    private Date birthday;
    private double salary;
    private Integer gender;

}

2.创建Executor.java接口

public interface Executor {

    //泛型方法
    public <T> T query(String sql, Object... parameter);
}

3.创建LLPExecutor.java实现类

public class LLPExecutor implements Executor {

    @Override
    public <T> T query(String sql, Object... parameter) {
        //1.获取连接
        Connection connection = getConnection();
        ResultSet resultSet = null;
        PreparedStatement preparedStatement = null;
        try {
            preparedStatement = connection.prepareStatement(sql);
            for (int i = 0; i < parameter.length; i++) {
                preparedStatement.setString(i + 1, parameter[i].toString());
            }
            resultSet = preparedStatement.executeQuery();
            Monster monster = new Monster();
            while (resultSet.next()) {
                monster.setAge(resultSet.getInt("age"));
                monster.setBirthday(resultSet.getDate("birthday"));
                monster.setEmail(resultSet.getString("email"));
                monster.setGender(resultSet.getInt("gender"));
                monster.setId(resultSet.getInt("id"));
                monster.setSalary(resultSet.getDouble("salary"));
                monster.setName(resultSet.getString("name"));
            }
            return (T) monster;
        } catch (SQLException throwables) {
            throwables.printStackTrace();
        }finally {
            if(connection!=null){
                try {
                    connection.close();
                } catch (SQLException throwables) {
                    throwables.printStackTrace();
                }
            }
            if(resultSet!=null){
                try {
                    resultSet.close();
                } catch (SQLException throwables) {
                    throwables.printStackTrace();
                }
            }
            if(preparedStatement!=null){
                try {
                    preparedStatement.close();
                } catch (SQLException throwables) {
                    throwables.printStackTrace();
                }
            }

        }
        return null;
    }

    private Connection getConnection() {
        LLPConfiguration llpConfiguration = new LLPConfiguration();
        Connection connection = llpConfiguration.build("llp_mybatis.xml");
        return connection;
    }
}

3.完成测试

1.测试方法

@Test
public void query() {
    Executor executor = new LLPExecutor();
    //lombok @Data注解会生成toString方法
    Monster monster =
            executor.query("select * from monster where id=?", 1);
    System.out.println("monster-- " + monster);
}

2.测试结果

image-20220627232033107

3.实现任务阶段 3- 将 Sqlsession 封装到执行器

1.分析+代码实现

● 分析示意图, 先观察原生 MyBatis 的 SqlSession 接口和默认实现

在SqlSession接口中,封装了执行器

通过执行器,完成对数据库的操作,比如selectone

image-20220629223324029

image-20220629223408795

image-20220629223501859

● 完成功能

1.创建LLPSqlSession.java

public class LLPSqlSession {

    //属性
    //执行器
    private Executor executor = new LLPExecutor();

    //配置
    private LLPConfiguration llpConfiguration = new LLPConfiguration();

    //编写方法SelectOne 返回一条记录-对象
    public <T> T selectOne(String statement, Object parameter) {
        return executor.query(statement, parameter);
    }

}

2.测试

@Test
public void selectOne() {
    Monster monster = new LLPSqlSession().selectOne("select * from monster where id=?", 1);
    System.out.println(monster);
}

image-20220629225007897

4.实现任务阶段 4- 开发 Mapper 接口和 Mapper.xml

1.分析+ 代码实现

分析【示意图】

image-20220629225059116

● 代码实现

1.创建src\main\java\com\llp\entity\Monster.java

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Monster {

    private Integer id;
    private Integer age;
    private String name;
    private String email;
    private Date birthday;
    private double salary;
    private Integer gender;

}

2.创建src\main\resources\MonsterMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<mapper namespace="com.llp.mapper.MonsterMapper">
<!--实现配置接口方法-->
<select id="getMonsterById" resultType="com.llp.entity.Monster">
    select * from monster where id = ?
</select>

</mapper>

5.实现任务阶段 5- 开发和 Mapper 接口相映射的 MapperBean

● 分析示意图

image-20220630230017659

● 代码实现

1.创建src\main\java\com\llp\llpmybatis\config\Function.java 对应 Mapper 的方法信息

/**
 * 记录对应的mapper的方法信息
 */
@Data
public class Function {

    //属性
    //sql类型,比如select,inster,update,delete
    private String sqlType;

    //方法名
    private String funcName;

    //要执行的sql语句
    private String sql;

    //返回类型
    private Object resultType;

    //参数类型
    private String parameterType;

}

2.创建src\main\java\com\llp\llpmybatis\config\MapperBean.java, 将 Mapper 的信息,进行封装

/**
 * 将mapper信息,进行封装
 */
@Data
public class MapperBean {

    //接口名
    private String interfaceName;

    //接口下的所以方法-集合
    private List<Function> functions;

}

6.实现任务阶段 6- 在 LLPConfiguration, 读取 XxxMapper.xml,能够创建 MappperBean 对象

1.分析+代码实现

● 分析示例

MapperBean(interfaceName=com.llp.mapper.MonsterMapper, functions=[Function(sqlType=select, funcName=getMonsterById, sql=select * from monster where id = ?, resultType=Monster(id=null, age=null, name=null, email=null, birthday=null, salary=0.0, gender=null), parameterType=null)])

● 代码实现

1.src\main\java\com\llp\llpmybatis\sqlsession\LLPConfiguration.java增加方法

//读取XxxMapper.xml,能够创建MapperBean对象
//path 就是xml的路径+文件名  是从类的加载路径计算的
//说明:如果XxxMapper.xml 文件是防在resources目录下,直接传入xml文件名即可
public MapperBean readMapper(String path) {
    MapperBean mapperBean = new MapperBean();
    try {
        InputStream resourceAsStream = loader.getResourceAsStream(path);
        SAXReader saxReader = new SAXReader();
        Document document = saxReader.read(resourceAsStream);
        /**
         * <?xml version="1.0" encoding="UTF-8" ?>
         * <mapper namespace="com.llp.mapper.MonsterMapper">
         * <!--实现配置接口方法-->
         * <select id="getMonsterById" resultType="com.llp.entity.Monster">
         *     select * from monster where id = ?
         * </select>
         *
         * </mapper>
         */
        Element rootElement = document.getRootElement();
        List<Function> functionList = new ArrayList<>();
        //获取命名空间
        String namespace = rootElement.attributeValue("namespace").trim();
        //接口名
        mapperBean.setInterfaceName(namespace);
        //获取迭代器
        Iterator iterator = rootElement.elementIterator();
        while (iterator.hasNext()) {
            Element element = (Element) iterator.next();
            //select
            String sqlType = element.getName().trim();
            //getMonsterById
            String funcName = element.attributeValue("id").trim();
            //这里得到resultType是全类名
            String resultType = element.attributeValue("resultType").trim();
            //select * from monster where id = ?
            String sql = element.getText().trim();
            Function function = new Function();
            function.setSqlType(sqlType);
            function.setFuncName(funcName);
            function.setSql(sql);
            Class<?> aClass = Class.forName(resultType);
            Object o = aClass.newInstance();
            function.setResultType(o);
            functionList.add(function);
        }
        mapperBean.setFunctions(functionList);
    } catch (Exception e) {
        e.printStackTrace();
    }
    return mapperBean;
}

2.完成测试

@Test
public void readMapper(){
    LLPConfiguration llpConfiguration = new LLPConfiguration();
    MapperBean mapperBean = llpConfiguration.readMapper("MonsterMapper.xml");
    System.out.println(mapperBean);
}

7.实现任务阶段 7- 实现动态代理 Mapper 的方法

1.分析+代码实现

● 分析示意图【看下示意图】

image-20220630230457132

● 代码实现

1.创建src\main\java\com\llp\llpmybatis\sqlsession\LLPMapperProxy.java

/**
 * 动态代理生成Mapper对象,调用LLPExecutor方法
 */
public class LLPMapperProxy implements InvocationHandler {

    //属性
    private LLPSqlSession llpSqlSession;
    private LLPConfiguration llpConfiguration;
    private String mapperFile;

    //构造器

    public LLPMapperProxy(LLPSqlSession llpSqlSession, LLPConfiguration llpConfiguration, Class clazz) {
        this.llpConfiguration = llpConfiguration;
        this.llpSqlSession = llpSqlSession;
        this.mapperFile = clazz.getSimpleName() + ".xml";
    }


    /**
     *
     * @param proxy  表示代理对象
     * @param method 就是通过代理对象调用方法时,的哪个方法 代理对象.run()
     * @param args   : 表示调用 代理对象.run(xx) 传入的参数
     * @return 表示 代理对象.run(xx) 执行后的结果.
     * @throws Throwable
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //读取mapper.xml配置文件封装接口名和function集合
        MapperBean mapperBean = llpConfiguration.readMapper(this.mapperFile);
        //判断是否是xml文件对应的接口
        if (!method.getDeclaringClass().getName().equals(mapperBean.getInterfaceName())) {
            return null;
        }
        //Function记录对应的mapper的方法信息:方法名、sql类型、要执行的sql语句、返回类型等
        List<Function> functions = mapperBean.getFunctions();
        if (functions != null && 0 != functions.size()) {
            for (Function function : functions) {
                //判断动态代理执行的方法名是否和function对象的方法名一致
                if(function.getFuncName().equals(method.getName())){
                    //如果我们当前的function 要执行的sqlType是select
                    //我们就去执行selectOne
                    /**
                     *
                     * 说明:
                     * 1. 如果要执行的方法是select , 就对应执行selectOne
                     * 2. 因为在LLPSqlSession就写了一个 selectOne
                     * 3. 实际上LLPSqlSession 应该对应不同的方法(多个方法)
                     * , 根据不同的匹配情况调用不同方法, 并且还需要进行参数解析处理, 还有比较复杂的字符串处理,拼接sql ,处理返回类型等等工作
                     */
                    if("select".equalsIgnoreCase(function.getSqlType())) {
                        return llpSqlSession.selectOne(function.getSql(),String.valueOf(args[0]));
                    }
                }
            }
        }
        return null;
    }
}

2.修改src\main\java\com\llp\llpmybatis\sqlsession\LLPSqlSession.java

public <T> T getMapper(Class clazz){
    //返回动态代理对象
    return (T) Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{clazz},
            new LLPMapperProxy(this,llpConfiguration,clazz));
}

3.创建LLPSessionFactory.java

public class LLPSessionFactory {

    public LLPSqlSession openSession(){
        return new LLPSqlSession();
    }
}

4.测试方法

@Test
    public void getMapper(){
        LLPSessionFactory llpSessionFactory = new LLPSessionFactory();
        LLPSqlSession llpSqlSession = llpSessionFactory.openSession();
        MonsterMapper mapper = llpSqlSession.getMapper(MonsterMapper.class);
        System.out.println(mapper.getClass());
        /**
         * 执行流程梳理
         * 1.首先 llpSqlSession.getMapper(MonsterMapper.class); 获取到的是代理对象class com.sun.proxy.$Proxy4
         * 2.mapper.getMonsterById(1)执行时,代理对象调用invoke方法
         * 3.  return llpSqlSession.selectOne(function.getSql(),String.valueOf(args[0]));
         * 在动态地理执行目标方法时,会调用LLPExecutor的query方法,从而获取连接执行sql并封装返回结果
         */
        //Monster(id=1, age=200, name=牛魔王, email=nmw@sohu.com, birthday=2000-11-11, salary=8888.88, gender=1)
        System.out.println(mapper.getMonsterById(1));
    }

5.测试结果

image-20220630235641600


标题:手动实现 MyBatis 底层机制
作者:llp
地址:https://blog.llp1110.cn/articles/2022/06/27/1656343396184.html