MyBatis 是一款优秀的持久层框架,它支持自定义 SQL、存储过程以及高级映射。MyBatis 免除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作。MyBatis 可以通过简单的 XML 或注解来配置和映射原始类型、接口和 Java POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录。
摘自mybatis官网
官网地址 https://mybatis.org/mybatis-3/zh/index.html
分析mybatis主要作用,让程序员仅仅只关注sql语句分析为下图事mybatis的主要作用
三个步骤:
mybatis如何获取数据源
mybatis如何获取sql语句并且执行
mybatis如何获取链接并且返回处理结果集
宏观>微观>图解
1.断点(观察调用栈,利用条件断点)
2.反调
3.根据接口方法找到具体实现
4.猜测类名,方法名
5看控制台日志
首先在我们使用Mybatis时会创建工厂,让工厂去创建sqlsession进行增删改查。
SqlSessionFactory factory = new SqlSessionFactoryBuilder()
.build(Thread.currentThread().getContextClassLoader().getResourceAsStream("configuration.xml"));
mybatis封装后的代码如下,一步一步进行分析
package org.apache.ibatis.demo;
import org.apache.ibatis.demo.domain.User;
import org.apache.ibatis.domain.blog.Blog;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import java.io.IOException;
import java.io.InputStream;
/**
* @auther tuhexuan
* @date 2021/7/19 22:23
*/
public class MybatisMain {
public static void main(String[] args) throws IOException {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
//上面是读取myabtis的配置文件从下面这行开始分析
SqlSessionFactory sqlSessionFactory= new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
// IUserDao userDao = sqlSession.getMapper(IUserDao.class);
// User user = userDao.findUserById(1);
User user =sqlSession.selectOne("org.apache.ibatis.demo.IUserDao.findUserById",1);
System.out.println(user);
}
}
直接进入build方法
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
try {
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
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.
}
}
}
我们将配置文件比如configuration.xml 读取转化为inputstream之后会创建XmlConfigurationBuilder进行解析,他是如何解析的呢,让我们点进去一探究竟
大家应该能够很清楚的看到这里 parseConfiguration(parser.evalNode("/configuration"));解析配置,传入的参数就是我们xml配置文件的configuration根节点。解析我们的配置文件就需要分别解析其中的各个节点。
可以通过断点中的XNode 对象,可以copy value 得到我们读取的配置文件也就是 config.xml文件
我们可以看到此方法解析了许多的节点,properties,settings...等许多接下来我们来一一深入。
首相propertiesElement这个方法是第一个执行的,意味着我们必须在配置前去指定配置文件。例如 jdbcconfig.properties里面是写的jdbc中的driver,url,username,password.
protiesElement()方法
// - -XMLConfiguration
private void propertiesElement(XNode context) throws Exception {
if (context != null) {
// 解析 propertis 的子节点,并将这些节点内容转换为属性对象 Properties
Properties defaults = context.getChildrenAsProperties();
//获取properties节点中resource和url的值
String resource = context.getStringAttribute("resource");
String url = context.getStringAttribute("url");
//❌ 两者都不用空,则抛出异常
//我们在写的时候要注意不可以两个路径都写上
if (resource != null && url != null) {
throw new BuilderException("properties元素不能同时指定URL和基于资源的属性文件引用。请指定一个或另一个.");
}
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);
}
}
大家可以看到properties节点先读取子节点的属性,在进行扫描是否有属性文件并加以解析,如果自身子节点属性名与属性文件相同时,不会报错而是属性文件中的属性会覆盖掉子结点中的赋值。
让我们在深入看一下如何解析读取的子节点属性。
// - -XNode
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属性文件,之后将我们读取到的属性信息设置到XPathParser与Configuration中。
setting当中的设置有很多,子节点也很多,如下面的代码。如果想要看详细信息大家可以到mybatis官网中去查看
让我们看一下它在解析setting结点的时候先将setting子节点将其转换为properties对象再进行设置
Properties settings = settingsAsProperties(root.evalNode("settings"));
下面我们开始深入代码
// - -XMLConfigBuilder
private Properties settingsAsProperties(XNode context) {
if (context == null) {
return new Properties();
}
//获取setting的子节点
Properties props = context.getChildrenAsProperties();
// Check that all settings are known to the configuration class
// 创建Configuration的元信息对象
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;
}
我们在看这段代码的时候,其实大致能明白此方式是干了什么,就是先读取子节点解析为properties对象props,然后为Configuration创建元信息对象,在props通过MetaClass的检测之后返回。大家主要陌生的可能就是MetaClass,接下来我们来看一下这是什么东西
//--MetaClass
public class MetaClass {
//这里又出现两个类反射器工厂与反射器。
private final ReflectorFactory reflectorFactory;
private final Reflector reflector;
//这里构造方法设为私有,我们只能通过下面的forClass进行创建元信息对象
private MetaClass(Class> type, ReflectorFactory reflectorFactory) {
this.reflectorFactory = reflectorFactory;
this.reflector = reflectorFactory.findForClass(type);
}
public static MetaClass forClass(Class> type, ReflectorFactory reflectorFactory) {
return new MetaClass(type, reflectorFactory);
}
//--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()方法其实就是调用了反射器的hasSetter()方法。元信息对象在创建的时候需要两个参数一个是Configuration对象类型,一个则是反射器工厂通过反射器工厂构造反射器进行赋值。ReflectorFactory是一个接口,我已我们需要移步到他的实现类DefaultReflectorFactory。
//--DefaultRefaultorFactory
public class DefaultReflectorFactory implements ReflectorFactory {
private boolean classCacheEnabled = true;
private final ConcurrentMap, Reflector> reflectorMap = new ConcurrentHashMap<>();
public DefaultReflectorFactory() {}
@Override
public boolean isClassCacheEnabled() {
return classCacheEnabled;
}
@Override
public void setClassCacheEnabled(boolean classCacheEnabled) {
this.classCacheEnabled = classCacheEnabled;
}
@Override
public Reflector findForClass(Class> type) {
// classCacheEnabled 默认为 true
if (classCacheEnabled) {
// 从缓存中获取 Reflector 对象
Reflector cached = reflectorMap.get(type);
// 缓存为空,则创建一个新的 Reflector 实例,并放入缓存中
if (cached == null) {
cached = new Reflector(type);
// 将 映射缓存到 map 中,方便下次取用
reflectorMap.put(type, cached);
}
}
反射器工厂为我们构建一个反射器Reflector,这里还具有缓存的功能,如果缓存开启则会创建一个新的Reflector实例放入缓存中。对于Reflector这个类主要通过反射获取目标类的信息的,这里我们就不去细说了。
在 MyBatis 中,我们可以为自己写的一些类定义一个别名。这样在使用的时候,只需要输入别名即可,无需再把全限定的类名写出来。
第一种是仅配置包名,让 MyBatis 去扫描包中的类型,并根据类型得到相应的别名。这种方式可配合 Alias 注解使用,即通过注解为某个类配置别名,而不是让 MyBatis 按照默认规则生成别名。这种方式的配置如下:
第二种方式是通过手动的方式,明确为某个类型配置别名。这种方式的配置如下:
对比这两种方式,第一种自动扫描的方式配置起来比较简单,缺点也不明显。唯一能想到缺点可能就是 MyBatis 会将某个包下所有符合要求的类的别名都解析出来,并形成映射关系。如果你不想让某些类被扫描,这个好像做不到,没发现 MyBatis 提供了相关的排除机制。不过我觉得这并不是什么大问题,最多是多解析并缓存了一些别名到类型的映射,在时间和空间上产生了一些的消耗而已。当然,如果无法忍受这些消耗,可以使用第二种配置方式,通过手工的方式精确配置某些类型的别名。不过这种方式比较繁琐,特别是配置项比较多时。至于两种方式怎么选择,这个看具体的情况了。配置项非常少时,两种皆可。比较多的话,还是让 MyBatis 自行扫描吧。接下来我们来看一下两种不同的配置是如何解析的:
private void typeAliasesElement(XNode parent) {
if (parent != null) {
for (XNode child : parent.getChildren()) {
// 从指定的包中解析别名和类型的映射
if ("package".equals(child.getName())) {
String typeAliasPackage = child.getStringAttribute("name");
configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage);
// 从 typeAlias 节点中解析别名和类型的映射
} else {
// 获取 alias 和 type 属性值,alias 不是必填项,可为空
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);
}
}
}
}
}
1.从指定的包中解析并注册别名
// TypeAliasRegistry
public void registerAliases(String packageName) {
//调用内部重载方法
registerAliases(packageName, Object.class);
}
public void registerAliases(String packageName, Class> superType) {
ResolverUtil> resolverUtil = new ResolverUtil<>();
// 查找某个包下的父类为 superType 的类。从调用栈来看,这里的
// superType = Object.class,所以 ResolverUtil 将查找所有的类。
// 查找完成后,查找结果将会被缓存到内部集合中。
resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
//获取查找结果
Set>> typeSet = resolverUtil.getClasses();
for (Class> type : typeSet) {
// 忽略匿名类,接口,内部类
if (!type.isAnonymousClass() && !type.isInterface() && !type.isMemberClass()) {
// 为类型注册别名
registerAlias(type);
}
}
}
总结来看,扫描包的步骤就两步一是查找指定包下的所有类;二是遍历查找到的类型集合,为每个类型注册别名。其中最后注册别名没有详细为大家讲,我们接下来说从
2. 从
在别名的配置中,type 属性是必须要配置的,而 alias 属性则不是必须的。。如果使用者未配置 alias 属性,则需要 MyBatis 自行为目标类型生成别名。对于别名为空的情况,注册别名的任务交由 registerAlias(Class>) 方法处理。若不为空,则由 registerAlias(String,Class>) 进行别名注册。代码如下:
// TypeAliasRegistry
public void registerAlias(Class> type) {
// 获取全路径类名的简称
String alias = type.getSimpleName();
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);
// 如果 TYPE_ALIASES 中存在了某个类型映射,这里判断当前类型与映射中的类型
// 是否一致,不一致则抛出异常,不允许一个别名对应两种类型
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() + "'.");
}
//缓存别名类型
typeAliases.put(key, value);
}
若用户未明确配置 alias 属性,MyBatis 会使用类名的小写形式作为别名。若类中有@Alias 注解,则从注解中取值作为别名。
别名解析并不是很难让我们来看一下大致步骤:
3. mybatis内部当中注册的别名
//Configuration
public Configuration() {
// 注册事务工厂的别名
typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class);
typeAliasRegistry.registerAlias("MANAGED", ManagedTransactionFactory.class);
typeAliasRegistry.registerAlias("JNDI", JndiDataSourceFactory.class);
// 注册数据源的别名
typeAliasRegistry.registerAlias("POOLED", PooledDataSourceFactory.class);
typeAliasRegistry.registerAlias("UNPOOLED", UnpooledDataSourceFactory.class);
typeAliasRegistry.registerAlias("PERPETUAL", PerpetualCache.class);
// 注册缓存策略的别名
typeAliasRegistry.registerAlias("FIFO", FifoCache.class); //先进先出
typeAliasRegistry.registerAlias("LRU", LruCache.class); //最少使用
typeAliasRegistry.registerAlias("SOFT", SoftCache.class); //软引用缓存
typeAliasRegistry.registerAlias("WEAK", WeakCache.class); //弱引用缓存
typeAliasRegistry.registerAlias("DB_VENDOR", VendorDatabaseIdProvider.class);
typeAliasRegistry.registerAlias("XML", XMLLanguageDriver.class);
typeAliasRegistry.registerAlias("RAW", RawLanguageDriver.class);
// 注册日志类的别名
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);
// 注册动态代理工厂的别名
typeAliasRegistry.registerAlias("CGLIB", CglibProxyFactory.class);
typeAliasRegistry.registerAlias("JAVASSIST", JavassistProxyFactory.class);
languageRegistry.setDefaultDriverClass(XMLLanguageDriver.class);
languageRegistry.register(RawLanguageDriver.class);
}
// -☆- TypeAliasRegistry
public TypeAliasRegistry() {
registerAlias("string", String.class);
//这里配置都是我们经常使用了一些类型
registerAlias("byte", Byte.class);
registerAlias("long", Long.class);
registerAlias("short", Short.class);
registerAlias("int", Integer.class);
registerAlias("integer", Integer.class);
registerAlias("double", Double.class);
registerAlias("float", Float.class);
registerAlias("boolean", Boolean.class);
.··· 省略 ···
下面就是最主要的mybatis解析配置文件的部分了
MyBatis 中,事务管理器(transactionManager)和数据源(dataSource)是配置在
⭐即使environments节点下可以写多个环境,但是我们每个 SqlSessionFactory 实例只能选择一种环境。
如何配置呢,我们来看一下
注意一些关键点:
如何解析的?
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 对象,并设置到 configuration 中
Environment.Builder environmentBuilder = new Environment.Builder(id)
.transactionFactory(txFactory)
.dataSource(dataSource);
configuration.setEnvironment(environmentBuilder.build());
}
}
}
}