1.@Value標簽
由於Spring對通過IOC的方式對對象進行統一管理,所以對任何對象而言,其生成方法均由Spring管理。傳統的方法是通過XML配置每一個Bean,並對這個Bean的所有Field進行聲明式配置。
以一個簡單的學校的例子為示范。假設有兩種角色,老師和班長。
package org.kingszelda.version3.controller; /** * Created by shining.cui on 2017/7/30. */ public class Teacher { /** * 姓名 */ private String name = "王老師"; /** * 教授科目 */ private String major = "數學"; /** * 教授課程班級的班長 */ private ClassMonitor classMonitor = new ClassMonitor(); /** * 老師會上課 */ public void teachLesson() { } /** * 老師要收作業,然而老師並不親自收,而是交給班長收 */ public void collectHomework() { classMonitor.collectHomework(); } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getMajor() { return major; } public void setMajor(String major) { this.major = major; } public ClassMonitor getClassMonitor() { return classMonitor; } public void setClassMonitor(ClassMonitor classMonitor) { this.classMonitor = classMonitor; } }
老師有自己的名字和教授的科目兩個屬性,這屬於老師的靜態屬性。老師有很多“功能“,比如上課和收作業。上課屬於老師可以自己完成的功能,而收作業則需要班長幫忙。所以老師為了完成自己的工作是不能獨立存在的,需要依賴班長。
/** * Created by shining.cui on 2017/7/30. */ public class ClassMonitor { public void collectHomework(){ System.out.println("開始收作業了!"); System.out.println("收作業完畢"); } }
這里我們假設班長只有一個功能,就是收作業。
上面的例子很好的說明了對象之間相互依賴共同合作的方法,即互相依賴。這些功能交給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:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <bean id="classMonitor" class="org.kingszelda.version3.controller.ClassMonitor"/> <bean id="teacher" class="org.kingszelda.version3.controller.Teacher"> <property name="name" value="王老師"/> <property name="major" value="數學"/> <property name="classMonitor" ref="classMonitor"/> </bean> </beans>
通過這種配置的方式之后,實體之間的依賴關系變得一清二楚。比如Teacher的名字,科目,所依賴的班長是哪個,只看配置文件就可以一目了然。但是,當實體變多了之后,可想而知,這個xml配置文件將龐大的不可想象,就更不要提可讀性了。
於是Spring從3.0開始推出了基於注解的形式,來簡化配置。
package org.kingszelda.version3.controller; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import javax.annotation.Resource; /** * Created by shining.cui on 2017/7/30. */ @Service public class Teacher { /** * 姓名 */ @Value("王老師") private String name; /** * 教授科目 */ @Value("數學") private String major; /** * 教授課程班級的班長 */ @Resource private ClassMonitor classMonitor; /** * 老師會上課 */ public void teachLesson() { } /** * 老師要收作業,然而老師並不親自收,而是交給班長收 */ public void collectHomework() { classMonitor.collectHomework(); } }
通過注解的形式已經減少了大量的get、set方法,通過@Resource注入了依賴的班長,並且通過@Value注入了老師的姓名和科目。
問題來了,當姓名與科目如何做到可配置,而不是寫死的呢?對應xml與注解都有其對應的方式。
首先聲明一個config.perproties文件:
name=張老師
major=數學
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:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <context:property-placeholder location="classpath:config.properties"/> <bean id="classMonitor" class="org.kingszelda.version3.controller.ClassMonitor"/> <bean id="teacher" class="org.kingszelda.version3.controller.Teacher"> <property name="name" value="${name}"/> <property name="major" value="${major}"/> <property name="classMonitor" ref="classMonitor"/> </bean> </beans>
注解:
package org.kingszelda.version3.controller; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import javax.annotation.Resource; /** * Created by shining.cui on 2017/7/30. */ @Service public class Teacher { /** * 姓名 */ @Value("${name}") private String name; /** * 教授科目 */ @Value("${major}") private String major; /** * 教授課程班級的班長 */ @Resource private ClassMonitor classMonitor; /** * 老師會上課 */ public void teachLesson() { } /** * 老師要收作業,然而老師並不親自收,而是交給班長收 */ public void collectHomework() { classMonitor.collectHomework(); } }
我們看到,不論是通過xml配置,還是通過注解@Value的方式,姓名與科目都在生成Teacher對象是被賦值進去了。
2.@Value是如何起作用的
Spirng在生命周期里關於Bean的處理大概可以分為下面幾步:
- 加載Bean定義(從xml或者從@Import等)
- 處理BeanFactoryPostProcessor
- 實例化Bean
- 處理Bean的property注入
- 處理BeanPostProcessor
而當我們在聲明了<context:property-placeholder location="classpath:config.properties"/>標簽之后,即聲明了一個配置型bean交給Spring容器進行管理,即PropertyPlaceholderConfigurer類。我們先看一下這個類的繼承結構。
藍色的是功能性結構,實現了解析文件,處理文件的功能,黃色的是Spring的鈎子功能,聲明了功能性模塊被調用的時機。這里PrepertyPlaceholderConfigurer實現了BeanFactoryPostProcesser接口,並實現了postProcessBeanFactory()方法即當Spring容器的BeanFactory被構造成功之后會調用這個方法。這里要注意,ApplicationContext也是BeanFactory的派生類。這時,我們先看父類的PropertyResourceConfigurer方法postProcessBeanFactory。因為這個類繼承了Spring的BeanFactoryPostProcesser接口,所以這個方法一定是操作BeanFactory的。
@Override public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { try {
//1.獲取當前容器配置的所有Properties文件,可能由多個文件merge而來 Properties mergedProps = mergeProperties(); //2.如果需要的話,將Properties文件的內容進行轉化,因為默認的Preperties都是String的key-value形式。
//Spring提供的默認方式是不轉化,保持String,String的key-value convertProperties(mergedProps); //3.由子類繼承,對容器與Properties進行操作,即value注入。 processProperties(beanFactory, mergedProps); } catch (IOException ex) { throw new BeanInitializationException("Could not load properties", ex); } }
這里最重要的第一步就是獲得Properties文件即mergeProperties方法,這是解析資源文件最基本的方法,所以這個方法一定存在於當前功能的最基類中,即PropertiesLoaderSupport。由於xml中是這樣配置的:
<context:property-placeholder location="classpath:config.properties"/>
這里聲明了一個PropertyPlaceholderConfigurer對象,顯然是調用了setLocation方法,而這個方法同樣存在於該功能模塊的最基本父類PropertiesLoaderSupport中。
public abstract class PropertiesLoaderSupport { //……省略 private Resource[] locations; //……省略 public void setLocation(Resource location) { this.locations = new Resource[] {location}; } //注意:晚聲明的文件內容將覆蓋早聲明的文件內容,所以請自己保證文件之間內容不要重疊,否則以最后一個文件為准 public void setLocations(Resource... locations) { this.locations = locations; } //……省略 }
mergeProperties方法中進行了配置化管理,針對某些標志位進行額外的操作,這里不做過多說明,其最重要的功能是調用了下面方法:
protected void loadProperties(Properties props) throws IOException { if (this.locations != null) {
//1.遍歷聲明的Resource文件地址 for (Resource location : this.locations) { if (logger.isInfoEnabled()) { logger.info("Loading properties file from " + location); } try {
//2.獲得Resource文件流,並加載內容到Properties對象中 PropertiesLoaderUtils.fillProperties( props, new EncodedResource(location, this.fileEncoding), this.propertiesPersister); } catch (IOException ex) { if (this.ignoreResourceNotFound) { if (logger.isWarnEnabled()) { logger.warn("Could not load properties from " + location + ": " + ex.getMessage()); } } else { throw ex; } } } } }
回想PropertyResourceConfigurer主流程中的三個方法,第一步已經執行完畢,加載了配置的properties文件,第二步是spring自己的默認實現,將非空的key對應的value放入Properties中,第三步則該由子類各自實現了,將BeanFactory與Properties進行統一操作。這時候我們看我們直接聲明的派生類PropertyPlaceholderConfigurer。
@Override protected void processProperties(ConfigurableListableBeanFactory beanFactoryToProcess, Properties props) throws BeansException { //1.聲明一個支持value為String類型的Resolver StringValueResolver valueResolver = new PlaceholderResolvingStringValueResolver(props); //2.將key-value注入到BeanFactory的某些bean中
doProcessProperties(beanFactoryToProcess, valueResolver); }
接下來就是真正的value注入環節了
protected void doProcessProperties(ConfigurableListableBeanFactory beanFactoryToProcess, StringValueResolver valueResolver) { //1.將key-value內容聲明為BeanDefinitionVisitor對象,用來根據BeanDefinition修改即將生成的對應的Bean內容 BeanDefinitionVisitor visitor = new BeanDefinitionVisitor(valueResolver); String[] beanNames = beanFactoryToProcess.getBeanDefinitionNames(); for (String curName : beanNames) {
//2.只有同一個容器內的才可以進行value注入,同時應該避免掉操作本身,避免進入循環遞歸 if (!(curName.equals(this.beanName) && beanFactoryToProcess.equals(this.beanFactory))) { BeanDefinition bd = beanFactoryToProcess.getBeanDefinition(curName); try { visitor.visitBeanDefinition(bd); } catch (Exception ex) { throw new BeanDefinitionStoreException(bd.getResourceDescription(), curName, ex.getMessage(), ex); } } } //3.處理一些擁有別名的類 beanFactoryToProcess.resolveAliases(valueResolver); //4.New in Spring 3.0: resolve placeholders in embedded values such as annotation attributes.(這一步有些不懂,以后再修正)
beanFactoryToProcess.addEmbeddedValueResolver(valueResolver); }
在上述代碼中,第2步已經修改了原始的BeanDefinition,我們一路跟進去看,原來核心的替換功能在PropertyPlaceholderHelper中:
protected String parseStringValue( String strVal, PlaceholderResolver placeholderResolver, Set<String> visitedPlaceholders) { //1.對每一個key進行處理 StringBuilder result = new StringBuilder(strVal); //2.首先考慮有占位符的情況,默認是${} int startIndex = strVal.indexOf(this.placeholderPrefix); while (startIndex != -1) { int endIndex = findPlaceholderEndIndex(result, startIndex); if (endIndex != -1) { String placeholder = result.substring(startIndex + this.placeholderPrefix.length(), endIndex); String originalPlaceholder = placeholder; if (!visitedPlaceholders.add(originalPlaceholder)) { throw new IllegalArgumentException( "Circular placeholder reference '" + originalPlaceholder + "' in property definitions"); } //3.如果有占位符,則去掉占位符遞歸調用本方法,即key=${abc},處理成key=abc的形式試圖獲取value placeholder = parseStringValue(placeholder, placeholderResolver, visitedPlaceholders); //4.真正的從key-value集合中獲得key對應的value String propVal = placeholderResolver.resolvePlaceholder(placeholder);
//5.如果沒有找到,則試圖按照${key:default}的形式解析 if (propVal == null && this.valueSeparator != null) { int separatorIndex = placeholder.indexOf(this.valueSeparator); if (separatorIndex != -1) {
//6.獲得:之前的內容,即真正的key String actualPlaceholder = placeholder.substring(0, separatorIndex); //7.獲得:之后的內容,即默認值
String defaultValue = placeholder.substring(separatorIndex + this.valueSeparator.length());
//8.再次嘗試從key-value集合中獲得內容,因為如果真的是key-value的形式,按照全名是肯定找不到的 propVal = placeholderResolver.resolvePlaceholder(actualPlaceholder);
//9.如果找到了就按照配置的走,如果沒有找到則附上默認值 if (propVal == null) { propVal = defaultValue; } } } if (propVal != null) { //10.如果找到了這個value,則再次遞歸調用自己,避免value也是占位符的情況 propVal = parseStringValue(propVal, placeholderResolver, visitedPlaceholders);
//11.將獲得的結果替換掉 result.replace(startIndex, endIndex + this.placeholderSuffix.length(), propVal); if (logger.isTraceEnabled()) { logger.trace("Resolved placeholder '" + placeholder + "'"); } startIndex = result.indexOf(this.placeholderPrefix, startIndex + propVal.length()); } 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 string value \"" + strVal + "\""); } visitedPlaceholders.remove(originalPlaceholder); } else { startIndex = -1; } } return result.toString(); }
3.總結
Spring管理bean的方式大大減少了編程人員的編碼復雜度,這個復雜度並沒有消失,而是轉嫁到Spring容器中。我們可以通過xml或者@value注解的方式注入field屬性,這對多環境profiles的應用是非常關鍵的,掌握了Spring的這條注入路徑之后,對於幫助理解Spring或者查找一些值注入的問題都非常有幫助。