Spring源码分析系列:从DispatcherServlet到Bean工厂

# Spring源码分析系列:从DispatcherServlet到Bean工厂 > 本文基于DDKK.COM的Spring教程系列进行二次创作,原文链接:https://ddkk.com/category/j2ee/spring/1/index.html ## 0. 简介 ## Spring Framework 5 中文文档 本项目为Spring 5 Reference的翻译作品,由并发编程网的同学一起合作而成。 希望读者多多支持,有错误之处及时增加pull request进行修正 ## 1. 入门指南 本参考指南提供了有关Spring Framework的详细信息。它全面的介绍了Spring的所有的功能,以及Spring涉及的基础概念(如 依赖注入 Dependency Injection)。 如果你是刚开始使用Spring,你可能需要首先创建一个Spring Boot应用程序来开始Spring框架之旅。Spring Boot提供了一个快速(和自治的)的方式来创建一个基于Spring的生产环境。它是基于Spring框架,支持约定优于配置,并且被设计成尽可能快地让你启动和运行程序。 您可以使用start.spring.io生成一个基本项目或按照新手入门指南里的任意一个指南构建项目,例如构建一个RESTful Web服务入门指南 。为了更容易帮助你理解,这些指南都是面向任务的,其中大部分都是基于Spring Boot的。 他们还涵盖了很多Spring原型工程,在您需要解决特定问题时可以考虑使用他们。 ## 2. Spring框架 Spring 框架是一个Java平台,它为开发Java应用程序提供全面的基础架构支持。Spring负责基础架构,因此您可以专注于应用程序的开发。 Spring可以让您从“plain old Java objects”(POJO)中构建应用程序和通过非侵入性的POJO实现企业应用服务。此功能适用于Java SE的编程模型,全部的或部分的适应Java EE模型。 这些例子告诉你,作为一个应用程序开发人员,如何从Spring平台中受益: 写一个Java方法执行数据库事务,而无需处理具体事务的APIs。 写一个本地Java方法去远程调用,而不必处理远程调用的APIs。 写一个本地Java方法实现管理操作,而不必处理JMX APIs。 写一个本地Java方法实现消息处理,而不必处理JMS APIs。 ## 2.1 依赖注入和控制反转 Java应用程序-这是一个宽松的术语,它包括的范围从受限的嵌入式应用程序到n层的服务器端企业应用程序-通常组成程序的对象互相协作而构成正确的应用程序。因此,在一个应用程序中的对象彼此具有依赖关系(dependencies)。 虽然Java平台提供了丰富的应用程序开发功能,但它缺乏将基本的模块组织成一个整体的方法,而将该任务留给了架构师和开发人员。虽然你可以使用如工厂,抽象工厂,Builder,装饰器和Service Locator等 设计模式来构建各种类和对象实例,使他们组合成应用程序,但这些模式无非只是:最佳实践赋予的一个名字,以及这是什么样的模式,应用于哪里,它能解决的问题等等。 模式是您必须在应用程序中自己实现的形式化的最佳实践。 Spring框架控制反转(IOC)组件通过提供一系列的标准化的方法把完全不同的组件组合成一个能够使用的应用程序来解决这个问题。Spring框架把形式化的设计模式编写为优秀的对象,你可以容易的集成到自己的应用程序中。许多组织和机构使用Spring框架,以这种方式(使用Spring的模式对象)来设计健壮的,可维护的应用程序。 “现在的问题是,什么方面的控制被(他们)反转了? ”马丁·福勒2004年在他的网站提出了这个有关控制反转(IOC)的问题 ,福勒建议重命名,使之能够自我描述,并提出了依赖注入( Dependency Injection)。 ## 2.2 模块 Spring框架的功能被有组织的分散到约20个模块中。这些模块分布在核心容器,数据访问/集成,Web,AOP(面向切面的编程),植入(Instrumentation),消息传输和测试,如下面的图所示。 图2.1 Spring框架概述 以下部分列出了每个可用模块,以及它们的工件名称和它们支持的主要功能。工件的名字对应的是 工件标识符,使用在依赖管理工具中。 ## 2.2.1核心容器 核心容器由以下模块组成,spring-core, spring-beans,spring-context,spring-context-support,和spring-expression (Spring表达式语言)。 spring-core和spring-beans模块提供了框架的基础功能,包括IOC和依赖注入功能。 BeanFactory是一个成熟的工厂模式的实现。你不再需要编程去实现单例模式,允许你把依赖关系的配置和描述从程序逻辑中解耦。 上下文(spring-context)模块建立在由Core和Beans模块提供的坚实的基础上:它提供一个框架式的对象访问方式,类似于一个JNDI注册表。上下文模块从Beans模块继承其功能,并添加支持国际化(使用,例如,资源集合),事件传播,资源负载,并且透明创建上下文,例如,Servlet容器。Context模块还支持Java EE的功能,如EJB,JMX和基本的远程处理。ApplicationContext接口是Context模块的焦点。 spring-context-support支持整合普通第三方库到Spring应用程序上下文,特别是用于高速缓存(ehcache,JCache)和调度(CommonJ,Quartz)的支持。 spring-expression模块提供了强大的表达式语言去支持查询和操作运行时对象图。这是对JSP 2.1规范中规定的统一表达式语言(unified EL)的扩展。该语言支持设置和获取属性值,属性分配,方法调用,访问数组,集合和索引器的内容,逻辑和算术运算,变量命名以及从Spring的IoC容器中以名称检索对象。 它还支持列表投影和选择以及常见的列表聚合。 ## 2.2.2 AOP和Instrumentation spring-aop模块提供了一个符合AOP联盟(要求)的面向方面的编程实现,例如,允许您定义方法拦截器和切入点(pointcuts),以便干净地解耦应该被分离的功能实现。 使用源级元数据(source-level metadata)功能,您还可以以类似于.NET属性的方式将行为信息合并到代码中。 单独的spring-aspects模块,提供了与AspectJ的集成。 spring-instrument模块提供了类植入(instrumentation)支持和类加载器的实现,可以应用在特定的应用服务器中。该spring-instrument-tomcat 模块包含了支持Tomcat的植入代理。 ## 2.2.3消息 Spring框架4包括spring-messaging(消息传递模块),其中包含来自Spring Integration的项目,例如,Message,MessageChannel,MessageHandler,和其他用来传输消息的基础应用。该模块还包括一组用于将消息映射到方法的注释(annotations),类似于基于Spring MVC注释的编程模型。 ## 2.2.4数据访问/集成 数据访问/集成层由JDBC,ORM,OXM,JMS和事务模块组成。 spring-jdbc模块提供了一个JDBC –抽象层,消除了需要的繁琐的JDBC编码和数据库厂商特有的错误代码解析。 spring-tx模块支持用于实现特殊接口和所有POJO(普通Java对象)的类的编程和声明式事务 管理。 spring-orm模块为流行的对象关系映射(object-relational mapping )API提供集成层,包括JPA和Hibernate。使用spring-orm模块,您可以将这些O / R映射框架与Spring提供的所有其他功能结合使用,例如前面提到的简单声明性事务管理功能。 spring-oxm模块提供了一个支持对象/ XML映射实现的抽象层,如JAXB,Castor,JiBX和XStream。 spring-jms模块(Java Messaging Service) 包含用于生产和消费消息的功能。自Spring Framework 4.1以来,它提供了与 spring-messaging模块的集成。 ## 2.2.5 Web Web层由spring-web,spring-webmvc和spring-websocket 模块组成。 spring-web模块提供基本的面向Web的集成功能,例如多部分文件上传功能,以及初始化一个使用了Servlet侦听器和面向Web的应用程序上下文的IoC容器。它还包含一个HTTP客户端和Spring的远程支持的Web相关部分。 spring-webmvc模块(也称为Web-Servlet模块)包含用于Web应用程序的Spring的模型-视图-控制器(MVC)和REST Web Services实现。 Spring的MVC框架提供了领域模型代码和Web表单之间的清晰分离,并与Spring Framework的所有其他功能集成。 ## 2.2.6测试 spring-test模块支持使用JUnit或TestNG对Spring组件进行单元测试和 集成测试。它提供了Spring ApplicationContexts的一致加载和这些上下文的缓存。它还提供可用于独立测试代码的模仿(mock)对象。 ## 2.3 使用场景 之前描述的构建模块使Spring成为许多应用场景的理性选择,从在资源受限设备上运行的嵌入式应用程序到使用Spring的事务管理功能和Web框架集成的全面的企业应用程序。 图2.2 典型的成熟完整的Spring Web应用程序 Spring的声明式事务管理功能使Web应用程序完全事务性,就像使用EJB容器管理的事务一样。所有您的定制业务逻辑都可以使用简单的POJO实现,并由Spring的IoC容器进行管理。附加服务包括支持发送电子邮件和独立于Web层的验证,可让您选择执行验证规则的位置。 Spring的ORM支持与JPA和Hibernate集成;例如,当使用Hibernate时,可以继续使用现有的映射文件和标准的Hibernate SessionFactory配置。表单控制器将Web层与域模型无缝集成,从而无需ActionForms或将HTTP参数转换为域模型的值的其他类。 图2.3 使用第三方web框架的Spring中间层 有时情况不允许你完全切换到不同的框架。 Spring框架并不强制您使用其中的一切;这不是一个全有或全无的解决方案。使用Struts,Tapestry,JSF或其他UI框架构建的现有前端可以与基于Spring的中间层集成,从而允许您使用Spring事务功能。您只需要使用ApplicationContext连接您的业务逻辑,并使用WebApplicationContext来集成您的Web层。 图2.4 远程使用场景 当您需要通过Web服务访问现有代码时,你可以使用Spring的 Hessian-,Rmi-或HttpInvokerProxyFactoryBean类。启用对现有应用程序的远程访问并不困难。 图2.5 EJBs – 包装现有的POJOs Spring Framework还为Enterprise JavaBeans提供了一个访问和抽象层,使您能够重用现有的POJO,并将其包装在无状态会话bean中,以便在可能需要声明式安全性的, 可扩展的,故障安全的Web应用程序中使用。 ## 2.3.1依赖管理和命名约定 依赖关系管理和依赖注入是不同的。为了将Spring的这些不错的功能(如依赖注入)集成到应用程序中,您需要组装所有需要的库(jar文件),并在运行时导入到类路径(classpath)中,也有可能在编译时就需要加入类路径。这些依赖关系不是注入的虚拟组件,而是文件系统中的物理资源(通常是这样)。依赖关系管理的过程包括定位这些资源,存储它们并将其添加到类路径中。依赖关系可以是直接的(例如,我的应用程序在运行时依赖于Spring)或间接的(例如我的应用程序依赖于commons-dbcp ,而commons-dbcp 又依赖于commons-pool)。间接依赖关系具有“传递性”,它们是最难识别和管理的依赖关系。 如果你要使用Spring,你需要获得一个包含你所需要的Spring模块的jar库的副本。为了使这更容易,Spring被打包为一组尽可能分离依赖关系的模块,例如,如果您不想编写Web应用程序,则不需要spring-web模块。要引用本指南中的Spring库模块,我们使用一个简写命名约定spring- *或spring – *.jar,其中*表示模块的简称(例如spring-core,spring-webmvc,spring-jms等) )。您实际使用的jar文件名通常是与版本号连接的模块名称(例如spring-core-5.0.0.M5.jar)。 Spring框架的每个版本都会发布到以下几个地方: Maven Central,它是Maven查询的默认存储库,不需要任何特殊配置。 Spring的许多常见的库也可以从Maven Central获得,Spring社区的大部分使用Maven进行依赖关系管理,所以这对他们来说很方便。这里的jar的名字是spring – * – <version> .jar,Maven groupId是org.springframework。 在专门用于Spring的公共Maven存储库中。除了最终的GA版本,该存储库还承载开发快照和里程碑版本。 jar文件名与Maven Central格式相同,因此这是一个有用的地方,可以将开发中的版本的Spring与在Maven Central中部署的其他库配合使用。该存储库还包含集中分发的zip文件,其中包含所有Spring jar,捆绑在一起以便于下载。 所以你需要决定的第一件事是如何管理你的依赖关系:我们通常建议使用像Maven,Gradle或Ivy这样的自动化系统,但你也可以通过自己下载所有的jar来手动执行。 您将在下面找到Spring artifacts列表。有关每个模块的更完整的描述,第2.2节“模块”. 表2.1 Spring框架的Artifacts ## Spring的依赖和依赖于Spring 虽然Spring为大量企业和其他外部工具提供集成和支持,但它有意将其强制性的依赖保持在最低限度:在使用Spring用于简单的用例时,您不必定位和下载(甚至自动的去做)大量的jar库。对于基本依赖注入功能,只有一个强制性的外部依赖关系,也就是用于日志记录的依赖(有关日志记录选项的更详细描述,请参阅下文)。 接下来,我们概述了配置依赖于Spring的应用程序所需的基本步骤,首先是使用Maven,然后使用Gradle,最后使用Ivy。在任何情况下,如果不清楚,请参阅依赖关系管理系统的文档,或查看一些示例代码 – Spring本身在构建时使用Gradle来管理依赖关系,我们的示例主要使用Gradle或Maven。 ## Maven的依赖管理 如果您使用Maven进行依赖关系管理,则甚至不需要显式提供依赖关系。 例如,要创建应用程序上下文并使用依赖注入来配置应用程序,您的Maven依赖配置如下所示: <dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.0.0.M5</version> <scope>runtime</scope> </dependency> </dependencies> (依赖配置)就是这样。注意,如果您不需要针对Spring API进行编译,那么范围(scope)可以被声明为运行时,通常情况下这是基本的依赖注入用例。 以上示例适用于Maven Central存储库。要使用Spring Maven仓库(例如,使用里程碑或开发中的快照版本),您需要在Maven配置中指定仓库位置。完整版本: <repositories> <repository> <id>io.spring.repo.maven.release</id> <url>http://repo.spring.io/release/</url> <snapshots><enabled>false</enabled></snapshots> </repository> </repositories> 对于里程碑(milestones): <repositories> <repository> <id>io.spring.repo.maven.milestone</id> <url>http://repo.spring.io/milestone/</url> <snapshots><enabled>false</enabled></snapshots> </repository> </repositories> 而对于快照(snapshots): <repositories> <repository> <id>io.spring.repo.maven.snapshot</id> <url>http://repo.spring.io/snapshot/</url> <snapshots><enabled>true</enabled></snapshots> </repository> </repositories> Maven的“材料清单”依赖 使用Maven时,可能会意外混合不同版本的Spring JAR。例如,您可能会发现第三方库或另一个Spring项目会传递依赖于旧版本的Spring JARs。如果您忘记自己明确声明直接依赖,可能会出现各种意外问题。 为了克服这些问题,Maven支持“材料清单(bill of materials)”(BOM)依赖的概念。您可以在dependencyManagement部分中导入spring-framework-bom,以确保所有Spring依赖(直接和传递)都是相同的版本 <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-framework-bom</artifactId> <version>5.0.0.M5</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> 使用BOM的另外一个好处是,您不再需要在依赖于Spring Framework artifacts时指定<version>属性: <dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-web</artifactId> </dependency> <dependencies> Gradle 依赖管理 要使用Gradle 构建系统的Spring仓库,请在仓库部分中包含适当的URL: repositories { mavenCentral() // and optionally... maven { url "http://repo.spring.io/release" } } 您可以根据需要将repositoriesURL从/release更改为/milestone或/snapshot。一旦repositories被配置,你可以通常使用Gradle方式来声明依赖: dependencies { compile("org.springframework:spring-context:5.0.0.M5") testCompile("org.springframework:spring-test:5.0.0.M5") } Ivy依赖管理 如果您喜欢使用 Ivy 来管理依赖关系,那么还有类似的配置选项。 要配置Ivy指向Spring仓库,请将以下解析器添加到您的ivysettings.xml中: <resolvers> <ibiblio name="io.spring.repo.maven.release" m2compatible="true" root="http://repo.spring.io/release/"/> </resolvers> 您可以更改root从URL /release/到/milestone/或/snapshot/适当。 配置完成后,您可以在通常的方式添加依赖。例如(在ivy.xml): 您可以根据需要将rootURL从 /release/更改为/milestone/或/snapshot/。 配置完成后,您可以按通常的方式添加依赖项。例如(在ivy.xml中): <dependency org="org.springframework" name="spring-core" rev="5.0.0.M5" conf="compile->runtime"/> Zip文件发行 虽然使用依赖关系管理的构建系统是推荐的获取Spring框架的方法,但仍然可以下载发布的zip文件。 Zip文件发布到Spring Maven存储库(这仅仅是为了我们的方便,您不需要使用Maven或任何其他构建系统才能下载它们)。 要下载发布的zip文件,打开Web浏览器到http://repo.spring.io/release/org/springframework/spring,并为所需的版本选择相应的子文件夹。zip文件以-dist.zip结尾,例如spring-framework- {spring-version} -RELEASE-dist.zip。里程碑和 快照也会发布在这里。 ## 2.3.2 日志 日志是Spring非常重要的依赖,因为a)它是唯一的强制性外部依赖关系,b)每个人都喜欢看到他们使用的工具的一些输出,以及c)Spring集成了许多其他工具,都会具有日志依赖关系。应用程序开发人员的目标之一通常是将统一的日志配置放在整个应用程序的中央位置,包括所有外部组件。这比以前有更多的困难,因为有这么多的日志框架可以选择。 Spring中的强制性日志依赖关系是Jakarta Commons Logging API(JCL)。我们针对JCL进行编译,我们还使JCL Log 对象对于扩展了Spring Framework的类可见。对于用户来说,所有版本的Spring都使用相同的日志库很重要:迁移很简单,因为即使扩展了Spring的应用程序,但仍然保留了向后兼容性。我们这样做的方式是使Spring中的一个模块显式地依赖于commons-logging(遵循JCL规范的实现),然后在编译时使所有其他模块依赖于它。例如,如果您使用Maven,并且想知道在哪里可以获取对commons-logging的依赖,那么它来自Spring,特别是来自名为spring-core的中央模块。 commons-logging 的好处在于,您不需要任何其他操作来使您的应用程序正常工作。它具有运行时发现算法,可以在类路径中查找其他日志框架,并使用它认为合适的日志框架(或者您可以告诉它需要哪一个)。如果没有其他可用的,您可以从JDK(简称java.util.logging或JUL)获得好用的日志框架。您应该发现,在大多数情况下,您的Spring应用程序可以快乐地把日志输出到控制台,这很重要。 ## 不使用Commons Logging 不幸的是,commons-logging中的运行时发现算法虽然对最终用户很方便,但是也有很多问题。如果我们可以把时空倒回,现在开始使用Spring去开始一个新的项目,我们会使用不同的日志依赖关系。第一选择可能是Simple Logging Facade for Java(SLF4J),它也被许多其他使用Spring的应用工具所使用。 基本上有两种方法来关闭commons-logging: 1、 排除spring-core模块的依赖关系(因为它是明确依赖于commons-logging的唯一模块); 2、 依赖于一个特殊的commons-logging依赖关系,用空的jar代替库(更多的细节可以在; 要排除commons-logging,请将以下内容添加到dependencyManagement部分: <dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-core</artifactId> <version>5.0.0.M5</version> <exclusions> <exclusion> <groupId>commons-logging</groupId> <artifactId>commons-logging</artifactId> </exclusion> </exclusions> </dependency> </dependencies> 现在这个应用程序可能已经被破坏了,因为在类路径中没有实现JCL API,所以要修复它,必须提供一个新的JCL API。在下一节中,我们向您展示如何使用SLF4J提供JCL的替代实现。 ## 使用SLF4J SLF4J是一个更清洁的依赖关系,在运行时比commons-logging更有效率,因为它使用编译时绑定,而不是其集成的其他日志框架的运行时发现。这也意味着你必须更加明确地说明你在运行时想要发生什么,并声明它或相应地进行配置。 SLF4J提供对许多常见的日志框架的绑定,因此通常可以选择一个已经使用的日志框架,并绑定到该框架进行配置和管理。 SLF4J提供了绑定到许多常见的日志框架的方法,包括JCL,它也是可以反转的:是其他日志框架和自身(Spring)之间的桥梁。所以要使用SLF4J与Spring,您需要使用SLF4J-JCL bridge替换commons-logging依赖关系。一旦完成,那么在Spring中日志调用将被转换为对SLF4J API的日志调用,因此如果应用程序中的其他库使用该API,那么您有一个统一的地方来配置和管理日志记录。 常见的选择可能是将Spring链接到SLF4J,然后提供从SLF4J到Log4j的显式绑定。您需要提供多个依赖关系(并排除现有的commons-logging):the bridge,Log4j的SLF4J实现和Log4j实现本身。在Maven你会这样做: <dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-core</artifactId> <version>5.0.0.M5</version> <exclusions> <exclusion> <groupId>commons-logging</groupId> <artifactId>commons-logging</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>jcl-over-slf4j</artifactId> <version>1.7.22</version> </dependency> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-slf4j-impl</artifactId> <version>2.7</version> </dependency> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-api</artifactId> <version>2.7</version> </dependency> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-core</artifactId> <version>2.7</version> </dependency> </dependencies> 使用的Log4j Log4j的1.x版本已经寿终正寝,以下的内容特指Log4j 2 许多人使用Log4j 作为配置和管理日志的日志框架。它是高效和成熟的,当我们构建和测试Spring,实际上它是在运行时使用的。 Spring还提供了一些用于配置和初始化Log4j的实用功能,因此它在某些模块中对Log4j具有可选的编译时依赖性。 要使用JCL和Log4j,所有你需要做的就是把Log4j加到类路径,并为其提供一个配置文件(log4j2.xml,log4j2.properties或其他 支持的配置格式)。对于Maven用户,所需的最少依赖关系是: <dependencies> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-core</artifactId> <version>2.7</version> </dependency> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-jcl</artifactId> <version>2.7</version> </dependency> </dependencies> 如果你也想使用SLF4J,还需要以下依赖关系: <dependencies> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-slf4j-impl</artifactId> <version>2.7</version> </dependency> </dependencies> 下面是一个例子log4j2.xml用来把日志输出到控制台: <?xml version="1.0" encoding="UTF-8"?> <Configuration status="WARN"> <Appenders> <Console name="Console" target="SYSTEM_OUT"> <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/> </Console> </Appenders> <Loggers> <Logger name="org.springframework.beans.factory" level="DEBUG"/> <Root level="error"> <AppenderRef ref="Console"/> </Root> </Loggers> </Configuration> 运行时容器和原生JCL 许多人在那些本身提供JCL实现的容器中运行他们的Spring应用程序。IBM WebSphere应用服务器(WAS)为例。这往往会导致问题,遗憾的是没有一劳永逸的解决方案; 在大多数情况下,简单地从您的应用程序排除commons-logging是不够的。 要清楚这一点:报告的问题通常不是JCL本身,甚至commons-logging:而是将commons-logging绑定到另一个框架(通常是Log4j)。这可能会失败,因为commons-logging更改了在一些容器中发现的旧版本(1.0)和大多数人现在使用的版本(1.1)之间执行运行时发现的方式。 Spring不使用JCL API的任何不寻常的部分,所以没有什么破坏,但是一旦Spring或您的应用程序尝试输出日志,您可以发现与Log4j的绑定不起作用 在这种情况下,使用WAS最简单的方法是反转类加载器层次结构(IBM将其称为”parent last”),以便应用程序控制JCL依赖关系,而不是容器。该选项并不总是开放的,但是在公共领域还有许多其他建议可供选择,您的里程(集成程度)可能因容器的确切版本和功能集而异。 ## 07、Spring源码分析 - 07-BeanWrapper beans包里一个非常重要的类是BeanWrapper接口和它的相应实现(BeanWrapperImpl)。BeanWrapper提供了设置和获取属性值(单独或批量)、获取属性描述符以及查询属性以确定它们是可读还是可写的功能。BeanWrapper还提供对嵌套属性的支持,能够不受嵌套深度的限制启用子属性的属性设置。然后,BeanWrapper提供了无需目标类代码的支持就能够添加标准JavaBeans的PropertyChangeListeners和VetoableChangeListeners的能力。最后然而并非最不重要的是,BeanWrapper提供了对索引属性设置的支持。BeanWrapper通常不会被应用程序的代码直接使用,而是由DataBinder和BeanFactory使用。 BeanWrapper的名字已经部分暗示了它的工作方式:它包装一个bean以对其执行操作,比如设置和获取属性。 ## 设置并获取基本和嵌套属性 使用**setPropertyValue(s)和getPropertyValue(s)**可以设置并获取属性,两者都带有几个重载方法。在Spring自带的java文档中对它们有更详细的描述。重要的是要知道对象属性指示的几个约定。几个例子: account.name getAccount().setName() getAccount().getName() account[COMPANYNAME] BeanWrapperImpl继承关系。 BeanWrapperImpl实现了诸多接口,下面从上往下依次接受这些接口以及实现类。 PropertyEditorRegistry提供了属性编辑器注册与查找的方法,属性编辑器相关请看这里《属性编辑器PropertyEditor》。 public interface PropertyEditorRegistry { void registerCustomEditor(Class<?> requiredType, PropertyEditor propertyEditor); void registerCustomEditor(@Nullable Class<?> requiredType, @Nullable String propertyPath, PropertyEditor propertyEditor); @Nullable PropertyEditor findCustomEditor(@Nullable Class<?> requiredType, @Nullable String propertyPath); } Spring还定义了一个接口用于为PropertyEditorRegistry批量注册属性编辑器。 public interface PropertyEditorRegistrar { void registerCustomEditors(PropertyEditorRegistry registry); } 对于在Spring ApplicationContext中批量注册属性编辑器可是使用CustomEditorConfigurer。 @Override public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { if (this.propertyEditorRegistrars != null) { for (PropertyEditorRegistrar propertyEditorRegistrar : this.propertyEditorRegistrars) { beanFactory.addPropertyEditorRegistrar(propertyEditorRegistrar); } } if (this.customEditors != null) { this.customEditors.forEach(beanFactory::registerCustomEditor); } } 下面是ResourceEditorRegistrar批量注册属性编辑器的实现。 public class ResourceEditorRegistrar implements PropertyEditorRegistrar { private final PropertyResolver propertyResolver; private final ResourceLoader resourceLoader; public ResourceEditorRegistrar(ResourceLoader resourceLoader, PropertyResolver propertyResolver) { this.resourceLoader = resourceLoader; this.propertyResolver = propertyResolver; } @Override public void registerCustomEditors(PropertyEditorRegistry registry) { ResourceEditor baseEditor = new ResourceEditor(this.resourceLoader, this.propertyResolver); doRegisterEditor(registry, Resource.class, baseEditor); doRegisterEditor(registry, ContextResource.class, baseEditor); doRegisterEditor(registry, InputStream.class, new InputStreamEditor(baseEditor)); doRegisterEditor(registry, InputSource.class, new InputSourceEditor(baseEditor)); doRegisterEditor(registry, File.class, new FileEditor(baseEditor)); doRegisterEditor(registry, Path.class, new PathEditor(baseEditor)); doRegisterEditor(registry, Reader.class, new ReaderEditor(baseEditor)); doRegisterEditor(registry, URL.class, new URLEditor(baseEditor)); ClassLoader classLoader = this.resourceLoader.getClassLoader(); doRegisterEditor(registry, URI.class, new URIEditor(classLoader)); doRegisterEditor(registry, Class.class, new ClassEditor(classLoader)); doRegisterEditor(registry, Class[].class, new ClassArrayEditor(classLoader)); if (this.resourceLoader instanceof ResourcePatternResolver) { doRegisterEditor(registry, Resource[].class, new ResourceArrayPropertyEditor((ResourcePatternResolver) this.resourceLoader, this.propertyResolver)); } } private void doRegisterEditor(PropertyEditorRegistry registry, Class<?> requiredType, PropertyEditor editor) { if (registry instanceof PropertyEditorRegistrySupport) { ((PropertyEditorRegistrySupport) registry).overrideDefaultEditor(requiredType, editor); } else { registry.registerCustomEditor(requiredType, editor); } } } PropertyEditorRegistry实现类有两个分支一个是PropertyEditorRegistrySupport和DataBinder。DataBinder以后再介绍。 PropertyEditorRegistrySupport主要成员变量。 @Nullable private ConversionService conversionService;//可以注入,通过getConversionService()方法获取。 private boolean defaultEditorsActive = false;//为true,getDefaultEditor()方法才会注册默认提供的属性编辑器。 private boolean configValueEditorsActive = false;//为true会为String[].class,short[].class,int[].class,long[].class注册一个StringArrayPropertyEditor @Nullable private Map<Class<?>, PropertyEditor> defaultEditors;//默认属性编辑器的容器 @Nullable private Map<Class<?>, PropertyEditor> overriddenDefaultEditors;//用于覆盖默认属性编辑器 @Nullable private Map<Class<?>, PropertyEditor> customEditors;//自定义属性编辑器 @Nullable private Map<String, CustomEditorHolder> customEditorsForPath;//带path的 PropertyEditorRegistrySupport提供了PropertyEditorRegistry的默认实现,并且getDefaultEditor()方法会注册一批Spring提供的属性编辑器。在PropertyEditorRegistrySupport这个分支的集成体系下,TypeConverterSupport的convertIfNecessary()方法会调用到getDefaultEditor()方法,这个稍后再讲。 public PropertyEditor getDefaultEditor(Class<?> requiredType) { if (!this.defaultEditorsActive) { return null; } if (this.overriddenDefaultEditors != null) { PropertyEditor editor = this.overriddenDefaultEditors.get(requiredType); if (editor != null) { return editor; } } if (this.defaultEditors == null) { createDefaultEditors();//注册默认提供了属性编辑器 } return this.defaultEditors.get(requiredType); } private void createDefaultEditors() { this.defaultEditors = new HashMap<>(64); // Simple editors, without parameterization capabilities. // The JDK does not contain a default editor for any of these target types. this.defaultEditors.put(Charset.class, new CharsetEditor()); this.defaultEditors.put(Class.class, new ClassEditor()); this.defaultEditors.put(Class[].class, new ClassArrayEditor()); this.defaultEditors.put(Currency.class, new CurrencyEditor()); this.defaultEditors.put(File.class, new FileEditor()); this.defaultEditors.put(InputStream.class, new InputStreamEditor()); this.defaultEditors.put(InputSource.class, new InputSourceEditor()); this.defaultEditors.put(Locale.class, new LocaleEditor()); this.defaultEditors.put(Path.class, new PathEditor()); this.defaultEditors.put(Pattern.class, new PatternEditor()); this.defaultEditors.put(Properties.class, new PropertiesEditor()); this.defaultEditors.put(Reader.class, new ReaderEditor()); this.defaultEditors.put(Resource[].class, new ResourceArrayPropertyEditor()); this.defaultEditors.put(TimeZone.class, new TimeZoneEditor()); this.defaultEditors.put(URI.class, new URIEditor()); this.defaultEditors.put(URL.class, new URLEditor()); this.defaultEditors.put(UUID.class, new UUIDEditor()); this.defaultEditors.put(ZoneId.class, new ZoneIdEditor()); // Default instances of collection editors. // Can be overridden by registering custom instances of those as custom editors. this.defaultEditors.put(Collection.class, new CustomCollectionEditor(Collection.class)); this.defaultEditors.put(Set.class, new CustomCollectionEditor(Set.class)); this.defaultEditors.put(SortedSet.class, new CustomCollectionEditor(SortedSet.class)); this.defaultEditors.put(List.class, new CustomCollectionEditor(List.class)); this.defaultEditors.put(SortedMap.class, new CustomMapEditor(SortedMap.class)); // Default editors for primitive arrays. this.defaultEditors.put(byte[].class, new ByteArrayPropertyEditor()); this.defaultEditors.put(char[].class, new CharArrayPropertyEditor()); // The JDK does not contain a default editor for char! this.defaultEditors.put(char.class, new CharacterEditor(false)); this.defaultEditors.put(Character.class, new CharacterEditor(true)); // Spring's CustomBooleanEditor accepts more flag values than the JDK's default editor. this.defaultEditors.put(boolean.class, new CustomBooleanEditor(false)); this.defaultEditors.put(Boolean.class, new CustomBooleanEditor(true)); // The JDK does not contain default editors for number wrapper types! // Override JDK primitive number editors with our own CustomNumberEditor. this.defaultEditors.put(byte.class, new CustomNumberEditor(Byte.class, false)); this.defaultEditors.put(Byte.class, new CustomNumberEditor(Byte.class, true)); this.defaultEditors.put(short.class, new CustomNumberEditor(Short.class, false)); this.defaultEditors.put(Short.class, new CustomNumberEditor(Short.class, true)); this.defaultEditors.put(int.class, new CustomNumberEditor(Integer.class, false)); this.defaultEditors.put(Integer.class, new CustomNumberEditor(Integer.class, true)); this.defaultEditors.put(long.class, new CustomNumberEditor(Long.class, false)); this.defaultEditors.put(Long.class, new CustomNumberEditor(Long.class, true)); this.defaultEditors.put(float.class, new CustomNumberEditor(Float.class, false)); this.defaultEditors.put(Float.class, new CustomNumberEditor(Float.class, true)); this.defaultEditors.put(double.class, new CustomNumberEditor(Double.class, false)); this.defaultEditors.put(Double.class, new CustomNumberEditor(Double.class, true)); this.defaultEditors.put(BigDecimal.class, new CustomNumberEditor(BigDecimal.class, true)); this.defaultEditors.put(BigInteger.class, new CustomNumberEditor(BigInteger.class, true)); // Only register config value editors if explicitly requested. if (this.configValueEditorsActive) { StringArrayPropertyEditor sae = new StringArrayPropertyEditor(); this.defaultEditors.put(String[].class, sae); this.defaultEditors.put(short[].class, sae); this.defaultEditors.put(int[].class, sae); this.defaultEditors.put(long[].class, sae); } } 如果上面默认类型没有我们需要的,可以调用registerCustomEditor()方法为我们的类型注册属性编辑器。 @Override public void registerCustomEditor(Class<?> requiredType, PropertyEditor propertyEditor) { registerCustomEditor(requiredType, null, propertyEditor); } @Override public void registerCustomEditor(@Nullable Class<?> requiredType, @Nullable String propertyPath, PropertyEditor propertyEditor) { if (requiredType == null && propertyPath == null) { throw new IllegalArgumentException("Either requiredType or propertyPath is required"); } if (propertyPath != null) { if (this.customEditorsForPath == null) { this.customEditorsForPath = new LinkedHashMap<>(16); } this.customEditorsForPath.put(propertyPath, new CustomEditorHolder(propertyEditor, requiredType)); } else { if (this.customEditors == null) { this.customEditors = new LinkedHashMap<>(16); } this.customEditors.put(requiredType, propertyEditor); this.customEditorCache = null; } } 如果默认注册的属性编辑器不能满足我们的需求,可以使用overrideDefaultEditor()方法。因为getDefaultEditor()方法优先从overriddenDefaultEditors查找。 public void overrideDefaultEditor(Class<?> requiredType, PropertyEditor propertyEditor) { if (this.overriddenDefaultEditors == null) { this.overriddenDefaultEditors = new HashMap<>(); } this.overriddenDefaultEditors.put(requiredType, propertyEditor); } 查找属性编辑器的过程,如果传入的是带path的,会先尝试从customEditorsForPath中查找,如果不存在则试图去掉path中的“[...]”在试图从customEditorsForPath查找。如果requiredType==null,调用getPropertyType()返回一个Class对象,默认返回null子类可覆盖。不能从path得到属性编辑器,则再从customEditors查找。 @Override @Nullable public PropertyEditor findCustomEditor(@Nullable Class<?> requiredType, @Nullable String propertyPath) { Class<?> requiredTypeToUse = requiredType; if (propertyPath != null) { if (this.customEditorsForPath != null) { // Check property-specific editor first. PropertyEditor editor = getCustomEditor(propertyPath, requiredType); if (editor == null) { List<String> strippedPaths = new LinkedList<>(); addStrippedPropertyPaths(strippedPaths, "", propertyPath); for (Iterator<String> it = strippedPaths.iterator(); it.hasNext() && editor == null;) { String strippedPath = it.next(); editor = getCustomEditor(strippedPath, requiredType); } } if (editor != null) { return editor; } } if (requiredType == null) { requiredTypeToUse = getPropertyType(propertyPath); } } // No property-specific editor -> check type-specific editor. return getCustomEditor(requiredTypeToUse); } private void addStrippedPropertyPaths(List<String> strippedPaths, String nestedPath, String propertyPath) { int startIndex = propertyPath.indexOf(PropertyAccessor.PROPERTY_KEY_PREFIX_CHAR);//[ if (startIndex != -1) { int endIndex = propertyPath.indexOf(PropertyAccessor.PROPERTY_KEY_SUFFIX_CHAR);//] if (endIndex != -1) { String prefix = propertyPath.substring(0, startIndex); String key = propertyPath.substring(startIndex, endIndex + 1); String suffix = propertyPath.substring(endIndex + 1, propertyPath.length()); // Strip the first key. strippedPaths.add(nestedPath + prefix + suffix); // Search for further keys to strip, with the first key stripped. addStrippedPropertyPaths(strippedPaths, nestedPath + prefix, suffix); // Search for further keys to strip, with the first key not stripped. addStrippedPropertyPaths(strippedPaths, nestedPath + prefix + key, suffix); } } } 根据propertyName从customEditorsForPath中推断出bean的Class。 protected Class<?> guessPropertyTypeFromEditors(String propertyName) { if (this.customEditorsForPath != null) { CustomEditorHolder editorHolder = this.customEditorsForPath.get(propertyName); if (editorHolder == null) { List<String> strippedPaths = new LinkedList<>(); addStrippedPropertyPaths(strippedPaths, "", propertyName); for (Iterator<String> it = strippedPaths.iterator(); it.hasNext() && editorHolder == null;) { String strippedName = it.next(); editorHolder = this.customEditorsForPath.get(strippedName); } } if (editorHolder != null) { return editorHolder.getRegisteredType(); } } return null; } 上面提到TypeConverterSupport的convertIfNecessary()方法会触发getDefaultEditor()方法完成默认属性编辑器的注册,这个类实现了TypeConverter接口,convertIfNecessary()方法就定义在这个接口。 public interface TypeConverter { @Nullable <T> T convertIfNecessary(@Nullable Object value, @Nullable Class<T> requiredType) throws TypeMismatchException; @Nullable <T> T convertIfNecessary(@Nullable Object value, @Nullable Class<T> requiredType, @Nullable MethodParameter methodParam) throws TypeMismatchException; @Nullable <T> T convertIfNecessary(@Nullable Object value, @Nullable Class<T> requiredType, @Nullable Field field) throws TypeMismatchException; } public abstract class TypeConverterSupport extends PropertyEditorRegistrySupport implements TypeConverter { @Nullable TypeConverterDelegate typeConverterDelegate; @Override @Nullable public <T> T convertIfNecessary(@Nullable Object value, @Nullable Class<T> requiredType) throws TypeMismatchException { return doConvert(value, requiredType, null, null); } @Override @Nullable public <T> T convertIfNecessary(@Nullable Object value, @Nullable Class<T> requiredType, @Nullable MethodParameter methodParam) throws TypeMismatchException { return doConvert(value, requiredType, methodParam, null); } @Override @Nullable public <T> T convertIfNecessary(@Nullable Object value, @Nullable Class<T> requiredType, @Nullable Field field) throws TypeMismatchException { return doConvert(value, requiredType, null, field); } @Nullable private <T> T doConvert(@Nullable Object value,@Nullable Class<T> requiredType, @Nullable MethodParameter methodParam, @Nullable Field field) throws TypeMismatchException { Assert.state(this.typeConverterDelegate != null, "No TypeConverterDelegate"); try { if (field != null) { return this.typeConverterDelegate.convertIfNecessary(value, requiredType, field); } else { return this.typeConverterDelegate.convertIfNecessary(value, requiredType, methodParam); } } catch (ConverterNotFoundException | IllegalStateException ex) { throw new ConversionNotSupportedException(value, requiredType, ex); } catch (ConversionException | IllegalArgumentException ex) { throw new TypeMismatchException(value, requiredType, ex); } } } 三个convertIfNecessary()接口方法都是调用doConvert()方法,内部又是委托了TypeConverterDelegate对象,TypeConverterSupport是一个抽象类,需要其子类实例化typeConverterDelegate成员变量。这正的转化都是通过typeConverterDelegate的convertIfNecessary()方法完成的。 TypeConverterDelegate通过构造方法持有一个PropertyEditorRegistrySupport一个引用,核心方法是: @Nullable public <T> T convertIfNecessary(@Nullable String propertyName, @Nullable Object oldValue, @Nullable Object newValue, @Nullable Class<T> requiredType, @Nullable TypeDescriptor typeDescriptor) throws IllegalArgumentException { // Custom editor for this type? PropertyEditor editor = this.propertyEditorRegistry.findCustomEditor(requiredType, propertyName); ConversionFailedException conversionAttemptEx = null; // No custom editor but custom ConversionService specified? ConversionService conversionService = this.propertyEditorRegistry.getConversionService(); if (editor == null && conversionService != null && newValue != null && typeDescriptor != null) { TypeDescriptor sourceTypeDesc = TypeDescriptor.forObject(newValue); if (conversionService.canConvert(sourceTypeDesc, typeDescriptor)) { try { return (T) conversionService.convert(newValue, sourceTypeDesc, typeDescriptor); } catch (ConversionFailedException ex) { // fallback to default conversion logic below conversionAttemptEx = ex; } } } Object convertedValue = newValue; // Value not of required type? if (editor != null || (requiredType != null && !ClassUtils.isAssignableValue(requiredType, convertedValue))) { if (typeDescriptor != null && requiredType != null && Collection.class.isAssignableFrom(requiredType) && convertedValue instanceof String) { TypeDescriptor elementTypeDesc = typeDescriptor.getElementTypeDescriptor(); if (elementTypeDesc != null) { Class<?> elementType = elementTypeDesc.getType(); if (Class.class == elementType || Enum.class.isAssignableFrom(elementType)) { convertedValue = StringUtils.commaDelimitedListToStringArray((String) convertedValue);//目标是集合类型支持逗号分隔 } } } if (editor == null) { editor = findDefaultEditor(requiredType);//在这完成的默认属性编辑器的注册 } convertedValue = doConvertValue(oldValue, convertedValue, requiredType, editor); } boolean standardConversion = false; if (requiredType != null) { // Try to apply some standard type conversion rules if appropriate. if (convertedValue != null) { if (Object.class == requiredType) { return (T) convertedValue; } else if (requiredType.isArray()) { // Array required -> apply appropriate conversion of elements. if (convertedValue instanceof String && Enum.class.isAssignableFrom(requiredType.getComponentType())) { convertedValue = StringUtils.commaDelimitedListToStringArray((String) convertedValue); } return (T) convertToTypedArray(convertedValue, propertyName, requiredType.getComponentType()); } else if (convertedValue instanceof Collection) { // Convert elements to target type, if determined. convertedValue = convertToTypedCollection( (Collection<?>) convertedValue, propertyName, requiredType, typeDescriptor); standardConversion = true; } else if (convertedValue instanceof Map) { // Convert keys and values to respective target type, if determined. convertedValue = convertToTypedMap( (Map<?, ?>) convertedValue, propertyName, requiredType, typeDescriptor); standardConversion = true; } if (convertedValue.getClass().isArray() && Array.getLength(convertedValue) == 1) { convertedValue = Array.get(convertedValue, 0); standardConversion = true; } if (String.class == requiredType && ClassUtils.isPrimitiveOrWrapper(convertedValue.getClass())) { // We can stringify any primitive value... return (T) convertedValue.toString(); } else if (convertedValue instanceof String && !requiredType.isInstance(convertedValue)) { if (conversionAttemptEx == null && !requiredType.isInterface() && !requiredType.isEnum()) { try { Constructor<T> strCtor = requiredType.getConstructor(String.class); return BeanUtils.instantiateClass(strCtor, convertedValue); } catch (NoSuchMethodException ex) { // proceed with field lookup if (logger.isTraceEnabled()) { logger.trace("No String constructor found on type [" + requiredType.getName() + "]", ex); } } catch (Exception ex) { if (logger.isDebugEnabled()) { logger.debug("Construction via String failed for type [" + requiredType.getName() + "]", ex); } } } String trimmedValue = ((String) convertedValue).trim(); if (requiredType.isEnum() && "".equals(trimmedValue)) { // It's an empty enum identifier: reset the enum value to null. return null; } convertedValue = attemptToConvertStringToEnum(requiredType, trimmedValue, convertedValue); standardConversion = true; } else if (convertedValue instanceof Number && Number.class.isAssignableFrom(requiredType)) { convertedValue = NumberUtils.convertNumberToTargetClass( (Number) convertedValue, (Class<Number>) requiredType); standardConversion = true; } } else { // convertedValue == null if (requiredType == Optional.class) { convertedValue = Optional.empty(); } } if (!ClassUtils.isAssignableValue(requiredType, convertedValue)) { if (conversionAttemptEx != null) { // Original exception from former ConversionService call above... throw conversionAttemptEx; } else if (conversionService != null && typeDescriptor != null) { // ConversionService not tried before, probably custom editor found // but editor couldn't produce the required type... TypeDescriptor sourceTypeDesc = TypeDescriptor.forObject(newValue); if (conversionService.canConvert(sourceTypeDesc, typeDescriptor)) { return (T) conversionService.convert(newValue, sourceTypeDesc, typeDescriptor); } } // Definitely doesn't match: throw IllegalArgumentException/IllegalStateException StringBuilder msg = new StringBuilder(); msg.append("Cannot convert value of type '").append(ClassUtils.getDescriptiveType(newValue)); msg.append("' to required type '").append(ClassUtils.getQualifiedName(requiredType)).append("'"); if (propertyName != null) { msg.append(" for property '").append(propertyName).append("'"); } if (editor != null) { msg.append(": PropertyEditor [").append(editor.getClass().getName()).append( "] returned inappropriate value of type '").append( ClassUtils.getDescriptiveType(convertedValue)).append("'"); throw new IllegalArgumentException(msg.toString()); } else { msg.append(": no matching editors or conversion strategy found"); throw new IllegalStateException(msg.toString()); } } } if (conversionAttemptEx != null) { if (editor == null && !standardConversion && requiredType != null && Object.class != requiredType) { throw conversionAttemptEx; } logger.debug("Original ConversionService attempt failed - ignored since " + "PropertyEditor based conversion eventually succeeded", conversionAttemptEx); } return (T) convertedValue; } 首先从propertyEditorRegistry得到属性编辑器editor,如果editor==null并且propertyEditorRegistry含有ConversionService对象,则尝试使用ConversionService完成属性值转换。如果editor==null,findDefaultEditor()方法中调用propertyEditorRegistry.getDefaultEditor(requiredType)注册默认属性编辑器并从默认的属性编辑器查找requiredType对应的返回。doConvertValue()方法使用requiredType对应的一个默认的属性编辑器进行属性转换(针对convertedValue是String或String[])。后面是针对数组集合或Map的转换,不再一一查看了。 PropertyAccessor接口定义了对象属性读写相关的方法: public interface PropertyAccessor { String NESTED_PROPERTY_SEPARATOR = "."; char NESTED_PROPERTY_SEPARATOR_CHAR = '.'; String PROPERTY_KEY_PREFIX = "["; char PROPERTY_KEY_PREFIX_CHAR = '['; String PROPERTY_KEY_SUFFIX = "]"; char PROPERTY_KEY_SUFFIX_CHAR = ']'; boolean isReadableProperty(String propertyName); boolean isWritableProperty(String propertyName); @Nullable Class<?> getPropertyType(String propertyName) throws BeansException; @Nullable TypeDescriptor getPropertyTypeDescriptor(String propertyName) throws BeansException; @Nullable Object getPropertyValue(String propertyName) throws BeansException; void setPropertyValue(String propertyName, @Nullable Object value) throws BeansException; void setPropertyValue(PropertyValue pv) throws BeansException; void setPropertyValues(Map<?, ?> map) throws BeansException; void setPropertyValues(PropertyValues pvs) throws BeansException; void setPropertyValues(PropertyValues pvs, boolean ignoreUnknown) throws BeansException; void setPropertyValues(PropertyValues pvs, boolean ignoreUnknown, boolean ignoreInvalid) throws BeansException; } ConfigurablePropertyAccessor又扩展了PropertyAccessor。 public interface ConfigurablePropertyAccessor extends PropertyAccessor, PropertyEditorRegistry, TypeConverter { void setConversionService(@Nullable ConversionService conversionService); @Nullable ConversionService getConversionService(); void setExtractOldValueForEditor(boolean extractOldValueForEditor); boolean isExtractOldValueForEditor(); void setAutoGrowNestedPaths(boolean autoGrowNestedPaths);//对一个为null的属性设置值是是否实例化这个属性 boolean isAutoGrowNestedPaths(); } AbstractPropertyAccessor中主要处理了ignoreUnknown与ignoreInvalid,其读写属性值交给抽象方法getPropertyValue()和setPropertyValue()由子类完成具体的实现。 public abstract class AbstractPropertyAccessor extends TypeConverterSupport implements ConfigurablePropertyAccessor { private boolean extractOldValueForEditor = false; private boolean autoGrowNestedPaths = false; @Override public void setExtractOldValueForEditor(boolean extractOldValueForEditor) { this.extractOldValueForEditor = extractOldValueForEditor; } @Override public boolean isExtractOldValueForEditor() { return this.extractOldValueForEditor; } @Override public void setAutoGrowNestedPaths(boolean autoGrowNestedPaths) { this.autoGrowNestedPaths = autoGrowNestedPaths; } @Override public boolean isAutoGrowNestedPaths() { return this.autoGrowNestedPaths; } @Override public void setPropertyValue(PropertyValue pv) throws BeansException { setPropertyValue(pv.getName(), pv.getValue()); } @Override public void setPropertyValues(Map<?, ?> map) throws BeansException { setPropertyValues(new MutablePropertyValues(map)); } @Override public void setPropertyValues(PropertyValues pvs) throws BeansException { setPropertyValues(pvs, false, false); } @Override public void setPropertyValues(PropertyValues pvs, boolean ignoreUnknown) throws BeansException { setPropertyValues(pvs, ignoreUnknown, false); } @Override public void setPropertyValues(PropertyValues pvs, boolean ignoreUnknown, boolean ignoreInvalid) throws BeansException { List<PropertyAccessException> propertyAccessExceptions = null; List<PropertyValue> propertyValues = (pvs instanceof MutablePropertyValues ? ((MutablePropertyValues) pvs).getPropertyValueList() : Arrays.asList(pvs.getPropertyValues())); for (PropertyValue pv : propertyValues) { try { // This method may throw any BeansException, which won't be caught // here, if there is a critical failure such as no matching field. // We can attempt to deal only with less serious exceptions. setPropertyValue(pv); } catch (NotWritablePropertyException ex) { if (!ignoreUnknown) { throw ex; } // Otherwise, just ignore it and continue... } catch (NullValueInNestedPathException ex) { if (!ignoreInvalid) { throw ex; } // Otherwise, just ignore it and continue... } catch (PropertyAccessException ex) { if (propertyAccessExceptions == null) { propertyAccessExceptions = new LinkedList<>(); } propertyAccessExceptions.add(ex); } } // If we encountered individual exceptions, throw the composite exception. if (propertyAccessExceptions != null) { PropertyAccessException[] paeArray = propertyAccessExceptions.toArray(new PropertyAccessException[propertyAccessExceptions.size()]); throw new PropertyBatchUpdateException(paeArray); } } // Redefined with public visibility. @Override @Nullable public Class<?> getPropertyType(String propertyPath) { return null; } @Override @Nullable public abstract Object getPropertyValue(String propertyName) throws BeansException; @Override public abstract void setPropertyValue(String propertyName, @Nullable Object value) throws BeansException; } AbstractNestablePropertyAccessor实现了读取写入的具体实现,它有四个重要的成员变量: @Nullable Object wrappedObject;//当前属性所代表的对象 private String nestedPath = "";//当前属性所在父对象的路径 @Nullable Object rootObject;//父对象 /** Map with cached nested Accessors: nested path -> Accessor instance */ @Nullable private Map<String, AbstractNestablePropertyAccessor> nestedPropertyAccessors;//当前对象的属性路径与该属性PropertyAccessor的映射缓存 @Override public void setPropertyValue(String propertyName, @Nullable Object value) throws BeansException { AbstractNestablePropertyAccessor nestedPa; try { nestedPa = getPropertyAccessorForPropertyPath(propertyName); } catch (NotReadablePropertyException ex) { throw new NotWritablePropertyException(getRootClass(), this.nestedPath + propertyName, "Nested property in path '" + propertyName + "' does not exist", ex); } PropertyTokenHolder tokens = getPropertyNameTokens(getFinalPath(nestedPa, propertyName)); nestedPa.setPropertyValue(tokens, new PropertyValue(propertyName, value)); } 对于设置属性值: 1、 首先找到该propertyName对应的AbstractNestablePropertyAccessor; protected AbstractNestablePropertyAccessor getPropertyAccessorForPropertyPath(String propertyPath) { int pos = PropertyAccessorUtils.getFirstNestedPropertySeparatorIndex(propertyPath); // Handle nested properties recursively. if (pos > -1) { String nestedProperty = propertyPath.substring(0, pos); String nestedPath = propertyPath.substring(pos + 1); AbstractNestablePropertyAccessor nestedPa = getNestedPropertyAccessor(nestedProperty); return nestedPa.getPropertyAccessorForPropertyPath(nestedPath); } else { return this; } } 1、 1根据第一个非中括号内逗号,找到当前nestedPa的顶层属性; 1、 1.1如果pos==-1,则说明propertyPath是一个非嵌套属性,直接返回当前对象; 1、 1.2如果pos>-1则代表propertyPath是一个嵌套属性这时候需要沿着根对象一层一层向下找,直到当前层的propertyPath又是一个非嵌套属性; private AbstractNestablePropertyAccessor getNestedPropertyAccessor(String nestedProperty) { if (this.nestedPropertyAccessors == null) { this.nestedPropertyAccessors = new HashMap<>(); } // Get value of bean property. PropertyTokenHolder tokens = getPropertyNameTokens(nestedProperty); String canonicalName = tokens.canonicalName; Object value = getPropertyValue(tokens); if (value == null || (value instanceof Optional && !((Optional) value).isPresent())) { if (isAutoGrowNestedPaths()) { value = setDefaultValue(tokens); } else { throw new NullValueInNestedPathException(getRootClass(), this.nestedPath + canonicalName); } } // Lookup cached sub-PropertyAccessor, create new one if not found. AbstractNestablePropertyAccessor nestedPa = this.nestedPropertyAccessors.get(canonicalName); if (nestedPa == null || nestedPa.getWrappedInstance() != ObjectUtils.unwrapOptional(value)) { if (logger.isTraceEnabled()) { logger.trace("Creating new nested " + getClass().getSimpleName() + " for property '" + canonicalName + "'"); } nestedPa = newNestedPropertyAccessor(value, this.nestedPath + canonicalName + NESTED_PROPERTY_SEPARATOR); // Inherit all type-specific PropertyEditors. copyDefaultEditorsTo(nestedPa); copyCustomEditorsTo(nestedPa, canonicalName); this.nestedPropertyAccessors.put(canonicalName, nestedPa); } else { if (logger.isTraceEnabled()) { logger.trace("Using cached nested property accessor for property '" + canonicalName + "'"); } } return nestedPa; } 1、 1.2.1将nestedProperty转换为PropertyTokenHolder; private PropertyTokenHolder getPropertyNameTokens(String propertyName) { String actualName = null; List<String> keys = new ArrayList<>(2); int searchIndex = 0; while (searchIndex != -1) { int keyStart = propertyName.indexOf(PROPERTY_KEY_PREFIX, searchIndex); searchIndex = -1; if (keyStart != -1) { int keyEnd = propertyName.indexOf(PROPERTY_KEY_SUFFIX, keyStart + PROPERTY_KEY_PREFIX.length()); if (keyEnd != -1) { if (actualName == null) { actualName = propertyName.substring(0, keyStart); } String key = propertyName.substring(keyStart + PROPERTY_KEY_PREFIX.length(), keyEnd); if (key.length() > 1 && (key.startsWith("'") && key.endsWith("'")) || (key.startsWith("\"") && key.endsWith("\""))) { key = key.substring(1, key.length() - 1); } keys.add(key); searchIndex = keyEnd + PROPERTY_KEY_SUFFIX.length(); } } } PropertyTokenHolder tokens = new PropertyTokenHolder(actualName != null ? actualName : propertyName); if (!keys.isEmpty()) { tokens.canonicalName += PROPERTY_KEY_PREFIX + StringUtils.collectionToDelimitedString(keys, PROPERTY_KEY_SUFFIX + PROPERTY_KEY_PREFIX) + PROPERTY_KEY_SUFFIX; tokens.keys = StringUtils.toStringArray(keys); } return tokens; } 1、 1.2.2读取当前嵌套属性的值; protected Object getPropertyValue(PropertyTokenHolder tokens) throws BeansException { String propertyName = tokens.canonicalName; String actualName = tokens.actualName; PropertyHandler ph = getLocalPropertyHandler(actualName); if (ph == null || !ph.isReadable()) { throw new NotReadablePropertyException(getRootClass(), this.nestedPath + propertyName); } try { Object value = ph.getValue(); if (tokens.keys != null) { if (value == null) { if (isAutoGrowNestedPaths()) { value = setDefaultValue(new PropertyTokenHolder(tokens.actualName)); } else { throw new NullValueInNestedPathException(getRootClass(), this.nestedPath + propertyName, "Cannot access indexed value of property referenced in indexed " + "property path '" + propertyName + "': returned null"); } } String indexedPropertyName = tokens.actualName; // apply indexes and map keys for (int i = 0; i < tokens.keys.length; i++) { String key = tokens.keys[i]; if (value == null) { throw new NullValueInNestedPathException(getRootClass(), this.nestedPath + propertyName, "Cannot access indexed value of property referenced in indexed " + "property path '" + propertyName + "': returned null"); } else if (value.getClass().isArray()) { int index = Integer.parseInt(key); value = growArrayIfNecessary(value, index, indexedPropertyName); value = Array.get(value, index); } else if (value instanceof List) { int index = Integer.parseInt(key); List<Object> list = (List<Object>) value; growCollectionIfNecessary(list, index, indexedPropertyName, ph, i + 1); value = list.get(index); } else if (value instanceof Set) { // Apply index to Iterator in case of a Set. Set<Object> set = (Set<Object>) value; int index = Integer.parseInt(key); if (index < 0 || index >= set.size()) { throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName, "Cannot get element with index " + index + " from Set of size " + set.size() + ", accessed using property path '" + propertyName + "'"); } Iterator<Object> it = set.iterator(); for (int j = 0; it.hasNext(); j++) { Object elem = it.next(); if (j == index) { value = elem; break; } } } else if (value instanceof Map) { Map<Object, Object> map = (Map<Object, Object>) value; Class<?> mapKeyType = ph.getResolvableType().getNested(i + 1).asMap().resolveGeneric(0); // IMPORTANT: Do not pass full property name in here - property editors // must not kick in for map keys but rather only for map values. TypeDescriptor typeDescriptor = TypeDescriptor.valueOf(mapKeyType); Object convertedMapKey = convertIfNecessary(null, null, key, mapKeyType, typeDescriptor); value = map.get(convertedMapKey); } else { throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName, "Property referenced in indexed property path '" + propertyName + "' is neither an array nor a List nor a Set nor a Map; returned value was [" + value + "]"); } indexedPropertyName += PROPERTY_KEY_PREFIX + key + PROPERTY_KEY_SUFFIX; } } return value; } catch (IndexOutOfBoundsException ex) { throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName, "Index of out of bounds in property path '" + propertyName + "'", ex); } catch (NumberFormatException ex) { throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName, "Invalid index in property path '" + propertyName + "'", ex); } catch (TypeMismatchException ex) { throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName, "Invalid index in property path '" + propertyName + "'", ex); } catch (InvocationTargetException ex) { throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName, "Getter for property '" + actualName + "' threw exception", ex); } catch (Exception ex) { throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName, "Illegal attempt to get property '" + actualName + "' threw exception", ex); } } 1、 1.2.2.1通过当前属性简单名称得到一个PropertyHandler,该handler持有该属性描述符,通过属性描述符调用getter和setter方法实现对属性值的读写; protected BeanPropertyHandler getLocalPropertyHandler(String propertyName) { PropertyDescriptor pd = getCachedIntrospectionResults().getPropertyDescriptor(propertyName); if (pd != null) { return new BeanPropertyHandler(pd); } return null; } 1、 1.2.3通过PropertyHandler获取当前属性值如果当前属性非集合或数组(“包含[]”)直接返回属性值,否则需要判断值是否为null,如果是null并且setAutoGrowNestedPaths(true),构造一个默认值赋值给当前属性; private Object setDefaultValue(PropertyTokenHolder tokens) { pv = createDefaultPropertyValue(tokens); setPropertyValue(tokens, pv); Object defaultValue = getPropertyValue(tokens); Assert.state(defaultValue != null, "Default value must not be null"); return defaultValue; } 1、 1.2.3.1创建一个PropertyValue用于setPropertyValue()赋值; private PropertyValue createDefaultPropertyValue(PropertyTokenHolder tokens) { TypeDescriptor desc = getPropertyTypeDescriptor(tokens.canonicalName); if (desc == null) { throw new NullValueInNestedPathException(getRootClass(), this.nestedPath + tokens.canonicalName, "Could not determine property type for auto-growing a default value"); } Object defaultValue = newValue(desc.getType(), desc, tokens.canonicalName); return new PropertyValue(tokens.canonicalName, defaultValue); } 1、 1.2.3.1.1得到当前属性的TypeDescriptor,内部通过子类的PropertyHandler取得; public TypeDescriptor getPropertyTypeDescriptor(String propertyName) throws BeansException { try { AbstractNestablePropertyAccessor nestedPa = getPropertyAccessorForPropertyPath(propertyName); String finalPath = getFinalPath(nestedPa, propertyName); PropertyTokenHolder tokens = getPropertyNameTokens(finalPath); PropertyHandler ph = nestedPa.getLocalPropertyHandler(tokens.actualName); if (ph != null) { if (tokens.keys != null) { if (ph.isReadable() || ph.isWritable()) { return ph.nested(tokens.keys.length); } } else { if (ph.isReadable() || ph.isWritable()) { return ph.toTypeDescriptor(); } } } } catch (InvalidPropertyException ex) { // Consider as not determinable. } return null; } 1、 1.2.3.1.2为这个属性生成一个默认的值,具体默认值规则看下面代码,其中数组最多支持二维的; private Object newValue(Class<?> type, @Nullable TypeDescriptor desc, String name) { try { if (type.isArray()) { Class<?> componentType = type.getComponentType(); // TODO - only handles 2-dimensional arrays if (componentType.isArray()) { Object array = Array.newInstance(componentType, 1); Array.set(array, 0, Array.newInstance(componentType.getComponentType(), 0)); return array; } else { return Array.newInstance(componentType, 0); } } else if (Collection.class.isAssignableFrom(type)) { TypeDescriptor elementDesc = (desc != null ? desc.getElementTypeDescriptor() : null); return CollectionFactory.createCollection(type, (elementDesc != null ? elementDesc.getType() : null), 16); } else if (Map.class.isAssignableFrom(type)) { TypeDescriptor keyDesc = (desc != null ? desc.getMapKeyTypeDescriptor() : null); return CollectionFactory.createMap(type, (keyDesc != null ? keyDesc.getType() : null), 16); } else { Constructor<?> ctor = type.getDeclaredConstructor(); if (Modifier.isPrivate(ctor.getModifiers())) { throw new IllegalAccessException("Auto-growing not allowed with private constructor: " + ctor); } return BeanUtils.instantiateClass(ctor); } } catch (Throwable ex) { throw new NullValueInNestedPathException(getRootClass(), this.nestedPath + name, "Could not instantiate property type [" + type.getName() + "] to auto-grow nested property path", ex); } } 1、 1.2.3.2设置默认值,分两种情况; protected void setPropertyValue(PropertyTokenHolder tokens, PropertyValue pv) throws BeansException { if (tokens.keys != null) { processKeyedProperty(tokens, pv);//使用父类的typeConverterDelegate的convertIfNessary()方法完成对数组,集合,Map的转换 } else { processLocalProperty(tokens, pv);//普通java对象的转换 } } 1、 1.2.3.3读取默认值返回ObjectdefaultValue=getPropertyValue(tokens);; 1、 1.2.4用当前属性值及当前propertyPath创建当前属性的属性访问器; protected BeanWrapperImpl newNestedPropertyAccessor(Object object, String nestedPath) { return new BeanWrapperImpl(object, nestedPath, this); } 1、 1.2.5复制根对象的默认属性编辑器和自定义编辑器; copyDefaultEditorsTo(nestedPa); copyCustomEditorsTo(nestedPa, canonicalName); 2、 分解属性路径为PropertyTokenHolder上述过程的递归一直找到nestedPath; 3、 调用该属性propertyName对应的AbstractNestablePropertyAccessor的另一个setPropertyValue()方法完成对属性赋值,上面已经分析过; 最后说一下BeanWrapperImpl实现了BeanWrapper接口,提供了获取属性描述符的方法。 @Override public PropertyDescriptor[] getPropertyDescriptors() { return getCachedIntrospectionResults().getPropertyDescriptors(); } @Override public PropertyDescriptor getPropertyDescriptor(String propertyName) throws InvalidPropertyException { BeanWrapperImpl nestedBw = (BeanWrapperImpl) getPropertyAccessorForPropertyPath(propertyName); String finalPath = getFinalPath(nestedBw, propertyName); PropertyDescriptor pd = nestedBw.getCachedIntrospectionResults().getPropertyDescriptor(finalPath); if (pd == null) { throw new InvalidPropertyException(getRootClass(), getNestedPath() + propertyName, "No property '" + propertyName + "' found"); } return pd; } PropertyDescriptor getPropertyDescriptor(String name) { PropertyDescriptor pd = this.propertyDescriptorCache.get(name); if (pd == null && StringUtils.hasLength(name)) { // Same lenient fallback checking as in Property... pd = this.propertyDescriptorCache.get(StringUtils.uncapitalize(name)); if (pd == null) { pd = this.propertyDescriptorCache.get(StringUtils.capitalize(name)); } } return (pd == null || pd instanceof GenericTypeAwarePropertyDescriptor ? pd : buildGenericTypeAwarePropertyDescriptor(getBeanClass(), pd)); } PropertyDescriptor[] getPropertyDescriptors() { PropertyDescriptor[] pds = new PropertyDescriptor[this.propertyDescriptorCache.size()]; int i = 0; for (PropertyDescriptor pd : this.propertyDescriptorCache.values()) { pds[i] = (pd instanceof GenericTypeAwarePropertyDescriptor ? pd : buildGenericTypeAwarePropertyDescriptor(getBeanClass(), pd)); i++; } return pds; } 还有一个就是上文提到的BeanPropertyHandler protected BeanPropertyHandler getLocalPropertyHandler(String propertyName) { PropertyDescriptor pd = getCachedIntrospectionResults().getPropertyDescriptor(propertyName); if (pd != null) { return new BeanPropertyHandler(pd); } return null; } BeanPropertyHandler持有了属性描述符,获取属性的getter和setter方法可以对属性完成读取和设置。 private class BeanPropertyHandler extends PropertyHandler { private final PropertyDescriptor pd; public BeanPropertyHandler(PropertyDescriptor pd) { super(pd.getPropertyType(), pd.getReadMethod() != null, pd.getWriteMethod() != null); this.pd = pd; } @Override public ResolvableType getResolvableType() { return ResolvableType.forMethodReturnType(this.pd.getReadMethod()); } @Override public TypeDescriptor toTypeDescriptor() { return new TypeDescriptor(property(this.pd)); } @Override @Nullable public TypeDescriptor nested(int level) { return TypeDescriptor.nested(property(pd), level); } @Override @Nullable public Object getValue() throws Exception { final Method readMethod = this.pd.getReadMethod(); if (System.getSecurityManager() != null) { AccessController.doPrivileged((PrivilegedAction<Object>) () -> { ReflectionUtils.makeAccessible(readMethod); return null; }); try { return AccessController.doPrivileged((PrivilegedExceptionAction<Object>) () -> readMethod.invoke(getWrappedInstance(), (Object[]) null), acc); } catch (PrivilegedActionException pae) { throw pae.getException(); } } else { ReflectionUtils.makeAccessible(readMethod); return readMethod.invoke(getWrappedInstance(), (Object[]) null); } } @Override public void setValue(final @Nullable Object value) throws Exception { final Method writeMethod = (this.pd instanceof GenericTypeAwarePropertyDescriptor ? ((GenericTypeAwarePropertyDescriptor) this.pd).getWriteMethodForActualAccess() : this.pd.getWriteMethod()); if (System.getSecurityManager() != null) { AccessController.doPrivileged((PrivilegedAction<Object>) () -> { ReflectionUtils.makeAccessible(writeMethod); return null; }); try { AccessController.doPrivileged((PrivilegedExceptionAction<Object>) () -> writeMethod.invoke(getWrappedInstance(), value), acc); } catch (PrivilegedActionException ex) { throw ex.getException(); } } else { ReflectionUtils.makeAccessible(writeMethod); writeMethod.invoke(getWrappedInstance(), value); } } } } ## 3.1 Spring IoC容器和beans的介绍 本章涵盖了Spring框架实现控制反转(IoC)[1]的原理。IoC又叫依赖注入(DI)。它描述了对象的定义和依赖的一个过程,也就是说,依赖的对象通过构造参数、工厂方法参数或者属性注入,当对象实例化后依赖的对象才被创建,当创建bean后容器注入这些依赖对象。这个过程基本上是反向的,因此命名为控制反转(IoC),它通过直接使用构造类来控制实例化,或者定义它们之间的依赖关系,或者类似于服务定位模式的一种机制。 org.springframework.beans和org.springframework.context是Spring框架中IoC容器的基础,BeanFactory接口提供一种高级的配置机制能够管理任何类型的对象。ApplicationContext是BeanFactory的子接口。它能更容易集成Spring的AOP功能、消息资源处理(比如在国际化中使用)、事件发布和特定的上下文应用层比如在网站应用中的WebApplicationContext。 总之,BeanFactory提供了配置框架和基本方法,ApplicationContext添加更多的企业特定的功能。ApplicationContext是BeanFactory的一个子接口,在本章它被专门用于Spring的IoC容器描述。更多关于使用BeanFactory替代ApplicationContext的信息请参考章节 3.16, “The BeanFactory”。 在Spring中,由Spring IoC容器管理的对象叫做beans。 bean就是由Spring IoC容器实例化、组装和以其他方式管理的对象。此外bean只是你应用中许多对象中的一个。Beans以及他们之间的依赖关系是通过容器配置元数据反映出来。 ## 3.2 容器概述 org.springframework.context.ApplicationContext接口代表了Spring Ioc容器,它负责实例化、配置、组装之前的beans。容器通过读取配置元数据获取对象的实例化、配置和组装的描述信息。它配置的0元数据用xml、Java注解或Java代码表示。它允许你表示组成你应用的对象以及这些对象之间丰富的内部依赖关系。 Spring提供几个开箱即用的ApplicationContext接口的实现类。在独立应用程序中通常创建一个ClassPathXmlApplicationContext或FileSystemXmlApplicationContext实例对象。虽然XML是用于定义配置元数据的传统格式,你也可以指示容器使用Java注解或代码作为元数据格式,但要通过提供少量XML配置来声明启用对这些附加元数据格式的支持。 在大多数应用场景中,显示用户代码不需要实例化一个或多个Spring IoC容器的实例。比如在web应用场景中,在web.xml中简单的8行(或多点)样板式的xml配置文件就可以搞定(参见第3.15.4节“Web应用程序的便利的ApplicationContext实例化”)。如果你正在使用Eclipse开发环境中的Spring Tool Suite插件,你只需要鼠标点点或者键盘敲敲就能轻松搞定这几行配置。 下图是Spring如何工作的高级展示。你应用中所有的类都由元数据组装到一起 所以当ApplicationContext创建和实例化后,你就有了一个完全可配置和可执行的系统或应用。 Figure 5.1. Spring IoC容器 ## 3.2.1 配置元数据 如上图所示,Spring IoC容器使用了一种配置元数据的形式。此配置元数据表示应用程序的开发人员告诉Spring容器怎样去实例化、配置和装备你应用中的对象。 配置元数据传统上以简单直观的XML格式提供,本章大部分都使用这种格式来表达Spring IoC容器核心概念和特性。 基于XML的元数据不是允许配置元数据的唯一形式,Spring IoC容器与实际写入配置元数据的格式是分离的。这些天许多的开发者在他们的Spring应用中选择基于Java配置。 更多关于Spring容器使用其他形式的元数据信息,请查看: 基于注解配置: 在Spring2.5中有过介绍支持基于注解的配置元数据 基于Java配置: 从Spring3.0开始,由Spring JavaConfig提供的许多功能已经成为Spring框架中的核心部分。这样你可以使用Java程序而不是XML文件定义外部应用程序中的bean类。使用这些新功能,可以查看@Configuration,@Bean,@Import和@DependsOn这些注解 Spring配置由必须容器管理的一个或通常多个定义好的bean组成。基于XML配置的元数据中,这些bean通过标签定义在顶级标签内部。在Java配置中通常在使用@Configuration注解的类中使用@Bean注解方法。 这些bean的定义所对应的实际对象就组成了你的应用。通常你会定义服务层对象,数据访问层对象(DAO),展现层对象比如Struts的Action实例,底层对象比如Hibernate的SessionFactories,JMSQueues等等。通常在容器中不定义细粒度的域对象,因为一般是由DAO层或者业务逻辑处理层负责创建和加载这些域对象。但是,你可以使用Spring集成Aspectj来配置IoC容器管理之外所创建的对象。详情请查看Spring使用AspectJ依赖注入域对象 接下来这个例子展示了基于XML配置元数据的基本结构 <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <bean id="..." class="..."> <!-- 在这里写 bean 的配置和相关引用 --> </bean> <bean id="..." class="..."> <!-- 在这里写 bean 的配置和相关引用 --> </bean> <!-- 在这里配置更多的bean --> </beans> id属性用来使用标识每个独立的bean定义的字符串。class属性定义了bean的类型,这个类型必须使用全路径类名(必须是包路径+类名)。id属性值可以被依赖对象引用。该例中没有体现XML引用其他依赖对象。更多请查看bean的依赖。 ## 3.11 使用JSR 330标准注解 ## 3.12 基于Java的容器配置 ## 3.12.1 基本概念:@Bean 和 @Configuration 最核心的是Spring支持全新的Java配置,例如@Configuration注解的类和@Bean注解的方法。 @Bean注解用来说明通过Spring IoC容器来管理时一个新对象的实例化,配置和初始化的方法。这对于熟悉Spring以XML配置的方式,@Bean和 element元素扮演了相同的角色。你可以在任何使用@Componen的地方使用@Bean,但是更常用的是在配置@Configuration的类中使用。 一个用@Configuration注解的类说明这个类的主要是作为一个bean定义的资源文件。进一步的讲,被@Configuration注解的类通过简单地在调用同一个类中其他的@Bean方法来定义bean之间的依赖关系。简单的@Configuration 配置类如下所示: @Configuration public class AppConfig { @Bean public MyService myService() { return new MyServiceImpl(); } } 上面的AppConfig类和Spring XML 的配置是等价的: <beans> <bean id="myService" class="com.acme.services.MyServiceImpl"/> </beans> Full @Configuration vs ‘lite’ @Beans mode? 当@Bean方法在没有使用@Configuration注解的类中声明时,它们被称为以“lite”进行处理。例如,用@Component修饰的类或者简单的类中都被认为是“lite”模式。 不同于full @Configuration,lite @Bean 方法不能简单的在类内部定义依赖关系。通常,在“lite”模式下一个@Bean方法不应该调用其他的@Bean方法。 只有在@Configuration注解的类中使用@Bean方法是确保使用“full”模式的推荐方法。这也可以防止同样的@Bean方法被意外的调用很多次,并有助于减少在’lite’模式下难以被追踪的细小bug。 这个模块下我们深入的讨论了@Configuration和@Beans注解,首先我们将介绍基于Java配置的各种Spring容器的创建。 ## 3.12.2 使用AnnotationConfigApplicationContext实例化Spring容器 下面的部分介绍Spring的AnnotationConfigApplicationContext,Spring 3.0的新内容。这个通用的ApplicationContext实现不仅可以接受@Configuration注解类为输入,还可以接受使用JSR-330元数据注解的简单类和@Component类。 当@Configuration注解的类作为输入时,@Configuration类本身会被注册为一个bean,在这个类中所有用@Bean注解的方法都会被定义为一个bean。 当使用@Component和JSR-330类时,它们被注册为bean的定义,并且假设在有必要时使用这些类内部诸如@Autowired或@Inject之类的DI元数据。 实例化使用@Configuration类作为输入实例化AnnotationConfigApplicationContext和实例化ClassPathXmlApplicationContext时使用Spring的XML文件作为输入的方式大致相同。这在无XML配置的Spring容器时使用: public static void main(String[] args) { ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class); MyService myService = ctx.getBean(MyService.class); myService.doStuff(); } 如上所述,AnnotationConfigApplicationContext不限于仅使用@Configuration类。任何@Component或JSR-330注解的类都可以作为输入提供给构造函数。例如: public static void main(String[] args) { ApplicationContext ctx = new AnnotationConfigApplicationContext(MyServiceImpl.class, Dependency1.class, Dependency2.class); MyService myService = ctx.getBean(MyService.class); myService.doStuff(); } 上面假设MyServiceImpl、Dependency1和Dependency2都用了Spring的依赖注入的注解,例如@Autowired。 使用register(Class<?>…)的方式构建容器 也可以使用无参构造函数实例化AnnotationConfigApplicationContext,然后使用register()方法配置。当使用编程方式构建AnnotationConfigApplicationContext时,这种方法特别有用。 使用scan(String …)组件扫描 启用组件扫描,只需要在你的@Configuration类中做如下配置: @Configuration @ComponentScan(basePackages = "com.acme") public class AppConfig { ... } 有Spring使用经验的用户,对Spring XML的context的声明非常熟悉: <beans> <context:component-scan base-package="com.acme"/> </beans> 在上面的例子中,com.acme将会被扫描,它会寻找任何@Component注解的类,这些类将会在Spring的容器中被注册成为一个bean。AnnotationConfigApplicationContext暴露的scan(String…)方法以达到相同组件扫描的功能: public static void main(String[] args) { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); ctx.scan("com.acme"); ctx.refresh(); MyService myService = ctx.getBean(MyService.class); } 记住:使用@Configuration注解的类是使用@Component进行元注解,所以它们也是组件扫描的候选,假设AppConfig被定义在com.acme这个包下(或者它下面的任何包),它们都会在调用scan()方法期间被找出来,然后在refresh()方法中它们所有的@Bean方法都会被处理,在容器中注册成为bean。 AnnotationConfigWebApplicationContext对于web应用的支持 AnnotationConfigApplicationContext在WebApplicationContext中的变体为 AnnotationConfigWebApplicationContext。当配置Spring ContextLoaderListener servlet 监听器、Spring MVC DispatcherServlet的时候,可以用此实现。下面为配置典型的Spring MVC DispatcherServlet的web.xml代码段。注意contextClass上下文参数和init-param的使用: <web-app> <!-- Configure ContextLoaderListener to use AnnotationConfigWebApplicationContext instead of the default XmlWebApplicationContext --> <context-param> <param-name>contextClass</param-name> <param-value> org.springframework.web.context.support.AnnotationConfigWebApplicationContext </param-value> </context-param> <!-- Configuration locations must consist of one or more comma- or space-delimited fully-qualified @Configuration classes. Fully-qualified packages may also be specified for component-scanning --> <context-param> <param-name>contextConfigLocation</param-name> <param-value>com.acme.AppConfig</param-value> </context-param> <!-- Bootstrap the root application context as usual using ContextLoaderListener --> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <!-- Declare a Spring MVC DispatcherServlet as usual --> <servlet> <servlet-name>dispatcher</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <!-- Configure DispatcherServlet to use AnnotationConfigWebApplicationContext instead of the default XmlWebApplicationContext --> <init-param> <param-name>contextClass</param-name> <param-value> org.springframework.web.context.support.AnnotationConfigWebApplicationContext </param-value> </init-param> <!-- Again, config locations must consist of one or more comma- or space-delimited and fully-qualified @Configuration classes --> <init-param> <param-name>contextConfigLocation</param-name> <param-value>com.acme.web.MvcConfig</param-value> </init-param> </servlet> <!-- map all requests for /app/* to the dispatcher servlet --> <servlet-mapping> <servlet-name>dispatcher</servlet-name> <url-pattern>/app/*</url-pattern> </servlet-mapping> </web-app> 3.12.3 使用@Bean注解 @Bean是XML元素方法级注解的直接模拟。它支持由提供的一些属性,例如:init-method,destroy-method,autowiring和name。 您可以在@Configuration或@Component注解的类中使用@Bean注解。 要定义一个bean,只需在一个方法上使用@Bean注解。您可以使用此方法在指定方法返回值类型的ApplicationContext中注册bean定义。默认情况下,bean名称与方法名称相同。以下是@Bean方法声明的简单示例: @Configuration public class AppConfig { @Bean public TransferService transferService() { return new TransferServiceImpl(); } } 这种配置完全和下面的Spring XML配置等价: <beans> <bean id="transferService" class="com.acme.TransferServiceImpl"/> </beans> 两种声明都可以使得一个名为transferService的bean在ApplicationContext可用,绑定到TransferServiceImpl类型的对象实例上: transferService -> com.acme.TransferServiceImpl @Bean注解方法可以具有描述构建该bean所需依赖关系的任意数量的参数。例如,如果我们的TransferService需要一个AccountRepository,我们可以通过一个方法参数实现该依赖: @Configuration public class AppConfig { @Bean public TransferService transferService(AccountRepository accountRepository) { return new TransferServiceImpl(accountRepository); } } 这种解决原理和基于构造函数的依赖注入几乎相同,请参考相关章节,查看详细信息。 任何使用了@Bean定义了的类都支持常规生命周期回调,并且可以使用JSR-250中的@PostConstruct和@PreDestroy注解,详细信息,参考JSR-250注解。 完全支持常规的Spring生命周期回调。如果一个bean实现了InitializingBean,DisposableBean或Lifecycle接口,它们的相关方法就会被容器调用。 完全支持*Aware系列的接口,例如:BeanFactoryAware,BeanNameAware,MessageSourceAware,ApplicationContextAware等。 @Bean注解支持任意的初始化和销毁回调方法,这与Spring XML 中bean元素上的init方法和destroy-method属性非常相似: public class Foo { public void init() { // initialization logic } } public class Bar { public void cleanup() { // destruction logic } } @Configuration public class AppConfig { @Bean(initMethod = "init") public Foo foo() { return new Foo(); } @Bean(destroyMethod = "cleanup") public Bar bar() { return new Bar(); } } 默认情况下,使用Java config定义的具有公开关闭或停止方法的bean将自动加入销毁回调。如果你有一个公开的关闭或停止方法,但是你不希望在容器关闭时被调用,只需将@Bean(destroyMethod =””)添加到你的bean定义中即可禁用默认(推测)模式。 默认情况下,您可能希望通过JNDI获取资源,因为它的生命周期在应用程序之外进行管理。特别地,请确保始终为DataSource执行此操作,因为它已知在Java EE应用程序服务器上有问题。 @Bean(destroyMethod="") public DataSource dataSource() throws NamingException { return (DataSource) jndiTemplate.lookup("MyDS"); } 另外,通过@Bean方法,通常会选择使用编程来进行JNDI查找:要么使用Spring的JndiTemplate/JndiLocatorDelegate帮助类,要么直接使用JNDI InitialContext,但不能使用JndiObjectFactoryBean变体来强制将返回类型声明为FactoryBean类型以代替目标的实际类型,它将使得在其他@Bean方法中更难用于交叉引用调用这些在此引用提供资源的方法。 当然上面的Foo例子中,在构造期间直接调用init()方法同样有效: @Configuration public class AppConfig { @Bean public Foo foo() { Foo foo = new Foo(); foo.init(); return foo; } // ... } 当您直接在Java中工作时,您可以对对象执行任何您喜欢的操作,并不总是需要依赖容器生命周期! 你可以指定@Bean注解定义的bean应具有的特定作用域。你可以使用Bean作用域章节中的任何标准作用域。 默认的作用域是单例,但是你可以用@Scope注解重写作用域。 @Configuration public class MyConfiguration { @Bean @Scope("prototype") public Encryptor encryptor() { // ... } } @Scope和scope 代理 Spring提供了一个通过范围代理来处理范围依赖的便捷方法。使用XML配置创建此类代理的最简单方法是元素。使用@Scope注解配置Java中的bean提供了与proxyMode属性相似的支持。默认是没有代理(ScopedProxyMode.NO),但您可以指定ScopedProxyMode.TARGET_CLASS或ScopedProxyMode.INTERFACES。 如果你使用Java将XML参考文档(请参阅上述链接)到范围的@Bean中移植范围限定的代理示例,则它将如下所示 如果你将XML 参考文档的scoped代理示例转化为Java @Bean,如下所示: // an HTTP Session-scoped bean exposed as a proxy @Bean @SessionScope public UserPreferences userPreferences() { return new UserPreferences(); } @Bean public Service userService() { UserService service = new SimpleUserService(); // a reference to the proxied userPreferences bean service.setUserPreferences(userPreferences()); return service; } 自定义Bean命名 默认情况下,配置类使用@Bean方法的名称作为生成的bean的名称。但是,可以使用name属性来重写此功能。 @Configuration public class AppConfig { @Bean(name = "myFoo") public Foo foo() { return new Foo(); } } Bean别名 如3.3.1节”bean 命名”中所讨论的,有时一个单一的bean需要给出多个名称,称为bean别名。 为了实现这个目标,@Bean注解的name属性接受一个String数组。 @Configuration public class AppConfig { @Bean(name = { "dataSource", "subsystemA-dataSource", "subsystemB-dataSource" }) public DataSource dataSource() { // instantiate, configure and return DataSource bean... } } Bean描述 有时候需要提供一个详细的bean描述文本是非常有用的。当对bean暴露(可能通过JMX)进行监控使,特别有用。 可以使用@Description注解对Bean添加描述: @Configuration public class AppConfig { @Bean @Description("Provides a basic example of a bean") public Foo foo() { return new Foo(); } } 3.12.4 使用@Configuration注解 @Configuration是一个类级别的注解,指明此对象是bean定义的源。@Configuration类通过public @Bean注解的方法来声明bean。在@Configuration类上对@Bean方法的调用也可以用于定义bean之间的依赖。概述,请参考第3.12.1节”基本概念:@Bean和@Configuration”。 当@Beans相互依赖时,表示依赖关系就像一个bean方法调用另一个方法一样简单: @Configuration public class AppConfig { @Bean public Foo foo() { return new Foo(bar()); } @Bean public Bar bar() { return new Bar(); } } 上面的例子,foo接受一个bar的引用来进行构造器注入: 这种方法声明的bean的依赖关系只有在@Configuration类的@Bean方法中有效。你不能在@Component类中来声明bean的依赖关系。 如前所述,方法查找注入是一个你很少用用到的高级特性。在单例的bean对原型的bean有依赖性的情况下,它非常有用。这种类型的配置使用,Java提供了实现此模式的自然方法。 public abstract class CommandManager { public Object process(Object commandState) { // grab a new instance of the appropriate Command interface Command command = createCommand(); // set the state on the (hopefully brand new) Command instance command.setState(commandState); return command.execute(); } // okay... but where is the implementation of this method? protected abstract Command createCommand(); } 使用Java支持配置,您可以创建一个CommandManager的子类,覆盖它抽象的createCommand()方法,以便它查找一个新的(原型)命令对象: @Bean @Scope("prototype") public AsyncCommand asyncCommand() { AsyncCommand command = new AsyncCommand(); // inject dependencies here as required return command; } @Bean public CommandManager commandManager() { // return new anonymous implementation of CommandManager with command() overridden // to return a new prototype Command object return new CommandManager() { protected Command createCommand() { return asyncCommand(); } } } 有关基于Java配置内部如何工作的更多信息 下面的例子展示了一个@Bean注解的方法被调用两次: @Configuration public class AppConfig { @Bean public ClientService clientService1() { ClientServiceImpl clientService = new ClientServiceImpl(); clientService.setClientDao(clientDao()); return clientService; } @Bean public ClientService clientService2() { ClientServiceImpl clientService = new ClientServiceImpl(); clientService.setClientDao(clientDao()); return clientService; } @Bean public ClientDao clientDao() { return new ClientDaoImpl(); } } clientDao()方法clientService1()和clientService2()被各自调用了一次。因为这个方法创建并返回了一个新的ClientDaoImpl实例,你通常期望会有2个实例(每个服务各一个)。这有一个明显的问题:在Spring中,bean实例默认情况下是单例。神奇的地方在于:所有的@Configuration类在启动时都使用CGLIB进行子类实例化。在子类中,子方法在调用父方法创建一个新的实例之前会首先检查任何缓存(作用域)的bean。注意,从Spring 3.2开始,不再需要将CGLIB添加到类路径中,因为CGLIB类已经被打包在org.springframework.cglib下,直接包含在spring-core JAR中。 根据不同的bean作用域,它们的行为也是不同的。我们这里讨论的都是单例模式。 这里有一些限制是由于CGLIB在启动时动态添加的特性,特别是配置类都不能是final类型。然而从4.3开始,配置类中允许使用任何构造函数,包含@Autowired使用或单个非默认构造函数声明进行默认注入。如果你希望避免CGLIB带来的任何限制,那么可以考虑子在非@Configuration注解类中声明@Bean注解方法。例如,使用@Component注解类。在 @Bean方法之间的交叉调用不会被拦截,所以你需要在构造器或者方法级别上排除依赖注入。 ## 3.12.5 基于Java组合配置 使用@Import注解 和Spring XML文件中使用元素来帮助模块化配置类似,@Import注解允许从另一个配置类加载@Bean定义: @Configuration public class ConfigA { @Bean public A a() { return new A(); } } @Configuration @Import(ConfigA.class) public class ConfigB { @Bean public B b() { return new B(); } } 现在,在实例化上下文时不是同时指明ConfigA.class和ConfigB.class,而是仅仅需要明确提供ConfigB: public static void main(String[] args) { ApplicationContext ctx = new AnnotationConfigApplicationContext(ConfigB.class); // now both beans A and B will be available... A a = ctx.getBean(A.class); B b = ctx.getBean(B.class); } 这种方法简化了容器实例化,因为只需要处理一个类,而不是需要开发人员在构建期间记住大量的@Configuration注解类。 从Spring Framework 4.2开始,@Import注解也支持对常规组件类的引用,类似AnnotationConfigApplicationContext.register方法。如果你希望避免组件扫描,使用一些配置类作为所有组件定义的入口,这个方法特别有用。 引入的@Bean定义中注入依赖 上面的类中可以运行,但是太过简单。在大多数实际场景中,bean在配置类之间相互依赖。当使用XML时,这没有问题,因为没有编译器参与,一个bean可以简单的声明为ref=”someBean”并且相信Spring在容器初始化过程处理它。当然,当使用@Configuration注解类,Java编译器会对配置模型放置约束,以便其他对其他引用的bean进行Java语法校验。 幸运的是,解决这个问题也很简单。正如我们讨论过的,@Bean方法可以有任意数量的参数来描述bean的依赖。让我们考虑一下真实场景和一系列@Configuration类,每个bean都依赖了其他配置中声明的bean: @Configuration public class ServiceConfig { @Bean public TransferService transferService(AccountRepository accountRepository) { return new TransferServiceImpl(accountRepository); } } @Configuration public class RepositoryConfig { @Bean public AccountRepository accountRepository(DataSource dataSource) { return new JdbcAccountRepository(dataSource); } } @Configuration @Import({ServiceConfig.class, RepositoryConfig.class}) public class SystemTestConfig { @Bean public DataSource dataSource() { // return new DataSource } } public static void main(String[] args) { ApplicationContext ctx = new AnnotationConfigApplicationContext(SystemTestConfig.class); // everything wires up across configuration classes... TransferService transferService = ctx.getBean(TransferService.class); transferService.transfer(100.00, "A123", "C456"); } 这里有其他的方法实现相同的结果。记住@Configuration类最终只是容器中的另一个bean:这意味着它们可以像任何其他bean一样利用@Autowired和@Value注入等! 确保您注入的依赖关系是最简单的。@Configuration类在上下文初始化期间处理,强制要求依赖使用这种方式进行注入可能导致意外的早期初始化问题。如果可能,就采用如上述例子所示的基于参数的注入。 同时,也要特别小心通过@Bean的BeanPostProcessor BeanFactoryPostProcessor定义。它们应该被声明为static的@Bean方法,不会触发包含它们的配置类的实例化。否则,@Autowired和@Value将在配置类上不生效,因为它太早被创建为一个实例了。 @Configuration public class ServiceConfig { @Autowired private AccountRepository accountRepository; @Bean public TransferService transferService() { return new TransferServiceImpl(accountRepository); } } @Configuration public class RepositoryConfig { private final DataSource dataSource; @Autowired public RepositoryConfig(DataSource dataSource) { this.dataSource = dataSource; } @Bean public AccountRepository accountRepository() { return new JdbcAccountRepository(dataSource); } } @Configuration @Import({ServiceConfig.class, RepositoryConfig.class}) public class SystemTestConfig { @Bean public DataSource dataSource() { // return new DataSource } } public static void main(String[] args) { ApplicationContext ctx = new AnnotationConfigApplicationContext(SystemTestConfig.class); // everything wires up across configuration classes... TransferService transferService = ctx.getBean(TransferService.class); transferService.transfer(100.00, "A123", "C456"); } 只有Spring Framework 4.3才支持@Configuration类中的构造方法注入。注意,如果目标bean只定义了一个构造函数,那么则不需要指定@Autowired;如果目标bean只定义一个构造函数,则不需要指定@Autowired;在上面的例子中,@Autowired在RepositoryConfig构造函数中是不必要的。 在上面的场景中,使用@Autowired可以很好的提供所需的模块化,但是准确的决定在哪里自动注入定义的bean还是模糊的。例如,作为开发者来看待ServiceConfig,如何准确的确定自动注入 AccountRepository 是在哪里声明的?它没有明确的出现在代码中,这可能还不错。记住,Spring Tool Suite 提供工具可以渲染图形展示对象是如何装配的,这些可能是你所需要的。同时,你的Java IDE 也可以很简单的找出所有AccountRepository类型的声明和使用,这将很快的展示出你@Bean方法的位置和返回类型。 在这种歧义不可接受的情况下,你希望从IDE中直接从一个@Configuration类导航到另一个类,可以考虑自动装配配置类的本身: @Configuration public class ServiceConfig { @Autowired private RepositoryConfig repositoryConfig; @Bean public TransferService transferService() { // navigate 'through' the config class to the @Bean method! return new TransferServiceImpl(repositoryConfig.accountRepository()); } } 在上面的情形中,定义的AccountRepository是完全透明的。但是,ServiceConfig和RepositoryConfig是紧密耦合在一起了;这需要权衡。这种紧密耦合的可以通过基于接口或者抽象@Configuration类来缓解。可以考虑下面的代码: @Configuration public class ServiceConfig { @Autowired private RepositoryConfig repositoryConfig; @Bean public TransferService transferService() { return new TransferServiceImpl(repositoryConfig.accountRepository()); } } @Configuration public interface RepositoryConfig { @Bean AccountRepository accountRepository(); } @Configuration public class DefaultRepositoryConfig implements RepositoryConfig { @Bean public AccountRepository accountRepository() { return new JdbcAccountRepository(...); } } @Configuration @Import({ServiceConfig.class, DefaultRepositoryConfig.class}) // import the concrete config! public class SystemTestConfig { @Bean public DataSource dataSource() { // return DataSource } } public static void main(String[] args) { ApplicationContext ctx = new AnnotationConfigApplicationContext(SystemTestConfig.class); TransferService transferService = ctx.getBean(TransferService.class); transferService.transfer(100.00, "A123", "C456"); } 现在ServiceConfig与具体DefaultRepositoryConfig就是送耦合,IDE内置的工具也仍然有用:对开发人员来说,可以轻松的获取RepositoryConfig实现层级类型。用这种方法,定位到@Configuration类及它的依赖类和定位基于接口的代码就没什么区别。 有条件的包括@Configuration类或@Bean方法 通常,有条件的开启或者禁用一个完整的@Configuration类,甚至是基于有任意系统状态的单独@Bean方法。一个常见的例子就是使用@Profile注解来激活仅在Spring 环境中启用的特定的profile文件(有关详细信息,请参阅第3.13.1节“Bean definition profiles”) @Profile注解是用一个更加灵活的@Conditional注解实现的。@Conditional注解表示@Bean在被注册前应该查阅特定的org.springframework.context.annotation.Condition实现。 Condition接口的实现只提供了一个返回true或者false的matches(…)方法。例如 @Profile是Condition的具体实现: @Override public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { if (context.getEnvironment() != null) { // Read the @Profile annotation attributes MultiValueMap<String, Object> attrs = metadata.getAllAnnotationAttributes(Profile.class.getName()); if (attrs != null) { for (Object value : attrs.get("value")) { if (context.getEnvironment().acceptsProfiles(((String[]) value))) { return true; } } return false; } } return true; } @Conditional详细信息请参考javadocs。 Java and XML 混合配置 Spring 对@Configuration配置类的支持的目的不是100%来替换Spring XML配置的。一些基本特性,例如:Spring XML命名空间仍然是容器配置的一个理想方式。在 XML更便于使用或者是必须使用的情况下,要么以“XML为中心”的方式来实例化容器,比如,ClassPathXmlApplicationContext,要么以“Java为中心”的方式,使用AnnotationConfigurationApplicationContext 和@ImportResource注解来引入所需的XML。 以XML为中心使用@Configuration类 假设你可能会以XML包含@Configuration类的方式来启动一个Spring容器。例如,在一个现有使用Spring XML的大型代码库中,根据需要从已有的XML文件中创建@Configuration类是很简单的。下面你可以发现在以XML为中心的情形下使用@Configuration类的选项。谨记,@Configuration类最终只是容器中的一个bean。在这个例子中,我们会创建一个名为AppConfig的@Configuration类,它作为一个bean的定义包含在system-test-config.xml中。因为context:annotation-config/是打开的,容器会识别@Configuration,并且会处理AppConfig中声明的@Bean方法。 @Configuration public class AppConfig { @Autowired private DataSource dataSource; @Bean public AccountRepository accountRepository() { return new JdbcAccountRepository(dataSource); } @Bean public TransferService transferService() { return new TransferService(accountRepository()); } } system-test-config.xml: <beans> <!-- enable processing of annotations such as @Autowired and @Configuration --> <context:annotation-config/> <context:property-placeholder location="classpath:/com/acme/jdbc.properties"/> <bean class="com.acme.AppConfig"/> <bean class="org.springframework.jdbc.datasource.DriverManagerDataSource"> <property name="url" value="${jdbc.url}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> </bean> </beans> jdbc.properties: jdbc.url=jdbchsql://localhost/xdb jdbc.username=sa jdbc.password= public static void main(String[] args) { ApplicationContext ctx = new ClassPathXmlApplicationContext("classpath:/com/acme/system-test-config.xml"); TransferService transferService = ctx.getBean(TransferService.class); // ... } 在上面的system-test-config.xml中,AppConfig中的bean没有声明id的元素。然而它也可以被接受,在没有其他bean引用的情况下也没有必要给出,也不太可能通过名字的方式从容器中显式取出。像DataSource bean一样,只能通过类型自动注入,所以明确的bean id也不严格要求。 因为@Configuration是@Component的一个元注解,对于component的扫描@Configuration注解类会自动成为候选者。和上面的场景相同,利用component扫描可以重新定义system-test-config.xml。注意在个案例中,我们不需要明确的声明context:annotation-config/,因为开启context:component-scan/,功能是相同的。 system-test-config.xml: <beans> <!-- picks up and registers AppConfig as a bean definition --> <context:component-scan base-package="com.acme"/> <context:property-placeholder location="classpath:/com/acme/jdbc.properties"/> <bean class="org.springframework.jdbc.datasource.DriverManagerDataSource"> <property name="url" value="${jdbc.url}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> </bean> </beans> @Configuration类为中心的 XML @ImportResource的使用 在以@Configuration类为主要机制的配置容器的应用程序中,仍然有必要使用一些XML。在这些场景中,只需使用@ImportResource,并根据需要定义一些XML。这样实现了“以Java为中心”方式来配置容器,并将XML保持在最低限度。 @Configuration @ImportResource("classpath:/com/acme/properties-config.xml") public class AppConfig { @Value("${jdbc.url}") private String url; @Value("${jdbc.username}") private String username; @Value("${jdbc.password}") private String password; @Bean public DataSource dataSource() { return new DriverManagerDataSource(url, username, password); } } properties-config.xml <beans> <context:property-placeholder location="classpath:/com/acme/jdbc.properties"/> </beans> jdbc.properties jdbc.url=jdbchsql://localhost/xdb jdbc.username=sa jdbc.password= public static void main(String[] args) { ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class); TransferService transferService = ctx.getBean(TransferService.class); // ... } ## 3.13 环境抽象 在应用环境中,集成在容器的抽象环境模型有两个方面:profiles和properties。只有给出的profile被激活,一组逻辑命名的bean定义才会在容器中注册。无论是在XML中或者通过注解,bean都会被分配给一个profile。环境变量对象角色和profiles的关系来决定哪个profiles(如果有)处于当前激活状态,哪个profiles默认被激活。几乎在所有的应用中,Properties都扮演了一个重要的对象,这可能有各种来源:属性文件, JVM 系统属性文件,系统环境变量,JNDI,servlet上下文参数,属性查询对象,Maps等等。环境变量对象的角色和properties的关系用于配置属性并从中解析属性提供给用户一个便捷的服务接口。 ## 3.13.1 Bean的profiles定义 Bean定义profiles是在核心容器中允许不同的bean在不同环境注册的机制。环境对于不同的用户意味着不同的东西,这个特性可以帮助许多用例,包括: 在开发中不使用内存中的数据源 VS 在质量测试或生产环境中从JNDI查找相同的数据源。 当把应用部署在可执行的环境中时注册监控基础架构 对于客户A注册的自定义实现VS.客户B部署 让我首先考虑在一个需要数据源的应用中使用这个例子。在测试环境中,配置可能如下: @Bean public DataSource dataSource() { return new EmbeddedDatabaseBuilder() .setType(EmbeddedDatabaseType.HSQL) .addScript("my-schema.sql") .addScript("my-test-data.sql") .build(); } 让我现在考虑一下,如何把这个应用部署在测试环境或者生产环境中,假设应用所需的数据源将会被注册在生产应用环境中的JNDI目录。现在我们的数据源bean看起来像这样: @Bean(destroyMethod="") public DataSource dataSource() throws Exception { Context ctx = new InitialContext(); return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource"); } 问题就是如何根据当前的环境在这两种变量之间进行切换。随着时间的推移,Spring的用户已经设计了很多种方法来实现此功能,通常依赖于系统环境变量和包含${placeholder}的XML 语句,根据环境变量的值可以解决正确的文件路径配置。Bean定义profiles是容器为了解决这个问题而提供的一个核心功能。 如果我们概括一下上面bean定义环境变量的示例,我们最终需要在特定的上下文中注册特定的bean,而不是其他的。你可以说你想要在情形A中注册一个特定的Bean定义的profile,在情形B中是另外一个。我们首先看下如何更新我们的配置以反映这种需求。 当一个或者多个特定的profiles被激活,@Profile注解允许你指定一个有资格的组件来注册。使用我们上面的例子,我们可以按照下面的重写dataSource配置: @Configuration @Profile("dev") public class StandaloneDataConfig { @Bean public DataSource dataSource() { return new EmbeddedDatabaseBuilder() .setType(EmbeddedDatabaseType.HSQL) .addScript("classpath:com/bank/config/sql/schema.sql") .addScript("classpath:com/bank/config/sql/test-data.sql") .build(); } } @Configuration @Profile("production") public class JndiDataConfig { @Bean(destroyMethod="") public DataSource dataSource() throws Exception { Context ctx = new InitialContext(); return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource"); } } 如前所述,使用@Bean方法,通常会选择使用程序话的JNDI查找:要么使用Spring的 JndiTemplate/JndiLocatorDelegate帮助要么直接使用上面展示的JNDI InitialContext,而不是强制声明返回类型为FactoryBean的JndiObjectFactoryBean变体。 @Profile可以被用作为创建一个自定义组合注解的元注解。下面的例子定义了一个@Production注解,它可以被用作替换@Profile(“production”)的注解。 @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Profile("production") public @interface Production { } 在仅仅包含一个特殊bean的配置类中,@Profile也可以被声明在方法级别: @Configuration public class AppConfig { @Bean @Profile("dev") public DataSource devDataSource() { return new EmbeddedDatabaseBuilder() .setType(EmbeddedDatabaseType.HSQL) .addScript("classpath:com/bank/config/sql/schema.sql") .addScript("classpath:com/bank/config/sql/test-data.sql") .build(); } @Bean @Profile("production") public DataSource productionDataSource() throws Exception { Context ctx = new InitialContext(); return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource"); } } 如果一个@Configuration类被标记为@Profile,那么所有的@Bean方法和@Import注解相关的类都会被忽略,除非一个或多个特别的profiles被激活。如果一个@Component或@Configuration类被标记为@Profile({“p1”, “p2”}),那么这个类将不会被注册/处理,除非被标记为’p1’和/或’p2’的profiles已经被激活。如果给出的profile的前缀带有取反的操作符(!),那么注解的元素将会被注册,除非这个profile没有被激活。例如,给出@Profile({“p1”, “!p2”}),如果profile ‘p1’是激活状态或者profile ‘p2’不是激活状态的时候才会注册。 ## 3.13.2 XML bean 定义 profiles XML对应元素的profile属性。我们上面的示例配置可以重写为下面的两个XML配置: <beans profile="dev" xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:jdbc="http://www.springframework.org/schema/jdbc" xsi:schemaLocation="..."> <jdbc:embedded-database id="dataSource"> <jdbc:script location="classpath:com/bank/config/sql/schema.sql"/> <jdbc:script location="classpath:com/bank/config/sql/test-data.sql"/> </jdbc:embedded-database> </beans> <beans profile="production" xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:jee="http://www.springframework.org/schema/jee" xsi:schemaLocation="..."> <jee:jndi-lookup id="dataSource" jndi-name="java:comp/env/jdbc/datasource"/> </beans> 也可以避免在同一个文件中分割和嵌套元素: <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:jdbc="http://www.springframework.org/schema/jdbc" xmlns:jee="http://www.springframework.org/schema/jee" xsi:schemaLocation="..."> <!-- other bean definitions --> <beans profile="dev"> <jdbc:embedded-database id="dataSource"> <jdbc:script location="classpath:com/bank/config/sql/schema.sql"/> <jdbc:script location="classpath:com/bank/config/sql/test-data.sql"/> </jdbc:embedded-database> </beans> <beans profile="production"> <jee:jndi-lookup id="dataSource" jndi-name="java:comp/env/jdbc/datasource"/> </beans> </beans> spring-bean.xsd 约束允许这样的元素仅作为文件中的最后一个元素。这有助于XML的灵活性,且不会产生混乱。 现在我们已经更新了我们的配置,我们仍然需要说明哪个profile是激活的。如果我们现在开始我们示例应用程序,我们将会看到一个NoSuchBeanDefinitionException被抛出,因为容器找不到一个名为dataSource的Spring bean。 激活一个profile可以通过多种方式完成,但是大多数情况下,最直接的办法就是通过存在ApplicationContext当中的环境变量的API进行编程: AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); ctx.getEnvironment().setActiveProfiles("dev"); ctx.register(SomeConfig.class, StandaloneDataConfig.class, JndiDataConfig.class); ctx.refresh(); 除此之外,profiles也可以通过声明spring.profiles.active属性来激活,这个可以通过在系统环境变量,JVM系统属性,web.xml中的servlet上下文环境参数,甚至JNDI的入口(请参考 3.13.3, “PropertySource abstraction”)。在集成测试中,激活profiles可以通过在Spring-test模块中的@ActiveProfiles注解来声明(参见“使用profiles来配置上下文环境”章节)。 注意,profiles不是“二者选一”的命题;它可以一次激活多个profiles。以编程的方式来看,简单的传递多个profile名字给接受String 可变变量参数的setActiveProfiles()方法: ctx.getEnvironment().setActiveProfiles("profile1", "profile2"); 在声明式中,spring.profiles.active可以接受以逗号分隔的profile 名称列表: -Dspring.profiles.active="profile1,profile2" 默认的profile 默认配置文件表示默认启用的配置文件。考虑以下几点: @Configuration @Profile("default") public class DefaultDataConfig { @Bean public DataSource dataSource() { return new EmbeddedDatabaseBuilder() .setType(EmbeddedDatabaseType.HSQL) .addScript("classpath:com/bank/config/sql/schema.sql") .build(); } } 如果没有profile是激活状态,上面的dataSource将会被创建;这种方式可以被看做是对一个或者多个bean提供了一种默认的定义方式。如果启用任何的profile,那么默认的profile都不会被应用。 在上下文环境可以使用setDefaultProfiles()或者spring.profiles.default属性来修改默认的profile名字。 ## 3.13.3 属性源抽象 Spring 环境抽象提供了可配置的属性源层次结构的搜索操作。为了充分的解释,请考虑下面的例子: ApplicationContext ctx = new GenericApplicationContext(); Environment env = ctx.getEnvironment(); boolean containsFoo = env.containsProperty("foo"); System.out.println("Does my environment contain the 'foo' property? " + containsFoo); 在上面的代码段中,我们看到了一个高级别的方法来要求Spring是否为当前环境定义foo属性。为了回答这个问题,环境对象对一组PropertySource对象执行搜索。一个PropertySource是对任何key-value资源的简单抽象,并且Spring 的标准环境是由两个PropertySource配置的,一个表示一系列的JVM 系统属性(System.getProperties()),一个表示一系列的系统环境变量(System.getenv())。 这些默认的属性资源存在于StandardEnvironment,可以在应用中独立使用。StandardServletEnvironment包含其他默认的属性资源,包括servlet配置和servlet上下文参数。它可以选择性的启用JndiPropertySource。详细信息请查看javadocs。 具体的说,当使用StandardEnvironment时,如果在运行时系统属性或者环境变量中包括foo,那么调用env.containsProperty(“foo”)方法将会返回true。 搜索是按照层级执行的。默认情况,系统属性优先于环境变量,所以这两个地方同时存在属性foo的时候,调用env.getProperty(“foo”)将会返回系统属性中的foo值。注意,属性值不会被合并而是被之前的值覆盖。对于一个普通的StandardServletEnvironment,它完整的层次结构如下,最顶端的优先级最高: ServletConfig参数(如果适用,例如DispatcherServlet上下文环境) ServletContext参数(web.xml中的context-param) JNDI环境变量(“java:comp/env/”) JVM系统属性(“-D”命令行参数) JVM系统环境变量(操作系统环境变量) 更重要的是,整个机制都是可配置的。也许你有个自定义的属性来源,你想把它集成到这个搜到里面。这也没问题,只需简单的实现和实例化自己的PropertySource,并把它添加到当前环境的PropertySources集合中: ConfigurableApplicationContext ctx = new GenericApplicationContext(); MutablePropertySources sources = ctx.getEnvironment().getPropertySources(); sources.addFirst(new MyPropertySource()); 在上面的代码中,MyPropertySource被添加到搜索中的最高优先级。如果它包含了一个foo属性,在任何其他的PropertySource中的foo属性之前它会被检测到并返回。MutablePropertySources API暴露了很多允许精确操作该属性源集合的方法。 ## 3.13.4 @PropertySource @PropertySource注解对添加一个PropertySource到Spring的环境变量中提供了一个便捷的和声明式的机制。 给出一个名为”app.properties”的文件,它含了testbean.name=myTestBean的键值对,下面的@Configuration类使用@PropertySource的方式来调用testBean.getName(),将会返回”myTestBean”。 @Configuration @PropertySource("classpath:/com/myco/app.properties") public class AppConfig { @Autowired Environment env; @Bean public TestBean testBean() { TestBean testBean = new TestBean(); testBean.setName(env.getProperty("testbean.name")); return testBean; } } 任何出现在@PropertySource中的资源位置占位符都会被注册在环境变量中的资源解析。 @Configuration @PropertySource("classpath:/com/${my.placeholder:default/path}/app.properties") public class AppConfig { @Autowired Environment env; @Bean public TestBean testBean() { TestBean testBean = new TestBean(); testBean.setName(env.getProperty("testbean.name")); return testBean; } } 假设”my.placeholder”已经在其中的一个资源中被注册,例如:系统属性或环境变量,占位符将会被正确的值解析。如果没有,”default/path”将会使用默认值。如果没有默认值,而且无法解释属性,则抛出IllegalArgumentException异常。 ## 3.13.5 声明中的占位符解决方案 以前,元素中占位符的值只能被JVM系统熟悉或者环境变量解析。现在已经解决了这种情况。因为抽象的环境已经通过容器被集成了,很容易通过它来分配占位符。这意味着你可以使用任何你喜欢的方式配置:可以通过系统属性和环境变量来改变搜索优先级,或者完全删除它们;可以以适当的方式添加你自己混合属性资源。 具体来说,下面的声明无论customer属性被定义在哪里,只要它存在环境变量中就有作用: ## 3.14 注册一个加载时编织器 在类被加载进JVM时Spring使用LoadTimeWeaver进行动态转换。 为了使的load-time weaving可用,那么你只需在配置了@Configuration的类上添加@EnableLoadTimeWeaving。 @Configuration @EnableLoadTimeWeaving public class AppConfig { } 相应的xml配置使用context:load-time-weaver元素: <beans> <context:load-time-weaver/> </beans> 一旦配置了ApplicationContext,那么在ApplicationContext中的任何bean都可以实现LoadTimeWeaverAware,从而接受对类加载时编织器实例的引用。这与Spring JPA支持相结合时非常有用,JPA类转化必须使用加载时编织。可以通过javadocs的LocalContainerEntityManagerFactoryBean获取更多详细信息,对于AspectJ加载时的编织请参考: Section 7.8.4, “Load-time weaving with AspectJ in the Spring Framework”. ## 3.15 ApplicationContext的额外功能 正如本章开头所讨论的那样,org.springframework.beans.factory包提供基本的功能来管理和操作bean,包括以编程的方式。The org.springframework.context包增加了ApplicationContext接口,它继承了BeanFactory接口,除了以面向应用框架的风格扩展接口来提供一些额外的功能。很多人以完全声明的方式使用ApplicationContext,甚至没有以编程的方式去创建它,而是依赖诸如ContextLoader等支持类来自动的实例化ApplicationContext,作为Java EE web应用程序正常启动的一部分。 为了增强BeanFactory在面向框架风格的功能,上下文的包还提供了以下的功能: 通过MessageSource接口访问i18n风格的消息 通过ResourceLoader接口访问类似URL和文件资源 通过ApplicationEventPublisher接口,即bean实现ApplicationListener接口来进行事件发布 通过HierarchicalBeanFactory接口实现加载多个(分层)上下文,允许每个上下文只关注特定的层,例如应用中的web层 ## 3.15.1 使用MessageSource 国际化 ApplicationContext接口继承了一个叫做MessageSource的接口,因此它也提供了国际化(i18n)的功能。Spring也提供了HierarchicalMessageSource接口,它可以分层去解析信息。这些接口共同为Spring消息效应解析提供了基础。这些接口上定义的方法包括: String getMessage(String code, Object[] args, String default, Locale loc): 这个基础的方法用来从MessageSource检索消息。当指定的区域中没有发现消息时,将使用默认的。任何参数传递都将使用标准库提供的MessageFormat变成替换值。 String getMessage(String code, Object[] args, Locale loc): 本质上和前面提供的方法相同,只有一个区别,就是当没有指定消息,又没有发现消息,将会抛出NoSuchMessageException 异常。 String getMessage(MessageSourceResolvable resolvable, Locale locale): 所有的属性处理方法都被包装在一个名为MessageSourceResolvable的类中,你可以使用此方法。 当ApplicationContext被载入的时候,它会自动的在上下文中去搜索定义的MessageSource bean。这个bean必须有messageSource的名称。如果找到这么一个bean,所有上述方法的调用都会委托给消息源。如果没有发现消息源,ApplicationContext会尝试寻找一个同名的父消息源。如果是这样,它会将那个bean作为MessageSource。如果ApplicationContext没有找到任何的消息源,那么一个空的DelegatingMessageSource将被实例化,以便能够接受到对上述定义方法的调用。 Spring提供了ResourceBundleMessageSource和StaticMessageSource两个MessageSource实现。它们两个都实现了HierarchicalMessageSource以便处理嵌套消息。StaticMessageSource很少使用,但是它提供了通过编程的方式增加消息源。下面展示ResourceBundleMessageSource使用的例子: <beans> <bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource"> <property name="basenames"> <list> <value>format</value> <value>exceptions</value> <value>windows</value> </list> </property> </bean> </beans> 在上面的例子中,假设在类路径下定义了format,exceptions和windows三个资源包。解析消息的任何请求都会通过ResourceBundles被JDK以标准方式处理。为了举例说明,假设上述两个资源包的文件内容是… # in format.properties message=Alligators rock! # in exceptions.properties argument.required=The {0} argument is required. 下面的实例展示了执行MessageSource功能的程序。记住所有的ApplicationContext的实现也是MessageSource的实现,而且它可以被强转为MessageSource接口。 public static void main(String[] args) { MessageSource resources = new ClassPathXmlApplicationContext("beans.xml"); String message = resources.getMessage("message", null, "Default", null); System.out.println(message); } 上面的程序输出的结果为: Alligators rock! 所以总结一下,MessageSource是定义在一个名为beans.xml,它存在类路径的跟目录下。messageSource bean定义通过basenames属性引用了很多的资源。在列表中传递给basenames属性的三个文件作为类路径下根目录中的文件存在,分别为format.properties, exceptions.properties, and windows.properties。 下一个例子展示传递给消息查找的参数;这些参数将会被转换为字符串并插入到消息查找的占位符中。 <beans> <!-- this MessageSource is being used in a web application --> <bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource"> <property name="basename" value="exceptions"/> </bean> <!-- lets inject the above MessageSource into this POJO --> <bean id="example" class="com.foo.Example"> <property name="messages" ref="messageSource"/> </bean> </beans> public class Example { private MessageSource messages; public void setMessages(MessageSource messages) { this.messages = messages; } public void execute() { String message = this.messages.getMessage("argument.required", new Object [] {"userDao"}, "Required", null); System.out.println(message); } } 调用execute()方法的输出结果为: TheuserDao argument is required. 关于国际化(i18n),Spring的各种MessageSource实现遵循与标准JDK ResourceBundle相同的语言环境和回退规则。简而言之,并继续用前面messageSource 为例,如果你想根据英国(en-GB)解析消息,你可以创建这些文件format_en_GB.properties,exceptions_en_GB.properties, and windows_en_GB.properties。 通常,地域设置通过应用周围环境管理的。在此示例中,手动指定对(英国)区域消息进行解析。 # in exceptions_en_GB.properties argument.required=Ebagum lad, the {0} argument is required, I say, required. public static void main(final String[] args) { MessageSource resources = new ClassPathXmlApplicationContext("beans.xml"); String message = resources.getMessage("argument.required", new Object [] {"userDao"}, "Required", Locale.UK); System.out.println(message); } 上面的程序运行输出为: Ebagum lad, the ‘userDao’ argument is required, I say, required. 你也可以使用MessageSourceAware接口来获取对已定义MessageSource的引用。任何在ApplicationContext定义的bean都会实现MessageSourceAware,当bean被创建或者配置的时候,它会在应用上下文的MessageSource中被被注入。 作为ResourceBundleMessageSource的替代方法,Spring提供了一个ReloadableResourceBundleMessageSource类。这个变体支持同样打包文件格式,但是它更灵活而不是标准JDK基于ResourceBundleMessageSource的实现。特别的,它允许从任何Spring 资源位置读取文件(不仅仅是从类路径)而且还支持属性文件热加载(同时高效缓存他们)。ReloadableResourceBundleMessageSource的详细信息参考javadocs。 ## 3.15.2 标准和自定义事件 ApplicationEvent类和ApplicationListener接口提供了ApplicationContext中的事件处理。如果一个bean实现了ApplicationListener接口,然后它被部署到上下问中,那么每次ApplicationEvent发布到ApplicationContext中时,bean都会收到通知。本质上,这是观察者模型。 从Spring 4.2开始,事件的基础得到了重要的提升,并提供了基于注解模型及任意事件发布的能力,这个对象不一定非要继承ApplicationEvent。当这个对象被发布时,我们把他包装在事件中。 Spring提供了一下的标准事件: 你可以创建并发布自己的自定义事件。这个例子演示了一个继承Spring ApplicationEvent的简单类: public class BlackListEvent extends ApplicationEvent { private final String address; private final String test; public BlackListEvent(Object source, String address, String test) { super(source); this.address = address; this.test = test; } // accessor and other methods... } 为了发布一个自定义的ApplicationEvent,在ApplicationEventPublisher中调用publishEvent()方法。通常在实现了ApplicationEventPublisherAware接口并把它注册为一个Spring bean的时候它就完成了。下面的例子展示了这么一个类: public class EmailService implements ApplicationEventPublisherAware { private List<String> blackList; private ApplicationEventPublisher publisher; public void setBlackList(List<String> blackList) { this.blackList = blackList; } public void setApplicationEventPublisher(ApplicationEventPublisher publisher) { this.publisher = publisher; } public void sendEmail(String address, String text) { if (blackList.contains(address)) { BlackListEvent event = new BlackListEvent(this, address, text); publisher.publishEvent(event); return; } // send email... } } 在配置时,Spring容器将检测到EmailService实现了ApplicationEventPublisherAware,并将自动调用setApplicationEventPublisher()方法。实际上,传入的参数将是Spring容器本身;您只需通过ApplicationEventPublisher接口与应用程序上下文进行交互。 为了自定义ApplicationEvent,创建一个试下了ApplicationListener的类并把他注册为一个Spring bean。下面例子展示这样一个类: public class BlackListNotifier implements ApplicationListener<BlackListEvent> { private String notificationAddress; public void setNotificationAddress(String notificationAddress) { this.notificationAddress = notificationAddress; } public void onApplicationEvent(BlackListEvent event) { // notify appropriate parties via notificationAddress... } } 请注意,ApplicationListener通常用你自定义的事件BlackListEvent类型参数化的。这意味着onApplicationEvent()方法可以保持类型安全,避免向下转型的需要。您可以根据需要注册许多的事件侦听器,但请注意,默认情况下,事件侦听器将同步接收事件。这意味着publishEvent()方法会阻塞直到所有的监听者都处理完。这种同步和单线程方法的一个优点是,如果事务上下文可用,它就会在发布者的事务上下文中处理。如果必须需要其他的时间发布策略,请参考javadoc的 Spring ApplicationEventMulticaster 接口。 下面例子展示了使用配置和注册上述每个类的bean定义: <bean id="emailService" class="example.EmailService"> <property name="blackList"> <list> <value>known.spammer@example.org</value> <value>known.hacker@example.org</value> <value>john.doe@example.org</value> </list> </property> </bean> <bean id="blackListNotifier" class="example.BlackListNotifier"> <property name="notificationAddress" value="blacklist@example.org"/> </bean> 把他们放在一起,当调用emailService的sendEmail()方法时,如果有任何应该被列入黑名单的邮件,那么自定义的BlackListEvent事件会被发布。blackListNotifier 会被注册为一个ApplicationListener,从而接受BlackListEvent,届时通知适当的参与者。 Spring 的事件机制的设计是用在Spring bean和相同应用上下文的简单通讯。然而,对于更复杂的企业集成需求,单独维护Spring Integration工程对构建著名的Spring编程模型轻量级,面向模式,事件驱动架构提供了完整的支持。 从Spring 4.2开始,一个事件监听器可以通过EventListener注解注册在任何managed bean的公共方法上。BlackListNotifier可以重写如下: public class BlackListNotifier { private String notificationAddress; public void setNotificationAddress(String notificationAddress) { this.notificationAddress = notificationAddress; } @EventListener public void processBlackListEvent(BlackListEvent event) { // notify appropriate parties via notificationAddress... } } 如上所示,方法签名实际上会推断出它监听的是哪一个类型的事件。这也适用于泛型嵌套,只要你在过滤的时候可以根据泛型参数解析出实际的事件。 如果你的方法需要监听好几个事件或根本没有参数定义它,事件类型也可以用注解本身指明: @EventListener({ContextStartedEvent.class, ContextRefreshedEvent.class}) public void handleContextStart() { } 对特殊的时间调用方法,根据定义的SpEL表达式来匹配实际情况,通过条件属性注解, 也可以通过condition注解来添加额外的运行过滤,它对一个特殊事件的方法实际调用是根据它是否匹配condition注解所定义的SpEL表达式。 例如,只要事件的测试属性等于foo,notifier可以被重写为只被调用: @EventListener(condition = "#blEvent.test == 'foo'") public void processBlackListEvent(BlackListEvent blEvent) { // notify appropriate parties via notificationAddress... } 每个SpEL表达式在此评估专用的上下文。下表列出的条目存在上下文中可用,所以可以调用他们处理conditional事件: 表3.8. 存在元数据中的Event SpEL 表达式 注意,#root.event允许你访问底层的时间,即使你的方法签名实际上是指已发布的任意对象。 如果您需要发布一个事件作为处理另一个事件的结果,只需更改方法签名来返回应该被发布的事件,如下所示: @EventListener public ListUpdateEvent handleBlackListEvent(BlackListEvent event) { // notify appropriate parties via notificationAddress and // then publish a ListUpdateEvent... } 异步监听器不支持这个特性 这个新方法将对上述方法处理的每个BlackListEvent都会发布一个新的ListUpdateEvent。如果需要发布多个时间,只需要返回事件集合即可。 如果你希望一个特定的监听器去异步处理事件,只需要重新使用常规的@Async支持: @EventListener @Async public void processBlackListEvent(BlackListEvent event) { // BlackListEvent is processed in a separate thread } 当使用异步事件的时候有下面两个限制: 1、 如果事件监听器抛出异常,则不会将其传播给调用者,查看AsyncUncaughtExceptionHandler获取详细信息; 2、 此类事件监听器无法发送回复如果你需要将处理结果发送给另一个时间,注入ApplicationEventPublisher里面手动发送事件; 如果你需要一个监听器在另一个监听器调用前被调用,只需要在方法声明上添加@Order注解: @EventListener @Order(42) public void processBlackListEvent(BlackListEvent event) { // notify appropriate parties via notificationAddress... } 泛型事件 你可以使用泛型来进一步的定义事件的结构。考虑EntityCreatedEvent,T的类型就是你要创建的真实类型。你可以创建下面的监听器定义,它只接受Person类型的EntityCreatedEvent: @EventListener public void onPersonCreated(EntityCreatedEvent<Person> event) { ... } 触发了事件解析泛型参数, 由于类型擦除,只有在触发了事件解析事件监听过器滤的泛型参数(类似于PersonCreatedEvent继承了EntityCreatedEvent { … }),此操作才会起作用。 在某些情况下,如果所有的时间都遵循相同的结果(上述事件应该是这样),这可能有点冗余。在这种情况下,你可以实现ResolvableTypeProvider来引导超出框架运行是环境提供的范围: public class EntityCreatedEvent<T> extends ApplicationEvent implements ResolvableTypeProvider { public EntityCreatedEvent(T entity) { super(entity); } @Override public ResolvableType getResolvableType() { return ResolvableType.forClassWithGenerics(getClass(), ResolvableType.forInstance(getSource())); } } 这不仅适用于ApplicationEvent,还可以作为时间发送的任意对象。 ## 3.15.3 便捷访问低优先级的资源 为了最佳使用和理解上下文,用户应该熟悉Spring资源抽象,如章节:章节4,资源。 一个应用上下文就是一个ResourceLoader,它可以用来载入资源。一个资源本质上讲就是JDK类java.net.URL功能更丰富的版本。实际上,Resource的实现在合适的地方包装了一个java.net.URL实例。资源可以以透明的方式从任何位置获得获取低优先级的资源,包括一个标准的URL,本地文件系统,任何描述标准URL的地方,其他的一些扩展。如果资源位置的字符串是一个没有任何特殊字符前缀的简单路径,那么这些资源就来自特定的并适合实际应用程序上下文类型。 你可以将bean的配置部署到一个实现了应用上下文的特殊回调接口ResourceLoaderAware中,以便在初始化的时候应用上下文把自己作为ResourceLoader传递进去可以自动调用。你也可以暴露资源的属性类型,用于访问静态资源;它们像其他属性一样会被注入。你可以像字符串路径一样指定这些资源的属性,并依赖自动注入上下文的特殊JavaBean的属性编辑器,以便在部署bean时将这些文本字符串转换为真实的对象。 提供给ApplicationContext 构造器的位置路径或路径都是真实的资源字符串,并以简单的形式对特定的上下文进行了适当的处理。ClassPathXmlApplicationContext将简单的路径作为类路径的位置。你也可以使用特殊前缀的位置路径(资源字符串)强制从类路径或者URL加载定义信息,而不管实际的上下文类型。 ## 3.15.4 便捷的ApplicationContext 实例化web 应用程序 你可以通过声明式创建ApplicationContext的实例,例如,ContextLoader。当然你也可以通过使用一个ApplicationContext的实现用编程的方式创建ApplicationContext的实例。 你可以像下面一样通过ContextLoaderListener来注册一个ApplicationContext: <context-param> <param-name>contextConfigLocation</param-name> <param-value>/WEB-INF/daoContext.xml /WEB-INF/applicationContext.xml</param-value> </context-param> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> 监听器会检查contextConfigLocation的参数。如果参数不存在,监听器默认会使用/WEB-INF/applicationContext.xml。当参数存在的是,监听器会通过预定义的分隔符来(逗号,分号和空格)来分隔字符串,并将其作为应用程序上下文的搜索位置。它也支持Ant风格的路径模式。例如:/WEB-INF/*Context.xml,在WEB-INF目录下,/WEB-INF/**/*Context.xml,WEB-INF子目录的所有这样类似的文件都会被发现。 ## 3.15.5 Spring ApplicationContext 作为Java EE RAR文件部署 可以将Spring ApplicationContext部署为RAR文件,将上下文及其所有必需的bean类和库JAR封装在Java EE RAR部署单元中。这相当于引导一个独立的ApplicationContext,只是托管在Java EE环境中,能够访问Java EE服务器设施。 在部署无头WAR文件(实际上,没有任何HTTP入口点,仅用于在Java EE环境中引导Spring ApplicationContext的WAR文件)的情况下RAR部署是更自然的替代方案。 RAR部署非常适合不需要HTTP入口点但仅由消息端点和调度作业组成的应用程序上下文。在这种情况下,Bean可以使用应用程序服务器资源,例如JTA事务管理器和JNDI绑定的JDBC DataSources和JMS ConnectionFactory实例,并且还可以通过Spring的标准事务管理和JNDI和JMX支持设施向平台的JMX服务器注册。应用程序组件还可以通过Spring的TaskExecutor抽象实现与应用程序服务器的JCA WorkManager交互。 通过查看 SpringContextResourceAdapter类的JavaDoc,可以知道用于RAR部署中涉及的配置详细信息。 对于Spring ApplicationContext作为Java EE RAR文件的简单部署:将所有应用程序类打包到RAR文件中,这是具有不同文件扩展名的标准JAR文件。将所有必需的库JAR添加到RAR归档的根目录中。添加一个“META-INF / ra.xml”部署描述符(如SpringContextResourceAdapter的JavaDoc中所示)和相应的Spring XML bean定义文件(通常为“META-INF / applicationContext.xml”),导致RAR文件进入应用程序服务器的部署目录。 [Note] 这种RAR部署单元通常是独立的; 它们不会将组件暴露给外界,甚至不会暴露给同一应用程序的其他模块。 与基于RAR的ApplicationContext的交互通常通过发生在与其他模块共享的JMS目标的情况下。 基于RAR的ApplicationContext还会在其他情况下使用,例如调度一些作业,对文件系统中的新文件(等等)作出反应。 如果需要允许从外部同步访问,它可以做到如导出RMI端点,然后很自然的可以由同一机器上的其他应用模块使用 可以将Spring ApplicationContext部署为RAR文件,将上下文和所有他所需的bean的类和JAR库封装在Java EE RAR部署单元中。这相当于独立启动一个ApplicationContext,它在Java EE环境中可以访问Java EE服务资源。RAR部署在一些没用头信息的war文件中更自然的选择,实际上,一个war文件在没有http入口的时候,那么它就仅仅是用来在Java EE环境中启动Spring ApplicationContext。 对于不需要HTTP入口点的应用上下文来说RAR部署是一种理想的方式,而不仅是一些消息端点和计划的任务。Bean在这样的上下文中可以使用应用服务器的资源,例如:JTA事务管理器、JNDI-bound JDBC DataSources 和JMS连接工厂实例,也可以通过Spring标准事务管理器、JNDI和JMX支持来注册平台的JMX服务。应用组件也可以通过Spring 抽象TaskExecutor来和应用服务器的JCA WorkManager来进行交互。 有关RAR部署中涉及的配置详细信息请查看 JavaDoc中的SpringContextResourceAdapter。 Spring ApplicationContext 作为Java EE RAR文件的简单部署:将所有的应用类打包进一个RAR文件,它和标准的JAR文件有不同的文件扩展名。将所有需要的库JAR文件添加到RAR归档的根目录中。添加一个”META-INF/ra.xml”部署描述文件(如SpringContextResourceAdapters JavaDoc所示),并更改Spring XML中bean的定义文件(通常为“META-INF / applicationContext.xml”),将生成的rar文件放到音符服务器的部署目录。 这种RAR部署单元通常是独立的;它们不会将组建暴露给外界,甚至是同一个应用的其他模块。同基于RAR的ApplicationContext交互通常是通过模块共享的JMS来实现的。一个基于RAR的应用上下文,例如:某些调度任务,文件系统对新文件产生的响应等。如果要允许外界的同步访问,则可以导出RMI端点,这当然可能是同一台机器上的其他应用模块。 ## 3.16 BeanFactory BeanFactory为Spring的IoC功能提供了底层的基础,但是它仅仅被用于和第三方框架的集成,现在对于大部分的Spring用户来说都是历史了。BeanFactory及其相关的接口,例如:BeanFactoryAware,InitializingBean,DisposableBean,在Spring中仍然有所保留,目的就是为了让大量的第三方框架和Spring集成时保持向后兼容。通常第三方组件不会更加现代的等价物,例如:@PostConstruct 或 @PreDestroy,以便可以与JDK1.4兼容,或避免依赖JSR-250。 这部分提供了BeanFactory 和 ApplicationContext之间的背景差异以及用户怎样通过查找单例的模式来访问IoC容器。 ## 3.16.1 BeanFactory or ApplicationContext? 尽量使用ApplicationContext除非你有更好的理由不用它。 因为ApplicationContext包括了BeanFactory的所有功能,通常也优于BeanFactory,除非一些少数的场景,例如:在受资源约束的嵌入式设备上运行一个嵌入式应用,它的内存消耗可能至关重要,并且可能会产生字节。然而,对于大多数典型的企业级应用和系统来说,ApplicationContext才是你想使用的。Spring大量使用了BeanPostProcessor扩展点(以便使用代理等)。如果你仅仅只使用简单的BeanFactory,很多的支持功能将不会有效,例如:事务和AOP,但至少不会有额外的步骤。这可能会比较迷惑,毕竟配置又没有错。 下表列了BeanFactory 和 ApplicationContext接口和实现的一些特性: 用BeanFactory的实现来明确的注册一个bean的后置处理器,你需要写和下面类似的代码: DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); // populate the factory with bean definitions // now register any needed BeanPostProcessor instances MyBeanPostProcessor postProcessor = new MyBeanPostProcessor(); factory.addBeanPostProcessor(postProcessor); // now start using the factory 当使用一个BeanFactory的实现来明确的注册一个BeanFactoryPostProcessor时,你写的代码必须和下面类似: DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(factory); reader.loadBeanDefinitions(new FileSystemResource("beans.xml")); // bring in some property values from a Properties file PropertyPlaceholderConfigurer cfg = new PropertyPlaceholderConfigurer(); cfg.setLocation(new FileSystemResource("jdbc.properties")); // now actually do the replacement cfg.postProcessBeanFactory(factory); 在这两种情况下,明确的注册步不是很方便,这也就是为什么在大多数支持Spring的应用中,ApplicationContext的各种实现都优于BeanFactory实现的原因之一,特别是当使用BeanFactoryPostProcessors和BeanPostProcessors的时候。这些机制实现了一些很重要的功能,例如:属性的占位替换和AOP。 ## 16、Spring源码分析 - 16-AutowiredAnnotationBeanPostProcessor AutowiredAnnotationBeanPostProcessor继承了InstantiationAwareBeanPostProcessorAdapter间接实现了SmartInstantiationAwareBeanPostProcessor接口覆盖了determineCandidateConstructors()方法,此方法在实例化bean前选取一个构造方法数组,请参考bean创建的流程/zhuanlan/j2ee/spring/7/11.html和/zhuanlan/j2ee/spring/7/12.html 先看determineCandidateConstructors()方法的前一小段是处理@Lookup注解的。 if (!this.lookupMethodsChecked.contains(beanName)) { try { ReflectionUtils.doWithMethods(beanClass, method -> { Lookup lookup = method.getAnnotation(Lookup.class); if (lookup != null) { Assert.state(beanFactory != null, "No BeanFactory available"); LookupOverride override = new LookupOverride(method, lookup.value()); try { RootBeanDefinition mbd = (RootBeanDefinition) beanFactory.getMergedBeanDefinition(beanName); mbd.getMethodOverrides().addOverride(override); } catch (NoSuchBeanDefinitionException ex) { throw new BeanCreationException(beanName, "Cannot apply @Lookup to beans without corresponding bean definition"); } } }); } catch (IllegalStateException ex) { throw new BeanCreationException(beanName, "Lookup method resolution failed", ex); } this.lookupMethodsChecked.add(beanName); } 将beanClass被@Lookup标记的方法封装到LookupOverride中,通过mbd.getMethodOverrides().addOverride(override)加入到BeanDefinition中,后面会在AbstractAutowireCapableBeanFactory的instantiateBean()方法中,会使用CglibSubclassingInstantiationStrategy的instantiate()方法创建bean对象。 @Override public Object instantiate(RootBeanDefinition bd, @Nullable String beanName, BeanFactory owner) { // Don't override the class with CGLIB if no overrides. if (!bd.hasMethodOverrides()) { Constructor<?> constructorToUse; synchronized (bd.constructorArgumentLock) { constructorToUse = (Constructor<?>) bd.resolvedConstructorOrFactoryMethod; if (constructorToUse == null) { final Class<?> clazz = bd.getBeanClass(); if (clazz.isInterface()) { throw new BeanInstantiationException(clazz, "Specified class is an interface"); } try { if (System.getSecurityManager() != null) { constructorToUse = AccessController.doPrivileged( (PrivilegedExceptionAction<Constructor<?>>) clazz::getDeclaredConstructor); } else { constructorToUse = clazz.getDeclaredConstructor(); } bd.resolvedConstructorOrFactoryMethod = constructorToUse; } catch (Throwable ex) { throw new BeanInstantiationException(clazz, "No default constructor found", ex); } } } return BeanUtils.instantiateClass(constructorToUse); } else { // Must generate CGLIB subclass. return instantiateWithMethodInjection(bd, beanName, owner); } } 可以看到如果!bd.hasMethodOverrides()则走instantiateWithMethodInjection()方法,这个方法内使用了CglibSubclassingInstantiationStrategy内部类CglibSubclassCreator来创建cglib的代理对象。CglibSubclassCreator使用的callback拦截器就包含LookupOverrideMethodInterceptor。 private static class LookupOverrideMethodInterceptor extends CglibIdentitySupport implements MethodInterceptor { private final BeanFactory owner; public LookupOverrideMethodInterceptor(RootBeanDefinition beanDefinition, BeanFactory owner) { super(beanDefinition); this.owner = owner; } @Override public Object intercept(Object obj, Method method, Object[] args, MethodProxy mp) throws Throwable { // Cast is safe, as CallbackFilter filters are used selectively. LookupOverride lo = (LookupOverride) getBeanDefinition().getMethodOverrides().getOverride(method); Assert.state(lo != null, "LookupOverride not found"); Object[] argsToUse = (args.length > 0 ? args : null); // if no-arg, don't insist on args at all if (StringUtils.hasText(lo.getBeanName())) { return (argsToUse != null ? this.owner.getBean(lo.getBeanName(), argsToUse) : this.owner.getBean(lo.getBeanName())); } else { return (argsToUse != null ? this.owner.getBean(method.getReturnType(), argsToUse) : this.owner.getBean(method.getReturnType())); } } } 如果调用的方法是被标记的,如果配置了@Lookup的value属性则从BeanFactory中取出bean name为@Lookup的value属性的bean对象返回,否则根据方法的返回类型从BeanFactory中取出相应的bean对象。 以上就是@Lookup的处理过程。 determineCandidateConstructors()方法的剩余部分就是挑选出候选的构造方法,从所有声明的构造方法选取带有@Autowired注解的,但是@Autowired的required==true的只能有一个,如果还有无参构造方法,再加上无参构造方法。如下: // Quick check on the concurrent map first, with minimal locking. Constructor<?>[] candidateConstructors = this.candidateConstructorsCache.get(beanClass); if (candidateConstructors == null) { // Fully synchronized resolution now... synchronized (this.candidateConstructorsCache) { candidateConstructors = this.candidateConstructorsCache.get(beanClass); if (candidateConstructors == null) { Constructor<?>[] rawCandidates; try { rawCandidates = beanClass.getDeclaredConstructors(); } catch (Throwable ex) { throw new BeanCreationException(beanName, "Resolution of declared constructors on bean Class [" + beanClass.getName() + "] from ClassLoader [" + beanClass.getClassLoader() + "] failed", ex); } List<Constructor<?>> candidates = new ArrayList<>(rawCandidates.length); Constructor<?> requiredConstructor = null; Constructor<?> defaultConstructor = null; Constructor<?> primaryConstructor = BeanUtils.findPrimaryConstructor(beanClass); int nonSyntheticConstructors = 0; for (Constructor<?> candidate : rawCandidates) { // 非合成的,用户自定义的构造方法都是非合成的,JVM可能会产生合成的因此过滤掉 if (!candidate.isSynthetic()) { nonSyntheticConstructors++; } else if (primaryConstructor != null) { continue; } // 构造方法上所有注解属性 AnnotationAttributes ann = findAutowiredAnnotation(candidate); // 子类没有的话可能父类构造方法存在(覆盖了) if (ann == null) { Class<?> userClass = ClassUtils.getUserClass(beanClass); if (userClass != beanClass) { try { Constructor<?> superCtor = userClass.getDeclaredConstructor(candidate.getParameterTypes()); ann = findAutowiredAnnotation(superCtor); } catch (NoSuchMethodException ex) { // Simply proceed, no equivalent superclass constructor found... } } } if (ann != null) { // 只允许存在一个@Autowired(required=true)的构造方法 if (requiredConstructor != null) { throw new BeanCreationException(beanName, "Invalid autowire-marked constructor: " + candidate + ". Found constructor with 'required' Autowired annotation already: " + requiredConstructor); } boolean required = determineRequiredStatus(ann); if (required) { if (!candidates.isEmpty()) { throw new BeanCreationException(beanName, "Invalid autowire-marked constructors: " + candidates + ". Found constructor with 'required' Autowired annotation: " + candidate); } requiredConstructor = candidate; } candidates.add(candidate); } // 默认构造方法 else if (candidate.getParameterCount() == 0) { defaultConstructor = candidate; } } if (!candidates.isEmpty()) { // Add default constructor to list of optional constructors, as fallback. if (requiredConstructor == null) { // 如果不存在@Autowired构造方法,无惨构造方法没有@Autowired也作为候选 if (defaultConstructor != null) { candidates.add(defaultConstructor); } else if (candidates.size() == 1 && logger.isInfoEnabled()) { logger.info("Inconsistent constructor declaration on bean with name '" + beanName + "': single autowire-marked constructor flagged as optional - " + "this constructor is effectively required since there is no " + "default constructor to fall back to: " + candidates.get(0)); } } candidateConstructors = candidates.toArray(new Constructor<?>[0]); } // 只存在一个构造方法只能用它了 else if (rawCandidates.length == 1 && rawCandidates[0].getParameterCount() > 0) { candidateConstructors = new Constructor<?>[] {rawCandidates[0]}; } // 如果只有两个构造方法,一个是带有@Primary一个是无参不带注解的,则这两个都作为候选构造方法 else if (nonSyntheticConstructors == 2 && primaryConstructor != null && defaultConstructor != null && !primaryConstructor.equals(defaultConstructor)) { candidateConstructors = new Constructor<?>[] {primaryConstructor, defaultConstructor}; } // 只有一个@Primary构造方法 else if (nonSyntheticConstructors == 1 && primaryConstructor != null) { candidateConstructors = new Constructor<?>[] {primaryConstructor}; } else { candidateConstructors = new Constructor<?>[0]; } this.candidateConstructorsCache.put(beanClass, candidateConstructors); } } } return (candidateConstructors.length > 0 ? candidateConstructors : null); 一个bean实例创建之后但此时还没有调用populateBean()方法完成属性的填充时,会经历MergedBeanDefinitionPostProcessor的postProcessMergedBeanDefinition()方法。 @Override public void postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition, Class<?> beanType, String beanName) { // InjectionMetadata可对指定对象进行注入(字段或方法) InjectionMetadata metadata = findAutowiringMetadata(beanName, beanType, null); metadata.checkConfigMembers(beanDefinition); } private InjectionMetadata findAutowiringMetadata(String beanName, Class<?> clazz, @Nullable PropertyValues pvs) { // Fall back to class name as cache key, for backwards compatibility with custom callers. String cacheKey = (StringUtils.hasLength(beanName) ? beanName : clazz.getName()); // Quick check on the concurrent map first, with minimal locking. InjectionMetadata metadata = this.injectionMetadataCache.get(cacheKey); if (InjectionMetadata.needsRefresh(metadata, clazz)) { synchronized (this.injectionMetadataCache) { metadata = this.injectionMetadataCache.get(cacheKey); if (InjectionMetadata.needsRefresh(metadata, clazz)) { if (metadata != null) { metadata.clear(pvs); } // 构建由@Autowired、@Value、@Inject注解的字段或方法的InjectionMetadata metadata = buildAutowiringMetadata(clazz); this.injectionMetadataCache.put(cacheKey, metadata); } } } return metadata; } injectionMetadataCache是一个Map对象,缓存beanName与InjectionMetadata。InjectionMetadata对象通过buildAutowiringMetadata()方法来创建的。 private InjectionMetadata buildAutowiringMetadata(final Class<?> clazz) { LinkedList<InjectionMetadata.InjectedElement> elements = new LinkedList<>(); Class<?> targetClass = clazz; do { final LinkedList<InjectionMetadata.InjectedElement> currElements = new LinkedList<>(); ReflectionUtils.doWithLocalFields(targetClass, field -> { AnnotationAttributes ann = findAutowiredAnnotation(field); if (ann != null) { if (Modifier.isStatic(field.getModifiers())) { if (logger.isWarnEnabled()) { logger.warn("Autowired annotation is not supported on static fields: " + field); } return; } boolean required = determineRequiredStatus(ann); // 保存带有自动装配注解的非静态字段 currElements.add(new AutowiredFieldElement(field, required)); } }); ReflectionUtils.doWithLocalMethods(targetClass, method -> { Method bridgedMethod = BridgeMethodResolver.findBridgedMethod(method); if (!BridgeMethodResolver.isVisibilityBridgeMethodPair(method, bridgedMethod)) { return; } AnnotationAttributes ann = findAutowiredAnnotation(bridgedMethod); if (ann != null && method.equals(ClassUtils.getMostSpecificMethod(method, clazz))) { if (Modifier.isStatic(method.getModifiers())) { if (logger.isWarnEnabled()) { logger.warn("Autowired annotation is not supported on static methods: " + method); } return; } if (method.getParameterCount() == 0) { if (logger.isWarnEnabled()) { logger.warn("Autowired annotation should only be used on methods with parameters: " + method); } } boolean required = determineRequiredStatus(ann); PropertyDescriptor pd = BeanUtils.findPropertyForMethod(bridgedMethod, clazz); // 保存带有自动装配注解的非静态方法 currElements.add(new AutowiredMethodElement(method, required, pd)); } }); elements.addAll(0, currElements); targetClass = targetClass.getSuperclass(); } while (targetClass != null && targetClass != Object.class); return new InjectionMetadata(clazz, elements); } buildAutowiringMetadata()方法分为对字段与方法的@Autowired(@Value,@Inject)注解处理。将标记@Autowired的费静态字段封装为AutowiredFieldElement对象,将标记@Autowired的非静态方法封装到AutowiredMethodElement对象然后都加入到InjectionMetadata中,此过程是一个循环过程此类处理完会沿着父类继续向上处理。AutowiredFieldElement与AutowiredMethodElement都是InjectionMetadata.InjectedElement的子类,都覆盖了父类的inject()方法这个下面会介绍。 通过findAutowiringMetadata()方法得到了InjectionMetadata对象之后调用了给对象的checkConfigMembers()方法。这个方法就是将上面保存的InjectedElement对象筛选出那些不存在于beanDefinition中的赋值给checkedElements。 public void checkConfigMembers(RootBeanDefinition beanDefinition) { Set<InjectedElement> checkedElements = new LinkedHashSet<>(this.injectedElements.size()); for (InjectedElement element : this.injectedElements) { Member member = element.getMember(); if (!beanDefinition.isExternallyManagedConfigMember(member)) { beanDefinition.registerExternallyManagedConfigMember(member); checkedElements.add(element); if (logger.isDebugEnabled()) { logger.debug("Registered injected element on class [" + this.targetClass.getName() + "]: " + element); } } } this.checkedElements = checkedElements; } 到这为止就是postProcessMergedBeanDefinition()方法的全部实现逻辑并没有做什么实际事情,其实是为了后面的行为做铺垫,请看postProcessPropertyValues()方法(在Spring5.1推荐使用postProcessProperties方法),此方法是在属性填充阶段被调用的。 @Override public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName) { InjectionMetadata metadata = findAutowiringMetadata(beanName, bean.getClass(), pvs); try { metadata.inject(bean, beanName, pvs); } catch (BeanCreationException ex) { throw ex; } catch (Throwable ex) { throw new BeanCreationException(beanName, "Injection of autowired dependencies failed", ex); } return pvs; } @Deprecated @Override public PropertyValues postProcessPropertyValues( PropertyValues pvs, PropertyDescriptor[] pds, Object bean, String beanName) { return postProcessProperties(pvs, bean, beanName); } 首先调用 findAutowiringMetadata()方法从缓存中再次取到InjectionMetadata对象,调用inject()方法完成autowired注入。 public void inject(Object target, @Nullable String beanName, @Nullable PropertyValues pvs) throws Throwable { Collection<InjectedElement> checkedElements = this.checkedElements; Collection<InjectedElement> elementsToIterate = (checkedElements != null ? checkedElements : this.injectedElements); if (!elementsToIterate.isEmpty()) { boolean debug = logger.isDebugEnabled(); for (InjectedElement element : elementsToIterate) { if (debug) { logger.debug("Processing injected element of bean '" + beanName + "': " + element); } element.inject(target, beanName, pvs); } } } 因为之前已经把类@Autowired注解标记的字段或方法封装到了响应的InjectedElement子类中了,此时依次调用子类的inject()方法完成对象的注入,下面分别看一看这两种注入的实现。 AutowiredFieldElement,调用BeanFactory的resolveDependency()方法返回一个符合字段类型的bean对象设置到此bean上。 @Override protected void inject(Object bean, @Nullable String beanName, @Nullable PropertyValues pvs) throws Throwable { Field field = (Field) this.member; Object value; // 解析过一次会缓存cachedFieldValue if (this.cached) { value = resolvedCachedArgument(beanName, this.cachedFieldValue); } else { DependencyDescriptor desc = new DependencyDescriptor(field, this.required); desc.setContainingClass(bean.getClass()); Set<String> autowiredBeanNames = new LinkedHashSet<>(1); Assert.state(beanFactory != null, "No BeanFactory available"); TypeConverter typeConverter = beanFactory.getTypeConverter(); try { value = beanFactory.resolveDependency(desc, beanName, autowiredBeanNames, typeConverter); } catch (BeansException ex) { throw new UnsatisfiedDependencyException(null, beanName, new InjectionPoint(field), ex); } synchronized (this) { if (!this.cached) { if (value != null || this.required) { this.cachedFieldValue = desc; registerDependentBeans(beanName, autowiredBeanNames); if (autowiredBeanNames.size() == 1) { String autowiredBeanName = autowiredBeanNames.iterator().next(); if (beanFactory.containsBean(autowiredBeanName) && beanFactory.isTypeMatch(autowiredBeanName, field.getType())) { // 以后注入就可以直接使用这个带有beanname的DependencyDescriptor更高效 this.cachedFieldValue = new ShortcutDependencyDescriptor( desc, autowiredBeanName, field.getType()); } } } else { this.cachedFieldValue = null; } this.cached = true; } } } // 字段赋值 if (value != null) { ReflectionUtils.makeAccessible(field); field.set(bean, value); } } AutowiredMethodElement,类似通过setter方法完成属性的注入。 @Override protected void inject(Object bean, @Nullable String beanName, @Nullable PropertyValues pvs) throws Throwable { if (checkPropertySkipping(pvs)) { return; } Method method = (Method) this.member; Object[] arguments; if (this.cached) { // Shortcut for avoiding synchronization... arguments = resolveCachedArguments(beanName); } else { Class<?>[] paramTypes = method.getParameterTypes(); arguments = new Object[paramTypes.length]; DependencyDescriptor[] descriptors = new DependencyDescriptor[paramTypes.length]; Set<String> autowiredBeans = new LinkedHashSet<>(paramTypes.length); Assert.state(beanFactory != null, "No BeanFactory available"); TypeConverter typeConverter = beanFactory.getTypeConverter(); for (int i = 0; i < arguments.length; i++) { MethodParameter methodParam = new MethodParameter(method, i); DependencyDescriptor currDesc = new DependencyDescriptor(methodParam, this.required); currDesc.setContainingClass(bean.getClass()); descriptors[i] = currDesc; try { Object arg = beanFactory.resolveDependency(currDesc, beanName, autowiredBeans, typeConverter); if (arg == null && !this.required) { arguments = null; break; } arguments[i] = arg; } catch (BeansException ex) { throw new UnsatisfiedDependencyException(null, beanName, new InjectionPoint(methodParam), ex); } } synchronized (this) { if (!this.cached) { if (arguments != null) { Object[] cachedMethodArguments = new Object[paramTypes.length]; System.arraycopy(descriptors, 0, cachedMethodArguments, 0, arguments.length); registerDependentBeans(beanName, autowiredBeans); if (autowiredBeans.size() == paramTypes.length) { Iterator<String> it = autowiredBeans.iterator(); for (int i = 0; i < paramTypes.length; i++) { String autowiredBeanName = it.next(); if (beanFactory.containsBean(autowiredBeanName) && beanFactory.isTypeMatch(autowiredBeanName, paramTypes[i])) { cachedMethodArguments[i] = new ShortcutDependencyDescriptor( descriptors[i], autowiredBeanName, paramTypes[i]); } } } this.cachedMethodArguments = cachedMethodArguments; } else { this.cachedMethodArguments = null; } this.cached = true; } } } if (arguments != null) { try { ReflectionUtils.makeAccessible(method); method.invoke(bean, arguments); } catch (InvocationTargetException ex){ throw ex.getTargetException(); } } } ## 4.1 介绍 仅仅使用 JAVA 的 java.net.URL 和针对不同 URL 前缀的标准处理器,并不能满足我们对各种底层资源的访问,比如:我们就不能通过 URL 的标准实现来访问相对类路径或者相对 ServletContext 的各种资源。虽然我们可以针对特定的 url 前缀来注册一个新的 URLStreamHandler(和现有的针对各种特定前缀的处理器类似,比如 http:),然而这往往会是一件比较麻烦的事情(要求了解 url 的实现机制等),而且 url 接口也缺少了部分基本的方法,如检查当前资源是否存在的方法。 ## 4.2 Resource 接口 相对标准 url 访问机制,spring 的 Resource 接口对抽象底层资源的访问提供了一套更好的机制。 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(); } public interface InputStreamSource { InputStream getInputStream() throws IOException; } Resource 接口里的最重要的几个方法: getInputStream(): 定位并且打开当前资源,返回当前资源的 InputStream。预计每一次调用都会返回一个新的 – InputStream,因此关闭当前输出流就成为了调用者的责任。 exists(): 返回一个 boolean,表示当前资源是否真的存在。 isOpen(): 返回一个 boolean,表示当前资源是否一个已打开的输入流。如果结果为 true,返回的 InputStream 不能多次读取,只能是一次性读取之后,就关闭 InputStream,以防止内存泄漏。除了 InputStreamResource,其他常用 Resource 实现都会返回 false。 getDescription(): 返回当前资源的描述,当处理资源出错时,资源的描述会用于错误信息的输出。一般来说,资源的描述是一个完全限定的文件名称,或者是当前资源的真实 url。 Resource 接口里的其他方法可以让你获得代表当前资源的 URL 或 File 对象(前提是底层实现可兼容的,也支持该功能)。 Resource抽象在Spring本身被广泛使用,作为需要资源的许多方法签名中的参数类型。 某些Spring API中的其他方法(例如各种ApplicationContext实现的构造函数)采用一个String,它以未安装或简单的形式用于创建适用于该上下文实现的资源,或者通过String路径上的特殊前缀,允许调用者 以指定必须创建和使用特定的资源实现。 Resource 接口(实现)不仅可以被 spring 大量的应用,其也非常适合作为你编程中访问资源的辅助工具类。当你仅需要使用到 Resource 接口实现时,可以直接忽略 spring 的其余部分。单独使用 Rsourece 实现,会造成代码与 spring 的部分耦合,可也仅耦合了其中一小部分辅助类,而且你可以将 Reource 实现作为 URL 的一种访问底层更为有效的替代,与你引入其他库来达到这种目的是一样的。 需要注意的是 Resource 实现并没有去重新发明轮子,而是尽可能地采用封装。举个例子,UrlResource 里就封装了一个 URL 对象,在其内的逻辑就是通过封装的 URL 对象来完成的。 ## 4.3 内置的 Resource 实现 spring 直接提供了多种开箱即用的 Resource 实现。 ## 4.3.1 UrlResource UrlResource 封装了一个 java.net.URL 对象,用来访问 URL 可以正常访问的任意对象,比如文件、an HTTP target, an FTP target, 等等。所有的 URL 都可以用一个标准化的字符串来表示。如通过正确的标准化前缀,可以用来表示当前 URL 的类型,当中就包括用于访问文件系统路径的 file:,通过 http 协议访问资源的 http:,通过 ftp 协议访问资源的 ftp:,还有很多…… 可以显式化地使用 UrlResource 构造函数来创建一个 UrlResource,不过通常我们可以在调用一个 api 方法是,使用一个代表路径的 String 参数来隐式创建一个 UrlResource。对于后一种情况,会由一个 javabean PropertyEditor 来决定创建哪一种 Resource。如果路径里包含某一个通用的前缀(如 classpath:),PropertyEditor 会根据这个通用的前缀来创建恰当的 Resource;反之,如果 PropertyEditor 无法识别这个前缀,会把这个路径作为一个标准的 URL 来创建一个 UrlResource。 ## 4.3.2 ClassPathResource ClassPathResource 可以从类路径上加载资源,其可以使用线程上下文加载器、指定加载器或指定的 class 类型中的任意一个来加载资源。 当类路径上资源存于文件系统中,ClassPathResource 支持以 java.io.File 的形式访问,可当类路径上的资源存于尚未解压(没有 被Servlet 引擎或其他可解压的环境解压)的 jar 包中,ClassPathResource 就不再支持以 java.io.File 的形式访问。鉴于上面所说这个问题,spring 中各式 Resource 实现都支持以 jave.net.URL 的形式访问。 可以显式使用 ClassPathResource 构造函数来创建一个 ClassPathResource ,不过通常我们可以在调用一个 api 方法时,使用一个代表路径的 String 参数来隐式创建一个 ClassPathResource。对于后一种情况,会由一个 javabean PropertyEditor 来识别路径中 classpath: 前缀,从而创建一个 ClassPathResource。 ## 4.3.3 FileSystemResource 这是针对 java.io.File 提供的 Resource 实现。显然,我们可以使用 FileSystemResource 的 getFile() 函数获取 File 对象,使用 getURL() 获取 URL 对象。 ## 4.3.4 ServletContextResource 这是为了获取 web 根路径的 ServletContext 资源而提供的 Resource 实现。 ServletContextResource 完全支持以流和 URL 的方式访问,可只有当 web 项目是已解压的(不是以 war 等压缩包形式存在)且该 ServletContext 资源存于文件系统里,ServletContextResource 才支持以 java.io.File 的方式访问。至于说到,我们的 web 项目是否已解压和相关的 ServletContext 资源是否会存于文件系统里,这个取决于我们所使用的 Servlet 容器。若 Servlet 容器没有解压 web 项目,我们可以直接以 JAR 的形式的访问,或者其他可以想到的方式(如访问数据库)等。 ## 4.3.5 InputStreamResource 这是针对 InputStream 提供的 Resource 实现。建议,在确实没有找到其他合适的 Resource 实现时,才使用 InputSteamResource。如果可以,尽量选择 ByteArrayResource 或其他基于文件的 Resource 实现来代替。 与其他Resource 实现已比较,InputStreamRsource 倒像一个已打开资源的描述符,因此,调用 isOpen() 方法会返回 true。除了在需要获取资源的描述符或需要从输入流多次读取时,都不要使用 InputStreamResource 来读取资源。 ## 4.3.6 ByteArrayResource 这是针对字节数组提供的 Resource 实现。可以通过一个字节数组来创建 ByteArrayResource。 当需要从字节数组加载内容时,ByteArrayResource 是一个不错的选择,使用 ByteArrayResource 可以不用求助于 InputStreamResource。 ## 4.4 ResourceLoader 接口 ResourceLoader 接口是用来加载 Resource 对象的,换句话说,就是当一个对象需要获取 Resource 实例时,可以选择实现 ResourceLoader 接口。 public interface ResourceLoader { Resource getResource(String location); } spring 里所有的应用上下文都是实现了 ResourceLoader 接口,因此,所有应用上下文都可以通过 getResource() 方法获取 Resource 实例。 当你在指定应用上下文调用 getResource() 方法时,而指定的位置路径又没有包含特定的前缀,spring 会根据当前应用上下文来决定返回哪一种类型 Resource。举个例子,假设下面的代码片段是通过 ClassPathXmlApplicationContext 实例来调用的, Resource template = ctx.getResource("some/resource/path/myTemplate.txt"); 那spring 会返回一个 ClassPathResource 对象;类似的,如果是通过实例 FileSystemXmlApplicationContext 实例调用的,返回的是一个 FileSystemResource 对象;如果是通过 WebApplicationContext 实例的,返回的是一个 ServletContextResource 对象…… 如上所说,你就可以在指定的应用上下中使用 Resource 实例来加载当前应用上下文的资源。 还有另外一种场景里,如在其他应用上下文里,你可能会强制需要获取一个 ClassPathResource 对象,这个时候,你可以通过加上指定的前缀来实现这一需求,如: Resource template = ctx.getResource("classpath:some/resource/path/myTemplate.txt"); 类似的,你可以通过其他任意的 url 前缀来强制获取 UrlResource 对象: Resource template = ctx.getResource("file:///some/resource/path/myTemplate.txt"); Resource template = ctx.getResource("http://myhost.com/resource/path/myTemplate.txt"); 下面,给出一个表格来总结一下 spring 根据各种位置路径加载资源的策略: Table 4.1. Resource strings ## 4.5 ResourceLoaderAware 接口 ResourceLoaderAware 是一个特殊的标记接口,用来标记提供 ResourceLoader 引用的对象。 public interface ResourceLoaderAware { void setResourceLoader(ResourceLoader resourceLoader); } 当将一个 ResourceLoaderAware 接口的实现类部署到应用上下文时(此类会作为一个 spring 管理的 bean), 应用上下文会识别出此为一个 ResourceLoaderAware 对象,并将自身作为一个参数来调用 setResourceLoader() 函数,如此,该实现类便可使用 ResourceLoader 获取 Resource 实例来加载你所需要的资源。(附:为什么能将应用上下文作为一个参数来调用 setResourceLoader() 函数呢?不要忘了,在前文有谈过,spring 的所有上下文都实现了 ResourceLoader 接口)。 当然了,一个 bean 若想加载指定路径下的资源,除了刚才提到的实现 ResourcesLoaderAware 接口之外(将 ApplicationContext 作为一个 ResourceLoader 对象注入),bean 也可以实现 ApplicationContextAware 接口,这样可以直接使用应用上下文来加载资源。但总的来说,在需求满足都满足的情况下,最好是使用的专用 ResourceLoader 接口,因为这样代码只会与接口耦合,而不会与整个 spring ApplicationContext 耦合。与 ResourceLoader 接口耦合,抛开 spring 来看,就是提供了一个加载资源的工具类接口。 从spring 2.5 开始,除了实现 ResourceLoaderAware 接口,也可采取另外一种替代方案——依赖于 ResourceLoader 的自动装配。”传统”的 constructor 和 bytype 自动装配模式都支持 ResourceLoader 的装配(可参阅 Section 5.4.5, “自动装配协作者” )——前者以构造参数的形式装配,后者以 setter 方法中参数装配。若为了获得更大的灵活性(包括属性注入的能力和多参方法),可以考虑使用基于注解的新注入方式。使用注解 @Autowiring 标记 ResourceLoader 变量,便可将其注入到成员属性、构造参数或方法参数中( @autowiring 详细的使用方法可参考Section 3.9.2, “@Autowired”.)。 ## 4.6 资源依赖 如果bean本身将通过某种动态过程来确定和提供资源路径,那么bean可以使用ResourceLoader接口来加载资源。 j假设以某种方式加载一个模板,其中需要的特定资源取决于用户的角色。 如果资源是静态的,那么完全消除ResourceLoader接口的使用是有意义的,只需让bean公开它需要的Resource属性,那么它们就会以你所期望的方式被注入。 什么使得它们轻松注入这些属性,是所有应用程序上下文注册和使用一个特殊的JavaBeans PropertyEditor,它可以将String路径转换为Resource对象。 因此,如果myBean具有“资源”类型的模板属性,则可以使用该资源的简单字符串进行配置,如下所示: <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"/> ## 4.7 应用上下文和资源路径 ## 4.7.1 构造应用上下文 (某一特定)应用上下文的构造器通常可以使用字符串或字符串数组所指代的(多个)资源(如 xml 文件)来构造当前上下文。 当指定的位置路径没有带前缀时,那从指定位置路径创建的 Resource 类型(用于后续加载 bean 定义),取决于所使用应用上下文。举个列子,如下所创建的 ClassPathXmlApplicationContext : ApplicationContext ctx = new ClassPathXmlApplicationContext("conf/appContext.xml"); 会从类路径加载 bean 的定义,因为所创建的 Resource 实例是 ClassPathResource.但所创建的是 FileSystemXmlApplicationContext 时, ApplicationContext ctx = new FileSystemXmlApplicationContext("conf/appContext.xml"); 则会从文件系统加载 bean 的定义,这种情况下,资源路径是相对工作目录而言的。 注意:若位置路径带有 classpath 前缀或 URL 前缀,会覆盖默认创建的用于加载 bean 定义的 Resource 类型,比如这种情况下的 FileSystemXmlApplicationContext ApplicationContext ctx = new FileSystemXmlApplicationContext("classpath:conf/appContext.xml"); ,实际是从类路径下加载了 bean 的定义。可是,这个上下文仍然是 FileSystemXmlApplicationContext,而不是 ClassPathXmlApplicationContext,在后续作为 ResourceLoader 来使用时,不带前缀的路径仍然会从文件系统中加载。 构造 ClassPathXmlApplicationContext 实例 – 快捷方式 ClassPathXmlApplicationContext 提供了多个构造函数,以利于快捷创建 ClassPathXmlApplicationContext 的实例。最好莫不过使用只包含多个 xml 文件名(不带路径信息)的字符串数组和一个 Class 参数的构造器,所省略路径信息 ClassPathXmlApplicationContext 会从 Class 参数 获取: 下面的这个例子,可以让你对个构造器有比较清晰的认识。试想一个如下类似的目录结构: com/ foo/ services.xml daos.xml MessengerService.class 由services.xml 和 daos.xml 中 bean 所组成的 ClassPathXmlApplicationContext,可以这样来初始化: ApplicationContext ctx = new ClassPathXmlApplicationContext( new String[] {"services.xml", "daos.xml"}, MessengerService.class); 欲要知道 ClassPathXmlApplicationContext 更多不同类型的构造器,请查阅 Javadocs 文档。 ## 4.7.2 使用通配符构造应用上下文 从前文可知,应用上下文构造器的中的资源路径可以是单一的路径(即一对一地映射到目标资源);另外资源路径也可以使用高效的通配符——可包含 classpath*:前缀 或 ant 风格的正则表达式(使用 spring 的 PathMatcher 来匹配)。 通配符机制的其中一种应用可以用来组装组件式的应用程序。应用程序里所有组件都可以在一个共知的位置路径发布自定义的上下文片段,则最终应用上下文可使用 classpath*: 在同一路径前缀(前面的共知路径)下创建,这时所有组件上下文的片段都会被自动组装。 谨记,路径中的通配符特定用于应用上下文的构造器,只会在应用构造时有效,与其 Resource 自身类型没有任何关系。不可以使用 classpth*:来构造任一真实的 Resource,因为一个资源点一次只可以指向一个资源。(如果直接使用 PathMatcher 的工具类,也可以在路径中使用通配符) ## Ant 风格模式 以下是一些使用了 Ant 风格的位置路径: /WEB-INF/*-context.xml com/mycompany/**/applicationContext.xml file:C:/some/path/*-context.xml classpath:com/mycompany/**/applicationContext.xml 当位置路径使用了 ant 风格,解释器会遵循一套复杂且预定义的逻辑来解释这些位置路径。解释器会先从位置路径里获取最靠前的不带通配符的路径片段,使用这个路径片段来创建一个 Resource ,并从 Resource 里获取其 URL,若所获取到 URL 前缀并不是 “jar:”,或其他特殊容器产生的特殊前缀(如 WebLogic 的 zip:,WebSphere wsjar),则从 Resource 里获取 java.io.File 对象,并通过其遍历文件系统。进而解决位置路径里通配符;若获取的是 “jar:”的 URL ,解析器会从其获取一个 java.net.JarURLConnection 或手动解析此 URL,并遍历 jar 文件的内容进而解决位置路径的通配符。 ## 对可移植性的影响 如果指定的路径已经是文件URL(显式地或隐含地,因为基本的ResourceLoader是一个文件系统的,那么通配符将保证以完全可移植的方式工作。 如果指定的路径是类路径位置,则解析器必须通过Classloader.getResource()调用获取最后一个非通配符路径段URL。 由于这只是路径的一个节点(而不是最后的文件),在这种情况下,它实际上是未定义的(在ClassLoader javadocs中)返回的是什么样的URL。 实际上,它始终是一个java.io.File,它表示类路径资源解析为文件系统位置的目录或某种类型的jar URL,其中类路径资源解析为一个jar位置。 尽管如此,这种操作仍然存在可移植性问题。 如果为最后一个非通配符段获取了一个jar URL,解析器必须能够从中获取java.net.JarURLConnection,或者手动解析jar URL,以便能够遍历该jar的内容,然后解析 通配符。 这将在大多数环境中正常工作,但在其他环境中将会失败,并且强烈建议您在依赖它之前,彻底地在您的特定环境中彻底测试来自jar的资源的通配符解析。 ## classpath*: 的可移植性 当构造基于 xml 文件的应用上下文时,位置路径可以使用 classpath*:前缀: ApplicationContext ctx = new ClassPathXmlApplicationContext("classpath*:conf/appContext.xml"); classpath*:的使用表示类路径下所有匹配文件名称的资源都会被获取(本质上就是调用了 ClassLoader.getResources(…) 方法),接着将获取到的资源组装成最终的应用上下文。 通配符路径依赖了底层 classloader 的 getResource 方法。可是现在大多数应用服务器提供了自身的 classloader 实现,其处理 jar 文件的形式可能各有不同。要在指定服务器测试 classpath*: 是否有效,简单点可以使用 getClass().getClassLoader().getResources(“”) 去加载类路径 jar包里的一个文件。尝试在两个不同的路径加载名称相同的文件,如果返回的结果不一致,就需要查看一下此服务器中与 classloader 行为设置相关的文档。 在位置路径的其余部分,classpath*: 前缀可以与 PathMatcher 结合使用,如:” classpath*:META-INF/*-beans.xml”。这种情况的解析策略非常简单:取位置路径最靠前的无通配符片段,调用 ClassLoader.getResources() 获取所有匹配的类层次加载器可加载的的资源,随后将 PathMacher 的策略应用于每一个获得的资源(起过滤作用)。 ## 通配符的补充说明 除非所有目标资源都存于文件系统,否则classpath*:和 ant 风格模式的结合使用,都只能在至少有一个确定根包路径的情况下,才能达到预期的效果。换句话说,就是像 classpath*:*.xml 这样的 pattern 不能从根目录的 jar 文件中获取资源,只能从根目录的扩展目录获取资源。此问题的造成源于 jdk ClassLoader.getResources() 方法的局限性——当向 ClassLoader.getResources() 传入空串时(表示搜索潜在的根目录),只能获取的文件系统的文件位置路径,即获取不了 jar 中文件的位置路径。 如果在多个类路径上存在所搜索的根包,那使用 classpath: 和 ant 风格模式一起指定的资源不保证找到匹配的资源。因为使用如下的 pattern classpath:com/mycompany/**/service-context.xml 去搜索只在某一个路径存在的指定资源com/mycompany/package1/service-context.xml 时,解析器只会对 getResource(“com/mycompany”) 返回的(第一个) URL 进行遍历和解释,则当在多个类路径存在基础包节点 “com/mycompany” 时(如在多个 jar 存在这个基础节点),解析器就不一定会找到指定资源。因此,这种情况下建议结合使用 classpath*: 和 ant 风格模式,classpath*:会让解析器去搜索所有包含基础包节点的类路径。 ## 4.7.3 FileSystemResource 注意事项 FileSystemResource 没有依附 FileSystemApplicationContext,因为 FileSystemApplicationContext 并不是一个真正的 \ResourceLoader。FileSystemResource 并没有按约定规则来处理绝对和相对路径。相对路径是相对与当前工作而言,而绝对路径则是相对文件系统的根目录而言。 然而为了向后兼容,当 FileSystemApplicationContext 是一个 ResourceLoader 实例时,我们做了一些改变 —— 不管 FileSystemResource\ 实例的位置路径是否以 / 开头, FileSystemApplicationContext 都强制将其作为相对路径来处理。事实上,这意味着以下例子等效: 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: 的 UrlResource。 // Resource 只会是 UrlResource,与上下文的真实类型无关 ctx.getResource("file:///some/resource/path/myTemplate.txt"); // 强制 FileSystemXmlApplicationContext 通过 UrlResource 加载资源 ApplicationContext ctx = new FileSystemXmlApplicationContext("file:///conf/context.xml"); ## 24、Spring源码分析 - 24-基于注解@Aspect的AOP实现 ## 1、AnnotationAwareAspectJAutoProxyCreator注册过程 Spring应用如果想开启基于注解形式的AOP代理,只需要在@Configuration类上加入@EnableAspectJAutoProxy,因为EnableAspectJAutoProxy上有@Import(AspectJAutoProxyRegistrar.class),就可以在bean加载阶段调用AspectJAutoProxyRegistrar.registerBeanDefinitions()方法,具体原理请看《ConfigurationClassPostProcessor原理》。 @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Import(AspectJAutoProxyRegistrar.class) public @interface EnableAspectJAutoProxy { /** * Indicate whether subclass-based (CGLIB) proxies are to be created as opposed * to standard Java interface-based proxies. The default is {@code false}. */ boolean proxyTargetClass() default false; /** * Indicate that the proxy should be exposed by the AOP framework as a {@code ThreadLocal} * for retrieval via the {@link org.springframework.aop.framework.AopContext} class. * Off by default, i.e. no guarantees that {@code AopContext} access will work. * @since 4.3.1 */ boolean exposeProxy() default false; } AspectJAutoProxyRegistrar.registerBeanDefinitions()方法内部会注册一个bean name为org.springframework.aop.config.internalAutoProxyCreator的AnnotationAwareAspectJAutoProxyCreator用于解析@Aspect。 class AspectJAutoProxyRegistrar implements ImportBeanDefinitionRegistrar { /** * Register, escalate, and configure the AspectJ auto proxy creator based on the value * of the @{@link EnableAspectJAutoProxy#proxyTargetClass()} attribute on the importing * {@code @Configuration} class. */ @Override public void registerBeanDefinitions( AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { //注册bean name为org.springframework.aop.config.internalAutoProxyCreator的AnnotationAwareAspectJAutoProxyCreator AopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(registry); AnnotationAttributes enableAspectJAutoProxy = AnnotationConfigUtils.attributesFor(importingClassMetadata, EnableAspectJAutoProxy.class); if (enableAspectJAutoProxy != null) { //如果@EnableAspectJAutoProxy的proxyTargetClass是true代表使用cglib代理目标类 //会为AnnotationAwareAspectJAutoProxyCreator添加一个属性 //definition.getPropertyValues().add("proxyTargetClass", Boolean.TRUE); if (enableAspectJAutoProxy.getBoolean("proxyTargetClass")) { AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(registry); } //definition.getPropertyValues().add("exposeProxy", Boolean.TRUE); if (enableAspectJAutoProxy.getBoolean("exposeProxy")) { AopConfigUtils.forceAutoProxyCreatorToExposeProxy(registry); } } } } 2、原理 首先看下AnnotationAwareAspectJAutoProxyCreator继承结构。 AnnotationAwareAspectJAutoProxyCreator通过继承AbstractAutoProxyCreator实现了BeanPostProcessor接口,postProcessAfterInitialization()方法会判断当前bean是否可以当做代理对象,如果可以则使用代理对象替换原来的bean。 @Override public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) { if (bean != null) { //bean name或class Object cacheKey = getCacheKey(bean.getClass(), beanName); if (!this.earlyProxyReferences.contains(cacheKey)) { //bean符合代理对象的条件则包装成代理对象返回 return wrapIfNecessary(bean, beanName, cacheKey); } } return bean; } //bean符合代理对象的条件则包装成代理对象返回 protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) { if (StringUtils.hasLength(beanName) && this.targetSourcedBeans.contains(beanName)) { return bean; } //不符合代理对象的也会缓存,避免重复调用此方法重新计算 if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) { return bean; } //此方法是protected,判断bean是否可以作为拦截器,拦截器不需要被代理, //是否是Advice、Pointcut、Advisor、AopInfrastructureBean之一, //AnnotationAwareAspectJAutoProxyCreator实现的具体行为还包括类有@Aspect注解并且不是被Ajc编译的 if (isInfrastructureClass(bean.getClass()) || shouldSkip(bean.getClass(), beanName)) { this.advisedBeans.put(cacheKey, Boolean.FALSE); return bean; } // Create proxy if we have advice. //取得用户定义的拦截器 Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null); if (specificInterceptors != DO_NOT_PROXY) { this.advisedBeans.put(cacheKey, Boolean.TRUE); //使用ProxyFactory创建代理对象 Object proxy = createProxy( bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean)); this.proxyTypes.put(cacheKey, proxy.getClass()); return proxy; } this.advisedBeans.put(cacheKey, Boolean.FALSE); return bean; } 上面代码要创建一个代理对象的核心就是要得到我们定义的拦截器, 然后createProxy()方法内部使用ProxyFactory创建代理对象,使用ProxyFactory与ProxyFactoryBean原理相同可以参考/zhuanlan/j2ee/spring/7/22.html。 下面就具体看一下AbstractAdvisorAutoProxyCreator的getAdvicesAndAdvisorsForBean()方法如何取得通知器数组了。 @Override @Nullable protected Object[] getAdvicesAndAdvisorsForBean( Class<?> beanClass, String beanName, @Nullable TargetSource targetSource) { List<Advisor> advisors = findEligibleAdvisors(beanClass, beanName); if (advisors.isEmpty()) { return DO_NOT_PROXY; } return advisors.toArray(); } //找到所有适合自动代理这个类的通知器 protected List<Advisor> findEligibleAdvisors(Class<?> beanClass, String beanName) { //找出容器中所有的通知器 List<Advisor> candidateAdvisors = findCandidateAdvisors(); //从所有的通知器中找出可以应用到此bean的通知器 List<Advisor> eligibleAdvisors = findAdvisorsThatCanApply(candidateAdvisors, beanClass, beanName); //在使用AspectJ表达式切入点在通知链的开头添加ExposeInvocationInterceptor extendAdvisors(eligibleAdvisors); if (!eligibleAdvisors.isEmpty()) { eligibleAdvisors = sortAdvisors(eligibleAdvisors); } return eligibleAdvisors; } AnnotationAwareAspectJAutoProxyCreator重写了findCandidateAdvisors()方法,首先调用父类AbstractAdvisorAutoProxyCreator的findCandidateAdvisors()方法获得一批通知器,然后使用aspectJAdvisorsBuilder在获得一批通知器。 @Override protected List<Advisor> findCandidateAdvisors() { // Add all the Spring advisors found according to superclass rules. List<Advisor> advisors = super.findCandidateAdvisors(); // Build Advisors for all AspectJ aspects in the bean factory. if (this.aspectJAdvisorsBuilder != null) { advisors.addAll(this.aspectJAdvisorsBuilder.buildAspectJAdvisors()); } return advisors; } AbstractAdvisorAutoProxyCreator的findCandidateAdvisors()方法内部使用一个BeanFactoryAdvisorRetrievalHelper取得容器内所有的通知器,这个BeanFactoryAdvisorRetrievalHelper是在setBeanFactory()方法被调用的时候实例化的。 @Override public void setBeanFactory(BeanFactory beanFactory) { super.setBeanFactory(beanFactory); if (!(beanFactory instanceof ConfigurableListableBeanFactory)) { throw new IllegalArgumentException( "AdvisorAutoProxyCreator requires a ConfigurableListableBeanFactory: " + beanFactory); } initBeanFactory((ConfigurableListableBeanFactory) beanFactory); } protected void initBeanFactory(ConfigurableListableBeanFactory beanFactory) { this.advisorRetrievalHelper = new BeanFactoryAdvisorRetrievalHelperAdapter(beanFactory); } protected List<Advisor> findCandidateAdvisors() { Assert.state(this.advisorRetrievalHelper != null, "No BeanFactoryAdvisorRetrievalHelper available"); return this.advisorRetrievalHelper.findAdvisorBeans(); } BeanFactoryAdvisorRetrievalHelperAdapter是AbstractAdvisorAutoProxyCreator的内部类继承了BeanFactoryAdvisorRetrievalHelper拥有了findAdvisorBeans()的能力。 public List<Advisor> findAdvisorBeans() { // Determine list of advisor bean names, if not cached already. String[] advisorNames = this.cachedAdvisorBeanNames; if (advisorNames == null) { // Do not initialize FactoryBeans here: We need to leave all regular beans // uninitialized to let the auto-proxy creator apply to them! advisorNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors( this.beanFactory, Advisor.class, true, false); this.cachedAdvisorBeanNames = advisorNames; } if (advisorNames.length == 0) { return new ArrayList<>(); } List<Advisor> advisors = new ArrayList<>(); //advisorNames是容器中所有Advisor类型的bean name for (String name : advisorNames) { //直接返回的是true if (isEligibleBean(name)) { //忽略当前正在创建的bean if (this.beanFactory.isCurrentlyInCreation(name)) { if (logger.isTraceEnabled()) { logger.trace("Skipping currently created advisor '" + name + "'"); } } else { try { advisors.add(this.beanFactory.getBean(name, Advisor.class)); } catch (BeanCreationException ex) { Throwable rootCause = ex.getMostSpecificCause(); if (rootCause instanceof BeanCurrentlyInCreationException) { BeanCreationException bce = (BeanCreationException) rootCause; String bceBeanName = bce.getBeanName(); if (bceBeanName != null && this.beanFactory.isCurrentlyInCreation(bceBeanName)) { if (logger.isTraceEnabled()) { logger.trace("Skipping advisor '" + name + "' with dependency on currently created bean: " + ex.getMessage()); } // Ignore: indicates a reference back to the bean we're trying to advise. // We want to find advisors other than the currently created bean itself. continue; } } throw ex; } } } } return advisors; } AnnotationAwareAspectJAutoProxyCreator使用父类findCandidateAdvisors()方法获取容器内所有的通知器后,再使用BeanFactoryAspectJAdvisorsBuilder来发掘@Aspect代表的通知器。这个BeanFactoryAspectJAdvisorsBuilder也是在通过调用setFactory()方法的时候实例化的,是AnnotationAwareAspectJAutoProxyCreator的内部类,通过继承BeanFactoryAspectJAdvisorsBuilder拥有了buildAspectJAdvisors()的能力,并重写了isEligibleBean()方法,方法的具体实现是直接调用AnnotationAwareAspectJAutoProxyCreator的isEligibleAspectBean()方法。 @Override protected void initBeanFactory(ConfigurableListableBeanFactory beanFactory) { super.initBeanFactory(beanFactory); if (this.aspectJAdvisorFactory == null) { this.aspectJAdvisorFactory = new ReflectiveAspectJAdvisorFactory(beanFactory); } this.aspectJAdvisorsBuilder = new BeanFactoryAspectJAdvisorsBuilderAdapter(beanFactory, this.aspectJAdvisorFactory); } protected boolean isEligibleAspectBean(String beanName) { //默认是null if (this.includePatterns == null) { return true; } else { //正则过滤 for (Pattern pattern : this.includePatterns) { if (pattern.matcher(beanName).matches()) { return true; } } return false; } } //寻找被@Aspect注解的类,返回一个Spring AOP的通知器代表它, //为这个类的每个通知方法创建一个通知器 public List<Advisor> buildAspectJAdvisors() { List<String> aspectNames = this.aspectBeanNames; if (aspectNames == null) { synchronized (this) { aspectNames = this.aspectBeanNames; if (aspectNames == null) { List<Advisor> advisors = new ArrayList<>(); aspectNames = new ArrayList<>(); String[] beanNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors( this.beanFactory, Object.class, true, false); for (String beanName : beanNames) { //使用正则做一层过滤 if (!isEligibleBean(beanName)) { continue; } // We must be careful not to instantiate beans eagerly as in this case they // would be cached by the Spring container but would not have been weaved. Class<?> beanType = this.beanFactory.getType(beanName); if (beanType == null) { continue; } //判断该bean是否是@Aspect类 if (this.advisorFactory.isAspect(beanType)) { aspectNames.add(beanName); AspectMetadata amd = new AspectMetadata(beanType, beanName); //@Aspect value默认情况 if (amd.getAjType().getPerClause().getKind() == PerClauseKind.SINGLETON) { MetadataAwareAspectInstanceFactory factory = new BeanFactoryAspectInstanceFactory(this.beanFactory, beanName); // List<Advisor> classAdvisors = this.advisorFactory.getAdvisors(factory); if (this.beanFactory.isSingleton(beanName)) { this.advisorsCache.put(beanName, classAdvisors); } else { this.aspectFactoryCache.put(beanName, factory); } advisors.addAll(classAdvisors); } else { // Per target or per this. if (this.beanFactory.isSingleton(beanName)) { throw new IllegalArgumentException("Bean with name '" + beanName + "' is a singleton, but aspect instantiation model is not singleton"); } MetadataAwareAspectInstanceFactory factory = new PrototypeAspectInstanceFactory(this.beanFactory, beanName); this.aspectFactoryCache.put(beanName, factory); advisors.addAll(this.advisorFactory.getAdvisors(factory)); } } } this.aspectBeanNames = aspectNames; return advisors; } } } if (aspectNames.isEmpty()) { return Collections.emptyList(); } List<Advisor> advisors = new ArrayList<>(); for (String aspectName : aspectNames) { List<Advisor> cachedAdvisors = this.advisorsCache.get(aspectName); if (cachedAdvisors != null) { advisors.addAll(cachedAdvisors); } else { MetadataAwareAspectInstanceFactory factory = this.aspectFactoryCache.get(aspectName); advisors.addAll(this.advisorFactory.getAdvisors(factory)); } } return advisors; } 上面的核心方法就是这三行: MetadataAwareAspectInstanceFactory factory = new BeanFactoryAspectInstanceFactory(this.beanFactory, beanName); List<Advisor> classAdvisors = this.advisorFactory.getAdvisors(factory); BeanFactoryAspectInstanceFactory包含当前的bean name,通过ReflectiveAspectJAdvisorFactory的getAdvisors()方法返回此bean name的@Aspect类代表的所有的Advisor。BeanFactoryAspectInstanceFactory这个类主要用于获取@Aspect类的AspectMetadata对象。 public class AspectMetadata implements Serializable { //bean name private final String aspectName; //bean class private final Class<?> aspectClass; //AspectJ反射信息,参考AjTypeImpl private transient AjType<?> ajType; //默认情况@Aspect value=singleton --> Pointcut.TRUE private final Pointcut perClausePointcut; } 下面是ReflectiveAspectJAdvisorFactory的getAdvisors()方法。 @Override public List<Advisor> getAdvisors(MetadataAwareAspectInstanceFactory aspectInstanceFactory) { Class<?> aspectClass = aspectInstanceFactory.getAspectMetadata().getAspectClass(); String aspectName = aspectInstanceFactory.getAspectMetadata().getAspectName(); //非抽象父类不能是@Aspect类 validate(aspectClass); // We need to wrap the MetadataAwareAspectInstanceFactory with a decorator // so that it will only instantiate once. MetadataAwareAspectInstanceFactory lazySingletonAspectInstanceFactory = new LazySingletonAspectInstanceFactoryDecorator(aspectInstanceFactory); List<Advisor> advisors = new ArrayList<>(); //循环处理没有@Pointcut的方法 for (Method method : getAdvisorMethods(aspectClass)) { Advisor advisor = getAdvisor(method, lazySingletonAspectInstanceFactory, advisors.size(), aspectName); if (advisor != null) { advisors.add(advisor); } } // If it's a per target aspect, emit the dummy instantiating aspect. if (!advisors.isEmpty() && lazySingletonAspectInstanceFactory.getAspectMetadata().isLazilyInstantiated()) { Advisor instantiationAdvisor = new SyntheticInstantiationAdvisor(lazySingletonAspectInstanceFactory); advisors.add(0, instantiationAdvisor); } // Find introduction fields. for (Field field : aspectClass.getDeclaredFields()) { //为切入点引入共同的接口实现,参考 //https://blog.csdn.net/elim168/article/details/78166296 Advisor advisor = getDeclareParentsAdvisor(field); if (advisor != null) { advisors.add(advisor); } } return advisors; } //一个非@Pointcut方法对应一个通知器 public Advisor getAdvisor(Method candidateAdviceMethod, MetadataAwareAspectInstanceFactory aspectInstanceFactory, int declarationOrderInAspect, String aspectName) { validate(aspectInstanceFactory.getAspectMetadata().getAspectClass()); //@Pointcut, @Around,@Before, @After, @AfterReturning,@AfterThrowing都会对应一个AspectJExpressionPointcut AspectJExpressionPointcut expressionPointcut = getPointcut( candidateAdviceMethod, aspectInstanceFactory.getAspectMetadata().getAspectClass()); if (expressionPointcut == null) { return null; } //绑定切入点与通知,通知就是方法 return new InstantiationModelAwarePointcutAdvisorImpl(expressionPointcut, candidateAdviceMethod, this, aspectInstanceFactory, declarationOrderInAspect, aspectName); } 总结:基于注解的AOP实现,最终还是要将注解类转换成相应的Adviser、advice、Pointcut的。 ## 5.1 介绍 JSR-303/JSR-349 Bean Validation 在设置支持方面,Spring Framework 4.0支持Bean Validation 1.0(JSR-303)和Bean Validation 1.1(JSR-349),也将其改写成了Spring的Validator接口。 正如5.8 Spring验证所述,应用程序可以选择一次性全局启用Bean验证,并使其专门用于所有的验证需求。 正如5.8.3 配置DataBinder所述,应用程序也可以为每个DataBinder实例注册额外的SpringValidator实例,这可能有助于不通过使用注解而插入验证逻辑。 考虑将验证作为业务逻辑是有利有弊的,Spring提供了一种不排除利弊的用于验证(和数据绑定)的设计。具体的验证不应该捆绑在web层,应该容易本地化并且它应该能够插入任何可用的验证器。考虑到以上这些,Spring想出了一个Validator接口,它在应用程序的每一层基本都是可用的。数据绑定对于将用户输入动态绑定到应用程序的领域模型上(或者任何你用于处理用户输入的对象)是非常有用的。Spring提供了所谓的DataBinder来处理这个。Validator和DataBinder组成了validation包,其主要用于但并不局限于MVC框架。 BeanWrapper是Spring框架中的一个基本概念且在很多地方使用。然而,你可能并不需要直接使用BeanWrapper。尽管这是参考文档,我们仍然觉得有一些说明需要一步步来。我们将会在本章中解释BeanWrapper,因为你极有可能会在尝试将数据绑定到对象的时候使用它。 Spring的DataBinder和底层的BeanWrapper都使用PropertyEditor来解析和格式化属性值。PropertyEditor概念是JavaBeans规范的一部分,并会在本章进行说明。Spring 3不仅引入了”core.convert”包来提供一套通用类型转换工具,还有一个高层次的”format”包用于格式化UI字段值。可以将这些新包视作更简单的PropertyEditor替代方式来使用,本章还会对此进行讨论。 ## 5.2 使用Spring的验证器接口进行验证 Spring具有一个Validator接口可以让你用于验证对象。Validator接口在工作时需要使用一个Errors对象,以便于在验证过程中,验证器可以将验证失败的信息报告给这个Errors对象。 让我们考虑一个小的数据对象: public class Person { private String name; private int age; // the usual getters and setters... } 通过实现org.springframework.validation.Validator的下列两个接口,我们打算为Person类提供验证行为: support(Class)– 这个Validator是否可以验证给定Class的实例validate(Object,org.springframework.validation.Errors) – 验证给定的对象并且万一验证错误,可以将这些错误注册到给定的Errors对象 实现一个Validator是相当简单的,特别是当你知道Spring框架还提供了ValidationUtils辅助类: public class PersonValidator implements Validator { /** * This Validator validates *just* 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(..)被用于拒绝那些值为null或者空字符串的'name'属性。除了上面展示的例子之外,去看一看ValidationUtils的java文档有助于了解它提供的功能。 通过实现单个的Validator类来逐个验证富对象中的嵌套对象当然是有可能的,然而将验证逻辑封装在每个嵌套类对象自身的Validator实现中可能是一种更好的选择。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(); } } } 验证错误被报告给传递到验证器的Errors对象。在使用Spring Web MVC的情况下,你可以使用<spring:bind/>标签来检查错误信息,不过当然你也可以自己检查错误对象。有关它提供的方法的更多信息可以在java文档中找到。 ## 5.3 将代码解析成错误消息 在之前我们已经谈论了数据绑定和验证,最后一件值得讨论的事情是输出对应于验证错误的消息。在我们上面展示的例子里,我们拒绝了name和age字段。如果我们要使用MessageSource来输出错误消息,我们将会使用我们在拒绝该字段(这个情况下是’姓名’和’年龄’)时给出的错误代码。当你调用(不管是直接调用还是间接通过使用ValidationUtils类调用)来自Errors接口的rejectValue或者其他reject方法时,其底层实现不仅会注册你传入的代码,还会注册一些额外的错误代码。注册怎样的错误代码取决于它所使用的MessageCodesResolver,默认情况下,会使用DefaultMessageCodesResolver,其不仅会使用你提供的代码注册消息,还会注册包含你传递给拒绝方法的字段名称的消息。所以如果你使用rejectValue("age", "too.darn.old")来拒绝一个字段,除了too.darn.old代码,Spring还会注册too.darn.old.age和too.darn.old.age.int(第一个会包含字段名称且第二个会包含字段类型)。这样做是为了方便开发人员来定位错误消息等。 有关MessageCodesResolver和其默认策略的更多信息可以分别在MessageCodesResolver以及DefaultMessageCodesResolver的在线java文档中找到。 ## 5.4 Bean操作和BeanWrapper org.springframework.beans包遵循Oracle提供的JavaBeans标准。一个JavaBean只是一个包含默认无参构造器的类,它遵循一个命名约定(通过一个例子):一个名为bingoMadness属性将有一个设置方法setBingoMadness(..)和一个获取方法getBingoMadness(..)。有关JavaBeans和其规范的更多信息,请参考Oracle的网站(javabeans)。 beans包里一个非常重要的类是BeanWrapper接口和它的相应实现(BeanWrapperImpl)。引用自java文档,BeanWrapper提供了设置和获取属性值(单独或批量)、获取属性描述符以及查询属性以确定它们是可读还是可写的功能。BeanWrapper还提供对嵌套属性的支持,能够不受嵌套深度的限制启用子属性的属性设置。然后,BeanWrapper提供了无需目标类代码的支持就能够添加标准JavaBeans的PropertyChangeListeners和VetoableChangeListeners的能力。最后然而并非最不重要的是,BeanWrapper提供了对索引属性设置的支持。BeanWrapper通常不会被应用程序的代码直接使用,而是由DataBinder和BeanFactory使用。 BeanWrapper的名字已经部分暗示了它的工作方式:它包装一个bean以对其执行操作,比如设置和获取属性。 ## 5.4.1 设置并获取基本和嵌套属性 使用setPropertyValue(s)和getPropertyValue(s)可以设置并获取属性,两者都带有几个重载方法。在Spring自带的java文档中对它们有更详细的描述。重要的是要知道对象属性指示的几个约定。几个例子: account.name getAccount().setName() getAccount().getName() account[COMPANYNAME] 下面你会发现一些使用BeanWrapper来获取和设置属性的例子。 (如果你不打算直接使用BeanWrapper*,那么下一部分对你来说并不重要。如果你仅使用DataBinder和BeanFactory以及它们开箱即用的实现,你应该跳到关于PropertyEditor部分的开头)。* 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; } } 以下的代码片段展示了如何检索和操纵实例化的Companies和Employees的某些属性: 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"); 5.4.2 内置PropertyEditor实现 Spring使用PropertyEditor的概念来实现Object和String之间的转换。如果你考虑到它,有时候换另一种方式表示属性可能比对象本身更方便。举个例子,一个Date可以以人类可读的方式表示(如String'2007-14-09'),同时我们依然能把人类可读的形式转换回原始的时间(甚至可能更好:将任何以人类可读形式输入的时间转换回Date对象)。这种行为可以通过注册类型为PropertyEditor的自定义编辑器来实现。在BeanWrapper或上一章提到的特定IoC容器中注册自定义编辑器,可以使其了解如何将属性转换为期望的类型。请阅读Oracle为java.beans包提供的java文档来获取更多关于PropertyEditor的信息。 这是Spring使用属性编辑的两个例子: 使用PropertyEditor来完成bean的属性设置。当提到将java.lang.String作为你在XML文件中声明的某些bean的属性值时,Spring将会(如果相应的属性的设置方法具有一个Class参数)使用ClassEditor尝试将参数解析成Class对象。 在Spring的MVC框架中解析HTTP请求的参数是由各种PropertyEditor完成的,你可以把它们手动绑定到CommandController 的所有子类。 Spring有一些内置的PropertyEditor使生活变得轻松。它们中的每一个都已列在下面,并且它们都被放在org.springframework.beans.propertyeditors包中。大部分但并不是全部(如下所示),默认情况下会由BeanWrapperImpl注册。在某种方式下属性编辑器是可配置的,那么理所当然,你可以注册你自己的变种来覆盖默认编辑器: Table 5.2. 内置PropertyEditor ByteArrayPropertyEditor BeanWrapperImpl ClassEditor IllegalArgumentException BeanWrapperImpl CustomBooleanEditor BeanWrapperImpl CustomCollectionEditor CustomDateEditor CustomNumberEditor BeanWrapperImpl java.io.File BeanWrapperImpl InputStreamEditor ResourceEditor InputStream InputStream InputStream BeanWrapperImpl LocaleEditor BeanWrapperImpl PatternEditor java.util.regex.Pattern PropertiesEditor java.util.Properties BeanWrapperImpl StringTrimmerEditor BeanWrapperImpl Spring使用java.beans.PropertyEditorManager来设置可能需要的属性编辑器的搜索路径。搜索路径中还包括了sun.bean.editors ,这个包里面包含如Font、Color类型以及其他大部分基本类型的PropertyEditor实现。还要注意的是,如果PropertyEditor类与它们所处理的类位于同一个包并且除了’Editor’后缀之外拥有相同的名字,那么标准的JavaBeans基础设施会自动发现这些它们(不需要你显式的注册它们)。例如,有人可能会有以下的类和包结构,这已经足够识别出FooEditor类并将其作为Foo类型属性的PropertyEditor。 com chank pop Foo FooEditor // the PropertyEditor for the Foo class 要注意的是在这里你也可以使用标准JavaBeans机制的BeanInfo(在[in not-amazing-detail here][]有描述)。在下面的示例中,你可以看使用BeanInfo机制为一个关联类的属性显式注册一个或多个PropertyEditor实例。 com chank pop Foo FooBeanInfo // the BeanInfo for the Foo class 这是被引用到的FooBeanInfo类的Java源代码。它会将一个CustomNumberEditor同Foo类的age属性关联。 public class FooBeanInfo extends SimpleBeanInfo { public PropertyDescriptor[] getPropertyDescriptors() { try { final PropertyEditor numberPE = new CustomNumberEditor(Integer.class, true); PropertyDescriptor ageDescriptor = new PropertyDescriptor("age", Foo.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(例如将一个以字符串表示的类名转换成真正的Class对象)。此外,Java的标准JavaBeansPropertyEditor查找机制允许一个PropertyEditor只需要恰当的命名并同它支持的类位于相同的包,就能够自动发现它。 如果需要注册其他自定义的PropertyEditor,还有几种可用机制。假设你有一个BeanFactory引用,最人工化的方式(但通常并不方便或者推荐)是直接使用ConfigurableBeanFactory接口的registerCustomEditor()方法。另一种略为方便的机制是使用一个被称为CustomEditorConfigurer的特殊的bean factory后处理器(post-processor)。虽然bean factory后处理器可以与BeanFactory实现一起使用,但是因为CustomEditorConfigurer有一个嵌套属性设置过程,所以强烈推荐它与ApplicationContext一起使用,这样就可以采用与其他bean类似的方式来部署它,并自动检测和应用。 请注意所有的bean工厂和应用上下文都会自动地使用一些内置属性编辑器,这些编辑器通过一个被称为BeanWrapper的接口来处理属性转换。BeanWrapper注册的那些标准属性编辑器已经列在上一部分。 此外,针对特定的应用程序上下文类型,ApplicationContext会用适当的方法覆盖或添加一些额外的编辑器来处理资源查找。 标准的JavaBeansPropertyEditor实例用于将字符串表示的属性值转换成实际的复杂类型属性。CustomEditorConfigurer,一个bean factory后处理器,可以为添加额外的PropertyEditor到ApplicationContext提供便利支持。 考虑一个用户类ExoticType和另外一个需要将ExoticType设为属性的类DependsOnExoticType: 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 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将一个新的PropertyEditor注册到ApplicationContext,那么在需要的时候就能够使用它: <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。当你需要在几个不同场景里使用同一组属性编辑器,这个接口会特别有用:编写一个相应的registrar并在每个用例里重用。PropertyEditorRegistrar与一个被称为PropertyEditorRegistry的接口配合工作,后者被Spring的BeanWrapper(以及DataBinder)实现。当与CustomEditorConfigurer配合使用的时候,PropertyEditorRegistrar特别方便(这里有介绍),因为前者暴露了一个方法setPropertyEditorRegistrars(..):以这种方式添加到CustomEditorConfigurerd的PropertyEditorRegistrar可以很容易地在DataBinder和Spring MVCControllers之间共享。另外,它避免了在自定义编辑器上的同步需求:一个PropertyEditorRegistrar可以为每一次bean创建尝试创建新的PropertyEditor实例。 使用PropertyEditorRegistrar可能最好还是以一个例子来说明。首先,你需要创建你自己的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.springframework.beans.support.ResourceEditorRegistrar当作一个PropertyEditorRegistrar实现的示例。注意在它的registerCustomEditors(..)方法实现里是如何为每个属性编辑器创建新的实例的。 接着我们配置了一个CustomEditorConfigurerd并将我们的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’s MVC web framework的那些人,使用PropertyEditorRegistrar与数据绑定的Controller(比如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注册代码封装到一个类里然后根据需要在尽可能多的Controller之间共享。 ## 5.5 Spring类型转换 Spring 3引入了core.convert包来提供一个一般类型的转换系统。这个系统定义了实现类型转换逻辑的服务提供接口(SPI)以及在运行时执行类型转换的API。在Spring容器内,这个系统可以当作是PropertyEditor的替代选择,用于将外部bean的属性值字符串转换成所需的属性类型。这个公共的API也可以在你的应用程序中任何需要类型转换的地方使用。 ## 5.5.1 Converter SPI 实现类型转换逻辑的SPI是简单并且强类型的: package org.springframework.core.convert.converter; public interface Converter<S, T> { T convert(S source); } 要创建属于你自己的转换器,只需要简单的实现以上接口即可。泛型参数S表示你想要进行转换的源类型,而泛型参数T表示你想要转换的目标类型。如果一个包含S类型元素的集合或数组需要转换为一个包含T类型的数组或集合,那么这个转换器也可以被透明地应用,前提是已经注册了一个委托数组或集合的转换器(默认情况下会是DefaultConversionService处理)。 对每次方法convert(S)的调用,source参数值必须确保不为空。如果转换失败,你的转换器可以抛出任何非受检异常(unchecked exception);具体来说,为了报告一个非法的source参数值,应该抛出一个IllegalArgumentException。还有要注意确保你的Converter实现必须是线程安全的。 为方便起见,core.convert.support包已经提供了一些转换器实现,这些实现包括了从字符串到数字以及其他常见类型的转换。考虑将StringToInteger作为一个典型的Converter实现示例: package org.springframework.core.convert.support; final class StringToInteger implements Converter<String, Integer> { public Integer convert(String source) { return Integer.valueOf(source); } } 5.5.2 ConverterFactory 当你需要集中整个类层次结构的转换逻辑时,例如,碰到将String转换到java.lang.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(Class),其中T就是R的一个子类。 考虑将StringToEnum作为ConverterFactory的一个示例: 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()); } } } 5.5.3 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()方法要返回支持的源-目标类型对,然后实现convert(Object,TypeDescriptor,TypeDescriptor)方法来实现你的转换逻辑。源TypeDescriptor提供了对持有被转换值的源字段的访问,目标TypeDescriptor提供了对设置转换值的目标字段的访问。 一个很好的GenericConverter的示例是一个在Java数组和集合之间进行转换的转换器。这样一个ArrayToCollectionConverter可以通过内省声明了目标集合类型的字段以解析集合元素的类型,这将允许原数组中每个元素可以在集合被设置到目标字段之前转换成集合元素的类型。 由于GenericConverter是一个更复杂的SPI接口,所以对基本类型的转换需求优先使用Converter或者ConverterFactory。 ## ConditionalGenericConverter 有时候你只想要在特定条件成立的情况下Converter才执行,例如,你可能只想要在目标字段存在特定注解的情况下才执行Converter,或者你可能只想要在目标类中定义了特定方法,比如staticvalueOf方法,才执行Converter。ConditionalGenericConverter是GenericConverter和ConditionalConveter接口的联合,允许你定义这样的自定义匹配条件: public interface ConditionalGenericConverter extends GenericConverter, ConditionalConverter { boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType); } ConditionalGenericConverter的一个很好的例子是一个在持久化实体标识和实体引用之间进行转换的实体转换器。这个实体转换器可能只匹配这样的条件–目标实体类声明了一个静态的查找方法,例如findAccount(Long),你将在matches(TypeDescriptor,TypeDescriptor)方法实现里执行这样的查找方法的检测。 ## 5.5.4 ConversionService 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配置提供了便利。 ## 5.5.5 配置ConversionService ConversionService是一个被设计成在应用程序启动时会进行实例化的无状态对象,随后可以在多个线程之间共享。在一个Spring应用程序中,你通常会为每一个Spring容器(或者应用程序上下文ApplicationContext)配置一个ConversionService实例,它会被Spring接收并在框架需要执行一个类型转换时使用。你也可以将这个ConversionService直接注入到你任何的Bean中并直接调用。 如果Spring没有注册ConversionService,则会使用原始的基于PropertyEditor的系统。 要向Spring注册默认的ConversionService,可以用conversionService作为id来添加如下的bean定义: <bean id="conversionService" class="org.springframework.context.support.ConversionServiceFactoryBean"/> 默认的ConversionService可以在字符串、数字、枚举、映射和其他常见类型之间进行转换。为了使用你自己的自定义转换器来补充或者覆盖默认的转换器,可以设置converters属性,该属性值可以是Converter、ConverterFactory或者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章节的Section 18.16.3 “Conversion and Formatting”。 在某些情况下,你可能希望在转换期间应用格式化,可以看5.6.3 “FormatterRegistry SPI”获取使用FormattingConversionServiceFactoryBean的细节。 ## 5.5.6 编程方式使用ConversionService 要以编程方式使用ConversionService,你只需要像处理其他bean一样注入一个引用即可: @Service public class MyService { @Autowired public MyService(ConversionService conversionService) { this.conversionService = conversionService; } public void doIt() { this.conversionService.convert(...) } } 对大多数用例来说,convert方法指定了可以使用的目标类型,但是它不适用于更复杂的类型比如参数化元素的集合。例如,如果你想要以编程方式将一个Integer的List转换成一个String的List,就需要为原类型和目标类型提供一个正式的定义。 幸运的是,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会自动注册对大部分环境都适用的转换器,这其中包括了集合转换器、标量转换器还有基本的Object到String的转换器。可以通过调用DefaultConversionService类上的静态方法addDefaultConverters来向任意的ConverterRegistry注册相同的转换器。 因为值类型的转换器可以被数组和集合重用,所以假设标准集合处理是恰当的,就没有必要创建将一个S的Collection转换成一个T的Collection的特定转换器。 ## 5.6 Spring字段格式化 如上一节所述,core.convert包是一个通用类型转换系统,它提供了统一的ConversionService API以及强类型的Converter SPI用于实现将一种类型转换成另外一种的转换逻辑。Spring容器使用这个系统来绑定bean属性值,此外,Spring表达式语言(SpEL)和DataBinder也都使用这个系统来绑定字段值。举个例子,当SpEL需要将Short强制转换成Long来完成一次expression.setValue(Object bean, Object value)尝试时,core.convert系统就会执行这个强制转换。 现在让我们考虑一个典型的客户端环境如web或桌面应用程序的类型转换要求,在这样的环境里,你通常会经历将字符串进行转换以支持客户端回传的过程以及转换回字符串以支持视图渲染的过程。此外,你经常需要对字符串值进行本地化。更通用的core.convert包中的Converter SPI不直接解决这种格式化要求。Spring 3为此引入了一个方便的Formatter SPI来直接解决这些问题,这个接口为客户端环境提供一种简单强大并且替代PropertyEditor的方案。 一般来说,当你需要实现通用的类型转换逻辑时请使用Converter SPI,例如,在java.util.Date和java.lang.Long之间进行转换。当你在一个客户端环境(比如web应用程序)工作并且需要解析和打印本地化的字段值时,请使用Formatter SPI。ConversionService接口为这两者提供了一套统一的类型转换API。 ## 5.6.1 Formatter SPI Formatter SPI实现字段格式化逻辑是简单并且强类型的: 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接口。泛型参数T代表你想要格式化的对象的类型,例如,java.util.Date。实现print()操作可以将类型T的实例按客户端区域设置的显示方式打印出来。实现parse()操作可以从依据客户端区域设置返回的格式化表示中解析出类型T的实例。如果解析尝试失败,你的格式化器应该抛出一个ParseException或者IllegalArgumentException。请注意确保你的格式化器实现是线程安全的。 为方便起见,format子包中已经提供了一些格式化器实现。number包提供了NumberFormatter、CurrencyFormatter和PercentFormatter,它们通过使用java.text.NumberFormat来格式化java.lang.Number对象 。datetime包提供了DateFormatter,其通过使用java.text.DateFormat来格式化java.util.Date。datetime.joda包基于Joda Time library提供了全面的日期时间格式化支持。 考虑将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团队欢迎社区驱动的Formatter贡献,可以登陆网站jira.spring.io了解如何参与贡献。 ## 5.6.2 注解驱动的格式化 如你所见,字段格式化可以通过字段类型或者注解进行配置,要将一个注解绑定到一个格式化器,可以实现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()方法返回一个可以打印被注解字段的值的打印机(Printer),让getParser()方法返回一个可以解析被注解字段的客户端值的解析器(Parser)。 下面这个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 NumberFormatter(annotation.pattern()); } else { Style style = annotation.style(); if (style == Style.PERCENT) { return new PercentFormatter(); } else if (style == Style.CURRENCY) { return new CurrencyFormatter(); } else { return new NumberFormatter(); } } } } 要触发格式化,只需要使用@NumberFormat对字段进行注解: public class MyModel { @NumberFormat(style=Style.CURRENCY) private BigDecimal decimal; } Format Annotation API org.springframework.format.annotation包中存在一套可移植(portable)的格式化注解API。请使用@NumberFormat格式化java.lang.Number字段,使用@DateTimeFormat格式化java.util.Date、java.util.Calendar、*java.util.Long(注:此处可能是原文错误,应为java.lang.Long)*或者Joda Time字段。 下面这个例子使用@DateTimeFormat将java.util.Date格式化为ISO时间(yyyy-MM-dd) public class MyModel { @DateTimeFormat(iso=ISO.DATE) private Date date; } 5.6.3 FormatterRegistry SPI FormatterRegistry是一个用于注册格式化器和转换器的服务提供接口(SPI)。FormattingConversionService是一个适用于大多数环境的FormatterRegistry实现,可以以编程方式或利用FormattingConversionServiceFactoryBean声明成Spring bean的方式来进行配置。由于它也实现了ConversionService,所以可以直接配置它与Spring的DataBinder以及Spring表达式语言(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,你可以只定义这些规则一次,而在需要格式化的时候应用它们。 ## 5.6.4 FormatterRegistrar SPI FormatterRegistrar是一个通过FormatterRegistry注册格式化器和转换器的服务提供接口(SPI): package org.springframework.format; public interface FormatterRegistrar { void registerFormatters(FormatterRegistry registry); } 当要为一个给定的格式化类别(比如时间格式化)注册多个关联的转换器和格式化器时,FormatterRegistrar会非常有用。 下一部分提供了更多关于转换器和格式化器注册的信息。 ## 5.6.5 在Spring MVC中配置格式化 请查看Spring MVC章节的Section 18.16.3 “Conversion and Formatting”。 ## 5.7 配置一个全局的日期&amp;时间格式 默认情况下,未被@DateTimeFormat注解的日期和时间字段会使用DateFormat.SHORT风格从字符串转换。如果你愿意,你可以定义你自己的全局格式来改变这种默认行为。 你将需要确保Spring不会注册默认的格式化器,取而代之的是你应该手动注册所有的格式化器。请根据你是否依赖Joda Time库来确定是使用 org.springframework.format.datetime.joda.JodaTimeFormatterRegistrar类还是org.springframework.format.datetime.DateFormatterRegistrar类。 例如,下面的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 Time: <?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 http://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提供了不同的类型来表示date、time和date-time的值,JodaTimeFormatterRegistrar中的dateFormatter、timeFormatter和dateTimeFormatter属性应该为每种类型配置不同的格式。DateTimeFormatterFactoryBean提供了一种方便的方式来创建格式化器。在 如果你在使用Spring MVC,请记住要明确配置所使用的转换服务。针对基于@Configuration的Java配置方式这意味着要继承WebMvcConfigurationSupport并且覆盖mvcConversionService()方法。针对XML的方式,你应该使用mvc:annotation-drive元素的'conversion-service'属性。更多细节请看Section 18.16.3 “Conversion and Formatting”。 ## 5.8 Spring验证 Spring 3对验证支持引入了几个增强功能。首先,现在全面支持JSR-303 Bean Validation API;其次,当采用编程方式时,Spring的DataBinder现在不仅可以绑定对象还能够验证它们;最后,Spring MVC现在已经支持声明式地验证@Controller的输入。 ## 5.8.1 JSR-303 Bean Validation 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,请继续保持阅读。 ## 5.8.2 配置Bean验证器提供程序 Spring提供了对Bean Validation API的全面支持,这包括将实现JSR-303/JSR-349规范的Bean验证提供程序引导为Spring Bean的方便支持。这样就允许在应用程序任何需要验证的地方注入javax.validation.ValidatorFactory或者javax.validation.Validator。 把LocalValidatorFactoryBean当作Spring bean来配置成默认的验证器: <bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean"/> 以上的基本配置会触发Bean Validation使用它默认的引导机制来进行初始化。作为实现JSR-303/JSR-349规范的提供程序,如Hibernate Validator,可以存在于类路径以使它能被自动检测到。 ## 注入验证器 LocalValidatorFactoryBean实现了javax.validation.ValidatorFactory和javax.validation.Validator这两个接口,以及Spring的org.springframework.validation.Validator接口,你可以将这些接口当中的任意一个注入到需要调用验证逻辑的Bean里。 如果你喜欢直接使用Bean Validtion API,那么就注入javax.validation.Validator的引用: import javax.validation.Validator; @Service public class MyService { @Autowired private Validator validator; 如果你的Bean需要Spring Validation API,那么就注入org.springframework.validation.Validator的引用: import org.springframework.validation.Validator; @Service public class MyService { @Autowired private Validator validator; } 配置自定义约束 每一个Bean验证约束由两部分组成,第一部分是声明了约束和其可配置属性的@Constraint注解,第二部分是实现约束行为的javax.validation.ConstraintValidator接口实现。为了将声明与实现关联起来,每个@Constraint注解会引用一个相应的验证约束的实现类。在运行期间,ConstraintValidatorFactory会在你的领域模型遇到约束注解的情况下实例化被引用到的实现。 默认情况下,LocalValidatorFactoryBean会配置一个SpringConstraintValidatorFactory,其使用Spring来创建约束验证器实例。这允许你的自定义约束验证器可以像其他Spring bean一样从依赖注入中受益。 下面显示了一个自定义的@Constraint声明的例子,紧跟着是一个关联的ConstraintValidator实现,其使用Spring进行依赖注入: @Target({ElementType.METHOD, ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) @Constraint(validatedBy=MyConstraintValidator.class) public @interface MyConstraint { } import javax.validation.ConstraintValidator; public class MyConstraintValidator implements ConstraintValidator { @Autowired; private Foo aDependency; ... } 如你所见,一个约束验证器实现可以像其他Spring bean一样使用@Autowired注解来自动装配它的依赖。 ## Spring驱动的方法验证 被Bean Validation 1.1以及作为Hibernate Validator 4.3中的自定义扩展所支持的方法验证功能可以通过配置MethodValidationPostProcessor的bean定义集成到Spring的上下文中: <bean class="org.springframework.validation.beanvalidation.MethodValidationPostProcessor"/> 为了符合Spring驱动的方法验证,需要对所有目标类用Spring的@Validated注解进行注解,且有选择地对其声明验证组,这样才可以使用。请查阅MethodValidationPostProcessor的java文档来了解针对Hibernate Validator和Bean Validation 1.1提供程序的设置细节。 ## 附加配置选项 对于大多数情况,默认的LocalValidatorFactoryBean配置应该足够。有许多配置选项来处理从消息插补到遍历解析的各种Bean验证结构。请查看LocalValidatorFactoryBean的java文档来获取关于这些选项的更多信息。 ## 5.8.3 配置DataBinder 从Spring 3开始,DataBinder的实例可以配置一个验证器。一旦配置完成,那么可以通过调用binder.validate()来调用验证器,任何的验证错误都会自动添加到DataBinder的绑定结果(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.addValidators和dataBinder.replaceValidators,一个DataBinder也可以配置多个Validator实例。当需要将全局配置的Bean验证与一个DataBinder实例上局部配置的SpringValidator结合时,这一点是非常有用的。 ## 5.8.4 Spring MVC 3 验证 请查看Spring MVC章节的Section 18.16.4 “Validation”。 ## 33、Spring源码分析 - 32-基于注解@Transactional的事物实现 @EnableTransactionManagement启用Spring的注解驱动的事务管理功能,它背后的处理器是TransactionManagementConfigurationSelector,在使用ASPECTJ模式它会导入AspectJJtaTransactionManagementConfiguration和AspectJTransactionManagementConfiguration用于基于ASPECTJ代理模式实现基于注解的事物实现,这里主要讨论PROXY模式基于接口的JDK动态代理模式:两个核心组件一个是AutoProxyRegistrar用于注册一个自动代理创建者(APC)去发现可被代理的目标对象,详细请参考《基于注解@Aspect的AOP实现》和《Spring缓存原理详解》;另一个是ProxyTransactionManagementConfiguration注册启用基于代理的注解驱动的事务管理所需的Spring基础架构bean。ProxyTransactionManagementConfiguration继承于AbstractTransactionManagementConfiguration,这个父类有一个@Autowired方法setConfigurers()以提供用户配置PlatformTransactionManager的入口。 @Configuration public abstract class AbstractTransactionManagementConfiguration implements ImportAware { @Nullable protected AnnotationAttributes enableTx; @Nullable protected PlatformTransactionManager txManager; @Override public void setImportMetadata(AnnotationMetadata importMetadata) { this.enableTx = AnnotationAttributes.fromMap( importMetadata.getAnnotationAttributes(EnableTransactionManagement.class.getName(), false)); if (this.enableTx == null) { throw new IllegalArgumentException( "@EnableTransactionManagement is not present on importing class " + importMetadata.getClassName()); } } @Autowired(required = false) void setConfigurers(Collection<TransactionManagementConfigurer> configurers) { if (CollectionUtils.isEmpty(configurers)) { return; } if (configurers.size() > 1) { throw new IllegalStateException("Only one TransactionManagementConfigurer may exist"); } TransactionManagementConfigurer configurer = configurers.iterator().next(); this.txManager = configurer.annotationDrivenTransactionManager(); } @Bean(name = TransactionManagementConfigUtils.TRANSACTIONAL_EVENT_LISTENER_FACTORY_BEAN_NAME) @Role(BeanDefinition.ROLE_INFRASTRUCTURE) public static TransactionalEventListenerFactory transactionalEventListenerFactory() { return new TransactionalEventListenerFactory(); } } 另外还注册了一个TransactionalEventListenerFactory,ProxyTransactionManagementConfiguration注册了三个组件:TransactionInterceptor、AnnotationTransactionAttributeSource、BeanFactoryTransactionAttributeSourceAdvisor。 @Configuration public class ProxyTransactionManagementConfiguration extends AbstractTransactionManagementConfiguration { @Bean(name = TransactionManagementConfigUtils.TRANSACTION_ADVISOR_BEAN_NAME) @Role(BeanDefinition.ROLE_INFRASTRUCTURE) public BeanFactoryTransactionAttributeSourceAdvisor transactionAdvisor() { BeanFactoryTransactionAttributeSourceAdvisor advisor = new BeanFactoryTransactionAttributeSourceAdvisor(); advisor.setTransactionAttributeSource(transactionAttributeSource()); advisor.setAdvice(transactionInterceptor()); if (this.enableTx != null) { advisor.setOrder(this.enableTx.<Integer>getNumber("order")); } return advisor; } @Bean @Role(BeanDefinition.ROLE_INFRASTRUCTURE) public TransactionAttributeSource transactionAttributeSource() { return new AnnotationTransactionAttributeSource(); } @Bean @Role(BeanDefinition.ROLE_INFRASTRUCTURE) public TransactionInterceptor transactionInterceptor() { TransactionInterceptor interceptor = new TransactionInterceptor(); interceptor.setTransactionAttributeSource(transactionAttributeSource()); if (this.txManager != null) { interceptor.setTransactionManager(this.txManager); } return interceptor; } } 下面就主要分析一下这四个组件的设计与实现。 ## 1、TransactionalEventListenerFactory @TransactionalEventListener是一个根据事务阶段调用的事件监听器。如果事件没有在托管事务的边界内发布,则丢弃该事件,除非显式设置fallbackExecution标志。如果事务正在运行,则根据其TransactionPhase处理该事件。添加@Order允许在事务完成之前或之后运行的其他侦听器中对该侦听器进行优先级排序。 public class TransactionalEventListenerFactory implements EventListenerFactory, Ordered { private int order = 50; public void setOrder(int order) { this.order = order; } @Override public int getOrder() { return this.order; } @Override public boolean supportsMethod(Method method) { return AnnotatedElementUtils.hasAnnotation(method, TransactionalEventListener.class); } @Override public ApplicationListener<?> createApplicationListener(String beanName, Class<?> type, Method method) { return new ApplicationListenerMethodTransactionalAdapter(beanName, type, method); } } 在《ApplicationEvent事件机制》中分析过Spring会从容器中选出能处理事件注解的方法,根据TransactionalEventListenerFactory的supportsMethod()的方法可以看出是处理@TransactionalEventListener的,ApplicationListenerMethodTransactionalAdapter就是处理该事件的事件处理器。 class ApplicationListenerMethodTransactionalAdapter extends ApplicationListenerMethodAdapter { private final TransactionalEventListener annotation; public ApplicationListenerMethodTransactionalAdapter(String beanName, Class<?> targetClass, Method method) { super(beanName, targetClass, method); TransactionalEventListener ann = AnnotatedElementUtils.findMergedAnnotation(method, TransactionalEventListener.class); if (ann == null) { throw new IllegalStateException("No TransactionalEventListener annotation found on method: " + method); } this.annotation = ann; } @Override public void onApplicationEvent(ApplicationEvent event) { //《ApplicationEvent事件机制》中可知事件处理默认是同步机制的,因此在事物边界内发布事件后 //TransactionSynchronizationManager.isSynchronizationActive()的返回值为true if (TransactionSynchronizationManager.isSynchronizationActive()) { //创建事物同步对象TransactionSynchronizationEventAdapter注册到TransactionSynchronizationManager中 //事物同步对象实现了TransactionSynchronization接口方法beforeCommit()和afterCompletion() //待到事物的提交前后会分别执行所有同步事物的这两个方法 TransactionSynchronization transactionSynchronization = createTransactionSynchronization(event); TransactionSynchronizationManager.registerSynchronization(transactionSynchronization); } //如果是异步事件处理,则可以设置TransactionalEventListener的fallbackExecution标志位true一样可以调用@TransactionalEventListener方法处理事件 else if (this.annotation.fallbackExecution()) { if (this.annotation.phase() == TransactionPhase.AFTER_ROLLBACK && logger.isWarnEnabled()) { logger.warn("Processing " + event + " as a fallback execution on AFTER_ROLLBACK phase"); } processEvent(event); } else { //当前线程无事物则不处理 if (logger.isDebugEnabled()) { logger.debug("No transaction is active - skipping " + event); } } } private TransactionSynchronization createTransactionSynchronization(ApplicationEvent event) { return new TransactionSynchronizationEventAdapter(this, event, this.annotation.phase()); } private static class TransactionSynchronizationEventAdapter extends TransactionSynchronizationAdapter { private final ApplicationListenerMethodAdapter listener; private final ApplicationEvent event; private final TransactionPhase phase; public TransactionSynchronizationEventAdapter(ApplicationListenerMethodAdapter listener, ApplicationEvent event, TransactionPhase phase) { this.listener = listener; this.event = event; this.phase = phase; } @Override public int getOrder() { return this.listener.getOrder(); } @Override public void beforeCommit(boolean readOnly) { if (this.phase == TransactionPhase.BEFORE_COMMIT) { processEvent(); } } @Override public void afterCompletion(int status) { if (this.phase == TransactionPhase.AFTER_COMMIT && status == STATUS_COMMITTED) { processEvent(); } else if (this.phase == TransactionPhase.AFTER_ROLLBACK && status == STATUS_ROLLED_BACK) { processEvent(); } else if (this.phase == TransactionPhase.AFTER_COMPLETION) { processEvent(); } } protected void processEvent() { this.listener.processEvent(this.event); } } } 在《ApplicationEvent事件机制》中可知Spring事件处理默认是同步的,因此在事件监听器接收到事物边界内发布的事件时TransactionSynchronizationManager.isSynchronizationActive()总是true,因此会注册一个TransactionSynchronizationEventAdapter,它是一个TransactionSynchronization并实现了事物提交声明周期方法beforeCommit()和afterCompletion(),在《Spring编程式事物的设计与实现》中分析过,这两个方法会在事物管理器AbstractPlatformTransactionManager事物提交阶段processCommit()方法中调用事物管理器doCommit()方法前后执行的。通过这两个方法可以看出,执行事件处理方法是根据@TransactionalEventListener的phase(事物阶段)具体值来决定是否执行。 如果选择了异步事件处理或在事物边界外进行事件的发布则可以通过设置@TransactionalEventListener的fallbackExecution为true来执行事件处理方法,只不过此时获取不到了当前线程的所有事物同步对象,也就不能根据事物阶段来选择是否执行事件处理方法了。 ## 2、TransactionInterceptor 用于事物处理的拦截器,这个在《Spring编程式事物的设计与实现》已经详细分析过了,这里再提一句Spring AOP通知器的切入点会根据TransactionInterceptor中的TransactionAttributeSource获取的TransactionAttribute决定的,基于注解事物实现的TransactionAttributeSource是下面所讲的AnnotationTransactionAttributeSource。 ## 3、AnnotationTransactionAttributeSource 实现TransactionAttributeSource}接口,该类读取Spring的@Transactional注解数据,并将相应的事务属性公开给Spring的事务基础结构。还支持JTA 1.2的@javax.transaction.Transactional和EJB3的@javax.ejb.TransactionAttribute(如果存在)。这个类也可以作为定制TransactionAttributeSource的基类,或者通过TransactionAnnotationParser策略进行定制。 AbstractFallbackTransactionAttributeSource是TransactionAttributeSource接口的抽象实现,它缓存方法的属性并实现一个回退策略: 1、 特定的目标方法;; 2、 目标类;; 3、 声明方法;; 4、 声明类/接口; 如果没有与目标方法关联,则默认使用目标类的事务属性。与目标方法关联的任何事务属性都将完全覆盖类事务属性。如果在目标类中没有找到,将检查调用的方法通过的接口(对于JDK代理)。此实现在首次使用属性之后按方法缓存属性。如果希望允许事务属性的动态更改(这是非常不可能的),那么可以将缓存配置为可配置的。缓存是可取的,因为评估回滚规则的成本很高。 下面是AbstractFallbackTransactionAttributeSource的getTransactionAttribute()方法的实现。 //确定此方法调用的事务属性。如果没有找到方法属性,则默认为类的事务属性。 @Override @Nullable public TransactionAttribute getTransactionAttribute(Method method, @Nullable Class<?> targetClass) { if (method.getDeclaringClass() == Object.class) { return null; } //检查缓存中是否有 Object cacheKey = getCacheKey(method, targetClass); TransactionAttribute cached = this.attributeCache.get(cacheKey); if (cached != null) { //值要么是指示没有事务属性的规范值,要么是实际的事务属性。 if (cached == NULL_TRANSACTION_ATTRIBUTE) { return null; } else { return cached; } } else { //computeTransactionAttribute()是真正获取TransactionAttribute的方法, //而getTransactionAttribute()相当于computeTransactionAttribute()方法的缓存装饰器 TransactionAttribute txAttr = computeTransactionAttribute(method, targetClass); // Put it in the cache. if (txAttr == null) { this.attributeCache.put(cacheKey, NULL_TRANSACTION_ATTRIBUTE); } else { String methodIdentification = ClassUtils.getQualifiedMethodName(method, targetClass); if (txAttr instanceof DefaultTransactionAttribute) { ((DefaultTransactionAttribute) txAttr).setDescriptor(methodIdentification); } if (logger.isTraceEnabled()) { logger.trace("Adding transactional method '" + methodIdentification + "' with attribute: " + txAttr); } this.attributeCache.put(cacheKey, txAttr); } return txAttr; } } @Nullable protected TransactionAttribute computeTransactionAttribute(Method method, @Nullable Class<?> targetClass) { //AnnotationTransactionAttributeSource中只允许public方法 if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) { return null; } //方法可能在接口上,但我们需要来自目标类的属性。如果目标类为null,则该方法将保持不变。 Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass); //首先尝试目标类中的方法。 TransactionAttribute txAttr = findTransactionAttribute(specificMethod); if (txAttr != null) { return txAttr; } //如果没有则在尝试目标类上的事务属性。 txAttr = findTransactionAttribute(specificMethod.getDeclaringClass()); if (txAttr != null && ClassUtils.isUserLevelMethod(method)) { return txAttr; } if (specificMethod != method) { //回退到声明的方法寻找属性 txAttr = findTransactionAttribute(method); if (txAttr != null) { return txAttr; } //最后回退到原始方法的类上寻找事物属性 txAttr = findTransactionAttribute(method.getDeclaringClass()); if (txAttr != null && ClassUtils.isUserLevelMethod(method)) { return txAttr; } } return null; } AbstractFallbackTransactionAttributeSource中下面这两个方法是模板方法,由子类提供具体的实现。 //子类需要实现这一点,以返回给定类的事务属性(如果有的话)。 @Nullable protected abstract TransactionAttribute findTransactionAttribute(Class<?> clazz); //子类需要实现这一点,以返回给定方法的事务属性(如果有的话)。 @Nullable protected abstract TransactionAttribute findTransactionAttribute(Method method); 子类AnnotationTransactionAttributeSource是实现这两个方法如下,都是调用determineTransactionAttribute()方法,这个方法使用构造方法中创建的TransactionAnnotationParser来从给定的Class或Method解析出TransactionAttribute。 public AnnotationTransactionAttributeSource() { this(true); } //实例化了三个类型的TransactionAnnotationParser分别处理不同的事物注解 public AnnotationTransactionAttributeSource(boolean publicMethodsOnly) { this.publicMethodsOnly = publicMethodsOnly; if (jta12Present || ejb3Present) { this.annotationParsers = new LinkedHashSet<>(4); this.annotationParsers.add(new SpringTransactionAnnotationParser()); if (jta12Present) { this.annotationParsers.add(new JtaTransactionAnnotationParser()); } if (ejb3Present) { this.annotationParsers.add(new Ejb3TransactionAnnotationParser()); } } else { this.annotationParsers = Collections.singleton(new SpringTransactionAnnotationParser()); } } @Override @Nullable protected TransactionAttribute findTransactionAttribute(Class<?> clazz) { return determineTransactionAttribute(clazz); } @Override @Nullable protected TransactionAttribute findTransactionAttribute(Method method) { return determineTransactionAttribute(method); } //确定给定方法或类的事务属性。 //这个实现委托给构造方法中配置的TransactionAnnotationParser、TransactionAnnotationParsers, //将已知的注释解析为Spring的元数据属性类。 //如果不是事务性的,返回{@code null}。 //可以重写以支持携带事务元数据的自定义注释。 @Nullable protected TransactionAttribute determineTransactionAttribute(AnnotatedElement element) { for (TransactionAnnotationParser annotationParser : this.annotationParsers) { TransactionAttribute attr = annotationParser.parseTransactionAnnotation(element); if (attr != null) { return attr; } } return null; } 这里以解析org.springframework.transaction.annotation.TransactionalSpring的TransactionAnnotationParser举例分析如何从指定可注解的元素上获取事物属性。 public class SpringTransactionAnnotationParser implements TransactionAnnotationParser, Serializable { @Override @Nullable public TransactionAttribute parseTransactionAnnotation(AnnotatedElement element) { AnnotationAttributes attributes = AnnotatedElementUtils.findMergedAnnotationAttributes( element, Transactional.class, false, false); if (attributes != null) { return parseTransactionAnnotation(attributes); } else { return null; } } public TransactionAttribute parseTransactionAnnotation(Transactional ann) { return parseTransactionAnnotation(AnnotationUtils.getAnnotationAttributes(ann, false, false)); } protected TransactionAttribute parseTransactionAnnotation(AnnotationAttributes attributes) { RuleBasedTransactionAttribute rbta = new RuleBasedTransactionAttribute(); Propagation propagation = attributes.getEnum("propagation"); rbta.setPropagationBehavior(propagation.value()); Isolation isolation = attributes.getEnum("isolation"); rbta.setIsolationLevel(isolation.value()); rbta.setTimeout(attributes.getNumber("timeout").intValue()); rbta.setReadOnly(attributes.getBoolean("readOnly")); rbta.setQualifier(attributes.getString("value")); //RuleBasedTransactionAttribute继承DefaultTransactionAttribute,并重写了rollbackOn()方法 //可从注解属性得到四种回滚或不回滚的异常类型来决定rollbackOn()参数中的异常是否回滚 List<RollbackRuleAttribute> rollbackRules = new ArrayList<>(); for (Class<?> rbRule : attributes.getClassArray("rollbackFor")) { rollbackRules.add(new RollbackRuleAttribute(rbRule)); } for (String rbRule : attributes.getStringArray("rollbackForClassName")) { rollbackRules.add(new RollbackRuleAttribute(rbRule)); } for (Class<?> rbRule : attributes.getClassArray("noRollbackFor")) { rollbackRules.add(new NoRollbackRuleAttribute(rbRule)); } for (String rbRule : attributes.getStringArray("noRollbackForClassName")) { rollbackRules.add(new NoRollbackRuleAttribute(rbRule)); } rbta.setRollbackRules(rollbackRules); return rbta; } @Override public boolean equals(Object other) { return (this == other || other instanceof SpringTransactionAnnotationParser); } @Override public int hashCode() { return SpringTransactionAnnotationParser.class.hashCode(); } } 4、BeanFactoryTransactionAttributeSourceAdvisor 此通知器由TransactionAttributeSource驱动,用于为事务性方法包含事务通知bean。 public class BeanFactoryTransactionAttributeSourceAdvisor extends AbstractBeanFactoryPointcutAdvisor { @Nullable private TransactionAttributeSource transactionAttributeSource; private final TransactionAttributeSourcePointcut pointcut = new TransactionAttributeSourcePointcut() { @Override @Nullable protected TransactionAttributeSource getTransactionAttributeSource() { return transactionAttributeSource; } }; //设置用于查找事务属性的事务属性源。这通常应该与事务拦截器本身上的事务属性源相同 public void setTransactionAttributeSource(TransactionAttributeSource transactionAttributeSource) { this.transactionAttributeSource = transactionAttributeSource; } public void setClassFilter(ClassFilter classFilter) { this.pointcut.setClassFilter(classFilter); } @Override public Pointcut getPointcut() { return this.pointcut; } } abstract class TransactionAttributeSourcePointcut extends StaticMethodMatcherPointcut implements Serializable { @Override public boolean matches(Method method, Class<?> targetClass) { if (TransactionalProxy.class.isAssignableFrom(targetClass) || PlatformTransactionManager.class.isAssignableFrom(targetClass) || PersistenceExceptionTranslator.class.isAssignableFrom(targetClass)) { return false; } //AnnotationTransactionAttributeSource,和TransactionInterceptor是同一个引用 TransactionAttributeSource tas = getTransactionAttributeSource(); return (tas == null || tas.getTransactionAttribute(method, targetClass) != null); } } ## 6.1 介绍 Spring Expression Language(简称SpEL)是一种功能强大的表达式语言、用于在运行时查询和操作对象图;语法上类似于Unified EL,但提供了更多的特性,特别是方法调用和基本字符串模板函数。 虽然目前已经有许多其他的Java表达式语言,例如OGNL,MVEL和Jboss EL,SpEL的诞生是为了给Spring社区提供一种能够与Spring生态系统所有产品无缝对接,能提供一站式支持的表达式语言。它的语言特性由Spring生态系统的实际项目需求驱动而来,比如基于eclipse的Spring Tool Suite(Spring开发工具集)中的代码补全工具需求。尽管如此、SpEL本身基于一套与具体实现技术无关的API,在需要的时候允许其他的表达式语言实现集成进来。 尽管SpEL在Spring产品中是作为表达式求值的核心基础模块,本身可以脱离Spring独立使用。为了体现它的独立性,本章节中的许多例子都将SpEL作为独立的表达式语言来使用。不过这样就需要每次都先创建一些基础框架类如解析器,而对于大多数Spring用户来说并不需要去关注这些基础框架类,仅仅只需要写相应的字符串求值表达式即可。一个典型的例子就是把SpEL集成进XML bean配置或者基于注解的Bean定义声明中(详见章节:Expression support for defining bean definitions) 本章节包含SpEL的语言特性,它的API及语法。很多地方用到了Inventor类及相关的Society类作为表达式求值的操作例子对象,这几个类的定义及操作它们的数据都列在本章的末尾. ## 6.2 功能特性 SpEL支持以下的一些特性: 字符表达式 布尔和关系操作符 正则表达式 类表达式 访问properties,arrays,lists,maps等集合 方法调用 关系操作符 赋值 调用构造器 Bean对象引用 创建数组 内联lists 内联maps 三元操作符 变量 用户自定义函数 集合投影 集合选择 模板表达式 ## 6.3 使用SpEL的接口进行表达式求值 本节介绍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支持一系列广泛的特性,例如方法调用,访问属性,调用构造函数等。 下面举一个方法调用的例子,在String文本后面调用concat方法。 ExpressionParser parser = new SpelExpressionParser(); Expression exp = parser.parseExpression("'Hello World'.concat('!')"); String message = (String) exp.getValue(); message的值现在就是 ‘Hello World!’. 接下去是一个访问JavaBean属性的例子,String类的Bytes属性通过以下的方法调用 // 调用'getBytes()' Expression exp = parser.parseExpression("'Hello World'.bytes"); byte[] bytes = (byte[]) exp.getValue(); SpEL同时也支持级联属性调用、和标准的prop1.prop2.prop3方式是一样的;同样属性值设置也是类似的方式. 公共方法也可以被访问到: ExpressionParser parser = new SpelExpressionParser(); // 调用 'getBytes().length' Expression exp = parser.parseExpression("'Hello World'.bytes.length"); int length = (Integer) exp.getValue(); 除了使用字符串表达式、也可以调用String的构造函数: 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) 使用这样的方法不需要将表达式的值转换成具体的结果类型。如果具体的值类型或者使用类型转换器都无法转成对应的类型、会抛EvaluationException的异常 SpEL中更常见的用途是提供一个针对特定对象实例(叫做根对象)求值的表达式字符串。使用方法有两种, 具体用哪一种要看每次调用表达式求值时相应的对象实例是否每次都会变化。接下来我们分别举两个例子说明, 第一个例子我们要做的是从Inventor类的实例中解析name属性: // 创建并设置一个calendar实例 GregorianCalendar c = new GregorianCalendar(); c.set(1856, 7, 9); // 构造器参数有: name, birthday, and nationality. Inventor tesla = new Inventor("Nikola Tesla", c.getTime(), "Serbian"); ExpressionParser parser = new SpelExpressionParser(); Expression exp = parser.parseExpression("name"); EvaluationContext context = new StandardEvaluationContext(tesla); String name = (String) exp.getValue(context); 最后一行,字符串变量name将会被设置成”Nikola Tesla”. 通过StandardEvaluationContext类你能指定哪个对象的”name”会被求值这种机制用于当根对象不会被改变的场景,在求值上下文中只会被设置一次。相反,如果根对象会经常改变,则需要在每次调用getValue的时候被设置,就像如下的示例代码: // 创建并设置一个calendar实例 GregorianCalendar c = new GregorianCalendar(); c.set(1856, 7, 9); // 构造器参数有: name, birthday, and nationality. Inventor tesla = new Inventor("Nikola Tesla", c.getTime(), "Serbian"); ExpressionParser parser = new SpelExpressionParser(); Expression exp = parser.parseExpression("name"); String name = (String) exp.getValue(tesla); 在上面这个例子中inventor类实例tesla直接在getValue中设置,表达式解析器底层框架会自动创建和管理一个默认的求值上下文-不需要被显式声明 StandardEvaluationContext创建相对比较耗资源,在重复多次使用的场景下内部会缓存部分中间状态加快后续的表达式求值效率。因此建议在使用过程中尽可能被缓存和重用,而不是每次在表达式求值时都重新创建一个对象。 在很多使用场景下理想的方式是事先配置好求值上下文,但是在实际调用中仍然可以给getValue设置一个不同的根对象。getValue允许在同一个调用中同时指定两者,在这种场景下运行时传入的根对象会覆盖在求值上下文中事先指定的根对象。 Note:在单独使用SpEL时需要创建解析器、解析表达式、以及求值上下文和对应的根对象。但是在实际使用过程中、更常用的使用方式是只需要在配置文件里面配置SpEL字符串表达式即可,例如针对Spring Bean或者Spring Web Flow的定义。在这种场景下解析器,求值上下文,根对象和任何事先定义的变量都会被容器默认创建好,用户除了写表达式不需要做任何其他事情。 作为最后一个介绍性的例子,我们沿用上面的例子写一个使用布尔操作符的求值例子 Expression exp = parser.parseExpression("name == 'Nikola Tesla'"); boolean result = exp.getValue(context, Boolean.class); // 求值结果是True 6.3.1 EvaluationContext接口 EvaluationContext接口在求值表达式中需要解析属性,方法,字段的值以及类型转换中会用到。其默认实现类StandardEvaluationContext使用反射机制来操作对象。为获得更好的性能缓存了java.lang.reflect.Method,java.lang.reflect.Field, 和java.lang.reflect.Constructor实例。 StandardEvaluationContext中你可以使用setRootObject()方法显式设置根对象,或通过构造器直接传入根对象。你还可以通过调用setVariable()和registerFunction()方法指定在表达式中用到的变量和函数。变量和函数的使用在语言参考的 Variables和Functions两章节有详细说明。使用StandardEvaluationContext你还可以注册自定义的构造器解析器(ConstructorResolvers),方法解析器(MethodResolvers),和属性存取器(PropertyAccessor)来扩展SpEL如何计算表达式,详见具体类的JavaDoc文档。 SpEL默认使用Spring核心代码中的conversion service来做类型转换(org.springframework.core.convert.ConversionService)。这个类本身内置了很多常用的转换器,同时也可以扩展使用自定义的类型转换器。另外一个核心功能是它可以识别泛型。这意味着当在表达式中使用泛型类型时、SpEL会确保任何处理的对象的类型正确性。 实际应用中这意味着什么?这里举一个例子、拿赋值来说,比如使用setValue来设置List属性。属性的类型实际上是List<Boolean>,SpEL可以识别List中的元素类型并转换成Boolean类型。下面是示例代码: class Simple { public List<Boolean> booleanList = new ArrayList<Boolean>(); } Simple simple = new Simple(); simple.booleanList.add(true); StandardEvaluationContext simpleContext = new StandardEvaluationContext(simple); // false is passed in here as a string. SpEL and the conversion service will // correctly recognize that it needs to be a Boolean and convert it parser.parseExpression("booleanList[0]").setValue(simpleContext, "false"); // b will be false Boolean b = simple.booleanList.get(0); 6.3.2 解析器配置 可以通过使用一个解析器配置对象 (org.springframework.expression.spel.SpelParserConfiguration)来配置SpEL表达式解析器。这个配置对象可以控制一些表达式组件的行为。例如:数组或者集合元素查找的时候如果当前位置对应的对象是Null,可以通过事先配置来自动创建元素。这个在表达式是由多次属性链式引用的时候比较重要。如果设置的数组或者List位置越界时可以自动增加数组或者List长度来兼容. 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表达式编译器的行为也可以通过配置来实现 ## 6.3.3 SpEL编译 Spring4.1 包括了一个基本的表达式编译器。表达式求值过程中往往可以利用动态解释功能来提供很多灵活性,但是却达不到理想的性能。当表达式不是被经常使用时是可以接受的,但是当被其他组件像Spring Integration使用时,性能往往更重要,这时动态性没有太多的需求。 新的SpEL编译器针对这样的需求进行了定制。编译器会在求值时同时创建一个真实的Java类包含表达式的行为,利用这种方式来达到更快的表达式求值速度。在编译过程中、编译器刚开始不知道表达式具体的类型、但它会在表达式求值过程中收集相应的信息来确定最终的类型。例如,编译器无法仅仅从表达式自身来分析出某个属性引用的类型,但是在首次进行解释求值时就可以确定了。当然,如果不同的表达式元素类型后续发生变化、基于这种信息来编译结果就不准确了。因此编译最适合的场景是表达式在多次求值过程中类型信息不会变化。 例如一个简单的表达式如下: someArray[0].someProperty.someOtherProperty < 0.1 这个表达式包含数组访问,属性引用和数值运算,其性能开销不可忽视。 在一次50000次循环的性能基准评测中,如果只用解析器需要耗时75毫秒,但如果使用已编译的版本则只需要3毫秒 ## 编译器配置 编译器模式不是默认打开的,有两种方法可以打开。一种是前面已经提到过的解析器配置的时候,另一种是当SpEL集成到其他组件时通过设置系统属性的方式打开。本节会同时介绍两种方式. 首先比较重要的是编译器本身有几种操作模式,都定义在枚举类(org.springframework.expression.spel.SpelCompilerMode)中。所有的模式如下: OFF-编译器关闭;默认是关闭的 IMMEDIATE-即时生效模式,表达式会尽快的被编译。基本是在第一次求值后马上就会执行。如果编译表达式出错(往往都是因为上面提到的类型发生改变的情况)则表达式求值的调用点会抛出异常。 MIXED-混合模式,在混合模式中表达式会自动在解释器模式和编译器模式之间切换。在发生了几次解释处理后会切换到编译模式,如果编译模式哪里出错了(像上面提到的类型发生变化)则表达式会自动切换回解释器模式。过一段时间如果运用正常又会切换回编译模式。基本上像在IMMEDIATE模式下会抛出的那些异常都会被内部处理掉。 即时生效模式之所以会存在是因为混合模式会带来副作用。如果一个已编译的表达式在部分执行成功后发生错误的话,有可能已经导致系统的状态发生变化。这种场景下调用方并不希望表达式在解释模式下重新执行一遍、因为这意味着部分表达式会被执行两遍。 在选择了一个模式后,使用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)。 ## 编译器的局限性 在Srping4.1中基础的编译框架就已经有了。但框架还不支持编译所有类型的表达式。最初的设计初衷主要在于先集中优化比较耗性能且又经常使用的表达式。以下类型的表达式目前还不能被编译: 涉及到赋值的表达式 依赖于转换服务的表达式 使用到自定义解析器或者存取器的表达式 使用到选择器或者投影的表达式 更多类型的表达式将来都会被支持编译 ## 6.4 Bean定义时使用表达式 无论XML还是注解类型的Bean定义都可以使用SpEL表达式。在两种方式下定义的表达式语法都是一样的,即:{{"#"}}{ } ## 6.4.1 XML类型的配置 Bean属性或者构造函数使用表达式的方式如下: <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> 你还可以通过Name注入的方式使用其他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> 6.4.2 基于注解的配置 @Value可以在属性字段,方法和构造器变量中使用,指定一个默认值。 下面的例子中给属性字段设置默认值: public static class FieldValueTestBean @Value("{{"#"}}{ systemProperties['user.region'] }") private String defaultLocale; public void setDefaultLocale(String defaultLocale) { this.defaultLocale = defaultLocale; } public String getDefaultLocale() { return this.defaultLocale; } } 通过Set方法设置默认值: public static 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; @Autowired public MovieRecommender(CustomerPreferenceDao customerPreferenceDao, @Value("{{"#"}}{systemProperties['user.country']}") String defaultLocale) { this.customerPreferenceDao = customerPreferenceDao; this.defaultLocale = defaultLocale; } // ... } ## 6.5 语言参考 ## 6.5.1 字符表达式 字符串表达式类型支持strings,数值类型 (int, real, hex),布尔和null.strings值通过单引号引用。如果字符串里面又包含字符串,通过双引号引用。 下面列出了字符串表达式的常用例子。通常它们不会被单独使用,而是结合一个更复杂的表达式一起使用,例如,在逻辑比较运算符中使用表达式: 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()解析。 ## 6.5.2 属性, 数组, 列表, Maps, 索引 属性引用比较简单:只需要用点号(.)标识级联的各个属性值。Inventor类的实例:pupin,和tesla,所用到的数据在Classes used in the examples一节有列出。 下面的表达式示例用来解析Tesla的出生年及Pupin的出生城市。 // 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(); // Inventions Array StandardEvaluationContext teslaContext = new StandardEvaluationContext(tesla); // evaluates to "Induction motor" String invention = parser.parseExpression("inventions[3]").getValue( teslaContext, String.class); // Members List StandardEvaluationContext societyContext = new StandardEvaluationContext(ieee); // evaluates to "Nikola Tesla" String name = parser.parseExpression("Members[0].Name").getValue( societyContext, String.class); // List and Array navigation // evaluates to "Wireless communication" String invention = parser.parseExpression("Members[0].Inventions[6]").getValue( societyContext, String.class); Maps的值由方括号内指定字符串的Key来标识引用。在下面这个例子中,因为Officers map的Key是string类型,我们可以用过字符串常量指定。 // 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"); 6.5.3 内联列表 列表(Lists)可以用过大括号{}直接引用 // 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); {}本身代表一个空list.因为性能的关系,如果列表本身完全由固定的常量值组成,这个时候会创建一个常量列表来代替表达式,而不是每次在求值的时候创建一个新的列表。 ## 6.5.4 内联Maps Maps也可以直接通过{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); {:} 本身代表一个空的Map。因为性能的原因:如果Map本身包含固定的常量或者其他级联的常量结构(lists或者maps)则一个常量Map会创建来代表表达式,而不是每次求值的时候都创建一个新的Map.Map的Key并不一定需要引号引用、比如上面的例子就没有引用。 ## 6.5.5 创建数组 数组可以使用类似于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); 在创建多维数组时还不支持事先指定初始化的值。 ## 6.5.6 方法 方法可以使用典型的Java编程语法来调用。方法可以直接在字符串常量上调用。可变参数也是支持的。 // string literal, evaluates to "bc" String c = parser.parseExpression("'abc'.substring(2, 3)").getValue(String.class); // evaluates to true boolean isMember = parser.parseExpression("isMember('Mihajlo Pupin')").getValue( societyContext, Boolean.class); 6.5.7 运算符 关系运算符 关系运算符;包括==,<>,<,<=,>,>=等标准运算符都是可以直接支持的 // 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); Note:>/<运算符和null做比较时遵循一个简单的规则:null代表什么都没有(不代表0).因此,所有的值总是大于null(X>null总是true) 也就是没有一个值会小于什么都没有(x<null总是返回false).尽量不要在数值比较中使用null,而是和0做比较(例如X>0或者X<0). 除了标准的关系运算符,SpEL还支持instanceof关键字和基于matches操作符的正则表达式。 备注:需要注意元数据类型会自动装箱成包装类型,因此1 instanceof T(int)结果是false,1 instanceof T(Integer)的结果是true. 每一个符号操作符也可以通过首字母简写的方式标识。这样可以避免表达式所用的符号在当前表达式所在的文档中存在特殊含义而带来的冲突(比如XML文档的<).简写的符号有:lt (<), gt (>), le (?), ge (>=), eq (==), ne (!=), div (/), mod (%), not (!). 这里不区分大小写。 ## 逻辑运算符 支持的逻辑运算符有: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 6.5.8 赋值 属性可以使用赋值运算符设置。可以通过调用setValue设置。但是也可以在getValue方法中设置 Inventor inventor = new Inventor(); StandardEvaluationContext inventorContext = new StandardEvaluationContext(inventor); parser.parseExpression("Name").setValue(inventorContext, "Alexander Seovic2"); // alternatively String aleks = parser.parseExpression( "Name = 'Alexandar Seovic'").getValue(inventorContext, String.class); 6.5.9 类型 T操作符是一个特殊的操作符、可以同于指定java.lang.Class的实例(类型)。静态方法也可以通过这个操作符调用。 StandardEvaluationContext使用TypeLocator来查找类型,其中StandardTypeLocator(这个可以被替换使用其他类)默认对java.lang包里的类型可见。也就是说 T()引用java.lang包里面的类型不需要限定包全名,但是其他类型的引用必须要。 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); 6.5.10 构造器 构造器可以使用new操作符来调用。除了元数据类型和String(比如int,float等可以直接使用)都需要限定类的全名。 nventor 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); 6.5.11 变量 表达式中的变量可以通过语法#变量名使用。变量可以在StandardEvaluationContext中通过方法setVariable设置。 Inventor tesla = new Inventor("Nikola Tesla", "Serbian"); StandardEvaluationContext context = new StandardEvaluationContext(tesla); context.setVariable("newName", "Mike Tesla"); parser.parseExpression("Name = #newName").getValue(context); System.out.println(tesla.getName()) // "Mike Tesla" #this和#root变量 #this变量永远指向当前表达式正在求值的对象(这时不需要限定全名)。变量#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(); StandardEvaluationContext context = new StandardEvaluationContext(); 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); 6.5.12 函数 你可以扩展SpEL,在表达式字符串中使用自定义函数。这些自定义函数是通过StandardEvaluationContext的registerFunction来注册的 public void registerFunction(String name, Method m) 首先定义一个Java方法作为函数的实现、例如下面是一个将字符串反转的方法。 public abstract class StringUtils { public static String reverseString(String input) { StringBuilder backwards = new StringBuilder(); for (int i = 0; i < input.length(); i++) backwards.append(input.charAt(input.length() - 1 - i)); } return backwards.toString(); } } 然后将这个方法注册到求值上下文中就可以应用到表达式字符串中。 ExpressionParser parser = new SpelExpressionParser(); StandardEvaluationContext context = new StandardEvaluationContext(); context.registerFunction("reverseString", StringUtils.class.getDeclaredMethod("reverseString", new Class[] { String.class })); String helloWorldReversed = parser.parseExpression( "#reverseString('hello')").getValue(context, String.class); 6.5.13 Bean引用 如果求值上下文已设置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); 如果是访问工厂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); 6.5.14 三元操作符 (If-Then-Else) 你可以在表达式中使用三元操作符来实现if-then-else的条件逻辑。下面是一个小例子: String falseString = parser.parseExpression( "false ? 'trueExp' : 'falseExp'").getValue(String.class); 在这个例子中,因为布尔值false返回的结果一定是’falseExp’。下面是一个更实际的例子。 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'"; 6.5.15 Elvis运算符 Elvis运算符可以简化Java的三元操作符,是Groovy中使用的一种操作符。如果使用三元操作符语法你通常需要重复写两次变量名,例如: String name = "Elvis Presley"; String displayName = name != null ? name : "Unknown"; 使用Elvis运算符可以简化写法,这个符号的名字由来是它很像Elvis的发型(译者注:Elvis=Elvis Presley,猫王,著名摇滚歌手) ExpressionParser parser = new SpelExpressionParser(); String name = parser.parseExpression("name?:'Unknown'").getValue(String.class); System.out.println(name); // 'Unknown' 下面是一个复杂一点的例子: ExpressionParser parser = new SpelExpressionParser(); Inventor tesla = new Inventor("Nikola Tesla", "Serbian"); StandardEvaluationContext context = new StandardEvaluationContext(tesla); String name = parser.parseExpression("Name?:'Elvis Presley'").getValue(context, String.class); System.out.println(name); // Nikola Tesla tesla.setName(null); name = parser.parseExpression("Name?:'Elvis Presley'").getValue(context, String.class); System.out.println(name); // Elvis Presley 6.5.16 安全引用运算符 安全引用运算符主要为了避免空指针,源于Groovy语言。很多时候你引用一个对象的方法或者属性时都需要做非空校验。为了避免此类问题、使用安全引用运算符只会返回null而不是抛出一个异常。 ExpressionParser parser = new SpelExpressionParser(); Inventor tesla = new Inventor("Nikola Tesla", "Serbian"); tesla.setPlaceOfBirth(new PlaceOfBirth("Smiljan")); StandardEvaluationContext context = new StandardEvaluationContext(tesla); String city = parser.parseExpression("PlaceOfBirth?.City").getValue(context, String.class); System.out.println(city); // Smiljan tesla.setPlaceOfBirth(null); city = parser.parseExpression("PlaceOfBirth?.City").getValue(context, String.class); System.out.println(city); // null - does not throw NullPointerException!!! 备注:Elvis操作符可以在表达式中赋默认值,例如。在一个@Value表达式中:@Value(“{{"#"}}{systemProperties[‘pop3.port’] ?: 25}”) 上面的例子如果系统属性pop3.port已定义会直接注入,如果未定义,则返回默认值25. ## 6.5.17 集合筛选 该功能是SpEL中一项强大的语言特性,允许你将源集合选择其中的某几项生成另外一个集合。选择器使用语法.?[selectionExpression].通过该表达式可以过滤集合并返回原集合中的子集。例如,下面的例子我们返回inventors对象中的国籍为塞尔维亚的子集: List<Inventor> list = (List<Inventor>) parser.parseExpression( "Members.?[Nationality == 'Serbian']").getValue(societyContext); 筛选可以同时在list和maps上面使用。对于list来说是选择的标准是针对单个列表的每一项来比对求值,对于map来说选择的标准是针对Map的每一项(类型为Java的Map.Entry)。Map项的Key和alue都可以作为筛选的比较选项 下面的例子中表达式会返回一个新的map,包含原map中值小于27的所有子项。 Map newMap = parser.parseExpression("map.?[value<27]").getValue(); 除了可以返回所有被选择的元素,也可以只返回第一或者最后一项。返回第一项的选择语法是: ^[…],返回最后一项的选择语法是 $[…]. ## 6.5.18 集合投影 投影使得一个集合通过子表达式求值,并返回一个新的结果。投影的语法是 ![projectionExpression]. 举一个通俗易懂的例子,假设我们有一个inventors 对象列表,但是我们想返回每一个inventor出生的城市列表。我们需要遍历inventor的每一项,通过 ‘placeOfBirth.city’来求值。下面是具体的代码例子: // returns ['Smiljan', 'Idvor' ] List placesOfBirth = (List)parser.parseExpression("Members.![placeOfBirth.city]"); 也可以在Map上使用投影、在这种场景下投影表达式会作用于Map的每一项(类型为Java的Map.Entry)。Map项的Key和alue都可以作为选择器的比较选项Map投影的结果是一个list,包含map每一项被投影表达式求值后的结果。 ## 6.5.19 表达式模板 表达式模板运行在一段文本中混合包含一个或多个求值表达式模块。各个求值块都通过可被自定义的前后缀字符分隔,一个通用的选择是使用{{"#"}}{ }作为分隔符。例如: 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; } } ## 6.6 本章节例子中使用的类 Inventor.java 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; } } PlaceOfBirth.java 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; } } Society.java 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; } } ## 9. Spring框架下的测试 测试乃企业级软件开发的重要组成部分之一。本章专注于讲解采用 IoC 原则进行编码而给单元测试带来的好处,以及 Spring 框架对集成测试的支持如何为测试带来帮助。(对企业开发中如何进行代码测试的详尽讨论不在本文档讨论范围之内) ## 10. 单元测试 比起传统的Java EE开发方式,依赖注入可以弱化你的代码对容器的依赖。在基于Junit或TestNG的测试代码中,无需依赖于Spring或其他容器,你只需通过new操作符,便可以创建出组成你的应用程序的各种POJO对象。而通过mock对象(以及其它各种测试技术的综合运用),你可以将被测试的代码单独隔离开来进行测试。如果你在进行架构设计时遵循了Spring所推荐的模式,那么由此带来的诸如清晰的分层、组件化等等优点也会使你更容易地对你的代码进行单元测试。例如,通过mock DAO层或Repositry接口的实现,针对服务层对象的单元测试代码并不需要真正地访问持久层的数据。 真正的单元测试代码运行的非常快,因为并不需要一整套的运行时架构去支持测试的运转。所以在开发中强调真正的单元测试必须成为编码方法论中的一部分将会极大地提高你的生产力。 也许没有这一节内容的帮助你也能写出高效的针对IoC应用的单元测试,但是以下将要提到的 Spring 所提供的 mock 对象和测试支持类,在一些特定的场景中会对单元测试很有帮助。 ## 10.1 Mock 对象 ## 10.1.1 环境 org.springframework.mock.env 包含了对抽象 Environment 和 PropertySource 的 Mock 实现(参考 3.13.1 节 “Bean的定义文件” 和 3.13.3 节 “PropertySource抽象”)。MockEnvironment 和 MockPropertySource 对于编写针对依赖于环境相关属性的代码的,与容器无关的测试用例很有帮助。 ## 10.1.2 JNDI org.springframework.mock.jndi 包含了JNDI SPI的实现。这一实现可以让你为你的测试套件或独立应用配置起一个简单的JNDI环境。例如,如果在测试代码和Java EE容器中JDBC的 DataSource 都绑定到同一个JNDI名字上,你就可以在测试中直接复用应用程序的代码和配置而不需做任何改动。 ## 10.1.3 Servlet API org.springframework.mock.web 包含了十分全面的Servlet API mock 对象。这些对象在测试web context,controllers 和 filters 的时候很有用。由于这些 mock 对象是有针对性地为了与 Spring 的 Web MVC 框架共同使用而编写的,因此相比起诸如 EasyMock 这种动态mock 对象或 MockObjects 这种替代性的 Servlet API mock 对象,使用起来要更为方便。 ## 10.2 单元测试支持类 ## 10.2.1 通用支持工具 org.springframework.test.util 包含了一些供单元测试和集成测试中使用的通用工具。 ReflectionTestUtils 是一组基于反射的方法集合。在测试包含如下一些测试用例的应用时,开发人员可以使用这些工具方法应对更改一个常量的值,设置一个非公有字段,调用一个非公有setter方法,或调用一个非公有的配置或生命周期回调方法等测试场景: 诸如JPA和Hibernate等广泛采用private或protected访问方式而非公有setter方法来访问domain entity属性的ORM(Object-Relational Mapping)框架 @Autowired,@Inject 和 @Resource 等用于对 private 或 protected 字段,setter 方法和配置方法进行依赖注入的 Spring 注解。 @PostConstruct 和 @PreDestroy 等用在生命周期回调方法上的注解。 AopTestUtils 是一组 AOP 相关工具方法的集合。这些方法可以用于帮助获取隐藏于一重或多重 Spring 代理之后目标对象的引用。举个栗子,你使用 EasyMock 或 Mockito mock 了一个被包装在 Spring 代理之中的 bean,这时你可能需要对这个 mock 进行直接的访问,从而能够配置对此 mock 的期望行为并在稍后执行验证。关于 Spring 提供的核心 AOP 工具,请参考 AopUtils 和 AopProxyUtils 这两个类。 ## 10.2.2 Spring MVC org.springframework.test.web 包含了 ModelAndViewAssert 类。你可以在 Junit,TestNG或用任何测试框架编写的单元测试中使用这个类来帮助你跟 Spring MVC 框架的 ModelAndView 对象进行互动。 如果想要像测试 POJO 一样来测试你的 Spring MVC 控制器,你可以将 ModelAndViewAssert 与 MockHttpServletRequest,MockHttpSession 等来自 Servlet API mocks 的 Mock 类结合使用。而假如是要把 Spring MVC 和 REST 控制器和对 Spring MVC 的WebApplicationContext 配置结合起来进行一番彻底的集成测试,请使用 Spring MVC Test Framework 。 ## 42、Spring源码分析 - 42-@Conditional详解 ## 1、@Conditional简介 @Conditional注解指示组件仅在所有指定条件匹配时才有资格注册。@Conditional注解的value属性就是条件匹配Class对象,具体说是Condition接口实例数组。 @Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Conditional { //必须匹配的所有条件才能注册组件 Class<? extends Condition>[] value(); } @Conditional注释可以以下任何方式使用: 作为任何直接或间接用@Component注释的类的类型级注释,包括@Configuration类 作为一个元注释,用于编写自定义构造型注释 作为任何@Bean方法的方法级注释 如果@Configuration类标记为@Conditional,则与该类关联的所有@Bean方法、@Import注释和@ComponentScan注释都将受这些条件的约束。 下面在看一下接口Condition的定义。 @FunctionalInterface public interface Condition { //确定条件是否匹配。 boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata); } 2、原理分析 在SpringFramework中@Conditional注释背后的处理器是ConditionEvaluator,通过跟踪源码发现共有三处使用了到了ConditionEvaluator: ConfigurationClassBeanDefinitionReader.loadBeanDefinitionsForBeanMethod(BeanMethod) 解析@Bean方法 ConfigurationClassParser.processConfigurationClass(ConfigurationClass) 解析@Configuration类 ConfigurationClassParser.doProcessConfigurationClass(ConfigurationClass, SourceClass) 中解析被@ComponentScan注解的类 这三处使用ConditionEvaluator的原理一样,ConditionEvaluator的shouldSkip()方法返回一个boolean值,true代表跳过被注解组件的注册或解析,下面就是第二种的实现。 shouldSkip方法第二个参数是一个枚举类ConfigurationPhase,代表当前调用的阶段,该类共有两个值 PARSE_CONFIGURATION:代表解析配置类阶段,也就是将配置类转换为ConfigurationClass阶段 REGISTER_BEAN:代表配置类注册为bean阶段,也就是将配置类是否需要在将其注册到IOC容器阶段 具体作用看ConditionEvaluator的实现,如下: public boolean shouldSkip(@Nullable AnnotatedTypeMetadata metadata, @Nullable ConfigurationPhase phase) { //如果配置类没有被@Conditional注解标注,则不能跳过,需要解析 if (metadata == null || !metadata.isAnnotated(Conditional.class.getName())) { return false; } //若没有明确传递当前调用阶段,则根据配置类元信息分析当前应处于哪个阶段 if (phase == null) { //如果元数据是AnnotationMetadata的实例,并且是通过注解方式作为候选配置类的 //则判定当前阶段为解析配置类阶段 if (metadata instanceof AnnotationMetadata && ConfigurationClassUtils.isConfigurationCandidate((AnnotationMetadata) metadata)) { return shouldSkip(metadata, ConfigurationPhase.PARSE_CONFIGURATION); } //否则判定为将ConfigurationClass转换为BeanDefinition阶段 return shouldSkip(metadata, ConfigurationPhase.REGISTER_BEAN); } List<Condition> conditions = new ArrayList<>(); for (String[] conditionClasses : getConditionClasses(metadata)) { for (String conditionClass : conditionClasses) { Condition condition = getCondition(conditionClass, this.context.getClassLoader()); conditions.add(condition); } } AnnotationAwareOrderComparator.sort(conditions); for (Condition condition : conditions) { ConfigurationPhase requiredPhase = null; if (condition instanceof ConfigurationCondition) { requiredPhase = ((ConfigurationCondition) condition).getConfigurationPhase(); } //只有Condition的requiredPhase与当前调用阶段一致,matches方法才生效,否则所有配置类都解析 //有一个condition返回false就跳过类的解析或注册 if ((requiredPhase == null || requiredPhase == phase) && !condition.matches(this.context, metadata)) { return true; } } return false; } --- *本文基于DDKK.COM的Spring教程系列进行二次创作* *原文链接:https://ddkk.com/zhuanlan/j2ee/spring/1/1.html*