MybatisMapperインターフェースのソースコード分析
TSMYKJavaテクノロジープログラミング
この記事の最初のアドレスは個人ブログ
https://my.oschina.net/mengyuankan/blog/2873220です。
関連記事
Mybatis分析構成ファイルソースコード分析
Mybatisタイプ変換ソースコード分析
Mybatisデータベース接続プールソースコード分析
序文
Mybatisを使用する場合は、対応するインターフェイス、つまりdaoレイヤーのMapperインターフェイスを記述するだけで済みます。実装クラスを記述しなくても、Mybatisは、インターフェイスの対応するメソッド名に従って、xmlファイルで構成された対応するSQLを見つけることができます。 、メソッドパラメータとSQL xmlのSQLでは、#{0}、#{1}を介してパラメータをバインドでき、#{arg0}、#{arg1}を介してパラメータをバインドすることもできます。 name、ageなどのメソッド内のパラメーター名。#{param1}、#{param2}などでバインドすることもできます。次に、Mybatisのソースコードがどのように実装されているかを見てみましょう。
ソースコード分析
Mybatisでは、マッパーインターフェイスを解析するためのソースコードは主にバインディングパッケージの下にあります。このパッケージには4つのクラスと、メソッドパラメータの名前解決のためのツールクラスParamNameResolverがあります。合計5つのクラスがあります。コードの量は異なります。これらのカテゴリを一度に分析します。
最初にこれらのカテゴリを簡単に見てみましょう。
- BindingException:カスタム例外、無視されます
- MapperMethod:このクラスでは、Mapperインターフェイスの対応するメソッドの情報と対応するSQLステートメントの情報がカプセル化されます。MapperMethodクラスは、Mapperインターフェイスと構成ファイル内のSQLステートメントの間の接続ブリッジと見なすことができます。これらのクラスにあります。最も重要なクラスにはより多くのコードがありますが、他のクラスには非常に少ないコードがあります。
- MapperProxy:Mapperインターフェースのプロキシオブジェクトです。Mybatisを使用する場合、Mapperインターフェースを実装する必要はありません。JDKの動的プロキシを使用して実装されます。
- MapperProxyFactory:MapperProxyクラスのファクトリクラス。MapperProxyの作成に使用されます。
- MapperRegistry:Mybatisインターフェイスとそのプロキシオブジェクトファクトリの登録センターです。Mybatisが初期化されると、このクラスに登録するための構成ファイルとMapperインターフェイス情報が読み込まれます。
- ParamNameResolverこのクラスは、バインディングパッケージの下のクラスではなく、リフレクションパッケージの下のツールクラスです。主に、インターフェイスメソッドのパラメーターを解決するために使用されます。
次に、各クラスの実装プロセスを見てください。
MapperProxy
まず、Mapperインターフェイスのプロキシオブジェクトであり、InvocationHandlerインターフェイスを実装するMapperProxyクラスを確認します。つまり、JDKの動的プロキシを使用してMapperインターフェイスのプロキシオブジェクトを生成します。
public class MapperProxy<T> implements InvocationHandler, Serializable {
// 关联的 sqlSession 对象
private final SqlSession sqlSession;
// 目标接口,即 Mapper 接口对应的 class 对象
private final Class<T> mapperInterface;
// 方法缓存,用于缓存 MapperMethod对象,key 为 Mapper 接口中对应方法的 Method 对象,value 则是对应的 MapperMethod,MapperMethod 会完成参数的转换和 SQL 的执行功能
private final Map<Method, MapperMethod> methodCache;
public MapperProxy(SqlSession sqlSession, Class<T> mapperInterface, Map<Method, MapperMethod> methodCache) {
this.sqlSession = sqlSession;
this.mapperInterface = mapperInterface;
this.methodCache = methodCache;
}
// 代理对象执行的方法,代理以后,所有 Mapper 的方法调用时,都会调用这个invoke方法
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 并不是每个方法都需要调用代理对象进行执行,如果这个方法是Object中通用的方法,则无需执行
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
// 如果是默认方法,则执行默认方法,Java 8 提供了默认方法
} else if (isDefaultMethod(method)) {
return invokeDefaultMethod(proxy, method, args);
}
// 从缓存中获取 MapperMethod 对象,如果缓存中没有,则创建一个,并添加到缓存中
final MapperMethod mapperMethod = cachedMapperMethod(method);
// 执行方法对应的 SQL 语句
return mapperMethod.execute(sqlSession, args);
}
// 缓存 MapperMethod
private MapperMethod cachedMapperMethod(Method method) {
MapperMethod mapperMethod = methodCache.get(method);
if (mapperMethod == null) {
mapperMethod = new MapperMethod(mapperInterface, method, sqlSession.getConfiguration());
methodCache.put(method, mapperMethod);
}
return mapperMethod;
}
}
上記はMapperProxyプロキシクラスのメインコードです。MapperMethodの関連メソッドが呼び出されることに注意してください。MapperMethodは後で分析されます。まず、メソッドパラメータと実行メソッドの変換に対応するSQLであることを確認してください。 。もう1つのポイント、ターゲットメソッドを実行する場合、オブジェクト内のメソッドの場合はターゲットメソッドが直接実行され、デフォルトメソッドの場合はデフォルトメソッドの関連ロジックが実行されます。それ以外の場合はターゲットメソッドが実行されます。プロキシオブジェクトを使用して実行される
MapperProxyFactory
上記のMapperProxyプロキシクラスを読み取った後、MapperProxyFactoryを使用して、ファクトリクラスであるプロキシクラスを作成します。
public class MapperProxyFactory<T> {
// 当前的 MapperProxyFactory 对象可以创建的 mapperInterface 接口的代理对象
private final Class<T> mapperInterface;
// MapperMetho缓存
private final Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<Method, MapperMethod>();
// 创建 mapperInterface 的代理对象
@SuppressWarnings("unchecked")
protected T newInstance(MapperProxy<T> mapperProxy) {
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
public T newInstance(SqlSession sqlSession) {
final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
}
主にマッパーインターフェイスのプロキシオブジェクトを作成するために使用されるファクトリクラス。コードは単純で、言うことは何もありません。
MapperRegistry
MapperRegistry Mapperインターフェースとそれに対応するプロキシファクトリオブジェクトの登録センターです。Mybatisが初期化されると、構成ファイルとMapperインターフェース情報が読み込まれ、MapperRegistryクラスに登録されます。特定のSQLを実行する必要がある場合は、最初にレジストリから取得されます。マッパーインターフェイスのプロキシオブジェクト。
public class MapperRegistry {
// 配置对象,包含所有的配置信息
private final Configuration config;
// 接口和代理对象工厂的对应关系,会用工厂去创建接口的代理对象
private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<Class<?>, MapperProxyFactory<?>>();
// 注册 Mapper 接口
public <T> void addMapper(Class<T> type) {
// 是接口,才进行注册
if (type.isInterface()) {
// 如果已经注册过了,则抛出异常,不能重复注册
if (hasMapper(type)) {
throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
}
// 是否加载完成的标记
boolean loadCompleted = false;
try {
// 会为每个 Mapper 接口创建一个代理对象工厂
knownMappers.put(type, new MapperProxyFactory<T>(type));
// 下面这个先不看,主要是xml的解析和注解的处理
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
parser.parse();
loadCompleted = true;
} finally {
// 如果注册失败,则移除掉该Mapper接口
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}
// 获取 Mapper 接口的代理对象,
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
// 从缓存中获取该 Mapper 接口的代理工厂对象
final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
// 如果该 Mapper 接口没有注册过,则抛异常
if (mapperProxyFactory == null) {
throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
}
//使用代理工厂创建 Mapper 接口的代理对象
return mapperProxyFactory.newInstance(sqlSession);
}
}
上記はMapperRegistryが登録したコードで、主にMapperインターフェースを登録し、理解しやすいMapperインターフェースのプロキシオブジェクトを取得します。
ParamNameResolver
ParamNameResolverは、バインディングパッケージの下のクラスではなく、リフレクションパッケージの下のツールクラスです。主に、インターフェイスメソッドのパラメーターを解決するために使用されます。
つまり、メソッドパラメータでは、パラメータが最初のパラメータです。つまり、パラメータ名とインデックスの間の対応する関係が解析されます。このクラスのコードは、上記のいくつかのクラスよりも複雑で、少しです。複雑です。理解しやすいようにmainメソッドを作成しましたが、コードの量はそれほど多くありません。
このクラスの主なメソッドは、主に構築メソッドとgetNamedParamsメソッドです。最初に他のメソッドを見てみましょう。
public class ParamNameResolver {
// 参数前缀,在 SQL 中可以通过 #{param1}之类的来获取
private static final String GENERIC_NAME_PREFIX = "param";
// 参数的索引和参数名称的对应关系,有序的,最重要的一个属性
private final SortedMap<Integer, String> names;
// 参数中是否有 @Param 注解
private boolean hasParamAnnotation;
// 获取对应参数索引实际的名称,如 arg0, arg1,arg2......
private String getActualParamName(Method method, int paramIndex) {
Object[] params = (Object[]) GET_PARAMS.invoke(method);
return (String) GET_NAME.invoke(params[paramIndex]);
}
// 是否是特殊参数,如果方法参数中有 RowBounds 和 ResultHandler 则会特殊处理,不会存入到 names 集合中
private static boolean isSpecialParameter(Class<?> clazz) {
return RowBounds.class.isAssignableFrom(clazz) || ResultHandler.class.isAssignableFrom(clazz);
}
// 返回所有的参数名称 (toArray(new String[0])又学习了一种新技能)
public String[] getNames() {
return names.values().toArray(new String[0]);
}
上記のコードは、ParamNameResolverのいくつかの補助メソッドです。最も重要なのはnames属性で、パラメーターインデックスとパラメーター名の間の対応を格納するために使用されます。これはマップですが、例外があります。パラメーターに2つのタイプが含まれる場合RowBoundsとResultHandlerの場合、次のように、インデックスと対応する関係を名前コレクションに入れません。
aMethod(@Param("M") int a, @Param("N") int b) -- names = {{0, "M"}, {1, "N"}}
aMethod(int a, int b) -- names = {{0, "0"}, {1, "1"}}
aMethod(int a, RowBounds rb, int b) -- names = {{0, "0"}, {2, "1"}}
構築メソッドでは、ParamNameResolverの構築メソッドを見てみましょう。このタイプのオブジェクトを作成するために構築メソッドが呼び出されると、メソッドのパラメーターが解析され、解析結果が名前データに配置されます。コードは次のとおりです。次のように:
フォーカス
public ParamNameResolver(Configuration config, Method method) {
// 方法所有参数的类型
final Class<?>[] paramTypes = method.getParameterTypes();
// 所有的方法参数,包括 @Param 注解的参数
final Annotation[][] paramAnnotations = method.getParameterAnnotations();
// 参数索引和参数名称的对应关系
final SortedMap<Integer, String> map = new TreeMap<Integer, String>();
int paramCount = paramAnnotations.length;
// get names from @Param annotations
for (int paramIndex = 0; paramIndex < paramCount; paramIndex++) {
// 不处理 RowBounds 和 ResultHandler 这两种特殊的参数
if (isSpecialParameter(paramTypes[paramIndex])) {
continue;
}
String name = null;
for (Annotation annotation : paramAnnotations[paramIndex]) {
// 如果参数被 @Param 修饰
if (annotation instanceof Param) {
hasParamAnnotation = true;
// 则参数名称取其值
name = ((Param) annotation).value();
break;
}
}
// 如果是一般的参数
if (name == null) {
// 是否使用真实的参数名称,true 使用,false 跳过
if (config.isUseActualParamName()) {
// 如果为 true ,则name = arg0, arg1 之类的
name = getActualParamName(method, paramIndex);
}
// 如果上述为false,
if (name == null) {
// name为参数索引,0,1,2 之类的
name = String.valueOf(map.size());
}
}
// 存入参数索引和参数名称的对应关系
map.put(paramIndex, name);
}
// 赋值给 names 属性
names = Collections.unmodifiableSortedMap(map);
}
上記の構造を読んだ後、mainメソッドをテストします。次のメソッドがあります。
Person queryPerson(@Param("age") int age, String name, String address, @Param("money") double money);
上記のメソッドで、本名が使用されている場合、つまりconfig.isUseActualParamName()がtrueの場合、解析後、names属性は次のように出力されます。
{0=age, 1=arg1, 2=money, 3=arg3}
config.isUseActualParamName()がfalseの場合、解析後、names属性は次のように出力されます。
{0=age, 1=1, 2=money, 3=3}
メソッドのパラメーターが解析されたので、着信メソッドのパラメーター値が入った場合、それを取得するにはどうすればよいですか?これは、このクラスのgetNamedParamsメソッドです。
public Object getNamedParams(Object[] args) {
// 参数的个数
final int paramCount = names.size();
if (args == null || paramCount == 0) {
return null;
// 如果参数没有被 @Param 修饰,且只有一个,则直接返回
} else if (!hasParamAnnotation && paramCount == 1) {
return args[names.firstKey()];
} else {
// 参数名称和参数值的对应关系
final Map<String, Object> param = new ParamMap<Object>();
int i = 0;
for (Map.Entry<Integer, String> entry : names.entrySet()) {
// key = 参数名称,value = 参数值
param.put(entry.getValue(), args[entry.getKey()]);
final String genericParamName = "param"+ String.valueOf(i + 1);
// 默认情况下它们将会以它们在参数列表中的位置来命名,比如:#{param1},#{param2}等
if (!names.containsValue(genericParamName)) {
param.put(genericParamName, args[entry.getKey()]);
}
i++;
}
// 返回参数名称和参数值的对应关系,是一个 map
return param;
}
}
次に、mainメソッドでテストします。
config.isUseActualParamName()がtrueで、メソッドがコンストラクターによって解析される場合、パラメーターインデックスと名前の間の対応する関係は次のとおりです。
{0=age, 1=arg1, 2=money, 3=arg3}
現在、パラメータは次のとおりです。
Object[] argsArr = {24, "zhangsan", 1000.0, "chengdou"};
ここで、getNamedParamsメソッドを呼び出して、メソッド名とメソッド値をバインドします。得られる結果は次のとおりです。メソッドは、実際にはマップであるオブジェクトを返すことに注意してください。
{age=24, param1=24, arg1=zhangsan, param2=zhangsan, money=1000.0, param3=1000.0, arg3=chengdou, param4=chengdou}
したがって、XMLのSQLでは、対応する#{name}を介して値を取得するか、#{param1}などを介して値を取得できます。
別のケースでは、config.isUseActualParamName()はfalseです。解析後、パラメーターのインデックスと名前の対応は次のようになります。
{0=age, 1=1, 2=money, 3=3}
その後、パラメーターとパラメーター値のバインディングはどのようになりますか?mainメソッドは引き続き次のようにテストされ、パラメーターは上記のargsArrパラメーターです。
{age=24, param1=24, 1=zhangsan, param2=zhangsan, money=1000.0, param3=1000.0, 3=chengdou param4=chengdou}
SQLでは、パラメータ値は#{0}、#{1}などからも取得できます。
したがって、ParamNameResolverクラスがMapperインターフェイスを解析した後、#{name}、#{param1}、#{0}などのメソッドを使用して、SQLで対応するパラメーター値を取得できます。
MapperMethod
上記のParamNameResolverツールクラスを理解すると、MapperMethodを簡単に理解できます。
このクラスでは、Mapperインターフェイスの対応するメソッドの情報と対応するSQLステートメントの情報がカプセル化されます。MapperMethodクラスは、Mapperインターフェイスと構成ファイル内のSQLステートメントの間の接続ブリッジと見なすことができます。
このクラスには、それぞれSqlCommandとMethodSignatureの2つの内部クラスに対応する2つのプロパティしかありません。SqlCommandはSQLステートメントの名前とタイプを記録し、MethodSignatureはマッパーインターフェイスの対応するメソッド情報です。
public class MapperMethod {
private final SqlCommand command;
private final MethodSignature method;
public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
this.command = new SqlCommand(config, mapperInterface, method);
this.method = new MethodSignature(config, mapperInterface, method);
}
}
まず、これら2つの内部クラスを見てみましょう。
SqlCommand
public static class SqlCommand {
// SQL 的名称,是接口的全限定名+方法名组成
private final String name;
// SQL 的类型,取值:UNKNOWN, INSERT, UPDATE, DELETE, SELECT, FLUSH;
private final SqlCommandType type;
public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) {
// SQL 名称
String statementName = mapperInterface.getName() + "." + method.getName();
// MappedStatement 封装了 SQL 语句的相关信息
MappedStatement ms = null;
// 在配置文件中检测是否有该 SQL 语句
if (configuration.hasStatement(statementName)) {
ms = configuration.getMappedStatement(statementName);
} else if (!mapperInterface.equals(method.getDeclaringClass())) {
// 是否父类中有该 SQL 的语句
String parentStatementName = method.getDeclaringClass().getName() + "." + method.getName();
if (configuration.hasStatement(parentStatementName)) {
ms = configuration.getMappedStatement(parentStatementName);
}
}
// 处理 @Flush 注解
if (ms == null) {
if(method.getAnnotation(Flush.class) != null){
name = null;
type = SqlCommandType.FLUSH;
}
} else {
// 获取 SQL 名称和类型
name = ms.getId();
type = ms.getSqlCommandType();
}
}
MethodSignature
public static class MethodSignature {
private final boolean returnsMany; // 方法的返回值为 集合或数组
private final boolean returnsMap; // 返回值为 map
private final boolean returnsVoid; // void
private final boolean returnsCursor; // Cursor
private final Class<?> returnType; // 方法的返回类型
private final String mapKey; // 如果返回值为 map,则该字段记录了作为 key 的列名
private final Integer resultHandlerIndex; // ResultHandler 参数在参数列表中的位置
private final Integer rowBoundsIndex; // RowBounds参数在参数列表中的位置
// 用来解析接口参数,上面已介绍过
private final ParamNameResolver paramNameResolver;
public MethodSignature(Configuration configuration, Class<?> mapperInterface, Method method) {
// 方法的返回值类型
Type resolvedReturnType = TypeParameterResolver.resolveReturnType(method, mapperInterface);
if (resolvedReturnType instanceof Class<?>) {
this.returnType = (Class<?>) resolvedReturnType;
} else if (resolvedReturnType instanceof ParameterizedType) {
this.returnType = (Class<?>) ((ParameterizedType) resolvedReturnType).getRawType();
} else {
this.returnType = method.getReturnType();
}
this.returnsVoid = void.class.equals(this.returnType);
this.returnsMany = (configuration.getObjectFactory().isCollection(this.returnType) || this.returnType.isArray());
this.returnsCursor = Cursor.class.equals(this.returnType);
this.mapKey = getMapKey(method);
this.returnsMap = (this.mapKey != null);
this.rowBoundsIndex = getUniqueParamIndex(method, RowBounds.class);
this.resultHandlerIndex = getUniqueParamIndex(method, ResultHandler.class);
this.paramNameResolver = new ParamNameResolver(configuration, method);
}
// 根据参数值来获取参数名称和参数值的对应关系 是一个 map,看上面的main方法测试
public Object convertArgsToSqlCommandParam(Object[] args) {
return paramNameResolver.getNamedParams(args);
}
}
MapperMethod
次に、MapperMethodを見てみましょう。コアメソッドはexecuteメソッドであり、メソッドに対応するSQLを実行するために使用されます。
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
switch (command.getType()) {
case INSERT: {
// insert 语句,param 为 参数名和参数值的对应关系
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: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.delete(command.getName(), param));
break;
}
case SELECT:
if (method.returnsVoid() && method.hasResultHandler()) {
// void 类型且方法有 ResultHandler 参数,调用 sqlSession.select 执行
executeWithResultHandler(sqlSession, args);
result = null;
} else if (method.returnsMany()) {
// 返回集合或数组,调用 sqlSession.<E>selectList 执行
result = executeForMany(sqlSession, args);
} else if (method.returnsMap()) {
// 返回 map ,调用 sqlSession.<K, V>selectMap
result = executeForMap(sqlSession, args);
} else if (method.returnsCursor()) {
result = executeForCursor(sqlSession, args);
} else {
Object param = method.convertArgsToSqlCommandParam(args);
result = sqlSession.selectOne(command.getName(), param);
}
break;
case FLUSH:
result = sqlSession.flushStatements();
break;
default:
throw new BindingException("Unknown execution method for: " + command.getName());
}
return result;
}
上記は、対応するインターフェイスの名前とパラメータを取得し、sqlSessionの対応するメソッド値を呼び出し、対応するSQLを実行して結果を取得するMapperMethodクラスの主な実装です。このクラスにはいくつかの補助メソッドがあります。これは無視できます。
総括する
上記は、マッパーインターフェイス、つまりバインディングモジュールの基礎となる分析です。Mybatisは、JDKの動的プロキシを使用して各マッパーインターフェイスのプロキシオブジェクトを作成し、ParamNameResolverツールクラスを使用してマッパーのパラメーターを解析します。インターフェイス。XMLのSQLが3つを使用できるようにします。パラメータの値を取得するには、#{name}、#{0}、および#{param1}の2つの方法があります。インターフェイスパラメータの解析が完了すると、 MapperMethodのexecuteメソッドを使用してインターフェイスの名前を呼び出します。つまり、sqlSessionのSQL関連メソッドの対応する名前とパラメーターを呼び出してSQLを実行し、結果を取得します。