23、Mapper解析之基于注解的总体过程

mapper文件入口

加载mapper配置文件的入口在XMLConfigBuilder类中的mapperElement方法

mapperElement方法

  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.");
                    }
                }
            }
        }
    }

这个方法首先会加载标签下的package节点,如果package节点为空,则解析
resource url class这三个节点从上面的if判断可以看出,如果三个不满足只有其中一个的话就会发生异常,异常的信息为
A mapper element may only specify a url, resource or class, but not more than one.

package

我们首先来分析package节点,package配置的mapper接口所在的包,涉及的重要方法有以下几个

MapperRegistry

mapper文件的注册

字段

  private final Configuration config;
  private final Map, MapperProxyFactory> knownMappers = new HashMap, MapperProxyFactory>();

knownMappers存放注册的mapper文件,key为mapper接口的类型,value为mapper接口的动态代理对象,动态代理对象如何生成,随后分析

方法

  /**根据包名,解析出包下的所有类
   * @since 3.2.2
   */
  public void addMappers(String packageName, Class superType) {
    ResolverUtil> resolverUtil = new ResolverUtil>();
    resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
    Set>> mapperSet = resolverUtil.getClasses();
    for (Class mapperClass : mapperSet) {
      addMapper(mapperClass);
    }
  }
  • 根据包名,找出包下的所有类
  • 遍历包下的所有类
    java
    public void addMapper(Class 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);
    }
    }
    }
    }
  • 先判断这个接口是否已经注册过,注册过抛出异常
  • 生成mapper的动态代理对象,放到map中
  • 使用mapper打的注解解析器

MapperAnnotationBuilder

此类主要用来解析mapper接口中的注解

字段

    private final Set> sqlAnnotationTypes = new HashSet>();
    private final Set> sqlProviderAnnotationTypes = new HashSet>();

    private Configuration configuration;
    private MapperBuilderAssistant assistant;
    private Class type;
  • sqlAnnotationTypes sqlProviderAnnotationTypes是用来存放注解的在这个类初始化的时候,会添加进去
  • MapperBuilderAssistant 是mapper文件解析的辅助工具,这个类随后分析
  • type mapper接口的类型

构造器

public MapperAnnotationBuilder(Configuration configuration, Class type) {
        //生成类的唯一表示,全限定名.java (best guess)
        String resource = type.getName().replace('.', '/') + ".java (best guess)";
        this.assistant = new MapperBuilderAssistant(configuration, resource);
        this.configuration = configuration;
        this.type = type;

        sqlAnnotationTypes.add(Select.class);
        sqlAnnotationTypes.add(Insert.class);
        sqlAnnotationTypes.add(Update.class);
        sqlAnnotationTypes.add(Delete.class);

        sqlProviderAnnotationTypes.add(SelectProvider.class);
        sqlProviderAnnotationTypes.add(InsertProvider.class);
        sqlProviderAnnotationTypes.add(UpdateProvider.class);
        sqlProviderAnnotationTypes.add(DeleteProvider.class);
    }
  • 根据mapper接口的类型生成类的全限定名,例如com/lf/mapper/Auth.java (best guess)
  • 生成对应的mapper辅助解析工具
  • 初始化sqlAnnotationTypes和sqlProviderAnnotationTypes

parse

重要用来解析注解

    public void parse() {
        String resource = type.toString();
        if (!configuration.isResourceLoaded(resource)) {//是否加载过这个类
            //xml配置文件必须与Class对象所在的包路径一致,且文件名要与类名一致
//        在解析完xml配置文件后,才会开始解析Class对象中包含的注解
            loadXmlResource();
            // 把Class对应的标识添加到已加载的资源列表中
            configuration.addLoadedResource(resource);
            // 设置当前namespace为接口Class的全限定名
            assistant.setCurrentNamespace(type.getName());
            // 解析缓存对象
            parseCache();
            // 解析缓存引用,会覆盖之前解析的缓存对象
            parseCacheRef();
            // 获取所有方法,解析方法上的注解,生成MappedStatement和ResultMap
            Method[] methods = type.getMethods();
            for (Method method : methods) {
                try {
                    // issue #237
                    // 解析一个方法生成对应的MapperedStatement对象
                    // 并添加到配置对象中
                    if (!method.isBridge()) {
                        parseStatement(method);
                    }
                } catch (IncompleteElementException e) {
                    configuration.addIncompleteMethod(new MethodResolver(this, method));
                }
            }
        }
        parsePendingMethods();
    }
  • 首先判断这个资源是否加载过
  • 加载mapper对应的配置文件, com.lixin.mapper.TopicMapper类对应的配置文件为com/lixin/mapper/TopicMapper.xml
  • 把Class对应的标识添加到已加载的资源列表中
  • 设置当前namespace为接口Class的全限定名
  • 解析缓存对象
  • 解析缓存引用,会覆盖之前解析的缓存对象
  • 解析一个方法生成对应的MapperedStatement对象
    此处我们重点关注解析class对应的配置文件

loadXmlResource

加载mapper接口对应的配置文件

   private void loadXmlResource() {
        // 如果已加载资源列表中指定key已存在,则不再解析xml文件
        // 资源名称为namepace:全限定名
        if (!configuration.isResourceLoaded("namespace:" + type.getName())) {
            // 根据Class对象生成xml配置文件路径
            String xmlResource = type.getName().replace('.', '/') + ".xml";
            InputStream inputStream = null;
            try {
                // 获取文件字节流
                inputStream = Resources.getResourceAsStream(type.getClassLoader(), xmlResource);
            } catch (IOException e) {
                // ignore, resource is not required
            }
            // 如果xml文件存在,则创建XMLMapperBuilder进行解析
            if (inputStream != null) {
                XMLMapperBuilder xmlParser = new XMLMapperBuilder(inputStream, assistant.getConfiguration(),
                        xmlResource, configuration.getSqlFragments(), type.getName());
                xmlParser.parse();
            }
        }
    }
  1. 首先判断是否加载过此资源
  2. 根据全限定类名生成配置文件的路径
  3. 如果可以找到配置文件,则创建XMLMapperBuilder对象,进行解析
  4. 如果没有找到对应的xml文件,则忽略异常,使用MapperAnnotationBuilder,当做注解的方式解析
    此处暂不对解析的做具体的分析

parseCache

@CacheNamespace注解对应mapper.xml配置文件中的元素,但是注解方式不支持properties自定义属性的配置。
此处我们假设上一步没有找到,接口对应的mapper文件,接下来会解析注解配置的缓存

 private void parseCache() {
           CacheNamespace cacheDomain = type.getAnnotation(CacheNamespace.class);
           if (cacheDomain != null) {//是否使用缓存注解
               Integer size = cacheDomain.size() == 0 ? null : cacheDomain.size();//缓存的大小
               Long flushInterval = cacheDomain.flushInterval() == 0 ? null : cacheDomain.flushInterval();//缓存的刷新时间
               Properties props = convertToProperties(cacheDomain.properties());
               assistant.useNewCache(cacheDomain.implementation(), cacheDomain.eviction(), flushInterval, size, cacheDomain.readWrite(), cacheDomain.blocking(), props);
           }
       }

parseCacheRef()

@CacheNamespaceRef注解对应mapper.xml配置文件中的元素。
解析缓存引用,会覆盖之前解析的缓存对象

  private void parseCacheRef() {
        CacheNamespaceRef cacheDomainRef = type.getAnnotation(CacheNamespaceRef.class);
        if (cacheDomainRef != null) {
            Class refType = cacheDomainRef.value();
            String refName = cacheDomainRef.name();
            if (refType == void.class && refName.isEmpty()) {
                throw new BuilderException("Should be specified either value() or name() attribute in the @CacheNamespaceRef");
            }
            if (refType != void.class && !refName.isEmpty()) {
                throw new BuilderException("Cannot use both value() and name() attribute in the @CacheNamespaceRef");
            }
            String namespace = (refType != void.class) ? refType.getName() : refName;
            assistant.useCacheRef(namespace);
        }
    }

parseStatement

MapperAnnotationBuilder会遍历Class对象中的所有方法,一个Method对象对应一个MappedStatement对象,ResultMap的定义与xml配置文件方式不同,配置文件由单独的元素定义,而注解方式则定义在方法上。每个方法可以创建新的ResultMap对象,也可以引用已经存在的ResultMap对象的id
解析一个方法生成对应的MapperedStatement对象

  void parseStatement(Method method) {
        // 获取输入参数的类型,这个参数没用,
        // 因为现在都是以输入参数对象为根,通过ognl表达式寻值的
        Class parameterTypeClass = getParameterType(method);
        // 通过方法上的@Lang注解获取语言驱动
        LanguageDriver languageDriver = getLanguageDriver(method);
        // 通过方法上的@Select等注解获取SqlSource
        SqlSource sqlSource = getSqlSourceFromAnnotations(method, parameterTypeClass, languageDriver);
        if (sqlSource != null) {
            Options options = method.getAnnotation(Options.class);
            // 映射语句id为类的全限定名.方法名
            final String mappedStatementId = type.getName() + "." + method.getName();
            Integer fetchSize = null;
            Integer timeout = null;
            StatementType statementType = StatementType.PREPARED;
            ResultSetType resultSetType = ResultSetType.FORWARD_ONLY;
            // 通过注解获取Sql命令类型
            SqlCommandType sqlCommandType = getSqlCommandType(method);
            boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
            boolean flushCache = !isSelect;
            boolean useCache = isSelect;

            KeyGenerator keyGenerator;
            String keyProperty = "id";
            String keyColumn = null;
            // 如果是insert或update命令
            if (SqlCommandType.INSERT.equals(sqlCommandType) || SqlCommandType.UPDATE.equals(sqlCommandType)) {
                // first check for SelectKey annotation - that overrides everything else
                // 首先检查@SelectKey注解 ,它会覆盖任何其他的配置
                // 获取方法上的SelectKey注解
                SelectKey selectKey = method.getAnnotation(SelectKey.class);
                // 如果存在@SelectKey注解
                if (selectKey != null) {
                    keyGenerator = handleSelectKeyAnnotation(selectKey, mappedStatementId, getParameterType(method), languageDriver);
                    keyProperty = selectKey.keyProperty();
                } else if (options == null) {
                    keyGenerator = configuration.isUseGeneratedKeys() ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
                } else {
                    keyGenerator = options.useGeneratedKeys() ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
                    keyProperty = options.keyProperty();
                    keyColumn = options.keyColumn();
                }
            } else {
                // 其他sql命令均没有键生成器
                keyGenerator = NoKeyGenerator.INSTANCE;
            }

            if (options != null) {
                if (FlushCachePolicy.TRUE.equals(options.flushCache())) {
                    flushCache = true;
                } else if (FlushCachePolicy.FALSE.equals(options.flushCache())) {
                    flushCache = false;
                }
                useCache = options.useCache();
                fetchSize = options.fetchSize() > -1 || options.fetchSize() == Integer.MIN_VALUE ? options.fetchSize() : null; //issue #348
                timeout = options.timeout() > -1 ? options.timeout() : null;
                statementType = options.statementType();
                resultSetType = options.resultSetType();
            }
            // 处理方法上的@ResultMap注解
            String resultMapId = null;
            // 获取注解,@ResultMap注解代表引用已经存在的resultMap对象的id
            ResultMap resultMapAnnotation = method.getAnnotation(ResultMap.class);
            // 如果方法上存在@ResultMap注解,则生成引用id即可
            if (resultMapAnnotation != null) {
                // 获取指定的多个resultMapId
                String[] resultMaps = resultMapAnnotation.value();
                StringBuilder sb = new StringBuilder();
                // 遍历String数组,拼接为一个String,逗号分隔
                for (String resultMap : resultMaps) {
                    if (sb.length() > 0) {
                        sb.append(",");
                    }
                    sb.append(resultMap);
                }
                resultMapId = sb.toString();
                // 不存在@ResultMap注解,且语句为select类型,
                // 则通过解析@Args、@Results等注解生成新的ResultMap对象
            } else if (isSelect) {
                // 生成新的ResultMap对象,加入Configuration配置对象
                // 同时返回resultMapId
                resultMapId = parseResultMap(method);
            }
// 构建MappedStatement并添加到配置对象中
            assistant.addMappedStatement(
                    mappedStatementId,
                    sqlSource,
                    statementType,
                    sqlCommandType,
                    fetchSize,
                    timeout,
                    // ParameterMapID
                    null,
                    parameterTypeClass,
                    resultMapId,
                    getReturnType(method),
                    resultSetType,
                    flushCache,
                    useCache,
                    // TODO gcode issue #577
                    false,
                    keyGenerator,
                    keyProperty,
                    keyColumn,
                    // DatabaseID
                    null,
                    languageDriver,
                    // ResultSets
                    options != null ? nullOrEmpty(options.resultSets()) : null);
        }
    }

从代码逻辑可以看出,如果不存在@Select、@Insert、@Update、@Delete或者对应的@xxxProvider中的任何一个,则后续注解全是无效,只有@Lang会起作用。当使用@ResultMap注解引用已存在的结果映射时,后续关于创建新的结果映射的注解将失效。

parseResultMap

private String parseResultMap(Method method) {
        // 获取目标bean的类型
        Class returnType = getReturnType(method);
        // 获取方法上的@ConstructorArgs注解
        ConstructorArgs args = method.getAnnotation(ConstructorArgs.class);
        // 获取方法上的@Results
        Results results = method.getAnnotation(Results.class);
        // 获取方法上的@TypeDiscriminator注解
        TypeDiscriminator typeDiscriminator = method.getAnnotation(TypeDiscriminator.class);
        // 根据方法生成resultMap的唯一标识,格式为:类全限定名.方法名-参数类型简单名称
        String resultMapId = generateResultMapName(method);
        // resultMapId和返回值类型已经解析完毕,
        // 再解析剩下的构造方法映射、属性映射和鉴别器,之后添加结果映射到配置对象中
        applyResultMap(resultMapId, returnType, argsIf(args), resultsIf(results), typeDiscriminator);
        return resultMapId;
    }

注解方式只支持属性映射时使用另外的select语句,不支持嵌套的属性映射的配置。

小结

loadXmlResource可以看出,当在全局配置文件中是使用package配置的时候,会先根据包名,扫描出包下的所有类,
然后根据全限定的类名,把.替换成/在加上.xml当做对应的mapper路径
(com.lf.entity.user)的对应的mapper的路径为com/entity/user.xml
其中class节点配置和package类似。

至此,通过注解配置mapper接口的的流程已经分析完毕了。接下来,会主要解析配置文件的方式,具体分析如何解析的

你可能感兴趣的:(Mybatis源码解析)