Mybatis(2)-自定义mybatis分析(理解其原理)
一、流程分析
1.我们通过前面的例子发现我们执行的顺序为:
//1.读取配置文件 InputStream in = Resources.getResourceAsStream("SqlMapConfig.xml"); //2.创建SqlSessionFactory工厂 SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder(); SqlSessionFactory factory = builder.build(in); //3.使用工厂生产SqlSession对象 SqlSession session = factory.openSession(); //4.使用SqlSession创建Dao接口的代理对象 IUserDao userDao = session.getMapper(IUserDao.class); //5.使用代理对象执行方法 List<User> users = userDao.findAll(); for(User user : users){ System.out.println(user); } //6.释放资源 session.close(); in.close();
2.分析上述流程,首先我们读取的是SqlMapConfig.xml配置文件,此时是一个解析xml的过程(dom4j技术解析),然后通过封装之前JDBC模式进行数据库连接创建,sql执行等操作。
//2.创建SqlSessionFactory工厂 SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder(); SqlSessionFactory factory = builder.build(in);
此处涉及构造者模式(Builder 模式)。
(1)读取xml文件转输入流
// 1.读取配置文件 InputStream in = Resources.getResourceAsStream("SqlMapConfig.xml");
(2)读取SqlMapConfig.xml配置文件数据库相关内容,解析xml,封装Configuration。
读取内如:
<environments default="mysql"> <!-- 配置mysql环境 --> <environment id="mysql"> <!-- 配置事务类型 --> <transactionManager type="JDBC"></transactionManager> <!-- 配置数据源 --> <dataSource type="POOLED"> <property name="driver" value="com.mysql.cj.jdbc.Driver" /> <property name="url" value="jdbc:mysql://localhost:3306/easy_mybatis?autoReconnect=true&useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=CONVERT_TO_NULL&useSSL=false&serverTimezone=CTT" /> <property name="username" value="root" /> <property name="password" value="root" /> </dataSource> </environment> </environments>
(3)同时读取SqlMapConfig.xml配置文件数据库相关内容,解析xml
读取SqlMapConfig.xml中的映射配置信息:
<!-- 配置映射文件 --> <mappers> <mapper class="com.xhbjava.dao.IUserDao" /> </mappers>
然后通过映射配置文件读取sql:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.xhbjava.dao.IUserDao">
<select id="findAll" resultType="com.xhbjava.domain.User">
select * from user;
</select>
</mapper>
读取这些配置文件解析,并将对应的类方法作为key,sql和返回类型封装成map作为value再添加进去map中,然后作为Configuration属性返回
(4)生产SqlSession对象,其实就是建立数据库连接并返回数据库连接。
//3.使用工厂生产SqlSession对象 SqlSession session = factory.openSession();
(5)使用动态代理,增强方法,将结果封装成List,返回代理对象
//4.使用SqlSession创建Dao接口的代理对象 IUserDao userDao = session.getMapper(IUserDao.class);
(6)执行查询方法,返回结果
//5.使用代理对象执行方法 List<User> users = userDao.findAll();
二、具体代码分析实现(重点)
项目结构:
(1)从上面测试类分析我们发现涉及class Resources、class SqlSessionFactoryBuilder、interface SqlSessionFactory和interface SqlSession,我们按照流程进行代码完善。
(1)创建Maven工程
(2)引入程序依赖jar包
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.xhbjava</groupId> <artifactId>Mybatis01</artifactId> <packaging>war</packaging> <version>0.0.1-SNAPSHOT</version> <name>Mybatis01 Maven Webapp</name> <url>http://maven.apache.org</url> <dependencies> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.15</version> </dependency> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.12</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> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>3.8.1</version> <scope>test</scope> </dependency> </dependencies> <build> <finalName>Mybatis01</finalName> </build> </project>
(3)编写Resources类
package com.xhbjava.mybatis.io; import java.io.IOException; import java.io.InputStream; /** * 使用类加载器读取配置文件的类 * * @author Mr.wang * @date 2020年1月13日 */ public class Resources { /** * 根据出入参数获取一个字节流输入 * * @param filePath * @return * @throws IOException */ public static InputStream getResourceAsStream(String filePath) throws IOException { return Resources.class.getClassLoader.getResourceAsStream(filePath); } }
(4)编写SqlSessionFactoryBuilder类,同时创建SqlSessionFactory接口。
package com.xhbjava.mybatis.sqlsession; import java.io.InputStream; /** * 该类用于创建SqlSessionFactory对象 * * @author Mr.wang * @date 2020年1月13日 */ public class SqlSessionFactoryBuilder { /** * 创建build方法 * * @param config * @return */ public SqlSessionFactory build(InputStream config) { return null; } }
package com.xhbjava.mybatis.sqlsession; public interface SqlSessionFactory { /** * 用于打开一个新的SqlSession对象 * * @return */ SqlSession openSession(); }
(5)创建SqlSession接口
package com.itheima.mybatis.sqlsession; /** * * @author Mr.wang * @date 2020年1月13日 */ public interface SqlSession { /** * 根据参数创建一个代理对象 * @param daoInterfaceClass dao的接口字节码 * @param <T> * @return */ <T> T getMapper(Class<T> daoInterfaceClass); /** * 释放资源 */ void close(); }
(6)创建XMLConfigBuilder工具类以及关联的类
关联类:
package com.xhbjava.mybatis.cfg; /** * 用于封装执行的SQL语句和结果类型的全限定类名 * * @author Mr.wang * @date 2020年1月13日 */ public class Mapper { private String queryString; 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; } }
package com.xhbjava.mybatis.cfg; import java.util.Map; /** * 自定义配置类 * * @author Mr.wang * @date 2020年1月13日 */ public class Configuration { private String driver; private String url; private String username; private String password; private Map<String,Mapper> mappers = new HashMap<String,Mapper>();; public Map<String, Mapper> getMappers() { return this.mappers; } public void setMappers(Map<String, Mapper> mappers) { this.mappers.putAll(mappers); } public String getDriver() { return driver; } public void setDriver(String driver) { this.driver = driver; } public String getUrl() { return url; } public void setUrl(String url) { this.url = url; } 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; } }
package com.xhbjava.mybatis.annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * c查询注解 * * @author Mr.wang * @date 2020年1月13日 */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface Select { /** * 配置SQL语句的 * * @return */ String value(); }
工具类:
package com.xhbjava.mybatis.util; import org.dom4j.Attribute; import org.dom4j.Document; import org.dom4j.Element; import org.dom4j.io.SAXReader; import com.xhbjava.mybatis.annotation.Select; import com.xhbjava.mybatis.cfg.Configuration; import com.xhbjava.mybatis.cfg.Mapper; import com.xhbjava.mybatis.io.Resources; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Method; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.HashMap; import java.util.List; import java.util.Map; /** * 用于解析配置文件 * * @author Mr.wang * @date 2020年1月13日 */ public class XMLConfigBuilder { /** * 解析主配置文件,把里面的内容填充到DefaultSqlSession所需要的地方 使用的技术: dom4j+xpath */ public static Configuration loadConfiguration(InputStream config) { try { // 定义封装连接信息的配置对象(mybatis的配置对象) Configuration cfg = new Configuration(); // 1.获取SAXReader对象 SAXReader reader = new SAXReader(); // 2.根据字节输入流获取Document对象 Document document = reader.read(config); // 3.获取根节点 Element root = document.getRootElement(); // 4.使用xpath中选择指定节点的方式,获取所有property节点 List<Element> propertyElements = root.selectNodes("//property"); // 5.遍历节点 for (Element propertyElement : propertyElements) { // 判断节点是连接数据库的哪部分信息 // 取出name属性的值 String name = propertyElement.attributeValue("name"); if ("driver".equals(name)) { // 表示驱动 // 获取property标签value属性的值 String driver = propertyElement.attributeValue("value"); cfg.setDriver(driver); } if ("url".equals(name)) { // 表示连接字符串 // 获取property标签value属性的值 String url = propertyElement.attributeValue("value"); cfg.setUrl(url); } if ("username".equals(name)) { // 表示用户名 // 获取property标签value属性的值 String username = propertyElement.attributeValue("value"); cfg.setUsername(username); } if ("password".equals(name)) { // 表示密码 // 获取property标签value属性的值 String password = propertyElement.attributeValue("value"); cfg.setPassword(password); } } // 取出mappers中的所有mapper标签,判断他们使用了resource还是class属性 List<Element> mapperElements = root.selectNodes("//mappers/mapper"); // 遍历集合 for (Element mapperElement : mapperElements) { // 判断mapperElement使用的是哪个属性 Attribute attribute = mapperElement.attribute("resource"); if (attribute != null) { System.out.println("使用的是XML"); // 表示有resource属性,用的是XML // 取出属性的值 String mapperPath = attribute.getValue();// 获取属性的值"com/itheima/dao/IUserDao.xml" // 把映射配置文件的内容获取出来,封装成一个map Map<String, Mapper> mappers = loadMapperConfiguration(mapperPath); // 给configuration中的mappers赋值 cfg.setMappers(mappers); } else { System.out.println("使用的是注解"); // 表示没有resource属性,用的是注解 // 获取class属性的值 String daoClassPath = mapperElement.attributeValue("class"); // 根据daoClassPath获取封装的必要信息 Map<String, Mapper> mappers = loadMapperAnnotation(daoClassPath); // 给configuration中的mappers赋值 cfg.setMappers(mappers); } } // 返回Configuration return cfg; } catch (Exception e) { throw new RuntimeException(e); } finally { try { config.close(); } catch (Exception e) { e.printStackTrace(); } } } /** * 根据传入的参数,解析XML,并且封装到Map中 * * @param mapperPath 映射配置文件的位置 * @return map中包含了获取的唯一标识(key是由dao的全限定类名和方法名组成) * 以及执行所需的必要信息(value是一个Mapper对象,里面存放的是执行的SQL语句和要封装的实体类全限定类名) */ private static Map<String, Mapper> loadMapperConfiguration(String mapperPath) throws IOException { InputStream in = null; try { // 定义返回值对象 Map<String, Mapper> mappers = new HashMap<String, Mapper>(); // 1.根据路径获取字节输入流 in = Resources.getResourceAsStream(mapperPath); // 2.根据字节输入流获取Document对象 SAXReader reader = new SAXReader(); Document document = reader.read(in); // 3.获取根节点 Element root = document.getRootElement(); // 4.获取根节点的namespace属性取值 String namespace = root.attributeValue("namespace");// 是组成map中key的部分 // 5.获取所有的select节点 List<Element> selectElements = root.selectNodes("//select"); // 6.遍历select节点集合 for (Element selectElement : selectElements) { // 取出id属性的值 组成map中key的部分 String id = selectElement.attributeValue("id"); // 取出resultType属性的值 组成map中value的部分 String resultType = selectElement.attributeValue("resultType"); // 取出文本内容 组成map中value的部分 String queryString = selectElement.getText(); // 创建Key String key = namespace + "." + id; // 创建Value Mapper mapper = new Mapper(); mapper.setQueryString(queryString); mapper.setResultType(resultType); // 把key和value存入mappers中 mappers.put(key, mapper); } return mappers; } catch (Exception e) { throw new RuntimeException(e); } finally { in.close(); } } /** * 根据传入的参数,得到dao中所有被select注解标注的方法。 根据方法名称和类名,以及方法上注解value属性的值,组成Mapper的必要信息 * * @param daoClassPath * @return */ private static Map<String, Mapper> loadMapperAnnotation(String daoClassPath) throws Exception { // 定义返回值对象 Map<String, Mapper> mappers = new HashMap<String, Mapper>(); // 1.得到dao接口的字节码对象 Class daoClass = Class.forName(daoClassPath); // 2.得到dao接口中的方法数组 Method[] methods = daoClass.getMethods(); // 3.遍历Method数组 for (Method method : methods) { // 取出每一个方法,判断是否有select注解 boolean isAnnotated = method.isAnnotationPresent(Select.class); if (isAnnotated) { // 创建Mapper对象 Mapper mapper = new Mapper(); // 取出注解的value属性值 Select selectAnno = method.getAnnotation(Select.class); String queryString = selectAnno.value(); mapper.setQueryString(queryString); // 获取当前方法的返回值,还要求必须带有泛型信息 Type type = method.getGenericReturnType();// List<User> // 判断type是不是参数化的类型 if (type instanceof ParameterizedType) { // 强转 ParameterizedType ptype = (ParameterizedType) type; // 得到参数化类型中的实际类型参数 Type[] types = ptype.getActualTypeArguments(); // 取出第一个 Class domainClass = (Class) types[0]; // 获取domainClass的类名 String resultType = domainClass.getName(); // 给Mapper赋值 mapper.setResultType(resultType); } // 组装key的信息 // 获取方法的名称 String methodName = method.getName(); String className = method.getDeclaringClass().getName(); String key = className + "." + methodName; // 给map赋值 mappers.put(key, mapper); } } return mappers; } }
(7)SqlSessionFactory接口的实现类
package com.xhbjava.mybatis.sqlsession.defaults; import com.xhbjava.mybatis.cfg.Configuration; import com.xhbjava.mybatis.sqlsession.SqlSession; import com.xhbjava.mybatis.sqlsession.SqlSessionFactory; /** * SqlSessionFactory接口的实现类 * * @author Mr.wang * @date 2020年1月14日 */ public class DefaultSqlSessionFactory implements SqlSessionFactory { private Configuration cfg; public DefaultSqlSessionFactory(Configuration cfg) { this.cfg = cfg; } /** * 用于创建一个新的操作数据库对象 * * @return */ public SqlSession openSession() { return new DefaultSqlSession(cfg); } }
(8)完善SqlSessionFactoryBuilder中bulid方法
public SqlSessionFactory build(InputStream config) { Configuration cfg = XMLConfigBuilder.loadConfiguration(config); return new DefaultSqlSessionFactory(cfg); }
(9)完善SqlSession接口实现类,实现获取数据库链接
private Configuration cfg; private Connection connection; public DefaultSqlSession(Configuration cfg){ this.cfg = cfg; connection = DataSourceUtil.getConnection(cfg); }
添加DataSourceUtil类
package com.xhbjava.mybatis.util; import java.sql.Connection; import java.sql.DriverManager; import com.xhbjava.mybatis.cfg.Configuration; /** * 用于创建数据源的工具类 * * @author Mr.wang * @date 2020年1月14日 */ public class DataSourceUtil { /** * 用于获取一个连接 * * @param cfg * @return */ public static Connection getConnection(Configuration cfg) { try { Class.forName(cfg.getDriver()); return DriverManager.getConnection(cfg.getUrl(), cfg.getUsername(), cfg.getPassword()); } catch (Exception e) { throw new RuntimeException(e); } } }
(10)创建MapperProxy类,完善DefaultSqlSession类实现SqlSession接口中的方法
package com.xhbjava.mybatis.sqlsession.defaults; import java.lang.reflect.Proxy; import java.sql.Connection; import com.xhbjava.mybatis.cfg.Configuration; import com.xhbjava.mybatis.sqlsession.SqlSession; import com.xhbjava.mybatis.util.DataSourceUtil; /** * SqlSession接口的实现类 * * @author Mr.wang * @date 2020年1月14日 */ public class DefaultSqlSession implements SqlSession { private Configuration cfg; private Connection connection; public DefaultSqlSession(Configuration cfg){ this.cfg = cfg; connection = DataSourceUtil.getConnection(cfg); } /** * 用于创建代理对象 * @param daoInterfaceClass dao的接口字节码 * @param <T> * @return */ public <T> T getMapper(Class<T> daoInterfaceClass) { return (T) Proxy.newProxyInstance(daoInterfaceClass.getClassLoader(), new Class[]{daoInterfaceClass},new MapperProxy(cfg.getMappers(),connection)); } /** * 用于释放资源 */ public void close() { if(connection != null) { try { connection.close(); } catch (Exception e) { e.printStackTrace(); } } } }
(11)添加Executor类,完善MapperProxy类
package com.xhbjava.mybatis.util; import java.beans.PropertyDescriptor; import java.lang.reflect.Method; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.ResultSetMetaData; import java.util.ArrayList; import java.util.List; import com.xhbjava.mybatis.cfg.Mapper; /** * 负责执行SQL语句,并且封装结果集,selectList实现 * @author Mr.wang *@date 2020年1月14日 */ public class Executor { public <E> List<E> selectList(Mapper mapper, Connection conn) { PreparedStatement pstm = null; ResultSet rs = null; try { // 1.取出mapper中的数据 String queryString = mapper.getQueryString();// select * from user String resultType = mapper.getResultType();// com.xhb.domain.User Class domainClass = Class.forName(resultType); // 2.获取PreparedStatement对象 pstm = conn.prepareStatement(queryString); // 3.执行SQL语句,获取结果集 rs = pstm.executeQuery(); // 4.封装结果集 List<E> list = new ArrayList<E>();// 定义返回值 while (rs.next()) { // 实例化要封装的实体类对象 E obj = (E) domainClass.newInstance(); // 取出结果集的元信息:ResultSetMetaData ResultSetMetaData rsmd = rs.getMetaData(); // 取出总列数 int columnCount = rsmd.getColumnCount(); // 遍历总列数 for (int i = 1; i <= columnCount; i++) { // 获取每列的名称,列名的序号是从1开始的 String columnName = rsmd.getColumnName(i); // 根据得到列名,获取每列的值 Object columnValue = rs.getObject(columnName); // 给obj赋值:使用Java内省机制(借助PropertyDescriptor实现属性的封装) PropertyDescriptor pd = new PropertyDescriptor(columnName, domainClass);// 要求:实体类的属性和数据库表的列名保持一种 // 获取它的写入方法 Method writeMethod = pd.getWriteMethod(); // 把获取的列的值,给对象赋值 writeMethod.invoke(obj, columnValue); } // 把赋好值的对象加入到集合中 list.add(obj); } return list; } catch (Exception e) { throw new RuntimeException(e); } finally { release(pstm, rs); } } private void release(PreparedStatement pstm, ResultSet rs) { if (rs != null) { try { rs.close(); } catch (Exception e) { e.printStackTrace(); } } if (pstm != null) { try { pstm.close(); } catch (Exception e) { e.printStackTrace(); } } } }
(12)此时基本完成代码工作,可以点击测试类进行测试。
总结:记录本代码逻辑,还需多看多理解多动手。