Appearance
可以先来看一张大致的类图,然后先看文章底部的测试代码,看看是如何使用的,然后进行开发,这样思路可能会更加清晰。
点击查看自研框架整体目录结构
sql
|-- com.xk857
|-- config
| |-- BoundSql.java # 处理后的SQL对象,包含SQL语句(参数使用问号)和参数名称集合
| |-- XMLConfigBuilder.java # 将配置文件转换成Configuration对象
| |-- XMLMapperBuilder.java # 将单个mapper.xml配置文件转换成MappedStatement对象存储到Configuration集合中
|-- executor
| |-- Executor.java # 执行器接口
| |-- SimpleExecutor.java # 实现执行器,使用JDBC完成数据库操作
|-- io
| |-- Resources.java # 加载resources目录下的xml配置文件,转换内容为输入流形式
|-- pojo
| |-- Configuration.java # 全局配置类,存放核心配置文件解析出来的内容
| |-- MappedStatement.java # 单个mapper转换成的java对象,包含SQL语句、返回值类型、唯一标识……
|-- session
| |-- DefaultSqlSession.java # SqlSession实现类,实现selectList、selectOne等
| |-- DefaultSqlSessionFactory.java # SqlSessionFactory的实现类,获取执行器,返回DefaultSqlSession
| |-- SqlSession.java # 定义查询、修改接口
| |-- SqlSessionFactory.java # 接口,用于生产提供操作数据库的对象
| |-- SqlSessionFactoryBuilder.java # 构造最基本的操作对象,解析配置文件,创建SqlSessionFactory工厂对象
|-- utils # 这个包下的工具方法,都是用于将#{参数},改成?并记录参数集合的工具类
|-- GenericTokenParser.java
|-- ParameterMapping.java
|-- ParameterMappingTokenHandler.java
|-- TokenHandler.java
创建执行器接口及默认实现
执行器是执行真正的JDBC代码,这里暂时不做具体显示,先定义方便后续调用;为什么要单独写个执行器,而不是直接写到SqlSession中,首先是单一职责原则,其次则是模仿Mybatis。
java
package com.xk857.executor;
import com.xk857.pojo.Configuration;
import com.xk857.pojo.MappedStatement;
public interface Executor {
<E> List<E> query(Configuration configuration, MappedStatement mappedStatement, Object param) throws Exception;
void close();
}
具体实现看后文,现在暂时先不写,创建SimpleExecutor
防止报错
java
package com.xk857.executor;
import com.xk857.pojo.Configuration;
import com.xk857.pojo.MappedStatement;
public class SimpleExecutor implements Executor {
private Connection connection = null;
private PreparedStatement preparedStatement = null;
private ResultSet resultSet = null;
@Override // user
public <E> List<E> query(Configuration configuration, MappedStatement mappedStatement, Object param) throws Exception {
ArrayList<E> list = new ArrayList<>();
return list;
}
@Override
public void close() {
// 释放资源
}
}
处理SQL语句
想一下Mapper里面的SQL语句是什么形式的?①我们需要将#{}
替换成?
;②保存#{}
中的参数
sql
select * from user where id = #{id} and username = #{username}
复制Mybatis中的工具类
TokenHandler定义标记处理接口
java
package com.xk857.utils;
public interface TokenHandler {
String handleToken(String content);
}
ParameterMappingTokenHandler创建标记处理器,需配合标记解析器使用
java
public class ParameterMappingTokenHandler implements TokenHandler {
private List<ParameterMapping> parameterMappings = new ArrayList<ParameterMapping>();
// context是参数名称 #{id} #{username}
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;
}
}
GenericTokenParser标记解析器,将开始到结束变成问号,并将标记值保存下来
java
package com.xk857.utils;
/**
* @author Clinton Begin
*/
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;
}
/**
* 解析${}和#{}
* @param text
* @return
* 该方法主要实现了配置文件、脚本等片段中占位符的解析、处理工作,并返回最终需要的数据。
* 其中,解析工作由该方法完成,处理工作是由处理器handler的handleToken()方法来实现
*/
public String parse(String text) {
// 验证参数问题,如果是null,就返回空字符串。
if (text == null || text.isEmpty()) {
return "";
}
// 下面继续验证是否包含开始标签,如果不包含,默认不是占位符,直接原样返回即可,否则继续执行。
int start = text.indexOf(openToken, 0);
if (start == -1) {
return text;
}
// 把text转成字符数组src,并且定义默认偏移量offset=0、存储最终需要返回字符串的变量builder,
// text变量中占位符对应的变量名expression。判断start是否大于-1(即text中是否存在openToken),如果存在就执行下面代码
char[] src = text.toCharArray();
int offset = 0;
final StringBuilder builder = new StringBuilder();
StringBuilder expression = null;
while (start > -1) {
// 判断如果开始标记前如果有转义字符,就不作为openToken进行处理,否则继续处理
if (start > 0 && src[start - 1] == '\\') {
builder.append(src, offset, start - offset - 1).append(openToken);
offset = start + openToken.length();
} else {
//重置expression变量,避免空指针或者老数据干扰。
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] == '\\') {//如果结束标记前面有转义字符时
// this close token is escaped. remove the backslash and continue.
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) {
// close token was not found.
builder.append(src, start, src.length - start);
offset = src.length;
} else {
//首先根据参数的key(即expression)进行参数处理,返回?作为占位符
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();
}
}
创建BoundSQL对象
java
package com.xk857.config;
@Data
@AllArgsConstructor
public class BoundSql {
// 处理后的SQL语句(带?号)
private String finalSql;
// 参数列表(#{}的内容,不是实际传递的参数值)
private List<ParameterMapping> parameterMappingList;
}
ParameterMapping的content就是存放#{}
中的值,比如id、nickName等;
java
package com.xk857.utils;
@Data
@AllArgsConstructor
public class ParameterMapping {
private String content;
}
处理SQL语句解析成BoundSql对象
java
package com.xk857.executor;
import com.xk857.config.BoundSql;
import com.xk857.pojo.Configuration;
import com.xk857.pojo.MappedStatement;
import com.xk857.utils.GenericTokenParser;
import com.xk857.utils.ParameterMapping;
import com.xk857.utils.ParameterMappingTokenHandler;
public class SimpleExecutor implements Executor {
@Override
public <E> List<E> query(Configuration configuration, MappedStatement mappedStatement, Object param) throws Exception {
ArrayList<E> list = new ArrayList<>();
return list;
}
/**
* #{}占位符替换成?并将#{}里面的值保存下来
* @param sql 处理前的SQL
* @return BoundSql对象
*/
private BoundSql getBoundSql(String sql) {
// 1.创建标记处理器:配合标记解析器完成标记的处理解析工作
ParameterMappingTokenHandler parameterMappingTokenHandler = new ParameterMappingTokenHandler();
// 2.创建标记解析器
GenericTokenParser genericTokenParser = new GenericTokenParser("#{", "}", parameterMappingTokenHandler);
// 2.1 #{}占位符替换成? 2.解析替换的过程中 将#{}里面的值保存下来 ParameterMapping
String finalSql = genericTokenParser.parse(sql);
// 2.2 #{}里面的值的一个集合 id username,注意此时是值就是"id"而不是替换成的值,值还没传过来
List<ParameterMapping> parameterMappings = parameterMappingTokenHandler.getParameterMappings();
// 3.处理成BoundSql对象
return new BoundSql(finalSql, parameterMappings);
}
}
实现执行器查询操作
- 加载驱动,获取数据库连接
- 获取preparedStatement预编译对象(需要将#{参数}处理成?,并将参数保存)
- 设置参数,将SQL语句的参数传递进去
- 传递过来的JavaBean,通过反射根据参数名称获取对象中的属性内容
- 执行SQL语句
- 处理返回的结果集,先遍历结果集,然后根据数据库字段设置数据到JavaBean中,用List集合存储
java
public class SimpleExecutor implements Executor {
private Connection connection = null;
private PreparedStatement preparedStatement = null;
private ResultSet resultSet = null;
@Override
public <E> List<E> query(Configuration configuration, MappedStatement mappedStatement, Object param) throws Exception {
// 1.加载驱动,获取数据库连接
connection = configuration.getDataSource().getConnection();
// 2.获取preparedStatement预编译对象(需要将#{参数}处理成?,并将参数保存)
String sql = mappedStatement.getSql();
BoundSql boundSql = getBoundSql(sql);
String finalSql = boundSql.getFinalSql();
preparedStatement = connection.prepareStatement(finalSql);
// 3.设置参数
String parameterType = mappedStatement.getParameterType();
// 3.1 如果参数类型不为空,则开始解析设置参数(parameterType是根据xml的标签获取的)
if (parameterType != null) {
// 3.2 获取到参数类型的Clas对象
Class<?> parameterTypeClass = Class.forName(parameterType);
// 3.3 遍历#{}的参数列表
List<ParameterMapping> parameterMappingList = boundSql.getParameterMappingList();
for (int i = 0; i < parameterMappingList.size(); i++) {
ParameterMapping parameterMapping = parameterMappingList.get(i);
// 根据参数名称,获取对象的属性
Field declaredField = parameterTypeClass.getDeclaredField(parameterMapping.getContent());
// 暴力访问,否则私有属性获取不到值
declaredField.setAccessible(true);
Object value = declaredField.get(param);
// 赋值占位符
preparedStatement.setObject(i + 1, value);
}
}
// 4.执行sql,发起查询
resultSet = preparedStatement.executeQuery();
// 5.处理返回结果集
ArrayList<E> list = new ArrayList<>();
String resultType = mappedStatement.getResultType();
Class<?> resultTypeClass = Class.forName(resultType);
while (resultSet.next()) {
// 元数据信息包含了字段名、字段的值
ResultSetMetaData metaData = resultSet.getMetaData();
Object o = resultTypeClass.newInstance();
for (int i = 1; i <= metaData.getColumnCount(); i++) {
// 字段名
String columnName = metaData.getColumnName(i);
// 字段的值
Object value = resultSet.getObject(columnName);
// 通过反射设置参数的值
Field declaredField = resultTypeClass.getDeclaredField(columnName);
declaredField.setAccessible(true);
declaredField.set(o, value);
}
list.add((E) o);
}
return list;
}
}
创建SqlSession接口
java
package com.xk857.session;
public interface SqlSession {
/**
* 查询多个
* @param statementId 根据statementId可找到Map对应的sql语句
* @param param 查询参数
*/
<E> List<E> selectList(String statementId, Object param) throws Exception;
/** 查询单个结果 */
<T> T selectOne(String statementId, Object param) throws Exception;
/** 清除资源 */
void close();
/** 生成代理对象 */
<T> T getMapper(Class<?> mapperClass);
}
创建SqlSession默认实现
java
public class DefaultSqlSession implements SqlSession {
private final Configuration configuration;
private final Executor executor;
public DefaultSqlSession(Configuration configuration, Executor executor) {
this.configuration = configuration;
this.executor = executor;
}
@Override
public <E> List<E> selectList(String statementId, Object param) throws Exception {
// 1.获取MappedStatement对象
MappedStatement mappedStatement = configuration.getMappedStatementMap().get(statementId);
// 2.调用执行器,使用JDBC操作数据库
return executor.query(configuration, mappedStatement, param);
}
@Override
public <T> T selectOne(String statementId, Object param) throws Exception {
// 去调用selectList();
List<Object> list = this.selectList(statementId, param);
if (list.size() == 1) {
return (T) list.get(0);
} else if (list.size() > 1) {
throw new RuntimeException("返回结果过多");
} else {
return null;
}
}
}
创建DefaultSqlSessionFactory
java
public class DefaultSqlSessionFactory implements SqlSessionFactory {
private final Configuration configuration;
public DefaultSqlSessionFactory(Configuration configuration) {
this.configuration = configuration;
}
@Override
public SqlSession openSession() {
// 1.创建执行器对象
Executor simpleExecutor = new SimpleExecutor();
// 2.生产sqlSession对象
return new DefaultSqlSession(configuration, simpleExecutor);
}
}
测试查询功能
java
@Test
public void test() throws Exception {
// 1.读取配置文件信息
InputStream is = Resources.getResourceAsSteam("sqlMapConfig.xml");
// 2.解析配置文件封装成JavaBean对象
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);
// 3.配置执行器及默认实现DefaultSqlSessionFactory
SqlSession sqlSession = sqlSessionFactory.openSession();
// 4.通过namespace+id,找到MapperStatement(其中存放SQL语句),再调用执行器完成查询操作
List<User> userList = sqlSession.selectList("user.selectList", null);
for (User user : userList) {
System.out.println(user);
}
}