结论
MyBatis 可以同时使用 XML 和注解的方式配置
假设项目中有一个Mapper:com.inaction.webmybatisinaction.UserMapper 和他的XML配置文件放置在resource目录下:UserMapper.xml
方式一
只写明 XM L的 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);
}