zoukankan      html  css  js  c++  java
  • Mybatis实现原理探究-实现部分Mybatis功能(上)

    一、前言:
      MyBatis 是一款优秀的持久层框架,它支持定制化 SQL、存储过程以及高级映射。MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。MyBatis 可以使用简单的 XML 或注解来配置和映射原生信息,将接口和 Java 的 POJOs(Plain Old Java Objects,普通的 Java对象)映射成数据库中的记录
      通过Mybatis框架,可以将一个URL指向某段SQL代码,在Java代码里只需指定这个URL即可,然后如果代码进行了修改涉及到SQL的变更则可以在Mapper文件里统一查找并动态的修改。
      为了能够更好的理解Mybatis的实现原理,个人认为最好的办法就是对其各个类型的功能都实现一遍,这个实现可以不涉及各种优化、异常处理和设计模式的应用等;通过实现这些功能一来是能够最直接的理解它的原理,二来也是为自己将来能够组合成一系列产品打下基础。
      所有的框架的实现都离不开反射,不清楚怎么用反射的一定要多写一些demo去积累。
    二、实现的Mybatis的功能:
      在这个demo里只是先实现一个session.selectOne(statement, params)的功能(对于其他如delete、insert原理差不多),至于对数据库连接池的集成(如Druid)和其它的一些高级功能(比如各种缓存、优化、解耦等等)等有机会也是要实现一番方能有更深的理解。
    三、准备工作:
    1.MySQL里建立student表,里面有字段uid:bigint、name:varchar、no:varchar、gender:varchar;并新增几条不同记录。
    2.创建StudentMapper.xml文件,其内容为:

    <?xml version="1.0" encoding="utf-8"?>
    <mapper namespace="me.silentdoer.simulatemybatis.mapping.StudentMapper">
    <select id="getSingleStudent" parameterType="java.lang.Long"
    resultType="me.silentdoer.simulatemybatis.pojo.Student">
    select no, name from student where uid=#{?}
    </select>
    <!-- 其中parameterType可选 -->
    </mapper>

    3.在对应位置新建Student类(返回类型也可以用Map来代替具体的pojo类/一条记录,其中key是列名,值是具体值):

    package me.silentdoer.simulatemybatis.pojo;
    
    /**
    * @Author Silentdoer
    * @Since 1.0
    * @Version 1.0
    * @Date 2018-2-19 19:16
    */
    public class Student {
    private Long uid;
    private String no;
    private String name;
    private String gender;

    public Student(){
    this.uid = -1L;
    }
    public Long getUid() { return uid; } public void setUid(Long uid) { this.uid = uid; } public String getNo() { return no; } public void setNo(String no) { this.no = no; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getGender() { return gender; } public void setGender(String gender) { this.gender = gender; } @Override public String toString(){ return String.format("Student [uid=%s, no=%s, name=%s, gender=%s]", this.uid, this.no, this.name, this.gender); } }

    四、代码实现:
    1.新建类MySession:

    package me.silentdoer.simulatemybatis.core;
    
    import org.dom4j.Attribute;
    import org.dom4j.Document;
    import org.dom4j.DocumentException;
    import org.dom4j.Element;
    import org.dom4j.io.SAXReader;
    
    import java.lang.reflect.InvocationTargetException;
    import java.lang.reflect.Method;
    import java.net.URL;
    import java.sql.*;
    import java.util.Arrays;
    import java.util.HashSet;
    import java.util.List;
    import java.util.Set;
    import java.util.stream.Stream;
    
    /**
    * @Author Silentdoer
    * @Since 1.0
    * @Version 1.0
    * @Date 2018-2-19 18:47
    */
    public class MySession {
    private Set<String> opsSet;
    
    public void init(){
    opsSet = new HashSet<>(4);
    opsSet.addAll(Arrays.asList("select", "insert", "update", "delete"));
    }
    
    // me.silentdoer.simulatemybatis.mapping.StudentMapper.getSingleStudent, 2L
    public <T> T selectOne(String statement, Object ... params) throws DocumentException, NoSuchMethodException, ClassNotFoundException, SQLException, IllegalAccessException, InstantiationException, InvocationTargetException {
    Object result = null;
    //region 加载Mapper文件,提取SQL语句
    SAXReader saxReader = new SAXReader();
    // 这个Mapper文件应该由一个专门的解析器去解析并缓存起来,这里只是为了了解原理故不做这些优化。
    URL mapperConfig = Thread.currentThread().getContextClassLoader().getResource("me/silentdoer/simulatemybatis/mapping/StudentMapper.xml");
    //System.out.println(mapperConfig == null);
    Document doc = saxReader.read(mapperConfig);
    Element mapper = doc.getRootElement();
    /**获取Mapper的名称空间,如me.silentdoer.simulatemybatis.mapping.StudentMapper*/
    Attribute mapperNs = mapper.attribute("namespace");
    if(!statement.contains(mapperNs.getValue())){
    throw new NoSuchMethodException("没有此Mapper文件");
    }
    String part = statement.substring(statement.lastIndexOf(".") + 1); // 具体的方法如getSingleStudent
    List<Element> selects = mapper.elements("select");
    //Element ele = mapper.elementByID(part); // 本质上也是通过attribute实现的,但是这个ID似乎只能大写的(看了下源码应该是的)
    Element partEle = null;
    for(Element ele : selects){
    String id = ele.attributeValue("id");
    if(id.equals(part)){
    partEle = ele;
    break;
    }
    }
    if(partEle == null){
    throw new IllegalArgumentException("Mapper里没有提供对应的方法");
    }
    // java.lang.Long
    String paramType = partEle.attributeValue("parameterType");
    // me.silentdoer.simulatemybatis.pojo.Student
    String resultType = partEle.attributeValue("resultType"); // resultMap只是再做了一个映射,自己使用Mybatis最好为每个pojo都做resultMap的映射
    // select name, gender from student where uid=#{?}
    String sql = partEle.getTextTrim();
    // 判断sql里是否包含参数
    if(sql.contains("#")){ // $这种暂且不考虑
    //select * from student where uid=?
    sql = sql.replaceAll("#\{.+?}", "?");
    }
    /** 开始真正去操作数据库了 */
    //ClassLoader.getSystemClassLoader().loadClass("com.mysql.jdbc.Driver");
    Class.forName("com.mysql.jdbc.Driver"); // 注册驱动,为了能正确解析MySQL服务传来的数据和正确发送数据给MySQL服务
    // Druid的initialSize可以通过这个方法初始化一个连接并将其存入连接池,隔一定空闲时间要发一些心跳数据给MySQL服务器防止被关闭
    Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/db_test", "root", "12345678");
    PreparedStatement preparedStatement = conn.prepareStatement(sql);
    preparedStatement.setObject(1, Class.forName(paramType).cast(params[0]));
    ResultSet resultSet = preparedStatement.executeQuery();
    ResultSetMetaData metaData = resultSet.getMetaData();
    int colNum = metaData.getColumnCount();
    result = Class.forName(resultType).newInstance();
    // 这里先不做结果集是否只有一条记录的判断
    resultSet.next();
    for(int i=1;i<=colNum;i++){
    // 假设都是小写字母
    final String name = metaData.getColumnName(i); // such as uid
    Object nwVal = resultSet.getObject(i); // 要考虑到这个值可能是null,且比如boolean在数据库里设置为tinyint这里需要自己强制转换一下
    Method[] methods = result.getClass().getMethods();
    Stream<Method> methodsStream = Arrays.asList(methods).stream();
    Method setter = methodsStream.filter((e) -> e.getName().equals("set".concat(name.substring(0, 1).toUpperCase().concat(name.substring(1))))).findFirst().get();
    //System.out.println(setter.getName().concat("#").concat(nwVal + ""));
    setter.invoke(result, nwVal);
    }
    //endregion
    return (T)result;
    }
    }

    2.在main方法里调用:

    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InstantiationException, SQLException, DocumentException, InvocationTargetException, ClassNotFoundException {
    MySession session = new MySession();
    Student result = session.selectOne("me.silentdoer.simulatemybatis.mapping.StudentMapper.getSingleStudent", 2L);
    System.out.println(result);
    }

    最终输出结果为:
    Student [uid=-1, no=Stu10002, name=Silentdoer2, gender=male],可见我们只select no和name则只是在Student的默认对象的基础上再修改了这两个属性的值。
    对于session的其它方法,如insert、delete等内部原理都差不多,都是通过PreparedStatement来最终实现操作数据库的;如果参数是${?}的话则是通过拼串的形式实现该参数的传入,这种方式容易产生安全漏洞(有SQL注入的风险)。

  • 相关阅读:
    如何改变常用编辑器(eclipse)的字号大小
    IOConsole Updater error
    RNAfold的使用方法
    单因素方差分析(oneway ANOVA)
    Perl内部保留变量(系统变量)
    Perl 中的正则表达式
    Perl Eclipse 格式化代码
    卸载Oracle 9i
    Ubuntu的菜鸟常用的基础命令
    linux as4u2 下安装openssh
  • 原文地址:https://www.cnblogs.com/silentdoer/p/8413853.html
Copyright © 2011-2022 走看看