写点什么

天哪!手动编写 mybatis 雏形竟然长这样

  • 2021 年 11 月 11 日
  • 本文字数:8165 字

    阅读完需:约 27 分钟

if (!userList.isEmpty()) {


for (User user : userList) {


System.out.println(user.toString());


}


}


}


小伙伴们发现了上面有哪些不友好的地方?


我这里总结了以下几点:


1、数据库的连接信息存在硬编码,即是写死在代码中的。


2、每次操作都会建立和释放 connection 连接,操作资源的不必要的浪费。


3、sql 和参数存在硬编码。


4、将返回结果集封装成实体类麻烦,要创建不同的实体类,并通过 set 方法一个个的注入。


存在上面的问题,所以 mybatis 就对上述问题进行了改进。


对于硬编码,我们很容易就想到配置文件来解决。mybatis 也是这么解决的。


对于资源浪费,我们想到使用连接池,mybatis 也是这个解决的。


对于封装结果集麻烦,我们想到是用 JDK 的反射机制,好巧,mybatis 也是这么解决的。



设计思路


===================================================================


既然如此,我们就来写一个自定义持久层框架,来解决上述问题,当然是参照 mybatis 的设计思路,这样我们在写完之后,再来看 mybatis 的源码就恍然大悟,这个地方这样配置原来是因为这样啊。


我们分为使用端和框架端两部分。


使用端




我们在使用 mybatis 的时候是不是需要使用 SqlMapConfig.xml 配置文件,用来存放数据库的连接信息,以及 mapper.xml 的指向信息。mapper.xml 配置文件用来存放 sql 信息。


所以我们在使用端来创建两个文件 SqlMapConfig.xml 和 mapper.xml。


框架端




框架端要做哪些事情呢?如下:


1、获取配置文件。也就是获取到使用端的 SqlMapConfig.xml 以及 mapper.xml 的 文件


2、解析配置文件。对获取到的文件进行解析,获取到连接信息,sql,参数,返回类型等等。这些信息都会保存在 configuration 这个对象中。


3、创建 SqlSessionFactory,目的是创建 SqlSession 的一个实例。


4、创建 SqlSession ,用来完成上面原始 JDBC 的那些操作。


那在 SqlSession 中 进行了哪些操作呢?


1、获取数据库连接


2、获取 sql,并对 sql 进行解析


3、通过内省,将参数注入到 preparedStatement 中


4、执行 sql


5、通过反射将结果集封装成对象


使用端实现


====================================================================


好了,上面说了一下,大概的设计思路,主要也是仿照 mybatis 主要的类实现的,保证类名一致,方便我们后面阅读源码。我们先来配置好使用端吧,我们创建一个 maven 项目。


在项目中,我们创建一个 User 实体类


public class User {


private Integer id;


private String username;


private String password;


private String birthday;


//getter()和 setter()方法


}


创建 SqlMapConfig.xml 和 Mapper.xml


SqlMapConfig.xml


<?xml version="1.0" encoding="UTF-8" ?>


<configuration>


<property name="driverClass" value="com.mysql.jdbc.Driver"></property>


<property name="jdbcUrl" value="jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC&characterEncoding=utf8&useUnicode=true&useSSL=false"></property>


<property name="userName" value="root"></property>


<property name="password" value="123456"></property>


<mapper resource="UserMapper.xml">


</mapper>


</configuration>


可以看到我们 xml 中就配置了数据库的连接信息,以及 mapper 一个索引。mybatis 中的 SqlMapConfig.xml 中还包含其他的标签,只是丰富了功能而已,所以我们只用最主要的。


mapper.xml


是每个类的 sql 都会生成一个对应的 mapper.xml 。我们这里就用 User 类来说吧,所以我们就创建一个 UserMapper.xml


<?xml version="1.0" encoding="UTF-8" ?>


<mapper namespace="cn.quellanan.dao.UserDao">


<select id="selectAll" resultType="cn.quellanan.pojo.User">


select * from user


</select>


<select id="selectByName" resultType="cn.quellanan.pojo.User" paramType="cn.quellanan.pojo.User">


select * from user where username=#{username}


</select>


</mapper>


可以看到有点 mybatis 里面文件的味道,有 namespace 表示命名空间,id 唯一标识,resultType 返回结果集的类型,paramType 参数的类型。


我们使用端先创建到这,主要是两个配置文件,我们接下来看看框架端是怎么实现的。


加油哈哈。



框架端实现


====================================================================


框架端,我们按照上面的设计思路一步一步来。


获取配置




怎么样获取配置文件呢?我们可以使用 JDK 自带自带的类 Resources 加载器来获取文件。我们创建一个自定义 Resource 类来封装一下:


import java.io.InputStream;


public class Resources {


public static InputStream getResources(String path){


//使用系统自带的类 Resources 加载器来获取文件。


return Resources.class.getClassLoader().getResourceAsStream(path);


}


}


这样通过传入路径,就可以获取到对应的文件流啦。


解析配置文件




上面获取到了 SqlMapConfig.xml 配置文件,我们现在来解析它。


不过在此之前,我们需要做一点准备工作,就是解析的内存放到什么地方?


所以我们来创建两个实体类 Mapper 和 Configuration。


Mapper


Mapper 实体类用来存放使用端写的 mapper.xml 文件的内容,我们前面说了里面有.id、sql、resultType 和 paramType .所以我们创建的 Mapper 实体如下:


public class Mapper {


private String id;


private Class<?> resultType;


private Class<?> parmType;


private String sql;


//getter()和 setter()方法


}


这里我们为什么不添加 namespace 的值呢?


聪明的你肯定发现了,因为 mapper 里面这些属性表明每个 sql 都对应一个 mapper,而 namespace 是一个命名空间,算是 sql 的上一层,所以在 mapper 中暂时使用不到,就没有添加了。


Configuration


Configuration 实体用来保存 SqlMapConfig 中的信息。所以需要保存数据库连接,我们这里直接用 JDK 提供的 DataSource。还有一个就是 mapper 的信息。每个 mapper 有自己的标识,所以这里采用 hashMap 来存储。如下:


public class Configuration {


private DataSource dataSource;


HashMap <String,Mapper> mapperMap=new HashMap<>();


//getter()和 setter 方法


}

XmlMapperBuilder

做好了上面的准备工作,我们先来解析 mapper 吧。我们创建一个 XmlMapperBuilder 类来解析。通过 dom4j 的工具类来解析 XML 文件。我这里用的 dom4j 依赖为:


<dependency>


<groupId>org.dom4j</groupId>


<artifactId>dom4j</artifactId>


<version>2.1.3</version>


</dependency>


思路:


1、获取文件流,转成 document。


2、获取根节点,也就是 mapper。获取根节点的 namespace 属性值


3、获取 select 节点,获取其 id,sql,resultType,paramType


4、将 select 节点的属性封装到 Mapper 实体类中。


5、同理获取 update/insert/delete 节点的属性值封装到 Mapper 中


6、通过 namespace.id 生成 key 值将 mapper 对象保存到 Configuration 实体中的 HashMap 中。


7、返回 Configuration 实体


代码如下:


public class XmlMapperBuilder {


private Configuration configuration;


public XmlMapperBuilder(Configuration configuration){


this.configuration=configuration;


}


public Configuration loadXmlMapper(InputStream in) throws DocumentException, ClassNotFoundException {


Document document=new SAXReader().read(in);


Element rootElement=document.getRootElement();


String namespace=rootElement.attributeValue("namespace");


List<Node> list=rootElement.selectNodes("//select");


for (int i = 0; i < list.size(); i++) {


Mapper mapper=new Mapper();


Element element= (Element) list.get(i);


String id=element.attributeValue("id");


mapper.setId(id);


String paramType = element.attributeValue("paramType");


if(paramType!=null && !paramType.isEmpty()){


mapper.setParmType(Class.forName(paramType));


}


String resultType = element.attributeValue("resultType");


if (resultType != null && !resultType.isEmpty()) {


mapper.setResultType(Class.forName(resultType));


}


mapper.setSql(element.getTextTrim());


String key=namespace+"."+id;


configuration.getMapperMap().put(key,mapper);


}


return configuration;


}


}


上面我只解析了 select 标签。大家可以解析对应 insert/delete/uupdate 标签,操作都是一样的。

XmlConfigBuilder

我们再来解析一下 SqlMapConfig.xml 配置信息思路是一样的,


1、获取文件流,转成 document。


2、获取根节点,也就是 configuration。


3、获取根节点中所有的 property 节点,并获取值,也就是获取数据库连接信息


4、创建一个 dataSource 连接池


5、将连接池信息保存到 Configuration 实体中


6、获取根节点的所有 mapper 节点


7、调用 XmlMapperBuilder 类解析对应 mapper 并封装到 Configuration 实体中


8、完


代码如下:


public class XmlConfigBuilder {


private Configuration configuration;


public XmlConfigBuilder(Configuration configuration){


this.configuration=configuration;


}


public Configuration loadXmlConfig(InputStream in) throws DocumentException, PropertyVetoException, ClassNotFoundException {


Document document=new SAXReader().read(in);


Element rootElement=document.getRootElement();


//获取连接信息


List<Node> propertyList=rootElement.selectNodes("//property");


Properties properties=new Properties();


for (int i = 0; i < propertyList.size(); i++) {


Element element = (Element) propertyList.get(i);


properties.setProperty(element.attributeValue("name"),element.attributeValue("value"));


}


//是用连接池


ComboPooledDataSource dataSource = new ComboPooledDataSource();


dataSource.setDriverClass(properties.getProperty("driverClass"));


dataSource.setJdbcUrl(properties.getProperty("jdbcUrl"));


dataSource.setUser(properties.getProperty("userName"));


dataSource.setPassword(properties.getProperty("password"));


configuration.setDataSource(dataSource);


//获取 mapper 信息


List<Node> mapperList=rootElement.selectNodes("//mapper");


for (int i = 0; i < mapperList.size(); i++) {


Element element= (Element) mapperList.get(i);


String mapperPath=element.attributeValue("resource");


XmlMapperBuilder xmlMapperBuilder = new XmlMapperBuilder(configuration);


configuration=xmlMapperBuilder.loadXmlMapper(Resources.getResources(mapperPath));


}


return configuration;


}


}


创建 SqlSessionFactory




完成解析后我们创建 SqlSessionFactory 用来创建 Sqlseesion 的实体,这里为了尽量还原 mybatis 设计思路,也也采用的工厂设计模式。


SqlSessionFactory 是一个接口,里面就一个用来创建 SqlSessionf 的方法。


如下:


public interface SqlSessionFactory {


public SqlSession openSqlSession();


}


单单这个接口是不够的,我们还得写一个接口的实现类,所以我们创建一个 DefaultSqlSessionFactory。


如下:


public class DefaultSqlSessionFactory implements SqlSessionFactory {


private Configuration configuration;


public DefaultSqlSessionFactory(Configuration configuration) {


this.configuration = configuration;


}


public SqlSession openSqlSession() {


return new DefaultSqlSeeion(configuration);


}


}


可以看到就是创建一个 DefaultSqlSeeion 并将包含配置信息的 configuration 传递下去。DefaultSqlSeeion 就是 SqlSession 的一个实现类。


创建 SqlSession




在 SqlSession 中我们就要来处理各种操作了,比如 selectList,selectOne,insert.update,delete 等等。


我们这里 SqlSession 就先写一个 selectList 方法。


如下:


public interface SqlSession {


/**


  • 条件查找

  • @param statementid 唯一标识,namespace.selectid

  • @param parm 传参,可以不传也可以一个,也可以多个

  • @param <E>

  • @return


*/


public <E> List<E> selectList(String statementid,Object...parm) throws Exception;


然后我们创建 DefaultSqlSeeion 来实现 SqlSeesion 。


public class DefaultSqlSeeion implements SqlSession {


private Configuration configuration;


private Executer executer=new SimpleExecuter();


public DefaultSqlSeeion(Configuration configuration) {


this.configuration = configuration;


}


@Override


public <E> List<E> selectList(String statementid, Object... parm) throws Exception {


Mapper mapper=configuration.getMapperMap().get(statementid);


List<E> query = executer.query(configuration, mapper, parm);


return query;


}


}


我们可以看到 DefaultSqlSeeion 获取到了 configuration,并通过 statementid 从 configuration 中获取 mapper。 然后具体实现交给了 Executer 类来实现。我们这里先不管 Executer 是怎么实现的,就假装已经实现了。那么整个框架端就完成了。通过调用 Sqlsession.selectList() 方法,来获取结果。



感觉我们都还没有处理,就框架搭建好了?骗鬼呢,确实前面我们从获取文件解析文件,然后创建工厂。都是做好准备工作。下面开始我们 JDBC 的实现。


SqlSession 具体实现


==============================================================================


我们前面说 SqlSeesion 的具体实现有下面 5 步


1、获取数据库连接


2、获取 sql,并对 sql 进行解析


3、通过内省,将参数注入到 preparedStatement 中


4、执行 sql


5、通过反射将结果集封装成对象


但是我们在 DefaultSqlSeeion 中将实现交给了 Executer 来执行。所以我们就要在 Executer 中来实现这些操作。


我们首先来创建一个 Executer 接口,并写一个 DefaultSqlSeeion 中调用的 query 方法。


public interface Executer {


<E> List<E> query(Configuration configuration,Mapper mapper,Object...parm) throws Exception;


}


接着我们写一个 SimpleExecuter 类来实现 Executer 。


然后 SimpleExecuter.query()方法中,我们一步一步的实现。


获取数据库连接




因为数据库连接信息保存在 configuration,所以直接获取就好了。


//获取连接


connection=configuration.getDataSource().getConnection();


获取 sql,并对 sql 进行解析




我们这里想一下,我们在 Usermapper.xml 写的 sql 是什么样子?


select * from user where username=#{username}


#{username} 这样的 sql 我们该怎么解析呢?


分两步


1、将 sql 找到 #{***},并将这部分替换成 ?号


2、对 #{***} 进行解析获取到里面的参数对应的 paramType 中的值。


具体实现用到下面几个类。


GenericTokenParser 类,可以看到有三个参数,开始标记,就是我们的“#{” ,结束标记就是 “}”, 标记处理器就是处理标记里面的内容也就是 username。


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) {


//具体实现


}



主要的就是 parse() 方法,用来获取操作 1 的 sql。获取结果例如:


select * from user where username=?


那上面用到 TokenHandler 来处理参数。


ParameterMappingTokenHandler 实现 TokenHandler 的类


public class ParameterMappingTokenHandler implements TokenHandler {


private List<ParameterMapping> parameterMappings = new ArrayList<ParameterMapping>();


// context 是参数名称 #{id} #{username}


@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;


}


}


可以看到将参数名称存放 ParameterMapping 的集合中了。


ParameterMapping 类就是一个实体,用来保存参数名称的。


public class ParameterMapping {


private String content;


public ParameterMapping(String content) {


this.content = content;


}


//getter()和 setter() 方法。


}


所以我们在我们通过 GenericTokenParser 类,就可以获取到解析后的 sql,以及参数名称。我们将这些信息封装到 BoundSql 实体类中。


public class BoundSql {


private String sqlText;


private List<ParameterMapping> parameterMappingList=new ArrayList<>();


public BoundSql(String sqlText, List<ParameterMapping> parameterMappingList) {


this.sqlText = sqlText;


this.parameterMappingList = parameterMappingList;


}


getter()和 setter() 方法。


}


好了,那么分两步走,先获取,后解析


获取


获取原始 sql 很简单,sql 信息就存在 mapper 对象中,直接获取就好了。


String sql=mapper.getSql()


解析


1、创建一个 ParameterMappingTokenHandler 处理器


2、创建一个 GenericTokenParser 类,并初始化开始标记,结束标记,处理器


3、执行 genericTokenParser.parse(sql);获取解析后的 sql‘’,以及在 parameterMappingTokenHandler 中存放了参数名称的集合。


4、将解析后的 sql 和参数封装到 BoundSql 实体类中。


/**


  • 解析自定义占位符

  • @param sql

  • @return


*/


private BoundSql getBoundSql(String sql){


ParameterMappingTokenHandler parameterMappingTokenHandler = new ParameterMappingTokenHandler();


GenericTokenParser genericTokenParser = new GenericTokenParser("#{","}",parameterMappingTokenHandler);


String parse = genericTokenParser.parse(sql);


return new BoundSql(parse,parameterMappingTokenHandler.getParameterMappings());


}


将参数注入到 preparedStatement 中




上面的就完成了 sql,的解析,但是我们知道上面得到的 sql 还是包含 JDBC 的 占位符,所以我们需要将参数注入到 preparedStatement 中。


1、通过 boundSql.getSqlText()获取带有占位符的 sql.


2、接收参数名称集合 parameterMappingList


3、通过 mapper.getParmType() 获取到参数的类。


4、通过 getDeclaredField(content)方法获取到参数类的 Field。


5、通过 Field.get() 从参数类中获取对应的值


6、注入到 preparedStatement 中


BoundSql boundSql=getBoundSql(mapper.getSql());


String sql=boundSql.getSqlText();


List<ParameterMapping> parameterMappingList = boundSql.getParameterMappingList();


//获取 preparedStatement,并传递参数值


PreparedStatement preparedStatement=connection.prepareStatement(sql);


Class<?> parmType = mapper.getParmType();


for (int i = 0; i < parameterMappingList.size(); i++) {


ParameterMapping parameterMapping = parameterMappingList.get(i);


String content = parameterMapping.getContent();


Field declaredField = parmType.getDeclaredField(content);


declaredField.setAccessible(true);


Object o = declaredField.get(parm[0]);


preparedStatement.setObject(i+1,o);


}


System.out.println(sql);


return preparedStatement;


执行 sql




其实还是调用 JDBC 的 executeQuery()方法或者 execute()方法


//执行 sql


ResultSet resultSet = preparedStatement.executeQuery();


通过反射将结果集封装成对象




在获取到 resultSet 后,我们进行封装处理,和参数处理是类似的。


1、创建一个 ArrayList


2、获取返回类型的类


3、循环从 resultSet 中取数据


4、获取属性名和属性值


5、创建属性生成器


6、为属性生成写方法,并将属性值写入到属性中


7、将这条记录添加到 list 中


8、返回 list


/**


  • 封装结果集

  • @param mapper

  • @param resultSet

  • @param <E>

  • @return

  • @throws Exception


*/


private <E> List<E> resultHandle(Mapper mapper,ResultSet resultSet) throws Exception{


ArrayList<E> list=new ArrayList<>();


//封装结果集

评论

发布
暂无评论
天哪!手动编写mybatis雏形竟然长这样