一、mybatis的简单使用
根据mybatis官网提供的例子,需要这么几个步骤
1、获取mybatis配置文件的输入流对象
2、创建SqlSessionFactoryBuilder对象
3、调用SqlSessionFactoryBuilder.build方法,传入前面获取的输入流对象,得到SqlSessionFactory对象
4、获取SqlSession对象
InputStream resource = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
SqlSessionFactory sqlsessionFactory = builder.build(resource);
SqlSession sqlSession = sqlsessionFactory.openSession();
拿到sqlsession对象后就可以执行sql了,执行sql的方式有两种
第一种直接通过sqlsession调用select等方法
Object one = sqlSession.selectOne("com.lyy.mybatis_source.mapper.BankMapper.count");
System.out.println(one);
这里selectOne方法传入的是sql文件中的statmentId,
第二种方式需要先获取到mapper接口的代理对象,然后通过代理对象执行sql
BankMapper mapper = sqlSession.getMapper(BankMapper.class);
int count = mapper.count();
System.out.println(count);
上边的代码是在builder.build方法中完成了对配置文件的解析,mybatis提供了多个重载的build方法
这些重载方法要么提供了输入流参数,要么提供了Configuration对象作为参数。
上边演示的是传入输入流来获取SqlSessionFactory,其实也可以先构造一个Configuration对象,然后传入build方法
Configuration configuration = new Configuration();
TransactionFactory transactionFactory = new JdbcTransactionFactory();
DruidDataSource dataSource = new DruidDataSource();
dataSource.setUrl("jdbc:mysql://localhost:3306/transaction_test");
dataSource.setUsername("root");
dataSource.setPassword("root");
dataSource.setDriverClassName("com.mysql.jdbc.Driver");
Environment environment = new Environment("dev",transactionFactory,dataSource);
configuration.setEnvironment(environment);
configuration.addMapper(BankMapper.class);
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
SqlSessionFactory sqlSessionFactory = builder.build(configuration);
SqlSession sqlSession = sqlSessionFactory.openSession();
Object o = sqlSession.selectOne("com.lyy.mybatis_source.mapper.BankMapper.count");
二、mybatis对配置文件的解析
以下是一个简单的mybatis配置文件
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"https://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.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/transaction_test"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="com/lyy/mybatis_source/mapper/BankMapper.xml"/>
</mappers>
</configuration>
根据上边的代码可以知道对配置文件的解析是在SqlSessionFactoryBuilder.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.
}
}
}
public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}
可以看到传入输入流的build方法是先创建了一个XMLConfigBuilder对象parser,在调用parser.parse()方法完成对输入流的解析,得到一个Configuration对象,最后把这个对象传给重载的build方法完成SqlSessionFactory对象的创建。这里的关键步骤就是解析配置文件得到Configuration对象。
接着看一下XMLConfigBuilder
的构造方法
public XMLConfigBuilder(InputStream inputStream, String environment, Properties props) {
this(new XPathParser(inputStream, true, props, new XMLMapperEntityResolver()), environment, props);
}
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;
}
先根据输入流创建一个解析器对象XPathParser
,在调用重载的构造方法把解析器对象赋值给类中的属性。解析器内部会根据输入流把xml文件封装成一个Document
对象,后边的步骤中就是从这个Document对象中获取配置信息。
接着看一下XMLConfigBuilder.parse方法
public Configuration parse() {
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
// 这里evalNode方法就是从document对象中获取节点信息
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
private void parseConfiguration(XNode root) {
try {
// 解析properties标签
propertiesElement(root.evalNode("properties"));
Properties settings = settingsAsProperties(root.evalNode("settings"));
loadCustomVfs(settings);
loadCustomLogImpl(settings);
// 解析typeAliases标签
typeAliasesElement(root.evalNode("typeAliases"));
pluginElement(root.evalNode("plugins"));
objectFactoryElement(root.evalNode("objectFactory"));
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
reflectorFactoryElement(root.evalNode("reflectorFactory"));
settingsElement(settings);
// read it after objectFactory and objectWrapperFactory issue #631
environmentsElement(root.evalNode("environments"));
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
typeHandlerElement(root.evalNode("typeHandlers"));
// 解析mappers标签
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
可以看到在parseConfiguration方法中会对mybaits配置文件中的各种标签进行解析。XMLConfigBuilder中有一个configuration属性,解析过程中会对这个属性赋值,解析完成后返回这个属性。然后回到SqlSessionFactoryBuilder.build方法中继续创建SqlSessionFactory对象。
整个过程涉及到两个对象
SqlSessionFactoryBuilder
, ----> XMLConfigBuilder
三、sql文件的解析
上边的XMLConfigBuilder.parseConfiguration方法中有一句mapperElement(root.evalNode("mappers"));
,这里就是在解析sql文件,接着看下这个方法的源码
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
if ("package".equals(child.getName())) {
String mapperPackage = child.getStringAttribute("name");
configuration.addMappers(mapperPackage);
} else {
String resource = child.getStringAttribute("resource");
String url = child.getStringAttribute("url");
String mapperClass = child.getStringAttribute("class");
if (resource != null && url == null && mapperClass == null) {
ErrorContext.instance().resource(resource);
try(InputStream inputStream = Resources.getResourceAsStream(resource)) {
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
mapperParser.parse();
}
} else if (resource == null && url != null && mapperClass == null) {
ErrorContext.instance().resource(url);
try(InputStream inputStream = Resources.getUrlAsStream(url)){
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
mapperParser.parse();
}
} else if (resource == null && url == null && mapperClass != null) {
Class<?> mapperInterface = Resources.classForName(mapperClass);
configuration.addMapper(mapperInterface);
} else {
throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
}
}
}
}
}
我们在mappers标签中可以配置多个mapper标签,所以有一个for循环,每一个mapper标签上可以有
package,resource,class,url这几种属性,所以这个方法中对这几种情况分别进行处理。
其中package,class这两种情况类似,都调用的是configuration对象中的添加Mapper的方法。
resource,url这两种都是先创建XMLMapperBuilder
对象,在调用parse方法。
parse方法会完成对sql文件的解析最终会创建MappedStatement
对象,
Configuration类中有一个属性mappedStatements
Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>("Mapped Statements collection")
这是一个Map,里边存储Statement id和MappedStatement对象的对应关系
parse方法执行完后会把创建的MappedStatement
对象添加到这个Map中,这样后续执行sql的时候就可以根据id找到这个MappedStatement
下面接着看下XMLMapperBuilder.parse
对sql文件的解析过程。
public void parse() {
if (!configuration.isResourceLoaded(resource)) {
configurationElement(parser.evalNode("/mapper"));
configuration.addLoadedResource(resource);
bindMapperForNamespace();
}
parsePendingResultMaps();
parsePendingCacheRefs();
parsePendingStatements();
}
private void configurationElement(XNode context) {
try {
String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.isEmpty()) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
builderAssistant.setCurrentNamespace(namespace);
cacheRefElement(context.evalNode("cache-ref"));
cacheElement(context.evalNode("cache"));
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
resultMapElements(context.evalNodes("/mapper/resultMap"));
sqlElement(context.evalNodes("/mapper/sql"));
buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
} catch (Exception e) {
throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
}
}
这里这个buildStatementFromContext方法会对sql语句进行解析。
private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
for (XNode context : list) {
final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
try {
statementParser.parseStatementNode();
} catch (IncompleteElementException e) {
configuration.addIncompleteStatement(statementParser);
}
}
}
然后就会调转到一个新对象XMLStatementBuilder.parseStatementNode
方法中继续解析
这个过程涉及这几个对象
XMLConfigBuilder
,--->XMLMapperBuilder
,--->XMLStatementBuilder
四、解析过程总结
以提供xml配置文件的方式来使用mybatis,上边简单描述了xml配置文件的解析和sql文件的解析过程。
五、sql语句执行过程分析
mybatis提供了一个SqlSession
接口,该接口中定义了对数据库的操作方法,默认的实现是DefaultSqlSession
sql执行过程中涉及到了这几个对象
上面提到mybatis执行sql时有两种方式,通过sqlsession执行和通过mapper代理对象来执行,实际上通过mapper代理对象来执行sql时最终还是通过sqlsession来执行的。
5.1 执行器Executor的继承体系
sqlsession中会持有Executor(执行器)对象,下面来介绍下Executor的继承体系
需要注意的是一级缓存和二级缓存实现的地方,一级缓存在BaseExecutor中,二级缓存在CachingExecutor中
六、通过mapper接口的代理对象执行sql的原理
BankMapper mapper = sqlSession.getMapper(BankMapper.class);
int count = mapper.count();
使用这种方式执行sql时,可以肯定的是mybatis提供了接口的代理对象,来看下Sqlsession
接口的默认实现DefaultSqlSession
中的getMapper方法
public <T> T getMapper(Class<T> type) {
return configuration.getMapper(type, this);
}
可以看到调用的是Configuration类中的getMapper方法,继续看这个方法
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
return mapperRegistry.getMapper(type, sqlSession);
}
可以看到是从Configuration类中的属性mapperRegistry
中获取代理对象,并且传递了sqlSession对象,所以最终生成的代理对象也是通过这个sqlsession对象来执行sql的。来看下mapperRegistry这个属性
protected final MapperRegistry mapperRegistry = new MapperRegistry(this);
来看下这个类中的getMapper方法源码
public class MapperRegistry {
private final Configuration config;
private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();
public MapperRegistry(Configuration config) {
this.config = config;
}
@SuppressWarnings("unchecked")
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
if (mapperProxyFactory == null) {
throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
}
try {
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception e) {
throw new BindingException("Error getting mapper instance. Cause: " + e, e);
}
}
......省略......
这个MapperRegistry中有一个knownMappers属性,这是一个map,getMapper方法就是从这个map中拿出对应的MapperProxyFactory对象,然后调用newInstance方法。
这个knownMappers集合中的元素,是在解析sql文件的过程中进行赋值的,解析过程中会找到Configuration类的
mapperRegistry属性,然后找到mapperRegistry.knownMappers
这个集合,给这个集合进行赋值。
再看下MapperProxyFactory
类的newInstance
方法
public class MapperProxyFactory<T> {
private final Class<T> mapperInterface;
private final Map<Method, MapperMethodInvoker> methodCache = new ConcurrentHashMap<>();
public MapperProxyFactory(Class<T> mapperInterface) {
this.mapperInterface = mapperInterface;
}
public Class<T> getMapperInterface() {
return mapperInterface;
}
public Map<Method, MapperMethodInvoker> getMethodCache() {
return methodCache;
}
@SuppressWarnings("unchecked")
protected T newInstance(MapperProxy<T> mapperProxy) {
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
public T newInstance(SqlSession sqlSession) {
final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
}
可以看到先创建了一个MapperProxy
对象,然后利用jdk动态代理创建mapperInterface的代理对象,
这个mapperInterface就是我们传过来的mapper接口的class文件。
哪这个MapperProxy
肯定实现了jdk的InvocationHandler
接口,在它的invoke方法中最终会调用sqlsession的各种方法。
MapperProxy类的invoke方法
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
if (Object.class.equals(method.getDeclaringClass())) {
//如果调用的是Object类的方法直接反射执行
return method.invoke(this, args);
} else {
// 执行方法时会走到这个分支
// cachedInvoker这个方法会返回一个MapperMethodInvoker对象
//MapperMethodInvoker是MapperProxy中的一个内部接口
// 在这个类中提供了两个实现类 DefaultMethodInvoker(执行接口默认方法),PlainMethodInvoker(其他sql)
return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
private MapperMethodInvoker cachedInvoker(Method method) throws Throwable {
try {
return MapUtil.computeIfAbsent(methodCache, method, m -> {
if (m.isDefault()) {
try {
if (privateLookupInMethod == null) {
return new DefaultMethodInvoker(getMethodHandleJava8(method));
} else {
return new DefaultMethodInvoker(getMethodHandleJava9(method));
}
} catch (IllegalAccessException | InstantiationException | InvocationTargetException
| NoSuchMethodException e) {
throw new RuntimeException(e);
}
} else {
return new PlainMethodInvoker(new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
}
});
} catch (RuntimeException re) {
Throwable cause = re.getCause();
throw cause == null ? re : cause;
}
}
所以当我们执行mapper接口中的方法时最终会执行PlainMethodInvoker中的invoke方法。
来看下这个方法的源码
private static class PlainMethodInvoker implements MapperMethodInvoker {
private final MapperMethod mapperMethod;
public PlainMethodInvoker(MapperMethod mapperMethod) {
super();
this.mapperMethod = mapperMethod;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
return mapperMethod.execute(sqlSession, args);
}
}
可以看到执行的是MapperMethod类的execute方法,这个方法中会调用sqlsession执行具体的操作
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
//这个convertArgsToSqlCommandParam方法会完成把接口参数转成mabatis需要的参数形式
// 如果接口参数只有一个且没有加@param注解 就不做转换直接传给接下来的步骤
// 如果接口参数有多个或者添加了@param注解,就会把接口参数放到一个map里再传给后续的步骤
switch (command.getType()) {
case INSERT: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.insert(command.getName(), param));
break;
}
case UPDATE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.update(command.getName(), param));
break;
}
case DELETE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.delete(command.getName(), param));
break;
}
case SELECT:
if (method.returnsVoid() && method.hasResultHandler()) {
executeWithResultHandler(sqlSession, args);
result = null;
} else if (method.returnsMany()) {
result = executeForMany(sqlSession, args);
} else if (method.returnsMap()) {
result = executeForMap(sqlSession, args);
} else if (method.returnsCursor()) {
result = executeForCursor(sqlSession, args);
} else {
Object param = method.convertArgsToSqlCommandParam(args);
result = sqlSession.selectOne(command.getName(), param);
if (method.returnsOptional()
&& (result == null || !method.getReturnType().equals(result.getClass()))) {
result = Optional.ofNullable(result);
}
}
break;
case FLUSH:
result = sqlSession.flushStatements();
break;
default:
throw new BindingException("Unknown execution method for: " + command.getName());
}
if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
throw new BindingException("Mapper method '" + command.getName()
+ " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
}
return result;
}