实现简单的MyBatis框架
一、传统JDBC存在的问题与解决思路
首先我们要知道为什么我们需要引入向MyBatis这样的ORM框架(传统JDBC访问存在的问题),以及怎样才能解决传统所发现的问题(实现简单的ORM框架)
传统JDBC存在的问题
上图是一个简单的原生JDBC操作,我们不难发现存在以下问题:
- 数据库连接创建、释放频繁造成系统资源浪费,从⽽影响系统性能
- sql语句在代码中硬编码,造成代码不易维护,实际应⽤中sql变化的可能较⼤,sql变动需要改变java代码。
- 使⽤preparedStatement向占有位符号传参数存在硬编码,因为sql语句的where条件不⼀定,可多可少,修改sql还要修改代码,系统不易维护。
- 对结果集解析存在硬编码(查询列名),sql变化导致解析代码变化,系统不易维护,如果能将数据库记录封装成pojo对象解析⽐较⽅便
解决思路
- 频繁创建/释放资源,可以考虑使用如线程池相似的概念(数据库连接池)
- sql语句不硬编码在java代码中,实现sql与java业务代码解耦,可放在其它文件(xml,或其它java文件),且可动态添加参数(使用集合或pojo传递入参),并动态构建出sql
- 使⽤反射、内省等底层技术,⾃动将实体与表进⾏属性与字段的⾃动映射(可动态设置返回值)
二、实现简单的mybatis框架
流程图
框架整体结构
- config包为常用设置类
- enums包为常用枚举
- io包为读取文件(配置文件/映射文件)相关类
- pojo包为封装中间态/结果集
- sqlsession包中有sqlsession工厂类,对应工厂实例与执行器executor方法
- utils为常用工具类
三、详细流程讲解
调用方传递配置文件与映射文件路径
// 传递配置文件
InputStream inputStream = Resource.getResourceAsSteam("SqlMapConfig.xml");
SqlMapConfig.xml
<configuration>
<!-- 数据源 -->
<dataSource>
<property name="driverClass" value="com.mysql.cj.jdbc.Driver" ></property>
<property name="jdbcUrl" value="jdbc:mysql://127.0.0.1:3306/blog?characterEncoding=utf-8&serverTimezone=UTC" ></property>
<property name="username" value="root" ></property>
<property name="password" value="***" ></property>
</dataSource>
<!-- 加载配置文件无需加载两次 所以这里还需存放mapper.xml 的全路径 -->
<mapper resource = "UserMapper.xml"></mapper>
</configuration>
UserMapper.xml
<mapper namespace = "dao.SysUserDAO">
<select parameterType = "entity.SysUserEntity" id="queryById" resultType = "entity.SysUserEntity">
SELECT * FROM sys_user WHERE id = #{id}
</select>
<select id="queryAll" resultType = "entity.SysUserEntity">
SELECT * FROM sys_user
</select>
<select id="countAll" resultType = "java.lang.Integer">
SELECT count(1) FROM sys_user
</select>
<insert parameterType = "entity.SysUserEntity" id="insertUser">
INSERT INTO sys_user VALUES (#{id}, #{loginName}, #{password}, #{email}, #{phone}, #{headPhoto}, #{loginIp}, #{loginDate}, #{loginFlag}, #{motto}, #{createBy}, #{createDate}, #{updateBy}, #{updateDate}, #{remarks}, #{delFlag})
</insert>
<update parameterType = "entity.SysUserEntity" id="updateNameById">
UPDATE sys_user SET login_name = #{loginName} WHERE id = #{id}
</update>
<delete parameterType = "entity.SysUserEntity" id="deleteById">
DELETE FROM sys_user WHERE id = #{id}
</delete>
</mapper>
解析配置文件
调用方调用SqlSessionFactoryBuilder 的build方法将输入流传入
// 传递配置文件
InputStream inputStream = Resource.getResourceAsSteam("SqlMapConfig.xml");
// 创建sqlSession
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
框架方的SqlSessionFactoryBuilder build 方法
public class SqlSessionFactoryBuilder {
public SqlSessionFactory build(InputStream in) throws PropertyVetoException, DocumentException {
// 1.使用dom4j解析配置文件,将解析结果封装至Configuration
XMLConfigBuilder xmlConfigBuilder = new XMLConfigBuilder();
Configuration configuration = xmlConfigBuilder.parseConfig(in);
// 2.创建sqlSessionFactory 工厂类:生产sqlsession会话对象(与数据库家交互的对象)工厂模式
DefaultSqlSessionFactory defaultSqlSessionFactory = new DefaultSqlSessionFactory(configuration);
return defaultSqlSessionFactory;
}
}
com.gray.config.XMLConfigBuilder#parseConfig(解析配置文件与mapper文件并封装为Configuration对象)
public class XMLConfigBuilder {
private Configuration configuration;
public XMLConfigBuilder() {
this.configuration = new Configuration();
}
/**
* 使用dom4j将配置文件解析封装Configuration
*
* @param inputStream
* @return
*/
public Configuration parseConfig(InputStream inputStream) throws DocumentException, PropertyVetoException {
Document document = new SAXReader().read(inputStream);
Element rootElement = document.getRootElement();
// 基础配置信息读取/封装
// 读取所有property 节点
List<Element> propertyNodes = rootElement.selectNodes("//property");
Properties properties = new Properties();
for (Element element : propertyNodes) {
String name = element.attributeValue(ElementAttributeValue.NAME.getAttributeValue());
String value = element.attributeValue(ElementAttributeValue.VALUE.getAttributeValue());
properties.setProperty(name, value);
}
ComboPooledDataSource comboPooledDataSource = new ComboPooledDataSource();
comboPooledDataSource.setDriverClass(properties.getProperty(PropertiesProperty.DRIVER_CLASS.getProperty()));
comboPooledDataSource.setJdbcUrl(properties.getProperty(PropertiesProperty.JDBC_URL.getProperty()));
comboPooledDataSource.setUser(properties.getProperty(PropertiesProperty.USER_NAME.getProperty()));
comboPooledDataSource.setPassword(properties.getProperty(PropertiesProperty.PASSWORD.getProperty()));
configuration.setDataSource(comboPooledDataSource);
// mapper文件读取
// 读取所有mapper节点
List<Element> mapperList = rootElement.selectNodes("//mapper");
for (Element element : mapperList) {
String mapperPath = element.attributeValue(ElementAttributeValue.RESOURCE.getAttributeValue());
// 获取所有mapper配置文件路径并解析
InputStream resourceAsSteam = Resource.getResourceAsSteam(mapperPath);
XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(configuration);
xmlMapperBuilder.parse(resourceAsSteam);
}
return configuration;
}
}
解析mapper文件
这里的statementId 十分重要,主要是为了确定唯一一条sql执行计划,通常使用mapper上的namespace + sql上的id确定
String id = element.attributeValue(ElementAttributeValue.ID.getAttributeValue());
String parameterType = element.attributeValue(ElementAttributeValue.PARAMETER_TYPE.getAttributeValue());
String resultType = element.attributeValue(ElementAttributeValue.RESULT_TYPE.getAttributeValue());
// 获取sql
String elementName = element.getName();
String sqlText = element.getTextTrim();
DMLOperating dmlOperating = DMLOperating.sqlToOperating(sqlText);
if (StringUtils.isBlank(sqlText) || dmlOperating == null || !elementName.toLowerCase().equals(dmlOperating.getOperating())) {
throw new RuntimeException("标签类型与sql类型不一致");
}
MappedStatement mappedStatement = new MappedStatement();
mappedStatement.setId(id);
// 入参类型
mappedStatement.setParameterType(parameterType);
// 返回值类型
mappedStatement.setResultType(resultType);
// 所写入sql
mappedStatement.setSql(sqlText);
// dml 类型 select, insert等
mappedStatement.setDmlOperating(dmlOperating);
StringJoiner statementId = new StringJoiner(".");
// statementId id 为 namespace与sql id 相结合唯一确定一个sql执行语句
statementId.add(namespace).add(id);
configuration.getMappedStatementMap().put(statementId.toString(), mappedStatement);
封装完成的Configuration 对象
@Data
public class Configuration implements Serializable {
private DataSource dataSource;
/**
* 加载的映射对象,这样向下传递参数时就只用传递Configuration对象
* key:Statement namespace + id
* value:封装的MappedStatement
*/
private Map<String, MappedStatement> mappedStatementMap = Maps.newHashMap();
}
Configuration 对象中的MappedStatement
@Data
public class MappedStatement implements Serializable {
// id标识
private String id;
// 返回值类型
private String resultType;
// 参数值
private String parameterType;
// sql语句
private String sql;
// dml 类型
private DMLOperating dmlOperating;
}
最后生成将生成的configuration 放入工厂实例中返回给调用端(第二步)
public class SqlSessionFactoryBuilder {
public SqlSessionFactory build(InputStream in) throws PropertyVetoException, DocumentException {
// 1.使用dom4j解析配置文件,将解析结果封装至Configuration
XMLConfigBuilder xmlConfigBuilder = new XMLConfigBuilder();
Configuration configuration = xmlConfigBuilder.parseConfig(in);
// 2.创建sqlSessionFactory 工厂类:生产sqlsession会话对象(与数据库家交互的对象)工厂模式
DefaultSqlSessionFactory defaultSqlSessionFactory = new DefaultSqlSessionFactory(configuration);
return defaultSqlSessionFactory;
}
}
寻找对应sql计划
调用端获取到工厂后通过工厂生成sqlsession(sqlsession用于sql会话)
// 创建sqlSession
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlsession = sqlSessionFactory.openSession();
openSession 其实就只是创建了一个sqlsession
@AllArgsConstructor
public class DefaultSqlSessionFactory implements SqlSessionFactory {
private Configuration configuration;
@Override
public SqlSession openSession() {
return new DefaultSqlSession(configuration);
}
}
通过getmapper形式获取对应sql实例
先看看sqlsession中内容
public interface SqlSession {
<E> List<E> selectList(String statementId, Object... params) throws ClassNotFoundException, SQLException, NoSuchFieldException, InstantiationException, IllegalAccessException;
<E> E selectOne(String statementId, Object... params) throws ClassNotFoundException, SQLException, InstantiationException, IllegalAccessException, NoSuchFieldException;
void insert(String statementId, Object... params) throws ClassNotFoundException, SQLException, NoSuchFieldException, InstantiationException, IllegalAccessException;
void update(String statementId, Object... params) throws ClassNotFoundException, SQLException, NoSuchFieldException, InstantiationException, IllegalAccessException;
void delete(String statementId, Object... params) throws ClassNotFoundException, SQLException, NoSuchFieldException, InstantiationException, IllegalAccessException;
<E> E getMapper(Class<?> mapperClass);
}
再来看看它的实现类DefaultSqlSession
@AllArgsConstructor
public class DefaultSqlSession implements SqlSession {
private Configuration configuration;
@Override
public <E> List<E> selectList(String statementId, Object... params) throws ClassNotFoundException, SQLException, NoSuchFieldException, InstantiationException, IllegalAccessException {
SimpleExecutor simpleExecutor = new SimpleExecutor();
List<Object> resultList = simpleExecutor.query(configuration, configuration.getMappedStatementMap().get(statementId), params);
return (List<E>) resultList;
}
@Override
public <E> E selectOne(String statementId, Object... params) throws ClassNotFoundException, SQLException, InstantiationException, IllegalAccessException, NoSuchFieldException {
List<Object> objects = selectList(statementId, params);
if (objects.size() == 1) {
return (E) objects.get(0);
} else {
throw new RuntimeException("查询结果为空/有多条");
}
}
@Override
public void insert(String statementId, Object... params) throws ClassNotFoundException, SQLException, NoSuchFieldException, InstantiationException, IllegalAccessException {
SimpleExecutor simpleExecutor = new SimpleExecutor();
simpleExecutor.query(configuration, configuration.getMappedStatementMap().get(statementId), params);
}
@Override
public void update(String statementId, Object... params) throws ClassNotFoundException, SQLException, NoSuchFieldException, InstantiationException, IllegalAccessException {
SimpleExecutor simpleExecutor = new SimpleExecutor();
simpleExecutor.query(configuration, configuration.getMappedStatementMap().get(statementId), params);
}
@Override
public void delete(String statementId, Object... params) throws ClassNotFoundException, SQLException, NoSuchFieldException, InstantiationException, IllegalAccessException {
SimpleExecutor simpleExecutor = new SimpleExecutor();
simpleExecutor.query(configuration, configuration.getMappedStatementMap().get(statementId), params);
}
@Override
public <E> E getMapper(Class<?> mapperClass) {
// 使用JDK动态代理为DAO接口生成代理对象
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;
MappedStatement mappedStatement = configuration.getMappedStatementMap().get(statementId);
if (mappedStatement == null) {
return null;
}
// 暂时只处理简单dml
DMLOperating dmlOperating = mappedStatement.getDmlOperating();
if (dmlOperating == null) {
return null;
}
switch (dmlOperating) {
case SELECT: {
Type genericExceptionTypes = method.getGenericReturnType();
if (genericExceptionTypes instanceof ParameterizedType) {
return selectList(statementId, args);
}
return selectOne(statementId, args);
}
case UPDATE: {
update(statementId, args);
}
case DELETE: {
delete(statementId, args);
}
case INSERT: {
insert(statementId, args);
}
}
return null;
}
});
return (E) instance;
}
}
这里主要要说下getmapper方法,调用端使用getmapper方法将本身的类(mapper接口)当做入参放入,之后mapper这里使用动态代理,当有当前对象执行对象方法时都会先执行getmapper中的invoke方法,这样就能实现动态根据当前方法的classname拼装出statementId 找出对应mappedStatement 来执行对应sql
执行sql
不管是执行到SqlSession 哪个方法最后都会执行到executor的query方法,在这里就会连接JDBC最后拼装sql执行sql
public class SimpleExecutor implements Executor {
private static Pattern linePattern = Pattern.compile("_(\\w)");
@Override
public <E> List<E> query(Configuration configuration, MappedStatement mappedStatement, Object... object) throws SQLException, ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchFieldException {
// 1.注册驱动,获取连接
Connection connection = configuration.getDataSource().getConnection();
// 2.获取sql 转换#{} 转换为 ?(?才能被jdbc识别)
String sql = mappedStatement.getSql();
BoundSql boundSql = getBoundSql(sql);
// 3. 获取预处理对象
PreparedStatement preparedStatement = connection.prepareStatement(boundSql.getSqlText());
// 4.设置参数
// 获取参数对象属性值
if (StringUtils.isNotBlank(mappedStatement.getParameterType())) {
Class<?> parameterTypeClass = Class.forName(mappedStatement.getParameterType());
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(object[0]);
preparedStatement.setObject(i + 1, o);
}
}
// 5.执行sql
ResultSet resultSet = null;
if (DMLOperating.executeQuery(mappedStatement.getDmlOperating())) {
resultSet = preparedStatement.executeQuery();
} else {
preparedStatement.execute();
}
ArrayList<Object> objects = new ArrayList<>();
// 6.封装返回结果
// 获取返回值
if (StringUtils.isNotBlank(mappedStatement.getResultType()) && resultSet != null) {
Class<?> resultTypeClass = Class.forName(mappedStatement.getResultType());
boolean isBaseType = isBaseType(resultTypeClass, false);
while (resultSet.next()) {
// 生成结果对象
Object resultObject = null;
if (!isBaseType) {
resultObject = resultTypeClass.newInstance();
}
// 取出原始数据(其中有字段名)
ResultSetMetaData metaData = resultSet.getMetaData();
for (int i = 1; i <= metaData.getColumnCount(); i++) {
// 字段名(i要从1开始)
String columnName = metaData.getColumnName(i);
// 获取字段值
Object columnValue = resultSet.getObject(columnName);
if (isBaseType) {
resultObject = handleBaseTypeObject(resultTypeClass, columnValue);
} else {
try {
// 使用反射/内省完成结果封装(转换驼峰)
PropertyDescriptor propertyDescriptor = new PropertyDescriptor(lineToHump(columnName), resultTypeClass);
Method writeMethod = propertyDescriptor.getWriteMethod();
// 将值设置带结果对象中
writeMethod.invoke(resultObject, columnValue);
} catch (Exception e) {
System.out.println(e);
}
}
}
objects.add(resultObject);
}
}
return (List<E>) objects;
}
/**
* 完成对#{} 解析
* 1.#{} -> ?
* 2.解析#{}中的值
*
* @param sql
* @return
*/
public BoundSql getBoundSql(String sql) {
// 标记处理类:配置标记解析器完成对占位符的解析处理工作
ParameterMappingTokenHandler parameterMappingTokenHandler = new ParameterMappingTokenHandler();
GenericTokenParser genericTokenParser = new GenericTokenParser("#{", "}", parameterMappingTokenHandler);
// 解析出来的sql
String parseSql = genericTokenParser.parse(sql);
// #{} 解析出来的参数名称(属性名)
List<ParameterMapping> parameterMappings = parameterMappingTokenHandler.getParameterMappings();
return new BoundSql(parseSql, parameterMappings);
}
/**
* 下划线转驼峰
*
* @param str
* @return
*/
public static String lineToHump(String str) {
str = str.toLowerCase();
Matcher matcher = linePattern.matcher(str);
StringBuffer sb = new StringBuffer();
while (matcher.find()) {
matcher.appendReplacement(sb, matcher.group(1).toUpperCase());
}
matcher.appendTail(sb);
return sb.toString();
}
/**
* 判断对象属性是否是基本数据类型,包括是否包括string
*
* @param className
* @param incString 是否包括string判断,如果为true就认为string也是基本数据类型
* @return
*/
public boolean isBaseType(Class className, boolean incString) {
if (incString && className.equals(String.class)) {
return true;
}
return className.equals(Integer.class) ||
className.equals(int.class) ||
className.equals(Byte.class) ||
className.equals(byte.class) ||
className.equals(Long.class) ||
className.equals(long.class) ||
className.equals(Double.class) ||
className.equals(double.class) ||
className.equals(Float.class) ||
className.equals(float.class) ||
className.equals(Character.class) ||
className.equals(char.class) ||
className.equals(Short.class) ||
className.equals(short.class) ||
className.equals(Boolean.class) ||
className.equals(boolean.class);
}
public Object handleBaseTypeObject(Class<?> resultTypeClass, Object columnValue) {
if ((resultTypeClass.equals(Integer.class) || resultTypeClass.equals(int.class)) && columnValue != null) {
return Integer.valueOf(columnValue.toString());
}
return columnValue;
}
}