发布时间:2022-09-06 13:00
在进行Mybatis的源码流程之前,我们首先应该知道mybatis的使用方法。
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!--namespace=绑定一个对应的Dao/Mapper接口-->
<mapper namespace="com.stefan.dao.UserDao">
<!--select查询语句-->
<select id="getUserList" resultType="com.stefan.pojo.User">
select * from demo.user
</select>
</mapper>
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<!--核心配置文件-->
<configuration>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/demo?useSSL=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</dataSource>
</environment>
<environment id="test">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/demo?useSSL=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</dataSource>
</environment>
</environments>
<!--每一个Mapper.xml都需要在MyBatis核心配置文件中注册!-->
<mappers>
<mapper resource="com/stefan/dao/UserMapper.xml"/>
</mappers>
</configuration>
我们最常用的应该是上面这种方法,这样就可以很简单的读到数据库里面的内容并且把他们自动的封装成对象返回。
我们首先来认识一下Mybatis里面的几个比较重要的类
1.Configuration类
1.这个类里面有非常多的内容,这里面封装了mybatis-config.xml的所有内容,比如 environment ,再有typeAliases,还有mappers等等内容。他还对mappers标签里面的内容进行了汇总。
Configuration还负责创建了mybatis其他的核心对象:newParameterHandler(),newResultSetHandler(),
newStatementHandler(),newExecutor()方法等等
2.MappedStatement类
MappedStatement并不是将每个mapper.xml文件封装为一个个对象,而是将每个文件内的每个CRUD标签单独封装成一个MappedStatement对象。标签里的属性就是MappedStatement类里面的属性。自然,这个对象里面的属性也和crud标签所对应。
值得注意的是,此类里面的id属性,是通过命名空间.id保证其唯一性的。还有 这里的StatementType可以是 STATEMENT, PREPARED,CALLABLE中的一种。默认是PrepareStatement。
自然,MappedStatement还对sql语句进行了封装,封装到了BoundSql这个类里面。
不止如此,MappedStatement中也包含有Configuration属性,也就是说,两者是双向关联的关系,所以能通过MappedStatement也能找到Configuration。
3.操作类对象
Mybatis里面其实封装了很多操作类对象,表面看上去是SqlSession来帮我们完成了一系列的操作,其实是SqlSession调用了其他的一些操作类对象。如下图
Configuration类里面对他的默认类型进行了定义
this.defaultExecutorType = ExecutorType.SIMPLE;
1.我们知道,mybatis想要读取到xml里面的内容,就需要去解析。我们所知的解析xml的方式,主要是DOM,SAX和XPath三种。而mybatis中使用的就是第三种XPath。我们先用一个简单的示例来了解一下。
(1) 首先定义一个xml文件。
(2) 利用XPath解析
利用XPathParser的evalNodes方法,我们就可以讲xml解析,封装在一个XNode的集合里面进行使用。而里面所传的参数叫做XPath语法,大家可以自行百度,这里不再赘述。
2.下面我们一行一行来分析。
在这句上面,我们已经拿到了mybatis-config.xml的输入流,利用它来得到一个SqlSessionFactory对象,我们进入到这个方法。
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
SqlSessionFactory var5;
try {
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
var5 = this.build(parser.parse());
} catch (Exception var14) {
throw ExceptionFactory.wrapException("Error building SqlSession.", var14);
} finally {
ErrorContext.instance().reset();
try {
inputStream.close();
} catch (IOException var13) {
}
}
return var5;
}
我们很清楚的可以看到,它创建出一个XMLConfigBuilder 对象去进行解析xml,那么这个对象里就一定封装了XPathParser对象。
进入到parse方法。
public Configuration parse() {
if (this.parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
} else {
this.parsed = true;
this.parseConfiguration(this.parser.evalNode("/configuration"));
return this.configuration;
}
}
this.parser.evalNode("/configuration")这句代码呢就已经把我们的XML文件封装成一个XNode对象了,我们进入到parseConfiguration方法。
这里面就就很明显了,利用传过来的XNode对象,一个个的去解析各个标签,然后用各个方法将他封装给Configuration对象。值得注意的是,他还会在最后一句代码中去解析mappers标签,this.mapperElement(root.evalNode(“mappers”));,所以这就是为什么我们只需要读取mybatis-config.xml文件而不用去读取xxxDaoMapper.xml文件的原因。接下来我们进入这个方法。
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
Iterator var2 = parent.getChildren().iterator();
while(true) {
while(var2.hasNext()) {
XNode child = (XNode)var2.next();
String resource;
if ("package".equals(child.getName())) {
resource = child.getStringAttribute("name");
this.configuration.addMappers(resource);
} else {
resource = child.getStringAttribute("resource");
String url = child.getStringAttribute("url");
String mapperClass = child.getStringAttribute("class");
XMLMapperBuilder mapperParser;
InputStream inputStream;
if (resource != null && url == null && mapperClass == null) {
ErrorContext.instance().resource(resource);
inputStream = Resources.getResourceAsStream(resource);
mapperParser = new XMLMapperBuilder(inputStream, this.configuration, resource, this.configuration.getSqlFragments());
mapperParser.parse();
} else if (resource == null && url != null && mapperClass == null) {
ErrorContext.instance().resource(url);
inputStream = Resources.getUrlAsStream(url);
mapperParser = new XMLMapperBuilder(inputStream, this.configuration, url, this.configuration.getSqlFragments());
mapperParser.parse();
} else {
if (resource != null || url != null || mapperClass == null) {
throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
}
Class<?> mapperInterface = Resources.classForName(mapperClass);
this.configuration.addMapper(mapperInterface);
}
}
}
return;
}
}
}
这个方法,就是用来解析xxxMapper.xml的。
我们可以看到,mappers这个标签下面有两个标签,package和mapper,而mapper下面又有三个属性,所以package和吗,mapper肯定是要分开解析的。源码中也有体现。
再下面,就是对mapper的三个属性的解析,去拿到xxxDaoMapper.xml的资源了。但是,值得注意的是,他的判断语句。
也就是说,每一个mapper标签,都只能在这三个属性中任意存在一个,否则就会抛出异常。
那么,对于package标签来说,拿到内容之后,会把它封装给我们上面所提到的Configuration类。对于mapper标签来说,它会进一步的解析,通过mapperParser.parse();方法。我们进入。
熟悉的代码再一次出现,this.parser.evalNode("/mapper"),我们知道,这句代码就是把一个标签封装成一个XNode对象。传入到configurationElement方法中。
private void configurationElement(XNode context) {
try {
String namespace = context.getStringAttribute("namespace");
if (namespace != null && !namespace.equals("")) {
this.builderAssistant.setCurrentNamespace(namespace);
this.cacheRefElement(context.evalNode("cache-ref"));
this.cacheElement(context.evalNode("cache"));
this.parameterMapElement(context.evalNodes("/mapper/parameterMap"));
this.resultMapElements(context.evalNodes("/mapper/resultMap"));
this.sqlElement(context.evalNodes("/mapper/sql"));
this.buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
} else {
throw new BuilderException("Mapper's namespace cannot be empty");
}
} catch (Exception var3) {
throw new BuilderException("Error parsing Mapper XML. The XML location is '" + this.resource + "'. Cause: " + var3, var3);
}
}
这下就比较明了了,一步步的解析mapper标签里的属性,最后会把他们封装到Configuration中,最后返回到最初build方法的位置,接下来,Configuration已经封装了一切想到的东西,xml文件解析完成。
解析完成之后,会调用this.build(parser.parse());方法,去创建SqlSessionFactory。
很明显,这个SqlSessionFactory是一个DefaultSqlSessionFactory。创建完成之后,返回到我们最开始的位置。执行下一句代码,去创建SqlSession,我们进入到openSession方法。
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
DefaultSqlSession var8;
try {
Environment environment = this.configuration.getEnvironment();
TransactionFactory transactionFactory = this.getTransactionFactoryFromEnvironment(environment);
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
Executor executor = this.configuration.newExecutor(tx, execType);
var8 = new DefaultSqlSession(this.configuration, executor, autoCommit);
} catch (Exception var12) {
this.closeTransaction(tx);
throw ExceptionFactory.wrapException("Error opening session. Cause: " + var12, var12);
} finally {
ErrorContext.instance().reset();
}
return var8;
}
openSession方法中又会调用这个方法,他会先处理一些关于事务的操作,最后创建一个Executor,也就是执行器,执行我们的crud操作。最后再创建一个DefaultSqlSession类型的SqlSession。然后返回。
前面的准备工作已经完成,下面就要开始干活了。我们知道,我们的UserDao是一个接口,那么他就一定要去创建代理对象。我们跟着源码去验证。DeBug进入。
一路进去之后,就到了这个方法。我们看到了一个熟悉的newInstance方法,我们进去之后。
首先创建一个MapperProxy对象,这个对象,它实现了InvocationHandler接口,我们知道,在newInstance创建代理的时候,代理对象调用处理程序的时候,就会去调用这个接口里的invoke方法。
然后下一步newInstance。
protected T newInstance(MapperProxy<T> mapperProxy) {
return Proxy.newProxyInstance(this.mapperInterface.getClassLoader(), new Class[]{this.mapperInterface}, mapperProxy);
}
熟悉的创建代理对象。返回代理对象。一路返回到getMapper方法。此时的UserDao就是一个代理对象了。
创建好代理对象之后,我们就可以去执行具体方法了。这里以查询所有用户的方法getUserList为例。
上面提到,调用方法的时候会直接进入invoke方法。
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
}
if (method.isDefault()) {
return this.invokeDefaultMethod(proxy, method, args);
}
} catch (Throwable var5) {
throw ExceptionUtil.unwrapThrowable(var5);
}
MapperMethod mapperMethod = this.cachedMapperMethod(method);
return mapperMethod.execute(this.sqlSession, args);
}
try里面的语句只是进行一些健壮性的操作,比如被调用的方法是否是Object里面的方法,如果是就直接执行,没必要继续走下面的代码。如果不是就继续。首先创建一个MapperMethod的对象。我们有必要说明一下这个对象。
Java Web现代化开发:Spring Boot + Mybatis + Redis二级缓存
渗透测试 10 --- 扫描 web目录 (dirb、wfuzz、wpscan、nikto)
.net webapi 实现 接口版本控制并打通swagger支持
leetcode-每日一题2022.3.13 UTF-8 编码验证
6年测试工程师被整破防:6年经验只不过是一段经历,根本不值一提!
中国电信发布运营商行业首个云原生关系型数据库TeleDB for openGauss
猿创征文|vue vant-ui数据列表上拉加载更多,下拉刷新功能