本文主要是介绍手写乞丐版-Mybatis,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
手写乞丐版Mybatis
需求背景:
针对传统的JDBC编程的痛点:
1、硬编码 针对数据库连接、以及sql 会写在代码中,不利于后期维护
2、频繁的数据库连接,会造成资源浪费,影响系统性能
3、查询条件占位符存在硬编码,不利于维护
4、对于结果的解析,如果需要修改sql结果,也需要进行相应的代码修改
如何结果:
1、数据库连接、sql全部采用配置文件的方式进行可配置化
2、使用数据库连接池
3、将查询条件以及结果使用反射,自动映射字段。
框架设计
使用端:
1、配置sqlMapConfig.xml 文件 :内容包括数据库配置信息、并引入Mapper.xml文件
2、配置Mapper.xml 文件: 内容包括sql语句
框架层:
-
读取配置文件,可以将配置文件映射为下面的类
-
Configuration类:核心配置类,存放数据库信息,以及MappedStatement映射
-
MappedStatement类: 包括sql,namespace,入参类型,出参类型
-
-
解析配置文件
创建sqlSessionFactoryBuilder类
使用sqlSessionFactory.build():
(1) 使用dom4j 解析xml文件,将内容封装到Configuration和MapperedStatement中
(2) 创建sqlSessionFactrory的实现类 DefaultSqlSession
-
创建sqlSessionFactory:
创建方法:openSession() 获取到sqlSession接口的实现类实例对象
-
创建sqlSession接口及实现类:主要封装crud方法
方法:selectList(String statementId,Object param):
查询所有 selectOne(String statementId,Object param): 查询单个 具体实现:封装JDBC完成对数据库表的查询操作
具体实现:
创建一个Maven工程:
使用端:
配置sqlMapConfig.xml
<?xml version="1.0" encoding="UTF-8" ?>
<configuration><!-- 数据库配置信息 --><property name="driverClass" value="com.mysql.jdbc.Driver"></property><property name="url" value="jdbc:mysql://127.0.0.1:3306/test?serverTimezone=Asia/Shanghai"></property><property name="username" value="root"></property><property name="password" value="root"></property><!-- mapper配置 --><mapper resource="mapper.xml"></mapper>
</configuration>
配置mapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<mapper namespace="test"><select id="selectoneuser" paramterType="com.xugw.pojo.User" resultType="com.xugw.pojo.User">select * from userwhere id = ${id}and username = ${username}</select><select id="selectalluser" resultType="com.xugw.pojo.User">select * from user</select>
</mapper>
新建实体类
public class User {private int id;private String name;public int getId() {return id;}public void setId(int id) {this.id = id;}public String getName() {return name;}public void setName(String name) {this.name = name;}@Overridepublic String toString() {return "User{" +"id=" + id +", name='" + name + '\'' +'}';}
}
使用端编写完毕
自定义Mybatis框架
新建一个module
xml文件解析
解析前我们先把两个解析对应的对象类创建下:
public class Configuration {// 数据源private DataSource dataSource;// 记录 namespace + id 与 对应sql 的关系private Map<String,MapperedStatement> mapperedStatementMap = new HashMap<String,MapperedStatement>();public DataSource getDataSource() {return dataSource;}public void setDataSource(DataSource dataSource) {this.dataSource = dataSource;}public Map<String, MapperedStatement> getMapperedStatementMap() {return mapperedStatementMap;}public void setMapperedStatementMap(Map<String, MapperedStatement> mapperedStatementMap) {this.mapperedStatementMap = mapperedStatementMap;}
}public class MapperedStatement {// namespaceidprivate String id;// 解析后的sql内容private String sql;// 入参实体类private Class<?> paramterType;// 结果实体类private Class<?> resultType;public String getId() {return id;}public void setId(String id) {this.id = id;}public String getSql() {return sql;}public void setSql(String sql) {this.sql = sql;}public Class<?> getParamterType() {return paramterType;}public void setParamterType(Class<?> paramterType) {this.paramterType = paramterType;}public Class<?> getResultType() {return resultType;}public void setResultType(Class<?> resultType) {this.resultType = resultType;}
}
创建完成后:
第一步:读取xml文件
public class Resource {public static InputStream getResourceAsStream(String path){return Resource.class.getClassLoader().getResourceAsStream(path);}
}
第二步:解析配置文件
// 对 sqlMapConfig.xml 解析 同时因为我们在这个xml中已经配置了 Mapper.xml 对应的路径,也可以一起对 mapper.xml 进行解析
public class XMLMapperBuilder {private Configuration configuration ;public XMLMapperBuilder(Configuration configuration) {this.configuration = configuration;}/*** 解析 mapper.xml* @param resourceAsStream*/public void parse(InputStream resourceAsStream) throws DocumentException, ClassNotFoundException {Document read = new SAXReader().read(resourceAsStream);Element rootElement = read.getRootElement();// mapper的namespaceString namespace = rootElement.attributeValue("namespace");// 获取selectList<Element> selectlists = rootElement.selectNodes("select");installconfiguration(selectlists,namespace);List<Element> updatelists = rootElement.selectNodes("update");installconfiguration(updatelists,namespace);}/*** 组装 mapperedStatement* @param namespace*/private void installconfiguration(List<Element> elements, String namespace) throws ClassNotFoundException {for (Element element : elements) {String id = element.attributeValue("id");String resultType = element.attributeValue("resultType");String paramterType = element.attributeValue("paramterType");String sqlText = element.getTextTrim();MapperedStatement mappedStatement = new MapperedStatement();mappedStatement.setId(id);mappedStatement.setResultType(getClassType(resultType));mappedStatement.setParamterType(getClassType(paramterType));mappedStatement.setSql(sqlText);configuration.getMapperedStatementMap().put(namespace+"."+id,mappedStatement);}}private Class<?> getClassType(String resultType) throws ClassNotFoundException {return Class.forName(resultType);}
}public class XMLConfigerBuilder {private Configuration configuration;public XMLConfigerBuilder(Configuration configuration) {this.configuration = new Configuration();}// 解析public Configuration parse(InputStream inputStream) throws DocumentException, PropertyVetoException, ClassNotFoundException {Document read = new SAXReader().read(inputStream);Element rootElement = read.getRootElement();// 数据库连接信息List<Element> configlist = rootElement.selectNodes("//property");Properties properties = new Properties();for (Element property :configlist) {properties.setProperty(property.attributeValue("name"), property.attributeValue("value"));}// 连接池ComboPooledDataSource comboPooledDataSource = new ComboPooledDataSource();comboPooledDataSource.setDriverClass(properties.getProperty("driverClass"));comboPooledDataSource.setJdbcUrl(properties.getProperty("url"));comboPooledDataSource.setUser(properties.getProperty("username"));comboPooledDataSource.setPassword(properties.getProperty("password"));// 填充configuration.setDataSource(comboPooledDataSource);// 解析mapper.xmlList<Element> mappers = rootElement.selectNodes("//mapper");XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(configuration) ;for (Element mapper:mappers) {String resource = mapper.attributeValue("resource");InputStream resourceAsStream = Resource.getResourceAsStream(resource);xmlMapperBuilder.parse(resourceAsStream);}return configuration;}}
已上我们已经将xml文件的信息放到配置类中了
sql底层执行
这里我们提取了一个Executor ,这个Executor是实际上真正执行sql的
在Executor中,我们需要对 mapperedstatement中的内容进行处理,包括
sql占位符替换,入参和出参的映射
sql占位符替换完成后生成BoundSql
public class BoundSql {private String sqlText;private List<ParameterMapping> parameterMappings = new ArrayList<>();public BoundSql(String sqlText, List<ParameterMapping> parameterMappings) {this.sqlText = sqlText;this.parameterMappings = parameterMappings;}public String getSqlText() {return sqlText;}public void setSqlText(String sqlText) {this.sqlText = sqlText;}public List<ParameterMapping> getParameterMappings() {return parameterMappings;}public void setParameterMappings(List<ParameterMapping> parameterMappings) {this.parameterMappings = parameterMappings;}
}
这里需要一个工具包,从mybatis源码中薅下来的:
/*** Copyright 2009-2017 the original author or authors.** Licensed under the Apache License, Version 2.0 (the "License");* you may not use this file except in compliance with the License.* You may obtain a copy of the License at** http://www.apache.org/licenses/LICENSE-2.0** Unless required by applicable law or agreed to in writing, software* distributed under the License is distributed on an "AS IS" BASIS,* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.* See the License for the specific language governing permissions and* limitations under the License.*//*** @author Clinton Begin*/
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;}/*** 解析${}和#{}* @param text* @return* 该方法主要实现了配置文件、脚本等片段中占位符的解析、处理工作,并返回最终需要的数据。* 其中,解析工作由该方法完成,处理工作是由处理器handler的handleToken()方法来实现*/public String parse(String text) {// 验证参数问题,如果是null,就返回空字符串。if (text == null || text.isEmpty()) {return "";}// 下面继续验证是否包含开始标签,如果不包含,默认不是占位符,直接原样返回即可,否则继续执行。int start = text.indexOf(openToken, 0);if (start == -1) {return text;}// 把text转成字符数组src,并且定义默认偏移量offset=0、存储最终需要返回字符串的变量builder,// text变量中占位符对应的变量名expression。判断start是否大于-1(即text中是否存在openToken),如果存在就执行下面代码char[] src = text.toCharArray();int offset = 0;final StringBuilder builder = new StringBuilder();StringBuilder expression = null;while (start > -1) {// 判断如果开始标记前如果有转义字符,就不作为openToken进行处理,否则继续处理if (start > 0 && src[start - 1] == '\\') {builder.append(src, offset, start - offset - 1).append(openToken);offset = start + openToken.length();} else {//重置expression变量,避免空指针或者老数据干扰。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] == '\\') {//如果结束标记前面有转义字符时// this close token is escaped. remove the backslash and continue.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) {// close token was not found.builder.append(src, start, src.length - start);offset = src.length;} else {//首先根据参数的key(即expression)进行参数处理,返回?作为占位符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();}
}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;}
}public class ParameterMappingTokenHandler implements TokenHandler {private List<ParameterMapping> parameterMappings = new ArrayList<ParameterMapping>();// context是参数名称 #{id} #{username}// 进了List 最终生成sql时会按照顺序 进行值传递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;}}/*** @author Clinton Begin*/
public interface TokenHandler {String handleToken(String content);
}
public interface Executor {/*** 针对查询* @param <E>* @param configuration* @param mapperedStatement* @param params* @return* @throws Exception*/public <E> List<E> query(Configuration configuration, MapperedStatement mapperedStatement,Object... params) throws Exception;/*** 针对不需要返回结果,比如 update/insert/delete等* @throws Exception*/public void execute(Configuration configuration, MapperedStatement mapperedStatement, Object... params) throws Exception;}public class SimpleExecutor implements Executor{@Overridepublic <E> List<E> query(Configuration configuration,MapperedStatement mapperedStatement, Object... params) throws Exception {Class<?> paramterType = mapperedStatement.getParamterType();String sql = mapperedStatement.getSql();BoundSql boundSql = getBoundSql(sql);List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();ArrayList<Object> objects = new ArrayList<>();try {// 遍历java集合Connection connection = configuration.getDataSource().getConnection();PreparedStatement preparedStatement = connection.prepareStatement(boundSql.getSqlText());if (paramterType != null) {for (int i = 0; i < parameterMappings.size(); i++) {String content = parameterMappings.get(i).getContent();//反射//获取paraterType的全路径 ;Field declaredField = paramterType.getDeclaredField(content);// 暴力访问declaredField.setAccessible(true);Object o = declaredField.get(params[0]);preparedStatement.setObject(i + 1, o);}}// 执行sqlResultSet resultSet = preparedStatement.executeQuery();Class<?> resultTypeClass = mapperedStatement.getResultType();// 封装返回结果集while (resultSet.next()) {Object o = null;//TODO Map类型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.toLowerCase(), resultTypeClass);Method writeMethod = propertyDescriptor.getWriteMethod();writeMethod.invoke(o, value);}objects.add(o);}// 释放resultSet.close();preparedStatement.close();} catch (SQLException throwables) {throwables.printStackTrace();} catch (IntrospectionException e) {e.printStackTrace();} catch (NoSuchFieldException e) {e.printStackTrace();} catch (InvocationTargetException e) {e.printStackTrace();} catch (IllegalAccessException e) {e.printStackTrace();} catch (InstantiationException e) {e.printStackTrace();}return (List<E>) objects;}@Overridepublic void execute(Configuration configuration, MapperedStatement mapperedStatement, Object... params) throws Exception {Class<?> paramterType = mapperedStatement.getParamterType();String sql = mapperedStatement.getSql();// 特殊处理 资源需要根据本地网 获取不同的数据库名BoundSql boundSql = null;boundSql = getBoundSql(sql);List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();try {// 遍历java集合Connection connection = configuration.getDataSource().getConnection();PreparedStatement preparedStatement = connection.prepareStatement(boundSql.getSqlText());if (paramterType != null) {for (int i = 0; i < parameterMappings.size(); i++) {String content = parameterMappings.get(i).getContent();//反射//获取paraterType的全路径 ;Class<?> paratertypeClass = paramterType;Field declaredField = paratertypeClass.getDeclaredField(content);// 暴力访问declaredField.setAccessible(true);Object o = declaredField.get(params[0]);preparedStatement.setObject(i + 1, o);}}preparedStatement.execute();preparedStatement.close();} catch (SQLException throwables) {throwables.printStackTrace();} catch (NoSuchFieldException e) {e.printStackTrace();} catch (IllegalAccessException e) {e.printStackTrace();}}private BoundSql getBoundSql(String sql, String... params) {// 标记处理类ParameterMappingTokenHandler parameterMappingTokenHandler = new ParameterMappingTokenHandler();// 标记解析器GenericTokenParser genericTokenParser = new GenericTokenParser("${", "}", parameterMappingTokenHandler);// 解析过后的sqlString parsesql = genericTokenParser.parse(sql);// ${} 解析出来的参数名称List<ParameterMapping> parameterMappings = parameterMappingTokenHandler.getParameterMappings();BoundSql boundSql = new BoundSql(parsesql, parameterMappings);return boundSql;}}
到这边 实际上我们的核心已经写完了,下面就是把 业务代码和 执行代码结合
SqlSession创建
我们创建下数据查询的接口和它的实现类:
public interface SqlSession {public <E> List<E> selectList(String statementId, Object... params) throws Exception;public <T> T selectOne(String statementId,Object... params) throws Exception;
}public class DefaultSqlSession implements SqlSession {private Configuration configuration;private SimpleExecutor executor;public DefaultSqlSession(Configuration configuration) {this.configuration = configuration;executor = new SimpleExecutor();}@Overridepublic <E> List<E> selectList(String statementId, Object... params) throws Exception {return executor.query(configuration,configuration.getMapperedStatementMap().get(statementId));}@Overridepublic <T> T selectOne(String statementId, Object... params) throws Exception {List<Object> results = executor.query(configuration, configuration.getMapperedStatementMap().get(statementId));if(results.size() == 1){return (T) results.get(0);}else {throw new Exception("返回结果过多");}}
}
我们采用工厂+建造者模式进行创建
SqlSessionFactory创建
public interface SqlSessionFactory {public SqlSession openSession();
}public class DefaultSqlSessionFactory implements SqlSessionFactory {private Configuration configuration;public DefaultSqlSessionFactory(Configuration configuration) {this.configuration = configuration;}@Overridepublic SqlSession openSession() {return new DefaultSqlSession(configuration);}
}
SqlSessionFactoryBuilder创建
public class SqlSessionFactoryBuilder {private Configuration configuration;public SqlSessionFactoryBuilder() {this.configuration = new Configuration();}public SqlSessionFactory build(InputStream inputStream) throws PropertyVetoException, DocumentException, ClassNotFoundException {XMLConfigerBuilder xmlConfigerBuilder = new XMLConfigerBuilder(configuration);// 解析配置文件Configuration configuration = xmlConfigerBuilder.parse(inputStream);// 创建sqlSessionFactorySqlSessionFactory sqlSessionFactory = new DefaultSqlSessionFactory(configuration);return sqlSessionFactory;}}
框架部分编写完成
数据验证:
我们在使用端新建一个测试类:
public class TestMybatis {@Testpublic void test() throws Exception {InputStream resourceAsStream = Resource.getResourceAsStream("sqlMapConfig.xml");SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);SqlSession sqlSession = sqlSessionFactory.openSession();List<User> users = sqlSession.selectList("test.selectalluser");for (User user :users) {System.out.println(user.toString());}}
}
执行后
反思进化:
上面已经完成了核心方法的编写,但是我们发现,测试类中使用的是sqlSession.selectList(mapperstatemetnId)的形式,与我们平常使用的好像有点不一样。平时我们好像只需要对应下 mapper接口 与 实际mapper.xml的位置,然后就可以直接使用了。
同时上述方法也存在硬编码的问题,与我们初衷背离。
那么这是怎么实现的呢?我们能不能直接通过mapper接口直接进行数据操作呢?
=>
我们可以使用代理模式,对UserMapper 生成代理,并进行方法增强,这样我们就可以完成上述事情。
通过代理,我们可以直接取得Mapper接口类的全限定名,然后从configuration 中取到对应的数据信息。这也是为什么我们在mybatis里面 要求namespace 以及 id 要对应类、方法名的原因
sqlSession改造:
我们在sqlSession中新建一个getMapper.xml方法
public interface SqlSession {public <E> List<E> selectList(String statementId, Object... params) throws Exception;public <T> T selectOne(String statementId,Object... params) throws Exception;public Object update(String statementId, Object... params) throws Exception;public <T> T getMapper(Class<?> mapperclass);
}
@Overridepublic <T> T getMapper(Class<?> mapperclass) {// 使用Jdk动态代理生成代理对象Object o = Proxy.newProxyInstance(mapperclass.getClassLoader(), new Class[]{mapperclass}, new InvocationHandler() {@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {// 底层还是执行jdbc 根据不同情况 调用selectList / selectOne// 获取statementid = 接口全限定名.方法名// 方法名String methodName = method.getName();String className = method.getDeclaringClass().getName();String statementId = className + "." + methodName;// 获取参数 args// 获取被调用方法的返回值类型Type genericReturnType = method.getGenericReturnType();// 根据sql 字符串的第一个单词 判断操作类型String[] s = configuration.getMapperedStatementMap().get(statementId).getSql().split(" ");// 同一大写switch (s[0].toUpperCase()) {case "SELECT":// 判断类型 是否泛型参数化if (genericReturnType instanceof ParameterizedType) {List<Object> objects = selectList(statementId, args);return objects;}return selectOne(statementId, args);case "INSERT":case "UPDATE":case "DELETE":return update(statementId, args);default:throw new Exception("找不到对应的执行方法");}}});return (T)o;}
数据验证:
新建一个Mapper接口
package com.xugw.com.xugw.mapper;import com.xugw.pojo.User;import java.util.List;/*** @author xugw* @description*/
public interface UserMapper {List<User> selectAllUser();
}
修改对应的mapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<mapper namespace="com.xugw.com.xugw.mapper.UserMapper"><select id="selectAllUser" paramterType="com.xugw.pojo.User" resultType="com.xugw.pojo.User">select * from user</select>
</mapper>
测试类:
@Test
public void test() throws Exception {InputStream resourceAsStream = Resource.getResourceAsStream("sqlMapConfig.xml");SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);SqlSession sqlSession = sqlSessionFactory.openSession();// List<User> users = sqlSession.selectList("test.selectalluser");UserMapper mapper = sqlSession.getMapper(UserMapper.class);List<User> users = mapper.selectAllUser();for (User user :users) {System.out.println(user.toString());}
}
结果:
这篇关于手写乞丐版-Mybatis的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!