Today this is the fifth article of Mybatis. In this article, we will learn the binding module. This module is used to bind a Mapper to its mapping file.

This module is located in the org. Apache. Ibatis. Binding package, class has the following four core

  • MapperRegistryThe mapper registry
  • MapperProxyFactoryThe mapper proxy class factory
  • MapperProxyThe mapper proxy class
  • MapperMethodMapper method class

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

1 MapperRegistry

MapperRegistry is the Mapper registry. This class records the mapping between Mapper classes and MapperProxyFactory. The property fields of this class are as follows:

// Configuration object The Configuration information of Mybatis will be parsed and stored in this object
private final Configuration config;
// Record the mapping between Mapper interfaces and MapperProxyFactory
private finalMap<Class<? >, MapperProxyFactory<? >> knownMappers =new HashMap<>();
Copy the code

The Configuration object here is a core class in Mybatis. Relevant configurations in Mybatis will be recorded in this object after parsing. We will introduce this class in detail in the following articles. I’m not going to expand it here.

In this class there are two core methods, addMapper and getMapper, which are used to register and get the mapper, respectively. Let’s take a look at the logic of these two methods.

1.1 addMapper()methods

The addMapper method is used to register the mapper and add it to knownMappers. The content of the execution process and impression, we mentioned this method in the article Mapper mapping file parsing, forget it also doesn’t matter, we learn in detail today, the source code of this method is as follows:

public <T> void addMapper(Class<T> type) {
    // Type is an interface
    if (type.isInterface()) {
        // Check whether it has been loaded
        if (hasMapper(type)) {
            throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
        }
        // Successfully loaded the tag
        boolean loadCompleted = false;
        try {
            // Add to knowMappers
            knownMappers.put(type, new MapperProxyFactory<>(type));
            // Save this for the builder module later
            MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
            parser.parse();
            loadCompleted = true;
        } finally {
            // If an exception occurs, the mapper needs to be removed
            if(! loadCompleted) { knownMappers.remove(type); }}}}Copy the code

In addition to a single Mapper registration method, the class also provides a method to register by 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<>();
    // Use reflection to find the superType class
    resolverUtil.find(newResolverUtil.IsA(superType), packageName); Set<Class<? extends Class<? >>> mapperSet = resolverUtil.getClasses();// Walk through to register
    for (Class<?> mapperClass : mapperSet) {
        addMapper(mapperClass);
    }
}
Copy the code

1.2 getMapper()methods

In our previous example code, we will call sqlSession. getMapper method to get the Mapper object when performing data operations. The logic of this method is to call MapperRegistry.

public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    // Find the MapperProxyFactory object corresponding to the specified type
    final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
    // No exception was thrown
    if (mapperProxyFactory == null) {
        throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
    }
    try {
        MapperProxy is a proxy object created by the JDK
        return mapperProxyFactory.newInstance(sqlSession);
    } catch (Exception e) {
        throw new BindingException("Error getting mapper instance. Cause: "+ e, e); }}Copy the code

2 MapperProxyFactory

MapperProxyFactory is the object factory of the mapperProxy class. The name should remind you of the factory pattern. The main function of this class is to create mapperproxy objects. The attributes of this class are as follows:

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

This class creates a mapper proxy object as follows:

public T newInstance(SqlSession sqlSession) {
    // Create a MapperProxy object
    final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
    return newInstance(mapperProxy);
}
​
protected T newInstance(MapperProxy<T> mapperProxy) {
    // Create a proxy object implementing the mapperInterface interface
    return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
Copy the code

GetMapper gets a proxy object from the Mapper interface generated by the JDK dynamic proxy. This is why we only need to define the Mapper interface and not its implementation when using Mybatis.

3 MapperProxy

MapperProxy is a MapperProxy class that implements the InvocationHandler interface and implements the Invoke method in the JDK dynamic proxy.

This class has the following property fields:

/ / SqlSession objects
private final SqlSession sqlSession;
// Mapper interface Class object
private final Class<T> mapperInterface;
// Mapper interface methods and MapperMethodInvoker mapping
private final Map<Method, MapperMethodInvoker> methodCache;
Copy the code

MapperMethodInvoker is an internal interface in MapperProxy, which has two implementation classes, 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(a);this.mapperMethod = mapperMethod;
    }
​
    @Override
    public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
        Execute the execute method in MapperMethod
        returnmapperMethod.execute(sqlSession, args); }}private static class DefaultMethodInvoker implements MapperMethodInvoker {
    private final MethodHandle methodHandle;
​
    public DefaultMethodInvoker(MethodHandle methodHandle) {
        super(a);this.methodHandle = methodHandle;
    }
​
    @Override
    public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
        returnmethodHandle.bindTo(proxy).invokeWithArguments(args); }}Copy the code

When we call a method in Mapper, we call the mapPerProxy. invoke method, which has the following logic:

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
        // Execute directly if the target method inherits from object
        if (Object.class.equals(method.getDeclaringClass())) {
            return method.invoke(this, args);
        } else {
            returncachedInvoker(method).invoke(proxy, method, args, sqlSession); }}catch (Throwable t) {
        throwExceptionUtil.unwrapThrowable(t); }}private MapperMethodInvoker cachedInvoker(Method method) throws Throwable {
    try {
        return MapUtil.computeIfAbsent(methodCache, method, m -> {
            // Determine whether this is the default method of the interface
            if (m.isDefault()) {
                try {
                    if (privateLookupInMethod == null) {
                        return new DefaultMethodInvoker(getMethodHandleJava8(method));
                    } else {
                        return newDefaultMethodInvoker(getMethodHandleJava9(method)); }}catch (IllegalAccessException | InstantiationException | InvocationTargetException
                         | NoSuchMethodException e) {
                    throw newRuntimeException(e); }}else {
                // Return the PlainMethodInvoker object where the MapperMethod object is maintained
                return new PlainMethodInvoker(newMapperMethod(mapperInterface, method, sqlSession.getConfiguration())); }}); }catch (RuntimeException re) {
        Throwable cause = re.getCause();
        throw cause == null? re : cause; }}Copy the code

By analyzing this code, our own definition to the Mapper method, finally is called PlainMethodInvoker. Invoke method, and this method will be called MapperMethod. Excute method. So let’s look at the class MapperMethod.

4 MapperMethod

MapperMethod records the method information in Mapper and the SQL statement information in the mapping file. This class has the following attributes:

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

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);
}
Copy the code

In the constructor, we assign values to two properties, so let’s look at the SqlCommand and MethodSignature classes.

4.1 SqlCommand

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

Mapper Indicates the Class name of the interface. The method name
private final String name;
// The SQL type can be UNKNOWN, INSERT, UPDATE, DELETE, SELECT, or FLUSH
private final SqlCommandType type;
Copy the code

This class is constructed as follows:

public SqlCommand(Configuration configuration, Class
        mapperInterface, Method method) {
    // Get the method name
    final String methodName = method.getName();
    finalClass<? > declaringClass = method.getDeclaringClass();// Get the MappedStatement object
    MappedStatement ms = resolveMappedStatement(mapperInterface, methodName, declaringClass,
                                                configuration);
    if (ms == null) {
        // Handle the @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 consists of the interface name + "." + the method name in the interface
    String statementId = mapperInterface.getName() + "." + methodName;
    // Check whether the statement is recorded in the Configuration
    if (configuration.hasStatement(statementId)) {
        return configuration.getMappedStatement(statementId);
    } else if (mapperInterface.equals(declaringClass)) {
        // Return null if the interface is defined in the Mapper
        return null;
    }
    // Handle the parent class recursively
    for(Class<? > superInterface : mapperInterface.getInterfaces()) {if (declaringClass.isAssignableFrom(superInterface)) {
            MappedStatement ms = resolveMappedStatement(superInterface, methodName,
                                                        declaringClass, configuration);
            if(ms ! =null) {
                returnms; }}}return null;
}
Copy the code

4.2 MethodSignature

The MethodSignature method records information about methods in the mapper, with the following properties:

// Whether the return type is a list
private final boolean returnsMany;
// Whether the return type is map
private final boolean returnsMap;
// Whether the return value is null
private final boolean returnsVoid;
// Whether the return value is of type CURSOR
private final boolean returnsCursor;
// Whether the return type is Optional
private final boolean returnsOptional;
// Return value type
private finalClass<? > returnType;//
private final String mapKey;
// The location of the ResultHandler type parameter in the parameter list
private final Integer resultHandlerIndex;
// The position of the RowBounds type parameter in the argument list
private final Integer rowBoundsIndex;
/ / paramNameResolver object
private final ParamNameResolver paramNameResolver;
Copy the code

MethodSignature is constructed as follows:

public MethodSignature(Configuration configuration, Class
        mapperInterface, Method method) {
    // Parse the return value type of the method
    Type resolvedReturnType = TypeParameterResolver.resolveReturnType(method, mapperInterface);
    if (resolvedReturnType instanceofClass<? >) {this.returnType = (Class<? >) resolvedReturnType; }else if (resolvedReturnType instanceof ParameterizedType) {
        this.returnType = (Class<? >) ((ParameterizedType) resolvedReturnType).getRawType(); }else {
        this.returnType = method.getReturnType();
    }
    // Sets the corresponding type attribute
    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 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 valuemapKey = mapKeyAnnotation.value(); }}return mapKey;
}
Copy the code

Let’s start by looking at the class corresponding to the paramNameResolver property.

2ParamNameResolver

ParamNameResolver is the parameter list used to process methods in Mapper. This class has the following attributes:

// Record the relationship between method parameter positions and values
private final SortedMap<Integer, String> names;
// Record whether the @param annotation is used
private boolean hasParamAnnotation;
Copy the code
  • SortedMap<Integer, String> namesUsed to store parameter locations
  • boolean hasParamAnnotationMarks whether it is used in the parameter@Paramannotations

The class is constructed as follows:

public ParamNameResolver(Configuration config, Method method) {
    this.useActualParamName = config.isUseActualParamName();
    // Get parameters
    finalClass<? >[] paramTypes = method.getParameterTypes();// Get a list of annotations for the parameters
    final Annotation[][] paramAnnotations = method.getParameterAnnotations();
    final SortedMap<Integer, String> map = new TreeMap<>();
    // Number of arguments
    int paramCount = paramAnnotations.length;
    // get names from @Param annotations
    // Iterate over the parameters
    for (int paramIndex = 0; paramIndex < paramCount; paramIndex++) {
        // If it is a special type, skip it
        if (isSpecialParameter(paramTypes[paramIndex])) {
            // skip special parameters
            continue;
        }
        String name = null;
        // Iterate over the annotations on the 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);
}
Copy the code

The processing logic in its constructor is as follows:

  • Gets the argument list for the method
  • Check whether the parameter type is special (RowBoundsorResultHandlerFor special types, skip this step
  • or@ParamannotationsvalueProperty, as the parameter name, or the parameter name defined in the method if not specified
  • Put the parameter index and name into the map

The most important method in this class is getNamedParams. This method is used to associate the parameters received by the method with the parameter name. The source code is as follows:

public Object getNamedParams(Object[] args) {
    final int paramCount = names.size();
    // Whether the argument list is empty If empty returns NULL
    if (args == null || paramCount == 0) {
        return null;
    } else if(! hasParamAnnotation && paramCount ==1) {
        // The length of the argument 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;
        // Iterate over the argument 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
            // Param (I +1) is used if names does not contain a parameter name
            if(! names.containsValue(genericParamName)) { param.put(genericParamName, args[entry.getKey()]); } i++; }returnparam; }}Copy the code

4.3 excute()methods

The excute() method is the core method in MapperMethod. This method calls the corresponding method in SqlSession based on the SQL type. The logic is as follows:

public Object execute(SqlSession sqlSession, Object[] args) {
    Object result;
    switch (command.getType()) { Call the SqlSession method based on the type of the SQL statement
        case INSERT: {
            // ParamNameResolver processes the args[] array 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 based on 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()) {
                // A method whose return value is empty and whose ResultSet is processed by a 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 {
                // A method that returns a single object
                Object param = method.convertArgsToSqlCommandParam(args);
                // Normal select statement execution entry >>
                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;
}
Copy the code

RowCountResult (); rowCountResult (); rowCountResult ();

// There is no logic in this method to convert knowledge down based on the return type
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;
}
Copy the code

The query method also does not have much logic, SqlSession logic we will be introduced in detail in the later article, here will not be explained.

3 summary

That’s the end of our article for today. Let’s briefly review today’s content.

  • The Binding module is used to associate a Mapper with its mapping file

  • This module has four core classes, as follows

    • MapperRegistry MapperThe registry, inMybatisThe startup willMappertheClassClass andMapperProxyFactoryTo associate
    • MapperProxyFactory MapperProxyThe create factory class will passJDKDynamic proxy creationMapperThe proxy classMapperProxyobject
    • MapperProxy MapperThe proxy class of the interface we implementMapperTo which the method is calledinvoke()methods
    • MapperMethodUsed to describe theMapperThe method in the interface, which takes care of the processing of arguments and actually calls toSqlSessionRelated methods
  • Mybatis will create a proxy class object for the Mapper interface we created through dynamic proxies, we do not need to define its implementation

Thank you for reading, welcome to pay attention to my public number: Bug handling small expert

Java knowledge will be updated regularly