Configuration存儲著mybatis運行時所需要的全部配置信息,那么它是如何從mybatis-config.xml轉換過來的呢?實際運行中它又起到什么作用呢?今天我通過一個小例子,結合源碼一步一步探索一下Configuration的解析流程,以便更加深入的了解其運行機制。
# 從Demo開始
下面是一個小小的示例。指定了配置文件mybatis-config.xml,通過SqlSessionFactoryBuilder創建SqlSessionFactory,打開SqlSession獲取Mapper,執行Mapper方法。
~~~java
public static void main(String[] args) throws IOException {
// mybatis配置文件
String path = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(path);
// 獲取 SqlSessionFactory
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
// 創建 SqlSession
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
CompanyDao dao = sqlSession.getMapper(CompanyDao.class);
CompanyDO companyDO = dao.selectById(1, "test");
System.out.println(companyDO);
}
}
復制代碼
~~~
從main方法中我們并沒有看到Configuration的影子,是因為在實際運行中,操作數據庫是使用SqlSession,而SqlSession是由SqlSessionFactory按需創建的,Configuration對象保存在SqlSessionFactory中,當創建SqlSession時會把Configuration傳遞到SqlSession。
# 配置解析流程
示例代碼3-6行就完成了Configuration的解析工作,這個過程發生在方法SqlSessionFactoryBuilder#build()中,進入方法內部會發現實際執行xml解析的是類XMLConfigBuilder。
~~~java
public SqlSessionFactory build(InputStream inputStream) {
// 調用重載方法
return build(inputStream, null, null);
}
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
try {
// 創建XMLConfigBuilder對象,這個是Configuration解析類
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
// 首先把xml解析為Configuration對象,然后創建SqlSessionFactory對象。
// 我們下來主要關心parse()方法
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中XMLConfigBuilder負責解析mybatis-config.xml,實現配置信息從文件到內存對象的轉換與映射。首先看下構造方法。
~~~java
// 接收配置文件流對象、environment以及通過代碼傳入的屬性配置。
public XMLConfigBuilder(InputStream inputStream, String environment, Properties props) {
this(new XPathParser(inputStream, true, props, new XMLMapperEntityResolver()), environment, props);
}
private XMLConfigBuilder(XPathParser parser, String environment, Properties props) {
super(new Configuration());
ErrorContext.instance().resource("SQL Mapper Configuration");
this.configuration.setVariables(props);
this.parsed = false;
this.environment = environment;
this.parser = parser;
}
復制代碼
~~~
XMLConfigBuilder構造方法創建了xml的解析器parser,parser負責處理復雜的xml讀取工作。另外,構造方法還接收了props對象,它會覆蓋從配置文件中解析的同名屬性信息。
parse()方法及parseConfiguration()是解析流程的主干流程,其中,parseConfiguration中逐個完成了xml中各部分的解析,每種配置封裝為了獨立的方法,理解起來比較容易。每個XMLConfigBuilder只能解析一次,重復解析會導致異常。
~~~java
public Configuration parse() {
//只能解析一次,否則異常
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
// 標記為已解析
parsed = true;
//解析configuration節點下的內容
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
private void parseConfiguration(XNode root) {
try {
//issue #117 read properties first
//解析屬性
propertiesElement(root.evalNode("properties"));
//解析配置
Properties settings = settingsAsProperties(root.evalNode("settings"));
//加載vfs
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);
// read it after objectFactory and objectWrapperFactory issue #631
//加載環境配置
environmentsElement(root.evalNode("environments"));
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
//加載類型處理器
typeHandlerElement(root.evalNode("typeHandlers"));
//加載mapper
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
復制代碼
~~~
以上每部分配置的加載方法,其內部都會為configuration賦值,最終解析完成所有配置,再由parse方法返回給調用方。下面逐個了解各部分mybatis配置的解析流程。
## 屬性(properties)
用于定義mybatis運行所需的屬性信息,如數據庫連接相關配置。屬性的定義有三種方式:在mybatis-config.xml#properties中定義屬性、通過外部properties文件定義,然后使用resource方式引入mybatis-config.xml、通過 SqlSessionFactoryBuilder.build() 方法中傳入屬性值。
~~~java
// 入參為root.evalNode("properties"),即properties節點下所有子節點
private void propertiesElement(XNode context) throws Exception {
if (context != null) {
//讀取所有子節點中配置的屬性信息,所有信息存儲defaults
Properties defaults = context.getChildrenAsProperties();
//若以resource或url方式導入外部配置,則加載resource或url中配置信息
String resource = context.getStringAttribute("resource");
String url = context.getStringAttribute("url");
if (resource != null && url != null) {
throw new BuilderException("The properties element cannot specify both a URL and a resource based property file reference. Please specify one or the other.");
}
//外部配置寫入defaults,這里需要注意:外部配置會覆蓋上面從properties中讀取的配置信息
if (resource != null) {
defaults.putAll(Resources.getResourceAsProperties(resource));
} else if (url != null) {
defaults.putAll(Resources.getUrlAsProperties(url));
}
//從configuration讀取以代碼方式傳入的配置信息,如果有也寫入defaults,
//此時也會覆蓋前兩步加載的同名配置
Properties vars = configuration.getVariables();
if (vars != null) {
defaults.putAll(vars);
}
//屬性解析完成,存儲。
parser.setVariables(defaults);
configuration.setVariables(defaults);
}
}
復制代碼
~~~
從這個方法的執行流程可知,如果同一個屬性在不同的位置進行重復配置,那么MyBatis 將按照下面的順序來加載:
* 首先讀取在 properties 元素體內指定的屬性。
* 然后根據 properties 元素中的 resource 屬性讀取類路徑下屬性文件,或根據 url 屬性指定的路徑讀取屬性文件,并覆蓋之前讀取過的同名屬性。
* 最后讀取作為方法參數傳遞的屬性,并覆蓋之前讀取過的同名屬性。
## 設置(settings)
### 設置項
這是 MyBatis 中極為重要的調整設置,它們會改變 MyBatis 的運行時行為。通過以下代碼+注釋解釋各個setting及其用途。其中,大部分setting都擁有默認值,實際可按需更改。
~~~java
public class Configuration {
//環境配置
protected Environment environment;
//是否允許在嵌套語句中使用分頁(RowBounds),默認false
protected boolean safeRowBoundsEnabled;
//是否允許在嵌套語句中使用結果處理器(ResultHandler),默認true
protected boolean safeResultHandlerEnabled = true;
//是否開啟駝峰命名自動映射,即從經典數據庫列名 A_COLUMN 映射到經典 Java 屬性名 aColumn。
protected boolean mapUnderscoreToCamelCase;
//開啟時,任一方法的調用都會加載該對象的所有延遲加載屬性。 否則,每個延遲加載屬性會按需加載。
protected boolean aggressiveLazyLoading;
//是否允許單個語句返回多結果集(需要數據庫驅動支持),默認值true
protected boolean multipleResultSetsEnabled = true;
//允許 JDBC 支持自動生成主鍵,需要數據庫驅動支持。如果設置為 true,將強制使用自動生成主鍵。
// 盡管一些數據庫驅動不支持此特性,但仍可正常工作(如 Derby)。
protected boolean useGeneratedKeys;
//使用列標簽代替列名。實際表現依賴于數據庫驅動,具體可參考數據庫驅動的相關文檔,或通過對比測試來觀察。
protected boolean useColumnLabel = true;
//全局性地開啟或關閉所有映射器配置文件中已配置的任何緩存,默認true
protected boolean cacheEnabled = true;
//指定當結果集中值為 null 的時候是否調用映射對象的 setter(map 對象時為 put)方法,這在依賴于 Map.keySet() 或 null 值進行初始化時比較有用。
// 注意基本類型(int、boolean 等)是不能設置成 null 的。
// 默認false
protected boolean callSettersOnNulls;
//允許使用方法簽名中的名稱作為語句參數名稱。
//為了使用該特性,你的項目必須采用 Java 8 編譯,并且加上 -parameters 選項。
//默認值true
protected boolean useActualParamName = true;
//當返回行的所有列都是空時,MyBatis默認返回 null。 當開啟這個設置時,MyBatis會返回一個空實例。
// 請注意,它也適用于嵌套的結果集(如集合或關聯),默認false
protected boolean returnInstanceForEmptyRow;
//指定 MyBatis 增加到日志名稱的前綴。默認無
protected String logPrefix;
//指定 MyBatis 所用日志的具體實現,未指定時將自動查找。
//SLF4J | LOG4J | LOG4J2 | JDK_LOGGING | COMMONS_LOGGING | STDOUT_LOGGING | NO_LOGGING
//默認無
protected Class <? extends Log> logImpl;
//指定 VFS 的實現
protected Class <? extends VFS> vfsImpl;
//MyBatis 利用本地緩存機制(Local Cache)防止循環引用和加速重復的嵌套查詢。
// 默認值為 SESSION,會緩存一個會話中執行的所有查詢。 若設置值為 STATEMENT,本地緩存將僅用于執行語句,對相同 SqlSession 的不同查詢將不會進行緩存。
// 可選值:SESSION | STATEMENT
protected LocalCacheScope localCacheScope = LocalCacheScope.SESSION;
//當沒有為參數指定特定的 JDBC 類型時,空值的默認 JDBC 類型。
//某些數據庫驅動需要指定列的 JDBC 類型,多數情況直接用一般類型即可,比如 NULL、VARCHAR 或 OTHER。
//默認值:OTHER
protected JdbcType jdbcTypeForNull = JdbcType.OTHER;
//指定對象的哪些方法觸發一次延遲加載。用逗號分隔的方法列表。
//例如:equals,clone,hashCode,toString
protected Set<String> lazyLoadTriggerMethods = new HashSet<>(Arrays.asList("equals", "clone", "hashCode", "toString"));
//設置超時時間,它決定數據庫驅動等待數據庫響應的秒數。默認未設置。
protected Integer defaultStatementTimeout;
//為驅動的結果集獲取數量(fetchSize)設置一個建議值。此參數只可以在查詢設置中被覆蓋。默認未設置
protected Integer defaultFetchSize;
//配置默認的執行器。SIMPLE 就是普通的執行器;REUSE 執行器會重用預處理語句(PreparedStatement); BATCH 執行器不僅重用語句還會執行批量更新。
//可選值:SIMPLE,REUSE,BATCH,默認值:SIMPLE
protected ExecutorType defaultExecutorType = ExecutorType.SIMPLE;
//指定 MyBatis 應如何自動映射列到字段或屬性。
// NONE 表示關閉自動映射;
// PARTIAL 只會自動映射沒有定義嵌套結果映射的字段。
// FULL 會自動映射任何復雜的結果集(無論是否嵌套)。
//默認值:PARTIAL
protected AutoMappingBehavior autoMappingBehavior = AutoMappingBehavior.PARTIAL;
//指定發現自動映射目標未知列(或未知屬性類型)的行為。
//NONE: 不做任何反應
//WARNING: 輸出警告日志('org.apache.ibatis.session.AutoMappingUnknownColumnBehavior' 的日志等級必須設置為 WARN)
//FAILING: 映射失敗 (拋出 SqlSessionException)
//默認值:NONE
protected AutoMappingUnknownColumnBehavior autoMappingUnknownColumnBehavior = AutoMappingUnknownColumnBehavior.NONE;
// 屬性列表
protected Properties variables = new Properties();
protected ReflectorFactory reflectorFactory = new DefaultReflectorFactory();
//每次 MyBatis 創建結果對象的新實例時,它都會使用一個對象工廠(ObjectFactory)實例來完成實例化工作。
//默認的對象工廠需要做的僅僅是實例化目標類,要么通過默認無參構造方法,要么通過存在的參數映射來調用帶有參數的構造方法。
//如果想覆蓋對象工廠的默認行為,可以通過創建自己的對象工廠來實現。
protected ObjectFactory objectFactory = new DefaultObjectFactory();
protected ObjectWrapperFactory objectWrapperFactory = new DefaultObjectWrapperFactory();
//延遲加載的全局開關。當開啟時,所有關聯對象都會延遲加載。 特定關聯關系中可通過設置 fetchType 屬性來覆蓋該項的開關狀態。
//默認值:false
protected boolean lazyLoadingEnabled = false;
protected ProxyFactory proxyFactory = new JavassistProxyFactory(); // #224 Using internal Javassist instead of OGNL
protected String databaseId;
/**
* Configuration factory class.
* Used to create Configuration for loading deserialized unread properties.
*
* @see <a href='https://code.google.com/p/mybatis/issues/detail?id=300'>Issue 300 (google code)</a>
*/
protected Class<?> configurationFactory;
//語言驅動注冊中心
protected final LanguageDriverRegistry languageRegistry = new LanguageDriverRegistry();
//存儲所有Mapper中的映射聲明,k-v結構
protected final Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>("Mapped Statements collection")
.conflictMessageProducer((savedValue, targetValue) ->
". please check " + savedValue.getResource() + " and " + targetValue.getResource());
//數據緩存,k-v結構
protected final Map<String, Cache> caches = new StrictMap<>("Caches collection");
//存儲所有結果映射,來自Mapper
protected final Map<String, ResultMap> resultMaps = new StrictMap<>("Result Maps collection");
//存儲所有參數迎神,來自Mapper
protected final Map<String, ParameterMap> parameterMaps = new StrictMap<>("Parameter Maps collection");
protected final Map<String, KeyGenerator> keyGenerators = new StrictMap<>("Key Generators collection");
//存儲已加載的資源文件,如mapper.xml
protected final Set<String> loadedResources = new HashSet<>();
//存儲sql片段
protected final Map<String, XNode> sqlFragments = new StrictMap<>("XML fragments parsed from previous mappers");
//存儲未解析完成的聲明
protected final Collection<XMLStatementBuilder> incompleteStatements = new LinkedList<>();
//存儲未解析完成的緩存解析
protected final Collection<CacheRefResolver> incompleteCacheRefs = new LinkedList<>();
//存儲未解析完成的結果映射
protected final Collection<ResultMapResolver> incompleteResultMaps = new LinkedList<>();
//存儲未解析完成的方法
protected final Collection<MethodResolver> incompleteMethods = new LinkedList<>();
//……
}
復制代碼
~~~
### 加載流程
mybatis提供的配置項很多,為了方便使用,都提供了默認值。多數情況下,我們只需要按需更改即可。settingsAsProperties用于完成setting的配置解析工作:
~~~java
private Properties settingsAsProperties(XNode context) {
//節點為空,返回空的屬性對象
if (context == null) {
return new Properties();
}
//讀取所有設置信息,存入props
Properties props = context.getChildrenAsProperties();
// Check that all settings are known to the configuration class
// 通過反射方式檢查props加載的配置信息是否為已知的配置項,如果存在未知配置項,拋出異常。
MetaClass metaConfig = MetaClass.forClass(Configuration.class, localReflectorFactory);
for (Object key : props.keySet()) {
if (!metaConfig.hasSetter(String.valueOf(key))) {
throw new BuilderException("The setting " + key + " is not known. Make sure you spelled it correctly (case sensitive).");
}
}
return props;
}
復制代碼
~~~
設置加載過程中采用了反射方式檢查配置文件中配置項的合法性,存在未知配置項會導致異常。另外,這個方法把解析到的配置項返回,并沒有直接為configuration設置,而是通過后面的settingsElement方法進行初始化。因為還要繼續加載vfs、logImpl的配置信息。
## 類型別名
類型別名可為 Java 類型設置一個縮寫名字。 它僅用于 XML 配置,意在降低冗余的全限定類名書寫。例如:
~~~java
<typeAliases>
<typeAlias type="com.raysonxin.dataobject.CompanyDO" alias="CompanyDO"/>
</typeAliases>
復制代碼
~~~
別名降低了使用的復雜度,它在類型查找、映射方面起到很大的作用,typeAliasesElement負責完成別名的解析加載工作:
~~~java
private void typeAliasesElement(XNode parent) {
if (parent != null) {
// 逐個遍歷
for (XNode child : parent.getChildren()) {
//是否以包名方式配置別名
if ("package".equals(child.getName())) {
// 獲取包名
String typeAliasPackage = child.getStringAttribute("name");
//按照包名獲取包下所有類,然后注冊別名:若有注解,則使用注解;否則使用類名首字母小寫作為別名。
configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage);
} else {//這是按照類名配置別名的方式
//獲取別名
String alias = child.getStringAttribute("alias");
//獲取類型:全限定類名
String type = child.getStringAttribute("type");
try {
//獲取類型信息
Class<?> clazz = Resources.classForName(type);
//別名為空,默認類名首字母小寫;不是空,按照alias注冊
if (alias == null) {
typeAliasRegistry.registerAlias(clazz);
} else {
typeAliasRegistry.registerAlias(alias, clazz);
}
} catch (ClassNotFoundException e) {
throw new BuilderException("Error registering typeAlias for '" + alias + "'. Cause: " + e, e);
}
}
}
}
}
復制代碼
~~~
從代碼可知,mybatis支持兩種別名的注冊方式:
* 按照包名注冊:按照指定的包掃描類,若存在別名注解,則使用注解指定的名稱;否則使用類的首字母小寫默認;
* 按照類全限定名稱注冊:指定了別名則使用其作為別名,否則使用類的首字母小寫默認;
**另外需要注意的是,這是mybatis提供的自定義別名的方式,其實mybatis已經提供了絕大多數常用的別名,具體在Configuration的構造方法中,大家可以自行查看。**
## 插件(plugins)
MyBatis 允許我們在映射語句執行過程中的某一點進行攔截調用,這可以很方便的讓我們擴展mybatis的能力。默認情況下,MyBatis 允許使用插件來攔截的方法調用包括:
* Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
* ParameterHandler (getParameterObject, setParameters)
* ResultSetHandler (handleResultSets, handleOutputParameters)
* StatementHandler (prepare, parameterize, batch, update, query)
這些類中方法的細節可以通過查看每個方法的簽名來發現,或者直接查看 MyBatis 發行包中的源代碼。先看下接口Interceptor的定義:
~~~java
public interface Interceptor {
//攔截接口,這里面實現攔截處理邏輯
Object intercept(Invocation invocation) throws Throwable;
//為上述目標增加攔截,增強
Object plugin(Object target);
//設置插件屬性
void setProperties(Properties properties);
}
復制代碼
~~~
### 自定義插件
自定義插件只需實現 Interceptor 接口,并指定想要攔截的方法簽名即可,這里的接口方法和簽名必須與Mybatis源碼中的定義完全一致,下面通過一個例子進行說明。
~~~java
@Intercepts(
@Signature(
type = Executor.class,
method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
)
)
public class CustomInterceptor implements Interceptor {
Properties properties = new Properties();
@Override
public Object intercept(Invocation invocation) throws Throwable {
System.out.println("before CustomInterceptor");
Object object = invocation.proceed();
System.out.println("after CustomInterceptor");
return object;
}
@Override
public Object plugin(Object target) {
return target instanceof Executor ? Plugin.wrap(target, this) : target;
}
@Override
public void setProperties(Properties properties) {
this.properties = properties;
}
}
復制代碼
~~~
CustomInterceptor對Executor#query方法進行了攔截,在query前后輸出一些埋點信息(可以替換為自己的業務邏輯);plugin方法實現邏輯是:當目標組件為Executor時使用Plugin.wrap方法會為其增加插件功能。接下來在mybatis-config.xml中進行配置:
~~~xml
<plugins>
<plugin interceptor="com.raysonxin.plugin.CustomInterceptor">
<property name="name" value="abcd"/>
</plugin>
</plugins>
復制代碼
~~~
看下運行效果:
****
### 插件加載與執行流程
直接上代碼:org.apache.ibatis.builder.xml.XMLConfigBuilder#pluginElement
~~~java
//入參為root.evalNode("plugins")節點
private void pluginElement(XNode parent) throws Exception {
//節點為空不處理
if (parent != null) {
//依次遍歷子節點
for (XNode child : parent.getChildren()) {
//獲取插件名稱
String interceptor = child.getStringAttribute("interceptor");
//獲取插件屬性列表
Properties properties = child.getChildrenAsProperties();
//實例化插件對象
Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();
//設置插件屬性字段
interceptorInstance.setProperties(properties);
//添加插件信息到configurtation
configuration.addInterceptor(interceptorInstance);
}
}
}
復制代碼
~~~
插件加載流程比較簡單:獲取名稱,獲取屬性,實例化插件,設置插件屬性,最終添加到configuration。XMLConfigBuilder完成插件加載后把插件保存在Configuration#interceptorChain,那實際運行中是如何應用的呢?
如前文所說,mybatis支持在Executor、StatementHandler、ParameterHandler、ResultSetHandler四大組件的若干方法上通過插件增強,mybatis正是在四大組件的創建過程中采用動態代理的方式應用插件的。四大組件的創建方法在Configuration中,如下所示,以newExecutor為例對插件應用過程進行梳理。
~~~java
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
executorType = executorType == null ? defaultExecutorType : executorType;
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
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);
}
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
//添加插件邏輯,采用動態代理進行包裝
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
復制代碼
~~~
組件對象創建完成后通過interceptorChain.pluginAll()方法依次應用插件,四個方法的邏輯類似。interceptor.plugin()由插件實現,它會調用Plugin#wrap方法進行增強。看下這一段的源碼邏輯:
~~~java
public Object pluginAll(Object target) {
//循環依次遍歷插件
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}
private Plugin(Object target, Interceptor interceptor, Map<Class<?>, Set<Method>> signatureMap) {
this.target = target;
this.interceptor = interceptor;
this.signatureMap = signatureMap;
}
public static Object wrap(Object target, Interceptor interceptor) {
//獲取插件類的簽名:就是確認插件為哪個方法進行增強
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
Class<?> type = target.getClass();
//獲取插件類實現的所有接口
Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
if (interfaces.length > 0) {
//采用動態代理創建代理類實現,在我們的例子中相當于為Executor對象進行了增強。
return Proxy.newProxyInstance(
type.getClassLoader(),
interfaces,
new Plugin(target, interceptor, signatureMap));
}
return target;
}
復制代碼
~~~
所以,經過Plugin#wrap包裝后的Executor對象其實是一個代理對象,其中包含了Executor原有的邏輯,按照動態代理的原理,我們看下Plugin#invoke的執行邏輯:
~~~java
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
//從緩存中查詢緩存的方法
Set<Method> methods = signatureMap.get(method.getDeclaringClass());
//判斷當前方式是否為插件增強的方法
if (methods != null && methods.contains(method)) {
//如果是則執行插件的intercept方法,內部會調用原有的method,入口是:Invocation#proceed
return interceptor.intercept(new Invocation(target, method, args));
}
//未被增強,直接調用原來的方法
return method.invoke(target, args);
} catch (Exception e) {
throw ExceptionUtil.unwrapThrowable(e);
}
}
復制代碼
~~~
編寫插件需要注意對原有mybatis的影響,防止破壞 MyBatis 的核心模塊。通過 MyBatis 提供的強大機制,使用插件是非常簡單的,只需實現 Interceptor 接口,并指定想要攔截的方法簽名即可。
## 類型處理器(typeHandler)
MyBatis 在設置預處理語句(PreparedStatement)中的參數或從結果集中取出一個值時, 都會用類型處理器將獲取到的值以合適的方式轉換成 Java 類型。通過TypeHandler接口定義也可以清晰的看到TypeHandler的作用:
~~~java
public interface TypeHandler<T> {
void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException;
T getResult(ResultSet rs, String columnName) throws SQLException;
T getResult(ResultSet rs, int columnIndex) throws SQLException;
T getResult(CallableStatement cs, int columnIndex) throws SQLException;
}
復制代碼
~~~
### 自定義類處理器
如果默認的類型處理器不能滿足使用需求,可以按照要求自定義類型處理器。 具體做法為:實現 org.apache.ibatis.type.TypeHandler 接口, 或繼承類 org.apache.ibatis.type.BaseTypeHandler, 并且可以(可選地)將它映射到一個 JDBC 類型。
以第二種方式舉例,自定義類型處理器CustomTypeHandler,該處理器對應的java類型是String,jdbc類型是VARCHAR,includeNullJdbcType為true。
~~~java
@MappedJdbcTypes(value = JdbcType.VARCHAR,includeNullJdbcType = true)
public class CustomTypeHandler extends BaseTypeHandler<String> {
@Override
public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException {
ps.setString(i, parameter);
}
@Override
public String getNullableResult(ResultSet rs, String columnName) throws SQLException {
return rs.getString(columnName);
}
@Override
public String getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
return rs.getString(columnIndex);
}
@Override
public String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
return cs.getString(columnIndex);
}
}
復制代碼
~~~
然后在mybatis-config.xml中添加如下配置:
~~~xml
<typeHandlers>
<typeHandler handler="com.raysonxin.typehandler.CustomTypeHandler"/>
</typeHandlers>
復制代碼
~~~
### 加載流程
類型處理器的注冊過程比較復雜,原因是代碼中包含了許多個重載方法,看上去暈頭轉向,我們先來認識一下TypeHandlerRegistry是如何設計的,直接看代碼。
~~~java
public final class TypeHandlerRegistry {
//這個字段存儲了jdbcType到類型處理器的映射,使用EnumMap
private final Map<JdbcType, TypeHandler<?>> JDBC_TYPE_HANDLER_MAP = new EnumMap<>(JdbcType.class);
//存儲javaType對應的處理器映射,需要根據jdbcType選擇處理器
private final Map<Type, Map<JdbcType, TypeHandler<?>>> TYPE_HANDLER_MAP = new ConcurrentHashMap<>();
//未知類型處理器,內部會進一步解析處理器類型并路由
private final TypeHandler<Object> UNKNOWN_TYPE_HANDLER = new UnknownTypeHandler(this);
//存儲所有的類型與類型處理器的映射,jdbcType或者javaType
private final Map<Class<?>, TypeHandler<?>> ALL_TYPE_HANDLERS_MAP = new HashMap<>();
//NULL處理器
private static final Map<JdbcType, TypeHandler<?>> NULL_TYPE_HANDLER_MAP = Collections.emptyMap();
//默認枚舉類型處理器
private Class<? extends TypeHandler> defaultEnumTypeHandler = EnumTypeHandler.class;
//構造方法內默認注冊了常用的類型處理器
public TypeHandlerRegistry() {
register(Boolean.class, new BooleanTypeHandler());
register(boolean.class, new BooleanTypeHandler());
register(JdbcType.BOOLEAN, new BooleanTypeHandler());
register(JdbcType.BIT, new BooleanTypeHandler());
//....
}
//...
}
復制代碼
~~~
TypeHandlerRegistry內部維護了java類型、jdbc類型與類型處理器之間的關系,分別從不同的角度進行映射:
* JdbcType對應的處理器(JDBC\_TYPE\_HANDLER\_MAP):意思是當處理一個jdbc類型時,可以從這里獲取處理器;
* javaType對應的處理器(TYPE\_HANDLER\_MAP):這個字段使用了兩層映射。第一層是javaType,代表了該javaType具有的處理器列表;第二層是jdbcType與處理器之間的對應關系。綜合下來,確認一個處理器需要進行最終確認處理器。
* 全類型映射(ALL\_TYPE\_HANDLERS\_MAP):維護了jdbc類型和java類型所對應的所有映射器。
* 空類型處理器和默認枚舉類型處理器。
~~~java
//入參為root.evalNode("typeHandlers")節點
private void typeHandlerElement(XNode parent) {
if (parent != null) {
//遍歷節點的子節點
for (XNode child : parent.getChildren()) {
if ("package".equals(child.getName())) {
String typeHandlerPackage = child.getStringAttribute("name");
typeHandlerRegistry.register(typeHandlerPackage);
} else {
//單個類型處理器注冊的配置方式
//獲取javaType,這個是類型的別名,應該注冊在typeAliasRegistry
String javaTypeName = child.getStringAttribute("javaType");
//獲取jdbcType
String jdbcTypeName = child.getStringAttribute("jdbcType");
//獲取handler名稱,這是類的全限定名稱
String handlerTypeName = child.getStringAttribute("handler");
//通過javaType獲取其對應的類型,可能為空;若指定了但是沒有找到,會報異常
Class<?> javaTypeClass = resolveClass(javaTypeName);
//獲取JdbcType,枚舉類型
JdbcType jdbcType = resolveJdbcType(jdbcTypeName);
//獲取處理器對應的類型
Class<?> typeHandlerClass = resolveClass(handlerTypeName);
if (javaTypeClass != null) {
if (jdbcType == null) {
//Case-1:按照java類型、處理器類型注冊
//a、根據typeHandlerClass創建處理器對象typeHandler;
//b、獲取typeHandler注解中的jdbcTypes,可能為空
//c、jdbcTypes!=null:注冊javaType-jdbcType-handler,如果注解中includeNullJdbcType=true,注冊空類型;
// jdbcTypes==null:僅注冊javaType-handler;
typeHandlerRegistry.register(javaTypeClass, typeHandlerClass);
} else {
//Case-2:按照java類型、jdbc類型、處理器類型注冊
//a、創建typeHandler對象;
//b、注冊關聯javaType-jdbcType-handler
typeHandlerRegistry.register(javaTypeClass, jdbcType, typeHandlerClass);
}
} else {
//Case-3:按照處理器類型注冊
//a、獲取typeHandlerClass的MappedTypes注解;
//b、javaType不為空:按照javatype注冊,內部會獲取MappedJdbcTypes,關聯三者。
//c、這種情況還沒看明白。。。
typeHandlerRegistry.register(typeHandlerClass);
}
}
}
}
}
//Case-1~3,最終都會走到這里
private void register(Type javaType, JdbcType jdbcType, TypeHandler<?> handler) {
if (javaType != null) {
//從map中查詢已有的javaType對應的處理器
Map<JdbcType, TypeHandler<?>> map = TYPE_HANDLER_MAP.get(javaType);
//不存在或者為空,執行初始化與添加操作
if (map == null || map == NULL_TYPE_HANDLER_MAP) {
map = new HashMap<>();
TYPE_HANDLER_MAP.put(javaType, map);
}
//關聯javaType-jdbcType-handler
map.put(jdbcType, handler);
}
//加入總的映射
ALL_TYPE_HANDLERS_MAP.put(handler.getClass(), handler);
}
復制代碼
~~~
通過代碼可以發現,JDBC\_TYPE\_HANDLER\_MAP僅僅是在構造方法邏輯中進行了添加,相當于是mybatis默認的處理器。我們自定義的處理器會添加到TYPE\_HANDLER\_MAP、ALL\_TYPE\_HANDLERS\_MAP,雖然有不同的注冊方式,但是殊途同歸,都是通過上述方法實現了注冊。
總結一下:
* 通過配置文件自定義的類型處理器,會添加到TYPE\_HANDLER\_MAP、ALL\_TYPE\_HANDLERS\_MAP,而獲取類型處理器是優先使用javaType來查詢的。如果自定義的類型處理器了重復定義了javaType或jdbcType,會對系統默認的處理器產生覆蓋。
* 如果在類型處理器的注解和xml配置中同時聲明了javaType,那么xml中配置的javaType會生效。
## 映射器(mapper)
MyBatis 的真正強大在于它的語句映射,這是它的魔力所在。由于它的異常強大,映射器的 XML 文件就顯得相對簡單。如果拿它跟具有相同功能的 JDBC 代碼進行對比,你會立即發現省掉了將近 95% 的代碼。MyBatis 致力于減少使用成本,讓用戶能更專注于 SQL 代碼。——*摘自《XML 映射器》*
映射器是mybatis面向接口編程的利器,它使用動態代理技術,實現了接口與mapper中sql語句的動態綁定,免去了JDBC中手動實現執行Statement的繁雜過程。mybatis提供多種方式支持mapper資源查找:可以使用相對于類路徑的資源引用,或完全限定資源定位符(包括 file:/// 形式的 URL),或類名和包名等。
### 映射器元素
SQL 映射文件只有很少的幾個頂級元素(按照應被定義的順序列出):
* cache?– 該命名空間的緩存配置。
* cache-ref?– 引用其它命名空間的緩存配置。
* resultMap?– 描述如何從數據庫結果集中加載對象,是最復雜也是最強大的元素。
* ~parameterMap?– 老式風格的參數映射。此元素已被廢棄,并可能在將來被移除!請使用行內參數映射。文檔中不會介紹此元素。~
* sql?– 可被其它語句引用的可重用語句塊。
* insert?– 映射插入語句。
* update?– 映射更新語句。
* delete?– 映射刪除語句。
* select?– 映射查詢語句
以上頂級元素還有較多的屬性用于控制其行為,大家可以自行查閱官方文檔,這里不再貼出來了。
### 加載流程
我們隨著示例及源碼來看下mybatis是如何加載解析mapper的,然后通過另外一篇文章來剖析其執行過程。示例配置信息,示例中定義了CompanyMapper.xml,以資源方式引入到mybatis-config.xml。
~~~xml
<!--mybatis-config.xml-->
<mappers>
<mapper resource="mapper/CompanyMapper.xml"/>
</mappers>
<!--CompanyMapper.xml-->
<mapper namespace="com.raysonxin.dao.CompanyDao">
<resultMap id="baseResultMap" type="com.raysonxin.dataobject.CompanyDO">
<id column="id" property="id"/>
<result column="name" property="name"/>
<result column="cpy_type" property="cpyType"/>
</resultMap>
<sql id="BaseColumns">
id,name,cpy_type
</sql>
<select id="selectById" resultMap="baseResultMap">
select
<include refid="BaseColumns"></include>
from company
where id= #{id} and name = #{name}
</select>
</mapper>
復制代碼
~~~
mapper解析入口為XMLConfigBuilder#mapperElement,通過代碼注釋走下流程。
~~~java
//入參為root.evalNode("mappers"),即示例中的mappers節點,它可包含多個mapper子節點
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
// 整包方式添加mapper
if ("package".equals(child.getName())) {
String mapperPackage = child.getStringAttribute("name");
configuration.addMappers(mapperPackage);
} else {
//這里包含了三種方式:資源導入、資源全限定路徑、類名,分別獲取mapper節點屬性值
//三個值中只能配置一個,否則會報異常(最后的else)
String resource = child.getStringAttribute("resource");
String url = child.getStringAttribute("url");
String mapperClass = child.getStringAttribute("class");
// 資源方式
if (resource != null && url == null && mapperClass == null) {
ErrorContext.instance().resource(resource);
InputStream inputStream = Resources.getResourceAsStream(resource);
//使用XMLMapperBuilder類進行mapper解析,這里是重點。
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類進行mapper解析,同上。
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.");
}
}
}
}
}
復制代碼
~~~
示例代碼以resource方式引入mapper,按照這條線走下去,創建XMLMapperBuilder后調用其parse方法。這里與XMLConfigBuilder類似,同樣使用XPathParser進行xml解析,另外使用MapperBuilderAssistant構建MappedStatement。
~~~java
private XMLMapperBuilder(XPathParser parser, Configuration configuration, String resource, Map<String, XNode> sqlFragments) {
super(configuration);
this.builderAssistant = new MapperBuilderAssistant(configuration, resource);
this.parser = parser;
this.sqlFragments = sqlFragments;
this.resource = resource;
}
復制代碼
~~~
接下來進入解析流程,即XMLMapperBuilder#parse方法。大體流程是:若資源未被加載過,則解析mapper文件中的元素,把mapper添加到已加載資源,綁定mapper與namespace。無論是否加載過當前資源,都會處理那些之前未完成的ResultMap、CacheRef、Statement。
~~~java
public void parse() {
//判斷資源是否已經加載
if (!configuration.isResourceLoaded(resource)) {
//這個是mapper解析的核心方法,入參是mapper對應的資源文件,示例中的CompanyMapper.xml
configurationElement(parser.evalNode("/mapper"));
//添加已加載資源到configuration
configuration.addLoadedResource(resource);
//這個方法是綁定mapper與namespace
bindMapperForNamespace();
}
parsePendingResultMaps();
parsePendingCacheRefs();
parsePendingStatements();
}
復制代碼
~~~
核心解析方式是configurationElement,它依次解析mapper文件中的cache-ref、cache、parameterMap、resultMap、sql、select|insert|update|delete,本次重點分析后面三者的解析過程。
~~~java
private void configurationElement(XNode context) {
try {
//獲取mapper中的namespace,示例中為:com.raysonxin.dao.CompanyDao
String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.equals("")) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
//設置builderAssistant的namespace
builderAssistant.setCurrentNamespace(namespace);
//解析cache-ref內容
cacheRefElement(context.evalNode("cache-ref"));
//解析cache內容
cacheElement(context.evalNode("cache"));
//解析parameterMap節點
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
//解析resultMap節點
resultMapElements(context.evalNodes("/mapper/resultMap"));
//解析sql節點
sqlElement(context.evalNodes("/mapper/sql"));
//構建statement
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);
}
}
復制代碼
~~~
#### resultMap
下可以定義多個resultMap,通常我們會把常用的返回字段定義為一個resultMap,方便在查詢語句中引用。我們從resultMapElements開始看下源碼:
~~~java
private void resultMapElements(List<XNode> list) throws Exception {
for (XNode resultMapNode : list) {
try {
resultMapElement(resultMapNode);
} catch (IncompleteElementException e) {
// ignore, it will be retried
}
}
}
private ResultMap resultMapElement(XNode resultMapNode) throws Exception {
return resultMapElement(resultMapNode, Collections.<ResultMapping> emptyList(), null);
}
//真正的解析工作從這里開始
private ResultMap resultMapElement(XNode resultMapNode, List<ResultMapping> additionalResultMappings, Class<?> enclosingType) throws Exception {
ErrorContext.instance().activity("processing " + resultMapNode.getValueBasedIdentifier());
//獲取resultMap的id屬性,示例中baseResultMap
String id = resultMapNode.getStringAttribute("id",
resultMapNode.getValueBasedIdentifier());
//獲取resultMap的type屬性,后面是默認值設置順序
String type = resultMapNode.getStringAttribute("type",
resultMapNode.getStringAttribute("ofType",
resultMapNode.getStringAttribute("resultType",
resultMapNode.getStringAttribute("javaType"))));
//獲取extends屬性
String extend = resultMapNode.getStringAttribute("extends");
//獲取autoMapping屬性
Boolean autoMapping = resultMapNode.getBooleanAttribute("autoMapping");
//獲取type對應的類型對象,首先從typeAliasRegistry獲取,否則按照全路徑限定名稱反射創建
Class<?> typeClass = resolveClass(type);
if (typeClass == null) {
typeClass = inheritEnclosingType(resultMapNode, enclosingType);
}
Discriminator discriminator = null;
List<ResultMapping> resultMappings = new ArrayList<>();
resultMappings.addAll(additionalResultMappings);
List<XNode> resultChildren = resultMapNode.getChildren();
//依次解析子節點
for (XNode resultChild : resultChildren) {
//是否為構造方法
if ("constructor".equals(resultChild.getName())) {
//處理構造方法
processConstructorElement(resultChild, typeClass, resultMappings);
}
//discriminator鑒別器
else if ("discriminator".equals(resultChild.getName())) {
discriminator = processDiscriminatorElement(resultChild, typeClass, resultMappings);
}
//其他節點
else {
List<ResultFlag> flags = new ArrayList<>();
// 是否為id節點
if ("id".equals(resultChild.getName())) {
flags.add(ResultFlag.ID);
}
//構建resultMap:buildResultMappingFromContext
resultMappings.add(buildResultMappingFromContext(resultChild, typeClass, flags));
}
}
ResultMapResolver resultMapResolver = new ResultMapResolver(builderAssistant, id, typeClass, extend, discriminator, resultMappings, autoMapping);
try {
return resultMapResolver.resolve();
} catch (IncompleteElementException e) {
configuration.addIncompleteResultMap(resultMapResolver);
throw e;
}
}
復制代碼
~~~
不考慮復雜的resultMap,我們示例中的節點類型為id和result兩種,我們直接進入方法buildResultMappingFromContext:
~~~java
private ResultMapping buildResultMappingFromContext(XNode context, Class<?> resultType, List<ResultFlag> flags) throws Exception {
String property;
if (flags.contains(ResultFlag.CONSTRUCTOR)) {
property = context.getStringAttribute("name");
} else {
//獲取property值,對應javaType的字段
property = context.getStringAttribute("property");
}
//數據表的字段
String column = context.getStringAttribute("column");
//獲取javaType
String javaType = context.getStringAttribute("javaType");
//獲取jdbcType
String jdbcType = context.getStringAttribute("jdbcType");
String nestedSelect = context.getStringAttribute("select");
String nestedResultMap = context.getStringAttribute("resultMap",
processNestedResultMappings(context, Collections.<ResultMapping> emptyList(), resultType));
String notNullColumn = context.getStringAttribute("notNullColumn");
String columnPrefix = context.getStringAttribute("columnPrefix");
//類型處理器
String typeHandler = context.getStringAttribute("typeHandler");
String resultSet = context.getStringAttribute("resultSet");
String foreignColumn = context.getStringAttribute("foreignColumn");
boolean lazy = "lazy".equals(context.getStringAttribute("fetchType", configuration.isLazyLoadingEnabled() ? "lazy" : "eager"));
//獲取javaType類型對象
Class<?> javaTypeClass = resolveClass(javaType);
//獲取類型處理器
Class<? extends TypeHandler<?>> typeHandlerClass = resolveClass(typeHandler);
//jdbc類型枚舉
JdbcType jdbcTypeEnum = resolveJdbcType(jdbcType);
//創建ResultMapping對象并返回
return builderAssistant.buildResultMapping(resultType, property, column, javaTypeClass, jdbcTypeEnum, nestedSelect, nestedResultMap, notNullColumn, columnPrefix, typeHandlerClass, flags, resultSet, foreignColumn, lazy);
}
復制代碼
~~~
這個解析過程包含了resultMap高級用法的邏輯,比如association、collections等,我們先從簡單、常用的方式入手。buildResultMappingFromContext最終返回了resultMap中某個字段的映射信息,構建了java類型與數據庫字段的映射關系。這樣,通過遍歷所有字段把resultMap下的所有字段一一映射。
我們再回到方法resultMapElement,遍歷所有節點后得到resultMappings,創建了ResultMapResolver對象。從名稱可以知道,這個類是ResultMap的解析器,它最終把resultMap配置信息轉為mybaits的ResultMap對象并添加到configuration。
#### sql
實際開發中,我們可以使用sql標簽定義那些可復用的sql片段,簡化mapper文件的配置。在mybatis解析sql時,它也只是一個中間過程,它是解析select|update|insert|delete語句的基礎。
~~~java
private void sqlElement(List<XNode> list) {
if (configuration.getDatabaseId() != null) {
sqlElement(list, configuration.getDatabaseId());
}
sqlElement(list, null);
}
private void sqlElement(List<XNode> list, String requiredDatabaseId) {
for (XNode context : list) {
String databaseId = context.getStringAttribute("databaseId");
String id = context.getStringAttribute("id");
//拼接namespace,結果為:namespace+"."+id
id = builderAssistant.applyCurrentNamespace(id, false);
//是否符合datbaseId要求
if (databaseIdMatchesCurrent(id, databaseId, requiredDatabaseId)) {
//sqlFragments保存所有sql片段
sqlFragments.put(id, context);
}
}
}
復制代碼
~~~
#### select|insert|update|delete
select|insert|update|delete對于sql語句中的同名命令,是我們使用最頻繁的部分,最終mybatis會把這些標簽轉為MappedStatement,存儲在configuration,同時與Mapper接口方法在運行時綁定。
~~~java
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) {
for (XNode context : list) {
final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
try {
statementParser.parseStatementNode();
} catch (IncompleteElementException e) {
configuration.addIncompleteStatement(statementParser);
}
}
}
復制代碼
~~~
從代碼可以看到XMLStatementBuilder,它是命令標簽的處理器,核心方法是parseStatementNode。到這里,我們已經見過了XMLConfigBuilder、XMLMapperBuilder。看下處理過程:
~~~java
public void parseStatementNode() {
//獲取標簽中的id屬性,如:selectById
String id = context.getStringAttribute("id");
//databaseId,為空
String databaseId = context.getStringAttribute("databaseId");
if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
return;
}
//屬性fetchSize
Integer fetchSize = context.getIntAttribute("fetchSize");
//屬性timeout
Integer timeout = context.getIntAttribute("timeout");
//屬性parameterMap
String parameterMap = context.getStringAttribute("parameterMap");
//屬性parameterType
String parameterType = context.getStringAttribute("parameterType");
//解析參數類型信息
Class<?> parameterTypeClass = resolveClass(parameterType);
//獲取屬性resultMap
String resultMap = context.getStringAttribute("resultMap");
//獲取屬性resultType
String resultType = context.getStringAttribute("resultType");
String lang = context.getStringAttribute("lang");
LanguageDriver langDriver = getLanguageDriver(lang);
Class<?> resultTypeClass = resolveClass(resultType);
String resultSetType = context.getStringAttribute("resultSetType");
//獲取屬性statementType,未設置時使用默認值PREPARED
StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
//獲取nodeName,就是標簽的類型,select、insert之類
String nodeName = context.getNode().getNodeName();
//命令類型
SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
//判斷是否為select命令
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
//這里解析sql語句中的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,必須在inclue和selectKey解析完成后執行,
//內部會根據sql語句是否包含動態標簽創建不同類型的sqlSource,解析sql所需的參數映射信息
//這個地方需要一篇單獨的文章來分析
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
String resultSets = context.getStringAttribute("resultSets");
String keyProperty = context.getStringAttribute("keyProperty");
String keyColumn = context.getStringAttribute("keyColumn");
//處理主鍵生成器
KeyGenerator keyGenerator;
String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
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;
}
//創建MappedStatement,添加到configuration
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}
復制代碼
~~~
源碼的過程梳理一下:
* 首先解析標簽的基礎屬性,如id、fetchSize等,
* 解析sql語句中inclue標簽(把include的內容替換拼接進來),
* 處理selcetKey標簽(特定的databaseProvider需要),
* 接下來創建SqlSource:這個是mybatis的核心內容,涉及到sql語句中參數提取、動態賦值等,以后在詳細說明;
* 處理KeyGenerator;
* 使用builderAssistant創建MappedStatement,添加到configuration
# 總結
Configuration是mybatis的全局配置類,mybatis運行時所需的一切都緩存在這里,它伴隨mybatis的整個生命周期。理論上講,Configuration是配置文件mybatis-config.xml的代碼映射,mybatis提供了充分的靈活性、可擴展性,方便開發人員通過配置文件改變其運行行為。
本文通過示例及源碼把mybatis-config.xml的解析過程、Configuration配置的大部分內容進行了梳理,通過梳理確實獲益匪淺。由于水平優先,文中很多內容確實沒有描述清楚,隨著學習的不斷深入,再做完善。
作者:碼路印記
鏈接:https://juejin.cn/post/6878979290252361741
來源:掘金
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。
- 一.JVM
- 1.1 java代碼是怎么運行的
- 1.2 JVM的內存區域
- 1.3 JVM運行時內存
- 1.4 JVM內存分配策略
- 1.5 JVM類加載機制與對象的生命周期
- 1.6 常用的垃圾回收算法
- 1.7 JVM垃圾收集器
- 1.8 CMS垃圾收集器
- 1.9 G1垃圾收集器
- 2.面試相關文章
- 2.1 可能是把Java內存區域講得最清楚的一篇文章
- 2.0 GC調優參數
- 2.1GC排查系列
- 2.2 內存泄漏和內存溢出
- 2.2.3 深入理解JVM-hotspot虛擬機對象探秘
- 1.10 并發的可達性分析相關問題
- 二.Java集合架構
- 1.ArrayList深入源碼分析
- 2.Vector深入源碼分析
- 3.LinkedList深入源碼分析
- 4.HashMap深入源碼分析
- 5.ConcurrentHashMap深入源碼分析
- 6.HashSet,LinkedHashSet 和 LinkedHashMap
- 7.容器中的設計模式
- 8.集合架構之面試指南
- 9.TreeSet和TreeMap
- 三.Java基礎
- 1.基礎概念
- 1.1 Java程序初始化的順序是怎么樣的
- 1.2 Java和C++的區別
- 1.3 反射
- 1.4 注解
- 1.5 泛型
- 1.6 字節與字符的區別以及訪問修飾符
- 1.7 深拷貝與淺拷貝
- 1.8 字符串常量池
- 2.面向對象
- 3.關鍵字
- 4.基本數據類型與運算
- 5.字符串與數組
- 6.異常處理
- 7.Object 通用方法
- 8.Java8
- 8.1 Java 8 Tutorial
- 8.2 Java 8 數據流(Stream)
- 8.3 Java 8 并發教程:線程和執行器
- 8.4 Java 8 并發教程:同步和鎖
- 8.5 Java 8 并發教程:原子變量和 ConcurrentMap
- 8.6 Java 8 API 示例:字符串、數值、算術和文件
- 8.7 在 Java 8 中避免 Null 檢查
- 8.8 使用 Intellij IDEA 解決 Java 8 的數據流問題
- 四.Java 并發編程
- 1.線程的實現/創建
- 2.線程生命周期/狀態轉換
- 3.線程池
- 4.線程中的協作、中斷
- 5.Java鎖
- 5.1 樂觀鎖、悲觀鎖和自旋鎖
- 5.2 Synchronized
- 5.3 ReentrantLock
- 5.4 公平鎖和非公平鎖
- 5.3.1 說說ReentrantLock的實現原理,以及ReentrantLock的核心源碼是如何實現的?
- 5.5 鎖優化和升級
- 6.多線程的上下文切換
- 7.死鎖的產生和解決
- 8.J.U.C(java.util.concurrent)
- 0.簡化版(快速復習用)
- 9.鎖優化
- 10.Java 內存模型(JMM)
- 11.ThreadLocal詳解
- 12 CAS
- 13.AQS
- 0.ArrayBlockingQueue和LinkedBlockingQueue的實現原理
- 1.DelayQueue的實現原理
- 14.Thread.join()實現原理
- 15.PriorityQueue 的特性和原理
- 16.CyclicBarrier的實際使用場景
- 五.Java I/O NIO
- 1.I/O模型簡述
- 2.Java NIO之緩沖區
- 3.JAVA NIO之文件通道
- 4.Java NIO之套接字通道
- 5.Java NIO之選擇器
- 6.基于 Java NIO 實現簡單的 HTTP 服務器
- 7.BIO-NIO-AIO
- 8.netty(一)
- 9.NIO面試題
- 六.Java設計模式
- 1.單例模式
- 2.策略模式
- 3.模板方法
- 4.適配器模式
- 5.簡單工廠
- 6.門面模式
- 7.代理模式
- 七.數據結構和算法
- 1.什么是紅黑樹
- 2.二叉樹
- 2.1 二叉樹的前序、中序、后序遍歷
- 3.排序算法匯總
- 4.java實現鏈表及鏈表的重用操作
- 4.1算法題-鏈表反轉
- 5.圖的概述
- 6.常見的幾道字符串算法題
- 7.幾道常見的鏈表算法題
- 8.leetcode常見算法題1
- 9.LRU緩存策略
- 10.二進制及位運算
- 10.1.二進制和十進制轉換
- 10.2.位運算
- 11.常見鏈表算法題
- 12.算法好文推薦
- 13.跳表
- 八.Spring 全家桶
- 1.Spring IOC
- 2.Spring AOP
- 3.Spring 事務管理
- 4.SpringMVC 運行流程和手動實現
- 0.Spring 核心技術
- 5.spring如何解決循環依賴問題
- 6.springboot自動裝配原理
- 7.Spring中的循環依賴解決機制中,為什么要三級緩存,用二級緩存不夠嗎
- 8.beanFactory和factoryBean有什么區別
- 九.數據庫
- 1.mybatis
- 1.1 MyBatis-# 與 $ 區別以及 sql 預編譯
- Mybatis系列1-Configuration
- Mybatis系列2-SQL執行過程
- Mybatis系列3-之SqlSession
- Mybatis系列4-之Executor
- Mybatis系列5-StatementHandler
- Mybatis系列6-MappedStatement
- Mybatis系列7-參數設置揭秘(ParameterHandler)
- Mybatis系列8-緩存機制
- 2.淺談聚簇索引和非聚簇索引的區別
- 3.mysql 證明為什么用limit時,offset很大會影響性能
- 4.MySQL中的索引
- 5.數據庫索引2
- 6.面試題收集
- 7.MySQL行鎖、表鎖、間隙鎖詳解
- 8.數據庫MVCC詳解
- 9.一條SQL查詢語句是如何執行的
- 10.MySQL 的 crash-safe 原理解析
- 11.MySQL 性能優化神器 Explain 使用分析
- 12.mysql中,一條update語句執行的過程是怎么樣的?期間用到了mysql的哪些log,分別有什么作用
- 十.Redis
- 0.快速復習回顧Redis
- 1.通俗易懂的Redis數據結構基礎教程
- 2.分布式鎖(一)
- 3.分布式鎖(二)
- 4.延時隊列
- 5.位圖Bitmaps
- 6.Bitmaps(位圖)的使用
- 7.Scan
- 8.redis緩存雪崩、緩存擊穿、緩存穿透
- 9.Redis為什么是單線程、及高并發快的3大原因詳解
- 10.布隆過濾器你值得擁有的開發利器
- 11.Redis哨兵、復制、集群的設計原理與區別
- 12.redis的IO多路復用
- 13.相關redis面試題
- 14.redis集群
- 十一.中間件
- 1.RabbitMQ
- 1.1 RabbitMQ實戰,hello world
- 1.2 RabbitMQ 實戰,工作隊列
- 1.3 RabbitMQ 實戰, 發布訂閱
- 1.4 RabbitMQ 實戰,路由
- 1.5 RabbitMQ 實戰,主題
- 1.6 Spring AMQP 的 AMQP 抽象
- 1.7 Spring AMQP 實戰 – 整合 RabbitMQ 發送郵件
- 1.8 RabbitMQ 的消息持久化與 Spring AMQP 的實現剖析
- 1.9 RabbitMQ必備核心知識
- 2.RocketMQ 的幾個簡單問題與答案
- 2.Kafka
- 2.1 kafka 基礎概念和術語
- 2.2 Kafka的重平衡(Rebalance)
- 2.3.kafka日志機制
- 2.4 kafka是pull還是push的方式傳遞消息的?
- 2.5 Kafka的數據處理流程
- 2.6 Kafka的腦裂預防和處理機制
- 2.7 Kafka中partition副本的Leader選舉機制
- 2.8 如果Leader掛了的時候,follower沒來得及同步,是否會出現數據不一致
- 2.9 kafka的partition副本是否會出現腦裂情況
- 十二.Zookeeper
- 0.什么是Zookeeper(漫畫)
- 1.使用docker安裝Zookeeper偽集群
- 3.ZooKeeper-Plus
- 4.zk實現分布式鎖
- 5.ZooKeeper之Watcher機制
- 6.Zookeeper之選舉及數據一致性
- 十三.計算機網絡
- 1.進制轉換:二進制、八進制、十六進制、十進制之間的轉換
- 2.位運算
- 3.計算機網絡面試題匯總1
- 十四.Docker
- 100.面試題收集合集
- 1.美團面試常見問題總結
- 2.b站部分面試題
- 3.比心面試題
- 4.騰訊面試題
- 5.哈羅部分面試
- 6.筆記
- 十五.Storm
- 1.Storm和流處理簡介
- 2.Storm 核心概念詳解
- 3.Storm 單機版本環境搭建
- 4.Storm 集群環境搭建
- 5.Storm 編程模型詳解
- 6.Storm 項目三種打包方式對比分析
- 7.Storm 集成 Redis 詳解
- 8.Storm 集成 HDFS 和 HBase
- 9.Storm 集成 Kafka
- 十六.Elasticsearch
- 1.初識ElasticSearch
- 2.文檔基本CRUD、集群健康檢查
- 3.shard&replica
- 4.document核心元數據解析及ES的并發控制
- 5.document的批量操作及數據路由原理
- 6.倒排索引
- 十七.分布式相關
- 1.分布式事務解決方案一網打盡
- 2.關于xxx怎么保證高可用的問題
- 3.一致性hash原理與實現
- 4.微服務注冊中心 Nacos 比 Eureka的優勢
- 5.Raft 協議算法
- 6.為什么微服務架構中需要網關
- 0.CAP與BASE理論
- 十八.Dubbo
- 1.快速掌握Dubbo常規應用
- 2.Dubbo應用進階
- 3.Dubbo調用模塊詳解
- 4.Dubbo調用模塊源碼分析
- 6.Dubbo協議模塊