自定义持久层框架
自定义框架
最近学习mybatis,为了提高阅读mybatis源码的效率,手撸了一个简单的mybatis框架,以下是搭建自定义框架中一些问题的解决方法。
此文可以算是笔者的一些笔记,但也同样希望能够为学习mybatis的小伙伴做一点微不足道的贡献。
从JDBC到持久层框架
犹记在上学时只学过用jdbc来连接数据库,当时每一次连接都要创建一个Connection,并且还不能忘记要将其关闭,实在烦不胜烦。
以下就要分析一遍jdbc存在的问题,我们自定义的框架也是围绕解决这些问题为中心实现的。
分析JDBC操作问题
首先来一段jdbc连接代码
public static void main(String[] args) {
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", "root");
// 定义sql语句?表示占位符
String sql = "select * from user where username = ?";
// 获取预处理statement
preparedStatement = connection.prepareStatement(sql);
// 设置参数,第一个参数为sql语句中参数的序号(从1开始),第二个参数为设置的参数值
preparedStatement.setString(1, "zhangsan");
// 向数据库发出sql执行查询,查询出结果集
resultSet = preparedStatement.executeQuery();
// 遍历查询结果集
while (resultSet.next()) {
int id = resultSet.getInt("id");
String username = resultSet.getString("username");
// 封装User
user.setId(id);
user.setUsername(username);
}
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();
}
}
}
}
JDBC存在的问题:
- 数据库连接创建释放频繁,浪费系统资源,影响系统资源
- 连接参数,sql语句,传入参数存在硬编码,后续变化困难,不易维护
- 结果集需要进一步加工进集合返回,太复杂
解决JDBC问题思路:
- 使用数据库线程池(C3P0)
- 将连接参数,sql语句抽取到xml
- 将实体类与表映射(与mybatis一样,传入sql参数用实体类封装,返回结果也用实体类封装)
自定义框架设计
首先明白框架的组成架构,就可以更好的去理解代码实现。框架的设计分为两端,使用端和框架端
使用端: 提供两个配置文件SqlMapConfig.xml和mapper.xml
- SqlMapConfig.xml:存放数据源信息和引入mapper文件信息
- mapper.xml:sql语句配置信息
框架端: 读取,解析配置文件,并且执行crud操作
- 读取配置文件
1. Resources:读取SqlMapConfig.xml字节文件加载成InputStream字节流
2. Configuration:POJO类,利用DOM4J解析 InputStream字节流 获取到 数据源参数 和map<key,MapperStatement> 后,封装到 Configuration
3. MappedStatement:封装解析mapper.xml后获得的sql语句、statement类型、输入参数java类型、输出参数java类型 - 解析配置文件
1.SqlSessionFactoryBuilder 的 build() 方法
用dom4j解析配置文件,将解析出来的内容封装到Configuration和MappedStatement中。
创建SqlSessionFactory的实现类DefaultSqlSession。
2.SqlSessionFactory 的 openSession() 方法
生产DefaultSqlSession对象
3.sqlSession接口及实现类的
主要封装crud方法(底层由Executor执行)
自定义框架实现
先贴出执行sql语句的main方法,便于后续理解和对照
@Test
public void test() throws Exception {
//1.加载主配置文件SqlMapConfig.xml成字节流
InputStream resourceAsSteam = Resources.getResourceAsSteam("sqlMapConfig.xml");
//2.解析配置文件并生成SqlSessionFactory
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsSteam);
//3.得到SqlSession
SqlSession sqlSession = sqlSessionFactory.openSession();
//创建参数
User user = new User();
user.setId(1);
user.setUsername("王五");
//4.调用执行方法 5.封装结果都User
User user2 = sqlSession.selectOne("user.findAll", user);
System.out.println(user2);*/
}
需要的配置文件和实体类
按照自定义框架设计步骤,我会将使用端和框架端的代码贴出,请仔细查看
使用端SqlMapConfig.xml
<configuration>
<!--数据库配置信息-->
<dataSource>
<property name="driverClass" value="com.mysql.jdbc.Driver"></property>
<property name="jdbcUrl" value="jdbc:mysql:///zidiyimybatis"></property>
<property name="username" value="root"></property>
<property name="password" value="root"></property>
</dataSource>
<!--存放mapper.xml的全路径-->
<mapper resource="UserMapper.xml"></mapper>
</configuration>
使用端UserMapper.xml
<mapper namespace="com.hzc.dao.IUserDao">
<select id="findAll" resultType="com.hzc.pojo.User" >
select * from user
</select>
<select id="findByCondition" resultType="com.hzc.pojo.User" paramterType="com.hzc.pojo.User">
select * from user where id = #{id} and username = #{username}
</select>
</mapper>
框架端MappedStatement
public class MappedStatement {
//id标识
private String id;
//返回值类型
private String resultType;
//参数值类型
private String paramterType;
//sql语句
private String sql;
......
//省略getter和setter方法
}
框架端Configuration
public class Configuration {
private DataSource dataSource;
// key: statementid,value:封装好的mappedStatement对象,对应SqlMapConfig.xml的<mapper>
Map<String,MappedStatement> mappedStatementMap = new HashMap<>();
......
//省略getter和setter方法
}
第一步 Resources怎么加载配置文件到字节流
框架端Resources
作用:加载配置文件到字节流
public class Resources {
// 根据配置文件的路径,将配置文件加载成字节输入流,存储在内存中
public static InputStream getResourceAsSteam(String path){
InputStream resourceAsStream = Resources.class.getClassLoader().getResourceAsStream(path);
return resourceAsStream;
}
}
第二步 如何解析字节流并封装到实体类
框架端XMLConfigBuilder
作用:使用dom4j解析字节流将SqlMapConfig配置文件数据内容封装到Configuration对象中
public class XMLConfigBuilder {
private Configuration configuration;
public XMLConfigBuilder() { this.configuration = new Configuration();}
/**
* 该方法就是使用dom4j对配置文件进行解析,封装Configuration
*/
public Configuration parseConfig(InputStream inputStream) throws DocumentException, PropertyVetoException {
Document document = new SAXReader().read(inputStream);
//从dom树中得到根元素<configuration>
Element rootElement = document.getRootElement();
//得到<configuration>中的所有<peoperty>
List<Element> list = rootElement.selectNodes("//property");
Properties properties = new Properties();
//遍历<peoperty>元素集合,获取属性值,将其放入到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
configuration.setDataSource(comboPooledDataSource);
//得到<mapper>元素集合
List<Element> mapperList = rootElement.selectNodes("//mapper");
//遍历集合
for (Element element : mapperList) {
//逐个获得resource属性对应的mapper.xml的路径
String mapperPath = element.attributeValue("resource");
//将mapper.xml加载进字节流
InputStream resourceAsSteam = Resources.getResourceAsSteam(mapperPath);
XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(configuration);
//解析mapper.xml字节流,将数据封装到mapperdStatement,再将mapperdStatement封装到configuration的map<namespace.id,mapperdStatement>集合
xmlMapperBuilder.parse(resourceAsSteam);
}
return configuration;
}
}
框架端XMLMapperBuilder
作用:使用dom4j解析字节流将mapper配置文件数据内容封装到MappedStatement对象,再将MappedStatement对象封到Configuration对象中
public class XMLMapperBuilder {
//步骤大同小异,不做注释重复累赘
private Configuration configuration;
public XMLMapperBuilder(Configuration configuration) {
this.configuration =configuration;
}
public void parse(InputStream inputStream) throws DocumentException {
Document document = new SAXReader().read(inputStream);
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 paramterType = element.attributeValue("paramterType");
String sqlText = element.getTextTrim();
MappedStatement mappedStatement = new MappedStatement();
mappedStatement.setId(id);
mappedStatement.setResultType(resultType);
mappedStatement.setParamterType(paramterType);
mappedStatement.setSql(sqlText);
//key值是namespace.id , value值是mappedStatement
String key = namespace+"."+id;
configuration.getMappedStatementMap().put(key,mappedStatement);
}
}
}
第三步 Builder模式构建Configuration
框架端SqlSessionFactoryBuilder
作用:构建configuration,返回SqlSessionFactroy工厂对象
public class SqlSessionFactoryBuilder {
public SqlSessionFactory build(InputStream in) throws DocumentException, PropertyVetoException {
// 第一:使用dom4j解析配置文件,将解析出来的内容封装到Configuration中
XMLConfigBuilder xmlConfigBuilder = new XMLConfigBuilder();
Configuration configuration = xmlConfigBuilder.parseConfig(in);
// 第二:创建sqlSessionFactory对象:工厂类:生产sqlSession:会话对象
DefaultSqlSessionFactory defaultSqlSessionFactory = new DefaultSqlSessionFactory(configuration);
return defaultSqlSessionFactory;
}
工厂模式和构建者模式多用于构建对象较为复杂的情况,但工厂模式下的对象往往都是一个完整的对象,而构建者模式下对象是可以一部分一部分去构建,可以得到不同的对象。
Mybatis中Configuration的构建比这复杂的多,在parseConfiguration()方法中有许多解析配置的方法,如下
private void parseConfiguration(XNode root) {
try {
//issue #117 read properties first
//解析<properties />标签
propertiesElement(root.evalNode("properties"));
// 解析 <settings /> 标签
Properties settings = settingsAsProperties(root.evalNode("settings"));
//加载自定义的VFS实现类
loadCustomVfs(settings);
// 解析 <typeAliases /> 标签
typeAliasesElement(root.evalNode("typeAliases"));
//解析<plugins />标签
pluginElement(root.evalNode("plugins"));
// 解析 <objectFactory /> 标签
objectFactoryElement(root.evaINode("obj ectFactory"));
// 解析 <objectWrapper Factory /> 标签
obj ectWrappe rFacto ryElement(root.evalNode("objectWrapperFactory"));
// 解析 <reflectorFactory /> 标签
reflectorFactoryElement(root.evalNode("reflectorFactory"));
// 赋值 <settings /> 到 Configuration 属性
settingsElement(settings);
// read it after objectFactory and objectWrapperFactory issue #631
// 解析 <environments /> 标签
environmentsElement(root.evalNode("environments"));
// 解析 <databaseIdProvider /> 标签
databaseldProviderElement(root.evalNode("databaseldProvider"));
}
第三步 工厂模式生产SqlSession
框架端SqlSessionFactory接口及实现类DefaultSqlSessionFactory
作用:传递configuration到SqlSession,并且生产SqlSession
public class DefaultSqlSessionFactory implements SqlSessionFactory {
private Configuration configuration;
public DefaultSqlSessionFactory(Configuration configuration) {
this.configuration = configuration;
}
@Override
public SqlSession openSession() {
return new DefaultSqlSession(configuration);
}
}
第三步 SqlSession执行语句
框架端SqlSession接口及实现类DefaultSqlSession
作用:传递封装的参数和sql给Executor并接收返回的数据集合
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("查询结果为空或者返回结果过多");
}
}
}
第四步 真正封装JDBC操作的Executor
框架端Executor接口及实现类SimpleExecutor
作用:执行jdbc操作
public class simpleExecutor implements Executor {
@Override //user
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 paramterType = mappedStatement.getParamterType();
Class<?> paramtertypeClass = getClassType(paramterType);
List<ParameterMapping> parameterMappingList = boundSql.getParameterMappingList();
//遍历BoundSql中的存放的数据(#{**}中的**)
//使用filed.get(ParameterMap)方法获取对应**在ParameterMap(传入到sql的变量)的值
//再用preparedStatement.setObject设置为sql的参数
for (int i = 0; i < parameterMappingList.size(); i++) {
ParameterMapping parameterMapping = parameterMappingList.get(i);
String content = parameterMapping.getContent();
//反射
Field declaredField = paramtertypeClass.getDeclaredField(content);
//暴力访问
declaredField.setAccessible(true);
Object o = declaredField.get(params[0]);
preparedStatement.setObject(i+1,o);
}
// 5. 执行sql
ResultSet resultSet = preparedStatement.executeQuery();
String resultType = mappedStatement.getResultType();
Class<?> resultTypeClass = getClassType(resultType);
ArrayList<Object> objects = new ArrayList<>();
// 6. 封装返回结果集
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 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 paramterType) throws ClassNotFoundException {
if(paramterType!=null){
Class<?> aClass = Class.forName(paramterType);
return aClass;
}
return null;
}
/**
* 完成对#{}的解析工作:1.将#{}使用?进行代替,2.解析出#{}里面的值进行存储
* @param sql
* @return
*/
private BoundSql getBoundSql(String sql) {
//标记处理类:配置标记解析器来完成对占位符的解析处理工作
ParameterMappingTokenHandler parameterMappingTokenHandler = new ParameterMappingTokenHandler();
GenericTokenParser genericTokenParser = new GenericTokenParser("#{", "}", parameterMappingTokenHandler);
//解析出来的sql
String parseSql = genericTokenParser.parse(sql);
//#{}里面解析出来的参数名称
List<ParameterMapping> parameterMappings = parameterMappingTokenHandler.getParameterMappings();
BoundSql boundSql = new BoundSql(parseSql,parameterMappings);
return boundSql;
}
}
框架端BoundSql和ParameterMapping
作用:存放解析出#{}的sql语句和被“?”替换的输入参数名
public class BoundSql {
private String sqlText; //解析过后的sql
private List<ParameterMapping> parameterMappingList = new ArrayList<>();
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;
}
}
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; }
}
第五步 用于解析sql语句的工具类
用于解析sql语句,将#{}替换为?的工具类,从mybatis源码中copy。
分别是:
- GenericTokenParser
- TokenHandler接口和它的实现类ParameterMappingTokenHandler
因为代码太多就不贴出来啦,大家可以去源码中找~~