自定义MyBatis

基于XML文件方式自定义简单的MyBoptis

这里用到的技术

  • 代理模式(JDK动态代理)

  • 工厂模式

  • C3P0连接池

  • DOM4J

  • XPATH


在这里插入图片描述

竟然要自定义MyBatis,那就要把pom.xml里MyBatis的做标干掉,添加C3P0,DOM4J,XPATH坐标

<!-- C3P0 -->
<dependency>
	<groupId>c3p0</groupId>
	<artifactId>c3p0</artifactId>
	<version>0.9.1.2</version>
</dependency>
<!-- 解析 xml 的 dom4j -->
<dependency>
	<groupId>dom4j</groupId>
	<artifactId>dom4j</artifactId>
	<version>1.6.1</version>
</dependency>
<!-- dom4j 的依赖包 jaxen -->
<dependency>
    <groupId>jaxen</groupId>
    <artifactId>jaxen</artifactId>
    <version>1.1.6</version>
</dependency>

去掉mybatis坐标后,发现单元测试里面报错了. 那么下面我们就来开始自定义MyBatis框架!!!

在这里插入图片描述

思路分析

 // 1.把配置文件转成流
 InputStream is = Resources.getResourceAsStream("SqlMapConfig.xml");

 // 2.根据构建SqlSessionFactory,把流传给SqlSessionFactory
 SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
 SqlSessionFactory sqlSessionFactory = builder.build(is);

 // 3.在SessionFactory根据流解析xml,读取配置文件的内容,
 //给sqlSession赋值(a.连接数据库的基本信息 b.映射文件相关信息[为代理对象做准备])
 SqlSession sqlSession = sqlSessionFactory.openSession();

 // 4.通过动态代理创建UserDao的代理对象,提供InvocationHandler实现,对它实现时候,
 // 需要提供sql语句,执行sql封装结果集(会用到低3步中的b的部分)
 UserDao userDao = sqlSession.getMapper(UserDao.class);
第一步入手

1、创建Resources.java

public class Resources {
    /**
     * 读取classpath下面的文件,转成字节输入流
     */
    public static InputStream getResourceAsStream(String path) {
        InputStream is = Resources.class.getClassLoader().getResourceAsStream(path);
        return  is;
    }
}

完成第一步

在这里插入图片描述

第二步

2、使用建造者模式创建SqlSessionFactoryBuilder

2.1 创建SqlSessionFactory接口

2.2 创建SqlSessionFactoryBuilder类里添加build(InputStream is)方法,该方法根据(主配置文件)字节输入流, 构建SqlSessionFactory,在里创建工厂实现类返回,要问为什么?我们来看一下源码,我们发现源码都是抄我们的(我们抄源码的)

在这里插入图片描述

2.3 接下来创建SqlSessionFactory的实现类,如果直接new SqlSessionFactory返回,和主配置文件无瓜,我们要加载(配置文件流)所有在DefautlSqlSessionFactory添加一个加载配置文件流的方法

public class SqlSessionFactoryBuilder {

    /**
     * 根据(主配置文件)字节输入流, 构建SqlSessionFactory
     * @param is
     * @return
     */
    public SqlSessionFactory build(InputStream is) {
        DefaultSqlSessionFactory sqlSessionFactory = new DefaultSqlSessionFactory();
        // 加载资源(流)
        sqlSessionFactory.setConfiguration(is);
        return  sqlSessionFactory;
    }
}
public class DefaultSqlSessionFactory implements SqlSessionFactory {

    //主配置文件输入流
    private InputStream config;

    public void setConfiguration(InputStream config) {
        this.config = config;
    }
}

第二步搞定(不报错了)
在这里插入图片描述

第三步

在SqlSessionFactory里创建openSession返回SqlSession获得SqlSession

我们先来看SqlSession(抄一下)源码

[外链图片转存失败(img-bO1qfGn2-1566818707943)(sqlSession源码.png)]

1、创建SqlSession接口

2、创建SqlSessiond的实现类DefaultSqlSession(封装了Connection)

显然这样返回是不够的,SqlSession需要根据config解析config,给SqlSession赋值(SqlSession就好比跟数据库连接的通道。。【需要连接数据库,封装SQL语句等…】);这个我们等下再做
在这里插入图片描述
第三步不报错了(表面完成)

第四步

在SqlSession里创建getMapper(xxxx.calss)方法(得到接口的代理对象,也就是说这个方法内部用的就是动态代理)

public interface SqlSession {

    /**
     * @param daoClass  接口类的字节码对象(class)
     * @author lys
     * @Description 生成代理对象
     * @return T 
     * @date 2019/8/26 12:40
     */
    <T>T getMapper(Class<T> daoClass);
}

在这里插入图片描述

4.2、DefaultSqlSession再实现这个方法

似乎还有一个地方报错!!!不能忍

在这里插入图片描述

4.3再给SqlSession添加一个close方法,DefaultSqlSession再实现一下

不报错了 完成!!!跑一个???

在这里插入图片描述

在这里插入图片描述
???

想啥呢 兄dei ,我们只搭建了一个整体的架构,才刚刚开始;

第一第二步我们已经完成了

从第三步开始

5.1我们先创建一个工具类,该类用来解析配置文件封装到pojo里

public class XmlConfigUtils {

    public static Configuration parseXml(InputStream config) {
        try {
            //0 创建Configuration对象
            Configuration configuration = new Configuration();

            //1. 创建解析器对象
            SAXReader saxReader = new SAXReader();
            //2. 读取配置文件, 获得document
            Document document = saxReader.read(config);
            //3. 获得所有的property对象集合
            List<Element> propertyEles = document.selectNodes("//dataSource/property");
            //4. 遍历property对象集合(解析连接数据库四个基本项)
            for (Element propertyEle : propertyEles) {
                String name = propertyEle.attributeValue("name");
                String value = propertyEle.attributeValue("value");

                if ("driver".equals(name)) {
                    configuration.setDriver(value);
                }

                if ("url".equals(name)) {
                    configuration.setUrl(value);
                }

                if ("username".equals(name)) {
                    configuration.setUsername(value);
                }

                if ("password".equals(name)) {
                    configuration.setPassword(value);
                }
            }
            //5. 判断是否使用连接池
            Element dataSourceEle = (Element) document.selectSingleNode("//dataSource");
            String type = dataSourceEle.attributeValue("type");
            if ("POOLED".equals(type)) {
                //使用连接池
               configuration.setUsePool(true);
            } else {
                //不使用连接池
                configuration.setUsePool(false);

            }
            //6. 解析所有的mapper节点
            List<Element> mapperEles = document.selectNodes("//mapper");
            //7. 遍历
            for (Element mapperEle : mapperEles) {
                //8 判断是xml方式还是注解方式
                Attribute resourceAttr = mapperEle.attribute("resource");
                if (resourceAttr != null) {
                    //9.xml方式, 解析映射文件,获得Map<String,Mapper> mapperMap
                    String mapPath = resourceAttr.getValue();
                    Map<String, Mapper> mapperMap = loadXmlMapperConfig(mapPath);
                    configuration.setMappers(mapperMap);
                } else {
                    //10 注解方式
                }
            }
            return  configuration;

        } catch (Exception e) {
            e.printStackTrace();
            return  null;
        }
    }

    /**
     * 加载映射文件,生成Map<String,Mapper>
     *
     * @param mapPath
     * @return
     */
    private static Map<String, Mapper> loadXmlMapperConfig(String mapPath) throws Exception {
        InputStream is = null;
        try {
            Map<String, Mapper> mapperMap = new HashMap<String, Mapper>();
            //1. 根据路径获得流对象
            is = Resources.getResourceAsStream(mapPath);
            //2. 创建解析器
            SAXReader saxReader = new SAXReader();
            //3. 读取流, 获得document对象
            Document document = saxReader.read(is);
            //4. 获得根元素 mapper
            Element mapperElement = document.getRootElement();
            //5. 获得namespace
            String namespace = mapperElement.attributeValue("namespace");
            //6. 获得所有的select节点
            List<Element> selectEles = mapperElement.elements("select");
            //7. 遍历(没遍历一次对应一个mapper)
            for (Element selectEle : selectEles) {

                String id = selectEle.attributeValue("id");
                String resultType = selectEle.attributeValue("resultType");
                String sql = selectEle.getText();

                Mapper mapper = new Mapper();
                mapper.setSql(sql);
                mapper.setResultType(resultType);

                String key = namespace + "." + id;

                mapperMap.put(key, mapper);
            }
            return mapperMap;
        } finally {
            is.close();
        }
    }
}

5.2 居然要封装到pojo我们也需要一个pojo,创建一个配置类

public class Configuration {

    private String driver;
    private String url;
    private String username;
    private String password;
  
    private boolean usePool;  // POOLED:使用连接池(mybatis内置的); UNPOOLED:不使用连接池
 	private Map<String,Mapper> mappers; // 映射文件对应的对象(sql语句,返回结果类型....)
  	get  set.....
      

在这里插入图片描述

5.3 这里的mappers是什么? 我们先来看看,映射文件、数据库和pojo

[外链图片转存失败(img-5fjPdGVP-1566818707953)(分析.png)]

执行SQL语句,根据结果类型(resutType),封装结果集到pojo对象里

根据已知的id(调用的方法名)来获取sql语句和resutType,所有Map来作为数据结构,仅仅把id作为map的key行不行?当然是不行的,因为可能有很多的接口和接口映射文件(eg:UserDao.xml,ProductDao.xml…),可能id(方法名)会重复,所有我们不能仅仅把id作为Key

我们把namespace(接口的全限定名)+id(方法名)作为Key

在这里插入图片描述

因为只就两个(Sql语句和resutType),我们可以封装成一个对象(Mapper类),map的value就是Mapper对象

public class Mapper {
    private String sql;
    private String resultType;

  	get set...
}

Configuration里mappers的结构

keyvalue
接口的全限定名.方法名()Mapper对象

5.4 分析完成,调用该工具类获得Configuration

在这里插入图片描述

六、使用代理模式编写 ProxyMethodInvocationHandler类

代理模式分为静态和动态代理。 静态代理,我们通常都很熟悉。有一个写好的代理类,实现与要代理的类的一个共同的接口,目的是为了约束也为了安全。具体不再多说。

今天我们就会使用 JDK 动态代理方式来编写MapperProxyFactory 类

在这里插入图片描述

我们先来完成getMapper方法,该方法就是通过JDK的动态代理产生接口的对象

    @Override
    public <T> T getMapper(Class <T> daoClass) {
        return (T) Proxy.newProxyInstance(daoClass.getClassLoader(), 
            new Class[]{daoClass}, 
            new   MethodProxyInvocationHandler
                        (getConnection(),configuration.getMappers()));
    }

new MethodProxyInvocationHandler(getConnection(),configuration.getMappers());增强方法类,该类控制了代理对象的执行方法逻辑。操作数据;(通过构造方法传参)

参数1:连接对象

参数2:刚才我们创建的类,封装了sql语句

我们现在来完成增强方法

  • 1.获得方法名(id)

  • 2.获得当前方法所在类的全限定名(namespace)

  • 3.获得mapper对象

  • 4.获得sql语句和结果类型

  • 5.创建预编译sql语句对象

  • 6.执行

  • 6.1我们需要一个工具类,该通过泛型的反射获得结果集

    开搞!!

public class MethodProxyInvocationHandler implements InvocationHandler {

    private Connection connection;

    private Map <String, Mapper> mappers;


    public MethodProxyInvocationHandler(Connection connection, Map <String, Mapper> mappers) {
            this.connection = connection;
            this.mappers = mappers;
    }
    
    /**
     * 控制代理对象的执行方法逻辑 增强,操作数据
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            PreparedStatement preparedStatement = null;
            ResultSet resultSet = null;
        try {
            //1. 获得方法名(id)
            String methodName = method.getName();
            //2. 获得当前方法所在类的全限定名(namespace)
            String className = method.getDeclaringClass().getName();
            //3. 获得mapper对象
            Mapper mapper = mappers.get(className + "." + methodName);
            //4.获得sql语句和结果类型
            String sql = mapper.getSql();
            String resultType = mapper.getResultType();
            //5创建预编译sql语句对象
            preparedStatement = connection.prepareStatement(sql);
            //6.执行
            resultSet = preparedStatement.executeQuery();
            // 6.1 把resultSet封装成List集合,list里面的对象类型就是resultType
            List list = ResultUtils.handleResult(resultSet, resultType);
            return list;
        } finally {
            //  释放资源。。。。。。。。。。。
            if (resultSet != null) {
                resultSet.close();
            }
            if (preparedStatement != null){
                preparedStatement.close();
            }
        }
    }
}

结果集工具类

public class ResultUtils {
    /**
     * 把结果集,封装成List
     * @param resultSet  结果集
     * @param className 类的全限定名
     * @return
     * @throws Exception
     */
    public static List handleResult(ResultSet resultSet, String className) throws Exception {
        List list = new ArrayList();
        Class clazz = Class.forName(className);
        //1.遍历结果集, 没遍历一次创建一个对象
        while(resultSet.next()){
            Object obj = clazz.newInstance();
            // 2. 获得元数据
            ResultSetMetaData metaData = resultSet.getMetaData();
            //3. 获得列的个数
            int columnCount = metaData.getColumnCount();
            for(int i = 1; i <= columnCount;i++){
                //4. 获得列名(其实就是对象的属性名)
                String columnName = metaData.getColumnName(i);
                //5. 反射得到字段对象
                Field declaredField = clazz.getDeclaredField(columnName);
                declaredField.setAccessible(true);
                //6. 获得当前列的数据
                Object value = resultSet.getObject(columnName);
                //7. 赋值
                declaredField.set(obj,value);
            }
            list.add(obj);
        }
        return  list;
    }

}

对了释放资源方法还没完成,完成以下

      @Override
    public void close() {
        if(connection != null){
            try {
                connection.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }

大功告成!!!跑起来

在这里插入图片描述

这个简单的自定义MyBatis希望可以帮助到大家更好理解MyBatis
当然真正的MyBatis比这个要复杂很多…

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值