闭关修炼180天--手写持久层框架(mybatis简易版)
闭关修炼180天–手写持久层框架(mybatis简易版)
抛砖引玉
首先先看一段传统的JDBC编码的代码实现:
//传统的JDBC实现
public static void main(String[] args) {
Connection connection = null;
PreparedStatement preparedStatement = null;
ResultSet resultSet = null;
try {
//加载数据库驱动
Class.forName("com.mysql.jdbc.Driver");
//通过驱动管理类获取数据库管理
connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/mybatis?characterEncoding=utf-8","root","root");
//定义sql语句
String sql = "select * from user where username = ?";
//获取预处理对象statement
preparedStatement = connection.prepareStatement(sql);
//设置参数,第一个参数为sql语句中的参数的序号(从1开始),第二个参数为设置的参数值
preparedStatement.setString(1,"tom");
//像数据库发出sql执行查询,查询出结果集
resultSet = preparedStatement.executeQuery();
while (resultSet.next()){
int id = resultSet.getInt("id");
String username = resultSet.getString("username");
String password = resultSet.getString("password");
//将查询出的结果集封装进实体中
User user = new User();
user.setId(id);
user.setUsername(username);
user.setPassword(password);
}
} catch (Exception e) {
e.printStackTrace();
}finally {
//释放资源
try {
connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
通过以上传统的JDBC操作数据库的代码可以发现,我们能总结出来以下几条问题:
-
每执行一次sql都要建立一次数据库连接,数据库连接创建、释放频繁造成系统资源浪费,从而影响系统性能。
-
Sql编写在了代码中存在硬编码问题,实际上工作中sql变化是比较大的,每次都要修改代码,sql语句不易维护。
-
使用preparedStatement向占有位符号传参数存在硬编码,因为sql语句的where条件不一定,可能 多也可能少,修改sql还要修改代码,系统不易维护。
-
对结果集解析存在硬编码(查询列名),sql变化导致解析代码变化,系统不易维护,如果能将数据库 记录自动封装成pojo对象解析比较方便。
针对以上的几条JDBC问题,我们可以大致的延伸出一下几点的解决思路:
-
使用数据库连接池初始化连接资源。
-
将sql语句写在xml文件中,在代码中剥离出来单独维护。
-
使用反射内省等技术,完成数据库的表字段对实体的属性进行自动的映射。
深入剖析
本次完成持久层框架的自定义编写便是从以上几个方面入手,来解决传统的JDBC存在的问题,在编写之前,我们首先要明白,框架属于开发的一个半成品,是我们在开发过程中可以直接拿来用的东西,我们在自定义编写时,什么代码是框架中所有的,什么代码属于使用端(一般开发人员)提供的,这点要想明白。经分析,大体划分如下:
使用端
- 提供核心配置文件:
- sqlMapConfig.xml文件,配置数据源等信息,同时引入Mapper.xml。
- Mapper.xml文件,配置sql语句等信息。
框架端
- 读取配置文件,将配置文件加载成字节输入流存放在内存中,准备好两个javaBean用来存储以后解析配置文件出来的数据。
- Configuration:存放数据源信息dataBase、Map<String,MappedStatement>、key为唯一标识:namespace+id,value是sql相关信息实体。
- MappedStatement:存放sql相关信息,包含id,sql语句,入参类型,返回值类型等。
-
解析配置文件,创建SqlSessionFactoryBuild类,提供build(InputStream in)方法,用于构建SqlSessionFactory。
- 使用dom4j技术解析xml配置文件,将解析出来的数据存放到javaBean中。
- 创建SqlSessionFactory的实现类DefaultSqlSessionFactory
-
生产会话对象。在SqlSessionFactory中提供openSession()方法,用于生产SqlSession。
-
创建SqlSession接口及其实现类,用于封装crud方法。
- selectList(String statementId,Object… param) 查询全部
- selectOne(String statementId,Object… param) 查询单个
-
执行实际的JDBC操作。创建Executor接口及其实现类SimpleExecutor,提供query(Configuration configuration, MappedStatement mappedStatement, Object… params)方法,完成实际的与数据库交互的工作。
- 从连接池中获取连接
- 处理sql语句
- 设置参数
- 封装结果集
涉及到的设计模式
- 构建者模式
- 工厂模式
- 代理模式
代码实现
这里只贴出核心代码,全部代码请看我的码云:https://gitee.com/zang-chuanlei/FrameMyBatis.git
在这里需要创建两个项目:FrameMyBatistest和FrameMyBatis,其中FrameMyBatis_test代表的是使用端,FrameMyBatis代表的是框架端,项目结构如下:
1.在FrameMyBatis_test下面的resources目录下添加两个配置文件sqlMapConfig.xml和UserMapper.xml
<configuration> <dataSource> <property name="dataDriver" value="com.mysql.jdbc.Driver"></property> <property name="dataUrl" value="jdbc:mysql:///mybatis"></property> <property name="username" value="root"></property> <property name="password" value="root"></property> </dataSource> <!--加载UserMapper.xml配置文件--> <mapper resource="UserMapper.xml"></mapper> </configuration>
<mapper namespace="com.zae.dao.UserDao"> <select id="findAll" resultType="com.zae.entity.User"> select * from users </select> <select id="findOne" resultType="com.zae.entity.User" paramterType="com.zae.entity.User"> select * from users where id=#{id} and username=#{username} </select> </mapper>
2.给FrameMyBatis引入一些需要的坐标
<dependencies> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.17</version> </dependency> <dependency> <groupId>c3p0</groupId> <artifactId>c3p0</artifactId> <version>0.9.1.2</version> </dependency> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.12</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.10</version> </dependency> <dependency> <groupId>dom4j</groupId> <artifactId>dom4j</artifactId> <version>1.6.1</version> </dependency> <dependency> <groupId>jaxen</groupId> <artifactId>jaxen</artifactId> <version>1.1.6</version> </dependency> </dependencies>
3.在FrameMyBatis下创建Resources类,用于加载字节输入流。
import java.io.InputStream; public class Resources { /** * 根据xml路径将xml文件加载成字节流,存放在内存中 * @param path * @return */ public static InputStream getInputStreamByXml(String path){ InputStream resourceAsStream = Resources.class.getClassLoader().getResourceAsStream(path); return resourceAsStream; } }
4.创建两个bean,Configuration和MappedStatement
import javax.sql.DataSource; import java.util.HashMap; import java.util.Map; /** * javaBean之一:用来装sqlMapConfig.xml文件的内容 */ public class Configuration { /** * 存储数据源连接信息 */ private DataSource dataSource; /** * 存储加载进来的mapper.xml里面的数据 * key = statementId = namespace+"."+id * value = mapperStatement */ private Map<String,MapperStatement> mapperStatementMap = new HashMap<String, MapperStatement>(); 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; } }
/** * javaBean-2用来装载mapper.xml的数据 * 一条sql语句信息封装在一个MapperStatement对象中 */ public class MapperStatement { private String id; private String resultType; private String paramterType; private String sql; public String getId() { return id; } public void setId(String id) { this.id = id; } public String getResultType() { return resultType; } public void setResultType(String resultType) { this.resultType = resultType; } 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; } }
5.创建SqlSessionFactoryBuild
import com.zae.config.XmlConfigBuilder; import com.zae.pojo.Configuration; import java.io.InputStream; public class SqlSessionFactoryBuilder { /** * 构建SqlSessionFactory工厂 * @param inputStream * @return */ public SqlSessionFactory build(InputStream inputStream) throws Exception{ //完成xml文件的解析 XmlConfigBuilder xmlConfigBuilder = new XmlConfigBuilder(); Configuration configuration = xmlConfigBuilder.parasConfig(inputStream); DefaultSqlSessionFactory defaultSqlSessionFactory = new DefaultSqlSessionFactory(configuration); return defaultSqlSessionFactory; } }
6.创建解析XML文件的两个专属类XmlConfigBuilder和XmlMapperBuilder
import com.mchange.v2.c3p0.ComboPooledDataSource; import com.zae.io.Resources; import com.zae.pojo.Configuration; import org.dom4j.Document; import org.dom4j.Element; import org.dom4j.io.SAXReader; import java.io.InputStream; import java.util.List; import java.util.Properties; /** * 解析sqlMapConfig.xml文件存放在javaBean中 */ public class XmlConfigBuilder { private Configuration configuration; public XmlConfigBuilder(){ this.configuration = new Configuration(); } /** * 解析sqlMapConfig.xml文件 * @param inputStream * @return */ public Configuration parasConfig(InputStream inputStream) throws Exception{ SAXReader saxReader = new SAXReader(); Document document = saxReader.read(inputStream); //获取根标签里面的内容 Element rootElement = document.getRootElement(); //获取所有的property标签里面的内容 List<Element> list = rootElement.selectNodes("//property"); //将数据库连接信息读取到properties文件中 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("dataDriver")); comboPooledDataSource.setJdbcUrl(properties.getProperty("dataUrl")); comboPooledDataSource.setUser(properties.getProperty("username")); comboPooledDataSource.setPassword(properties.getProperty("password")); //给configuration里面的数据源属性赋值 configuration.setDataSource(comboPooledDataSource); //解析xl文件的数据 List<Element> mapperList = rootElement.selectNodes("//mapper"); for (Element element : mapperList) { //获取到mapper.xml文件的路径 String resource = element.attributeValue("resource"); //获取mapper文件的输入流 InputStream mapperInputStream = Resources.getInputStreamByXml(resource); XmlMapperBuilder xmlMapperBuilder = new XmlMapperBuilder(configuration); xmlMapperBuilder.parasMapper(mapperInputStream); } return configuration; } }
import com.zae.pojo.Configuration; import com.zae.pojo.MapperStatement; import org.dom4j.Document; import org.dom4j.Element; import org.dom4j.io.SAXReader; import java.io.InputStream; import java.util.List; /** * 解析mapper.xml数据存放到javaBean中 */ public class XmlMapperBuilder { private Configuration configuration; public XmlMapperBuilder(Configuration configuration){ this.configuration = configuration; } public void parasMapper(InputStream mapperInputStream) throws Exception { SAXReader saxReader = new SAXReader(); Document document = saxReader.read(mapperInputStream); //获取根标签的数据-mapper Element rootElement = document.getRootElement(); //获取命名空间 String namespace = rootElement.attributeValue("namespace"); //获取所有的select标签的内容 List<Element> elementList = rootElement.selectNodes("//select"); for (Element element : elementList) { String id = element.attributeValue("id"); String resultType = element.attributeValue("resultType"); String paramterType = element.attributeValue("paramterType"); String sql = element.getTextTrim(); MapperStatement mapperStatement = new MapperStatement(); mapperStatement.setId(id); mapperStatement.setResultType(resultType); mapperStatement.setParamterType(paramterType); mapperStatement.setSql(sql); String key = namespace+"."+id; configuration.getMapperStatementMap().put(key,mapperStatement); } } }
7.创建sqlSessionFactory接口及DefaultSqlSessionFactory 实现类
public interface SqlSessionFactory { SqlSession openSqlSession(); }
import com.zae.pojo.Configuration; public class DefaultSqlSessionFactory implements SqlSessionFactory { private Configuration configuration; public DefaultSqlSessionFactory(Configuration configuration){ this.configuration = configuration; } public SqlSession openSqlSession() { return new DefaultSqlSession(configuration); } }
8.创建sqlSession 接口及DefaultSqlSession 实现类
import java.util.List; public interface SqlSession { <E> List<E> findAll(String statementId,Object ... params) throws Exception; <T> T findOne(String statement,Object ...params)throws Exception; <T> T getMapper(Class<?> mapperClass); }
import com.zae.pojo.Configuration; import com.zae.pojo.MapperStatement; import java.lang.reflect.*; import java.util.List; import java.util.Map; public class DefaultSqlSession implements SqlSession { private Configuration configuration; public DefaultSqlSession(Configuration configuration){ this.configuration = configuration; } public <E> List<E> findAll(String statementId, Object... params) throws Exception{ Map<String, MapperStatement> statementMap = configuration.getMapperStatementMap(); MapperStatement mapperStatement = statementMap.get(statementId); Executor executor = new SimpleExecutor(); List<Object> execute = executor.execute(configuration,mapperStatement,params); return (List<E>) execute; } public <T> T findOne(String statement, Object... params) throws Exception{ List<Object> objectList = findAll(statement, params); if(objectList.size() == 1){ return (T) objectList.get(0); }else{ throw new RuntimeException("返回参数不唯一或者为空"); } } public <T> T getMapper(Class<?> aclass) { Object object = Proxy.newProxyInstance(DefaultSqlSession.class.getClassLoader(), new Class[]{aclass}, new InvocationHandler() { public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { //获取执行方法名 String methodName = method.getName(); //获取全限类名 String className = method.getDeclaringClass().getName(); //statementId String statementId = className+"."+methodName; //参数泛型化 Type type = method.getGenericReturnType(); if(type instanceof ParameterizedType){ //存在泛型,则调用findAll List<Object> all = findAll(statementId, args); return all; } return findOne(statementId,args); } }); return (T) object; } }
9.创建Executor接口及SimpleExecutor实现类,创建BoundSql实体,引入工具类。
import com.zae.pojo.Configuration; import com.zae.pojo.MapperStatement; import java.util.List; public interface Executor { /** * jdbc处理方法 * @param configuration * @param mapperStatement * @param params * @param <E> * @return */ <E> List<E> execute(Configuration configuration, MapperStatement mapperStatement, Object ...params) throws Exception; }
import com.zae.pojo.BorundSql; import com.zae.pojo.Configuration; import com.zae.pojo.MapperStatement; import com.zae.utils.GenericTokenParser; import com.zae.utils.ParameterMapping; import com.zae.utils.ParameterMappingTokenHandler; import java.beans.PropertyDescriptor; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.ResultSetMetaData; import java.util.ArrayList; import java.util.List; public class SimpleExecutor implements Executor { public <E> List<E> execute(Configuration configuration, MapperStatement mapperStatement, Object... params) throws Exception{ //1.获取连接对象 Connection connection = configuration.getDataSource().getConnection(); //2.处理sql语句 String sql = mapperStatement.getSql(); BorundSql borundSql = dealSql(sql); String sqlNow = borundSql.getSql(); List<ParameterMapping> mappingList = borundSql.getParameterMappingList(); //3.获取预处理对象 PreparedStatement preparedStatement = connection.prepareStatement(sqlNow); //4.设置参数 Class<?> classByType = getClassByType(mapperStatement.getParamterType()); for(int i = 0;i<mappingList.size();i++){ String content = mappingList.get(i).getContent(); //根据属性名获取该属性信息 Field declaredField = classByType.getDeclaredField(content); //设置暴力访问 declaredField.setAccessible(true); //获取参数里面的值 Object value = declaredField.get(params[0]); //设置参数 preparedStatement.setObject(i+1,value); } //5.处理返回结果集 ResultSet resultSet = preparedStatement.executeQuery(); Class<?> resultClass = getClassByType(mapperStatement.getResultType()); List resultList = new ArrayList(); while (resultSet.next()){ //生成该类的实例对象 Object object = resultClass.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,resultClass); //获取写入方法 Method writeMethod = propertyDescriptor.getWriteMethod(); //调用invoke方法,将数据写入实体 writeMethod.invoke(object,value); } resultList.add(object); } return resultList; } /** * 处理sql,将sql中的#{}处理成?,并把#{}里面的字段保存下来 * @param sourceSql * @return */ private BorundSql dealSql(String sourceSql){ ParameterMappingTokenHandler parameterMappingTokenHandler = new ParameterMappingTokenHandler(); GenericTokenParser genericTokenParser = new GenericTokenParser("#{","}",parameterMappingTokenHandler); String targetSql = genericTokenParser.parse(sourceSql); List<ParameterMapping> parameterMappings = parameterMappingTokenHandler.getParameterMappings(); BorundSql borundSql = new BorundSql(); borundSql.setSql(targetSql); borundSql.setParameterMappingList(parameterMappings); return borundSql; } /** * 根据类的全路径获取类对象 * @param path * @return */ private Class<?> getClassByType(String path) throws ClassNotFoundException { if(path!=null){ Class<?> aClass = Class.forName(path); return aClass; }else{ return null; } } }
import com.zae.utils.ParameterMapping; import java.util.List; public class BorundSql { private String sql; private List<ParameterMapping> parameterMappingList; public String getSql() { return sql; } public void setSql(String sql) { this.sql = sql; } public List<ParameterMapping> getParameterMappingList() { return parameterMappingList; } public void setParameterMappingList(List<ParameterMapping> parameterMappingList) { this.parameterMappingList = parameterMappingList; } }
工具类如下:
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; } }
import java.util.ArrayList; import java.util.List; public class ParameterMappingTokenHandler implements TokenHandler { private List<ParameterMapping> parameterMappings = new ArrayList<ParameterMapping>(); // context是参数名称 #{id} #{username} 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; } }
public interface TokenHandler { String handleToken(String content); }
10.在FrameMyBatis_test项目的pom文件中引入FrameMyBatis的坐标,在准备好的FrameMyBatis_test下编写测试类FrameTest
import com.zae.dao.UserDao; import com.zae.entity.User; import com.zae.io.Resources; import com.zae.session.SqlSession; import com.zae.session.SqlSessionFactory; import com.zae.session.SqlSessionFactoryBuilder; import org.junit.Before; import org.junit.Test; import java.io.InputStream; import java.util.List; public class FrameTest { private SqlSession sqlSession; private UserDao userDao; @Before public void test1() throws Exception{ //获取字节输入流 InputStream inputStream = Resources.getInputStreamByXml("sqlMapConfig.xml"); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); sqlSession = sqlSessionFactory.openSqlSession(); userDao = sqlSession.getMapper(UserDao.class); } /** * 全查 * @throws Exception */ @Test public void test2()throws Exception{ List<User> all = sqlSession.findAll("com.zae.dao.UserDao.findAll"); for (User user : all) { System.out.println(user); } } /** * 单条 * @throws Exception */ @Test public void test3() throws Exception{ User user = new User(); user.setId("1"); user.setUsername("sss"); User one = sqlSession.findOne("user.findOne", user); System.out.println(one); } /** * 代理对象查询 * @throws Exception */ @Test public void test4() throws Exception{ List<User> userList = userDao.findAll(null); for (User user : userList) { System.out.println(user); } } }
我是帝莘,期待与你的技术交流和思想碰撞。