Skip to content

mmsyLH/lh-mybatis

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

13 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

💡 **封装 Sqlsession 到执行器 + Mapper 接口和 Mapper.xml + MapperBean + 动态代理 **
代理 Mapper 的方法 GitHub地址:https://github.com/1072344372/lh-mybatis
分阶段完成 每个阶段可以github提交记录

MyBatis整体架构分析

一图胜千言

先来看看mybatis原本的执行流程
image.png
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.pngimage.pngimage.png
**5) MappedStatement 是通过 XxxMapper.xml 中定义, 生成的 statement 对象 **
**6) 参数输入执行并输出结果集, 无需手动判断参数类型和参数下标位置, 且自动将结果集 **
映射为 Java 对象

Lh-mybatis的设计思路

image.png

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

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

完成这2部分
image.png

1.2 分析+代码实现

package asia.lhweb.lhmybatis.sqlsession;

import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;

import java.io.InputStream;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;

/**
 * lh配置
 *
 * @author 罗汉
 * @date 2023/09/16
 */
public class LhConfiguration {

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

    /**
     * 读取xml文件信息,并处理
     *
     * @param resource 资源
     * @return {@link Connection}
     */
    public Connection build(String resource){
        Connection connection=null;
        try {
            //加载配置文件lh-mybatis.xml 获取到对应的InputStream
            InputStream stream = loader.getResourceAsStream(resource);

            //解析xml文件 dom4j
            SAXReader saxReader = new SAXReader();

            Document document = saxReader.read(stream);

            //获取lh-mybatis.xml的database标签
            Element rootElement = document.getRootElement();//拿到根元素
            // System.out.println(rootElement);
            //根据rootElement去解析
             connection = evalDataSource(rootElement);
        } catch (DocumentException e) {
            e.printStackTrace();
        }
        return connection;
    }

    /**
     * 解析xml文件返回连接
     *
     * @param root 根元素
     * @return {@link Connection}
     */
    private Connection evalDataSource(Element root){
        if (!"database".equals(root.getName())){
            throw new RuntimeException("root节点不是<database>");
        }

        //用来储存name和value
        Map<String, String> properties = new HashMap<>();
        //遍历root下的节点
        for (Object element : root.elements("property")) {
            Element i=(Element)element;//i就是 对应property节点
            // <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
            String name = i.attributeValue("name");
            String value = i.attributeValue("value");

            //判断是否得到了name和value
            if (name==null||value==null){
                throw new  RuntimeException("property节点没有设置name或者value属性");
            }
            properties.put(name, value);
        }
        String driverClassName = properties.get("driverClassName");
        String url = properties.get("url");
        String username = properties.get("username");
        String password = properties.get("password");
        Connection connection=null;
        try {
            Class.forName(driverClassName);
            connection= DriverManager.getConnection(url, username, password);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return connection;
    }
}
<?xml version="1.0" encoding="UTF-8" ?>
<database>
    <!--配置连接数据库的信息-->
    <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
    <property name="url" value="jdbc:mysql://127.0.0.1:3306/lh_mybatis?useSSL=true&amp;useUnicode=true&amp;characterEncoding=UTF-8"/>
    <property name="username" value="root"/>
    <property name="password" value="root"/>
</database>

1.3 完成测试

注意:测试文件我并没有上传到github
在这里设置的
image.png

image.png

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

2.1 说明:通过实现执行器机制,对数据库操作

直接通过Executor对db进行操作,走的线路1(BaseExecutor)
image.png

2.1 分析+代码实现

image.png

package asia.lhweb.entity;

import lombok.*;

import java.util.Date;

/**
 * 怪物
 *
 * @author 罗汉
 * @date 2023/09/16
 */
// @Getter
// @Setter
// @ToString

@Data//相当于上面全部的注解 不包括构造器
// @AllArgsConstructor
@NoArgsConstructor
@AllArgsConstructor
public class Monster {
    private Integer id;
    private Integer age;
    private Date birthday;
    private String email;
    private Integer gender;
    private String name;
    private Double salary;
}
package asia.lhweb.lhmybatis.sqlsession;

/**
 * 遗嘱执行人
 *
 * @author 罗汉
 * @date 2023/09/16
 */
public interface Executor {
    /**
     * 查询
     *
     * @param statement 声明
     * @param parameter 参数
     * @return {@link T}
     */
    public <T>T query(String statement,Object parameter);
}
package asia.lhweb.lhmybatis.sqlsession;

import asia.lhweb.entity.Monster;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

/**
 * @author :罗汉
 * @date : 2023/9/16
 */
public class LhExecutor implements Executor{
    private LhConfiguration lhConfiguration =new LhConfiguration();
    /**
     * 根据sql查询
     *
     * @param sql 声明
     * @param parameter 参数
     * @return {@link T}
     */
    @Override
    public <T> T query(String sql, Object parameter) {
        //得到一个连接
        Connection connection = getConnection();
        //查询返回的结果集
        ResultSet set=null;
        PreparedStatement pre=null;

        try {
            pre=connection.prepareStatement(sql);
            pre.setString(1, parameter.toString());
            set=pre.executeQuery();
            //把set数据封装到对象-monster
            //做了简化处理
            //认为返回的结果就是一个monster记录
            //完善的写法是一套完善的反射机制
            Monster monster = new Monster();

            //遍历结果集
            while (set.next()){
                monster.setId(set.getInt("id"));
                monster.setName(set.getString("name"));
                monster.setEmail(set.getString("email"));
                monster.setAge(set.getInt("age"));
                monster.setGender(set.getInt("gender"));
                monster.setSalary(set.getDouble("salary"));
                monster.setBirthday(set.getDate("birthday"));
            }
            return (T)monster;
        } catch (SQLException e) {
            e.printStackTrace();
        }finally {
            try {
                if (set!=null) set.close();
                if (pre!=null) pre.close();
                if (connection!=null) connection.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        return null;
    }
    private Connection getConnection(){
        Connection connection = lhConfiguration.build("lh-mybatis.xml");
        return connection;
    }
}

2.1 完成测试

image.png

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

3.1 说明

ctrl+n查找类
image.png
会封装2个重要的属性
一个LhConfiguration和一个LhExecutor
image.png

3.2 分析+代码实现

只有一个文件

package asia.lhweb.lhmybatis.sqlsession;

/**
 * SQL会话
 * LhSqlSession:搭建Configuration(连接)和Executor之间的桥梁
 * 注意!!原生的有很多方法,这里只写一个 体验流程
 * @author 罗汉
 * @date 2023/09/16
 */
public class LhSqlSession {
    //执行器
    private Executor executor=new LhExecutor();

    //配置
    private LhConfiguration lhConfiguration=new LhConfiguration();

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

3.3 完成测试

image.png

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

4.1 说明

开发Mapper接口和Mapper.xml文件

image.png

4.2 分析+代码实现

image.png

package asia.lhweb.mapper;

import asia.lhweb.entity.Monster;

/**
 * @author :罗汉
 * @date : 2023/9/16
 */
public interface MonsterMapper {

    //查询方法
    Monster getMonsterById(Integer id);
}

这里只是模仿过程,所以用"?"占位符

<?xml version="1.0" encoding="UTF-8" ?>
<mapper namespace="asia.lhweb.mapper.MonsterMapper">
    <select id="getMonsterById" resultType="asia.lhweb.entity.Monster">
        select * from monster where id=?
    </select>
</mapper>

4.3 完成测试

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

5.1 说明

image.png

为什么要有这个MapperBean?
将来这个Mapper里面可能有很多方法(虽然目前只写了一个),最终是要用动态代理方法去联系Mapper接口和Mapper.xml。就用这个来记录Mapper里的所有信息。传统方法是通过一个实现类
image.png

5.2 分析+代码实现

package asia.lhweb.lhmybatis.config;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * @author :罗汉
 * @date : 2023/9/16
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Function {
    //属性
    private String sqlType;//sql类型 比如select,insert,update,delete
    private String funcName;//方法名
    private String sql;//sql语句
    private Object resultType;//返回类型
    private String parameterType;//入参类型
}
package asia.lhweb.lhmybatis.config;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.List;

/**
 * 映射器bean
 * MapperBean:将Mapper信息进行封装
 *
 * @author 罗汉
 * @date 2023/09/16
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class MapperBean {
    private String interfaceName;// 接口全路径
    // 接口下的所有方法-集合
    private List<Function> functions;
}

5.3 完成测试

6.实现阶段6-在LhConfiguration读取XXXMapper.xml,能够创建MapperBean的对象

6.1 说明

要做到的效果 读取到以下设置
MapperBean(interfaceName=asia.lhweb.mapper.MonsterMapper, functions=[Function(sqlType=select, funcName=getMonsterById, sql=select * from monster where id=?, resultType=Monster(id=null, age=null, birthday=null, email=null, gender=null, name=null, salary=null), parameterType=null)])

6.2 分析+代码实现

LhConfiguration.java中添加如下方法readMapper()

public MapperBean readMapper(String path){
        MapperBean mapperBean = new MapperBean();
        //获取到xml对应的输入流
        InputStream resourceAsStream = loader.getResourceAsStream(path);
        SAXReader saxReader = new SAXReader();
        try {
            Document document = saxReader.read(resourceAsStream);
            /**
             * <mapper namespace="asia.lhweb.mapper.MonsterMapper">
             *     <select id="getMonsterById" resultType="asia.lhweb.entity.Monster">
             *         select * from monster where id=?
             *     </select>
             * </mapper>
             */
            Element rootElement = document.getRootElement();//得到文件的root元素
            //rootElement=mapper
            //设置mapperBean的InterfaceName 就是接口的全路径
            String namespace = rootElement.attributeValue("namespace").trim();
            mapperBean.setInterfaceName(namespace);
            //得到rootElement的迭代器
            Iterator rootIterator = rootElement.elementIterator();
            //保存接口信息
            ArrayList<Function> functions = new ArrayList<>();
            //开始遍历 生成function
            while (rootIterator.hasNext()) {
                /**
                 * 相当于拿到
                 * <select id="getMonsterById" resultType="asia.lhweb.entity.Monster">
                 *         select * from monster where id=?
                 *     </select>
                 */
                //取出一个子元素
                Element element = (Element) rootIterator.next();//dom4j下的
                // System.out.println("element-------------------:"+element);
                Function function = new Function();
                //设置属性
                String sqlType = element.getName().trim();
                String funName = element.attributeValue("id");
                String resultType = element.attributeValue("resultType");
                String sql = element.getText().trim();

                function.setFuncName(funName);
                function.setSql(sql);
                function.setSqlType(sqlType);
                //反射创建对象
                Class<?> aClass = Class.forName(resultType);
                Object instance = aClass.newInstance();
                function.setResultType(instance);
                functions.add(function);
            }
            mapperBean.setFunctions(functions);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }


        return mapperBean;
    }

6.3 完成测试

image.png

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

7.1 说明

image.png

7.2 分析+代码实现

新建LhMapperProxy.java

package asia.lhweb.lhmybatis.sqlsession;

import asia.lhweb.lhmybatis.config.Function;
import asia.lhweb.lhmybatis.config.MapperBean;

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

/**
 * Lh映射代理
 * 动态代理生成Mapper对象,调用LhExecutor方法
 *
 * @author 罗汉
 * @date 2023/09/18
 */
public class LhMapperProxy implements InvocationHandler {
    // 属性
    private LhSqlSession lhSqlSession;
    private String mapperFile;
    private LhConfiguration lhConfiguration;

    public LhMapperProxy(LhSqlSession lhSqlSession, Class aClass, LhConfiguration lhConfiguration) {
        this.lhSqlSession = lhSqlSession;
        this.mapperFile = aClass.getSimpleName() + ".xml";
        this.lhConfiguration = lhConfiguration;
    }

    /**
     * 调用
     * 当执行接口的动态代理对方方法时,会执行到invoke方法
     *
     * @param proxy  代理
     * @param method 方法
     * @param args   arg游戏
     * @return {@link Object}
     * @throws Throwable throwable
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        MapperBean mapperBean = lhConfiguration.readMapper(this.mapperFile);

        // 判断是否是xml文件对应的接口
        if (!method.getDeclaringClass().getName().equals(mapperBean.getInterfaceName())) {
            return null;
        }
        // 取出mapperBean的functions
        List<Function> functions = mapperBean.getFunctions();

        // 判断当前的mapperBean解析Mapper.xml后有方法
        if (null != functions && functions.size() != 0) {
            for (Function function : functions) {
                // 当前要执行的方法和function。getFuncName()一样
                // 说明我们可以从当前遍历的function对象中,取出一组相应的信息sql,并执行方法
                if (method.getName().equals(function.getFuncName())) {
                    // 如果我们当前的function 要执行的的sqlType是select
                    // 我们就去执行selectOne
                    /**
                     * 1.如果要执行的方法是select,就对应执行selectOne
                     * 2. 因为只是体验流程 这里就做了简化
                     * 3 主要讲解mybatis 生成mapper动态代理对象,调用方法的机制
                     *
                     */
                    if ("select".equals(function.getSqlType())) {
                        return lhSqlSession.selectOne(function.getSql(), String.valueOf(args[0]));
                    }
                }
            }
        }
        return null;
    }
}

在LhSqlSession中添加新方法getMapper()

    /**
     * 1 返回mapper的动态代理对象
     * 2 clazz到时传入的是MonsterMapper.class
     * 3 放回的就是MonsterMapper接口类型的代理对象
     * 4 当执行接口方法时(通过代理对象调用),根据动态代理机制会执行到invoke方法
     * @param clazz clazz
     * @return {@link T}
     */
    public <T> T getMapper(Class<T> clazz) {
        return (T) Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{clazz}
                , new LhMapperProxy(this, clazz, lhConfiguration));
    }

新建LhSessionFactory.java

package asia.lhweb.lhmybatis.sqlsession;

/**
 * Lh session工厂
 * 返回LhSqlSession
 * @author 罗汉
 * @date 2023/09/18
 */
public class LhSessionFactory {
    public static LhSqlSession openSession(){
        return new LhSqlSession();
    }
}

7.3 完成测试

image.png

总结

完善的写法是一套注解和反射机制-jdbc,这里只是体验流程。多debug体验过程就好了

About

手写简易的mybatis

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages