上一期我们定义了使用端
这一期我们定义框架端
首先我们想要自定义框架供使用者使用 我们思考的问题就是要从使用者的角度出发
使用者(项目): 引入我们自定义的持久层框架的jar包(目的是利用框架对数据库进行操作)
使用者需要提供两部分信息 1、数据库配置信息 2、sql配置信息(包括sql 、出入参)
(1)自定义文件存放数据库配置信息(sqlMapConfig.xml)
(2)自定义文件存放sql信息(mapper.xml)
自定义框架本身(完整工程): 本质上就是对JDBC代码进行封装
(1) 加载配置文件 : 根据配置文件的路径, 加载配置文件成字节输入流, 存储在内存中
(2) 创建两个javaBean (容器对象) :存放配置文件解析出来的内容
(3) 利用dom4j 解析配置文件
(4) 写相关代码执行JDBC代码
之前我们分析了自定义框架的流程以及要做的事情
现在我们要想一下我们要的自定义框架到底是什么样的
首先 我们的框架一定是一个完整的工程 只有完整的工程在打成jar包时才可以被使用端所引用
第二 我们的使用端尽可能的用最少的代码完成对数据库的操作
明确以上两点 我们就可以开始动手了
(注: 这里的一些名称的定义都可以自定义 为了更好理解Mybatis的源码 我们尽量用到Mybatis的名称)
我们要想办法完成可以加载配置文件的方法
我们创建类Resources 来完成这个操作
这个方法实际上就是用Class类的api 类加载器 把静态文件转成input流加载到内存
入参的path就是 我们要加载配置文件的路径
这个很好理解
package com.mrsoon.io;
import java.io.InputStream;
public class Resources {
//根据配置文件的路径, 将配置文件加载成字节输入流 存储在内存中
public static InputStream getResourceAsSteam(String path){
InputStream resourceAsSteam= Resources.class.getClassLoader().getResourceAsStream(path);
return resourceAsSteam;
}
}
为了节省资源 我就不写get set方法了(实际上就是提供一种思路)
我们定义第一个容器用来装我们解析后的mapper里的具体方法
例如 id就是我们第二章提到的 namespace.id来组成 的 statementId 利用这个我们可以很轻松的定位到我们要执行的sql语句
剩下的就更好理解了 返回值我们用resultType来定义
参数值我们用paramterType 具体sql我们用sql来接
public class MappedStatement {
//id标识
private String id;
//返回值类型
private String resultType;
//参数值类型
private String paramterType;
//sql语句
private String sql;
}
DataSource就是我们在sqlMapConfig.xml下的配置解析
下面就是我们解析出mapper的一个合集(因为一个mapper下有可能会对应多个sql)
我们用map来存封装好的MappedStatement对象
key statementId value: 封装好的MappedStatement对象
public class Configuration {
private DataSource dataSource;
//key statementId value: 封装好的MappedStatement对象
Map<String,MappedStatement> mappedStatementMap= new HashMap<>();
}
大致的流程是这样的
我们先创建一个方法 传入之前解析的input流 然后 利用dom4j解析出相关配置
然后把创建出的配置文件相关信息 传到c3p0连接池中(Mybatis自带的连接池配置是Pooled)
然后继续解析mapper 把解析好的全部信息封装到Configuration中返回上层
这时已经与数据库建立了连接
这些名称也是可以自己定义的 我也只是提供了一个思路和流程 方便理解Mybatis的源码
创建类SqlSessionFactoryBuilder以及builder方法
public class SqlSessionFactoryBuilder {
public SqlSessionFactory builder(InputStream inputStream) throws DocumentException, PropertyVetoException {
//第一 使用dom4j 解析配置文件, 将解析出来的内容封装到Configoration中
XMLConfigBuilder xmlConfigBuilder = new XMLConfigBuilder();
Configuration configuration = xmlConfigBuilder.parseConfig(inputStream);
//第二 创建sqlSessionFactory对象 工厂模式 生产sqlSession
DefaultSqlSessionFactory defaultSqlSessionFactory = new DefaultSqlSessionFactory(configuration);
return defaultSqlSessionFactory;
}
}
将配置文件解析 并封装 利用dom4j 然后传递出去
public class XMLConfigBuilder {
private Configuration configuration;
public XMLConfigBuilder(){
this.configuration= new Configuration();
}
/**
* 该方法就是将配置文件解析 并封装 利用dom4j
* @param inputStream
* @return
*/
public Configuration parseConfig(InputStream inputStream) throws DocumentException, PropertyVetoException {
Document document = new SAXReader().read(inputStream);
//
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);
}
//连接池 c3p0
ComboPooledDataSource comboPooledDataSource = new ComboPooledDataSource();
comboPooledDataSource.setDriverClass(properties.getProperty("driverClass"));
comboPooledDataSource.setJdbcUrl("jdbcUrl");
comboPooledDataSource.setUser("username");
comboPooledDataSource.setPassword("password");
configuration.setDataSource(comboPooledDataSource);
//mapper.xml 解析 拿到路径--加载成字节输入流---dom4j字节流
List<Element> mapperList = rootElement.selectNodes("//mapper");
for (Element element : mapperList) {
String mapperPath = element.attributeValue("resoirces");
InputStream resourceAsSteam = Resources.getResourceAsSteam(mapperPath);
XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(configuration);
xmlMapperBuilder.parse(resourceAsSteam);
}
return configuration;
}
}
我们创建SqlSession接口 以及实现类DefaultSqlSession
这里主要是sql的实现方法 具体的sql执行实际上就是jdbc的执行
用到的方法就是 simpleExecutor.query 中
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 SQLException, IllegalAccessException, IntrospectionException, InstantiationException, NoSuchFieldException, InvocationTargetException, ClassNotFoundException {
simpleExecutor simpleExecutor = new simpleExecutor();
MappedStatement mappedStatement = configuration.getMappedStatementMap().get(statementId);
List<Object> query = simpleExecutor.query(configuration, mappedStatement, params);
return (List<E>) query;
}
@Override
public <T> T selectOne(String statementId, Object... params) throws SQLException, IllegalAccessException, IntrospectionException, InstantiationException, ClassNotFoundException, InvocationTargetException, NoSuchFieldException {
List<Object> objects = selectList(statementId, params);
if (objects.size()==1){
return (T)objects.get(0);
}else{
throw new RuntimeException("查询结果为空或者返回结果过多");
}
}
}
我们创建query方法 然后执行jdbc代码
本质上我们对数据库的操作就是jdbc的实现
我们创建Executor接口 以及它的实现类simpleExecutor
这里用到了反射的方法
public class simpleExecutor implements Executor {
@Override
public <E> List<E> query(Configuration configuration, MappedStatement mappedStatement, Object... params) throws SQLException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException, IntrospectionException, InstantiationException, InvocationTargetException {
//执行JDBC代码
//1。注册驱动 获取连接
Connection connection = configuration.getDataSource().getConnection();
//2。获取sql语句 替换出入参
String sql = mappedStatement.getSql();
BoundSql boundSql= getBoundSql(sql);
//3.获取预处理对象
PreparedStatement preparedStatement = connection.prepareStatement(boundSql.getSqlText());
//4.设置参数
String paramterType = mappedStatement.getParamterType();
//获取入参数class
Class<?> paramterTypeClass=getClassType(paramterType);
String resultType = mappedStatement.getResultType();
//获取出参数class
Class<?> resultTypeClass=getClassType(resultType);
List<ParameterMapping> parameterMappingList = boundSql.getParameterMappingList();
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 object = declaredField.get(params[0]);
preparedStatement.setObject(i+1,object);
}
//5。执行sql
ResultSet resultSet = preparedStatement.executeQuery();
List<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;
}else{
return null;
}
}
//1。将#{}使用? 代替
//2. 替换相关值
private BoundSql getBoundSql(String sql) {
//标记处理类 配置标记解析器来完成对占位符的解析处理工作
ParameterMappingTokenHandler parameterMappingTokenHandler = new ParameterMappingTokenHandler();
GenericTokenParser genericTokenParser = new GenericTokenParser("#{", "}", parameterMappingTokenHandler);
//解析出来的sql
String parse = genericTokenParser.parse(sql);
//#{} 里面解析出来的参数名称
List<ParameterMapping> parameterMappings = parameterMappingTokenHandler.getParameterMappings();
BoundSql boundSql = new BoundSql(parse,parameterMappings);
return boundSql;
}
}
替换占位符用的mybatis源码中的标记处理器
TokenHandler 这个实现很简单也很好理解 当然也可以自己写
主要是替换和映射的问题
这样我们就完成了自定义持久层框架
也是一个小型mybatis的一个雏形
是可以用的了
一些mybatis的动态标签还有自动化映射问题我们还没有解决
下一章我们将优化我们的自定义框架