Mybatis框架概述
Mybatis是持久层的框架,它内部封装了jdbc,使开发的时候只需要关注sql语句本身,不需要话费精力去处理加载驱动、创建连接、创建statement等。下面我们也是通过一个实例来对它进行分析。
首先编写一个SqlMapConfig.xml
<?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>
<!--配置Mybatis的环境-->
<environments default="mysql">
<!--配置mysql环境-->
<environment id="mysql">
<!--配置事务类型-->
<transactionManager type="JDBC"></transactionManager>
<!--配置数据源,也就是连接池-->
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://127.0.0.1:3306/ssm?
characterEncoding=utf8"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</dataSource>
</environment>
</environments>
<!--配置mybatis映射的位置-->
<mappers>
<mapper resource="mappers/UserMapper.xml"/>
</mappers>
</configuration>
在resources目录下新建一个mappers目录,在然后新建一个UserMapper.xml配置文件
<?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">
<mapper namespace="com.jd.dao.UserMapper">
<select id="findUser" resultType="com.jd.domain.User">
select id ,userName as userName,birthday as birthday,sex as sex,
address as address FROM user where yn=1
</select>
</mapper>
新建一个实体类
@Data
public class User {
private Integer id;
private Date birthday;
private String userName;
private String sex;
private String address;
private Integer yn;
}
创建一个接口类
public interface UserMapper {
/**
* 查询所用用户
*/
public List<User> findUser();
}
编写测试类
public class MybatisTest {
@Test
public void test() throws IOException {
//第一部分初始化工作,解析配置文件
//1、读取配置文件
InputStream in=Resources.getResourceAsStream("SqlMapConfig.xml");
//2、创建SqlSessionFactory的构建者对象
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
//3、使用构建者创建工厂对象SqlSessionFactory
SqlSessionFactory factory= builder.build(in);
//第二部分,执行sql
//4、使用SqlSessionFactory创建SqlSession
SqlSession session=factory.openSession();
//5、使用SqlSession创建dao接口的代理对象
UserMapper userDao =session.getMapper(UserMapper.class);
//6、使用代理对象执行查询方法
List<User> list=userDao.findUser();
for (User user:list){
System.out.println(user);
}
//7、释放资源
session.close();
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
在这个位置,其实我们还有另外一种写法,
我们本次说说使用代理对象调用接口中的方法
接下来我们就要分析原生Mybatis的原理了
主要分为两个部分,
这部分代码主要做的是初始化工作,解析配置文件
这部分代码主要做的是执行sql
首先我们先分析第一部分
//1、读取配置文件
InputStream in=Resources.getResourceAsStream("SqlMapConfig.xml");
//2、创建SqlSessionFactory的构建者对象
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
//3、使用构建者创建工厂对象SqlSessionFactory
SqlSessionFactory factory= builder.build(in);
开始上源码:
// 1.我们最初调用的build
public SqlSessionFactory build(InputStream inputStream, Properties properties) {
//调用了重载方法
return this.build((InputStream)inputStream, (String)null, properties);
}
// 2.调用的重载方法
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
SqlSessionFactory var5;
try {
// XMLConfigBuilder是专门解析mybatis的配置文件的类
XMLConfigBuilder e = new XMLConfigBuilder(inputStream, environment, properties);
//又调用buid的一个重载方法,e.parse()的返回值是Configuration对象 注
var5 = this.build(e.parse());
} catch (Exception var14) {
throw ExceptionFactory.wrapException("Error building SqlSession.", var14);
} finally {
ErrorContext.instance().reset();
try {
inputStream.close();
} catch (IOException var13) {
;
}
}
return var5;
}
解:Configuration的对象是什么呢
Configuration的对象跟xml配置文件的对象类似
比如xml配置文件中的标签包括:
properties(属性),settings(设置),typeAliases(类型别名),typeHandlers(类型处理器),objectFactory(对象工厂),mappers(映射器)等
Configuration类的成员变量
public class Configuration {
protected Environment environment;
protected boolean safeRowBoundsEnabled;
protected boolean safeResultHandlerEnabled;
protected boolean mapUnderscoreToCamelCase;
protected boolean aggressiveLazyLoading;
protected boolean multipleResultSetsEnabled;
protected boolean useGeneratedKeys;
protected boolean useColumnLabel;
protected boolean cacheEnabled;
protected boolean callSettersOnNulls;
protected boolean useActualParamName;
protected boolean returnInstanceForEmptyRow;
protected String logPrefix;
protected Class<? extends Log> logImpl;
protected Class<? extends VFS> vfsImpl;
protected LocalCacheScope localCacheScope;
protected JdbcType jdbcTypeForNull;
protected Set<String> lazyLoadTriggerMethods;
protected Integer defaultStatementTimeout;
protected Integer defaultFetchSize;
protected ExecutorType defaultExecutorType;
protected AutoMappingBehavior autoMappingBehavior;
protected AutoMappingUnknownColumnBehavior autoMappingUnknownColumnBehavior;
protected Properties variables;
protected ReflectorFactory reflectorFactory;
protected ObjectFactory objectFactory;
protected ObjectWrapperFactory objectWrapperFactory;
protected boolean lazyLoadingEnabled;
protected ProxyFactory proxyFactory;
protected String databaseId;
protected Class<?> configurationFactory;
protected final MapperRegistry mapperRegistry;
protected final InterceptorChain interceptorChain;
protected final TypeHandlerRegistry typeHandlerRegistry;
protected final TypeAliasRegistry typeAliasRegistry;
protected final LanguageDriverRegistry languageRegistry;
protected final Map<String, MappedStatement> mappedStatements;
protected final Map<String, Cache> caches;
protected final Map<String, ResultMap> resultMaps;
protected final Map<String, ParameterMap> parameterMaps;
protected final Map<String, KeyGenerator> keyGenerators;
protected final Set<String> loadedResources;
protected final Map<String, XNode> sqlFragments;
protected final Collection<XMLStatementBuilder> incompleteStatements;
protected final Collection<CacheRefResolver> incompleteCacheRefs;
protected final Collection<ResultMapResolver> incompleteResultMaps;
protected final Collection<MethodResolver> incompleteMethods;
protected final Map<String, String> cacheRefMap;
// 省略部分源码
}
从而可以看出初始化本质,就是把xml配置文件里的数据封装到Configuration这个类的属性中。
接下来我们继续分析e.prase这个方法
//3、进入XMLConfigBuilder 类中的prase
public Configuration parse() {
if(this.parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
} else {
this.parsed = true;
//parser是XPathParser解析器对象,读取节点内数据,<configuration>是MyBatis配置文件
// 中的顶层标签
this.parseConfiguration(this.parser.evalNode("/configuration"));
return this.configuration;
}
}
//4、进入parseConfiguration方法,这个方法就是是把xml配置文件里的标签封装到Configuration属性中
private void parseConfiguration(XNode root) {
try {
this.propertiesElement(root.evalNode("properties"));
Properties e = this.settingsAsProperties(root.evalNode("settings"));
this.loadCustomVfs(e);
this.typeAliasesElement(root.evalNode("typeAliases"));
this.pluginElement(root.evalNode("plugins"));
this.objectFactoryElement(root.evalNode("objectFactory"));
this.objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
this.reflectorFactoryElement(root.evalNode("reflectorFactory"));
this.settingsElement(e);
this.environmentsElement(root.evalNode("environments"));
this.databaseIdProviderElement(root.evalNode("databaseIdProvider"));
this.typeHandlerElement(root.evalNode("typeHandlers"));
this.mapperElement(root.evalNode("mappers"));
} catch (Exception var3) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + var3, var3);
}
}
到这里xml的解析就完事了,接着我们由回到build的重载方法
//5、接着回到这个build重载的方法 var5 = this.build(e.parse());
//把Configuration的对象当做入参传入,返回一个SessionFactory
public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}
下面是第二部分执行sql了
SqlSession是一个接口,它有一个默认的实现类DefaultSqlSession,一个会话只要一个SqlSession,并且在使用完成后需要close()关闭。
Executor这个也是个接口,他有三个实现类,BatchExecutor(重用语句并执行批量更新),ReuseExecutor(重用预处理语句prepared statements),SimpleExecutor(普通的执行器,默认)。
//使用SqlSessionFactory创建SqlSession
SqlSession session=factory.openSession();
获得session
//6、进入DefaultSqlSessionFactory 类中的openSession
public SqlSession openSession() {
//getDefaultExecutorType() 返回的是一个SimpleExecutor
return this.openSessionFromDataSource(this.configuration.getDefaultExecutorType(), (TransactionIsolationLevel)null, false);
}
//7、进入openSessionFromDataSource这个方法
//ExecutorType 为Executor的类型,TransactionIsolationLevel为事务隔离级别,autoCommit是否开启事务
//openSession的多个重载方法可以指定获得的SeqSession的Executor类型和事务的处理
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
DefaultSqlSession var8;
try {
Environment e = this.configuration.getEnvironment();
TransactionFactory transactionFactory = this.getTransactionFactoryFromEnvironment(e);
tx = transactionFactory.newTransaction(e.getDataSource(), level, autoCommit);
//根据参数创建指定类型的Executor
Executor executor = this.configuration.newExecutor(tx, execType);
//返回的是DefaultSqlSession
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;
}
接下来回到之前的测试代码
//使用SqlSession创建dao接口的代理对象
UserMapper userDao =session.getMapper(UserMapper.class);
//使用代理对象执行查询方法
List<User> list=userDao.findUser();
我们创建的Mapper接口类,没有实现这个接口类,但是让然可以调用里面的方法,这其实是采用了设计模式中的jdk动态代理技术,如果大家对这个动态代理不是很清楚,可以看我写的这篇博客。https://blog.csdn.net/qq_30353203/article/details/103681191。
在这说源码之前,先介绍下Mybatis初始化时对接口的处理,大家可以翻看上面面Configuration的源码属性里 ,就有一个MapperRegistry属性,它的内部维护了一个HashMap用于存储mapper接口的工厂类,每个接口对应一个工厂。mappers可以配置接口的包路径、或者某个具体的接口类
当解析mappers标签时,它会判断解析到的是mapper配置文件时,会再将对应配置文件中的增删改查标签一 一封装成MappedStatement对象,存入mappedStatements中。
当解析到mappers标签的时候,会做个判断,如果是解析到接口,会创建此接口对应的MapperProxyFactory对象,存入HashMap中,key = 接口的字节码对象,value = 此接口对应的MapperProxyFactory对象。
//进入MapperRegistry这个类
public class MapperRegistry {
private final Configuration config;
//创建一个HashMap,用于存储key是字节码对象,value是对应接口类的工厂MapperProxyFactory
private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap();
//判断类型,如果是解析到的接口类型,就存入到HashMap中
public <T> void addMapper(Class<T> type) {
if(type.isInterface()) {
if(this.hasMapper(type)) {
throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
}
boolean loadCompleted = false;
try {
this.knownMappers.put(type, new MapperProxyFactory(type));
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(this.config, type);
parser.parse();
loadCompleted = true;
} finally {
if(!loadCompleted) {
this.knownMappers.remove(type);
}
}
}
}
省略部分源码
}
接下来给大家看下源码,Mybatis是如何通过jdk动态代理创建代理对象的。
//使用SqlSession创建dao接口的代理对象
UserMapper userDao =session.getMapper(UserMapper.class);
//进入DefaultSqlSession 类,是SqlSession 接口的实现类
public <T> T getMapper(Class<T> type) {
return this.configuration.getMapper(type, this);
}
//进入Configuration类中 的getMapper方法
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
return this.mapperRegistry.getMapper(type, sqlSession);
}
//进入MapperRegistry类中,的getMapper方法
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
//从刚才存到HashMap中 根据不同的key,取出value的值,也就是MapperProxyFactory
MapperProxyFactory mapperProxyFactory =
(MapperProxyFactory)this.knownMappers.get(type);
if(mapperProxyFactory == null) {
throw new BindingException("Type " + type + " is not known to the
MapperRegistry.");
} else {
try {
//通过动态代理工厂,实例化对象
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception var5) {
throw new BindingException("Error getting mapper instance. Cause: " + var5, var5);
}
}
}
//进入MapperProxyFactory 中的newInstance 方法
public T newInstance(SqlSession sqlSession) {
//创建了JDK动态代理的Handler类
MapperProxy mapperProxy = new MapperProxy(sqlSession, this.mapperInterface,
this.methodCache);
//调用构造函数 newInstance
return this.newInstance(mapperProxy);
}
//这个构造函数 ,重载的方法,由动态代理创建新示例返回。newProxyInstance有三个入参,最后一个入参需要实现InvocationHandler接口类。这个返回的对象就是动态代理的对象,会执行invoke方法
protected T newInstance(MapperProxy<T> mapperProxy) {
return Proxy.newProxyInstance(this.mapperInterface.getClassLoader(), new Class[]{this.mapperInterface}, mapperProxy);
}
//MapperProxy 实现了InvocationHandler接口类
public class MapperProxy<T> implements InvocationHandler, Serializable {
private static final long serialVersionUID = -6424540398559729838L;
private final SqlSession sqlSession;
private final Class<T> mapperInterface;
private final Map<Method, MapperMethod> methodCache;
//构造函数,传入的入参是SqlSession,说明每个session中的代理对象的不同的!
public MapperProxy(SqlSession sqlSession, Class<T> mapperInterface, Map<Method, MapperMethod> methodCache) {
this.sqlSession = sqlSession;
this.mapperInterface = mapperInterface;
this.methodCache = methodCache;
}
省略部分源码
}
动态代理后返回的实例,就可以调用mapper接口中的方法,说明在MapperProxy中的invoke方法中已经为我们实现了方法。
//进入MapperProxy 中的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(this.isDefaultMethod(method)) {
return this.invokeDefaultMethod(proxy, method, args);
}
} catch (Throwable var5) {
throw ExceptionUtil.unwrapThrowable(var5);
}
MapperMethod mapperMethod = this.cachedMapperMethod(method);
// 重点在这:MapperMethod最终调用了执行的方
return mapperMethod.execute(this.sqlSession, args);
}
//进入excute方法
public Object execute(SqlSession sqlSession, Object[] args) {
Object param;
Object result;
//根据命令转顺序执行不同的crud,调用的还是SqlSession中的方法.
switch(null.$SwitchMap$org$apache$ibatis$mapping$SqlCommandType[this.command.getType().ordinal()]) {
case 1:
param = this.method.convertArgsToSqlCommandParam(args);
result = this.rowCountResult(sqlSession.insert(this.command.getName(), param));
break;
case 2:
param = this.method.convertArgsToSqlCommandParam(args);
result = this.rowCountResult(sqlSession.update(this.command.getName(), param));
break;
case 3:
param = this.method.convertArgsToSqlCommandParam(args);
result = this.rowCountResult(sqlSession.delete(this.command.getName(), param));
break;
case 4:
if(this.method.returnsVoid() && this.method.hasResultHandler()) {
this.executeWithResultHandler(sqlSession, args);
result = null;
} else if(this.method.returnsMany()) {
result = this.executeForMany(sqlSession, args);
} else if(this.method.returnsMap()) {
result = this.executeForMap(sqlSession, args);
} else if(this.method.returnsCursor()) {
result = this.executeForCursor(sqlSession, args);
} else {
param = this.method.convertArgsToSqlCommandParam(args);
result = sqlSession.selectOne(this.command.getName(), param);
}
break;
case 5:
result = sqlSession.flushStatements();
break;
default:
throw new BindingException("Unknown execution method for: " + this.command.getName());
}
if(result == null && this.method.getReturnType().isPrimitive() && !this.method.returnsVoid()) {
throw new BindingException("Mapper method \'" + this.command.getName() + " attempted to return null from a method with a primitive return type (" + this.method.getReturnType() + ").");
} else {
return result;
}
}
至此,Mybatis的原理,源码分析就结束了。