Mybatis: detailed explanation of binding module

Posted by badapple on Sat, 20 Nov 2021 12:34:25 +0100

Mybatis (V): detailed explanation of binding module



This is learning today Mybatis's fifth article, let's learn in today's article binding module. This module is used to Mapper binds to its mapping file.

This module is located in the org.apache.ibatis.binding package. The core classes are as follows

  • MapperRegistry mapper registrar
  • MapperProxyFactory mapper proxy class factory
  • MapperProxy mapper proxy class
  • MapperMethod mapper method class

In today's article, we will introduce the above four classes.

1 MapperRegistry

MapperRegistry is the Mapper registrar. This class is used to record the mapping relationship between Mapper class and MapperProxyFactory. The attribute fields of this class are as follows:

// The Configuration information of the Configuration object mybatis will be parsed and stored in the object
private final Configuration config;
// Record the correspondence between Mapper interface and MapperProxyFactory
private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();

The Configuration object here is a core class in Mybatis, and relevant configurations in Mybatis will be recorded in this object after parsing. We will introduce this class in detail in the following article. There is no explanation here.

In this class, there are two core methods, addMapper and getMapper, which are used to register and obtain mappers respectively. Next, let's take a look at the logic of these two methods.

1.1 addMapper() method

The addMapper method is used to register the Mapper and add it to knownMappers. If you are still impressed with the content of mybatis (II): execution process, we mentioned this method in the analysis of Mapper mapping file in that article. It doesn't matter if you forget. Let's learn more today. The source code of this method is as follows:

public <T> void addMapper(Class<T> type) {
    // type is the interface
    if (type.isInterface()) {
        // Determine whether it has been loaded
        if (hasMapper(type)) {
            throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
        }
        // Tags loaded successfully
        boolean loadCompleted = false;
        try {
            // Add to knowMappers
            knownMappers.put(type, new MapperProxyFactory<>(type));
            // Leave it to the later builder module for introduction
            MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
            parser.parse();
            loadCompleted = true;
        } finally {
            // If an exception occurs, you need to remove the mapper
            if (!loadCompleted) {
                knownMappers.remove(type);
            }
        }
    }
}

In this class, in addition to the registration method of a single Mapper, it also provides a method to register according to the package. The logic is as follows:

public void addMappers(String packageName) {
    addMappers(packageName, Object.class);
}

public void addMappers(String packageName, Class<?> superType) {
    ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<>();
    // Find a class whose parent class is superType through reflection
    resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
    Set<Class<? extends Class<?>>> mapperSet = resolverUtil.getClasses();
    // Traverse to register
    for (Class<?> mapperClass : mapperSet) {
        addMapper(mapperClass);
    }
}

1.2 getMapper() method

In our previous example code, when performing data operations, we will first call SqlSession.getMapper method to obtain Mapper object. The logic of this method is to call MapperRegistry.getMapper method. The logic of this method is as follows:

public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    // Finds the MapperProxyFactory object corresponding to the specified type
    final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
    // No exception thrown
    if (mapperProxyFactory == null) {
        throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
    }
    try {
        // Here, the dynamic proxy of JDK is used to generate Mapper's proxy object MapperProxy
        return mapperProxyFactory.newInstance(sqlSession);
    } catch (Exception e) {
        throw new BindingException("Error getting mapper instance. Cause: " + e, e);
    }
}

2 MapperProxyFactory

MapperProxyFactory is the object factory of mapper proxy class. It should be easy to think of the factory pattern when you see this name. The main function of this class is to create mapper proxy objects. The properties of this class are as follows:

// Mapper interface Class object to which the proxy object belongs
private final Class<T> mapperInterface;
// Stores the relationship between methods in Mapper and MapperMethodInvoker
private final Map<Method, MapperMethodInvoker> methodCache = new ConcurrentHashMap<>();

The method to create the mapper proxy object in this class is as follows:

public T newInstance(SqlSession sqlSession) {
    // Create MapperProxy object
    final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
    return newInstance(mapperProxy);
}

protected T newInstance(MapperProxy<T> mapperProxy) {
    // Create a proxy object that implements the mapperInterface interface
    return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}

Through the above code, we find that getMapper obtains a proxy object of Mapper interface generated through JDK dynamic proxy. This is why we only need to define the Mapper interface when using Mybatis without defining its implementation.

3 MapperProxy

MapperProxy is a mapper proxy class that implements the InvocationHandler interface. In JDK dynamic proxy, the proxy class needs to implement this interface and implement the invoke method.

The attribute fields of this class are as follows:

// SqlSession object
private final SqlSession sqlSession;
// Class object of Mapper interface
private final Class<T> mapperInterface;
// Correspondence between methods in Mapper interface and MapperMethodInvoker
private final Map<Method, MapperMethodInvoker> methodCache;

MapperMethodInvoker is an internal interface in MapperProxy. It has two implementation classes. The source code is as follows:

interface MapperMethodInvoker {
    Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable;
}

private static class PlainMethodInvoker implements MapperMethodInvoker {
    private final MapperMethod mapperMethod;

    public PlainMethodInvoker(MapperMethod mapperMethod) {
        super();
        this.mapperMethod = mapperMethod;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
        // Execute the execute method in MapperMethod
        return mapperMethod.execute(sqlSession, args);
    }
}

private static class DefaultMethodInvoker implements MapperMethodInvoker {
    private final MethodHandle methodHandle;

    public DefaultMethodInvoker(MethodHandle methodHandle) {
        super();
        this.methodHandle = methodHandle;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
        return methodHandle.bindTo(proxy).invokeWithArguments(args);
    }
}

When we call the method in Mapper, we will call MapperProxy.invoke method. The logic of this method is as follows:

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
        // If the target method inherits from the object, it is executed directly
        if (Object.class.equals(method.getDeclaringClass())) {
            return method.invoke(this, args);
        } else {
            return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
        }
    } catch (Throwable t) {
        throw ExceptionUtil.unwrapThrowable(t);
    }
}

private MapperMethodInvoker cachedInvoker(Method method) throws Throwable {
    try {
        return MapUtil.computeIfAbsent(methodCache, method, m -> {
            // Determine whether it is the default method of the interface
            if (m.isDefault()) {
                try {
                    if (privateLookupInMethod == null) {
                        return new DefaultMethodInvoker(getMethodHandleJava8(method));
                    } else {
                        return new DefaultMethodInvoker(getMethodHandleJava9(method));
                    }
                } catch (IllegalAccessException | InstantiationException | InvocationTargetException
                         | NoSuchMethodException e) {
                    throw new RuntimeException(e);
                }
            } else {
                // Return the PlainMethodInvoker object, where the MapperMethod object is maintained
                return new PlainMethodInvoker(new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
            }
        });
    } catch (RuntimeException re) {
        Throwable cause = re.getCause();
        throw cause == null ? re : cause;
    }
}

Through this code analysis, we define our own methods in Mapper, and finally call the PlainMethodInvoker.invoke method, which will call the MapperMethod.extract method. Next, let's look at the MapperMethod class.

4 MapperMethod

MapperMethod records the method information in Mapper and the SQL statement information in the corresponding mapping file. The properties of this class are as follows:

// The SqlCommand object records the name and type of the SQL statement
private final SqlCommand command;
// Method information in Mapper interface
private final MethodSignature method;

The construction method is as follows:

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

In the construction method, we assign values to two attributes. Next, let's take a look at SqlCommand and MethodSignature.

4.1 SqlCommand

SqlCommand records the name and type of the method in Mapper, and its properties are as follows:

// Name composition method Class name and method name of Mapper interface
private final String name;
// SQL type values include unknown, insert, update, delete, select and flush
private final SqlCommandType type;

This class is constructed as follows:

public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) {
    // Get method name
    final String methodName = method.getName();
    final Class<?> declaringClass = method.getDeclaringClass();
    // Get MappedStatement object
    MappedStatement ms = resolveMappedStatement(mapperInterface, methodName, declaringClass,
                                                configuration);
    if (ms == null) {
        // Process @ Flush annotation
        if (method.getAnnotation(Flush.class) != null) {
            name = null;
            type = SqlCommandType.FLUSH;
        } else {
            throw new BindingException("Invalid bound statement (not found): "
                                       + mapperInterface.getName() + "." + methodName);
        }
    } else {
        // Set name and type
        name = ms.getId();
        type = ms.getSqlCommandType();
        if (type == SqlCommandType.UNKNOWN) {
            throw new BindingException("Unknown execution method for: " + name);
        }
    }
}

private MappedStatement resolveMappedStatement(Class<?> mapperInterface, String methodName,
                                               Class<?> declaringClass, Configuration configuration) {
    // statementId is composed of interface name + ".. + method name in the interface
    String statementId = mapperInterface.getName() + "." + methodName;
    // Judge whether the statement is recorded in the configuration
    if (configuration.hasStatement(statementId)) {
        return configuration.getMappedStatement(statementId);
    } else if (mapperInterface.equals(declaringClass)) {
        // If this interface is defined in the Mapper, null is returned
        return null;
    }
    // Recursive processing of parent classes
    for (Class<?> superInterface : mapperInterface.getInterfaces()) {
        if (declaringClass.isAssignableFrom(superInterface)) {
            MappedStatement ms = resolveMappedStatement(superInterface, methodName,
                                                        declaringClass, configuration);
            if (ms != null) {
                return ms;
            }
        }
    }
    return null;
}

4.2 MethodSignature

The MethodSignature method records the information of the method in the mapper, and its properties are as follows:

// Whether the return type is list
private final boolean returnsMany;
// Whether the return value type is map
private final boolean returnsMap;
// Whether the return value is empty
private final boolean returnsVoid;
// Whether the return value is of cursor type
private final boolean returnsCursor;
// Whether the return value type is Optional
private final boolean returnsOptional;
// return type
private final Class<?> returnType;
//
private final String mapKey;
// The location of the ResultHandler type parameter in the parameter list
private final Integer resultHandlerIndex;
// The location of the rowboundaries type parameter in the parameter list
private final Integer rowBoundsIndex;
// paramNameResolver object
private final ParamNameResolver paramNameResolver;

The construction method of MethodSignature is as follows:

public MethodSignature(Configuration configuration, Class<?> mapperInterface, Method method) {
    // Resolve the return value type of the method
    Type resolvedReturnType = TypeParameterResolver.resolveReturnType(method, mapperInterface);
    if (resolvedReturnType instanceof Class<?>) {
        this.returnType = (Class<?>) resolvedReturnType;
    } else if (resolvedReturnType instanceof ParameterizedType) {
        this.returnType = (Class<?>) ((ParameterizedType) resolvedReturnType).getRawType();
    } else {
        this.returnType = method.getReturnType();
    }
    // Set whether it is the corresponding type property
    this.returnsVoid = void.class.equals(this.returnType);
    this.returnsMany = configuration.getObjectFactory().isCollection(this.returnType) || this.returnType.isArray();
    this.returnsCursor = Cursor.class.equals(this.returnType);
    this.returnsOptional = Optional.class.equals(this.returnType);
    // Get mapKey
    this.mapKey = getMapKey(method);
    this.returnsMap = this.mapKey != null;
    this.rowBoundsIndex = getUniqueParamIndex(method, RowBounds.class);
    this.resultHandlerIndex = getUniqueParamIndex(method, ResultHandler.class);
    this.paramNameResolver = new ParamNameResolver(configuration, method);
}

private String getMapKey(Method method) {
    String mapKey = null;
    // Whether the return value type is Map
    if (Map.class.isAssignableFrom(method.getReturnType())) {
        // @MapKey annotation
        final MapKey mapKeyAnnotation = method.getAnnotation(MapKey.class);
        if (mapKeyAnnotation != null) {
            // Use the @ MapKey annotation to specify the value
            mapKey = mapKeyAnnotation.value();
        }
    }
    return mapKey;
}

Let's first look at the class corresponding to the paramNameResolver attribute.

4.2.1 ParamNameResolver

ParamNameResolver is used to process the parameter list of methods in Mapper. The properties of this class are as follows:

// Record the relationship between parameter position and value of the method
private final SortedMap<Integer, String> names;
// Does the record use the @ Param annotation
private boolean hasParamAnnotation;
  • SortedMap < integer, string > names is used to store parameter locations
  • Whether the @ Param annotation is used in the boolean hasParamAnnotation tag parameter

The construction method of this class is as follows:

public ParamNameResolver(Configuration config, Method method) {
    this.useActualParamName = config.isUseActualParamName();
    // Get parameters
    final Class<?>[] paramTypes = method.getParameterTypes();
    // Gets the annotation list for the parameter
    final Annotation[][] paramAnnotations = method.getParameterAnnotations();
    final SortedMap<Integer, String> map = new TreeMap<>();
    // Number of parameters
    int paramCount = paramAnnotations.length;
    // get names from @Param annotations
    // Traversal parameters
    for (int paramIndex = 0; paramIndex < paramCount; paramIndex++) {
        // Skip if special type
        if (isSpecialParameter(paramTypes[paramIndex])) {
            // skip special parameters
            continue;
        }
        String name = null;
        // Comments on traversal parameters
        for (Annotation annotation : paramAnnotations[paramIndex]) {
            // If the annotation is @ Param
            if (annotation instanceof Param) {
                hasParamAnnotation = true;
                // The name takes the value specified in the annotation
                name = ((Param) annotation).value();
                break;
            }
        }
        if (name == null) {
            // @Param was not specified.
            if (useActualParamName) {
                name = getActualParamName(method, paramIndex);
            }
            if (name == null) {
                // use the parameter index as the name ("0", "1", ...)
                // gcode issue #71
                name = String.valueOf(map.size());
            }
        }
        map.put(paramIndex, name);
    }
    names = Collections.unmodifiableSortedMap(map);
}

The processing logic in its construction method is as follows:

  • Gets the parameter list of the method
  • Judge whether the parameter type is a special type (RowBounds or ResultHandler), and skip the special type directly
  • Or the value attribute of the @ Param annotation is used as the parameter name. If it is not specified, the parameter name defined in the method is used
  • Put the parameter index and name into the map

An important method in this class is getNamedParams, which is used to associate the parameters received by the method with the parameter name. Its source code is as follows:

public Object getNamedParams(Object[] args) {
    final int paramCount = names.size();
    // Whether the parameter list is empty. If it is empty, null will be returned
    if (args == null || paramCount == 0) {
        return null;
    } else if (!hasParamAnnotation && paramCount == 1) {
        // Parameter length is 1
        Object value = args[names.firstKey()];
        return wrapToMapIfCollection(value, useActualParamName ? names.get(0) : null);
    } else {
        final Map<String, Object> param = new ParamMap<>();
        int i = 0;
        // Traversal parameter list
        for (Map.Entry<Integer, String> entry : names.entrySet()) {
            // Put the parameter name as key and the received value as value into the map
            param.put(entry.getValue(), args[entry.getKey()]);
            // add generic param names (param1, param2, ...)
            final String genericParamName = GENERIC_NAME_PREFIX + (i + 1);
            // ensure not to overwrite parameter named with @Param
            // If names does not contain a parameter name, param(i+1) is used as the parameter name
            if (!names.containsValue(genericParamName)) {
                param.put(genericParamName, args[entry.getKey()]);
            }
            i++;
        }
        return param;
    }
}

4.3 excute() method

The extract () method is the core method in MapperMethod. In this method, the corresponding method in SqlSession will be called according to the SQL type. Its logic is as follows:

public Object execute(SqlSession sqlSession, Object[] args) {
    Object result;
    switch (command.getType()) { // Call the method corresponding to SqlSession according to the type of SQL statement
        case INSERT: {
            // The args [] array is processed by the ParamNameResolver to associate the argument passed in by the user with the specified parameter name
            Object param = method.convertArgsToSqlCommandParam(args);
            // sqlSession.insert(command.getName(), param) calls the insert method of SqlSession
            // The rowCountResult method converts the result according to the return value type of the method recorded in the method field
            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()) {
                // Method whose return value is null and ResultSet is processed by ResultHandler
                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 {
                // Method whose return value is a single object
                Object param = method.convertArgsToSqlCommandParam(args);
                // Execution entry of normal select statement > >
                result = sqlSession.selectOne(command.getName(), param);
                if (method.returnsOptional()
                    && (result == null || !method.getReturnType().equals(result.getClass()))) {
                    result = Optional.ofNullable(result);
                }
            }
            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;
}

From the above source code, we can see that INSERT, UPDATE and DELETE will call the rowCountResult method. The logic of this method is as follows:

// In this method, the knowledge is down converted according to the return type. There is no logic
private Object rowCountResult(int rowCount) {
    final Object result;
    if (method.returnsVoid()) {
        result = null;
    } else if (Integer.class.equals(method.getReturnType()) || Integer.TYPE.equals(method.getReturnType())) {
        result = rowCount;
    } else if (Long.class.equals(method.getReturnType()) || Long.TYPE.equals(method.getReturnType())) {
        result = (long)rowCount;
    } else if (Boolean.class.equals(method.getReturnType()) || Boolean.TYPE.equals(method.getReturnType())) {
        result = rowCount > 0;
    } else {
        throw new BindingException("Mapper method '" + command.getName() + "' has an unsupported return type: " + method.getReturnType());
    }
    return result;
}

There is not much logic in the query method. The logic of SqlSession will be introduced in detail in the following articles, which will not be described here.

3 Summary

This concludes today's article. Let's briefly review today's content.

  • The binding module is used to associate Mapper with its mapping file
  • This module has four core classes, as follows
    • MapperRegistry Mapper registrar will associate Mapper's Class with MapperProxyFactory when Mybatis starts
    • MapperProxyFactory MapperProxy's creation factory class will create Mapper's proxy class MapperProxy object through JDK dynamic proxy
    • The proxy class of MapperProxy Mapper interface. When we execute the methods in Mapper, we will call the invoke() method
    • MapperMethod is used to describe the methods in Mapper interface, which will be responsible for parameter processing and really call the methods related to SqlSession
  • Mybatis will create a proxy class object for the Mapper interface we created through a dynamic proxy. We don't need to define its implementation

Thank you for your reading. Welcome to my official account: Bug portable expert.

Topics: Java Mybatis