1.问题与分析
在没有orm框架时,可能我们大多数操作都停留在了对jdbc的操作上,也许我们查到一个user对象,可能需要画大量篇幅来写。如下
public static void main(String[] args) {
Connection connection = null;
PreparedStatement preparedStatement = null;
ResultSet resultSet = null;
try {
Class.forName("com.mysql.cj.jdbc.Driver");
connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/test?serverTimezone=UTC", "root", "root");
String sql = "select * from user";
preparedStatement = connection.prepareStatement(sql);
resultSet = preparedStatement.executeQuery();
while (resultSet.next()) {
System.out.println(resultSet.getInt("id") + " " + resultSet.getString("username"));
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (resultSet != null) {
try {
resultSet.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (preparedStatement != null) {
try {
preparedStatement.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (connection != null) {
try {
connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
所以我们在针对不同对象、不同数据表时可能需要编写大量冗余的jdbc的连接与释放以及相同处理逻辑的代码。 总结一下有一下几点: 1、 数据库连接创建、释放频繁造成系统资源浪费,从?影响系统性能。 2、 Sql语句在代码中硬编码,造成代码不易维护,实际应?中sql变化的可能较?,sql变动需要改变java代码。 3、 使?preparedStatement向占有位符号传参数存在硬编码,因为sql语句的where条件不?定,可能多也可能少,修改sql还要修改代码,系统不易维护。 4、 对结果集解析存在硬编码(查询列名),sql变化导致解析代码变化,系统不易维护,如果能将数据 库记录封装成pojo对象解析比较方便。
2.解决思路
针对上述问题提出一下的解决思路 ①使?数据库连接池初始化连接资源 ②将sql语句抽取到xml配置?件中 ③使?反射、内省等底层技术,?动将实体与表进?属性与字段的?动映射
3.自定义框架设计
通过对思路的抽象,做出以下的设计:
3.1 使用端:
提供核心配置文件: sqlMapConfig.xml : 存放数据源信息,引?mapper.xml Mapper.xml : sql语句的配置?件信息
3.2 框架端:
1.读取配置文件
读取完成以后以流的形式存在,我们不能将读取到的配置信息以流的形式存放在内存中,不好操作,可以创建javaBean来存储 (1)Configuration : 存放数据库基本信息、Map<唯?标识,Mapper> 唯?标识:namespace .id (2)MappedStatement:sql语句、statement类型、输?参数java类型、输出参数java类型
2.解析配置文件
创建sqlSessionFactoryBuilder类: 方法:sqlSessionFactory build(): 第?:使?dom4j解析配置?件,将解析出来的内容封装到Configuration和MappedStatement中 第?:创建SqlSessionFactory的实现类DefaultSqlSession
3.创建SqlSessionFactory
方法:openSession() : 获取sqlSession接?的实现类实例对象
4. 创建sqlSession接口及实现类
方法: selectList(String statementId,Object param):查询所有 selectOne(String statementId,Object param):查询单个 具体实现:封装JDBC完成对数据库表的查询操作
自定义框架实现
首先我们先模拟出我们实现后的效果,再针对效果进行具体的实现。 使用自定义框架时我们需要配置数据源信息,在sqlMapConfig.xml中,如下:
<configuration>
<dataSource>
<property name = "driverClass" value="com.mysql.cj.jdbc.Driver"></property>
<property name = "jdbcUrl" value="jdbc:mysql://localhost:3306/test?serverTimezone=UTC"></property>
<property name = "username" value="root"></property>
<property name = "password" value="root"></property>
</dataSource>
<mapper resource="UserMapper.xml"></mapper>
</configuration>
实体对象与数据源的sql映射关系存放于mapper.xml文件中,上面代码中的标签定义的就是对sql映射关系的声明。
<mapper namespace="com.hunter.dao.IUserDao">
<select id="findAll" resultType="com.hunter.pojo.User">
select * from user
</select>
<select id="findByCondition" resultType="com.hunter.pojo.User" paramterType="com.hunter.pojo.User">
select * from user where id = #{id} and username = #{username}
</select>
</mapper>
实体对象
public class User {
private Integer id;
private String username;
public Integer getId() {
return id;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", username='" + username + '\'' +
'}';
}
public void setId(Integer id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
调用测试方法:
public void test() throws Exception {
InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(resourceAsStream);
SqlSession sqlSession = factory.openSession();
IUserDao userDao = sqlSession.getMapper(IUserDao.class);
List<User> all = userDao.findAll();
for (User user : all) {
System.out.println(user);
}
}
我们传统的使用mybatis的流程如上所示,接下来我们根据分析的流程来着手对mybatis自定义框架的开发。 在mybatis的使用过程中mapper.xml是最核心的一个配置,定义了namespace和自定义的SQL语句,我们可以将其封装成MapperStatement,MapperStatement是以sql为维度的封装方式,其中类中会存储namespace的标识id,id主要由(namespace+自定义sql的id构成)。结构如下:
package com.hunter.pojo;
public class MapperStatement {
private String id;
private String returnType;
private String paramterType;
private String sql;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getReturnType() {
return returnType;
}
public void setReturnType(String returnType) {
this.returnType = returnType;
}
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;
}
}
定义好了我们所需要的加载后的数据结构,接下来,我们需要通过读取数据库和mybatis的配置。该配置主要通过xml形式声明,所以我们需要一个初始化一个xml解析器,并把转换后的文件信息转化为配置实例。注意:这里的configuration我们使用的是建造者模式,对configuration有多道程序的赋值配置。 配置的结构如下:
package com.hunter.pojo;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
public class Configuration {
private DataSource dataSource;
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;
}
}
这里的配置来主要包括数据库连接配置与加载mapper.xml的MapperStatement映射。 这里的configuration封装了我们的数据库信息与mybatis的mapper映射信息,我们在获取sqlsession时可以从configuration获取相关的连接以及mapper语句。而我们使用工厂类来封装sqlsession,每个获取数据库连接请求时都从工厂类获取session。具体的实现如下:
public class SqlSessionFactoryBuilder {
public SqlSessionFactory build(InputStream in) throws DocumentException, PropertyVetoException {
XMLConfigBuilder xmlConfigBuilder = new XMLConfigBuilder();
Configuration configuration = xmlConfigBuilder.parseConfig(in);
DefaultSqlSessionFactory defaultSqlSessionFactory = new DefaultSqlSessionFactory(configuration);
return defaultSqlSessionFactory;
}
}
XMLConfigBuilder的主要功能是对xml进行解析与configuration的构造等。
package com.hunter.config;
import com.hunter.io.Resources;
import com.hunter.pojo.Configuration;
import com.mchange.v2.c3p0.ComboPooledDataSource;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import java.beans.PropertyVetoException;
import java.io.InputStream;
import java.util.List;
import java.util.Properties;
public class XMLConfigBuilder {
private Configuration configuration;
public XMLConfigBuilder(){
this.configuration = new Configuration();
}
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 mapperPath = element.attributeValue("resource");
InputStream resourceAsStream = Resources.getResourceAsStream(mapperPath);
XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(configuration);
xmlMapperBuilder.parse(resourceAsStream);
}
return configuration;
}
}
在前段我们主要做的是对sqlMapConfig.xml的解析(数据库连接信息以及mapper的声明信息)。 xmlMapperBuilder.parse(resourceAsStream)是对mapper.xml信息的解析并将解析后的sql封装到configuration中。
package com.hunter.config;
import com.hunter.pojo.Configuration;
import com.hunter.pojo.MapperStatement;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import java.io.InputStream;
import java.util.List;
public class XMLMapperBuilder {
private Configuration configuration;
public XMLMapperBuilder(Configuration configuration) {
this.configuration = configuration;
}
public void parse(InputStream inputStream) throws DocumentException {
Document document = new SAXReader().read(inputStream);
Element rootElement = document.getRootElement();
String namespace = rootElement.attributeValue("namespace");
List<Element> selectList = rootElement.selectNodes("//select");
if (selectList != null && selectList.size() > 0){
for (Element element : selectList) {
String id = element.attributeValue("id");
String resultType = element.attributeValue("resultType");
String paramterType = element.attributeValue("paramterType");
String textTrim = element.getTextTrim();
MapperStatement mapperStatement = new MapperStatement();
mapperStatement.setId(id);
mapperStatement.setReturnType(resultType);
mapperStatement.setParamterType(paramterType);
mapperStatement.setSql(textTrim);
String key = namespace + "." + id;
configuration.getMapperStatementMap().put(key,mapperStatement);
}
}
}
}
这里需要注意的是,我们在对mapperStatement封装映射的时候,通过namespace+mapper标签语句的id构成唯一id。configuration中的mapperStatementMap存储的sql语句是未对其中标签进行解析的原始语句。如select id from user where id =#{id}, 并未对#{id}进行解析,当然这里的#{id}也只是个标记,具体的需要在真正执行时做替换填充。
我们对configuration进行配置赋值后,我们需要对session工厂获取连接上做处理。
public class DefaultSqlSessionFactory implements SqlSessionFactory{
private Configuration configuration;
public DefaultSqlSessionFactory(Configuration configuration) {
this.configuration = configuration;
}
@Override
public SqlSession openSession() {
return new DefaultSqlSession(configuration);
}
}
sqlsession主要的参数其实就是configuration,这里主要sqlSession主要以open以及crud操作为主,暂略去close功能。
想象一下,我们使用sqlsession有什么功能,我们可以执行selectList或者selectOne操作等等。我们这里就用这两个操作为例。当然,我们得考虑为我们的Dao接口生成一个代理类,这样我们直接调用dao层就可以实现我们想要的功能了。
package com.hunter.sqlSession;
import java.beans.IntrospectionException;
import java.lang.reflect.InvocationTargetException;
import java.sql.SQLException;
import java.util.List;
public interface SqlSession {
<E> List<E> selectList(String statementId, Object... params) throws SQLException, IllegalAccessException, IntrospectionException, InstantiationException, NoSuchFieldException, InvocationTargetException, ClassNotFoundException;
public <T> T selectOne(String statementId, Object... params) throws SQLException, IllegalAccessException, IntrospectionException, InstantiationException, ClassNotFoundException, InvocationTargetException, NoSuchFieldException;
public <T> T getMapper(Class<?> mapperClass);
}
我们想象一下,如果我们通过Dao层直接获取接口数据要怎么做?这很常见,我们只需要告诉mybatis我们需要对哪个Dao做操作即可,如下
IUserDao userDao = sqlSession.getMapper(IUserDao.class);
List<User> all = userDao.findAll();
for (User user : all) {
System.out.println(user);
}
很明显我们需要对getMapper这个方法进行功能上的增强,换种专业术语就是做代理,那怎么做代理,代理什么功能。这里我们主要用jdk动态代理实现,因为我们的Dao层一般是接口化的,代理的功能我们挑选最简单的select功能。
那么下一个问题来了,我怎么通过接口类获取到Dao下面的方法以及sql呢? 使用过mybatis的人都知道,我们的dao层其实是与mapper.xml中的标签id映射的,而namespace也是我们dao层的全限定类名。 因此我们可以从configuration中获取到namespace,通过需要执行的操作获取到操作id,我们的唯一key是namespace+id从而可以确定到唯一的MapperStatement,里面封装了mapper操作标签的信息。 实现如下:
@Override
public <T> T getMapper(Class<?> mapperClass) {
Object instance = 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 returnType = method.getGenericReturnType();
if (returnType instanceof ParameterizedType){
return selectList(statementId,args);
}
return selectOne(statementId,args);
}
});
return (T) instance;
}
接下来我们要如何实现selectList和selectOne,其实就是我们的告诉configuration我们需要执行的statementId还有我需要查询的参数信息。而我们的selectList方法会通过Executor去执行具体的sql操作。
我们来看一下Executor的接口信息(暂时只演示query方法)
package com.hunter.sqlSession;
import com.hunter.pojo.Configuration;
import com.hunter.pojo.MapperStatement;
import java.beans.IntrospectionException;
import java.lang.reflect.InvocationTargetException;
import java.sql.SQLException;
import java.util.List;
public interface Executor {
<E> List<E> query(Configuration configuration, MapperStatement mapperStatement, Object... params) throws SQLException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException, IntrospectionException, InstantiationException, InvocationTargetException;
}
虽然这个接口看似简单,但是我们总结一下执行一个query方法需要做什么 1.首先需要获取数据库连接 2.由于我们mapperStatement中的sql是未解析的所以需要最占位符等做转换。 3.对转化为占位符的sql做数据的填充 4.执行具体的sql 5.对结果集进行封装
这里的标签解析并转换为占位符其实使用mybatis原有的工具方法。
package com.hunter.utils;
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;
}
public String parse(String text) {
if (text == null || text.isEmpty()) {
return "";
}
int start = text.indexOf(openToken, 0);
if (start == -1) {
return text;
}
char[] src = text.toCharArray();
int offset = 0;
final StringBuilder builder = new StringBuilder();
StringBuilder expression = null;
while (start > -1) {
if (start > 0 && src[start - 1] == '\\') {
builder.append(src, offset, start - offset - 1).append(openToken);
offset = start + openToken.length();
} else {
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] == '\\') {
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) {
builder.append(src, start, src.length - start);
offset = src.length;
} else {
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();
}
}
这里的ParameterMappingTokenHandler其实就是封装这占位符
package com.hunter.utils;
import java.util.ArrayList;
import java.util.List;
public class ParameterMappingTokenHandler implements TokenHandler {
private List<ParameterMapping> parameterMappings = new ArrayList<ParameterMapping>();
@Override
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;
}
}
package com.hunter.utils;
public class ParameterMapping {
private String content;
public ParameterMapping(String content) {
this.content = content;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
}
接下来就很简单了,就是通过反射对查询的实体数据进行注入,执行sql语句和封装
```java
package com.hunter.sqlSession;
import com.hunter.config.BoundSql;
import com.hunter.pojo.Configuration;
import com.hunter.pojo.MapperStatement;
import com.hunter.utils.GenericTokenParser;
import com.hunter.utils.ParameterMapping;
import com.hunter.utils.ParameterMappingTokenHandler;
import com.hunter.utils.TokenHandler;
import javafx.geometry.Bounds;
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 Executor {
@Override
public <E> List<E> query(Configuration configuration, MapperStatement mapperStatement, Object... params) throws SQLException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException, IntrospectionException, InstantiationException, InvocationTargetException {
Connection connection = configuration.getDataSource().getConnection();
String sql = mapperStatement.getSql();
BoundSql boundSql = getBoundSql(sql);
PreparedStatement preparedStatement = connection.prepareStatement(boundSql.getSqlText());
String paramterType = mapperStatement.getParamterType();
Class<?> parameterTypeClass = 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 = parameterTypeClass.getDeclaredField(content);
declaredField.setAccessible(true);
Object o = declaredField.get(params[0]);
preparedStatement.setObject(i+1,o);
}
ResultSet resultSet = preparedStatement.executeQuery();
String returnType = mapperStatement.getReturnType();
Class<?> resultTypeClass = getClassType(returnType);
ArrayList<Object> objects = new ArrayList<>();;
while (resultSet.next()){
ResultSetMetaData metaData = resultSet.getMetaData();
Object instance = resultTypeClass.newInstance();
for (int i = 1; i <= metaData.getColumnCount(); i++) {
String columnName = metaData.getColumnName(i);
Object value = resultSet.getObject(columnName);
PropertyDescriptor propertyDescriptor = null;
try {
propertyDescriptor = new PropertyDescriptor(columnName, resultTypeClass);
}catch (Exception e){
continue;
}
if (propertyDescriptor != null) {
Method writeMethod = propertyDescriptor.getWriteMethod();
writeMethod.invoke(instance, value);
}
}
objects.add(instance);
}
return (List<E>) objects;
}
private Class<?> getClassType(String paramterType) throws ClassNotFoundException {
if (paramterType != null){
Class<?> aClass = Class.forName(paramterType);
return aClass;
}
return null;
}
private BoundSql getBoundSql(String sql){
ParameterMappingTokenHandler parameterMappingTokenHandler = new ParameterMappingTokenHandler();
GenericTokenParser genericTokenParser = new GenericTokenParser("#{","}",parameterMappingTokenHandler);
String parseSql = genericTokenParser.parse(sql);
List<ParameterMapping> parameterMappings = parameterMappingTokenHandler.getParameterMappings();
BoundSql boundSql = new BoundSql(parseSql,parameterMappings);
return boundSql;
}
}
大概的mybatis执行流程如下,我们做个试验测一下
@Test
public void test() throws Exception {
InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(resourceAsStream);
SqlSession sqlSession = factory.openSession();
IUserDao userDao = sqlSession.getMapper(IUserDao.class);
List<User> all = userDao.findAll();
for (User user : all) {
System.out.println(user);
}
System.out.println();
}
查询成功,mybatis的原理其实不难,底层的实现还是通过jdbc对数据库进行操作,需要注意主要是对相关配置文件的解析与configuration的封装,并能对预处理的sql进行转换等。
|