Roo中ReflectionToStringBuilder引起的OutOfMemoryError

问题描述

项目里出现了java.lang.OutOfMemoryError: Java heap space错误,根据trace定位到entity.toString()方法上,再追溯到entity_Roo_ToString.aj文件的toString()方法上。为什么这个方法会引起内存错误呢?

线索

entity_Roo_ToString.aj的代码如下所示:

可以看出,这个aj文件定义了该实体的toString()方法。

那么,这个文件是如何生成的呢?有何作用?注意到其中的ReflectionToStringBuilder类,它的作用是什么?

@RooToString

当我们在Roo Shell中用命令生成以个Entity时,Roo会为实体自动添加注解@RooToString这里对该注解有如下定义:

Provides a Object.toString() method if requested.

Whilst it is possible to apply this annotation to any class that you’d like a Object.toString() method produced for, it is generally triggered automatically via the use of most other annotations in the system. The created method is conservative and only includes public accessor methods within the produced string. Further, any accessor which returns a common JDK Collection type is restricted to displaying its size only.

可以看出,@RooToString注解为实体类添加了toString()方法,而且会自动遍历该实体的public accessor,组装成可读的字符串信息,返回给调用者。注意到:

any accessor which returns a common JDK Collection type is restricted to displaying its size only.

这一句的意思是,返回Collection的accessor不会被逐个遍历输出,而是输出它的size。

ReflectionToStringBuilder

Roo通过@RooToString注解为我们生成toString()方法,具体的实现由ReflectionToStringBuilder这个类来完成。这里是这样定义这个类的:

Assists in implementing Object.toString() methods using reflection.

通常可以这样使用:

也可以这样用,方便我们debug:

由此可以推断,Java heap space应该是出在这个ReflectionToStringBuilder身上。具体地,entity文件如下所示:

留意到ProductBatch之间是一个ManyToOne的关系,难道是ReflectionToStringBuilder遍历了products属性导致内存不足?但上文中的说明里提到Collection的accessor不会被逐个遍历输出,而是输出它的size。为什么会导致内存不足呢?尝试在控制台中打印出batch.toString(),如下所示:

可见,ReflectionToStringBuilder居然遍历了products属性,将其中的product全部输出。当batch中的product数量很大时,便会有内存不足的危险。

解决方案

ReflectionToStringBuilderOutOfMemoryError为关键字在google上搜索,找到了这里

I assume that as the ReflectionToStringBuilder moved down the object graph, it eventually exceeded the memory. By adding an ‘excludedFields’ property to @RooToString and adding any object fields, the problem was stopped.

可知道@RooToString注解有一个’excludedFields’的参数,可以指定哪些属性被忽略。这里是这样定义的:

可知,excludeFields可传入一个字符串数组。于是我们为@RooToString添加如下参数:

Roo自动update了aj文件,内容如下:

其中setExcludeFieldNames("products")一句设置了忽略的属性的名字。这里可以找到该方法的定义:

接下来运行项目,问题解决!

Roo中的Test

问题引入

我们在Roo Shell里新建一个entity时,如果添加一个 -testAutomatically 的参数,Roo会自动帮我们在srctestjava路径下生成如下几个文件:

和相应的切面文件:

为什么要生成这些文件呢?这些文件的用途是什么呢?

背景知识

先让我们来了解一些背景知识,来自《Spring Roo in Action》

Roo中testing的层次

对于一般的web-based application来说,测试可分为三种:

  • isolated, method-level unit tests
  • more sophisticated, in-container integration tests
  • live, externally exe­cuted website tests

在Roo中,这三种类型的tests,分别命名为Unit TestIntegration TestFunctional Test。它们之间的层次结构如下所示:

层次结构

可以看到,随着层次的升高,测试的复杂度和所需时间都在不断增加。

这里有对上述三种测试层次的通俗描述,注意里面给出的例子:

Unit Tests

Tests the smallest unit of functionality, typically a method/function (e.g. given a class with a particular state, calling x method on the class should cause y to happen). Unit tests should be focussed on one particular feature (e.g., calling the pop method when the stack is empty should throw an InvalidOperationException).

Integration Tests

Integration tests build on unit tests by combining the units of code and testing the resulting combination. This can be either the innards of one system, or combining multiple systems together to do something useful. Also, another thing that differentiates integration tests from unit tests is the environment. Integration tests can and will use threads, access the database or do whatever is required to ensure that all of the code and the different environment changes will work correctly.

Functional & Acceptance Tests

Functional tests usually check a particular feature for correctness by comparing the results for a given input against the specification. Functional tests don’t concern themselves with intermediate results or program state (they don’t care that after doing x, object y has state z), they are written to test specified behaviour such as, “when the user clicks the magnifying glass button on the side bar, the document is magnified by 25%”.

可以看出,从上到下测试的粒度从小到大,所牵涉的“关注点”从少到多,关注的视角也从开发者角度转移到用户角度。

Roo中testing不同层次的实现方式:

对于上文说到的三层测试,Roo中的实现方式如下所示:

  • Unit Tests:
    采用JUnit实现。
  • Integration Tests:
    同样是采用JUnit实现。在运行时通过加载特殊的runner来运行testing的environment(例如某个Server)。
  • Functional Tests:
    采用Selenium框架来进行测试。

分别对应以下Roo Command:

  • Unit Tests:
    test stub 和 test mock
  • Integration Tests:
    test integration
  • Functional Tests:
    selenium test –controller

在讲解这三种command之前要了解一下Roo的Testing组件:DataOnDemand Test Framework Component

DataOnDemand Test Framework Component

上文第一节中提到的EntityDataOnDemand类是Roo Testing的核心Util类,根据《Spring Roo In Action》定义如下:

The DataOnDemand component is a useful test class that helps you generate test fix­tures, the data required to set up your test case.

只要你在Roo Shell中输入test integration --entity ~.model.Entity或者dod --entity ~.model.Entity,Roo即可自动为你生成:

  • EntityDataOnDemand.java
  • EntityDataOnDemand_Roo_DataOnDemand.aj
  • EntityDataOnDemand_Roo_Configurable.aj

其中EntityDataOnDemand_Roo_Configurable.aj在本blog这里曾经详细讲述过。EntityDataOnDemand_Roo_DataOnDemand.aj切面文件通过运行时织入,和EntityDataOnDemand.java组成了EntityDataOnDemand类。

Course类为例,可用下图表示:

EntityDataOnDemand

留意到其中三个methods:

“EntityDataOnDemand”可以理解成“按需生成的数据”,上述就是实现这种“需求”的三个核心的方法。下面对其一一展开。

getNewTransientCourse()

有如下定义:

The getNewTransientEntity method returns an initialized, transient entity with sam­ple data stored in each field. The entity is not persistent, so this method can be used by unit tests and integration tests.

具体的用法如下所示:

从上可知,getNewTransientEntity()方法返回一个瞬时态(New/Transient)的实体,该实体的每一个属性都根据index参数,按照某种逻辑赋予了初始值:

  • String类型的属性按照“属性名_index值”的形式初始化,如:description_5。
  • 数值型的属性以index的值初始化。
  • date类型的属性以接近当前时间的随机时间初始化。
  • Boolean类型的属性初始化为true。
  • 联系的实体(单的一方)会通过调用getNewTransient***()方法初始化。

可以看出,getNewTransientEntity()方法为我们获得一个具有一定标识能力的实体提供了很方便的途径。在实际运用中,常用来测试实体类某个方法是否正确,或者生成一个实体参数供测试的方法调用,例如:

在Intergration Test中,也可以先调用getNewTransientEntity()方法获得实体,再调用save()flush()方法使entity持久化:

getSpecificCourse()

有如下定义:

The getSpecificEntity method returns an entity from the internal list of persisted entities.

getSpecificEntity()方法与getNewTransientCourse()方法相似,也有index作为参数。不同的是getSpecificEntity()方法仅对集成测试(Integra­tion Tests)有价值,仅能用于JPA环境中,并且已经被持久化,托管给某个EntityManager。

getSpecificEntity()方法被调用时,Roo会在一个已经持久化的“内部列表”中返回一个指定index值的实体,如果这个列表不存在(例如第一次调用这个方法),Roo会自动生成10个实体到list里,并且将其持久化。所以,如果你不想数据库处于一个不一致的状态(有10个奇怪的测试用的实体),那么在你的integration test方法声明处添加@Transactional注解,Roo会在测试完成时roll back你的的修改。

在Intergration Test中,可以这样用:

getRandomCourse()

有如下描述:

If you don’t care which persistent entity instance you work with, ask for a random one with getRandomEntity().

值得注意的是,该方法只会返回10个不同的实体,也就是说重复调用getRandomCourse()并不一定返回两个不同的实体。可以通过为getNewTransientEntity()方法设置不同的index值来获得任意个不同的实体。

总的来说,DataOnDemand的作用是:

speed up your test writing, making the cre­ation of simple entity instances a trivial operation.

你可以重写DataOnDemand中的方法,使其适应你的需求。但要注意的是,要保持上述三个关键方法的正确性(因为后面某些测试方式依赖于这三个方法)。若要知道这三个方法的具体实现方式,可以自己用-testAutomatically命令看Roo自动生成的aj文件中的源代码,由于篇幅问题,这里就不作详细描述了。

了解完DataOnDemand Test Framework Component之后,再来看看Roo是如何实现三种不同类型的测试的。

Unit Test

test stub命令

test stub命令的定义如下:

The test stub command creates a JUnit test that constructs an instance of a class, and creates test stubs for each of the public methods.

例如,Sensor类的定义如下所示:

在Roo Shell中输入命令:test stub --class ~.entity.Sensor,Roo在src/test/java下以相同的包名新建了一个SensorTest.java文件,文件内容如下所示:

可以看出,test stubSensor类的每一个public方法,包括aj文件中的,都生成了同名的测试方法。如何理解@Test注解?可以看这里这里

mock

Roo中我们还可以通过“mock object”来进行单元测试。

什么是mock?可以参考如下定义:

Mock objects are objects that pretend to be instances of particular classes, but arecompletely controlled by the test developer. They appear to implement the specified interfaces, and you can configure them to return predictable values when their methods are called.

也就是说,通过“mock”我们可以模拟出一个指定的对象,这个对象可以协助我们对目标进行测试。通常我们可以令mock对象实现某个接口,由此代替测试目标的某个依赖。由于mock是可控可定义的,所以为我们控制测试目标提供了很大的方便:

Mock objects are often used when a layered application requires a particular Spring bean to collaborate with other beans, either in the same level or lower levels of the application.

test mock command

下面通过一个例子来说明Roo中mock test的步骤:

在Roo Shell中输入:test mock --entity ~.model.Course,Roo自动生成CourseTest.java文件(若文件已存在,则无任何改变),文件部分内容如下所示:

可以看到,Roo自动生成了CourseTest类,并用@MockStaticEntityMethods标注。自动生成testMethod()示例方法(本身只作示例用,并无特殊含义),该方法中通过调用AnnotationDrivenStaticEntityMockingControl类的方法来控制mock对象的输出。整个过程可以简述成:

  1. Roo为CourseTest单元测试类添加了@MockStaticEntityMethods注解。@MockStaticEntityMethods注解使整个CourseTest运行在“expectation record”模式中。在该模式下,所有静态方法的调用都不会被真正地执行,而是将该“执行”的调用放到某个内部的队列内。具体地,例子中的Course.countCourses()不会真正地执行(也就是该静态方法没有访问数据库),而是向mock维护着的某个内部队列中添加了类似“Course调用了countCourses()”的信息。
  2. 紧接着,调用expectReturn(expectedCount)方法,将expectedCount放进另外一个内部队列中,表明这个“expectation”应该由目前队列中最前面的静态方法返回(Course.countCourses()方法)。
  3. 调用playback()使CourseTest处于“回放(playback)”模式。在该模式下,随后的每一次静态的调用都会按其对应“expectation”入队的顺序返回相应的值。具体地,例子中最后一次Course.countCourses()的调用将会返回13。

可以理解成,通过@MockStaticEntityMethodsexpectReturn()方法登记那些需要mock的方法,然后在playback模式下,由mock对象(隐藏的)取代原对象的调用返回设定的return值。所以,利用其队列的性质,可以如下使用:

该测试可以正确通过。

书中还提到一个具体的例子,这里简单地叙述一下:

假设我们要测试“学生选课”这个用例,“选课”服务的代码如下:

可以看出,这个方法牵涉到三个实体类,分别是OfferingStudentRegistrationOfferingStudent之间是多对多的关系,它们通过Registration联系起来。具体的时序图如下所示:

时序图

那么,我们如何通过mock来测试completeRegistration方法呢?可以按如下步骤来进行:

  1. 输入命令test mock --entity ~.service.RegistrationServiceBeanImpl。Roo会自动生成RegistrationServiceDefaultImplBeanTest类,并用@MockStaticEntityMethods标注。
  2. 声明三个dod类:

  3. 声明测试用的service:

  4. 利用@before注解初始化上述变量:

  5. 编写测试方法:

至此mock测试编写完成。可以看出,在Unit Test中利用mock,我们可以将某个测试点跟运行环境隔离开来,并控制输入参数的变化,通过比较测试对象的输出或者其他行为来判断是否通过测试。

Integration Test

在Roo中进行集成测试(Integration Test)的步骤比较简单,可以分为两种,一种是利用Roo自动生成的EntityIntegrationTest类,另一种是手写代码进行Integration Test。

test integration command

在Roo Shell中我们输入test integration --entity ~.model.Sensor,Roo自动生成了EntityIntegrationTest.javaEntityIntegrationTest_Roo_Configurable.ajEntityIntegrationTest_Roo_IntegrationTest.aj文件。其中EntityIntegrationTest_Roo_IntegrationTest.aj文件如下所示:

可以看出,Roo对每个通过Roo命令生成的方法(finder,CRUD等)都生成了测试方法。

手写Integration Test

你可以采用这个命令:class --class ~.web.***Test --path SRC_TEST_JAVA为你的测试目标生成一个测试类。然后你要为这个测试类添加如下注解:

Spring Roo in Action中是这样解释@ContextConfiguration@RunWith注解的:

The @ContextConfiguration annotation defines the Spring context files to search for to load your test.

Next, you tell JUnit that it has to run under the Spring Framework with your @RunWith annotation.

接着你便可以在***Test.java中编写你的测试方法了。

Functional Test

上述的Unit Test和Integration Test都属于white-box tests,white-box tests关心的是程序内部运行的逻辑。下面我们要进行的是另一测试,叫做Functional Test,属于black-box tests的一种。我们从用户的角度出发,从程序的界面开始进行测试。

Selenium简介

在Roo中,Functional Test由Selenium web testing framework来实现。Spring Roo in Action中是这样介绍Selenium的:

Selenium (http://seleniumhq.org) is a suite of web testing tools. It can exercise browser-based tests against an application, and has a Firefox-based IDE (Selenium IDE) for building tests interactively against a live application.

利用Selenium,你可以:

  • 用例测试:基于浏览器的用例测试。
  • 监控:通过检测controller是否返回正确的值来监控整个程序是否正确运行。
  • 压力测试:利用Selenium自带的测试引擎和多台电脑来对程序进行压力测试。

下面我们开始一步步探索Selenium。

selenium command

在Roo Shell中输入:selenium test --controller ~.web.**Controller,自动生成如下文件:

可以知道,Roo自动地在menu.jspx中添加了链接到test-suite.xhtml的menu项,在pom.xml添加了selenium的依赖包,在src/main/webapp/selenium文件夹下新建了test-suite.xhtmltest-sensor.xhtml两个文件。

打开menu.jspx文件,看到Roo添加了如下代码:

可知道该menu项访问了/resources/selenium/test-suite.xhtml这个文件,但为什么可以直接访问程序的资源路径呢?所有url不是转发给controller了吗?打开webmvc-config.xml文件,看到如下代码:

可以看到,改代码声明了一个允许用get方法访问项目资源路径的配置,将/resources/**映射到/, classpath:/META-INF/web-resources/实际路径。所以我们可以直接访问上述文件。

再打开test-suite.xhtml文件,有如下代码:

可以看出,这个文件是各个selenium test的总入口,它以列表的形式提供了各个test-***.xhtml文件的链接。再打开test-sensor.xhtml文件,部分代码如下所示:

看上去像一个不知所云的html文件,但其实这是“HTML-based Selenium test language”,书中是这样解释的:

The HTML-based Selenium test language was designed so that power users and advanced business experts could read and interpret it.

可以把它想象成一个测试脚本,只不过这个脚本文件以html语言为基础而已。这样做有一个好处,就是我们可以用浏览器直接打开它,直观地观察这个配置文件:

示例

可以这样理解这些代码:test-sensor.xhtml模拟了一个用户的整个操作流程以及用户对流程中程序的反馈结果的响应方式,即通过“open”、“type”等参数模拟用户进行用例时的操作步骤,“verifyText”、“assertText”等参数定义用户判断用例的结果是否正确的标准。

接下来简单解释“open”、“type”这些参数的含义:

  • open:定义了测试的起点,即controller对应的url。
  • type:定义要填入参数的field(例如_identifier_id),和其相应的值(例如someIdentifier1)。
  • clickAndWait:让selenium框架触发id为proceed按钮(//input[@id = ‘proceed’]的含义),并且等待返回一个合法的值。
  • verifyText:监测某个html node的值,看是否跟预设的相符合,若不符合,输出信息,然后继续执行。
  • assertText:监测某个html node的值,看是否跟预设的相符合,若不符合,中断测试。

更多常用参数请看这里

运行 selenium

首先,确保你安装了Firefox浏览器(filefox可执行程序要在系统变量PATH里)和可以在命令行(或者终端)中运行mvn命令。然后开启web服务器,cd到项目文件夹,输入:mvn selenium:selenese来启动selenium tests。

输入命令后,firefox会自动启动,并进入了selenium test模式,在该模式下你会发现浏览器按照预先编写好的脚本一步步进行测试,测试完成后firefox会自动退出,mvn会显示测试的大致结果,并在target目录下生成了一个名为selenium.html的详细的测试报告。

总结

测试分为三种,分别是Unit Test,Integration Test和Functional Test。Roo中以test stub和test mock来实现Unit Test,其中stub最为简单,mock适合模拟依赖关系;以test integration来实现Integration Test,注意要用注解来指定运行环境;以selenium框架来实现Functional Test,用selenium language来模拟用户的操作和判断结果的逻辑。

Roo中的i18n

什么是i18n

中文维基上对i18n有这样的解释:

国际化与本地化(英文:Internationalization and Localization)通常简写为I18n和L10n。

基于他们的英文单字长度过长,常被分别简称成i18n(18意味着在国际化这个单字中,i 和 n 之间有 18 个字母)及L10n。

注意区别程序国际化和程序本地化的区别:程序国际化一般指为程序将程序的显示语言与程序内部显示逻辑脱钩的过程,程序本地化一般指根据特定区域设置调整程序显示语言的过程。

Roo中的i18n

在Roo项目的WEB-INF/i18n文件夹下,默认会有两个message文件,分别为:application.propertiesmessages.properties。如何理解这两个文件?

application.properties 和 messages_*.properties

在Roo项目中全局搜索application.properties关键字,在webmvc-config.xml文件中发现如下代码:

从注释可以看出,messages*.propertiesapplication.properties 文件的作用是为程序的国际化提供本地化翻译(有点绕)。其中messages*.properties提供了的Roo自动生成的通用的本地化描述,application.properties提供了程序业务实体等“application specific”的本地化描述。

注意application.properties文件也可以进行扩展,例如:

Spring 框架会自动匹配语言设置,选取适当的application文件。

那么,这两者有什么区别呢?这里有一段描述:

The main purpose of two different files here is that the messages_*.properties files contain translations of commonly used labels in a Roo Web UI. Roo does actually not manage these files, it just copies them into the target project.

The application.properties file contains all labels that are specific to your application (like field labels, entity labels, menu labels). Roo would manage this file if a new field or entity is added but it would obviously not be able to not translate those.

可以看出,messages_*.properties中的“键值对”不受Roo控制,属于完全由用户自定义的范围。application.properties文件则由Roo管理,例如当你为某个Entity用Roo Shell建立了CRUD页面的时候,Roo自动在application.properties中添加了一系列类似“label_demo_imlab_ims_entity_Sensor”的键值对作为某个label的显示内容。这些键值对的key是根据entity生成的,后面会提到。值得注意的是,由于Roo不会帮你自动翻译label,生成对应的application_*.properties文件,所以Roo将其统一添加到唯一的application.properties文件中。

那么,spring框架是如何使用这两个文件的呢?注意到上述代码中的:

也可以写成:

messageSource怎么理解?ReloadableResourceBundleMessageSource有什么作用?

ResourceBundle和MessageSource

我们先把ReloadableResourceBundleMessageSource分解成两个概念:ResourceBundle和MessageSource。这里有一篇好文章,详细地介绍了spring mvc的Messages和Internationalization机制:

ResourceBundle

A ResourceBundle is the Java abstraction to get locale-specific resources such as objects and text strings.

ResourceBundle就像一个工具类,实现了根据baseName和locale参数读取不同message文件。例如我们可以这样使用ResourceBundle:

我们可以根据locale的值拿到名为“baseName_*”的不同的resources。ResourceBundle搜索资源文件的顺序如下所示,设locale的设置为(language1, country1, and variant1):

  • baseName + “” + language1 + “” + country1 + “_” + variant1
  • baseName + “” + language1 + “” + country1
  • baseName + “_” + language1
  • baseName + “” + language0 + “” + country0 + “_” + variant0
  • baseName + “” + language0 + “” + country0
  • baseName + “_” + language0
  • baseName

ResourceBundle会在依照这个顺序从上网下搜索,返回第一个满足条件的文件。

MessageSource

MessageSource是一个接口,如下所示:

我们通过MessageSource接口的getMessage()方法取得本地化值。值得注意的是,实现MessageSource接口时,读取资源的工作一般来说交由ResourceBundle来负责,再在其之上添加占位符参数支持等功能。注意MessageSource接口的实现类在context中必须以messageSource命名。

ReloadableResourceBundleMessageSource

Roo默认采用ReloadableResourceBundleMessageSourceMessageResource的实现类,它具有自动定时更新Message缓存而无需重启服务器的特性。默认“WEB-INF/i18n/messages”和“WEB-INF/i18n/application”这两个basename作为搜索的关键字。fallbackToSystemLocale属性设置为false意味着当MessageSource找不到对应的资源文件时将使用默认message文件而不是服务器本地化(如message_EN)文件。

使用方法

在java文件里, 我们可以这样用:

在jsp里,我们可以这样用:

就可以取出本地化label值。或者:

把“message”对应的本地化值保存在”message_var”这个jsp变量中。

但是我们注意到,Roo默认生成的项目中,语言选项选定以后,项目中每一个url地址的资源都被本地化了,这是怎么做到的呢?

webmvc-config.xml文件中我们可以看到这样的代码:

可以看到代码为HandlerMapping注册了LocaleChangeInterceptor这个拦截器。LocaleChangeInterceptor的作用可以看这里

Interceptor that allows for changing the current locale on every request, via a configurable request parameter.

可以看出spring mvc正是通过这个拦截器调用MessageSource来完成本地化的。但locale参数如何获得?留意到webmvc-config.xml文件中这一行代码:

参考这里,可知:

在Spring MVC应用程序中,用户的区域是通过区域解析器来识别的,它必须实现LocaleResolver接口。Spring MVC提供了几个LocaleResolver实现,让你可以按照不同的条件来解析区域。

注意要将LocaleResolver的实现bean命名为“localeResolver”,DispatcherSelvlet才能自动侦测到。具体地可以分为如下几种:

AcceptHeaderLocaleResolver:通过检验HTTP请求的accept-language头部来解析locale。 SessionLocaleResolver:通过检验用户session中预置的属性来解析locale。 CookieLocaleResolver:通过检验用户cookie中预置的属性来解析locale。

相应bean的属性设置请参考api文档。

最后

综上所述,我们可以看到spring mvc实现i18n的方式:通过LocaleChangeInterceptor来调用LocaleResolver获得用户设定或者默认的locale参数;项目中某个label的显示不依赖于语言,而是通过message机制结合locale属性动态地改变。显示层在渲染label时通过message code和locale属性在message.properties和application.properties文件中寻找对应的本地化值。

Roo中的@Version

问题提出

当我们为entity添加@RooJpaActiveRecord注解时,Roo为我们自动生成了一个名为Entity_Roo_Jpa_Entity.aj的文件,其中有一句:

这是什么意思呢?“Version”是版本的意思,那么“@Version”注解是版本号的意思吗?Entity为什么需要版本管理? 下面我们对此展开探寻。

线索

注意到Entity_Roo_Jpa_Entity.aj与@RooJpaActiveRecord有关,由上一篇博客可推测,@Version与JPA有关。打开Entity_Roo_Jpa_Entity.aj文件,鼠标移到@Version注解上,显示:

Specifies the version field or property of an entity class that serves as its optimistic lock value. The version is used to ensure integrity when performing the merge operation and for optimistic concurrency control.

可知,@Version的作用是声明注释的属性或者返回值作为optimistic lock value。version属性的作用是optimistic concurrency control

Lock

optimistic lock的一般译为“乐观锁”,属于数据库事务并发控制领域的概念。数据库并发控制一般由“Lock(锁)”来实现的。这里提到:

Transactional isolation is usually implemented by locking whatever is accessed in a transaction. There are two different approaches to transactional locking: Pessimistic locking and optimistic locking.

可知transaction的isolation性质(ACID中的I)通常通过lock进行访问控制来实现。lock具体可分为pessimistic lock(悲观锁)和optimistic lock(乐观锁)。

pessimistic lock(悲观锁)

pessimistic lock的意思是:

a resource is locked from the time it is first accessed in a transaction until the transaction is finished, making it inaccessible to other transactions during that time.

也就是说,在一个transaction中,data会被完全锁住,直到transaction结束。另一个transaction若要访问该data,则需要等待。注意pessimistic lock可能会引起循环等待导致死锁。由于pessimistic lock的特性,采取该并法策略可能会导致运行效率大幅度下降,故并不适合高并发、长事务的场景。

optimistic lock(乐观锁)

这里提到:

Optimistic locking does not use exclusive locks when reading. Instead, a check is made during the update to make sure that the record has not been changed since it was read.

这里也提到:

At commit time, when the resource is about to be updated in persistent storage, the state of the resource is read from storage again and compared to the state that was saved when the resource was first accessed in the transaction. If the two states differ, a conflicting update was made, and the transaction will be rolled back.

可以看出optimistic lock的实现原理是通过在准备同步数据库时(commit时)对比数据在transaction中初次读取时和当前在数据库中的状态,看其是否满足某种规则来决定此次同步是否成功,若不成功则rollback。

具体的实现,这里举了一个例子:

一般的应用是采用数据版本的方式(version)实现,在读取数据的时候将version读取出来,在保存数据的时候判断version 的值是否小于数据库中的version的值,小于则不允许更新,否则可以更新。

值得注意的是,optimistic lock的实现并不依赖于数据库,而是基于系统中的数据存储逻辑。故系统外部的更新并不受optimistic lock的控制,可能会造成脏数据被更新到系统当中。

@Version

那么,@Version注解的具体用法是怎么样的呢?结合API文档,如上所述,我们只需要在某个数值属性或者timestamp类型的属性上标注@Version注解即可:

或者:

要注意:

  1. 每个class只能有一个@Version标注的属性。
  2. @Version标注的属性应该属于该class的primary table(主表)。
  3. @Version标注的属性的类型包括:int, Integer, short, Short, long, Long, java.sql.Timestamp
  4. 程序不应该手动修改@Version标注的属性(交由框架自动处理)。

Roo中的ActiveRecord以及EntityManagement

《ROO中的*.aj文件》一文中提到,当我们在Roo Shell中新建Entity时,Roo会自动生成一个名为***_Roo_Jpa_ActiveRecord.aj的文件,里面定义了关于该Entity的CRUD操作的方法。为什么Roo要采取这种模式呢?如何理解这种模式?

Active Record

***_Roo_Jpa_ActiveRecord.aj切面文件代表了一种领域模型的模式,称之为Active RecordWiki上有这样的一段话:

Active Record是一种领域模型模式,特点是一个模型类对应关系型数据库中的一个表,而模型类的一个实例对应表中的一行记录。

提出者Martin Fowler对该模式作了如下定义:

An object that wraps a row in a database table or view, encapsulates the database access, and adds domain logic on that data.

简单地说就是:

An object carries both data and behavior.

下图是一个简单的例子:

例子

可以看到,Person类除了属性之外,还负责了本身的CRUD操作,还有一些属于自己的业务逻辑。我们新建Person类的时候,这样写就可以了:

由此可以总结一下Active Record的特性:

  1. Entity中封装了数据库访问。
  2. 每个Entity代表数据库中的一行数据。
  3. Entity是一种领域模型,封装了部分业务逻辑。

由于这些特性,Active Record模式对于数据库的操作(单表操作)特别方便,对于简单的业务来说逻辑比较清晰明了,故特别适用于web快速开发。

JPA

**_ActiveRecord.aj文件中,有类似如下的代码:

@PersistenceContextEntityManager是什么呢?这牵涉到一个名为“JPA”的概念。Spring Roo in Action对JPA下了这样一个定义:

The Java Persistence API, or JPA for short, was created to provide a standard program­ ming interface for object-oriented database mapping.

可以看出JPA实际上是一系列接口的定义:

Hibernate, EclipseLink, Open- JPA‚ and other frameworks implement the JPA 2.0 specification, which allows developers to write code to a single API regardless of the implementing vendor.

JPA为我们提供了统一的数据访问层API。具体地,由Roo的API文档

The Spring JPA, available under the org.springframework.orm.jpa package, offers comprehensive support for the Java Persistence API in a similar manner to the integration with Hibernate or JDO, while being aware of the underlying implementation in order to provide additional features.

可以看出,Spring JPA是一种JAP的实现,在org.springframework.orm.jpa包中。但它的ORM功能主要是集成其他框架(例如Hibernate)的,并在其之上添加了一些其他的功能。

所以,当我们在Roo Shell中输入:

时,Roo为我们添加了JAP、Hibernate的依赖jar包,在applicationContext.xml中添加了相应的JDBC datasource,建立了persistence.xml文件来关联JAP与数据库,建立了数据库配置文件database.properties文件和加载了Bean Validation Framework等等一系列的工作。注意到该Roo命令中--provider--database两个参数,前者指定了JAP的实现框架,后者指定了数据库种类。

下面看一下JAP的几个主要的组件:

  1. JPA entity
    用注解或者xml文件定义的一个映射到数据库中某个表的java类。
  2. persistence context
    负责管理entity的生命周期,依赖关系的context类,拥有自己的线程或者session。
  3. EntityManager API
    提供了persistence context的访问接口。
  4. JPA configuration file
    实现了JAP标准的ORM框架的配置文件,通常为META-INF/persistence.xml

后面我们会讲到,在***_ActiveRecord.aj文件中可以找到这些组件的踪迹。

实例

下面我们以Sensor类为例,其中Sensor_Roo_Jpa_ActiveRecord.aj文件代码如下所示:

我们可以看到,Roo为Sensor类自动生成了CRUD方法和一些finder方法,注意其中的@PersistenceContext注解和@Transactional注解,后面会进行说明。

Sensor_Roo_Jpa_Entity.aj文件代码如下所示:

Sensor.java文件如下所示:

下面分点进行解析。

@RooJpaActiveRecord注解

根据Spring Roo in Action中介绍,当我们为一个java类添加@RooJpaActiveRecord标签时,Roo自动为其生成了***_Roo_Configurable.aj,***_Roo_Jpa_ActiveRecord.aj,***_Roo_Jpa_Entity.aj,三个切面文件,分别为该java类添加了Configurable支持,JPA-based methods(delete()、save()等)和id、version属性。

@Entity注解

可以看到在Sensor_Roo_Jpa_Entity.aj中有如下代码:

本blog在这里解释过declare @type: 的用途和用法,这行代码的意思是为每个Sensor类添加了@Entity注解。那么如何理解@Entity?

entity annotation jpa为关键词在google上进行搜索,找到这里

JPA entities are plain POJOs. Actually, they are Hibernate persistent entities. Their mappings are defined through JDK 5.0 annotations instead of hbm.xml files.

JPA annotations are in the javax.persistence.* package.

我们可以知道,在JDK 5.0及以上的版本中可以通过annotations来配置JPA,而不是一个xml配置文件,还有javax.persistence.*包里的类都是于JPA有关。

Every persistent POJO class is an entity and is declared using the @Entity annotation (at the class level)

可以知道,@Entity注解声明了对应的pojo为实体类,即声明了该pojo交由JAP管理,JAP会自动地根据该pojo的属性进行底层数据库处理(除非你自己用@Table等注解进行配置)。

所以@Entity使Sensor纳入到JAP的持久化管理之中,@Id注解声明了Sensor.id为主键,@Column(name=**)为Sensor的属性指定了映射的列名,@Version声明了Sensor.version为实体类版本号属性(提供了optimistic locking功能?)。总的来说,Sensor_Roo_Jpa_Entity切面使得Sensor成为了JAP中的一个Entity。

@PersistenceContext注解

google到了这里

Expresses a dependency on a container-managed EntityManager and its associated persistence context.

前面我们提到,EntityManager是访问persistence context的接口。那么这句话的意思是@PersistenceContext的作用是使spring注入一个EntityManager实例。在这段代码:

中我们也可以看出,@PersistenceContext令spring将EntityManager注入到Sensor.entityManager中,以供下面的CRUD方法使用。

这里解释了PersistenceContext和EntityManager之间的关系:

The persistence context is the collection of all the managed objects of an EntityManager.

另外,在项目中搜索“entityManagerFactory”,在applicationContext.xml文件中有如下配置声明:

可以看出,@PersistenceContext每次注入的EntityManager实例都是由LocalContainerEntityManagerFactoryBean产生(关于FactoryBean请看这里中的介绍)。

其中的<property name="dataSource" ref="dataSource"/>由以下代码指定:

另外要注意的是

EntityManager instances should not be shared between concurrently executing threads. According to JPA specification, the methods of the EntityManagerFactory are thread-safe (this is also explicitly stated in Hibernate documentation). Hence, you can use a single instance of an EntityManagerFactory for every data source in your application.

即EntityManager不是线程安全的,EntityManagerFactory是线程安全的。所以确保EntityManager实例不要在线程之间共享

@Transactional注解

这里有对@Transactional的详细描述:

Describes transaction attributes on a method or class.

“transaction”是什么?wiki上是这样定义的:

Transactions provide an “all-or-nothing” proposition, stating that each work-unit performed in a database must either complete in its entirety or have no effect whatsoever. Further, the system must isolate each transaction from other transactions, results must conform to existing constraints in the database, and transactions that complete successfully must get written to durable storage.

也就是说,“事务”是指一系列数据库操作的集合。该集合中所有操作要么全部执行成功(然后被持久化),要么全部无效(日志回滚)。数据库系统要保证事务之间的独立性(锁机制),事务处理结果要满足数据库的约束性(一致性)。可以简单地概括成“ACID”性质:

A database transaction, by definition, must be atomic, consistent, isolated and durable. Database practitioners often refer to these properties of database transactions using the acronym ACID.

ACID的详细介绍可以看这里

由此我们可以推断,@Transactional注解可以为entity或者某个method添加”事务特性“。例如:

DefaultFooService类的每个public方法便有了事务特性。那么@Transactional注解是如何起作用的呢?在项目中搜索transactionManager,在applicationContext.xml文件中会发现如下声明:

这句声明使得系统自动扫描java文件,对@Transactional标注的类进行处理,添加事务支持。注意到如下声明:

该声明指定了org.springframework.orm.jpa.JpaTransactionManager为transactionManager的实现,还指定了entityManagerFactory属性的引用。

综上,可以看出,由于效率方面的考虑,Sensor_Roo_Jpa_ActiveRecord切面中并没有对整个类标注@Transactional,而是在方法粒度上进行事务管理。由于EntityManager不是线程安全的,所以Sensor_Roo_Jpa_ActiveRecord中CRUD方法中引用到entityManager的方法都添加了@Transactional进行事务管理。也就是说,当我们自定义某些持久化相关的方法时,如果引用了entityManager,要给该方法标注@Transactional以防止出现问题,或者直接用entity的save、flush方法。

Sensor.entityManager()静态方法

我们把目光集中在如下几个方法上:

这里有一段关于entity的描述:

Entity objects are in-memory instances of entity classes (persistable user defined classes), which can represent physical objects in the database.

可以看出,entity存在于内存之中,不一定存在于数据库中,所以才有“持久化”一说。也就是说,必须存在一种机制去管理entity的“状态”,让entity和数据库数据之间切换和同步。于是可以引入“Entity Object Life Cycle”这个概念:

The life cycle of entity objects consists of four states: New(Transient), Managed, Removed and Detached.

状态转换

上述图片展示了entity在其生命周期内各个状态、状态之间的转换规则和触发转换的条件。具体的转换规则如下所示:

  1. 当一个entity被初始化的时候(通过new语句),该entity的状态是New,表明该实体尚未交由EntityManager管理,也尚未存在于数据库中。
  2. 当entity通过EntityManager的persist()方法持久化后(commit/flush后才被写进数据库),该entity处于Managed状态。
    当entity通过EntityManager从数据库中retrieve出来时,也处在Managed状态。
  3. 当entity通过EntityManager的remove()方法删除后,处在Removed状态。commit后entity对应的数据将从数据库中删除。
  4. 当entity脱离EntityManager的管理(通过调用EntityManager的close()或clear()方法)时,entity处于Detached状态。

由于Roo默认采取了ActiveRecord模式,所以对EntityManager的方法进行了包装,我们直接调用entity.save()、entity.delete()等即可。值得注意的是,当我们调用entity.persist()后该entity并没有马上被写进数据库,它只是标记了该entity可以被持久化。必须等待EntityManager调用flush()之后entity才正式持久化(EntityManager什么时候调用flush()默认的FlushMode为AUTO,即在query execution之前的某个时刻自动调用flush())。

总结

Roo为了实现快速web开发,引入了ActiveRecord模式来管理entity以及他们之间的关系。我们可以直接通过调用实体类的save()、remove()方法来CRUD。Roo利用切面文件为entity添加ActiveRecord模式,通过向entity注入EntityManager实例实现了JPA的支持,并结合spring的事务管理方式,通过注解解决了多线程下entity的同步问题。

ROO中的@Configuration

问题提出

当我们在ROO中为Entity添加了@RooJavaBean注解时,ROO会为我们自动生成了一个Region_Roo_Configurable.aj文件,其中代码如下:

如何理解这段代码?下面一步步进行解析。

declare @type:

google了这篇文章

AspectJ 5 supports a new kind of declare statement, declare annotation. This takes different forms according to the recipient of the annotation: declare @type for types, declare @method for methods, declare @constructor for constructors, and declare @field for fields.

可以看到,除了@type之外还有许多形如@**的标签。一般形式如下所示:

要理解这个注解的作用,首先要了解AOP中静态横切动态横切的概念。这里有一段通俗的解释:

动态横切是通过切入点(pointcut)和链接点(joint point)在一个方面(aspect)中的创建行为的过程;方面(aspect)定义了所有的链接点,切入点以及通知(advice),以便把需要切入的职责(interweave)注入到原来的对象中;

静态横切是通过在不修改原有职责的基础上增加新的职责;以往我们用过类的继承来实现,但继承是种强依赖关系,怎么让他们松藕,这个时候我们用静态横切,用mixin;

declare @<kind>注解实现了静态横切的声明功能,例如:

由此可以看出declare @type: Region: @Configurable;的意思是给所有的Region添加了@Configurable注解。

@Configurable

google了这篇文章,里面提到:

Since spring 2, you were writing your bean configurations to xml files. But, Spring 3 gave the freedom to move bean definitions out of xml files. Now, you can give bean definitions in your java files itself. This is called JavaConfig feature (using @Configuration annotation).

也就是说,我们可以用过@Configuration来配置bean,而不是通过xml配置文件。

假设我们有如下接口声明:

实现如下:

那么我们不用xml对其进行配置,用@Configuration,如下所示:

可见,@Configuration注解声明了ApplicationConfiguration为一个配置类,@Bean(name="demoService")可以看成是xml配置中的<bean class="**.**.ApplicationConfiguration " id="demoService"/>
</beans>
。于是,我们可以这样取出bean:

另外,@Configuration注解从属于Annotation Type Configuration这个主题下,这里有详细的介绍。

例如,可以通过

或者

令Configuration生效。还可以通过@Inject Environment env;

令Configuration拥有访问其他bean的能力。也可以用@Value注解做到这一点:

更详细的用法参考上面给出的文档

总结

综上,可以这样理解ROO的行为:@RooJavaBean注解让ROO用declare @type: Region: @Configurable;语句为我们的实体类添加了Configurable特性,使我们可以直接在entity.java里面配置bean,而不用去写xml配置。

ROO中的ApplicationConversionServiceFactoryBean

问题提出

ROO项目中,第一次用web mvc命令新建一个controller后, ROO会在该controller所在的包中新建一个ApplicationConversionServiceFactoryBean类,这个类是干什么的呢?

线索

打开ApplicationConversionServiceFactoryBean.java文件,可得如下代码:

从这段代码出发,我们一步步搞清楚这个ApplicationConversionServiceFactoryBean

FormattingConversionServiceFactoryBean

ApplicationConversionServiceFactoryBean继承于FormattingConversionServiceFactoryBean,打开FormattingConversionServiceFactoryBean.java文件,可见如下代码:

先从它实现的接口出发,google了一下FactoryBeanFormattingConversionServiceEmbeddedValueResolverAwareInitializingBean这几个关键词:

FactoryBean

FactoryBean接口声明如下所示:

这里可以大致了解其用途:

Spring中有两种类型的Bean,一种是普通Bean,另一种是工厂Bean,即FactoryBean。工厂Bean跟普通Bean不同,其返回的对象不是指定类的一个实例,其返回的是该工厂Bean的getObject方法所返回的对象。
使用场景:1、通过外部对类是否是单例进行控制,该类自己无法感知 2、对类的创建之前进行初始化的操作,在afterPropertiesSet()中完成。

例如如下FactoryBean实现:

注册到容器:

若nextDayDateDisplayer类如下所示:

尽管注册时用的是NextDayDateFactoryBean类,但dateOfNextDay属性的类型是DateTime而不是NextDayDateFactoryBean。这就是说:

容器返回的是FactoryBean所”生产”的对象类型,而非FactoryBean实现本身。

所以,FactoryBean提供了一种高级的配置机制,令复杂的bean配置成为可能。

FormattingConversionService

之前写过一篇博文提到了spring mvc中数据类型转换、验证和格式化的流程,如图所示:

流程

从图中可以看出,Spring3引入了格式化转换器(Formatter SPI) 和格式化服务API(FormattingConversionService)来实现格式化(从Obejct到String),例如Date对象到String对象。该类实现了四个接口,分别是:EmbeddedValueResolverAware, ConversionService, ConverterRegistryFormatterRegistry

Formatter

先说一下Formatter接口,如下所示:

其中Printer和Parser接口分别定义了String print(T object, Locale locale);T parse(String text, Locale locale) throws ParseException;方法。

我们用的时候可以这样用

可以看出,Formatter接口包装了Object和String类型之间的转换,主要应用在把view层用户输入或选择的数据转换成controller层的实体类型。

Converter

Converter接口定义的方法如下所示:

若要实现多种类型之间的转换,可以用GenericConverter接口:

从上述可以看出,于Formatter不同,Converter负责任意类型之间的转换。

ConverterRegistry & FormatterRegistry

这两个接口主要用于用于注册格式化转换器

可以看出,ConverterRegistry 和 FormatterRegistry 声明了格式转换服务的注册接口。什么是“注册”?个人认为可以理解成spring在读取配置时,要将formatter等托管给某个统一的服务接口(facade模式?),这个服务接口由某个类来实现。而ConverterRegistry和FormatterRegistry尽管没有声明服务接口,但是给予了spring给该托管类注入formatter的能力。

ConversionService

参考api文档

A service interface for type conversion. This is the entry point into the convert system. Call convert(Object, Class) to perform a thread-safe type conversion using this system.

FormattingConversionService继承自ConversionService。而ConversionService接口声明了从任意类到任意类之间转换的接口:

故FormattingConversionService代表从String到Object或者从Object的转换,即格式化转换。

综上,再来看看FormattingConversionService。其一般使用方法如下

从该类的使用方法可以看出,ConversionService接口给了它转换类型的能力,ConverterRegistry和FormatterRegistry给了它被管理(注入)的能力。FormattingConversionService将不同的Formatter包装起来,对外提供统一的类型格式化服务接口。整个convert框架如下图所示:

框架

InitializingBean

google到这里

InitializingBean
Spirng的InitializingBean为bean提供了定义初始化方法的方式。InitializingBean是一个接口,它仅仅包含一个方法:afterPropertiesSet()。
Spring在设置完一个bean所有的合作者(依赖)后,会检查bean是否实现了InitializingBean接口,如果实现就调用bean的afterPropertiesSet方法。

可以看出InitializingBean接口的意义在于提供了一种bean装配完毕以后的自定义初始化入口。

EmbeddedValueResolverAware

请看这里

Interface to be implemented by any object that wishes to be notified of a StringValueResolver for the resolution of embedded definition values.
This is an alternative to a full ConfigurableBeanFactory dependency via the ApplicationContextAware/BeanFactoryAware interfaces.

留意到其中的StringValueResolver,google了一下,找到了这里

Simple strategy interface for resolving a String value. Used by ConfigurableBeanFactory.

这是什么意思呢?StringValueResolver是一个接口,resolve可以理解成“解析”,而spring在根据配置装配bean的时候,需要将配置中的String类型转换成其它类型,这个转换可以由实现来StringValueResolver接口的类来完成。

Aware的意思是“意识到的”,因此EmbeddedValueResolverAware接口的意思大概是某个bean实现了该接口,则在其被装配时,如果StringValueResolver被调用,则该resolver会通过void setEmbeddedValueResolver(StringValueResolver resolver)方法传入bean内部,用户可以在这里取得resolver。

综上,FormattingConversionServiceFactoryBean可以这样理解:

  1. 它是一个FactoryBean,在初始化时需要复杂的配置。
  2. 它“生成”一个FormattingConversionService实例,负责数据的格式化。
  3. 可以为它添加不同的formatter,令其支持不同的数据的格式化转换。
  4. afterPropertiesSet()方法在其初始化工作完成后被调用。

FormattingConversionServiceFactoryBean的实现

我们打开org.springframework.format.support.FormattingConversionServiceFactoryBean.java文件,可以观察到:

可以看出,FormattingConversionServiceFactoryBean提供了installFormatters()方法,使子类可以在父类完全生成后添加Formatter或者Converter。

ApplicationConversionServiceFactoryBean使用方法

默认的ApplicationConversionServiceFactoryBean实现中,只有一个空实现的方法:

从上文可以得知,installFormatters方法是FormattingConversionServiceFactoryBean为实现方便地增添formatter功能而暴露给子类的一个空方法,其在FormattingConversionServiceFactoryBean装配完成后被调用。打开ApplicationConversionServiceFactoryBean_Roo_ConversionService.aj文件,可见一系列如下代码:

这些方法都是由ROO自动根据项目中的Entity的设置自动生成的,返回包括从ID到Object,Object到String等一系列Converter。文件的最后有还有两个方法:

由此可以看出ApplicationConversionServiceFactoryBean的调用顺序为:

可以看出,getStringToSensorLayoutConverter()等一系列方法返回从某类到某类的converter,通过registry接口加入到service中,而installFormatters()方法暴露了一个“添加点”。所以,在自定义converter工厂方法后,要在installFormatters中调用registry.addConverter()完成注册。

ROO如何与ApplicationConversionServiceFactoryBean结合?

按ctrl+H,输入ApplicationConversionServiceFactoryBean进行全局文件搜索,在webmvc-config.xml文件中可见如下bean定义:

又见如下定义:

根据注解可以大概了解到这个的作用是令Spring MVC支持@Controller注解和注册了一系列Formatters和Validators用于@**Format和@vaild标签。那么这个究竟是什么意思呢?

这里提到,实际上声明了两个bean:DefaultAnnotationHandlerMappingAnnotationMethodHandlerAdapter

DefaultAnnotationHandlerMapping

这幅图 可以了解到,HandlerMapping负责url到controller的转发,而DefaultAnnotationHandlerMapping则负责

Implementation of the HandlerMapping interface that maps handlers based on portlet modes expressed through the RequestMapping annotation at the type or method level.

可以得知,DefaultAnnotationHandlerMapping实现了url到通过@RequestMapping注解标志的controller的转发功能。

AnnotationMethodHandlerAdapter

先说一下HandlerAdapterHandlerAdapterHandlerMapping的一个代理类,DispatcherServlet通过该代理来访问所有装配好的Handlers。 而AnnotationMethodHandlerAdapter从这里可以了解到:

Implementation of the HandlerAdapter interface that maps handler methods based on HTTP paths, HTTP methods and request parameters expressed through the RequestMapping annotation.

Supports request parameter binding through the RequestParam annotation. Also supports the ModelAttribute annotation for exposing model attribute values to the view, as well as InitBinder for binder initialization methods and SessionAttributes for automatic session management of specific attributes.

AnnotationMethodHandlerAdapter实现了handler转发请求过程中的参数绑定,@RequestParam、@ModelAttribute注解的解析。

综上,可以知道,<mvc:annotation-driven conversion-service="applicationConversionService"/>的意思是,实现了把请求转发到@controller注解标注的controller类,并解析了@RequestParam,@Valid等等标签,最后该标签的conversion-service属性指定了在转发请求中采用名为applicationConversionService的bean作为数据格式化,数据转换service。

总结

最后总结一下ApplicationConversionServiceFactoryBean的作用和调用流程:spring容器根据webmvc-config.xml进行初始化,根据<mvc:annotation-driven conversion-service="applicationConversionService"/>标签初始化了整个URLMapping和HTTP请求参数绑定、验证等工作,然后我们可以通过@RequestMapping来注册转发路径和通过@RequestParam等注解处理HTTP参数。该标签同时指定了ApplicationConversionServiceFactoryBean返回的ApplicationConversionService作为这些请求的数据格式化和数据转化的服务类,该服务类负责请求参数(通常为字符串)到实体的转换,所以我们能在controller的某个method里面直接取得实体类。我们可以通过修改或自定义Convertor来实现不同类型之间的转换,这种转换被注册到ApplicationConversionServiceFactoryBean之中,通常用来自定义实体类在view层的显示方式(转换成怎么样的字符串),也可以实现自定义form参数到实体类的转换方式。