【Mybatis】我抄袭了Mybatis,手写一套MyMybatis框架:编写MyMybatis框架

前面我们编写了一个正常的项目,他需要使用到我们的框架,但是我们还没有开始编写我们的MyMybatis框架,我们现在已经学会了使用mybatis框架,已经学会了使用jdbc连接mysql,并且已经搭好了一个引用MyMybatis框架的正常项目,所以这次我们开始真正的开始编写我们的MyMybatis框架,开始"抄袭"之路。

首先我们要做的准备一个maven项目,名字叫做my-mybatis-core,之后就是在pom文件下面引入以下的jar包

java 复制代码
 <dependencies>
        <!-- mysql 依赖-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.6</version>
        </dependency>


        <!--dom4j 依赖-->
        <dependency>
            <groupId>dom4j</groupId>
            <artifactId>dom4j</artifactId>
            <version>1.6.1</version>
        </dependency>

        <!--xpath 依赖-->
        <dependency>
            <groupId>jaxen</groupId>
            <artifactId>jaxen</artifactId>
            <version>1.1.6</version>
        </dependency>


        <!--druid连接池-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.21</version>
        </dependency>


        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.30</version>
        </dependency>

    </dependencies>

前面我们已经知道了如何使用MyMybatis框架,所以我们从引用我们框架的第一行代码开始入手

第一行代码

第一行代码长这样

java 复制代码
InputStream resourceAsSteam = Resources.getResourceAsStream("myMybatisConfig.xml");

我们再来回顾一下myMybatisConfig.xml文件

java 复制代码
<configuration>

    <!--1.配置数据库信息-->
    <dataSource>
        <property name="driverClassName" value="com.mysql.jdbc.Driver"></property>
        <property name="url" value="jdbc:mysql://masiyi.obmtj0gc1rgs0ho0-mi.oceanbase.aliyuncs.com:3306/test_ob"></property>
        <property name="username" value="rootmsy"></property>
        <property name="password" value="Msy18719255298"></property>
    </dataSource>

    <!--2.引入映射配置文件-->
    <mappers>
        <mapper resource="mapper/UserMapper.xml"></mapper>
        <mapper resource="mapper/UserMapperCopy.xml"></mapper>
    </mappers>


</configuration>

根据第一行代码的内容,我们需要创建一个Resources类,里面有一个getResourceAsStream方法传入一个字符串返回一个InputStream 的方法,类似这样:

java 复制代码
package com.masiyi.io;

import java.io.InputStream;

/**
 * 解析配置文件
 */
public class Resources {

    /**
     * 加载配置文件
     * @param path
     * @return
     */
    public static InputStream getResourceAsStream(String path) {
        return Resources.class.getClassLoader().getResourceAsStream(path);
    }
}

这个方法的作用就是根据xml文件的路径转换为一个输入流,目的是加载配置文件

第二行代码

我们再看看第二行代码:

java 复制代码
Configuration configuration = new ConfigParse().parse(resourceAsSteam);

这一行的代码是解析为一个Configuration对象,首先我们来创建一个Configuration类用来存储数据库的信息:

java 复制代码
package com.masiyi.entity;

import lombok.Data;

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

/**
 * 存放核心配置文件解析出来的内容UserMapper.xml
 */
@Data
public class Configuration {

    // 数据源对象
    private DataSource dataSource;

    //  key:statementId:namespace.id   MappedStatement:封装好的MappedStatement对象
    private Map<String, MappedStatement> mappedStatementMap = new HashMap();

}

里面的DataSource 属性是javax.sql包里面的,而MappedStatement类是我们自定义的类,用于存放mapper.xml解析内容,他是长这样:

java 复制代码
package com.masiyi.entity;

import lombok.Data;

/**
 * 映射配置类:存放mapper.xml解析内容,如UserMapper.xml
 */
@Data
public class MappedStatement {

    // 唯一标识 statementId:namespace.id
    private String statementId;
    // 返回值类型
    private String resultType;
    // 参数值类型
    private String parameterType;
    // sql语句
    private String sql;

}

这个类里面的属性用来解析xml中自定义的属性,一一对应于xml文件中的

java 复制代码
    <select id="findById" resultType="com.masiyi.entity.User" parameterType="java.lang.Integer">
        select * from user where id = #{id}
    </select>

通过这段代码,Configuration类中的dataSource被赋值

最后通过MapperParsemapperParse方法,Configuration类中的mappedStatementMap被赋值,所以最终解析出来的configuration内容如下:

里面包含了数据库的属性和各个sql解析出来的mappedStatementMap

第三行代码

java 复制代码
SimpleSqlSession simpleSqlSession = new SimpleSqlSession(configuration);

这行代码的作用是创建一个sqlSession,用于连接数据库,SimpleSqlSession 类长这样,里面有一个configuration属性,用来存第二步解析出来的configuration,第二个属性是一个执行类,里面放的就是我们之前用jdbc写的代码,只不过多了一个封装返回成一个实体类的步骤罢了。

java 复制代码
package com.masiyi.executor;

import com.masiyi.entity.Configuration;
import com.masiyi.entity.MappedStatement;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

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

/**
 * @Author masiyi
 * @Date 2023/11/10
 * @PackageName:com.masiyi.sqlSession
 * @ClassName: SqlSession
 * @Description: 放操作(查询)的地方
 * @Version 1.0
 */
@Data
public class SimpleSqlSession {
    private Configuration configuration;
    private SimpleExecutor simpleExecutor;

    public SimpleSqlSession(Configuration configuration) {
        this.configuration = configuration;
        this.simpleExecutor = new SimpleExecutor();
    }

    /**
     * 查询列表
     *
     * @param param
     * @param <E>
     * @return
     */
    public <E> List<E> selectList(MappedStatement mappedStatement, Object param) {

        //拿到 MappedStatement 对象,例如
        // <select id="findById" resultType="com.masiyi.entity.User" parameterType="java.lang.Integer">
        //        select * from user where id = #{id}
        //    </select>
        return simpleExecutor.query(configuration, mappedStatement, param);
    }


    public <T> T newProxyClass(Class<T> targetClass) {
        return (T) Proxy.newProxyInstance(SimpleSqlSession.class.getClassLoader(), new Class[]{targetClass}, new InvocationHandler() {

            /**
             *
             * @param proxy 调用该方法的代理实例
             *
             * @param method {@code Method}实例对应于在代理实例上调用的接口方法。{@code Method}对象的声明类将是该方法被声明的接口,
             *                             该接口可能是代理类继承该方法所通过的代理接口的超接口。
             *
             * @param args 一个对象数组,包含在代理实例上的方法调用中传递的参数值,或者如果接口方法不接受参数,则{@code null}。
             *             基本类型的参数被包装在适当的基本包装器类的实例中,例如{@code java.lang。Integer}或{@code java.lang.Boolean}。
             *
             * @return 方法的返回值
             * @throws Throwable
             */
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) {
                //拿到statementId
                String statementId = method.getDeclaringClass().getName() + "." + method.getName();

                MappedStatement mappedStatement = configuration.getMappedStatementMap().get(statementId);

                //todo 添加其他的增删改查方法,渲染传递的参数值
                return selectList(mappedStatement, args == null ? null : args[0]);
            }
        });

    }


    /**
     * 关闭资源
     */
    public void close() {
        simpleExecutor.close();
    }
}

SimpleExecutor类长这样:

java 复制代码
package com.masiyi.executor;

import com.masiyi.entity.Configuration;
import com.masiyi.entity.MappedStatement;
import com.masiyi.util.MyMybatisUtil;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.SneakyThrows;

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

/**
 * @Author masiyi
 * @Date 2023/11/10
 * @PackageName:com.masiyi.executor
 * @ClassName: SimpleExecutor
 * @Description: 执行器,把jdbc里面的代码拿过来
 * Connection conn = null;
 * Statement stmt = null;
 * ResultSet rs = null;
 * @Version 1.0
 */

@Data
@AllArgsConstructor
@NoArgsConstructor
public class SimpleExecutor {

    private Connection conn = null;
    private Statement stmt = null;
    private ResultSet rs = null;


    @SneakyThrows
    public <E> List<E> query(Configuration configuration, MappedStatement mappedStatement, Object param) {
        // 获取数据库连接
        conn = configuration.getDataSource().getConnection();

        // 获取 SQL 语句
        String sql = mappedStatement.getSql();

        // 创建 PreparedStatement 对象,并将 SQL 语句中的占位符替换为实际的参数值
        PreparedStatement preparedStatement = conn.prepareStatement(MyMybatisUtil.replacePlaceholders(sql,param));

        // 执行查询操作,获取结果集
        rs = preparedStatement.executeQuery();

        // 处理返回结果集
        ArrayList<E> list = new ArrayList<>();
        // 遍历结果集的每一行
        while (rs.next()){
            // 获取结果集的元数据信息,包含字段名和字段值的信息
            ResultSetMetaData metaData = rs.getMetaData();

            // 获取结果类型
            String resultType = mappedStatement.getResultType();
            // 根据结果类型获取对应的类对象
            Class<?> resultTypeClass = Class.forName(resultType);
            // 创建结果对象的实例
            Object o = resultTypeClass.newInstance();

            // 遍历结果集的每一列
            for (int i = 1; i <= metaData.getColumnCount() ; i++) {
                // 获取字段名
                String columnName = metaData.getColumnName(i);
                // 获取字段的值
                Object value = rs.getObject(columnName);

                PropertyDescriptor propertyDescriptor = new PropertyDescriptor(columnName, resultTypeClass);
                // 创建属性描述器,用于获取属性的读写方法
                Method writeMethod = propertyDescriptor.getWriteMethod();
                // 调用属性的写方法,将字段值设置到结果对象中
                writeMethod.invoke(o,value);
            }
            // 将结果对象添加到列表中
            list.add((E) o);
        }


        return list;
    }


    /**
     * 关闭资源
     */
    public void close() {
        // 关闭资源
        try {
            if (rs != null) {
                rs.close();
            }
            if (stmt != null) {
                stmt.close();
            }
            if (conn != null) {
                conn.close();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

    }
}

这一部分代码就是封装返回集为一个实体类:

第四行代码

java 复制代码
UserDao userDao = simpleSqlSession.newProxyClass(UserDao.class);

这行代码的目的就是使用第三步创建的SimpleSqlSession类创建一个代理类UserDao,从而实现代理每个方法,在调用每个方法之前都会调用SimpleSqlSession类里面的这个方法从而实现代理模式的应用。

第五行代码

java 复制代码
  		//findById
        User user = new User();
        user.setId(1);
        userDao.findById(user).forEach(System.out::println);

        System.out.println("===================");

        //findAll
        userDao.findAll().forEach(System.out::println);

userDao对应的xml文件内容如下:

java 复制代码
<mapper namespace="com.masiyi.dao.UserDao">


    <select id="findAll" resultType="com.masiyi.entity.User">
        select * from user
    </select>

    <select id="findById" resultType="com.masiyi.entity.User" parameterType="java.lang.Integer">
        select * from user where id = #{id}
    </select>

</mapper>

最后便通过上面代理模式的方法执行得到结果:

第六行代码

java 复制代码
simpleSqlSession.close();

执行SimpleExecutor类里面的close方法,将

编写成功,至此我们的MyMybatis框架就完全地"抄袭"mybatis成功,我们自己的框架里面成功实现了通过封装sql成xml文件,最后进行解析,成功实现了sql的select的功能,但是我们没能实现增删改方法,这里给大家留一个课后作业,大家可以根据现有的基础自己完成剩余功能的编写,而这个项目已经完全开源,仅供大家参考,代码地址为:gitee.com/WangFuGui-M...

最后附上文章里面工具类:

java 复制代码
package com.masiyi.util;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * @Author masiyi
 * @Date 2023/11/10
 * @PackageName:com.masiyi.util
 * @ClassName: MyMybatisUtil
 * @Description: 工具类
 * @Version 1.0
 */
public class MyMybatisUtil {

    static final Pattern pattern = Pattern.compile("#\\{([^}]+)}");


    /**
     * 将 SQL 查询语句中的占位符替换为对象的属性值
     *
     * @param sql 原始的 SQL 查询语句
     * @param obj 包含属性值的对象
     * @return 替换占位符后的 SQL 查询语句
     */
    public static String replacePlaceholders(String sql, Object obj) {
        // 创建匹配器,用于匹配占位符
        Matcher matcher = pattern.matcher(sql);
        // 创建字符串缓冲区,用于存储替换后的结果
        StringBuffer sb = new StringBuffer();
        // 循环查找匹配的占位符
        while (matcher.find()) {
            // 获取占位符的名称
            String placeholder = matcher.group(1);
            // 获取占位符对应的属性值
            Object replacement = getPropertyValue(obj, placeholder);
            // 如果属性值不为空
            if (replacement != null) {
                if (replacement.getClass() == String.class && !((String) replacement).isEmpty()) {
                    // 如果属性值是字符串类型且非空,添加单引号
                    replacement = "'" + replacement + "'";
                } else {
                    // 将属性值转换为字符串
                    replacement = replacement.toString();
                }
                // 将匹配到的占位符替换为属性值,并将结果添加到字符串缓冲区
                matcher.appendReplacement(sb, Matcher.quoteReplacement(replacement.toString()));
            }
        }
        // 将剩余的部分添加到字符串缓冲区
        matcher.appendTail(sb);
        return sb.toString();
    }

    /**
     * 获取对象的属性值
     *
     * @param obj          包含属性值的对象
     * @param propertyName 属性名
     * @return 属性值的字符串表示
     */
    public static Object getPropertyValue(Object obj, String propertyName) {
        try {
            // 根据属性名使用反射获取属性值
            Field field = obj.getClass().getDeclaredField(propertyName);
            field.setAccessible(true);
            return field.get(obj);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 将字符串的首字母大写
     *
     * @param str 输入字符串
     * @return 首字母大写后的字符串
     */
    public static String capitalize(String str) {
        if (str == null || str.isEmpty()) {
            return str;
        }
        return Character.toUpperCase(str.charAt(0)) + str.substring(1);
    }

}
相关推荐
Cobyte5 分钟前
AI全栈实战:使用 Python+LangChain+Vue3 构建一个 LLM 聊天应用
前端·后端·aigc
麦聪聊数据9 分钟前
Web 原生架构如何重塑企业级数据库协作流?
数据库·sql·低代码·架构
未来之窗软件服务10 分钟前
数据库优化提速(四)新加坡房产系统开发数据库表结构—仙盟创梦IDE
数据库·数据库优化·计算机软考
程序员侠客行1 小时前
Mybatis连接池实现及池化模式
java·后端·架构·mybatis
Honmaple1 小时前
QMD (Quarto Markdown) 搭建与使用指南
后端
PP东1 小时前
Flowable学习(二)——Flowable概念学习
java·后端·学习·flowable
invicinble1 小时前
springboot的核心实现机制原理
java·spring boot·后端
Goat恶霸詹姆斯2 小时前
mysql常用语句
数据库·mysql·oracle
全栈老石2 小时前
Python 异步生存手册:给被 JS async/await 宠坏的全栈工程师
后端·python
大模型玩家七七2 小时前
梯度累积真的省显存吗?它换走的是什么成本
java·javascript·数据库·人工智能·深度学习