【紧急避坑】mybatis可以同时使用XML和注解

先摆结论

MyBatis可以同时使用XML和注解的方式配置。

怎么使用

假设项目中有一个Mapper:com.inaction.webmybatisinaction.UserMapper 和他的XML配置文件放置在resource目录下:UserMapper.xml

方式一

只写明XML的resource路径(或者URL路径)

<mappers>
    <mapper resource="UserMapper.xml"/>
 </mappers>

方式二:

只写明注解Mapper的类全路径名(这种方式只适合于只包含注解的配置)

<mappers>
    <mapper class="com.inaction.webmybatisinaction.UserMapper"/>
</mappers>

方式三:

同时都注明,但是类全路径名必须写在xml的前面

<mappers>
    <mapper class="com.inaction.webmybatisinaction.UserMapper"/>
    <mapper resource="UserMapper.xml"/>
</mappers>

注意:

虽然可以同时采用XML和注解两种方式配置,但是不能同时对同一个方法既注解又XML配置,不然会报错。

原因分析

方式一成立的原因

在SqlSessionFactory创建的过程中,会先创建Configuration对象,会先解析SqlMapConfig.xml中的节点,最后解析的就是节点,其中会调用XMLMapperBuilder的parse()方法解析,当解析了XML的方式节点时,会在解析XML文件配置到Configuration中之后进行一个命名空间绑定的操作: bindMapperForNamespace();

//java的XMLMapperBuilder类
public void parse() {
    
    
  if (!configuration.isResourceLoaded(resource)) {
    
    
    configurationElement(parser.evalNode("/mapper"));
    configuration.addLoadedResource(resource);
    bindMapperForNamespace();
  }

  parsePendingResultMaps();
  parsePendingCacheRefs();
  parsePendingStatements();
}
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)) {
    
    
      //这里如果检测到之前注册过Mapper之后就不会重复注册了也不会报错
        // 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);
      }
    }
  }
}

该操作首先会判断Configuration有没有事先解析过Mapper对象,如果事先解析过则不做处理直接退出,如果没有解析过他则会通过XML文件中配置的命名空间反射到对应的Mapper类,然后通过一系列的反射操作解析注解。所以,只写明XML文件路径依然是可以解析到Mapper注解。

方式二的局限

这种方式会直接向Configuration的MapperRegistry注册Mapper,但是由于Mapper对象不知道XML的位置所欲不会解析XML中的配置。故这种方式是不安全的。

//这个方法位于MapperRegistry类之中,只会解析Mapper注解不会解析XML
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<>(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);
        }
      }
    }
  }

方式三成立的原因

方式三一定要把类的配置写在xml的配置之前,说先解析完mapper之后,可以继续解析xml,解析xml时如果判断mapper解析过之后则不会重复解析也不会抛错,但是如果先解析xml,会向Configuration中注册Mapper,当之后解析Mapper时如果检测到有加载过则会抛出异常并终止程序创建SqlSessionFactory。

//这段代码参考第二点分析,这里会调用hasMapper(type)检测是否已经注册了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.");
      }

注意

虽然可以同时采用XML和注解两种方式配置,但是不能同时对同一个方法既注解又XML配置,不然会报错。因为在解析每个sqlmap的时候会给其生成唯一的ID,并存入MapperRegistry中,这个注册中心本质上是一个HashMap,且不允许插入已经存在的key值,做插入操作时如果检测到已存在同名ID就会报错终止解析。所以不允许对一个方法既注解又XML配置。

//这个是Configuration类中的方法,用于讲语句注册进来
public void addMappedStatement(MappedStatement ms) {
    
    
    mappedStatements.put(ms.getId(), ms);
  }
//这个是mappedStatements的内部实现类StictMap的实现方法,其中第一步就是检验重复并报错
	@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);
    }

猜你喜欢

转载自blog.csdn.net/liangcheng0523/article/details/106882135