Spring源码阅读笔记04:默认xml标签解析

  上文我们主要学习了Spring是如何获取xml配置文件并且将其转换成Document,我们知道xml文件是由各种标签组成,Spring需要将其解析成对应的配置信息。之前提到过Spring中的标签包括默认标签和自定义标签两种,而两种标签的用法以及解析方式存在着很大的不同,本文详细分析默认标签的解析过程。

  默认标签的解析是在parseDefaultElement函数中进行的,函数中的功能逻辑一目了然,分别对4种不同标签(import、alias、bean和 beans)做了不同的处理。

private void parseDefaultElement(Element ele, BeanDefinitionParserDelegate delegate) {
    if (delegate.nodeNameEquals(ele, IMPORT_ELEMENT)) {
        // 对import标签的处理
        importBeanDefinitionResource(ele);
    }
    else if (delegate.nodeNameEquals(ele, ALIAS_ELEMENT)) {
        // 对alias标签的处理
        processAliasRegistration(ele);
    }
    else if (delegate.nodeNameEquals(ele, BEAN_ELEMENT)) {
        // 对bean标签的处理
        processBeanDefinition(ele, delegate);
    }
    else if (delegate.nodeNameEquals(ele, NESTED_BEANS_ELEMENT)) {
        // recurse
        // 对beans标签的处理
        doRegisterBeanDefinitions(ele);
    }
}

  在4种标签的解析中,对bean标签的解析最为复杂也最为重要,这是本文重点分析对象,如果能理解此标签的解析过程,其他标签的解析自然会迎刃而解。首先我们进入函数processBeanDefinition(ele, delegate):

protected void processBeanDefinition(Element ele, BeanDefinitionParserDelegate delegate) {
    BeanDefinitionHolder bdHolder = delegate.parseBeanDefinitionElement(ele);
    if (bdHolder != null) {
        bdHolder = delegate.decorateBeanDefinitionIfRequired(ele, bdHolder);
        try {
            // Register the final decorated instance.
            BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder, getReaderContext().getRegistry());
        }
        catch (BeanDefinitionStoreException ex) {
            getReaderContext().error("Failed to register bean definition with name '" +
                    bdHolder.getBeanName() + "'", ele, ex);
        }
        // Send registration event.
        getReaderContext().fireComponentRegistered(new BeanComponentDefinition(bdHolder));
    }
}

  大致的逻辑总结如下: 

  1. 首先委托BeanDefinitionDelegate类的parseBeanDefinitionElement方法进行元素解析,返回BeanDefinitionHolder类型的实例bdHolder,经过这个方法后,bdHolder实例已经包含我们配置文件中配置的各种属性了,例如class、name、id、alias之类的属性;
  2. 当返回的bdHolder不为空的情况下若存在默认标签的子节点下再有自定义属性,还需要再次对自定义标签进行解析;
  3. 解析完成后,需要对解析后的bdHolder进行注册,同样,注册操作委托给了BeanDefinitionReaderUtils的registerBeanDefinition方法;
  4. 最后发出响应事件,通知相关的监听器,这个bean已经加载完成了;

  下面会针对各个操作做进一步分析。

1. 解析BeanDefinition

  首先我们从元素解析及信息提取开始,也就是BeanDefinitionHolder bdHolder = delegate.parseBeanDefinitionElement(ele),进入BeanDefinitionDelegate类的parseBeanDefinitionElement方法:

public BeanDefinitionHolder parseBeanDefinitionElement(Element ele) {
    return parseBeanDefinitionElement(ele, null);
}

public BeanDefinitionHolder parseBeanDefinitionElement(Element ele, BeanDefinition containingBean) {
    // 解析id属性
    String id = ele.getAttribute(ID_ATTRIBUTE);
    // 解析name属性
    String nameAttr = ele.getAttribute(NAME_ATTRIBUTE);
    // 分割name属性
    List<String> aliases = new ArrayList<String>();
    if (StringUtils.hasLength(nameAttr)) {
        String[] nameArr = StringUtils.tokenizeToStringArray(nameAttr, MULTI_VALUE_ATTRIBUTE_DELIMITERS);
        aliases.addAll(Arrays.asList(nameArr));
    }

    String beanName = id;
    if (!StringUtils.hasText(beanName) && !aliases.isEmpty()) {
        beanName = aliases.remove(0);
        if (logger.isDebugEnabled()) {
            logger.debug("No XML 'id' specified - using '" + beanName +
                    "' as bean name and " + aliases + " as aliases");
        }
    }

    if (containingBean == null) {
        checkNameUniqueness(beanName, aliases, ele);
    }

    AbstractBeanDefinition beanDefinition = parseBeanDefinitionElement(ele, beanName, containingBean);
    if (beanDefinition != null) {
        if (!StringUtils.hasText(beanName)) {
            try {
                // 如果不存在beanName那么根据Spring中提供的命名规则为当前bean生成对应的beanName
                if (containingBean != null) {
                    beanName = BeanDefinitionReaderUtils.generateBeanName(
                            beanDefinition, this.readerContext.getRegistry(), true);
                }
                else {
                    beanName = this.readerContext.generateBeanName(beanDefinition);
                    // Register an alias for the plain bean class name, if still possible,
                    // if the generator returned the class name plus a suffix.
                    // This is expected for Spring 1.2/2.0 backwards compatibility.
                    String beanClassName = beanDefinition.getBeanClassName();
                    if (beanClassName != null &&
                            beanName.startsWith(beanClassName) && beanName.length() > beanClassName.length() &&
                            !this.readerContext.getRegistry().isBeanNameInUse(beanClassName)) {
                        aliases.add(beanClassName);
                    }
                }
                if (logger.isDebugEnabled()) {
                    logger.debug("Neither XML 'id' nor 'name' specified - " +
                            "using generated bean name [" + beanName + "]");
                }
            }
            catch (Exception ex) {
                error(ex.getMessage(), ele);
                return null;
            }
        }
        String[] aliasesArray = StringUtils.toStringArray(aliases);
        return new BeanDefinitionHolder(beanDefinition, beanName, aliasesArray);
    }

    return null;
}

  以上便是对默认标签解析的全过程了。在开始对属性展开全面解析前,Spring又做了一些功能划分,主要如下:

  1. 提取元素中的id以及name属性;
  2. 进一步解析其他所有属性并统一封装至GenericBeanDefinition类型的实例中;
  3. 如果检测到bean没有指定beanName,那么使用默认规则为此Bean生成beanName;
  4. 将获取到的信息封装到BeanDefinitionHolder的实例中;

  我们进一步地查看步骤2中对标签其他属性的解析过程:

public AbstractBeanDefinition parseBeanDefinitionElement(
            Element ele, String beanName, BeanDefinition containingBean) {
    this.parseState.push(new BeanEntry(beanName));
    String className = null;
    // 解析class属性
    if (ele.hasAttribute(CLASS_ATTRIBUTE)) {
        className = ele.getAttribute(CLASS_ATTRIBUTE).trim();
    }

    try {
        String parent = null;
        // 解析parent属性
        if (ele.hasAttribute(PARENT_ATTRIBUTE)) {
            parent = ele.getAttribute(PARENT_ATTRIBUTE);
        }
        // 创建用于承载属性的AbstractBeanDefinition类型的GenericBeanDefinition
        AbstractBeanDefinition bd = createBeanDefinition(className, parent);
        // 硬编码解析默认bean的各种属性
        parseBeanDefinitionAttributes(ele, beanName, containingBean, bd);
        // 提取description
        bd.setDescription(DomUtils.getChildElementValueByTagName(ele, DESCRIPTION_ELEMENT));
        // 解析元数据
        parseMetaElements(ele, bd);
        // 解析lookup-method属性
        parseLookupOverrideSubElements(ele, bd.getMethodOverrides());
        // 解析replaced-method属性
        parseReplacedMethodSubElements(ele, bd.getMethodOverrides());
        // 解析构造函数参数
        parseConstructorArgElements(ele, bd);
        // 解析property子元素
        parsePropertyElements(ele, bd);
        // 解析qualifier子元素
        parseQualifierElements(ele, bd);
        bd.setResource(this.readerContext.getResource());
        bd.setSource(extractSource(ele));
        return bd;
    }
    catch (ClassNotFoundException ex) {
        error("Bean class [" + className + "] not found", ele, ex);
    }
    catch (NoClassDefFoundError err) {
        error("Class that bean class [" + className + "] depends on not found", ele, err);
    }
    catch (Throwable ex) {
        error("Unexpected failure during bean definition parsing", ele, ex);
    }
    finally {
        this.parseState.pop();
    }
    return null;
}

  到这里,bean标签的所有属性解析,不论常用的还是不常用的我们都看到了,尽管有些复杂的属性还需要进一步的解析。我们着重看一些复杂标签属性的解析。

1.1 创建用于属性承载的BeanDefinition

  BeanDefinition是一个接口,在Spring中有三个实现类:RootBeanDefinition、ChildBeanDefinition以及GenericBeanDefinition,均继承自AbstractBeanDefiniton,其中BeanDefinition是配置文件<bean>元素标签在容器中的内部表示形式。<bean>元素标签拥有class、scope、lazy-init等配置属性,BeanDefinition则提供了相应的beanClass、scope、lazyInit属性,BeanDefinition和<bean>中的属性是一一对应的。其中RootBeanDefinition是最常用的实现类,它对应一般性的<bean>元素标签, GenericBeanDefinition是自2.5版本以后新加入的bean文件配置属性定义类,是一站式服务类。

  在配置文件中可以定义父<bean>和子<bean>,父<bean>用RootBeanDefinition表示,而子<bean>用ChildBeanDefiniton表示,而没有父<bean>的<bean>就使用RootBeanDefinition表示。

  Spring通过BeanDefinition将配置文件中的<bean>配置信息转换为容器的内部表示,并将这些BeanDefiniton注册到BeanDefinitonRegistry中。Spring容器的BeanDefinitionRegistry就像是Spring配置信息的内存数据库,主要是以map的形式保存,后续操作直接从BeanDefinitionRegistry中读取配置信息

  由此可知,要解析属性首先要创建用于承载属性的实例,也就是创建GenericBeanDefinition类型的实例。而代码createBeanDefinition(className,parent)的作用就是实现此功能:

protected AbstractBeanDefinition createBeanDefinition(String className, String parentName)
            throws ClassNotFoundException {
    return BeanDefinitionReaderUtils.createBeanDefinition(
            parentName, className, this.readerContext.getBeanClassLoader());
}

public static AbstractBeanDefinition createBeanDefinition(
            String parentName, String className, ClassLoader classLoader) throws ClassNotFoundException {
    GenericBeanDefinition bd = new GenericBeanDefinition();
    // parentName可能为空
    bd.setParentName(parentName);
    if (className != null) {
        // 如果classLoader不为空,则使用已传入的classLoader加载类对象,否则只是记录className
        if (classLoader != null) {
            bd.setBeanClass(ClassUtils.forName(className, classLoader));
        }
        else {
            bd.setBeanClassName(className);
        }
    }
    return bd;
}

1.2 解析各种属性

  当我们创建了bean信息的承载实例后,便可以开始各种属性解析了,这部分的主要逻辑在parseBeanDefinitionAttributes方法中:

public AbstractBeanDefinition parseBeanDefinitionAttributes(Element ele, String beanName,
            BeanDefinition containingBean, AbstractBeanDefinition bd) {
    // 解析scope属性
    if (ele.hasAttribute(SCOPE_ATTRIBUTE)) {
        // Spring 2.x "scope" attribute
        bd.setScope(ele.getAttribute(SCOPE_ATTRIBUTE));
        if (ele.hasAttribute(SINGLETON_ATTRIBUTE)) {
            // scope与singleton两个属性只能指定其中之一,不可以同时出现,否则Spring将会报出异常
            error("Specify either 'scope' or 'singleton', not both", ele);
        }
    }
    // 解析singleton属性
    else if (ele.hasAttribute(SINGLETON_ATTRIBUTE)) {
        // Spring 1.x "singleton" attribute
        bd.setScope(TRUE_VALUE.equals(ele.getAttribute(SINGLETON_ATTRIBUTE)) ?
                BeanDefinition.SCOPE_SINGLETON : BeanDefinition.SCOPE_PROTOTYPE);
    }
    else if (containingBean != null) {
        // Take default from containing bean in case of an inner bean definition.
        // 在嵌入beanDefinition情况下且没有单独指定scope属性则使用父类默认的属性
        bd.setScope(containingBean.getScope());
    }
    // 解析abstract属性
    if (ele.hasAttribute(ABSTRACT_ATTRIBUTE)) {
        bd.setAbstract(TRUE_VALUE.equals(ele.getAttribute(ABSTRACT_ATTRIBUTE)));
    }
    // 解析lazy-init属性
    String lazyInit = ele.getAttribute(LAZY_INIT_ATTRIBUTE);
    if (DEFAULT_VALUE.equals(lazyInit)) {
        lazyInit = this.defaults.getLazyInit();
    }
    // 若没有设置或设置成其他字符都会被设置为false
    bd.setLazyInit(TRUE_VALUE.equals(lazyInit));
    // 解析autowire属性
    String autowire = ele.getAttribute(AUTOWIRE_ATTRIBUTE);
    bd.setAutowireMode(getAutowireMode(autowire));
    // 解析dependency-check属性
    String dependencyCheck = ele.getAttribute(DEPENDENCY_CHECK_ATTRIBUTE);
    bd.setDependencyCheck(getDependencyCheck(dependencyCheck));
    // 解析depends-on属性
    if (ele.hasAttribute(DEPENDS_ON_ATTRIBUTE)) {
        String dependsOn = ele.getAttribute(DEPENDS_ON_ATTRIBUTE);
        bd.setDependsOn(StringUtils.tokenizeToStringArray(dependsOn, MULTI_VALUE_ATTRIBUTE_DELIMITERS));
    }
    // 解析autowire-candidate属性
    String autowireCandidate = ele.getAttribute(AUTOWIRE_CANDIDATE_ATTRIBUTE);
    if ("".equals(autowireCandidate) || DEFAULT_VALUE.equals(autowireCandidate)) {
        String candidatePattern = this.defaults.getAutowireCandidates();
        if (candidatePattern != null) {
            String[] patterns = StringUtils.commaDelimitedListToStringArray(candidatePattern);
            bd.setAutowireCandidate(PatternMatchUtils.simpleMatch(patterns, beanName));
        }
    }
    else {
        bd.setAutowireCandidate(TRUE_VALUE.equals(autowireCandidate));
    }
    // 解析primary属性
    if (ele.hasAttribute(PRIMARY_ATTRIBUTE)) {
        bd.setPrimary(TRUE_VALUE.equals(ele.getAttribute(PRIMARY_ATTRIBUTE)));
    }
    // 解析init-method属性
    if (ele.hasAttribute(INIT_METHOD_ATTRIBUTE)) {
        String initMethodName = ele.getAttribute(INIT_METHOD_ATTRIBUTE);
        if (!"".equals(initMethodName)) {
            bd.setInitMethodName(initMethodName);
        }
    }
    else {
        if (this.defaults.getInitMethod() != null) {
            bd.setInitMethodName(this.defaults.getInitMethod());
            bd.setEnforceInitMethod(false);
        }
    }
    // 解析destroy-method属性
    if (ele.hasAttribute(DESTROY_METHOD_ATTRIBUTE)) {
        String destroyMethodName = ele.getAttribute(DESTROY_METHOD_ATTRIBUTE);
        if (!"".equals(destroyMethodName)) {
            bd.setDestroyMethodName(destroyMethodName);
        }
    }
    else {
        if (this.defaults.getDestroyMethod() != null) {
            bd.setDestroyMethodName(this.defaults.getDestroyMethod());
            bd.setEnforceDestroyMethod(false);
        }
    }
    // 解析factory-method属性
    if (ele.hasAttribute(FACTORY_METHOD_ATTRIBUTE)) {
        bd.setFactoryMethodName(ele.getAttribute(FACTORY_METHOD_ATTRIBUTE));
    }
    // 解析factory-bean属性
    if (ele.hasAttribute(FACTORY_BEAN_ATTRIBUTE)) {
        bd.setFactoryBeanName(ele.getAttribute(FACTORY_BEAN_ATTRIBUTE));
    }
    return bd;
}

  我们可以清楚地看到Spring完成了对所有bean属性的解析,这些属性中有很多是我们经常使用的,也有些是我们不熟悉的,有兴趣可以查阅相关资料进一步了解。当然除了这部分属性解析,后面还有一些其他的属性解析(比如constructor-arg、property等),在此就不全部列出了。

 

2. AbstractBeanDefinition

  至此便完成了对XML文档到GenericBeanDefinition的转换,也就是说到这里,XML中所有的配置都可以在GenericBeanDefinition的实例类中找到对应的属性。

  GenericBeanDefinition只是子类实现,大部分的通用属性都保存在了AbstractBeanDefinition中,所以这里再次通过AbstractBeanDefinition的属性来回顾一下都解析了哪些对应的配置,以加深理解:

public abstract class AbstractBeanDefinition extends BeanMetadataAttributeAccessor
        implements BeanDefinition, Cloneable {
    // 此处省略静态变量以及final常量
    
    /** bean的作用范围,对应bean属性scope */    
    private String scope = SCOPE_DEFAULT;
    
    /** 是否是单例,来自bean属性scope */
    private boolean singleton = true;
    
    /** 是否是原型,来自bean属性scope */
    private boolean prototype = false;

    /** 是否是抽象,对应bean属性abstract */
    private boolean abstractFlag = false;
    
    /** 是否延迟加载,对应bean属性lazy-init */
    private boolean lazyInit = false;
    
    /** 自动注入模式,对应bean属性autowire */
    private int autowireMode = AUTOWIRE_NO;
    
    /** 依赖检查,Spring 3.0后弃用这个属性 */
    private int dependencyCheck = DEPENDENCY_CHECK_NONE;

    /** 用来表示一个bean的实例化依靠另一个bean先实例化,对应bean属性depend-on */
    private String[] dependsOn;
    
    /** autowire-candidate属性设置为false,这样容器在查找自动装配对象时,
     *将不考虑该bean,即它不会被考虑作为其他bean自动装配的候选
     *者,但是该bean本身还是可以使用自动装配来注入其他bean的
     *对应bean属性autowire-candidate */
    private boolean autowireCandidate = true;

    /** 自动装配时当出现多个bean候选着时,将作为首选着,对应bean属性primary */
    private boolean primary = false;
    
    /**  */
    private final Map<String, AutowireCandidateQualifier> qualifiers =
            new LinkedHashMap<String, AutowireCandidateQualifier>(0);

    /**  */
    private boolean nonPublicAccessAllowed = true;
    
    /**  */
    private boolean lenientConstructorResolution = true;
    
    /**  */
    private ConstructorArgumentValues constructorArgumentValues;

    /**  */
    private MutablePropertyValues propertyValues;

    /**  */
    private MethodOverrides methodOverrides = new MethodOverrides();

    /**  */
    private String factoryBeanName;

    /**  */
    private String factoryMethodName;

    /**  */
    private String initMethodName;

    /**  */
    private String destroyMethodName;

    /**  */
    private boolean enforceInitMethod = true;

    /**  */
    private boolean enforceDestroyMethod = true;

    /**  */
    private boolean synthetic = false;
    
    /**  */
    private int role = BeanDefinition.ROLE_APPLICATION;

    /**  */
    private String description;
    
    /** 这个bean定义的资源 */ 
    private Resource resource;
}

3. 注册解析的BeanDefinition

  解析完成之后得到的beanDinition已经可以满足后续的使用要求了,唯一还剩下的工作就是注册,也就是processBeanDefinition函数中的BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder, getReaderContext(). get())所完成的工作:

public static void registerBeanDefinition(
            BeanDefinitionHolder definitionHolder, BeanDefinitionRegistry registry)
            throws BeanDefinitionStoreException {

    // Register bean definition under primary name.
    String beanName = definitionHolder.getBeanName();
    registry.registerBeanDefinition(beanName, definitionHolder.getBeanDefinition());

    // Register aliases for bean name, if any.
    String[] aliases = definitionHolder.getAliases();
    if (aliases != null) {
        for (String aliase : aliases) {
            registry.registerAlias(beanName, aliase);
        }
    }
}

  从上面的代码可以看出,解析好的beanDefinition都会被注册到BeanDefinitionRegistry类型的实例registry中,beanDefinition的注册分成了两部分:通过beanName的注册以及通过别名的注册。

3.1 通过beanName注册BeanDefinition

public void registerBeanDefinition(String beanName, BeanDefinition beanDefinition)
            throws BeanDefinitionStoreException {

    Assert.hasText(beanName, "Bean name must not be empty");
    Assert.notNull(beanDefinition, "BeanDefinition must not be null");

    if (beanDefinition instanceof AbstractBeanDefinition) {
        try {
            // 注册前的最后一次校验,这里的校验不同于之前的XML文件校验
            // 主要是对于AbstractBeanDefinition属性中的methodOverrides校验
            // 校验methodOverrides是否与工厂方法并存或者methodOverrides对应的方法根本不存在
            ((AbstractBeanDefinition) beanDefinition).validate();
        }
        catch (BeanDefinitionValidationException ex) {
            throw new BeanDefinitionStoreException(beanDefinition.getResourceDescription(), beanName,
                    "Validation of bean definition failed", ex);
        }
    }

    BeanDefinition oldBeanDefinition;
    // 因为beanDefinitionMap是全局变量,这里肯定会存在并发访问的情况
    synchronized (this.beanDefinitionMap) {
        oldBeanDefinition = this.beanDefinitionMap.get(beanName);
        // 处理注册已经注册的beanName情况
        if (oldBeanDefinition != null) {
            // 如果对应的BeanName已经注册并且在配置中配置了bean不允许被覆盖,则抛出异常
            if (!this.allowBeanDefinitionOverriding) {
                throw new BeanDefinitionStoreException(beanDefinition.getResourceDescription(), beanName,
                        "Cannot register bean definition [" + beanDefinition + "] for bean '" + beanName +
                        "': There is already [" + oldBeanDefinition + "] bound.");
            }
            else {
                if (this.logger.isInfoEnabled()) {
                    this.logger.info("Overriding bean definition for bean '" + beanName +
                            "': replacing [" + oldBeanDefinition + "] with [" + beanDefinition + "]");
                }
            }
        }
        else {
            // 记录beanName
            this.beanDefinitionNames.add(beanName);
            this.frozenBeanDefinitionNames = null;
        }
        // 注册beanDefinition
        this.beanDefinitionMap.put(beanName, beanDefinition);
    }
    // 重置所有beanName对应的缓存
    if (oldBeanDefinition != null || containsSingleton(beanName)) {
        resetBeanDefinition(beanName);
    }
}

  如上,在对于bean的注册处理方式上,主要进行了如下几个步骤:

  1. 对AbstractBeanDefinition的校验。在解析XML文件的时候我们提过校验,但是此校验非彼校验,之前的校验是针对于XML格式的校验,而此时的校验则是对于AbstractBeanDefinition的methodOverrides属性的;
  2. 对beanName已经注册的情况的处理。如果设置了不允许bean的覆盖,会抛出异常,否则直接覆盖;
  3. 加入map缓存;
  4. 清除解析之前留下的对应 beanName的缓存;

3.2 通过别名注册BeanDefinition

public void registerAlias(String name, String alias) {
    Assert.hasText(name, "'name' must not be empty");
    Assert.hasText(alias, "'alias' must not be empty");
    // 如果beanName与alias相同的话不记录alias,并删除对应的alias
    if (alias.equals(name)) {
        this.aliasMap.remove(alias);
    }
    else {
        // 如果alias不允许被覆盖则抛出异常
        if (!allowAliasOverriding()) {
            String registeredName = this.aliasMap.get(alias);
            if (registeredName != null && !registeredName.equals(name)) {
                throw new IllegalStateException("Cannot register alias '" + alias + "' for name '" +
                        name + "': It is already registered for name '" + registeredName + "'.");
            }
        }
        // 当A->B存在时,若再次出现A->C->B时候则会抛出异常
        checkForAliasCircle(name, alias);
        this.aliasMap.put(alias, name);
    }
}

  由以上代码中可以得知注册alias的步骤如下:

  1. alias与beanName相同情况处理。若alias与beanName名称相同则不需要处理并删除掉原有alias;
  2. alias覆盖处理。若aliasName已经使用并已经指向了另一beanName则需要根据用户的设置进行处理;
  3. alias循环检查。当A->B存在时,若再次出现A->C->B时候则会抛出异常;
  4. 注册alias;

4. 总结

  本文主要集中在分析从如何将默认xml标签解析成BeanDefinition到将其注册到容器中这一过程,至此Spring对配置的转化工作就完成了,后面就要开始Bean的获取这部分的逻辑的分析了。

posted on 2020-03-07 19:30  木瓜芒果  阅读(458)  评论(0编辑  收藏  举报