前言
最近在准备换工作,所以开始复习java和常用框架的知识,mybatis框架是工作中常用的一个ORM框架,在复习的过程中,我就简单阅读了一下mybatis的源码,通过debug源码,我更加理解了mybatis的配置原理和映射原理,同时也增强了自己的源码阅读和分析能力
开始
首先我们新建一个maven工程,引入mybatis和mysql的jdbc驱动
<dependencies>
<!-- https://mvnrepository.com/artifact/org.mybatis/mybatis -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.4.6</version>
</dependency>
<!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.10</version>
<scope>compile</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/junit/junit -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
</dependencies>
然后我在本地建了一个库名叫mybatistest,建了一张表叫user,表中分别有四个字段为id,name,height和weight,按照官方文档上的说教程,新建了一个mybatis-config.xml配置文件,pojo文件User,一个接口文件UserMapper,和一个UserMapper.xml。UserMapper.xml里就写了一句select语句,目的就是测试mybatis有没有映射成功,能不能查询出数据
mybatis-config.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>
<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/testmybatis"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper class="com.ww.mybatis.mapper.UserMapper"/>
</mappers>
</configuration>
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.ww.mybatis.mapper.UserMapper">
<select id="selectUser" parameterType="integer" resultType="com.ww.mybatis.pojo.User">
select * from user where id = #{id}
</select>
</mapper>
Pojo:
@Data
public class User implements Serializable {
private int id;
private String name;
private String height;
private String weight;
}
Interface:
@Mapper
public interface UserMapper {
@Select("select id,name,height,weight from user where id=#{id}")
public User selectUser(Integer id);
}
然后我创建了一个用来测试的主类,按照教程,写下如下代码,随后我会debug进入mybatis源码来分析这几行代码背后的执行流程
public class MybatisTest {
public static void main(String[] args) throws IOException {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
User user = sqlSession.selectOne("com.ww.mybatis.mapper.UserMapper.selectUser", 1);
//UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
//User user = userMapper.selectUser(1);
System.out.println(user);
}
}
首先我把断点打在了SqlsessionFactory上,我们知道mybatis连接数据库需要一个sqlsession,那么sqlsessionFactory是怎么去创建出来然后获得sqlSession的呢,我们看,首先它来到了org.apache.ibatis.session.SqlSessionFactory的build方法
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
try {
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
return build(parser.parse());//这里有个parse方法,目的是解析inputstream拿到的配置文件
} 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.
}
}
}
然后我们进入这个parse方法看看
public Configuration parse() {
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
parseConfiguration(parser.evalNode("/configuration"));//这行代码开始加载配置
return configuration;
}
//开始解析配置文件,回想一下配置文件的开头的标签,是不是以configuration开头的?
private void parseConfiguration(XNode root) {
try {
//issue #117 read properties first
propertiesElement(root.evalNode("properties"));
Properties settings = settingsAsProperties(root.evalNode("settings"));
loadCustomVfs(settings);
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"));
mapperElement(root.evalNode("mappers"));//这一部分代码我们主要看这一行,看看配置文件中的mapper是怎么被加载的
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
我们断点进入mapperElement方法,可以看到里面有一个configuration的引用,然后由这个configuration来添加mapper,这个configuration是什么呢,configuration就像是Mybatis的总管,Mybatis的所有配置信息都存放在这里。同时通过这段代码我们也发现了mapper有四种配置方式,分别是package,resource,class和url,正好,我们在mybatis-config.dtd文件里看到mapper那一块显示也是可以用四种方式进行配置,以后面试官问你mybatis有几种方式可以配置mapper,就告诉他有四种,是通过看源码得出的结论,保证不会错
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);
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);
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);//我用的是class方式配置,所以主要看这段代码
} else {
throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
}
}
}
}
}
//dtd文件中的四种配置方式
<!ELEMENT mappers (mapper*,package*)>
<!ELEMENT mapper EMPTY>
<!ATTLIST mapper
resource CDATA #IMPLIED
url CDATA #IMPLIED
class CDATA #IMPLIED
>
接下来再看,我用的是class方式配置的mapper,所以就要进到最后一个else if语句那里去看
protected final MapperRegistry mapperRegistry = new MapperRegistry(this);//mapper注册器
public <T> void addMapper(Class<T> type) {
mapperRegistry.addMapper(type);//这是一个mapper注册器,所有的配置在<mappers>标签中的mapper都会被添加到这个注册器中
}
public class MapperRegistry {
private final Configuration config;
//实际上最终mapper都会加到这个knownMappers的hashmap中去
private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<Class<?>, MapperProxyFactory<?>>();
public MapperRegistry(Configuration config) {
this.config = config;
}
public <T> void addMapper(Class<T> type) {
if (type.isInterface()) {
if (hasMapper(type)) {
throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
}
boolean loadCompleted = false;
try {
knownMappers.put(type, new MapperProxyFactory<T>(type));//添加mapper映射
// It's important that the type is added before the parser is run
// otherwise the binding may automatically be attempted by the
// mapper parser. If the type is already known, it won't try.
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
parser.parse();//开始解析映射
loadCompleted = true;
} finally {
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}
我们看到addMapper方法中出现了一个MapperAnnotationBuilder类,这个类也是mybatis的核心组件之一,它的职责很简单,就是解析指定的mapper接口对应的Class对象中,包含的所有mybatis框架中定义的注解,并生成Cache、ResultMap、MappedStatement三种类型对象。我们进入parse方法
public void parse() {
String resource = type.toString();
if (!configuration.isResourceLoaded(resource)) {
//首先加载xml资源,也就是xxxMapper.xml
loadXmlResource();
//添加已经解析过的映射
configuration.addLoadedResource(resource);
//命名空间
assistant.setCurrentNamespace(type.getName());
//缓存处理
parseCache();
parseCacheRef();
//方法
Method[] methods = type.getMethods();
for (Method method : methods) {
try {
// issue #237
if (!method.isBridge()) {
//开始解析方法
parseStatement(method);
}
} catch (IncompleteElementException e) {
configuration.addIncompleteMethod(new MethodResolver(this, method));
}
}
}
parsePendingMethods();
}
我们先看加载xml资源的方法
private void loadXmlResource() {
// Spring may not know the real resource name so we check a flag
// to prevent loading again a resource twice
// this flag is set at XMLMapperBuilder#bindMapperForNamespace
if (!configuration.isResourceLoaded("namespace:" + type.getName())) {
String xmlResource = type.getName().replace('.', '/') + ".xml";
InputStream inputStream = null;
try {
inputStream = Resources.getResourceAsStream(type.getClassLoader(), xmlResource);
} catch (IOException e) {
// ignore, resource is not required
//这句英文注释通俗一点的解释就是没有xml文件不要紧,直接忽略,因为接下去会去解析注解形式的sql
}
if (inputStream != null) {
XMLMapperBuilder xmlParser = new XMLMapperBuilder(inputStream, assistant.getConfiguration(), xmlResource, configuration.getSqlFragments(), type.getName());
xmlParser.parse();//xml文件解析,这里就不展开来看了
}
}
}
主要是要看parseStatement方法,这是解析语句的方法
void parseStatement(Method method) {
Class<?> parameterTypeClass = getParameterType(method);
LanguageDriver languageDriver = getLanguageDriver(method);
SqlSource sqlSource = getSqlSourceFromAnnotations(method, parameterTypeClass, languageDriver);//从注解获得sql,我会进入这个方法来看sql语句是怎么获得的
if (sqlSource != null) {
Options options = method.getAnnotation(Options.class);
final String mappedStatementId = type.getName() + "." + method.getName();
Integer fetchSize = null;
Integer timeout = null;
StatementType statementType = StatementType.PREPARED;//熟悉吗,preparedStatement
ResultSetType resultSetType = ResultSetType.FORWARD_ONLY;
SqlCommandType sqlCommandType = getSqlCommandType(method);
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
boolean flushCache = !isSelect;
boolean useCache = isSelect;
//主键生成器
KeyGenerator keyGenerator;
String keyProperty = "id";
String keyColumn = null;
if (SqlCommandType.INSERT.equals(sqlCommandType) || SqlCommandType.UPDATE.equals(sqlCommandType)) {
// first check for SelectKey annotation - that overrides everything else
SelectKey selectKey = method.getAnnotation(SelectKey.class);
if (selectKey != null) {
keyGenerator = handleSelectKeyAnnotation(selectKey, mappedStatementId, getParameterType(method), languageDriver);
keyProperty = selectKey.keyProperty();
} else if (options == null) {
keyGenerator = configuration.isUseGeneratedKeys() ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
} else {
keyGenerator = options.useGeneratedKeys() ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
keyProperty = options.keyProperty();
keyColumn = options.keyColumn();
}
} else {
keyGenerator = NoKeyGenerator.INSTANCE;
}
if (options != null) {
if (FlushCachePolicy.TRUE.equals(options.flushCache())) {
flushCache = true;
} else if (FlushCachePolicy.FALSE.equals(options.flushCache())) {
flushCache = false;
}
useCache = options.useCache();
fetchSize = options.fetchSize() > -1 || options.fetchSize() == Integer.MIN_VALUE ? options.fetchSize() : null; //issue #348
timeout = options.timeout() > -1 ? options.timeout() : null;
statementType = options.statementType();
resultSetType = options.resultSetType();
}
//返回的结果集
String resultMapId = null;
ResultMap resultMapAnnotation = method.getAnnotation(ResultMap.class);
if (resultMapAnnotation != null) {
String[] resultMaps = resultMapAnnotation.value();
StringBuilder sb = new StringBuilder();
for (String resultMap : resultMaps) {
if (sb.length() > 0) {
sb.append(",");
}
sb.append(resultMap);
}
resultMapId = sb.toString();
} else if (isSelect) {
resultMapId = parseResultMap(method);
}
//重点在这个构建mapperedStatement,好吧,我看到这么多参数也震惊了
assistant.addMappedStatement(
mappedStatementId,
sqlSource,
statementType,
sqlCommandType,
fetchSize,
timeout,
// ParameterMapID
null,
parameterTypeClass,
resultMapId,
getReturnType(method),
resultSetType,
flushCache,
useCache,
// TODO gcode issue #577
false,
keyGenerator,
keyProperty,
keyColumn,
// DatabaseID
null,
languageDriver,
// ResultSets
options != null ? nullOrEmpty(options.resultSets()) : null);
}
}
先进入getSqlSourceFromAnnotations方法看看sql语句是怎么拿到的
//获得普通sql语句的注解类型
private Class<? extends Annotation> getSqlAnnotationType(Method method) {
return chooseAnnotationType(method, sqlAnnotationTypes);//后面这个参数sqlAnnotationTypes,是一个不可重复的set,我带大家来看一下
}
//就是这么个set,在构造MapperAnnotationBuilder的时候,默认有一些注解类型会被添加到这个set中
private final Set<Class<? extends Annotation>> sqlAnnotationTypes = new HashSet<Class<? extends Annotation>>();
//构造函数
public MapperAnnotationBuilder(Configuration configuration, Class<?> type) {
String resource = type.getName().replace('.', '/') + ".java (best guess)";
this.assistant = new MapperBuilderAssistant(configuration, resource);
this.configuration = configuration;
this.type = type;
sqlAnnotationTypes.add(Select.class);
sqlAnnotationTypes.add(Insert.class);
sqlAnnotationTypes.add(Update.class);
sqlAnnotationTypes.add(Delete.class);
sqlProviderAnnotationTypes.add(SelectProvider.class);
sqlProviderAnnotationTypes.add(InsertProvider.class);
sqlProviderAnnotationTypes.add(UpdateProvider.class);
sqlProviderAnnotationTypes.add(DeleteProvider.class);
}
//这时候再结合直接我创建的UserMapper接口文件,看看是不是就好理解了呢
public interface UserMapper {
//sql注解就是从这里拿到的,sql语句也是从这里获得的
@Select("select id,name,height,weight from user where id=#{id}")
public User selectUser(Integer id);
}
//sqlProvide我还不是很清楚是什么概念,以后有空研究
private Class<? extends Annotation> getSqlProviderAnnotationType(Method method) {
return chooseAnnotationType(method, sqlProviderAnnotationTypes);
}
private SqlSource getSqlSourceFromAnnotations(Method method, Class<?> parameterType, LanguageDriver languageDriver) {
try {
Class<? extends Annotation> sqlAnnotationType = getSqlAnnotationType(method);
Class<? extends Annotation> sqlProviderAnnotationType = getSqlProviderAnnotationType(method);
if (sqlAnnotationType != null) {
if (sqlProviderAnnotationType != null) {
throw new BindingException("You cannot supply both a static SQL and SqlProvider to method named " + method.getName());
}
Annotation sqlAnnotation = method.getAnnotation(sqlAnnotationType);
final String[] strings = (String[]) sqlAnnotation.getClass().getMethod("value").invoke(sqlAnnotation);
return buildSqlSourceFromStrings(strings, parameterType, languageDriver);
} else if (sqlProviderAnnotationType != null) {
Annotation sqlProviderAnnotation = method.getAnnotation(sqlProviderAnnotationType);
return new ProviderSqlSource(assistant.getConfiguration(), sqlProviderAnnotation, type, method);
}
return null;
} catch (Exception e) {
throw new BuilderException("Could not find value method on SQL annotation. Cause: " + e, e);
}
}
可以看到执行完getSqlSourceFromAnnotations方法,sql语句其实已经拿到了,就连参数类型和参数名称也已经拿到了
进入addMappedStatement方法
public MappedStatement addMappedStatement(
String id,
SqlSource sqlSource,
StatementType statementType,
SqlCommandType sqlCommandType,
Integer fetchSize,
Integer timeout,
String parameterMap,
Class<?> parameterType,
String resultMap,
Class<?> resultType,
ResultSetType resultSetType,
boolean flushCache,
boolean useCache,
boolean resultOrdered,
KeyGenerator keyGenerator,
String keyProperty,
String keyColumn,
String databaseId,
LanguageDriver lang,
String resultSets) {
if (unresolvedCacheRef) {
throw new IncompleteElementException("Cache-ref not yet resolved");
}
id = applyCurrentNamespace(id, false);
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType)
.resource(resource)
.fetchSize(fetchSize)
.timeout(timeout)
.statementType(statementType)
.keyGenerator(keyGenerator)
.keyProperty(keyProperty)
.keyColumn(keyColumn)
.databaseId(databaseId)
.lang(lang)
.resultOrdered(resultOrdered)
.resultSets(resultSets)
.resultMaps(getStatementResultMaps(resultMap, resultType, id))
.resultSetType(resultSetType)
.flushCacheRequired(valueOrDefault(flushCache, !isSelect))
.useCache(valueOrDefault(useCache, isSelect))
.cache(currentCache);
ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);
if (statementParameterMap != null) {
statementBuilder.parameterMap(statementParameterMap);
}
MappedStatement statement = statementBuilder.build();
configuration.addMappedStatement(statement);
return statement;
}
到这里执行完,XMLConfigBuilder的parse方法整体执行结束,返回一个DefaultSqlSessionFactory对象
return build(parser.parse());
public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}
费了好些笔墨,sqlSessionFactory的创建过程分析完了,下一篇我们分析sqlSession的创建过程和查询的执行流程