手写简单的mybatis框架

一、核心

mybatis的核心是通过解析核心配置xml(获取数据源、存放mapper.xml文件的路径 ),根据mapper.xml路径解析出sql的类型(crud哪一种)、返回值类型(结果集是什么类型的)、参数类型(带不带参数就行sql操作)这些东西存放在一个Configuration 类里面(用一个map保存key为:接口全限定名.方法名。)。最后利用动态代理(jdk)去生成一个代理对象。代理对象根据Configuration类里面的存放的sql信息,利用jdbc操作数据库。

二、代码

(一)pom

<dependencies>
   <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.12</version>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.12</version>
        <scope>provided</scope>
    </dependency>
    <!--mysql jdbc-->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.19</version>
    </dependency>
    <!--数据源-->
    <dependency>
        <groupId>c3p0</groupId>
        <artifactId>c3p0</artifactId>
        <version>0.9.1.2</version>
    </dependency>

    <!--解析xml的工具包-->
    <dependency>
        <groupId>dom4j</groupId>
        <artifactId>dom4j</artifactId>
        <version>1.6.1</version>
    </dependency>
    <dependency>
        <groupId>jaxen</groupId>
        <artifactId>jaxen</artifactId>
        <version>1.1.6</version>
    </dependency>
</dependencies>

(二)解析xml

  • 使用dom4j工具解析SqlMapConfig.xml和UserMapper.xml

SqlMapConfig.xml

<configuration>

    <!--数据库配置信息-->
    <dataSource>
        <property name="driverClass" value="com.mysql.jdbc.Driver"></property>
        <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/book?useUnicode=true&amp;characterEncoding=utf8&amp;useSSL=false&amp;serverTimezone=UTC&amp;allowPublicKeyRetrieval=true"></property>
        <property name="username" value="root"></property>
        <property name="password" value="123"></property>
    </dataSource>

    <!--存放mapper.xml的全路径-->
    <mapper resource="UserMapper.xml"></mapper>

</configuration>

UserMapper.xml

<mapper namespace="com.lihua.mapper.UserMapper">

    <!--sql的唯一标识:namespace.id来组成 :statementId-->
    <select id="queryUserAll" resultType="com.lihua.pojo.User">
        select * from user
    </select>

    <!--
        User user = new User();
        user.setId(1);
        user.setUsername("riemann");
    -->
<!--    <select id="selectOne" resultType="com.lihua.pojo.User" parameterType="com.lihua.pojo.User">-->
<!--        select * from user where id = ?-->
<!--    </select>-->

</mapper>
  1. 需要找到文件位置(路径)。通过类加载器获取resources下的配置文件的输入流
package com.lihua.custommybatis;

import java.io.InputStream;

/**
 * @author lihua
 * @date 2022/12/6 17:28
 *
 * 利用 ClassLoader加载配置
 */
public class Resources {

    /**
     * 根据配置文件的路径,将配置文件加载成字节输入流,存储在内存中。
     * @param path 文件路径
     * @return     字节流
     */
    public static InputStream getResourceAsStream(String path) {
        InputStream resourceAsStream = Resources.class.getClassLoader().getResourceAsStream(path);
        return resourceAsStream;
    }

}

  1. 解析xml
  • 解析核心配置文件
package com.lihua.custommybatis;

import com.mchange.v2.c3p0.ComboPooledDataSource;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;

import java.beans.PropertyVetoException;
import java.io.InputStream;
import java.util.List;
import java.util.Properties;

/**
 * @author lihua
 * @date 2022/12/6 16:09
 * 用于读取mybatis核心xml文件,对mybatis核心配置文件进行读取。
 */
public class XMLConfigBuilder {

    private Configuration configuration;

    public XMLConfigBuilder() {
        this.configuration = new Configuration();
    }

    /**
     * 该方法就是使用dom4j对配置文件进行解析,封装成Configuration对象
     * @param in 字节输入流
     * @return   Configuration
     */
    public Configuration parseConfig(InputStream in) throws DocumentException, PropertyVetoException {
        Document document = new SAXReader().read(in);
        // <configuration>
        Element rootElement = document.getRootElement();
        List<Element> list = rootElement.selectNodes("//property");
        Properties properties = new Properties();
        for (Element element : list) {
            String name = element.attributeValue("name");
            String value = element.attributeValue("value");
            properties.setProperty(name, value);
        }

        ComboPooledDataSource comboPooledDataSource = new ComboPooledDataSource();
        comboPooledDataSource.setDriverClass(properties.getProperty("driverClass"));
        comboPooledDataSource.setJdbcUrl(properties.getProperty("jdbcUrl"));
        comboPooledDataSource.setUser(properties.getProperty("username"));
        comboPooledDataSource.setPassword(properties.getProperty("password"));

        configuration.setDataSource(comboPooledDataSource);

        // mapper.xml解析:拿到路径--字节输入流--dom4j进行解析
        List<Element> mapperList = rootElement.selectNodes("//mapper");
        for (Element element : mapperList) {
            String mapperPath = element.attributeValue("resource");
            InputStream resourceAsStream = Resources.getResourceAsStream(mapperPath);
            XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(configuration);
            xmlMapperBuilder.parse(resourceAsStream);
        }

        return configuration;
    }
}

  • 解析mapper.xml配置文件
package com.lihua.custommybatis;

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

import java.io.InputStream;
import java.util.List;

/**
 * @author lihua
 * @date 2022/12/6 16:11
 */
public class XMLMapperBuilder {

    private Configuration configuration;

    public XMLMapperBuilder(Configuration configuration) {
        this.configuration = configuration;
    }

    public void parse(InputStream in) throws DocumentException {
        Document document = new SAXReader().read(in);
        Element rootElement = document.getRootElement();
        String namespace = rootElement.attributeValue("namespace");

        List<Element> list = rootElement.selectNodes("//select");
        for (Element element : list) {
            String id = element.attributeValue("id");
            String resultType = element.attributeValue("resultType");
            String parameterType = element.attributeValue("parameterType");
            String sqlText = element.getTextTrim();
            MappedStatement mappedStatement = new MappedStatement();
            mappedStatement.setId(id);
            mappedStatement.setResultType(resultType);
            mappedStatement.setParameterType(parameterType);
            mappedStatement.setSql(sqlText);
            mappedStatement.setSqlType(element.getName());

            String key = namespace + "." + id;
            configuration.getMappedStatement().put(key, mappedStatement);
        }
    }

}

(三)Configuration

解析xml后,将结果放到一个Configuration 类里存放。在前面的解析步骤中,需要解析两个配置文件。

  • 核心配置文件:存放了我们连接数据库的数据源信息。我们用一个javax.sql.DataSource;下的DataSource类存放。

  • mapper.xml:存放了我们编写的sql语句。我们创建一个MappedStatement类存放sql语句的信息,比如:sql类型(insert\select),参数类型,结果集类型,具体的sql。因为一个mapper中有很多sql语句,因此我们需要用一个map存放,key为:接口全限定名.方法名。value为:MappedStatement

MappedStatement

package com.lihua.custommybatis;

import lombok.Data;

/**
 * @author lihua
 * @date 2022/12/6 15:28
 * 用于存放解析后的sql语句
 *
 */
@Data
public class MappedStatement {
    // id标识
    private String id;

    //sql类型,crud
    private String sqlType;

    // 返回值类型
    private String resultType;

    // 参数值类型
    private String parameterType;

    // sql语句
    private String sql;
}

Configuration

package com.lihua.custommybatis;


import lombok.Data;

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

/**
 * @author lihua
 * @date 2022/12/6 15:24
 * 自定义config,用于存放读取到的xml文件
 */
@Data
public class Configuration {
    private DataSource dataSource;

    private Map<String ,MappedStatement> mappedStatement = new HashMap<>();
}

(四)SqlSessionFactoryBuilder

使用建造者模式创建一个SqlSessionFactory 对象。一般以Builder 结尾的都是建造者模式。建造者模式使用多个简单的对象一步一步构建成一个复杂的对象。(与工厂模式一样,是创建型模式,用于屏蔽复杂对象的构建过程)。

package com.lihua.custommybatis;

import org.dom4j.DocumentException;

import java.beans.PropertyVetoException;
import java.io.InputStream;

/**
 * @author lihua
 * @date 2022/12/6 16:07
 * 建造者模式创建SqlSessionFactory 工厂对象
 */
public class SqlSessionFactoryBuilder {
    public SqlSessionFactory build(InputStream in) {
        // 1.使用dom4j解析配置文件,将解析出来的内容封装到Configuration
        XMLConfigBuilder xmlConfigBuilder = new XMLConfigBuilder();
        Configuration configuration = null;
        try {
            configuration = xmlConfigBuilder.parseConfig(in);
        } catch (DocumentException e) {
            e.printStackTrace();
        } catch (PropertyVetoException e) {
            e.printStackTrace();
        }
        // 2.创建SqlSessionFactory对象:工厂类:生产SqlSession:会话对象
        DefaultSqlSessionFactory defaultSqlSessionFactory = new DefaultSqlSessionFactory(configuration);
        return defaultSqlSessionFactory;
    }
}

(五)通过SqlSessionFactory获取SqlSession

SqlSessionFactory 是简单工厂模式(工厂模式),也是用于构建一个对象。这里用来构建SqlSession。

SqlSessionFactory

public interface SqlSessionFactory {
    /**
     * 通过工厂模式获取session
     *
     * @return
     */
    DefaultSqlSession openSession();
}

默认的实现类

package com.lihua.custommybatis;

/**
 * @author lihua
 * @date 2022/12/6 15:48
 * 默认会话工厂实现
 */
public class DefaultSqlSessionFactory implements SqlSessionFactory {

    private Configuration configuration;

    /**
     * 通过构造器注入
     * @param configuration
     */
    public DefaultSqlSessionFactory(Configuration configuration) {
        this.configuration = configuration;
    }

    @Override
    public DefaultSqlSession openSession() {

        return new DefaultSqlSession(configuration);
    }
}

(六)核心类SqlSession

在SqlSession中就使用了动态代理(jdk动态代理)模式创建一个代理类去操作数据库。

  • 接口
package com.lihua.custommybatis;

import java.util.List;

/**
 * @author lihua
 * @date 2022/12/6 15:56
 */
public interface SqlSession {

    /**
     * 查询所有
     * @param statementId sql唯一id
     * @param params      sql有可能十四模糊查询,传可变参数
     * @param <E>         泛型
     * @return            List集合
     */
    <E> List<E> selectList(String statementId, Object... params) throws Exception;

    /**
     * 根据条件查询单个
     * @param statementId sql唯一id
     * @param params      sql有可能十四模糊查询,传可变参数
     * @param <T>         泛型
     * @return            某一对象
     */
    <T> T selectOne(String statementId, Object... params) throws Exception;

    /**
     * 为Dao层接口生成代理实现类(获取代理对象,执行crud)
     * @param mapperClass 字节码
     * @param <T>         泛型
     * @return <T>        某一对象
     */
    <T> T getMapper(Class<?> mapperClass) throws Exception;
}

  • 这里没有单独创建一个代理类,直接new了一个匿名类
package com.lihua.custommybatis;

import java.lang.reflect.*;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

/**
 * @author lihua
 * @date 2022/12/6 15:49
 * 会话
 */
public class DefaultSqlSession implements SqlSession {

    private Configuration configuration;

    private Connection connection;

    public DefaultSqlSession(Configuration configuration) {
        this.configuration = configuration;
        try {
            //操作数据库的连接
            this.connection= configuration.getDataSource().getConnection();
        } catch (SQLException throwables) {
            throwables.printStackTrace();
        }
    }


    @Override
    public <E> List<E> selectList(String statementId, Object... params) throws Exception {

        return null;
    }

    @Override
    public <T> T selectOne(String statementId, Object... params) throws Exception {
        return null;
    }


    @Override
    public <T> T getMapper(Class<?> mapperClass) throws Exception {
        // 使用JDK动态代理来为Dao层接口生成代理对象,并返回。
        Object proxyInstance = Proxy.newProxyInstance(mapperClass.getClassLoader(), new Class[]{mapperClass}, new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

                /**
                 * 底层都还是去执行JDBC代码
                 * 根据不同情况来调用findAll或者findByCondition方法
                 * 准备参数:
                 * 1.statementId: sql语句的唯一标识 nnamespace.id = 接口全限定名.方法名
                 */
                // 方法名
                String methodNme = method.getName();
                String className = method.getDeclaringClass().getName();

                String   statementId  = className + "." + methodNme;

                System.out.println(methodNme);
                MappedStatement mappedStatement = configuration.getMappedStatement().get(statementId);//根据方法名得到它的环境信息
                PreparedStatement statement = connection.prepareStatement(mappedStatement.getSql());//根据sql得到预编译语句
                if ("insert".equals(mappedStatement.getSqlType())){
                    //假设传入一个参数
                    String paramType = mappedStatement.getParameterType();//得到参数类型
                    Class<?> clazz = args[0].getClass();
                    Field[] fields = clazz.getDeclaredFields();
                    for(int i=0;i<fields.length;i++){
                        fields[i].setAccessible(true);
                        statement.setObject(i+1,fields[i].get(args[0]));
                    }
                    return statement.executeUpdate();
                }
                else if("delete".equals(mappedStatement.getSqlType())){

                    for (int i=0;i<args.length;i++){
                        statement.setObject(i+1,args[i]);
                    }
                    return statement.executeUpdate();
                }
                else if("select".equals(mappedStatement.getSqlType())){
                    if (args!=null){
                        for (int i=0;i<args.length;i++){//替换占位符的参数
                            statement.setObject(i+1,args[i]);
                        }
                    }
                    ResultSet resultSet = statement.executeQuery();//执行查询语句得到返回集合
                    List list=new ArrayList();
                    while (resultSet.next()){//遍历返回集合
                        Class<?> clazz = Class.forName(mappedStatement.getResultType());//通过反射机制,根据返回值类型创建实例
                        Object object = clazz.newInstance();
                        Field[] fields = clazz.getDeclaredFields();//通过反射机制,该类型的所有属性
                        for (int i=0;i<fields.length;i++){
                            fields[i].setAccessible(true);//设置属性是可以访问的,避免private
                            fields[i].set(object,resultSet.getObject(fields[i].getName()));//给每个属性赋值
                        }
                        list.add(object);//把对象放在list集合中
                    }
                    return list;
                }
                return null;
            }
        });
        return (T) proxyInstance;
    }
}

MyBatis 框架中的代理类是这样的

/**
 *    Copyright 2009-2017 the original author or authors.
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */
package org.apache.ibatis.binding;

import java.io.Serializable;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Map;

import org.apache.ibatis.lang.UsesJava7;
import org.apache.ibatis.reflection.ExceptionUtil;
import org.apache.ibatis.session.SqlSession;

/**
 * @author Clinton Begin
 * @author Eduardo Macarron
 */
public class MapperProxy<T> implements InvocationHandler, Serializable {

  private static final long serialVersionUID = -6424540398559729838L;
  private final SqlSession sqlSession;
  private final Class<T> mapperInterface;
  private final Map<Method, MapperMethod> methodCache;

  public MapperProxy(SqlSession sqlSession, Class<T> mapperInterface, Map<Method, MapperMethod> methodCache) {
    this.sqlSession = sqlSession;
    this.mapperInterface = mapperInterface;
    this.methodCache = methodCache;
  }

  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
      if (Object.class.equals(method.getDeclaringClass())) {
        return method.invoke(this, args);
      } else if (isDefaultMethod(method)) {
        return invokeDefaultMethod(proxy, method, args);
      }
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
    final MapperMethod mapperMethod = cachedMapperMethod(method);
    return mapperMethod.execute(sqlSession, args);
  }

  private MapperMethod cachedMapperMethod(Method method) {
    MapperMethod mapperMethod = methodCache.get(method);
    if (mapperMethod == null) {
      mapperMethod = new MapperMethod(mapperInterface, method, sqlSession.getConfiguration());
      methodCache.put(method, mapperMethod);
    }
    return mapperMethod;
  }

  @UsesJava7
  private Object invokeDefaultMethod(Object proxy, Method method, Object[] args)
      throws Throwable {
    final Constructor<MethodHandles.Lookup> constructor = MethodHandles.Lookup.class
        .getDeclaredConstructor(Class.class, int.class);
    if (!constructor.isAccessible()) {
      constructor.setAccessible(true);
    }
    final Class<?> declaringClass = method.getDeclaringClass();
    return constructor
        .newInstance(declaringClass,
            MethodHandles.Lookup.PRIVATE | MethodHandles.Lookup.PROTECTED
                | MethodHandles.Lookup.PACKAGE | MethodHandles.Lookup.PUBLIC)
        .unreflectSpecial(method, declaringClass).bindTo(proxy).invokeWithArguments(args);
  }

  /**
   * Backport of java.lang.reflect.Method#isDefault()
   */
  private boolean isDefaultMethod(Method method) {
    return ((method.getModifiers()
        & (Modifier.ABSTRACT | Modifier.PUBLIC | Modifier.STATIC)) == Modifier.PUBLIC)
        && method.getDeclaringClass().isInterface();
  }
}

(七)使用

使用方式和Mybatis基本一样,只是功能简单。

  1. 创建一个实体类,一个操作数据库的接口(比如IUserMapper.class)
  2. 创建与接口(IUserMapper.class)对应的UserMapper.xml
  3. 在SqlMapConfig.xml 的配置中加上UserMapper.xml路径
  4. 通过SqlSession的getMapper()方法获取 ,通过动态代理生成的IUserMapper对象,然后调用IUserMapper里面的方法操作数据库

IUserMapper

<mapper namespace="com.lihua.mapper.IUserMapper">

    <!--sql的唯一标识:namespace.id来组成 :statementId-->
    <select id="queryUserAll" resultType="com.lihua.pojo.User">
        select * from user
    </select>

    <!--
        User user = new User();
        user.setId(1);
        user.setUsername("riemann");
    -->
<!--    <select id="selectOne" resultType="com.lihua.pojo.User" parameterType="com.lihua.pojo.User">-->
<!--        select * from user where id = ?-->
<!--    </select>-->

</mapper>

SqlMapConfig.xml

<configuration>

    <!--数据库配置信息-->
    <dataSource>
        <property name="driverClass" value="com.mysql.jdbc.Driver"></property>
        <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/book?useUnicode=true&amp;characterEncoding=utf8&amp;useSSL=false&amp;serverTimezone=UTC&amp;allowPublicKeyRetrieval=true"></property>
        <property name="username" value="root"></property>
        <property name="password" value="123"></property>
    </dataSource>

    <!--存放mapper.xml的全路径-->
    <mapper resource="IUserMapper.xml"></mapper>

</configuration>

IUserMapper

package com.lihua.mapper;

import com.lihua.pojo.User;

import java.util.List;

/**
 * @author lihua
 * @date 2022/12/6 16:22
 */
public interface IUserMapper {

    List<User> queryUserAll();

}

User

package com.lihua.pojo;

import lombok.Data;
import lombok.ToString;

/**
 * @author lihua
 * @date 2022/12/6 15:17
 * user
 */
@Data
@ToString
public class User {
    private long id;
    private String account;
    private String password;
    private String email;
}

test

package com.lihua.test;

import com.lihua.custommybatis.DefaultSqlSession;
import com.lihua.custommybatis.Resources;
import com.lihua.custommybatis.SqlSessionFactory;
import com.lihua.custommybatis.SqlSessionFactoryBuilder;
import com.lihua.mapper.UserMapper;
import com.lihua.pojo.User;

import java.io.InputStream;

/**
 * @author lihua
 * @date 2022/12/6 15:20
 * test
 */
public class Test {

    @org.junit.Test
    public void  test(){

        InputStream in = Resources.getResourceAsStream("SqlMapConfig.xml");
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(in);
        DefaultSqlSession sqlSession = sqlSessionFactory.openSession();
        try {
            UserMapper mapper = sqlSession.getMapper(UserMapper.class);
            for (User user : mapper.queryUserAll()) {
                System.out.println(user.toString());
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

    }
}

三、参考

手写mybatis 1

手写mybatis 2

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值