Mybaits Source Parsing - The most detailed network, none: ResultSet automatically maps to entity class objects (last)

In the previous article, we sent SQL to the database and returned a ResultSet. Next, we automatically mapped the ResultSet to an entity class object.This eliminates the need for users to manually manipulate result sets and populate data into entity class objects.This can greatly reduce the workload of development and improve work efficiency.

Mapping Result Entry

Let's see where we last looked at the source code

public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
    PreparedStatement ps = (PreparedStatement)statement;
    //Execution Database SQL
    ps.execute();
    //Conduct resultSet Automatic Mapping
    return this.resultSetHandler.handleResultSets(ps);
}

Result set processing entry method is handleResultSets

public List<Object> handleResultSets(Statement stmt) throws SQLException {
    
    final List<Object> multipleResults = new ArrayList<Object>();

    int resultSetCount = 0;
    //Get First ResultSet,Usually there is only one
    ResultSetWrapper rsw = getFirstResultSet(stmt);
    //Read the corresponding from the configuration ResultMap,Usually there is only one. Settings are separated by commas. Do we usually have this setting?
    List<ResultMap> resultMaps = mappedStatement.getResultMaps();
    int resultMapCount = resultMaps.size();
    validateResultMapsCount(rsw, resultMapCount);

    while (rsw != null && resultMapCount > resultSetCount) {
        ResultMap resultMap = resultMaps.get(resultSetCount);
        // Processing result set
        handleResultSet(rsw, resultMap, multipleResults, null);
        rsw = getNextResultSet(stmt);
        cleanUpAfterHandlingResultSet();
        resultSetCount++;
    }

    // The following logic is all about multiple result sets, no analysis, code omission
    String[] resultSets = mappedStatement.getResultSets();
    if (resultSets != null) {...}

    return collapseSingleResultList(multipleResults);
}

In practice, a Sql statement usually returns only one result set and does not analyze the case of multiple result sets.It is rarely used in practice.Continue with the handleResultSet method

private void handleResultSet(ResultSetWrapper rsw, ResultMap resultMap, List<Object> multipleResults, ResultMapping parentMapping) throws SQLException {
    try {
        if (parentMapping != null) {
            handleRowValues(rsw, resultMap, null, RowBounds.DEFAULT, parentMapping);
        } else {
            if (resultHandler == null) {
                // Create a default result processor
                DefaultResultHandler defaultResultHandler = new DefaultResultHandler(objectFactory);
                // Processing row data for result sets
                handleRowValues(rsw, resultMap, defaultResultHandler, rowBounds, null);
                // Add results to multipleResults in
                multipleResults.add(defaultResultHandler.getResultList());
            } else {
                handleRowValues(rsw, resultMap, resultHandler, rowBounds, null);
            }
        }
    } finally {
        closeResultSet(rsw.getResultSet());
    }
}

ResultSet result is mapped by handleRowValues, the result of the final mapping will be in ResultList collection of defaultResultHandler, the result will be returned by adding it to multipleResults, we continue to follow the core method handleRowValues

public void handleRowValues(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler,
        RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {
    if (resultMap.hasNestedResultMaps()) {
        ensureNoRowBounds();
        checkResultHandler();
        // Processing nested maps, we will analyze them separately in the next article
        handleRowValuesForNestedResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
    } else {
        // To deal with simple mappings, this article will only analyze simple mappings
        handleRowValuesForSimpleResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
    }
}

We can use resultMap.hasNestedResultMaps() to know if a query statement is a nested query, or if a resultMap contains <association>and <collection>and its select attribute is not empty, you can see my third article about Resolve resultMap node .This article first analyzes simple mappings

private void handleRowValuesForSimpleResultMap(ResultSetWrapper rsw, ResultMap resultMap,
        ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {

    DefaultResultContext<Object> resultContext = new DefaultResultContext<Object>();
    // according to RowBounds Locate to specified row record
    skipRows(rsw.getResultSet(), rowBounds);
    // ResultSet Is a collection, and most likely what we're looking for is a List,This traverses through each piece of data
    while (shouldProcessMoreRows(resultContext, rowBounds) && rsw.getResultSet().next()) {
        ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(rsw.getResultSet(), resultMap, null);
        // from resultSet Get results in
        Object rowValue = getRowValue(rsw, discriminatedResultMap);
        // Store results to resultHandler Of ResultList,Last ResultList join multipleResults Return in
        storeObject(resultHandler, resultContext, rowValue, parentMapping, rsw.getResultSet());
    }
}

Most likely the result of our query is a collection, so we're going to iterate through the collection, mapping each result individually, and adding the mapped result to the ResultList of the resultHandler

MyBatis provides RowBounds by default for paging, which, as you can see from the code above, is not an efficient way of paging, it is to find out all the data and paging in memory.In addition to using RowBounds, there are some third-party paging plug-ins available for paging.For the next article, let's look at the key code getRowValue, which handles a row of data

private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap) throws SQLException {
    // this Map Is used to store delayed loads BountSql Yes, let's look at it below
    final ResultLoaderMap lazyLoader = new ResultLoaderMap();
 // Create entity class objects, such as Employ object
    Object rowValue = createResultObject(rsw, resultMap, lazyLoader, null);
    if (rowValue != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {
        final MetaObject metaObject = configuration.newMetaObject(rowValue);
        boolean foundValues = this.useConstructorMappings;
        
        if (shouldApplyAutomaticMappings(resultMap, false)) {
            //Automatic Mapping,Result Set Has column,but resultMap Not configured in  
            foundValues = applyAutomaticMappings(rsw, resultMap, metaObject, null) || foundValues;
        }
      // according to <resultMap> Mapping relationships configured in nodes
        foundValues = applyPropertyMappings(rsw, resultMap, metaObject, lazyLoader, null) || foundValues;
        foundValues = lazyLoader.size() > 0 || foundValues;
        rowValue = foundValues || configuration.isReturnInstanceForEmptyRow() ? rowValue : null;
    }
    return rowValue;
}

Important logic has been commented out.They are as follows:

  1. Create entity class object
  2. Automatically map column s in the result set, but not configured in resultMap

  3. Map according to the mapping relationship configured in the <resultMap>node

Create entity class object

We want to map query results to entity class objects. The first step is to create entity class objects. Let's take a look at MyBatis's process of creating entity class objects.

private Object createResultObject(ResultSetWrapper rsw, ResultMap resultMap, ResultLoaderMap lazyLoader, String columnPrefix) throws SQLException {

    this.useConstructorMappings = false;
    final List<Class<?>> constructorArgTypes = new ArrayList<Class<?>>();
    final List<Object> constructorArgs = new ArrayList<Object>();

    // Calling overloaded methods to create entity class objects
    Object resultObject = createResultObject(rsw, resultMap, constructorArgTypes, constructorArgs, columnPrefix);
    if (resultObject != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {
        final List<ResultMapping> propertyMappings = resultMap.getPropertyResultMappings();
        for (ResultMapping propertyMapping : propertyMappings) {
            // If delayed loading is turned on, resultObject Proxy classes will not be created if only configured Association queries are generated without delayed loading turned on
            if (propertyMapping.getNestedQueryId() != null && propertyMapping.isLazy()) {
                /*
                 * Create a proxy class, which is generated by default using the Javassist framework.
                 * Because entity classes usually do not implement interfaces, you cannot use the JDK dynamic proxy API to generate proxies for entity classes.
                 * And passed lazyLoader in
                 */
                resultObject = configuration.getProxyFactory()
                    .createProxy(resultObject, lazyLoader, configuration, objectFactory, constructorArgTypes, constructorArgs);
                break;
            }
        }
    }
    this.useConstructorMappings =
        resultObject != null && !constructorArgTypes.isEmpty();
    return resultObject;
}

Let's start with the logic of the createResultObject overload method

private Object createResultObject(ResultSetWrapper rsw, ResultMap resultMap, List<Class<?>> constructorArgTypes, List<Object> constructorArgs, String columnPrefix) throws SQLException {

    final Class<?> resultType = resultMap.getType();
    final MetaClass metaType = MetaClass.forClass(resultType, reflectorFactory);
    final List<ResultMapping> constructorMappings = resultMap.getConstructorResultMappings();

    if (hasTypeHandlerForResultObject(rsw, resultType)) {
        return createPrimitiveResultObject(rsw, resultMap, columnPrefix);
    } else if (!constructorMappings.isEmpty()) {
        return createParameterizedResultObject(rsw, resultType, constructorMappings, constructorArgTypes, constructorArgs, columnPrefix);
    } else if (resultType.isInterface() || metaType.hasDefaultConstructor()) {
        // adopt ObjectFactory Call the default constructor of the target class to create an instance
        return objectFactory.create(resultType);
    } else if (shouldApplyAutomaticMappings(resultMap, false)) {
        return createByConstructorSignature(rsw, resultType, constructorArgTypes, constructorArgs, columnPrefix);
    }
    throw new ExecutorException("Do not know how to create an instance of " + resultType);
}

Typically, MyBatis creates entity class objects by calling the default construction method through ObjectFactory.See how it was created

public <T> T create(Class<T> type, List<Class<?>> constructorArgTypes, List<Object> constructorArgs) {
    Class<?> classToCreate = this.resolveInterface(type);
    return this.instantiateClass(classToCreate, constructorArgTypes, constructorArgs);
}

<T> T instantiateClass(Class<T> type, List<Class<?>> constructorArgTypes, List<Object> constructorArgs) {
    try {
        Constructor constructor;
        if (constructorArgTypes != null && constructorArgs != null) {
            constructor = type.getDeclaredConstructor((Class[])constructorArgTypes.toArray(new Class[constructorArgTypes.size()]));
            if (!constructor.isAccessible()) {
                constructor.setAccessible(true);
            }

            return constructor.newInstance(constructorArgs.toArray(new Object[constructorArgs.size()]));
        } else {
            //Get Constructors from Reflection
            constructor = type.getDeclaredConstructor();
            if (!constructor.isAccessible()) {
                constructor.setAccessible(true);
            }
            //Instantiate an object through a constructor
            return constructor.newInstance();
        }
    } catch (Exception var9) {
        throw new ReflectionException("Error instantiating " + type + " with invalid types (" + argTypes + ") or values (" + argValues + "). Cause: " + var9, var9);
    }
}

It's easy to create objects through reflection

Result Set Mapping

Mapping result sets can be divided into two cases: one is automatic mapping (fields with result sets but not configured in resultMap), which is used in practice to reduce the work of configuration.Automatic mapping is also turned on by default in Meybatis.The second is to map the configurations in ResultMap, so let's look at the two mappings

Automatic Mapping

private boolean applyAutomaticMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject, String columnPrefix) throws SQLException {

    // Obtain UnMappedColumnAutoMapping list
    List<UnMappedColumnAutoMapping> autoMapping = createAutomaticMappings(rsw, resultMap, metaObject, columnPrefix);
    boolean foundValues = false;
    if (!autoMapping.isEmpty()) {
        for (UnMappedColumnAutoMapping mapping : autoMapping) {
            // adopt TypeHandler Get data for the specified column from the result set
            final Object value = mapping.typeHandler.getResult(rsw.getResultSet(), mapping.column);
            if (value != null) {
                foundValues = true;
            }
            if (value != null || (configuration.isCallSettersOnNulls() && !mapping.primitive)) {
                // Setting through meta-information objects value To the specified field of the entity class object
                metaObject.setValue(mapping.property, value);
            }
        }
    }
    return foundValues;
}

First, get the UnMappedColumnAutoMapping collection, then iterate through it, get the data from the result set through TypeHandler, and set the data to the entity class object.

UnMappedColumnAutoMapping is used to record mapping relationships that are not configured in <resultMap>nodes.Its code is as follows:

private static class UnMappedColumnAutoMapping {

    private final String column;
    private final String property;
    private final TypeHandler<?> typeHandler;
    private final boolean primitive;

    public UnMappedColumnAutoMapping(String column, String property, TypeHandler<?> typeHandler, boolean primitive) {
        this.column = column;
        this.property = property;
        this.typeHandler = typeHandler;
        this.primitive = primitive;
    }
}

Use only for recording mapping relationships.Here's a look at the process of getting the UnMappedColumnAutoMapping collection, as follows:

private List<UnMappedColumnAutoMapping> createAutomaticMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject, String columnPrefix) throws SQLException {

    final String mapKey = resultMap.getId() + ":" + columnPrefix;
    // Get from Cache UnMappedColumnAutoMapping list
    List<UnMappedColumnAutoMapping> autoMapping = autoMappingsCache.get(mapKey);
    // Cache Miss
    if (autoMapping == null) {
        autoMapping = new ArrayList<UnMappedColumnAutoMapping>();
        // from ResultSetWrapper Get unconfigured in <resultMap> Column name in
        final List<String> unmappedColumnNames = rsw.getUnmappedColumnNames(resultMap, columnPrefix);
        for (String columnName : unmappedColumnNames) {
            String propertyName = columnName;
            if (columnPrefix != null && !columnPrefix.isEmpty()) {
                if (columnName.toUpperCase(Locale.ENGLISH).startsWith(columnPrefix)) {
                    propertyName = columnName.substring(columnPrefix.length());
                } else {
                    continue;
                }
            }
            // Convert column names in underscore to hump, for example AUTHOR_NAME -> authorName
            final String property = metaObject.findProperty(propertyName, configuration.isMapUnderscoreToCamelCase());
            if (property != null && metaObject.hasSetter(property)) {
                // Detect if the current property exists resultMap in
                if (resultMap.getMappedProperties().contains(property)) {
                    continue;
                }
                // Get the type of property pair
                final Class<?> propertyType = metaObject.getSetterType(property);
                if (typeHandlerRegistry.hasTypeHandler(propertyType, rsw.getJdbcType(columnName))) {
                    final TypeHandler<?> typeHandler = rsw.getTypeHandler(propertyType, columnName);
                    // Encapsulate the information obtained above to UnMappedColumnAutoMapping In object
                    autoMapping.add(new UnMappedColumnAutoMapping(columnName, property, typeHandler, propertyType.isPrimitive()));
                } else {
                    configuration.getAutoMappingUnknownColumnBehavior()
                        .doAction(mappedStatement, columnName, property, propertyType);
                }
            } else {
                configuration.getAutoMappingUnknownColumnBehavior()
                    .doAction(mappedStatement, columnName, (property != null) ? property : propertyName, null);
            }
        }
        // Write Cache
        autoMappingsCache.put(mapKey, autoMapping);
    }
    return autoMapping;
}

Let's start by getting column names from ResultSetWrapper that are not configured in <resultMap>

public List<String> getUnmappedColumnNames(ResultMap resultMap, String columnPrefix) throws SQLException {
    List<String> unMappedColumnNames = unMappedColumnNamesMap.get(getMapKey(resultMap, columnPrefix));
    if (unMappedColumnNames == null) {
        // Load mapped and unmapped column names
        loadMappedAndUnmappedColumnNames(resultMap, columnPrefix);
        // Get unmapped column names
        unMappedColumnNames = unMappedColumnNamesMap.get(getMapKey(resultMap, columnPrefix));
    }
    return unMappedColumnNames;
}

private void loadMappedAndUnmappedColumnNames(ResultMap resultMap, String columnPrefix) throws SQLException {
    List<String> mappedColumnNames = new ArrayList<String>();
    List<String> unmappedColumnNames = new ArrayList<String>();
    final String upperColumnPrefix = columnPrefix == null ? null : columnPrefix.toUpperCase(Locale.ENGLISH);
    // Obtain <resultMap> All column names configured in
    final Set<String> mappedColumns = prependPrefixes(resultMap.getMappedColumns(), upperColumnPrefix);
    /*
     * Traversing through columnNames, which is a member variable of ResultSetWrapper, saves all column names in the current result set
     * Here you get the column names that are not configured in the resultMap by using all the column names in the ResultSet
     * This means that when automatic assignments are made later, only the column names found by the assignments are assigned
     */
    for (String columnName : columnNames) {
        final String upperColumnName = columnName.toUpperCase(Locale.ENGLISH);
        // Detect if the current column name is included in the mapped column name collection
        if (mappedColumns.contains(upperColumnName)) {
            mappedColumnNames.add(upperColumnName);
        } else {
            // Save column names in unmappedColumnNames in
            unmappedColumnNames.add(columnName);
        }
    }
    // Cached Column Name Collection
    mappedColumnNamesMap.put(getMapKey(resultMap, columnPrefix), mappedColumnNames);
    unMappedColumnNamesMap.put(getMapKey(resultMap, columnPrefix), unmappedColumnNames);
}

First, get the column name collection from the current dataset, then get the column name collection configured in <resultMap>.Then iterate through the collection of column names in the dataset and determine if the column names are configured in the <resultMap>node.If configured, it indicates that the column name already has a mapping relationship and is stored in mappedColumnNames.If not configured, the column name is not mapped to a field in the entity class, and is stored in unmappedColumnNames.

Map result node

Next, you'll analyze how MyBatis populates data from the result set into entity class fields that have been configured with ResultMap mappings.

private boolean applyPropertyMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject,ResultLoaderMap lazyLoader, String columnPrefix) throws SQLException {
    
    // Get mapped column names
    final List<String> mappedColumnNames = rsw.getMappedColumnNames(resultMap, columnPrefix);
    boolean foundValues = false;
    // Obtain ResultMapping aggregate
    final List<ResultMapping> propertyMappings = resultMap.getPropertyResultMappings();
    // Be-all ResultMapping Traverse to map
    for (ResultMapping propertyMapping : propertyMappings) {
        String column = prependPrefix(propertyMapping.getColumn(), columnPrefix);
        if (propertyMapping.getNestedResultMapId() != null) {
            column = null;
        }
        if (propertyMapping.isCompositeResult()
            || (column != null && mappedColumnNames.contains(column.toUpperCase(Locale.ENGLISH)))
            || propertyMapping.getResultSet() != null) {
            
            // Get data for the specified column from the result set
            Object value = getPropertyMappingValue(rsw.getResultSet(), metaObject, propertyMapping, lazyLoader, columnPrefix);
            
            final String property = propertyMapping.getProperty();
            if (property == null) {
                continue;

            // If the value obtained is DEFERED,Then delay loading the value
            } else if (value == DEFERED) {
                foundValues = true;
                continue;
            }
            if (value != null) {
                foundValues = true;
            }
            if (value != null || (configuration.isCallSettersOnNulls() && !metaObject.getSetterType(property).isPrimitive())) {
                // Set the obtained value to the entity class object
                metaObject.setValue(property, value);
            }
        }
    }
    return foundValues;
}

private Object getPropertyMappingValue(ResultSet rs, MetaObject metaResultObject, ResultMapping propertyMapping,ResultLoaderMap lazyLoader, String columnPrefix) throws SQLException {

    if (propertyMapping.getNestedQueryId() != null) {
        // Get associated query results
        return getNestedQueryMappingValue(rs, metaResultObject, propertyMapping, lazyLoader, columnPrefix);
    } else if (propertyMapping.getResultSet() != null) {
        addPendingChildRelation(rs, metaResultObject, propertyMapping);
        return DEFERED;
    } else {
        final TypeHandler<?> typeHandler = propertyMapping.getTypeHandler();
        final String column = prependPrefix(propertyMapping.getColumn(), columnPrefix);
        // from ResultSet Gets the value of the specified column in
        return typeHandler.getResult(rs, column);
    }
}

Gets the ResultMapping collection of mapping objects from ResultMap.Then iterate through the ResultMapping collection, call getPropertyMappingValue to get the data for the specified column, and set the data to the entity class object.

This is a bit different from automatic mapping, which gets the value of the specified column directly from the ResultSet, but there is one more case with ResultMap: an associated query, or a delayed query, which gets the value of an associated query if no delayed loading is configured and returns DEFERED if delayed loading is configured

Association Query and Delayed Loading

Our queries often encounter one-to-one, one-to-many situations. Usually, we can use one SQL to do multi-table queries.Of course, we can also use related queries to split one SQL into two to complete the query task.MyBatis provides two tags to support one-to-one and one-to-many scenarios, namely <association> and <collection>.Let me show you how to use <association>to complete one-to-one association queries.Let's first look at the definition of an entity class:

/** Author class */
public class Author {
    private Integer id;
    private String name;
    private Integer age;
    private Integer sex;
    private String email;
    
    // ellipsis getter/setter
}

/** Article Class */
public class Article {
    private Integer id;
    private String title;
    // One-to-one relationship
    private Author author;
    private String content;
    private Date createTime;
    
    // ellipsis getter/setter
}

Next, look at the Mapper interface and the definition of the map file.

public interface ArticleDao {
    Article findOne(@Param("id") int id);
    Author findAuthor(@Param("id") int authorId);
}

 

<mapper namespace="xyz.coolblog.dao.ArticleDao">
    <resultMap id="articleResult" type="Article">
        <result property="createTime" column="create_time"/>
        //column Attribute value contains column information only, parameter type is author_id Column corresponding type, here is Integer
        //Meaning will be author_id Query statement passed as a parameter to the Association findAuthor
        <association property="author" column="author_id" javaType="Author" select="findAuthor"/>
    </resultMap>

    <select id="findOne" resultMap="articleResult">
        SELECT
            id, author_id, title, content, create_time
        FROM
            article
        WHERE
            id = #{id}
    </select>

    <select id="findAuthor" resultType="Author">
        SELECT
            id, name, age, sex, email
        FROM
            author
        WHERE
            id = #{id}
    </select>
</mapper>

Turn on delayed loading

<!--Turn on delayed loading-->
<setting name="lazyLoadingEnabled" value="true"/>
<!--Turn off the active loading strategy-->
<setting name="aggressiveLazyLoading" value="false"/>
<!--Trigger method for delayed loading-->
<setting name="lazyLoadTriggerMethods" value="equals,hashCode"/>

The Association node now uses select to point to another query statement and passes the author_id as a parameter to the statement of the association query

If deferred loading is not turned on at this point, two SQL s are generated, findOne is executed first, findAuthor is executed with the result returned by findOne as a parameter, and the result is set to the author property

What if delayed loading is turned on?Then only one SQL is executed for findOne, and when the article.getAuthor() method is called, the findAuthor query is executed. Let's see how it is implemented

Let's start with mapping the result node above

private Object getPropertyMappingValue(ResultSet rs, MetaObject metaResultObject, ResultMapping propertyMapping,ResultLoaderMap lazyLoader, String columnPrefix) throws SQLException {

    if (propertyMapping.getNestedQueryId() != null) {
        // Get associated query results
        return getNestedQueryMappingValue(rs, metaResultObject, propertyMapping, lazyLoader, columnPrefix);
    } else if (propertyMapping.getResultSet() != null) {
        addPendingChildRelation(rs, metaResultObject, propertyMapping);
        return DEFERED;
    } else {
        final TypeHandler<?> typeHandler = propertyMapping.getTypeHandler();
        final String column = prependPrefix(propertyMapping.getColumn(), columnPrefix);
        // from ResultSet Gets the value of the specified column in
        return typeHandler.getResult(rs, column);
    }
}

We see that if ResultMapping sets an association query, that is, association or collection configures select, then the result is queried through the Association statement and set to the properties of the entity class object.If you do not configure select, it is easy to get results directly from the ResultSet by column name.Let's take a look at getNestedQueryMappingValue

private Object getNestedQueryMappingValue(ResultSet rs, MetaObject metaResultObject, ResultMapping propertyMapping, ResultLoaderMap lazyLoader, String columnPrefix) throws SQLException {

    // Get associated queries id,id = Namespace + <association> Of select Attribute Value
    final String nestedQueryId = propertyMapping.getNestedQueryId();
    final String property = propertyMapping.getProperty();
    // according to nestedQueryId Get Associated MappedStatement
    final MappedStatement nestedQuery = configuration.getMappedStatement(nestedQueryId);
    //Get associated queries MappedStatement Parameter type
    final Class<?> nestedQueryParameterType = nestedQuery.getParameterMap().getType();
    /*
     * Generate associated query statement parameter object, parameter type may be some wrapper class, Map or custom entity class.
     * The specific type depends on the configuration information.Based on the above examples, the following is an analysis of the impact of different configurations on parameter types:
     *   1. <association column="author_id"> 
     *      column Attribute value only contains column information, parameter type is the type corresponding to author_id column, here is Integer
     * 
     *   2. <association column="{id=author_id, name=title}"> 
     *      column The property value contains the composite information of the property name and the column name, which MyBatis uses from the ResultSet
     *      Gets the column data and sets it to the specified properties of the entity class object, such as:
     *          Author{id=1, name="Chen Hao "}
     *      Or you can save them in Map as key-value pairs <attributes, column data>For example:
     *          {"id": 1, "name": "Chen Hao "}
     *
     *      Whether the parameter type is an entity class or a Map depends on the configuration information of the associated query statement.For example:
     *          <select id="findAuthor">  ->  Parameter type is Map
     *          <select id="findAuthor" parameterType="Author"> -> Parameter type is entity class
     */
    final Object nestedQueryParameterObject = prepareParameterForNestedQuery(rs, propertyMapping, nestedQueryParameterType, columnPrefix);
    Object value = null;
    if (nestedQueryParameterObject != null) {
        // Obtain BoundSql,Runtime parameters are set here, so it can be executed directly
        final BoundSql nestedBoundSql = nestedQuery.getBoundSql(nestedQueryParameterObject);
        final CacheKey key = executor.createCacheKey(nestedQuery, nestedQueryParameterObject, RowBounds.DEFAULT, nestedBoundSql);
        final Class<?> targetType = propertyMapping.getJavaType();

        if (executor.isCached(nestedQuery, key)) {
            executor.deferLoad(nestedQuery, metaResultObject, property, key, targetType);
            value = DEFERED;
        } else {
            // Create a result loader
            final ResultLoader resultLoader = new ResultLoader(configuration, executor, nestedQuery, nestedQueryParameterObject, targetType, key, nestedBoundSql);
            // Detect whether current properties require delayed loading
            if (propertyMapping.isLazy()) {
                // Add delayed load related objects to loaderMap In collection
                lazyLoader.addLoader(property, metaResultObject, resultLoader);
                value = DEFERED;
            } else {
                // Direct execution of associated queries
                // If you configure the association query only, but do not turn on lazy loading, execute the association query directly and return the result, which is set to the properties of the entity class object
                value = resultLoader.loadResult();
            }
        }
    }
    return value;
}

The logic of this approach is summarized below:

  1. Get MappedStatement from nestedQueryId
  2. Generate Parameter Object
  3. Get BoundSql
  4. Create ResultLoader
  5. Detects whether the current property needs to be delayed loaded and, if necessary, adds objects related to delayed loading to the loaderMap collection
  6. Load results directly through the result loader if no delay loading is required

In the above process, it is necessary to check the first level cache. If the cache hits, the results can be directly retrieved without executing the associated query SQL.If the cache is not hit, the next step is to perform the delayed load related logic step by step

Let's look at the logic for adding delayed loading related objects to the loaderMap collection, as follows:

public void addLoader(String property, MetaObject metaResultObject, ResultLoader resultLoader) {
    // Convert property names to uppercase
    String upperFirst = getUppercaseFirstProperty(property);
    if (!upperFirst.equalsIgnoreCase(property) && loaderMap.containsKey(upperFirst)) {
        throw new ExecutorException("Nested lazy loaded result property '" + property +
                                    "' for query id '" + resultLoader.mappedStatement.getId() +
                                    " already exists in the result map. The leftmost property of all lazy loaded properties must be unique within a result map.");
    }
    // Establish LoadPair,And will <Uppercase property name, LoadPair object> Key-value pairs added to loaderMap in
    loaderMap.put(upperFirst, new LoadPair(property, metaResultObject, resultLoader));
}

Let's go back to creating entity classes at the beginning of this article

private Object createResultObject(ResultSetWrapper rsw, ResultMap resultMap, ResultLoaderMap lazyLoader, String columnPrefix) throws SQLException {

    this.useConstructorMappings = false;
    final List<Class<?>> constructorArgTypes = new ArrayList<Class<?>>();
    final List<Object> constructorArgs = new ArrayList<Object>();

    // Calling overloaded methods to create entity class objects
    Object resultObject = createResultObject(rsw, resultMap, constructorArgTypes, constructorArgs, columnPrefix);
    if (resultObject != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {
        final List<ResultMapping> propertyMappings = resultMap.getPropertyResultMappings();
        for (ResultMapping propertyMapping : propertyMappings) {
            // If delayed loading is turned on, resultObject Proxy classes will not be created if only configured Association queries are generated without delayed loading turned on
            if (propertyMapping.getNestedQueryId() != null && propertyMapping.isLazy()) {
                /*
                 * Create a proxy class, which is generated by default using the Javassist framework.
                 * Because entity classes usually do not implement interfaces, you cannot use the JDK dynamic proxy API to generate proxies for entity classes.
                 * And passed lazyLoader in
                 */
                resultObject = configuration.getProxyFactory()
                    .createProxy(resultObject, lazyLoader, configuration, objectFactory, constructorArgTypes, constructorArgs);
                break;
            }
        }
    }
    this.useConstructorMappings =
        resultObject != null && !constructorArgTypes.isEmpty();
    return resultObject;
}

If delayed loading is turned on and there is an associated query, a proxy object is created, passing in the lazyLoader holding BondSql and the resultObject of the created target object as parameters.

Mybatis provides two implementation classes, CglibProxyFactory and JavassistProxyFactory, based on org.javassist:javassist and cglib:cglib, respectively.The createProxy method is the core method to achieve lazy loading logic and is the goal of our analysis.

CglibProxyFactory

CglibProxyFactory generates dynamic proxy classes by inheriting the parent class based on the cglib dynamic proxy mode.

@Override
public Object createProxy(Object target, ResultLoaderMap lazyLoader, Configuration configuration, ObjectFactory objectFactory, List<Class<?>> constructorArgTypes, List<Object> constructorArgs) {
  return EnhancedResultObjectProxyImpl.createProxy(target, lazyLoader, configuration, objectFactory, constructorArgTypes, constructorArgs);
}

public static Object createProxy(Object target, ResultLoaderMap lazyLoader, Configuration configuration, ObjectFactory objectFactory, List<Class<?>> constructorArgTypes, List<Object> constructorArgs) {
  final Class<?> type = target.getClass();
  EnhancedResultObjectProxyImpl callback = new EnhancedResultObjectProxyImpl(type, lazyLoader, configuration, objectFactory, constructorArgTypes, constructorArgs);
  //from CglibProxyFactory Generate Object
  Object enhanced = crateProxy(type, callback, constructorArgTypes, constructorArgs);
  //Copy Attributes
  PropertyCopier.copyBeanProperties(type, target, enhanced);
  return enhanced;
}

static Object crateProxy(Class<?> type, Callback callback, List<Class<?>> constructorArgTypes, List<Object> constructorArgs) {
  Enhancer enhancer = new Enhancer();
  enhancer.setCallback(callback);
  //Set Parent Object
  enhancer.setSuperclass(type);
  try {
    type.getDeclaredMethod(WRITE_REPLACE_METHOD);
    // ObjectOutputStream will call writeReplace of objects returned by writeReplace
    if (log.isDebugEnabled()) {
      log.debug(WRITE_REPLACE_METHOD + " method was found on bean " + type + ", make sure it returns this");
    }
  } catch (NoSuchMethodException e) {
    enhancer.setInterfaces(new Class[]{WriteReplaceInterface.class});
  } catch (SecurityException e) {
    // nothing to do here
  }
  Object enhanced;
  if (constructorArgTypes.isEmpty()) {
    enhanced = enhancer.create();
  } else {
    Class<?>[] typesArray = constructorArgTypes.toArray(new Class[constructorArgTypes.size()]);
    Object[] valuesArray = constructorArgs.toArray(new Object[constructorArgs.size()]);
    enhanced = enhancer.create(typesArray, valuesArray);
  }
  return enhanced;
}

You can see that the Enhancer is initialized and the construction method is called to generate the object.From enhancer.setSuperclass(type); you can also see that cglib inherits its parent class.

EnhancedResultObjectProxyImpl

EnhancedResultObjectProxyImpl implements the MethodInterceptor interface, which Cglib intercepts the entry of the target object method through which all calls to the target object method pass the intercept method.

@Override
public Object intercept(Object enhanced, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
  final String methodName = method.getName();
  try {
    synchronized (lazyLoader) {
      if (WRITE_REPLACE_METHOD.equals(methodName)) {
        Object original;
        if (constructorArgTypes.isEmpty()) {
          original = objectFactory.create(type);
        } else {
          original = objectFactory.create(type, constructorArgTypes, constructorArgs);
        }
        PropertyCopier.copyBeanProperties(type, enhanced, original);
        if (lazyLoader.size() > 0) {
          return new CglibSerialStateHolder(original, lazyLoader.getProperties(), objectFactory, constructorArgTypes, constructorArgs);
        } else {
          return original;
        }
      } else {
        if (lazyLoader.size() > 0 && !FINALIZE_METHOD.equals(methodName)) {
        /*
         * If aggressive is true, or trigger methods (such as equals, hashCode, and so on) are called,
         * Load all delayed loaded data
         */
          if (aggressive || lazyLoadTriggerMethods.contains(methodName)) {
            lazyLoader.loadAll();
          } else if (PropertyNamer.isSetter(methodName)) {
            // If the consumer shows that the call was made setter Method, the corresponding delay-loaded class is loaded from loaderMap Remove in
            final String property = PropertyNamer.methodToProperty(methodName);
            lazyLoader.remove(property);
          // Detect whether the consumer invokes getter Method
          } else if (PropertyNamer.isGetter(methodName)) {
            final String property = PropertyNamer.methodToProperty(methodName);
            if (lazyLoader.hasLoader(property)) {
              // Perform delayed load logic
              lazyLoader.load(property);
            }
          }
        }
      }
    }
    //Execute the original method (that is, the parent method)
    return methodProxy.invokeSuper(enhanced, args);
  } catch (Throwable t) {
    throw ExceptionUtil.unwrapThrowable(t);
  }
}

Complete code

import java.lang.reflect.Method;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;

import net.sf.cglib.proxy.Callback;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

import org.apache.ibatis.executor.loader.AbstractEnhancedDeserializationProxy;
import org.apache.ibatis.executor.loader.AbstractSerialStateHolder;
import org.apache.ibatis.executor.loader.ProxyFactory;
import org.apache.ibatis.executor.loader.ResultLoaderMap;
import org.apache.ibatis.executor.loader.WriteReplaceInterface;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.logging.Log;
import org.apache.ibatis.logging.LogFactory;
import org.apache.ibatis.reflection.ExceptionUtil;
import org.apache.ibatis.reflection.factory.ObjectFactory;
import org.apache.ibatis.reflection.property.PropertyCopier;
import org.apache.ibatis.reflection.property.PropertyNamer;
import org.apache.ibatis.session.Configuration;

/**
 * cglib Proxy Factory Class, Implementing Delayed Load Properties
 * @author Clinton Begin
 */
public class CglibProxyFactory implements ProxyFactory {

  /**
   * finalize Method
   */
  private static final String FINALIZE_METHOD = "finalize";
  /**
   * writeReplace Method
   */
  private static final String WRITE_REPLACE_METHOD = "writeReplace";

  /**
   * Load Enhancer, this is the entry to Cglib
   */
  public CglibProxyFactory() {
    try {
      Resources.classForName("net.sf.cglib.proxy.Enhancer");
    } catch (Throwable e) {
      throw new IllegalStateException("Cannot enable lazy loading because CGLIB is not available. Add CGLIB to your classpath.", e);
    }
  }

  /**
   * Create Proxy Object
   * @param target Target object
   * @param lazyLoader Delay Loader
   * @param configuration Configuration Class
   * @param objectFactory Object Factory
   * @param constructorArgTypes Constructor Type[]
   * @param constructorArgs  Value of constructor []
   * @return
   */
  @Override
  public Object createProxy(Object target, ResultLoaderMap lazyLoader, Configuration configuration, ObjectFactory objectFactory, List<Class<?>> constructorArgTypes, List<Object> constructorArgs) {
    return EnhancedResultObjectProxyImpl.createProxy(target, lazyLoader, configuration, objectFactory, constructorArgTypes, constructorArgs);
  }

  /**
   * Create a deserialization agent
   * @param target target
   * @param unloadedProperties
   * @param objectFactory Object Factory
   * @param constructorArgTypes Array of constructor types
   * @param constructorArgs Constructor Value
   * @return
   */
  public Object createDeserializationProxy(Object target, Map<String, ResultLoaderMap.LoadPair> unloadedProperties, ObjectFactory objectFactory, List<Class<?>> constructorArgTypes, List<Object> constructorArgs) {
    return EnhancedDeserializationProxyImpl.createProxy(target, unloadedProperties, objectFactory, constructorArgTypes, constructorArgs);
  }

  @Override
  public void setProperties(Properties properties) {
      // Not Implemented
  }

  /**
   * Returns a proxy object that calls the intercept method of this class whenever any method is called
   * Enhancer Think of this as a factory for custom classes, such as what interfaces this class needs to implement
   * @param type Target type
   * @param callback Result object proxy implementation class with invoke callback method
   * @param constructorArgTypes Array of constructor types
   * @param constructorArgs Array of values for the corresponding field of the constructor
   * @return
   */
  static Object crateProxy(Class<?> type, Callback callback, List<Class<?>> constructorArgTypes, List<Object> constructorArgs) {
    // enhancer Configure some parameters for the Tuning Agent object
    // Set Callback Method
    // Set Superclass
    //Determine if the incoming target type has writeReplace Method, or configure one that has writeReplace Method's interface (serialized write out)
    Enhancer enhancer = new Enhancer();
    enhancer.setCallback(callback);
    enhancer.setSuperclass(type);
    try {
      type.getDeclaredMethod(WRITE_REPLACE_METHOD);
      // ObjectOutputStream will call writeReplace of objects returned by writeReplace
      if (LogHolder.log.isDebugEnabled()) {
        LogHolder.log.debug(WRITE_REPLACE_METHOD + " method was found on bean " + type + ", make sure it returns this");
      }
    } catch (NoSuchMethodException e) {
      //this enhancer Add one WriteReplaceInterface Interface
      enhancer.setInterfaces(new Class[]{WriteReplaceInterface.class});
    } catch (SecurityException e) {
      // nothing to do here
    }
    //Create an object from a constructor
    //Parametric construction
    //Parametric construction
    Object enhanced;
    if (constructorArgTypes.isEmpty()) {
      enhanced = enhancer.create();
    } else {
      Class<?>[] typesArray = constructorArgTypes.toArray(new Class[constructorArgTypes.size()]);
      Object[] valuesArray = constructorArgs.toArray(new Object[constructorArgs.size()]);
      enhanced = enhancer.create(typesArray, valuesArray);
    }
    return enhanced;
  }

  /**
   * Result object proxy implementation class,
   * It implements the intercept method of the method interceptor
   */
  private static class EnhancedResultObjectProxyImpl implements MethodInterceptor {

    private final Class<?> type;
    private final ResultLoaderMap lazyLoader;
    private final boolean aggressive;
    private final Set<String> lazyLoadTriggerMethods;
    private final ObjectFactory objectFactory;
    private final List<Class<?>> constructorArgTypes;
    private final List<Object> constructorArgs;

    /**
     * Proxy Object Creation
     * @param type Target class type
     * @param lazyLoader Delay Loader
     * @param configuration configuration information
     * @param objectFactory Object Factory
     * @param constructorArgTypes Array of constructor types
     * @param constructorArgs Array of constructor values
     */
    private EnhancedResultObjectProxyImpl(Class<?> type, ResultLoaderMap lazyLoader, Configuration configuration, ObjectFactory objectFactory, List<Class<?>> constructorArgTypes, List<Object> constructorArgs) {
      this.type = type;
      this.lazyLoader = lazyLoader;
      this.aggressive = configuration.isAggressiveLazyLoading();
      this.lazyLoadTriggerMethods = configuration.getLazyLoadTriggerMethods();
      this.objectFactory = objectFactory;
      this.constructorArgTypes = constructorArgTypes;
      this.constructorArgs = constructorArgs;
    }

    /**
     * Create proxy object, assign source object value to proxy object
     * @param target Target object
     * @param lazyLoader Delay Loader
     * @param configuration Configuration object
     * @param objectFactory Object Factory
     * @param constructorArgTypes Array of constructor types
     * @param constructorArgs Array of constructor values
     * @return
     */
    public static Object createProxy(Object target, ResultLoaderMap lazyLoader, Configuration configuration, ObjectFactory objectFactory, List<Class<?>> constructorArgTypes, List<Object> constructorArgs) {
      //Get the type of target
      //Create a result object proxy implementation class that implements cglib Of MethodInterface Interface, complete callback invoke Method)
      final Class<?> type = target.getClass();
      EnhancedResultObjectProxyImpl callback = new EnhancedResultObjectProxyImpl(type, lazyLoader, configuration, objectFactory, constructorArgTypes, constructorArgs);
      Object enhanced = crateProxy(type, callback, constructorArgTypes, constructorArgs);
      PropertyCopier.copyBeanProperties(type, target, enhanced);
      return enhanced;
    }

    /**
     * callback
     * @param enhanced Proxy Object
     * @param method Method
     * @param args Method parameters
     * @param methodProxy Agent Method
     * @return
     * @throws Throwable
     */
    @Override
    public Object intercept(Object enhanced, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
      //Get method name
      final String methodName = method.getName();
      try {
        // Synchronize Getting Delayed Loading Objects
        // If executed writeReplace Method(Serialized Write Out)
        // Instantiate an instance of a target object
        synchronized (lazyLoader) {
          if (WRITE_REPLACE_METHOD.equals(methodName)) {
            Object original;
            if (constructorArgTypes.isEmpty()) {
              original = objectFactory.create(type);
            } else {
              original = objectFactory.create(type, constructorArgTypes, constructorArgs);
            }
            // take enhanced Copy attributes from to orignal In object
            // If the number of delayed loads>0,
            PropertyCopier.copyBeanProperties(type, enhanced, original);
            if (lazyLoader.size() > 0) {
              return new CglibSerialStateHolder(original, lazyLoader.getProperties(), objectFactory, constructorArgTypes, constructorArgs);
            } else {
              return original;
            }
          } else {
            //No writeReplace Method
            // The delay load length is greater than 0 and is not finalize Method
            // configuration Configure the delayed load parameters, which are included in the method that delayed load triggers
            // Delay Loading All Data
            if (lazyLoader.size() > 0 && !FINALIZE_METHOD.equals(methodName)) {
              if (aggressive || lazyLoadTriggerMethods.contains(methodName)) {
                lazyLoader.loadAll();
                // setter Method, remove directly
              } else if (PropertyNamer.isSetter(methodName)) {
                final String property = PropertyNamer.methodToProperty(methodName);
                lazyLoader.remove(property);
                // getter Method, load the property
              } else if (PropertyNamer.isGetter(methodName)) {
                final String property = PropertyNamer.methodToProperty(methodName);
                if (lazyLoader.hasLoader(property)) {
                  lazyLoader.load(property);
                }
              }
            }
          }
        }
        return methodProxy.invokeSuper(enhanced, args);
      } catch (Throwable t) {
        throw ExceptionUtil.unwrapThrowable(t);
      }
    }
  }

  /**
   * He inherits the abstract deserialization proxy and implements method interception
   */
  private static class EnhancedDeserializationProxyImpl extends AbstractEnhancedDeserializationProxy implements MethodInterceptor {

    private EnhancedDeserializationProxyImpl(Class<?> type, Map<String, ResultLoaderMap.LoadPair> unloadedProperties, ObjectFactory objectFactory,
            List<Class<?>> constructorArgTypes, List<Object> constructorArgs) {
      super(type, unloadedProperties, objectFactory, constructorArgTypes, constructorArgs);
    }

    /**
     * Create Proxy Object
     * @param target
     * @param unloadedProperties
     * @param objectFactory
     * @param constructorArgTypes
     * @param constructorArgs
     * @return
     */
    public static Object createProxy(Object target, Map<String, ResultLoaderMap.LoadPair> unloadedProperties, ObjectFactory objectFactory,
            List<Class<?>> constructorArgTypes, List<Object> constructorArgs) {
      final Class<?> type = target.getClass();
      EnhancedDeserializationProxyImpl callback = new EnhancedDeserializationProxyImpl(type, unloadedProperties, objectFactory, constructorArgTypes, constructorArgs);
      Object enhanced = crateProxy(type, callback, constructorArgTypes, constructorArgs);
      PropertyCopier.copyBeanProperties(type, target, enhanced);
      return enhanced;
    }

    @Override
    public Object intercept(Object enhanced, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
      final Object o = super.invoke(enhanced, method, args);
      return o instanceof AbstractSerialStateHolder ? o : methodProxy.invokeSuper(o, args);
    }

    @Override
    protected AbstractSerialStateHolder newSerialStateHolder(Object userBean, Map<String, ResultLoaderMap.LoadPair> unloadedProperties, ObjectFactory objectFactory,
            List<Class<?>> constructorArgTypes, List<Object> constructorArgs) {
      return new CglibSerialStateHolder(userBean, unloadedProperties, objectFactory, constructorArgTypes, constructorArgs);
    }
  }


}

As mentioned above, the proxy method first checks if aggressive is true, and if it is not, then checks if lazyLoadTriggerMethods contains the current method name.As long as only one of these two conditions is true, all of the current entity classes need to be delayed loading.The values of aggressive and lazyLoadTriggerMethods depend on the configuration below.

<setting name="aggressiveLazyLoading" value="false"/>
<setting name="lazyLoadTriggerMethods" value="equals,hashCode"/>

The proxy logic then checks whether the consumer has invoked the setter method of the entity class and, if so, removes the LoadPair corresponding to that property from the loaderMap.Why?The answer is: since the user manually calls the setter method, the user wants to customize the value of a property.At this point, the delayed load logic should no longer modify the value of this property, so here, LoadPair for the property pair is removed from the loaderMap.

Finally, if the consumer invokes a property's getter method and the property is configured for delayed loading, the delayed loading logic is triggered.Next, let's see how delayed loading logic works.

public boolean load(String property) throws SQLException {
    // from loaderMap Remove in property Corresponding LoadPair
    LoadPair pair = loaderMap.remove(property.toUpperCase(Locale.ENGLISH));
    if (pair != null) {
        // Load results
        pair.load();
        return true;
    }
    return false;
}

public void load(final Object userObject) throws SQLException {
    /*
     * Call the loadResult method of ResultLoader to load the result,
     * And set the result to the entity class object through metaResultObject
     */
    this.metaResultObject.setValue(property, this.resultLoader.loadResult());
}

public Object loadResult() throws SQLException {
    // Execute associated queries
    List<Object> list = selectList();
    // Extraction results
    resultObject = resultExtractor.extractObjectFromList(list, targetType);
    return resultObject;
}

private <E> List<E> selectList() throws SQLException {
    Executor localExecutor = executor;
    if (Thread.currentThread().getId() != this.creatorThreadId || localExecutor.isClosed()) {
        localExecutor = newExecutor();
    }
    try {
        // adopt Executor Line query, which has been analyzed before
        // There parameterObject and boundSql That's what we stored before LoadPair And now it's taken directly
        return localExecutor.<E>query(mappedStatement, parameterObject, RowBounds.DEFAULT,
                                      Executor.NO_RESULT_HANDLER, cacheKey, boundSql);
    } finally {
        if (localExecutor != executor) {
            localExecutor.close(false);
        }
    }
}

Okay, delayed loading is almost clear. Let's talk about another proxy method

JavassistProxyFactory

JavassistProxyFactory uses javassist to directly modify the byte code format of the class file.

import java.lang.reflect.Method;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;

import javassist.util.proxy.MethodHandler;
import javassist.util.proxy.Proxy;
import javassist.util.proxy.ProxyFactory;

import org.apache.ibatis.executor.ExecutorException;
import org.apache.ibatis.executor.loader.AbstractEnhancedDeserializationProxy;
import org.apache.ibatis.executor.loader.AbstractSerialStateHolder;
import org.apache.ibatis.executor.loader.ResultLoaderMap;
import org.apache.ibatis.executor.loader.WriteReplaceInterface;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.logging.Log;
import org.apache.ibatis.logging.LogFactory;
import org.apache.ibatis.reflection.ExceptionUtil;
import org.apache.ibatis.reflection.factory.ObjectFactory;
import org.apache.ibatis.reflection.property.PropertyCopier;
import org.apache.ibatis.reflection.property.PropertyNamer;
import org.apache.ibatis.session.Configuration;

/**JavassistProxy Byte Code Generation Agent
 * 1.Create a proxy object and assign the value of the target object to the proxy object, which can implement other interfaces
 * 2. JavassistProxyFactory Implement the ProxyFactory interface createProxy (a method of creating proxy objects)
 * @author Eduardo Macarron
 */
public class JavassistProxyFactory implements org.apache.ibatis.executor.loader.ProxyFactory {

  /**
   * finalize Method (garbage collection)
   */
  private static final String FINALIZE_METHOD = "finalize";

  /**
   * writeReplace(Serialized Write Out Method)
   */
  private static final String WRITE_REPLACE_METHOD = "writeReplace";

  /**
   * Load ProxyFactory, the entry to JavassistProxy
   */
  public JavassistProxyFactory() {
    try {
      Resources.classForName("javassist.util.proxy.ProxyFactory");
    } catch (Throwable e) {
      throw new IllegalStateException("Cannot enable lazy loading because Javassist is not available. Add Javassist to your classpath.", e);
    }
  }

  /**
   * Create proxy
   * @param target Target object
   * @param lazyLoader Delayed loading of Map collections (those attributes require delayed loading)
   * @param configuration Configuration Class
   * @param objectFactory Object Factory
   * @param constructorArgTypes Constructor Type[]
   * @param constructorArgs  Value of constructor []
   * @return
   */
  @Override
  public Object createProxy(Object target, ResultLoaderMap lazyLoader, Configuration configuration, ObjectFactory objectFactory, List<Class<?>> constructorArgTypes, List<Object> constructorArgs) {
    return EnhancedResultObjectProxyImpl.createProxy(target, lazyLoader, configuration, objectFactory, constructorArgTypes, constructorArgs);
  }

  public Object createDeserializationProxy(Object target, Map<String, ResultLoaderMap.LoadPair> unloadedProperties, ObjectFactory objectFactory, List<Class<?>> constructorArgTypes, List<Object> constructorArgs) {
    return EnhancedDeserializationProxyImpl.createProxy(target, unloadedProperties, objectFactory, constructorArgTypes, constructorArgs);
  }

  @Override
  public void setProperties(Properties properties) {
      // Not Implemented
  }

  /**
   * Get the proxy object, that is, invoke the MethodHanlder method before executing the method
   * @param type Target type
   * @param callback callback object
   * @param constructorArgTypes Array of constructor types
   * @param constructorArgs Array of constructor values
   * @return
   */
  static Object crateProxy(Class<?> type, MethodHandler callback, List<Class<?>> constructorArgTypes, List<Object> constructorArgs) {
    // Create a proxy factory class
    // Configuration Superclass
    ProxyFactory enhancer = new ProxyFactory();
    enhancer.setSuperclass(type);
    //Determine if there is writeReplace Method, if this proxy object is not implemented WriteReplaceInterface Interface, this interface has only one writeReplace Method
    try {
      type.getDeclaredMethod(WRITE_REPLACE_METHOD);
      // ObjectOutputStream will call writeReplace of objects returned by writeReplace
      if (LogHolder.log.isDebugEnabled()) {
        LogHolder.log.debug(WRITE_REPLACE_METHOD + " method was found on bean " + type + ", make sure it returns this");
      }
    } catch (NoSuchMethodException e) {
      enhancer.setInterfaces(new Class[]{WriteReplaceInterface.class});
    } catch (SecurityException e) {
      // nothing to do here
    }

    Object enhanced;
    Class<?>[] typesArray = constructorArgTypes.toArray(new Class[constructorArgTypes.size()]);
    Object[] valuesArray = constructorArgs.toArray(new Object[constructorArgs.size()]);
    try {
      // Create a proxy object from the constructor
      enhanced = enhancer.create(typesArray, valuesArray);
    } catch (Exception e) {
      throw new ExecutorException("Error creating lazy proxy.  Cause: " + e, e);
    }
    // Set Callback Object
    ((Proxy) enhanced).setHandler(callback);
    return enhanced;
  }

  /**
   * Implement the MethodHandler interface for Javassist, relative to Cglib's MethodInterceptor
   * The method names of their interfaces are also different, Javassist is invoke, and cglib is intercept, called differently, and the implementation functions are the same
   */
  private static class EnhancedResultObjectProxyImpl implements MethodHandler {

    /**
     * Target type
     */
    private final Class<?> type;
    /**
     * Delay Loading Map Collections
     */
    private final ResultLoaderMap lazyLoader;

    /**
     * Whether to configure delayed loading
     */
    private final boolean aggressive;

    /**
     * Delayed Load Triggering Method
     */
    private final Set<String> lazyLoadTriggerMethods;

    /**
     * Object Factory
     */
    private final ObjectFactory objectFactory;

    /**
     * Array of constructor types
     */
    private final List<Class<?>> constructorArgTypes;

    /**
     * Array of values of constructor type
     */
    private final List<Object> constructorArgs;

    /**
     * Constructor Privateized
     * @param type
     * @param lazyLoader
     * @param configuration
     * @param objectFactory
     * @param constructorArgTypes
     * @param constructorArgs
     */
    private EnhancedResultObjectProxyImpl(Class<?> type, ResultLoaderMap lazyLoader, Configuration configuration, ObjectFactory objectFactory, List<Class<?>> constructorArgTypes, List<Object> constructorArgs) {
      this.type = type;
      this.lazyLoader = lazyLoader;
      this.aggressive = configuration.isAggressiveLazyLoading();
      this.lazyLoadTriggerMethods = configuration.getLazyLoadTriggerMethods();
      this.objectFactory = objectFactory;
      this.constructorArgTypes = constructorArgTypes;
      this.constructorArgs = constructorArgs;
    }

    public static Object createProxy(Object target, ResultLoaderMap lazyLoader, Configuration configuration, ObjectFactory objectFactory, List<Class<?>> constructorArgTypes, List<Object> constructorArgs) {
      // Get the target type
      // Create a EnhancedResultObjectProxyImpl Object, Callback Object
      final Class<?> type = target.getClass();
      EnhancedResultObjectProxyImpl callback = new EnhancedResultObjectProxyImpl(type, lazyLoader, configuration, objectFactory, constructorArgTypes, constructorArgs);
      Object enhanced = crateProxy(type, callback, constructorArgTypes, constructorArgs);
      PropertyCopier.copyBeanProperties(type, target, enhanced);
      return enhanced;
    }

    /**
     * callback
     * @param enhanced Proxy Object
     * @param method Method
     * @param methodProxy Agent Method
     * @param args Entry
     * @return
     * @throws Throwable
     */
    @Override
    public Object invoke(Object enhanced, Method method, Method methodProxy, Object[] args) throws Throwable {
      //Get method name
      final String methodName = method.getName();
      try {
        synchronized (lazyLoader) {
          if (WRITE_REPLACE_METHOD.equals(methodName)) {
            //If the method is writeReplace
            Object original;
            if (constructorArgTypes.isEmpty()) {
              original = objectFactory.create(type);
            } else {
              original = objectFactory.create(type, constructorArgTypes, constructorArgs);
            }
            PropertyCopier.copyBeanProperties(type, enhanced, original);
            if (lazyLoader.size() > 0) {
              return new JavassistSerialStateHolder(original, lazyLoader.getProperties(), objectFactory, constructorArgTypes, constructorArgs);
            } else {
              return original;
            }
          } else {
            //No writeReplace Method
            // The delay load length is greater than 0 and is not finalize Method
            // configuration Configure the delayed load parameters, which are included in the method that delayed load triggers
            // Delay Loading All Data
            if (lazyLoader.size() > 0 && !FINALIZE_METHOD.equals(methodName)) {
              if (aggressive || lazyLoadTriggerMethods.contains(methodName)) {
                lazyLoader.loadAll();
              } else if (PropertyNamer.isSetter(methodName)) {
                final String property = PropertyNamer.methodToProperty(methodName);
                lazyLoader.remove(property);
              } else if (PropertyNamer.isGetter(methodName)) {
                final String property = PropertyNamer.methodToProperty(methodName);
                if (lazyLoader.hasLoader(property)) {
                  lazyLoader.load(property);
                }
              }
            }
          }
        }
        return methodProxy.invoke(enhanced, args);
      } catch (Throwable t) {
        throw ExceptionUtil.unwrapThrowable(t);
      }
    }
  }

  private static class EnhancedDeserializationProxyImpl extends AbstractEnhancedDeserializationProxy implements MethodHandler {

    private EnhancedDeserializationProxyImpl(Class<?> type, Map<String, ResultLoaderMap.LoadPair> unloadedProperties, ObjectFactory objectFactory,
            List<Class<?>> constructorArgTypes, List<Object> constructorArgs) {
      super(type, unloadedProperties, objectFactory, constructorArgTypes, constructorArgs);
    }

    public static Object createProxy(Object target, Map<String, ResultLoaderMap.LoadPair> unloadedProperties, ObjectFactory objectFactory,
            List<Class<?>> constructorArgTypes, List<Object> constructorArgs) {
      final Class<?> type = target.getClass();
      EnhancedDeserializationProxyImpl callback = new EnhancedDeserializationProxyImpl(type, unloadedProperties, objectFactory, constructorArgTypes, constructorArgs);
      Object enhanced = crateProxy(type, callback, constructorArgTypes, constructorArgs);
      PropertyCopier.copyBeanProperties(type, target, enhanced);
      return enhanced;
    }

    @Override
    public Object invoke(Object enhanced, Method method, Method methodProxy, Object[] args) throws Throwable {
      final Object o = super.invoke(enhanced, method, args);
      return o instanceof AbstractSerialStateHolder ? o : methodProxy.invoke(o, args);
    }

    @Override
    protected AbstractSerialStateHolder newSerialStateHolder(Object userBean, Map<String, ResultLoaderMap.LoadPair> unloadedProperties, ObjectFactory objectFactory,
            List<Class<?>> constructorArgTypes, List<Object> constructorArgs) {
      return new JavassistSerialStateHolder(userBean, unloadedProperties, objectFactory, constructorArgTypes, constructorArgs);
    }
  }

  private static class LogHolder {
    private static final Log log = LogFactory.getLog(JavassistProxyFactory.class);
  }

}

The notes are clear enough that I won't get tired of them

Tags: Java Apache SQL Mybatis

Posted on Thu, 07 Nov 2019 22:55:07 -0500 by mark123$