美文网首页
Mybatis随笔(十) 聊聊NameSpace

Mybatis随笔(十) 聊聊NameSpace

作者: sunyelw | 来源:发表于2020-04-05 08:41 被阅读0次

    在我刚接触Mybatis那会,有位先生说到这个NameSpace时,说这个东西不一定就要写对应Mapper接口的全限定类名,我就来试了试。

    原来当年先生少说了一句,或是我走神漏听了一句,才有了现在的Mybatis系列。


    首先我的目录结构


    package
    • 注意 Mapper.java 与 Mapper.xml 文件不在同一目录下就行了

    先看几种用法及其效果

    1. NameSpace 使用全限定类名 & mapper 配置 resource
    • 配置文件 mybatis-config.xml
    <mapper resource="resources/mapper/AccountMapper.xml" />
    
    • sql映射文件 AccountMaper.xml
    <mapper namespace="com.hy.test.dao.AccountMapper" >
    
    • 使用
    @Test
    public void queryAccountTest() {
    
        basicMethod("queryAccountTest", k -> {
            AccountMapper accountMapper = k.getMapper(AccountMapper.class);
            Account account = accountMapper.selectByPrimaryKey(31);
            log.info("---- account: " + account.toString() + " -----------");
            return SUCCESS;
        });
    }
    
    /**
     * SqlSessionFactory -> SqlSession
     */
    private void basicMethod(String methodName, Function<SqlSession, String> mappingFunction) {
        String config = "resources/mybatis-config.xml";
        // 测试代码
        SqlSession sqlSession = null;
        try (InputStream is = Resources.getResourceAsStream(config)) {
            SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(is);
            sqlSession = factory.openSession();
            log.error("function: " + methodName + ", result: " + mappingFunction.apply(sqlSession));
            sqlSession.commit();
        } catch (Exception ignore) {
            log.error(ignore);
            ignore.printStackTrace();
        } finally {
            if (null != sqlSession ) sqlSession.close();
        }
    }
    

    输出

    ==>  Preparing: select id, name, balance from account where id = ? 
    ==> Parameters: 31(Integer)
    Account: class com.hy.test.po.Account
    object test: com.hy.test.po.Account@6a1aab78
    <==      Total: 1
    ---- account: Account{id=31, name='java', balance=2} -----------
    

    这种方式无疑是可以正确读取的

    2. NameSpace 不使用全限定类名 & mapper 配置 resource

    修改一下NameSpace,其他不变
    sql映射文件 AccountMaper.xml

    <mapper namespace="lalalala" >
    

    执行结果

    org.apache.ibatis.binding.BindingException: Type interface com.hy.test.dao.AccountMapper is not known to the MapperRegistry.
        at org.apache.ibatis.binding.MapperRegistry.getMapper(MapperRegistry.java:47)
    
        at org.apache.ibatis.session.Configuration.getMapper(Configuration.java:779)
        at org.apache.ibatis.session.defaults.DefaultSqlSession.getMapper(DefaultSqlSession.java:291)
        at com.hy.test.main.MainTest.lambda$queryAccountTest$1(MainTest.java:52)
        at com.hy.test.main.MainTest.basicMethod(MainTest.java:93)
        at com.hy.test.main.MainTest.queryAccountTest(MainTest.java:51)
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.lang.reflect.Method.invoke(Method.java:498)
        at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
        at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
        at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
        at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
        at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
        at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
        at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
        at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
        at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
        at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
        at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
        at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
        at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
        at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
        at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
        at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:47)
        at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242)
        at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)
    

    报错 MapperRegistry 没找到接口,前面的源码解析我们知道这个 MapperRegistry 是 Configuration 的一个属性,其中有一个 Map类型的knownMappers 用于存储加载过的 *Mapper.java 接口类型与对应的JDK动态代理工厂的映射。

    那我们是不是可以手动将 Mapper接口注入到这个 MapperRegistry 呢?

    3. NameSpace 不使用全限定类名 & mapper 配置 resource/class

    配置文件 mybatis-config.xml

    <mapper resource="resources/mapper/AccountMapper.xml" />
    

    sql映射文件 AccountMaper.xml

    <mapper namespace="com.hy.test.dao.AccountMapper" >
    <mapper class="com.hy.test.dao.AccountMapper"/>
    
    • 添加了 class 属性就可以指定加载 AccountMapper 接口类了

    执行结果

    org.apache.ibatis.binding.BindingException: Invalid bound statement (not found): com.hy.test.dao.AccountMapper.selectByPrimaryKey
        at org.apache.ibatis.binding.MapperMethod.<init>(MapperMethod.java:53)
    
        at org.apache.ibatis.binding.MapperProxy.lambda$cachedInvoker$0(MapperProxy.java:107)
        at java.util.concurrent.ConcurrentHashMap.computeIfAbsent(ConcurrentHashMap.java:1660)
        at org.apache.ibatis.binding.MapperProxy.cachedInvoker(MapperProxy.java:94)
        at org.apache.ibatis.binding.MapperProxy.invoke(MapperProxy.java:85)
        at com.sun.proxy.$Proxy6.selectByPrimaryKey(Unknown Source)
        at com.hy.test.main.MainTest.lambda$queryAccountTest$1(MainTest.java:53)
        at com.hy.test.main.MainTest.basicMethod(MainTest.java:93)
        at com.hy.test.main.MainTest.queryAccountTest(MainTest.java:51)
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.lang.reflect.Method.invoke(Method.java:498)
        at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
        at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
        at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
        at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
        at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
        at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
        at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
        at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
        at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
        at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
        at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
        at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
        at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
        at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
        at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
        at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:47)
        at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242)
        at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)
    

    这个报错是告诉我们,你想要执行的SQL方法在 Configuration 的mappedStatements 中没有对应的 MapperStatement#id

    Configuration.java 类源码

    protected final Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>("Mapped Statements collection")
          .conflictMessageProducer((savedValue, targetValue) ->
              ". please check " + savedValue.getResource() + " and " + targetValue.getResource());
    
    public void addMappedStatement(MappedStatement ms) {
        mappedStatements.put(ms.getId(), ms);
    }
    

    其中 id 可以查询调用轨迹

    public String applyCurrentNamespace(String base, boolean isReference) {
        if (base == null) {
            return null;
        }
        if (isReference) {
            // is it qualified with any namespace yet?
            if (base.contains(".")) {
                return base;
            }
        } else {
            // is it qualified with this namespace yet?
            if (base.startsWith(currentNamespace + ".")) {
                return base;
            }
            if (base.contains(".")) {
                throw new BuilderException("Dots are not allowed in element names, please remove it from " + base);
            }
        }
        return currentNamespace + "." + base;
    }
    
    • id 的值是 namespace + "." + 方法名

    这就很明显了

    • 配置接口类加载进去只是为了获取到对应的动态代理对象,可是执行的时候是从 mappedStatements 中取对应的 MapperStatement 执行的
    • 而 mappedStatements 中的id是 lalalala.selectByPrimaryKey,而你代理对象用的是com.hy.test.dao.AccountMapper.selectByPrimaryKey 自然取不到
    public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) {
        final String methodName = method.getName();
        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 {
                // here
                // here
                // here
                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);
            }
        }
    }
    
    private MappedStatement resolveMappedStatement(Class<?> mapperInterface, String methodName, Class<?> declaringClass, Configuration configuration) {
        // 接口名 + 方法名 作为 statementId
        String statementId = mapperInterface.getName() + "." + methodName;
        if (configuration.hasStatement(statementId)) {
            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;
    }
    

    归根究底,就是 MapperStatement 的 id 不一致,也就是 NameSpace 没有写接口类的全限定类名。

    那么 NameSpace 一定要写接口类的全限定类名吗?

    4. 换种思路

    Configuration类在解析时有两个属性很重要

    1. mapperRegistry
    • 判断是否加载过 Mapper.java
    • 内有属性 Configuration configuration
    • Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();
    • if (configuration.hasMapper(Class<T> type)) {}
    1. loadedResources
    • 通过 XML 的命名空间判断是否解析过 Mapper.xml
    • Set<String> loadedResources = new HashSet<>();
    • 命名空间是否有效取决于是否能被类加载器加载
    • if (configuration.isResourceLoade(String resource)) {}

    由于mappedStatements存储的是具体的SQL实现,那么mappedStatements属性的构建可以有两个来源,一个就是通过配置文件的形式,也就是 XML;一个就是通过 Mapper.java 接口方法上的注解,比如

    @SelectKey(keyProperty = "account",
            before = false,
            statementType = StatementType.STATEMENT,
            statement = "select * from account where id = #{id}",
            resultType = Account.class)
    Account selectByPrimaryKey(@Param("id") Integer id);
    

    既然我们使用的是 XML 方式,那么mappedStatements中的key就只能是 namespace + 方法ID,而代理对象用于执行的SqlId只能是 mapperInterface.getName + 方法ID,这么来看好像我们只能把 XML 的 NameSpace 写成接口类的全限定类名了。

    不妨换种思路。

    既然配置文件我们已经没办法再修改,试试修改执行方式呢

    @Test
    public void Three() {
        try {
            SqlSessionFactory sqlSessionFactory =
                    new SqlSessionFactoryBuilder().build(Resources.getResourceAsReader("resources/mybatis-config.xml"));
            SqlSession session = sqlSessionFactory.openSession();
            Account a = session.selectOne("selectByPrimaryKey", 31);
            System.out.println(a.toString());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    

    执行结果

    ==>  Preparing: select id, name, balance from account where id = ? 
    ==> Parameters: 31(Integer)
    Account: class com.hy.test.po.Account
    object test: Account{id=null, name='null', balance=null}
    <==      Total: 1
    Account{id=31, name='java', balance=2}
    

    ok,这种方式是可以成功的。

    来分析下这种方式为什么可以
    直接找到对应SqlSession的具体执行

    public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
        try {
            MappedStatement ms = configuration.getMappedStatement(statement);
            return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
        } catch (Exception e) {
            throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
        } finally {
            ErrorContext.instance().reset();
        }
    }
    

    这里也是直接从configuration的mapperStatements中取MapperStatement的,不过id就换成了selectByPrimaryKey,为什么这里可以直接取到值呢?

    很简单,往mapperStatements中塞值的时候,不仅会塞一个长id,还会再加一个短id,我们再看下mapperStatements的实现

    protected final Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>("Mapped Statements collection")
          .conflictMessageProducer((savedValue, targetValue) ->
              ". please check " + savedValue.getResource() + " and " + targetValue.getResource());
    

    注意到这不是一个常见的 HashMap,而是一个 StrictMap
    其重写了put方法

    protected static class StrictMap<V> extends HashMap<String, V> {
    
        @Override
        @SuppressWarnings("unchecked")
        public V put(String key, V value) {
            if (containsKey(key)) {
                throw new IllegalArgumentException(name + " already contains value for " + key + (conflictMessageProducer == null ? "" : conflictMessageProducer.apply(super.get(key), value)));
            }
            if (key.contains(".")) {
                final String shortKey = getShortName(key);
                if (super.get(shortKey) == null) {
                    super.put(shortKey, value);
                } else {
                    super.put(shortKey, (V) new Ambiguity(shortKey));
                }
            }
            return super.put(key, value);
        }
    
        private String getShortName(String key) {
            final String[] keyParts = key.split("\\.");
            return keyParts[keyParts.length - 1];
        }
    }
    
    • 所以debug可以看到mapperStatements的长度是接口方法的两倍,就是因为加了 shortName
    mapperStatements AccountMapper.java
    • 6个接口方法,12的mapperStatements长度
    • 再仔细看下 mapperStatements 里面的值都是一长key带一短key

    所以通过 SqlSession 直接调用的方式是可以忽略 NameSpace 的校验的,只要唯一就行了,不然多个 xml 使用同一个 NameSpace 则只会加载一个。

    总结

    1. NameSpace的值并不一定是要配置成接口的全限定类名,前提是不使用类型安全的代理映射方式来执行SQL,而是直接使用 SqlSession 来直接使用方法ID调用指定方法,这种方式就不怎么优雅
    2. 配置class- Mapper.java或package - name 指定加载接口类,解析时会去尝试加载同目录下同名的xml文件(支持 classPath)
    3. 配置resource - Mapper.xml 指定加载 XML 文件,解析时会尝试将其中 NameSpace 当做接口Mapper进行加载,前提是NameSpace 可以被类加载器加载
    private void bindMapperForNamespace() {
        String namespace = builderAssistant.getCurrentNamespace();
        if (namespace != null) {
            Class<?> boundType = null;
            try {
                boundType = Resources.classForName(namespace);
            } catch (ClassNotFoundException e) {
                //ignore, bound type is not required
            }
            if (boundType != null) {
                // 先判断是否已经加载
                if (!configuration.hasMapper(boundType)) {
                    // Spring may not know the real resource name so we set a flag
                    // to prevent loading again this resource from the mapper interface
                    // look at MapperAnnotationBuilder#loadXmlResource
                    configuration.addLoadedResource("namespace:" + namespace);
                    configuration.addMapper(boundType);
                }
            }
        }
    }
    
    • 先判断是否已经加载,若是已经记载再直接加载会报错
    if (hasMapper(type)) {
        throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
    }
    

    看个配置实例,在NameSpace写了全限定类名时,下面三种配置方式

    <mapper resource="resources/mapper/AccountMapper.xml" />
    
    <mapper class="com.hy.test.dao.AccountMapper"/>
    <mapper resource="resources/mapper/AccountMapper.xml" />
    
    <mapper resource="resources/mapper/AccountMapper.xml" />
    <mapper class="com.hy.test.dao.AccountMapper"/>
    

    1与2 都能成功且效果是一样的,而3 会报错

    ### Cause: org.apache.ibatis.builder.BuilderException: Error parsing SQL Mapper Configuration. 
    Cause: org.apache.ibatis.binding.BindingException: 
    
    Type interface com.hy.test.dao.AccountMapper is already known to the MapperRegistry.
    

    注意我的代码目录,Mapper.java 与 Mapper.xml 不在同一级目录下


    谢谢当年先生没说完的后面半句话。

    相关文章

      网友评论

          本文标题:Mybatis随笔(十) 聊聊NameSpace

          本文链接:https://www.haomeiwen.com/subject/bjkyphtx.html