【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);
    }

}
相关推荐
Hello.Reader25 分钟前
StarRocks实时分析数据库的基础与应用
大数据·数据库
执键行天涯26 分钟前
【经验帖】JAVA中同方法,两次调用Mybatis,一次更新,一次查询,同一事务,第一次修改对第二次的可见性如何
java·数据库·mybatis
yanglamei196237 分钟前
基于GIKT深度知识追踪模型的习题推荐系统源代码+数据库+使用说明,后端采用flask,前端采用vue
前端·数据库·flask
Adolf_199340 分钟前
Flask-JWT-Extended登录验证, 不用自定义
后端·python·flask
叫我:松哥1 小时前
基于Python flask的医院管理学院,医生能够增加/删除/修改/删除病人的数据信息,有可视化分析
javascript·后端·python·mysql·信息可视化·flask·bootstrap
海里真的有鱼1 小时前
Spring Boot 项目中整合 RabbitMQ,使用死信队列(Dead Letter Exchange, DLX)实现延迟队列功能
开发语言·后端·rabbitmq
工作中的程序员1 小时前
ES 索引或索引模板
大数据·数据库·elasticsearch
严格格1 小时前
三范式,面试重点
数据库·面试·职场和发展
工业甲酰苯胺1 小时前
Spring Boot 整合 MyBatis 的详细步骤(两种方式)
spring boot·后端·mybatis
微刻时光1 小时前
Redis集群知识及实战
数据库·redis·笔记·学习·程序人生·缓存