preface
This article will analyzeMybatis
How to resolve the in the mapping file during the loading of the configuration fileSQL
Statement and eachSQL
Statement is associated with the method of the mapping interface. Before looking at this part of the source code, you need to haveJDK
If you don’t know much about dynamic agents, you can first look at the basics of Java – dynamic agentsJDk
The principle of dynamic agent.
text
I Configuration of mapping file / mapping interface
giveMybatis
Configuration file formybatis-config.xml
As shown below.
<?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>
<settings>
<setting name="useGeneratedKeys" value="true"/>
</settings>
<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/test?characterEncoding=utf-8&serverTimezone=UTC&useSSL=false"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
</dataSource>
</environment>
</environments>
<mappers>
<package name="com.mybatis.learn.dao"/>
</mappers>
</configuration>
Of the above configuration filesmappersNode for configurationMapping file / mapping interface,mappersThere are two sub nodes under the node, with labels respectively<mapper>
and<package>
, these two labels are described below.
label | explain |
---|---|
<mapper> |
The tag has three attributes, namelyresource,urlandclass, and in the same<mapper> Only one of these three attributes can be set in the tag, otherwise an error will be reported.resourceandurlProperties are through tellingMybatis The location path of the mapping file is used to register the mapping file. The former uses the relative path (relative to theclasspathFor example“mapper/BookMapper.xml”), which uses absolute paths.classProperty is by tellingMybatis The mapping interface is registered with the fully qualified name of the mapping interface corresponding to the mapping file. At this time, the mapping file and the mapping interface are required to have the same name and directory. |
<package> |
The mapping interface is registered by setting the package name where the mapping interface is located. At this time, the mapping file must have the same name and directory as the mapping interface. |
According to the table above, the configuration file in the examplemybatis-config.xml
The mapping interface is registered by setting the package name of the mapping interface, so the mapping file and the mapping interface need to have the same name and directory, as shown in the following figure.
The specific reasons will be given in the source code analysis below.
II Source code analysis of loading mapping file
In mybatis source code – configuration loading, you already know that you can useMybatis
The configuration file will be read firstmybatis-config.xml
For character stream or byte stream, and then throughSqlSessionFactoryBuilder
Build based on the character stream or byte stream of the configuration fileSqlSessionFactory
。 In the whole process, it will be parsedmybatis-config.xml
And enrich the analysis resultsConfiguration
, andConfiguration
stayMybatis
Is a single example. No matter the parsing result of the configuration file, the parsing result of the mapping file, or the parsing result of the mapping interface, it will eventually existConfiguration
Yes. nextMybatis source code – configuration loadingAt the end of this article, we continue to say that the parsing of configuration files takes place inXMLConfigBuilder
ofparseConfiguration()
Method, as shown below.
private void parseConfiguration(XNode root) {
try {
propertiesElement(root.evalNode("properties"));
Properties settings = settingsAsProperties(root.evalNode("settings"));
loadCustomVfs(settings);
loadCustomLogImpl(settings);
typeAliasesElement(root.evalNode("typeAliases"));
pluginElement(root.evalNode("plugins"));
objectFactoryElement(root.evalNode("objectFactory"));
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
reflectorFactoryElement(root.evalNode("reflectorFactory"));
settingsElement(settings);
environmentsElement(root.evalNode("environments"));
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
typeHandlerElement(root.evalNode("typeHandlers"));
//According to the attributes of the mappers tag, find the mapping file / mapping interface and parse it
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
As shown above, in parsingMybatis
When the configuration file is selected, it will be based on the information in the configuration file<mappers>
Tag to find the mapping file / mapping interface and parse it. Here ismapperElement()
Implementation of method.
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
if ("package".equals(child.getName())) {
//Processing package child nodes
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) {
//Process the mapper child node with the resource attribute set
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) {
//Process the mapper child node with the URL attribute set
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) {
//Process the mapper child node with the class attribute set
Class<?> mapperInterface = Resources.classForName(mapperClass);
configuration.addMapper(mapperInterface);
} else {
//When two or more attributes of the mapper child node are set at the same time, an error is reported
throw new BuilderException(
"A mapper element may only specify a url, resource or class, but not more than one.");
}
}
}
}
}
Combined with the configuration file in the example, themapperElement()
Method should enter processingpackageBranches of child nodes, so continue to look down,Configuration
ofaddMappers(String packageName)
The method is as follows.
public void addMappers(String packageName) {
mapperRegistry.addMappers(packageName);
}
mapperRegistryyesConfiguration
Internal member variable, which has three overloadedaddMappers()
Method, first lookaddMappers(String packageName)
Method, as shown below.
public void addMappers(String packageName) {
addMappers(packageName, Object.class);
}
Go on,addMappers(String packageName, Class<?> superType)
The implementation of is as follows.
public void addMappers(String packageName, Class<?> superType) {
ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<>();
resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
//Gets the class object of the mapping interface under the package path
Set<Class<? extends Class<?>>> mapperSet = resolverUtil.getClasses();
for (Class<?> mapperClass : mapperSet) {
addMapper(mapperClass);
}
}
Finally, look againaddMapper(Class<T> type)
The implementation of is as follows.
public <T> void addMapper(Class<T> type) {
if (type.isInterface()) {
//Judge whether there is a current mapping interface in knownmappers
//Knownmappers is a map storage structure. Key is the mapping interface class object and value is mapperproxyfactory
//Mapperproxyfactory is the dynamic proxy factory corresponding to the mapping interface
if (hasMapper(type)) {
throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
}
boolean loadCompleted = false;
try {
knownMappers.put(type, new MapperProxyFactory<>(type));
//Rely on mapperannotationbuilder to complete SQL parsing in mapping file and mapping interface
//First parse the mapping file and then the mapping interface
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
parser.parse();
loadCompleted = true;
} finally {
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}
Top threeaddMapper()
Methods are called layer by layer, which is actually based on the configuration file<mappers>
Tagged<package>
The fully qualified name of the mapping file / package where the mapping interface is located set by the sub tag to obtain the name of the mapping interfaceClass
Object, and then based on theClass
Object to create aMapperProxyFactory
, as the name suggests,MapperProxyFactory
It is the dynamic proxy factory of mapping interface, which is responsible for generating dynamic proxy classes for the corresponding mapping interface. Let’s take a brief look hereMapperProxyFactory
Implementation of.
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);
}
}
Very standard basedJDK
Implementation of dynamic agent, so you can know,Mybatis
One is created for each mapping interfaceMapperProxyFactory
And then connect the mapping interface withMapperProxyFactory
Stored in the form of key value pairsMapperRegistry
ofknownMappersCache, and thenMapperProxyFactory
Will map interfaces based onJDK
The proxy class is generated by dynamic proxy. As for how to generate it, we will discuss it in the third sectionMapperProxyFactory
Further analysis.
Continue the previous process and finish creating the mapping interfaceMapperProxyFactory
After that, the mapping file and theSQL
For parsing, the classes relied on for parsing areMapperAnnotationBuilder
, its class diagram is shown below.
So one mapping interface corresponds to one mapping interfaceMapperAnnotationBuilder
And eachMapperAnnotationBuilder
Globally unique inConfiguration
Class, the parsing results will be enrichedConfiguration
Yes.MapperAnnotationBuilder
Analytical method ofparse()
As shown below.
public void parse() {
String resource = type.toString();
//Judge whether the mapping interface has been resolved, and continue to execute until it has not been resolved
if (!configuration.isResourceLoaded(resource)) {
//Parsing SQL in SQL file first
loadXmlResource();
//Add the current mapping interface to the cache to indicate that the current mapping interface has been resolved
configuration.addLoadedResource(resource);
assistant.setCurrentNamespace(type.getName());
parseCache();
parseCacheRef();
//Parsing SQL statements in mapping interfaces
for (Method method : type.getMethods()) {
if (!canHaveStatement(method)) {
continue;
}
if (getAnnotationWrapper(method, false, Select.class, SelectProvider.class).isPresent()
&& method.getAnnotation(ResultMap.class) == null) {
parseResultMap(method);
}
try {
parseStatement(method);
} catch (IncompleteElementException e) {
configuration.addIncompleteMethod(new MethodResolver(this, method));
}
}
}
parsePendingMethods();
}
according toparse()
Method, which will first parse the data in the mapping fileSQL
Statement, and then parse the in the mapping interfaceSQL
Statement, taking parsing mapping file as an example.loadXmlResource()
The method is implemented as follows.
private void loadXmlResource() {
if (!configuration.isResourceLoaded("namespace:" + type.getName())) {
//The path of the mapping file is spliced according to the fully qualified name of the mapping interface
//This also explains why mapping files and mapping interfaces are required to be in the same directory
String xmlResource = type.getName().replace('.', '/') + ".xml";
InputStream inputStream = type.getResourceAsStream("/" + xmlResource);
if (inputStream == null) {
try {
inputStream = Resources.getResourceAsStream(type.getClassLoader(), xmlResource);
} catch (IOException e2) {
}
}
if (inputStream != null) {
XMLMapperBuilder xmlParser = new XMLMapperBuilder(inputStream, assistant.getConfiguration(),
xmlResource, configuration.getSqlFragments(), type.getName());
//Parse mapping file
xmlParser.parse();
}
}
}
loadXmlResource()
In the method, firstly, the path of the mapping file should be spliced according to the fully qualified name of the mapping interface. The splicing rule is to splice the fully qualified name“.”replace with“/”, then add at the end“.xml”, which is why the mapping file and mapping interface need to be in the same directory and have the same name. The parsing of mapping files depends onXMLMapperBuilder
, its class diagram is shown below.
As shown in the figure, the parsing classes of parsing configuration file and parsing mapping file inherit fromBaseBuilder
, thenBaseBuilder
Globally unique inConfiguration
Therefore, the analysis results will be enrichedConfiguration
, special attention,XMLMapperBuilder
There’s another one calledsqlFragmentsCache for storage<sql>
Label correspondingXNode
, thissqlFragmentsandConfiguration
MediumsqlFragmentsIt is the same cache. Keep this in mind, which will be analyzed and processed later<include>
Used when labeling.XMLMapperBuilder
ofparse()
The method is as follows.
public void parse() {
if (!configuration.isResourceLoaded(resource)) {
//Start parsing from the < mapper > tag of the mapping file
//The parsing results will enrich the configuration
configurationElement(parser.evalNode("/mapper"));
configuration.addLoadedResource(resource);
bindMapperForNamespace();
}
parsePendingResultMaps();
parsePendingCacheRefs();
parsePendingStatements();
}
Keep lookingconfigurationElement()
The implementation of the method is as follows.
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"));
//Parse the < parametermap > tag to generate a parametermap and cache it to the configuration
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
//Parse the < resultmap > tag to generate a resultmap and cache it to the configuration
resultMapElements(context.evalNodes("/mapper/resultMap"));
//Save the node xnode corresponding to the < SQL > tag to sqlfragments
//In fact, it is also saved to the sqlfragments cache of configuration
sqlElement(context.evalNodes("/mapper/sql"));
//Parse < Select >, < Insert >, < update > and < delete > tags
//Generate mappedstatement and cache it to configuration
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);
}
}
configurationElement()
Method will map the file<mapper>
The sub tags under the are parsed into corresponding classes, and then cached in theConfiguration
Yes. Typically, in the mapping file<mapper>
Under the tag, the commonly used sub tags are<parameterMap>
,<resultMap>
,<select>
,<insert>
,<update>
and<delete>
, here is a simple table of the generated classes for these tags andConfiguration
The unique identification in the.
label | Parsing generated classes | stayConfiguration Unique identification in |
---|---|---|
<parameterMap> |
ParameterMap |
namespace + “.” + Tag ID |
<resultMap> |
ResultMap |
namespace + “.” + Tag ID |
<select> ,<insert> ,<update> ,<delete> |
MappedStatement |
namespace + “.” + Tag ID |
In the table abovenamespaceIs a mapping file<mapper>
TaggednamespaceProperty, so for the configuration in the mapping fileparameterMap,resultMapperhapsSQL
Execute the statement inMybatis
The only identification in isnamespace + “.” + Tag ID。 Here’s how to parse<select>
,<insert>
,<update>
and<delete>
Take the content of the label as an example to illustrate,buildStatementFromContext()
The method is as follows.
private void buildStatementFromContext(List<XNode> list) {
if (configuration.getDatabaseId() != null) {
buildStatementFromContext(list, configuration.getDatabaseId());
}
buildStatementFromContext(list, null);
}
private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
//A mappedstatement will be created for each < Select >, < Insert >, < update > and < delete > tag
//Each mappedstatement is stored in the mappedstatements cache of configuration
//Mappedstatements is a map. The key is the fully qualified name + "." of the mapping interface+ Tag ID, with the value mappedstatement
for (XNode context : list) {
final XMLStatementBuilder statementParser = new XMLStatementBuilder(
configuration, builderAssistant, context, requiredDatabaseId);
try {
statementParser.parseStatementNode();
} catch (IncompleteElementException e) {
configuration.addIncompleteStatement(statementParser);
}
}
}
For each<select>
,<insert>
,<update>
and<delete>
Labels, one is createdXMLStatementBuilder
To parse and generateMappedStatement
, again, take a lookXMLStatementBuilder
Class diagram of, as shown below.
XMLStatementBuilder
Held in<select>
,<insert>
,<update>
and<delete>
Node corresponding to labelXNode
, and help createMappedStatement
And enrichConfiguration
ofMapperBuilderAssistant
Class. Let’s have a lookXMLStatementBuilder
ofparseStatementNode()
method.
public void parseStatementNode() {
//Get tag ID
String id = context.getStringAttribute("id");
String databaseId = context.getStringAttribute("databaseId");
if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
return;
}
String nodeName = context.getNode().getNodeName();
//Get the type of tag, such as select, insert, etc
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);
//If the < include > tag is used, replace the < include > tag with the SQL fragment in the matching < SQL > tag
//The matching rule is based on namespace + "." in configuration+ Refid to match < SQL > tags
XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
includeParser.applyIncludes(context.getNode());
//Get input parameter type
String parameterType = context.getStringAttribute("parameterType");
Class<?> parameterTypeClass = resolveClass(parameterType);
//Get languagedriver to support dynamic SQL implementation
//What you get here is actually the xmllanguagedriver
String lang = context.getStringAttribute("lang");
LanguageDriver langDriver = getLanguageDriver(lang);
processSelectKeyNodes(id, parameterTypeClass, langDriver);
//Get keygenerator
KeyGenerator keyGenerator;
String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
//Get the keygenerator from the cache first
if (configuration.hasKeyGenerator(keyStatementId)) {
keyGenerator = configuration.getKeyGenerator(keyStatementId);
} else {
//If it cannot be obtained from the cache, it is determined whether to use keygenerator according to the configuration of usegeneratedkeys
//If you want to use, the keygenerator used in mybatis is jdbc3keygenerator
keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
}
//Creating sqlsource through xmllanguagedriver can be understood as a SQL statement
//If < if >, < foreach > and other tags are used to splice dynamic SQL statements, the created sqlsource is dynamicsqlsource
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
StatementType statementType = StatementType
.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
//Get the attributes on the < Select >, < Insert >, < update > and < delete > tags
Integer fetchSize = context.getIntAttribute("fetchSize");
Integer timeout = context.getIntAttribute("timeout");
String parameterMap = context.getStringAttribute("parameterMap");
String resultType = context.getStringAttribute("resultType");
Class<?> resultTypeClass = resolveClass(resultType);
String resultMap = context.getStringAttribute("resultMap");
String resultSetType = context.getStringAttribute("resultSetType");
ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
if (resultSetTypeEnum == null) {
resultSetTypeEnum = configuration.getDefaultResultSetType();
}
String keyProperty = context.getStringAttribute("keyProperty");
String keyColumn = context.getStringAttribute("keyColumn");
String resultSets = context.getStringAttribute("resultSets");
//According to the parameters obtained above, create a mappedstatement and add it to the configuration
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}
parseStatementNode()
The overall process of this method is slightly longer. To sum up, this method does the following things.
- take
<include>
Replace the label with the one it points toSQL
Fragment; - If dynamic is not used
SQL
, createRawSqlSource
To saveSQL
Statement, if dynamicSQL
(e.g. using<if>
,<foreach>
And so on), then createDynamicSqlSource
To supportSQL
Dynamic splicing of sentences; - obtain
<select>
,<insert>
,<update>
and<delete>
Attributes on the label; - Will get
SqlSource
And the attributes on the labelMapperBuilderAssistant
ofaddMappedStatement()
Method to createMappedStatement
And add toConfiguration
Yes.
MapperBuilderAssistant
Is the final creationMappedStatement
And willMappedStatement
Add toConfiguration
The processing class ofaddMappedStatement()
The method is as follows.
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");
}
//Splice out the unique identifier of the mappedstatement
//The rule is namespace + "+ id
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);
}
//Create mappedstatement
MappedStatement statement = statementBuilder.build();
//Add mappedstatement to configuration
configuration.addMappedStatement(statement);
return statement;
}
At this point, it is resolved<select>
,<insert>
,<update>
and<delete>
The contents of the tag are then generatedMappedStatement
And add toConfiguration
The process analysis of is completed. In fact, it is analyzed<parameterMap>
Tags, parsing<resultMap>
The general process of the label is basically the same as that above, and finally it is with the help ofMapperBuilderAssistant
Generate corresponding classes (e.gParameterMap
,ResultMap
)Then cache toConfiguration
And the unique identification of each class generated by parsing in the corresponding cache isnamespace + “.” + Tag ID。
Finally, go back to the beginning of this section, that isXMLConfigBuilder
MediummapperElement()
Method, in which<mappers>
Different sub tags of tags enter different branches to execute the logic of loading mapping file / mapping interface. In fact, the whole process of loading mapping file / loading mapping interface is a ring, which can be illustrated by the following figure.
XMLConfigBuilder
MediummapperElement()
Different branches of the method just enter the whole loading process from different entrances, andMybatis
Before each operation is executed, it will judge whether the current operation has been done, and if it has been done, it will not be repeated. Therefore, it ensures that the whole ring processing process will only be executed once without dead loop. And, if it is based onJavaConfig
To configureMybatis
, then usually directlyConfiguration
Setting parameter values and callingConfiguration
ofaddMappers(String packageName)
To load the mapping file / mapping interface.
III Dynamic agent in mybatis
Known inMapperRegistry
One of them is calledknownMappersofmapCache, whose key is the key of the mapping interfaceClass
Object with a value ofMybatis
Dynamic proxy factory created for mapping interfaceMapperProxyFactory
, when calling the method defined by the mapping interface to perform database operation, the actual call request will be executed byMapperProxyFactory
The proxy object generated for the mapping interface. Given hereMapperProxyFactory
The implementation of is as follows.
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);
}
}
stayMapperProxyFactory
In,mapperInterfaceFor mapping interfacesClass
Object,methodCacheIt’s amapCache, whose key is the method object of the mapping interface, and its value is the value corresponding to this methodMapperMethodInvoker
, actually,SQL
The execution of the will ultimately beMapperMethodInvoker
Complete, which will be described in detail later. Now watchMapperProxyFactory
Two overloaded innewInstance()
Method, you can know that this is based onJDK
Dynamic agent, inpublic T newInstance(SqlSession sqlSession)
In this method, aMapperProxy
And call it as a parameterprotected T newInstance(MapperProxy<T> mapperProxy)
Method, in whichProxy
ofnewProxyInstance()
Method to create a dynamic proxy object, so it can be concluded that,MapperProxy
It will come trueInvocationHandler
Interface,MapperProxy
The class diagram of is shown below.
Sure enough,MapperProxy
RealizedInvocationHandler
Interface and createMapperProxy
TimeMapperProxyFactory
Will hold itmethodCachePass toMapperProxy
ThereforemethodCacheThe actual reading and writing is byMapperProxy
To finish. Let’s have a lookMapperProxy
Realizedinvoke()
Method, as shown below.
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
} else {
//Get mappermethodinvoker from methodcache according to the method object to execute SQL
//If it cannot be obtained, create a mappermethodinvoker and add it to the methodcache, and then execute SQL
return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
be based onJDK
The principle of dynamic proxy can be known when callingJDK
When the dynamic proxy generates a method that maps the proxy object of the interface, the final call request will be sent toMapperProxy
ofinvoke()
Method, inMapperProxy
ofinvoke()
Method is actually based on the object of the method called by the mapping interfacemethodCacheGet from cacheMapperMethodInvoker
To actually execute the request. If you can’t get it, create one for the current method object firstMapperMethodInvoker
And joinmethodCacheCache, and then use the createdMapperMethodInvoker
To execute the request.cachedInvoker()
The method is implemented as follows.
private MapperMethodInvoker cachedInvoker(Method method) throws Throwable {
try {
MapperProxy.MapperMethodInvoker invoker = methodCache.get(method);
//Get mappermethodinvoker from the methodcache cache. If it is not empty, it will be returned directly
if (invoker != null) {
return invoker;
}
//The mappermethodinvoker obtained from the methodcache cache is null
//Then create a mappermethodinvoker, add it to the methodcache cache, and return
return methodCache.computeIfAbsent(method, m -> {
//JDK1. 8 processing logic of the default () method in the interface
if (m.isDefault()) {
try {
if (privateLookupInMethod == null) {
return new MapperProxy.DefaultMethodInvoker(getMethodHandleJava8(method));
} else {
return new MapperProxy.DefaultMethodInvoker(getMethodHandleJava9(method));
}
} catch (IllegalAccessException | InstantiationException | InvocationTargetException
| NoSuchMethodException e) {
throw new RuntimeException(e);
}
} else {
//First create a mappermethod
//Then use mappermethod as a parameter to create plainmethodinvoker
return new MapperProxy.PlainMethodInvoker(
new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
}
});
} catch (RuntimeException re) {
Throwable cause = re.getCause();
throw cause == null ? re : cause;
}
}
MapperMethodInvoker
It is an interface, which is usually createdMapperMethodInvoker
byPlainMethodInvoker
, take a lookPlainMethodInvoker
Constructor for.
public PlainMethodInvoker(MapperMethod mapperMethod) {
super();
this.mapperMethod = mapperMethod;
}
So createPlainMethodInvoker
You need to create aMapperMethod
, andPlainMethodInvoker
When executing, it also passes the executed request toMapperMethod
So go on,MapperMethod
The constructor for is shown below.
public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
this.command = new SqlCommand(config, mapperInterface, method);
this.method = new MethodSignature(config, mapperInterface, method);
}
establishMapperMethod
The parameters to be passed in areMapping interfaceClass
object,Object that maps the method whose interface is calledandConfiguration classConfiguration
, inMapperMethod
In the constructor of, it will be created based on the above three parametersSqlCommand
andMethodSignature
,SqlCommand
It is mainly used to save and map the data associated with the called method of the interfaceMappedStatement
Information,MethodSignature
It mainly stores the parameter information and return value information of the called method of the mapping interface. Let’s have a look firstSqlCommand
Constructor, as shown below.
public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) {
//Gets the method name of the called method of the mapping interface
final String methodName = method.getName();
//Gets the class object that declares the interface of the called method
final Class<?> declaringClass = method.getDeclaringClass();
//Gets mappedstatement object associated with the called method of the mapping interface
MappedStatement ms = resolveMappedStatement(mapperInterface, methodName, declaringClass,
configuration);
if (ms == null) {
if (method.getAnnotation(Flush.class) != null) {
name = null;
type = SqlCommandType.FLUSH;
} else {
throw new BindingException("Invalid bound statement (not found): "
+ mapperInterface.getName() + "." + methodName);
}
} else {
//Assign the ID of mappedstatement to the name field of SqlCommand
name = ms.getId();
//Assign the SQL command type of mappedstatement to the type field of SqlCommand
//For example, select, insert, etc
type = ms.getSqlCommandType();
if (type == SqlCommandType.UNKNOWN) {
throw new BindingException("Unknown execution method for: " + name);
}
}
}
The constructor mainly does these things: first get the information associated with the called methodMappedStatement
Object, and thenMappedStatement
ofidField assigned toSqlCommand
ofnameField, and finallyMappedStatement
ofsqlCommandTypeField assigned toSqlCommand
oftypeField, so,SqlCommand
It has the function associated with the called methodMappedStatement
Information about. So how to get the information associated with the called methodMappedStatement
What about the object? Keep lookingresolveMappedStatement()
The implementation of is as follows.
private MappedStatement resolveMappedStatement(Class<?> mapperInterface, String methodName,
Class<?> declaringClass, Configuration configuration) {
//Fully qualified name + "." according to the interface+ The method name splices the ID of mappedstatement
String statementId = mapperInterface.getName() + "." + methodName;
//If the mappedstatement corresponding to statementid is cached in the configuration, the mappedstatement will be returned directly
//This is one of the termination conditions of recursion
if (configuration.hasStatement(statementId)) {
return configuration.getMappedStatement(statementId);
} else if (mapperInterface.equals(declaringClass)) {
//Currently, mapperinterface is already a class object that declares the interface of the called method, and does not match the cached mappedstatement. Null is returned
//This is one of the termination conditions for resolvemappedstatement() recursion
return null;
}
//Recursive call
for (Class<?> superInterface : mapperInterface.getInterfaces()) {
if (declaringClass.isAssignableFrom(superInterface)) {
MappedStatement ms = resolveMappedStatement(superInterface, methodName,
declaringClass, configuration);
if (ms != null) {
return ms;
}
}
}
return null;
}
resolveMappedStatement()
The method will be based onInterface fully qualified name + “.”+ “Method name”AsstatementIdgoConfiguration
Get from cacheMappedStatement
, at the same timeresolveMappedStatement()
The method will recursively traverse from the mapping interface to the interface that declares the called method. The termination conditions of recursion are as follows.
- according toInterface fully qualified name + “.”+ “Method name”AsstatementIdgo
Configuration
Got from the cache ofMappedStatement
; - Recursively traverses from the mapping interface to the interface declaring the called method, and according toDeclare the fully qualified name of the interface of the called method + “+ “Method name”AsstatementIdgo
Configuration
Cannot get from cacheMappedStatement
。
The above statement is rather convoluted. Let’s illustrate it with an exampleresolveMappedStatement()
The reason why the method is written like this. The following figure shows the package path where the mapping interface and mapping file are located.
BaseMapper
,BookBaseMapper
andBookMapper
The relationship between is shown in the figure below.
thatMybatis
Will beBaseMapper
,BookBaseMapper
andBookMapper
All generate oneMapperProxyFactory
, as shown below.
Similarly, inConfiguration
Parsing is also cached in theBookBaseMapper.xml
Generated by mapping fileMappedStatement
, as shown below.
stayMybatis
of3.4.2
And previous versions will only be based onFully qualified name of mapping interface + “+ Method nameandDeclare the fully qualified name of the interface of the called method + “+ Method namegoConfiguration
ofmappedStatementsGet from cacheMappedStatement
Well, according to this logic,BookMapper
CorrespondingSqlCommand
Will only be based oncom.mybatis.learn.dao.BookMapper.selectAllBooks
andcom.mybatis.learn.dao.BaseMapper.selectAllBooks
gomappedStatementsGet from cacheMappedStatement
, then combined with the above diagrammappedStatementsThe cache content cannot be obtainedMappedStatement
Yes, so inMybatis
of3.4.3
And later versionsresolveMappedStatement()
Method to supportInherits the corresponding interface of the mapped interfaceSqlCommand
It can also correspond to the mapping interfaceMappedStatement
Associated。
aboutSqlCommand
This is the end of the analysis, andMapperMethod
MediumMethodSignature
It is mainly used to store the parameter information and return value information of the called method, which will not be repeated here.
Finally, an execution chain when the proxy object of the mapping interface executes the method is described. First, throughJDK
We can know the principle of dynamic proxy. When calling the method of proxy object, the call request will be sent to the method in proxy objectInvocationHandler
, inMybatis
The request to call the method of the proxy object of the mapping interface will be sent toMapperProxy
Therefore, when calling the method of the proxy object of the mapping interface,MapperProxy
ofinvoke()
The method executes as follows.
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
} else {
//Get mappermethodinvoker from methodcache according to the method object to execute SQL
//If it cannot be obtained, create a mappermethodinvoker and add it to the methodcache, and then execute SQL
return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
So here,Mybatis
Just like the traditionalJDK
Dynamic agents make a little difference from traditional agentsJDK
Dynamic agents are usually in theirInvocationHandler
In, some decorative logic will be added before and after the execution of the proxy object methodMybatis
In, there is no proxy object, only the proxy interface, so there is no logic to call the method of the proxy object. Instead, it is obtained according to the method object of the called methodMapperMethodInvoker
And implement itsinvoke()
Method, usually getPlainMethodInvoker
, so keep lookingPlainMethodInvoker
ofinvoke()
Method, as shown below.
@Override
public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
return mapperMethod.execute(sqlSession, args);
}
PlainMethodInvoker
ofinvoke()
Method has no logic, just continue to call itsMapperMethod
ofexecute()
Method, and through the above analysis,MapperMethod
MediumSqlCommand
AssociatedMappedStatement
, andMappedStatement
Contains the information associated with the called methodSQL
Information, combinedSqlSession
, you can complete the operation on the database. How to operate the database will be introduced in subsequent articlesMybatis
This concludes the analysis of dynamic agents in. Finally, summarize it with a pictureMybatis
The dynamic agent execution process in is shown below.
summary
This article can be summarized as follows.
- In the mapping file, each
<select>
,<insert>
,<update>
and<delete>
Labels will be createdMappedStatement
And stored inConfiguration
ofmappedStatementsIn the cache,MappedStatement
It mainly contains the information under this labelSQL
Statement, parameter information and output parameter information of this tag, etc. every lastMappedStatement
The unique identifier of the isnamespace + “.” + Tag IDThe reason for setting the unique ID in this way is to call the method of the mapping interface according to theFully qualified name of mapping interface + “+ “Method name”Gets the associated with the called methodMappedStatement
Therefore, the mapping filenamespaceIt needs to be consistent with the fully qualified name of the mapping interface<select>
,<insert>
,<update>
and<delete>
Each tag corresponds to a method of mapping interface, and each<select>
,<insert>
,<update>
and<delete>
TaggedidIt needs to be consistent with the method name of the mapping interface; - call
Mybatis
When mapping the method of the interface, the actual execution of the call request is based onJDK
The dynamic proxy is completed by the proxy object generated by the mapping interface, and the proxy object of the mapping interface is generated byMapperProxyFactory
ofnewInstance()
Method generation, one for each mapping interfaceMapperProxyFactory
; - stay
Mybatis
ofJDK
In dynamic proxy, theMapperProxy
RealizedInvocationHandler
Interface, soMapperProxy
stayMybatis
ofJDK
Dynamic agent plays the role of calling processor, that is, when calling the method of mapping interface, it is actually calledMapperProxy
Realizedinvoke()
method; - stay
Mybatis
ofJDK
In dynamic proxy, there is no proxy object, which can be understood as a proxy for the interfaceMapperProxy
ofinvoke()
Method, instead of calling the method of the proxy object, it will be generated based on the mapping interface and the method object of the called methodMapperMethod
And executeMapperMethod
ofexecute()
Method, that is, the request to call the method of the mapping interface will be sent toMapperMethod
, it can be understood as the method of mapping interfaceMapperMethod
Agent.