MyBatis精髓揭秘:Mapper代理实现的黑盒探索

简介: MyBatis精髓揭秘:Mapper代理实现的黑盒探索

前言

利用 Mybatis 框架,我们只要提供一个 Mapper接口,定义好相应的方法,再利用 XML 文件,就可以调用 Mapper 接口的方法来实现SQL语句的查询,这其中是如何实现的呢?我们仅仅是定义了一个接口,并没有为它创建任何的实现类,那么为什么我们还是可以成功的执行这个方法呢?

从最初我写过的入门的文章里面可以看到,即使没有 Java 接口,也可以直接使用 sqlSession 来调用 Mapper.xml 映射文件里面的语句执行数据库的操作,只要定位到映射文件中正确的 namespace + id 即可,这是原始的 ibatis 编程模型。

List<Account> list = sqlSession.selectList("world.xuewei.mybatis.dao.AccountDao.getAll");
System.out.println(list);

那么有了 Java 接口之后,我们直接调用接口,底层实际上就会走对应的映射文件里面的方法。这个映射关系是 Mybatis 框架帮我们做的,这个过程中框架做了什么呢?

AccountDao accountDao = sqlSession.getMapper(AccountDao.class);
System.out.println(accountDao.getAll());

本文我们将带着这个问题,结合源码来解答。

主要阶段

初始化阶段

Configuration
public class Configuration { 
    protected final MapperRegistry mapperRegistry = new MapperRegistry(this);
    
    public <T> void addMapper(Class<T> type) {
        mapperRegistry.addMapper(type);
    }
    
    // 注册 Mapper 接口
    public void addMappers(String packageName) {
        mapperRegistry.addMappers(packageName);
    }
    
    // 获取指定 Mapper 的代理
    public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
        return mapperRegistry.getMapper(type, sqlSession);
    }
}

在 Mybatis 初始化阶段,会解析 mybatis-config.xml 文件,实例化出单例的 Configuration 对象,并调用其 addMappers 方法将指定包下满足条件的所有 Mapper 接口都注册到 MapperRegistry 中(或者在解析 mapper.xml 文件阶段,以 namaspace 命名空间值,调用 configuration.addMapper 方法注册)。

MapperRegistry

Mapper 注册表,内部持有所有满足条件的 Mapper 接口的 Mapper 代理工厂实例集合,并以 Mapper 接口的 Class 名作为 key,MapperProxyFactory 作为 value。

public class MapperRegistry {
    private final Configuration config;
    
    // 保存了 Mapper 接口类型和该类型对应的 MapperProxyFactory 之间的关联关系,实际上就是记录了接口类型和动态代理工厂之间的关系
    // 由此就可以很快得找到一个类型应该要哪一个工厂来创建代理实例
    private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<Class<?>, MapperProxyFactory<?>>();
    // 注册指定包下,满足指定超类的所有 Mapper 接口
    public void addMappers(String packageName, Class<?> superType) {
        ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<Class<?>>();
        resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
        Set<Class<? extends Class<?>>> mapperSet = resolverUtil.getClasses();
        for (Class<?> mapperClass : mapperSet) {
            // 遍历注册
            addMapper(mapperClass);
        }
    }
    // 注册 Mapper 接口,添加 Mapper,实际上就是把这个 Mapper 类型和它对应的代理工厂保存到 knownMappers 这个 Map 里面去
    public <T> void addMapper(Class<T> type) {
        // Class 代表接口才处理,否则不处理
        if (type.isInterface()) {
            if (hasMapper(type)) {
                // 重复添加抛出异常
                throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
            }
            boolean loadCompleted = false;
            try {
                // 添加到 knownMappers 集合中
                knownMappers.put(type, new MapperProxyFactory<T>(type));
                // It's important that the type is added before the parser is run
                // otherwise the binding may automatically be attempted by the
                // mapper parser. If the type is already known, it won't try.
                // 解析接口上的注解信息,并添加至 configuration 对象
                MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
                parser.parse();
                loadCompleted = true;
            } finally {
                if (!loadCompleted) {
                    knownMappers.remove(type);
                }
            }
        }
    }
    // getMapper 方法主要提供给 SqlSession,SqlSession 的 getMapper 底层就是走这个方法
    public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
        // 1. 先获取以 Mapper 的 Class 类型作为 key 对应的 MapperProxyFactory 实例
        final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
        if (mapperProxyFactory == null) {
            // 没有工厂则报错。有时候我们忘记在 mybatis 的主配置文件 的mapper 节点添加对应的映射文件的时候,就会抛出这个错误
            throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
        }
        try {
            // 2. 调用 MapperProxyFactory 的 newInstance 方法创建代理 Mapper 对象并返回
            return mapperProxyFactory.newInstance(sqlSession);
        } catch (Exception e) {
            throw new BindingException("Error getting mapper instance. Cause: " + e, e);
        }
    }
    //省略其他方法... 

从这里可以看出 Mybatis 初始化阶段,就已经将所有满足条件的 Mapper 以接口 Class 作为 key,MapperProxyFactory 对象作为 value 存入到 Map 集合中了。

接下来就是程序调用 configuration.getMapper(...) 方法获取代理 Mapper 对象,并注入到 Spring 的上下文中。

代理阶段

在初始化阶段主要完成了配置的初始化,代理阶段主要是封装 Mybatis 的编程模型,完成相关的工作以满足通过 Java 接口访问数据库的功能,代理阶段主要是在 binding 模块实现的,该模块通过读取配置信息,然后通过动态代理来实现面向接口的数据库操作。

在 Mybatis 中我们是面向 SqlSession 编程,getMapper 方法是获取代理对象的开始,这就是我们等下分析的代码入口。getMapper 方法里面找到了全局的配置对象,全局的配置对象里面在初始化的过程中已经注册了很多 Mapper 对象在里面去了,维护了一个 MapperRegister,因此这个 binding 的过程是依赖于之前的初始化过程的。

AccountDao accountDao = sqlSession.getMapper(AccountDao.class);
System.out.println(accountDao.getAll());
MapperProxyFactory

这是 Mapper 代理对象的工厂类,负责 Mapper 接口代理对象的创建工作。上面提到,在初始化阶段就已经将所有满足条件的 Mapper 以接口 Class 作为 key,MapperProxyFactory 对象作为 value 存储好了。也就是对应 MapperRegistry 的 addMapper 方法中的这一句:

knownMappers.put(type, new MapperProxyFactory<T>(type));
• 1

接下来我们来仔细看一下 MapperProxyFactory 类。

public class MapperProxyFactory<T> {
    
    // 持有需要创建代理的 Mapper 接口的类型
    private final Class<T> mapperInterface;
    // 持有一个空的以 Method 为 key,MapperMethod 对象为 value 的 Map 集合
    private final Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<Method, MapperMethod>();
    // 构造函数
    public MapperProxyFactory(Class<T> mapperInterface) {
        this.mapperInterface = mapperInterface;
    }
    // 省略相应属性的 get 方法...
    protected T newInstance(MapperProxy<T> mapperProxy) {
        // 创建代理对象,参数传递的 mapperProxy 就是 InvocationHandler 的实现类(也就是实现代理逻辑的类)
        return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
    }
    public T newInstance(SqlSession sqlSession) {
        // new 了一个 MapperProxy 作为 Mapper 代理对象的 handler
        final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
        return newInstance(mapperProxy);
    }
}

通过 newInstance 方法可以看到,实际上是利用 JDK 的动态代理创建了每个 Mapper 接口的一个代理类,其 handler 相关的逻辑交给了 MapperProxy。

MapperProxy

一个标准的 InvocationHandler,不仅兼任代理 handler 的职责,还持有方法缓存 Map 以及 Mybatis 中重要的 sqlSession 对象。MapperProxy 实现了 InvocationHandler 接口,是 Mapper 接口的代理,对接口功能进行了增强。

public class MapperProxy<T> implements InvocationHandler, Serializable {
    // 持有属性
    // 关联的 SqlSession 对象
    private final SqlSession sqlSession;
    // Mapper 接口的类型
    private final Class<T> mapperInterface;
    // Mapper 接口的方法缓存集合,以 Method 为 key,MapperMethod 实例为 value
    // MapperMethod 不存储任何信息,因此可以在多个代理对象之间共享
    private final Map<Method, MapperMethod> methodCache;
    // 省略构造函数...
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        try {
            // 如果是 Object 类的方法,那么就直接调用即可
            if (Object.class.equals(method.getDeclaringClass())) {
                return method.invoke(this, args);
            } else if (isDefaultMethod(method)) {
                return invokeDefaultMethod(proxy, method, args);
            }
        } catch (Throwable t) {
            throw ExceptionUtil.unwrapThrowable(t);
        }
        // 重要的 invoke 逻辑来了
        // 这里解释下,当我们调用 Mapper 代理对象的方法,比如 accountDao.getAll() 时,会执行到这里
        // 执行 cachedMapperMethod 方法以获取缓存的 MapperMethod 实例,再将具体的执行逻辑交给 MapperMethod 处理
        // 获取缓存的 MapperMethod 映射方法,缓存中没有则会创建一个并加到缓存
        final MapperMethod mapperMethod = cachedMapperMethod(method);
        // 执行 sql(MapperMethod 内部包含接口方法和参数,sql 等信息,可以直接执行 sql)
        return mapperMethod.execute(sqlSession, args);
    }
    // 尝试根据此次调用的方法作为 key,获取已缓存的 MapperMethod 实例
    private MapperMethod cachedMapperMethod(Method method) {
        MapperMethod mapperMethod = methodCache.get(method);
        // 没有就 new 一个出来
        if (mapperMethod == null) {
            // 重要的就是这个 new MapperMehtod 的方法
            mapperMethod = new MapperMethod(mapperInterface, method, sqlSession.getConfiguration());
            // new 之后放入 methodCache 中缓存起来
            methodCache.put(method, mapperMethod);
        }
        return mapperMethod;
    }
}

代码分析到了这里,我们可以知道当我们在我们的 service 层利用 @Autowired 注解拿到 Mapper 代理对象之后,第一次调用它的某个(比如查询)方法时,会进入 MapperProxy 的 invoke 方法中,并调用 new 出来的 MapperMethod 的 execute 方法执行真正的 SQL 调用,之后再调用同一个查询方法时,就不会再 new MapperMethod 实例了,而是从 mehtodCache 这个 map 缓存中获取,以提高性能。

接下来看看 mapperMethod.execute 到底是如何实现的。

MapperMethod

每个 Mapper 接口中定义的查询/删除/新增/更新方法都对应一个 MapperMethod 实例,该实例持有两个重要的内部类 SqlCommand 和 MehtodSignature 属性,通过这两个内部类,就可以囊括所有查询 SQL 之前所需的各种基础信息。

public class MapperMethod {
    // 持有 SQL 命令相关,主要两个属性一个 name 一个 type
    // sqlCommand 是对 sql 语句封装,从配置对象中获取方法的命名空间,方法名称和 sql 语句类型
    private final SqlCommand command;
    // Java 方法签名相关
    // 封装 mapper 接口方法的相关信息(入参和返回值类型)
    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);
    }
    
    // ...
}
SqlCommand
public class MapperMethod {
  // ...
    // 内部类 SqlCommand
    public static class SqlCommand {
        // sql 的名称,name 属性并不是方法名哦,而是 MappedStatement 实例的id属性(命名空间+方法名称),至于怎么获取这个值,下面会分析
        private final String name;
        // sql语句的类型,对应 MappedStatement 实例的 sqlCommandType 属性值,是一个枚举类型,表示这个 SQL 是 新增/删除/更新/查询等类型
        // UNKNOWN, INSERT, UPDATE, DELETE, SELECT, FLUSH
        private final SqlCommandType type;
        // 构造函数,就是想办法初始化上面的两个属性
        public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) {
            final String methodName = method.getName();
            final Class<?> declaringClass = method.getDeclaringClass();
            // 根据方法名以及方法所声明的 class 的类型以及 Mapper 代理类接口的类型去获取 configuration 中缓存的 MappedStatement 实例
            MappedStatement ms = resolveMappedStatement(mapperInterface, methodName, declaringClass,
                                                        configuration);
            // 若 MappedStatement 实例不存在,则判断方法是否含有 @Flush 注解  
            if (ms == null) {
                if (method.getAnnotation(Flush.class) != null) {
                    // 存在该注解,则将 name 置为 null,type 置为 SqlCommandType.FLUSH
                    name = null;
                    type = SqlCommandType.FLUSH;
                } else {
                    // 否则就抛出异常,找不到方法对应的 statement 实例
                    throw new BindingException("Invalid bound statement (not found): "
                                               + mapperInterface.getName() + "." + methodName);
                }
            } else {
                // 找到了对应的 MappedStatement 实例,则取其 id 属性作为 name,sqlCommandType 属性作为 type
                name = ms.getId();
                type = ms.getSqlCommandType();
                // 注意这里的 sqlCommandType 属性值若为 UNKNOWN 类型,则会抛错
                if (type == SqlCommandType.UNKNOWN) {
                    throw new BindingException("Unknown execution method for: " + name);
                }
            }
        }
        // 下面我们重点看其是如何根据 Method 就能找到对应的 MappedStatement 对象的
        private MappedStatement resolveMappedStatement(Class<?> mapperInterface, String methodName,
                                                       Class<?> declaringClass, Configuration configuration) {
            // 拼接 Mapper 接口名.方法名    
            String statementId = mapperInterface.getName() + "." + methodName;
            if (configuration.hasStatement(statementId)) {
                // 从 configuration 中根据上面拼接后的作为 statementId 查询
                return configuration.getMappedStatement(statementId);
                // 当前方法申明的类型就是 Mapper 接口的类型,说明找不到,返回 null    
            } else if (mapperInterface.equals(declaringClass)) {
                return null;
            }
            // 说明当前调用的方法可能是父类 Mapper 接口中定义的,那就递归调用直到找到对应的 MappedStatement 对象
            for (Class<?> superInterface : mapperInterface.getInterfaces()) {
                if (declaringClass.isAssignableFrom(superInterface)) {
                    MappedStatement ms = resolveMappedStatement(superInterface, methodName,
                                                                declaringClass, configuration);
                    if (ms != null) {
                        return ms;
                    }
                }
            }
            return null;
        }
        
        // ...
}

源码分析到这里我们可以看出 Mybatis 中 Mapper 接口是支持多层继承关系的,至此整个 SqlCommand 对象实例化完成,该有的属性也已经赋值完毕,接下来我们看 MethodSignature 这个内部类的实例化过程。

MethodSignature
public static class MethodSignature {
    // 返回参数是否为集合或者数组
    private final boolean returnsMany;
    // 返回参数是否为 map
    private final boolean returnsMap;
    // 返回值是否为空
    private final boolean returnsVoid;
    // 返回值是否为游标类型
    private final boolean returnsCursor;
    // 如果指定了 resultHandler,那么对应的参数 index 是多少,类似的还有 rowBounds 等
    private final Class<?> returnType;
    private final String mapKey;
    private final Integer resultHandlerIndex;
    private final Integer rowBoundsIndex;
    // 方法参数解析器,重要的属性来了,重点看这个属性的初始化过程
    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);
    }
    
    // ...
}

MethodSignature 内部类的实例化中有一个重要的属性 ParamNameResolver 对象,它会事先将 Method 的方法中的参数按照一定的规则先解析好,保存起来,将来执行 SQL 之前可以根据代理 invoke 方法中传入的 Object[] args 参数,生成真正 SQL 语句执行时需要的(paranName,paramValue),以方便使用。

ParamNameResolver

参数名称解析器,内部持有一个 SortedMap names 属性,利用 Java 的反射,事先将 Mapper 代理对象的 Method 方法的参数解析出来并缓存,以备将来 SQL 执行之前使用。

public class ParamNameResolver {
    private static final String GENERIC_NAME_PREFIX = "param";
    // names 属性有以下特征:
    // key 为参数列表的 index,value 则为参数的 name。
    // 参数 name 是从 @Param 注解获得的,当没有指定该注解时,则以 String 形式的 index 作为 value。
    // 当参数中存在特殊参数(RowBounds/ResultHandler)时,会跳过特殊参数。
    // 举几个例子:
    // aMethod(@Param("M") int a, @Param("N") int b)  --> {{0, "M"}, {1, "N"}}
    // aMethod(int a, int b) --> {{0, "0"}, {1, "1"}}
    // aMethod(int a, RowBounds rb, int b) --> {{0, "0"}, {2, "1"}}
    
    private final SortedMap<Integer, String> names;
    private boolean hasParamAnnotation;
    // 构造函数
    public ParamNameResolver(Configuration config, Method method) {
        final Class<?>[] paramTypes = method.getParameterTypes();
        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++) {
            if (isSpecialParameter(paramTypes[paramIndex])) {
                // 跳过特殊参数,RowBounds 或者 ResultHandler
                continue;
            }
            String name = null;
            for (Annotation annotation : paramAnnotations[paramIndex]) {
                if (annotation instanceof Param) {
                    hasParamAnnotation = true;
                    name = ((Param) annotation).value();
                    break;
                }
            }
            if (name == null) {
                // @Param was not specified.
                if (config.isUseActualParamName()) {
                    // 使用方法的参数名作为名称
                    name = getActualParamName(method, paramIndex);
                }
                if (name == null) {
                    // use the parameter index as the name ("0", "1", ...)
                    // gcode issue #71
                    // 使用参数下标作为名称
                    name = String.valueOf(map.size());
                }
            }
            map.put(paramIndex, name);
        }
        names = Collections.unmodifiableSortedMap(map);
    }
    // 当调用 SqlSession 的执行方法之前会调用该方法,以将代理 invoke 方法得到的实际参数数组根据 names 属性中保存的参数名集合转为 Sql 执行需要的参数名对应参数值形式
    
    // 注意:
    // 1. 当参数列表为 0 个时,返回 null
    // 2. 当参数列表为 1 个且没有 @Param 注解时,则直接返回实际的参数值(非 key,value 的 Map 的形式)
    // 3. 以上二者都不是的情况下,返回一个 Map<String, Object> 形式的以 name 作为 key,实际传递的参数值作为 value,需要注意的一点是多了一组默认参数  // 以"param1, param2..."作为key的entry对象,也就是说我们可以用这些 key 在 xml 文件中直接写 #{param1}, #{param2} 来获取实际传递的 value
    public Object getNamedParams(Object[] args) {
        final int paramCount = names.size();
        if (args == null || paramCount == 0) {
            return null;
        } 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()) {
                param.put(entry.getValue(), args[entry.getKey()]);
                // add generic param names (param1, param2, ...)
                final String genericParamName = GENERIC_NAME_PREFIX + String.valueOf(i + 1);
                // ensure not to overwrite parameter named with @Param
                if (!names.containsValue(genericParamName)) {
                    param.put(genericParamName, args[entry.getKey()]);
                }
                i++;
            }
            return param;
        }
        
        // ...
    }

这里简单举个例子,可以将 ParamNameResolver 类的作用看的更清楚!

List getOrdersByPhone(@Param("phone") String phone) 为例,当我们 getOrdersByPhone 方法对应的 MappedMehtod 实例创建完成后,也即对应的 MethodSignature.ParamNameResolver 实例也创建完成,此时 ParamNameResolver 实例所持有的 names 属性值为:

names = {{0, "phone"}} // 0 代表参数下标 index,"phone" 代表参数的 name

当我们调用实际查询 SQL 前,假设传递给查询方法的参数 phone 的值为 “10086”, 经过 Mapper 代理,调用 invoke 方法执行查询,最终在调用 Sqlsession 的查询方法前会调用 MethodSignature.convertArgsToSqlCommandParam(args) 方法进行参数的转换(将 args 转为 SqlCommand 可以使用的参数对象),会继而转到 ParamNameResolver.getNamedParams 方法处理,最终经过该方法处理后得到的 Object 对象如下:

{{"phone", "10086"}, {"param1", "10086"}}

这样在继续调用 sqlSession 的执行方法时就可以直接取出对应 name 的 value 值啦,也就是为什么我们在 xml 文件中可以写 #{phone} 就可以被替换成实际传递的手机号的原因。


相关文章
|
1月前
|
SQL XML Java
mybatis Mapper的概念与实战
MyBatis 是一个流行的 Java 持久层框架,它提供了对象关系映射(ORM)的功能,使得Java对象和数据库中的表之间的映射变得简单。在MyBatis中,Mapper是一个核心的概念,它定义了映射到数据库操作的接口。简而言之,Mapper 是一个接口,MyBatis 通过这个接口与XML映射文件或者注解绑定,以实现对数据库的操作。
39 1
|
2月前
|
Java 数据库连接 Maven
使用mybatis插件generator生成实体类,dao层和mapper映射
使用mybatis插件generator生成实体类,dao层和mapper映射
55 0
|
3月前
|
SQL Java 数据库连接
Mybatis之Mybatis简介、搭建Mybatis相关步骤(开发环境、maven、核心配置文件、mapper接口、映射文件、junit测试、log4j日志)
【1月更文挑战第2天】 MyBatis最初是Apache的一个开源项目iBatis, 2010年6月这个项目由Apache Software Foundation迁移到了Google Code。随着开发团队转投Google Code旗下,iBatis3.x正式更名为MyBatis。代码于2013年11月迁移到Github iBatis一词来源于“internet”和“abatis”的组合,是一个基于Java的持久层框架。iBatis提供的持久层框架包括SQL Maps和Data Access Objects(DAO)
204 3
Mybatis之Mybatis简介、搭建Mybatis相关步骤(开发环境、maven、核心配置文件、mapper接口、映射文件、junit测试、log4j日志)
|
4月前
|
Java 数据库连接 mybatis
|
4月前
|
SQL 存储 Java
MyBatis【付诸实践 02】 mapper文件未编译+statementType使用+返回结果字段顺序不一致+获取自增ID+一个update标签批量更新记录
MyBatis【付诸实践 02】 mapper文件未编译+statementType使用+返回结果字段顺序不一致+获取自增ID+一个update标签批量更新记录
36 0
|
4月前
|
SQL IDE Java
MyBatis【问题 01】mapper传入array\collection\list类型的参数时报BindingException:Parameter ‘xx‘ not found问题复现及解决
MyBatis【问题 01】mapper传入array\collection\list类型的参数时报BindingException:Parameter ‘xx‘ not found问题复现及解决
60 0
|
4月前
|
设计模式 SQL Java
Mybatis源码分析系列之第四篇:Mybatis中代理设计模型源码详解
Mybatis源码分析系列之第四篇:Mybatis中代理设计模型源码详解
|
4月前
|
XML 缓存 Java
MyBatis原理分析之获取Mapper接口的代理对象
MyBatis原理分析之获取Mapper接口的代理对象
52 0
|
1月前
|
SQL Java 数据库连接
挺详细的spring+springmvc+mybatis配置整合|含源代码
挺详细的spring+springmvc+mybatis配置整合|含源代码
45 1