この一連の記事は、書籍「一般ソース コード ガイド: MyBatis ソース コードの詳細な説明」からの私のメモと要約です
。この本は MyBatis-3.5.2 バージョンに基づいています。この本の著者はBrother Yiです。リンクは次のとおりです。 CSDN の Yi 兄弟の Weibo。しかし、私が読んだすべての記事の中で、この本を簡単に紹介した記事は 1 つだけでした。この本の魅力はあまり伝わってきません。次に、学習のまとめを記録していきます。作者が私が著作権を侵害していると思われる場合は、削除するよう私に連絡してください。教材を提供してくださったブラザー・イーに改めて感謝します。この説明は一連の記事全体に付随します。独創性を尊重してください。WeChat Reading の改訂本を購入しました。
著作権に関する声明: この記事は CSDN ブロガー「Architect Yi Ge」によるオリジナル記事であり、CC 4.0 BY-SA 著作権契約に従っています。転載する場合は、元のソース リンクとこの声明を添付してください。
元のリンク: https://blog.csdn.net/onlinedct/article/details/107306041
1。概要
MyBatis の設定ファイルは 2 つのカテゴリに分類されます。
- コア構成ファイル。MyBatis の基本構成情報が含まれます。ファイルは 1 つだけです。
- マッピング ファイル。Java オブジェクトとデータベース プロパティ、データベース操作ステートメントなどの間のマッピング関係を設定します。複数のファイルが存在する場合があります。
データベース操作を実行する前に、上記 2 種類のファイルの解析、変換、保存を完了してください。クラスの観点から見ると、構成解析に関連するクラスは次のように分類できます。
- パーサー クラス: 構成解析機能を提供し、コピーすることで構成情報の抽出と変換を完了します。XMLConfigBuilder、XMLMapperBuilder があります。
- エンティティクラスの解析: 設定保存機能を提供します。クラスの構造は構成情報に対応しており、構成情報は最終的に解析されたエンティティ クラスの属性にエラーを報告します。構成クラス、環境クラス、DataSource クラスなど
2.バインディングパッケージ
バインディング パッケージは、主に Java メソッドと SQL ステートメント間のバインディング関係を処理するために使用されるパッケージです。
バインディング パッケージには 2 つの機能があります。
- マッピング インターフェイスの抽象メソッドとデータベース操作ノードの間の関連付けを維持します。
- マッピング インターフェイスの抽象メソッドに対応するデータベース操作にアクセスします。
2.1 データベース操作へのアクセス
マッピング インターフェイスの抽象メソッドに対応するデータベース操作へのアクセスは、リフレクションに基づいた動的プロキシによって実装されます。
2.1.1 データベース操作の方法化
データベース操作を抽象メソッドに接続するには、まずデータベース操作ノードをメソッドに変換します。MapperMethod オブジェクトは、データベース操作の変換されたメソッドを表します。各 MapperMethod オブジェクトはデータベース操作ノードに対応し、ノード内の SQL ステートメントは MapperMethod インスタンスの実行メソッドを呼び出すことで開始できます。
MapperMethod クラスには 2 つの属性があり、これらは 2 つの重要な内部クラスに対応します。
- MethodSignature クラス: 特定のメソッドのシグネチャ
public static class MethodSignature {
// 返回类型是否为集合类型
private final boolean returnsMany;
// 返回类型是否是map
private final boolean returnsMap;
// 返回类型是否是空
private final boolean returnsVoid;
// 返回类型是否是cursor类型
private final boolean returnsCursor;
// 返回类型是否是optional类型
private final boolean returnsOptional;
// 返回类型
private final Class<?> returnType;
// 如果返回为map,这里记录所有的map的key
private final String mapKey;
// resultHandler参数的位置
private final Integer resultHandlerIndex;
// rowBounds参数的位置
private final Integer rowBoundsIndex;
// 引用参数名称解析器
private final ParamNameResolver paramNameResolver;
}
MethodSignature のプロパティは、メソッドの詳細を詳細に記述します。
- SqlCommand クラス: SQL ステートメント
public static class SqlCommand {
// SQL语句的名称
private final String name;
// SQL语句的种类,一共分为以下六种:增、删、改、查、清缓存、未知
private final SqlCommandType type;
public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) {
// 方法名称
final String methodName = method.getName();
// 方法所在的类。可能是mapperInterface,也可能是mapperInterface的子类
final Class<?> declaringClass = method.getDeclaringClass();
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 {
name = ms.getId();
type = ms.getSqlCommandType();
if (type == SqlCommandType.UNKNOWN) {
throw new BindingException("Unknown execution method for: " + name);
}
}
}
public String getName() {
return name;
}
public SqlCommandType getType() {
return type;
}
/**
* 找出指定接口指定方法对应的MappedStatement对象
* @param mapperInterface 映射接口
* @param methodName 映射接口中具体操作方法名
* @param declaringClass 操作方法所在的类。一般是映射接口本身,也可能是映射接口的子类
* @param configuration 配置信息
* @return MappedStatement对象
*/
private MappedStatement resolveMappedStatement(Class<?> mapperInterface, String methodName,
Class<?> declaringClass, Configuration configuration) {
// 数据库操作语句的编号是:接口名.方法名
String statementId = mapperInterface.getName() + "." + methodName;
// configuration保存了解析后的所有操作语句,去查找该语句
if (configuration.hasStatement(statementId)) {
// 从configuration中找到了对应的语句,返回
return configuration.getMappedStatement(statementId);
} else if (mapperInterface.equals(declaringClass)) {
// 说明递归调用已经到终点,但是仍然没有找到匹配的结果
return null;
}
// 从方法的定义类开始,沿着父类向上寻找。找到接口类时停止
for (Class<?> superInterface : mapperInterface.getInterfaces()) {
if (declaringClass.isAssignableFrom(superInterface)) {
MappedStatement ms = resolveMappedStatement(superInterface, methodName,
declaringClass, configuration);
if (ms != null) {
return ms;
}
}
}
return null;
}
}
}
SqlCommand の構築メソッドは主に、受信パラメータに従って名前フィールドと型フィールドの割り当てを完了することであり、resolveMappedStatement サブメソッドがすべての鍵となります。これは、resolveMappedStatement サブメソッドが MappedStatement オブジェクトをクエリし、MappedStatement がデータベース操作ステートメントに完全に対応しているためです。
したがって、MapperMethod オブジェクトの実行メソッドが呼び出されている限り、特定のデータベース操作をトリガーすることができ、データベース操作はメソッドに変換されます。
/**
* 执行映射接口中的方法
* @param sqlSession sqlSession接口的实例,通过它可以进行数据库的操作
* @param args 执行接口方法时传入的参数
* @return 数据库操作结果
*/
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
switch (command.getType()) {
// 根据SQL语句类型,执行不同操作
case INSERT: {
// 如果是插入语句
// 将参数顺序与实参对应好
Object param = method.convertArgsToSqlCommandParam(args);
// 执行操作并返回结果
result = rowCountResult(sqlSession.insert(command.getName(), param));
break;
}
case UPDATE: {
// 如果是更新语句
// 将参数顺序与实参对应好
Object param = method.convertArgsToSqlCommandParam(args);
// 执行操作并返回结果
result = rowCountResult(sqlSession.update(command.getName(), param));
break;
}
case DELETE: {
// 如果是删除语句MappedStatement
// 将参数顺序与实参对应好
Object param = method.convertArgsToSqlCommandParam(args);
// 执行操作并返回结果
result = rowCountResult(sqlSession.delete(command.getName(), param));
break;
}
case SELECT: // 如果是查询语句
if (method.returnsVoid() && method.hasResultHandler()) {
// 方法返回值为void,且有结果处理器
// 使用结果处理器执行查询
executeWithResultHandler(sqlSession, args);
result = null;
} else if (method.returnsMany()) {
// 多条结果查询
result = executeForMany(sqlSession, args);
} else if (method.returnsMap()) {
// Map结果查询
result = executeForMap(sqlSession, args);
} 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());
}
if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
// 查询结果为null,但返回类型为基本类型。因此返回变量无法接收查询结果,抛出异常。
throw new BindingException("Mapper method '" + command.getName()
+ " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
}
return result;
}
MapperMethodクラスのexecuteメソッドのソースコードです。実行メソッドは、独自の SQL ステートメントのタイプに基づいて、さまざまなデータベース操作をトリガーすることがわかります。MapperMethod クラスの助けを借りて、Java マッピング インターフェイスへの呼び出しを MapperMethod オブジェクトの実行メソッドへの呼び出しに変換できれば、Java マッピング インターフェイスの呼び出し時に指定されたデータベース操作を完了できます。
MapperMethod クラスには内部クラス ParamMap もあります。ParamMap 内部クラスはパラメータを格納するために使用されます。これは HashMap のサブクラスですが、HashMap よりも厳密です。存在しないキー値を取得しようとすると、直接例外がスローされます。これは、データベース操作で存在しない入力パラメータを参照すると、そのようなエラーを解決できないためです。
public static class ParamMap<V> extends HashMap<String, V> {
private static final long serialVersionUID = -2212268410512043556L;
@Override
public V get(Object key) {
if (!super.containsKey(key)) {
throw new BindingException("Parameter '" + key + "' not found. Available parameters are " + keySet());
}
return super.get(key);
}
}
2.1.2 データベース操作方法へのアクセス
前のセクションでは、データベース操作をメソッド (ここでは MapperMethod オブジェクトの実行メソッドを指します) に変換しましたが、このメソッドはどのように呼び出すことができるでしょうか?
「List<User>queryUserBySchoolName (User user)」などのマッピング インターフェイスでメソッドを呼び出すと、Java はインターフェイスの実装クラスでメソッドを検索して実行します。マッピング インターフェイスには実装クラスがないため、マッピング インターフェイスでメソッドを呼び出すとエラーが発生するはずですが、代わりに MapperMethod クラスの実行メソッドを呼び出すにはどうすればよいでしょうか?
上記の作業には MapperProxy クラスの助けが必要です。MapperProxy クラスは、マッピング インターフェイスのメソッド呼び出しを、動的プロキシに基づく MapperMethod オブジェクトの実行メソッドへの呼び出しに転送することで、データベース操作を実現します。MapperProxy は InvocationHandler インターフェイスを継承し、動的プロキシ クラスです。これは、プロキシ化されたオブジェクトの代わりにそのインスタンスが使用される場合、プロキシ化されたオブジェクトのメソッド呼び出しが MapperProxy の invoke メソッドに転送されることを意味します。
public class MapperProxy<T> implements InvocationHandler, Serializable {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
if (Object.class.equals(method.getDeclaringClass())) {
// 继承自Object的方法
// 直接执行原有方法
return method.invoke(this, args);
} else if (method.isDefault()) {
// 默认方法
// 执行默认方法
return invokeDefaultMethod(proxy, method, args);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
// 找对对应的MapperMethod对象
final MapperMethod mapperMethod = cachedMapperMethod(method);
// 调用MapperMethod中的execute方法
return mapperMethod.execute(sqlSession, args);
}
}
MapperProxyFactory は MapperProxy の実稼働ファクトリーであり、newInstance コア メソッドは MapperProxy オブジェクトを生成します。この時点で、対応する MapperProxy オブジェクトがマッピング インターフェイスの実装として使用されている限り、マッピング インターフェイスのデータベース操作にアクセスする機能を完全に実現できることがわかります。
2.1.3 抽象メソッドはデータベース操作ノードに関連付けられます
マッピング インターフェイスの抽象メソッドは、どの MapperMethod オブジェクトにアクセスするかをどのように決定するのでしょうか?
MyBatis は、この問題を 2 つのステップで解決します。
- 最初のステップで、MyBatis はマッピング インターフェイスを MapperProxyFactory に関連付けます。この関連付けは、MapperRegistry クラスの knownMappers 属性で維持されます。knownMappers は、キーがマッピング インターフェイス、値が対応する MapperProxyFactory オブジェクトである HashMap です。
public class MapperRegistry {
private final Configuration config;
// 已知的所有映射
// key:mapperInterface,即dao的数据库接口,不是方法
// value:MapperProxyFactory,即映射器代理工厂
private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();
/**
* 找到指定映射接口的映射文件,并根据映射文件信息为该映射接口生成一个代理实现
* @param type 映射接口
* @param sqlSession sqlSession
* @param <T> 映射接口类型
* @return 代理实现对象
*/
@SuppressWarnings("unchecked")
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
// 找出指定映射接口的代理工厂
final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
if (mapperProxyFactory == null) {
throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
}
try {
// 通过mapperProxyFactory给出对应代理器的实例
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception e) {
throw new BindingException("Error getting mapper instance. Cause: " + e, e);
}
}
MapperProxyFactory のコンストラクターには、マッピング インターフェイスであるパラメーターが 1 つだけあります。MapperProxyFactory の他の属性は変更できないため、生成される MapperProxy オブジェクトは一意です。したがって、MapperProxyFactory オブジェクトが決定される限り、MapperProxy オブジェクトも決定されます。したがって、MapperRegistry の knownMappers 属性は、マッピング インターフェイスを MapperProxy オブジェクトに間接的に関連付けます。
public class MapperProxyFactory<T> {
/**
* MapperProxyFactory构造方法
* @param mapperInterface 映射接口
*/
public MapperProxyFactory(Class<T> mapperInterface) {
this.mapperInterface = mapperInterface;
}
MapperRegistry はマッピング インターフェイスと MapperProxy の間の対応関係を保存するため、その getMapper メソッドはマッピング インターフェイスに対応するプロキシ オブジェクトを直接見つけることができます。MapperRegistry を通じて、マッピング インターフェイスとマッピング ファイルの間の対応関係が確立されます。
- 2 番目のステップでは、この時点でのスコープはマッピング インターフェイスまたは MapperProxy オブジェクトに縮小されています。インターフェイス メソッドと MapperMethod オブジェクトの間の対応関係は、MapperProxy の methodCache 属性によって維持されます。
このように、マッピング インターフェイスの抽象メソッドは、MapperProxy オブジェクトに関連付けられた MapperMethod オブジェクトに対応します。
// 该Map的键为方法,值为MapperMethod对象。通过该属性,完成了MapperProxy内(即映射接口内)方法和MapperMethod的绑定
private final Map<Method, MapperMethod> methodCache;
MapperProxy クラスは、マッピング インターフェイスのプロキシ クラスです。プロキシ関係が確立された後、マッピング インターフェイスのメソッドが呼び出されている限り、そのメソッドは対応する MapperProxy によってインターセプトされ、MapperProxy は適切な MapperMethod オブジェクトを作成または選択し、その実行メソッドをトリガーします。その結果、マッピング インターフェイスでの抽象メソッドの呼び出しは、特定のデータベース操作に変換されます。
2.1.4 データベース操作アクセスの概要
- 初期化フェーズ
MyBatis は、初期化フェーズ中に各マッピング ファイルを解析し、各データベース操作ノードの情報を Configuration オブジェクトの mappedStatements 属性に記録します。その構造は StrictMap (キー値の上書きを許可しない HashMap) であり、この StrictMap のキーは SQL ステートメントの「名前空間の値。ステートメント ID の値」です (ステートメント ID の値があいまいでない場合はステートメント ID値はデータのコピーに別途使用されます)、値はデータベース操作ノードの詳細情報です。
MyBatis はまた、初期化フェーズ中にすべてのマッピング インターフェイスをスキャンし、マッピング インターフェイスに基づいてそれに関連付けられた MapperProxyFactory を作成します。この 2 つの関係は MapperRegistry によって維持されます。MapperRegistry の getMapper メソッドが呼び出されると (最終的には SqlSession の getMapper メソッドがここで呼び出されます)、MapperProxyFactory はマッピング インターフェイスのプロキシとして MapperProxy オブジェクトを生成します。
- データの読み取りおよび書き込みフェーズ
マッピング インターフェイスのメソッドが呼び出されると、そのメソッドはプロキシ オブジェクト MapperProxy によってハイジャックされ、次に MapperProxy オブジェクトのメソッド呼び出しがトリガーされます。MapperProxy オブジェクトの invoke メソッドは、マッピング インターフェイス メソッドに対応する MapperMethod オブジェクトを作成または取得します。MapperMethod オブジェクトの作成プロセス中に、MapperMethod の SqlCommand サブクラスのコンストラクターは、それに応じて Configuration オブジェクトの mappedStatements 属性に移動します。現在のマッピングインタフェース名とメソッドに初期段階で名前インデックスに格納されているSQL文情報が格納されます。次に、MapperMethod オブジェクトの実行メソッドがトリガーされ、実行メソッド内のさまざまな SQL ステートメントの種類に従ってさまざまなデータベース操作が実行されます。このようにして、マッピング インターフェイスのメソッド呼び出しは、最終的に対応するデータベース操作に変換されます。
3.ビルダーパッケージ
ビルダーパッケージは種類ごとに分けられたパッケージであり、パッケージ内には多数のビルダークラスが含まれています。
ビルダーパッケージは種類ごとに分かれたパッケージですが、比較的充実した以下の2つの機能もこのパッケージで完結します。
- XML 構成ファイルとマッピング ファイルを解析します。関数のこの部分は xml サブパッケージにあります。
- アノテーションの形式でマッパー宣言を解析します。関数のこの部分はアノテーション サブパッケージに含まれています。
ビルダー パターンの利点:
- ビルダーの使用は非常に柔軟であり、構築されたオブジェクトのプロパティを 1 回または複数回設定できます。
- 呼び出し元はビルダーのメインプロセスを呼び出すだけでよく、ビルドされたオブジェクトの詳細を気にする必要はありません。
- ビルダーの動作は簡単に変更して、さまざまなオブジェクトを作成できます。
ビルダー クラスには通常、次の 2 種類のメソッドが含まれます。
- 1 つは属性の設定方法です。通常、ビルダーのプロパティを設定するためにさまざまなタイプのパラメーターを受け入れることができるこのタイプのメソッドが複数あります。
- 1 つは対象オブジェクトの生成方法です。このタイプのメソッドには通常、現在のビルダーのプロパティに基づいてターゲット オブジェクトを作成するメソッドが 1 つだけあります。
ビルダー パターンの利点は、複雑なオブジェクトを作成する場合により明らかになります。したがって、ビルダー パターンは一部の大規模システムでは非常に一般的です。
3.1 Builder の基本クラスとツール クラス
BaseBuilder はすべてのビルダー クラスの基本クラスです。BaseBuilder
クラスは抽象クラスとして宣言されていますが、抽象メソッドが含まれていないため、そのサブクラスはメソッドを実装する必要がありません。BaseBuilder クラスはユーティリティ クラスに似ており、それを継承するビルダー クラスに多くの実用的なユーティリティ メソッドを提供します。もちろん、BaseBuilder が提供するツール メソッドを必要としないため、BaseBuilder を継承しないビルダー クラスは実際に数多くあります。これらのクラスには、MapperAnnotationBuilder、SelectBuilder などが含まれます。
BaseBuilderクラスが提供するツールメソッドは、大きく以下のカテゴリに分類されます。
- *ValueOf: 入力パラメータを指定された型に変換し、デフォルト値設定をサポートする型変換関数。
- solve*: 文字列を列挙型関数に変換し、文字列に基づいて指定された列挙型を見つけて返します。
- createInstance: 型エイリアスに基づいて型インスタンスを作成します。
- solveTypeHandler: タイプ ハンドラーの別名に基づいてタイプ ハンドラー インスタンスを返します。
BaseBuilder クラスのサブクラスの中で、MapperBuilderAssistant クラスは、ビルダー クラス自体ではなくビルダー補助クラスであるため、最も特殊です。BaseBuilder クラスから継承する理由は、単純に BaseBuilder クラスのメソッドを使用するためです。
MyBatis マッピング ファイルには、ネームスペース、キャッシュ共有、結果マッピングなど、多くの設定があります。最終的に、これらの設定は解析されてさまざまなクラスが生成されます。MapperBuilderAssistant クラスは、これらの解析されたクラスの補助クラスです。MapperBuilderAssistant クラスは、Mapper 名前空間の設定、キャッシュの作成、識別子の作成など、多くの補助メソッドを提供します。
3.2 SqlSourceBuilder クラスと StaticSqlSource クラス
SqlSourceBuilder はビルダー クラスですが、その名前はやや曖昧で、すべての SqlSource オブジェクトの作成には使用できません (SqlSource は 4 つの実装を持つインターフェイスです)。parse メソッドを通じてのみ StaticSqlSource オブジェクトを生成できます。
正確に言うと、SqlSourceBuilder クラスは、DynamicSqlSource と RawSqlSource の "#{}" シンボルを置き換えて StaticSqlSource に変換できます。この変換プロセスは parse メソッドで発生します。したがって、SqlSourceBuilder クラスをパーサーまたはコンバーターと呼ぶ方が適切です。実際、SqlSourceBuilder オブジェクトを参照する多くの場所では、オブジェクトの変数に「sqlSourceParser」という名前が付けられています (この変数は DynamicSqlSource クラスと RawSqlSource クラスにあります)。
/**
* 将DynamicSqlSource和RawSqlSource中的“#{}”符号替换掉,从而将他们转化为StaticSqlSource
* @param originalSql sqlNode.apply()拼接之后的sql语句。已经不包含<if> <where>等节点,也不含有${}符号
* @param parameterType 实参类型
* @param additionalParameters 附加参数
* @return 解析结束的StaticSqlSource
*/
public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
// 用来完成#{}处理的处理器
ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
// 通用的占位符解析器,用来进行占位符替换
GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
// 将#{}替换为?的SQL语句
String sql = parser.parse(originalSql);
// 生成新的StaticSqlSource对象
return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
}
StaticSqlSource は、SqlSource の 4 つのサブクラスのうちの 1 つです。StaticSqlSource に含まれる SQL ステートメントには、2 つの記号 "${}" と "#{}" が存在しなくなり、"?" のみが存在します。
public class StaticSqlSource implements SqlSource {
// 经过解析后,不存在${}和#{}这两种符号,只剩下?符号的SQL语句
private final String sql;
// SQL语句对应的参数列表
private final List<ParameterMapping> parameterMappings;
// 配置信息
private final Configuration configuration;
/**
* 组建一个BoundSql对象
* @param parameterObject 参数对象
* @return 组件的BoundSql对象
*/
@Override
public BoundSql getBoundSql(Object parameterObject) {
return new BoundSql(configuration, sql, parameterMappings, parameterObject);
}
}
StaticSqlSource には、BoundSql オブジェクトを与えるという非常に重要な機能があります。StaticSqlSource の getBoundSql メソッドは、この関数を完了する役割を果たします。
3.3 CacheRefResolver クラスと ResultMapResolver クラス
CacheRefResolver クラスと ResultMapResolver クラスは、クラス名だけでなく構造と機能も似ています。これらはすべて、特定のクラスのパーサー クラスであり、属性には、解析されたクラスの関連属性が含まれており、パーサーも含まれています。クラス内のパーサーは、解析されたクラス属性の解析を完了できます。解析関数を備えたこれらの統合クラスは、MyBatis で標準化された名前を持っています。解析されたオブジェクトの名前が A の場合、統合された自己解析クラスは AResolver と呼ばれます。
その後の分析でこのような名前のクラスが見つかった場合は、その構成と機能を直接分析できます。この命名方法と機能は比較的普遍的ですが、絶対的なものではありません。たとえば、アノテーション サブパッケージ内の MethodResolver は、解析されたオブジェクトのプロパティとパーサーを含めてこのパターンに準拠しますが、ParamNameResolver は、その解析関数が独自のメソッドを通じて実装されており、依存する必要がないため、このパターンに準拠しません。他のパーサー上で。
<mapper namespace="com.github.yeecode.mybatisdemo.UserDao">
<cache-ref namespace="com.github.yeecode.mybatisdemo"/>
MyBatis は、複数の名前空間間の共有キャッシュをサポートします。「com.github.yeecode.mybatisdemo.dao.UserDao」の名前空間で、 <cache-ref> タグを通じて別の名前空間「com.github.yeecode.mybatisdemo.dao.TaskDao」を宣言します。前者は後者を使用します。キャッシュ。
3.3.1 CacheRefResolver クラス
CacheRefResolver は、複数の名前空間がキャッシュを共有する問題を処理するために使用されます。独自の 2 つのプロパティがあります。これら 2 つの属性のうち、assistant はパーサーであり、cacheRefNamespace は解析されるオブジェクトです。
/**
* @author Clinton Begin
*
* 缓存引用解析器
*
* 包含了被解析的对象cacheRefNamespace 和对应的解析器MapperBuilderAssistant 因此具有自解析功能。
*/
public class CacheRefResolver {
// Mapper建造者辅助类
private final MapperBuilderAssistant assistant;
// 被应用的namespace,即使用cacheRefNamespace的缓存空间
private final String cacheRefNamespace;
public CacheRefResolver(MapperBuilderAssistant assistant, String cacheRefNamespace) {
this.assistant = assistant;
this.cacheRefNamespace = cacheRefNamespace;
}
public Cache resolveCacheRef() {
return assistant.useCacheRef(cacheRefNamespace);
}
}
MapperBuilderAssistant の useCacheRef メソッドを使用すると、CacheRefResolver クラスでキャッシュ共有の問題を解決できます。
/**
* 使用其他namespace的缓存
* @param namespace 其他的namespace
* @return 其他namespace的缓存
*/
public Cache useCacheRef(String namespace) {
if (namespace == null) {
throw new BuilderException("cache-ref element requires a namespace attribute.");
}
try {
unresolvedCacheRef = true;
// 获取其他namespace的缓存
Cache cache = configuration.getCache(namespace);
if (cache == null) {
throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found.");
}
// 修改当前缓存为其他namespace的缓存,从而实现缓存共享
currentCache = cache;
unresolvedCacheRef = false;
return cache;
} catch (IllegalArgumentException e) {
throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found.", e);
}
}
3.3.2 ResultMapResolver クラス
MyBatis の resultMap タグは継承をサポートしています。コード 14-13 に示すように、「girlUserMap」は「extends="userMap"」を設定することで「userMap」に設定された属性マッピングを継承します。
<resultMap id="userMap" type="User" autoMapping="false">
<result property="id" column="id"/>
<result property="name" column="name"/>
<discriminator javaType="int" column="sex">
<case value="0" resultMap="boyUserMap"/>
<case value="1" resultMap="girlUserMap"/>
</discriminator>
</resultMap>
<resultMap id="girlUserMap" type="Girl" extends="userMap">
<result property="email" column="email"/>
</resultMap>
resultMap の継承関係の解決は、ResultMapResolver クラスによって完了します。ResultMapResolver クラスの属性アシスタント属性はパーサーであり、他の属性は解析された属性です。
public class ResultMapResolver {
// Mapper建造者辅助类
private final MapperBuilderAssistant assistant;
// ResultMap的id
private final String id;
// ResultMap的type属性,即目标对象类型
private final Class<?> type;
// ResultMap的extends属性,即继承属性
private final String extend;
// ResultMap中的Discriminator节点,即鉴别器
private final Discriminator discriminator;
// ResultMap中的属性映射列表
private final List<ResultMapping> resultMappings;
// ResultMap的autoMapping属性,即是否开启自动映射
private final Boolean autoMapping;
MapperBuilderAssistant の addResultMap メソッドの助けを借りて、ResultMapResolver は ResultMap の継承関係の分析を完了し、継承関係が解決された後、最終的に ResultMap オブジェクトを提供します。
/**
* 创建结果映射对象
* 入参参照ResultMap属性
* @return ResultMap对象
*/
public ResultMap addResultMap(
String id,
Class<?> type,
String extend,
Discriminator discriminator,
List<ResultMapping> resultMappings,
Boolean autoMapping) {
id = applyCurrentNamespace(id, false);
extend = applyCurrentNamespace(extend, true);
// 解析ResultMap的继承关系
if (extend != null) {
// 如果存在ResultMap的继承
if (!configuration.hasResultMap(extend)) {
throw new IncompleteElementException("Could not find a parent resultmap with id '" + extend + "'");
}
// 获取父级的ResultMap
ResultMap resultMap = configuration.getResultMap(extend);
// 获取父级的属性映射
List<ResultMapping> extendedResultMappings = new ArrayList<>(resultMap.getResultMappings());
// 删除当前ResultMap中已有的父级属性映射,为当前属性映射覆盖父级属性属性创造条件
extendedResultMappings.removeAll(resultMappings);
// 如果当前ResultMap设置有构建器,则移除父级构建器
boolean declaresConstructor = false;
for (ResultMapping resultMapping : resultMappings) {
if (resultMapping.getFlags().contains(ResultFlag.CONSTRUCTOR)) {
declaresConstructor = true;
break;
}
}
if (declaresConstructor) {
extendedResultMappings.removeIf(resultMapping -> resultMapping.getFlags().contains(ResultFlag.CONSTRUCTOR));
}
// 最终从父级继承而来的所有属性映射
resultMappings.addAll(extendedResultMappings);
}
// 创建当前的ResultMap
ResultMap resultMap = new ResultMap.Builder(configuration, id, type, resultMappings, autoMapping)
.discriminator(discriminator)
.build();
// 将当期的ResultMap加入到Configuration
configuration.addResultMap(resultMap);
return resultMap;
}
3.4 ParameterExpression クラス
ParameterExpression は、属性を説明する文字列をキーと値のペアに解析する属性パーサーです。ParameterExpression のコンストラクターは、属性解析の一般的なエントリ ポイントであり、クラス全体の唯一のパブリック メソッドでもあります。ParameterExpression クラスは HashMap を継承し、最終的な解析結果をキーと値のペアの形式で内部的に保存できます。
/**
* 一个属性解析器
* 能够将属性拆解开来
*/
public class ParameterExpression extends HashMap<String, String> {
private static final long serialVersionUID = -2417552199605158680L;
public ParameterExpression(String expression) {
parse(expression);
}
// content = id, javaType= int, jdbcType=NUMERIC, typeHandler=DemoTypeHandler ;
private void parse(String expression) {
// 跳过空格
int p = skipWS(expression, 0);
// 跳过左括号
if (expression.charAt(p) == '(') {
expression(expression, p + 1);
} else {
// 处理参数
property(expression, p);
}
}
3.5 XML ファイルの解析
MyBatis の設定ファイルとマッピング ファイルは XML ファイルであり、最終的にはこれらの XML ファイルを対応するクラスに解析する必要があります。ビルダー パッケージの xml サブパッケージは、XML ファイルの解析を完了するために使用されます。MyBatis の設定ファイルとマッピング ファイルには多くのノードが含まれています。これらのノードの解析は、xml サブパッケージ内の 5 つのパーサー クラスによって層ごとに完了します。
3.5.1 XML ファイル宣言の解析
XML ファイルは、外部 DTD ファイルを参照して XML ファイルを検証できます。上図の DOCTYPE ステートメントでは、現在の XML ファイルが参照する DTD ファイルのアドレスが「http://mybatis.org/dtd/mybatis-3-config.dtd」であることを示しています。MyBatis は非ネットワーク環境で実行される場合があり、インターネット経由で DTD ファイルをダウンロードできません。XMLMapperEntityResolver は、この問題を解決するために使用されます。
"org.xml.sax.EntityResolver" インターフェイスには、resolveEntity メソッドがあり、このメソッドを実装すると、インターネットから DTD ドキュメントをダウンロードするだけでなく、DTD ドキュメント ストリームが与えられる方法をカスタマイズできます。
/**
* 在一个XML文件的头部是这样的:
* <!DOCTYPE configuration
* PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
* "http://mybatis.org/dtd/mybatis-3-config.dtd">
* 那么上述例子中,
* @param publicId 为-//mybatis.org//DTD Config 3.0//EN
* @param systemId 为http://mybatis.org/dtd/mybatis-3-config.dtd
* @return 对应DTD文档的输入流
* @throws SAXException
*/
@Override
public InputSource resolveEntity(String publicId, String systemId) throws SAXException {
try {
if (systemId != null) {
// 将systemId转为全小写
String lowerCaseSystemId = systemId.toLowerCase(Locale.ENGLISH);
if (lowerCaseSystemId.contains(MYBATIS_CONFIG_SYSTEM) || lowerCaseSystemId.contains(IBATIS_CONFIG_SYSTEM)) {
// 说明这个是配置文档
// 直接把本地配置文档的dtd文件返回
return getInputSource(MYBATIS_CONFIG_DTD, publicId, systemId);
} else if (lowerCaseSystemId.contains(MYBATIS_MAPPER_SYSTEM) || lowerCaseSystemId.contains(IBATIS_MAPPER_SYSTEM)) {
// 说明这个是映射文档
// 直接把本地映射文档的dtd文件返回
return getInputSource(MYBATIS_MAPPER_DTD, publicId, systemId);
}
}
return null;
} catch (Exception e) {
throw new SAXException(e.toString());
}
}
3.5.2 設定ファイルの分析
XMLConfigBuilder クラスは構成ファイルの解析を担当し、このクラスは解析結果を使用して Configuration オブジェクトを構築します。XMLConfigBuilder クラスのエントリ メソッドは parse メソッドです。このメソッドは parseConfiguration メソッドを呼び出して、構成ファイルのレイヤーごとの解析を正式に開始します。
/**
* 从根节点configuration开始解析下层节点
* @param root 根节点configuration节点
*/
private void parseConfiguration(XNode root) {
try {
// 解析信息放入Configuration
// 首先解析properties,以保证在解析其他节点时便可以生效
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);
// read it after objectFactory and objectWrapperFactory issue #631
environmentsElement(root.evalNode("environments"));
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
typeHandlerElement(root.evalNode("typeHandlers"));
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
parseConfiguration メソッドは、下位ノードを解析するためにさまざまなサブメソッドを呼び出しますが、これらのメソッドは似ています。「/configuration/mappers」ノードを解析するmapperElementメソッドを例として紹介しましょう。
/**
* 解析mappers节点,例如:
* <mappers>
* <mapper resource="com/github/yeecode/mybatisDemo/UserDao.xml"/>
* <package name="com.github.yeecode.mybatisDemo" />
* </mappers>
* @param parent mappers节点
* @throws Exception
*/
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
// 处理mappers的子节点,即mapper节点或者package节点
if ("package".equals(child.getName())) {
// package节点
// 取出包的路径
String mapperPackage = child.getStringAttribute("name");
// 全部加入Mappers中
configuration.addMappers(mapperPackage);
} 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);
// 获取文件的输入流
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) {
// 配置的不是Mapper文件,而是Mapper接口
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.");
}
}
}
}
}
3.5.3 データベース操作文の分析
XMLMapperBuilder クラスはマッピング ファイルの解析を担当し、このクラスの構造は XMLConfigBuilder クラスとよく似ています。parse メソッドは解析のエントリ メソッドであり、その後、configurationElement メソッドが呼び出されて、レイヤごとの解析が完了します。
/**
* 解析Mapper文件
*/
public void parse() {
// 该节点是否被解析过
if (!configuration.isResourceLoaded(resource)) {
// 处理mapper节点
configurationElement(parser.evalNode("/mapper"));
// 加入到已经解析的列表,防止重复解析
configuration.addLoadedResource(resource);
// 将mapper注册给Configuration
bindMapperForNamespace();
}
// 下面分别用来处理失败的<resultMap>、<cache-ref>、SQL语句
parsePendingResultMaps();
parsePendingCacheRefs();
parsePendingStatements();
}
/**
* 解析Mapper文件的下层节点
* @param context Mapper文件的根节点
*/
private void configurationElement(XNode context) {
try {
// 读取当前Mapper文件的命名空间
String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.equals("")) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
builderAssistant.setCurrentNamespace(namespace);
// mapper文件中其他配置节点的解析
cacheRefElement(context.evalNode("cache-ref"));
cacheElement(context.evalNode("cache"));
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
resultMapElements(context.evalNodes("/mapper/resultMap"));
sqlElement(context.evalNodes("/mapper/sql"));
// 处理各个数据库操作语句
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);
}
}
XMLConfigBuilder クラスの parse メソッドとは異なり、XMLMapperBuilder の parse メソッドの最後には 3 つの parsePending* メソッドがあります。これらは、解析中の一時的なエラーを処理するために使用されます。configurationElement(parser.evalNode("/mapper")) ステートメントによってトリガーされた後、システムはマッピング ファイルの各ノードを順番に解析します。解析する場合、ファイルは上から下に読み取られて解析されます。ノードは解析される可能性がありますが、それが参照するノードはまだ定義されていません。
<resultMap id="userMap" type="User" autoMapping="false">
<result property="id" column="id"/>
<result property="name" column="name"/>
<discriminator javaType="int" column="sex">
<case value="0" resultMap="boyUserMap"/>
<case value="1" resultMap="girlUserMap"/>
</discriminator>
</resultMap>
<resultMap id="girlUserMap" type="Girl" extends="userMap">
<result property="email" column="email"/>
</resultMap>
"id="girlUserMap"" の resultMap を解析するときに、"extends="userMap"" を通じて参照される "id="userMap"" の resultMap がまだ読み込まれていません。このとき一時的なエラーが発生します。
構成にはコードのいくつかの属性があり、一時的なエラーのあるノードを保存するために使用されます。
// 暂存未处理完成的一些节点
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<>();
上記の依存関係が確認できない状況は一時的なものであり、最初の解析が完了した後にこれらのエラー ノードを再度処理するだけで済みます。順不同の依存関係を解決する一般的な方法は、最初のラウンドの解析を試行し、解析中にすべてのノードを読み取ることです。次に、2 回目の解析が実行され、1 回目の解析で見つけられなかったノードが信頼されます。最初のパスですべてのノードが読み取られているため、2 番目のパスの依存関係を常に見つけることができます。
より直接的で単純な別の方法もあります。つまり、最初の解析ラウンドではすべてのノードを読み取るだけで依存関係は処理せず、2 番目の解析ラウンドでは依存関係のみを処理します。これは、Spring が初期化中に Bean 間の依存関係を処理する方法です。
3.5.4 ステートメントの分析
マッピング ファイルの解析では、データベース操作ノード、つまり選択、挿入、更新、削除の 4 種類のノードを解析することが重要なタスクです。データベース操作ノードの解析は XMLStatementBuilder によって完了します。XMLStatementBuilder クラスの parseStatementNode メソッドは、主要な解析プロセスを完了します。
/**
* 解析select、insert、update、delete这四类节点
*/
public void parseStatementNode() {
// 读取当前节点的id与databaseId
String id = context.getStringAttribute("id");
String databaseId = context.getStringAttribute("databaseId");
// 验证id与databaseId是否匹配。MyBatis允许多数据库配置,因此有些语句只对特定数据库生效
if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
return;
}
// 读取节点名
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节点
XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
includeParser.applyIncludes(context.getNode());
// 参数类型
String parameterType = context.getStringAttribute("parameterType");
Class<?> parameterTypeClass = resolveClass(parameterType);
// 语句类型
String lang = context.getStringAttribute("lang");
LanguageDriver langDriver = getLanguageDriver(lang);
// 处理SelectKey节点,在这里会将KeyGenerator加入到Configuration.keyGenerators中
processSelectKeyNodes(id, parameterTypeClass, langDriver);
// 此时,<selectKey> 和 <include> 节点均已被解析完毕并被删除,开始进行SQL解析
KeyGenerator keyGenerator;
String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
// 判断是否已经有解析好的KeyGenerator
if (configuration.hasKeyGenerator(keyStatementId)) {
keyGenerator = configuration.getKeyGenerator(keyStatementId);
} else {
// 全局或者本语句只要启用自动key生成,则使用key生成
keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
}
// 读取各个配置属性
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
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");
// 在MapperBuilderAssistant的帮助下创建MappedStatement对象,并写入到Configuration中
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}
3.5.5 アプリケーション解析には以下が含まれます
MyBatis は、データベース操作ステートメントを作成する際のステートメントのフラグメントの引用をサポートしています。MyBatis でのデータベース操作ステートメントの作成効率を向上させます。
<sql id="bySchool">
AND `schoolName` = #{
schoolName}
</sql>
<select id="selectUserByNameAndSchoolName" parameterMap="userParam01" resultType="User">
SELECT * FROM `user` WHERE `name` = #{
name}
<include refid="bySchool"/>
</select>
<select id="selectUsersByNameOrSchoolName" parameterMap="userParam01" resultType="User">
SELECT * FROM `user`
<where>
<if test="name != null">
`name` = #{
name}
</if>
<if test="schoolName != null">
AND `schoolName` = #{
schoolName}
</if>
</where>
</select>
コード内の 2 つのメソッド selectUserByNameAndSchoolName は同等です。インクルード ノードの解析は XMLIncludeTransformer によって処理され、SQL ステートメント内のインクルード ノードを引用符で囲まれた SQL フラグメントに置き換えます。XMLIncludeTransformer クラスの applyinclude(Node) メソッドは、インクルード ノードを解析するためのエントリ メソッドであり、applyinclude(Node, Properties, boolean) メソッドはコア メソッドです。
/**
* 解析数据库操作节点中的include节点
* @param source 数据库操作节点或其子节点
* @param variablesContext 全局属性信息
* @param included 是否嵌套
*/
private void applyIncludes(Node source, final Properties variablesContext, boolean included) {
if (source.getNodeName().equals("include")) {
// 当前节点是include节点
// 找出被应用的节点
Node toInclude = findSqlFragment(getStringAttribute(source, "refid"), variablesContext);
Properties toIncludeContext = getVariablesContext(source, variablesContext);
// 递归处理被引用节点中的include节点
applyIncludes(toInclude, toIncludeContext, true);
if (toInclude.getOwnerDocument() != source.getOwnerDocument()) {
toInclude = source.getOwnerDocument().importNode(toInclude, true);
}
// 完成include节点的替换
source.getParentNode().replaceChild(toInclude, source);
while (toInclude.hasChildNodes()) {
toInclude.getParentNode().insertBefore(toInclude.getFirstChild(), toInclude);
}
toInclude.getParentNode().removeChild(toInclude);
} else if (source.getNodeType() == Node.ELEMENT_NODE) {
// 元素节点
if (included && !variablesContext.isEmpty()) {
// 用属性值替代变量
NamedNodeMap attributes = source.getAttributes();
for (int i = 0; i < attributes.getLength(); i++) {
Node attr = attributes.item(i);
attr.setNodeValue(PropertyParser.parse(attr.getNodeValue(), variablesContext));
}
}
// 循环到下层节点递归处理下层的include节点
NodeList children = source.getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
applyIncludes(children.item(i), variablesContext, included);
}
} else if (included && source.getNodeType() == Node.TEXT_NODE
&& !variablesContext.isEmpty()) {
// 文本节点
// 用属性值替代变量
source.setNodeValue(PropertyParser.parse(source.getNodeValue(), variablesContext));
}
}
3.6 アノテーションマッピングの分析
通常、XML マッピング ファイルを使用して MyBatis マッピング設定を完了します。同時に、MyBatis はマッピングを構成するためのアノテーションの使用もサポートしており、ビルダー パッケージ内のアノテーション サブパッケージを使用して、この形式のマッピング分析を完了できます。アノテーションを使用してマッピングを構成する方法は、それほど頻繁には使用されないかもしれませんが、このセクションでは、まずこの構成方法を紹介し、次にアノテーション サブパッケージのソース コードを読んで、MyBatis がアノテーション マッピングをどのように解析するかを理解します。
3.6.1 アノテーションの使用
マッピング インターフェイスで抽象メソッドにアノテーションを追加することで、抽象メソッドに関連付けられたデータベース操作ステートメントを宣言できます。
@Select アノテーションに加えて、@Insert、@Update、および @Delete アノテーションも同様の機能を実現できます。
@Select("SELECT * FROM `user` WHERE `id` = #{id}")
User queryUserById(Integer id);
@Select("<script>" +
" SELECT *\n" +
" FROM `user`\n" +
" WHERE id IN\n" +
" <foreach item=\"id\" collection=\"array\" open=\"(\" separator=\",\" close=\")\">\n" +
" #{id}\n" +
" </foreach>\n" +
" </script>")
List<User> queryUsersByIds(int[] ids);
MyBatis は、より柔軟な注釈方法もサポートしています
@SelectProvider(type = UserProvider.class, method = "queryUsersBySchoolName")
List<User> queryUsersBySchoolName(String schoolName);
このようにして、抽象メソッドに @SelectProvider アノテーションを追加できます。アノテーションの type フィールドはクラスを指し、メソッドはクラス内のメソッドを指します。最後に、型クラスのメソッドメソッドによって返された文字列は、queryUserBySchoolName メソッドによってバインドされた SQL ステートメントとして使用されます。
同様に、@SelectProvider アノテーションに加えて、@InsertProvider、@UpdateProvider、および @DeleteProvider の 3 つのアノテーションがあります。
4 つのアノテーション メソッド @Select、@Insert、@Update、および @Delete は直接アノテーション マッピングと呼ばれ、4 つの
アノテーション メソッド @SelectProvider、@InsertProvider、@UpdateProvider、および @DeleteProvider は間接アノテーション マッピングと呼ばれます。
3.6.2 アノテーションマッピングの分析
注釈マッピングの解析は、MapperAnnotationBuilder クラスの parse メソッドから開始されます。このメソッドがトリガーされる前に、MapperAnnotationBuilder クラスは静的コード ブロックでの初期化作業を完了しています。直接注釈マッピングの 4 つの注釈は SQL_ANNOTATION_TYPES 定数に入れられ、間接注釈マッピングの 4 つの注釈は SQL_PROVIDER_ANNOTATION_TYPES 定数に入れられます。 。
static {
SQL_ANNOTATION_TYPES.add(Select.class);
SQL_ANNOTATION_TYPES.add(Insert.class);
SQL_ANNOTATION_TYPES.add(Update.class);
SQL_ANNOTATION_TYPES.add(Delete.class);
SQL_PROVIDER_ANNOTATION_TYPES.add(SelectProvider.class);
SQL_PROVIDER_ANNOTATION_TYPES.add(InsertProvider.class);
SQL_PROVIDER_ANNOTATION_TYPES.add(UpdateProvider.class);
SQL_PROVIDER_ANNOTATION_TYPES.add(DeleteProvider.class);
}
マッパータグの設定が設定ファイルに存在する場合、MapperAnnotationBuilder クラスの parse メソッドがトリガーされ、マッピング インターフェイス ファイルの解析が開始されます。
<mappers>
<mapper resource="com/github/yeecode/mybatisdemo/UserDao.xml"/>
</mappers>
/**
* 解析包含注解的接口文档
*/
public void parse() {
String resource = type.toString();
// 防止重复分析
if (!configuration.isResourceLoaded(resource)) {
// 寻找类名对应的resource路径下是否有xml配置,如果有则解析掉。这样就支持注解和xml混合使用
loadXmlResource();
// 记录资源路径
configuration.addLoadedResource(resource);
// 设置命名空间
assistant.setCurrentNamespace(type.getName());
// 处理缓存
parseCache();
parseCacheRef();
Method[] methods = type.getMethods();
for (Method method : methods) {
try {
// 排除桥接方法
// JDK 1.5 引入泛型后,为了使Java的泛型方法生成的字节码和 1.5 版本前的字节码相兼容,由编译器自动生成的方法,这个就是桥接方法。
// 就是说一个子类在继承(或实现)一个父类(或接口)的泛型方法时,在子类中明确指定了泛型类型,那么在编译时编译器会自动生成桥接方法
if (!method.isBridge()) {
// 解析该方法
parseStatement(method);
}
} catch (IncompleteElementException e) {
// 解析异常的方法暂存起来
configuration.addIncompleteMethod(new MethodResolver(this, method));
}
}
}
// 处理解析异常的方法
parsePendingMethods();
}
コード内の「!method.isBridge()」ステートメントは、ブリッジ メソッドを除外します。ブリッジ メソッドは、ジェネリックスの型消去に一致するようにコンパイラによって自動的に導入されますが、ユーザーが作成したメソッドではないため、除外する必要があります。
parsePendingMethods メソッドでは、インターフェイス メソッドを解析するときに、解析されていない ResultMap 情報、解析されていない名前空間など、読み取られていない他の情報が見つかる可能性があり、その場合、メソッドは incompleteMethods に入れられます。 Configuration クラスの属性。最後に再度処理されます。再度処理するときは、MethodResolver.parseStatement メソッドを使用して、解析に失敗したインターフェイス メソッドを再度解析します。
/**
* 解析该方法。主要是解析该方法上的注解信息
* @param method 要解析的方法
*/
void parseStatement(Method method) {
// 通过子方法获取参数类型
Class<?> parameterTypeClass = getParameterType(method);
// 获取方法的脚本语言驱动
LanguageDriver languageDriver = getLanguageDriver(method);
// 通过注解获取SqlSource
SqlSource sqlSource = getSqlSourceFromAnnotations(method, parameterTypeClass, languageDriver);
if (sqlSource != null) {
// 获取方法上可能存在的配置信息,配置信息由@Options注解指定
Options options = method.getAnnotation(Options.class);
final String mappedStatementId = type.getName() + "." + method.getName();
// 用默认值初始化各项设置
Integer fetchSize = null;
Integer timeout = null;
StatementType statementType = StatementType.PREPARED;
ResultSetType resultSetType = configuration.getDefaultResultSetType();
SqlCommandType sqlCommandType = getSqlCommandType(method);
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
boolean flushCache = !isSelect;
boolean useCache = isSelect;
// 主键自动生成的处理
KeyGenerator keyGenerator;
String keyProperty = null;
String keyColumn = null;
if (SqlCommandType.INSERT.equals(sqlCommandType) || SqlCommandType.UPDATE.equals(sqlCommandType)) {
// first check for SelectKey annotation - that overrides everything else
SelectKey selectKey = method.getAnnotation(SelectKey.class);
if (selectKey != null) {
keyGenerator = handleSelectKeyAnnotation(selectKey, mappedStatementId, getParameterType(method), languageDriver);
keyProperty = selectKey.keyProperty();
} else if (options == null) {
// 这里不能单独配置,因此查看全局配置
keyGenerator = configuration.isUseGeneratedKeys() ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
} else {
keyGenerator = options.useGeneratedKeys() ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
keyProperty = options.keyProperty();
keyColumn = options.keyColumn();
}
} else {
keyGenerator = NoKeyGenerator.INSTANCE;
}
if (options != null) {
// 根据@Options中的配置信息重新设置配置
if (FlushCachePolicy.TRUE.equals(options.flushCache())) {
flushCache = true;
} else if (FlushCachePolicy.FALSE.equals(options.flushCache())) {
flushCache = false;
}
useCache = options.useCache();
fetchSize = options.fetchSize() > -1 || options.fetchSize() == Integer.MIN_VALUE ? options.fetchSize() : null; //issue #348
timeout = options.timeout() > -1 ? options.timeout() : null;
statementType = options.statementType();
if (options.resultSetType() != ResultSetType.DEFAULT) {
resultSetType = options.resultSetType();
}
}
// 返回结果ResultMap处理
String resultMapId = null;
ResultMap resultMapAnnotation = method.getAnnotation(ResultMap.class);
if (resultMapAnnotation != null) {
resultMapId = String.join(",", resultMapAnnotation.value());
} else if (isSelect) {
resultMapId = parseResultMap(method);
}
// 将获取的映射信息存入Configuration
assistant.addMappedStatement(
mappedStatementId,
sqlSource,
statementType,
sqlCommandType,
fetchSize,
timeout,
// ParameterMapID
null,
parameterTypeClass,
resultMapId,
getReturnType(method),
resultSetType,
flushCache,
useCache,
// TODO gcode issue #577
false,
keyGenerator,
keyProperty,
keyColumn,
// DatabaseID
null,
languageDriver,
// ResultSets
options != null ? nullOrEmpty(options.resultSets()) : null);
}
}
parseStatement メソッドは、パラメーターや構成情報などの追加情報を処理します。最も重要なものは、getSqlSourceFromAnnotations メソッドを呼び出して SqlSource オブジェクトを取得することです。
/**
* 通过注解获取SqlSource对象
* @param method 含有注解的方法
* @param parameterType 参数类型
* @param languageDriver 语言驱动
* @return SqlSource对象
*/
private SqlSource getSqlSourceFromAnnotations(Method method, Class<?> parameterType, LanguageDriver languageDriver) {
try {
// 遍历寻找是否有Select、Insert、Update、Delete 四个注解之一
Class<? extends Annotation> sqlAnnotationType = getSqlAnnotationType(method);
// 遍历寻找是否有SelectProvider、insertProvider、UpdateProvider、DeleteProvider四个注解之一
Class<? extends Annotation> sqlProviderAnnotationType = getSqlProviderAnnotationType(method);
if (sqlAnnotationType != null) {
if (sqlProviderAnnotationType != null) {
// 两类注解不可同时使用
throw new BindingException("You cannot supply both a static SQL and SqlProvider to method named " + method.getName());
}
// 含有Select、Insert、Update、Delete 四个注解之一
Annotation sqlAnnotation = method.getAnnotation(sqlAnnotationType);
// 取出value值
final String[] strings = (String[]) sqlAnnotation.getClass().getMethod("value").invoke(sqlAnnotation);
// 基于字符串构建SqlSource
return buildSqlSourceFromStrings(strings, parameterType, languageDriver);
} else if (sqlProviderAnnotationType != null) {
// 含有SelectProvider、insertProvider、UpdateProvider、DeleteProvider四个注解之一
Annotation sqlProviderAnnotation = method.getAnnotation(sqlProviderAnnotationType);
// 根据对应的方法获取SqlSource
return new ProviderSqlSource(assistant.getConfiguration(), sqlProviderAnnotation, type, method);
}
return null;
} catch (Exception e) {
throw new BuilderException("Could not find value method on SQL annotation. Cause: " + e, e);
}
}
直接注釈マッピングの SqlSource オブジェクトは buildSqlSourceFromStrings メソッドによって生成され、間接注釈マッピングの SqlSource オブジェクトは ProviderSqlSource クラスによって生成されます。
3.6.3 直接アノテーションマッピングの分析
直接アノテーション マッピングは、MapperAnnotationBuilder オブジェクトの buildSqlSourceFromStrings メソッドによって完了します。
/**
* 基于字符串创建SqlSource对象
* @param strings 字符串,即直接映射注解中的字符串
* @param parameterTypeClass 参数类型
* @param languageDriver 语言驱动
* @return 创建出来的SqlSource对象
*/
private SqlSource buildSqlSourceFromStrings(String[] strings, Class<?> parameterTypeClass, LanguageDriver languageDriver) {
final StringBuilder sql = new StringBuilder();
for (String fragment : strings) {
sql.append(fragment);
sql.append(" ");
}
return languageDriver.createSqlSource(configuration, sql.toString().trim(), parameterTypeClass);
}
buildSqlSourceFromStrings メソッドの処理は非常に単純で、SQL ステートメントを記述する文字列を直接連結し、処理のために LanguageDriver に渡します。
3.6.4 間接アノテーションマッピングの分析
間接アノテーション マッピングの解析は ProviderSqlSource によって完了します。これを導入する前に、ProviderContext クラスと ProviderMethodResolver クラスの 2 つの補助クラスが導入されます。
- ProviderContext
ProviderContext クラスは非常にシンプルで、内部的に 3 つのプロパティを統合しています。このクラスの機能は、3 つの内部属性を 1 つに統合して、簡単に転送して使用できるようにすることです。
// 提供映射信息的类
private final Class<?> mapperType;
// 提供映射信息的方法,该方法属于mapperType类
private final Method mapperMethod;
// 数据库编号
private final String databaseId;
- ProviderMethodResolver
ProviderMethodResolver は、デフォルト メソッドsolveMethod を持つインターフェイスです。このメソッドの機能は、@*Provider アノテーションの type 属性が指すクラスから、method 属性で指定されたメソッドを検索することです。
/**
* 从@*Provider注解的type属性所指向的类中找出method属性中所指的方法
* @param context 包含@*Provider注解中的type值和method值
* @return 找出的指定方法
*/
default Method resolveMethod(ProviderContext context) {
// 找出同名方法
List<Method> sameNameMethods = Arrays.stream(getClass().getMethods())
.filter(m -> m.getName().equals(context.getMapperMethod().getName()))
.collect(Collectors.toList());
// 如果没有找到指定的方法,则@*Provider注解中的type属性所指向的类中不含有method属性中所指的方法。
if (sameNameMethods.isEmpty()) {
throw new BuilderException("Cannot resolve the provider method because '"
+ context.getMapperMethod().getName() + "' not found in SqlProvider '" + getClass().getName() + "'.");
}
// 根据返回类型再次判断,返回类型必须是CharSequence类或其子类
List<Method> targetMethods = sameNameMethods.stream()
.filter(m -> CharSequence.class.isAssignableFrom(m.getReturnType()))
.collect(Collectors.toList());
if (targetMethods.size() == 1) {
// 方法唯一,返回该方法
return targetMethods.get(0);
}
if (targetMethods.isEmpty()) {
throw new BuilderException("Cannot resolve the provider method because '"
+ context.getMapperMethod().getName() + "' does not return the CharSequence or its subclass in SqlProvider '"
+ getClass().getName() + "'.");
} else {
throw new BuilderException("Cannot resolve the provider method because '"
+ context.getMapperMethod().getName() + "' is found multiple in SqlProvider '" + getClass().getName() + "'.");
}
}
指定されたメソッドを見つけるresolveMethodのプロセスは、主に2つのステップに分かれています。
- 最初のステップは、メソッド名に一致するすべてのメソッドを検索することです。
- 2 番目のステップでは、メソッドの戻り値に基づいてさらなる検証を実行します。
インターフェースのソースコードを読んで解析する際には、インターフェースのデフォルトメソッドで this が参照されていることに注意してください。solveMethod メソッドでは、これは ProviderMethodResolver インターフェイスではなく、メソッドを呼び出すエンティティ オブジェクトを参照します。
// 找出同名方法
List<Method> sameNameMethods = Arrays.stream(getClass().getMethods())
.filter(m -> m.getName().equals(context.getMapperMethod().getName()))
.collect(Collectors.toList());
上記のコードに含まれる「getClass().getMethods()」ステートメントは、「this.getClass().getMethods()」と記述することができます。solveMethod メソッドを呼び出すステートメントは、ProviderSqlSource クラスのコンストラクターです。
if (providerMethodName.length() == 0 && ProviderMethodResolver.class.isAssignableFrom(this.providerType)) {
this.providerMethod = ((ProviderMethodResolver) this.providerType.getDeclaredConstructor().newInstance())
.resolveMethod(new ProviderContext(mapperType, mapperMethod, configuration.getDatabaseId()));
}
したがって、resolveMethod メソッドの this は、providerType オブジェクトを参照する「this.providerType.getDeclaredConstructor().newInstance()」を参照します。ProviderType の代入ステートメントをさらに分析すると、providerType が @*Provider アノテーションの type 属性によって指されるクラスのインスタンスを参照しているという結論に至る可能性があります。
3.6.5 ProviderSqlSource クラス
// SqlSource的子类,能够根据*Provider的信息初始化得到
// 调用入口唯一,在MapperAnnotationBuilder:getSqlSourceFromAnnotations中
public class ProviderSqlSource implements SqlSource {
// Configuration对象
private final Configuration configuration;
// *Provider注解上type属性所指的类
private final Class<?> providerType;
// 语言驱动
private final LanguageDriver languageDriver;
// 含有注解的接口方法
private final Method mapperMethod;
// *Provider注解上method属性所指的方法
private Method providerMethod;
// 给定SQL语句的方法对应的参数
private String[] providerMethodArgumentNames;
// 给定SQL语句的方法对应的参数类型
private Class<?>[] providerMethodParameterTypes;
// ProviderContext对象
private ProviderContext providerContext;
// ProviderContext编号
private Integer providerContextIndex;
}
SqlSource インターフェイスのサブクラスとして、ProviderSqlSource クラスは getBoundSql メソッド (SqlSource インターフェイスの抽象メソッド) を実装します。その実装プロセスは、getBoundSql と createSqlSource の 2 つのメソッドに含まれています。
/**
* 获取一个BoundSql对象
* @param parameterObject 参数对象
* @return BoundSql对象
*/
public BoundSql getBoundSql(Object parameterObject) {
// 获取SqlSource对象
SqlSource sqlSource = createSqlSource(parameterObject);
// 从SqlSource中获取BoundSql对象
return sqlSource.getBoundSql(parameterObject);
}
/**
* 获取一个BoundSql对象
* @param parameterObject 参数对象
* @return SqlSource对象
*/
private SqlSource createSqlSource(Object parameterObject) {
try {
// SQL字符串信息
String sql;
if (parameterObject instanceof Map) {
// 参数是Map
int bindParameterCount = providerMethodParameterTypes.length - (providerContext == null ? 0 : 1);
if (bindParameterCount == 1 &&
(providerMethodParameterTypes[Integer.valueOf(0).equals(providerContextIndex) ? 1 : 0].isAssignableFrom(parameterObject.getClass()))) {
// 调用*Provider注解的type类中的method方法,从而获得SQL字符串
sql = invokeProviderMethod(extractProviderMethodArguments(parameterObject));
} else {
@SuppressWarnings("unchecked")
Map<String, Object> params = (Map<String, Object>) parameterObject;
// 调用*Provider注解的type类中的method方法,从而获得SQL字符串
sql = invokeProviderMethod(extractProviderMethodArguments(params, providerMethodArgumentNames));
}
} else if (providerMethodParameterTypes.length == 0) {
// *Provider注解的type类中的method方法无需入参
sql = invokeProviderMethod();
} else if (providerMethodParameterTypes.length == 1) {
if (providerContext == null) {
// *Provider注解的type类中的method方法有一个入参
sql = invokeProviderMethod(parameterObject);
} else {
// *Provider注解的type类中的method方法入参为providerContext对象
sql = invokeProviderMethod(providerContext);
}
} else if (providerMethodParameterTypes.length == 2) {
sql = invokeProviderMethod(extractProviderMethodArguments(parameterObject));
} else {
throw new BuilderException("Cannot invoke SqlProvider method '" + providerMethod
+ "' with specify parameter '" + (parameterObject == null ? null : parameterObject.getClass())
+ "' because SqlProvider method arguments for '" + mapperMethod + "' is an invalid combination.");
}
Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
// 调用languageDriver生成SqlSource对象
return languageDriver.createSqlSource(configuration, sql, parameterType);
} catch (BuilderException e) {
throw e;
} catch (Exception e) {
throw new BuilderException("Error invoking SqlProvider method '" + providerMethod
+ "' with specify parameter '" + (parameterObject == null ? null : parameterObject.getClass()) + "'. Cause: " + extractRootCause(e), e);
}
}
導入プロセス全体は次の 3 つのステップに要約できます。
- *Provider アノテーションが付けられた型クラスのメソッドメソッドを呼び出して、SQL 文字列を取得します。
- SQL 文字列とその他のパラメーターを languageDriver の createSqlSource メソッドに渡して、新しい SqlSource オブジェクトを生成します。
- 新しく生成された SqlSource オブジェクトの getBoundSql メソッドを呼び出して、BoundSql オブジェクトを取得します。