本文自定义的数据库持久层框架是参考Mybatis思想设计,本意并非想去重复造轮子,而是希望从框架实现原理角度出发分析其设计思想,以便对Mybatis有更深层次的理解,也有助于看其他框架源码。
public static void main(String[] args) throws SQLException {
Connection connection = null;
PreparedStatement preparedStatement = null;
ResultSet resultSet = null;
try {
Class.forName("com.mysql.jdbc.Driver");
connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/kd-jdbc", "root", "123456");
String sql = "select * from user where username = ?";
preparedStatement = connection.prepareStatement(sql);
preparedStatement.setString(1, "xian");
resultSet = preparedStatement.executeQuery();
ArrayList<User> users = new ArrayList<User>();
while (resultSet.next()) {
User user = new User();
int id = resultSet.getInt("id");
String username = resultSet.getString("username");
user.setId(id);
user.setUsername(username);
users.add(user);
}
} catch (Exception e) {
e.printStackTrace();
}finally {
resultSet.close();
preparedStatement.close();
connection.close();
}
}
以上是一段常见的原生jdbc查询的代码,可以发现实现是很不优雅的,主要存在的问题有:
对于以上的问题,数据库频繁创建、释放资源可以通过接入线程池处理;硬编码问题,可以将需要频繁改动的代码抽离成xml文件,使用dom4j去解析节点;而对于结果集映射,则可以通过对象的反射和内省去完成。同时,要求使用端提供核心配置文件SqlMapConfig.xml和Mapper.xml,而对于框架则需要读取相应的配置信息,完成数据源的构建,SQL语句的解析以及结果集的映射。
SqlMapConfig.xml
<configuration>
<dataSource>
<property name="driverClass" value="com.mysql.jdbc.Driver">property>
<property name="username" value="root">property>
<property name="jdbcUrl" value="jdbc:mysql:///kd-jdbc">property>
<property name="password" value="123456">property>
dataSource>
<mapper resource="UserMapper.xml">mapper>
configuration>
UserMapper.xml
<mapper namespace="com.keduw.dao.UserDao">
<select id="findAll" resultType="com.keduw.pojo.User" >
select * from user
select>
<select id="findById" resultType="com.keduw.pojo.User" paramterType="com.keduw.pojo.User">
select * from user where id = #{id}
select>
mapper>
定义一个Configuration用于保存解析出来的数据源信息和执行语句,执行语句保存在mappedStatementMap中,每一条执行语句就代表一个MappedStatement,而id则是由namespace.id组成。
public class Configuration {
private DataSource dataSource;
private Map<String, MappedStatement> mappedStatementMap = new HashMap<>();
/** 省略get,set方法 **/
}
封装一个工厂类SqlSessionFactoryBuilder用于数据解析和生成SqlSession,主要使用dom4j解析配置文件,将解析出来的内容封装到Configuration中,同时创建SqlSessionFactory对象,生产SqlSession,拿到SqlSession后就可以为所欲为了。
public class SqlSessionFactoryBuilder {
public SqlSessionFactory build(InputStream in) throws DocumentException, PropertyVetoException {
// 使用dom4j解析配置文件
XmlConfigBuilder xmlConfigBuilder = new XmlConfigBuilder();
Configuration configuration = xmlConfigBuilder.parseConfig(in);
// 创建sqlSessionFactory对象
DefaultSqlSessionFactory defaultSqlSessionFactory = new DefaultSqlSessionFactory(configuration);
return defaultSqlSessionFactory;
}
}
在解析SqlMapConfig.xml过程中,引入数据库连接池创建数据源信息,将数据源交由数据库连接池管理,解决数据库连接频繁创建连接和释放资源的问题。
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);
}
// 引入数据库连接池
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.setDataSource(comboPooledDataSource);
// 解析 标签
List<Element> mapperList = rootElement.selectNodes("//mapper");
for (Element element : mapperList) {
String resource = element.attributeValue("resource");
InputStream resourceAsStream = Resource.getResourceAsStream("/" + resource);
XmlMapperBuilder xmlMapperBuilder = new XmlMapperBuilder(configuration);
xmlMapperBuilder.parse(resourceAsStream);
}
return configuration;
}
构建一个XmlMapperBuilder实现对mapper中各种数据节点的解析,将解析出来的数据封装在MappedStatement中,MappedStatement主要包含id标识、返回值类型、参数值类型以及SQL语句。
public class MappedStatement {
//id标识
private String id;
//返回值类型
private String resultType;
//参数值类型
private String paramterType;
//sql语句
private String sql;
/** 省略一些set,get方法 **/
}
这里做了一些简化,实际上Mybatis会将SQL语句解析成一个个的Node节点,因为要支持一些动态查询,不过我们这里出于简单考虑,就忽略动态查询,直接用String去保存。
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);
String key = namespace + "." + id;
configuration.getMappedStatementMap().put(key,mappedStatement);
}
}
基于模板方法设计模式,统一定义查询接口Executor.query(),定义一个简单查询执行器实现具体的查询方法,完成对变量#{}的解析工作,设置参数,执行SQL语句,同时封装返回结果集。具体实现如下:
public <E> List<E> query(Configuration configuration, MappedStatement mappedStatement, Object... params) throws Exception {
//注册驱动, 获取连接
Connection connection = configuration.getDataSource().getConnection();
//获取sql语句
String sql = mappedStatement.getSql();
BoundSql boundSql = getBoundSql(sql);
PreparedStatement preparedStatement = connection.prepareStatement(boundSql.getSqlText());
//设置参数
String paramterType = mappedStatement.getParamterType();
Class<?> clazz = getClassType(paramterType);
List<ParameterMapping> parameterMappingList = boundSql.getParameterMappingList();
for (int i = 0; i < parameterMappingList.size(); i++) {
ParameterMapping parameterMapping = parameterMappingList.get(i);
String content = parameterMapping.getContent();
//反射获取当前对应的属性
Field declaredField = clazz.getDeclaredField(content);
declaredField.setAccessible(true);
Object o = declaredField.get(params[0]);
preparedStatement.setObject(i+1, o);
}
//执行sql
ResultSet resultSet = preparedStatement.executeQuery();
String resultType = mappedStatement.getResultType();
Class<?> resultTypeClass = getClassType(resultType);
//封装返回结果集
ArrayList<Object> objects = new ArrayList<>();
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;
}
使用方式:
public <E> List<E> selectList(String statementId, Object... params) throws Exception {
SimpleExecutor simpleExecutor = new SimpleExecutor();
MappedStatement mappedStatement = configuration.getMappedStatementMap().get(statementId);
List<Object> list = simpleExecutor.query(configuration, mappedStatement, params);
return (List<E>) list;
}
上面的使用方式其实还比较麻烦,可以基于动态代理实现面向接口编程。要求定义的类和方法要跟xml中声明的一样,那样才能生成statementId,获取对应的SQL语句。
/**
* 使用JDK动态代理来为Dao接口生成代理对象,并返回
* @param mapperClass
* @param
* @return
*/
@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;
}
使用方式:
UserDao userDao = sqlSession.getMapper(UserDao.class);
List<User> users = userDao.findAll();
for (User info : users) {
System.out.println(info);
}
到此,就基本实现了我们自定义的数据库持久层框架。如果有看过Mybatis源码会发现整体跟Mybatis很像,像是Mybatis的简化版。代码已经全部提交,感兴趣的可以看看。(地址:https://github.com/lveex/kd-jdbc)