首先,我们从MyBatis的入口方法入手:
sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
return build(parser.parse());
}
本系列所有源码为了方便阅读,都会删除一些“结构性”的代码,下同
可以看到,这里是直接新建了XMLConfigBuilder对象,然后调用了XMLConfigBuilder方法进行解析XML文件生成Configuration对象
XMLConfiguration###XMLConfigBuilder()
private XMLConfigBuilder(XPathParser parser, String environment, Properties props) {
super(new Configuration());
ErrorContext.instance().resource("SQL Mapper Configuration");
//设置变量
this.configuration.setVariables(props);
this.parsed = false;
this.environment = environment;
this.parser = parser;
}
XMLConfiguration###parse()
//简单的判断是否已经解析过了
public Configuration parse() {
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
XMLConfiguration###parseConfiguration()
//调用各个方法进行解析成Configuration对象
private void parseConfiguration(XNode root) {
try {
//读取用户自定义属性
propertiesElement(root.evalNode("properties"));
//读取用户的设置
Properties settings = settingsAsProperties(root.evalNode("settings"));
//加载用户自定义的VFS实现
loadCustomVfs(settings);
//加载日志设置
loadCustomLogImpl(settings);
//加载用户定义的别名
typeAliasesElement(root.evalNode("typeAliases"));
//加载用户定义的插件
pluginElement(root.evalNode("plugins"));
//加载用户定义的对象工厂
objectFactoryElement(root.evalNode("objectFactory"));
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
//加载用户定义的反射对象工厂
reflectorFactoryElement(root.evalNode("reflectorFactory"));
//加载用户定义的其他设置
settingsElement(settings);
//加载用户定义的环境变量
environmentsElement(root.evalNode("environments"));
//读取databaseIdProvider
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
//处理类型处理器
typeHandlerElement(root.evalNode("typeHandlers"));、
//处理mapper
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
这里可以看到parseConfiguration方法基本上是所有方法的汇总,基本上通过这个方法即解析了整个XML配置文件,因此我们一个一个看.
XMLConfiguration###propertiesElement()
//顾明思意,此方法便是用来读取properties节点的
private void propertiesElement(XNode context) throws Exception {
if (context != null) {
//分别读取properties节点的name和value元素,转换为properties对象
Properties defaults = context.getChildrenAsProperties();
//获取其他属性的资源路径
String resource = context.getStringAttribute("resource");
//获取其他属性的url
String url = context.getStringAttribute("url");
//resource和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.");
}
//如果resource不为null,则通过`ClassLoader` 加载此资源文件
//可以知道这里是通过`ClassLoader`加载的资源文件,因此不管这个资源文件放在哪个模块,都能被加到
if (resource != null) {
defaults.putAll(Resources.getResourceAsProperties(resource));
}
//如果url不为null,则通过JDK的URL类加载此资源
else if (url != null) {
defaults.putAll(Resources.getUrlAsProperties(url));
}
//将存入的属性取出来
Properties vars = configuration.getVariables();
if (vars != null) {
defaults.putAll(vars);
}
//最后再加入之前存入的属性,这样操作主要是为了保证不同地方的优先级
parser.setVariables(defaults);
configuration.setVariables(defaults);
}
}
可以看到,这个方法主要包含以下3个步骤:
*首先加入节点中的属性
*然后加入resource或url中的属性
*然后加入通过XMLConfiguration构造方法传入属性
以上上个步骤顺序严格执行,且后面的操作可以覆盖前面的key
XMLConfiguration###settingsAsProperties()
private Properties settingsAsProperties(XNode context) {
if (context == null) {
return new Properties();
}
Properties props = context.getChildrenAsProperties();
//检测所设置的值是否存在对应的`setter`,没有则抛出异常
MetaClass metaConfig = MetaClass.forClass(Configuration.class, localReflectorFactory);
for (Object key : props.keySet()) {
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;
}
这里MetaClass便是MyBatis中reflection包中的一个元数据类,用于通过反射获取/设置各个对象的值。
XMLConfiguration###loadCustomLogImpl()
//加载用户设置的日志实现类
private void loadCustomLogImpl(Properties props) {
Class<? extends Log> logImpl = resolveClass(props.getProperty("logImpl"));
configuration.setLogImpl(logImpl);
}
这个方法没什么特别的,但是可以从这里看看typeAlias的代码。
在平时配置中,我们都是进行如下配置的:
<setting name="logImpl" value="SLF4J"/>
我们都是写的简写,而不是com.xxx.xxx.Slf4j,这是因为MyBatis中维护了一个TypeAliasRegister,它维护了简写与实际的类的映射.
TypeAliasRegister###resolveAlias()
public <T> Class<T> resolveAlias(String string) {
try {
if (string == null) {
return null;
}
//将key都转换为小写
//这里在国际使用中有个bug,比如如果本地语言是Turkish,那i转成大写就不是I了,而是另外一个字符
//(İ)。这样土耳其的机器就用不了mybatis了
//因此这里要指定一个统一的本地语言
String key = string.toLowerCase(Locale.ENGLISH);
Class<T> value;
//如果别名中找到了所注册的key
if (typeAliases.containsKey(key)) {
value = (Class<T>) typeAliases.get(key);
} else {
//没找到就尝试直接加载此类
value = (Class<T>) Resources.classForName(string);
}
return value;
} catch (ClassNotFoundException e) {
throw new TypeException("Could not resolve type alias '" + string + "'. Cause: " + e, e);
}
}
由这里可以看到,在取值的时候,都是先将Key转换为小写后再取值,因此可以看出来MyBatis是不区分大小写的
同时,可以看到我既可以指定别名,也可以直接写全名。
而日志文件的映射,是在Configuration构造方法里面被注册:
typeAliasRegistry.registerAlias("SLF4J", Slf4jImpl.class);
typeAliasRegistry.registerAlias("COMMONS_LOGGING", JakartaCommonsLoggingImpl.class);
typeAliasRegistry.registerAlias("LOG4J", Log4jImpl.class);
typeAliasRegistry.registerAlias("LOG4J2", Log4j2Impl.class);
typeAliasRegistry.registerAlias("JDK_LOGGING", Jdk14LoggingImpl.class);
typeAliasRegistry.registerAlias("STDOUT_LOGGING", StdOutImpl.class);
typeAliasRegistry.registerAlias("NO_LOGGING", NoLoggingImpl.class);
XMLConfiguration###typeAliasesElement()
//加载用户自定义的别名
// <typeAliases>
// <package name="com.dengchengchao.demo.model"/>
// </typeAliases>
private void typeAliasesElement(XNode parent) {
if (parent != null) {
for (XNode child : parent.getChildren()) {
//如果子节点是`package` 则说明是自动获取包下所有类别名
if ("package".equals(child.getName())) {
String typeAliasPackage = child.getStringAttribute("name");
configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage);
} else {
//否则,根据type alias 来加载具体的类
String alias = child.getStringAttribute("alias");
String type = child.getStringAttribute("type");
try {
//加载此类
Class<?> clazz = Resources.classForName(type);
//如果别名为null,则默认为class名首字母小写
if (alias == null) {
typeAliasRegistry.registerAlias(clazz);
} else {
typeAliasRegistry.registerAlias(alias, clazz);
}
} catch (ClassNotFoundException e) {
throw new BuilderException("Error registering typeAlias for '" + alias + "'. Cause: " + e, e);
}
}
}
}
}
可以看到这里代码比较简单, 我们可以更加深入看看MyBatis是如何加载类的
首先看注册package目录下的所有类
public void registerAliases(String packageName, Class<?> superType) {
ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<>();
//这里ResolverUtil内部便是通过VFS读取了该包下所有的class
resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
Set<Class<? extends Class<?>>> typeSet = resolverUtil.getClasses();
for (Class<?> type : typeSet) {
//注册所有非匿名类,非接口以及非内存成员类
if (!type.isAnonymousClass() && !type.isInterface() && !type.isMemberClass()) {
registerAlias(type);
}
}
}
然后看看单独注册指定类:
public void registerAlias(Class<?> type) {
//获取类的类名
String alias = type.getSimpleName();
//查看此类型上是否有Alias注解
Alias aliasAnnotation = type.getAnnotation(Alias.class);
if (aliasAnnotation != null) {
alias = aliasAnnotation.value();
}
//如果有注解则使用注解的名字,否则使用类名
registerAlias(alias, type);
}
public void registerAlias(String alias, Class<?> value) {
if (alias == null) {
throw new TypeException("The parameter alias cannot be null");
}
//都取小写
String key = alias.toLowerCase(Locale.ENGLISH);
//别名注册不能重复
if (typeAliases.containsKey(key) && typeAliases.get(key) != null && !typeAliases.get(key).equals(value)) {
throw new TypeException("The alias '" + alias + "' is already mapped to the value '" + typeAliases.get(key).getName() + "'.");
}
//将对应的Key Value放进map
typeAliases.put(key, value);
}
这里可以看到有两点需要注意:
别名是不区分大小写的,这在前面已经说过
如果同时又Alias注解和在XML中也配置了alias,MyBatis会以XML中的为准
本章暂时到这里,有上面的源码我可以学到:
MyBatis中properties的优先级的实现
MyBatis中别名是不区分大小写的,以及String#toLowerCase()可能带来的bug
MyBatis中,指定一些类型的设置,也可以不通过类型别名,直接指定全名也行
学习MyBatis的结构划分,可以发现MyBatis的代码逻辑十分清晰,易读
网友评论