@ComponentScan注解用法之包路徑占位符解析
@ComponentScan注解的basePackages屬性支持占位符嗎?
答案是肯定的。
代碼測(cè)試
首先編寫一個(gè)屬性配置文件(Properties),名字隨意,放在resources目錄下。
在該文件中只需要定義一個(gè)屬性就可以,屬性名隨意,值必須是要掃描的包路徑。
basepackages=com.xxx.fame.placeholder
編寫一個(gè)Bean,空類即可。
package com.xxx.fame.placeholder; import org.springframework.stereotype.Component; @Component public class ComponentBean { }
編寫啟動(dòng)類,在啟動(dòng)類中通過(guò)@PropertySource注解來(lái)將外部配置文件加載到Spring應(yīng)用上下文中,其次在@ComponentScan注解的value屬性值中使用占位符來(lái)指定要掃描的包路徑(占位符中指定的屬性名必須和前面在屬性文件中定義的屬性名一致)。
使用Spring 應(yīng)用上下文來(lái)獲取前面編寫的Bean,執(zhí)行main方法,那么是會(huì)報(bào)錯(cuò)呢?還是正常返回實(shí)例呢?
package com.xxx.fame.placeholder; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.PropertySource; @PropertySource("classpath:componentscan.properties") @ComponentScan("${basepackages}") public class ComponentScanPlaceholderDemo { public static void main(String[] args) { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); context.register(ComponentScanPlaceholderDemo.class); context.refresh(); ComponentBean componentBean = context.getBean(ComponentBean.class); System.out.println(componentBean); } }
運(yùn)行結(jié)果如下,可以看到正常返回ComponentBean在IoC容器中的實(shí)例了。
這里我們是將@PropertySource注解添加到啟動(dòng)類上,那如果將@ProperSource注解添加到ComponentBean類上,程序還可以正常運(yùn)行嗎(將啟動(dòng)類上的@PropertySource注解移除掉)?
@Component @PropertySource("classpath:componentscan.properties") public class ComponentBean {}
啟動(dòng)應(yīng)用程序??梢园l(fā)現(xiàn)程序無(wú)法啟動(dòng),拋出以下異常。在這個(gè)錯(cuò)誤里有一個(gè)關(guān)鍵信息:“Could not resolve placeholder ‘basepackages' in value " b a s e p a c k a g e s " ” , 即 無(wú) 法 解 析 @ C o m p o n e n t S c a n 注 解 中 指 定 的 “ {basepackages}"”,即無(wú)法解析@ComponentScan注解中指定的“ basepackages"”,即無(wú)法解析@ComponentScan注解中指定的“{basepackages}”。
接下來(lái)我們就分析下,為什么在啟動(dòng)類中添加@PropertySource注解,導(dǎo)入屬性資源,然后在@ComponentScan注解中使用占位符就沒(méi)有問(wèn)題,而在非啟動(dòng)類中添加@PropertySource導(dǎo)入外部配置資源,在@ComponentScan注解中使用占位符就會(huì)拋出異常。
上面說(shuō)的啟動(dòng)類其實(shí)也存在問(wèn)題,更精準(zhǔn)的描述應(yīng)該是在調(diào)用Spring 應(yīng)用上下文的refresh方法之前調(diào)用register方法時(shí)傳入的Class。
// ConfigurationClassParser#doProcessConfigurationClass protected final SourceClass doProcessConfigurationClass( ConfigurationClass configClass, SourceClass sourceClass, Predicate<String> filter) throws IOException { // 掃描到的 配置類是否添加了@Component注解,如果外部類添加了@Component注解,再處理內(nèi)部類 if (configClass.getMetadata().isAnnotated(Component.class.getName())) { // Recursively process any member (nested) classes first processMemberClasses(configClass, sourceClass, filter); } // 處理所有的@PropertySource注解,由于@PropertySource被JDK1.8中的元注解@Repeatable所標(biāo)注 // 因此可以在一個(gè)類中添加多個(gè) for (AnnotationAttributes propertySource : AnnotationConfigUtils.attributesForRepeatable( sourceClass.getMetadata(), PropertySources.class, org.springframework.context.annotation.PropertySource.class)) { if (this.environment instanceof ConfigurableEnvironment) { processPropertySource(propertySource); } else { logger.info("Ignoring @PropertySource annotation on [" + sourceClass.getMetadata().getClassName() + "]. Reason: Environment must implement ConfigurableEnvironment"); } } // 處理所有的@ComponentScan注解,@ComponentScan注解也被@Repeatable注解所標(biāo)注 Set<AnnotationAttributes> componentScans = AnnotationConfigUtils.attributesForRepeatable( sourceClass.getMetadata(), ComponentScans.class, ComponentScan.class); if (!componentScans.isEmpty() && !this.conditionEvaluator.shouldSkip(sourceClass.getMetadata(), ConfigurationPhase.REGISTER_BEAN)) { for (AnnotationAttributes componentScan : componentScans) { // The config class is annotated with @ComponentScan -> perform the scan immediately Set<BeanDefinitionHolder> scannedBeanDefinitions = this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName()); // Check the set of scanned definitions for any further config classes and parse recursively if needed for (BeanDefinitionHolder holder : scannedBeanDefinitions) { BeanDefinition bdCand = holder.getBeanDefinition().getOriginatingBeanDefinition(); if (bdCand == null) { bdCand = holder.getBeanDefinition(); } if (ConfigurationClassUtils.checkConfigurationClassCandidate(bdCand, this.metadataReaderFactory)) { parse(bdCand.getBeanClassName(), holder.getBeanName()); } } } } // 刪除其它與本次分析無(wú)關(guān)的代碼.... // No superclass -> processing is complete return null; }
底層行為分析
要想完全搞明白這個(gè)問(wèn)題,就必須清楚Spring 應(yīng)用上下文關(guān)于Bean資源加載這一塊以及外部資源配置方面的邏輯,由于這一塊邏輯比較龐大,單篇文章很難說(shuō)明白,這里就只分析產(chǎn)生以上錯(cuò)誤的原因。
問(wèn)題的根源就出在ConfigurationClassParser的doProcessConfigurationClass方法中,在該方法中,Spring是先處理@PropertySource注解,再處理@ComponentScan或者@ComponentScans注解,而Spring應(yīng)用上下文最先處理的類是誰(shuí)呢?
答案就是我們通過(guò)應(yīng)用上下文的register方法注冊(cè)的類。當(dāng)我們?cè)趓egister方法注冊(cè)的類上添加@PropertySource注解,那么沒(méi)問(wèn)題,先解析@PropertySource注解導(dǎo)入的外部資源,接下來(lái)再解析@ComponentScan注解中的占位符,可以獲取到值。
當(dāng)我們將@PropertySource注解添加到非register方法注冊(cè)的類時(shí),由于是優(yōu)先解析通過(guò)register方法注冊(cè)的類,再去解析@ComponentScan注解,發(fā)現(xiàn)需要處理占位符才能進(jìn)行類路徑下的資源掃描,這時(shí)候就會(huì)使用Environment對(duì)象實(shí)例去解析。結(jié)果發(fā)現(xiàn)沒(méi)有Spring應(yīng)用上下文中并沒(méi)有一個(gè)名為"basepackages"屬性,所以拋出異常。
// ConfigurationClassParser#doProcessConfigurationClass protected final SourceClass doProcessConfigurationClass( ConfigurationClass configClass, SourceClass sourceClass, Predicate<String> filter) throws IOException { // 掃描到的 配置類是否添加了@Component注解,如果外部類添加了@Component注解,再處理內(nèi)部類 if (configClass.getMetadata().isAnnotated(Component.class.getName())) { // Recursively process any member (nested) classes first processMemberClasses(configClass, sourceClass, filter); } // 處理所有的@PropertySource注解,由于@PropertySource被JDK1.8中的元注解@Repeatable所標(biāo)注 // 因此可以在一個(gè)類中添加多個(gè) for (AnnotationAttributes propertySource : AnnotationConfigUtils.attributesForRepeatable( sourceClass.getMetadata(), PropertySources.class, org.springframework.context.annotation.PropertySource.class)) { if (this.environment instanceof ConfigurableEnvironment) { processPropertySource(propertySource); } else { logger.info("Ignoring @PropertySource annotation on [" + sourceClass.getMetadata().getClassName() + "]. Reason: Environment must implement ConfigurableEnvironment"); } } // 處理所有的@ComponentScan注解,@ComponentScan注解也被@Repeatable注解所標(biāo)注 Set<AnnotationAttributes> componentScans = AnnotationConfigUtils.attributesForRepeatable( sourceClass.getMetadata(), ComponentScans.class, ComponentScan.class); if (!componentScans.isEmpty() && !this.conditionEvaluator.shouldSkip(sourceClass.getMetadata(), ConfigurationPhase.REGISTER_BEAN)) { for (AnnotationAttributes componentScan : componentScans) { // The config class is annotated with @ComponentScan -> perform the scan immediately Set<BeanDefinitionHolder> scannedBeanDefinitions = this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName()); // Check the set of scanned definitions for any further config classes and parse recursively if needed for (BeanDefinitionHolder holder : scannedBeanDefinitions) { BeanDefinition bdCand = holder.getBeanDefinition().getOriginatingBeanDefinition(); if (bdCand == null) { bdCand = holder.getBeanDefinition(); } if (ConfigurationClassUtils.checkConfigurationClassCandidate(bdCand, this.metadataReaderFactory)) { parse(bdCand.getBeanClassName(), holder.getBeanName()); } } } } // 刪除其它與本次分析無(wú)關(guān)的代碼.... // No superclass -> processing is complete return null; }
那么除了將@PropertySource注解添加到通過(guò)register方法注冊(cè)類中(注意該方法的參數(shù)是可變參類型,即可以傳遞多個(gè)。如果傳遞多個(gè),需要注意Spring 應(yīng)用上下文解析它們的順序,如果順序不當(dāng)也可能拋出異常),還有其它的辦法嗎?
答案是有的,這里先給出答案,通過(guò)-D參數(shù)來(lái)完成,或者編碼(設(shè)置到系統(tǒng)屬性中),以Idea為例,只需在VM options中添加-Dbasepackages=com.xxx.fame即可。
或者在Spring應(yīng)用上下文啟動(dòng)之前(refresh方法執(zhí)行之前),調(diào)用System.setProperty(String,String)方法手動(dòng)將屬性以及屬性值設(shè)置進(jìn)去。
package com.xxx.fame.placeholder; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.ComponentScan; @ComponentScan("${basepackages}") public class ComponentScanPlaceholderDemo { public static void main(String[] args) { // 手動(dòng)調(diào)用System#setProperty方法,設(shè)置 “basepackages”及其屬性值。 System.setProperty("basepackages","com.xxx.fame"); AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); context.register(ComponentScanPlaceholderDemo.class); context.refresh(); ComponentBean componentBean = context.getBean(ComponentBean.class); System.out.println(componentBean); } }
而這由延伸出來(lái)一個(gè)很有意思的問(wèn)題,手動(dòng)調(diào)用System的setProperty方法來(lái)設(shè)置“basepackages” 屬性,但同時(shí)也通過(guò)@PropertySource注解導(dǎo)入的外部資源中也指定相同的屬性名但不同值,接下來(lái)通過(guò)Environment對(duì)象實(shí)例來(lái)解析占位符“${basepackages}”,那么究竟是哪個(gè)配置生效呢?
@ComponentScan("${basepackages}") @PropertySource("classpath:componentscan.properties") public class ComponentScanPlaceholderDemo { public static void main(String[] args) { System.setProperty("basepackages","com.xxx.fame"); AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); context.register(ComponentScanPlaceholderDemo.class); context.refresh(); String value = context.getEnvironment().resolvePlaceholders("${basepackages}"); System.out.println("${basepackages}占位符的值究竟是 com.xxx.fame 還是 com.unknow 呢? " + value); } }
#在componentscan.properties指定相同的屬性名,但值不同 basepackages=com.unknow
接下來(lái)啟動(dòng)應(yīng)用上下文,執(zhí)行結(jié)果如下:
可以看到是通過(guò)System的setProperty方法設(shè)置的屬性值生效,具體原因這里就不再展開(kāi)分析了,以后會(huì)詳細(xì)分析Spring中外部配置優(yōu)先級(jí)問(wèn)題,很有意思。
書歸正傳,Spring 應(yīng)用上下文是如何解析占位符的呢?想要知道答案,只需要跟著ComponentScanParser的parse方法走即可,因?yàn)樵赟pring應(yīng)用上下中是由該類來(lái)解析@ComponentScan注解并完成指定類路徑下的資源掃描和加載。
其實(shí)前面已經(jīng)給出了答案,就是通過(guò)Environment實(shí)例的resolvePlaceHolders方法,但這里稍有不同的是resolvePlaceholders方法對(duì)于不能解析的占位符不會(huì)拋出異常,只會(huì)原樣返回,那么為什么前面拋出異常呢?答案是使用了另一個(gè)方法-resolveRequiredPalceholders方法。
在ComponentScanAnnotationParser方法中,首先創(chuàng)建了ClassPathBeanDefinitionScanner對(duì)象,顧名思義該類就是用來(lái)處理類路徑下的BeanDefinition資源掃描的(在MyBatis和Spring整合中,MyBatis就通過(guò)繼承該類來(lái)完成@MapperScan注解中指定包路徑下的資源掃描)。
由于@ComponentScan注解中的basepackages方法的返回值是一個(gè)數(shù)組,因此這里使用String類型的數(shù)組來(lái)接受,遍歷該數(shù)組,沒(méi)有每一個(gè)獲取到每一個(gè)包路徑,調(diào)用Environment對(duì)象的resolvePlaceholder方法來(lái)解析可能存在的占位符。由于resolvePlaceholders方法對(duì)于不能解析的占位符不會(huì)拋出異常,因此顯然問(wèn)題不是出自這里(之所以要提出這一部分代碼,是想告訴大家,在@ComponentScan注解中可以使用占位符的,Spring是有處理的)。
在該方法的最后調(diào)用了ClassPathBeanDefinitionScanner的doScan方法,那么我們繼續(xù)往下追查,看哪里調(diào)用了Environment對(duì)象的resolveRequiredPlaceholders方法。
// ComponentScanAnnotationParser#parse public Set<BeanDefinitionHolder> parse(AnnotationAttributes componentScan, final String declaringClass) { ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(this.registry, componentScan.getBoolean("useDefaultFilters"), this.environment, this.resourceLoader); // 刪除與本次分析無(wú)關(guān)的代碼..... Set<String> basePackages = new LinkedHashSet<>(); String[] basePackagesArray = componentScan.getStringArray("basePackages"); for (String pkg : basePackagesArray) { String[] tokenized = StringUtils.tokenizeToStringArray(this.environment.resolvePlaceholders(pkg), ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS); Collections.addAll(basePackages, tokenized); } // 刪除與本次分析無(wú)關(guān)的代碼..... return scanner.doScan(StringUtils.toStringArray(basePackages)); }
在ClassPathBeanDefinitionScanner的doScan方法中,遍歷傳入的basePackages(即用戶在@ComponentScan注解的basepackes方法中指定的資源路徑,也許有小伙伴疑惑為什么我明明沒(méi)有指定,只指定了其value方法,這涉及到Spring注解中的顯式引用,有興趣的小伙伴可以查看Spring 在Github項(xiàng)目上的Wiki),對(duì)于遍歷到的每一個(gè)basepackge,都調(diào)用findCandidateComponents方法來(lái)處理,由于該方法的返回值是集合,泛型是BeanDefinitionHolder,這意味著已經(jīng)根據(jù)資源路徑完成了資源的掃描和加載,所以需要繼續(xù)往下追查。
// ClassPathBeanDefinitionScanner#doScan protected Set<BeanDefinitionHolder> doScan(String... basePackages) { Assert.notEmpty(basePackages, "At least one base package must be specified"); Set<BeanDefinitionHolder> beanDefinitions = new LinkedHashSet<>(); for (String basePackage : basePackages) { Set<BeanDefinition> candidates = findCandidateComponents(basePackage); // ...省略其它代碼 } return beanDefinitions; }
ClassPathBeanDefinitionScanner并沒(méi)有該方法,而是由其父類ClassPathScanningCandidateComponentProvider定義并實(shí)現(xiàn)。在findCandidateComponents方法中,首先判斷是否使用了索引,如果使用了索引則調(diào)用addCandidateComponentsFromIndex方法,否則調(diào)用scanCandidateComponents方法(索引是Spring為了減少應(yīng)用程序的啟動(dòng)時(shí)間,通過(guò)編譯器來(lái)在編譯期就確定那些類是需要IoC容器來(lái)進(jìn)行管理的)。
// ClassPathScanningCandidateComponentProvider#findCandidateComponents public Set<BeanDefinition> findCandidateComponents(String basePackage) { if (this.componentsIndex != null && indexSupportsIncludeFilters()) { return addCandidateComponentsFromIndex(this.componentsIndex, basePackage); } else { return scanCandidateComponents(basePackage); } }
由于并未使用索引,所以執(zhí)行scanCandidateComponents方法。在該方法中,首先創(chuàng)建了一個(gè)Set集合用來(lái)存放BeanDefinition,重點(diǎn)接下來(lái)的資源路徑拼接,首先在資源路徑前面拼接上“classpath*:”,然后調(diào)用resolveBasePackage方法,傳入basePackage,最后拼接上“**/*.class”,看起來(lái)值的懷疑的地方只有這個(gè)resolveBasePackage方法了。
// ClassPathScanningCandidateComponentProvider#DEFAULT_RESOURCE_PATTERN static final String DEFAULT_RESOURCE_PATTERN = "**/*.class"; //ResourcePatternResolver#CLASSPATH_ALL_URL_PREFIX String CLASSPATH_ALL_URL_PREFIX = "classpath*:"; // ClassPathScanningCandidateComponentProvider#scanCandidateComponents private Set<BeanDefinition> scanCandidateComponents(String basePackage) { Set<BeanDefinition> candidates = new LinkedHashSet<>(); try { String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX + resolveBasePackage(basePackage) + '/' + this.resourcePattern; // 刪除與本次分析無(wú)關(guān)的代碼...... } catch (IOException ex) { throw new BeanDefinitionStoreException("I/O failure during classpath scanning", ex); } return candidates; }
果不其然,在resolveBasePackage方法中,調(diào)用Environment實(shí)現(xiàn)類對(duì)象的resolveRequiredPlacehol-ders方法。
// ClassPathScanningCandidateComponentProvider#resolveBasePackage protected String resolveBasePackage(String basePackage) { return ClassUtils.convertClassNameToResourcePath(getEnvironment().resolveRequiredPlaceholders(basePackage)); }
最后我們分析下為什么resolvePlaceholders方法對(duì)于不能處理的占位符只會(huì)原樣返回,而resolveReq-uiredPlaceholders方法對(duì)于不能處理的占位符卻會(huì)拋出異常呢?
Environment實(shí)現(xiàn)類并不具備占位符解析能力,其只具有存儲(chǔ)外部配置以及查詢外部配置的能力,雖然也實(shí)現(xiàn)了PropertyResolver接口,這也是典型的裝飾者模式實(shí)現(xiàn)。
// AbstractEnvironment#resolvePlaceholders public String resolvePlaceholders(String text) { return this.propertyResolver.resolvePlaceholders(text); } // AbstractEnvironment#resolveRequiredPlaceholders public String resolveRequiredPlaceholders(String text) throws IllegalArgumentException { return this.propertyResolver.resolveRequiredPlaceholders(text); }
AbstractProperyResolver實(shí)現(xiàn)了resolvePlaceholders方法以及resolveRequiredPlaceholders方法,不過(guò)能明顯看到都是委派給PropertyPlaceholderHelper類來(lái)完成占位符的解析。不過(guò)重點(diǎn)是在通過(guò)cr-eatePlaceholders方法創(chuàng)建PropertyPlaceholderHelper實(shí)例傳入的布爾值。
在resolvePlaceholders方法中調(diào)用createPlaceholderHelper方法時(shí),傳入的布爾值為true,而在resolveRequiredPlaceholders方法中調(diào)用createPlaceholders方式時(shí)傳入的布爾值為false。
該值決定了PropertyPlaceholderHelper在面對(duì)無(wú)法解析的占位符時(shí)的行為。
@Nullable private PropertyPlaceholderHelper nonStrictHelper; @Nullable private PropertyPlaceholderHelper strictHelper; // AbstractPropertyResolver#resolvePlaceholders public String resolvePlaceholders(String text) { if (this.nonStrictHelper == null) { this.nonStrictHelper = createPlaceholderHelper(true); } return doResolvePlaceholders(text, this.nonStrictHelper); } // AbstractPropertyResolver#resolveRequiredPlaceholders public String resolveRequiredPlaceholders(String text) throws IllegalArgumentException { if (this.strictHelper == null) { this.strictHelper = createPlaceholderHelper(false); } return doResolvePlaceholders(text, this.strictHelper); }
在PropertyPlaceholderHelper的parseStringValue方法中(該方法就是Spring 應(yīng)用上下文中解析占位符的地方(例如@Value注解中配置的占位符))。
在該方法中,對(duì)于無(wú)法解析的占位符,首先會(huì)判斷ignoreUnresolvablePlaceholders屬性是否為true,如果為true,則繼續(xù)嘗試解析,否則(else分支)就是拋出我們前面的看到的異常。
ingnoreUresolvablePlaceholder屬性的含義是代表是否需要忽略不能解析的占位符。
// PropertyPlaceholderHelper#parseStringValue protected String parseStringValue( String value, PlaceholderResolver placeholderResolver, @Nullable Set<String> visitedPlaceholders) { // 省略與本次分析無(wú)關(guān)代碼...... StringBuilder result = new StringBuilder(value); while (startIndex != -1) { int endIndex = findPlaceholderEndIndex(result, startIndex); if (endIndex != -1) { // 省略與本次分析無(wú)關(guān)代碼...... } else if (this.ignoreUnresolvablePlaceholders) { // Proceed with unprocessed value. startIndex = result.indexOf(this.placeholderPrefix, endIndex + this.placeholderSuffix.length()); } else { throw new IllegalArgumentException("Could not resolve placeholder '" + placeholder + "'" + " in value \"" + value + "\""); } visitedPlaceholders.remove(originalPlaceholder); } else { startIndex = -1; } } return result.toString(); }
總結(jié)
我們可以在@ComponentScan注解中通過(guò)使用占位符來(lái)外部化指定Spring 應(yīng)用上下文要加載的資源路徑,但需要注意的是要配合@PropertySource注解使用或者通過(guò)-D參數(shù)來(lái)指定。
在使用@PropertySource注解來(lái)導(dǎo)入外部配置資源的時(shí)候,需要注意的是該注解必須添加到通過(guò)調(diào)用register方法注冊(cè)的配置類中,并且如果傳入的是多個(gè)配置類,那么需要注意它們之間的順序,以防因?yàn)轫樞騿?wèn)題而導(dǎo)致解析占位符失敗。
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
Java并發(fā)編程中的ReentrantLock類詳解
這篇文章主要介紹了Java并發(fā)編程中的ReentrantLock類詳解,ReentrantLock是juc.locks包中的一個(gè)獨(dú)占式可重入鎖,相比synchronized,它可以創(chuàng)建多個(gè)條件等待隊(duì)列,還支持公平/非公平鎖、可中斷、超時(shí)、輪詢等特性,需要的朋友可以參考下2023-12-12Java 8系列之Stream中萬(wàn)能的reduce用法說(shuō)明
這篇文章主要介紹了Java 8系列之Stream中萬(wàn)能的reduce用法說(shuō)明,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-08-08Springboot?中的?Filter?實(shí)現(xiàn)超大響應(yīng)?JSON?數(shù)據(jù)壓縮的方法
這篇文章主要介紹了Springboot?中的?Filter?實(shí)現(xiàn)超大響應(yīng)?JSON?數(shù)據(jù)壓縮,定義GzipFilter對(duì)輸出進(jìn)行攔截,定義 Controller該 Controller 非常簡(jiǎn)單,主要讀取一個(gè)大文本文件,作為輸出的內(nèi)容,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),需要的朋友可以參考下2022-10-10Mybatis基于注解實(shí)現(xiàn)多表查詢功能
這篇文章主要介紹了Mybatis基于注解實(shí)現(xiàn)多表查詢功能,非常不錯(cuò),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2019-09-09SpringBoot實(shí)現(xiàn)配置文件的替換
這篇文章主要介紹了SpringBoot實(shí)現(xiàn)配置文件的替換,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-12-12Java語(yǔ)言實(shí)現(xiàn)簡(jiǎn)單FTP軟件 FTP上傳下載隊(duì)列窗口實(shí)現(xiàn)(7)
這篇文章主要為大家詳細(xì)介紹了Java語(yǔ)言實(shí)現(xiàn)簡(jiǎn)單FTP軟件,F(xiàn)TP上傳下載隊(duì)列窗口的實(shí)現(xiàn)方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-04-04java對(duì)xml節(jié)點(diǎn)屬性的增刪改查實(shí)現(xiàn)方法
下面小編就為大家?guī)?lái)一篇java對(duì)xml節(jié)點(diǎn)屬性的增刪改查實(shí)現(xiàn)方法。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2016-10-10實(shí)例講解Java的MyBatis框架對(duì)MySQL中數(shù)據(jù)的關(guān)聯(lián)查詢
這里我們來(lái)以實(shí)例講解Java的MyBatis框架對(duì)MySQL中數(shù)據(jù)的關(guān)聯(lián)查詢,包括一對(duì)多、多對(duì)一的關(guān)聯(lián)查詢以及自身關(guān)聯(lián)映射的方法等,需要的朋友可以參考下2016-06-06