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
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章