狗血的Mybatis文件路径问题(org.apache.ibatis.binding.BindingException: Invalid bound statement (not found))

狗血的Mybatis文件路径问题org.apache.ibatis.binding.BindingException: Invalid bound statement not found

1、问题描述

  今天学习Mybatis运行程序的时候不出意外,出现异常了。

org.apache.ibatis.binding.BindingException: Invalid bound statement (not found): com.njust.mybatisplug.dao.CountryMapper.selectAll

	at org.apache.ibatis.binding.MapperMethod$SqlCommand.<init>(MapperMethod.java:223)
	at org.apache.ibatis.binding.MapperMethod.<init>(MapperMethod.java:48)
	at org.apache.ibatis.binding.MapperProxy.cachedMapperMethod(MapperProxy.java:59)
	at org.apache.ibatis.binding.MapperProxy.invoke(MapperProxy.java:52)
	at com.sun.proxy.$Proxy5.selectAll(Unknown Source)
	at com.njust.mybatisplug.dao.CountryMapperTest.testSelectAll(CountryMapperTest.java:47)
	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.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26)
	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)

嗯,错误显示statement里面没有com.njust.mybatisplug.dao.CountryMapper.selectAll。好的下面贴出我的源码和调试过程。

2、问题分析

首先总体看一下项目的结构吧。

在这里插入图片描述

很简单,就是Mybatis的基本主键。下面将源码贴出。

Country .java

package com.njust.mybatisplug.model;

/**
 * @author Chen
 * @version 1.0
 * @date 2020/4/18 9:52
 * @description:
 */
public class Country {
    private Long id;
    private String countryname;
    private String countrycode;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getCountryname() {
        return countryname;
    }

    public void setCountryname(String countryname) {
        this.countryname = countryname;
    }

    public String getCountrycode() {
        return countrycode;
    }

    public void setCountrycode(String countrycode) {
        this.countrycode = countrycode;
    }

    @Override
    public String toString() {
        return "Country{" +
                "id=" + id +
                ", countryname='" + countryname + '\'' +
                ", countrycode='" + countrycode + '\'' +
                '}';
    }
}

CountryMapper .java

package com.njust.mybatisplug.dao;

import com.njust.mybatisplug.model.Country;

import java.util.List;

public interface CountryMapper {
    List<Country> selectAll();
}

CountryMapper .xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.njust.mybatisplug.dao.CountryMapper">
    <select id="selectAll" resultType="com.njust.mybatisplug.model.Country">
        select id, countryname, countrycode
        from country
    </select>
</mapper>

log4j.properties

log4j.rootLogger=ERROR, stdout

log4j.logger.tk.mybatis.simple.mapper=TRACE

log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%5p [%t] - %m%n

mybatis-config.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
	<settings>
	    <setting name="logImpl" value="LOG4J"/>
	    <setting name="mapUnderscoreToCamelCase" value="true"/>
	</settings>
    
    <typeAliases>
        <package name="com.njust.mybatisplug.model"/>
    </typeAliases>


    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC">
                <property name="" value=""/>
            </transactionManager>
            <dataSource type="UNPOOLED">
                <property name="driver" value="com.mysql.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://localhost:3306/mybatis"/>
                <property name="username" value="root"/>
                <property name="password" value="root"/>
            </dataSource>
        </environment>
    </environments>

    <mappers>
        <package name="com.njust.mybatisplug.dao"/>
    </mappers>
    
    
</configuration>


CountryMapperTest.java

package com.njust.mybatisplug.dao;

import com.njust.mybatisplug.model.Country;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.junit.BeforeClass;
import org.junit.Test;

import java.io.IOException;
import java.io.Reader;
import java.util.List;


/**
 * @author Chen
 * @version 1.0
 * @date 2020/4/18 9:58
 * @description:
 */
public class CountryMapperTest {
    private static SqlSessionFactory sqlSessionFactory;

    @BeforeClass
    public static void init() {
        try {
            Reader reader = Resources.getResourceAsReader("mybatis-config.xml");
            sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
            reader.close();
        } catch (IOException ignore) {
            ignore.printStackTrace();
        }
    }

    public SqlSession getSqlSession() {
        return sqlSessionFactory.openSession();
    }

    @Test
    public void testSelectAll() {
        SqlSession sqlSession = getSqlSession();
        try {
            CountryMapper countryMapper = sqlSession.getMapper(CountryMapper.class);

            //调用 selectAll 方法查询所有用户
            List<Country> userList = countryMapper.selectAll();
            userList.forEach(System.out::println);
        } finally {
            //不要忘记关闭 sqlSession
            sqlSession.close();
        }
    }

    private void printCountryList(List<Country> countryList) {
        for (Country country : countryList) {
            System.out.printf("%-4d%4s%4s\n", country.getId(), country.getCountryname(), country.getCountrycode());
        }
    }

}

上述代码的逻辑很简单,只是将Country的数据全部查询并打印出来。

3、问题解决

接下来,我们一步一步的debug。老司机开车,上车请系好安全带!首先调试sqlSession是否有问题。OK,在CountryMapper countryMapper = sqlSession.getMapper(CountryMapper.class);这一行打个断点,便于查看。然后运行。如下图所示:

在这里插入图片描述

由于错误信息报的是statement没有绑定,所以我们直接查找statement相关信息即可。

在这里插入图片描述
在这里插入图片描述

根据debug信息。我们发现sqlSession.configuration.mappedStatements确实是size =0。哎?不对啊,我们这些路径不是配的好好的吗?怎么没有呢?那我们就查看mappedStatements的初始化实现,然后debug到此处即可。实现源码如下:

  public void parseStatementNode() {
    //...省略其他代码

    builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
        fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
        resultSetTypeEnum, flushCache, useCache, resultOrdered, 
        keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
  }

我们只要一步步debug进来,查看十分初始化即可。OK,继续debug。我们首先找到configuration的初始化实现方法。我们这里主要是在init()方法里面的new SqlSessionFactoryBuilder().build(reader);实现configuration的初始化,源码如下:

 public static void init() {
        try {
            Reader reader = Resources.getResourceAsReader("mybatis-config.xml");
            sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
            reader.close();
        } catch (IOException ignore) {
            ignore.printStackTrace();
        }
    }

在这里插入图片描述

直接进入build方法。

  public SqlSessionFactory build(Reader reader) {
    return build(reader, null, null);
  }

在这里插入图片描述

这里调用了另一个build函数,信息不大,继续进去。

  public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
    try {
      XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
      return build(parser.parse());
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error building SqlSession.", e);
    } finally {
      ErrorContext.instance().reset();
      try {
        reader.close();
      } catch (IOException e) {
        // Intentionally ignore. Prefer previous error.
      }
    }
  }

在这里插入图片描述

嗯有点意思了,这里的初始化主要是在parser.parse()方法里面,继续进入该方法。

  public Configuration parse() {
    if (parsed) {
      throw new BuilderException("Each XMLConfigBuilder can only be used once.");
    }
    parsed = true;
    parseConfiguration(parser.evalNode("/configuration"));
    return configuration;
  }

在这里插入图片描述

这里的parseConfiguration(parser.evalNode("/configuration"));方法实现了初始化,但是一眼也看不出来啊,继续进去。

  private void parseConfiguration(XNode root) {
    try {
      Properties settings = settingsAsPropertiess(root.evalNode("settings"));
      //issue #117 read properties first
      propertiesElement(root.evalNode("properties"));
      loadCustomVfs(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);
    }
  }

在这里插入图片描述

这里的mapperElement(root.evalNode("mappers"));方法实现了初始化,但是一眼也看不出来啊,继续进去。

  private void mapperElement(XNode parent) throws Exception {
    if (parent != null) {
      for (XNode child : parent.getChildren()) {
        if ("package".equals(child.getName())) {
          String mapperPackage = child.getStringAttribute("name");
          configuration.addMappers(mapperPackage);
        } else {
          String resource = child.getStringAttribute("resource");
          String url = child.getStringAttribute("url");
          String mapperClass = child.getStringAttribute("class");
          if (resource != null && url == null && mapperClass == null) {
            ErrorContext.instance().resource(resource);
            InputStream inputStream = Resources.getResourceAsStream(resource);
            XMLMapperBuilder 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 mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
            mapperParser.parse();
          } else if (resource == null && url == null && mapperClass != null) {
            Class<?> mapperInterface = Resources.classForName(mapperClass);
            configuration.addMapper(mapperInterface);
          } else {
            throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
          }
        }
      }
    }
  }

在这里插入图片描述

这里的configuration.addMappers(mapperPackage);方法实现了初始化,但是一眼也看不出来啊,继续进去。

  public void addMappers(String packageName) {
    mapperRegistry.addMappers(packageName);
  }

在这里插入图片描述

这里的mapperRegistry.addMappers(packageName);方法实现了初始化,但是一眼也看不出来啊,继续进去。

  /**
   * @since 3.2.2
   */
  public void addMappers(String packageName) {
    addMappers(packageName, Object.class);
  }

在这里插入图片描述

这里的addMappers(packageName, Object.class);方法实现了初始化,但是一眼也看不出来啊,继续进去。

  /**
   * @since 3.2.2
   */
  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);
    }
  }

在这里插入图片描述

这里的addMapper(mapperClass);方法实现了初始化,但是一眼也看不出来啊,继续进去。

  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 {
        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.
        MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
        parser.parse();
        loadCompleted = true;
      } finally {
        if (!loadCompleted) {
          knownMappers.remove(type);
        }
      }
    }
  }

在这里插入图片描述

这里的parser.parse();方法实现了初始化,但是一眼也看不出来啊,继续进去。

  public void parse() {
    String resource = type.toString();
    if (!configuration.isResourceLoaded(resource)) {
      loadXmlResource();
      configuration.addLoadedResource(resource);
      assistant.setCurrentNamespace(type.getName());
      parseCache();
      parseCacheRef();
      Method[] methods = type.getMethods();
      for (Method method : methods) {
        try {
          // issue #237
          if (!method.isBridge()) {
            parseStatement(method);
          }
        } catch (IncompleteElementException e) {
          configuration.addIncompleteMethod(new MethodResolver(this, method));
        }
      }
    }
    parsePendingMethods();
  }

在这里插入图片描述

这里的loadXmlResource();方法实现了初始化,但是一眼也看不出来啊,继续进去。


  private void loadXmlResource() {
    // Spring may not know the real resource name so we check a flag
    // to prevent loading again a resource twice
    // this flag is set at XMLMapperBuilder#bindMapperForNamespace
    if (!configuration.isResourceLoaded("namespace:" + type.getName())) {
      String xmlResource = type.getName().replace('.', '/') + ".xml";
      InputStream inputStream = null;
      try {
        inputStream = Resources.getResourceAsStream(type.getClassLoader(), xmlResource);
      } catch (IOException e) {
        // ignore, resource is not required
      }
      if (inputStream != null) {
        XMLMapperBuilder xmlParser = new XMLMapperBuilder(inputStream, assistant.getConfiguration(), xmlResource, configuration.getSqlFragments(), type.getName());
        xmlParser.parse();
      }
    }
  }

在这里插入图片描述

这里已经到了事发地点了。什么鬼inputStream怎么是null呢?这里相当于没有读取到资源文件,再谈设置 mappedStatements就没有意义了,肯定设置不了啊!看一下xmlResource的路径。

在这里插入图片描述
在看一下我们项目中的路径。

在这里插入图片描述
咋一看确实是没有问题的。但是问题就出现在这个咋一看上面。我们进入项目的win文件夹下面看看。

在这里插入图片描述
是不是有点奇怪,idea.win中不是一个个文件吗?这里怎么是一个文件。是的。
错误就在这里,我们在idea中以.分隔的文件,在win中其实是一个文件夹,这里和java中的package不同。
我们修改一下文件。

在这里插入图片描述

在这里插入图片描述
再次运行项目如下:

在这里插入图片描述
这里的inputStream就不是空了,有数值了。继续运行代码。我们发现可以给statement赋值了。

在这里插入图片描述

再次查看mappedStatements里面有值了。

在这里插入图片描述

最后程序运行结果如下:

Country{id=1, countryname='中国', countrycode='CN'}
Country{id=2, countryname='美国', countrycode='US'}
Country{id=3, countryname='俄罗斯', countrycode='RU'}
Country{id=4, countryname='英国', countrycode='GB'}
Country{id=5, countryname='法国', countrycode='FR'}
Disconnected from the target VM, address: '127.0.0.1:44522', transport: 'socket'

Process finished with exit code 0

成功输出结果。

最后我们看一下idea显示的目录结构。

在这里插入图片描述
注意这是我改过win下面的路径之后截的图,尼玛,这和我们上面报错的有什么区别呢?废了老子大半天时间。强行学一波Mybatis源码。

4、总结

  书上的代码直接运行绝大部分是对的,但是总有一些软件的更新使得作者无能为力。之前的API是对的,但是之后就废弃了或修改了是常有的事。所以我们需要跟踪源代码。这只是一个小小的问题,如果没有前辈的无私奉献,很难想象我们自己一天能学到多少内容。感谢各位前辈的辛勤付出,让我们少走了很多的弯路!

点个赞再走呗!欢迎留言哦!

  • 5
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值