本文主要对Mybatis中启动流程、Mapper解析、Mapper代理、四大对象、SQL执行、缓存、插件、与Sprin整合等相关内容进行解析,文章较长,能力有限,有问题麻烦指出,谢谢。关于调试方面,可以直接从Github上下载Mybatis源码,里面包含很多测试代码,下载下来安装依赖就可以直接运行,当然也可以直接通过IDEA关联源码,也比较方便。原创文章,转载请标明出处!
网上找到的一张Mybatis架构图:
public class Application {
public static void main(String[] args) throws Exception{
// 1、加载配置文件
InputStream is = Resources.getResourceAsStream("application.xml");
// 2、构建SqlSessionFactory
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);
// 3、获取SqlSession
SqlSession sqlSession = sqlSessionFactory.openSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
User user = mapper.selectByUserName("admin");
System.err.println(user);
sqlSession.commit();
}
}
从xml配置文件中读取配置,然后通过SqlSessionFactoryBuilder构建SqlSessionFactory实例(建造者模式)。SqlSessionFactory是Mybatis的关键对象,它是个单个数据库映射关系经过编译后的内存镜像。SqlSessionFactory是创建SqlSession的工厂,每一个Mybatis的应用程序都以一个SqlSessionFactory对象的实例为核心,同时SqlSessionFactory也是线程安全的,SqlSessionFactory一旦被创建,在应用执行期间都存在。
有关于SqlSessionFactoryBuilder的build方法
public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
try {
XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
return build(parser.parse());
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error building SqlSession.", e);
} finally {
ErrorContext.instance().reset();
try {
reader.close();
} catch (IOException e) {
// Intentionally ignore. Prefer previous error.
}
}
}
public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}
public XMLConfigBuilder(Reader reader, String environment, Properties props) {
// 在创建XMLConfigBuilder对象之前,创建了一个XPathParser对象,XPathParser用来解析xml文件。即:在构建XMLConfigBuilder之后,即完成了xml文件到dcument对象的转化
this(new XPathParser(reader, true, props, new XMLMapperEntityResolver()), environment, props);
}
private XMLConfigBuilder(XPathParser parser, String environment, Properties props) {
// 创建Configuration对象
super(new Configuration());
ErrorContext.instance().resource("SQL Mapper Configuration");
this.configuration.setVariables(props);
this.parsed = false;
this.environment = environment;
this.parser = parser;
}
public XPathParser(Reader reader, boolean validation, Properties variables, EntityResolver entityResolver) {
commonConstructor(validation, variables, entityResolver);
// 创建document对象
this.document = createDocument(new InputSource(reader));
}
在上面的代码中,我们重点关注两个东西:一个是XPathParser对象,在创建XPathParser对象的时候会将xml文件转换成document对象;两外一个是Configuration对象,在XMLConfigBuilder对象的构造方法内创建了Configuration对象。
Configuration对象是Myatis中非常非常重要的一个概念,它的作用就相当于是servlet中的ServletContext、spring中的容器,它就是Mybatis的中的Boss,Mybatis的运行就是依赖于这个对象。我们在xml文件中的一些配置,解析之后的mappedStatement等等等等,都维护在这个对像中,非常非常重要的一个对象。所以,有必要对这个对象有个简单的了解:
可以看到,Configuration有非常多的属性,每个属性都很重要,有关于每个属性的具体用途,这里没办法具体介绍,在接下来的源码解析中会慢慢接触到。可以看出,它的构造方法主要是设置了一些别名:
protected Environment environment;
protected boolean safeRowBoundsEnabled;
protected boolean safeResultHandlerEnabled = true;
protected boolean mapUnderscoreToCamelCase;
protected boolean aggressiveLazyLoading;
protected boolean multipleResultSetsEnabled = true;
protected boolean useGeneratedKeys;
protected boolean useColumnLabel = true;
protected boolean cacheEnabled = true;
protected boolean callSettersOnNulls;
protected boolean useActualParamName = true;
protected boolean returnInstanceForEmptyRow;
protected String logPrefix;
protected Class <? extends Log> logImpl;
protected Class <? extends VFS> vfsImpl;
protected LocalCacheScope localCacheScope = LocalCacheScope.SESSION;
protected JdbcType jdbcTypeForNull = JdbcType.OTHER;
protected Set<String> lazyLoadTriggerMethods = new HashSet<String>(Arrays.asList(new String[] { "equals", "clone", "hashCode", "toString" }));
protected Integer defaultStatementTimeout;
protected Integer defaultFetchSize;
protected ExecutorType defaultExecutorType = ExecutorType.SIMPLE;
protected AutoMappingBehavior autoMappingBehavior = AutoMappingBehavior.PARTIAL;
protected AutoMappingUnknownColumnBehavior autoMappingUnknownColumnBehavior = AutoMappingUnknownColumnBehavior.NONE;
protected Properties variables = new Properties();
protected ReflectorFactory reflectorFactory = new DefaultReflectorFactory();
protected ObjectFactory objectFactory = new DefaultObjectFactory();
protected ObjectWrapperFactory objectWrapperFactory = new DefaultObjectWrapperFactory();
protected boolean lazyLoadingEnabled = false;
protected ProxyFactory proxyFactory = new JavassistProxyFactory(); // #224 Using internal Javassist instead of OGNL
protected String databaseId;
protected Class<?> configurationFactory;
// 用于注册mapper,这个对象中缓存了所有mapper接口的代理对象
protected final MapperRegistry mapperRegistry = new MapperRegistry(this);
// 拦截器链
protected final InterceptorChain interceptorChain = new InterceptorChain();
protected final TypeHandlerRegistry typeHandlerRegistry = new TypeHandlerRegistry();
protected final TypeAliasRegistry typeAliasRegistry = new TypeAliasRegistry();
protected final LanguageDriverRegistry languageRegistry = new LanguageDriverRegistry();
// mapper.xml 中的每个方法对应一个 MappedStatement,重点关注
protected final Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>("Mapped Statements collection");
protected final Map<String, Cache> caches = new StrictMap<Cache>("Caches collection");
protected final Map<String, ResultMap> resultMaps = new StrictMap<ResultMap>("Result Maps collection");
protected final Map<String, ParameterMap> parameterMaps = new StrictMap<ParameterMap>("Parameter Maps collection");
protected final Map<String, KeyGenerator> keyGenerators = new StrictMap<KeyGenerator>("Key Generators collection");
protected final Set<String> loadedResources = new HashSet<String>();
protected final Map<String, XNode> sqlFragments = new StrictMap<XNode>("XML fragments parsed from previous mappers");
protected final Collection<XMLStatementBuilder> incompleteStatements = new LinkedList<XMLStatementBuilder>();
protected final Collection<CacheRefResolver> incompleteCacheRefs = new LinkedList<CacheRefResolver>();
protected final Collection<ResultMapResolver> incompleteResultMaps = new LinkedList<ResultMapResolver>();
protected final Collection<MethodResolver> incompleteMethods = new LinkedList<MethodResolver>();
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);
}
XMLConfigBuilder # mapperElement
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
// 如果配置文件中的mapper是以package的形式指定的,这里主要分析这种方式
if ("package".equals(child.getName())) {
// 获取到包名
String mapperPackage = child.getStringAttribute("name");
configuration.addMappers(mapperPackage);
} else {
// 非package方式,说明指明了具体的mapper接口或者xml文件
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);
} else {
throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
}
}
}
}
}
这里其实涉及到Mapper的配置方式,可以通过package的形式配置一个包下的所有接口,或者不以package的形式指明具体的mapper接口。我们在工作中主要是通过package的方式指明接口的位置,所以重点关注configuration.addMappers(mapperPackage)方法:
Configuration # addMappers
protected final MapperRegistry mapperRegistry = new MapperRegistry(this);
public void addMappers(String packageName) {
mapperRegistry.addMappers(packageName);
}
可以发现,这里引出了一个新对象:MapperRegistry,这是一个挺重要的对象,以下是其源码,非核心代码我已删除
public class MapperRegistry {
private final Configuration config;
// 缓存,每个Mapper接口对应一个 MapperProxyFactory
private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<Class<?>, MapperProxyFactory<?>>();
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);
}
}
public <T> boolean hasMapper(Class<T> type) {
return knownMappers.containsKey(type);
}
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));
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
parser.parse();
loadCompleted = true;
} finally {
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}
public Collection<Class<?>> getMappers() {
return Collections.unmodifiableCollection(knownMappers.keySet());
}
public void addMappers(String packageName, Class<?> superType) {
ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<Class<?>>();
resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
Set<Class<? extends Class<?>>> mapperSet = resolverUtil.getClasses();
for (Class<?> mapperClass : mapperSet) {
addMapper(mapperClass);
}
}
public void addMappers(String packageName) {
addMappers(packageName, Object.class);
}
}
在使用Mybatis这个框架的时候,我们往往只是定义了一些Mapper接口和一些XML文件,并没有写实现类,却可以直接调用方法,很明显是Mybatis为我们生成了代理。在上面的代码中,我们注意到上面的代码中MapperProxyFactory这么一个类,根据名字也可以猜测的到这是一个工厂类,用来创建Mapper接口的代理类。事实就是这样,可以看看MapperProxyFactory的源码
很明显newInstance方法中使用了JDK动态代理创建代理类,除此之外,这里还引如了一个MapperProxy对象,我们大概可以猜测到MapperProxy实现了InvocationHandler接口并重写了invoke方法,有关于这一块内容我们等下再来验证,目前可以确定的MapperProxyFactory是创建代理类的工厂,MapperProxy是创建代理类的关键,它是实现了InvocationHandler接口
public class MapperProxyFactory<T> {
private final Class<T> mapperInterface;
private final Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<Method, MapperMethod>();
public MapperProxyFactory(Class<T> mapperInterface) {
this.mapperInterface = mapperInterface;
}
public Class<T> getMapperInterface() {
return mapperInterface;
}
public Map<Method, MapperMethod> getMethodCache() {
return methodCache;
}
// JDK动态代理,MapperProxy类实现了InvocationHandler接口
@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<T>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
}
所以,综合我们对这两个类的观察,MapperRegistry类用于Mapper接口注册,当我们调用它的addMapperf方法的时候,会为该Mapper接口创建一个MapperProxyFactory对象,并缓存在一个Map中。而MapperProxyFactory主要用于创建代理对象,在创建代理对象的时候,用到了MapperProxy这个类,它是是实现了InvocationHandler接口,所以可以推断出,在调用Mapper方法时候,实际上是调用MapperProxy的invoke方法。
在了解了MapperRegistry类的大体流程之后,我们继续回到MapperRegistry的addMapper方法,该方法大概做了两件事情,一个是创建MapperProxyFactory对象并缓存起来;另一个就是执行Mapper解析。这里调用了MapperAnnotationBuilder的parse方法,根据名字应该可以猜测的到这是和注解的解析相关
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 {
// 添加到缓存,每个Mapper接口对应一个 MapperProxyFactory
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);
}
}
}
}
事实上,MapperAnnotationBuilder的作用就是解析Mapper接口中定义的注解,并生成Cache、ResultMap、MappedStatement三种类型对象。在创建一个的时候,会关联一个MapperBuilderAssistant对象,这是一个构建助理,实际上,真正将接口中的方法转换成MappedStatement对象就是通过MapperBuilderAssistant对象完成的
public MapperAnnotationBuilder(Configuration configuration, Class<?> type) {
String resource = type.getName().replace('.', '/') + ".java (best guess)";
// MapperBuilderAssistant类执行真正的解析操作
this.assistant = new MapperBuilderAssistant(configuration, resource);
this.configuration = configuration;
this.type = type;
// 下面就是Mybatis中的常见注解,下面那4个可用于实现通用Mapper
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);
}
public void parse() {
// 例:resource = "interface com.hand.sxy.mapper.UserMapper"
String resource = type.toString();
// loadedResources是configuration对象中的一个属性,是一个Set集合,用于缓存已解析过的Mapper
if (!configuration.isResourceLoaded(resource)) {
// 先对 Mapper.xml 文件进行解析
loadXmlResource();
// 添加到loadedResources缓存,Key为class对应的全类名
configuration.addLoadedResource(resource);
// 设置MapperBuilderAssistant的命名空间
assistant.setCurrentNamespace(type.getName());
// 解析缓存对象
parseCache();
// 解析缓存引用
parseCacheRef();
Method[] methods = type.getMethods();
for (Method method : methods) {
try {
// 这里涉及到桥接方法,关系到Java的泛型擦除,具体参考 https://www.zhihu.com/question/54895701/answer/141623158
if (!method.isBridge()) {
// 解析MappedStatement和ResultMap
parseStatement(method);
}
} catch (IncompleteElementException e) {
configuration.addIncompleteMethod(new MethodResolver(this, method));
}
}
}
// 遍历configuration的IncompleteMethods集合,重新解析MethodResolver
parsePendingMethods();
}
这里面涉及到的内容还挺多,接下里一个个解释一下吧
这个方法主要就是解析Mapper.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
// 对于已解析过的Mapper.xml文件,会缓存起来。这里为什么要通过 namespace: + 全类名的 方式来缓存呢?因为namespace指定的是一个具体的Mapper接口,而在每个Mapper.xml文件解析之后,会将该对应的
// 内容缓存起来,如:configuration.addLoadedResource("namespace:" + namespace)、configuration.addMapper(boundType)。可以认为"namespace:" + namespace标识着一个Mapper.xml文件,即每解析完一个Mapper.xml文件,都会缓存起该Mapper.xml文件和namespace对应的接口
if (!configuration.isResourceLoaded("namespace:" + type.getName())) {
// 通过对全类名中的 . 替换成 / 得到下 Maper.xml 文件路径,所以这也就要求 使用package的方式时, Mapper.xml 文件 和 Mapper接口名必须相同且在相同目录
// xmlResource = "com/hand/sxy/mapper/UserMapper.xml"
String xmlResource = type.getName().replace('.', '/') + ".xml";
InputStream inputStream = null;
try {
// 获取到xml文件输入流
inputStream = Resources.getResourceAsStream(type.getClassLoader(), xmlResource);
} catch (IOException e) {
// ignore, resource is not required
}
if (inputStream != null) {
XMLMapperBuilder xmlParser = new XMLMapperBuilder(inputStream, assistant.getConfiguration(), xmlResource, configuration.getSqlFragments(), type.getName());
// 通过XMLMapperBuilder进行解析,如果是不是通过package而是直接配置Mapper.xml文件的方式,就会直接执行下面XMLMapperBuilder的parse方法
xmlParser.parse();
}
}
}
public void parse() {
// 例:resource = "com/hand/sxy/mapper/UserMapper.xml",为什么这里又要判断一次,因为当不是通过package或者不是配置Mapper接口的方式,而是配置Mapper.xml文件的方式,会直接执行这个方法
if (!configuration.isResourceLoaded(resource)) {
// 真正的解析逻辑
configurationElement(parser.evalNode("/mapper"));
// 添加到缓存
configuration.addLoadedResource(resource);
// 将已经解析完的Mapper.xml文件标识和对应对应的Mapper接口标识缓存起来,Mapper.xml文件用"namespace:" + namespace表示;Mapper接口用全类名表示。
bindMapperForNamespace();
}
// 残缺不全的重新解析一遍?不太懂这里是为了处理什么情况
parsePendingResultMaps();
parsePendingCacheRefs();
parsePendingStatements();
}
private void configurationElement(XNode context) {
try {
// 获取命名空间, 即全类名
String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.equals("")) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
// 设置当前namespace
builderAssistant.setCurrentNamespace(namespace);
/** 以下用于处理各个标签的解析,详情请看:http://www.mybatis.org/mybatis-3/zh/sqlmap-xml.html#*/
// 用于启用本xml对应的namespace的二级缓存 <cache-ref namespace="com.someone.application.data.SomeMapper"/>
cacheRefElement(context.evalNode("cache-ref"));
// 共享指定namespace的二级缓存 <cache eviction="FIFO" flushInterval="60000" size="512" readOnly="true"/>
cacheElement(context.evalNode("cache"));
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
// 进行数据库列和返回Java对象的属性之间的映射
resultMapElements(context.evalNodes("/mapper/resultMap"));
// 可被其他语句引用的可重用sql语句块
sqlElement(context.evalNodes("/mapper/sql"));
// 增删改查映射语句
buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
} catch (Exception e) {
throw new BuilderException("Error parsing Mapper XML. Cause: " + e, e);
}
}
分别对应Mapper.xml文件中的不同标签,很多都是我们常见的,比如:resultMap、sql、select 等等。每个标签的解析对应不同的方法,虽然这些都是XMLStatementBuilder类中的方法,但实际上将各个标签解析为Mybatis中对应的类时,都是通过MapperBuilderAssistant这个助理解析类来完成的。这里就简单分析一下resultMapElements 和 buildStatementFromContext 这个两个方法
resultMapElements
private void resultMapElements(List<XNode> list) throws Exception {
// 每个XNode就代表一个 <resultMap>, <resultMap>可以有多个
for (XNode resultMapNode : list) {
try {
resultMapElement(resultMapNode);
} catch (IncompleteElementException e) {
// ignore, it will be retried
}
}
}
private ResultMap resultMapElement(XNode resultMapNode) throws Exception {
// 每次解析一个 <resultMap> 的时候,都传一个空集合,在解析的过程中再往里面填充
return resultMapElement(resultMapNode, Collections.<ResultMapping> emptyList());
}
private ResultMap resultMapElement(XNode resultMapNode, List<ResultMapping> additionalResultMappings) throws Exception {
ErrorContext.instance().activity("processing " + resultMapNode.getValueBasedIdentifier());
// 获取id
String id = resultMapNode.getStringAttribute("id",
resultMapNode.getValueBasedIdentifier());
// 获取 dto 全类名
String type = resultMapNode.getStringAttribute("type",
resultMapNode.getStringAttribute("ofType",
resultMapNode.getStringAttribute("resultType",
resultMapNode.getStringAttribute("javaType"))));
// 获取extend属性值
String extend = resultMapNode.getStringAttribute("extends");
// 获取autoMapping属性值
Boolean autoMapping = resultMapNode.getBooleanAttribute("autoMapping");
Class<?> typeClass = resolveClass(type);
Discriminator discriminator = null;
List<ResultMapping> resultMappings = new ArrayList<ResultMapping>();
resultMappings.addAll(additionalResultMappings);
// 获取到该<resultMap>标签下每个 数据库字段 和 dto字段对应关系
List<XNode> resultChildren = resultMapNode.getChildren();
for (XNode resultChild : resultChildren) {
if ("constructor".equals(resultChild.getName())) {
processConstructorElement(resultChild, typeClass, resultMappings);
} else if ("discriminator".equals(resultChild.getName())) {
discriminator = processDiscriminatorElement(resultChild, typeClass, resultMappings);
} else {
List<ResultFlag> flags = new ArrayList<ResultFlag>();
// 代表主键
if ("id".equals(resultChild.getName())) {
flags.add(ResultFlag.ID);
}
// buildResultMappingFromContext用于将<resultMap>标签下的每行对应关系维护成一个ResultMapping对象,其实该方法内最终也是调用了MapperBuilderAssistant类的buildResultMapping方法来完成这个操作
resultMappings.add(buildResultMappingFromContext(resultChild, typeClass, flags));
}
}
ResultMapResolver resultMapResolver = new ResultMapResolver(builderAssistant, id, typeClass, extend, discriminator, resultMappings, autoMapping);
try {
// 内部通过MapperBuilderAssistant类的addResultMap方法,构建一个ResultMap对象,并缓存到configuration的resultMaps属性中,以 Mapper全类名 + id 为key,如: com.hand.sxy.mapper.UserMapper.BaseResultMap
return resultMapResolver.resolve();
} catch (IncompleteElementException e) {
configuration.addIncompleteResultMap(resultMapResolver);
throw e;
}
}
可以看看buildResultMappingFromContext方法的返回结果,也就是ResultMapping对象
至此,有关于<resultMap>这个标签的解析到此结束,接下来看看buildStatementFromContext方法,这个方法主要时解析 select|insert|update|delete 这个四种情况,最后转换成一个MappedStatement对象,然后缓存在configuration的mappedStatements属性中,以 Mapper全类名 + id 为key,如: com.hand.sxy.mapper.UserMapper.selectByUserName
buildStatementFromContext
private void buildStatementFromContext(List<XNode> list) {
// 这里是为了处理 在Mybatis配置文件中配置了databaseId的情况
if (configuration.getDatabaseId() != null) {
buildStatementFromContext(list, configuration.getDatabaseId());
}
buildStatementFromContext(list, null);
}
private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
// 每个 select|insert|update|delet 就是一个XNode
for (XNode context : list) {
final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
try {
statementParser.parseStatementNode();
} catch (IncompleteElementException e) {
configuration.addIncompleteStatement(statementParser);
}
}
}
public void parseStatementNode() {
/** 各个属性 */
String id = context.getStringAttribute("id");
String databaseId = context.getStringAttribute("databaseId");
if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
return;
}
Integer fetchSize = context.getIntAttribute("fetchSize");
Integer timeout = context.getIntAttribute("timeout");
String parameterMap = context.getStringAttribute("parameterMap");
String parameterType = context.getStringAttribute("parameterType");
Class<?> parameterTypeClass = resolveClass(parameterType);
// 返回类型
String resultMap = context.getStringAttribute("resultMap");
String resultType = context.getStringAttribute("resultType");
String lang = context.getStringAttribute("lang");
LanguageDriver langDriver = getLanguageDriver(lang);
Class<?> resultTypeClass = resolveClass(resultType);
String resultSetType = context.getStringAttribute("resultSetType");
StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
// 获取到该标签的名称,即时 select|insert|update|delet 中的哪一种
String nodeName = context.getNode().getNodeName();
// SqlCommandType 是个枚举类型,包括 UNKNOWN,INSERT,UPDATE,DELETE,SELECT,FLUSH 这几个值
SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
boolean useCache = context.getBooleanAttribute("useCache", isSelect);
boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);
// 在解析SQL之前,处理SQL中的 <include> 标签,即引用了一段SQL
XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
includeParser.applyIncludes(context.getNode());
// 解析selectKey,SelectKey在Mybatis中是为了解决Insert数据时不支持主键自动生成的问题
processSelectKeyNodes(id, parameterTypeClass, langDriver);
// 正式开始解析SQL,这时候的SQL中的<selectKey> 和 <include> 已经解析好了并且替换成正常的sql语句了。
// SqlSource非常重要,是用来处理SQL语句的。它有一个getBoundSql(Object parameterObject)方法,就是用来返回最终的SQL的
// Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
String resultSets = context.getStringAttribute("resultSets");
String keyProperty = context.getStringAttribute("keyProperty");
String keyColumn = context.getStringAttribute("keyColumn");
KeyGenerator keyGenerator;
// query!selectKey
String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
// com.hand.sxy.mapper.UserMapper.query!selectKey
keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
if (configuration.hasKeyGenerator(keyStatementId)) {
keyGenerator = configuration.getKeyGenerator(keyStatementId);
} else {
keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
}
// MapperBuilderAssistant#addMappedStatement生成一个MappedStatement对象,并缓存在configuration的mappedStatements对象中,以Mapper全类名 + id 为key,如: com.hand.sxy.mapper.UserMapper.selectByUserName
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}
可以看看buildStatementFromContext方法的返回结果,也就是MappedStatement对象
解析完之后,还有一个方法需要执行,即bindMapperForNamespace,绑定命名空间,就是将已解析的内容缓存起来
private void bindMapperForNamespace() {
String namespace = builderAssistant.getCurrentNamespace();
if (namespace != null) {
Class<?> boundType = null;
try {
boundType = Resources.classForName(namespace);
} catch (ClassNotFoundException e) {
//ignore, bound type is not required
}
if (boundType != null) {
if (!configuration.hasMapper(boundType)) {
// Spring may not know the real resource name so we set a flag
// to prevent loading again this resource from the mapper interface
// look at MapperAnnotationBuilder#loadXmlResource
configuration.addLoadedResource("namespace:" + namespace);
configuration.addMapper(boundType);
}
}
}
}
有关于Mapper.xml文件的解析暂时就分析到这里,可以发现一个规律,就是将一类类标签解析成各种类,然后缓存到configuration对象的各个属性中。
前面讲解的loadXmlResource方法内容有点多,这里再将主流程的代码贴一下
public void parse() {
String resource = type.toString();
if (!configuration.isResourceLoaded(resource)) {
// 解析Mapper.xml
loadXmlResource();
configuration.addLoadedResource(resource);
assistant.setCurrentNamespace(type.getName());
// 解析@CacheNamespace注解,也就是对应Mapper.xml中的<cache>标签,因为在loadXmlResource方法中已经解析过<cache>标签了,所以这里只要解析@CacheNamespace注解
parseCache();
// 解析CacheNamespaceRef注解,也就是对应Mapper.xml中的<cache-ref>标签,与上面类似
parseCacheRef();
Method[] methods = type.getMethods();
for (Method method : methods) {
try {
// 排除桥接方法,关系到Java的泛型擦除,具体参考 https://www.zhihu.com/question/54895701/answer/141623158
if (!method.isBridge()) {
// 解析方法上的注解,例:@Options、@ResultMap 等注解
parseStatement(method);
}
} catch (IncompleteElementException e) {
configuration.addIncompleteMethod(new MethodResolver(this, method));
}
}
}
// 重新解析残缺的method?不太清楚什么情况下会产生残缺的方法
parsePendingMethods();
}
这个方法主要是解析Mapper接口方法上的一些注解,例:@Options、 @Inser、@Select、@Update、@SelectProvider、@InsertProvider、@UpdateProvider、@DeleteProvider 等注解,有关于我们常用这几个注解,还是很有必要解释一下。那几个Provider注解,允许我们自己在方法里面写生成SQL的逻辑,可以通过他们在开发通用Mapper组件
void parseStatement(Method method) {
Class<?> parameterTypeClass = getParameterType(method);
LanguageDriver languageDriver = getLanguageDriver(method);
// 获取 @Inser、@Select、@Update、@SelectProvider、@InsertProvider、@UpdateProvider、@DeleteProvider 注解上对应的 SqlSource
SqlSource sqlSource = getSqlSourceFromAnnotations(method, parameterTypeClass, languageDriver);
if (sqlSource != null) {
// @Options 注解
Options options = method.getAnnotation(Options.class);
final String mappedStatementId = type.getName() + "." + method.getName();
Integer fetchSize = null;
Integer timeout = null;
StatementType statementType = StatementType.PREPARED;
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 注解
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);
}
// 可以发现,最终也是生成一个MappedStatement对象
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);
}
}
重点看看这部分内容
SqlSource sqlSource = getSqlSourceFromAnnotations(method, parameterTypeClass, languageDriver);
// 先聊来SqlSource这个接口,它只有一个getBoundSql,就是根据参数计算出实际的可执行SQL,这个可执行SQL在Mybatis中用BoundSql表示,这里就时根据注解获取它对应的SqlSource
private SqlSource getSqlSourceFromAnnotations(Method method, Class<?> parameterType, LanguageDriver languageDriver) {
try {
// @Inser、@Select、@Update、@Delete 其中一个
Class<? extends Annotation> sqlAnnotationType = getSqlAnnotationType(method);
// @SelectProvider、@InsertProvider、@UpdateProvider、@DeleteProvider 其中一个
Class<? extends Annotation> sqlProviderAnnotationType = getSqlProviderAnnotationType(method);
// 这两种注解不能同时出现,如果你了解@xxprovider的用法就知道了
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);
// 获取该注解上的SQL
final String[] strings = (String[]) sqlAnnotation.getClass().getMethod("value").invoke(sqlAnnotation);
// 根据SLQ 创建一个SqlSource
return buildSqlSourceFromStrings(strings, parameterType, languageDriver);
// 如果是Provider类型注解,返回ProviderSqlSource。需要注意的是,@xxxProvider注解可以指定类和方法,然后自己在指定的方法里面写SQL的生成逻辑,也就是动态SQL
} 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);
}
}
这一小部分内容主要需要注意的是:@xxxProvider注解可以指定类和方法,然后自己在指定的方法里面写SQL的生成逻辑,也就是动态SQL。
可以看看SqlSource的继承结构图
有关去解析这部分内容,也就是XMLConfigBuilder的parse()方法,暂时就介绍到这里,主要是讲解了Mapper这部分的解析逻辑。下面再次回到SqlSessionFactoryBuilder中的build方法上来。
在构建好configuration对象后,接下来的逻辑比较简单,在XMLConfigBuilder的parse方法执行完之后,就已经构建了一个完整的configuration对象,剩下的只是新建一个DefaultSqlSessionFactory对象,并将已构建好的configuration对象作为DefaultSqlSessionFactory对象的一个熟属性
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.
}
}
}
// 创建了一个SqlSessionFactory,默认实现类 DefaultSqlSessionFactory
public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}
public DefaultSqlSessionFactory(Configuration configuration) {
this.configuration = configuration;
}
对于接口中的方法,我们也简单了解一下,主要是和SqlSession相关,具体内容在接下来的章节介绍
public interface SqlSessionFactory {
SqlSession openSession();
SqlSession openSession(boolean autoCommit);
SqlSession openSession(Connection connection);
SqlSession openSession(TransactionIsolationLevel level);
SqlSession openSession(ExecutorType execType);
SqlSession openSession(ExecutorType execType, boolean autoCommit);
SqlSession openSession(ExecutorType execType, TransactionIsolationLevel level);
SqlSession openSession(ExecutorType execType, Connection connection);
Configuration getConfiguration();
}
至此,有关于Mybatis这个框架的初始化操作已经完成了,现在可以正常使用了,接下来需要了解的就是这个框架的执行流程了。