Spring中@Value標簽的使用詳解


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的處理大概可以分為下面幾步:

  1. 加載Bean定義(從xml或者從@Import等)
  2. 處理BeanFactoryPostProcessor
  3. 實例化Bean
  4. 處理Bean的property注入
  5. 處理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或者查找一些值注入的問題都非常有幫助。

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM