一、前言
本文参考Mybatis设计思想,自定义了数据库持久层框架kd-jdbc,从jdbc存在问题的角度出发,实现参数映射,sql解析,面向接口编程等功能。
二、JDBC存在的问题
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查询的代码,可以发现实现是很不优雅的,主要存在的问题有:
1. 频繁的创建数据库连接,造成系统资源浪费
2. SQL语句硬编码在代码中,与业务代码耦合
3. PreparedStatement参数绑定的过程存在硬编码,对于一些where动态条件,硬编码不利于代码维护
4. 对SQL结果集映射存在硬编码
三、问题的解决思路
对于以上的问题,数据库频繁创建、释放资源可以通过接入线程池处理;硬编码问题,可以将需要频繁改动的代码抽离成xml文件,使用dom4j去解析节点;而对于结果集映射,则可以通过对象的反射和内省去完成。同时,要求使用端提供核心配置文件SqlMapConfig.xml和Mapper.xml,而对于框架则需要读取相应的配置信息,完成数据源的构建,SQL语句的解析以及结果集的映射。
四、代码实现
4.1 使用端核心配置文件
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.xml的全路径-->
<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>
<!-- 根据用户id查询信息 -->
<select id="findById" resultType="com.keduw.pojo.User" paramterType="com.keduw.pojo.User">
select * from user where id = #{id}
</select>
</mapper>
4.2 封装数据源信息和查询语句
定义一个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;
}
}
4.3 引入数据库连接池
在解析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);
// 解析<mapper />标签
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;
}
4.4 SQL解析
构建一个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");
// 解析所有的<select />标签
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);
}
}
4.5 查询,封装结果集
基于模板方法设计模式,统一定义查询接口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;
}
4.6 面向接口编程
上面的使用方式其实还比较麻烦,可以基于动态代理实现面向接口编程。要求定义的类和方法要跟xml中声明的一样,那样才能生成statementId,获取对应的SQL语句。
/**
* 使用JDK动态代理来为Dao接口生成代理对象,并返回
* @param mapperClass
* @param <T>
* @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的设计思想,对完整代码感兴趣的可以关注公众号"不孤独的字符串",私信获取!
网友评论