Spring 核心(第三部分)

=======續

= = BeanFactory
BeanFactory API爲Spring的IoC功能提供了基礎。它的特定契約主要用於與Spring的其他部分和相關第三方框架的集成,它的DefaultListableBeanFactory實現是更高級別的GenericApplicationContext容器中的一個關鍵委託。

BeanFactory和相關接口(如BeanFactoryAware、InitializingBean、一次性bean)是其他框架組件的重要集成點。通過不需要任何註釋甚至反射,它們允許容器及其組件之間非常有效的交互。應用程序級bean可能使用相同的回調接口,但通常更喜歡通過註釋或編程配置進行聲明性依賴項注入。

請注意,核心BeanFactory API級別及其DefaultListableBeanFactory實現不會對配置格式或要使用的任何組件註釋做任何假設。所有這些風格都是通過擴展(如XmlBeanDefinitionReader和AutowiredAnnotationBeanPostProcessor)來實現的,並作爲核心元數據表示對共享的BeanDefinition對象進行操作。這就是Spring的容器如此靈活和可擴展的本質。

=== BeanFactory還是ApplicationContext?
本節解釋BeanFactory和ApplicationContext容器級別之間的差異,以及對引導的影響。

除非有充分的理由,否則應該使用ApplicationContext,將GenericApplicationContext及其子類AnnotationConfigApplicationContext作爲自定義引導的公共實現。這些是Spring核心容器的主要入口點,用於所有常見目的:加載配置文件、觸發類路徑掃描、以編程方式註冊bean定義和帶註釋的類,以及(從5.0開始)註冊功能性bean定義。

因爲ApplicationContext包含了BeanFactory的所有功能,所以通常推薦使用它而不是簡單的BeanFactory,除非需要完全控制bean處理。在ApplicationContext(比如GenericApplicationContext實現)的幾種檢測到bean按照慣例(也就是說,由bean名稱或bean類型——特別是,後處理器),而普通DefaultListableBeanFactory不可知論者是關於任何特殊bean。

對於許多擴展的容器特性,例如註釋處理和AOP代理,BeanPostProcessor擴展點是必不可少的。如果只使用普通的DefaultListableBeanFactory,那麼默認情況下不會檢測到並激活這樣的後處理器。這種情況可能令人困惑,因爲您的bean配置實際上沒有任何錯誤。相反,在這種情況下,需要通過額外的設置完全引導容器。

下表列出了BeanFactory和ApplicationContext接口和實現提供的特性。

Table 9. Feature Matrix
Feature BeanFactory ApplicationContext
Bean實例化/配置

Yes

Yes

集成的生命週期管理

No

Yes

自動BeanPostProcessor註冊

No

Yes

自動BeanFactoryPostProcessor註冊

No

Yes

便捷的MessageSource訪問 (內部化)

No

Yes

內置的ApplicationEvent發佈機制

No

Yes

要顯式地用DefaultListableBeanFactory註冊bean後處理器,您需要以編程方式調用addBeanPostProcessor,如下面的示例所示:

DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
// populate the factory with bean definitions

// now register any needed BeanPostProcessor instances
factory.addBeanPostProcessor(new AutowiredAnnotationBeanPostProcessor());
factory.addBeanPostProcessor(new MyBeanPostProcessor());

// now start using the factory

要將BeanFactoryPostProcessor應用到普通的DefaultListableBeanFactory,您需要調用它的postProcessBeanFactory方法,如下面的示例所示:

DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(factory);
reader.loadBeanDefinitions(new FileSystemResource("beans.xml"));

// bring in some property values from a Properties file
PropertySourcesPlaceholderConfigurer cfg = new PropertySourcesPlaceholderConfigurer();
cfg.setLocation(new FileSystemResource("jdbc.properties"));

// now actually do the replacement
cfg.postProcessBeanFactory(factory);

在這兩種情況下,明確登記步驟是不方便的,這就是爲什麼各種ApplicationContext變體都優於純DefaultListableBeanFactory回彈應用程序,尤其是當依靠BeanFactoryPostProcessor和BeanPostProcessor實例擴展容器功能在一個典型的企業設置。

一個AnnotationConfigApplicationContext擁有所有已註冊的公共註釋後置處理器,並且可以通過配置註釋(如@EnableTransactionManagement)在幕後引入額外的處理器。在Spring的基於註釋的配置模型的抽象級別上,bean後處理器的概念變成了純粹的內部容器細節。

=資源
本章討論Spring如何處理資源,以及如何使用Spring中的資源。它包括下列主題:

= =介紹
不幸的是,Java的標準Java .net.URL類和各種URL前綴的標準處理程序並不足以滿足所有對底層資源的訪問。例如,沒有標準化的URL實現可用於訪問需要從類路徑或相對於ServletContext獲得的資源。雖然可以爲專業註冊新處理程序URL前綴(類似於現有的前綴,如http處理程序:),這通常是非常複雜的,和URL接口仍然缺乏一些可取的功能,比如一個方法來檢查存在的資源被指出。

==資源接口
Spring的資源接口是一個更強大的接口,用於抽象對底層資源的訪問。下面的清單顯示了資源接口定義:

public interface Resource extends InputStreamSource {

    boolean exists();

    boolean isOpen();

    URL getURL() throws IOException;

    File getFile() throws IOException;

    Resource createRelative(String relativePath) throws IOException;

    String getFilename();

    String getDescription();
}

正如Resource接口的定義所示,它擴展了InputStreamSource接口。下面的清單顯示了InputStreamSource接口的定義:

public interface InputStreamSource {

    InputStream getInputStream() throws IOException;
}

一些最重要的方法從資源接口是:

  • getInputStream():定位並打開資源,返回一個InputStream來讀取資源。預期每次調用都返回一個新的InputStream。調用者的職責是關閉流。
  • exists():返回一個布爾值,指示該資源是否以物理形式實際存在。
  • isOpen():返回一個布爾值,指示該資源是否表示具有打開流的句柄。如果爲真,則不能多次讀取InputStream,必須只讀取一次,然後關閉,以避免資源泄漏。對於所有常見的資源實現,返回false, InputStreamResource除外。
  • getDescription():返回該資源的描述,用於處理該資源時的錯誤輸出。這通常是完全限定的文件名或資源的實際URL。

其他方法允許您獲取表示資源的實際URL或文件對象(如果底層實現兼容並支持該功能)。

Spring本身廣泛地使用資源抽象,在需要資源時將其作爲許多方法簽名中的參數類型。其他方法在某些Spring api(如各種ApplicationContext實現構造函數)在樸素的弦或簡單的形式被用來創建一個資源適合上下文實現或通過特殊前綴弦上的路徑,讓調用者指定一個特定的資源實現必須創建和使用。

雖然資源接口在Spring中被大量使用,但是在您自己的代碼中作爲通用的實用程序類來訪問資源實際上非常有用,即使您的代碼不知道或不關心Spring的任何其他部分。雖然這將您的代碼耦合到Spring,但實際上它只將您的代碼耦合到這一小組實用工具類中,這些實用工具類作爲URL的更有能力的替代,可以認爲與您爲此目的使用的任何其他庫是等價的。

注意:資源抽象不能代替功能。在可能的地方進行包裝。例如,UrlResource包裝一個URL並使用包裝後的URL完成其工作。

==內置資源實現
Spring包含以下資源實現:

= = = UrlResource
UrlResource封裝了java.net.URL,可用於訪問通常可通過URL訪問的任何對象,如文件、HTTP目標、FTP目標等。所有URL都有一個標準化的字符串表示,這樣就可以使用適當的標準化前綴來表示不同的URL類型。這包括文件:用於訪問文件系統路徑,http:用於通過http協議訪問資源,ftp:用於通過ftp訪問資源,等等。

UrlResource是由Java代碼顯式地使用UrlResource構造函數創建的,但通常在調用API方法時隱式地創建,該方法接受一個表示路徑的字符串參數。對於後一種情況,JavaBeans PropertyEditor最終決定創建哪種類型的資源。如果路徑字符串包含已知的(也就是說)前綴(例如classpath:),它將爲該前綴創建適當的專門化資源。但是,如果它不識別前綴,則假定該字符串是標準URL字符串,並創建一個UrlResource。

= = = ClassPathResource
這個類表示應該從類路徑中獲得的資源。它使用線程上下文類裝入器、給定的類裝入器或給定的類裝入資源。

這個資源實現支持java.io格式的解析。如果類路徑資源駐留在文件系統中,但是不屬於類路徑資源,類路徑資源駐留在jar中,並且沒有(通過servlet引擎或環境)擴展到文件系統。爲了解決這個問題,各種資源實現總是支持解析爲java.net.URL。

ClassPathResource是由Java代碼顯式地使用ClassPathResource構造函數創建的,但通常在調用API方法時隱式地創建,該方法接受一個表示路徑的字符串參數。對於後一種情況,JavaBeans PropertyEditor識別字符串路徑上的特殊前綴classpath:,並在這種情況下創建ClassPathResource。

= = = FileSystemResource
這是java.io的資源實現。文件和java.nio.file.path處理。它支持以file和URL的形式進行解析。

= = = ServletContextResource
這是ServletContext資源的資源實現,用於解釋相關web應用程序根目錄中的相對路徑。

它始終支持流訪問和URL訪問,只有當web應用程序存檔被展開並且資源物理上位於文件系統上時,纔可以進行java.io.file文件訪問。它是在文件系統上展開還是直接從JAR或其他類似數據庫的地方訪問(這是可以想象的),實際上取決於Servlet容器。

= = = InputStreamResource
InputStreamResource是給定InputStream的資源實現。只有在沒有特定的資源實現可用時,才應該使用它。特別是,儘可能選擇ByteArrayResource或任何基於文件的資源實現。

與其他資源實現相比,這是已經打開的資源的描述符。因此,它從isOpen()返回true。如果您需要將資源描述符保存在某個地方,或者需要多次讀取流,那麼不要使用它。

= = = ByteArrayResource
這是一個給定字節數組的資源實現。它爲給定的字節數組創建ByteArrayInputStream。
它對於從任何給定的字節數組加載內容都很有用,而不必求助於一次性使用的InputStreamResource。

= = ResourceLoader
ResourceLoader接口是由可以返回(即加載)資源實例的對象實現的。下面的清單顯示了ResourceLoader接口定義:

public interface ResourceLoader {

    Resource getResource(String location);
}

所有應用程序上下文都實現ResourceLoader接口。因此,可以使用所有應用程序上下文來獲取資源實例。

當您在特定的應用程序上下文中調用getResource(),並且指定的位置路徑沒有特定的前綴時,您將獲得適合於特定應用程序上下文的資源類型。例如,假設下面的代碼片段是針對ClassPathXmlApplicationContext實例執行的:

Resource template = ctx.getResource("some/resource/path/myTemplate.txt");

對於ClassPathXmlApplicationContext,該代碼返回一個ClassPathResource。如果對FileSystemXmlApplicationContext實例執行相同的方法,則會返回FileSystemResource。對於WebApplicationContext,它將返回一個ServletContextResource。它同樣會爲每個上下文返回適當的對象。

因此,可以以適合特定應用程序上下文的方式加載資源。
另一方面,您也可以通過指定特殊的classpath:前綴強制使用ClassPathResource,而不管應用程序的上下文類型如何,如下面的示例所示:

Resource template = ctx.getResource("classpath:some/resource/path/myTemplate.txt");

類似地,您可以通過指定任何標準的java.net.URL前綴來強制使用UrlResource。下面的例子使用文件和http前綴:

Resource template = ctx.getResource("file:///some/resource/path/myTemplate.txt");

Resource template = ctx.getResource("https://myhost.com/resource/path/myTemplate.txt");

下表總結了將字符串對象轉換爲資源對象的策略:

Prefix Example Explanation

classpath:

classpath:com/myapp/config.xml

Loaded from the classpath.

file:

file:///data/config.xml

Loaded as a URL from the filesystem. See also [resources-filesystemresource-caveats].

http:

https://myserver/logo.png

Loaded as a URL.

(none)

/data/config.xml

Depends on the underlying ApplicationContext.

== ResourceLoaderAware接口
ResourceLoaderAware接口是一個特殊的回調接口,它標識希望使用ResourceLoader引用提供的組件。下面的清單顯示了ResourceLoaderAware接口的定義:

public interface ResourceLoaderAware {

    void setResourceLoader(ResourceLoader resourceLoader);
}

當類實現ResourceLoaderAware並部署到應用程序上下文(作爲spring管理的bean)時,應用程序上下文將該類識別爲ResourceLoaderAware。然後,應用程序上下文調用setResourceLoader(ResourceLoader),將自己作爲參數提供(請記住,Spring中的所有應用程序上下文都實現了ResourceLoader接口)。

由於ApplicationContext是ResourceLoader, bean還可以實現ApplicationContext ware接口,並直接使用提供的應用程序上下文來加載資源。但是,一般來說,如果您需要的話,最好使用專用的ResourceLoader接口。代碼將只耦合到資源加載接口(可以認爲是一個實用程序接口),而不耦合到整個Spring ApplicationContext接口。

在應用程序組件中,您還可以依賴ResourceLoader的自動裝配來實現ResourceLoaderAware接口。“傳統的”構造函數和byType自動裝配模式(如自動裝配協作者中所述)能夠分別爲構造函數參數或setter方法參數提供ResourceLoader。爲了獲得更大的靈活性(包括自動裝配字段和多個參數方法的能力),可以考慮使用基於註釋的自動裝配特性。在這種情況下,ResourceLoader被自動拖放到一個字段、構造函數參數或方法參數中,只要字段、構造函數或方法帶有@Autowired註解,就可以使用ResourceLoader類型。更多信息,請參見使用@Autowired。

==資源作爲依賴項
如果bean本身要通過某種動態過程來確定和提供資源路徑,那麼使用ResourceLoader接口來加載資源可能是有意義的。例如,考慮加載某種類型的模板,其中所需的特定資源取決於用戶的角色。如果資源是靜態的,那麼完全消除ResourceLoader接口的使用是有意義的,讓bean公開它需要的資源屬性,並期望將它們注入到bean中。

然後注入這些屬性非常簡單,因爲所有應用程序上下文都註冊並使用一個特殊的JavaBeans PropertyEditor,它可以將字符串路徑轉換爲資源對象。因此,如果myBean有一個Resource類型的模板屬性,它可以使用該資源的簡單字符串進行配置,如下面的示例所示:

<bean id="myBean" class="...">
    <property name="template" value="some/resource/path/myTemplate.txt"/>
</bean>

注意,資源路徑沒有前綴。因此,由於應用程序上下文本身將用作ResourceLoader,所以根據上下文的確切類型,通過ClassPathResource、FileSystemResource或ServletContextResource加載資源本身。

如果需要強制使用特定的資源類型,可以使用前綴。下面兩個例子展示瞭如何強制使用ClassPathResource和UrlResource(後者用於訪問文件系統文件):

<property name="template" value="classpath:some/resource/path/myTemplate.txt">
<property name="template" value="file:///some/resource/path/myTemplate.txt"/>

==應用程序上下文和資源路徑
本節介紹如何使用資源創建應用程序上下文,包括使用XML的快捷方式、如何使用通配符和其他細節。

===構造應用程序上下文
應用程序上下文構造器(用於特定的應用程序上下文類型)通常將字符串或字符串數組作爲資源的位置路徑,例如組成上下文定義的XML文件。

當這樣的位置路徑沒有前綴時,從該路徑構建並用於加載bean定義的特定資源類型依賴於特定的應用程序上下文,並且適合於特定的應用程序上下文。例如,考慮下面的例子,它創建了一個ClassPathXmlApplicationContext:

ApplicationContext ctx = new ClassPathXmlApplicationContext("conf/appContext.xml");

因爲使用了ClassPathResource,所以bean定義是從類路徑加載的。但是,考慮下面的示例,它創建了一個FileSystemXmlApplicationContext:

ApplicationContext ctx =
    new FileSystemXmlApplicationContext("conf/appContext.xml");

現在bean定義從文件系統位置加載(在本例中,相對於當前工作目錄)。
注意,在位置路徑上使用特殊的類路徑前綴或標準URL前綴會覆蓋爲加載定義而創建的默認資源類型。考慮下面的例子:

ApplicationContext ctx =
    new FileSystemXmlApplicationContext("classpath:conf/appContext.xml");

使用FileSystemXmlApplicationContext從類路徑加載bean定義。但是,它仍然是FileSystemXmlApplicationContext。如果它隨後被用作ResourceLoader,那麼任何沒有前綴的路徑仍然被視爲文件系統路徑。

====構造ClassPathXmlApplicationContext實例-快捷方式
ClassPathXmlApplicationContext公開了許多構造函數來支持方便的實例化。基本思想是,您可以只提供一個字符串數組,其中只包含XML文件本身的文件名(不包含引導路徑信息),並且還提供一個類。然後,ClassPathXmlApplicationContext從提供的類派生路徑信息。

考慮以下目錄佈局:

com/
  foo/
    services.xml
    daos.xml
    MessengerService.class

下面的示例展示瞭如何實例化ClassPathXmlApplicationContext實例,該實例由名爲services.xml和daos.xml(在類路徑上)文件中定義的bean組成:

ApplicationContext ctx = new ClassPathXmlApplicationContext(
    new String[] {"services.xml", "daos.xml"}, MessengerService.class);

有關各種構造函數的詳細信息,請參閱ClassPathXmlApplicationContext javadoc。

===應用程序上下文構造函數資源路徑中的通配符
應用程序上下文構造函數值中的資源路徑可以是簡單路徑(如前面所示),每個路徑都有到目標資源的一對一映射,或者包含特殊的“classpath*:”前綴或內部ant樣式的正則表達式(通過使用Spring的PathMatcher實用程序進行匹配)。後者都是有效的通配符。

此機制的一個用途是,當您需要進行組件樣式的應用程序組裝時。所有組件都可以將上下文定義片段“發佈”到一個已知的位置路徑,並且,當使用以classpath*:作爲前綴的相同路徑創建最終的應用程序上下文時,所有組件片段都會自動被獲取。

請注意,這種通配符是特定於在應用程序上下文構造函數中使用資源路徑的(或直接使用PathMatcher實用程序類層次結構時),並在構造時解析。它與資源類型本身無關。您不能使用classpath*:前綴來構造實際的資源,因爲一個資源一次只能指向一個資源。

= = = = ant是基於模式
路徑位置可以包含ant樣式的模式,如下面的例子所示:

/WEB-INF/-context.xml
com/mycompany//applicationContext.xml
file:C:/some/path/-context.xml
classpath:com/mycompany//applicationContext.xml

====對可移植性的影響
如果指定的路徑已經是一個文件URL(因爲基本ResourceLoader是一個文件系統URL,所以它是隱式的,或者是顯式的),那麼可以保證通配符以完全可移植的方式工作。

如果指定的路徑是類路徑位置,則解析器必須通過調用Classloader.getResource()來獲取最後一個非通配符路徑段URL。由於這只是路徑的一個節點(而不是末尾的文件),所以它實際上沒有定義(在類加載器javadoc中)在本例中返回的URL的類型。實際上,它總是一個java.io。表示目錄(其中類路徑資源解析爲文件系統位置)或某種jar URL(其中類路徑資源解析爲jar位置)的文件。不過,這種操作的可移植性令人擔憂。

如果爲最後一個非通配符段獲得了jar URL,則解析器必須能夠從中獲得java.net.JarURLConnection,或者手動解析jar URL,以便能夠遍歷jar的內容並解析通配符。這在大多數環境中都有效,但在其他環境中無效,我們強烈建議在依賴jar之前,在您的特定環境中徹底測試來自jar的資源的通配符解析。

==== classpath*:前綴
在構造基於xml的應用程序上下文時,位置字符串可以使用特殊的classpath*:前綴,如下例所示:

ApplicationContext ctx =
    new ClassPathXmlApplicationContext("classpath*:conf/appContext.xml");

這個特殊的前綴指定必須獲得所有與給定名稱匹配的類路徑資源(在內部,這實際上是通過調用ClassLoader.getResources(…)來實現的),然後合併形成最終的應用程序上下文定義。

注意:通配符類路徑依賴於底層類裝入器的getResources()方法。由於現在大多數應用程序服務器都提供自己的類加載器實現,所以其行爲可能有所不同,特別是在處理jar文件時。檢查classpath*是否工作的一個簡單測試是使用類裝入器從classpath上的jar中裝入一個文件:getClass(). getclassloader (). getresources ("<someFileInsideTheJar>")。對具有相同名稱但放在兩個不同位置中的文件進行此測試。如果返回不適當的結果,請檢查application server文檔中可能影響類加載器行爲的設置。

您還可以將classpath*:前綴與位置路徑其餘部分中的PathMatcher模式相結合(例如,classpath*:META-INF/*-beans.xml)。在這種情況下,解決策略是相當簡單的:一個ClassLoader.getResources()調用用於non-wildcard路徑段最後一個類加載器層次結構中的所有匹配的資源,然後每個資源,前面描述的相同PathMatcher解決策略用於通配符子路徑。

===與通配符有關的其他說明
注意,當與ant樣式模式結合使用時,除非實際的目標文件駐留在文件系統中,否則classpath*:只能在模式啓動之前可靠地與至少一個根目錄一起工作。這意味着,類似classpath*:*.xml這樣的模式可能不會從jar文件的根目錄檢索文件,而是隻從擴展目錄的根目錄檢索文件。

Spring檢索類路徑條目的能力源於JDK的ClassLoader.getResources()方法,該方法只返回空字符串的文件系統位置(表示要搜索的潛在根)。Spring計算URLClassLoader運行時配置和java.class。路徑清單也在jar文件中,但這不能保證會導致可移植的行爲。

掃描類路徑包需要在類路徑中存在相應的目錄條目。當您使用Ant構建JAR時,不要激活JAR任務的只文件開關。而且,在某些環境中,類路徑目錄可能不會根據安全策略公開—例如,JDK 1.7.0_45或更高版本上的獨立應用程序(這要求在清單中設置“受信任庫”)。見 https://stackoverflow.com/questions/19394570/java-jre-7u45-breaks-classloader-getresources)。

在JDK 9的模塊路徑(Jigsaw)上,Spring的類路徑掃描通常按預期工作。在這裏,將資源放入專用目錄也是非常值得推薦的,這樣可以避免在搜索jar文件根級別時出現前面提到的可移植性問題。

帶有類路徑的ant樣式模式:如果要搜索的根包在多個類路徑位置可用,則不能保證資源能夠找到匹配的資源。考慮下面的資源位置示例:

com/mycompany/package1/service-context.xml

現在考慮一個ant樣式的路徑,有人可能會使用它來查找文件:

classpath:com/mycompany/**/service-context.xml

這樣的資源可能只有一個位置,但是當使用前面的示例這樣的路徑來嘗試解析它時,解析器會處理getResource返回的(第一個)URL。如果此基本包節點存在於多個類加載器位置,則實際的端資源可能不存在。因此,在這種情況下,您應該使用與ant樣式相同的classpath*:,它搜索包含根包的所有類路徑位置。

= = = FileSystemResource警告
未附加到FileSystemApplicationContext(即當FileSystemApplicationContext不是實際的ResourceLoader時)的文件系統資源按預期處理絕對路徑和相對路徑。相對路徑相對於當前工作目錄,而絕對路徑相對於文件系統的根目錄。

但是,出於向後兼容性(歷史原因)的原因,當FileSystemApplicationContext是ResourceLoader時,這種情況會發生變化。FileSystemApplicationContext強制所有附加的FileSystemResource實例將所有位置路徑視爲相對路徑,不管它們是否以正斜槓開頭。在實踐中,這意味着下面的例子是等價的:

ApplicationContext ctx =
    new FileSystemXmlApplicationContext("conf/context.xml");
ApplicationContext ctx =
    new FileSystemXmlApplicationContext("/conf/context.xml");

下面的例子也是等價的(儘管它們不同是有意義的,因爲一種情況是相對的,另一種是絕對的):

FileSystemXmlApplicationContext ctx = ...;
ctx.getResource("some/resource/path/myTemplate.txt");
FileSystemXmlApplicationContext ctx = ...;
ctx.getResource("/some/resource/path/myTemplate.txt");

在實踐中,如果需要真正的絕對文件系統路徑,應該避免將絕對路徑與FileSystemResource或FileSystemXmlApplicationContext一起使用,並使用file: URL前綴強制使用UrlResource。下面的例子說明了如何做到這一點:

// actual context type doesn't matter, the Resource will always be UrlResource
ctx.getResource("file:///some/resource/path/myTemplate.txt");
// force this FileSystemXmlApplicationContext to load its definition via a UrlResource
ApplicationContext ctx =
    new FileSystemXmlApplicationContext("file:///conf/context.xml");

=驗證、數據綁定和類型轉換
將驗證視爲業務邏輯有其優點和缺點,Spring提供了一種驗證(和數據綁定)設計,不排除其中任何一種。具體來說,驗證不應該綁定到web層,應該易於本地化,並且應該可以插入任何可用的驗證器。考慮到這些問題,Spring提出了一個驗證器接口,它是基本的,並且在應用程序的每一層都非常有用。

數據綁定對於將用戶輸入動態綁定到應用程序的域模型(或用於處理用戶輸入的任何對象)非常有用。Spring提供了正確命名的DataBinder來完成這一任務。驗證器和DataBinder組成驗證包,主要用於但不限於MVC框架。

BeanWrapper是Spring框架中的一個基本概念,並且在很多地方被使用。但是,您可能不需要直接使用BeanWrapper。但是,因爲這是參考文檔,所以我們認爲應該進行一些解釋。我們將在本章中解釋BeanWrapper,因爲如果您要使用它,那麼在嘗試將數據綁定到對象時,您很可能會這樣做。

Spring的DataBinder和底層的BeanWrapper都使用PropertyEditorSupport實現來解析和格式化屬性值。PropertyEditor和PropertyEditorSupport類型是javabean規範的一部分,本章也將對此進行解釋。Spring 3引入了一個核心。提供一般類型轉換功能的轉換包,以及用於格式化UI字段值的高級“格式”包。您可以使用這些包作爲PropertyEditorSupport實現的更簡單的替代方案。本章也討論了這些問題。

jsr - 303 / jsr - 349 Bean驗證
從4.0版本開始,Spring框架支持Bean Validation 1.0 (JSR-303)和Bean Validation 1.1 (JSR-349),用於設置支持並將它們調整到Spring的Validator接口。
應用程序可以選擇全局啓用一次Bean驗證(如[Validation -beanvalidation]中所述),並僅用於所有驗證需求。

應用程序還可以爲每個DataBinder實例註冊額外的Spring驗證器實例,如[validationbinder]中所述。這對於在不使用註釋的情況下插入驗證邏輯可能很有用。

==使用Spring的Validator接口進行驗證
Spring提供了一個驗證器接口,您可以使用它來驗證對象。驗證器接口通過使用錯誤對象來工作,這樣,在進行驗證時,驗證器可以向錯誤對象報告驗證失敗。
下面是一個小數據對象的例子:

public class Person {

    private String name;
    private int age;

    // the usual getters and setters...
}

下一個示例通過實現org.springframework.validation的以下兩個方法爲Person類提供驗證行爲。驗證器接口:

  • 支持(類):這個驗證器可以驗證提供的類的實例嗎?
  • 驗證(Object, org.springframework. validate . errors):驗證給定的對象,如果出現驗證錯誤,則使用給定的errors對象註冊這些對象。

實現驗證器非常簡單,特別是當您知道Spring框架還提供了ValidationUtils幫助類時。下面的例子實現了Person實例的驗證器:

public class PersonValidator implements Validator {

    /**
     * This Validator validates only Person instances
     */
    public boolean supports(Class clazz) {
        return Person.class.equals(clazz);
    }

    public void validate(Object obj, Errors e) {
        ValidationUtils.rejectIfEmpty(e, "name", "name.empty");
        Person p = (Person) obj;
        if (p.getAge() < 0) {
            e.rejectValue("age", "negativevalue");
        } else if (p.getAge() > 110) {
            e.rejectValue("age", "too.darn.old");
        }
    }
}

ValidationUtils類上的靜態rejectIfEmpty(..)方法用於拒絕name屬性(如果它是null或空字符串)。查看ValidationUtils javadoc,看看除了前面顯示的示例外,它還提供了什麼功能。

雖然可以實現單個驗證器類來驗證富對象中的每個嵌套對象,但更好的做法是將每個嵌套對象類的驗證邏輯封裝到自己的驗證器實現中。“rich”對象的一個簡單示例是一個Customer,它由兩個字符串屬性(第一個和第二個名稱)和一個複雜的Address對象組成。Address對象可以獨立於Customer對象使用,因此實現了一個不同的AddressValidator。如果你想讓你的CustomerValidator重用AddressValidator類中包含的邏輯而不需要複製和粘貼,你可以依賴地在你的CustomerValidator中注入或實例化一個AddressValidator,如下面的例子所示:

public class CustomerValidator implements Validator {

    private final Validator addressValidator;

    public CustomerValidator(Validator addressValidator) {
        if (addressValidator == null) {
            throw new IllegalArgumentException("The supplied [Validator] is " +
                "required and must not be null.");
        }
        if (!addressValidator.supports(Address.class)) {
            throw new IllegalArgumentException("The supplied [Validator] must " +
                "support the validation of [Address] instances.");
        }
        this.addressValidator = addressValidator;
    }

    /**
     * This Validator validates Customer instances, and any subclasses of Customer too
     */
    public boolean supports(Class clazz) {
        return Customer.class.isAssignableFrom(clazz);
    }

    public void validate(Object target, Errors errors) {
        ValidationUtils.rejectIfEmptyOrWhitespace(errors, "firstName", "field.required");
        ValidationUtils.rejectIfEmptyOrWhitespace(errors, "surname", "field.required");
        Customer customer = (Customer) target;
        try {
            errors.pushNestedPath("address");
            ValidationUtils.invokeValidator(this.addressValidator, customer.getAddress(), errors);
        } finally {
            errors.popNestedPath();
        }
    }
}

驗證錯誤被報告給傳遞給驗證器的錯誤對象。對於Spring Web MVC,您可以使用< Spring:bind/>標記來檢查錯誤消息,但是您也可以自己檢查錯誤對象。有關它提供的方法的更多信息可以在javadoc中找到。
==將代碼解析爲錯誤消息

我們討論了數據綁定和驗證。本節討論與驗證錯誤對應的輸出消息。在上一節顯示的示例中,我們拒絕了name和age字段。如果我們想通過使用MessageSource輸出錯誤消息,我們可以使用拒絕字段時提供的錯誤代碼(本例中是'name'和'age')來實現。當您從Errors接口調用(通過使用,例如,ValidationUtils類)rejectValue或其他拒絕方法時,底層實現不僅註冊您傳入的代碼,而且註冊許多額外的錯誤代碼。MessageCodesResolver確定錯誤接口寄存器的錯誤代碼。默認情況下,使用DefaultMessageCodesResolver,它(例如)不僅用您提供的代碼註冊消息,而且還註冊包含傳遞給reject方法的字段名的消息。因此,如果您使用rejectValue(“age”、“toon .darn.old”)來拒絕一個字段,那麼toon . damn就除外。舊碼,春天也註冊。該死。舊。age和.darn.old.age.int(第一個包含字段名,第二個包含字段類型)。這樣做是爲了方便開發人員查找錯誤消息。

有關MessageCodesResolver和默認策略的更多信息可以分別在MessageCodesResolver和DefaultMessageCodesResolver的javadoc中找到。

== Bean操作和BeanWrapper
org.springframework。beans包遵循javabean標準。JavaBean是一個具有默認無參數構造函數的類,它遵循命名約定,其中(例如)名爲bingoMadness的屬性將具有setter方法setBingoMadness(..)和getter方法getBingoMadness()。有關javabean和規範的更多信息,請參見 javabeans

bean包中一個非常重要的類是BeanWrapper接口及其相應的實現(BeanWrapperImpl)。引用javadoc, BeanWrapper提供了設置和獲取屬性值的功能(單獨的或者成批的),獲取屬性描述符,以及查詢屬性來決定它們是可讀的還是可寫的。此外,BeanWrapper還提供了對嵌套屬性的支持,允許在子屬性上設置無限深度的屬性。BeanWrapper還支持添加標準JavaBeans PropertyChangeListeners和VetoableChangeListeners的能力,而不需要在目標類中支持代碼。最後,BeanWrapper提供了對設置索引屬性的支持。BeanWrapper通常不被應用程序代碼直接使用,而是被DataBinder和BeanFactory使用。

BeanWrapper的工作方式部分是由它的名字來指示的:它包裝一個bean來對那個bean執行操作,例如設置和檢索屬性。

===設置和獲取基本屬性和嵌套屬性
設置和獲取屬性是通過使用setPropertyValue、setPropertyValues、getPropertyValue和getPropertyValues方法來完成的,這些方法帶有兩個重載的變體。springjavadoc更詳細地描述了它們。JavaBeans規範有指示對象屬性的約定。下表顯示了這些慣例的一些例子:

Expression Explanation

name

指示與getName()或isName()和setName(..)方法相對應的屬性名。

account.name

指示與(例如)getAccount(). setname()或getAccount(). getname()方法相對應的屬性帳戶的嵌套屬性名。

account[2]

指示索引屬性帳戶的第三個元素。索引屬性的類型可以是數組、列表或其他自然有序的集合。

account[COMPANYNAME]

指示由account map屬性的COMPANYNAME鍵索引的映射條目的值。

(如果您不打算直接使用BeanWrapper,那麼下一節對您來說就不是那麼重要了。如果您只使用DataBinder和BeanFactory及其默認實現,那麼應該跳到PropertyEditors一節。)
下面兩個示例類使用BeanWrapper來獲取和設置屬性:

public class Company {

    private String name;
    private Employee managingDirector;

    public String getName() {
        return this.name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Employee getManagingDirector() {
        return this.managingDirector;
    }

    public void setManagingDirector(Employee managingDirector) {
        this.managingDirector = managingDirector;
    }
}
public class Employee {

    private String name;

    private float salary;

    public String getName() {
        return this.name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public float getSalary() {
        return salary;
    }

    public void setSalary(float salary) {
        this.salary = salary;
    }
}

下面的代碼片段展示瞭如何檢索和操作實例化的公司和僱員的一些屬性的一些示例:

BeanWrapper company = new BeanWrapperImpl(new Company());
// setting the company name..
company.setPropertyValue("name", "Some Company Inc.");
// ... can also be done like this:
PropertyValue value = new PropertyValue("name", "Some Company Inc.");
company.setPropertyValue(value);

// ok, let's create the director and tie it to the company:
BeanWrapper jim = new BeanWrapperImpl(new Employee());
jim.setPropertyValue("name", "Jim Stravinsky");
company.setPropertyValue("managingDirector", jim.getWrappedInstance());

// retrieving the salary of the managingDirector through the company
Float salary = (Float) company.getPropertyValue("managingDirector.salary");

===內置的PropertyEditor實現
Spring使用PropertyEditor的概念來實現對象和字符串之間的轉換。用不同於對象本身的方式表示屬性是很方便的。例如,日期可以用人類可讀的方式表示(字符串:'2007-14-09'),而我們仍然可以將人類可讀的形式轉換回原始日期(或者,更好的方法是將以人類可讀形式輸入的任何日期轉換回日期對象)。可以通過註冊java.bean . propertyeditor類型的自定義編輯器來實現此行爲。在BeanWrapper或特定的IoC容器中註冊自定義編輯器(如前一章所提到的),可以讓它知道如何將屬性轉換成所需的類型。有關PropertyEditor的更多信息,請參見the javadoc of the。來自package from Oracle.。

在Spring中使用屬性編輯的幾個例子:

  • 通過使用PropertyEditor實現來設置bean上的屬性。當您使用String作爲在XML文件中聲明的某個bean的屬性值時,Spring(如果相應屬性的setter有一個類參數)使用ClassEditor嘗試將該參數解析爲一個類對象。
  • 在Spring的MVC框架中解析HTTP請求參數是通過使用各種PropertyEditor實現來完成的,您可以在CommandController的所有子類中手動綁定這些實現。

Spring有許多內建的PropertyEditor實現,使工作變得簡單。它們都位於org.springframe .bean中。propertyeditors包。大多數(但不是所有,如下表所示)在缺省情況下由BeanWrapperImpl註冊。如果屬性編輯器以某種方式可配置,您仍然可以註冊自己的變體來覆蓋默認的變體。下表描述了Spring提供的各種PropertyEditor實現:

Class Explanation

ByteArrayPropertyEditor

字節數組的編輯器。將字符串轉換爲相應的字節表示形式。默認由BeanWrapperImpl註冊。

ClassEditor

將表示類的字符串解析爲實際類,反之亦然。當沒有找到類時,拋出IllegalArgumentException。默認情況下,由BeanWrapperImpl註冊。

CustomBooleanEditor

布爾屬性的可自定義屬性編輯器。默認情況下,由BeanWrapperImpl註冊,但是可以通過將它的自定義實例註冊爲自定義編輯器來覆蓋它。

CustomCollectionEditor

屬性編輯器,將任何源集合轉換爲給定的目標集合類型。

CustomDateEditor

爲java.util定製的屬性編輯器。支持自定義日期格式。默認未註冊。必須根據需要以適當的格式進行用戶註冊。

CustomNumberEditor

可自定義的屬性編輯器的任何數字子類,如整數,長,浮點數,或雙。默認情況下,由BeanWrapperImpl註冊,但是可以通過將它的自定義實例註冊爲自定義編輯器來覆蓋它。

FileEditor

將字符串解析爲java.io.File對象。默認情況下,由BeanWrapperImpl註冊。

InputStreamEditor

單向屬性編輯器,它可以獲取一個字符串並(通過中間的ResourceEditor和Resource)生成一個InputStream,以便可以直接將InputStream屬性設置爲字符串。注意,默認用法不會爲您關閉InputStream。默認情況下,由BeanWrapperImpl註冊。

LocaleEditor

可以將字符串解析爲Locale對象,反之亦然(字符串格式是[country][變體],與Locale的toString()方法相同)。默認情況下,由BeanWrapperImpl註冊。

PatternEditor

可以將字符串解析爲java.util.regex。模式對象,反之亦然。

PropertiesEditor

可以轉換字符串(使用java.util的javadoc中定義的格式進行格式化)。屬性類)到屬性對象。默認情況下,由BeanWrapperImpl註冊。

StringTrimmerEditor

編輯字符串的屬性編輯器。可選地允許將空字符串轉換爲空值。默認未註冊-必須是用戶註冊的。

URLEditor

可以將URL的字符串表示形式解析爲實際的URL對象。默認情況下,由BeanWrapperImpl註冊。

Spring使用java.bean。PropertyEditorManager爲可能需要的屬性編輯器設置搜索路徑。搜索路徑還包括sun.bean。編輯器,它包括PropertyEditor實現的類型,如字體,顏色,和大多數的基本類型。還要注意的是,標準JavaBeans基礎結構會自動發現PropertyEditor類(不需要顯式地註冊它們),前提是這些類與它們處理的類在同一個包中,並且具有與該類相同的名稱,並且附加了編輯器。例如,可以有以下類和包結構,這足以使SomethingEditor類被識別並用作某種類型屬性的PropertyEditor。

com
  chank
    pop
      Something
      SomethingEditor // the PropertyEditor for the Something class

請注意,您也可以在這裏使用標準的BeanInfo JavaBeans機制(在一定程度上在這裏進行了描述)。下面的例子使用BeanInfo機制顯式註冊一個或多個PropertyEditor實例與相關類的屬性:

com
  chank
    pop
      Something
      SomethingBeanInfo // the BeanInfo for the Something class

下面是引用的SomethingBeanInfo類的Java源代碼,它將CustomNumberEditor與Something類的age屬性相關聯:

public class SomethingBeanInfo extends SimpleBeanInfo {

    public PropertyDescriptor[] getPropertyDescriptors() {
        try {
            final PropertyEditor numberPE = new CustomNumberEditor(Integer.class, true);
            PropertyDescriptor ageDescriptor = new PropertyDescriptor("age", Something.class) {
                public PropertyEditor createPropertyEditor(Object bean) {
                    return numberPE;
                };
            };
            return new PropertyDescriptor[] { ageDescriptor };
        }
        catch (IntrospectionException ex) {
            throw new Error(ex.toString());
        }
    }
}

===註冊額外的自定義PropertyEditor實現
當將bean屬性設置爲字符串值時,Spring IoC容器最終使用標準JavaBeans PropertyEditor實現將這些字符串轉換爲屬性的複雜類型。Spring預先註冊了許多定製的PropertyEditor實現(例如,將表示爲字符串的類名轉換爲類對象)。另外,Java的標準JavaBeans PropertyEditor查找機制允許對類的PropertyEditor進行適當的命名,並將其放置在與其提供支持的類相同的包中,以便能夠自動找到該類。

如果需要註冊其他自定義的propertyeditor,可以使用幾種機制。最手動的方法(通常不方便或不推薦)是使用ConfigurableBeanFactory接口的registerCustomEditor()方法,假設您有一個BeanFactory引用。另一種(稍微方便一些)機制是使用一種稱爲CustomEditorConfigurer的特殊bean工廠後處理器。雖然可以使用bean工廠後處理器BeanFactory實現,CustomEditorConfigurer有一個嵌套的屬性設置,所以我們強烈建議您使用它與ApplicationContext,以類似的方式,您可以將其部署到任何其他bean,它可以自動檢測和應用。

注意,所有的bean工廠和應用程序上下文都自動使用許多內置的屬性編輯器,通過它們使用BeanWrapper來處理屬性轉換。前一節列出了BeanWrapper寄存器的標準屬性編輯器。此外,ApplicationContexts還會覆蓋或添加額外的編輯器,以適合特定應用程序上下文類型的方式處理資源查找。

標準JavaBeans PropertyEditor實例用於將表示爲字符串的屬性值轉換爲屬性的實際複雜類型。您可以使用CustomEditorConfigurer(一個bean工廠後處理器)來方便地將對其他PropertyEditor實例的支持添加到ApplicationContext中。
考慮下面的示例,它定義了一個名爲ExoticType的用戶類和另一個名爲DependsOnExoticType的類,後者需要設置ExoticType作爲屬性:

package example;

public class ExoticType {

    private String name;

    public ExoticType(String name) {
        this.name = name;
    }
}

public class DependsOnExoticType {

    private ExoticType type;

    public void setType(ExoticType type) {
        this.type = type;
    }
}

正確設置之後,我們希望能夠將type屬性賦值爲字符串,PropertyEditor將其轉換爲實際的ExoticType實例。下面的bean定義說明了如何建立這種關係:

<bean id="sample" class="example.DependsOnExoticType">
    <property name="type" value="aNameForExoticType"/>
</bean>

PropertyEditor實現可能與以下類似:

// converts string representation to ExoticType object
package example;

public class ExoticTypeEditor extends PropertyEditorSupport {

    public void setAsText(String text) {
        setValue(new ExoticType(text.toUpperCase()));
    }
}

最後,下面的例子展示瞭如何使用CustomEditorConfigurer在ApplicationContext中註冊新的PropertyEditor,這樣就可以根據需要使用它了:

<bean class="org.springframework.beans.factory.config.CustomEditorConfigurer">
    <property name="customEditors">
        <map>
            <entry key="example.ExoticType" value="example.ExoticTypeEditor"/>
        </map>
    </property>
</bean>

= = = = =使用PropertyEditorRegistrar
向Spring容器註冊屬性編輯器的另一種機制是創建和使用PropertyEditorRegistrar。當您需要在幾種不同的情況下使用同一組屬性編輯器時,此接口特別有用。您可以編寫一個相應的註冊商,並在每種情況下重用它。PropertyEditorRegistrar實例與稱爲PropertyEditorRegistry的接口一起工作,該接口由Spring BeanWrapper(和DataBinder)實現。propertyeditorregistrars實例與CustomEditorConfigurer(這裏描述的)一起使用時特別方便,後者公開了一個名爲setPropertyEditorRegistrars(..)的屬性。以這種方式添加到CustomEditorConfigurer中的PropertyEditorRegistrar實例可以很容易地與DataBinder和Spring MVC控制器共享。此外,它還避免了在定製編輯器上進行同步的需要:PropertyEditorRegistrar需要爲每個bean創建嘗試創建新的PropertyEditor實例。

下面的例子展示瞭如何創建自己的PropertyEditorRegistrar實現:

package com.foo.editors.spring;

public final class CustomPropertyEditorRegistrar implements PropertyEditorRegistrar {

    public void registerCustomEditors(PropertyEditorRegistry registry) {

        // it is expected that new PropertyEditor instances are created
        registry.registerCustomEditor(ExoticType.class, new ExoticTypeEditor());

        // you could register as many custom property editors as are required here...
    }
}

參見org.springframe .bean .support.ResourceEditorRegistrar,作爲PropertyEditorRegistrar實現的示例。注意,在registercustomeditor(..)方法的實現中,它如何創建每個屬性編輯器的新實例。
下一個例子展示瞭如何配置CustomEditorConfigurer並將我們的CustomPropertyEditorRegistrar實例注入其中:

<bean class="org.springframework.beans.factory.config.CustomEditorConfigurer">
    <property name="propertyEditorRegistrars">
        <list>
            <ref bean="customPropertyEditorRegistrar"/>
        </list>
    </property>
</bean>

<bean id="customPropertyEditorRegistrar"
    class="com.foo.editors.spring.CustomPropertyEditorRegistrar"/>

最後(對於使用Spring的MVC web框架的讀者來說,這與本章的重點略有不同),將PropertyEditorRegistrars與數據綁定控制器(如SimpleFormController)結合使用非常方便。下面的例子在initBinder(..)方法的實現中使用了PropertyEditorRegistrar:

public final class RegisterUserController extends SimpleFormController {

    private final PropertyEditorRegistrar customPropertyEditorRegistrar;

    public RegisterUserController(PropertyEditorRegistrar propertyEditorRegistrar) {
        this.customPropertyEditorRegistrar = propertyEditorRegistrar;
    }

    protected void initBinder(HttpServletRequest request,
            ServletRequestDataBinder binder) throws Exception {
        this.customPropertyEditorRegistrar.registerCustomEditors(binder);
    }

    // other methods to do with registering a User
}

這種風格的PropertyEditor註冊可以產生簡潔的代碼(initBinder(..)的實現只有一行),並讓通用的PropertyEditor註冊代碼封裝在一個類中,然後根據需要在儘可能多的控制器之間共享。

== Spring類型轉換
Spring 3引入了一個核心。提供通用類型轉換系統的轉換包。系統定義了一個SPI來實現類型轉換邏輯和一個API來在運行時執行類型轉換。在Spring容器中,可以使用這個系統替代PropertyEditor實現,將外部化bean屬性值字符串轉換爲所需的屬性類型。您還可以在應用程序中任何需要類型轉換的地方使用公共API。

= = =轉換器SPI
實現類型轉換邏輯的SPI是簡單且強類型的,如下面的接口定義所示:

package org.springframework.core.convert.converter;

public interface Converter<S, T> {

    T convert(S source);
}

要創建自己的轉換器,請實現轉換器接口並將S作爲要轉換的類型參數化,將T作爲要轉換的類型參數化。如果需要將S的集合或數組轉換爲T的數組或集合,還可以透明地應用這樣的轉換器,前提是還註冊了委託數組或集合轉換器(默認情況下DefaultConversionService註冊了該轉換器)。

對於每個要轉換的調用,源參數都保證不爲空。如果轉換失敗,您的轉換器可能會拋出任何未檢查的異常。具體來說,它應該拋出一個IllegalArgumentException來報告一個無效的源值。注意確保轉換器實現是線程安全的。

在core.convert.support中提供了幾個轉換器實現。這包括從字符串到數字和其他常見類型的轉換器。下面的清單顯示了StringToInteger類,它是一個典型的轉換器實現:

package org.springframework.core.convert.support;

final class StringToInteger implements Converter<String, Integer> {

    public Integer convert(String source) {
        return Integer.valueOf(source);
    }
}

= = =使用ConverterFactory
當需要集中整個類層次結構的轉換邏輯時(例如,從字符串轉換爲Enum對象時),可以實現ConverterFactory,如下面的示例所示:

package org.springframework.core.convert.converter;

public interface ConverterFactory<S, R> {

    <T extends R> Converter<S, T> getConverter(Class<T> targetType);
}

參數化S爲您要轉換的類型,R爲定義您可以轉換爲的類範圍的基類型。然後實現getConverter(類<T>),其中T是R的子類。
以StringToEnumConverterFactory爲例:

package org.springframework.core.convert.support;

final class StringToEnumConverterFactory implements ConverterFactory<String, Enum> {

    public <T extends Enum> Converter<String, T> getConverter(Class<T> targetType) {
        return new StringToEnumConverter(targetType);
    }

    private final class StringToEnumConverter<T extends Enum> implements Converter<String, T> {

        private Class<T> enumType;

        public StringToEnumConverter(Class<T> enumType) {
            this.enumType = enumType;
        }

        public T convert(String source) {
            return (T) Enum.valueOf(this.enumType, source.trim());
        }
    }
}

= = =使用GenericConverter
當您需要複雜的轉換器實現時,可以考慮使用GenericConverter接口。與轉換器相比,GenericConverter具有更靈活但類型更弱的簽名,它支持在多個源類型和目標類型之間進行轉換。此外,GenericConverter提供了可用的源和目標字段上下文,您可以在實現轉換邏輯時使用它們。這樣的上下文允許類型轉換由字段註釋或字段簽名上聲明的通用信息驅動。下面的清單顯示了GenericConverter的接口定義:

package org.springframework.core.convert.converter;

public interface GenericConverter {

    public Set<ConvertiblePair> getConvertibleTypes();

    Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType);
}

要實現一個GenericConverter,讓getConvertibleTypes()返回支持的源→目標類型對。然後實現轉換(對象、類型描述符、類型描述符)來包含轉換邏輯。源類型描述符提供對保存轉換值的源字段的訪問。目標類型描述符提供對要設置轉換值的目標字段的訪問。

GenericConverter的一個很好的例子是在Java數組和集合之間進行轉換的轉換器。這樣的ArrayToCollectionConverter內省聲明目標集合類型的字段,以解析集合的元素類型。這使得源數組中的每個元素都可以在目標字段上設置集合之前轉換爲集合元素類型。

注意:

因爲GenericConverter是一個更復雜的SPI接口,所以應該只在需要的時候使用它。支持轉換器或轉換器工廠的基本類型轉換需求。

= = = =使用ConditionalGenericConverter
有時,您希望轉換器僅在特定條件爲真時才運行。例如,您可能希望僅在目標字段上出現特定註釋時才運行轉換器,或者僅在目標類上定義特定方法(例如靜態valueOf方法)時才運行轉換器。ConditionalGenericConverter是聯合的GenericConverter和條件轉換器接口,讓您定義這樣的自定義匹配標準:

public interface ConditionalConverter {

    boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType);
}

public interface ConditionalGenericConverter extends GenericConverter, ConditionalConverter {
}

ConditionalGenericConverter的一個很好的例子是EntityConverter,它在持久實體標識符和實體引用之間進行轉換。只有在目標實體類型聲明瞭靜態查找器方法(例如,findAccount(Long))時,這樣的EntityConverter纔可能匹配。您可以執行這樣的查找器方法來檢查匹配的實現(TypeDescriptor, TypeDescriptor)。

===轉換服務API
ConversionService定義了一個統一的API,用於在運行時執行類型轉換邏輯。轉換器通常在以下facade接口之後執行:

package org.springframework.core.convert;

public interface ConversionService {

    boolean canConvert(Class<?> sourceType, Class<?> targetType);

    <T> T convert(Object source, Class<T> targetType);

    boolean canConvert(TypeDescriptor sourceType, TypeDescriptor targetType);

    Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType);

}

大多數ConversionService實現還實現了ConverterRegistry,它爲註冊轉換器提供了SPI。在內部,ConversionService實現委託給它註冊的轉換器來執行類型轉換邏輯。

在core.convert.support包中提供了一個健壯的ConversionService實現。GenericConversionService是適合在大多數環境中使用的通用實現。ConversionServiceFactory爲創建公共的ConversionService配置提供了一個方便的工廠。

===配置一個ConversionService
ConversionService是一個無狀態對象,設計用於在應用程序啓動時實例化,然後在多個線程之間共享。在Spring應用程序中,通常爲每個Spring容器(或ApplicationContext)配置一個ConversionService實例。Spring獲取這個轉換服務,並在框架需要執行類型轉換時使用它。您還可以將這個轉換服務注入到任何bean中並直接調用它。

注意:如果沒有向Spring註冊轉換服務,則使用原始的基於propertyeditor的系統。

要向Spring註冊一個默認的ConversionService,添加以下帶有ConversionService id的bean定義:

<bean id="conversionService"
    class="org.springframework.context.support.ConversionServiceFactoryBean"/>

默認的轉換服務可以在字符串、數字、枚舉、集合、映射和其他常見類型之間進行轉換。若要使用自定義轉換器補充或覆蓋默認轉換器,請設置轉換器屬性。屬性值可以實現任何轉換器、轉換器工廠或GenericConverter接口。

<bean id="conversionService"
        class="org.springframework.context.support.ConversionServiceFactoryBean">
    <property name="converters">
        <set>
            <bean class="example.MyCustomConverter"/>
        </set>
    </property>
</bean>

在Spring MVC應用程序中使用ConversionService也很常見。請參閱Spring MVC章節中的 Conversion and Formatting
在某些情況下,您可能希望在轉換期間應用格式。有關使用FormattingConversionServiceFactoryBean的詳細信息,請參閱[format-FormatterRegistry-SPI]

===以編程方式使用ConversionService
要以編程方式處理ConversionService實例,可以像處理任何其他bean一樣將引用注入到它。下面的例子演示瞭如何做到這一點:

@Service
public class MyService {

    public MyService(ConversionService conversionService) {
        this.conversionService = conversionService;
    }

    public void doIt() {
        this.conversionService.convert(...)
    }
}

對於大多數用例,您可以使用指定targetType的convert方法,但是它不能用於更復雜的類型,比如參數化元素的集合。例如,如果希望以編程方式將整數列表轉換爲字符串列表,則需要提供源類型和目標類型的正式定義。
幸運的是,TypeDescriptor提供了各種簡單的選項,如下面的例子所示:

DefaultConversionService cs = new DefaultConversionService();

List<Integer> input = ...
cs.convert(input,
    TypeDescriptor.forObject(input), // List<Integer> type descriptor
    TypeDescriptor.collection(List.class, TypeDescriptor.valueOf(String.class)));

注意,DefaultConversionService自動註冊適用於大多數環境的轉換器。這包括集合轉換器、標量轉換器和基本的對象到字符串轉換器。通過在DefaultConversionService類上使用靜態addDefaultConverters方法,可以將相同的轉換器註冊到任何ConverterRegistry。
值類型的轉換器被數組和集合重用,因此不需要創建特定的轉換器來將S集合轉換爲T集合(假設標準集合處理是適當的)。

== Spring字段格式化
如前一節所述,core.convert是一種通用類型轉換系統。它提供了一個統一的ConversionService API和一個強類型轉換器SPI,用於實現從一種類型到另一種類型的轉換邏輯。Spring容器使用這個系統來綁定bean屬性值。此外,Spring表達式語言(SpEL)和DataBinder都使用這個系統來綁定字段值。例如,當SpEL需要強制一個短到一個長來完成一個表達式時。setValue(對象bean,對象值)嘗試,core.convert系統執行強制轉換。

現在考慮典型客戶機環境(如web或桌面應用程序)的類型轉換需求。在這種環境中,通常要將字符串轉換爲支持客戶端回發流程,也要將字符串轉換爲支持視圖呈現流程。此外,您經常需要本地化字符串值。更一般的核心。轉換轉換器SPI不直接處理這種格式要求。爲了直接解決這些問題,Spring 3引入了一個方便的格式化程序SPI,它爲客戶機環境提供了PropertyEditor實現的簡單而健壯的替代方案。

通常,在需要實現通用類型轉換邏輯時,可以使用轉換器SPI——例如,在java.util之間進行轉換。約會很長。當您在客戶端環境(例如web應用程序)中工作並需要解析和打印本地化的字段值時,可以使用格式化程序SPI。ConversionService爲這兩個spi提供了統一的類型轉換API。

===格式化程序SPI
用於實現字段格式化邏輯的格式化程序SPI很簡單,而且是強類型的。下面的清單顯示了Formatter接口定義:

package org.springframework.format;

public interface Formatter<T> extends Printer<T>, Parser<T> {
}

Formatter是從Printer和Parser構建塊接口擴展而來的。下面的清單顯示了這兩個接口的定義:

public interface Printer<T> {

    String print(T fieldValue, Locale locale);
}
import java.text.ParseException;

public interface Parser<T> {

    T parse(String clientValue, Locale locale) throws ParseException;
}

要創建自己的Formatter,請實現前面顯示的Formatter接口。將T參數化爲希望格式化的對象類型——例如,java.util.Date。實現print()操作來打印一個T實例,以便在客戶端區域設置中顯示。實現parse()操作,從客戶端語言環境返回的格式化表示中解析T的實例。如果解析嘗試失敗,格式化程序應該拋出ParseException或IllegalArgumentException。請注意確保格式化程序實現是線程安全的。

爲了方便起見,format子包提供了幾個Formatter實現。number包提供NumberStyleFormatter、CurrencyStyleFormatter和PercentStyleFormatter來格式化使用java.text.NumberFormat的數字對象。datetime包提供了一個DateFormatter來格式化java.util。使用java.text.DateFormat的日期對象。datetime。joda包提供了基於joda - time庫的全面的datetime格式化支持。

下面的DateFormatter是一個示例Formatter實現:

package org.springframework.format.datetime;

public final class DateFormatter implements Formatter<Date> {

    private String pattern;

    public DateFormatter(String pattern) {
        this.pattern = pattern;
    }

    public String print(Date date, Locale locale) {
        if (date == null) {
            return "";
        }
        return getDateFormat(locale).format(date);
    }

    public Date parse(String formatted, Locale locale) throws ParseException {
        if (formatted.length() == 0) {
            return null;
        }
        return getDateFormat(locale).parse(formatted);
    }

    protected DateFormat getDateFormat(Locale locale) {
        DateFormat dateFormat = new SimpleDateFormat(this.pattern, locale);
        dateFormat.setLenient(false);
        return dateFormat;
    }
}

Spring團隊歡迎社區驅動的格式化程序貢獻。請參見 GitHub Issues

= = =註解驅動的格式
字段格式可以通過字段類型或註釋進行配置。要將註釋綁定到格式化程序,請實現AnnotationFormatterFactory。下面的清單顯示了AnnotationFormatterFactory接口的定義:

package org.springframework.format;

public interface AnnotationFormatterFactory<A extends Annotation> {

    Set<Class<?>> getFieldTypes();

    Printer<?> getPrinter(A annotation, Class<?> fieldType);

    Parser<?> getParser(A annotation, Class<?> fieldType);
}

要創建實現:。參數化A爲您希望與之關聯格式邏輯的字段註釋類型——例如org.springframework.format.annotation.DateTimeFormat..。讓getFieldTypes()返回可以使用註釋的字段的類型。讓getPrinter()返回一個打印機來打印一個帶註釋字段的值。讓getParser()返回一個解析器來解析帶註釋字段的clientValue。

下面的示例AnnotationFormatterFactory實現將@NumberFormat註釋綁定到一個格式化程序,以便指定數字樣式或模式:

public final class NumberFormatAnnotationFormatterFactory
        implements AnnotationFormatterFactory<NumberFormat> {

    public Set<Class<?>> getFieldTypes() {
        return new HashSet<Class<?>>(asList(new Class<?>[] {
            Short.class, Integer.class, Long.class, Float.class,
            Double.class, BigDecimal.class, BigInteger.class }));
    }

    public Printer<Number> getPrinter(NumberFormat annotation, Class<?> fieldType) {
        return configureFormatterFrom(annotation, fieldType);
    }

    public Parser<Number> getParser(NumberFormat annotation, Class<?> fieldType) {
        return configureFormatterFrom(annotation, fieldType);
    }

    private Formatter<Number> configureFormatterFrom(NumberFormat annotation, Class<?> fieldType) {
        if (!annotation.pattern().isEmpty()) {
            return new NumberStyleFormatter(annotation.pattern());
        } else {
            Style style = annotation.style();
            if (style == Style.PERCENT) {
                return new PercentStyleFormatter();
            } else if (style == Style.CURRENCY) {
                return new CurrencyStyleFormatter();
            } else {
                return new NumberStyleFormatter();
            }
        }
    }
}

要觸發格式化,可以使用@NumberFormat註釋字段,如下面的示例所示:

public class MyModel {

    @NumberFormat(style=Style.CURRENCY)
    private BigDecimal decimal;
}

===格式註釋API
在org.springframe .format中存在一個可移植的格式註釋API。註釋包。您可以使用@NumberFormat來格式化數字字段(如Double和Long),使用@DateTimeFormat來格式化java.util。日期、java.util。日曆,長(毫秒時間戳)以及JSR-310 java。時間和Joda-Time值類型。

下面的示例使用@DateTimeFormat來格式化java.util.Date作爲ISO日期(yyyy-MM-dd):

public class MyModel {

    @DateTimeFormat(iso=ISO.DATE)
    private Date date;
}

=== FormatterRegistry SPI
FormatterRegistry是一個用於註冊格式化程序和轉換器的SPI。FormattingConversionService是FormatterRegistry的一個實現,適用於大多數環境。您可以通過編程或聲明的方式將此變體配置爲Spring bean,例如通過使用FormattingConversionServiceFactoryBean。因爲這個實現也實現了ConversionService,所以您可以直接將其配置爲與Spring的DataBinder和Spring Expression Language (SpEL)一起使用。

下面的清單顯示了FormatterRegistry SPI:

package org.springframework.format;

public interface FormatterRegistry extends ConverterRegistry {

    void addFormatterForFieldType(Class<?> fieldType, Printer<?> printer, Parser<?> parser);

    void addFormatterForFieldType(Class<?> fieldType, Formatter<?> formatter);

    void addFormatterForFieldType(Formatter<?> formatter);

    void addFormatterForAnnotation(AnnotationFormatterFactory<?> factory);
}

如前一個清單所示,可以通過字段類型或註釋註冊格式化程序。
FormatterRegistry SPI允許您集中配置格式化規則,而不是在控制器之間複製這樣的配置。例如,您可能希望強制所有日期字段都以某種方式格式化,或者強制具有特定註釋的字段以某種方式格式化。使用共享的FormatterRegistry,只需定義一次這些規則,並且在需要格式化時應用它們。

=== FormatterRegistrar SPI
FormatterRegistrar是一個SPI,用於通過FormatterRegistry註冊格式器和轉換器。下面的清單顯示了它的接口定義:

package org.springframework.format;

public interface FormatterRegistrar {

    void registerFormatters(FormatterRegistry registry);
}

在爲給定格式類別(如日期格式)註冊多個相關轉換器和格式器時,FormatterRegistrar非常有用。它在聲明性註冊不足的情況下也很有用——例如,當格式化程序需要在不同於其自身的<T>的特定字段類型下建立索引時,或者在註冊打印機/解析器對時。下一節提供有關轉換器和格式化程序註冊的更多信息。

===在Spring MVC中配置格式
請參閱Spring MVC章節中的轉換和格式化 Conversion and Formatting
==配置全局日期和時間格式
默認情況下,未使用@DateTimeFormat註釋的日期和時間字段通過使用DateFormat從字符串轉換而來。短的風格。如果您願意,您可以通過定義自己的全局格式來改變這一點。

爲此,您需要確保Spring不註冊默認格式器。相反,應該手動註冊所有格式器。使用org.springframework.format.datetime.joda.JodaTimeFormatterRegistrar或 org.springframework.format.datetime.DateFormatterRegistrar類。取決於是否使用Joda-Time庫。
例如,下面的Java配置註冊了一個全局yyyyMMdd格式(這個例子不依賴於Joda-Time庫):

@Configuration
public class AppConfig {

    @Bean
    public FormattingConversionService conversionService() {

        // Use the DefaultFormattingConversionService but do not register defaults
        DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService(false);

        // Ensure @NumberFormat is still supported
        conversionService.addFormatterForFieldAnnotation(new NumberFormatAnnotationFormatterFactory());

        // Register date conversion with a specific global format
        DateFormatterRegistrar registrar = new DateFormatterRegistrar();
        registrar.setFormatter(new DateFormatter("yyyyMMdd"));
        registrar.registerFormatters(conversionService);

        return conversionService;
    }
}

如果喜歡基於xml的配置,可以使用FormattingConversionServiceFactoryBean。下面的例子展示瞭如何做到這一點(這次使用Joda時間):

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    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>

    <bean id="conversionService" class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
        <property name="registerDefaultFormatters" value="false" />
        <property name="formatters">
            <set>
                <bean class="org.springframework.format.number.NumberFormatAnnotationFormatterFactory" />
            </set>
        </property>
        <property name="formatterRegistrars">
            <set>
                <bean class="org.springframework.format.datetime.joda.JodaTimeFormatterRegistrar">
                    <property name="dateFormatter">
                        <bean class="org.springframework.format.datetime.joda.DateTimeFormatterFactoryBean">
                            <property name="pattern" value="yyyyMMdd"/>
                        </bean>
                    </property>
                </bean>
            </set>
        </property>
    </bean>
</beans>

注意:Joda-Time提供不同的類型來表示日期、時間和日期-時間值。應該使用JodaTimeFormatterRegistrar的dateFormatter、timeFormatter和dateTimeFormatter屬性爲每種類型配置不同的格式。DateTimeFormatterFactoryBean提供了一種創建格式化程序的方便方法。

如果您使用Spring MVC,請記住顯式配置所使用的轉換服務。對於基於java的@Configuration,這意味着擴展WebMvcConfigurationSupport類並覆蓋mvcConversionService()方法。對於XML,應該使用mvc的轉換服務屬性:註釋驅動元素。有關詳細信息,請參見轉換和格式化。

= =Spring Validation
Spring 3對其驗證支持進行了幾個增強。首先,完全支持JSR-303 Bean驗證API。其次,當以編程方式使用時,Spring的DataBinder可以驗證對象並綁定到它們。第三,Spring MVC支持聲明式驗證@Controller輸入。

=== JSR-303 Bean驗證API概述
JSR-303對Java平臺的驗證約束聲明和元數據進行了標準化。通過使用此API,您可以使用聲明性驗證約束來註釋域模型屬性,並由運行時強制執行它們。您可以使用許多內置的約束。您還可以定義自己的自定義約束。

考慮下面的例子,它顯示了一個簡單的帶有兩個屬性的PersonForm模型:

public class PersonForm {
    private String name;
    private int age;
}

JSR-303允許您針對這些屬性定義聲明性驗證約束,如下面的示例所示:

public class PersonForm {

    @NotNull
    @Size(max=64)
    private String name;

    @Min(0)
    private int age;
}

當JSR-303驗證器驗證該類的實例時,將強制執行這些約束。
有關JSR-303和JSR-349的一般信息,請參閱 Bean Validation website。有關默認參考實現的特定功能的信息,請參閱 Hibernate Validator文檔。要了解如何將bean驗證提供者設置爲Spring bean,請繼續閱讀。

===配置Bean驗證提供程序
Spring爲Bean驗證API提供了完整的支持。這包括將JSR-303或JSR-349 Bean驗證提供者作爲Spring Bean進行引導的方便支持。這允許您注入javax.validation。ValidatorFactory或javax.validation。在應用程序中需要驗證的地方使用驗證器。

可以使用LocalValidatorFactoryBean將默認驗證器配置爲Spring bean,如下面的示例所示:

<bean id="validator"
    class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean"/>

前面示例中的基本配置通過使用bean的默認引導機制來觸發bean驗證初始化。JSR-303或JSR-349提供程序(如Hibernate驗證程序)將出現在類路徑中,並被自動檢測到。

===注入驗證器
LocalValidatorFactoryBean實現了兩個javax.validation.ValidatorFactory 和javax.validation.Validator。以及Spring的org.springframework.validation.Validator。您可以將對這兩個接口的引用注入到需要調用驗證邏輯的bean中。

您可以將引用注入到javax.validation.Validator。如果你喜歡直接使用Bean驗證API,如下面的例子所示:

import javax.validation.Validator;

@Service
public class MyService {

    @Autowired
    private Validator validator;
}

您可以將引用注入到org.springframework.validation.Validator。如果你的bean需要Spring驗證API,如下面的例子所示:

import org.springframework.validation.Validator;

@Service
public class MyService {

    @Autowired
    private Validator validator;
}

===配置自定義約束
每個bean驗證約束由兩部分組成:*一個@Constraint註釋聲明約束及其可配置屬性。一個javax.validation的實現。實現約束行爲的ConstraintValidator接口。

要將聲明與實現關聯起來,每個@Constraint註釋引用一個對應的ConstraintValidator實現類。在運行時,ConstraintValidatorFactory在域模型中遇到約束註釋時實例化引用的實現。

默認情況下,LocalValidatorFactoryBean配置一個SpringConstraintValidatorFactory,它使用Spring創建ConstraintValidator實例。這讓您的定製constraintvalidator像其他任何Spring bean一樣從依賴項注入中受益。
下面的例子顯示了一個自定義的@Constraint聲明和一個關聯的ConstraintValidator實現,該實現使用Spring進行依賴注入:

@Target({ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy=MyConstraintValidator.class)
public @interface MyConstraint {
}
mport javax.validation.ConstraintValidator;

public class MyConstraintValidator implements ConstraintValidator {

    @Autowired;
    private Foo aDependency;

    // ...
}

如上例所示,ConstraintValidator實現的依賴項@Autowired與任何其他Spring bean一樣。
==== spring驅動的方法驗證
您可以通過MethodValidationPostProcessor Bean定義將Bean validation 1.1支持的方法驗證特性(作爲自定義擴展,也由Hibernate Validator 4.3支持)集成到Spring上下文中,如下所示:

<bean class="org.springframework.validation.beanvalidation.MethodValidationPostProcessor"/>

要獲得Spring驅動的方法驗證,所有目標類都需要使用Spring的@Validated annotation進行註釋。(還可以聲明要使用的驗證組。)有關Hibernate驗證器和Bean驗證1.1提供程序的設置細節,請參閱MethodValidationPostProcessor javadoc。

===其他配置選項
默認的LocalValidatorFactoryBean配置可以滿足大多數情況。從消息插值到遍歷解析,各種Bean驗證構造有許多配置選項。有關這些選項的更多信息,請參見LocalValidatorFactoryBean javadoc。

===配置數據庫
從Spring 3開始,您可以使用驗證器配置DataBinder實例。配置好之後,您可以通過調用bind .validate()來調用驗證器。任何驗證錯誤都會自動添加到綁定器的BindingResult中。
下面的示例演示如何在綁定到目標對象後以編程方式使用DataBinder來調用驗證邏輯:

Foo target = new Foo();
DataBinder binder = new DataBinder(target);
binder.setValidator(new FooValidator());

// bind to the target object
binder.bind(propertyValues);

// validate the target object
binder.validate();

// get BindingResult that includes any validation errors
BindingResult results = binder.getBindingResult();

您還可以通過DataBinder配置帶有多個驗證器實例的DataBinder。addValidators dataBinder.replaceValidators。這在將全局配置的bean驗證與在DataBinder實例上本地配置的Spring驗證器組合時非常有用。看到[validation-mvc-configuring]

=== Spring MVC 3驗證
請參閱Spring MVC章節中的 Validation
= Spring表達式語言(SpEL)
Spring表達式語言(簡稱SpEL)是一種強大的表達式語言,它支持在運行時查詢和操作對象圖。語言語法類似於Unified EL,但提供了其他特性,最顯著的是方法調用和基本的字符串模板功能。

= Spring表達式語言(SpEL)
Spring表達式語言(簡稱SpEL)是一種強大的表達式語言,它支持在運行時查詢和操作對象圖。語言語法類似於Unified EL,但提供了其他特性,最顯著的是方法調用和基本的字符串模板功能。

儘管還有其他幾種可用的Java表達式語言—OGNL、MVEL和JBoss EL,僅舉幾例—Spring表達式語言的創建是爲了向Spring社區提供一種得到良好支持的表達式語言,這種語言可以跨Spring產品組合中的所有產品使用。它的語言特性是由Spring項目組合中的需求驅動的,包括在基於eclipse的Spring工具套件中對代碼完成支持的工具需求。也就是說,SpEL基於一個與技術無關的API,該API允許在需要時集成其他表達式語言實現。

雖然SpEL是Spring portfolio中表達式評估的基礎,但它並不直接與Spring綁定,可以獨立使用。爲了自我包含,本章中的許多示例使用SpEL,就好像它是一種獨立的表達式語言一樣。這需要創建一些引導基礎結構類,比如解析器。大多數Spring用戶不需要處理這種基礎結構,相反,可以只編寫用於計算的表達式字符串。這種典型用法的一個例子是將SpEL集成到創建XML或基於註釋的bean定義中,如用於定義bean定義的表達式支持所示 Expression support for defining bean definitions

本章將介紹表達式語言的特性、API和語法。在一些地方,發明人和社會階層被用作表達評價的目標對象。這些類聲明和用於填充它們的數據列在本章的最後。
表達式語言支持以下功能:

  • 文字表達方式
  • 布爾運算符和關係運算符
  • 正則表達式
  • 類表達式
  • 訪問屬性、數組、列表和映射
  • 方法調用
  • 關係運算符
  • 賦值
  • 調用構造函數
  • Bean的引用
  • 陣列結構
  • 內聯列表
  • 內聯映射
  • 三元運算符
  • 變量
  • 用戶定義函數
  • 收集投影
  • 選擇集合
  • 模板化表達式

= =評估
本節介紹SpEL接口的簡單使用及其表達式語言。完整的語言參考可以在語言參考中找到Language Reference.。
下面的代碼引入了SpEL API來計算字符串表達式Hello World的值。

ExpressionParser parser = new SpelExpressionParser(); Expression exp = parser.parseExpression("'Hello World'"); String message = (String) exp.getValue();

您最有可能使用的SpEL類和接口位於org.springframework.expression包及其子包中,如spel.support。
ExpressionParser接口負責解析表達式字符串。在前面的示例中,表達式字符串是由周圍的單引號表示的字符串文字。Expression接口負責計算前面定義的表達式字符串。當直接調用parser.parseExpression和exp.getValue時可以拋出兩個異常,ParseException和EvaluationException。

SpEL支持各種各樣的特性,比如調用方法、訪問屬性和調用構造函數。
在下面的方法調用示例中,我們對字符串文字調用concat方法:

ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("'Hello World'.concat('!')"); 
String message = (String) exp.getValue();

以下調用JavaBean屬性的示例調用字符串屬性字節:

ExpressionParser parser = new SpelExpressionParser();

// invokes 'getBytes()'
Expression exp = parser.parseExpression("'Hello World'.bytes"); 
byte[] bytes = (byte[]) exp.getValue();

SpEL還通過使用標準的點表示法(比如prop1.prop2.prop3)和相應的屬性值設置來支持嵌套屬性。也可以訪問公共字段。
下面的例子演示瞭如何使用點符號來獲得文字的長度:

ExpressionParser parser = new SpelExpressionParser();

// invokes 'getBytes().length'
Expression exp = parser.parseExpression("'Hello World'.bytes.length"); 
int length = (Integer) exp.getValue();

可以調用字符串的構造函數,而不是使用字符串文字,如下面的例子所示:

ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("new String('hello world').toUpperCase()"); 
String message = exp.getValue(String.class);

注意泛型方法的使用:public <T> T getValue(Class<T> desiredResultType)。使用此方法可以將表達式的值轉換爲所需的結果類型。如果無法將值轉換爲類型T或使用已註冊的類型轉換器進行轉換,則會引發EvaluationException異常。

SpEL更常見的用法是提供針對特定對象實例(稱爲根對象)求值的表達式字符串。下面的示例演示如何從Inventor類的實例檢索name屬性或創建布爾條件:

// Create and set a calendar
GregorianCalendar c = new GregorianCalendar();
c.set(1856, 7, 9);

// The constructor arguments are name, birthday, and nationality.
Inventor tesla = new Inventor("Nikola Tesla", c.getTime(), "Serbian");

ExpressionParser parser = new SpelExpressionParser();

Expression exp = parser.parseExpression("name"); // Parse name as an expression
String name = (String) exp.getValue(tesla);
// name == "Nikola Tesla"

exp = parser.parseExpression("name == 'Nikola Tesla'");
boolean result = exp.getValue(tesla, Boolean.class);
// result == true

= = = EvaluationContext理解
EvaluationContext接口用於在計算表達式時解析屬性、方法或字段,並幫助執行類型轉換。Spring提供了兩種實現。

  • SimpleEvaluationContext:公開SpEL語言的基本特性和配置選項的一個子集,用於不需要完全使用SpEL語言語法的表達式類別,並且應該進行有意義的限制。示例包括但不限於數據綁定表達式和基於屬性的過濾器。
  • StandardEvaluationContext:公開SpEL語言的全部特性和配置選項。您可以使用它來指定默認的根對象,並配置每個可用的與評估相關的策略。

SimpleEvaluationContext只支持SpEL語言語法的一個子集。它排除了Java類型引用、構造函數和bean引用。它還要求您顯式地選擇表達式中對屬性和方法的支持級別。默認情況下,create()靜態工廠方法只允許對屬性進行讀訪問。您還可以獲得一個生成器來配置所需的支持的確切級別,目標是以下一個或多個組合:

  • 只自定義PropertyAccessor(沒有反射)
  • 用於只讀訪問的數據綁定屬性
  • 用於讀寫的數據綁定屬性

= = = =類型轉換
默認情況下,SpEL使用Spring core中可用的轉換服務(org.springframe .core.convert. conversionservice)。此轉換服務提供了許多用於常見轉換的內置轉換器,但也是完全可擴展的,因此您可以添加類型之間的自定義轉換。另外,它是泛型感知的。這意味着,當您在表達式中使用泛型類型時,SpEL嘗試轉換以維護它遇到的任何對象的類型正確性。

這在實踐中意味着什麼?假設使用setValue()來設置List屬性。屬性的類型實際上是List<Boolean>。SpEL認識到,在將列表中的元素放入之前,需要將其轉換爲布爾值。下面的例子演示瞭如何做到這一點:

class Simple {
    public List<Boolean> booleanList = new ArrayList<Boolean>();
}

Simple simple = new Simple();
simple.booleanList.add(true);

EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();

// "false" is passed in here as a String. SpEL and the conversion service
// will recognize that it needs to be a Boolean and convert it accordingly.
parser.parseExpression("booleanList[0]").setValue(context, simple, "false");

// b is false
Boolean b = simple.booleanList.get(0);

= = =解析器配置
可以通過使用解析器配置對象(org.springframe .expression. SpEL . spelparserconfiguration)來配置SpEL表達式解析器。配置對象控制一些表達式組件的行爲。例如,如果在數組或集合中建立索引,並且指定索引處的元素爲null,則可以自動創建該元素。這在使用由屬性引用鏈組成的表達式時非常有用。如果在數組或列表中建立索引並指定超出當前數組或列表大小的索引,則可以自動增大數組或列表以適應該索引。下面的例子演示瞭如何自動增長列表:

class Demo {
    public List<String> list;
}

// Turn on:
// - auto null reference initialization
// - auto collection growing
SpelParserConfiguration config = new SpelParserConfiguration(true,true);

ExpressionParser parser = new SpelExpressionParser(config);

Expression expression = parser.parseExpression("list[3]");

Demo demo = new Demo();

Object o = expression.getValue(demo);

// demo.list will now be a real collection of 4 entries
// Each entry is a new empty String

= = = SpEL編譯
Spring Framework 4.1包含一個基本的表達式編譯器。表達式通常是解釋的,這在評估期間提供了很多動態靈活性,但並沒有提供最佳性能。對於偶爾使用的表達式,這是可以的,但是,當被其他組件(如Spring Integration)使用時,性能可能非常重要,並且不需要真正的動態性。

SpEL編譯器旨在滿足這種需求。在求值期間,編譯器生成一個Java類,它在運行時包含表達式行爲,並使用該類實現更快的表達式求值。由於缺少對表達式的鍵入,編譯器在執行編譯時使用在表達式的解釋計算期間收集的信息。例如,它僅僅從表達式中並不知道屬性引用的類型,但是在第一次解釋求值期間,它會找出它是什麼。當然,如果各種表達式元素的類型隨時間而改變,基於這些派生信息的編譯可能會在以後造成麻煩。因此,編譯最適合那些類型信息在重複計算時不會改變的表達式。

考慮以下基本表達式:

someArray[0].someProperty.someOtherProperty < 0.1

由於前面的表達式涉及數組訪問、一些屬性取消引用和數字操作,因此性能收益可能非常顯著。在一個運行50000次迭代的微基準測試示例中,使用解釋器需要花費75ms來計算,而使用編譯後的表達式只需花費3ms。

= = = =編譯器配置
默認情況下編譯器不會打開,但是您可以通過兩種不同的方式之一打開它。您可以通過使用解析器配置過程(前面已經討論過)或在SpEL使用被嵌入到另一個組件中時使用系統屬性來打開它。本節討論這兩個選項。

編譯器可以在三種模式之一進行操作,這三種模式都是在org.springframework.expression.spel中捕獲的。SpelCompilerMode枚舉。模式如下:

  • OFF(默認):編譯器關閉。
  • IMMEDIATE:在立即模式下,表達式將被儘快編譯。這通常是在第一次解釋評估之後。如果編譯後的表達式失敗(如前所述,通常是由於類型更改),則表達式求值的調用者將收到異常。
  • MIXED:在混合模式下,表達式隨時間在解釋模式和編譯模式之間悄然切換。經過一些解釋的運行之後,它們切換到編譯後的表單,如果編譯後的表單出現問題(如前面描述的類型改變),表達式將自動切換回解釋後的表單。稍後,它可能會生成另一個已編譯的表單並切換到它。基本上,用戶在即時模式中獲得的異常是在內部處理的。

IMMEDIATE 模式的存在是因爲MIXED 模式可能會對有副作用的表達式造成問題。如果編譯後的表達式在部分成功後崩潰,那麼它可能已經做了一些影響系統狀態的事情。如果發生了這種情況,調用者可能不希望它在解釋模式下無聲地重新運行,因爲表達式的一部分可能會運行兩次。

選擇模式之後,使用SpelParserConfiguration配置解析器。下面的例子演示瞭如何做到這一點:

SpelParserConfiguration config = new SpelParserConfiguration(SpelCompilerMode.IMMEDIATE,
    this.getClass().getClassLoader());

SpelExpressionParser parser = new SpelExpressionParser(config);

Expression expr = parser.parseExpression("payload");

MyMessage message = new MyMessage();

Object payload = expr.getValue(message);

在指定編譯器模式時,還可以指定類裝入器(允許傳遞null)。編譯後的表達式是在提供的類加載器下創建的子類加載器中定義的。重要的是要確保,如果指定了類裝入器,它可以看到表達式求值過程中涉及的所有類型。如果不指定類加載器,則使用默認的類加載器(通常是在表達式求值期間運行的線程的上下文類加載器)。

配置編譯器的第二種方法是當SpEL被嵌入到其他組件中時使用,並且可能無法通過配置對象來配置它。在這些情況下,可以使用系統屬性。可以將spring.expression.compiler.mode屬性設置爲SpelCompilerMode枚舉值之一(off、immediate或mixed)。

= = = =編譯器的限制
從Spring Framework 4.1開始,基本的編譯框架就已經就位了。然而,該框架還不支持編譯所有類型的表達式。最初的重點是可能在性能關鍵上下文中使用的公共表達式。下列表達式目前無法編譯:

  • 表達式包括賦值
  • 依賴於轉換服務的表達式
  • 使用自定義解析器或訪問器的表達式
  • 使用選擇或投影的表達式

將來可以編譯更多類型的表達式。

== Bean定義中的表達式
您可以使用帶有基於xml或基於註釋的配置元數據的SpEL表達式來定義BeanDefinition實例。在這兩種情況下,定義表達式的語法都是形式#{<表達式字符串>}。

= = = XML配置
屬性或構造函數參數值可以使用表達式來設置,如下例所示:

<bean id="numberGuess" class="org.spring.samples.NumberGuess">
    <property name="randomNumber" value="#{ T(java.lang.Math).random() * 100.0 }"/>

    <!-- other properties -->
</bean>

systemProperties變量是預定義的,因此可以在表達式中使用它,如下面的示例所示:

<bean id="taxCalculator" class="org.spring.samples.TaxCalculator">
    <property name="defaultLocale" value="#{ systemProperties['user.region'] }"/>

    <!-- other properties -->
</bean>

注意,在這個上下文中,不必在預定義的變量前面加上#符號。
您還可以通過名稱引用其他bean屬性,如下面的示例所示:

<bean id="numberGuess" class="org.spring.samples.NumberGuess">
    <property name="randomNumber" value="#{ T(java.lang.Math).random() * 100.0 }"/>

    <!-- other properties -->
</bean>

<bean id="shapeGuess" class="org.spring.samples.ShapeGuess">
    <property name="initialShapeSeed" value="#{ numberGuess.randomNumber }"/>

    <!-- other properties -->
</bean>

= = =註釋配置
要指定默認值,可以將@Value註釋放在字段、方法和方法或構造函數參數上。
以下示例設置字段變量的默認值:

public class FieldValueTestBean {

    @Value("#{ systemProperties['user.region'] }")
    private String defaultLocale;

    public void setDefaultLocale(String defaultLocale) {
        this.defaultLocale = defaultLocale;
    }

    public String getDefaultLocale() {
        return this.defaultLocale;
    }
}

下面的例子展示了一個等價的屬性設置方法:

public class PropertyValueTestBean {

    private String defaultLocale;

    @Value("#{ systemProperties['user.region'] }")
    public void setDefaultLocale(String defaultLocale) {
        this.defaultLocale = defaultLocale;
    }

    public String getDefaultLocale() {
        return this.defaultLocale;
    }
}

Autowired方法和構造函數也可以使用@Value註解,如下圖所示:

public class SimpleMovieLister {

    private MovieFinder movieFinder;
    private String defaultLocale;

    @Autowired
    public void configure(MovieFinder movieFinder,
            @Value("#{ systemProperties['user.region'] }") String defaultLocale) {
        this.movieFinder = movieFinder;
        this.defaultLocale = defaultLocale;
    }

    // ...
}
public class MovieRecommender {

    private String defaultLocale;

    private CustomerPreferenceDao customerPreferenceDao;

    public MovieRecommender(CustomerPreferenceDao customerPreferenceDao,
            @Value("#{systemProperties['user.country']}") String defaultLocale) {
        this.customerPreferenceDao = customerPreferenceDao;
        this.defaultLocale = defaultLocale;
    }

    // ...
}

= =語言參考
本節描述Spring表達式語言如何工作。它包括下列主題:

= = =文字表達式
支持的文字表達式類型有字符串、數值(int、real、hex)、布爾值和null。字符串由單引號分隔。若要將單引號本身放入字符串中,請使用兩個單引號字符。

下面的清單顯示了文字的簡單用法。通常,它們不會像這樣單獨使用,而是作爲更復雜的表達式的一部分—例如,使用邏輯比較運算符一側的文字。

ExpressionParser parser = new SpelExpressionParser();

// evals to "Hello World"
String helloWorld = (String) parser.parseExpression("'Hello World'").getValue();

double avogadrosNumber = (Double) parser.parseExpression("6.0221415E+23").getValue();

// evals to 2147483647
int maxValue = (Integer) parser.parseExpression("0x7FFFFFFF").getValue();

boolean trueValue = (Boolean) parser.parseExpression("true").getValue();

Object nullValue = parser.parseExpression("null").getValue();

數字支持使用負號、指數符號和小數點。默認情況下,使用Double.parseDouble()解析實數。
===屬性、數組、列表、映射和索引器

使用屬性引用導航很容易。爲此,使用句點來指示嵌套的屬性值。Inventor類(pupin和tesla)的實例填充了示例部分中使用的類中列出的數據。爲了導航“向下”,獲得特斯拉的出生年份和普潘的出生城市,我們使用以下表達式:

// evals to 1856
int year = (Integer) parser.parseExpression("Birthdate.Year + 1900").getValue(context);

String city = (String) parser.parseExpression("placeOfBirth.City").getValue(context);

允許對屬性名稱的第一個字母不區分大小寫。數組和列表的內容採用方括號表示法,如下例所示:

ExpressionParser parser = new SpelExpressionParser();
EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();

// Inventions Array

// evaluates to "Induction motor"
String invention = parser.parseExpression("inventions[3]").getValue(
        context, tesla, String.class);

// Members List

// evaluates to "Nikola Tesla"
String name = parser.parseExpression("Members[0].Name").getValue(
        context, ieee, String.class);

// List and Array navigation
// evaluates to "Wireless communication"
String invention = parser.parseExpression("Members[0].Inventions[6]").getValue(
        context, ieee, String.class);

映射的內容是通過在方括號中指定文字鍵值來獲得的。在下面的例子中,因爲officer映射的鍵是字符串,我們可以指定字符串的文字:

// Officer's Dictionary

Inventor pupin = parser.parseExpression("Officers['president']").getValue(
        societyContext, Inventor.class);

// evaluates to "Idvor"
String city = parser.parseExpression("Officers['president'].PlaceOfBirth.City").getValue(
        societyContext, String.class);

// setting values
parser.parseExpression("Officers['advisors'][0].PlaceOfBirth.Country").setValue(
        societyContext, "Croatia");

= = =內聯列表
您可以使用{}符號直接在表達式中表示列表。

// evaluates to a Java list containing the four numbers
List numbers = (List) parser.parseExpression("{1,2,3,4}").getValue(context);

List listOfLists = (List) parser.parseExpression("{{'a','b'},{'x','y'}}").getValue(context);

{}本身意味着一個空列表。出於性能原因,如果列表本身完全由固定的文字組成,則會創建一個常量列表來表示表達式(而不是在每次求值時都構建一個新列表)。
= = =內聯映射map
您還可以使用{key:value}符號直接在表達式中表示映射。下面的例子演示瞭如何做到這一點:

// evaluates to a Java map containing the two entries
Map inventorInfo = (Map) parser.parseExpression("{name:'Nikola',dob:'10-July-1856'}").getValue(context);

Map mapOfMaps = (Map) parser.parseExpression("{name:{first:'Nikola',last:'Tesla'},dob:{day:10,month:'July',year:1856}}").getValue(context);

{:}本身表示一個空映射。出於性能原因,如果映射本身由固定的文字或其他嵌套的常量結構(列表或映射)組成,則會創建一個常量映射來表示表達式(而不是在每個計算值上構建一個新的映射)。映射鍵的引用是可選的。上面的例子不使用帶引號的鍵。
= = =數組結構
可以使用熟悉的Java語法構建數組,也可以提供初始化器,以便在構建時填充數組。下面的例子演示瞭如何做到這一點:

int[] numbers1 = (int[]) parser.parseExpression("new int[4]").getValue(context);

// Array with initializer
int[] numbers2 = (int[]) parser.parseExpression("new int[]{1,2,3}").getValue(context);

// Multi dimensional array
int[][] numbers3 = (int[][]) parser.parseExpression("new int[4][5]").getValue(context);

在構造多維數組時,目前無法提供初始化器。
= = =方法
您可以使用典型的Java編程語法來調用方法。您還可以對文字調用方法。還支持變量參數。下面的例子演示瞭如何調用方法:

// string literal, evaluates to "bc"
String bc = parser.parseExpression("'abc'.substring(1, 3)").getValue(String.class);

// evaluates to true
boolean isMember = parser.parseExpression("isMember('Mihajlo Pupin')").getValue(
        societyContext, Boolean.class);

= = =操作符
Spring表達式語言支持以下類型的操作符:

= = = =關係運算符
關係運算符(等於、不等於、小於、小於或等於、大於、大於或等於)由標準運算符符號支持。下面的清單顯示了一些操作符的例子:

// evaluates to true
boolean trueValue = parser.parseExpression("2 == 2").getValue(Boolean.class);

// evaluates to false
boolean falseValue = parser.parseExpression("2 < -5.0").getValue(Boolean.class);

// evaluates to true
boolean trueValue = parser.parseExpression("'black' < 'block'").getValue(Boolean.class);

針對null的大於和小於比較遵循一個簡單的規則:null被視爲nothing(不是0)。因此,任何其他值總是大於null (X > null總是true),並且沒有任何其他值小於nothing (X < null總是false)。
如果您更喜歡數字比較,那麼應該避免基於數字的null比較,而應該使用針對0的比較(例如,X >或X < 0)。

除了標準的關係運算符之外,SpEL還支持instanceof和基於正則表達式的匹配運算符。下面的清單展示了這兩種方法的示例:

// evaluates to false
boolean falseValue = parser.parseExpression(
        "'xyz' instanceof T(Integer)").getValue(Boolean.class);

// evaluates to true
boolean trueValue = parser.parseExpression(
        "'5.00' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean.class);

//evaluates to false
boolean falseValue = parser.parseExpression(
        "'5.0067' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean.class);

警告:要小心使用基本類型,因爲它們會立即被裝箱到包裝器類型,所以1 instanceof T(int)的計算結果爲false,而1 instanceof T(Integer)的計算結果爲true,這與預期一致。

每個符號運算符也可以被指定爲純字母等價的。這避免了使用的符號對於表達式所嵌入的文檔類型具有特殊意義的問題(例如在XML文檔中)。文本等價物爲:

  • lt (<)

  • gt (>)

  • le (<=)

  • ge (>=)

  • eq (==)

  • ne (!=)

  • div (/)

  • mod (%)

  • not (!).

所有的文本操作符都是不區分大小寫的。
= = = =邏輯運算符
SpEL支持以下邏輯運算符:

  • and

  • or

  • not

下面的示例演示如何使用邏輯運算符

// -- AND --

// evaluates to false
boolean falseValue = parser.parseExpression("true and false").getValue(Boolean.class);

// evaluates to true
String expression = "isMember('Nikola Tesla') and isMember('Mihajlo Pupin')";
boolean trueValue = parser.parseExpression(expression).getValue(societyContext, Boolean.class);

// -- OR --

// evaluates to true
boolean trueValue = parser.parseExpression("true or false").getValue(Boolean.class);

// evaluates to true
String expression = "isMember('Nikola Tesla') or isMember('Albert Einstein')";
boolean trueValue = parser.parseExpression(expression).getValue(societyContext, Boolean.class);

// -- NOT --

// evaluates to false
boolean falseValue = parser.parseExpression("!true").getValue(Boolean.class);

// -- AND and NOT --
String expression = "isMember('Nikola Tesla') and !isMember('Mihajlo Pupin')";
boolean falseValue = parser.parseExpression(expression).getValue(societyContext, Boolean.class);

= = = =數學操作符
可以對數字和字符串都使用加法運算符。您只能在數字上使用減法、乘法和除法運算符。還可以使用模數(%)和指數冪(^)運算符。執行標準操作符優先級。下面的例子展示了使用的數學運算符:

// Addition
int two = parser.parseExpression("1 + 1").getValue(Integer.class);  // 2

String testString = parser.parseExpression(
        "'test' + ' ' + 'string'").getValue(String.class);  // 'test string'

// Subtraction
int four = parser.parseExpression("1 - -3").getValue(Integer.class);  // 4

double d = parser.parseExpression("1000.00 - 1e4").getValue(Double.class);  // -9000

// Multiplication
int six = parser.parseExpression("-2 * -3").getValue(Integer.class);  // 6

double twentyFour = parser.parseExpression("2.0 * 3e0 * 4").getValue(Double.class);  // 24.0

// Division
int minusTwo = parser.parseExpression("6 / -3").getValue(Integer.class);  // -2

double one = parser.parseExpression("8.0 / 4e0 / 2").getValue(Double.class);  // 1.0

// Modulus
int three = parser.parseExpression("7 % 4").getValue(Integer.class);  // 3

int one = parser.parseExpression("8 / 5 % 2").getValue(Integer.class);  // 1

// Operator precedence
int minusTwentyOne = parser.parseExpression("1+2-3*8").getValue(Integer.class);  // -21

===賦值運算符
要設置屬性,請使用賦值操作符(=)。這通常在對setValue的調用中完成,但也可以在對getValue的調用中完成。下面的清單顯示了使用賦值運算符的兩種方法:

Inventor inventor = new Inventor();
EvaluationContext context = SimpleEvaluationContext.forReadWriteDataBinding().build();

parser.parseExpression("Name").setValue(context, inventor, "Aleksandar Seovic");

// alternatively
String aleks = parser.parseExpression(
        "Name = 'Aleksandar Seovic'").getValue(context, inventor, String.class);

= = =類型
您可以使用特殊的T操作符來指定java.lang.Class的實例(類型)。靜態方法也可以通過使用這個操作符來調用。StandardTypeLocator(可以替換)是在理解java的基礎上構建的。朗包。這意味着T()引用java中的類型。lang不需要完全限定,但是所有其他類型引用都必須限定。下面的例子展示瞭如何使用T運算符:

Class dateClass = parser.parseExpression("T(java.util.Date)").getValue(Class.class);

Class stringClass = parser.parseExpression("T(String)").getValue(Class.class);

boolean trueValue = parser.parseExpression(
        "T(java.math.RoundingMode).CEILING < T(java.math.RoundingMode).FLOOR")
        .getValue(Boolean.class);

= = =構造函數
您可以通過使用新的操作符來調用構造函數。除了基本類型(int、float等)和字符串之外,應該對所有的類名使用全限定名。下面的例子演示瞭如何使用新的操作符來調用構造函數:

Inventor einstein = p.parseExpression(
        "new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German')")
        .getValue(Inventor.class);

//create new inventor instance within add method of List
p.parseExpression(
        "Members.add(new org.spring.samples.spel.inventor.Inventor(
            'Albert Einstein', 'German'))").getValue(societyContext);

= = =變量
可以使用#variableName語法引用表達式中的變量。變量是通過在EvaluationContext實現上使用setVariable方法設置的。下面的例子展示瞭如何使用變量:

Inventor tesla = new Inventor("Nikola Tesla", "Serbian");

EvaluationContext context = SimpleEvaluationContext.forReadWriteDataBinding().build();
context.setVariable("newName", "Mike Tesla");

parser.parseExpression("Name = #newName").getValue(context, tesla);
System.out.println(tesla.getName())  // "Mike Tesla"

=== #this和#root變量
這個變量總是被定義,並引用當前的計算對象(對不合格的引用進行解析)。始終定義#root變量並引用根上下文對象。雖然隨着表達式組件的計算而變化,但#root總是指向根。下面的例子演示瞭如何使用#this和#root變量:

// create an array of integers
List<Integer> primes = new ArrayList<Integer>();
primes.addAll(Arrays.asList(2,3,5,7,11,13,17));

// create parser and set variable 'primes' as the array of integers
ExpressionParser parser = new SpelExpressionParser();
EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataAccess();
context.setVariable("primes", primes);

// all prime numbers > 10 from the list (using selection ?{...})
// evaluates to [11, 13, 17]
List<Integer> primesGreaterThanTen = (List<Integer>) parser.parseExpression(
        "#primes.?[#this>10]").getValue(context);

= = =函數
可以通過註冊可以在表達式字符串中調用的用戶定義函數來擴展SpEL。函數通過EvaluationContext註冊。下面的例子展示瞭如何註冊一個用戶定義的函數:

Method method = ...;

EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
context.setVariable("myFunction", method);

例如,考慮下面的實用程序方法來反轉一個字符串:

public abstract class StringUtils {

    public static String reverseString(String input) {
        StringBuilder backwards = new StringBuilder(input.length());
        for (int i = 0; i < input.length(); i++) {
            backwards.append(input.charAt(input.length() - 1 - i));
        }
        return backwards.toString();
    }
}

你可以註冊並使用上述方法,如下例所示:

ExpressionParser parser = new SpelExpressionParser();

EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
context.setVariable("reverseString",
        StringUtils.class.getDeclaredMethod("reverseString", String.class));

String helloWorldReversed = parser.parseExpression(
        "#reverseString('hello')").getValue(context, String.class);

= = = Bean引用
如果已使用bean解析器配置了計算上下文,則可以使用@符號從表達式中查找bean。下面的例子演示瞭如何做到這一點:

ExpressionParser parser = new SpelExpressionParser();
StandardEvaluationContext context = new StandardEvaluationContext();
context.setBeanResolver(new MyBeanResolver());

// This will end up calling resolve(context,"something") on MyBeanResolver during evaluation
Object bean = parser.parseExpression("@something").getValue(context);

要訪問工廠bean本身,應該在bean名稱前面加上&符號。下面的例子演示瞭如何做到這一點:

ExpressionParser parser = new SpelExpressionParser();
StandardEvaluationContext context = new StandardEvaluationContext();
context.setBeanResolver(new MyBeanResolver());

// This will end up calling resolve(context,"&foo") on MyBeanResolver during evaluation
Object bean = parser.parseExpression("&foo").getValue(context);

===三元操作符(If-Then-Else)
可以使用三元運算符在表達式中執行if-then-else條件邏輯。下面的清單展示了一個簡單的例子:

String falseString = parser.parseExpression(
        "false ? 'trueExp' : 'falseExp'").getValue(String.class);

在本例中,boolean false返回字符串值'false exp '。下面是一個更現實的例子:

parser.parseExpression("Name").setValue(societyContext, "IEEE");
societyContext.setVariable("queryName", "Nikola Tesla");

expression = "isMember(#queryName)? #queryName + ' is a member of the ' " +
        "+ Name + ' Society' : #queryName + ' is not a member of the ' + Name + ' Society'";

String queryResultString = parser.parseExpression(expression)
        .getValue(societyContext, String.class);
// queryResultString = "Nikola Tesla is a member of the IEEE Society"

有關Elvis操作符的下一節介紹三元操作符的更簡短語法。
=== Elvis操作符
Elvis操作符是三元操作符語法的縮寫,在Groovy語言中使用。使用三元操作符語法,您通常需要重複一個變量兩次,如下面的例子所示:

String name = "Elvis Presley";
String displayName = (name != null ? name : "Unknown");

相反,您可以使用Elvis操作符(得名於其髮型的相似性)。下面的例子展示瞭如何使用Elvis操作符:

ExpressionParser parser = new SpelExpressionParser();

String name = parser.parseExpression("name?:'Unknown'").getValue(String.class);
System.out.println(name);  // 'Unknown'

下面的清單顯示了一個更復雜的例子:

ExpressionParser parser = new SpelExpressionParser();
EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();

Inventor tesla = new Inventor("Nikola Tesla", "Serbian");
String name = parser.parseExpression("Name?:'Elvis Presley'").getValue(context, tesla, String.class);
System.out.println(name);  // Nikola Tesla

tesla.setName(null);
name = parser.parseExpression("Name?:'Elvis Presley'").getValue(context, tesla, String.class);
System.out.println(name);  // Elvis Presley

注意:可以使用Elvis操作符在表達式中應用默認值。下面的例子演示瞭如何在@Value表達式中使用Elvis操作符:

@Value("#{systemProperties['pop3.port'] ?: 25}")

這將注入一個系統屬性pop3的端口。如果定義了端口,則爲端口;如果沒有定義,則25爲端口。

===安全導航操作符
safe navigation操作符用於避免NullPointerException,它來自Groovy語言。通常,當您有一個對象的引用時,您可能需要在訪問對象的方法或屬性之前驗證它是否爲空。爲了避免這種情況,安全導航操作符返回null,而不是拋出異常。下面的例子演示瞭如何使用安全導航操作符:

ExpressionParser parser = new SpelExpressionParser();
EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();

Inventor tesla = new Inventor("Nikola Tesla", "Serbian");
tesla.setPlaceOfBirth(new PlaceOfBirth("Smiljan"));

String city = parser.parseExpression("PlaceOfBirth?.City").getValue(context, tesla, String.class);
System.out.println(city);  // Smiljan

tesla.setPlaceOfBirth(null);
city = parser.parseExpression("PlaceOfBirth?.City").getValue(context, tesla, String.class);
System.out.println(city);  // null - does not throw NullPointerException!!!

= = =選擇集合
選擇是一個功能強大的表達式語言特性,它允許您通過從源集合的條目中進行選擇,將源集合轉換爲另一個集合。
選擇使用。?[selectionExpression]的語法。它過濾集合並返回一個包含原始元素子集的新集合。例如,選擇讓我們很容易得到塞爾維亞發明家的列表,如下面的例子所示:

List<Inventor> list = (List<Inventor>) parser.parseExpression(
        "Members.?[Nationality == 'Serbian']").getValue(societyContext);

可以在列表和映射上進行選擇。對於列表,將根據每個列表元素計算選擇條件。對於映射,將根據每個映射條目(Java類型map . entry的對象)計算選擇條件。每個映射條目的鍵和值都可以作爲屬性訪問,以便在選擇中使用。
下面的表達式返回一個新的映射,它由那些條目值小於27的原始映射元素組成:

Map newMap = parser.parseExpression("map.?[value<27]").getValue();

除了返回所有選擇的元素外,還可以只檢索第一個或最後一個值。要獲得與選擇匹配的第一個條目,語法是.^[selectionExpression]。要獲得最後一個匹配的選擇,語法是.$[selectionExpression]。
= = =投影集合

投影讓集合驅動子表達式的計算,結果是一個新的集合。投影的語法是。![projectionExpression]。例如,假設我們有一個發明家列表,但是想要他們出生的城市列表。實際上,我們想要評估“出生地點”。對於發明人名單中的每一項。下面的例子使用投影來做到這一點:

// returns ['Smiljan', 'Idvor' ]
List placesOfBirth = (List)parser.parseExpression("Members.![placeOfBirth.city]");

您還可以使用映射來驅動投影,在本例中,投影表達式針對映射中的每個條目求值(表示爲Java map . entry)。映射的結果是一個列表,其中包含對每個映射條目的投影表達式的求值。

= = =表達式模板
表達式模板允許將文本與一個或多個計算塊混合。每個計算塊由您可以定義的前綴和後綴字符分隔。常見的選擇是使用#{}作爲分隔符,如下面的示例所示:

String randomPhrase = parser.parseExpression(
        "random number is #{T(java.lang.Math).random()}",
        new TemplateParserContext()).getValue(String.class);

// evaluates to "random number is 0.7038186818312008"

通過將文本“random number is”與對#{}分隔符內的表達式求值的結果(在本例中是調用random()方法的結果)連接在一起,對字符串求值。parseExpression()方法的第二個參數屬於ParserContext類型。ParserContext接口用於影響表達式的解析方式,以支持表達式模板功能。TemplateParserContext的定義如下:

 

public class TemplateParserContext implements ParserContext {

    public String getExpressionPrefix() {
        return "#{";
    }

    public String getExpressionSuffix() {
        return "}";
    }

    public boolean isTemplate() {
        return true;
    }
}

==本例中使用的類
本節列出本章示例中使用的類。

package org.spring.samples.spel.inventor;

import java.util.Date;
import java.util.GregorianCalendar;

public class Inventor {

    private String name;
    private String nationality;
    private String[] inventions;
    private Date birthdate;
    private PlaceOfBirth placeOfBirth;

    public Inventor(String name, String nationality) {
        GregorianCalendar c= new GregorianCalendar();
        this.name = name;
        this.nationality = nationality;
        this.birthdate = c.getTime();
    }

    public Inventor(String name, Date birthdate, String nationality) {
        this.name = name;
        this.nationality = nationality;
        this.birthdate = birthdate;
    }

    public Inventor() {
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getNationality() {
        return nationality;
    }

    public void setNationality(String nationality) {
        this.nationality = nationality;
    }

    public Date getBirthdate() {
        return birthdate;
    }

    public void setBirthdate(Date birthdate) {
        this.birthdate = birthdate;
    }

    public PlaceOfBirth getPlaceOfBirth() {
        return placeOfBirth;
    }

    public void setPlaceOfBirth(PlaceOfBirth placeOfBirth) {
        this.placeOfBirth = placeOfBirth;
    }

    public void setInventions(String[] inventions) {
        this.inventions = inventions;
    }

    public String[] getInventions() {
        return inventions;
    }
}
package org.spring.samples.spel.inventor;

public class PlaceOfBirth {

    private String city;
    private String country;

    public PlaceOfBirth(String city) {
        this.city=city;
    }

    public PlaceOfBirth(String city, String country) {
        this(city);
        this.country = country;
    }

    public String getCity() {
        return city;
    }

    public void setCity(String s) {
        this.city = s;
    }

    public String getCountry() {
        return country;
    }

    public void setCountry(String country) {
        this.country = country;
    }
}
package org.spring.samples.spel.inventor;

import java.util.*;

public class Society {

    private String name;

    public static String Advisors = "advisors";
    public static String President = "president";

    private List<Inventor> members = new ArrayList<Inventor>();
    private Map officers = new HashMap();

    public List getMembers() {
        return members;
    }

    public Map getOfficers() {
        return officers;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public boolean isMember(String name) {
        for (Inventor inventor : members) {
            if (inventor.getName().equals(name)) {
                return true;
            }
        }
        return false;
    }
}

=使用Spring進行面向方面的編程
通過提供另一種考慮程序結構的方式,面向方面編程(AOP)補充了面向對象編程(OOP)。OOP中模塊化的關鍵單元是類,而在AOP中模塊化的單元是方面。方面支持跨多個類型和對象的關注點(例如事務管理)的模塊化。(在AOP文獻中,這樣的關注點通常被稱爲“橫切”關注點。)

Spring的關鍵組件之一是AOP框架。雖然Spring IoC容器不依賴於AOP(這意味着如果不想使用AOP,就不需要使用AOP),但是AOP補充了Spring IoC,提供了一個功能非常強大的中間件解決方案。

 

 

發佈了23 篇原創文章 · 獲贊 0 · 訪問量 472
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章