我們把Mybatis的功能架構分為三層:
API介面層:提供給外部使用的介面 API,開發人員通過這些本地API來操縱資料庫。介面層一接收到呼叫請求就會呼叫資料處理層來完成具體的資料處理。
MyBatis和資料庫的互動有兩種方式:
(1)使用傳統的MyBati s提供的API ;
(2)使用Mapper代理的方式
資料處理層:負責具體的SQL查詢、SQL解析、SQL執行和執行結果對映處理等。它主要的目的是根據呼叫的請求完成一次資料庫操作。
基礎支撐層:負責最基礎的功能支撐,包括連線管理、事務管理、設定載入和快取處理,這些都是共用的東西,將他們抽取出來作為最基礎的元件。為上層的資料處理層提供最基礎的支撐。
構件 | 描述 |
---|---|
SqlSession | 作為MyBatis工作的主要頂層API,表示和資料庫互動的對談,完成必要資料庫增刪改查功能 |
Executor | MyBatis執行器,是MyBatis排程的核心,負責SQL語句的生成和查詢緩 存的維護 |
StatementHandler | 封裝了JDBC Statement操作,負責對JDBC statement的操作,如設定引數、將Statement結果集轉換成List集合。 |
ParameterHandler | 負責對使用者傳遞的引數轉換成JDBC Statement所需要的引數, |
ResultSetHandler | 負責將JDBC返回的ResultSet結果集物件轉換成List型別的集合; |
TypeHandler | 負責java資料型別和jdbc資料型別之間的對映和轉換 |
MappedStatement | MappedStatement維護了一條select、delete、update、insert節點的封裝 |
SqlSource | 負責根據使用者傳遞的parameterObject,動態地生成SQL語句,將資訊封裝到BoundSql物件中,並返回 |
BoundSql | 表示動態生成的SQL語句以及相應的引數資訊 |
載入設定並初始化
觸發條件:載入組態檔
設定來源於兩個地方,一個是組態檔(主組態檔conf.xml,mapper檔案*.xml),—個是java程式碼中的 註解,將主組態檔內容解析封裝到Configuration,將sql的設定資訊載入成為一個Mappedstatement 物件,儲存在記憶體之中。
接收呼叫請求
觸發條件:呼叫Mybatis提供的API
傳入引數:為SQL的ID和傳入引數物件
處理過程:將請求傳遞給下層的請求處理層進行處理。
處理操作請求
觸發條件:API介面層傳遞請求過來
傳入引數:為SQL的ID和傳入引數物件
處理過程:
① 根據SQL的ID查詢對應的MappedStatement物件。
② 根據傳入引數物件解析MappedStatement物件,得到最終要執行的SQL和執行傳入引數。
③ 獲取資料庫連線,根據得到的最終SQL語句和執行傳入引數到資料庫執行,並得到執行結果。
④ 根據MappedStatement物件中的結果對映設定對得到的執行結果進行轉換處理,並得到最終的處理結果。
⑤ 釋放連線資源。
返回處理結果
將最終的處理結果返回。
public class MybatisTest {
/**
* 傳統方式
*
* @throws IOException
*/
public void test1() throws IOException {
// 1. 讀取組態檔,讀成位元組輸入流,注意:現在還沒解析
InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
// 2. (1)解析組態檔,封裝Configuration物件 (2)建立DefaultSqlSessionFactory物件
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
// 3. 生產了DefaultSqlsession範例物件 設定了事務不自動提交 完成了executor物件的建立
SqlSession sqlSession = sqlSessionFactory.openSession();
// 4.(1)根據statementid來從Configuration中map集合中獲取到了指定的MappedStatement物件
//(2)將查詢任務委派了executor執行器
List<Object> objects = sqlSession.selectList("namespace.id");
// 5.釋放資源
sqlSession.close();
}
}
首先呼叫Resources中getResourceAsStream()方法讀取MyBatis核心組態檔,讀成位元組輸入流
// 1. 讀取組態檔,讀成位元組輸入流,注意:現在還沒解析
InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
Resources類
/**
* Returns a resource on the classpath as a Stream object
*
* @param resource The resource to find
* @return The resource
* @throws java.io.IOException If the resource cannot be found or read
*/
public static InputStream getResourceAsStream(String resource) throws IOException {
return getResourceAsStream(null, resource);
}
/**
* Returns a resource on the classpath as a Stream object
*
* @param loader The classloader used to fetch the resource
* @param resource The resource to find
* @return The resource
* @throws java.io.IOException If the resource cannot be found or read
*/
public static InputStream getResourceAsStream(ClassLoader loader, String resource) throws IOException {
InputStream in = classLoaderWrapper.getResourceAsStream(resource, loader);
if (in == null) {
throw new IOException("Could not find resource " + resource);
}
return in;
}
// 2. (1)解析組態檔,封裝Configuration物件 (2)建立DefaultSqlSessionFactory物件
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
首先看下構建SqlSessionFactory的整個流程,1:解析核心組態檔;2:建立DefaultSqlSessionFactory物件
SqlSessionFactoryBuilder類
// 1.我們最初呼叫的build
public SqlSessionFactory build(InputStream inputStream) {
//呼叫了過載方法
return build(inputStream, null, null);
}
public SqlSessionFactory build(InputStream inputStream, String environment) {
return build(inputStream, environment, null);
}
public SqlSessionFactory build(InputStream inputStream, Properties properties) {
return build(inputStream, null, properties);
}
// 2.呼叫的過載方法
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
try {
// 建立 XMLConfigBuilder, XMLConfigBuilder是專門解析mybatis的組態檔的類
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
// 執行 XML 解析
// 建立 DefaultSqlSessionFactory 物件
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.
}
}
}
MyBatis在初始化的時候,會將MyBatis的設定資訊全部載入到記憶體中,使用Configuratio容器範例來維護
首先對Configuration物件進行介紹:
作用:MappedStatement與Mapper組態檔中的一個select/update/insert/delete節點相對應。mapper中設定的標籤都被封裝到了此物件中,主要用途是描述一條SQL語句。
初始化過程:回顧剛開 始介紹的載入組態檔的過程中,會對mybatis-config.xm l中的各個標籤都進行解析,其中有mappers 標籤用來引入mapper.xml檔案或者設定mapper介面的目錄。
<select id="findAll" resultType="com.cyd.pojo.User">
select * FROM user
</select>
像這樣的一個select標籤會在初始化組態檔時被解析封裝成一個MappedStatement物件,然後儲存在Configuration物件的mappedStatements屬性中,mappedStatements 是一個HashMap,儲存時key=全限定類名+方法名,value =對應的MappedStatement物件。
在configuration中對應的屬性為
/**
* MappedStatement 對映
*
* KEY:`${namespace}.${id}`
*/
protected final Map<String, MappedStatement> mappedStatements = new StrictMap<>("Mapped Statements collection");
MyBatis將核心組態檔交由XMLConfigBuilder的parse()方法來解析
/**
* 解析 XML 成 Configuration 物件。
*
* @return Configuration 物件
*/
public Configuration parse() {
// 若已解析,丟擲 BuilderException 異常
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
// 標記已解析
parsed = true;
// parser是XPathParser解析器物件,讀取節點內資料,<configuration>是MyBatis組態檔中的頂層標籤
// 解析 XML configuration 節點
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
/**
* 解析 XML
* <p>
* 具體 MyBatis 有哪些 XML 標籤,參見 《XML 對映組態檔》http://www.mybatis.org/mybatis-3/zh/configuration.html
*
* @param root 根節點
*/
private void parseConfiguration(XNode root) {
try {
//issue #117 read properties first
// 解析 <properties /> 標籤
propertiesElement(root.evalNode("properties"));
// 解析 <settings /> 標籤
Properties settings = settingsAsProperties(root.evalNode("settings"));
// 載入自定義的 VFS 實現類
loadCustomVfs(settings);
// 解析 <typeAliases /> 標籤
typeAliasesElement(root.evalNode("typeAliases"));
// 解析 <plugins /> 標籤
pluginElement(root.evalNode("plugins"));
// 解析 <objectFactory /> 標籤
objectFactoryElement(root.evalNode("objectFactory"));
// 解析 <objectWrapperFactory /> 標籤
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
// 解析 <reflectorFactory /> 標籤
reflectorFactoryElement(root.evalNode("reflectorFactory"));
// 賦值 <settings /> 到 Configuration 屬性
settingsElement(settings);
// read it after objectFactory and objectWrapperFactory issue #631
// 解析 <environments /> 標籤
environmentsElement(root.evalNode("environments"));
// 解析 <databaseIdProvider /> 標籤
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
// 解析 <typeHandlers /> 標籤
typeHandlerElement(root.evalNode("typeHandlers"));
// 解析 <mappers /> 標籤
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
一般我們在MyBatis核心組態檔中可以使用以下四種方式設定對映檔案的位置
<mappers>
<!-- 將包內的對映器介面實現全部註冊為對映器 -->
<package name="com.cyd.mapper"/>
<!-- 使用對映器介面的完全限定類名 -->
<mapper class="com.cyd.mapper.UserMapper"/>
<!-- 使用相對於類路徑的資源參照 -->
<mapper resource="UserMapper.xml"/>
<!-- 使用完全限定資源定位符(URL) -->
<mapper url="file:///xxx/UserMapper.xml"/>
</mappers>
/**
* 解析 <mappers /> 標籤
*
* @param parent <mappers />標籤
* @throws Exception
*/
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
// 遍歷子節點
for (XNode child : parent.getChildren()) {
// 如果是 package 標籤,則掃描該包
if ("package".equals(child.getName())) {
// 獲得包名
String mapperPackage = child.getStringAttribute("name");
// 新增到 configuration 中
configuration.addMappers(mapperPackage);
// 如果是 mapper 標籤,
} else {
// 獲得 resource、url、class 屬性
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);
// 獲得 resource 的 InputStream 物件
InputStream inputStream = Resources.getResourceAsStream(resource);
// 建立 XMLMapperBuilder 物件
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
// 執行解析
mapperParser.parse();
// 使用完全限定資源定位符(URL)
} else if (resource == null && url != null && mapperClass == null) {
ErrorContext.instance().resource(url);
// 獲得 url 的 InputStream 物件
InputStream inputStream = Resources.getUrlAsStream(url);
// 建立 XMLMapperBuilder 物件
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
// 執行解析
mapperParser.parse();
// 使用對映器介面實現類的完全限定類名
} else if (resource == null && url == null && mapperClass != null) {
// 獲得 Mapper 介面
Class<?> mapperInterface = Resources.classForName(mapperClass);
// 新增到 configuration 中
configuration.addMapper(mapperInterface);
} else {
throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
}
}
}
}
}
1、解析package標籤
// 如果是 package 標籤,則掃描該包
if ("package".equals(child.getName())) {
// 獲得包名
String mapperPackage = child.getStringAttribute("name");
// 新增到 configuration 中
configuration.addMappers(mapperPackage);
}
configuration.addMappers(mapperPackage)
public void addMappers(String packageName) {
// 掃描該包下所有的 Mapper 介面,並新增到 mapperRegistry 中
mapperRegistry.addMappers(packageName);
}
mapperRegistry.addMappers(packageName),掃描指定包,並將符合的類,新增到MapperRegistry的屬性knownMappers中
//這個類中維護一個HashMap存放MapperProxyFactory
private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();
/**
* @since 3.2.2
*/
public void addMappers(String packageName) {
addMappers(packageName, Object.class);
}
/**
* 掃描指定包,並將符合的類,新增到 {@link #knownMappers} 中
*
* @since 3.2.2
*/
public void addMappers(String packageName, Class<?> superType) {
// 掃描指定包下的指定類
ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<>();
resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
Set<Class<? extends Class<?>>> mapperSet = resolverUtil.getClasses();
// 遍歷,新增到 knownMappers 中
for (Class<?> mapperClass : mapperSet) {
addMapper(mapperClass);
}
}
public <T> void addMapper(Class<T> type) {
// 判斷,必須是介面。
if (type.isInterface()) {
// 已經新增過,則丟擲 BindingException 異常
if (hasMapper(type)) {
throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
}
boolean loadCompleted = false;
try {
// 新增到 knownMappers 中
knownMappers.put(type, new MapperProxyFactory<>(type));
// 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.
// 解析 Mapper 的註解設定
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
parser.parse();
// 標記載入完成
loadCompleted = true;
} finally {
// 若載入未完成,從 knownMappers 中移除
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}
上面的knowMappers.put(type, new MapperProxyFactory<>(type)),為該類生成一個動態代理工廠放入到Map集合中,key為該類Class,value為該類動態代理工廠。
2、解析mapper標籤(分別有resource、class、url三種屬性)
// 如果是 package 標籤,則掃描該包
if ("package".equals(child.getName())) {
// 獲得包名
String mapperPackage = child.getStringAttribute("name");
// 新增到 configuration 中
configuration.addMappers(mapperPackage);
// 如果是 mapper 標籤,
} else {
// 獲得 resource、url、class 屬性
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);
// 獲得 resource 的 InputStream 物件
InputStream inputStream = Resources.getResourceAsStream(resource);
// 建立 XMLMapperBuilder 物件
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
// 執行解析
mapperParser.parse();
// 使用完全限定資源定位符(URL)
} else if (resource == null && url != null && mapperClass == null) {
ErrorContext.instance().resource(url);
// 獲得 url 的 InputStream 物件
InputStream inputStream = Resources.getUrlAsStream(url);
// 建立 XMLMapperBuilder 物件
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
// 執行解析
mapperParser.parse();
// 使用對映器介面實現類的完全限定類名
} else if (resource == null && url == null && mapperClass != null) {
// 獲得 Mapper 介面
Class<?> mapperInterface = Resources.classForName(mapperClass);
// 新增到 configuration 中
configuration.addMapper(mapperInterface);
} else {
throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
}
}
(1)如果是使用對映器介面實現類的完全限定類名,即使用class的話,和package標籤類似,呼叫configuration.addMapper()介面將該類新增到MapperRegistry的knownMappers中。
(2)如果是/使用相對於類路徑的資源參照(resource)或使用完全限定資源定位符(URL),則交由XMLMapperBuilder解析Mapper組態檔,呼叫的是XMLMapperBuilder的parse()方法。
在 XMLMapperBuilder 中的處理:
/**
* 解析***Mapper.xml組態檔
*/
public void parse() {
// 判斷當前 Mapper 是否已經載入過
if (!configuration.isResourceLoaded(resource)) {
// 解析 `<mapper />` 節點
configurationElement(parser.evalNode("/mapper"));
// 標記該 Mapper 已經載入過
configuration.addLoadedResource(resource);
// 繫結 Mapper
bindMapperForNamespace();
}
// 解析待定的 <resultMap /> 節點
parsePendingResultMaps();
// 解析待定的 <cache-ref /> 節點
parsePendingCacheRefs();
// 解析待定的 SQL 語句的節點
parsePendingStatements();
}
// 解析 `<mapper />` 節點
private void configurationElement(XNode context) {
try {
// 獲得 namespace 屬性
String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.equals("")) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
// 設定 namespace 屬性
builderAssistant.setCurrentNamespace(namespace);
// 解析 <cache-ref /> 節點
cacheRefElement(context.evalNode("cache-ref"));
// 解析 <cache /> 節點
cacheElement(context.evalNode("cache"));
// 已廢棄!老式風格的引數對映。內聯引數是首選,這個元素可能在將來被移除,這裡不會記錄。
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
// 解析 <resultMap /> 節點們
resultMapElements(context.evalNodes("/mapper/resultMap"));
// 解析 <sql /> 節點們
sqlElement(context.evalNodes("/mapper/sql"));
// 解析 <select /> <insert /> <update /> <delete /> 節點們
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);
}
}
// 解析 <select /> <insert /> <update /> <delete /> 節點們
private void buildStatementFromContext(List<XNode> list) {
if (configuration.getDatabaseId() != null) {
buildStatementFromContext(list, configuration.getDatabaseId());
}
buildStatementFromContext(list, null);
// 上面兩塊程式碼,可以簡寫成 buildStatementFromContext(list, configuration.getDatabaseId());
}
private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
//遍歷 <select /> <insert /> <update /> <delete /> 節點們
for (XNode context : list) {
// 建立 XMLStatementBuilder 物件,執行解析
final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
try {
statementParser.parseStatementNode();
} catch (IncompleteElementException e) {
// 解析失敗,新增到 configuration 中
configuration.addIncompleteStatement(statementParser);
}
}
}
在XMLStatementBuilder的parseStatementNode()中具體執行select、insert、update、delete節點的解析
/**
* 執行解析
*/
public void parseStatementNode() {
// 獲得 id 屬性,編號。
String id = context.getStringAttribute("id");
// 獲得 databaseId , 判斷 databaseId 是否匹配
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");
// 獲得 lang 對應的 LanguageDriver 物件
LanguageDriver langDriver = getLanguageDriver(lang);
// 獲得 resultType 對應的類
Class<?> resultTypeClass = resolveClass(resultType);
// 獲得 resultSet 對應的列舉值
String resultSetType = context.getStringAttribute("resultSetType");
ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
// 獲得 statementType 對應的列舉值
StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
// 獲得 SQL 對應的 SqlCommandType 列舉值
String nodeName = context.getNode().getNodeName();
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);
// Include Fragments before parsing
// 建立 XMLIncludeTransformer 物件,並替換 <include /> 標籤相關的內容
XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
includeParser.applyIncludes(context.getNode());
// Parse selectKey after includes and remove them.
// 解析 <selectKey /> 標籤
processSelectKeyNodes(id, parameterTypeClass, langDriver);
// Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
// 建立 SqlSource 物件
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
// 獲得 KeyGenerator 物件
String resultSets = context.getStringAttribute("resultSets");
String keyProperty = context.getStringAttribute("keyProperty");
String keyColumn = context.getStringAttribute("keyColumn");
KeyGenerator keyGenerator;
// 優先,從 configuration 中獲得 KeyGenerator 物件。如果存在,意味著是 <selectKey /> 標籤設定的
String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
if (configuration.hasKeyGenerator(keyStatementId)) {
keyGenerator = configuration.getKeyGenerator(keyStatementId);
// 其次,根據標籤屬性的情況,判斷是否使用對應的 Jdbc3KeyGenerator 或者 NoKeyGenerator 物件
} else {
keyGenerator = context.getBooleanAttribute("useGeneratedKeys", // 優先,基於 useGeneratedKeys 屬性判斷
configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType)) // 其次,基於全域性的 useGeneratedKeys 設定 + 是否為插入語句型別
? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
}
// 建立 MappedStatement 物件
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}
到此對xml組態檔的解析就結束了,回到步驟1.2中呼叫的過載build方法
// 2.呼叫的過載方法
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
try {
// 建立 XMLConfigBuilder, XMLConfigBuilder是專門解析mybatis的組態檔的類
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
// 執行 XML 解析
// 建立 DefaultSqlSessionFactory 物件
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.
}
}
}
上面的組態檔解析都是parser.parse()方法中的操作,解析完後返回一個Configuration物件,然後呼叫build(Configuration configuration)方法建立DefaultSqlSessionFactory物件
/**
* 建立 DefaultSqlSessionFactory 物件
*
* @param config Configuration 物件
* @return DefaultSqlSessionFactory 物件
*/
public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config); //構建者設計模式
}
// 3. 生產DefaultSqlsession範例物件 設定了事務不自動提交 完成了executor物件的建立
SqlSession sqlSession = sqlSessionFactory.openSession();
首先看一下建立SqlSession物件的整個流程
先簡單介紹SqlSession :
Executor:Executor也是一個介面,他有三個常用的實現類:
DefaultSqlsession
public class DefaultSqlSession implements SqlSession {
private final Configuration configuration;
private final Executor executor;
/**
* 是否自動提交事務
*/
private final boolean autoCommit;
/**
* 是否發生資料變更
*/
private boolean dirty;
/**
* Cursor 陣列
*/
private List<Cursor<?>> cursorList;
public DefaultSqlSession(Configuration configuration, Executor executor, boolean autoCommit) {
this.configuration = configuration;
this.executor = executor;
this.dirty = false;
this.autoCommit = autoCommit;
}
public DefaultSqlSession(Configuration configuration, Executor executor) {
this(configuration, executor, false);
}
}
DefaultSqlSessionFactory
public class DefaultSqlSessionFactory implements SqlSessionFactory {
private final Configuration configuration;
public DefaultSqlSessionFactory(Configuration configuration) {
this.configuration = configuration;
}
// 6. 進入openSession方法
@Override
public SqlSession openSession() {
// getDefaultExecutorType()傳遞的是SimpleExecutor
return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
}
// 7. 進入openSessionFromDataSource。
// ExecutorType 為Executor的型別,TransactionIsolationLevel為事務隔離級別,autoCommit是否開啟事務
// openSession的多個過載方法可以指定獲得的SeqSession的Executor型別和事務的處理
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
try {
// 獲得 Environment 物件
final Environment environment = configuration.getEnvironment();
// 建立 Transaction 物件
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
// 建立 Executor 物件
final Executor executor = configuration.newExecutor(tx, execType);
// 建立 DefaultSqlSession 物件
return new DefaultSqlSession(configuration, executor, autoCommit);
} catch (Exception e) {
// 如果發生異常,則關閉 Transaction 物件
closeTransaction(tx); // may have fetched a connection so lets call close()
throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
}
DefaultSqlSessionFactory的openSession()方法呼叫openSessionFromDataSource建立DefaultSqlSession物件,注意該方法中傳遞的第一個引數是執行器的型別,configuration.getDefaultExecutorType()傳遞的是SimpleExecutor,所以DefaultSqlSession中使用的Executor是SimpleExecutor。
呼叫configuration.newExecutor(Transaction transaction, ExecutorType executorType)建立Executor,使用的Executor是SimpleExecutor。
/**
* 建立 Executor 物件
*
* @param transaction 事務物件
* @param executorType 執行器型別
* @return Executor 物件
*/
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
// 獲得執行器型別
executorType = executorType == null ? defaultExecutorType : executorType; // 使用預設
executorType = executorType == null ? ExecutorType.SIMPLE : executorType; // 使用 ExecutorType.SIMPLE
// 建立對應實現的 Executor 物件
Executor executor;
if (ExecutorType.BATCH == executorType) {
executor = new BatchExecutor(this, transaction);
} else if (ExecutorType.REUSE == executorType) {
executor = new ReuseExecutor(this, transaction);
} else {
executor = new SimpleExecutor(this, transaction);
}
// 如果開啟快取,建立 CachingExecutor 物件,進行包裝
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
// 應用外掛
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
// 4.(1)根據statementid來從Configuration中map集合中獲取到了指定的MappedStatement物件
// (2)將查詢任務委派了executor執行器
List<Object> objects = sqlSession.selectList("namespace.id");
呼叫DefaultSqlSession中的selectList()方法
// 進入selectList方法,多個過載方法
@Override
public <E> List<E> selectList(String statement) {
return this.selectList(statement, null);
}
@Override
public <E> List<E> selectList(String statement, Object parameter) {
return this.selectList(statement, parameter, RowBounds.DEFAULT);
}
@Override
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
// 獲得 MappedStatement 物件
MappedStatement ms = configuration.getMappedStatement(statement);
// 執行查詢
return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
繼續原始碼中的步驟,進入executor.query(),此方法在SimpleExecutor的父類別BaseExecutor中實現
BaseExecutor類
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
// 根據傳入的引數動態獲得SQL語句,最後返回用BoundSql物件表示
BoundSql boundSql = ms.getBoundSql(parameter);
// 為本次查詢建立快取的Key
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
// 查詢
return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
@SuppressWarnings("unchecked")
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
// 已經關閉,則丟擲 ExecutorException 異常
if (closed) {
throw new ExecutorException("Executor was closed.");
}
// 清空本地快取,如果 queryStack 為零,並且要求清空本地快取。
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();
}
List<E> list;
try {
// queryStack + 1
queryStack++;
// 從一級快取中,獲取查詢結果
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
// 獲取到,則進行處理
if (list != null) {
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
// 獲得不到,則從資料庫中查詢
} else {
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
} finally {
// queryStack - 1
queryStack--;
}
if (queryStack == 0) {
// 執行延遲載入
for (DeferredLoad deferredLoad : deferredLoads) {
deferredLoad.load();
}
// issue #601
// 清空 deferredLoads
deferredLoads.clear();
// 如果快取級別是 LocalCacheScope.STATEMENT ,則進行清理
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
clearLocalCache();
}
}
return list;
}
// 從資料庫中讀取操作
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
List<E> list;
// 在快取中,新增佔位物件。此處的預留位置,和延遲載入有關,可見 `DeferredLoad#canLoad()` 方法
localCache.putObject(key, EXECUTION_PLACEHOLDER);
try {
// 執行讀操作
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
// 從快取中,移除佔位物件
localCache.removeObject(key);
}
// 新增到快取中
localCache.putObject(key, list);
// 暫時忽略,儲存過程相關
if (ms.getStatementType() == StatementType.CALLABLE) {
localOutputParameterCache.putObject(key, parameter);
}
return list;
}
SimpleExecutor中實現父類別的doQuery抽象方法
@Override
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
Statement stmt = null;
try {
Configuration configuration = ms.getConfiguration();
// 傳入引數建立StatementHanlder物件來執行查詢
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
// 建立jdbc中的statement物件
stmt = prepareStatement(handler, ms.getStatementLog());
// 執行 StatementHandler ,進行讀操作
return handler.query(stmt, resultHandler);
} finally {
// 關閉 StatementHandler 物件
closeStatement(stmt);
}
}
// 建立jdbc中的statement物件
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
Statement stmt;
// 獲得 Connection 物件
Connection connection = getConnection(statementLog);
// 建立 Statement 或 PrepareStatement 物件
stmt = handler.prepare(connection, transaction.getTimeout());
// 設定 SQL 上的引數,例如 PrepareStatement 物件上的預留位置
handler.parameterize(stmt);
return stmt;
}
上述的Executor.query()方法幾經轉折,最後會建立一個StatementHandler物件,然後將必要的引數傳遞給StatementHandler,使用StatementHandler來完成對資料庫的查詢,最終返回List結果集。
從上面的程式碼中我們可以看出,Executor的功能和作用是:
BoundSql,主要注意其中的兩個屬性sql、parameterMappings
public class BoundSql {
/**
* SQL語句,進行 #{ } 和 ${ } 替換完畢之後的結果sql, 注意每個 #{ }替換完之後就是一個 ?
*/
private final String sql;
/**
* ParameterMapping陣列,這裡的parameterMappings列表引數裡的item個數,
* 以及每個item的屬性名稱等等, 都是和上面的sql中的 ? 完全一一對應的.
*/
private final List<ParameterMapping> parameterMappings;
/**
* 引數物件
*/
private final Object parameterObject;
/**
* 附加的引數集合
*/
private final Map<String, Object> additionalParameters;
/**
* {@link #additionalParameters} 的 MetaObject 物件
*/
private final MetaObject metaParameters;
public BoundSql(Configuration configuration, String sql, List<ParameterMapping> parameterMappings, Object parameterObject) {
this.sql = sql;
this.parameterMappings = parameterMappings;
this.parameterObject = parameterObject;
this.additionalParameters = new HashMap<>();
this.metaParameters = configuration.newMetaObject(additionalParameters);
}
public String getSql() {
return sql;
}
public List<ParameterMapping> getParameterMappings() {
return parameterMappings;
}
public Object getParameterObject() {
return parameterObject;
}
public boolean hasAdditionalParameter(String name) {
String paramName = new PropertyTokenizer(name).getName();
return additionalParameters.containsKey(paramName);
}
public void setAdditionalParameter(String name, Object value) {
metaParameters.setValue(name, value);
}
public Object getAdditionalParameter(String name) {
return metaParameters.getValue(name);
}
}
進入到PreparedStatementHandler的 parameterize(statement) 方法的實現,該方法中呼叫ParameterHandler的setParameters()方法實現引數的設定。
PreparedStatementHandler
@Override
public void parameterize(Statement statement) throws SQLException {
// 使用ParameterHandler物件來完成對Statement的設值
parameterHandler.setParameters((PreparedStatement) statement);
}
DefaultParameterHandler
/**
* ParameterHandler 類的 setParameters(PreparedStatement ps) 實現對某一個Statement進行設定引數
*/
@Override
public void setParameters(PreparedStatement ps) {
ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
// 遍歷 ParameterMapping 陣列
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
if (parameterMappings != null) {
for (int i = 0; i < parameterMappings.size(); i++) {
// 獲得 ParameterMapping 物件
ParameterMapping parameterMapping = parameterMappings.get(i);
if (parameterMapping.getMode() != ParameterMode.OUT) {
// 獲得值
Object value;
String propertyName = parameterMapping.getProperty();
if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params
value = boundSql.getAdditionalParameter(propertyName);
} else if (parameterObject == null) {
value = null;
} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
value = parameterObject;
} else {
MetaObject metaObject = configuration.newMetaObject(parameterObject);
value = metaObject.getValue(propertyName);
}
// 獲得 typeHandler、jdbcType 屬性
TypeHandler typeHandler = parameterMapping.getTypeHandler();
JdbcType jdbcType = parameterMapping.getJdbcType();
if (value == null && jdbcType == null) {
jdbcType = configuration.getJdbcTypeForNull();
}
// 設定 ? 預留位置的引數
try {
typeHandler.setParameter(ps, i + 1, value, jdbcType);
} catch (TypeException | SQLException e) {
throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
}
}
}
}
}
從上述的程式碼可以看到,StatementHandler的parameterize(Statement)方法呼叫了ParameterHandler的setParameters(statement)方法,ParameterHandler的setParameters(Statement )方法負責根據我們輸入的引數,對statement物件的 ?預留位置處進行賦值。
進入到StatementHandler 的 List query(Statement statement, ResultHandler resultHandler)方法的實現:
@Override
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
PreparedStatement ps = (PreparedStatement) statement;
// 執行查詢
ps.execute();
// 處理返回結果
return resultSetHandler.handleResultSets(ps);
}
從上述程式碼我們可以看出,StatementHandler 的List query()方法首先呼叫statement的execute()方法執行資料庫查詢,再交由ResultSetHandler進行結果集封裝。
ResultSetHandler 的 handleResultSets()方法會將 Statement 語句執行後生成的 resultSet結果集轉換成List結果集。
DefaultResultSetHandler
@Override
public List<Object> handleResultSets(Statement stmt) throws SQLException {
ErrorContext.instance().activity("handling results").object(mappedStatement.getId());
// 多 ResultSet 的結果集合,每個 ResultSet 對應一個 Object 物件。而實際上,每個 Object 是 List<Object> 物件。
// 在不考慮儲存過程的多 ResultSet 的情況,普通的查詢,實際就一個 ResultSet ,也就是說,multipleResults 最多就一個元素。
final List<Object> multipleResults = new ArrayList<>();
int resultSetCount = 0;
// 獲得首個 ResultSet 物件,並封裝成 ResultSetWrapper 物件
ResultSetWrapper rsw = getFirstResultSet(stmt);
// 獲得 ResultMap 陣列
// 在不考慮儲存過程的多 ResultSet 的情況,普通的查詢,實際就一個 ResultSet ,也就是說,resultMaps 就一個元素。
List<ResultMap> resultMaps = mappedStatement.getResultMaps();
int resultMapCount = resultMaps.size();
validateResultMapsCount(rsw, resultMapCount); // 校驗
while (rsw != null && resultMapCount > resultSetCount) {
// 獲得 ResultMap 物件
ResultMap resultMap = resultMaps.get(resultSetCount);
// 處理 ResultSet ,將結果新增到 multipleResults 中
handleResultSet(rsw, resultMap, multipleResults, null);
// 獲得下一個 ResultSet 物件,並封裝成 ResultSetWrapper 物件
rsw = getNextResultSet(stmt);
// 清理
cleanUpAfterHandlingResultSet();
// resultSetCount ++
resultSetCount++;
}
// 因為 `mappedStatement.resultSets` 只在儲存過程中使用,本系列暫時不考慮,忽略即可
String[] resultSets = mappedStatement.getResultSets();
if (resultSets != null) {
while (rsw != null && resultSetCount < resultSets.length) {
ResultMapping parentMapping = nextResultMaps.get(resultSets[resultSetCount]);
if (parentMapping != null) {
String nestedResultMapId = parentMapping.getNestedResultMapId();
ResultMap resultMap = configuration.getResultMap(nestedResultMapId);
handleResultSet(rsw, resultMap, null, parentMapping);
}
rsw = getNextResultSet(stmt);
cleanUpAfterHandlingResultSet();
resultSetCount++;
}
}
// 如果是 multipleResults 單元素,則取首元素返回
return collapseSingleResultList(multipleResults);
}
private List<Object> collapseSingleResultList(List<Object> multipleResults) {
return multipleResults.size() == 1 ? (List<Object>) multipleResults.get(0) : multipleResults;
}
public class MybatisTest {
/**
* mapper代理方式
*/
public void test2() throws IOException {
InputStream inputStream = Resources.getResourceAsStream("sqlMapConfig.xml");
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = factory.openSession();
// 使用JDK動態代理對mapper介面產生代理物件
IUserMapper mapper = sqlSession.getMapper(IUserMapper.class);
//代理物件呼叫介面中的任意方法,執行的都是動態代理中的invoke方法
List<Object> allUser = mapper.findAllUser();
}
}
思考一個問題,通常的Mapper介面我們都沒有實現的方法卻可以使用,是為什麼呢?答案很簡單動態代理
開始之前介紹一下MyBatis初始化時對介面的處理:MapperRegistry是Configuration中的一個屬性,它內部維護一個HashMap用於存放mapper介面的工廠類,每個介面對應一個工廠類。mappers中可以設定介面的包路徑,或者某個具體的介面類。
<mappers>
<mapper class="com.cyd.mapper.UserMapper"/>
<package name="com.cyd.mapper"/>
</mappers>
當解析mappers標籤時,它會判斷解析到的是mapper組態檔時,會再將對應組態檔中的增刪改查標籤封裝成MappedStatement物件,存入Configuration容器的mappedStatements中。(上文介紹了)當判斷解析到介面時,會建此介面對應的MapperProxyFactory物件,存入HashMap中,key=介面的位元組碼物件,value =此介面對應的MapperProxyFactory物件。
進入 sqlSession.getMapper(UserMapper.class )中
DefaultSqlSession
@Override
public <T> T getMapper(Class<T> type) {
return configuration.getMapper(type, this);
}
Configuration
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
return mapperRegistry.getMapper(type, sqlSession);
}
MapperRegistry
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
// 獲得 MapperProxyFactory 物件
final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
// 不存在,則丟擲 BindingException 異常
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);
}
}
MapperProxyFactory
// MapperProxyFactory類中的newInstance方法
public T newInstance(SqlSession sqlSession) {
// 建立了JDK動態代理的invocationHandler介面的實現類mapperProxy
final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
// 呼叫了過載方法
return newInstance(mapperProxy);
}
protected T newInstance(MapperProxy<T> mapperProxy) {
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[]{mapperInterface}, mapperProxy);
}
MapperProxy
public class MapperProxy<T> implements InvocationHandler, Serializable {
private static final long serialVersionUID = -6424540398559729838L;
/**
* SqlSession 物件
*/
private final SqlSession sqlSession;
/**
* Mapper 介面
*/
private final Class<T> mapperInterface;
/**
* 方法與 MapperMethod 的對映
*
* 從 {@link MapperProxyFactory#methodCache} 傳遞過來
*/
private final Map<Method, MapperMethod> methodCache;
// 構造,傳入了SqlSession,說明每個session中的代理物件的不同的!
public MapperProxy(SqlSession sqlSession, Class<T> mapperInterface, Map<Method, MapperMethod> methodCache) {
this.sqlSession = sqlSession;
this.mapperInterface = mapperInterface;
this.methodCache = methodCache;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
// 如果是 Object 定義的方法,直接呼叫
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
} else if (isDefaultMethod(method)) {
return invokeDefaultMethod(proxy, method, args);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
// 獲得 MapperMethod 物件
final MapperMethod mapperMethod = cachedMapperMethod(method);
// 重點在這:MapperMethod最終呼叫了執行的方法
return mapperMethod.execute(sqlSession, args);
}
//省略部分原始碼
}
在動態代理返回了範例後,我們就可以直接呼叫mapper類中的方法了,但代理物件呼叫方法,執行是在MapperProxy中的invoke方法中MapperProxy,該類實現InvocationHandler介面,並重寫invoke()方法。
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
// 如果是 Object 定義的方法,直接呼叫
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
} else if (isDefaultMethod(method)) {
return invokeDefaultMethod(proxy, method, args);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
// 獲得 MapperMethod 物件
final MapperMethod mapperMethod = cachedMapperMethod(method);
// 重點在這:MapperMethod最終呼叫了執行的方法
return mapperMethod.execute(sqlSession, args);
}
MapperMethod
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
//判斷mapper中的方法型別,最終呼叫的還是SqlSession中的方法
switch (command.getType()) {
case INSERT: {
// 轉換引數
Object param = method.convertArgsToSqlCommandParam(args);
// 執行 INSERT 操作
// 轉換 rowCount
result = rowCountResult(sqlSession.insert(command.getName(), param));
break;
}
case UPDATE: {
// 轉換引數
Object param = method.convertArgsToSqlCommandParam(args);
// 轉換 rowCount
result = rowCountResult(sqlSession.update(command.getName(), param));
break;
}
case DELETE: {
// 轉換引數
Object param = method.convertArgsToSqlCommandParam(args);
// 轉換 rowCount
result = rowCountResult(sqlSession.delete(command.getName(), param));
break;
}
case SELECT:
// 無返回,並且有 ResultHandler 方法引數,則將查詢的結果,提交給 ResultHandler 進行處理
if (method.returnsVoid() && method.hasResultHandler()) {
executeWithResultHandler(sqlSession, args);
result = null;
// 執行查詢,返回列表
} else if (method.returnsMany()) {
result = executeForMany(sqlSession, args);
// 執行查詢,返回 Map
} else if (method.returnsMap()) {
result = executeForMap(sqlSession, args);
// 執行查詢,返回 Cursor
} else if (method.returnsCursor()) {
result = executeForCursor(sqlSession, args);
// 執行查詢,返回單個物件
} else {
// 轉換引數
Object param = method.convertArgsToSqlCommandParam(args);
// 查詢單條
result = sqlSession.selectOne(command.getName(), param);
if (method.returnsOptional() &&
(result == null || !method.getReturnType().equals(result.getClass()))) {
result = Optional.ofNullable(result);
}
}
break;
case FLUSH:
result = sqlSession.flushStatements();
break;
default:
throw new BindingException("Unknown execution method for: " + command.getName());
}
// 返回結果為 null ,並且返回型別為基本型別,則丟擲 BindingException 異常
if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
throw new BindingException("Mapper method '" + command.getName()
+ " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
}
// 返回結果
return result;
}