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.