前言
前面两篇文章介绍了JDBC模式开发持久层的不足之处,并且做了下自定义ORM框架设计步骤的思考,约定了配置文件格式。这篇文章主要介绍如何一步步编写一个简单的类似MyBatis的自定义ORM框架,并且对其功能进行测试。
实现功能:
- 解析Mapper.xml配置执行相应SQL
- 根据配置文件初始化c3p0连接池
- 结果集自动封装
- 参数自动解析
- 根据Dao对象映射相应的Mapper.xml配置,来通过反射调用Mapper配置里的文件
代码设计
首先,我们使用c3p0连接池进行数据源连接池的管理,使用dom4j进行配置文件的解析。引入Maven依赖
<dependencies>
<!-- MySQL连接驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.17</version>
</dependency>
<!-- 配置文件解析依赖 -->
<dependency>
<groupId>org.dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>2.1.1</version>
</dependency>
<!-- 数据库连接池 -->
<dependency>
<groupId>c3p0</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.1.2</version>
</dependency>
<dependency>
<groupId>jaxen</groupId>
<artifactId>jaxen</artifactId>
<version>1.1.6</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.10</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.12</version>
</dependency>
</dependencies>
第一步,加载配置文件,创建数据源DataSource
-
配置文件加载器
Resource.java
:提供getResourceAsStream(String path)
方法,传入配置文件路径,把配置文件以字节输入流的形式加载到内存中。/** * 配置文件加载器 */ public class Resource { /** * 根据配置文件的路径,把配置文件加载成字节输入流,放到内存中 * @param path 配置文件路径 * @return 字节输入流 */ public static InputStream getResourceAsStream(String path){ InputStream resourceAsStream = Resource.class.getClassLoader().getResourceAsStream(path); return resourceAsStream; } }
-
创建配置文件容器对象
Configuration.java
、MappedStatement.java
,用来存储配置信息public class Configuration { /** * 数据库连接 */ private DataSource dataSource; /** * SQL映射配置存储 * key:由xxxMapper里的namespace+.+具体SQL映射配置的id组成 * value:SQL映射配置 */ private Map<String, MappedStatement> mappedStatementMap=new HashMap<String, MappedStatement>(); public DataSource getDataSource() { return dataSource; } public void setDataSource(DataSource dataSource) { this.dataSource = dataSource; } public Map<String, MappedStatement> getMappedStatementMap() { return mappedStatementMap; } public void setMappedStatementMap(Map<String, MappedStatement> mappedStatementMap) { this.mappedStatementMap = mappedStatementMap; } } /** * 存储xxxMapper.xml内的信息,每个SQL语句对应一个MappedStatement对象 */ public class MappedStatement { private String id; private Class<?> paramType; private Class<?> resultType; private String sql; private String queryType; public String getId() { return id; } public void setId(String id) { this.id = id; } public Class<?> getParamType() { return paramType; } public void setParamType(Class<?> paramType) { this.paramType = paramType; } public Class<?> getResultType() { return resultType; } public void setResultType(Class<?> resultType) { this.resultType = resultType; } public String getSql() { return sql; } public void setSql(String sql) { this.sql = sql; } public String getQueryType() { return queryType; } public void setQueryType(String queryType) { this.queryType = queryType; } }
-
配置文件装配器
XMLConfigBuilder.java
、XMLMapperBuilder.java
:提供parseConfig(InputStream xmlInputStream)
方法,解析配置文件字节输入流,并根据配置内容创建数据库连接,装填Mapper配置内容到容器对象中/** * 配置文件装配器 */ public class XMLConfigBuilder { private final Configuration configuration; public XMLConfigBuilder() { this.configuration = new Configuration(); } /** * 将配置文件字节输入流,解析封装成Configuration对象 * @param xmlInputStream 配置文件字节输入流 * @return {@link Configuration} 核心配置文件容器对象 */ public Configuration parseConfig(InputStream xmlInputStream) throws DocumentException, PropertyVetoException, ClassNotFoundException { // 把字节输入流使用dom4j解析成Document对象 Document document = new SAXReader().read(xmlInputStream); // 拿到配置文件的根元素对象 Element rootElement = document.getRootElement(); // 取出配置文件内<dataSource>所有的<property>标签元素 Element dataSourceElement = rootElement.element("dataSource"); List<Element> dataSourceElements = dataSourceElement.elements(); Properties configProperties=new Properties(); for (Element property : dataSourceElements) { configProperties.setProperty(property.attribute("name").getStringValue(),property.getText()); } // 使用c3p0连接池创建数据源对象 ComboPooledDataSource dataSource = new ComboPooledDataSource("c3p0");; dataSource.setDriverClass(configProperties.getProperty("driverClass")); dataSource.setJdbcUrl(configProperties.getProperty("url")); dataSource.setUser(configProperties.getProperty("user")); dataSource.setPassword(configProperties.getProperty("pass")); this.configuration.setDataSource(dataSource); // 解析xxxMapper.xml Element mappersElement = rootElement.element("mappers"); List<Element> mapperElementList = mappersElement.elements(); // 取出每一个配置文件的Mapper文件的路径 for (Element mapperElement : mapperElementList) { // 获取Mapper配置文件的字节输入流 InputStream mapperInputStream = Resource.getResourceAsStream(mapperElement.attribute("xml").getStringValue()); // 解析Mapper配置文件 XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(this.configuration); xmlMapperBuilder.parse(mapperInputStream); } return this.configuration; } } public class XMLMapperBuilder { private final Configuration configuration; public XMLMapperBuilder(Configuration configuration) { this.configuration=configuration; } public void parse(InputStream mapperInputStream) throws DocumentException, ClassNotFoundException { // 转换Document Document mapperDocument = new SAXReader().read(mapperInputStream); // 获取根元素 Element rootElement = mapperDocument.getRootElement(); // 获取namespace String namespace = rootElement.attribute("namespace").getStringValue(); // 获取每一个子级别元素(SQL映射配置元素) List<Element> mappedStatementElement = rootElement.elements(); for (Element element : mappedStatementElement) { String id = element.attribute("id").getStringValue(); String resultType = element.attribute("resultType")!=null?element.attribute("resultType").getStringValue():null; String paramType = element.attribute("paramType")!=null?element.attribute("paramType").getStringValue():null; String sql = element.getText(); String sqlQueryType = element.getName(); MappedStatement mappedStatement = new MappedStatement(); mappedStatement.setId(id); if (paramType!=null) { mappedStatement.setParamType(Class.forName(paramType)); } if (resultType!=null) { mappedStatement.setResultType(Class.forName(resultType)); } mappedStatement.setSql(sql); mappedStatement.setQueryType(sqlQueryType); this.configuration.getMappedStatementMap().put(namespace+"."+id,mappedStatement); } } }
第二步,创建数据库连接会话工厂,创建数据库连接会话
会话工厂接口
会话工厂接口SqlSessionFactory.java
及实现类,负责创建数据库会话对象
public interface SqlSessionFactory {
SqlSession openSession();
}
public class DefaultSqlSessionFactory implements SqlSessionFactory{
private Configuration configuration;
private DefaultSqlSessionFactory(){}
public DefaultSqlSessionFactory(Configuration configuration){
this.configuration=configuration;
}
public SqlSession openSession() {
return new DefaultSqlSession(this.configuration);
}
}
数据库连接会话
数据库连接会话对象SqlSession
、DefaultSqlSession
,负责实际SQL的调用执行
public interface SqlSession {
/**
* 调用S执行QL 返回单条结果
*/
<E> E execute(String statementId)throws Exception;
<E> E execute(String statementId,Object... params)throws Exception;
/**
* 调用S执行QL 返回多条结果
*/
<E> List<E> executeReturnList(String statementId)throws Exception;
<E> List<E> executeReturnList(String statementId,Object... params)throws Exception;
<M> M getMapper(Class<M> mClass);
}
public class DefaultSqlSession implements SqlSession{
private Configuration configuration;
public DefaultSqlSession(Configuration configuration) {
this.configuration = configuration;
}
public <E> E execute(String statementId) throws Exception {
List<E> list = this.executeReturnList(statementId);
if (list == null||list.isEmpty()){
return null;
}else if (list.size()>1){
throw new RuntimeException("查询结果超过1条");
}else {
return list.get(0);
}
}
public <E> E execute(String statementId, Object... params) throws Exception {
List<E> list = this.executeReturnList(statementId, params);
if (list == null||list.isEmpty()){
return null;
}else if (list.size()>1){
throw new RuntimeException("查询结果超过1条");
}else {
return list.get(0);
}
}
public <E> List<E> executeReturnList(String statementId) throws Exception {
return this.executeReturnList(statementId,null);
}
public <E> List<E> executeReturnList(String statementId, Object... params) throws Exception {
Executor executor = new SimpleExecutor();
return executor.query(this.configuration, configuration.getMappedStatementMap().get(statementId), params);
}
}
SQL执行器
SQL执行器Executor.java
、SimpleExecutor.java
,负责具体SQL的执行,参数解析和返回值的封装
public interface Executor {
<E>List<E> query(Configuration configuration, MappedStatement mappedStatement, Object... params) throws Exception;
}
public class SimpleExecutor implements Executor{
private Connection connection = null;
public <E> List<E> query(Configuration configuration, MappedStatement mappedStatement, Object... params) throws Exception {
// 获取数据库连接
connection = configuration.getDataSource().getConnection();
// 取得原生SQL语句
String sql = mappedStatement.getSql();
// 处理SQL#{}参数
BoundSQL boundSQL = getBoundSQL(sql);
// 获取要执行的SQL语句
String finalSql = boundSQL.getSqlText();
// 获取入参类型
Class<?> paramType = mappedStatement.getParamType();
//获取预编译PreparedStatement对象,预处理SQL并放入参数
PreparedStatement preparedStatement =connection.prepareStatement(finalSql);
List<ParameterMapping> parameterMappingList =boundSQL.getParameterMappingList();
// 遍历处理入参
for (int i = 0; i < parameterMappingList.size(); i++) {
ParameterMapping parameterMapping = parameterMappingList.get(i);
String name = parameterMapping.getContent();
// 反射
Field declaredField = paramType.getDeclaredField(name);
declaredField.setAccessible(true);
// 参数值
Object o = declaredField.get(params[0]);
// 给占位符赋值
preparedStatement.setObject(i+1,o);
}
// 执行SQL
ResultSet resultSet = preparedStatement.executeQuery();
Class<?> resultType = mappedStatement.getResultType();
// 结果集封装
ArrayList<E> results = new ArrayList<E>();
while (resultSet.next()) {
ResultSetMetaData metaData = resultSet.getMetaData();
E o = (E) resultType.newInstance();
int columnCount = metaData.getColumnCount();
for (int i = 1; i <= columnCount; i++) {
// 属性名
String columnName = metaData.getColumnName(i);
// 属性值
Object value = resultSet.getObject(columnName);
// 创建读写描述器,为属性生成读写方法
PropertyDescriptor propertyDescriptor = new PropertyDescriptor(columnName, resultType);
Method writeMethod = propertyDescriptor.getWriteMethod();
writeMethod.invoke(o,value);
}
results.add(o);
}
// 如果涉及数据的修改变动,需要提交事务
if (mappedStatement.getQueryType().equals("insert")||mappedStatement.getQueryType().equals("update")||mappedStatement.getQueryType().equals("delete")) {
connection.commit();
}
// 返回结果
return results;
}
/**
* 完成对#{}的解析工作:1.将#{}使用?进行代替,2.解析出#{}里面的值进行存储
* @param sql 配置文件内原生SQL
*/
private BoundSQL getBoundSQL(String sql) {
// 标记处理类:用来配合通用标记解析器GenericTokenParser类完成对配置文件等的解析工作,其中TokenHandler主要完成处理
ParameterMappingTokenHandler parameterMappingTokenHandler = new ParameterMappingTokenHandler();
// GenericTokenParser:通用标记解析器,完成代码片中的占位符的解析,然后根据给定的标记处理器TokenHandler来进行表达式的处理
// 三个参数:分别为openToken(开始标记)、closeToken(结束标记)、handler(标记处理器)
GenericTokenParser genericTokenParser = new GenericTokenParser("#{","}",parameterMappingTokenHandler);
// 解析出来的SQL
String parse = genericTokenParser.parse(sql);
//#{}里面解析出来的参数名称
List<ParameterMapping> parameterMappings = parameterMappingTokenHandler.getParameterMappings();
return new BoundSQL(parse, parameterMappings);
}
}
测试功能
-
数据库创建sm_user表
CREATE TABLE `sm_user` ( `id` varchar(11) NOT NULL, `userName` varchar(20) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-
创建实体对象
SmUser
与sm_user表对应package com.yxh.www.testorm.entity; public class SmUser { private String id; private String userName; public String getId() { return id; } public void setId(String id) { this.id = id; } public String getUserName() { return userName; } public void setUserName(String userName) { this.userName = userName; } @Override public String toString() { return "SmUser{" + "id='" + id + '\'' + ", userName='" + userName + '\'' + '}'; } }
-
新建项目,配置数据库配置、Mapper配置
dbConfig.xml
内容:<!-- 配置根标签 --> <configuration> <!-- 定义数据库连接信息配置 --> <dataSource> <!-- 数据库连接驱动 --> <property name="driverClass">com.mysql.jdbc.Driver</property> <!-- 数据库连接地址 --> <property name="url">jdbc:mysql://127.0.0.1:3306/simple-orm</property> <!-- 数据库连接用户 --> <property name="user">root</property> <!-- 数据库连接密码 --> <property name="pass">yxh123..</property> </dataSource> <!-- 定义SQL语句映射相关信息 --> <mappers> <mapper xml="SmUserMapper.xml" classPath="com.yxh.www.testorm.dao.SmUserDao"/> </mappers> </configuration>
SmUserMapper.xml
内容:<mapper namespace="SmUserDao"> <!-- 查询列表 --> <select id="listSmUser" resultType="com.yxh.www.testorm.entity.SmUser" paramType="com.yxh.www.testorm.entity.SmUser"> select * from sm_user where id=#{id} </select> <!-- 查询单个用户 需要传参 --> <select id="getSmUser" paramType="com.yxh.www.testorm.entity.SmUser" resultType="com.yxh.www.testorm.entity.SmUser"> select * from sm_user where id=#{id} and user_name=#{userName} </select> <!-- 增加用户 --> <insert id="insertSmUser" paramType="com.yxh.www.testorm.entity.SmUser"> insert into sm_user(id,user_name) values(#{id},#{userName}) </insert> <!-- 修改用户 --> <update id="updateUser" paramType="com.yxh.www.testorm.entity.SmUser"> update sm_user set user_name=#{userName} where id=#{id} </update> <!-- 删除用户 --> <delete id="deleteSmUserById" paramType="java.lang.String"> delete from sm_user where id=#{id} </delete> </mapper>
-
编写测试代码,调用listSmUser
// 参数 SmUser param=new SmUser(); param.setId("2"); // 获取配置信息 InputStream resourceAsStream = Resource.getResourceAsStream("dbConfig.xml"); // 创建数据库连接会话对象 SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream); SqlSession sqlSession = sqlSessionFactory.openSession(); // 调用SQL方法 List<SmUser> smUserList = sqlSession.selectList("smUser.listSmUser", SmUser.class,param); for (SmUser smUser : smUserList) { System.out.println(smUser.toString()); }
控制台输出:
优化框架
发现在调用实际SQL指定statementId时还是存在硬编码问题,不够方便,考虑建立Mapper.xml配置对应的Java接口对象,通过Java反射来解决这个问题
SqlSession.java添加方法:getMapper(Class<?> mClass),用来获取代理生成对象
SqlSession接口增加方法
<M> M getMapper(Class<M> mClass);
DefaultSqlSession实现类新增方法:
public <M> M getMapper(Class<M> mClass) {
M m=(M) Proxy.newProxyInstance(mClass.getClassLoader(), new Class[]{mClass}, new InvocationHandler() {
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 获取方法名
String methodName = method.getName();
String className = method.getDeclaringClass().getSimpleName();
// statementId
String statementId=className+"."+methodName;
Type genericReturnType=method.getGenericReturnType();
// 判断是否实现泛型类型参数化
if(genericReturnType instanceof ParameterizedType){
return executeReturnList(statementId,args);
}
return execute(statementId,Object.class,args);
}
});
return m;
}
建立SmUserDao
接口,与Mapper.xml配置之间映射
public interface SmUserDao {
List<SmUser> listSmUser(SmUser smUser);
}
修改测试代码,使用代理调用并执行SQL
// 参数
SmUser param=new SmUser();
param.setId("2");
// 获取配置信息
InputStream resourceAsStream = Resource.getResourceAsStream("dbConfig.xml");
// 创建数据库连接会话对象
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
// 使用代理方式调用SQL
SmUserDao smUserDao=sqlSession.getMapper(SmUserDao.class);
List<SmUser> smUserList = smUserDao.listSmUser(param);
for (SmUser smUser : smUserList) {
System.out.println(smUser.toString());
}
控制台输出: