一、mybatis 流程简介

最近在看 mybatis 的源码,大致了解整个框架流程后便手写了一个特别简单的simpmybatis的小demo,来巩固这整个框架的学习。下图是我所画的框架大致执行流程:

对上图分析后得出结论:

1.mybatis 的配置文件分为两种,且这两个配置文件会被封装到 configuration 中

  • 主配置文件(mybatisconfig.xml):配置 jdbc 等环境信息,全局唯一;
  • 映射文件(xxxmapper.xml):配置多个 sql ,可有多个。

2.通过 mybatis 配置文件得到 sqlsessionfactory ;
3.通过 sqlsessionfactory 得到 sqlsession,它就相当于 request 请求;
4.sqlsession 调用底层的 executor 执行器来操作数据库,同时执行器有两类实现

  • 基本实现
  • 带有缓存功能的实现

5.解析传入的参数,对其进行封装,执行并返回结果;
以上就是我梳理的 mybatis 大致流程,看似简单,却很精妙。

二、手写简化版 mybatis 设计思路

2.1 简化后的思路

2.2 读取 xml 文件,建立连接

从图中可以看出,myconfig 负责与人交互。待读取xml后,将属性和连接数据库的操作封装在 myconfig 对象中供后面的组件调用。本项目将使用 dom4j 来读取xml文件,它具有性能优异和非常方便使用的特点。

2.3 创建sqlsession,搭建 configuration 和 executor 之间的桥梁

从流程图中的箭头可以看出,mysqlsession 的成员变量中必须得有 myexecutorimpl 和 myconfig 去集中做调配。一个session仅拥有一个对应的数据库连接。类似于一个前段请求request,它负责直接调用对应 execute(sql) 来做 crud 操作。

2.4 创建 myexecutor,封装 jdbc 操作数据库

myexecutor 是一个执行器,负责sql语句的生成和查询缓存的维护,也就是 jdbc 的代码将在这里完成,不过本文只实现了单表,查询缓存并未实现。

2.5 创建 mysqlsessionproxy,使用动态代理生成 mapper 对象

只是希望对指定的接口生成一个对象,使得执行它的时候能运行一句 sql,而接口无法直接调用方法,所以这里使用动态代理生成对象,在执行时还是回到 mysqlsession 中调用查询,最终由 myexecutorimpl 做 jdbc查询。这样设计是为了单一职责,可扩展性更强。

三、实现自己的mybatis

这次会将其打成 jar 包,并将其导入项目实现,做一个 mybatis 的还原。

工程文件及目录:

3.1 导入两个所需 jar 包:数据库连接和xml解析

maven 导入如下:

<!-- https://mvnrepository.com/artifact/org.dom4j/dom4j -->
<!-- xml解析 -->
<dependency>
 <groupid>org.dom4j</groupid>
 <artifactid>dom4j</artifactid>
 <version>2.1.3</version>
</dependency>

<!-- mysql -->
<dependency>
 <groupid>mysql</groupid>
 <artifactid>mysql-connector-java</artifactid>
 <version>5.1.49</version>
</dependency>

3.2 创建 myconfig 类,对两大 xml 配置文件进行解析,并建立连接

/**
 * @author kenelm
 * @version 1.0
 * @date 2020/11/22 12:17
 */
public class myconfig {
 /**
  * 启动应用程序类加载器
  */
 private static final classloader loader = classloader.getsystemclassloader();
 /**
  * 数据库建立连接
  * @return 返回数据库连接对象
  *
  */
 public connection build() {

  // mybatis主配置文件名
  string resource = "mybatis-config.xml";

  // 获取文件根节点
  element root = parsexml(resource);
  // 获取文件对应的信息
  map<string, string> jdbcmap = parsenodes(root);
  try {
   class.forname(jdbcmap.get("driverclassname"));
  } catch (classnotfoundexception e) {
   throw new runtimeexception("驱动器未找到,请重新检查!");
  }
  connection connect = null;
  try {
   connect = drivermanager.getconnection(jdbcmap.get("url"), jdbcmap.get("username"), jdbcmap.get("password"));
  } catch (sqlexception throwables) {
   throw new runtimeexception("数据库连接错误,请检查路径、用户名、密码是否输入正确!");
  }
  return connect;
 }
 /**
  * 解析数据库配置文件
  * @param resource 数据库配置文件路径
  * @return 获取到的文件根节点
  */
 public static element parsexml(string resource) {
  try {
   // 返回用于读取指定资源的输入流
   inputstream stream = loader.getresourceasstream(resource);
   // 使用dom4j解析xml
   saxreader reader = new saxreader();
   // 使用sax从给定流中读取文件
   document doc = reader.read(stream);
   // 获取文件的根节点
   return doc.getrootelement();
  } catch (documentexception e) {
   throw new runtimeexception("解析 xml 时发生错误!" + resource);
  }
 }
 /**
  * 解析主xml文件标签节点
  * @param node 配置文件根节点
  * @return 返回从配置文件中拿到的开启数据库对应值
  */
 private map<string, string> parsenodes(element node) {
  // 判断根标签名称
  if (!node.getname().equals("database")) {
   throw new runtimeexception("数据库配置文件根标签名称必须为【database】");
  }

  // 存放配置文件取得的值
  map<string, string> map = new hashmap<string, string>();
  map.put("driverclassname", null);
  map.put("url", null);
  map.put("username", null);
  map.put("password", null);

  // 读取property的属性内容
  for (element item : node.elements()) {
   // 获取标签中存放的值,并删除其前导和结尾的空格
   string value = getvalue(item);
   // 获取标签中 name 的名称
   string name = item.attributevalue("name");
   // 如果name或value为空则有对应值未输入
   if (name == null || "".equals(value)) {
    throw new runtimeexception("[database]: <property> 中应该包含名称和值");
   }
   switch (name) {
    case "driverclassname" : map.put("driverclassname", value); break;
    case "url" : map.put("url", value); break;
    case "username" : map.put("username", value); break;
    case "password" : map.put("password", value); break;
    default: throw new runtimeexception("[database]: <property> 中有未知属性");
   }
  }
  return map;
 }
 /**
  * 获取property属性中的值
  * @param node 配置文件根节点
  * @return 如果有value值,则读取;没有设置value,则读取内容
  */
 private static string getvalue(element node) {
  return node.hascontent() ? node.gettext().trim() : node.attributevalue("value").trim();
 }
 /**
  *
  * @param path
  * @return
  */
 @suppresswarnings(value = "rawtypes")
 public mappingbean readmapper(string path) {
  mappingbean bean = new mappingbean();
  try {
   inputstream stream = loader.getresourceasstream(path);
   saxreader reader = new saxreader();
   document doc = reader.read(stream);
   element root = doc.getrootelement();
   // 把mapper节点的namespace值存为接口名
   bean.setinterfacename(root.attributevalue("namespace").trim());
   // 用来存储方法的list
   list<mapping> list = new arraylist<mapping>();
   //遍历根节点下所有子节点
   for(iterator rootiter = root.elementiterator(); rootiter.hasnext();) {
    // 存储一条方法的信息
    mapping fun = new mapping();
    element e = (element) rootiter.next();
    string sqltype = e.getname().trim();
    string funcname = e.attributevalue("id").trim();
    string sql = e.gettext().trim();
    string resulttype = e.attributevalue("resulttype").trim();
    fun.setsqltype(sqltype);
    fun.setfuncname(funcname);
    object newinstance = null;
    try {
     newinstance = class.forname(resulttype).newinstance();
    } catch (instantiationexception | illegalaccessexception | classnotfoundexception e1) {
     e1.printstacktrace();
    }
    fun.setresulttype(newinstance);
    fun.setsql(sql);
    list.add(fun);
   }
   bean.setlist(list);

  } catch (documentexception e) {
   e.printstacktrace();
  }
  return bean;
 }
 /**
  * 解析mapper映射xml文件
  * @param element mapper文件路径
  * @return
  */
 public mappingbean parsemapper(element element) {

  mappingbean bean = new mappingbean();
  string namespace = element.attributevalue("namespace");
  if (namespace == null) {
   throw new runtimeexception("映射文件namespace不存在");
  }
  bean.setinterfacename(namespace);
  list<mapping> list = new arraylist<>();
  iterator<element> it = element.elementiterator();
  while (it.hasnext()) {
   element ele=(element) it.next();
   mapping mapping =new mapping();
   string funcname =ele.attributevalue("id");
   if (funcname==null){
    throw new runtimeexception("mapper映射文件中id不存在");
   }
   string sqltype = ele.getname();
   string paramtype = ele.attributevalue("parametertype");
   string resulttype=ele.attributevalue("resulttype");
   string sql=ele.gettext().trim();
   mapping.setfuncname(funcname);
   mapping.setsqltype(sqltype);
   mapping.setparametertype(paramtype);
   mapping.setsql(sql);
   object object=null;
   try {
    object=class.forname(resulttype).newinstance();
   } catch (instantiationexception | illegalaccessexception | classnotfoundexception e) {
    e.printstacktrace();
   }
   mapping.setresulttype(object);
   list.add(mapping);
  }
  bean.setlist(list);
  return bean;
 }
}

由 myconfig类 代码可以得知:

  1. mybatis 主配置类名称必须为:mybatis-config.xml
  2. mybatis-config.xml 的根标签必须为: <database></database>
  3. mapper.xml 必须包括:namespace
  4. sql 是否有返回值都应包括:resulttype(个人偷懒,没做判断);… …

3.3 mysqlsession 代理

mysqlsession 肯定不会自己去执行,因为不能写死所以使用动态代理来使代理类去实现具体方法。

/**
 * @author kenelm
 * @version 1.0
 * @date 2020/11/22 12:52
 */
public class mysqlsession {
 private final myexcutor excutor= new myexcutorimpl();

 private final myconfig config = new myconfig();

 public <t> t selectvalue(mapping statement, list<object> parameter){
  return excutor.queryvalue(statement, parameter);
 }

 public <t> t selectnull(mapping statement){
  return excutor.querynull(statement);
 }

 public int deletevalue(mapping statement, list<object> parameter) {
  return excutor.deletevalue(statement, parameter);
 }

 public int updatevalue(mapping statement, list<object> parameter) {
  return excutor.updatevalue(statement, parameter);
 }

 public int insertvalue(mapping mapping, list<object> parameter) {
  return excutor.insertvalue(mapping, parameter);
 }

 @suppresswarnings("unchecked")
 public <t> t getmapper(class<t> clas){
  //动态代理调用
  return (t) proxy.newproxyinstance(clas.getclassloader(),new class[]{clas},
    new mysqlsessionproxy(config,this));
 }
}

编写代理类,把mapper映射文件解析进来

/**
 * @author kenelm
 * @version 1.0
 * @date 2020/11/22 12:55
 */
public class mysqlsessionproxy implements invocationhandler {
 private myconfig config;
 private mysqlsession sqlsession;

 public mysqlsessionproxy(myconfig config, mysqlsession sqlsession) {
  this.config = config;
  this.sqlsession = sqlsession;
 }

 @override
 public object invoke(object proxy, method method,object[] args) {
  string name = method.getdeclaringclass().getname();
  string mappername = name.substring(name.lastindexof(".")+1);
  mappingbean bean=config.parsemapper(myconfig.parsexml(mappername+".xml"));

  if (bean!=null && (bean.getlist()!=null && bean.getlist().size()>0)){
   for (mapping mapping : bean.getlist()){
    if (mapping.getfuncname().equals(method.getname())) {
     // 判断是否为查询语句
     if ("select".equals(mapping.getsqltype().tolowercase())) {
      system.out.println("执行查询方法:" + mapping.getsql());
      if (args!=null) {
       system.out.println("参数:"+ arrays.tostring(args));
       return sqlsession.selectvalue(mapping, arrays.aslist(args));
      } else {
       system.out.println("参数:null");
       return sqlsession.selectnull(mapping);
      }
     }
     // 判断是否为删除语句
     if ("delete".equals(mapping.getsqltype().tolowercase())){
      system.out.println("执行查询方法:"+mapping.getsql());
      system.out.println("参数:"+ arrays.tostring(args));
      return sqlsession.deletevalue(mapping, arrays.aslist(args));
     }
     // 判断是否为更新语句
     if ("update".equals(mapping.getsqltype().tolowercase())) {
      system.out.println("执行查询方法:"+mapping.getsql());
      system.out.println("参数:"+ arrays.tostring(args));
      return sqlsession.updatevalue(mapping, arrays.aslist(args));
     }
     // 判断是否为插入语句
     if ("insert".equals(mapping.getsqltype().tolowercase())) {
      system.out.println("执行查询方法:" + mapping.getsql());
      system.out.println("参数:" + arrays.tostring(args));
      return sqlsession.insertvalue(mapping, arrays.aslist(args));
     }
    }
   }
  }
  return null;
 }
}

注意:通过上段代码可知,映射文件必须和接口名称保持一致。

3.4 创建对应实体类和xml映射文件sql实体类

a. 接口实体类

/**
 * @author kenelm
 * @version 1.0
 * @date 2020/11/22 12:38
 */
public class mappingbean {
 /**
  * 接口名
  */
 private string interfacename;
 /**
  * 接口下所有方法
  */
 private list<mapping> list;
 // setter、getter略
}

b. 映射文件中 sql 的实体类

/**
 * @author kenelm
 * @version 1.0
 * @date 2020/11/22 12:38
 */
public class mapping {
 private string sqltype;
 private string funcname;
 private string sql;
 private object resulttype;
 private string parametertype;
  // setter、getter略
}

3.5 创建 myexcutor 接口以及实现类

myexcutor 接口

/**
 * @author kenelm
 * @version 1.0
 * @date 2020/11/22 12:42
 */
public interface myexcutor {
 // 无参查询
 <t> t querynull(mapping mapping);
	// 有参查询
 <t> t queryvalue(mapping mapping, list<object> params);
	// 删除
 int deletevalue(mapping mapping, list<object> params);
	// 更新
 int updatevalue(mapping mapping, list<object> params);
	// 插入
 int insertvalue(mapping mapping, list<object> params);
}

myexcutorimpl 实现类

这里通过反射将结果转换成对象

/**
 * @author kenelm
 * @version 1.0
 * @date 2020/11/22 12:42
 */
public class myexcutorimpl implements myexcutor {

 private myconfig config = new myconfig();

 @override
 public <t> t querynull(mapping mapping) {
  connection conn = config.build();
  preparedstatement preparedstatement;
  resultset resultset;
  object obj;
  list<object> list = new arraylist<>();
  try {
   preparedstatement=conn.preparestatement(mapping.getsql());
   if (mapping.getresulttype() == null){
    throw new runtimeexception("返回的映射结果不能为空!");
   }
   resultset = preparedstatement.executequery();
   int row = 0;
   resultsetmetadata rd = resultset.getmetadata();
   while (resultset.next()){
    obj=resulttoobject(resultset,mapping.getresulttype());
    row++;
    list.add(obj);
   }
   system.out.println("记录行数:"+row);
  } catch (sqlexception e) {
   e.printstacktrace();
  }
  return (t) list;
 }

 @override
 public <t> t queryvalue(mapping mapping, list<object> params) {
  connection conn = config.build();
  preparedstatement preparedstatement;
  resultset resultset;
  object obj;
  list<object> list = new arraylist<>();
  try {
   preparedstatement=conn.preparestatement(mapping.getsql());
   for (int i=0; i<params.size(); i++) {
    preparedstatement.setstring(i+1, params.get(i).tostring());
   }
   if (mapping.getresulttype() == null){
    throw new runtimeexception("返回的映射结果不能为空!");
   }
   resultset = preparedstatement.executequery();
   int row = 0;
   resultsetmetadata rd = resultset.getmetadata();
   while (resultset.next()){
    obj=resulttoobject(resultset,mapping.getresulttype());
    row++;
    list.add(obj);
   }
   system.out.println("记录行数:"+row);
  } catch (sqlexception e) {
   e.printstacktrace();
  }
  return (t) list;
 }

 @override
 public int deletevalue(mapping mapping, list<object> params) {
  connection conn = config.build();
  int rows = 0;
  preparedstatement preparedstatement=null;
  try {
   preparedstatement = conn.preparestatement(mapping.getsql());
   for (int i=0; i<params.size(); i++) {
    preparedstatement.setstring(i+1, params.get(i).tostring());
   }
   rows = preparedstatement.executeupdate();
   if (rows != 0) {
    system.out.println("删除成功,受影响行数:"+rows);
   } else {
    system.out.println("删除失败,数据库无相应数据...");
   }
  } catch (sqlexception e) {
   e.printstacktrace();
  }
  return rows;
 }

 @override
 public int updatevalue(mapping mapping, list<object> params) {
  connection conn = config.build();
  int rows = 0;
  preparedstatement preparedstatement=null;
  try {
   preparedstatement = conn.preparestatement(mapping.getsql());
   for (int i=0; i<params.size(); i++) {
    preparedstatement.setstring(i+1, params.get(i).tostring());
   }
   rows = preparedstatement.executeupdate();
   if (rows != 0) {
    system.out.println("修改成功,受影响行数:"+rows);
   } else {
    system.out.println("修改失败,数据库无相应数据...");
   }
  } catch (sqlexception e) {
   e.printstacktrace();
  }
  return rows;
 }

 @override
 public int insertvalue(mapping mapping, list<object> params) {
  connection conn = config.build();
  int rows = 0;
  preparedstatement preparedstatement=null;
  try {
   preparedstatement = conn.preparestatement(mapping.getsql());
   for (int i=0; i<params.size(); i++) {
    preparedstatement.setstring(i+1, params.get(i).tostring());
   }
   try {
    rows = preparedstatement.executeupdate();
    if (rows != 0) {
     system.out.println("插入成功,受影响行数:"+rows);
    } else {
     system.out.println("插入失败...");
    }
   } catch (sqlexception throwables) {
    throw new runtimeexception("插入重复 \"key\" 值数据");
   }
  } catch (sqlexception e) {
   e.printstacktrace();
  }
  return rows;
 }

 private <t> t resulttoobject(resultset rs, object object) {
  object obj=null;

  try {
   class<?> cls = object.getclass();
  /*
   这里为什么要通过class再new一个对象?
   因为如果不new一个新的对象,每次返回的都是形参上的object,
   而这个object都是同一个,会导致list列表后面覆盖前面值。
   */
   obj=cls.newinstance();
   //获取结果集元数据(获取此 resultset 对象的列的编号、类型和属性。)
   resultsetmetadata rd=rs.getmetadata();
   for (int i = 0; i < rd.getcolumncount(); i++) {
    //获取列名
    string columnname=rd.getcolumnlabel(i+1);
    //组合方法名
    string methodname="set"+columnname.substring(0, 1).touppercase()+columnname.substring(1);
    //获取列类型
    int columntype=rd.getcolumntype(i+1);
    method method=null;
    switch(columntype) {
     case java.sql.types.varchar:
     case java.sql.types.char:
      method=cls.getmethod(methodname, string.class);
      method.invoke(obj, rs.getstring(columnname));
      break;
     case java.sql.types.integer:
      method=cls.getmethod(methodname, integer.class);
      method.invoke(obj, rs.getint(columnname));
      break;
     default:
      break;
    }
   }
  } catch (illegalaccessexception | instantiationexception | nosuchmethodexception | invocationtargetexception | sqlexception e) {
   e.printstacktrace();
  }
  return (t) obj;
 }
}

四、打包测试

4.1 将其打成 jar 包

4.2 创建一个maven项目,因为需要导入对应的包

<!-- https://mvnrepository.com/artifact/org.dom4j/dom4j -->
<!-- xml解析 -->
<dependency>
 <groupid>org.dom4j</groupid>
 <artifactid>dom4j</artifactid>
 <version>2.1.3</version>
</dependency>

<!-- mysql -->
<dependency>
 <groupid>mysql</groupid>
 <artifactid>mysql-connector-java</artifactid>
 <version>5.1.49</version>
</dependency>
<!-- 自己写的mybatis,首先要将其放入本地仓库 -->
<dependency>
 <groupid>top.kk233</groupid>
 <artifactid>simpmybatis</artifactid>
 <version>1.0.0</version>
</dependency>

maven导入本地jar包方法自行百度,这里就不赘述。

4.3 创建数据库

这里提供一个我测试的,你们可以自行创建其他的

create database if not exists `test`;
use `test`;
create table `user` (
	`id` int ( 10 ) not null,
	`sex` varchar ( 2 ) not null,
	`password` varchar ( 255 ) default null,
	`username` varchar ( 255 ) default null,
	primary key ( `id` ) 
) engine = innodb auto_increment = 2 default charset = utf8;
insert into `test`.`user` ( `id`, `sex`, `password`, `username` )
values
	( 1, '男', '12344', '五六' ),
	( 2, '女', '12643', '张三' ),
	( 3, '男', '1245453', '李四' );

4.4 创建实体类

/**
 * @author kenelm
 * @version 1.0
 * @date 2020/11/22 16:17
 */
public class user {
 private integer id;
 private string sex;
 private string password;
 private string username;
 // setter、getter略
}

4.5 创建 usermapper 接口

/**
 * @author kenelm
 * @version 1.0
 * @date 2020/11/22 16:17
 */
public interface usermapper {
 list<user> getusers();

 list<user> getuserbysexandname(string sex, string username);

 int deleteuserbyid(integer id);

 int updateuserbyname(string username, string password);

 int insertuser(int id, string sex, string password, string username);
}

4.6 创建 usermapper.xml 映射文件

<?xml version="1.0" encoding="utf-8"?>
<mapper namespace="top.kk233.mapper.usermapper">
 <select id="getusers" resulttype="top.kk233.pojo.user">
  select * from user
 </select>
 <select id="getuserbysexandname" resulttype="top.kk233.pojo.user">
  select * from user where sex=? and username=?
 </select>

 <delete id="deleteuserbyid" resulttype="top.kk233.pojo.user">
  delete from user where id=?
 </delete>

 <update id="updateuserbyname" resulttype="top.kk233.pojo.user">
  update user set password=? where username=?
 </update>

 <insert id="insertuser" resulttype="top.kk233.pojo.user">
  insert into user values(?,?,?,?)
 </insert>
</mapper>

4.7 创建 mybatis-config.xml 数据库配置文件

<?xml version="1.0" encoding="utf-8"?>
<database>
 <property name="driverclassname">com.mysql.jdbc.driver</property>
 <property name="url">jdbc:mysql://localhost:3306/test?usessl=false</property>
 <property name="username">root</property>
 <property name="password">124760</property>
</database>

4.8 创建启动类测试

/**
 * @author kenelm
 * @version 1.0
 * @date 2020/11/22 16:24
 */
public class app {

 public static void main(string[] args) {

  mysqlsession sql = new mysqlsession();
  usermapper mapper = sql.getmapper(usermapper.class);
  list<user> users = mapper.getusers();
  users.foreach(system.out::println);
  system.out.println("==========================");
  list<user> users1 = mapper.getuserbysexandname("女", "张三");
  users1.foreach(system.out::println);
  system.out.println("==========================");
  mapper.deleteuserbyid(1);
  system.out.println("==========================");
  mapper.updateuserbyname("五六", "女");
  system.out.println("==========================");
  mapper.insertuser(10, "男", "123123", "五七");

 }
}

4.9 测试结果

测试成功,这就是本人所手写的mybatis,虽然比较简单,但还是学习到了很多东西。

项目放在 gitee 上有需要自行下载,觉得可以还请点个star

到此这篇关于mybatis实现动态增删改查功能的示例代码的文章就介绍到这了,更多相关mybatis增删改查内容请搜索www.887551.com以前的文章或继续浏览下面的相关文章希望大家以后多多支持www.887551.com!