MyBatis 源码分析 - 配置文件解析过程

首先,我们使用 MyBatis 提供的工具类 Resources 加载配置文件,得到一个输入流。然后再通过 SqlSessionFactoryBuilder 对象的build方法构建 SqlSessionFactory 对象。所以这里的 build 方法是我们分析配置文件解析过程的入口方法。那下面我们来看一下这个方法的代码:

// -☆- SqlSessionFactoryBuilder

public SqlSessionFactory build(InputStream inputStream) {

// 调用重载方法

return build(inputStream, null, null);

}

public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {

try {

// 创建配置文件解析器

XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);

// 调用 parse 方法解析配置文件,生成 Configuration 对象

return build(parser.parse());

} catch (Exception e) {

throw ExceptionFactory.wrapException(“Error building SqlSession.”, e);

} finally {

ErrorContext.instance().reset();

try {

inputStream.close();

} catch (IOException e) {

// Intentionally ignore. Prefer previous error.

}

}

}

public SqlSessionFactory build(Configuration config) {

// 创建 DefaultSqlSessionFactory

return new DefaultSqlSessionFactory(config);

}

从上面的代码中,我们大致可以猜出 MyBatis 配置文件是通过XMLConfigBuilder进行解析的。不过目前这里还没有非常明确的解析逻辑,所以我们继续往下看。这次来看一下 XMLConfigBuilder 的parse方法,如下:

// -☆- XMLConfigBuilder

public Configuration parse() {

if (parsed) {

throw new BuilderException(“Each XMLConfigBuilder can only be used once.”);

}

parsed = true;

// 解析配置

parseConfiguration(parser.evalNode(“/configuration”));

return configuration;

}

到这里大家可以看到一些端倪了,注意一个 xpath 表达式 - /configuration。这个表达式代表的是 MyBatis 的<configuration/>标签,这里选中这个标签,并传递给parseConfiguration方法。我们继续跟下去。

private void parseConfiguration(XNode root) {

try {

// 解析 properties 配置

propertiesElement(root.evalNode(“properties”));

// 解析 settings 配置,并将其转换为 Properties 对象

Properties settings = settingsAsProperties(root.evalNode(“settings”));

// 加载 vfs

loadCustomVfs(settings);

// 解析 typeAliases 配置

typeAliasesElement(root.evalNode(“typeAliases”));

// 解析 plugins 配置

pluginElement(root.evalNode(“plugins”));

// 解析 objectFactory 配置

objectFactoryElement(root.evalNode(“objectFactory”));

// 解析 objectWrapperFactory 配置

objectWrapperFactoryElement(root.evalNode(“objectWrapperFactory”));

// 解析 reflectorFactory 配置

reflectorFactoryElement(root.evalNode(“reflectorFactory”));

// settings 中的信息设置到 Configuration 对象中

settingsElement(settings);

// 解析 environments 配置

environmentsElement(root.evalNode(“environments”));

// 解析 databaseIdProvider,获取并设置 databaseId 到 Configuration 对象

databaseIdProviderElement(root.evalNode(“databaseIdProvider”));

// 解析 typeHandlers 配置

typeHandlerElement(root.evalNode(“typeHandlers”));

// 解析 mappers 配置

mapperElement(root.evalNode(“mappers”));

} catch (Exception e) {

throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);

}

}

到此,一个 MyBatis 的解析过程就出来了,每个配置的解析逻辑都封装在了相应的方法中。在下面分析过程中,我不打算按照方法调用的顺序进行分析,我会适当进行一定的调整。同时,MyBatis 中配置较多,对于一些不常用的配置,这里会略过。那下面我们开始进行分析吧。

2.2 解析 properties 配置

解析properties节点是由propertiesElement这个方法完成的,该方法的逻辑比较简单。在分析方法源码前,先来看一下 properties 节点的配置内容。如下:

在上面的配置中,我为 properties 节点配置了一个 resource 属性,以及两个子节点。下面我们参照上面的配置,来分析一下 propertiesElement 的逻辑。相关分析如下。

// -☆- XMLConfigBuilder

private void propertiesElement(XNode context) throws Exception {

if (context != null) {

// 解析 propertis 的子节点,并将这些节点内容转换为属性对象 Properties

Properties defaults = context.getChildrenAsProperties();

// 获取 propertis 节点中的 resource 和 url 属性值

String resource = context.getStringAttribute(“resource”);

String url = context.getStringAttribute(“url”);

// 两者都不用空,则抛出异常

if (resource != null && url != null) {

throw new BuilderException(“The properties element cannot specify both a URL and a resource based property file reference. Please specify one or the other.”);

}

if (resource != null) {

// 从文件系统中加载并解析属性文件

defaults.putAll(Resources.getResourceAsProperties(resource));

} else if (url != null) {

// 通过 url 加载并解析属性文件

defaults.putAll(Resources.getUrlAsProperties(url));

}

Properties vars = configuration.getVariables();

if (vars != null) {

defaults.putAll(vars);

}

parser.setVariables(defaults);

// 将属性值设置到 configuration 中

configuration.setVariables(defaults);

}

}

public Properties getChildrenAsProperties() {

Properties properties = new Properties();

// 获取并遍历子节点

for (XNode child : getChildren()) {

// 获取 property 节点的 name 和 value 属性

String name = child.getStringAttribute(“name”);

String value = child.getStringAttribute(“value”);

if (name != null && value != null) {

// 设置属性到属性对象中

properties.setProperty(name, value);

}

}

return properties;

}

// -☆- XNode

public List getChildren() {

List children = new ArrayList();

// 获取子节点列表

NodeList nodeList = node.getChildNodes();

if (nodeList != null) {

for (int i = 0, n = nodeList.getLength(); i < n; i++) {

Node node = nodeList.item(i);

if (node.getNodeType() == Node.ELEMENT_NODE) {

// 将节点对象封装到 XNode 中,并将 XNode 对象放入 children 列表中

children.add(new XNode(xpathParser, node, variables));

}

}

}

return children;

}

上面是 properties 节点解析的主要过程,不是很复杂。主要包含三个步骤,一是解析 properties 节点的子节点,并将解析结果设置到 Properties 对象中。二是从文件系统或通过网络读取属性配置,这取决于 properties 节点的 resource 和 url 是否为空。第二步对应的代码比较简单,这里就不分析了。有兴趣的话,大家可以自己去看看。最后一步则是将解析出的属性对象设置到 XPathParser 和 Configuration 对象中。

需要注意的是,propertiesElement 方法是先解析 properties 节点的子节点内容,后再从文件系统或者网络读取属性配置,并将所有的属性及属性值都放入到 defaults 属性对象中。这就会存在同名属性覆盖的问题,也就是从文件系统,或者网络上读取到的属性及属性值会覆盖掉 properties 子节点中同名的属性和及值。比如上面配置中的jdbc.properties内容如下:

jdbc.driver=com.mysql.cj.jdbc.Driver

jdbc.url=jdbc:mysql://localhost:3306/myblog?..

jdbc.username=root

jdbc.password=1234

与 properties 子节点内容合并后,结果如下:

img

如上,原jdbc.username值为coolblog,现在被覆盖为了root。同名属性覆盖的问题需要大家注意一下,其他的就没什么了,继续往下分析。

2.3 解析 settings 配置

2.3.1 settings 节点的解析过程

settings 相关配置是 MyBatis 中非常重要的配置,这些配置用于调整 MyBatis 运行时的行为。settings 配置繁多,在对这些配置不熟悉的情况下,保持默认配置即可。关于 settings 相关配置,MyBatis 官网上进行了比较详细的描述,大家可以去了解一下。在本节中,暂时还用不到这些配置,所以即使不了解这些配置也没什么关系。下面先来看一个比较简单的配置,如下:

接下来,对照上面的配置,来分析源码。如下:

// -☆- XMLConfigBuilder

private Properties settingsAsProperties(XNode context) {

if (context == null) {

return new Properties();

}

// 获取 settings 子节点中的内容,getChildrenAsProperties 方法前面已分析过,这里不再赘述

Properties props = context.getChildrenAsProperties();

// 创建 Configuration 类的“元信息”对象

MetaClass metaConfig = MetaClass.forClass(Configuration.class, localReflectorFactory);

for (Object key : props.keySet()) {

// 检测 Configuration 中是否存在相关属性,不存在则抛出异常

if (!metaConfig.hasSetter(String.valueOf(key))) {

throw new BuilderException(“The setting " + key + " is not known. Make sure you spelled it correctly (case sensitive).”);

}

}

return props;

}

如上,settingsAsProperties 方法看起来并不复杂,不过这是一个假象。在上面的代码中出现了一个陌生的类MetaClass,这个类是用来做什么的呢?答案是用来解析目标类的一些元信息,比如类的成员变量,getter/setter 方法等。关于这个类的逻辑,待会我会详细解析。接下来,简单总结一下上面代码的逻辑。如下:

  1. 解析 settings 子节点的内容,并将解析结果转成 Properties 对象

  2. 为 Configuration 创建元信息对象

  3. 通过 MetaClass 检测 Configuration 中是否存在某个属性的 setter 方法,不存在则抛异常

  4. 若通过 MetaClass 的检测,则返回 Properties 对象,方法逻辑结束

下面,我们来重点关注一下第2步和第3步的流程。这两步流程对应的代码较为复杂,需要一点耐心阅读。好了,下面开始分析。

2.3.2 元信息对象创建过程

元信息类MetaClass的构造方法为私有类型,所以不能直接创建,必须使用其提供的forClass方法进行创建。它的创建逻辑如下:

public class MetaClass {

private final ReflectorFactory reflectorFactory;

private final Reflector reflector;

private MetaClass(Class<?> type, ReflectorFactory reflectorFactory) {

this.reflectorFactory = reflectorFactory;

// 根据类型创建 Reflector

this.reflector = reflectorFactory.findForClass(type);

}

public static MetaClass forClass(Class<?> type, ReflectorFactory reflectorFactory) {

// 调用构造方法

return new MetaClass(type, reflectorFactory);

}

// 省略其他方法

}

上面的代码看起来很简单,不过这只是冰山一角。上面代码出现了两个新的类ReflectorFactoryReflector,MetaClass 通过引入这些新类帮助它完成功能。下面我们看一下hasSetter方法的源码就知道是怎么回事了。

// -☆- MetaClass

public boolean hasSetter(String name) {

// 属性分词器,用于解析属性名

PropertyTokenizer prop = new PropertyTokenizer(name);

// hasNext 返回 true,则表明 name 是一个复合属性,后面会进行分析

if (prop.hasNext()) {

// 调用 reflector 的 hasSetter 方法

if (reflector.hasSetter(prop.getName())) {

// 为属性创建 MetaClass

MetaClass metaProp = metaClassForProperty(prop.getName());

// 再次调用 hasSetter

return metaProp.hasSetter(prop.getChildren());

} else {

return false;

}

} else {

// 调用 reflector 的 hasSetter 方法

return reflector.hasSetter(prop.getName());

}

}

从上面的代码中,我们可以看出 MetaClass 中的 hasSetter 方法最终调用了 Reflector 的 hasSetter 方法。关于 Reflector 的 hasSetter 方法,这里先不分析,Reflector 这个类的逻辑较为复杂,本节会在随后进行详细说明。下面来简单介绍一下上面代码中出现的几个类:

  1. ReflectorFactory -> 顾名思义,Reflector 的工厂类,兼有缓存 Reflector 对象的功能

  2. Reflector -> 反射器,用于解析和存储目标类中的元信息

  3. PropertyTokenizer -> 属性名分词器,用于处理较为复杂的属性名

上面的描述比较简单,仅从上面的描述中,还不能让大家有更深入的理解。所以下面单独分析一下这几个类的逻辑,首先是ReflectorFactoryReflectorFactory 是一个接口,MyBatis 中目前只有一个实现类DefaultReflectorFactory,它的分析如下:

2.3.2.1 DefaultReflectorFactory 源码分析

DefaultReflectorFactory 用于创建 Reflector,同时兼有缓存的功能,它的源码如下。

public class DefaultReflectorFactory implements ReflectorFactory {

private boolean classCacheEnabled = true;

/** 目标类和反射器映射缓存 */

private final ConcurrentMap<Class<?>, Reflector> reflectorMap = new ConcurrentHashMap

// 省略部分代码

@Override

public Reflector findForClass(Class<?> type) {

// classCacheEnabled 默认为 true

if (classCacheEnabled) {

// 从缓存中获取 Reflector 对象

Reflector cached = reflectorMap.get(type);

// 缓存为空,则创建一个新的 Reflector 实例,并放入缓存中

if (cached == null) {

cached = new Reflector(type);

// 将 <type, cached> 映射缓存到 map 中,方便下次取用

reflectorMap.put(type, cached);

}

return cached;

} else {

// 创建一个新的 Reflector 实例

return new Reflector(type);

}

}

}

如上,DefaultReflectorFactory 的findForClass方法逻辑不是很复杂,包含两个访存操作,和一个对象创建操作。代码注释的比较清楚了,就不多说了。接下来,来分析一下反射器 Reflector。

2.3.2.2 Reflector 源码分析

本小节,我们来看一下 Reflector 的源码。Reflector 这个类的用途主要是是通过反射获取目标类的 getter 方法及其返回值类型,setter 方法及其参数值类型等元信息。并将获取到的元信息缓存到相应的集合中,供后续使用。Reflector 本身代码比较多,这里不能一一分析。本小节,我将会分析三部分逻辑,分别如下:

  1. Reflector 构造方法及成员变量分析

  2. getter 方法解析过程

  3. setter 方法解析过程

下面我们按照这个步骤进行分析,先来分析 Reflector 构造方法。

● Reflector 构造方法及成员变量分析

Reflector 构造方法中包含了很多初始化逻辑,目标类的元信息解析过程也是在构造方法中完成的,这些元信息最终会被保存到 Reflector 的成员变量中。下面我们先来看看 Reflector 的构造方法和相关的成员变量定义,代码如下:

public class Reflector {

private final Class<?> type;

private final String[] readablePropertyNames;

private final String[] writeablePropertyNames;

private final Map<String, Invoker> setMethods = new HashMap<String, Invoker>();

private final Map<String, Invoker> getMethods = new HashMap<String, Invoker>();

private final Map<String, Class<?>> setTypes = new HashMap

private final Map<String, Class<?>> getTypes = new HashMap

private Constructor<?> defaultConstructor;

private Map<String, String> caseInsensitivePropertyMap = new HashMap<String, String>();

public Reflector(Class<?> clazz) {

type = clazz;

// 解析目标类的默认构造方法,并赋值给 defaultConstructor 变量

addDefaultConstructor(clazz);

// 解析 getter 方法,并将解析结果放入 getMethods 中

addGetMethods(clazz);

// 解析 setter 方法,并将解析结果放入 setMethods 中

addSetMethods(clazz);

// 解析属性字段,并将解析结果添加到 setMethods 或 getMethods 中

addFields(clazz);

// 从 getMethods 映射中获取可读属性名数组

readablePropertyNames = getMethods.keySet().toArray(new String[getMethods.keySet().size()]);

// 从 setMethods 映射中获取可写属性名数组

writeablePropertyNames = setMethods.keySet().toArray(new String[setMethods.keySet().size()]);

// 将所有属性名的大写形式作为键,属性名作为值,存入到 caseInsensitivePropertyMap 中

for (String propName : readablePropertyNames) {

caseInsensitivePropertyMap.put(propName.toUpperCase(Locale.ENGLISH), propName);

}

for (String propName : writeablePropertyNames) {

caseInsensitivePropertyMap.put(propName.toUpperCase(Locale.ENGLISH), propName);

}

}

// 省略其他方法

}

如上,Reflector 的构造方法看起来略为复杂,不过好在一些比较复杂的逻辑都封装在了相应的方法中,这样整体的逻辑就比较清晰了。Reflector 构造方法所做的事情均已进行了注释,大家对照着注释先看一下。相关方法的细节待会会进行分析。看完构造方法,下面我来通过表格的形式,列举一下 Reflector 部分成员变量的用途。如下:

| 变量名 | 类型 | 用途 |

| :-- | :-- | :-- |

| readablePropertyNames | String[] | 可读属性名称数组,用于保存 getter 方法对应的属性名称 |

| writeablePropertyNames | String[] | 可写属性名称数组,用于保存 setter 方法对应的属性名称 |

| setMethods | Map<String, Invoker> | 用于保存属性名称到 Invoke 的映射。setter 方法会被封装到 MethodInvoker 对象中,Invoke 实现类比较简单,大家自行分析 |

| getMethods | Map<String, Invoker> | 用于保存属性名称到 Invoke 的映射。同上,getter 方法也会被封装到 MethodInvoker 对象中 |

| setTypes | Map<String, Class<?>> | 用于保存 setter 对应的属性名与参数类型的映射 |

| getTypes | Map<String, Class<?>> | 用于保存 getter 对应的属性名与返回值类型的映射 |

| caseInsensitivePropertyMap | Map<String, String> | 用于保存大写属性名与属性名之间的映射,比如 <NAME, name> |

上面列举了一些集合变量,这些变量用于缓存各种原信息。关于这些变量,这里描述的不太好懂,主要是不太好解释。要想了解这些变量更多的细节,还是要深入到源码中。所以我们趁热打铁,继续往下分析。

● getter 方法解析过程

getter 方法解析的逻辑被封装在了addGetMethods方法中,这个方法除了会解析形如getXXX的方法,同时也会解析isXXX方法。该方法的源码分析如下:

private void addGetMethods(Class<?> cls) {

Map<String, List> conflictingGetters = new HashMap<String, List>();

// 获取当前类,接口,以及父类中的方法。该方法逻辑不是很复杂,这里就不展开了

Method[] methods = getClassMethods(cls);

for (Method method : methods) {

// getter 方法不应该有参数,若存在参数,则忽略当前方法

if (method.getParameterTypes().length > 0) {

continue;

}

String name = method.getName();

// 过滤出以 get 或 is 开头的方法

if ((name.startsWith(“get”) && name.length() > 3)

|| (name.startsWith(“is”) && name.length() > 2)) {

// 将 getXXX 或 isXXX 等方法名转成相应的属性,比如 getName -> name

name = PropertyNamer.methodToProperty(name);

/*

  • 将冲突的方法添加到 conflictingGetters 中。考虑这样一种情况:

  • getTitle 和 isTitle 两个方法经过 methodToProperty 处理,

  • 均得到 name = title,这会导致冲突。

  • 对于冲突的方法,这里先统一起存起来,后续再解决冲突

*/

addMethodConflict(conflictingGetters, name, method);

}

}

// 解决 getter 冲突

resolveGetterConflicts(conflictingGetters);

}

如上,addGetMethods 方法的执行流程如下:

  1. 获取当前类,接口,以及父类中的方法

  2. 遍历上一步获取的方法数组,并过滤出以getis开头的方法

  3. 将方法名转换成相应的属性名

  4. 将属性名和方法对象添加到冲突集合中

  5. 解决冲突

在上面的执行流程中,前三步比较简单,大家自行分析吧。第4步也不复杂,下面我会把源码贴出来,大家看一下就能懂。在这几步中,第5步逻辑比较复杂,这一步逻辑我们重点关注一下。下面继续看源码吧。

/** 添加属性名和方法对象到冲突集合中 */

private void addMethodConflict(Map<String, List> conflictingMethods, String name, Method method) {

List list = conflictingMethods.get(name);

if (list == null) {

list = new ArrayList();

conflictingMethods.put(name, list);

}

list.add(method);

}

/** 解决冲突 */

private void resolveGetterConflicts(Map<String, List> conflictingGetters) {

for (Entry<String, List> entry : conflictingGetters.entrySet()) {

Method winner = null;

String propName = entry.getKey();

for (Method candidate : entry.getValue()) {

if (winner == null) {

winner = candidate;

continue;

}

// 获取返回值类型

Class<?> winnerType = winner.getReturnType();

Class<?> candidateType = candidate.getReturnType();

/*

  • 两个方法的返回值类型一致,若两个方法返回值类型均为 boolean,则选取 isXXX 方法

  • 为 winner。否则无法决定哪个方法更为合适,只能抛出异常

*/

if (candidateType.equals(winnerType)) {

if (!boolean.class.equals(candidateType)) {

throw new ReflectionException(

"Illegal overloaded getter method with ambiguous type for property "

  • propName + " in class " + winner.getDeclaringClass()

  • “. This breaks the JavaBeans specification and can cause unpredictable results.”);

/*

  • 如果方法返回值类型为 boolean,且方法名以 “is” 开头,

  • 则认为候选方法 candidate 更为合适

*/

} else if (candidate.getName().startsWith(“is”)) {

winner = candidate;

}

/*

  • winnerType 是 candidateType 的子类,类型上更为具体,

  • 则认为当前的 winner 仍是合适的,无需做什么事情

*/

} else if (candidateType.isAssignableFrom(winnerType)) {

/*

  • candidateType 是 winnerType 的子类,此时认为 candidate 方法更为合适,

  • 故将 winner 更新为 candidate

*/

} else if (winnerType.isAssignableFrom(candidateType)) {

winner = candidate;

} else {

throw new ReflectionException(

"Illegal overloaded getter method with ambiguous type for property "

  • propName + " in class " + winner.getDeclaringClass()

  • “. This breaks the JavaBeans specification and can cause unpredictable results.”);

}

}

// 将筛选出的方法添加到 getMethods 中,并将方法返回值添加到 getTypes 中

addGetMethod(propName, winner);

}

}

private void addGetMethod(String name, Method method) {

if (isValidPropertyName(name)) {

getMethods.put(name, new MethodInvoker(method));

// 解析返回值类型

Type returnType = TypeParameterResolver.resolveReturnType(method, type);

// 将返回值类型由 Type 转为 Class,并将转换后的结果缓存到 setTypes 中

getTypes.put(name, typeToClass(returnType));

}

}

以上就是解除冲突的过程,代码有点长,不太容易看懂。这里大家只要记住解决冲突的规则即可理解上面代码的逻辑。相关规则如下:

  1. 冲突方法的返回值类型具有继承关系,子类返回值对应的方法被认为是更合适的选择

  2. 冲突方法的返回值类型相同,如果返回值类型为boolean,那么以is开头的方法则是更合适的方法

  3. 冲突方法的返回值类型相同,但返回值类型非boolean,此时出现歧义,抛出异常

  4. 冲突方法的返回值类型不相关,无法确定哪个是更好的选择,此时直接抛异常

分析完 getter 方法的解析过程,下面继续分析 setter 方法的解析过程。

● setter 方法解析过程

与 getter 方法解析过程相比,setter 方法的解析过程与此有一定的区别。主要体现在冲突出现的原因,以及冲突的解决方法上。那下面,我们深入源码来找出两者之间的区别。

private void addSetMethods(Class<?> cls) {

Map<String, List> conflictingSetters = new HashMap<String, List>();

// 获取当前类,接口,以及父类中的方法。该方法逻辑不是很复杂,这里就不展开了

Method[] methods = getClassMethods(cls);

for (Method method : methods) {

String name = method.getName();

// 过滤出 setter 方法,且方法仅有一个参数

if (name.startsWith(“set”) && name.length() > 3) {

if (method.getParameterTypes().length == 1) {

name = PropertyNamer.methodToProperty(name);

/*

  • setter 方法发生冲突原因是:可能存在重载情况,比如:

  • void setSex(int sex);
    
  • void setSex(SexEnum sex);
    

*/

addMethodConflict(conflictingSetters, name, method);

}

}

}

// 解决 setter 冲突

resolveSetterConflicts(conflictingSetters);

}

从上面的代码和注释中,我们可知道 setter 方法之间出现冲突的原因。即方法存在重载,方法重载导致methodToProperty方法解析出的属性名完全一致。而 getter 方法之间出现冲突的原因是getXXXisXXX对应的属性名一致。既然冲突发生了,要进行调停,那接下来继续来看看调停冲突的逻辑。

private void resolveSetterConflicts(Map<String, List> conflictingSetters) {

for (String propName : conflictingSetters.keySet()) {

List setters = conflictingSetters.get(propName);

/*

  • 获取 getter 方法的返回值类型,由于 getter 方法不存在重载的情况,

  • 所以可以用它的返回值类型反推哪个 setter 的更为合适

*/

Class<?> getterType = getTypes.get(propName);

Method match = null;

ReflectionException exception = null;

for (Method setter : setters) {

// 获取参数类型

Class<?> paramType = setter.getParameterTypes()[0];

if (paramType.equals(getterType)) {

// 参数类型和返回类型一致,则认为是最好的选择,并结束循环

match = setter;

break;

}

if (exception == null) {

try {

// 选择一个更为合适的方法

match = pickBetterSetter(match, setter, propName);

} catch (ReflectionException e) {

match = null;

exception = e;

}

}

}

// 若 match 为空,表示没找到更为合适的方法,此时抛出异常

if (match == null) {

throw exception;

} else {

// 将筛选出的方法放入 setMethods 中,并将方法参数值添加到 setTypes 中

addSetMethod(propName, match);

}

}

}

/** 从两个 setter 方法中选择一个更为合适方法 */

private Method pickBetterSetter(Method setter1, Method setter2, String property) {

if (setter1 == null) {

return setter2;

}

Class<?> paramType1 = setter1.getParameterTypes()[0];

Class<?> paramType2 = setter2.getParameterTypes()[0];

// 如果参数2可赋值给参数1,即参数2是参数1的子类,则认为参数2对应的 setter 方法更为合适

if (paramType1.isAssignableFrom(paramType2)) {

return setter2;

// 这里和上面情况相反

} else if (paramType2.isAssignableFrom(paramType1)) {

return setter1;

}

// 两种参数类型不相关,这里抛出异常

throw new ReflectionException(“Ambiguous setters defined for property '” + property + “’ in class '”

  • setter2.getDeclaringClass() + “’ with types '” + paramType1.getName() + “’ and '”

  • paramType2.getName() + “'.”);

}

private void addSetMethod(String name, Method method) {

if (isValidPropertyName(name)) {

setMethods.put(name, new MethodInvoker(method));

// 解析参数类型列表

Type[] paramTypes = TypeParameterResolver.resolveParamTypes(method, type);

// 将参数类型由 Type 转为 Class,并将转换后的结果缓存到 setTypes

setTypes.put(name, typeToClass(paramTypes[0]));

}

}

关于 setter 方法冲突的解析规则,这里也总结一下吧。如下:

  1. 冲突方法的参数类型与 getter 的返回类型一致,则认为是最好的选择

  2. 冲突方法的参数类型具有继承关系,子类参数对应的方法被认为是更合适的选择

  3. 冲突方法的参数类型不相关,无法确定哪个是更好的选择,此时直接抛异常

到此关于 setter 方法的解析过程就说完了。我在前面说过 MetaClass 的hasSetter最终调用了 Refactor 的hasSetter方法,那么现在是时候分析 Refactor 的hasSetter方法了。代码如下如下:

public boolean hasSetter(String propertyName) {

return setMethods.keySet().contains(propertyName);

}

代码如上,就两行,很简单,就不多说了。

2.3.2.3 PropertyTokenizer 源码分析

对于较为复杂的属性,需要进行进一步解析才能使用。那什么样的属性是复杂属性呢?来看个测试代码就知道了。

public class MetaClassTest {

private class Author {

private Integer id;

private String name;

private Integer age;

/** 一个作者对应多篇文章 */

private Article[] articles;

// 省略 getter/setter

}

private class Article {

private Integer id;

private String title;

private String content;

/** 一篇文章对应一个作者 */

private Author author;

// 省略 getter/setter

}

@Test

public void testHasSetter() {

// 为 Author 创建元信息对象

MetaClass authorMeta = MetaClass.forClass(Author.class, new DefaultReflectorFactory());

System.out.println(“------------☆ Author ☆------------”);

System.out.println("id -> " + authorMeta.hasSetter(“id”));

System.out.println("name -> " + authorMeta.hasSetter(“name”));

System.out.println("age -> " + authorMeta.hasSetter(“age”));

// 检测 Author 中是否包含 Article[] 的 setter

System.out.println("articles -> " + authorMeta.hasSetter(“articles”));

System.out.println("articles[] -> " + authorMeta.hasSetter(“articles[]”));

System.out.println("title -> " + authorMeta.hasSetter(“title”));

// 为 Article 创建元信息对象

MetaClass articleMeta = MetaClass.forClass(Article.class, new DefaultReflectorFactory());

System.out.println(“\n------------☆ Article ☆------------”);

System.out.println("id -> " + articleMeta.hasSetter(“id”));

System.out.println("title -> " + articleMeta.hasSetter(“title”));

System.out.println("content -> " + articleMeta.hasSetter(“content”));

// 下面两个均为复杂属性,分别检测 Article 类中的 Author 类是否包含 id 和 name 的 setter 方法

System.out.println("author.id -> " + articleMeta.hasSetter(“author.id”));

System.out.println("author.name -> " + articleMeta.hasSetter(“author.name”));

}

}

如上,Article类中包含了一个Author引用。然后我们调用 articleMeta 的 hasSetter 检测author.idauthor.name属性是否存在,我们的期望结果为 true。测试结果如下:

img

如上,标记⑤处的输出均为 true,我们的预期达到了。标记②处检测 Article 数组的是否存在 setter 方法,结果也均为 true。这说明 PropertyTokenizer 对数组和复合属性均进行了处理。那它是如何处理的呢?答案如下:

public class PropertyTokenizer implements Iterator {

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

面试资料整理汇总

成功从小公司跳槽进蚂蚁定级P7,只因刷了七遍这些面试真题

成功从小公司跳槽进蚂蚁定级P7,只因刷了七遍这些面试真题

这些面试题是我朋友进阿里前狂刷七遍以上的面试资料,由于面试文档很多,内容更多,没有办法一一为大家展示出来,所以只好为大家节选出来了一部分供大家参考。

面试的本质不是考试,而是告诉面试官你会做什么,所以,这些面试资料中提到的技术也是要学会的,不然稍微改动一下你就凉凉了

在这里祝大家能够拿到心仪的offer!
《一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码》点击传送门即可获取!
“author.name”));

}

}

如上,Article类中包含了一个Author引用。然后我们调用 articleMeta 的 hasSetter 检测author.idauthor.name属性是否存在,我们的期望结果为 true。测试结果如下:

img

如上,标记⑤处的输出均为 true,我们的预期达到了。标记②处检测 Article 数组的是否存在 setter 方法,结果也均为 true。这说明 PropertyTokenizer 对数组和复合属性均进行了处理。那它是如何处理的呢?答案如下:

public class PropertyTokenizer implements Iterator {

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。[外链图片转存中…(img-s1Bv4K1s-1711794288849)]

[外链图片转存中…(img-5pV6zyVn-1711794288850)]

[外链图片转存中…(img-Mb7HyWWD-1711794288850)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

面试资料整理汇总

[外链图片转存中…(img-HyUqb0vT-1711794288850)]

[外链图片转存中…(img-N0rEYKFk-1711794288851)]

这些面试题是我朋友进阿里前狂刷七遍以上的面试资料,由于面试文档很多,内容更多,没有办法一一为大家展示出来,所以只好为大家节选出来了一部分供大家参考。

面试的本质不是考试,而是告诉面试官你会做什么,所以,这些面试资料中提到的技术也是要学会的,不然稍微改动一下你就凉凉了

在这里祝大家能够拿到心仪的offer!
《一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码》点击传送门即可获取!

  • 29
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值