Loading

【MyBatis】自定义 MyBatis

自定义 MyBatis

文章源码

执行查询信息的分析

我们知道,MyBatis 在使用代理 DAO 的方式实现增删改查时只做两件事:

  • 创建代理对象
  • 在代理对象中调用 selectList()

配置信息 1:连接数据库的信息,有了它们就能创建 Connection 对象

```xml
<dataSource type="POOLED">
    <property name="driver" value="com.mysql.jdbc.Driver"/>
    <property name="url" value="jdbc:mysql://localhost:3306/mybatisT?useSSL=false"/>
    <property name="username" value="root"/>
    <property name="password" value="root"/>
</dataSource>
```

配置信息 2:有了它们就有了映射配置信息

```xml
<mappers>
    <!--使用 xml-->
    <mapper resource="cn/parzulpan/dao/UserDAO.xml"/>
    <!--使用 注解-->
    <!--指定被注解的 DAO 全限定类名-->
    <mapper class="cn.parzulpan.dao.UserDAOA"/>
</mappers>
```

配置信息 3:有了它们就有了要执行 SQL 语句,即能获取 PreparedStatement 对象,并且还指定了封装的实体类全限定类名

```xml
<!--持久层接口的映射文件-->
<mapper namespace="cn.parzulpan.dao.UserDAO">
    <select id="findAll" resultType="cn.parzulpan.domain.User">
        select * from user;
    </select>
</mapper>
```

对于上面三个配置信息,需要读取配置文件,用到的就是解析 XML 的技术,这里选用 dom4j

有了上面的准备,现在可以准备 第二件事 selectList()

  • 根据配置文件信息创建 Connection 对象

    • 注册驱动,获取连接等
  • 获取预处理对象 PrepareStatement

    • 执行 connection.prepareStatement(sql),此时需要 SQL 语句,可以由配置信息 3 得到
  • 执行查询

    • 执行 ResultSet rs = prepareStatement.executeQuery()
  • 遍历结果用于封装

    ArrayList<T> list = new ArrayList<>();
    while (resultSet.next()) {
        T t = (T)Class.forName(配置信息 3 的实体类全限定类名).newInstance();
        // 使用反射封装 
        list.add(t);
    }
    
  • 返回 list

    • return list;

要想让 selectList() 执行,需要给方法提供两个信息

  • 连接信息
  • 映射信息,包括 执行的 SQL 语句 和 封装结果的实体类全限定类名,可以把这两个信息组合起来定义成一个 Mapper 对象

这个对象可以用用一个 Map 存储起来:

  • key 是一个 String,值为 cn.parzulpan.dao.UserDAO.findAll
  • value 即这个 Mapper 对象,属性有 String sqlString domainClassPath

现在需要准备 第一件事 创建代理对象

// 5. 使用 SqlSession 对象 创建 DAO 接口的的代理对象
UserDAO userDAO = session.getMapper(UserDAO.class);
// 根据 DAO 接口的字节码创建 DAO 的代理对象
public <T> getMapper(Class<T> DAOInterfaceClass) {

    /**
    loader,类加载器,它使用和被代理类相同的类加载,即  DAOInterfaceClass.getClass().getClassLoader()

    interfaces,代理对象要实现的接口字节码数组,它使用和被代理类相同的接口,即 DAOInterfaceClass.getClass().getInterfaces()

    handler,如何代理,它需要自己实现,写一个实现接口 InvocationHandler 的类,类中调用第二件事 selectList()
    */

    Proxy.newProxyInstance(ClassLoader loader, Class[] interfaces, InvocationHandler handler);
}

自定义实现

万变不离其宗,看别人是如何实现的?

package cn.parzulpan.test;

import cn.parzulpan.dao.UserDAO;
import cn.parzulpan.domain.User;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;

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

/**
 * @Author : parzulpan
 * @Time : 2020-12-15
 * @Desc :
 */

public class MyBatisTest {
    public static void main(String[] args) throws IOException {
        // 1. 读取配置文件
        // 使用类加载器,它只能读取类路径的配置文件
        // 使用 ServletContext 对象的 getRealPath()
        InputStream is = Resources.getResourceAsStream("SqlMapConfig.xml");

        // 2. 创建 SqlSessionFactory 的构建者对象
        SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
        // 3. 使用构建者创建工厂对象 SqlSessionFactory
        // 创建工厂对象 使用了建造者模式
        // 优势:把对象的创建细节隐藏,使用者直接调用方法即可拿到对象
        SqlSessionFactory factory = builder.build(is);

        // 4. 使用 SqlSessionFactory 生产 SqlSession 对象
        // 生产 SqlSession 对象 使用了工厂模式
        // 优势:解藕,降低了类之间的依赖关系
        SqlSession session = factory.openSession();

        // 5. 使用 SqlSession 对象 创建 DAO 接口的的代理对象
        // 创建 DAO 接口的代理对象 使用了代理模式
        // 优势:在不修改源码的基础上对已有方法增强
        UserDAO userDAO = session.getMapper(UserDAO.class);

        // 6. 使用代理对象执行方法
        List<User> users = userDAO.findAll();
        users.forEach(System.out::println);

        // 7. 释放资源
        session.close();
        is.close();
    }
}

通过 上面的示例代码 和 MyBatis 入门 我们知道,需要实现以下类和接口:

  • class Resources
  • class SqlSessionFactoryBuilder
  • interface SqlSessionFactory
  • interface SqlSession

引入工具类

编写 主配置文件

SqlMapConfig.xml:

<?xml version="1.0" encoding="UTF-8"?>

<!--Mybatis 的主配置文件-->
<configuration>
    <!--配置 MyBatis 环境-->
    <environments default="mysql">
        <!--配置 MySQL 环境-->
        <environment id="mysql">
            <!--配置事务的类型-->
            <transactionManager type="JDBC"/>
            <!--配置连接数据库的信息,用的是数据源(连接池)-->
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://localhost:3306/mybatisT?useSSL=false"/>
                <property name="username" value="root"/>
                <property name="password" value="root"/>
            </dataSource>
        </environment>
    </environments>

    <!--告知 MyBatis 映射配置的位置-->
    <mappers>
        <!--使用 xml-->
        <mapper resource="cn/parzulpan/dao/UserDAO.xml"/>
        <!--使用 注解-->
        <!--指定被注解的 DAO 全限定类名-->
        <mapper class="cn.parzulpan.dao.UserDAOA"/>
    </mappers>
</configuration>

注意:由于没有使用 MyBatis 的 jar 包,所以要把配置文件的约束删掉,否则会报错。

编写 读取配置文件类

package cn.parzulpan.mybatis.io;

import java.io.InputStream;

/**
 * @Author : parzulpan
 * @Time : 2020-12
 * @Desc : 使用类加载器读取配置文件
 */

public class Resources {

    /**
     * 用于加载 xml 文件,并且得到一个流对象
     * @param xmlPath xml 文件路径
     * @return 流对象
     */
    public static InputStream getResourceAsStream(String xmlPath) {
        return Resources.class.getClassLoader().getResourceAsStream(xmlPath);
    }
}

编写 Mapper 类

package cn.parzulpan.mybatis.cfg;

/**
 * @Author : parzulpan
 * @Time : 2020-12
 * @Desc : 用于封装查询时的必要信息,包括 执行的 SQL 语句 和 封装结果的实体类全限定类名
 */

public class Mapper {
    private String queryString; // sql 语句
    private String resultType;  // 结果的实体类全限定类名

    public String getQueryString() {
        return queryString;
    }

    public void setQueryString(String queryString) {
        this.queryString = queryString;
    }

    public String getResultType() {
        return resultType;
    }

    public void setResultType(String resultType) {
        this.resultType = resultType;
    }
}

编写 Configuration 配置类

package cn.parzulpan.mybatis.cfg;

import java.util.HashMap;
import java.util.Map;

/**
 * @Author : parzulpan
 * @Time : 2020-12
 * @Desc : 核心配置类,包含数据库信息、sql 的 map 集合
 */

public class Configuration {
    private String username;    //用户名
    private String password;    //密码
    private String url; //地址
    private String driver;  //驱动

    /**
     要想让 **`selectList()`** 执行,需要给方法提供两个信息

     * 连接信息
     * 映射信息,包括 执行的 SQL 语句 和 封装结果的实体类全限定类名,可以把这两个信息组合起来定义成一个 **Mapper 对象**

     这个对象可以用用一个 Map 存储起来:

     * **key** 是一个 String,值为 `cn.parzulpan.dao.UserDAO.findAll`
     * **value** 即这个 Mapper 对象,属性有 `String sql` 和 `String domainClassPath`

     */

    private Map<String, Mapper> mappers = new HashMap<>();

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String getUrl() {
        return url;
    }

    public void setUrl(String url) {
        this.url = url;
    }

    public String getDriver() {
        return driver;
    }

    public void setDriver(String driver) {
        this.driver = driver;
    }

    public Map<String, Mapper> getMappers() {
        return mappers;
    }

    public void setMappers(Map<String, Mapper> mappers) {
        this.mappers.putAll(mappers);   // 注意这里是追加的方式,而不是覆盖
    }
}

编写 持久层接口的映射文件

<?xml version="1.0" encoding="UTF-8"?>

<!--持久层接口的映射文件-->
<mapper namespace="cn.parzulpan.dao.UserDAO">
    <select id="findAll" resultType="cn.parzulpan.domain.User">
        select * from user;
    </select>
</mapper>

注意:由于没有使用 MyBatis 的 jar 包,所以要把映射文件的约束删掉,否则会报错。

编写 SqlSessionFactoryBuilder 建造者类

package cn.parzulpan.mybatis.session;

import cn.parzulpan.mybatis.session.impl.SqlSessionFactoryImpl;

import java.io.InputStream;

/**
 * @Author : parzulpan
 * @Time : 2020-12
 * @Desc : 用于 SqlSessionFactory 的创建
 */

public class SqlSessionFactoryBuilder {

    /**
     * 根据传入的流,实现对 SqlSessionFactory 的创建
     * @param is
     * @return
     */
    public SqlSessionFactory build(InputStream is) {
        SqlSessionFactoryImpl factory = new SqlSessionFactoryImpl();
        factory.setIs(is);  // //给 factory 中 is 赋值
        return factory;
    }
}

编写 SqlSessionFactory 接口和实现类

package cn.parzulpan.mybatis.session;

/**
 * @Author : parzulpan
 * @Time : 2020-12
 * @Desc : SqlSessionFactory 接口
 */

public interface SqlSessionFactory {

    /**
     * 创建一个新的 SqlSession 对象
     * @return
     */
    SqlSession openSession();
}

package cn.parzulpan.mybatis.session.impl;

import cn.parzulpan.mybatis.cfg.Configuration;
import cn.parzulpan.mybatis.session.SqlSession;
import cn.parzulpan.mybatis.session.SqlSessionFactory;
import cn.parzulpan.mybatis.utils.XMLConfigBuilder;

import java.io.InputStream;

/**
 * @Author : parzulpan
 * @Time : 2020-12
 * @Desc : SqlSessionFactory 接口的实现类
 */

public class SqlSessionFactoryImpl implements SqlSessionFactory {
    private InputStream is = null;

    public InputStream getIs() {
        return is;
    }

    public void setIs(InputStream is) {
        this.is = is;
    }

    /**
     * 创建一个新的 SqlSession 对象
     *
     * @return
     */
    @Override
    public SqlSession openSession() {
        SqlSessionImpl session = new SqlSessionImpl();
        Configuration cfg = XMLConfigBuilder.loadConfiguration(session, is);    // 调用工具类解析 xml 文件
        session.setCfg(cfg);
        return session;
    }
}

编写 SqlSession 接口和实现类

package cn.parzulpan.mybatis.session;

/**
 * @Author : parzulpan
 * @Time : 2020-12
 * @Desc : SqlSession 接口,操作数据库的核心对象
 */

public interface SqlSession {

    /**
     * 创建 DAO 接口的的代理对象
     * @param DAOInterfaceClass
     * @param <T>
     * @return
     */
    <T> T getMapper(Class<T> DAOInterfaceClass);

    /**
     * 释放资源
     */
    void close();
}

package cn.parzulpan.mybatis.session.impl;

import cn.parzulpan.mybatis.cfg.Configuration;
import cn.parzulpan.mybatis.session.SqlSession;
import cn.parzulpan.mybatis.session.handler.MapperInvocationHandler;
import cn.parzulpan.mybatis.utils.DataSourceUtil;

import java.lang.reflect.Proxy;
import java.sql.Connection;
import java.sql.SQLException;

/**
 * @Author : parzulpan
 * @Time : 2020-12
 * @Desc : SqlSession 接口的实现类
 */

public class SqlSessionImpl implements SqlSession {
    private Configuration cfg;  // 核心配置对象
    private Connection connection;  // 连接对象

    public Configuration getCfg() {
        return cfg;
    }

    public void setCfg(Configuration cfg) {
        this.cfg = cfg;
        this.connection = DataSourceUtil.getConnection(this.cfg);
    }

    public void setConnection(Connection connection) {
        this.connection = connection;
    }

    /**
     * 创建 DAO 接口的的代理对象
     *
     * @param DAOInterfaceClass DAO 接口的字节码
     * @return
     */
    @Override
    public <T> T getMapper(Class<T> DAOInterfaceClass) {

//        Proxy.newProxyInstance(DAOInterfaceClass.getClassLoader(),
//                DAOInterfaceClass.getInterfaces(),
//                new MapperInvocationHandler(cfg.getMappers(), connection));

        T DAOProxy = (T)Proxy.newProxyInstance(DAOInterfaceClass.getClassLoader(),
                new Class[]{DAOInterfaceClass},
                new MapperInvocationHandler(cfg.getMappers(), connection));

        return DAOProxy;
    }

    /**
     * 释放资源
     */
    @Override
    public void close() {
        if (connection != null) {
            try {
                connection.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }
}

编写 创建 DAO 的代理对象的类

package cn.parzulpan.mybatis.session.handler;

import cn.parzulpan.mybatis.cfg.Mapper;
import cn.parzulpan.mybatis.utils.Executor;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.sql.Connection;
import java.util.Map;

/**
 * @Author : parzulpan
 * @Time : 2020-12
 * @Desc : 用于创建代理对象是增强方法
 */

public class MapperInvocationHandler implements InvocationHandler {
    private Map<String, Mapper> mappers;    //  key 包含实体类全限定类名和方法名
    private Connection connection;

    public MapperInvocationHandler(Map<String, Mapper> mappers, Connection connection) {
        this.mappers = mappers;
        this.connection = connection;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 1. 获取方法名
        String methodName = method.getName();
        // 2. 获取方法所在类名
        String className = method.getDeclaringClass().getName();
        // 3. 组合 key
        String key = className + "." + methodName;
        // 4. 获取 mappers 中的 Mapper 对象
        Mapper mapper = mappers.get(key);
        // 5. 判断是否有 mapper
        if (mapper == null) {
            throw new IllegalArgumentException("传入的参数有误,无法获取执行的必要条件。");
        }
        // 6. 创建 Executor 对象,负责执行 SQL 语句,并且封装结果集
        return new Executor().selectList(mapper, connection);
    }
}

自定义注解

package cn.parzulpan.mybatis.annotations;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @Author : parzulpan
 * @Time : 2020-12
 * @Desc : 自定义 Select 注解
 */

// 生命周期为 RUNTIME,出现位置为 METHOD
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Select {
    String value(); // 配置 SQL 语句
}

总结和练习


自定义 MyBatis 步骤总结

  • 第一步:SqlSessionBuilder 接收 SqlMapConfig.xml 文件流,构建出 SqlSessionFactory 对象;
  • 第二步:SqlSessionFactory 加载解析 SqlMapConfig.xml 文件流,得到连接信息和映射信息,用来生产出真正操作数据库的 SqlSession 对象;
  • 第三步:SqlSession 对象有两大作用,分别是生成接口代理对象和定义通用增删改查方法。
  • 第四步:
    • 第一步:在 SqlSessionImpl 对象的 getMapper() 分两步实现:1. 先用核心配置对象和连接对象;2. 通过代理模式创建出代理类对象;
    • 第二步:在 Executor 工具类 selectList() 等方法分两步实现:1. 得到连接对象;2. 得到 SQL 语句,进行 JDBC 操作。
  • 第五步:封装结果集,变成 Java 对象返回给调用者。

posted @ 2020-12-16 01:02  Parzulpan  阅读(93)  评论(0编辑  收藏  举报