Mybatis imitates 1. Let me have a look first

Time:2021-10-19

Mybatis has been used for a long time, but it has always been used. I think it can’t be used. I have to go out of the comfort zone.
So I want to see the implementation of mybatis myself, and then write one by imitation. Ha ha, of course, I don’t require a high degree of completion at the beginning.
Let’s take a look at the mysteries of mybatis.

The source code version of mybatis referred to here is 3.4.5.

First, write a simple example of mybatis.
Mybatis imitates 1. Let me have a look first

//Use
public static void main(String[] args) throws IOException {
    
    //Create a sqlsessionfactory object based on the configuration file
    String resource = "mybatis-config.xml";
    InputStream inputStream = Resources.getResourceAsStream(resource);
    SqlSessionFactory sqlSessionFactory 
        = new SqlSessionFactoryBuilder().build(inputStream);
    
    //Get sqlsession object
    SqlSession session = sqlSessionFactory.openSession();
    
    try{
        
        //Gets the implementation class instance of the interface
        IUserMapper mapper = session.getMapper(IUserMapper.class);
        
        //Call method
        User user = mapper.findById(1);
        System.out.println(user.getName());
        
    }finally{
        session.close();
    }
}

Recall that the step to using mybatis is

  1. Write the configuration file, configure the parameters for connecting to the database, and the parameters for mybatis.
  2. Define the interface and use it in the form of annotations or XML filesProvide SQL statements。 Then register this interface in the configuration file.
  3. Create sqlsessionfactory and pass in the configuration file. Obtain the sqlsession object through the factory.
  4. Get the instance of the custom interface through the sqlsession object, and then call the method of the interface.

In the whole process, players only participateconfiguration parameter, andProvide SQLThese two steps. So these two steps are the entrance to see how mybatis operates, which is the gate to enter the underground city of mybatis.
This operation is common in the configuration parameters section when using the framework. Therefore, it is a branch plot, and providing SQL is the main plot of mybatis. Here, first pass the main plot.


What happened in scenario 1

IUserMapper mapper = session.getMapper(IUserMapper.class);
User user = mapper.findById(1);

We can see that when using, we get an implementation class instance of our interface,
Burning goose, we didn’t write the implementation of this interface. So I think it’s magic. I want to make a breakpoint here.

  1. On the breakpoint of getmapper method, we enter defaultsqlsession. Getmapper (class < T >),
    So by default, we get an instance of defaultsqlsession from sqlsessionfactory.

    /*
    Pass in our interface type and sqlsession instance through getmapper method of configuration, and return
    Returns a generic type. Here is an instance of the implementation class of our iusermapper interface*/
    @Override
    public <T> T getMapper(Class<T> type) {
        return configuration.<T>getMapper(type, this);
    }
  2. Next in is configuration. Getmapper (class < T >, sqlsession).

    /*
    Here we get the object from mapperregistry,
    Mapperregistry is a property of the configuration class*/
    public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
        return mapperRegistry.getMapper(type, sqlSession);
    }
  3. Take a look at what is inside the getmapper of mapperregistry.
    I see an exciting word here, proxy,
    Guess that the iusermapper instance we finally get is a proxy object

    @SuppressWarnings("unchecked")
    public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
        //Look, knownmappers is a map object, map < class <? >, MapperProxyFactory<?>>
        final MapperProxyFactory<T> mapperProxyFactory 
            = (MapperProxyFactory<T>) knownMappers.get(type);
        
        if (mapperProxyFactory == null) {
            throw new BindingException("Type " + type + 
                " is not known to the MapperRegistry.");
        }
    
        try {
            /*To create a new instance, you need to go in and have a look*/
            return mapperProxyFactory.newInstance(sqlSession);
        } catch (Exception e) {
            throw new BindingException("Error getting mapper instance. Cause: " + e, e);
        }
    }
  4. Explore the newinstance of mapperproxyfactory
    In fact, the one named xxfactory must produce XX. It can be guessed that the returned one is mapperproxy

    public T newInstance(SqlSession sqlSession) {
        / * here new a MapperProxy, then call newInstance*/.
        final MapperProxy<T> mapperProxy 
            = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
        return newInstance(mapperProxy);
    }
  5. What is mapperproxy

    /*This class implements invocationhandler, the interface of dynamic agent*/
    public class MapperProxy<T> implements InvocationHandler, Serializable
  6. See what newinstance (mapperproxy) does.
    Use proxy to construct an instance of the proxy class that implements our iusermapper interface!

    @SuppressWarnings("unchecked")
    protected T newInstance(MapperProxy<T> mapperProxy) {
        return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
    }         
  7. After digestion, the initial question is that we do not provide the implementation of iusermapper, but we can get an object of the implementation class of iusermapper through the getmapper method of sqlsession.
    The answer is that we finally returned an instance of a proxy class of our interface.
    Mapperproxy implements the invocationhandler interface, and the mapperproxy object is passed in when we construct the proxy object,
    Therefore, when calling all methods of iusermapper, you will enter the invoke method of mapperproxy class.
    In fact, it’s not like the operation above. You can also see it by printing this object directly

    System.out.println(mapper);
    System.out.println(Proxy.isProxyClass(mapper.getClass()));
    //Printing results and pasting pictures are too ugly, so we don't paste result pictures.
    [email protected]
    true

Scenario 2 mapperproxy what did you do

Dynamic proxy is generally used. The classes that implement the invocationhandler interface will hold the reference of the proxy class, which is mapperproxy here. Then perform additional operations in the invoke method, and then call the method of the proxy class. No reference to the proxy class was found in the mapperproxy class.

public class MapperProxy<T> implements InvocationHandler, Serializable {
    private static final long serialVersionUID = -6424540398559729838L;
    private final SqlSession sqlSession;
    private final Class<T> mapperInterface;
    private final Map<Method, MapperMethod> methodCache;
    。。。
}

So what did the pangolin say?
So what happens when we call findbyid of iusermapper?
Here we will look at the invoke method of mapperproxy.

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
        if (Object.class.equals(method.getDeclaringClass())) {
            return method.invoke(this, args);
        } else if (isDefaultMethod(method)) {
            return invokeDefaultMethod(proxy, method, args);
        }
    } catch (Throwable t) {
        throw ExceptionUtil.unwrapThrowable(t);
    }
    final MapperMethod mapperMethod = cachedMapperMethod(method);
    return mapperMethod.execute(sqlSession, args);
}
  1. First, make an if judgment. The logic is that if the provider class of the called method is the object class, the method will be executed directly.
    It’s easy to think about which class is not a subclass of object
    In fact, if it is a method in object, it should be executed directly.
    What are the methods of object? ToString these. When you call mapper. Tostring(), it will be executed directly without following the logic below.

    if (Object.class.equals(method.getDeclaringClass())) {
        return method.invoke(this, args);
    }    
    
  2. This is followed by the second if. The logic is that if the permission modifier of this method is public and provided by the interface, the invokedefaultmethod method method will be executed.
    For example, if a default method is written in iusermapper, isdefaultmethod will return true when this method is executed.
    Here, the provider of our method is a proxy class, not an interface, so false is returned.

    else if (isDefaultMethod(method)) {
        return invokeDefaultMethod(proxy, method, args);
    }
    
    // isDefaultMethod
    private boolean isDefaultMethod(Method method) {
    return 
        ((method.getModifiers() & (Modifier.ABSTRACT | Modifier.PUBLIC | Modifier.STATIC)) 
            == Modifier.PUBLIC)
            && 
        method.getDeclaringClass().isInterface();
    }
    
  3. The first two steps are filtering, and the next is the key.
    You can see that a mappermethod object is obtained through the cachedmappermethod method.
    Look, the name is taken from the cache. The execute method of mappermethod is then executed.

    final MapperMethod mapperMethod = cachedMapperMethod(method);
    return mapperMethod.execute(sqlSession, args);
    
  4. Take it easy and make a summary. The initial question is that the mapperproxy class is not referenced by the proxy class object.
    Then what does it want to do. In the invoke method, we find the answer.
    A mappermethod object is obtained through the method parameter of the invoke method,
    Then the execute method of the object is executed, and it’s gone. In the middle, some conventional methods are implemented directly.
    Therefore, it is purely to enter the invoke method and get the mappermethod. There is no proxy class from beginning to end.
    Wow, the magic usage of agent, little Ben, remember.
  5. Next, let’s see how to get mappermethod through the method parameter
    It’s very simple here. If there is one in the map, you can return directly, and if not, you can create a new one. A method of the interface corresponds to a mappermethod.
    so easy ~

    //cachedMapperMethod
    private MapperMethod cachedMapperMethod(Method method) {
        //Methodcache is a map < method, mappermethod >
        MapperMethod mapperMethod = methodCache.get(method);
        if (mapperMethod == null) {
            mapperMethod 
                = new MapperMethod(mapperInterface, method, sqlSession.getConfiguration());
            methodCache.put(method, mapperMethod);
        }
        return mapperMethod;
    }
  6. Look at the construction process of mappermethod and find that the interface information, method information and configuration information are passed in.
    The main work is to initialize the command and method fields.
    The command contains the name of the saved method (COM. Mapper. Iusermapper. Findbyid) and the corresponding SQL type (select).
    Method stores the return type of the method, whether it is a collection, whether it is a cursor and other information.
    See here, in fact, I’ve been ignoring what’s in the configuration class. I’ll look at it after imitation.

    public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
        this.command = new SqlCommand(config, mapperInterface, method);
        this.method = new MethodSignature(config, mapperInterface, method);
    }

Scenario 3 mappermethod what did you do with it

We call mapper.findbyid, and finally get the result by executing mappermethod.
So let’s look at the secrets hidden in the execute method.

  1. Below is the content of the execute method

    public Object execute(SqlSession sqlSession, Object[] args) {
    
        Object result;
        switch (command.getType()) {
            case INSERT: {
                Object param = method.convertArgsToSqlCommandParam(args);
                result = rowCountResult(sqlSession.insert(command.getName(), param));
                break;
            }
            case UPDATE: {
                Object param = method.convertArgsToSqlCommandParam(args);
                result = rowCountResult(sqlSession.update(command.getName(), param));
                break;
            }
            case DELETE: {
                Object param = method.convertArgsToSqlCommandParam(args);
                result = rowCountResult(sqlSession.delete(command.getName(), param));
                break;
            }
            case SELECT:
                if (method.returnsVoid() && method.hasResultHandler()) {
                    executeWithResultHandler(sqlSession, args);
                    result = null;
                } else if (method.returnsMany()) {
                    result = executeForMany(sqlSession, args);
                } else if (method.returnsMap()) {
                    result = executeForMap(sqlSession, args);
                } else if (method.returnsCursor()) {
                    result = executeForCursor(sqlSession, args);
                } else {
                    Object param = method.convertArgsToSqlCommandParam(args);
                    result = sqlSession.selectOne(command.getName(), param);
                }
                break;
            case FLUSH:
                result = sqlSession.flushStatements();
                break;
            default:
                throw new BindingException("Unknown execution method for: " + command.getName());
        }
        
        if (result == null 
                && method.getReturnType().isPrimitive() 
                && !method.returnsVoid()) {   
            throw new BindingException("Mapper method '" + command.getName() + " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
        }
        return result;
    }
  2. You can see the familiar words insert update.
    Enter different branches through the command attribute in mappermethod.
    Findbyid is called here. It enters the select branch and finally executes the following statements. The first sentence is assembly parameters and the second sentence is query execution.

    Object param = method.convertArgsToSqlCommandParam(args);
    result = sqlSession.selectOne(command.getName(), param);
  3. Look at sqlsession. Selectone (), which calls the selectlist method, and then returns the result.

    @Override
    public <T> T selectOne(String statement, Object parameter) {
        // Popular vote was to return null on 0 results and throw exception on too many.
        List<T> list = this.<T>selectList(statement, parameter);
        if (list.size() == 1) {
            return list.get(0);
        } else if (list.size() > 1) {
           throw new TooManyResultsException("Expected one result (or null) to be returned by selectOne(), but found: " + list.size());
        } else {
            return null;
        }
    }
  4. Go to the selectlist and have a look. I feel that the process is going to be finished and the selection has started.
    Here I see another exciting word, statement. I feel close to JDBC.
    There is a mappedstatement object that needs attention.

    @Override
    public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
        try {
            MappedStatement ms = configuration.getMappedStatement(statement);
            return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
        } catch (Exception e) {
            throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
        } finally {
            ErrorContext.instance().reset();
        }
    }
  5. Recall how basic JDBC is used.

    /*
      1. Load the corresponding database driver
      Class.forName(driverClass);   
      
      2 get database connection
      Connection con = DriverManager.getConnection(jdbcUrl, user, password);
      
      3 prepare SQL statements
      String sql = " ... ";
      
      4 perform operations
      Statement statement = con.createStatement();
      statement.executeUpdate(sql);
      
      5 release resources
      statement.close();
      con.close();*/
  6. What is a mappedstatement? It corresponds to one of our SQL statements.
    It is obtained from the configuration through the name attribute of the command object of mappermethod.
  7. After you get the MappedStatement, you call the query method of executor, which is provided by CachingExecutor.
    As you can see, here we get our SQL through mappedstatement, and then generate a cache key, which reminds me of mybatis level 1 and level 2 cache in my memory.
    Then return the result of calling the query method.

    @Override
    public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
        BoundSql boundSql = ms.getBoundSql(parameterObject);
        CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
        return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
    }
  8. Or enter the query method of cachengexecution. It seems that a class such as executor is a class that really performs database operations.
    First, get the cache from the mappedstatement. If it is empty, call delegate.query,
    Delegate is a simpleexecution type. As the name suggests, cacheingexecution delegates simpleexecution to perform database operations.

    @Override
    public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
     throws SQLException {
        Cache cache = ms.getCache();
        if (cache != null) {
            flushCacheIfRequired(ms);
            if (ms.isUseCache() && resultHandler == null) {
                ensureNoOutParams(ms, parameterObject, boundSql);
                @SuppressWarnings("unchecked")
                List<E> list = (List<E>) tcm.getObject(cache, key);
                if (list == null) {
                    list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
                    tcm.putObject(cache, key, list); // issue #578 and #116
                }
                return list;
            }
        }
        return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
    }
  9. Continue to look at the query in simpleexecution. You can see that there is no query method in simpleexecution,
    Instead, simpleexecution inherits baseexecutor, and query is provided by baseexecutor class.
    After the breakpoint of the first sentence goes in, you will see the stored “executing a query”, which is the stack information when an exception occurs.
    EMM.. then there are a lot of code whether there is cache or whether to use cache.
    Let’s directly look at queryfromdatabase (). The name obviously tells players that the boss is in front.

    @SuppressWarnings("unchecked")
    @Override
    public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
        ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
        if (closed) {
            throw new ExecutorException("Executor was closed.");
        }
        if (queryStack == 0 && ms.isFlushCacheRequired()) {
            clearLocalCache();
        }
        List<E> list;
        try {
            queryStack++;
            list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
            if (list != null) {
                handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
            } else {
                list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
            }
        } finally {
            queryStack--;
        }
        if (queryStack == 0) {
            for (DeferredLoad deferredLoad : deferredLoads) {
                deferredLoad.load();
            }
            // issue #601
            deferredLoads.clear();
            if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
                // issue #482
                clearLocalCache();
            }
        }
        return list;
    }
  10. It is also the queryfromdatabase () method provided by baseexecutor.
    First, put in a cache. The key is our previous cache key. The value is a default value. It feels like it means occupying space.
    Then execute the doquery method and see the method starting with do. You know it’s not simple. doGet doPost
    Doquery is an abstract method. We have to go to simpleexecution to see the implementation.

    private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
        List<E> list;
        localCache.putObject(key, EXECUTION_PLACEHOLDER);
        try {
            list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
        } finally {
            localCache.removeObject(key);
        }
        localCache.putObject(key, list);
        if (ms.getStatementType() == StatementType.CALLABLE) {
            localOutputParameterCache.putObject(key, parameter);
        }
        return list;
    }
  11. SimpleExecutor.doQuery
    Come on! Statement! And we are familiar with the word preparestatement. Ha ha, it’s all JDBC
    Finally, you can see that it is executed by a handler. Take a look at this handler.

    @Override
    public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
        Statement stmt = null;
        try {
            Configuration configuration = ms.getConfiguration();
            StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
            stmt = prepareStatement(handler, ms.getStatementLog());
            return handler.<E>query(stmt, resultHandler);
        } finally {
            closeStatement(stmt);
        }
    }
  12. First, it enters the routingstatementhandler, and then the routingstatementhandler is delegated to the preparedstatementhandler. Next is the query of the preparedstatementhandler.
    See what you want to see, PS. execute()
    After that, the result is handed over to resultsethandler for processing.

    @Override
    public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
        PreparedStatement ps = (PreparedStatement) statement;
        ps.execute();
        return resultSetHandler.<E> handleResultSets(ps);
    }
  13. In retrospect, our question is what the execute method of mappermethod object does. The conclusion is,
    We know the type of SQL to be executed through the command attribute and method attribute of mappermethod,
    Here we take the select route. After knowing the type, the sqlsession executes the selectone method.
    Then the selectlist method of defaultsqlsession is called. Defaultsqlsession indicates that you don’t want to work,
    It was handed over to the industrious baseexecutor. There is a query method in the baseexecutor. The query method does some general operations,
    Take a look at the cache. When there is no cache or no cache, call the doquery method. The doquery method has different implementations.
    The familiar JDBC is used in doquery and its methods to be called. After performing the operation, give the result to resultsethandler.

summary

  • What do we use?

A: an instance of the proxy class of our interface is used.
When constructing an instance of a proxy class,
We passed in a mapperproxy instance that implements the invocationhandler interface,
When the proxy object calls the method, it will enter the invoke method of mapperproxy.
Find mappermethod through the method object in the invoke method,
Then execute the execute method of the mappermethod object.
Here, the role of the proxy is to let us know which method of which interface is used.

Mapperproxy corresponds to one of our interfaces,
Mappermethod corresponds to a method in the interface,
Mappedstatement corresponds to an SQL

  • Summarize the responsibilities of each category from the above process

MapperProxy:Defines the action to be performed when a proxy object invokes a method.
That is to get the MapperMethod corresponding to the calling method in invoke () and then call the execute of MapperMethod.

MapperMethod:Corresponding to the methods in our interface, hold SqlCommand (command) and methodsignature (method),
You can know the full name of the method and the corresponding SQL type.

MappedStatement:Saved SQL information.

SqlSession:Where players get mapper. Pretended to execute SQL and actually handed it over to the executor.

Executor:Actually perform database operations.

Know roughly what the process is like, and then you can imitate and write
EMM… It doesn’t feel so simple.