第4章 自定義標(biāo)簽的解析


??在之前的章節(jié)中,我們提到了在Spring中存在默認(rèn)標(biāo)簽與自定義標(biāo)簽兩種,而在上一章節(jié)中我們分析了Spring中自定義標(biāo)簽的加載過(guò)程.同樣,我們還是先再次回顧一下,當(dāng)完成從配置文件到Document的轉(zhuǎn)換并提取對(duì)應(yīng)的root后,將開(kāi)始了所有元素的解析,而在這一過(guò)程中便開(kāi)始了默認(rèn)標(biāo)簽與自定義標(biāo)簽兩中格式的區(qū)分,方法如下:

protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {
        if (delegate.isDefaultNamespace(root)) {
            NodeList nl = root.getChildNodes();
            for (int i = 0; i < nl.getLength(); i++) {
                Node node = nl.item(i);
                if (node instanceof Element) {
                    Element ele = (Element) node;
                    if (delegate.isDefaultNamespace(ele)) {
                        parseDefaultElement(ele, delegate);
                    }
                    else {
                        delegate.parseCustomElement(ele);
                    }
                }
            }
        }
        else {
            delegate.parseCustomElement(root);
        }
    }

??在本章中,所有的功能都是圍繞其中的一句代碼delegate.parseCustomElement(root)開(kāi)展的.從上面的函數(shù)我們可以看出,當(dāng)Spring拿到一個(gè)元素時(shí)首先要做的是根據(jù)命名空間進(jìn)行解析,如果是默認(rèn)的命名空間,則使用parseDefaultElement方法進(jìn)行元素解析,否則使用parseCustomElement方法進(jìn)行解析.在分析自定義標(biāo)簽的解析過(guò)程前,我們先了解一下自定義標(biāo)簽的使用過(guò)程.


4.1 自定義標(biāo)簽使用

??在很多情況下,我們需要為系統(tǒng)提供可配置化支持,簡(jiǎn)單的做法可以直接基于Spring的標(biāo)準(zhǔn)bean來(lái)配置,但配置較為復(fù)雜或者需要更多豐富控制的時(shí)候,會(huì)顯得非常笨拙.一般的做法會(huì)用原生態(tài)的方式去解析定義好的XML文件,然后轉(zhuǎn)化為配置對(duì)象.這種方式當(dāng)然可以解決所有問(wèn)題,但實(shí)現(xiàn)起來(lái)比較繁瑣,特別是在配置非常復(fù)雜的時(shí)候,解析工作是一個(gè)不得不考慮的負(fù)擔(dān).Spring提供了可擴(kuò)展Schema的支持,這個(gè)一個(gè)不錯(cuò)的折中方案,擴(kuò)展Spring自定義標(biāo)簽配置大致需要以下幾個(gè)步驟(前提是要把SpringCore包加入項(xiàng)目中).

  • 創(chuàng)建一個(gè)需要擴(kuò)展的組件.
  • 定義一個(gè)XSD文件描述組件內(nèi)容.
  • 創(chuàng)建一個(gè)文件,實(shí)現(xiàn)BeanDefinitionParser接口,用來(lái)解析XSD文件中的定義和組件定義.
  • 創(chuàng)建一個(gè)Handler文件,擴(kuò)展自NamespaceHandlerSupport,目的是將組件注冊(cè)到Spring容器.
  • 編寫Spring.handlersSpring.schemas文件.

?現(xiàn)在我們就按照上面的步驟和大家一起體驗(yàn)自定義標(biāo)簽的過(guò)程.

(1)首先我們創(chuàng)建一個(gè)普通的POJO,這個(gè)POJO沒(méi)有任何特別之處,只是用來(lái)接收配置文件.

public class User {
    private String userName;
    private String email;
    // 省略get/set方法
}

(2)定義一個(gè)XSD文件描述組件內(nèi)容.

<?xml version="1.0" encoding="UTF-8"?>
<schema xmlns="http://www.w3.org/2001/XMLSchema"
        targetNamespace="http://www.test.com/schema/user"
        xmlns:tns="http://www.test.com/schema/user"
        elementFormDefault="qualified">

    <element name="user">
        <complexType>
            <attribute name="id" type="string" />
            <attribute name="userName" type="string" />
            <attribute name="email" type="string" />
        </complexType>
    </element>

</schema>

??在上面的XSD文件中描述了一個(gè)新的targetNamespace,并在這個(gè)空間中定義了一個(gè)nameuserelement,user有3個(gè)屬性id,userNameemail,其中email的類型為string.這3個(gè)類主要用于驗(yàn)證Spring配置文件中自定義格式.XSD文件是XML,DTD的替代者,使用XML Schema語(yǔ)言進(jìn)行編寫,這里對(duì)XSD Schema不做太多解釋,大家有興趣可以自己研究一下.

(3)創(chuàng)建一個(gè)文件,實(shí)現(xiàn)BeanDefinitionParser接口,用來(lái)解析XSD文件中的定義和組件定義.

public class UserBeanDefinitionParser extends AbstractSingleBeanDefinitionParser {

    // Element對(duì)應(yīng)的類
    protected Class getBeanClass(Element element) {
        return User.class;
    }

    // 從element中解析并提取對(duì)應(yīng)的元素
    protected void doParse(Element element, BeanDefinitionBuilder bean) {
        String userName = element.getAttribute("userName");
        String email = element.getAttribute("email");
        // 將提取的數(shù)據(jù)放入到BeanDefinitionBuilder中,待到完成所有bean的解析后統(tǒng)一注冊(cè)到beanFatory中
        if (StringUtils.hasText(userName)) {
            bean.addPropertyValue("userName", userName);
        }
        if (StringUtils.hasText(email)) {
            bean.addPropertyValue("email", email);
        }
    }
}

(4) 創(chuàng)建一個(gè)Handler文件,擴(kuò)展自NamespaceHandlerSupport,目的是將組件注冊(cè)到Spring容器.

public class MyNamespaceHandler extends NamespaceHandlerSupport {
    public void init() {
        registerBeanDefinitionParser("user", new UserBeanDefinitionParser());
    }
}

??以上代碼很簡(jiǎn)單,無(wú)非是當(dāng)遇到自定義標(biāo)簽<user:aaa>這樣類似于以user開(kāi)頭的元素,就會(huì)把這個(gè)元素扔給對(duì)應(yīng)的UserBeanDefinitionParser去解析.

(5) 編寫Spring.handlersSpring.schemas文件,默認(rèn)位置是在工程的/ META-INF/文件夾下,當(dāng)然,你可以通過(guò)Spring的擴(kuò)展或者修改源碼的方式改變路徑.

  • Spring.handlers :
    http\://www.test.com/schema/user=test.MyNamespaceHandler
  • Spring.schema :
    http\://www.test.com/schema/user.xsd=META-INF/Spring-test.xsd

?到這里,自定義的配置就結(jié)束了,而Spring加載自定義的大致流程是遇到自定義標(biāo)簽然后就去Spring.handlersSpring.schemas中去找對(duì)應(yīng)的handlerXSD,默認(rèn)位置是/META-INF/下,進(jìn)而有找到對(duì)應(yīng)的handler以及解析元素的Parser,從而完成整個(gè)自定義元素的解析,也就是說(shuō)自定義與Spring中默認(rèn)的標(biāo)準(zhǔn)配置不同在于Spring將自定義標(biāo)簽解析的工作委托給了用戶去實(shí)現(xiàn).

(6) 創(chuàng)建測(cè)試配置文件,在配置文件中引入對(duì)應(yīng)的命名空間以及XSD后,便可以直接使用自定義標(biāo)簽了.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:myname="http://www.test.com/schema/user"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.test.com/schema/user http://www.test.com/scheame/user.xsd">

    <myname:user id="testbean" userName="aaa" email="bbb" />

</beans>

(7) 測(cè)試

public class test {

    public static void main(String[] args) {
        ApplicationContext bf = new ClassPathXmlApplicationContext("test/test.xml");
        User user = (User) bf.getBean("testbean");
        System.out.println(user.getUserName() + ", " + user.getEmail());
    }
}

?不出意外的話,應(yīng)該可以看到控制臺(tái)打印了如下結(jié)果:
?aaa,bbb
?在上面的例子中,我們實(shí)現(xiàn)了通過(guò)自定義標(biāo)簽通過(guò)屬性的方式將user類型的Bean賦值,在Spring中自定義標(biāo)簽非常常用,例如我們熟知的事物標(biāo)簽:tx(<tx:annotation-driven>).


4.2 自定義標(biāo)簽解析

??了解了自定義標(biāo)簽的使用后,我們帶著強(qiáng)烈的好奇心來(lái)探究一下自定義的解析過(guò)程.

    @Nullable
    public BeanDefinition parseCustomElement(Element ele) {
        return parseCustomElement(ele, null);
    }

    // containingBd為父類bean,對(duì)頂層元素的解析應(yīng)該設(shè)置為null
    @Nullable
    public BeanDefinition parseCustomElement(Element ele, @Nullable BeanDefinition containingBd) {
        // 獲取對(duì)應(yīng)的命名空間
        String namespaceUri = getNamespaceURI(ele);
        if (namespaceUri == null) {
            return null;
        }
        // 根據(jù)命名空間找到對(duì)應(yīng)的NamespaceHandler
        NamespaceHandler handler = this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri);
        if (handler == null) {
            error("Unable to locate Spring NamespaceHandler for XML schema namespace [" + namespaceUri + "]", ele);
            return null;
        }
        // 調(diào)用自定義的NamespaceHandler進(jìn)行解析
        return handler.parse(ele, new ParserContext(this.readerContext, this, containingBd));
    }

??相信了解了自定義標(biāo)簽的使用方法后,或多或少會(huì)對(duì)自定義標(biāo)簽的實(shí)現(xiàn)過(guò)程有一個(gè)自己的想法.其實(shí)實(shí)現(xiàn)思路非常的簡(jiǎn)單,無(wú)非是根據(jù)對(duì)應(yīng)的bean獲取對(duì)應(yīng)的命名空間,根據(jù)命名空間解析對(duì)應(yīng)的處理器,然后根據(jù)用戶自定義的處理器進(jìn)行解析.可是有些事情說(shuō)起來(lái)簡(jiǎn)單做起來(lái)難,我們接下來(lái)先看看如何獲取命名空間.

4.2.1 獲取標(biāo)簽的命名空間

??標(biāo)簽的解析是從命名空間的提起開(kāi)始的,無(wú)論是區(qū)分Spring中默認(rèn)標(biāo)簽和自定義標(biāo)簽還是區(qū)分自定義標(biāo)簽中不同標(biāo)簽的處理器都是以標(biāo)簽所提供的命名空間為基礎(chǔ)的,而至于如何提取對(duì)應(yīng)元素的命名空間其實(shí)不需要我們親自去實(shí)現(xiàn),在org.w3c.dom.Node中已經(jīng)提供了方法供我們直接調(diào)用:

public String getNamespaceURI(Node node){
    return node.getNamespaceURI();
}
4.2.2 提取自定義標(biāo)簽處理器

??有了命名空間,就可以進(jìn)行NamespaceHandler的提取了,繼續(xù)之前的parseCustomElement方法的跟蹤,分析NamespaceHandler handler = this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri);,在readerContext初始化的時(shí)候其屬性namespaceHandlerResolver已經(jīng)被初始化為了DefaultNamespaceHandlerResolver的實(shí)例,所以這里調(diào)用的resolve方法其實(shí)調(diào)用的是DefaultNamespaceHandlerResolver類中的方法.我們進(jìn)入DefaultNamespaceHandlerResolverresolve方法進(jìn)行查看.


    @Override
    @Nullable
    public NamespaceHandler resolve(String namespaceUri) {
        // 獲取所有已經(jīng)配置的handler映射
        Map<String, Object> handlerMappings = getHandlerMappings();
        // 根據(jù)命名空間找到對(duì)應(yīng)的信息
        Object handlerOrClassName = handlerMappings.get(namespaceUri);
        if (handlerOrClassName == null) {
            return null;
        }
        else if (handlerOrClassName instanceof NamespaceHandler) {
            // 已經(jīng)做過(guò)解析的情況直接從緩存讀取
            return (NamespaceHandler) handlerOrClassName;
        }
        else {
            // 沒(méi)有做過(guò)解析則返回類路徑
            String className = (String) handlerOrClassName;
            try {
                Class<?> handlerClass = ClassUtils.forName(className, this.classLoader);
                if (!NamespaceHandler.class.isAssignableFrom(handlerClass)) {
                    throw new FatalBeanException("Class [" + className + "] for namespace [" + namespaceUri +
                            "] does not implement the [" + NamespaceHandler.class.getName() + "] interface");
                }
                // 初始化類
                NamespaceHandler namespaceHandler = (NamespaceHandler) BeanUtils.instantiateClass(handlerClass);
                // 調(diào)用自定義的NamespaceHandler的初始化方法
                namespaceHandler.init();
                // 記錄在緩存中
                handlerMappings.put(namespaceUri, namespaceHandler);
                return namespaceHandler;
            }
            catch (ClassNotFoundException ex) {
                throw new FatalBeanException("Could not find NamespaceHandler class [" + className +
                        "] for namespace [" + namespaceUri + "]", ex);
            }
            catch (LinkageError err) {
                throw new FatalBeanException("Unresolvable class definition for NamespaceHandler class [" +
                        className + "] for namespace [" + namespaceUri + "]", err);
            }
        }
    }

??上面的函數(shù)清晰地闡述了解析自定義NamespaceHandler的過(guò)程,通過(guò)之前的示例程序我們了解到如果要使用自定義標(biāo)簽,那么其中一項(xiàng)必不可少的操作就是在Spring.handlers文件中配置命名空間與命名空間處理的映射關(guān)系.只有這樣,Spring才能根據(jù)映射關(guān)系找到匹配的處理器,而尋找匹配的處理器就是在上面方法中實(shí)現(xiàn),當(dāng)獲取到自定義的NamespaceHandler之后就可以進(jìn)行處理器初始化并解析了.我們這里在回憶一下對(duì)命名空間處理器的定義內(nèi)容:

public class MyNamespaceHandler extends NamespaceHandlerSupport {
    public void init() {
        registerBeanDefinitionParser("user", new UserBeanDefinitionParser());
    }
}

??當(dāng)?shù)玫阶远x命名空間處理后會(huì)馬上執(zhí)行namespaceHandler.init()來(lái)進(jìn)行自定義BeanDefinitionParser的注冊(cè).在這里,可以注冊(cè)多個(gè)標(biāo)簽解釋器,當(dāng)前實(shí)例中只有支持<myname:user>的寫法,也可以在這里注冊(cè)多個(gè)解析器,如<myname:A> <myname:B>等,是的myname的命名空間中可以支持多種標(biāo)簽解析.
??注冊(cè)后,命名空間處理器就可以根據(jù)標(biāo)簽的不同來(lái)調(diào)用不同的解析器進(jìn)行解析.那么,根據(jù)上面的函數(shù)與之前介紹過(guò)的例子,我們基本上可以推斷getHandlerMappings的主要功能就是讀取Spring.handlers配置文件并將配置文件緩存在map中.

private Map<String, Object> getHandlerMappings() {
        Map<String, Object> handlerMappings = this.handlerMappings;
        // 如果沒(méi)有被緩存則開(kāi)始進(jìn)行緩存
        if (handlerMappings == null) {
            synchronized (this) {
                handlerMappings = this.handlerMappings;
                if (handlerMappings == null) {
                    if (logger.isDebugEnabled()) {
                        logger.debug("Loading NamespaceHandler mappings from [" + this.handlerMappingsLocation + "]");
                    }
                    try {
                        Properties mappings =
                                PropertiesLoaderUtils.loadAllProperties(this.handlerMappingsLocation, this.classLoader);
                        if (logger.isDebugEnabled()) {
                            logger.debug("Loaded NamespaceHandler mappings: " + mappings);
                        }
                        handlerMappings = new ConcurrentHashMap<>(mappings.size());
                        // 將Properties格式文件合并到Map格式的handlerMappings中
                        CollectionUtils.mergePropertiesIntoMap(mappings, handlerMappings);
                        this.handlerMappings = handlerMappings;
                    }
                    catch (IOException ex) {
                        throw new IllegalStateException(
                                "Unable to load NamespaceHandler mappings from location [" + this.handlerMappingsLocation + "]", ex);
                    }
                }
            }
        }
        return handlerMappings;
    }

??同我們想象的一樣,接住了工具類PropertiesLoaderUtils對(duì)屬性handlerMappingsLocation進(jìn)行了配置文件的讀取,handlerMappingsLocation被默認(rèn)初始化為META-INF/Spring.handlers.

4.2.3 標(biāo)簽解析

??得到了解析器以及要分析的元素后,Spring就可以將解析工作委托給自定義解析器去解析了.在Spring中的代碼為:

return handler.parse(ele, new ParserContext(this.readerContext, this, containingBd));

??以之前提到的示例進(jìn)行分析,此時(shí)的handler已經(jīng)被實(shí)例化成我們自定義的MyNamespaceHandler了,而MyNamespaceHandler也已經(jīng)完成了初始化的工作,但是在我們實(shí)現(xiàn)的自定義命名空間處理器中并沒(méi)有實(shí)現(xiàn)parse方法,所以推斷,這個(gè)方法是父類中的實(shí)現(xiàn),查看父類NamespaceHandlerSupport中的parse方法.

    @Override
    @Nullable
    public BeanDefinition parse(Element element, ParserContext parserContext) {
        // 尋找解析器并進(jìn)行解析操作
        BeanDefinitionParser parser = findParserForElement(element, parserContext);
        return (parser != null ? parser.parse(element, parserContext) : null);
    }

??解析過(guò)程中首先是尋找元素對(duì)應(yīng)的解析器,進(jìn)而調(diào)用解析器中的parse方法,那么結(jié)合示例來(lái)講,其實(shí)就是首先獲取在MyNameSpaceHandler類中的init方法中注冊(cè)的對(duì)應(yīng)的UserBeanDefinitionParser實(shí)例,并調(diào)用其parse方法進(jìn)行進(jìn)一步解析.

    @Nullable
    private BeanDefinitionParser findParserForElement(Element element, ParserContext parserContext) {
        // 獲取元素名稱,也就是<myname:user中的user,若在示例中,此時(shí)localName為user
        String localName = parserContext.getDelegate().getLocalName(element);
        // 根據(jù)user找到對(duì)應(yīng)的解析器,也就是在registerBeanDefinitionParser("user", new UserBeanDefinitionParser());注冊(cè)的解析器
        BeanDefinitionParser parser = this.parsers.get(localName);
        if (parser == null) {
            parserContext.getReaderContext().fatal(
                    "Cannot locate BeanDefinitionParser for element [" + localName + "]", element);
        }
        return parser;
    }

而對(duì)于parse方法的處理

    @Override
    @Nullable
    public final BeanDefinition parse(Element element, ParserContext parserContext) {
        AbstractBeanDefinition definition = parseInternal(element, parserContext);
        if (definition != null && !parserContext.isNested()) {
            try {
                String id = resolveId(element, definition, parserContext);
                if (!StringUtils.hasText(id)) {
                    parserContext.getReaderContext().error(
                            "Id is required for element '" + parserContext.getDelegate().getLocalName(element)
                                    + "' when used as a top-level tag", element);
                }
                String[] aliases = null;
                if (shouldParseNameAsAliases()) {
                    String name = element.getAttribute(NAME_ATTRIBUTE);
                    if (StringUtils.hasLength(name)) {
                        aliases = StringUtils.trimArrayElements(StringUtils.commaDelimitedListToStringArray(name));
                    }
                }
                // 將AbstractBeanDefinition轉(zhuǎn)化為BeanDefinitionHolder并注冊(cè)
                BeanDefinitionHolder holder = new BeanDefinitionHolder(definition, id, aliases);
                registerBeanDefinition(holder, parserContext.getRegistry());
                if (shouldFireEvents()) {
                    // 需要通知監(jiān)聽(tīng)器則進(jìn)行處理
                    BeanComponentDefinition componentDefinition = new BeanComponentDefinition(holder);
                    postProcessComponentDefinition(componentDefinition);
                    parserContext.registerComponent(componentDefinition);
                }
            }
            catch (BeanDefinitionStoreException ex) {
                String msg = ex.getMessage();
                parserContext.getReaderContext().error((msg != null ? msg : ex.toString()), element);
                return null;
            }
        }
        return definition;
    }

??雖說(shuō)是對(duì)自定義配置文件的解析,但是,我們可以看到,在這個(gè)方法中大部分的代碼是用來(lái)處理將解析后的AbstractBeanDefinition轉(zhuǎn)化為BeanDefinitionHolder并注冊(cè)的功能,而真正去做解析的事情委托給了函數(shù)parseInternal,正是這句代碼調(diào)用了我們自定義的解析函數(shù).
??在parseInternal中并不是直接調(diào)用自定義的doParse函數(shù),而是進(jìn)行了一系列的數(shù)據(jù)準(zhǔn)備,包括對(duì)beanClass scope lazyInit等屬性的準(zhǔn)備.

    @Override
    protected final AbstractBeanDefinition parseInternal(Element element, ParserContext parserContext) {
        BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition();
        String parentName = getParentName(element);
        if (parentName != null) {
            builder.getRawBeanDefinition().setParentName(parentName);
        }
        // 獲取自定義標(biāo)簽中的class,此時(shí)會(huì)調(diào)用自定義解析器如UserBeanDefinitionParser中的getBeanClass方法
        Class<?> beanClass = getBeanClass(element);
        if (beanClass != null) {
            builder.getRawBeanDefinition().setBeanClass(beanClass);
        }
        else {
            // 若子類沒(méi)有重寫getBeanClass方法則嘗試檢查子類是否重寫getBeanClassName方法
            String beanClassName = getBeanClassName(element);
            if (beanClassName != null) {
                builder.getRawBeanDefinition().setBeanClassName(beanClassName);
            }
        }
        builder.getRawBeanDefinition().setSource(parserContext.extractSource(element));
        BeanDefinition containingBd = parserContext.getContainingBeanDefinition();
        if (containingBd != null) {
            // 若存在父類則使用父類的scope屬性
            // Inner bean definition must receive same scope as containing bean.
            builder.setScope(containingBd.getScope());
        }
        if (parserContext.isDefaultLazyInit()) {
            // 調(diào)用子類重寫的doParse方法進(jìn)行解析
            // Default-lazy-init applies to custom bean definitions as well.
            builder.setLazyInit(true);
        }
        doParse(element, parserContext, builder);
        return builder.getBeanDefinition();
    }

??回顧一下全部的自定義標(biāo)簽處理過(guò)程,雖然在實(shí)例中我們定義UserBeanDefinitionParser,但是在其中我們只是做了與自己業(yè)務(wù)邏輯相關(guān)的部分.不過(guò)我們沒(méi)做但是并不代表沒(méi)有,在這個(gè)處理過(guò)程中同樣也是按照Spring中默認(rèn)標(biāo)簽的處理方式進(jìn)行,包括創(chuàng)建BeanDefinition以及進(jìn)行相應(yīng)默認(rèn)屬性的設(shè)置,對(duì)于這些工作Spring都默默地幫我們實(shí)現(xiàn)了,只是暴露出一些接口來(lái)供用戶實(shí)現(xiàn)個(gè)性化的業(yè)務(wù).通過(guò)對(duì)本章的了解,相信讀者對(duì)Spring中自定義標(biāo)簽的使用以及在解析自定義標(biāo)簽過(guò)程中Spring為我們做了哪些工作會(huì)有一個(gè)全面的了解,到此為止我們已經(jīng)完成了Spring中全部的解析工作,也就是說(shuō)到現(xiàn)在為止我們已經(jīng)理解了Springbean從配置文件到加載到內(nèi)存中的全過(guò)程,而接下來(lái)的任務(wù)便是如何使用這些bean.

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容