Ganyibo ~ write a simple version of Mybatis, and take you to have a deep understanding of its charm!

Keywords: Java SQL Mybatis JDBC

Zero. Preparations

<dependencies>
    <!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>8.0.20</version>
    </dependency>

    <!-- https://mvnrepository.com/artifact/org.mybatis/mybatis -->
    <dependency>
      <groupId>org.mybatis</groupId>
      <artifactId>mybatis</artifactId>
      <version>3.5.5</version>
    </dependency>

    <!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
    <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
      <version>1.18.12</version>
      <scope>provided</scope>
    </dependency>
</dependencies>

 

1, The complexity of JDBC

1. Overview

There are many shortcomings in the disgusting group

  • In order to execute a SQL, I need to write a mess of garbage, such as Class.forName , DriverManager.getConnection , connection.createStatement Wait, are you sick?

  • After executing SQL, we need to resultSet.getXxx(int num) to manually encapsulate into our entity object, disgusting?

  • SQL is directly and strongly coupled to business code, which makes modification and reading extremely disgusting.

2. Code

Take a look at some JDBC code.

package com.chentongwei.study.jdbc;

import com.chentongwei.study.entity.User;

import java.sql.*;
import java.util.ArrayList;
import java.util.List;

/**
 * It's disgusting!!!
 */
public class JdbcDemo {
    public static void main( String[] args ) {
        try {
            Class.forName("com.mysql.cj.jdbc.Driver");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        Connection connection = null;
        Statement statement = null;
        ResultSet resultSet = null;
        try {
            connection = DriverManager.getConnection("xxx");
            statement = connection.createStatement();
            // Only this sentence is the key, others are rubbish!!!
            // Only this sentence is the key, others are rubbish!!!
            // Only this sentence is the key, others are rubbish!!!
            resultSet = statement.executeQuery("SELECT * FROM user");
            List<User> userList = new ArrayList<>();
            while (resultSet.next()) {
                int id = resultSet.getInt(1);
                String name = resultSet.getString(2);
                int age = resultSet.getInt(3);
                userList.add(new User(id, name, age));
            }
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            if (null != resultSet) {
                try {
                    resultSet.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
            if (null != statement) {
                try {
                    statement.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
            if (null != connection) {
                try {
                    connection.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
        }

    }
}
/**
 * Description:
 * <p>
 * Project mybatis-source-study
 *
 * @author TongWei.Chen 2020-06-06 17:12:07
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
    private Integer id;
    private String name;
    private Integer age;
}

 

2, Power of Mybatis

1. Overview

He's a half ORM framework, why half? Because it supports you to directly use selectOne and other things packaged in it, and it also supports handwritten SQL. The great advantage over Hibernate is that it's easy to use and half orm. Yes, this half ORM has become one of its advantages. In this way, how can we optimize our handwritten SQL? Is it not fragrant?

Advantages of mybatis (in fact, the advantages of most ORM frameworks)

  • You write your SQL and it's over, what Class.forName When the garbage code is gone, but there will be a few additional pieces of code, but if you use spring mybatis, you can write your SQL directly. There are no other fancy things, they are all encapsulated for you.

  • No, resultSet.getXxx(int num) this disgusting code is mapped to us automatically. It can be guessed that there are components in it that encapsulate the returned ResultSet into the corresponding entity for us.

  • When SQL is written to mapper or method annotation of interface, it will not be mixed into business code.

2. Write a Mybatis

2.1 description

In order to better express the underlying principle of mybatis, a simple version of mybatis is written here to prove its core source code. Here, we only demonstrate the annotation type (such as @ Select) instead of the mapper file.

2.2 ideas

  • There has to be an interface (Mapper/DAO interface layer)

  • jdk dynamic agent generates concrete implementation for interface

  • In the specific implementation, we must obtain the SQL in the @ Select annotation

  • Then get the method parameter value

  • Parameters in SQL are all in {xxx} format, so we need to have methods to parse method parameters, such as finding the location of {and}, and then replacing this content with specific parameter values

  • Get the complete SQL (spell the parameter value)

  • Execute SQL

  • Parse result set to entity

2.3 realization

2.3.1,interface

package com.chentongwei.mybatis;

import com.chentongwei.study.entity.User;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;

import java.util.List;

/**
 * Description:
 * <p>
 * Project mybatis-source-study
 *
 * @author TongWei.Chen 2020-06-06 17:32:52
 */
public interface UserMapper {

    @Select("SELECT * FROM user WHERE id = #{id} AND name = #{name}")
    List<User> listUser(@Param("id") Integer id, @Param("name") String name);
}

 

2.3.2. jdk dynamic agent

public static void main(String[] args) {
    // jdk Dynamic agent UserMapper Interface
    UserMapper userMapper = (UserMapper) Proxy.newProxyInstance(MybatisDemo.class.getClassLoader(), new Class[]{UserMapper.class}, new InvocationHandler() {
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            // obtain@Select Notes,
            Select annotation = method.getAnnotation(Select.class);
            // Get parameters to key-value In the form of map For example map.put("id", 1); map.put("name", "test");
            Map<String, Object> argsMap = buildMethodArgsMap(method, args);
            if (null != annotation) {
                // obtain SQL: SELECT * FROM user WHERE id = #{id} AND name = #{name}
                String[] values = annotation.value();
                // 1 individual select There can only be one annotation sql,So straight-meet values[0]
                String sql = values[0];
                // sql: SELECT * FROM user WHERE id = #{id} AND name = #{name}
                System.out.println("sql: " + sql);
                // take SQL Of#Replace the {xxx} part with the real value to get the complete SQL statement
                sql = parseSQL(sql, argsMap);
                System.out.println("parseSQL: " + sql);

                // The following parts are omitted, SQL We got it. Here we go jdbc Execution, encapsulation is over.
                // jdbc implement
                // ResultSet Get result set reflected to entity The reflection has methods to get return value types and return value generics, such as List,Generics are User 
            }

            return null;
        }
    });
    userMapper.listUser(1, "test");
}

 

This method describes all processes:

1. Dynamic agent UserMapper interface

2. The agent class executes the listUser method. The parameter is 1, test

3. Get @ Select annotation on listUser method

4. Get the value on the @ Select annotation, that is, the SQL statement

5. Get the two parameter values of listUser method, 1 and test, and save them in map. The format is

 Map<String, Object> argsMap = new HashMap<>();
 argsMap.put("id", 1);
 argsMap.put("name", "test");

 

6. Replace the {xxx} part of SQL with the real value to get the complete SQL statement

SELECT * FROM user WHERE id = 1 AND name = test`

7.jdbc executes SQL

8. The resultset gets the result set and reflects it into entity

2.3.3,buildMethodArgsMap

public static Map<String, Object> buildMethodArgsMap(Method method, Object[] args) {
    // Final parameters-All the parameter values are put here
    Map<String, Object> argsMap = new HashMap<>();
    // obtain listUser All parameters of
    Parameter[] parameters = method.getParameters();
    if (parameters.length != args.length) {
        throw new RuntimeException("The number of parameters is inconsistent, brother");
    }
    // Don't ask me why, because java8 Of foreach Syntax requires that internal and external variables must final Type, final No way++Operation, so use array to play tricks
    int[] index = {0};
    Arrays.asList(parameters).forEach(parameter -> {
        // Get the@Param Annotation, the value is the parameter key
        Param paramAnno = parameter.getAnnotation(Param.class);
        // Get parameter value: id and name
        String name = paramAnno.value();
        System.out.println(name);
        // Put the parameter value to the final map Inside. id:1,name:test
        argsMap.put(name, args[index[0]]);
        index[0] ++;
    });
    return argsMap;
}

 

The ultimate goal is to return the parameter map.

  1. Get all parameters of listUser method

  2. Get the @ Param annotation value of each parameter, which is the key in the map

  3. Get the args[i] passed in as value

  4. Put key value in map

2.3.4,parseSQL

/**
 * sql: SELECT * FROM user WHERE id = #{id} AND name = #{name}
 * argsMap: 
     Map<String, Object> argsMap = new HashMap<>();
    argsMap.put("id", 1);
    argsMap.put("name", "test");
 */
public static String parseSQL(String sql, Map<String, Object> argsMap) {
    StringBuilder sqlBuilder = new StringBuilder();
    // ergodic sql Every letter of the#At the beginning, if you do#{, and then request the parseSQLArg method to fill in the parameter value (1, test)
    for (int i = 0; i < sql.length(); i++) {
        char c = sql.charAt(i);
        if (c == '#') {
            // find#The next position of the{
            int nextIndex = i + 1;
            char nextChar = sql.charAt(nextIndex);
            // If#If {is not followed by {, the syntax reports an error
            if (nextChar != '{') {
                throw new RuntimeException(
                    String.format("This is supposed to be#{nsql:%snindex:%d", sqlBuilder.toString(), nextIndex));
            }
            StringBuilder argsStringBuilder = new StringBuilder();
            // take#Replace {xxx} with specific parameter value, find the location of}, and put xxx in argsStringBuilder
            i = parseSQLArg(argsStringBuilder, sql, nextIndex);
            String argName = argsStringBuilder.toString();
            // obtain xxx Corresponding value,Fill to SQL Inside.
            Object argValue = argsMap.get(argName);
            if (null == argValue) {
                throw new RuntimeException(
                    String.format("Parameter value not found:%s", argName));
            }
            // Place parameter values in SQL Corresponding#In {xxx}
            sqlBuilder.append(argValue.toString());
            continue;
        }
        sqlBuilder.append(c);
    }

    return sqlBuilder.toString();
}

 

I mainly did the following:

Replace select * from user where id = {ID} and name = {name} with

SELECT * FROM user WHERE id = 1 AND name = test

But you need the following parseSQLArg to parse the parameters and find the location of} in {xxx}.

2.3.5,parseSQLArg

/**
 * argsStringBuilder: Put the key value, such as "id" and "name"
 * sql: SELECT * FROM user WHERE id = #{id} AND name = #{name}
 * nextIndex: The current position is the position of "{".
 */
private static int parseSQLArg(StringBuilder argsStringBuilder, String sql, int nextIndex) {
    // Why?++Once, because now nextIndex Point to{,So+1 find{Next to
    nextIndex ++;
    // One by one SQL Every letter of the"}"
    for (; nextIndex < sql.length(); nextIndex ++) {
        char c = sql.charAt(nextIndex);
        // If not},Then put argsStringBuilder Li, argsStringBuilder It's key Values, such as"id","name"
        if (c != '}') {
            argsStringBuilder.append(c);
            continue;
        }
        // If you find it}The location of represents argsStringBuilder It's complete key For example id perhaps name. because}Yes key hinder. Then return}Location of
        if (c == '}') {
            return nextIndex;
        }
    }
    // If we don't find any"}",That's obviously a syntax error, because the caller of this method has“#{"at the beginning, then you don't end"} ", and the exception is finished
    throw new RuntimeException(
        String.format("Syntax error, missing closing bracket('{')nindex:%d", nextIndex));
}

 

Find the parameter key value and put it in argsStringBuilder, find the position inextIndex of}, and return

Resolve every char letter in SQL, if not}, put it in argsStringBuilder. For example, if the current location is {, nextIndex + + is the i of i d, then append to argsStringBuilder, continue, in for, at this time, D of id, then append to argsStringBuilder, and so on. Find} and return the location.

2.3.6 complete code

package com.chentongwei.mybatis;

import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.lang.reflect.Proxy;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

/**
 * Description:
 * <p>
 * Project mybatis-source-study
 *
 * @author TongWei.Chen 2020-06-06 17:33:01
 */
public class MybatisDemo {
    public static void main(String[] args) {
        UserMapper userMapper = (UserMapper) Proxy.newProxyInstance(MybatisDemo.class.getClassLoader(), new Class[]{UserMapper.class}, new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                System.out.println("The proxy class has taken effect. Method name:" + method.getName() + ", The parameters are:" + Arrays.toString(args));
                Select annotation = method.getAnnotation(Select.class);
                Map<String, Object> argsMap = buildMethodArgsMap(method, args);
                if (null != annotation) {
                    String[] values = annotation.value();
                    // 1 individual select There can only be one annotation sql,So direct values[0]
                    String sql = values[0];
                    System.out.println("sql: " + sql);
                    sql = parseSQL(sql, argsMap);
                    System.out.println("parseSQL: " + sql);
                }

                return null;
            }
        });
        userMapper.listUser(1, "test");
    }

    public static String parseSQL(String sql, Map<String, Object> argsMap) {
        StringBuilder sqlBuilder = new StringBuilder();
        for (int i = 0; i < sql.length(); i++) {
            char c = sql.charAt(i);
            if (c == '#') {
                // find#The next position of the{
                int nextIndex = i + 1;
                char nextChar = sql.charAt(nextIndex);
                if (nextChar != '{') {
                    throw new RuntimeException(
                            String.format("This is supposed to be#{nsql:%snindex:%d", sqlBuilder.toString(), nextIndex));
                }
                StringBuilder argsStringBuilder = new StringBuilder();
                i = parseSQLArg(argsStringBuilder, sql, nextIndex);
                String argName = argsStringBuilder.toString();
                Object argValue = argsMap.get(argName);
                if (null == argValue) {
                    throw new RuntimeException(
                            String.format("Parameter value not found:%s", argName));
                }
                sqlBuilder.append(argValue.toString());
                continue;
            }
            sqlBuilder.append(c);
        }

        return sqlBuilder.toString();
    }

    private static int parseSQLArg(StringBuilder argsStringBuilder, String sql, int nextIndex) {
        // Why?++Once, because now nextIndex Point to{,So+1 find{Next to
        nextIndex ++;
        for (; nextIndex < sql.length(); nextIndex ++) {
            char c = sql.charAt(nextIndex);
            if (c != '}') {
                argsStringBuilder.append(c);
                continue;
            }
            if (c == '}') {
                return nextIndex;
            }
        }
        throw new RuntimeException(
                String.format("Syntax error, missing closing bracket('{')nindex:%d", nextIndex));
    }

    public static Map<String, Object> buildMethodArgsMap(Method method, Object[] args) {
        Map<String, Object> argsMap = new HashMap<>();
        Parameter[] parameters = method.getParameters();
        if (parameters.length != args.length) {
            throw new RuntimeException("The number of parameters is inconsistent, brother");
        }
        int[] index = {0};
        Arrays.asList(parameters).forEach(parameter -> {
            Param paramAnno = parameter.getAnnotation(Param.class);
            String name = paramAnno.value();
            System.out.println(name);
            argsMap.put(name, args[index[0]]);
            index[0] ++;
        });
        return argsMap;
    }
}

 

2.3.7 test

The test results of the above complete code are as follows:

The proxy class has taken effect. Method name: listUser, The parameters are:[1, test]
id
name
sql: SELECT * FROM user WHERE id = #{id} AND name = #{name}
parseSQL: SELECT * FROM user WHERE id = 1 AND name = test

 

It's obvious that we have got the desired SQL perfectly. Next, jdbc, parsing the ResultSet is finished. It's not involved here.

We deliberately write wrong SQL, remove {after {, and then see the effect

Modify the listUser method of the UserMapper interface as follows

public interface UserMapper {
    @Select("SELECT * FROM user WHERE id = #id} AND name = #{name}")
    List<User> listUser(@Param("id") Integer id, @Param("name") String name);
}

 

The output is directly wrong

Exception in thread "main" java.lang.RuntimeException: This is supposed to be#{
sql:SELECT * FROM user WHERE id = 
index:31
    at com.chentongwei.mybatis.MybatisDemo.parseSQL(MybatisDemo.java:54)
    at com.chentongwei.mybatis.MybatisDemo$1.invoke(MybatisDemo.java:34)
    at com.sun.proxy.$Proxy0.listUser(Unknown Source)
    at com.chentongwei.mybatis.MybatisDemo.main(MybatisDemo.java:41)

 

Write the wrong SQL again. The parameter names in @ Param are not consistent with those in SQL. See the effect:

public interface UserMapper {

    @Select("SELECT * FROM user WHERE id = #{id} AND name = #{name}")
    List<User> listUser(@Param("id") Integer id, @Param("name1") String name);
}
Exception in thread "main" java.lang.RuntimeException: Parameter value not found:name
    at com.chentongwei.mybatis.MybatisDemo.parseSQL(MybatisDemo.java:62)
    at com.chentongwei.mybatis.MybatisDemo$1.invoke(MybatisDemo.java:34)
    at com.sun.proxy.$Proxy0.listUser(Unknown Source)
    at com.chentongwei.mybatis.MybatisDemo.main(MybatisDemo.java:41)S

 

3. Summary

  • The underlying source code of mybatis is much more optimized than this. Various parsing components are not for splicing each SQL character

  • In fact, the underlying layer of mybatis has its own encapsulated exception instead of a direct RuntimeException

  • This is just to demonstrate the principle, so it does not involve JDBC execution, mapping ResultSet to entity, etc

3, Some pictures

In fact, the source code of mybatis is well written, and each component is well packaged and clear. The function of daiyou interceptor makes it pluggable.

Here is a more detailed core component diagram of mybatis

mybatis source package

Posted by coco777 on Thu, 11 Jun 2020 20:04:13 -0700