读取配置文件
编写Resource类,将路径转化为InputStream流。
利用SqlSesssionFactoryBuilder,来创建SqlSessionFactory对象。
public class Resources {
public static InputStream getResourceAsStream(String path) {
return Resources.class.getClassLoader().getResourceAsStream(path);
}
}
public class SqlSessionFactoryBuilder {
// SqlSessionFactory后续创建
public SqlSessionFactory build(InputStream inputStream) throws PropertyVetoException, DocumentException {
return null;
}
}
创建Configuration对象和MapperStatement对象
需要了解两个容器中需要存放什么内容,根据内容来创建对象,Configuration要保存DataSource的信息,并且需要持有MapperStatement对象。
MapperStatement存放的是sqlMapper中一个标签,也就是select或者其它的标签。
/**
* mapper的每一个标签都是一个mapperStatement
*/
public class MapperStatement {
/**
* id
*/
private String id;
/**
* 返回类型
*/
private String resultType;
/**
* 参数类型
*/
private String paramterType;
/**
* sql内容
*/
private String sql;
public MapperStatement() {
}
public MapperStatement(String id, String resultType, String paramterType, String sql) {
this.id = id;
this.resultType = resultType;
this.paramterType = paramterType;
this.sql = 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 getParamterType() {
return paramterType;
}
public void setParamterType(String paramterType) {
this.paramterType = paramterType;
}
public String getSql() {
return sql;
}
public void setSql(String sql) {
this.sql = sql;
}
}
public class Configuration {
/**
* 数据库的连接信息
*/
private DataSource dataSource;
/**
* key是什么呢? key要能唯一标识MapperStatement,前面说到,使用namespace.id可以唯一标识一个sql映射
*/
private Map<String, MapperStatement> mapperStatementMap = new HashMap<>();
public DataSource getDataSource() {
return dataSource;
}
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
}
public Map<String, MapperStatement> getMapperStatementMap() {
return mapperStatementMap;
}
public void setMapperStatementMap(Map<String, MapperStatement> mapperStatementMap) {
this.mapperStatementMap = mapperStatementMap;
}
}
dom4j解析配置文件进行封装
这个步骤非常重要,要解析Config配置文件和sql配置文件,解析应该放在 SqlSessionFactoryBuilder 类中,层层向下传递。
整个过程用到的pom依赖如下:
<dependencies>
<!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.22</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>
public class SqlSessionFactoryBuilder {
public SqlSessionFactory build(InputStream inputStream) throws PropertyVetoException, DocumentException {
// 读取配置文件
XMLConfigBuilder xmlConfigBuilder = new XMLConfigBuilder();
Configuration configuration = xmlConfigBuilder.parseConfiguration(inputStream);
return null;
}
}
工具类进行解析:
public class XMLConfigBuilder {
private Configuration configuration;
public XMLConfigBuilder() {
this.configuration = new Configuration();
}
/**
* 封装Configuration
* @param inputStream
* @return
* @throws DocumentException
* @throws PropertyVetoException
*/
public Configuration parseConfiguration(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) {
properties.put(element.attributeValue("name"), element.attributeValue("value"));
}
// 获取数据库连接池,使用c3p0
ComboPooledDataSource comboPooledDataSource = getComboPooledDataSource(properties);
configuration.setDataSource(comboPooledDataSource);
// 封装mapperStatement
XMLMapperConfigBuilder xmlMapperConfigBuilder = new XMLMapperConfigBuilder(configuration);
List<Element> mappers = document.selectNodes("//mapper");
for (Element mapper : mappers) {
String resource = mapper.attributeValue("resource");
xmlMapperConfigBuilder.parse(Resources.getResourceAsStream(resource));
}
return configuration;
}
/**
* 根据配置信息创建c3p0连接池
* @param properties
* @return
* @throws PropertyVetoException
*/
private ComboPooledDataSource getComboPooledDataSource(Properties properties) throws PropertyVetoException {
ComboPooledDataSource comboPooledDataSource = new ComboPooledDataSource();
comboPooledDataSource.setDriverClass(properties.getProperty("jdbcDriver"));
comboPooledDataSource.setJdbcUrl(properties.getProperty("jdbcUrl"));
comboPooledDataSource.setUser(properties.getProperty("jdbcUser"));
comboPooledDataSource.setPassword(properties.getProperty("jdbcPassword"));
return comboPooledDataSource;
}
}
public class XMLMapperConfigBuilder {
private Configuration configuration;
public XMLMapperConfigBuilder(Configuration configuration) {
this.configuration = configuration;
}
public void parse(InputStream inputStream) throws DocumentException {
Document document = new SAXReader().read(inputStream);
String nameSpaceURI = document.getRootElement().attributeValue("namespace");
List<Element> selectNodes = document.selectNodes("//select");
for (Element selectNode : selectNodes) {
String id = selectNode.attributeValue("id");
MapperStatement mapperStatement = new MapperStatement(id, selectNode.attributeValue("resultType"), selectNode.attributeValue("paramterType"), selectNode.getTextTrim());
configuration.getMapperStatementMap().put(nameSpaceURI + "." + id, mapperStatement);
}
}
}
SqlSessionFactory
SOLID原则,面向接口编程,SqlSessionFactory的作用是生产SqlSession会话对象,如下:
/**
* 工厂类,用于创建会话对象 SqlSession
*/
public interface SqlSessionFactory {
public 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);
}
}
public class SqlSessionFactoryBuilder {
public SqlSessionFactory build(InputStream inputStream) throws PropertyVetoException, DocumentException {
// 读取配置文件
XMLConfigBuilder xmlConfigBuilder = new XMLConfigBuilder();
Configuration configuration = xmlConfigBuilder.parseConfiguration(inputStream);
// 创建SqlSessionFactory
SqlSessionFactory sqlSessionFactory = new DefaultSqlSessionFactory(configuration);
return sqlSessionFactory;
}
}
SqlSession
同样,面向接口编程,SqlSession要做的是就是去定义数据库的执行方法
public interface SqlSession {
<E> List<E> selectAll(String statementId, Object... params) throws SQLException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException, NoSuchMethodException, InstantiationException, InvocationTargetException, IntrospectionException;
<T> T selectOne(String statementId, Object... params) throws SQLException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException, NoSuchMethodException, InvocationTargetException, InstantiationException, IntrospectionException;
}
public class DefaultSqlSession implements SqlSession {
private Configuration configuration;
public DefaultSqlSession(Configuration configuration) {
this.configuration = configuration;
}
@Override
public <E> List<E> selectAll(String statementId, Object... params) throws SQLException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException, NoSuchMethodException, InstantiationException, InvocationTargetException, IntrospectionException {
SimpleExecutor simpleExecutor = new SimpleExecutor();
List<E> query = simpleExecutor.query(configuration, configuration.getMapperStatementMap().get(statementId), params);
return query;
}
@Override
public <T> T selectOne(String statementId, Object... params) throws SQLException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException, NoSuchMethodException, InvocationTargetException, InstantiationException, IntrospectionException {
List<Object> objects = selectAll(statementId, params);
if (objects.size() == 1) {
return (T)objects.get(0);
} else {
throw new RuntimeException("查询结果过多或者查询结果为空");
}
}
}
Executor执行
执行器,进行具体的执行操作,从连接 - 预处理sql - 执行sql - 封装结果集都在这一步
public interface MyExecutor {
<E> List<E> query(Configuration configuration, MapperStatement mapperStatement, Object... params) throws SQLException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException, NoSuchMethodException, InvocationTargetException, InstantiationException, IntrospectionException;
}
package com.wangzhi.config.sqlsession;
import com.wangzhi.config.pojo.BoundSql;
import com.wangzhi.config.pojo.Configuration;
import com.wangzhi.config.pojo.MapperStatement;
import com.wangzhi.config.utils.GenericTokenParser;
import com.wangzhi.config.utils.ParameterMapping;
import com.wangzhi.config.utils.ParameterMappingTokenHandler;
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 MyExecutor {
@Override
public <E> List<E> query(Configuration configuration, MapperStatement mapperStatement, Object... params) throws SQLException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException, NoSuchMethodException, InvocationTargetException, InstantiationException, IntrospectionException {
// 封装数据库的操作步骤,获取数据库连接
Connection connection = configuration.getDataSource().getConnection();
// 替换占位符(这个步骤是重点,sql转换过程)
BoundSql boundSql = getBoundSql(mapperStatement.getSql());
// 预处理sql
PreparedStatement preparedStatement = connection.prepareStatement(boundSql.getSqlText());
// 设置参数开始
// 利用反射来获取,先要获取类的字节码文件
Class<?> parameterClazz = getClassType(mapperStatement.getParamterType());
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
for (int i = 0; i < parameterMappings.size(); i++) {
// 需要获取的字段
ParameterMapping parameterMapping = parameterMappings.get(i);
Field declaredField = parameterClazz.getDeclaredField(parameterMapping.getContent());
declaredField.setAccessible(true);
Object obj = declaredField.get(params[0]);
preparedStatement.setObject(i + 1, obj);
}
// 设置参数结束
// 执行sql
ResultSet resultSet = preparedStatement.executeQuery();
// 处理结果集
Class<?> resultClazzType = getClassType(mapperStatement.getResultType());
List<Object> resultList = new ArrayList<>();
while (resultSet.next()) {
Object obj = resultClazzType.getDeclaredConstructor().newInstance();
ResultSetMetaData metaData = resultSet.getMetaData();
for (int i = 1; i <= metaData.getColumnCount(); i++) {
// 字段名 metaDate的下标从1开始
String columnName = metaData.getColumnName(i);
// 字段值
Object columnValue = resultSet.getObject(columnName);
// 用来设置值,内省,用来获取方法的读写权限
PropertyDescriptor propertyDescriptor = new PropertyDescriptor(columnName, resultClazzType);
Method writeMethod = propertyDescriptor.getWriteMethod();
writeMethod.invoke(obj, columnValue);
}
resultList.add(obj);
}
return (List<E>) resultList;
}
private Class<?> getClassType(String paramterType) throws ClassNotFoundException {
if (paramterType != null) {
Class<?> aClass = Class.forName(paramterType);
return aClass;
}
return null;
}
/**
* 处理sql,替换占位符,接触的工具类属于Mybatis源码的内容
* @param sql 替换之前的sql
* @return
*/
private BoundSql getBoundSql(String sql) {
// 标记处理器,配置标记解析器来完成对占位符的解析处理工作
ParameterMappingTokenHandler parameterMappingTokenHandler = new ParameterMappingTokenHandler();
// 标记解析器
GenericTokenParser genericTokenParser = new GenericTokenParser("#{", "}", parameterMappingTokenHandler);
// 解析sql,返回替换占位符后的sql,也就是将占位符替换为 ?
String parse = genericTokenParser.parse(sql);
// 保存了占位符里面的内容,针对于示例中selectOne的id和name
List<ParameterMapping> parameterMappings = parameterMappingTokenHandler.getParameterMappings();
BoundSql boundSql = new BoundSql(parse, parameterMappings);
return boundSql;
}
}
上面用到了工具类,可以再具体的代码里面找到,放在码云中。
到这基本就结束了,目前存在的问题就是在使用方,还有硬编码的实现以及代码重复性严重。为了模仿mybatis,需要使用动态代理来进行方法的执行。
优化:
public interface UserMapper {
List<User> findAll();
User findByCondition(User user);
}
在sqlSession中添加getMapper方法,来进行动态代理执行
public interface SqlSession {
<E> List<E> selectAll(String statementId, Object... params) throws SQLException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException, NoSuchMethodException, InstantiationException, InvocationTargetException, IntrospectionException;
<T> T selectOne(String statementId, Object... params) throws SQLException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException, NoSuchMethodException, InvocationTargetException, InstantiationException, IntrospectionException;
<T> T getMapper(Class<T> mapperClass);
}
public class DefaultSqlSession implements SqlSession {
private Configuration configuration;
public DefaultSqlSession(Configuration configuration) {
this.configuration = configuration;
}
@Override
public <E> List<E> selectAll(String statementId, Object... params) throws SQLException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException, NoSuchMethodException, InstantiationException, InvocationTargetException, IntrospectionException {
SimpleExecutor simpleExecutor = new SimpleExecutor();
List<E> query = simpleExecutor.query(configuration, configuration.getMapperStatementMap().get(statementId), params);
return query;
}
@Override
public <T> T selectOne(String statementId, Object... params) throws SQLException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException, NoSuchMethodException, InvocationTargetException, InstantiationException, IntrospectionException {
List<Object> objects = selectAll(statementId, params);
if (objects.size() == 1) {
return (T)objects.get(0);
} else {
throw new RuntimeException("查询结果过多或者查询结果为空");
}
}
/**
* 动态代理生产对象,在使用该对象调用方法的时候,全部执行改方法里面的invoke方法
* @param mapperClass
* @param <T>
* @return
*/
@Override
public <T> T getMapper(Class<T> mapperClass) {
Object proxy = Proxy.newProxyInstance(DefaultSqlSession.class.getClassLoader(), new Class[]{mapperClass}, new InvocationHandler() {
// proxy 当前代理对象的引用 method 当前被调用方法的引用 args就是传递的参数
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// invoke方法中来调用具体的方法,要做的事有下面两件
// 封装参数开始
// 获取方法名
String methodName = method.getName();
// 获取全限定类名
String className = method.getDeclaringClass().getName();
// 组合成为statementId
// 封装参数结束
// 确认调用的方法
Type genericReturnType = method.getGenericReturnType();
// 判断是否进行了参数泛型化,如果存在泛型就是集合,调用List方法
if (genericReturnType instanceof ParameterizedType) {
return selectAll(className + "." + methodName, args);
}
return selectOne(className + "." + methodName, args);
}
});
return (T) proxy;
}
}
优化Mapper.xml,也就是sql配置,标准化并且保证statementId的唯一性
<mapper namespace="com.wangzhi.dao.UserMapper">
<select id="findAll" resultType="com.wangzhi.pojo.User">
select * from user
</select>
<select id="findByCondition" resultType="com.wangzhi.pojo.User" paramterType="com.wangzhi.pojo.User">
SELECT * FROM user
WHERE id = #{id} AND name = #{name}
</select>
</mapper>
至此,如果安装了mybatis插件就可以看到,在xml文件中提示可以跳转到interface
测试类的优化和使用如下:
@Test
public void testResources() throws PropertyVetoException, DocumentException, IllegalAccessException, InvocationTargetException, IntrospectionException, InstantiationException, NoSuchFieldException, SQLException, NoSuchMethodException, ClassNotFoundException {
InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapperConfig.xml");
SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
SqlSession sqlSession = sqlSessionFactoryBuilder.build(resourceAsStream).openSession();
// 这里动态代理生成对象
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
// 调用方法实际上调用的是getMapper中的invoke方法
List<User> all = mapper.findAll();
all.forEach(System.out::println);
User user = new User();
user.setId(1);
user.setName("王智");
User byCondition = mapper.findByCondition(user);
System.out.println(byCondition);
}
整个代码放在码云,IPersistence为编写的框架类,后面有_test的为测试类。
整个过程学到了什么,回顾了什么?
- JDBC的整个操作流程,回顾细化。 从加载驱动 - 建立连接 - 预处理sql - 执行sql - 处理结果集的整个流程
- dom4j的文档解析
- 反射技术
- 工厂模式
- SOLID原则,面向接口编程
- 动态代理
- mybatis的一点点原理