首先,我们从 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
对象
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; }
//简单的判断是否已经解析过了 public Configuration parse() { if (parsed) { throw new BuilderException("Each XMLConfigBuilder can only be used once."); } parsed = true; parseConfiguration(parser.evalNode("/configuration")); return configuration; }
//调用各个方法进行解析成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
配置文件,因此我们一个一个看.
//顾明思意,此方法便是用来读取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
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
包中的一个元数据类,用于通过反射获取/设置各个对象的值。
//加载用户设置的日志实现类 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
,它维护了简写与实际的类的映射.
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);
//加载用户自定义的别名 // <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
, MyBatis
会以 XML
中的为准 本章暂时到这里,有上面的源码我可以学到:
MyBatis
中 properties
的优先级的实现 MyBatis
中别名是不区分大小写的,以及 String#toLowerCase()
可能带来的 bug
MyBatis
中,指定一些类型的设置,也可以不通过类型别名,直接指定全名也行 MyBatis
的结构划分,可以发现 MyBatis
的代码逻辑十分清晰,易读