一、前言
为什么,读不懂框架源码?
我们都知道作为一个程序员,如果想学习到更深层次的技术,就需要阅读大量的框架源码,学习这些框架源码中的开发套路和设计思想,从而提升自己的编程能力。
事大家都清楚,但在实操上,很多码农根本没法阅读框架源码。首先一个非常大的问题是,面对如此庞大的框架源码,不知道从哪下手。与平常的业务需求开发相比,框架源码中运用了大量的设计原则和设计模式对系统功能进行解耦和实现,也使用了不少如反射、代理、字节码等相关技术。
当你还以为是平常的业务需求中的实例化对象调用方法,去找寻源码中的流程时,可能根本就找不到它是何时发起调用的、怎么进行传参、在哪处理赋值的等一连串的问题,都把一个好码农劝退在开始学习的路上。
二、目标
不知道大家在学习《手写 Mybatis》的过程中,是否有对照 Mybatis 源码一起学习,如果你有对照源码,那么大概率会发现我们在实现数据源池化时,对于属性信息的获取,采用的是硬编码的方式。如图 8-1 所示
图 8-1 数据源池化配置获取
- 也就是 props.getProperty("driver")、props.getProperty("url") 等属性,都是通过手动编码的方式获取的。
- 那其实像 driver、url、username、password 不都是标准的固定字段吗,这样获取有什么不对的。如果按照我们现在的理解来说,并没有什么不对,但其实除了这些字段以外,可能还有时候会配置一些扩展字段,那么怎么获取呢,总不能每次都是硬编码。
- 所以如果你有阅读 Mybatis 的源码,会发现这里使用了 Mybatis 自己实现的元对象反射工具类,可以完成一个对象的属性的反射填充。这块的工具类叫做 MetaObject 并提供相应的;元对象、对象包装器、对象工厂、对象包装工厂以及 Reflector 反射器的使用。那么本章节我们就来实现一下反射工具包的内容,因为随着我们后续的开发,也会有很多地方都需要使用反射器优雅的处理我们的属性信息。这也能为你添加一些关于反射的强大的使用!
三、设计
如果说我们需要对一个对象的所提供的属性进行统一的设置和获取值的操作,那么就需要把当前这个被处理的对象进行解耦,提取出它所有的属性和方法,并按照不同的类型进行反射处理,从而包装成一个工具包。如图 8-2 所示
图 8-2 对象属性反射处理
- 其实整个设计过程都以围绕如何拆解对象并提供反射操作为主,那么对于一个对象来说,它所包括的有对象的构造函数、对象的属性、对象的方法。而对象的方法因为都是获取和设置值的操作,所以基本都是get、set处理,所以需要把这些方法在对象拆解的过程中需要摘取出来进行保存。
- 当真正的开始操作时,则会依赖于已经实例化的对象,对其进行属性处理。而这些处理过程实际都是在使用 JDK 所提供的反射进行操作的,而反射过程中的方法名称、入参类型都已经被我们拆解和处理了,最终使用的时候直接调用即可。
四、实现
1. 工程结构
mybatis-step-06
└── src
├── main
│ └── java
│ └── cn.bugstack.mybatis
│ ├── binding
│ ├── builder
│ ├── datasource
│ │ ├── druid
│ │ │ └── DruidDataSourceFactory.java
│ │ ├── pooled
│ │ │ ├── PooledConnection.java
│ │ │ ├── PooledDataSource.java
│ │ │ ├── PooledDataSourceFactory.java
│ │ │ └── PoolState.java
│ │ ├── unpooled
│ │ │ ├── UnpooledDataSource.java
│ │ │ └── UnpooledDataSourceFactory.java
│ │ └── DataSourceFactory.java
│ ├── executor
│ ├── io
│ ├── mapping
│ ├── reflection
│ │ ├── factory
│ │ │ ├── DefaultObjectFactory.java
│ │ │ └── ObjectFactory.java
│ │ ├── invoker
│ │ │ ├── GetFieldInvoker.java
│ │ │ ├── Invoker.java
│ │ │ ├── MethodInvoker.java
│ │ │ └── SetFieldInvoker.java
│ │ ├── property
│ │ │ ├── PropertyNamer.java
│ │ │ └── PropertyTokenizer.java
│ │ ├── wrapper
│ │ │ ├── BaseWrapper.java
│ │ │ ├── BeanWrapper.java
│ │ │ ├── CollectionWrapper.java
│ │ │ ├── DefaultObjectWrapperFactory.java
│ │ │ ├── MapWrapper.java
│ │ │ ├── ObjectWrapper.java
│ │ │ └── ObjectWrapperFactory.java
│ │ ├── MetaClass.java
│ │ ├── MetaObject.java
│ │ ├── Reflector.java
│ │ └── SystemMetaObject.java
│ ├── session
│ ├── transaction
│ └── type
└── test
├── java
│ └── cn.bugstack.mybatis.test.dao
│ ├── dao
│ │ └── IUserDao.java
│ ├── po
│ │ └── User.java
│ ├── ApiTest.java
│ └── ReflectionTest.java
└── resources
├── mapper
│ └──User_Mapper.xml
└── mybatis-config-datasource.xml
元对象反射工具类,处理对象的属性设置和获取操作核心类,如图 8-3 所示:
图 8-3 所示 元对象反射工具类,处理对象的属性设置和获取操作核心类
- 以 Reflector 反射器类处理对象类中的 get/set 属性,包装为可调用的 Invoker 反射类,这样在对 get/set 方法反射调用的时候,使用方法名称获取对应的 Invoker 即可 getGetInvoker(String propertyName)。
- 有了反射器的处理,之后就是对原对象的包装了,由 SystemMetaObject 提供创建 MetaObject 元对象的方法,将我们需要处理的对象进行拆解和 ObjectWrapper 对象包装处理。因为一个对象的类型还需要进行一条细节的处理,以及属性信息的拆解,例如:班级[0].学生.成绩 这样一个类中的关联类的属性,则需要进行递归的方式拆解处理后,才能设置和获取属性值。
- 最终在 Mybatis 其他的地方就可以,有需要属性值设定时,就可以使用到反射工具包进行处理了。这里首当其冲的我们会把数据源池化中关于 Properties 属性的处理使用反射工具类进行改造。参考本章节中对应的源码类
2. 反射调用者
关于对象类中的属性值获取和设置可以分为 Field 字段的 get/set 还有普通的 Method 的调用,为了减少使用方的过多的处理,这里可以把集中调用者的实现包装成调用策略,统一接口不同策略不同的实现类。
定义接口
public interface Invoker {
Object invoke(Object target, Object[] args) throws Exception;
Class<?> getType();
}
无论任何类型的反射调用,都离不开对象和入参,只要我们把这两个字段和返回结果定义的通用,就可以包住不同策略的实现类了。
2.1 MethodInvoker
源码详见:cn.bugstack.mybatis.reflection.invoker.MethodInvoker
public class MethodInvoker implements Invoker {
private Class<?> type;
private Method method;
@Override
public Object invoke(Object target, Object[] args) throws Exception {
return method.invoke(target, args);
}
}
提供方法反射调用处理,构造函数会传入对应的方法类型。
2.2 GetFieldInvoker
源码详见:cn.bugstack.mybatis.reflection.invoker.GetFieldInvoker
public class GetFieldInvoker implements Invoker {
private Field field;
@Override
public Object invoke(Object target, Object[] args) throws Exception {
return field.get(target);
}
}
getter 方法的调用者处理,因为get是有返回值的,所以直接对 Field 字段操作完后直接返回结果。
2.3 SetFieldInvoker
源码详见:cn.bugstack.mybatis.reflection.invoker.SetFieldInvoker
public class SetFieldInvoker implements Invoker {
private Field field;
@Override
public Object invoke(Object target, Object[] args) throws Exception {
field.set(target, args[0]);
return null;
}
}
setter 方法的调用者处理,因为set只是设置值,所以这里就只返回一个 null 就可以了。
3. 反射器解耦对象
Reflector 反射器专门用于解耦对象信息的,只有把一个对象信息所含带的属性、方法以及关联的类都以此解析出来,才能满足后续对属性值的设置和获取。
源码详见:cn.bugstack.mybatis.reflection.Reflector
public class Reflector {
private static boolean classCacheEnabled = true;
private static final String[] EMPTY_STRING_ARRAY = new String[0];
// 线程安全的缓存
private static final Map<Class<?>, Reflector> REFLECTOR_MAP = new ConcurrentHashMap<>();
private Class<?> type;
// get 属性列表
private String[] readablePropertyNames = EMPTY_STRING_ARRAY;
// set 属性列表
private String[] writeablePropertyNames = EMPTY_STRING_ARRAY;
// set 方法列表
private Map<String, Invoker> setMethods = new HashMap<>();
// get 方法列表
private Map<String, Invoker> getMethods = new HashMap<>();
// set 类型列表
private Map<String, Class<?>> setTypes = new HashMap<>();
// get 类型列表
private Map<String, Class<?>> getTypes = new HashMap<>();
// 构造函数
private Constructor<?> defaultConstructor;
private Map<String, String> caseInsensitivePropertyMap = new HashMap<>();
public Reflector(Class<?> clazz) {
this.type = clazz;
// 加入构造函数
addDefaultConstructor(clazz);
// 加入 getter
addGetMethods(clazz);
// 加入 setter
addSetMethods(clazz);
// 加入字段
addFields(clazz);
readablePropertyNames = getMethods.keySet().toArray(new String[getMethods.keySet().size()]);
writeablePropertyNames = setMethods.keySet().toArray(new String[setMethods.keySet().size()]);
for (String propName : readablePropertyNames) {
caseInsensitivePropertyMap.put(propName.toUpperCase(Locale.ENGLISH), propName);
}
for (String propName : writeablePropertyNames) {
caseInsensitivePropertyMap.put(propName.toUpperCase(Locale.ENGLISH), propName);
}
}
// ... 省略处理方法
}
Reflector 反射器类中提供了各类属性、方法、类型以及构造函数的保存操作,当调用反射器时会通过构造函数的处理,逐步从对象类中拆解出这些属性信息,便于后续反射使用。
读者在对这部分源码学习时,可以参考对应的类和这里的处理方法,这些方法都是一些对反射的操作,获取出基本的类型、方法信息,并进行整理存放。
4. 元类包装反射器
Reflector 反射器类提供的是最基础的核心功能,很多方法也都是私有的,为了更加方便的使用,还需要做一层元类的包装。在元类 MetaClass 提供必要的创建反射器以及使用反射器获取 get/set 的 Invoker 反射方法。
源码详见:cn.bugstack.mybatis.reflection.MetaClass
public class MetaClass {
private Reflector reflector;
private MetaClass(Class<?> type) {
this.reflector = Reflector.forClass(type);
}
public static MetaClass forClass(Class<?> type) {
return new MetaClass(type);
}
public String[] getGetterNames() {
return reflector.getGetablePropertyNames();
}
public String[] getSetterNames() {
return reflector.getSetablePropertyNames();
}
public Invoker getGetInvoker(String name) {
return reflector.getGetInvoker(name);
}
public Invoker getSetInvoker(String name) {
return reflector.getSetInvoker(name);
}
// ... 方法包装
}
MetaClass 元类相当于是对我们需要处理对象的包装,解耦一个原对象,包装出一个元类。而这些元类、对象包装器以及对象工厂等,再组合出一个元对象。相当于说这些元类和元对象都是对我们需要操作的原对象解耦后的封装。有了这样的操作,就可以让我们处理每一个属性或者方法了。
5. 对象包装器Wrapper
对象包装器相当于是更加进一步反射调用包装处理,同时也为不同的对象类型提供不同的包装策略。框架源码都喜欢使用设计模式,从来不是一行行ifelse的代码
在对象包装器接口中定义了更加明确的需要使用的方法,包括定义出了 get/set 标准的通用方法、获取get\set属性名称和属性类型,以及添加属性等操作。
对象包装器接口
public interface ObjectWrapper {
// get
Object get(PropertyTokenizer prop);
// set
void set(PropertyTokenizer prop, Object value);
// 查找属性
String findProperty(String name, boolean useCamelCaseMapping);
// 取得getter的名字列表
String[] getGetterNames();
// 取得setter的名字列表
String[] getSetterNames();
//取得setter的类型
Class<?> getSetterType(String name);
// 取得getter的类型
Class<?> getGetterType(String name);
// ... 省略
}
后续所有实现了对象包装器接口的实现类,都需要提供这些方法实现,基本有了这些方法,也就能非常容易的处理一个对象的反射操作了。
无论你是设置属性、获取属性、拿到对应的字段列表还是类型都是可以满足的。
6. 元对象封装
在有了反射器、元类、对象包装器以后,在使用对象工厂和包装工厂,就可以组合出一个完整的元对象操作类了。因为所有的不同方式的使用,包括:包装器策略、包装工程、统一的方法处理,这些都需要一个统一的处理方,也就是我们的元对象进行管理。
源码详见:cn.bugstack.mybatis.reflection.MetaObject
public class MetaObject {
// 原对象
private Object originalObject;
// 对象包装器
private ObjectWrapper objectWrapper;
// 对象工厂
private ObjectFactory objectFactory;
// 对象包装工厂
private ObjectWrapperFactory objectWrapperFactory;
private MetaObject(Object object, ObjectFactory objectFactory, ObjectWrapperFactory objectWrapperFactory) {
this.originalObject = object;
this.objectFactory = objectFactory;
this.objectWrapperFactory = objectWrapperFactory;
if (object instanceof ObjectWrapper) {
// 如果对象本身已经是ObjectWrapper型,则直接赋给objectWrapper
this.objectWrapper = (ObjectWrapper) object;
} else if (objectWrapperFactory.hasWrapperFor(object)) {
// 如果有包装器,调用ObjectWrapperFactory.getWrapperFor
this.objectWrapper = objectWrapperFactory.getWrapperFor(this, object);
} else if (object instanceof Map) {
// 如果是Map型,返回MapWrapper
this.objectWrapper = new MapWrapper(this, (Map) object);
} else if (object instanceof Collection) {
// 如果是Collection型,返回CollectionWrapper
this.objectWrapper = new CollectionWrapper(this, (Collection) object);
} else {
// 除此以外,返回BeanWrapper
this.objectWrapper = new BeanWrapper(this, object);
}
}
public static MetaObject forObject(Object object, ObjectFactory objectFactory, ObjectWrapperFactory objectWrapperFactory) {
if (object == null) {
// 处理一下null,将null包装起来
return SystemMetaObject.NULL_META_OBJECT;
} else {
return new MetaObject(object, objectFactory, objectWrapperFactory);
}
}
// 取得值
// 如 班级[0].学生.成绩
public Object getValue(String name) {
PropertyTokenizer prop = new PropertyTokenizer(name);
if (prop.hasNext()) {
MetaObject metaValue = metaObjectForProperty(prop.getIndexedName());
if (metaValue == SystemMetaObject.NULL_META_OBJECT) {
// 如果上层就是null了,那就结束,返回null
return null;
} else {
// 否则继续看下一层,递归调用getValue
return metaValue.getValue(prop.getChildren());
}
} else {
return objectWrapper.get(prop);
}
}
// 设置值
// 如 班级[0].学生.成绩
public void setValue(String name, Object value) {
PropertyTokenizer prop = new PropertyTokenizer(name);
if (prop.hasNext()) {
MetaObject metaValue = metaObjectForProperty(prop.getIndexedName());
if (metaValue == SystemMetaObject.NULL_META_OBJECT) {
if (value == null && prop.getChildren() != null) {
// don't instantiate child path if value is null
// 如果上层就是 null 了,还得看有没有儿子,没有那就结束
return;
} else {
// 否则还得 new 一个,委派给 ObjectWrapper.instantiatePropertyValue
metaValue = objectWrapper.instantiatePropertyValue(name, prop, objectFactory);
}
}
// 递归调用setValue
metaValue.setValue(prop.getChildren(), value);
} else {
// 到了最后一层了,所以委派给 ObjectWrapper.set
objectWrapper.set(prop, value);
}
}
// ... 省略
}
MetaObject 元对象算是整个服务的包装,在构造函数中提供各类对象的包装器类型的创建。之后提供了一些基本的操作封装,这回封装后就更贴近实际的使用了。
包括这里提供的 getValue(String name) 、setValue(String name, Object value) 等,其中当一些对象的中的属性信息不是一个层次,是 班级[0].学生.成绩 还需要被拆解后才能获取到对应的对象和属性值。
当所有的这些内容提供完成以后,就可以使用 SystemMetaObject#forObject 提供元对象的获取了。
7. 数据源属性设置
好了,现在有了我们实现的属性反射操作工具包,那么对于数据源中属性信息的设置,就可以更加优雅的操作了。
源码详见:cn.bugstack.mybatis.datasource.unpooled.UnpooledDataSourceFactory
public class UnpooledDataSourceFactory implements DataSourceFactory {
protected DataSource dataSource;
public UnpooledDataSourceFactory() {
this.dataSource = new UnpooledDataSource();
}
@Override
public void setProperties(Properties props) {
MetaObject metaObject = SystemMetaObject.forObject(dataSource);
for (Object key : props.keySet()) {
String propertyName = (String) key;
if (metaObject.hasSetter(propertyName)) {
String value = (String) props.get(propertyName);
Object convertedValue = convertValue(metaObject, propertyName, value);
metaObject.setValue(propertyName, convertedValue);
}
}
}
@Override
public DataSource getDataSource() {
return dataSource;
}
}
在之前我们对于数据源中属性信息的获取都是采用的硬编码,那么这回在 setProperties 方法中则可以使用 SystemMetaObject.forObject(dataSource) 获取 DataSource 的元对象了,也就是通过反射就能把我们需要的属性值设置进去。
这样在数据源 UnpooledDataSource、PooledDataSource 中就可以拿到对应的属性值信息了,而不是我们那种在2个数据源的实现中硬编码操作。
五、测试
本章节的测试会分为2部分,一部分是我们这个章节实现的反射器工具类的测试,另外一方面是我们把反射器工具类接入到数据源的使用中,验证使用是否顺利。
1. 事先准备
1.1 创建库表
创建一个数据库名称为 mybatis 并在库中创建表 user 以及添加测试数据,如下:
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 配置数据源
<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>
通过 mybatis-config-datasource.xml 配置数据源信息,包括:driver、url、username、password
在这里 dataSource 测试验证 UNPOOLED 和 POOLED,因为这2个都属于被反射工具类处理
1.3 配置Mapper
<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>
这部分暂时不需要调整,目前还只是一个入参的类型的参数,后续我们全部完善这部分内容以后,则再提供更多的其他参数进行验证。
2. 单元测试
2.1 反射类测试
@Test
public void test_reflection() {
Teacher teacher = new Teacher();
List<Teacher.Student> list = new ArrayList<>();
list.add(new Teacher.Student());
teacher.setName("小傅哥");
teacher.setStudents(list);
MetaObject metaObject = SystemMetaObject.forObject(teacher);
logger.info("getGetterNames:{}", JSON.toJSONString(metaObject.getGetterNames()));
logger.info("getSetterNames:{}", JSON.toJSONString(metaObject.getSetterNames()));
logger.info("name的get方法返回值:{}", JSON.toJSONString(metaObject.getGetterType("name")));
logger.info("students的set方法参数值:{}", JSON.toJSONString(metaObject.getGetterType("students")));
logger.info("name的hasGetter:{}", metaObject.hasGetter("name"));
logger.info("student.id(属性为对象)的hasGetter:{}", metaObject.hasGetter("student.id"));
logger.info("获取name的属性值:{}", metaObject.getValue("name"));
// 重新设置属性值
metaObject.setValue("name", "小白");
logger.info("设置name的属性值:{}", metaObject.getValue("name"));
// 设置属性(集合)的元素值
metaObject.setValue("students[0].id", "001");
logger.info("获取students集合的第一个元素的属性值:{}", JSON.toJSONString(metaObject.getValue("students[0].id")));
logger.info("对象的序列化:{}", JSON.toJSONString(teacher));
}
这是一组比较常见的用于测试 Mybatis 源码中 MetaObject 的测试类,我们把这个单元测试用到我们自己实现的反射工具类上,看看是否可以正常运行。
测试结果
07:44:23.601 [main] INFO c.b.mybatis.test.ReflectionTest - getGetterNames:["student","price","name","students"]
07:44:23.608 [main] INFO c.b.mybatis.test.ReflectionTest - getSetterNames:["student","price","name","students"]
07:44:23.609 [main] INFO c.b.mybatis.test.ReflectionTest - name的get方法返回值:"java.lang.String"
07:44:23.609 [main] INFO c.b.mybatis.test.ReflectionTest - students的set方法参数值:"java.util.List"
07:44:23.609 [main] INFO c.b.mybatis.test.ReflectionTest - name的hasGetter:true
07:44:23.609 [main] INFO c.b.mybatis.test.ReflectionTest - student.id(属性为对象)的hasGetter:true
07:44:23.610 [main] INFO c.b.mybatis.test.ReflectionTest - 获取name的属性值:小傅哥
07:44:23.610 [main] INFO c.b.mybatis.test.ReflectionTest - 设置name的属性值:小白
07:44:23.610 [main] INFO c.b.mybatis.test.ReflectionTest - 获取students集合的第一个元素的属性值:"001"
07:44:23.665 [main] INFO c.b.mybatis.test.ReflectionTest - 对象的序列化:{"name":"小白","price":0.0,"students":[{"id":"001"}]}
Process finished with exit code 0
好了,那么这个测试中可以看到,我们拿到了对应的属性信息,并可以设置以及修改属性值,无论是单个属性还是对象属性,都可以操作。
2.2 数据源测试
@Test
public void test_SqlSessionFactory() throws IOException {
// 1. 从SqlSessionFactory中获取SqlSession
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsReader("mybatis-config-datasource.xml"));
SqlSession sqlSession = sqlSessionFactory.openSession();
// 2. 获取映射器对象
IUserDao userDao = sqlSession.getMapper(IUserDao.class);
// 3. 测试验证
User user = userDao.queryUserInfoById(1L);
logger.info("测试结果:{}", JSON.toJSONString(user));
}
这块的调用我们手写框架的测试类到不需要什么改变,只要数据源配置上使用 type="POOLED/UNPOOLED" 即可,这样就能测试我们自己开发的使用了反射器设置属性的数据源类了。
测试结果
图 8-4 使用MetaObject 设置属性值
07:51:54.898 [main] INFO c.b.m.d.pooled.PooledDataSource - Created connection 212683148.
07:51:55.006 [main] INFO cn.bugstack.mybatis.test.ApiTest - 测试结果:{"id":1,"userHead":"1_04","userId":"10001","userName":"小傅哥"}
根据单元测试和调试的截图,可以看到属性值通过反射的方式设置到对象中,也满足了我们在创建数据源时候的使用。这样就可以顺利的调用数据源完成数据的查询操作了。
七、总结
本章节关于反射工具类的实现中,使用了大量的 JDK 所提供的关于反射一些处理操作,也包括可以获取一个 Class 类中的属性、字段、方法的信息。那么再有了这些信息以后就可以按照功能流程进行解耦,把属性、反射、包装,都依次拆分出来,并按照设计原则,逐步包装让外接更少的知道内部的处理。
这里的反射也算是小天花板的使用级别了,封装的工具类方式,如果在我们也有类似的场景中,就可以直接拿来使用。因为整个工具类并没有太多的额外关联,直接拿来封装成一个工具包进行使用,处理平常的业务逻辑中组件化的部分,也是非常不错的。技术迁移、学以致用、升职加薪
由于整个工具包中涉及的类还是比较多的,大家在学习的过程中尽可能的验证和调试,以及对某个不清楚的方法进行单独开发和测试,这样才能滤清整个结构是如何实现的。当你把这块的内容全部拿下,以后再遇到反射就是小意思了