Spring自定義標(biāo)簽的定義和解析

一、自定義標(biāo)簽的定義

1. 什么是自定義的標(biāo)簽?

1.1 自定義標(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:muzi="http://www.jd.com/schema/mytags"
       xsi:schemaLocation="
       http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.jd.com/schema/mytags
       http://www.jd.com/schema/mytags.xsd
       ">
    <!--
        我自己的自定義標(biāo)簽
    -->
    <muzi:exercise-function id="redis" ip="118.89.164.186" port="6379" password="root"/>

</beans>

該案例是基于Jedis創(chuàng)建了一個(gè)自定義標(biāo)簽的案例,在上述配置代碼中,有幾點(diǎn)需要描述一下。

  1. xmlns是xml namespace的縮寫,xmlns:muzi=“http://www.jd.com/schema/mytags”,其中muzi是當(dāng)前配置文件自定義的命名空間的名稱,URL是命名空間的值。

  2. xsi:schemaLocation定義的是命名空間的內(nèi)容地址,第一個(gè)URI“http://www.jd.com/schema/mytags”是命名空間的值,第二個(gè)URI“http://www.jd.com/schema/mytags.xsd”是Schema文檔的位置,Schema處理器將從這個(gè)位置讀取Schema文檔,且該文檔的targetNamespace必須與第一個(gè)URI相匹配。

  3. muzi:exercise-function標(biāo)簽中的參數(shù)在命名空間在Schema文檔中去定義。具體的需要先定義一個(gè) mytags.xsd 文件,定義 exercise-function 標(biāo)簽有什么屬性,屬性是什么類型的。

1.2 自定義標(biāo)簽Schema文檔
<?xml version="1.0" encoding="UTF-8"?>
<xsd:schema xmlns="http://www.jd.com/schema/mytags"
    xmlns:xsd="http://www.w3.org/2001/XMLSchema" targetNamespace="http://www.jd.com/schema/mytags"
 elementFormDefault="qualified" attributeFormDefault="unqualified">

    <xsd:element name="exercise-function">
        <xsd:complexType>
            <xsd:attribute name="id" type="xsd:string"></xsd:attribute>
            <xsd:attribute name="ip" type="xsd:string"></xsd:attribute>
            <xsd:attribute name="port" type="xsd:string"></xsd:attribute>
            <xsd:attribute name="password" type="xsd:string"></xsd:attribute>
        </xsd:complexType>
    </xsd:element>
</xsd:schema>
Schema文檔概述
  • Schema文檔定義的位置需要在項(xiàng)目resources文件夾下定義,META-INF/mytags.xsd

  • targetNamespace="http://www.jd.com/schema/mytags"與之前配置文件中的namespaceURI是相對(duì)應(yīng)的。

  • xsd:element定義的是一類標(biāo)簽的內(nèi)容,xsd:complexType標(biāo)簽具體內(nèi)屬性類型。

1.3 程序如何定位Schema

配置文件中,Spring是通過(guò) xsi:schemaLocation=“.....” 里面定義的內(nèi)容找到這個(gè)xsd去解析的,但是“http://www.jd.com/schema/mytags.xsd”不是一個(gè)有效的URL,那么Spring是如何找到Schema文檔的呢?

# 地址映射文件:META-INF/spring.schemas

http\://www.jd.com/schema/mytags.xsd=META-INF/mytags.xsd

如上述代碼所示,Spring會(huì)通過(guò)掃描各個(gè)依賴包下面的“spring.schemas”文件,通過(guò)文檔的邏輯地址和物理地址的映射來(lái)找到自定義標(biāo)簽Schema文檔的具體位置。

簡(jiǎn)單來(lái)看,自定義標(biāo)簽無(wú)非就是定義一個(gè)內(nèi)容的格式,屬性,就是一種常用的配置的約束。

2、為什么要?jiǎng)?chuàng)建自定義標(biāo)簽?

日常開發(fā)中接觸到的自定義標(biāo)簽有很多,老版本的Spring基本都是依賴xml配置去使用的,spring擴(kuò)展內(nèi)容的jar包也好,第三方功能性jar包也好,在集成Spring的時(shí)候總要提供一個(gè)客戶端的配置方式,所以開源的jar包集成Spring一般都會(huì)提供一些自定義標(biāo)簽。
例如:druid,mybatis,jedis,dubbo等。

二、理解SPI設(shè)計(jì)模式

SPI ,全稱為 Service Provider Interface,是一種服務(wù)發(fā)現(xiàn)機(jī)制。它通過(guò)在ClassPath路徑下的META-INF/services文件夾查找配置文件,自動(dòng)加載文件里所定義的類。是Java提供的一套用來(lái)被第三方實(shí)現(xiàn)或者擴(kuò)展的API,它可以用來(lái)啟用框架擴(kuò)展和替換組件。

1. Java原生的SPI

SPI配置文件:META-INF/services/com.jd.nlp.dev.muzi.spring5.exercise.pattern.spi.SpiService
描述:
在resources的META-INF下創(chuàng)建一個(gè)services文件夾,以“com.jd.nlp.dev.muzi.spring5.exercise.pattern.spi.SpiService”接口的包路徑命名創(chuàng)建一個(gè)文件。

SPI配置文件內(nèi)容:

com.jd.nlp.dev.muzi.spring5.exercise.pattern.spi.SpiService01
com.jd.nlp.dev.muzi.spring5.exercise.pattern.spi.SpiService02

SPI接口定義:

/*
* service provider interface
* */
public interface SpiService {

    public String query(String param);
}

SPI接口兩個(gè)實(shí)現(xiàn)類定義:

public class SpiService01 implements SpiService {
    @Override
    public String query(String param) {
        System.out.println("SpiService01");
        return "OK";
    }
}

public class SpiService02 implements SpiService {
    @Override
    public String query(String param) {
        System.out.println("SpiService02");
        return null;
    }
}

Java原生SPI一般是怎么用的呢?
首先他會(huì)加載所有SpiService在配置文件中配置的實(shí)現(xiàn)類并創(chuàng)建實(shí)例,使用的時(shí)候一般是遍歷使用,通過(guò)判斷其類型執(zhí)行具體實(shí)現(xiàn)類的方法。當(dāng)然這種SPI也是有利弊的,不能不分場(chǎng)合的隨意效仿。其優(yōu)點(diǎn)是擴(kuò)展很容易,寫一個(gè)類加入即可。缺點(diǎn)也很明顯,粒度不夠細(xì),通過(guò)配置的方式寫了很多的類,當(dāng)需要通過(guò)配置的方式獲取唯一一個(gè)類是這種形式就不可以了,就要考慮使用策略模式。

        ServiceLoader<SpiService> load = ServiceLoader.load(SpiService.class);
        // 這么使用其實(shí)和BeanPostProcessor的使用很像
        for (SpiService spiService : load) {
            if (spiService instanceof SpiService01){
                spiService.query("90");
            }
            if (spiService instanceof SpiService02){
                spiService.query("90");
            }
        }
----------------------------------------------------------------------------------------
執(zhí)行結(jié)果:
SpiService01
SpiService02

Process finished with exit code 0

2. 了解Spring的SPI機(jī)制

上述代碼可以通過(guò)instanceof去判斷并具體類型對(duì)象的方法,這種方式其實(shí)和Spring中的SPI殊途同歸,Spring是通過(guò)掃描各個(gè)jar的META-INF中的spring.handlers,提取namespaceURI和實(shí)現(xiàn)類路徑,以namespaceURI做key,以實(shí)現(xiàn)類路徑反射創(chuàng)建的對(duì)象作為值,來(lái)構(gòu)建映射表。
spring.handlers中配置的解析類都需要繼承NamespaceHandler這個(gè)接口實(shí)現(xiàn)多態(tài),并且根據(jù)需求選擇重寫該接口的相關(guān)方法(init,parse,decorate)。

三、源碼解讀 - 自定義標(biāo)簽解析源碼

1、自定義標(biāo)簽解析流程源碼的位置

DefaultBeanDefinitionDocumentReader.java

    protected void doRegisterBeanDefinitions(Element root) {
        BeanDefinitionParserDelegate parent = this.delegate;
        /**
         * 主要是獲取delegate,用來(lái)委托給第三方解析起解析自定義標(biāo)簽
         */
        this.delegate = createDelegate(getReaderContext(), root, parent);
        if (this.delegate.isDefaultNamespace(root)) {
            // ... ... 省略
        }
        /**
         * 預(yù)處理模版方法
         */
        preProcessXml(root);
        /**
         * 主要看這個(gè)方法,標(biāo)簽的具體解析過(guò)程
         */
        parseBeanDefinitions(root, this.delegate);
        /**
         * 后處理模版方法
         */
        postProcessXml(root);
        this.delegate = parent;
    }

找到DefaultBeanDefinitionDocumentReader類,查看doRegisterBeanDefinitions方法,在之前的《Spring解析XML注冊(cè)BeanDefinition》的文章走過(guò)這個(gè)流程。先進(jìn)入parseBeanDefinitions方法。

    protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {
        if (delegate.isDefaultNamespace(root)) {
            /**
             * 獲取根節(jié)點(diǎn)中所有的子節(jié)點(diǎn)
             */
            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)) {
                        /**
                         * 默認(rèn)標(biāo)簽解析
                         */
                        parseDefaultElement(ele, delegate);
                    }
                    else {
                        /**
                         * 自定義標(biāo)簽解析,委托給delegate解析
                         */
                        delegate.parseCustomElement(ele);
                    }
                }
            }
        }
        // ... ... 
    }

進(jìn)入自定義的標(biāo)簽解析的方法。

BeanDefinitionParserDelegate.java

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

再次進(jìn)入重載的解析方法,找到了自定義標(biāo)簽解析的主流程內(nèi)容。

    public BeanDefinition parseCustomElement(Element ele, @Nullable BeanDefinition containingBd) {
        /**
         * 1.獲取標(biāo)簽元素的NamespaceURI
         */
        String namespaceUri = getNamespaceURI(ele);
        if (namespaceUri == null) {
            return null;
        }
        /**
         * 2.通過(guò)URI來(lái)獲得對(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;
        }
        /**
         * 3.使用handler來(lái)解析該標(biāo)簽,帶入 readerContext 進(jìn)去。
         */
        return handler.parse(ele, new ParserContext(this.readerContext, this, containingBd));
    }

上述代碼中自定義標(biāo)簽解析主要分三步:

  1. 獲取自定義標(biāo)簽的namespace URI。
  2. 根據(jù)namespace URI創(chuàng)建namespace URI和NamespaceHandler的映射并獲取對(duì)應(yīng)的NamespaceHandler的具體實(shí)例。
  3. 執(zhí)行該NamespaceHandler的parse方法。
    其中重點(diǎn)內(nèi)容是創(chuàng)建映射初始化和得到對(duì)應(yīng)的NamespaceHandler實(shí)例。

2、基于我的 exercise-function 自定義標(biāo)簽解讀源碼

2.1 exercise-function 使用

Spring配置文件:spring.xml

<?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:muzi="http://www.jd.com/schema/mytags"
       xsi:schemaLocation="
       http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.jd.com/schema/mytags
       http://www.jd.com/schema/mytags.xsd
       ">
    <!--
        我自己的自定義標(biāo)簽
    -->
    <muzi:exercise-function id="redis" ip="118.89.164.186" port="6379" password="root"/>

</beans>

Schema 映射文件:META-INF/spring.schemas

http\://www.jd.com/schema/mytags.xsd=META-INF/mytags.xsd

Schema 文檔文件:META-INF/mytags.xsd

<?xml version="1.0" encoding="UTF-8"?>
<xsd:schema xmlns="http://www.jd.com/schema/mytags"
    xmlns:xsd="http://www.w3.org/2001/XMLSchema" targetNamespace="http://www.jd.com/schema/mytags"
 elementFormDefault="qualified" attributeFormDefault="unqualified">

    <xsd:element name="exercise-function">
        <xsd:complexType>
            <xsd:attribute name="id" type="xsd:string"></xsd:attribute>
            <xsd:attribute name="ip" type="xsd:string"></xsd:attribute>
            <xsd:attribute name="port" type="xsd:string"></xsd:attribute>
            <xsd:attribute name="password" type="xsd:string"></xsd:attribute>
        </xsd:complexType>
    </xsd:element>
</xsd:schema>

自定義標(biāo)簽解析類映射文件:META-INF/spring.handlers


http\://www.jd.com/schema/mytags=com.jd.nlp.dev.muzi.spring5.exercise.demo08.exercise02.TagsNamespaceHandler

自定義標(biāo)簽解析類:TagsNamespaceHandler.java

public class TagsNamespaceHandler extends NamespaceHandlerSupport {
    
    public void init() {
        this.registerBeanDefinitionParser("exercise-function",
                new RedisBeanDifinitionParser());
    }

    @Override
    public BeanDefinitionHolder decorate(Node node, BeanDefinitionHolder definition, ParserContext parserContext) {
        return super.decorate(node, definition, parserContext);
    }
}

測(cè)試程序,加載上述配置文件,因?yàn)槲业臉?biāo)簽包裝的是自定義注解的

    @Test
    public void run03(){
        // 自定義標(biāo)簽掃描
        ApplicationContext app = new ClassPathXmlApplicationContext(
                "classpath:spring5/exercise/demo08/spring.xml");
        Jedis client1 = (Jedis)app.getBean("redis");
        System.out.println(client1);
        System.out.println(client1.set("laosiji", "pa pa pa !"));
        System.out.println(client1.get("laosiji"));
    }

執(zhí)行結(jié)果:

16:41:23.265 [main] DEBUG org.springframework.context.support.ClassPathXmlApplicationContext - Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@75bd9247
16:41:23.475 [main] DEBUG org.springframework.beans.factory.xml.XmlBeanDefinitionReader - Loaded 1 bean definitions from class path resource [spring5/exercise/demo08/spring.xml]
16:41:23.506 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'redis'
redis.clients.jedis.Jedis@57175e74
OK
pa pa pa !
2.2 DEBUG - 揭秘代碼中的 namespace URI

直接看 BeanDefinitionParserDelegate.java 中的 parseCustomElement(Element ele, @Nullable BeanDefinition containingBd) 方法。打斷點(diǎn)DEBUG,可以看到程序走到這獲取到的namespace URI 就是之前提到的 xmlns = " http://www.jd.com/schema/mytags " 這個(gè)配置的值。

DEBUG - namespace URI
2.3 exercise-function - resolve(namespaceUri)

點(diǎn)擊代碼中resolve(namespaceUri),可以看到有如下接口。

public interface NamespaceHandlerResolver {
    @Nullable
    NamespaceHandler resolve(String namespaceUri);
}

進(jìn)入實(shí)現(xiàn)類的resolve方法,如下代碼所示,其中最重要的是getHandlerMappings()這個(gè)方法,它就是掃描各個(gè)jar的META-INF目錄下的spring.handlers文件,將內(nèi)容構(gòu)建成映射表。后續(xù)的處理就比較簡(jiǎn)單了,通過(guò)類路徑,基于反射創(chuàng)建NamespaceHandler接口實(shí)現(xiàn)類的對(duì)象,調(diào)用init方法初始化,最后把該namespaceURI的值(類路徑)替換成 實(shí)現(xiàn)類對(duì)象,便于下一次直接獲取返回。

public NamespaceHandler resolve(String namespaceUri) {
        /**
         * 加載"META-INF/spring.handlers"文件,建立URI和處理類的映射關(guān)系。
         *
         * 方法:getHandlerMappings
         * 重要程度:* * * * *
         */
        Map<String, Object> handlerMappings = getHandlerMappings();

        // 根據(jù)URI就可以找到唯一的處理類(字符串)
        Object handlerOrClassName = handlerMappings.get(namespaceUri);
        // ... ... ... ... 
        else {
            // 處理類(字符串)反射
            String className = (String) handlerOrClassName;
            try {
                /**
                 * 反射這個(gè)類
                 */
                Class<?> handlerClass = ClassUtils.forName(className, this.classLoader);
                // ... ... ... ...
                /**
                 * 基于類對(duì)象來(lái)實(shí)例化
                 * 備注:所有處理類必須繼承NamespaceHandler,實(shí)現(xiàn)多態(tài)。
                 * 例如:
                 *   SimpleConstructorNamespaceHandler implements NamespaceHandler
                 *   所有spring.handlers這些命名解析類都有一個(gè)特點(diǎn)是必須實(shí)現(xiàn)NamespaceHandler接口,來(lái)實(shí)現(xiàn)多態(tài)
                 */
                NamespaceHandler namespaceHandler = (NamespaceHandler) BeanUtils.instantiateClass(handlerClass);

                // 調(diào)用處理類初始化方法
                namespaceHandler.init();

                // 替換映射關(guān)系key對(duì)應(yīng)的值
                handlerMappings.put(namespaceUri, namespaceHandler);
                return namespaceHandler;
            }
            // ... ... ... ...
        }
    }
2.4 exercise-function - getHandlerMappings()

getHandlerMappings()方法主要就是加載"META-INF/spring.handlers"文件過(guò)程(所有jar包中的spring.handlers),構(gòu)建映射。

private Map<String, Object> getHandlerMappings() {
        Map<String, Object> handlerMappings = this.handlerMappings;
        if (handlerMappings == null) {
            synchronized (this) {
                handlerMappings = this.handlerMappings;
                if (handlerMappings == null) {
                    try {
                        /**
                         * 加載"META-INF/spring.handlers"文件過(guò)程(所有jar包中的spring.handlers)
                         */
                        Properties mappings =PropertiesLoaderUtils.loadAllProperties(this.handlerMappingsLocation, this.classLoader);
                        // ... ... ... ...
                        /**
                         * 和處理類建立映射關(guān)系
                         */
                        handlerMappings = new ConcurrentHashMap<>(mappings.size());
                        CollectionUtils.mergePropertiesIntoMap(mappings, handlerMappings);
                        this.handlerMappings = handlerMappings;
                    }
                    // ... ... ... ...
                }
            }
        }
        return handlerMappings;
    }

如下圖所示,映射結(jié)果中有我們配置的 http://www.jd.com/schema/mytags ,這個(gè)自定義的namespaceURI,值目前還是配置的類路徑,還沒(méi)有進(jìn)行反射實(shí)例化和初始化后替換值。

namespace URI 映射結(jié)果

2.5 exercise-function - 自定義標(biāo)簽解析類init()

初始化方法中注冊(cè)RedisBeanDifinitionParser類對(duì)象。
TagsNamespaceHandler.java

public class TagsNamespaceHandler extends NamespaceHandlerSupport {
    
    public void init() {
        this.registerBeanDefinitionParser("exercise-function",
                new RedisBeanDifinitionParser());
    }

    @Override
    public BeanDefinitionHolder decorate(Node node, BeanDefinitionHolder definition, ParserContext parserContext) {
        return super.decorate(node, definition, parserContext);
    }
}

RedisBeanDifinitionParser類的doParse方法是創(chuàng)建了一個(gè)JedisShardInfo參數(shù),并設(shè)置到BeanDefinitionBuilder的ConstructorArgValue屬性中,之前文章中提到過(guò),ConstructorArgValue參數(shù)就是構(gòu)造器的參數(shù),在Bean實(shí)例化的時(shí)候會(huì)根據(jù)這個(gè)參數(shù)去對(duì)應(yīng)選擇構(gòu)造器實(shí)例化。
getBeanClass指定的是類的對(duì)象,自定義標(biāo)簽中通過(guò)getBeanClass來(lái)指定beanDefinition的class,在Bean的實(shí)例化過(guò)程中會(huì)先取BeanClass屬性,沒(méi)有的話才會(huì)根據(jù)className屬性的類路徑去ClassUtils.forName獲得BeanClass。
RedisBeanDifinitionParser.java

public class RedisBeanDifinitionParser extends
        AbstractSingleBeanDefinitionParser {
    
    protected Class<?> getBeanClass(Element element) {
        return Jedis.class;
    }
    
    protected void doParse(Element element, BeanDefinitionBuilder builder) {
        String ip = element.getAttribute("ip");
        String port = element.getAttribute("port");
        String password = element.getAttribute("password");

        JedisShardInfo jedisShardInfo = new JedisShardInfo(ip,Integer.parseInt(port));
        jedisShardInfo.setPassword(password);

        builder.addConstructorArgValue(jedisShardInfo);
    }
}
2.6 exercise-function - NamespaceHandlerSupport parse方法

TagsNamespaceHandler 繼承了 NamespaceHandlerSupport 類,TagsNamespaceHandler上述沒(méi)有實(shí)現(xiàn)parse方法,所以調(diào)用的是 NamespaceHandlerSupport 類的 parse方法。
在該parse方法中就是找之前init()方法中注冊(cè)的解析起,下述代碼中的localname就是"exercise-function"字符串。

    public BeanDefinition parse(Element element, ParserContext parserContext) {
        /**
         * 獲取這個(gè)自定義標(biāo)簽元素的解析器
         */
        BeanDefinitionParser parser = findParserForElement(element, parserContext);
        return (parser != null ? parser.parse(element, parserContext) : null);
    }

    private BeanDefinitionParser findParserForElement(Element element, ParserContext parserContext) {
        String localName = parserContext.getDelegate().getLocalName(element);
        /**
         * 獲取解析的parser,parsers這個(gè)map是在獲取Handler時(shí)調(diào)用init的時(shí)候把parser注冊(cè)進(jìn)Map的。
         */
        BeanDefinitionParser parser = this.parsers.get(localName);
        return parser;
    }

BeanDefinitionParser調(diào)用parse方法,首先是執(zhí)行基類AbstractBeanDefinitionParser的parse方法,在第一行就執(zhí)行子類 AbstractSingleBeanDefinitionParser 的 parseInternal(element, parserContext) 方法。
parseInternal(element, parserContext) 方法的最后一行會(huì)執(zhí)行doParse(element, parserContext, builder), 因?yàn)槲覀兊慕馕銎饘?shí)現(xiàn)了doParse方法所以執(zhí)行的是自定義解析器的doParse。自定義解析器重寫了getBeanClass(Element element)方法,也會(huì)在BeanDefinitionBuilder中插入beanClass屬性。
在BeanDefinitionBuilder中插入了一些自定義的構(gòu)造函數(shù)的參數(shù),最后使用BeanDefinitionBuilder創(chuàng)建BeanDefinition時(shí),就包含我們?cè)O(shè)置的構(gòu)造參數(shù)了。

2.6 exercise-function - BeanDefinition注冊(cè)DEBUG結(jié)果圖
注冊(cè)后的結(jié)果圖.png

BeanDefinition注冊(cè)成功后,就意味著我們隨時(shí)可以在工廠中創(chuàng)建并獲取我們自定義標(biāo)簽配置的實(shí)例。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容