主頁 > 後端開發 > 死磕Spring之IoC篇 - 決議自定義標簽(XML 檔案)

死磕Spring之IoC篇 - 決議自定義標簽(XML 檔案)

2021-02-26 06:16:28 後端開發

該系列文章是本人在學習 Spring 的程序中總結下來的,里面涉及到相關原始碼,可能對讀者不太友好,請結合我的原始碼注釋 Spring 原始碼分析 GitHub 地址 進行閱讀

Spring 版本:5.1.14.RELEASE

開始閱讀這一系列文章之前,建議先查看《深入了解 Spring IoC(面試題)》這一篇文章

該系列其他文章請查看:《死磕 Spring 之 IoC 篇 - 文章導讀》

決議自定義標簽(XML 檔案)

上一篇《BeanDefinition 的決議階段(XML 檔案)》文章分析了 Spring 處理 org.w3c.dom.Document 物件(XML Document)的程序,會決議里面的元素,默認命名空間(為慷訓者 http://www.springframework.org/schema/beans)的元素,例如 <bean /> 標簽會被決議成 GenericBeanDefinition 物件并注冊,本文會分析 Spring 是如何處理非默認命名空間的元素,通過 Spring 的實作方式我們如何自定義元素

先來了解一下 XML 檔案中的命名空間:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       https://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context
       https://www.springframework.org/schema/context/spring-context.xsd">
	<context:component-scan base-package="org.geekbang.thinking.in.spring.ioc.overview" />

    <bean id="user" >
        <property name="id" value="https://www.cnblogs.com/lifullmoon/p/1"/>
        <property name="name" value="https://www.cnblogs.com/lifullmoon/p/小馬哥"/>
    </bean>
</beans>

上述 XML 檔案 <beans /> 的默認命名空間為 http://www.springframework.org/schema/beans,內部的 <bean /> 標簽沒有定義命名空間,則使用默認命名空間

<beans /> 還定義了 context 命名空間為 http://www.springframework.org/schema/context,那么內部的 <context:component-scan /> 標簽就不是默認命名空間,處理方式也不同,其實 Spring 內部自定義了很多的命名空間,用于處理不同的場景,原理都一樣,接下來會進行分析,

自定義標簽的實作步驟

擴展 Spring XML 元素的步驟如下:

  1. 撰寫 XML Schema 檔案(XSD 檔案):定義 XML 結構

  2. 自定義 NamespaceHandler 實作:定義命名空間的處理器,實作 NamespaceHandler 介面,我們通常繼承 NamespaceHandlerSupport 抽象類,Spring 提供了通用實作,只需要實作其 init() 方法即可

  3. 自定義 BeanDefinitionParser 實作:系結命名空間下不同的 XML 元素與其對應的決議器,因為一個命名空間下可以有很多個標簽,對于不同的標簽需要不同的 BeanDefinitionParser 決議器,在上面的 init() 方法中進行系結

  4. 注冊 XML 擴展(META-INF/spring.handlers 檔案):命名空間與命名空間處理器的映射

  5. 撰寫 Spring Schema 資源映射檔案(META-INF/spring.schemas 檔案):XML Schema 檔案通常定義為網路的形式,在無網的情況下無法訪問,所以一般在本地的也有一個 XSD 檔案,可通過撰寫 spring.schemas 檔案,將網路形式的 XSD 檔案與本地的 XSD 檔案進行映射,這樣會優先從本地獲取對應的 XSD 檔案

Spring 內部自定義標簽預覽

spring-context 模塊的 ClassPath 下可以看到有 META-INF/spring.handlersMETA-INF/spring.schemas 以及對應的 XSD 檔案,如下:

  • META-INF/spring.handlers

    http\://www.springframework.org/schema/context=org.springframework.context.config.ContextNamespaceHandler
    http\://www.springframework.org/schema/jee=org.springframework.ejb.config.JeeNamespaceHandler
    http\://www.springframework.org/schema/lang=org.springframework.scripting.config.LangNamespaceHandler
    http\://www.springframework.org/schema/task=org.springframework.scheduling.config.TaskNamespaceHandler
    http\://www.springframework.org/schema/cache=org.springframework.cache.config.CacheNamespaceHandler
    
  • META-INF/spring.schemas

    http\://www.springframework.org/schema/context/spring-context.xsd=org/springframework/context/config/spring-context.xsd
    http\://www.springframework.org/schema/jee/spring-jee.xsd=org/springframework/ejb/config/spring-jee.xsd
    http\://www.springframework.org/schema/lang/spring-lang.xsd=org/springframework/scripting/config/spring-lang.xsd
    http\://www.springframework.org/schema/task/spring-task.xsd=org/springframework/scheduling/config/spring-task.xsd
    http\://www.springframework.org/schema/cache/spring-cache.xsd=org/springframework/cache/config/spring-cache.xsd
    https\://www.springframework.org/schema/context/spring-context.xsd=org/springframework/context/config/spring-context.xsd
    https\://www.springframework.org/schema/jee/spring-jee.xsd=org/springframework/ejb/config/spring-jee.xsd
    https\://www.springframework.org/schema/lang/spring-lang.xsd=org/springframework/scripting/config/spring-lang.xsd
    https\://www.springframework.org/schema/task/spring-task.xsd=org/springframework/scheduling/config/spring-task.xsd
    https\://www.springframework.org/schema/cache/spring-cache.xsd=org/springframework/cache/config/spring-cache.xsd
    ### ... 省略
    

其他模塊也有這兩種檔案,這里不一一展示,從上面的 spring.handlers 這里可以看到 context 命名空間對應的是 ContextNamespaceHandler 處理器,先來看一下:

public class ContextNamespaceHandler extends NamespaceHandlerSupport {
	@Override
	public void init() {
		registerBeanDefinitionParser("property-placeholder", new PropertyPlaceholderBeanDefinitionParser());
		registerBeanDefinitionParser("property-override", new PropertyOverrideBeanDefinitionParser());
		registerBeanDefinitionParser("annotation-config", new AnnotationConfigBeanDefinitionParser());
		registerBeanDefinitionParser("component-scan", new ComponentScanBeanDefinitionParser());
		registerBeanDefinitionParser("load-time-weaver", new LoadTimeWeaverBeanDefinitionParser());
		registerBeanDefinitionParser("spring-configured", new SpringConfiguredBeanDefinitionParser());
		registerBeanDefinitionParser("mbean-export", new MBeanExportBeanDefinitionParser());
		registerBeanDefinitionParser("mbean-server", new MBeanServerBeanDefinitionParser());
	}
}

可以看到注冊了不同的標簽所對應的決議器,其中 component-scan 對應 ComponentScanBeanDefinitionParser 決議器,這里先看一下,后面再具體分析

Spring 如何處理非默認命名空間的元素

回顧到 《BeanDefinition 的加載階段(XML 檔案)》 文章中的 XmlBeanDefinitionReader#registerBeanDefinitions 方法,決議 Document 前會先創建 XmlReaderContext 物件(讀取 Resource 資源的背景關系物件),創建方法如下:

// XmlBeanDefinitionReader.java

public XmlReaderContext createReaderContext(Resource resource) {
    return new XmlReaderContext(resource, this.problemReporter, this.eventListener,
            this.sourceExtractor, this, getNamespaceHandlerResolver());
}

public NamespaceHandlerResolver getNamespaceHandlerResolver() {
    if (this.namespaceHandlerResolver == null) {
        this.namespaceHandlerResolver = createDefaultNamespaceHandlerResolver();
    }
    return this.namespaceHandlerResolver;
}

protected NamespaceHandlerResolver createDefaultNamespaceHandlerResolver() {
    ClassLoader cl = (getResourceLoader() != null ? getResourceLoader().getClassLoader() : getBeanClassLoader());
    return new DefaultNamespaceHandlerResolver(cl);
}

在 XmlReaderContext 物件中會有一個 DefaultNamespaceHandlerResolver 物件

回顧到 《BeanDefinition 的決議階段(XML 檔案)》 文章中的 DefaultBeanDefinitionDocumentReader#parseBeanDefinitions 方法,如果不是默認的命名空間,則執行自定義決議,呼叫 BeanDefinitionParserDelegate#parseCustomElement(Element ele) 方法,方法如下

// BeanDefinitionParserDelegate.java

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

@Nullable
public BeanDefinition parseCustomElement(Element ele, @Nullable BeanDefinition containingBd) {
    // <1> 獲取 `namespaceUri`
    String namespaceUri = getNamespaceURI(ele);
    if (namespaceUri == null) {
        return null;
    }
    // <2> 通過 DefaultNamespaceHandlerResolver 根據 `namespaceUri` 獲取相應的 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> 根據 NamespaceHandler 命名空間處理器處理該標簽
    return handler.parse(ele, new ParserContext(this.readerContext, this, containingBd));
}

程序如下:

  1. 獲取該節點對應的 namespaceUri 命名空間
  2. 通過 DefaultNamespaceHandlerResolver 根據 namespaceUri 獲取相應的 NamespaceHandler 處理器
  3. 根據 NamespaceHandler 命名空間處理器處理該標簽

關鍵就在與 DefaultNamespaceHandlerResolver 是如何找到該命名空間對應的 NamespaceHandler 處理器,我們只是在 spring.handlers 檔案中進行關聯,它是怎么找到的呢,我們進入 DefaultNamespaceHandlerResolver 看看

DefaultNamespaceHandlerResolver

org.springframework.beans.factory.xml.DefaultNamespaceHandlerResolver,命名空間的默認處理器

建構式

public class DefaultNamespaceHandlerResolver implements NamespaceHandlerResolver {

	/**
	 * The location to look for the mapping files. Can be present in multiple JAR files.
	 */
	public static final String DEFAULT_HANDLER_MAPPINGS_LOCATION = "META-INF/spring.handlers";

	/** Logger available to subclasses. */
	protected final Log logger = LogFactory.getLog(getClass());

	/** ClassLoader to use for NamespaceHandler classes. */
	@Nullable
	private final ClassLoader classLoader;

	/** Resource location to search for. */
	private final String handlerMappingsLocation;

	/** Stores the mappings from namespace URI to NamespaceHandler class name / instance. */
	@Nullable
	private volatile Map<String, Object> handlerMappings;

	public DefaultNamespaceHandlerResolver() {
		this(null, DEFAULT_HANDLER_MAPPINGS_LOCATION);
	}

	public DefaultNamespaceHandlerResolver(@Nullable ClassLoader classLoader) {
		this(classLoader, DEFAULT_HANDLER_MAPPINGS_LOCATION);
	}

	public DefaultNamespaceHandlerResolver(@Nullable ClassLoader classLoader, String handlerMappingsLocation) {
		Assert.notNull(handlerMappingsLocation, "Handler mappings location must not be null");
		this.classLoader = (classLoader != null ? classLoader : ClassUtils.getDefaultClassLoader());
		this.handlerMappingsLocation = handlerMappingsLocation;
	}
}

注意有一個 DEFAULT_HANDLER_MAPPINGS_LOCATION 屬性為 META-INF/spring.handlers,我們定義的 spring.handlers 在這里出現了,說明命名空間和對應的處理器在這里大概率會有體現

還有一個 handlerMappingsLocation 屬性默認為 META-INF/spring.handlers

resolve 方法

resolve(String namespaceUri) 方法,根據命名空間找到對應的 NamespaceHandler 處理器,方法如下:

@Override
@Nullable
public NamespaceHandler resolve(String namespaceUri) {
    // <1> 獲取所有已經配置的命名空間與 NamespaceHandler 處理器的映射
    Map<String, Object> handlerMappings = getHandlerMappings();
    // <2> 根據 `namespaceUri` 命名空間獲取 NamespaceHandler 處理器
    Object handlerOrClassName = handlerMappings.get(namespaceUri);
    // <3> 接下來對 NamespaceHandler 進行初始化,因為定義在 `spring.handler` 檔案中,可能還沒有轉換成 Class 類物件
    // <3.1> 不存在
    if (handlerOrClassName == null) {
        return null;
    }
    // <3.2> 已經初始化
    else if (handlerOrClassName instanceof NamespaceHandler) {
        return (NamespaceHandler) handlerOrClassName;
    }
    // <3.3> 需要進行初始化
    else {
        String className = (String) handlerOrClassName;
        try {
            // 獲得類,并創建 NamespaceHandler 物件
            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);
            // 初始化 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);
        }
    }
}

程序如下:

  1. 獲取所有已經配置的命名空間與 NamespaceHandler 處理器的映射,呼叫 getHandlerMappings() 方法
  2. 根據 namespaceUri 命名空間獲取 NamespaceHandler 處理器
  3. 接下來對 NamespaceHandler 進行初始化,因為定義在 spring.handler 檔案中,可能還沒有轉換成 Class 類物件
    1. 不存在則回傳空物件
    2. 否則,已經初始化則直接回傳
    3. 否則,根據 className 創建一個 Class 物件,然后進行實體化,還呼叫其 init() 方法

該方法可以找到命名空間對應的 NamespaceHandler 處理器,關鍵在于第 1 步如何將 spring.handlers 檔案中的內容回傳的

getHandlerMappings 方法

getHandlerMappings() 方法,從所有的 META-INF/spring.handlers 檔案中獲取命名空間與處理器之間的映射,方法如下:

private Map<String, Object> getHandlerMappings() {
    // 雙重檢查鎖,延遲加載
    Map<String, Object> handlerMappings = this.handlerMappings;
    if (handlerMappings == null) {
        synchronized (this) {
            handlerMappings = this.handlerMappings;
            if (handlerMappings == null) {
                if (logger.isTraceEnabled()) {
                    logger.trace("Loading NamespaceHandler mappings from [" + this.handlerMappingsLocation + "]");
                }
                try {
                    // 讀取 `handlerMappingsLocation`,也就是當前 JVM 環境下所有的 `META-INF/spring.handlers` 檔案的內容都會讀取到
                    Properties mappings =
                            PropertiesLoaderUtils.loadAllProperties(this.handlerMappingsLocation, this.classLoader);
                    if (logger.isTraceEnabled()) {
                        logger.trace("Loaded NamespaceHandler mappings: " + mappings);
                    }
                    // 初始化到 `handlerMappings` 中
                    handlerMappings = new ConcurrentHashMap<>(mappings.size());
                    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;
}

邏輯不復雜,會讀取當前 JVM 環境下所有的 META-INF/spring.handlers 檔案,將里面的內容以 key-value 的形式保存在 Map 中回傳

到這里,對于 Spring XML 檔案中的自定義標簽的處理邏輯你是不是清晰了,接下來我們來看看 <context:component-scan /> 標簽的具體實作

ContextNamespaceHandler

org.springframework.context.config.ContextNamespaceHandler,繼承 NamespaceHandlerSupport 抽象類,context 命名空間(http://www.springframework.org/schema/context)的處理器,代碼如下:

public class ContextNamespaceHandler extends NamespaceHandlerSupport {

	@Override
	public void init() {
		registerBeanDefinitionParser("property-placeholder", new PropertyPlaceholderBeanDefinitionParser());
		registerBeanDefinitionParser("property-override", new PropertyOverrideBeanDefinitionParser());
		registerBeanDefinitionParser("annotation-config", new AnnotationConfigBeanDefinitionParser());
		registerBeanDefinitionParser("component-scan", new ComponentScanBeanDefinitionParser());
		registerBeanDefinitionParser("load-time-weaver", new LoadTimeWeaverBeanDefinitionParser());
		registerBeanDefinitionParser("spring-configured", new SpringConfiguredBeanDefinitionParser());
		registerBeanDefinitionParser("mbean-export", new MBeanExportBeanDefinitionParser());
		registerBeanDefinitionParser("mbean-server", new MBeanServerBeanDefinitionParser());
	}
}

init() 方法在 DefaultNamespaceHandlerResolver#resolve 方法中可以看到,初始化該物件的時候會被呼叫,注冊該命名空間下各種標簽的決議器

registerBeanDefinitionParser 方法

registerBeanDefinitionParser(String elementName, BeanDefinitionParser parser),注冊標簽的決議器,方法如下:

// NamespaceHandlerSupport.java

private final Map<String, BeanDefinitionParser> parsers = new HashMap<>();

protected final void registerBeanDefinitionParser(String elementName, BeanDefinitionParser parser) {
    this.parsers.put(elementName, parser);
}

將標簽名稱和對應的決議器保存在 Map 中

parse 方法

parse(Element element, ParserContext parserContext) 方法,決議標簽節點,方法如下:

@Override
@Nullable
public BeanDefinition parse(Element element, ParserContext parserContext) {
    // <1> 獲得元素對應的 BeanDefinitionParser 物件
    BeanDefinitionParser parser = findParserForElement(element, parserContext);
    // <2> 執行決議
    return (parser != null ? parser.parse(element, parserContext) : null);
}

@Nullable
private BeanDefinitionParser findParserForElement(Element element, ParserContext parserContext) {
    // 獲得元素名
    String localName = parserContext.getDelegate().getLocalName(element);
    // 獲得 BeanDefinitionParser 物件
    BeanDefinitionParser parser = this.parsers.get(localName);
    if (parser == null) {
        parserContext.getReaderContext().fatal(
                "Cannot locate BeanDefinitionParser for element [" + localName + "]", element);
    }
    return parser;
}

邏輯很簡單,從 Map<String, BeanDefinitionParser> parsers 找到標簽物件的 BeanDefinitionParser 決議器,然后進行決議

ComponentScanBeanDefinitionParser

org.springframework.context.annotation.ComponentScanBeanDefinitionParser,實作了 BeanDefinitionParser 介面,<context:component-scan /> 標簽的決議器

parse 方法

parse(Element element, ParserContext parserContext) 方法,<context:component-scan /> 標簽的決議程序,方法如下:

@Override
@Nullable
public BeanDefinition parse(Element element, ParserContext parserContext) {
    // <1> 獲取 `base-package` 屬性
    String basePackage = element.getAttribute(BASE_PACKAGE_ATTRIBUTE);
    // 處理占位符
    basePackage = parserContext.getReaderContext().getEnvironment().resolvePlaceholders(basePackage);
    // 根據分隔符進行分割
    String[] basePackages = StringUtils.tokenizeToStringArray(basePackage,
            ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);

    // Actually scan for bean definitions and register them.
    // <2> 創建 ClassPathBeanDefinitionScanner 掃描器,用于掃描指定路徑下符合條件的 BeanDefinition 們
    ClassPathBeanDefinitionScanner scanner = configureScanner(parserContext, element);
    // <3> 通過掃描器掃描 `basePackages` 指定包路徑下的 BeanDefinition(帶有 @Component 注解或其派生注解的 Class 類),并注冊
    Set<BeanDefinitionHolder> beanDefinitions = scanner.doScan(basePackages);
    // <4> 將已注冊的 `beanDefinitions` 在當前 XMLReaderContext 背景關系標記為已注冊,避免重復注冊
    registerComponents(parserContext.getReaderContext(), beanDefinitions, element);

    return null;
}

程序如下:

  1. 獲取 base-package 屬性,處理占位符,根據分隔符進行分割
  2. 創建 ClassPathBeanDefinitionScanner 掃描器,用于掃描指定路徑下符合條件的 BeanDefinition 們,呼叫 configureScanner(ParserContext parserContext, Element element) 方法
  3. 通過掃描器掃描 basePackages 指定包路徑下的 BeanDefinition(帶有 @Component 注解或其派生注解的 Class 類),并注冊
  4. 將已注冊的 beanDefinitions 在當前 XMLReaderContext 背景關系標記為已注冊,避免重復注冊

上面的第 3 步的決議程序和本文的主題有點不符,程序也比較復雜,下一篇文章再進行分析

configureScanner 方法

configureScanner(ParserContext parserContext, Element element) 方法,創建 ClassPathBeanDefinitionScanner 掃描器,方法如下:

protected ClassPathBeanDefinitionScanner configureScanner(ParserContext parserContext, Element element) {
    // <1> 默認使用過濾器(過濾出 @Component 注解或其派生注解的 Class 類)
    boolean useDefaultFilters = true;
    if (element.hasAttribute(USE_DEFAULT_FILTERS_ATTRIBUTE)) {
        useDefaultFilters = Boolean.valueOf(element.getAttribute(USE_DEFAULT_FILTERS_ATTRIBUTE));
    }

    // Delegate bean definition registration to scanner class.
    // <2> 創建 ClassPathBeanDefinitionScanner 掃描器 `scanner`,用于掃描指定路徑下符合條件的 BeanDefinition 們
    ClassPathBeanDefinitionScanner scanner = createScanner(parserContext.getReaderContext(), useDefaultFilters);
    // <3> 設定生成的 BeanDefinition 物件的相關默認屬性
    scanner.setBeanDefinitionDefaults(parserContext.getDelegate().getBeanDefinitionDefaults());
    scanner.setAutowireCandidatePatterns(parserContext.getDelegate().getAutowireCandidatePatterns());

    // <4> 根據標簽的屬性進行相關配置

    // <4.1> `resource-pattern` 屬性的處理,設定資源檔案運算式,默認為 `**/*.class`,即 `classpath*:包路徑/**/*.class`
    if (element.hasAttribute(RESOURCE_PATTERN_ATTRIBUTE)) {
        scanner.setResourcePattern(element.getAttribute(RESOURCE_PATTERN_ATTRIBUTE));
    }

    try {
        // <4.2> `name-generator` 屬性的處理,設定 Bean 的名稱生成器,默認為 AnnotationBeanNameGenerator
        parseBeanNameGenerator(element, scanner);
    }
    catch (Exception ex) {
        parserContext.getReaderContext().error(ex.getMessage(), parserContext.extractSource(element), ex.getCause());
    }

    try {
        // <4.3> `scope-resolver`、`scoped-proxy` 屬性的處理,設定 Scope 的模式和元資訊處理器
        parseScope(element, scanner);
    }
    catch (Exception ex) {
        parserContext.getReaderContext().error(ex.getMessage(), parserContext.extractSource(element), ex.getCause());
    }

    // <4.4> `exclude-filter`、`include-filter` 屬性的處理,設定 `.class` 檔案的過濾器
    parseTypeFilters(element, scanner, parserContext);

    // <5> 回傳 `scanner` 掃描器
    return scanner;
}

程序如下:

  1. 默認使用過濾器(過濾出 @Component 注解或其派生注解的 Class 類)
  2. 創建 ClassPathBeanDefinitionScanner 掃描器 scanner,用于掃描指定路徑下符合條件的 BeanDefinition 們
  3. 設定生成的 BeanDefinition 物件的相關默認屬性
  4. 根據標簽的屬性進行相關配置
    1. resource-pattern 屬性的處理,設定資源檔案運算式,默認為 **/*.class,即 classpath*:包路徑/**/*.class
    2. name-generator 屬性的處理,設定 Bean 的名稱生成器,默認為 AnnotationBeanNameGenerator
    3. scope-resolverscoped-proxy 屬性的處理,設定 Scope 的模式和元資訊處理器
    4. exclude-filterinclude-filter 屬性的處理,設定 .class 檔案的過濾器
  5. 回傳 scanner 掃描器

至此,對于 <context:component-scan /> 標簽的決議程序已經分析完

spring.schemas 的原理

META-INF/spring.handlers 檔案的原理在 DefaultNamespaceHandlerResolver 中已經分析過,那么 Sping 是如何處理 META-INF/spring.schemas 檔案的?

先回到 《BeanDefinition 的加載階段(XML 檔案)》 中的 XmlBeanDefinitionReader#doLoadDocument 方法,如下:

protected Document doLoadDocument(InputSource inputSource, Resource resource) throws Exception {
    // <3> 通過 DefaultDocumentLoader 根據 Resource 獲取一個 Document 物件
    return this.documentLoader.loadDocument(inputSource,
            getEntityResolver(), // <1> 獲取 `org.xml.sax.EntityResolver` 物體決議器,ResourceEntityResolver
            this.errorHandler,
            getValidationModeForResource(resource), isNamespaceAware()); // <2> 獲取 XML 檔案驗證模式,保證 XML 檔案的正確性
}

protected EntityResolver getEntityResolver() {
    if (this.entityResolver == null) {
        // Determine default EntityResolver to use.
        ResourceLoader resourceLoader = getResourceLoader();
        if (resourceLoader != null) {
            this.entityResolver = new ResourceEntityResolver(resourceLoader);
        }
        else {
            this.entityResolver = new DelegatingEntityResolver(getBeanClassLoader());
        }
    }
    return this.entityResolver;
}

1 步先獲取 org.xml.sax.EntityResolver 物體決議器,默認為 ResourceEntityResolver 資源決議器,根據 publicId 和 systemId 獲取對應的 DTD 或 XSD 檔案,用于對 XML 檔案進行驗證

ResourceEntityResolver

org.springframework.beans.factory.xml.ResourceEntityResolver,XML 資源實體決議器,獲取對應的 DTD 或 XSD 檔案

建構式
public class ResourceEntityResolver extends DelegatingEntityResolver {
    /** 資源加載器 */
	private final ResourceLoader resourceLoader;

	public ResourceEntityResolver(ResourceLoader resourceLoader) {
		super(resourceLoader.getClassLoader());
		this.resourceLoader = resourceLoader;
	}
}

public class DelegatingEntityResolver implements EntityResolver {
	/** Suffix for DTD files. */
	public static final String DTD_SUFFIX = ".dtd";

	/** Suffix for schema definition files. */
	public static final String XSD_SUFFIX = ".xsd";

	private final EntityResolver dtdResolver;

	private final EntityResolver schemaResolver;

	public DelegatingEntityResolver(@Nullable ClassLoader classLoader) {
		this.dtdResolver = new BeansDtdResolver();
		this.schemaResolver = new PluggableSchemaResolver(classLoader);
	}
}

注意 schemaResolver 為 XSD 的決議器,默認為 PluggableSchemaResolver 物件

resolveEntity 方法

resolveEntity(@Nullable String publicId, @Nullable String systemId) 方法,獲取命名空間對應的 DTD 或 XSD 檔案,方法如下:

// DelegatingEntityResolver.java
@Override
@Nullable
public InputSource resolveEntity(@Nullable String publicId, @Nullable String systemId)
        throws SAXException, IOException {
    if (systemId != null) {
        // DTD 模式
        if (systemId.endsWith(DTD_SUFFIX)) {
            return this.dtdResolver.resolveEntity(publicId, systemId);
        }
        // XSD 模式
        else if (systemId.endsWith(XSD_SUFFIX)) {
            return this.schemaResolver.resolveEntity(publicId, systemId);
        }
    }
    // Fall back to the parser's default behavior.
    return null;
}

// ResourceEntityResolver.java
@Override
@Nullable
public InputSource resolveEntity(@Nullable String publicId, @Nullable String systemId)
        throws SAXException, IOException {

    // <1> 呼叫父類的方法,進行決議,獲取本地 XSD 檔案資源
    InputSource source = super.resolveEntity(publicId, systemId);

    // <2> 如果沒有獲取到本地 XSD 檔案資源,則嘗試通直接通過 systemId 獲取(網路形式)
    if (source == null && systemId != null) {
        // <2.1> 將 systemId 決議成一個 URL 地址
        String resourcePath = null;
        try {
            String decodedSystemId = URLDecoder.decode(systemId, "UTF-8");
            String givenUrl = new URL(decodedSystemId).toString();
            // 決議檔案資源的相對路徑(相對于系統根路徑)
            String systemRootUrl = new File("").toURI().toURL().toString();
            // Try relative to resource base if currently in system root.
            if (givenUrl.startsWith(systemRootUrl)) {
                resourcePath = givenUrl.substring(systemRootUrl.length());
            }
        }
        catch (Exception ex) {
            // Typically a MalformedURLException or AccessControlException.
            if (logger.isDebugEnabled()) {
                logger.debug("Could not resolve XML entity [" + systemId + "] against system root URL", ex);
            }
            // No URL (or no resolvable URL) -> try relative to resource base.
            resourcePath = systemId;
        }
        // <2.2> 如果 URL 地址決議成功,則根據該地址獲取對應的 Resource 檔案資源
        if (resourcePath != null) {
            if (logger.isTraceEnabled()) {
                logger.trace("Trying to locate XML entity [" + systemId + "] as resource [" + resourcePath + "]");
            }
            // 獲得 Resource 資源
            Resource resource = this.resourceLoader.getResource(resourcePath);
            // 創建 InputSource 物件
            source = new InputSource(resource.getInputStream());
            // 設定 publicId 和 systemId 屬性
            source.setPublicId(publicId);
            source.setSystemId(systemId);
            if (logger.isDebugEnabled()) {
                logger.debug("Found XML entity [" + systemId + "]: " + resource);
            }
        }
        // <2.3> 否則,再次嘗試直接根據 systemId(如果是 "http" 則會替換成 "https")獲取 XSD 檔案(網路形式)
        else if (systemId.endsWith(DTD_SUFFIX) || systemId.endsWith(XSD_SUFFIX)) {
            // External dtd/xsd lookup via https even for canonical http declaration
            String url = systemId;
            if (url.startsWith("http:")) {
                url = "https:" + url.substring(5);
            }
            try {
                source = new InputSource(new URL(url).openStream());
                source.setPublicId(publicId);
                source.setSystemId(systemId);
            }
            catch (IOException ex) {
                if (logger.isDebugEnabled()) {
                    logger.debug("Could not resolve XML entity [" + systemId + "] through URL [" + url + "]", ex);
                }
                // Fall back to the parser's default behavior.
                source = null;
            }
        }
    }
    return source;
}

程序如下:

  1. 呼叫父類的方法,進行決議,獲取本地 XSD 檔案資源,如果是 XSD 模式,則先通過 PluggableSchemaResolver 決議
  2. 如果沒有獲取到本地 XSD 檔案資源,則嘗試通直接通過 systemId 獲取(網路形式)
    1. 將 systemId 決議成一個 URL 地址
    2. 如果 URL 地址決議成功,則根據該地址獲取對應的 Resource 檔案資源
    3. 否則,再次嘗試直接根據 systemId(如果是 "http" 則會替換成 "https")獲取 XSD 檔案(網路形式)

先嘗試獲取本地的 XSD 檔案,獲取不到再獲取遠程的 XSD 檔案

PluggableSchemaResolver

org.springframework.beans.factory.xml.PluggableSchemaResolver,獲取 XSD 檔案(網路形式)對應的本地的檔案資源

建構式
public class PluggableSchemaResolver implements EntityResolver {

	public static final String DEFAULT_SCHEMA_MAPPINGS_LOCATION = "META-INF/spring.schemas";

	private static final Log logger = LogFactory.getLog(PluggableSchemaResolver.class);

	@Nullable
	private final ClassLoader classLoader;

	/** Schema 檔案地址 */
	private final String schemaMappingsLocation;

	/** Stores the mapping of schema URL -> local schema path. */
	@Nullable
	private volatile Map<String, String> schemaMappings;

	public PluggableSchemaResolver(@Nullable ClassLoader classLoader) {
		this.classLoader = classLoader;
		this.schemaMappingsLocation = DEFAULT_SCHEMA_MAPPINGS_LOCATION;
	}
}

注意這里的 DEFAULT_SCHEMA_MAPPINGS_LOCATIONMETA-INF/spring.schemas,看到這個可以確定實作原理就在這里了

schemaMappingsLocation 屬性默認為 META-INF/spring.schemas

resolveEntity 方法

resolveEntity(@Nullable String publicId, @Nullable String systemId) 方法,獲取命名空間對應的 DTD 或 XSD 檔案(本地),方法如下:

@Override
@Nullable
public InputSource resolveEntity(@Nullable String publicId, @Nullable String systemId) throws IOException {
    if (logger.isTraceEnabled()) {
        logger.trace("Trying to resolve XML entity with public id [" + publicId +
                "] and system id [" + systemId + "]");
    }

    if (systemId != null) {
        // <1> 獲得對應的 XSD 檔案位置,從所有 `META-INF/spring.schemas` 檔案中獲取對應的本地 XSD 檔案位置
        String resourceLocation = getSchemaMappings().get(systemId);
        if (resourceLocation == null && systemId.startsWith("https:")) {
            // Retrieve canonical http schema mapping even for https declaration
            resourceLocation = getSchemaMappings().get("http:" + systemId.substring(6));
        }
        if (resourceLocation != null) { // 本地 XSD 檔案位置
            // <2> 創建 ClassPathResource 物件
            Resource resource = new ClassPathResource(resourceLocation, this.classLoader);
            try {
                // <3> 創建 InputSource 物件,設定 publicId、systemId 屬性,回傳
                InputSource source = new InputSource(resource.getInputStream());
                source.setPublicId(publicId);
                source.setSystemId(systemId);
                if (logger.isTraceEnabled()) {
                    logger.trace("Found XML schema [" + systemId + "] in classpath: " + resourceLocation);
                }
                return source;
            }
            catch (FileNotFoundException ex) {
                if (logger.isDebugEnabled()) {
                    logger.debug("Could not find XML schema [" + systemId + "]: " + resource, ex);
                }
            }
        }
    }

    // Fall back to the parser's default behavior.
    return null;
}

程序如下:

  1. 獲得對應的 XSD 檔案位置 resourceLocation,從所有 META-INF/spring.schemas 檔案中獲取對應的本地 XSD 檔案位置,會先呼叫 getSchemaMappings() 決議出本地所有的 XSD 檔案的位置資訊
  2. 根據 resourceLocation 創建 ClassPathResource 物件
  3. 創建 InputSource 物件,設定 publicId、systemId 屬性,回傳
getSchemaMappings 方法

getSchemaMappings()方法, 決議當前 JVM 環境下所有的 META-INF/spring.handlers 檔案的內容,方法如下:

private Map<String, String> getSchemaMappings() {
    Map<String, String> schemaMappings = this.schemaMappings;
    // 雙重檢查鎖,實作 schemaMappings 單例
    if (schemaMappings == null) {
        synchronized (this) {
            schemaMappings = this.schemaMappings;
            if (schemaMappings == null) {
                if (logger.isTraceEnabled()) {
                    logger.trace("Loading schema mappings from [" + this.schemaMappingsLocation + "]");
                }
                try {
                    // 讀取 `schemaMappingsLocation`,也就是當前 JVM 環境下所有的 `META-INF/spring.handlers` 檔案的內容都會讀取到
                    Properties mappings = PropertiesLoaderUtils.loadAllProperties(this.schemaMappingsLocation, this.classLoader);
                    if (logger.isTraceEnabled()) {
                        logger.trace("Loaded schema mappings: " + mappings);
                    }
                    // 將 mappings 初始化到 schemaMappings 中
                    schemaMappings = new ConcurrentHashMap<>(mappings.size());
                    CollectionUtils.mergePropertiesIntoMap(mappings, schemaMappings);
                    this.schemaMappings = schemaMappings;
                }
                catch (IOException ex) {
                    throw new IllegalStateException(
                            "Unable to load schema mappings from location [" + this.schemaMappingsLocation + "]", ex);
                }
            }
        }
    }
    return schemaMappings;
}

邏輯不復雜,會讀取當前 JVM 環境下所有的 META-INF/spring.schemas 檔案,將里面的內容以 key-value 的形式保存在 Map 中回傳,例如保存如下資訊:

key=http://www.springframework.org/schema/context/spring-context.xsd
value=https://www.cnblogs.com/lifullmoon/p/org/springframework/context/config/spring-context.xsd

這樣一來,會先獲取本地 org/springframework/context/config/spring-context.xsd 檔案,不存在則嘗試獲取 http://www.springframework.org/schema/context/spring-context.xsd 檔案,避免無網情況下無法獲取 XSD 檔案

自定義標簽實作示例

例如我們有一個 User 實體類和一個 City 列舉:

package org.geekbang.thinking.in.spring.ioc.overview.domain;

import org.geekbang.thinking.in.spring.ioc.overview.enums.City;
public class User implements BeanNameAware {
    private Long id;
    private String name;
    private City city;
    // ... 省略 getter、setter 方法
}

package org.geekbang.thinking.in.spring.ioc.overview.enums;
public enum City {
    BEIJING,
    HANGZHOU,
    SHANGHAI
}

撰寫 XML Schema 檔案(XSD 檔案)

org\geekbang\thinking\in\spring\configuration\metadata\users.xsd

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<xsd:schema xmlns="http://time.geekbang.org/schema/users"
            xmlns:xsd="http://www.w3.org/2001/XMLSchema"
            targetNamespace="http://time.geekbang.org/schema/users">

    <xsd:import namespace="http://www.w3.org/XML/1998/namespace"/>

    <!-- 定義 User 型別(復雜型別) -->
    <xsd:complexType name="User">
        <xsd:attribute name="id" type="xsd:long" use="required"/>
        <xsd:attribute name="name" type="xsd:string" use="required"/>
        <xsd:attribute name="city" type="City"/>
    </xsd:complexType>

    <!-- 定義 City 型別(簡單型別,列舉) -->
    <xsd:simpleType name="City">
        <xsd:restriction base="xsd:string">
            <xsd:enumeration value="https://www.cnblogs.com/lifullmoon/p/BEIJING"/>
            <xsd:enumeration value="https://www.cnblogs.com/lifullmoon/p/HANGZHOU"/>
            <xsd:enumeration value="https://www.cnblogs.com/lifullmoon/p/SHANGHAI"/>
        </xsd:restriction>
    </xsd:simpleType>

    <!-- 定義 user 元素 -->
    <xsd:element name="user" type="User"/>
</xsd:schema>

自定義 NamespaceHandler 實作

package org.geekbang.thinking.in.spring.configuration.metadata;

import org.springframework.beans.factory.xml.NamespaceHandler;
import org.springframework.beans.factory.xml.NamespaceHandlerSupport;

public class UsersNamespaceHandler extends NamespaceHandlerSupport {
    @Override
    public void init() {
        // 將 "user" 元素注冊對應的 BeanDefinitionParser 實作
        registerBeanDefinitionParser("user", new UserBeanDefinitionParser());
    }
}

自定義 BeanDefinitionParser 實作

package org.geekbang.thinking.in.spring.configuration.metadata;

import org.geekbang.thinking.in.spring.ioc.overview.domain.User;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser;
import org.springframework.beans.factory.xml.BeanDefinitionParser;
import org.springframework.beans.factory.xml.ParserContext;
import org.springframework.util.StringUtils;
import org.w3c.dom.Element;

public class UserBeanDefinitionParser extends AbstractSingleBeanDefinitionParser {

    @Override
    protected Class<?> getBeanClass(Element element) {
        return User.class;
    }

    @Override
    protected void doParse(Element element, ParserContext parserContext, BeanDefinitionBuilder builder) {
        setPropertyValue("id", element, builder);
        setPropertyValue("name", element, builder);
        setPropertyValue("city", element, builder);
    }

    private void setPropertyValue(String attributeName, Element element, BeanDefinitionBuilder builder) {
        String attributeValue = https://www.cnblogs.com/lifullmoon/p/element.getAttribute(attributeName);
        if (StringUtils.hasText(attributeValue)) {
            builder.addPropertyValue(attributeName, attributeValue); // -> 

注冊 XML 擴展(spring.handlers 檔案)

META-INF/spring.handlers

## 定義 namespace 與 NamespaceHandler 的映射
http\://time.geekbang.org/schema/users=org.geekbang.thinking.in.spring.configuration.metadata.UsersNamespaceHandler

撰寫 Spring Schema 資源映射檔案(spring.schemas 檔案)

META-INF/spring.schemas

http\://time.geekbang.org/schema/users.xsd = org/geekbang/thinking/in/spring/configuration/metadata/users.xsd

使用示例

<?xml version="1.0" encoding="UTF-8"?>
<beans
        xmlns="http://www.springframework.org/schema/beans"
        xmlns:users="http://time.geekbang.org/schema/users"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://time.geekbang.org/schema/users
        http://time.geekbang.org/schema/users.xsd">

    <!-- <bean id="user" >
           <property name="id" value="https://www.cnblogs.com/lifullmoon/p/1"/>
           <property name="name" value="https://www.cnblogs.com/lifullmoon/p/小馬哥"/>
           <property name="city" value="https://www.cnblogs.com/lifullmoon/p/HANGZHOU"/>
       </bean>  -->

    <users:user id="1" name="小馬哥" city="HANGZHOU"/>

</beans>

至此,通過使用 users 命名空間下的 user 標簽也能定義一個 Bean

Mybatis 對 Spring 的集成專案中的 <mybatis:scan /> 標簽就是這樣實作的,可以參考:NamespaceHandler、MapperScannerBeanDefinitionParser、XSD 等檔案

總結

Spring 默認命名空間為 http://www.springframework.org/schema/beans,也就是 <bean /> 標簽,決議程序在上一篇《BeanDefinition 的決議階段(XML 檔案)》文章中已經分析過了,

非默認命名空間的處理方式需要單獨的 NamespaceHandler 命名空間處理器進行處理,這中方式屬于擴展 Spring XML 元素,也可以說是自定義標簽,在 Spring 內部很多地方都使用到這種方式,例如 <context:component-scan /><util:list />、AOP 相關標簽都有對應的 NamespaceHandler 命名空間處理器

對于這種自定義 Spring XML 元素的實作步驟如下:

  1. 撰寫 XML Schema 檔案(XSD 檔案):定義 XML 結構

  2. 自定義 NamespaceHandler 實作:定義命名空間的處理器,實作 NamespaceHandler 介面,我們通常繼承 NamespaceHandlerSupport 抽象類,Spring 提供了通用實作,只需要實作其 init() 方法即可

  3. 自定義 BeanDefinitionParser 實作:系結命名空間下不同的 XML 元素與其對應的決議器,因為一個命名空間下可以有很多個標簽,對于不同的標簽需要不同的 BeanDefinitionParser 決議器,在上面的 init() 方法中進行系結

  4. 注冊 XML 擴展(META-INF/spring.handlers 檔案):命名空間與命名空間處理器的映射

  5. XML Schema 檔案通常定義為網路的形式,在無網的情況下無法訪問,所以一般在本地的也有一個 XSD 檔案,可通過撰寫 META-INF/spring.schemas 檔案,將網路形式的 XSD 檔案與本地的 XSD 檔案進行映射,這樣會優先從本地獲取對應的 XSD 檔案

關于上面的實作步驟的原理本文進行了比較詳細的分析,稍微總結一下:

  1. Spring 會掃描到所有的 META-INF/spring.schemas 檔案內容,每個命名空間對應的 XSD 檔案優先從本地獲取,用于 XML 檔案的校驗
  2. Spring 會掃描到所有的 META-INF/spring.handlers 檔案內容,可以找到命名空間對應的 NamespaceHandler 處理器
  3. 根據找到的 NamespaceHandler 處理器找到標簽對應的 BeanDefinitionParser 決議器
  4. 根據 BeanDefinitionParser 決議器決議該元素,生成對應的 BeanDefinition 并注冊

本文還分析了 <context:component-scan /> 的實作原理,底層會 ClassPathBeanDefinitionScanner 掃描器,用于掃描指定路徑下符合條件的 BeanDefinition 們(帶有 @Component 注解或其派生注解的 Class 類),@ComponentScan 注解底層原理也是基于 ClassPathBeanDefinitionScanner 掃描器實作的,這個掃描器和決議 @Component 注解定義的 Bean 相關,有關于面向注解定義的 Bean 在 Spring 中是如何決議成 BeanDefinition 在后續文章進行分析,

最后用一張圖來結束面向資源(XML)定義 Bean 的 BeanDefinition 的決議程序:

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/263647.html

標籤:Java

上一篇:mybatis(1)

下一篇:HTTP常用請求頭大揭秘

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more