likes
comments
collection
share

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

作者站长头像
站长
· 阅读数 7

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

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

 <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框架,所以我们从引用我们框架的第一行代码开始入手

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

第一行代码

第一行代码长这样

InputStream resourceAsSteam = Resources.getResourceAsStream("myMybatisConfig.xml");

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

<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 的方法,类似这样:

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文件的路径转换为一个输入流,目的是加载配置文件

第二行代码

我们再看看第二行代码:

Configuration configuration = new ConfigParse().parse(resourceAsSteam);

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

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解析内容,他是长这样:

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文件中的

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

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

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

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

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

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

第三行代码

SimpleSqlSession simpleSqlSession = new SimpleSqlSession(configuration);

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

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类长这样:

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

    }
}



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

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

第四行代码

UserDao userDao = simpleSqlSession.newProxyClass(UserDao.class);

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

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

第五行代码

  		//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文件内容如下:

<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>

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

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

第六行代码

simpleSqlSession.close();

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

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

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

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

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

}