一、前言
在手写mybatis框架之前 ,我们先来思考一下这个问题:为啥要有mybatis框架存在?它是为了解决什么问题的?我们带着这两个问题来开始我们手写mybatis框架之旅。
我们刚开始搞java的时候,貌似都知道用jdbc去连接数据库,那我们来看一下jdbc连接数据库的代码:
public static void main(String[] args) { Connection conn = null; PreparedStatement ps = null; ResultSet rs = null; try { // 加载数据库驱动 Class.forName("com.mysql.jdbc.Driver"); // 通过驱动管理类获取数据库连接 conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/mybatis?characterEncoding=utf-8", "root", "root"); // 定义sql语句 ?标识占位符 String sql = "select * from user where id = ? aand username = ?"; // 获取预处理statement ps = conn.prepareStatement(sql); // 设置参数,第一个参数为sql语句中参数的序号(从1开始),第二个参数为 设置的参数值 ps.setString(1, "riemann"); // 向数据库发出sql执行查询,查询出结果集 rs = ps.executeQuery(); // 遍历结果集 while (rs.next()) { int id = rs.getInt("id"); String username = rs.getString("username"); // 封装User user.setId(id); user.setUsername(username); } System.out.println(user); } catch (Exception e) { e.printStackTrace(); } finally { // 释放资源 if (rs != null) { try { rs.close(); } catch (SQLException e) { e.printStackTrace(); } } if (ps != null) { try { ps.close(); } catch (SQLException e) { e.printStackTrace(); } } if (conn != null) { try { conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } }
相信有一定年限工作年限的小伙伴们,不难发现jdbc操作的问题。
二、JDBC问题分析
- 数据库连接创建、释放频繁造成系统资源浪费,从而影响性能。
- sql语句存在硬编码,造成代码不易维护。
- 使用preparedStatement向占有位符号传参数存在硬编码问题。
- 对结果解析存在硬编码(查询列名),sql变化导致解析代码变化。
知道了JDBC存在的一些问题,我们就来对症下药找解决方案。
1、问题解决方案
- 数据库频繁创建连接以及释放资源:
连接池 - sql语句及参数存在硬编码:
配置文件 - 手动解析封装返回结果集:
反射、内省
这也就解释了前面提的问题,为什么要有mybatis框架的存在?这是因为为了解决JDBC操作存在的这些问题。
好了,下面我们就来针对上面的解决方案来设计一个mybatis框架。
2、自定义框架设计
2.1 客户端
提供核心配置文件
SqlMapConfig.xml: 存放数据源配置信息Mapper.xml: sql语句的配置文件信息
2.2 框架端
2.2.1 读取配置文件
读取完以后以流的形式存在,我们不能讲读取到的配置信息以流的形式存放在内存中,不好操作,可以创建JavaBean来存储。
Configuration:存放数据库基本信息和Map<唯一标识,Mapper> 唯一标识:namespace+"."+id。
MappedStatement:sql语句、statement类型、输入参数java类型、输出参数java类型。
2.2.2 解析配置文件
创建SqlSessionFactoryBuilder类:
方法:SqlSessionFactorybuild();
使用dom4j解析配置文件,将解析出来的内容封装到 Configuration 和 MappedStatement 中
创建SqlSessionFactory的实现类DefaultSqlSession
2.2.3 创建SqlSessionFactory
方法:openSession():获取SqlSession接口的实现类实例对象
2.2.4 创建SqlSession接口及实现类:主要封装CRUD方法
方法:
selectList(String statementId, Object... params); // 查询所有
selectOne(String statementId, Object... params); // 查询单个
close(); 释放资源
具体实现:封装JDBC完成对数据库表的查询操作
3、设计模式
构建者模式、工厂模式 、代理模式
三、自定义框架实现
在客户端项目中创建配置文件:
1、创建SqlMapConfig.xml
<configuration> <!--数据库配置信息--> <dataSource> <property name="driverClass" value="com.mysql.jdbc.Driver"></property> <property name="jdbcUrl" value="jdbc:mysql:///mybatis"></property> <property name="username" value="root"></property> <property name="password" value="root"></property> </dataSource> <!--存放mapper.xml的全路径--> <mapper resource="UserMapper.xml"></mapper> </configuration>
2、创建UserMapper.xml
<mapper namespace="user"> <!--sql的唯一标识:namespace.id来组成 :statementId--> <select id="selectList" resultType="com.riemann.pojo.User"> select * from user </select> <!-- User user = new User(); user.setId(1); user.setUsername("riemann"); --> <select id="selectOne" resultType="com.riemann.pojo.User" parameterType="com.riemann.pojo.User"> select * from user where id = #{id} and username = #{username} </select> </mapper>
接下来就来框架端编写相应的类:
3、User对象
@Data public class User { private Integer id; private String username; }
4、在父工程中引入依赖
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> </dependency> <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>28.2-jre</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.12</version> <scope>provided</scope> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.9</version> </dependency> </dependencies>
5、在子工程中mybatis-persistence引入依赖
<dependencies> <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> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.17</version> </dependency> <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>
6、完成框架端中的读取配置文件
6.1 编写Configuration类
@Data public class Configuration { private DataSource dataSource; /** * key:statementId * value:封装好的MappedStatement */ Map<String, MappedStatement> mappedStatementMap = Maps.newHashMap();
6.2 编写MappedStatement类
@Data public class MappedStatement { // id标识 private String id; // 返回值类型 private String resultType; // 参数值类型 private String parameterType; // sql语句 private String sql; }
7、完成框架端中的解析配置文件
7.1 编写Resources类,读取客户端的xml文件
public class Resources { /** * 根据配置文件的路径,将配置文件加载成字节输入流,存储在内存中。 * @param path 文件路径 * @return 字节流 */ public static InputStream getResourceAsStream(String path) { InputStream resourceAsStream = Resources.class.getClassLoader().getResourceAsStream(path); return resourceAsStream; } }
7.2 编写SqlSessionFactoryBuilder类
- 使用dom4j解析配置文件,将解析出来的内容封装到Configuration
- 创建SqlSessionFactory对象:工厂类:生产SqlSession:会话对象
public class SqlSessionFactoryBuilder { public SqlSessionFactory build(InputStream in) throws DocumentException, PropertyVetoException { // 1.使用dom4j解析配置文件,将解析出来的内容封装到Configuration XMLConfigBuilder xmlConfigBuilder = new XMLConfigBuilder(); Configuration configuration = xmlConfigBuilder.parseConfig(in); // 2.创建SqlSessionFactory对象:工厂类:生产SqlSession:会话对象 DefaultSqlSessionFactory defaultSqlSessionFactory = new DefaultSqlSessionFactory(configuration); return defaultSqlSessionFactory; } }
7.3 编写XMLConfigBuilder类
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; } }
7.4 编写XMLMapperBuilder类
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); String key = namespace + "." + id; configuration.getMappedStatementMap().put(key, mappedStatement); } } }
7.5 SqlSessionFactory接口以及DefaultSqlSessionFactory实现类
public interface SqlSessionFactory { SqlSession openSession(); }
public class DefaultSqlSessionFactory implements SqlSessionFactory { private Configuration configuration; public DefaultSqlSessionFactory(Configuration configuration) { this.configuration = configuration; } @Override public SqlSession openSession() { return new DefaultSqlSession(configuration); } }
7.6SqlSession接口以及DefaultSqlSession实现类
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; }
public class DefaultSqlSession implements SqlSession { private Configuration configuration; public DefaultSqlSession(Configuration configuration) { this.configuration = configuration; } @Override public <E> List<E> selectList(String statementId, Object... params) throws Exception { // 将要去完成对SimpleExecutor里的query方法的调用 SimpleExecutor simpleExecutor = new SimpleExecutor(); MappedStatement mappedStatement = configuration.getMappedStatementMap().get(statementId); List<Object> list = simpleExecutor.query(configuration, mappedStatement, params); return (List<E>) list; } @Override public <T> T selectOne(String statementId, Object... params) throws Exception { List<Object> objects = selectList(statementId, params); if (objects.size() == 1) { return (T) objects.get(0); } else { throw new RuntimeException("查询结果为空或者结果过多!"); } } }
7.7Executor接口以及SimpleExecutor实现类
public interface Executor { <E> List<E> query(Configuration configuration, MappedStatement mappedStatement, Object... params) throws SQLException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException, IntrospectionException, InstantiationException, Exception; }
public class SimpleExecutor implements Executor { @Override public <E> List<E> query(Configuration configuration, MappedStatement mappedStatement, Object... params) throws Exception { // 1.注册驱动,获取连接 Connection connection = configuration.getDataSource().getConnection(); // 2.获取sql语句 : select * from user where id = #{id} and username = #{username} // 转换sql语句 : select * from user where id = ? and username = ? // 转换的过程中 : 还要对#{}里面的值进行解析存储 String sql = mappedStatement.getSql(); BoundSql boundSql = getBoundSql(sql); // 3.获取预处理对象:preparedStatement PreparedStatement preparedStatement = connection.prepareStatement(boundSql.getSqlText()); // 4.设置参数 // 获取到了参数的全路径 String parameterType = mappedStatement.getParameterType(); Class<?> parameterTypeClass = getClassType(parameterType); List<ParameterMapping> parameterMappingList = boundSql.getParameterMappingList(); for (int i = 0; i < parameterMappingList.size(); i++) { ParameterMapping parameterMapping = parameterMappingList.get(i); String content = parameterMapping.getContent(); // 反射 Field declaredField = parameterTypeClass.getDeclaredField(content); // 暴力访问,防止访问的字段是private修饰 declaredField.setAccessible(true); Object obj = declaredField.get(params[0]); preparedStatement.setObject(i + 1, obj); } // 5.执行sql ResultSet resultSet = preparedStatement.executeQuery(); String resultType = mappedStatement.getResultType(); Class<?> resultTypeClass = getClassType(resultType); Object o = resultTypeClass.newInstance(); List<Object> objects = Lists.newArrayList(); // 6.封装返回结果集 while (resultSet.next()) { // 元数据 ResultSetMetaData metaData = resultSet.getMetaData(); for (int i = 1; i <= metaData.getColumnCount(); i++) { // 字段名 String columnName = metaData.getColumnName(i); // 字段的值 Object value = resultSet.getObject(columnName); // 使用反射或者内省,根据数据库表和实体的对应关系,完成封装 PropertyDescriptor propertyDescriptor = new PropertyDescriptor(columnName, resultTypeClass); Method writeMethod = propertyDescriptor.getWriteMethod(); writeMethod.invoke(o, value); } objects.add(o); } return (List<E>) objects; } private Class<?> getClassType(String parameterType) throws ClassNotFoundException { if (parameterType != null) { Class<?> clazz = Class.forName(parameterType); return clazz; } return null; } /** * 完成对#{}的解析工作: * 1.将#{}使用?进行代替 * 2.解析出#{}里面的值进行存储 * @param sql 原生sql * @return 解析后的sql */ private BoundSql getBoundSql(String sql) { // 1.标记处理类:配置标记解析器来完成对占位符的解析处理工作 ParameterMappingTokenHandler parameterMappingTokenHandler = new ParameterMappingTokenHandler(); GenericTokenParser genericTokenParser = new GenericTokenParser("#{", "}", parameterMappingTokenHandler); // 2.解析出来的sql String parseSql = genericTokenParser.parse(sql); // 3.#{}里面解析出来的参数名称 List<ParameterMapping> parameterMappings = parameterMappingTokenHandler.getParameterMappings(); BoundSql boundSql = new BoundSql(parseSql, parameterMappings); return boundSql; } }
四、自定义框架优化
前面我们提到的问题都解决完了,但是我们继续分析刚刚完成的自定义框架代码,还没有什么问题?
问题如下:
- dao的实现类中存在重复代码,整个操作的过程模板重复(创建SqlSession、调用SqlSession方法、关闭SqlSession )。
- dao的实现类存在硬编码,调用SqlSession的方法时,参数statementId硬编码。
解决思路:
使用代理模式生成dao层接口的代理对象!!!
在 SqlSession 接口中增加getMapper方法
/** * 为Dao层接口生成代理实现类 * @param mapperClass 字节码 * @param <T> 泛型 * @return 某一对象 */ <T> T getMapper(Class<?> mapperClass) throws Exception;
DefaultSqlSession 实现类
@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; // 准备参数 2.params:args // 获取被调用方法的返回值类型 Type genericReturnType = method.getGenericReturnType(); // 判断是否进行了泛型类型参数化 if (genericReturnType instanceof ParameterizedType) { List<Object> objects = selectList(statementId, args); return objects; } return selectOne(statementId, args); } }); return (T) proxyInstance; }
代理模式的过程如下图:
invoke()方法中的三个参数:
- Object proxy 当前代理对象的引用
- Method method 当前被调用方法的的引用
- Object[] args 传递的参数
最后我们来测试一下优化后的结果:
public class PersistenceTest { @Test public void test() throws Exception { InputStream resourceAsStream = Resources.getResourceAsStream("SqlMapConfig.xml"); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream); SqlSession sqlSession = sqlSessionFactory.openSession(); // 调用 User user = new User(); user.setId(1); user.setUsername("riemann"); // User user2 = sqlSession.selectOne("user.selectOne", user); // System.out.println(user2); UserDao userDao = sqlSession.getMapper(UserDao.class); User user2 = userDao.findByCondition(user); System.out.println(user2); } }
日志打印如下:
User(id=1, username=riemann)
单个对象的OK,我们再来测一下多个的对象的。
List<User> all = userDao.findAll(); System.out.println(all);
日志打印如下:
[User(id=3, username=edgar), User(id=3, username=edgar), User(id=3, username=edgar)]
什么鬼?多个对象的结果输出显示都是数据库中最后的那条记录。
分析问题:
断点跟踪发现,原来是SimpleExecutor这个类的query方法中有点小问题:
解决完上面以后,测试结果如下:
[User(id=1, username=riemann), User(id=2, username=andy), User(id=3, username=edgar)]
结果正确!!!这下我们就优化好了使用代理模式来生成dao层接口的代理对象,解决了dao的实现类中存在重复代码以及dao的实现类存在硬编码。
五、代码仓库
你居然看完了!!!那必须附上仓库完整代码地址。
https://github.com/riemannChow/perseverance/tree/master/handwriting-framework/mybatis
欢迎小伙伴们关注我的公众号,Java后端主流技术栈的原理、源码分析、架构以及各种互联网高并发、高性能、高可用的解决方案。
喜欢的话,点赞、再看、分享三连。


