2
头图

Author: Xiao Fu Ge Blog: https://bugstack.cn

Precipitate, share, grow, and let yourself and others gain something! 😄

I. Introduction

你这代码写的,咋这么轴呢!

Speaking of the shaft, it reminds me of what the teacher said when I was in middle school and high school: "Your brain is like a hand-cuffed child!" In Northeast dialect, the hand-cuffed child is the kind of big cotton gloves worn in winter. The cotton in the cotton gloves is all The pressure is heavy and hard, so it is a metaphor for a stupid brain.

Most of the coders who write axis code are coders who have just graduated or just started working. After all, they have little experience and experience, so it is understandable to write some code that is not easy to maintain. For those old coders who have been trained by most of them, in fact, the stability of the code, design experience, and careful logic are relatively much better. Of course, some old code farmers are just old, and the code is still the same code!

Therefore, companies need to recruit young people with young minds. But there's no need to be just an old code farmer with little hair, otherwise who will give you a smooth landing of your wild ideas! Shouldn’t experience, stability, and smoothness be more worthy of pursuit? You have to like the code that is all stunned, write hundreds of bugs, cause a lot of capital losses and customer complaints, and make the boss feel very happy?

2. Goals

In the previous chapter, Brother Fu took everyone's refined XML statement builder to decouple the Mapper information that needs to be processed in parsing XML, including; SQL, input parameters, output parameters, and types, and perform analysis on these information. Logged to the ParameterMapping parameter mapping processing class. Then in this chapter, we will combine the extraction of this part of the parameters to automatically set the parameters for the executed SQL, instead of writing the parameters as fixed as we did before, as shown in Figure 10-1

图 10-1 硬编码参数设置

  • In the process, the executor is called through the DefaultSqlSession#selectOne method, and the parameter setting and result query are executed through the prepared statement handler PreparedStatementHandler.
  • Then the parameter information we process in this process, that is, when each SQL is executed, those places ?号 that need to be replaced are currently processed by hard coding. And that's the problem that this chapter needs to solve. If you just hard-code the parameter settings, you won't be able to operate on all those different types of parameters.
  • Therefore, this chapter needs to combine the disassembly of the SQL parameter information with the statement builder completed in the previous chapter. This chapter will deal with the hard-coded automatic type setting according to the analysis of these parameters. What design patterns are used in this part for different types of parameter settings?

3. Design

Here you can think about it, the processing of parameters is usually the various parameters set by ps.setXxx(i, parameter); when we use JDBC to directly operate the database. Then after the SQL in the automatic parsing XML splits all the parameter types, you should set different types according to different parameters, that is; Long 调用 ps.setLong , String 调用 ps.setString So here you need to use The strategy mode , when parsing SQL, encapsulates the type handler ( that is, the process of implementing the TypeHandler<T> interface ) according to different execution strategies. The overall design is shown in Figure 10-2

图 10-2 策略模式处理参数处理器

  • In fact, there are many types of parameter processing ( Long\String\Object\... ), so the most important manifestation here is the use of strategy mode.
  • This includes selecting the corresponding policy type processor according to the type when constructing the parameter, and filling it into the parameter mapping set. Another aspect is the use of parameters, that is, in the link that executes DefaultSqlSession#selectOne, including parameter settings, according to the different types of parameters, the corresponding processors and input parameter values are obtained. Note: Since the input parameter value may be an attribute in an object, here we use the reflection class tool MetaObject implemented in the previous chapter to obtain the value, so as to avoid hard-coding to obtain the attribute value due to dynamic objects.

4. Realization

1. Engineering structure

 mybatis-step-09
└── src
    ├── main
    │   └── java
    │       └── cn.bugstack.mybatis
    │           ├── binding
    │           │   ├── MapperMethod.java
    │           │   ├── MapperProxy.java
    │           │   ├── MapperProxyFactory.java
    │           │   └── MapperRegistry.java
    │           ├── builder
    │           │   ├── xml
    │           │   │   ├── XMLConfigBuilder.java
    │           │   │   ├── XMLMapperBuilder.java
    │           │   │   └── XMLStatementBuilder.java
    │           │   ├── BaseBuilder.java
    │           │   ├── ParameterExpression.java
    │           │   ├── SqlSourceBuilder.java
    │           │   └── StaticSqlSource.java
    │           ├── datasource
    │           ├── executor
    │           │   ├── resultset
    │           │   │   └── ParameterHandler.java
    │           │   ├── resultset
    │           │   │   ├── DefaultResultSetHandler.java
    │           │   │   └── ResultSetHandler.java
    │           │   ├── statement
    │           │   │   ├── BaseStatementHandler.java
    │           │   │   ├── PreparedStatementHandler.java
    │           │   │   ├── SimpleStatementHandler.java
    │           │   │   └── StatementHandler.java
    │           │   ├── BaseExecutor.java
    │           │   ├── Executor.java
    │           │   └── SimpleExecutor.java
    │           ├── io
    │           ├── mapping
    │           │   ├── BoundSql.java
    │           │   ├── Environment.java
    │           │   ├── MappedStatement.java
    │           │   ├── ParameterMapping.java
    │           │   ├── SqlCommandType.java
    │           │   └── SqlSource.java
    │           ├── parsing
    │           ├── reflection
    │           ├── scripting
    │           │   ├── defaults
    │           │   │   └── DefaultParameterHandler.java
    │           │   ├── xmltags
    │           │   │   ├── DynamicContext.java
    │           │   │   ├── MixedSqlNode.java
    │           │   │   ├── SqlNode.java
    │           │   │   ├── StaticTextSqlNode.java
    │           │   │   ├── XMLLanguageDriver.java
    │           │   │   └── XMLScriptBuilder.java
    │           │   ├── LanguageDriver.java
    │           │   └── LanguageDriverRegistry.java
    │           ├── session
    │           │   ├── defaults
    │           │   │   ├── DefaultSqlSession.java
    │           │   │   └── DefaultSqlSessionFactory.java
    │           │   ├── Configuration.java
    │           │   ├── ResultHandler.java
    │           │   ├── SqlSession.java
    │           │   ├── SqlSessionFactory.java
    │           │   ├── SqlSessionFactoryBuilder.java
    │           │   └── TransactionIsolationLevel.java
    │           ├── transaction
    │           └── type
    │               ├── BaseTypeHandler.java
    │               ├── JdbcType.java
    │               ├── LongTypeHandler.java
    │               ├── StringTypeHandler.java
    │               ├── TypeAliasRegistry.java
    │               ├── TypeHandler.java
    │               └── TypeHandlerRegistry.java
    └── test
        ├── java
        │   └── cn.bugstack.mybatis.test.dao
        │       ├── dao
        │       │   └── IUserDao.java
        │       ├── po
        │       │   └── User.java
        │       └── ApiTest.java
        └── resources
            ├── mapper
            │   └──User_Mapper.xml
            └── mybatis-config-datasource.xml

Project source code : https://github.com/fuzhengwei/small-mybatis

Use the strategy pattern to handle the parameter processor core class relationship, as shown in Figure 10-3

图 10-3 使用策略模式,处理参数处理器核心类关系

The core processing is mainly divided into three parts; type processing, parameter setting, parameter use;

  • To define the TypeHandler type handler strategy interface, implement different processing strategies, including; Long, String, Integer, etc. Here we only implement 2 types first, readers can add other types according to this structure during the learning process.
  • After the implementation of the type policy processor is completed, it needs to be registered in the processor registration machine. The subsequent setting or use of other module parameters is obtained from the Configuration and used by the TypeHandlerRegistry.
  • Then, with such a strategy processor, when parsing SQL, the corresponding strategy processor can be set to the BoundSql#parameterMappings parameter according to different types, and subsequent use is also obtained from here.

2. Input parameter calibration

Here we have to solve a small problem first. I wonder if the reader has noticed the transmission of such a parameter in the source code we have implemented, as shown in Figure 10-4

图 10-4 参数设置时入参获取

  • After the parameters here are passed, the 0th parameter needs to be obtained, and it is hard-coded and fixed. Why is this? Where does the 0th parameter come from? Isn't the method called in our interface one parameter? Like: User queryUserInfoById(Long id);
  • In fact, this parameter comes from the mapper proxy class MapperProxy#invoke. Because the method invoked by invoke reflection, the input parameter is Object[] args, so this parameter is passed to the subsequent parameter settings. And our DAO test class is a known fixed parameter, so the 0th parameter is hard-coded later.

    • JDK reflection call method operation fixed method input parameters
  • Then combined with such a problem, we need to perform a signature operation on the method according to the method information, so as to convert the input parameter information into the method information. For example, arrays are converted to corresponding objects.

See the source code for details : cn.bugstack.mybatis.binding.MapperMethod

 public class MapperMethod {

    public Object execute(SqlSession sqlSession, Object[] args) {
        Object result = null;
        switch (command.getType()) {
            case SELECT:
                Object param = method.convertArgsToSqlCommandParam(args);
                result = sqlSession.selectOne(command.getName(), param);
                break;
            default:
                throw new RuntimeException("Unknown execution method for: " + command.getName());
        }
        return result;
    }

    /**
     * 方法签名
     */
    public static class MethodSignature {

        public Object convertArgsToSqlCommandParam(Object[] args) {
            final int paramCount = params.size();
            if (args == null || paramCount == 0) {
                // 如果没参数
                return null;
            } else if (paramCount == 1) {
                return args[params.keySet().iterator().next().intValue()];
            } else {
                // 否则,返回一个ParamMap,修改参数名,参数名就是其位置
                final Map<String, Object> param = new ParamMap<Object>();
                int i = 0;
                for (Map.Entry<Integer, String> entry : params.entrySet()) {
                    // 1.先加一个#{0},#{1},#{2}...参数
                    param.put(entry.getValue(), args[entry.getKey().intValue()]);
                    // ...
                }
                return param;
            }
        }

    }
}
  • In the mapper method, MapperMethod#execute passes the original parameter args directly to the SqlSession#selectOne method, and adjusts it to pass the object after conversion.
  • In fact, the conversion operation here comes from the acquisition and processing of parameters by Method#getParameterTypes, which is compared with args. If it is a single parameter, it will directly return the corresponding node value under the tree structure of the parameter. If it is not a single type, it needs to be processed in a loop, so that the converted parameters can be used directly.

3. Parameter strategy processor

In the source package of Mybatis, there is a type package, which provides a set of parameter processing strategies. It defines the interface of the type processor, implements it by the abstract template and defines the standard process, and assigns abstract methods to subclasses for implementation. These subclasses are the specific implementation of each type processor.

3.1 Policy Interface

See the source code : cn.bugstack.mybatis.type.TypeHandler

 public interface TypeHandler<T> {

    /**
     * 设置参数
     */
    void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException;

}
  • First define an interface of a type processor, which is similar to our daily business development, just like if it is a product to be shipped, define a unified standard interface, and then implement different shipping strategies based on this interface.
  • The same is true for setting parameters here. All different types of parameters can be extracted from these standard parameter fields and exceptions, and subsequent subclasses can be implemented according to this standard. There are 30+ types of processing in the Mybatis source code

3.2 Template mode

See the source code : cn.bugstack.mybatis.type.BaseTypeHandler

 public abstract class BaseTypeHandler<T> implements TypeHandler<T> {

    @Override
    public void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException {
        // 定义抽象方法,由子类实现不同类型的属性设置
        setNonNullParameter(ps, i, parameter, jdbcType);
    }

    protected abstract void setNonNullParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException;

}
  • The process template definition of the abstract base class facilitates the judgment and processing of some parameters. However, we don't need so many process verifications yet, so here is just a most basic abstract method setNonNullParameter is defined and called.
  • However, there is such a structure, which can make everyone more aware of the framework of the entire Mybatis source code, which is convenient for subsequent reading or expansion of this part of the source code, there is a sense of the framework structure.

3.3 Subclass Implementation

See the source code for details : cn.bugstack.mybatis.type.*

 /**
 * @description Long类型处理器
 */
public class LongTypeHandler extends BaseTypeHandler<Long> {

    @Override
    protected void setNonNullParameter(PreparedStatement ps, int i, Long parameter, JdbcType jdbcType) throws SQLException {
        ps.setLong(i, parameter);
    }

}

/**
 * @description String类型处理器
 */
public class StringTypeHandler extends BaseTypeHandler<String>{

    @Override
    protected void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException {
        ps.setString(i, parameter);
    }

}
  • Here is an example of the interface implementation, namely: LongTypeHandler, StringTypeHandler, there are many other types in the Mybatis source code, here we don't need to implement so many for the time being, as long as the processing and encoding methods are clear. Everyone is learning, you can try to add a few other types for learning verification

3.4 Type registration machine

The type handler registration machine TypeHandlerRegistry is implemented by us in the previous chapter. Here, we only need to register new types under this class structure.

See the source code for details : cn.bugstack.mybatis.type.TypeHandlerRegistry

 public final class TypeHandlerRegistry {

    private final Map<JdbcType, TypeHandler<?>> JDBC_TYPE_HANDLER_MAP = new EnumMap<>(JdbcType.class);
    private final Map<Type, Map<JdbcType, TypeHandler<?>>> TYPE_HANDLER_MAP = new HashMap<>();
    private final Map<Class<?>, TypeHandler<?>> ALL_TYPE_HANDLERS_MAP = new HashMap<>();

    public TypeHandlerRegistry() {
        register(Long.class, new LongTypeHandler());
        register(long.class, new LongTypeHandler());

        register(String.class, new StringTypeHandler());
        register(String.class, JdbcType.CHAR, new StringTypeHandler());
        register(String.class, JdbcType.VARCHAR, new StringTypeHandler());
    }
 
    //...
}
  • Here in the constructor, two types of registrars, LongTypeHandler and StringTypeHandler, are newly added.
  • At the same time, it can be noticed that whether it is an object type or a primitive type, it is a type processor. It's just that one more is registered at the time of registration. This mode of operation is the same as in our usual business development. One is multi-registration, and the other is judgment processing.

4. Parameter construction

Compared with the content completed in the previous chapters, this chapter needs to add the content of the parameter processor to the SqlSourceBuilder source code builder to create the parameter mapping ParameterMapping. Because only in this way can the corresponding type of processor be easily obtained from the parameter map for use.

Then it is necessary to improve ParameterMapping, add TypeHandler attribute information, and build parameter mapping when ParameterMappingTokenHandler#buildParameterMapping processes parameter mapping. This part is the perfect part of refinement in the implementation process of the previous chapter, as shown in Figure 10-6

图 10-6 构建参数映射

Then combined with the previous chapter, here we start to expand the type settings. Also pay attention to the use of the MetaClass reflection tool class

See the source code for details : cn.bugstack.mybatis.builder.SqlSourceBuilder

 // 构建参数映射
private ParameterMapping buildParameterMapping(String content) {
    // 先解析参数映射,就是转化成一个 HashMap | #{favouriteSection,jdbcType=VARCHAR}
    Map<String, String> propertiesMap = new ParameterExpression(content);
    String property = propertiesMap.get("property");
    Class<?> propertyType;
    if (typeHandlerRegistry.hasTypeHandler(parameterType)) {
        propertyType = parameterType;
    } else if (property != null) {
        MetaClass metaClass = MetaClass.forClass(parameterType);
        if (metaClass.hasGetter(property)) {
            propertyType = metaClass.getGetterType(property);
        } else {
            propertyType = Object.class;
        }
    } else {
        propertyType = Object.class;
    }
    logger.info("构建参数映射 property:{} propertyType:{}", property, propertyType);
    ParameterMapping.Builder builder = new ParameterMapping.Builder(configuration, property, propertyType);
    return builder.build();
}
  • This part is to refine the parameters and build the mapping relationship of the parameters. The first is to judge whether the corresponding parameter type is in the TypeHandlerRegistry register. If not, disassemble the object and obtain the propertyType according to the property.
  • This section also uses the use of the MetaClass reflection tool class. Its existence can make it more convenient for us to deal with. Otherwise, we need to write a reflection class to obtain object attributes.

5. Parameter usage

After the parameters are constructed, the parameters can be set and used when DefaultSqlSession#selectOne is called. Then the link relationship here; Executor#query - > SimpleExecutor#doQuery -> StatementHandler#parameterize -> PreparedStatementHandler#parameterize -> ParameterHandler#setParameters When you go to ParameterHandler#setParameters, you can see that the parameters are set cyclically according to the different processors of the parameters.

See the source code for details : cn.bugstack.mybatis.scripting.defaults.DefaultParameterHandler

 public class DefaultParameterHandler implements ParameterHandler {

    @Override
    public void setParameters(PreparedStatement ps) throws SQLException {
        List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
        if (null != parameterMappings) {
            for (int i = 0; i < parameterMappings.size(); i++) {
                ParameterMapping parameterMapping = parameterMappings.get(i);
                String propertyName = parameterMapping.getProperty();
                Object value;
                if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
                    value = parameterObject;
                } else {
                    // 通过 MetaObject.getValue 反射取得值设进去
                    MetaObject metaObject = configuration.newMetaObject(parameterObject);
                    value = metaObject.getValue(propertyName);
                }
                JdbcType jdbcType = parameterMapping.getJdbcType();

                // 设置参数
                logger.info("根据每个ParameterMapping中的TypeHandler设置对应的参数信息 value:{}", JSON.toJSONString(value));
                TypeHandler typeHandler = parameterMapping.getTypeHandler();
                typeHandler.setParameter(ps, i + 1, value, jdbcType);
            }
        }
    }

}
  • The parameter setting of each loop is to obtain the ParameterMapping collection from BoundSql for loop operation, and this collection parameter is processed when we build the parameter mapping in the previous ParameterMappingTokenHandler#buildParameterMapping.
  • When setting the parameter, judge whether it is a basic type according to the information of the parameterObject input parameter of the parameter. If not, it will be disassembled and obtained from the object (that is, an object A includes attribute b). After the processing is completed, the corresponding input can be accurately obtained. parameter value. Because the method signature has been processed in the mapper method MapperMethod, the input parameters here are more convenient to use
  • After the basic information is obtained, the corresponding TypeHandler type handler is obtained according to the parameter type, that is, LongTypeHandler, StringTypeHandler, etc. are found. After it is determined, the corresponding parameters can be set. typeHandler.setParameter(ps, i + 1, value , jdbcType) in this way decouples our hardcoded operations.

5. Test

1. Prepare in advance

1.1 Create library table

Create a database named mybatis and create a table user and add test data in the library, as follows:

 CREATE TABLE
    USER
    (
        id bigint NOT NULL AUTO_INCREMENT COMMENT '自增ID',
        userId VARCHAR(9) COMMENT '用户ID',
        userHead VARCHAR(16) COMMENT '用户头像',
        createTime TIMESTAMP NULL COMMENT '创建时间',
        updateTime TIMESTAMP NULL COMMENT '更新时间',
        userName VARCHAR(64),
        PRIMARY KEY (id)
    )
    ENGINE=InnoDB DEFAULT CHARSET=utf8;
    
insert into user (id, userId, userHead, createTime, updateTime, userName) values (1, '10001', '1_04', '2022-04-13 00:00:00', '2022-04-13 00:00:00', '小傅哥');

1.2 Configure the data source

 <environments default="development">
    <environment id="development">
        <transactionManager type="JDBC"/>
        <dataSource type="POOLED">
            <property name="driver" value="com.mysql.jdbc.Driver"/>
            <property name="url" value="jdbc:mysql://127.0.0.1:3306/mybatis?useUnicode=true"/>
            <property name="username" value="root"/>
            <property name="password" value="123456"/>
        </dataSource>
    </environment>
</environments>
  • Configure data source information through mybatis-config-datasource.xml , including: driver, url, username, password
  • Here dataSource can be configured as DRUID, UNPOOLED and POOLED for testing and verification.

1.3 Configure Mapper

 <select id="queryUserInfoById" parameterType="java.lang.Long" resultType="cn.bugstack.mybatis.test.po.User">
    SELECT id, userId, userName, userHead
    FROM user
    where id = #{id}
</select>

<select id="queryUserInfo" parameterType="cn.bugstack.mybatis.test.po.User" resultType="cn.bugstack.mybatis.test.po.User">
    SELECT id, userId, userName, userHead
    FROM user
    where id = #{id} and userId = #{userId}
</select>
  • This part does not need to be adjusted for the time being. At present, it is only a parameter of the type of input parameter. After we complete this part of the content, we will provide more other parameters for verification.

2. Unit testing

See the source code for details : cn.bugstack.mybatis.test.ApiTest

 @Before
public void init() throws IOException {
    // 1. 从SqlSessionFactory中获取SqlSession
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsReader("mybatis-config-datasource.xml"));
    sqlSession = sqlSessionFactory.openSession();
}
  • Because next we need to verify two different input parameters of the unit test, respectively to test the basic type parameters and object type parameters.

2.1 Basic type parameters

 @Test
public void test_queryUserInfoById() {
    // 1. 获取映射器对象
    IUserDao userDao = sqlSession.getMapper(IUserDao.class);
    // 2. 测试验证:基本参数
    User user = userDao.queryUserInfoById(1L);
    logger.info("测试结果:{}", JSON.toJSONString(user));
}

 07:40:08.531 [main] INFO  c.b.mybatis.builder.SqlSourceBuilder - 构建参数映射 property:id propertyType:class java.lang.Long
07:40:08.598 [main] INFO  c.b.m.s.defaults.DefaultSqlSession - 执行查询 statement:cn.bugstack.mybatis.test.dao.IUserDao.queryUserInfoById parameter:1
07:40:08.875 [main] INFO  c.b.m.d.pooled.PooledDataSource - Created connection 183284570.
07:40:08.894 [main] INFO  c.b.m.s.d.DefaultParameterHandler - 根据每个ParameterMapping中的TypeHandler设置对应的参数信息 value:1
07:40:08.961 [main] INFO  cn.bugstack.mybatis.test.ApiTest - 测试结果:{"id":1,"userHead":"1_04","userId":"10001","userName":"小傅哥"}
  • During the test, you can break the point in DefaultParameterHandler#setParameters to verify the method parameters and the obtained type handler. Here, the test verification is passed, and the input parameter information of the basic type object can be satisfied.

2.2 Object Type Parameters

 @Test
public void test_queryUserInfo() {
    // 1. 获取映射器对象
    IUserDao userDao = sqlSession.getMapper(IUserDao.class);
    // 2. 测试验证:对象参数
    User user = userDao.queryUserInfo(new User(1L, "10001"));
    logger.info("测试结果:{}", JSON.toJSONString(user));
}

 07:41:11.025 [main] INFO  c.b.mybatis.builder.SqlSourceBuilder - 构建参数映射 property:userId propertyType:class java.lang.String
07:41:11.232 [main] INFO  c.b.m.s.defaults.DefaultSqlSession - 执行查询 statement:cn.bugstack.mybatis.test.dao.IUserDao.queryUserInfo parameter:{"id":1,"userId":"10001"}
07:41:11.638 [main] INFO  c.b.m.d.pooled.PooledDataSource - Created connection 402405659.
07:41:11.661 [main] INFO  c.b.m.s.d.DefaultParameterHandler - 根据每个ParameterMapping中的TypeHandler设置对应的参数信息 value:1
07:43:28.516 [main] INFO  c.b.m.s.d.DefaultParameterHandler - 根据每个ParameterMapping中的TypeHandler设置对应的参数信息 value:"10001"
07:43:30.820 [main] INFO  cn.bugstack.mybatis.test.ApiTest - 测试结果:{"id":1,"userHead":"1_04","userId":"10001","userName":"小傅哥"}
  • This case mainly verifies that when the object parameter User contains two properties, check our code processing process to verify whether the two type processors can be correctly obtained and the process of setting the parameters respectively.
  • From the test results, it can be seen that the test passed, and the construction and use of the relevant parameters are printed.

6. Summary

  • In this chapter, we have connected the basic process of an ORM framework, and can complete the processing of simple SQL without hard coding. Readers can carefully read the subcontracting structure contained in the current framework. For example: construction, binding, mapping, reflection, execution, type, transaction, data source, etc., try to draw their link relationship, which will make you more clear about the current code decoupling structure.
  • The more important embodiment in this chapter is the strategic design of parameter types. Through strategy decoupling and template definition process, our entire parameter setting becomes clearer, so there is no need for hard coding.
  • In addition, there are also some detailed function points, such as adding method signatures in MapperMethod, creating and using type processors, and using reflector tool classes such as MetaObject for processing. For the function points of these details, readers need to debug and verify in the process of learning in order to better absorb the skills and experience of such coding design.

小傅哥
4.7k 声望28.4k 粉丝

CodeGuide | 程序员编码指南 - 原创文章、案例源码、资料书籍、简历模版等下载。