闭关修炼180天--手写持久层框架(mybatis简易版)

2022-11-14,,,,

闭关修炼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);
}
}
}

我是帝莘,期待与你的技术交流和思想碰撞。

闭关修炼180天--手写持久层框架(mybatis简易版)的相关教程结束。

《闭关修炼180天--手写持久层框架(mybatis简易版).doc》

下载本文的Word格式文档,以方便收藏与打印。