【Mybatis】【配置文件解析】【二】Mybatis源码解析-别名、环境变量、插件、ObjectFactory
1 前言
在上一节我们分析了properties和settings,这节我们分析下别名、环境变量、插件以及ObjectFactory的解析。
2 源码分析
2.1 解析typeAliases
typeAliases 标签下可以有多个package和多个typeAlias <!ELEMENT typeAliases (typeAlias*,package*)> <!ELEMENT typeAlias EMPTY> <!ATTLIST typeAlias type CDATA #REQUIRED 必要的 alias CDATA #IMPLIED >
类型别名可为 Java 类型设置一个缩写名字。 它仅用于 XML 配置,意在降低冗余的全限定类名书写。例如:
<typeAliases>
<typeAlias alias="Author" type="domain.blog.Author"/>
<typeAlias alias="Blog" type="domain.blog.Blog"/>
<typeAlias alias="Comment" type="domain.blog.Comment"/>
<typeAlias alias="Post" type="domain.blog.Post"/>
<typeAlias alias="Section" type="domain.blog.Section"/>
<typeAlias alias="Tag" type="domain.blog.Tag"/>
</typeAliases>
当这样配置时,Blog
可以用在任何使用 domain.blog.Blog
的地方。
也可以指定一个包名,MyBatis 会在包名下面搜索需要的 Java Bean,比如:
<typeAliases>
<package name="domain.blog"/>
</typeAliases>
每一个在包 domain.blog
中的 Java Bean,在没有注解的情况下,会使用 Bean 的首字母小写的非限定类名来作为它的别名。 比如 domain.blog.Author
的别名为 author
;若有注解,则别名为其注解值。见下面的例子:
@Alias("author")
public class Author {
...
}
别名 | 映射的类型 |
---|---|
_byte | byte |
_char (since 3.5.10) | char |
_character (since 3.5.10) | char |
_long | long |
_short | short |
_int | int |
_integer | int |
_double | double |
_float | float |
_boolean | boolean |
string | String |
byte | Byte |
char (since 3.5.10) | Character |
character (since 3.5.10) | Character |
long | Long |
short | Short |
int | Integer |
integer | Integer |
double | Double |
float | Float |
boolean | Boolean |
date | Date |
decimal | BigDecimal |
bigdecimal | BigDecimal |
biginteger | BigInteger |
object | Object |
date[] | Date[] |
decimal[] | BigDecimal[] |
bigdecimal[] | BigDecimal[] |
biginteger[] | BigInteger[] |
object[] | Object[] |
map | Map |
hashmap | HashMap |
list | List |
arraylist | ArrayList |
collection | Collection |
iterator | Iterator |
那么我们来看下两种方式解析的出发点:
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 { /** * typeAlias 节点中解析别名和类型的映射 * 获取 alias type 的属性值 */ String alias = child.getStringAttribute("alias"); String type = child.getStringAttribute("type"); try { // 加载 type 对应的类型 Class<?> clazz = Resources.classForName(type); // 注册别名到类型的映射 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); } } } } }
针对这两种方式我们详细看下各种方式的解析。
2.1.1 解析typeAliases-包方式的解析
public void registerAliases(String packageName) { // 调用重载方法 registerAliases(packageName, Object.class); } public void registerAliases(String packageName, Class<?> superType) { ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<>(); /* * 查找某个包下的父类为 superType 的类。从调用栈来看,这里的 * superType = Object.class,所以 ResolverUtil 将查找所有的类。 * 查找完成后,查找结果将会被缓存到内部集合中。 */ resolverUtil.find(new ResolverUtil.IsA(superType), packageName); // 获取查找结果 Set<Class<? extends Class<?>>> typeSet = resolverUtil.getClasses(); for (Class<?> type : typeSet) { // Ignore inner classes and interfaces (including package-info.java) // Skip also inner classes. 忽略匿名类,接口,内部类、package-info.java 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"); } // issue #748 // 名字全部改为小写模式 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() + "'."); } // 添加进别名映射map中 key就是别名 value是class typeAliases.put(key, value); }
可以看到ResolverUtil,又是一个Mybatis的一个基础工具,这个工具怎么获取的我们后续单独讲,那么针对包的别名解析大概过程如下:
- 获取包名下的所有的类
- 忽略掉匿名类、接口、内部类以及package-info
- 默认别名为类的简称,如果类存在注解@Alias的话则获取注解的值
- 注册别名到类的映射关系,若发现有多个别名会直接报错
其实最后我们的别名和类的映射关系是放到了TypeAliasRegistry中,我们再大概看下该类的信息:
public class TypeAliasRegistry { // 缓存 别名 -> Class private final Map<String, Class<?>> typeAliases = new HashMap<>(); public TypeAliasRegistry() { // 看这就是默认添加的 这就是人家的resultType敢写string的原因 registerAlias("string", String.class); registerAlias("byte", Byte.class); registerAlias("long", Long.class); registerAlias("short", Short.class); ...省略哈 } }
可以看到默认的别名映射在类实例化的时候,就已经添加进去了。
2.1.2 解析typeAliases-配置typeAlias方式的解析
我们看dtd约束typeAlias有一个type是必须的,alias不是必须的,那么其实就是当alias不存的话,其实就是会使用默认的类的简称作为别名,有的话那么就直接注册,其实就是我们刚才包方式解析里边的两个方法。
// 没有alias值的 public void registerAlias(Class<?> type) {} // 有alias值的 public void registerAlias(String alias, Class<?> value) {}
2.2 解析plugins
MyBatis 允许你在映射语句执行过程中的某一点进行拦截调用。默认情况下,MyBatis 允许使用插件来拦截的方法调用包括:
- Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
- ParameterHandler (getParameterObject, setParameters)
- ResultSetHandler (handleResultSets, handleOutputParameters)
- StatementHandler (prepare, parameterize, batch, update, query)
这些类中方法的细节可以通过查看每个方法的签名来发现,或者直接查看 MyBatis 发行包中的源代码。 如果你想做的不仅仅是监控方法的调用,那么你最好相当了解要重写的方法的行为。 因为在试图修改或重写已有方法的行为时,很可能会破坏 MyBatis 的核心模块。 这些都是更底层的类和方法,所以使用插件的时候要特别当心。这段来自官方的话奥。
那么怎么写一个插件呢,只需实现 Interceptor 接口,然后在插件类上添加@Intercepts
和@Signature
注解,用于指定想要拦截的目标方法。
官方示例哈:
@Intercepts({@Signature( type= Executor.class, method = "update", args = {MappedStatement.class,Object.class})}) public class ExamplePlugin implements Interceptor { private Properties properties = new Properties(); @Override public Object intercept(Invocation invocation) throws Throwable { // implement pre processing if need Object returnObject = invocation.proceed(); // implement post processing if need return returnObject; } @Override public void setProperties(Properties properties) { this.properties = properties; } }
<!-- mybatis-config.xml --> <plugins> <plugin interceptor="org.mybatis.example.ExamplePlugin"> <property name="someProperty" value="100"/> </plugin> </plugins>
上面的插件将会拦截在 Executor 实例中所有的 “update” 方法调用, 这里的 Executor 是负责执行底层映射语句的内部对象。
提示 覆盖配置类
除了用插件来修改 MyBatis 核心行为以外,还可以通过完全覆盖配置类来达到目的。只需继承配置类后覆盖其中的某个方法,再把它传递到 SqlSessionFactoryBuilder.build(myConfig) 方法即可。再次重申,这可能会极大影响 MyBatis 的行为,务请慎之又慎。
插件的dtd约束:
<!ELEMENT plugins (plugin+)> <!ELEMENT plugin (property*)> <!ATTLIST plugin interceptor CDATA #REQUIRED >
那么我们来看下如何解析plugin的:
private void pluginElement(XNode parent) throws Exception { if (parent != null) { for (XNode child : parent.getChildren()) { // plugin标签上的interceptor属性值 String interceptor = child.getStringAttribute("interceptor"); // 获取配置属性值 Properties properties = child.getChildrenAsProperties(); // 解析拦截器的类型,并创建拦截器 Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).getDeclaredConstructor().newInstance(); // 设置属性 interceptorInstance.setProperties(properties); // 添加拦截器到 Configuration 中 configuration.addInterceptor(interceptorInstance); } } }
如上,插件解析的过程还是比较简单的。首先是获取配置,然后再解析拦截器类型,并实例化拦截器。最后向拦截器中设置属性,并将拦截器添加到 Configuration 中。
2.3 解析environments
MyBatis 可以配置成适应多种环境,这种机制有助于将 SQL 映射应用于多种数据库之中, 现实情况下有多种理由需要这么做。例如,开发、测试和生产环境需要有不同的配置;或者想在具有相同 Schema 的多个生产数据库中使用相同的 SQL 映射。还有许多类似的使用场景。
不过要记住:尽管可以配置多个环境,但每个 SqlSessionFactory 实例只能选择一种环境。
所以,如果你想连接两个数据库,就需要创建两个 SqlSessionFactory 实例,每个数据库对应一个。而如果是三个数据库,就需要三个实例,依此类推,记起来很简单:
- 每个数据库对应一个 SqlSessionFactory 实例
为了指定创建哪种环境,只要将它作为可选的参数传递给 SqlSessionFactoryBuilder 即可。可以接受环境配置的两个方法签名是:
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(reader, environment);
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(reader, environment, properties);
如果忽略了环境参数,那么将会加载默认环境,如下所示:
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(reader);
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(reader, properties);
environments 元素定义了如何配置环境。
<environments default="development">
<environment id="development">
<transactionManager type="JDBC">
<property name="..." value="..."/>
</transactionManager>
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>
</environments>
注意一些关键点:
- 默认使用的环境 ID(比如:default="development")。
- 每个 environment 元素定义的环境 ID(比如:id="development")。
- 事务管理器的配置(比如:type="JDBC")。
- 数据源的配置(比如:type="POOLED")。
默认环境和环境 ID 顾名思义。 环境可以随意命名,但务必保证默认的环境 ID 要匹配其中一个环境 ID。我们这里先着重讲解析过程哈,具体的关于怎么用我们后边再讲哈。
环境变量的dtd约束:
<!ELEMENT environments (environment+)> <!ATTLIST environments default CDATA #REQUIRED > <!ELEMENT environment (transactionManager,dataSource)> <!ATTLIST environment id CDATA #REQUIRED > <!ELEMENT transactionManager (property*)> <!ATTLIST transactionManager type CDATA #REQUIRED > <!ELEMENT dataSource (property*)> <!ATTLIST dataSource type CDATA #REQUIRED >
那么我们来看下解析:
private void environmentsElement(XNode context) throws Exception { if (context != null) { if (environment == null) { // 获取环境变量上default属性值 environment = context.getStringAttribute("default"); } for (XNode child : context.getChildren()) { // 获取 id 属性 String id = child.getStringAttribute("id"); /* * 检测当前 environment 节点的 id 与其父节点 environments 的属性 default * 内容是否一致,一致则返回 true,否则返回 false甚至报错 */ if (isSpecifiedEnvironment(id)) { // 解析 transactionManager 节点 TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager")); // 解析 dataSource 节点 DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource")); // 创建 DataSource 对象 DataSource dataSource = dsFactory.getDataSource(); Environment.Builder environmentBuilder = new Environment.Builder(id) .transactionFactory(txFactory) .dataSource(dataSource); // 构建 Environment 对象,并设置到 configuration 中 configuration.setEnvironment(environmentBuilder.build()); } } } }
以上就是环境变量的解析,至于事务管理器、数据源都是怎么创建的其实跟插件的创建方式一样都是获取到构造器然后newInstance()完事这里就不再罗列了哈。
2.4 解析objectFactory
每次 MyBatis 创建结果对象的新实例时,它都会使用一个对象工厂(ObjectFactory)实例来完成实例化工作。 默认的对象工厂需要做的仅仅是实例化目标类,要么通过默认无参构造方法,要么通过存在的参数映射来调用带有参数的构造方法。 如果想覆盖对象工厂的默认行为,可以通过创建自己的对象工厂来实现。比如:
// ExampleObjectFactory.java public class ExampleObjectFactory extends DefaultObjectFactory { @Override public <T> T create(Class<T> type) { return super.create(type); } @Override public <T> T create(Class<T> type, List<Class<?>> constructorArgTypes, List<Object> constructorArgs) { return super.create(type, constructorArgTypes, constructorArgs); } @Override public void setProperties(Properties properties) { super.setProperties(properties); } @Override public <T> boolean isCollection(Class<T> type) { return Collection.class.isAssignableFrom(type); }
}
<!-- mybatis-config.xml --> <objectFactory type="org.mybatis.example.ExampleObjectFactory"> <property name="someProperty" value="100"/> </objectFactory>
ObjectFactory 接口很简单,它包含两个创建实例用的方法,一个是处理默认无参构造方法的,另外一个是处理带参数的构造方法的。 另外,setProperties 方法可以被用来配置 ObjectFactory,在初始化你的 ObjectFactory 实例后, objectFactory 元素体中定义的属性会被传递给 setProperties 方法。
我们来看下ObjectFactory的dtd约束:
<!ELEMENT objectFactory (property*)> <!ATTLIST objectFactory type CDATA #REQUIRED >
解析就不展示了哈,也是跟插件的创建方式一样的获取到构造器直接实例化哈,然后设置进configuration中。
3 小结
本节我们介绍了别名、插件、环境变量、对象工厂的解析,有理解不对的地方欢迎指正哈。