未来半年,有幸与导师们一起学习交流,趁这个机会,把所学所感记录下来:
一来是自毕业以后,自己先创业后上班,浮沉了近8年,内心着实焦躁,虽说一直是走科班路线,但在技术道路上却始终没静下心来研究、思考、梳理,机会来了,便抓牢。
二来希望自己记录下来的知识内容,对后来的学习之人,能有些许帮助。
对文章内容有任何建议或意见,或对互联网开发有希望交流学习,或单纯热爱生活
都欢迎随时微信我:panshenlian
第一个系列的文章主要围绕「架构师(Java)技术条线 」展开聊,不定时更新。
第一篇我以《手写一套持久层框架 》先来打个样,本篇文章我们先不介绍MyBatis,也不会分析源码,我们先聊一个 Java API:JDBC 。
JDBC 是Java的老朋友,我们再一次认识他吧,挑挑他的毛病,站在Java资老朋友的角度,给他提点优化意见,并送他一套《自定义持久层框架 》。
温馨提示:
如果大家在阅读过程中,对某些解决思路存在疑问,我建议大家先带着疑问阅读完,消化理解,因为导师们确实是通过研究Mybatis等持久层框架源码之后,反过来剖析的。
简单来说 “ 大厂都这么写,我们且这么跟随吧 ”。
Mybatis 全解系列脑图全览一直在更新哦
Mybaits系列全解 ( 传送门 )
目录 1、JDBC是谁?
2、JDBC如何工作?
3、JDBC存在哪些待优化的地方?
4、自定义持久层框架:思路分析
5、自定义持久层框架:编码
6、总结
一、JDBC是谁?
JDBC是谁?干啥的?到底有多能打?看看网络上的朋友们怎么说。
Java数据库连接,(Java Database Connectivity,简称JDBC)是Java语言中用来规范客户端程序如何来访问数据库的应用程序接口,提供了诸如查询和更新数据库中数据的方法。
– 来自百度百科
JDBC(Java DataBase Connectivity,java数据库连接)是一种用于执行SQL语句的Java API,可以为多种关系数据库提供统一访问,它由一组用Java语言编写的类和接口组成。JDBC提供了一种基准,据此可以构建更高级的工具和接口,使数据库开发人员能够编写数据库应用程序。
– 来自360百科
… 无法访问此网站
– 来自维基百科
以上基本就是JDBC的大致介绍,官方且严谨的说辞,That’s It , 我们往下看看,它曾经的高光时刻。
自从Java语言于1995年5月正式公布以来,Java风靡全球。出现大量的用java语言编写的程序,其中也包括数据库应用程序。由于没有一个Java语言的API,编程人员不得不在Java程序中加入C语言的ODBC函数调用。这就使很多Java的优秀特性无法充分发挥,比如平台无关性、面向对象特性等。随着越来越多的编程人员对Java语言的日益喜爱,越来越多的公司在Java程序开发上投入的精力日益增加,对java语言接口的访问数据库的API的要求越来越强烈。也由于ODBC的有其不足之处,比如它并不容易使用,没有面向对象的特性等等,SUN公司决定开发一Java语言为接口的数据库应用程序开发接口。在JDK1.x版本中,JDBC只是一个可选部件,到了JDK1.1公布时,SQL类包(也就是JDBCAPI)就成为Java语言的标准部件。
后面从JDBC1.0到JDBC4.0,一路发展。
– 来自网络
结合介绍说明加深我们对JDBC的了解。
不过,我想知道他平时是如何工作的?一张图 《 JDBC 基本架构 》 了解一下:
有了JDBC,向各种关系数据库发送SQL语句就是一件很容易的事。
换言之,有了JDBC API,就不必为访问Sybase数据库专门写一个程序,为访问Oracle数据库又专门写一个程序,或为访问Mysql数据库又编写另一个程序等等,程序员只需用JDBC API写一个程序就够了,它可向相应数据库发送SQL调用。
同时,将Java语言和JDBC结合起来使程序员不必为不同的平台编写不同的应用程序,只须写一遍程序就可以让它在任何平台上运行,这也是Java语言”编写一次,处处运行”的优势。
我们再来看看他工作的细节。
毕竟,有人说过:想了解一个人,就得先仔细了解Ta的工作。
二、JDBC如何工作?
JDBC API 允许应用程序访问任何形式的表格数据,特别是存储在关系数据库中的数据。 执行流程主要分三步:
连接数据源。
为数据库传递查询和更新指令。
处理数据库响应并返回的结果。
但实际上,每步流程都特别细节:
使用流程 (详细说明)
1.加载数据库驱动:
程序中使用Class.forName(‘驱动’)加载驱动,JVM会寻找并加载指定驱动类,同时执行驱动类的静态代码段,在JDK1.6之前JDBC规范中明确要求各家在实现Driver类时必须在静态代码段中向DriverManager注册实例,JDK1.6之后各家实现的Driver类则不再需要主动注册实例,因为DriverManager已经在初始化阶段对所有jar包中实现了java.sql.Driver的类进行扫描并进行初始化。
创建数据库连接:
DriverManager通过遍历所有已注册的驱动来尝试获取连接,第一个匹配上就会直接返回,并使用对应驱动建立起客户端与数据库服务器的网络连接(物理连接Socket了解一下)。
创建编译对象:
数据库连接connection成功之后,我们会向数据库发送一次请求(statement),执行一条sql语句,一个连接可以执行多次statement,除非你关闭连接,其中还有一个概念就是事务transaction,事务和请求可以是一对一,也可以是一对多,这取决于你是想把多个请求statement作为同一个事务提交,还是一个请求提交一次事务,JDBC默认是事务是自动提交,即auto-commit是打开的,所以默认是一对一。
设置入参执行SQL:
为了防止SQL注入,我们使用预处理在sql中使用?作为输入参数的占位符,sql在编译后成为安全的sql语句再进行查询(有缘我们可以聊聊为何预处理机制能防止SQL注入)。
封装返回结果集:
SQL执行之后会把结果集封装到ResultSet类,ResultSet类本身的迭代器初始行数的位置是1,所以我们会发现与java.util.Iterator接口的迭代初始行数为0有差异,同时ResultSet类本身没有提供hasNext方法,所以我们会不断的while(rs.next())往后定位,再通过不同的类型的访问器读取数据(例如getString,getInteger等)。
释放数据库连接资源:
考虑到数据库连接占用了数据库服务器的内存资源,所以不可能无限制建立连接,用完就释放,养成好习惯,目前很多成熟的数据连接池技术,很好的优化管理的数据连接问题。
我们通过一段简单的例子来演示一下使用流程,本例子使用JDBC操作mysql数据库,先看看我们最终的项目结构与JDBC API在JDK中rt.jar的结构:
默认已具备java开发环境、mysql数据库
创建mave工程,并且引入mysql驱动依赖
1 2 3 4 5 6 7 8 9 <dependencies > <dependency > <groupId > mysql</groupId > <artifactId > mysql-connector-java</artifactId > <version > 5.1.25</version > </dependency > </dependencies >
创建java测试类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 package com.panshenlian.jdbc;import com.panshenlian.po.User;import java.sql.*;public class Test01 { public static void main (String[] args) { User user = new User (); Connection connection = null ; PreparedStatement preparedStatement = null ; ResultSet resultSet = null ; try { Class.forName("com.mysql.jdbc.Driver" ); connection = DriverManager.getConnection( "jdbc:mysql://localhost:3306/mybatis" + "?characterEncoding=utf-8" , "root" ,"123456" ); String sql = " select * from user where username = ? " ; preparedStatement = connection.prepareStatement(sql); preparedStatement.setString(1 ,"panshenlian" ); resultSet = preparedStatement.executeQuery(); while (resultSet.next()){ int id = resultSet.getInt("id" ); String name = resultSet.getString("username" ); user.setId(id); user.setUserName(name); System.out.println(user); } } catch (Exception e) { e.printStackTrace(); } finally { if (resultSet!=null ){ try { resultSet.close(); } catch (SQLException e) { e.printStackTrace(); } } if (preparedStatement!=null ){ try { preparedStatement.close(); } catch (SQLException e) { e.printStackTrace(); } } if (connection!=null ){ try { connection.close(); } catch (SQLException e) { e.printStackTrace(); } } } } }
创建User类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 package com.panshenlian.po;public class User { private Integer id; private String userName; public Integer getId () { return id; } public void setId (Integer id) { this .id = id; } public String getUserName () { return userName; } public void setUserName (String userName) { this .userName = userName; } @Override public String toString () { return "User{" + "id=" + id + ", userName='" + userName + '\'' + '}' ; } }
创建sql语句
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 DROP TABLE IF EXISTS `user `;CREATE TABLE `user ` ( `id` int (11 ) NOT NULL AUTO_INCREMENT, `username` varchar (50 ) DEFAULT NULL , `password` varchar (50 ) DEFAULT NULL , `birthday` varchar (50 ) DEFAULT NULL , PRIMARY KEY (`id`) ) ENGINE= InnoDB AUTO_INCREMENT= 3 DEFAULT CHARSET= utf8; INSERT INTO `user ` VALUES ('1' , 'senly' , '123' , '2020-11-10' );INSERT INTO `user ` VALUES ('2' , 'panshenlian' , '123456' , '2020-11-10' );
执行结果,nice , 成功。
1 2 3 User{id=2 , userName='panshenlian'}
看完这段演示,大家是否发现一个问题?就是整个JDBC操作数据库的使用过程繁琐而尴尬,就如这场对话:
额(⊙o⊙)… JDBC你确实挺烦的。
我懂你需要和数据库建立连接、执行SQL语句、处理查询结果集…
但是,这整个过程,能不能优化一下呢?
三、JDBC存在哪些待优化的地方?
我们平时瘦身增肌,工作更得提质增效,来,我们剖开代码,逐个分析:
1 2 3 4 5 6 7 8 Class.forName("com.mysql.jdbc.Driver" ); connection = DriverManager.getConnection( "jdbc:mysql://localhost:3306/mybatis?characterEncoding=utf-8" , "root" ,"123456" );
存在问题1:数据库配置信息存在硬编码 问题。
优化思路:使用配置文件!
存在问题2:频繁创建、释放数据库连接 问题。
优化思路:使用数据连接池!
1 2 3 4 5 6 7 8 9 10 String sql = " select * from user where username = ? " ;preparedStatement = connection.prepareStatement(sql); preparedStatement.setString(1 ,"tom" ); resultSet = preparedStatement.executeQuery();
存在问题3:SQL语句、设置参数、获取结果集参数均存在硬编码 问题 。
优化思路:使用配置文件!
1 2 3 4 5 6 7 8 9 10 11 while (resultSet.next()){ int id = resultSet.getInt("id" ); String userName = resultSet.getString("username" ); user.setId(id); user.setUserName(userName); System.out.println(user); }
存在问题4:手动封装 返回结果集,较为繁琐。
优化思路:使用Java反射、自省!
针对JDBC各个环节中存在的不足,现在,我们整理出对应的优化思路,统一汇总:
存在问题
优化思路
数据库配置信息存在硬编码问题
使用配置文件
频繁创建、释放数据库连接问题
使用数据连接池
SQL语句、设置参数、获取结果集参数均存在硬编码问题
使用配置文件
手动封装返回结果集,较为繁琐
使用Java反射、自省
假如让你来优化,你会根据这些优化思路如何设计一套持久层框架呢?
四、自定义持久层框架:思路分析
JDBC是个人作战,凡事亲力亲为,低效而高险,自己加载驱动,自己建连接,自己 …
而持久层框架好比是多工种协作,分工明确,执行高效,有专门负责解析注册驱动建立连接的,有专门管理数据连接池的,有专门执行sql语句的,有专门做预处理参数的,有专门装配结果集的 …
1 2 3 框架的作用,就是为了帮助我们减去繁重开发细节与冗余代码,使我们能更加专注于业务应用开发。
来,我们一起看看使用JDBC和使用持久层框架有什么区别? 使用框架对于我们使用者(主要是研发人员),是有多舒爽呢?
是不是发现,拥有这么一套持久层框架是如此舒适,我们仅仅需要干两件事:
配置数据源 (地址/数据名/用户名/密码)
编写SQL与参数准备 (SQL语句/参数类型/返回值类型)
框架,除了思考本身的工程设计,还需要考虑到实际项目端的使用场景,干系方涉及两端:
以上两步,我们通过一张架构图《 手写持久层框架基本思路 》来梳理清楚:
核心接口/类重点说明:
分工协作
角色定位
类名定义
负责读取配置文件
资源辅助类
Resources
负责存储数据库连接信息
数据库资源类
Configuration
负责存储SQL映射定义、存储结果集映射定义
SQL与结果集资源类
MappedStatement
负责解析配置文件,创建会话工厂SqlSessionFactory
会话工厂构建者
SqlSessionFactoryBuilder
负责创建会话SqlSession
会话工厂
SqlSessionFactory
指派执行器Executor
会话
SqlSession
负责执行SQL (配合指定资源Mapped Statement)
执行器
Executor
正常来说项目只对应一套数据库环境,一般对应一个SqlSessionFactory实例对象,我们使用单例模式只创建一个SqlSessionFactory实例。
如果需要配置多套数据库环境,那需要做一些拓展,例如Mybatis中通过environments等配置就可以支持多套测试/生产数据库环境进行切换。
梳理完持久层框架的基本思路,明确了框架各角色分工,我们开始梳理详细方案: A、项目使用端,调用框架API,除了引入持久层框架的jar包之外,还需额外提供两部分配置信息:
1 2 3 4 1. sqlMapConfig.xml : 数据库配置信息(地址/数据名/用户名/密码),以及mapper.xml的全路径。 2. mapper.xml : SQL配置信息,存放SQL语句、参数类型、返回值类型相关信息。
B、框架本身,实质上就是对JDBC代码进行封装,基本6步:
加载配置文件:根据配置文件的路径,加载配置文件成字节输入流,存储在内存中。
1 2 3 创建Resource类,提供加载流方法:InputStream getResourceAsStream(String path)
创建两个javaBean(容器对象):存放配置文件解析出来的内容
1 2 3 4 Configuration(核心配置类):存放sqlMapConfig.xml解析出来的内容。 MappedStatement(映射配置类):存放mapper.xml解析出来的内容。
解析配置文件(使用dom4j) ,并创建SqlSession会话对象
1 2 3 4 5 创建类:SqlSessionFactoryBuilder 方法:build(InputStream in) > 使用dom4j解析配置文件,将解析出来的内容封装到容器对象中 > 创建SqlSessionFactory对象,生产sqlSession会话对象(工厂模式)
创建SqlSessionFactory接口以及实现类DefaultSqlSessionFactory
1 2 3 创建openSession()接口方法,生产sqlSession
创建SqlSession接口以及实现类DefaultSqlSession
1 2 3 4 5 6 7 定义对数据库的CRUD操作: > selectList(); > selectOne(); > update(); > delete();
创建Executor接口以及实现类SimpleExecutor
1 2 3 4 创建query(Configuration conf,MappedStatement ms,Object... params) 实际执行的就是JDBC代码。
基本过程我们已经清晰,我们再细化一下类图,更好的助于我们实际编码:
简约版
详细版
最终手写的持久层框架结构参考:
包接口类说明
接口/类
作用
BoundSql
保存Sql语句的对象,替换sql#{}成为?号并且存储#{}对应的参数名
XMLConfigBuilder
SqlMapConfig.xml配置文件解析工具类
XMLMapperBuilder
Mapper.xml配置文件解析工具类
接口/类
作用
Resource
读取SqlMapConfig.xml和Mapper.xml的工具类,转换为输入流inputStream
接口/类
作用
Configuration
封装SqlMapConfig.xml配置参数
MappedStatement
封装Mapper.xml配置的sql参数
接口/类
作用
SqlSessionFactoryBuilder
SqlSessionFactory构建者类
SqlSessionFactory
生产SqlSession的工厂接口
DefaultSqlSessionFactory
SqlSessionFactory的默认实现类
SqlSession
SqlSession接口定义数据库基本的CRUD方法
DefaultSqlSession
SqlSession的实现类
Executor
Executor接口sql的真正执行者,使用JDBC操作数据库
SimpleExecutor
Executor的实现类
接口/类
作用
ParameterMapping
来源于Mybatis框架,SQL参数映射类,存储#{}、${}中的参数名
TokenHandler
来源于Mybatis框架,标记处理器接口
ParameterMappingTokenHandler
来源于Mybatis框架,标记处理器实现类,解析#{}、${}成为?
GenericTokenParser
来源于Mybatis框架,通用标记解析器,标记#{与}开始结束处理
五、自定义持久层框架:编码
结合UML图和项目结构图,脑海里开始有点东西了,烧脑且枯燥的编码过程,我们开始吧。
框架依赖 pom.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 <?xml version="1.0" encoding="UTF-8"?> <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/xsd/maven-4.0.0.xsd" > <modelVersion > 4.0.0</modelVersion > <groupId > com.panshenlian</groupId > <artifactId > MyPersistence</artifactId > <version > 1.0-SNAPSHOT</version > <properties > <project.build.sourceEncoding > UTF-8</project.build.sourceEncoding > <maven.compiler.encoding > UTF-8</maven.compiler.encoding > <java.version > 1.8</java.version > <maven.compiler.source > 1.8</maven.compiler.source > <maven.compiler.target > 1.8</maven.compiler.target > </properties > <dependencies > <dependency > <groupId > mysql</groupId > <artifactId > mysql-connector-java</artifactId > <version > 5.1.17</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.12</version > </dependency > <dependency > <groupId > junit</groupId > <artifactId > junit</artifactId > <version > 4.10</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 > </project >
config包下BoundSql类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 package com.panshenlian.config;import com.panshenlian.utils.ParameterMapping;import java.util.ArrayList;import java.util.List;public class BoundSql { private String sqlText; private List<ParameterMapping> parameterMappingList = new ArrayList <ParameterMapping>(); public BoundSql (String sqlText, List<ParameterMapping> parameterMappingList) { this .sqlText = sqlText; this .parameterMappingList = parameterMappingList; } public String getSqlText () { return sqlText; } public void setSqlText (String sqlText) { this .sqlText = sqlText; } public List<ParameterMapping> getParameterMappingList () { return parameterMappingList; } public void setParameterMappingList (List<ParameterMapping> parameterMappingList) { this .parameterMappingList = parameterMappingList; } }
config包下XMLConfigBuilder类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 package com.panshenlian.config;import com.mchange.v2.c3p0.ComboPooledDataSource;import com.panshenlian.io.Resource;import com.panshenlian.pojo.Configuration;import com.sun.javafx.scene.control.skin.EmbeddedTextContextMenuContent;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;import java.util.Properties;public class XMLConfigBuilder { private Configuration configuration; public XMLConfigBuilder () { this .configuration = new Configuration (); } public Configuration parseConfig (InputStream inputStream) throws Exception { Document document = new SAXReader ().read(inputStream); Element configurationRootElement = document.getRootElement(); List<Element> elementList = configurationRootElement.selectNodes("//property" ); Properties properties = new Properties (); for (Element element : elementList){ String name = element.attributeValue("name" ); String value = element.attributeValue("value" ); properties.put(name,value); } ComboPooledDataSource dataSource = new ComboPooledDataSource (); dataSource.setDriverClass(properties.getProperty("driverClass" )); dataSource.setJdbcUrl(properties.getProperty("jdbcUrl" )); dataSource.setUser(properties.getProperty("userName" )); dataSource.setPassword(properties.getProperty("password" )); configuration.setDataSource(dataSource); List<Element> mapperElementList = configurationRootElement.selectNodes("//mapper" ); for (Element element : mapperElementList) { String mapperPath = element.attributeValue("resource" ); InputStream resourceAsStream = Resource.getResourceAsStream(mapperPath); XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder (configuration); xmlMapperBuilder.parseMapper(resourceAsStream); } return configuration; } }
config包下XMLMapperBuilder类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 package com.panshenlian.config;import com.panshenlian.pojo.Configuration;import com.panshenlian.pojo.MappedStatement;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;public class XMLMapperBuilder { private Configuration configuration; public XMLMapperBuilder (Configuration configuration) { this .configuration = configuration; } public void parseMapper (InputStream inputStream) throws DocumentException { Document mapperDocument = new SAXReader ().read(inputStream); Element rootElement = mapperDocument.getRootElement(); String namespace = rootElement.attributeValue("namespace" ); List<Element> selectNodes = mapperDocument.selectNodes("//select" ); for (Element element : selectNodes) { String id = element.attributeValue("id" ); String resultType = element.attributeValue("resultType" ); String parameterType = element.attributeValue("parameterType" ); String sql = element.getTextTrim(); MappedStatement mappedStatement = new MappedStatement (); mappedStatement.setId(id); mappedStatement.setResultType(resultType); mappedStatement.setParameterType(parameterType); mappedStatement.setSql(sql); String statementId = namespace + "." + id; configuration.getMappedStatementMap().put(statementId,mappedStatement); } } }
io包下Resource工具类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 package com.panshenlian.io;import java.io.InputStream;public class Resource { public static InputStream getResourceAsStream (String path) { InputStream inputStream = Resource.class.getClassLoader().getResourceAsStream(path); return inputStream; } }
pojo包下Configuration
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 package com.panshenlian.pojo;import javax.sql.DataSource;import java.util.HashMap;import java.util.Map;public class Configuration { private DataSource dataSource; private Map<String,MappedStatement> mappedStatementMap = new HashMap <String, MappedStatement>(); public DataSource getDataSource () { return dataSource; } public void setDataSource (DataSource dataSource) { this .dataSource = dataSource; } public Map<String, MappedStatement> getMappedStatementMap () { return mappedStatementMap; } public void setMappedStatementMap (Map<String, MappedStatement> mappedStatementMap) { this .mappedStatementMap = mappedStatementMap; } }
pojo包下MappedStatement
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 package com.panshenlian.pojo;public class MappedStatement { private String id; private String resultType; private String parameterType; private String sql; public String getId () { return id; } public void setId (String id) { this .id = id; } public String getResultType () { return resultType; } public void setResultType (String resultType) { this .resultType = resultType; } public String getParameterType () { return parameterType; } public void setParameterType (String parameterType) { this .parameterType = parameterType; } public String getSql () { return sql; } public void setSql (String sql) { this .sql = sql; } }
sqlSession包下DefaultSqlSession
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 package com.panshenlian.sqlSession;import com.panshenlian.pojo.Configuration;import com.panshenlian.pojo.MappedStatement;import java.lang.reflect.*;import java.util.List;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 simpleExecutor = new SimpleExecutor (); MappedStatement mappedStatement = configuration.getMappedStatementMap().get(statementId); List<Object> queryResultList = simpleExecutor.query(configuration, mappedStatement, params); return (List<E>)queryResultList; } @Override public <T> T selectOne (String statementId, Object... params) throws Exception { List<Object> objects = selectList(statementId, params); if (null != objects && objects.size() == 1 ){ return (T)objects.get(0 ); } else { throw new RuntimeException ("查询结果为空或者返回结果多于1条" ); } } @Override public int update (String statementId, Object... params) { return 0 ; } @Override public int delete (String statementId, Object... params) { return 0 ; } @Override public <T> T getMapper (Class<?> mapperClass) { Object proxyInstance = Proxy.newProxyInstance(DefaultSqlSession.class.getClassLoader(), new Class []{mapperClass}, new InvocationHandler (){ @Override public Object invoke (Object proxy, Method method, Object[] args) throws Throwable { String methodName = method.getName(); String className = method.getDeclaringClass().getName(); String statementId = className + "." + methodName; Type genericReturnType = method.getGenericReturnType(); if ( genericReturnType instanceof ParameterizedType){ List<Object> objects = selectList(statementId, args); return objects; } return selectOne(statementId,args); } }); return (T)proxyInstance; } }
sqlSession包下DefaultSqlSessionFactory
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 package com.panshenlian.sqlSession;import com.panshenlian.pojo.Configuration;public class DefaultSqlSessionFactory implements SqlSessionFactory { private Configuration configuration; public DefaultSqlSessionFactory (Configuration configuration) { this .configuration = configuration; } @Override public SqlSession openSession () { return new DefaultSqlSession (configuration); } }
sqlSession包下Executor
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 package com.panshenlian.sqlSession;import com.panshenlian.pojo.Configuration;import com.panshenlian.pojo.MappedStatement;import java.beans.IntrospectionException;import java.lang.reflect.InvocationTargetException;import java.sql.SQLException;import java.util.List;public interface Executor { public <E> List<E> query (Configuration configuration, MappedStatement mappedStatement, Object... params) throws Exception;}
sqlSession包下SimpleExecutor
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 package com.panshenlian.sqlSession;import com.mysql.jdbc.StringUtils;import com.panshenlian.config.BoundSql;import com.panshenlian.pojo.Configuration;import com.panshenlian.pojo.MappedStatement;import com.panshenlian.utils.GenericTokenParser;import com.panshenlian.utils.ParameterMapping;import com.panshenlian.utils.ParameterMappingTokenHandler;import java.beans.ExceptionListener;import java.beans.IntrospectionException;import java.beans.PropertyDescriptor;import java.lang.reflect.Field;import java.lang.reflect.InvocationTargetException;import java.lang.reflect.Method;import java.sql.*;import java.util.ArrayList;import java.util.List;public class SimpleExecutor implements Executor { @Override public <E> List<E> query (Configuration configuration, MappedStatement mappedStatement, Object... params) throws Exception { Connection connection = configuration.getDataSource().getConnection(); String sql = mappedStatement.getSql(); BoundSql bounSql = getBoundSql(sql); PreparedStatement preparedStatement = connection.prepareStatement(bounSql.getSqlText()); String parameterType = mappedStatement.getParameterType(); Class<?> parameterTypeClass = getClassType(parameterType); List<ParameterMapping> parameterMappingList = bounSql.getParameterMappingList(); for (int i = 0 ; i < parameterMappingList.size(); i++) { ParameterMapping parameterMapping = parameterMappingList.get(i); String filedName = parameterMapping.getContent(); Field declaredField = parameterTypeClass.getDeclaredField(filedName); declaredField.setAccessible(true ); Object declaredFieldValue = declaredField.get(params[0 ]); preparedStatement.setObject(i+1 ,declaredFieldValue); } ResultSet resultSet = preparedStatement.executeQuery(); String resultType = mappedStatement.getResultType(); Class<?> resultTypeClass = getClassType(resultType); List<Object> objects = new ArrayList <Object>(); while (resultSet.next()){ Object o = resultTypeClass.newInstance(); ResultSetMetaData metaData = resultSet.getMetaData(); for (int i = 1 ; i <= metaData.getColumnCount(); i++) { String columnName = metaData.getColumnName(i); Object columnValue = resultSet.getObject(columnName); PropertyDescriptor propertyDescriptor = new PropertyDescriptor (columnName, resultTypeClass); Method writeMethod = propertyDescriptor.getWriteMethod(); writeMethod.invoke(o,columnValue); } objects.add(o); } return (List<E>)objects; } private Class<?> getClassType(String parameterType) throws ClassNotFoundException { if (StringUtils.isNullOrEmpty(parameterType)) { return null ; } Class<?> clazz = Class.forName(parameterType); return clazz; } private BoundSql getBoundSql (String sql) { ParameterMappingTokenHandler parameterMappingTokenHandler = new ParameterMappingTokenHandler (); GenericTokenParser genericTokenParser = new GenericTokenParser ("#{" ,"}" , parameterMappingTokenHandler); String parseSql = genericTokenParser.parse(sql); List<ParameterMapping> parameterMappings = parameterMappingTokenHandler.getParameterMappings(); BoundSql boundSql = new BoundSql (parseSql, parameterMappings); return boundSql; } }
sqlSession包下SqlSession
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 package com.panshenlian.sqlSession;import java.util.List;public interface SqlSession { public <E> List<E> selectList (String statementId , Object ... params) throws Exception; public <T> T selectOne (String statementId , Object ... params) throws Exception; public int update (String statementId , Object ... params) ; public int delete (String statementId , Object ... params) ; public <T> T getMapper (Class<?> mapperClass) ; }
sqlSession包下SqlSessionFactory
1 2 3 4 5 6 7 8 9 10 11 12 package com.panshenlian.sqlSession;public interface SqlSessionFactory { public SqlSession openSession () ; }
sqlSession包下SqlSessionFactoryBuilder
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 package com.panshenlian.sqlSession;import com.panshenlian.config.XMLConfigBuilder;import com.panshenlian.pojo.Configuration;import java.io.InputStream;public class SqlSessionFactoryBuilder { public SqlSessionFactory build (InputStream inputStream) throws Exception { XMLConfigBuilder xmlConfigBuilder = new XMLConfigBuilder (); Configuration configuration = xmlConfigBuilder.parseConfig(inputStream); DefaultSqlSessionFactory defaultSqlSessionFactory = new DefaultSqlSessionFactory (configuration); return defaultSqlSessionFactory; } }
utils包下GenericTokenParser
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 package com.panshenlian.utils;public class GenericTokenParser { private final String openToken; private final String closeToken; private final TokenHandler handler; public GenericTokenParser (String openToken, String closeToken, TokenHandler handler) { this .openToken = openToken; this .closeToken = closeToken; this .handler = handler; } public String parse (String text) { if (text == null || text.isEmpty()) { return "" ; } int start = text.indexOf(openToken, 0 ); if (start == -1 ) { return text; } char [] src = text.toCharArray(); int offset = 0 ; final StringBuilder builder = new StringBuilder (); StringBuilder expression = null ; while (start > -1 ) { if (start > 0 && src[start - 1 ] == '\\' ) { builder.append(src, offset, start - offset - 1 ).append(openToken); offset = start + openToken.length(); } else { if (expression == null ) { expression = new StringBuilder (); } else { expression.setLength(0 ); } builder.append(src, offset, start - offset); offset = start + openToken.length(); int end = text.indexOf(closeToken, offset); while (end > -1 ) { if (end > offset && src[end - 1 ] == '\\' ) { expression.append(src, offset, end - offset - 1 ).append(closeToken); offset = end + closeToken.length(); end = text.indexOf(closeToken, offset); } else { expression.append(src, offset, end - offset); offset = end + closeToken.length(); break ; } } if (end == -1 ) { builder.append(src, start, src.length - start); offset = src.length; } else { builder.append(handler.handleToken(expression.toString())); offset = end + closeToken.length(); } } start = text.indexOf(openToken, offset); } if (offset < src.length) { builder.append(src, offset, src.length - offset); } return builder.toString(); } }
utils包下ParameterMapping
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 package com.panshenlian.utils;public class ParameterMapping { private String content; public ParameterMapping (String content) { this .content = content; } public String getContent () { return content; } public void setContent (String content) { this .content = content; } }
utils包下ParameterMappingTokenHandler
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 package com.panshenlian.utils;import java.util.ArrayList;import java.util.List;public class ParameterMappingTokenHandler implements TokenHandler { private List<ParameterMapping> parameterMappings = new ArrayList <ParameterMapping>(); public String handleToken (String content) { parameterMappings.add(buildParameterMapping(content)); return "?" ; } private ParameterMapping buildParameterMapping (String content) { ParameterMapping parameterMapping = new ParameterMapping (content); return parameterMapping; } public List<ParameterMapping> getParameterMappings () { return parameterMappings; } public void setParameterMappings (List<ParameterMapping> parameterMappings) { this .parameterMappings = parameterMappings; } }
utils包下TokenHandler
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 package com.panshenlian.utils;public interface TokenHandler { String handleToken (String content) ; }
框架书写好了,我们写一个测试工程验证一下框架,我们在现有框架下新加一个测试项目(以module模块的方式创建)保证测试工程和框架项目在一个工作组下面:
由于我已经写好了测试工程,我直接引入即可,效果都一样,创建和引入都以module方式就可以:
测试工程基本流程也说明一下:
1、引入依赖pom.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 <?xml version="1.0" encoding="UTF-8"?> <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/xsd/maven-4.0.0.xsd" > <modelVersion > 4.0.0</modelVersion > <groupId > com.panshenlian</groupId > <artifactId > MyPersistenceTest</artifactId > <version > 1.0-SNAPSHOT</version > <properties > <project.build.sourceEncoding > UTF-8</project.build.sourceEncoding > <maven.compiler.encoding > UTF-8</maven.compiler.encoding > <java.version > 1.8</java.version > <maven.compiler.source > 1.8</maven.compiler.source > <maven.compiler.target > 1.8</maven.compiler.target > </properties > <dependencies > <dependency > <groupId > com.panshenlian</groupId > <artifactId > MyPersistence</artifactId > <version > 1.0-SNAPSHOT</version > </dependency > </dependencies > </project >
2、配置数据源sqlMapConfig.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <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 ="123456" > </property > </dataSource > <mapper resource ="userMapper.xml" > </mapper > <mapper resource ="orderMapper.xml" > </mapper > </configuration >
3、我们以用户表为例子,建立用户sql配置userMapper.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 <mapper namespace ="com.panshenlian.dao.IUserDao" > <select id ="findAll" resultType ="com.panshenlian.pojo.User" > select * from user </select > <select id ="findByCondition" resultType ="com.panshenlian.pojo.User" parameterType ="com.panshenlian.pojo.User" > select * from user where id= #{id} and username = #{username} and password= #{password} and birthday = #{birthday} </select > </mapper >
4、用户dao接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 package com.panshenlian.dao;import com.panshenlian.pojo.User;import java.util.List;public interface IUserDao { public List<User> findAll () throws Exception; public User findByCondition (User user) throws Exception; }
5、用户dao的实体类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 package com.panshenlian.pojo;public class User { private Integer id; private String username; private String password; private String birthday; public String getPassword () { return password; } public void setPassword (String password) { this .password = password; } public String getBirthday () { return birthday; } public void setBirthday (String birthday) { this .birthday = birthday; } public Integer getId () { return id; } public void setId (Integer id) { this .id = id; } public String getUsername () { return username; } public void setUsername (String username) { this .username = username; } @Override public String toString () { return "User{" + "id=" + id + ", username='" + username + '\'' + ", password='" + password + '\'' + ", birthday='" + birthday + '\'' + '}' ; } }
注意:用户sql配置文件userMapper.xml中的namespace需要和用户dao的全限定名一致,这是我们框架默认规则:namespace=”com.panshenlian.dao.IUserDao” 同时select标签的id和用户dao接口的方法名保持一致,也是框架默认的规则,例如id=”findAll”
6、最终我们创建测试类:MyPersistenceTest
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 package com.panshenlian.test;import com.panshenlian.dao.IUserDao;import com.panshenlian.io.Resource;import com.panshenlian.pojo.User;import com.panshenlian.sqlSession.SqlSession;import com.panshenlian.sqlSession.SqlSessionFactory;import com.panshenlian.sqlSession.SqlSessionFactoryBuilder;import org.junit.Test;import java.io.InputStream;import java.util.List;public class MyPersistenceTest { @Test public void test () throws Exception { InputStream resourceAsStream = Resource.getResourceAsStream("sqlMapConfig.xml" ); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder ().build(resourceAsStream); SqlSession sqlSession = sqlSessionFactory.openSession(); User user = new User (); user.setId(3 ); user.setUsername("panshenlian" ); user.setBirthday("2020-11-12" ); user.setPassword("123456" ); User dbUser = sqlSession.selectOne("com.panshenlian.dao.IUserDao.findByCondition" ,user); System.out.println(dbUser); List<User> userList = sqlSession.selectList("com.panshenlian.dao.IUserDao.findAll" , user); for (User db : userList) { System.out.println(db); } IUserDao userDao = sqlSession.getMapper(IUserDao.class); List<User> users = userDao.findAll(); for (User db : users) { System.out.println("代理调用=" + db); } } }
7、运行测试类,结果符合预期
框架和测试验证我们基本完成,其实以上主要是对于持久层框架的一个简单框架介绍,方面我们以后学习分析Mybatis框架,基本我们做到了一个模拟雏形,流程大致是这样。
同时框架和测试工程的源码都已上传,传送门:点击看看
编码实现过程中涉及到几个有意思的知识点,我们后续找时间聊聊,包括:
内省机制
反射机制
JDK动态代理
设计模式
泛型
总结 如今大型项目一般都不会直接使用JDBC,要么采用市面上成熟的持久层方案,要么自研持久层框架,说到底,还是单纯的JDBC无法保证高效高稳定性能的数据层访问与应用,而越来越多持久层框架方案,不仅消除了大量的JDBC冗余代码,还提供极低的学习曲线,既能保证协同传统的数据库还接受SQL语句,也为其他框架提供了拓展集成支持,包括连接池、缓存、性能等都做了极大的优化与提升,所以框架大行其道是必然趋势。
JDBC在90年代诞生之初也是高光而伟大,只不过随着技术水平的跃迁和业务场景的迭代更新,旧技术满足不了现有的诉求,所有事物都会轮换更新,我们仅仅是站在伟人的肩膀上,顺势变迁。
好,本篇完,晚安。
下一篇,我们或许会聊聊 Mybatis基础和架构 。