首先, 我们从 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 的代码逻辑十分清晰, 易读
来源: http://www.jianshu.com/p/ff89136a06df