1引言
Spring 4为MVC应用程序带来了一些改进 。 在这篇文章中,我将重点介绍宁静的Web服务,并通过采用Spring 3.2实现的项目并将其升级到Spring 4来尝试这些改进。以下几点总结了本文的内容:
- 从Spring 3.2迁移到Spring 4.0
- 变化中的@ResponseBody和包容的@RestController
- 同步和异步调用
以下项目的源代码可以在github上找到:
- 原始项目(Spring3.2)
- 迁移到Spring 4
2 Spring 3.2 RESTful示例
起始项目使用Spring 3.2( pom.xml )实现。 它包含一个Spring MVC应用程序,该应用程序访问数据库以检索有关电视连续剧的数据。 让我们看一下其REST API,以使其更加清晰:
弹簧配置
<import resource="db-context.xml"/>
<!-- Detects annotations like @Component, @Service, @Controller, @Repository, @Configuration -->
<context:component-scan base-package="xpadro.spring.web.controller,xpadro.spring.web.service"/>
<!-- Detects MVC annotations like @RequestMapping -->
<mvc:annotation-driven/>
db-context.xml
<!-- Registers a mongo instance -->
<bean id="mongo" class="org.springframework.data.mongodb.core.MongoFactoryBean">
<property name="host" value="localhost" />
</bean>
<bean id="mongoTemplate" class="org.springframework.data.mongodb.core.MongoTemplate">
<constructor-arg name="mongo" ref="mongo" />
<constructor-arg name="databaseName" value="rest-db" />
</bean>
服务实施
此类负责从mongoDB数据库中检索数据:
@Service
public class SeriesServiceImpl implements SeriesService {
@Autowired
private MongoOperations mongoOps;
@Override
public Series[] getAllSeries() {
List<Series> seriesList = mongoOps.findAll(Series.class);
return seriesList.toArray(new Series[0]);
}
@Override
public Series getSeries(long id) {
return mongoOps.findById(id, Series.class);
}
@Override
public void insertSeries(Series series) {
mongoOps.insert(series);
}
@Override
public void deleteSeries(long id) {
Query query = new Query();
Criteria criteria = new Criteria("_id").is(id);
query.addCriteria(criteria);
mongoOps.remove(query, Series.class);
}
}
控制器实施
该控制器将处理请求并与服务进行交互,以检索系列数据:
@Controller
@RequestMapping(value="/series")
public class SeriesController {
private SeriesService seriesService;
@Autowired
public SeriesController(SeriesService seriesService) {
this.seriesService = seriesService;
}
@RequestMapping(method=RequestMethod.GET)
@ResponseBody
public Series[] getAllSeries() {
return seriesService.getAllSeries();
}
@RequestMapping(value="/{seriesId}", method=RequestMethod.GET)
public ResponseEntity<Series> getSeries(@PathVariable("seriesId") long id) {
Series series = seriesService.getSeries(id);
if (series == null) {
return new ResponseEntity<Series>(HttpStatus.NOT_FOUND);
}
return new ResponseEntity<Series>(series, HttpStatus.OK);
}
@RequestMapping(method=RequestMethod.POST)
@ResponseStatus(HttpStatus.CREATED)
public void insertSeries(@RequestBody Series series, HttpServletRequest request, HttpServletResponse response) {
seriesService.insertSeries(series);
response.setHeader("Location", request.getRequestURL().append("/").append(series.getId()).toString());
}
@RequestMapping(value="/{seriesId}", method=RequestMethod.DELETE)
@ResponseStatus(HttpStatus.NO_CONTENT)
public void deleteSeries(@PathVariable("seriesId") long id) {
seriesService.deleteSeries(id);
}
}
整合测试
这些集成测试将在模拟Spring MVC环境中测试我们的控制器。 这样,我们将能够测试处理程序方法的映射。 为此, MockMvc类变得非常有用。 如果您想学习如何编写Spring MVC控制器的测试,我强烈推荐Petri Kainulainen编写的Spring MVC Test Tutorial系列。
@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@ContextConfiguration(locations={
"classpath:xpadro/spring/web/test/configuration/test-root-context.xml",
"classpath:xpadro/spring/web/configuration/app-context.xml"})
public class SeriesIntegrationTest {
private static final String BASE_URI = "/series";
private MockMvc mockMvc;
@Autowired
private WebApplicationContext webApplicationContext;
@Autowired
private SeriesService seriesService;
@Before
public void setUp() {
reset(seriesService);
mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
when(seriesService.getAllSeries()).thenReturn(new Series[]{
new Series(1, "The walking dead", "USA", "Thriller"),
new Series(2, "Homeland", "USA", "Drama")});
when(seriesService.getSeries(1L)).thenReturn(new Series(1, "Fringe", "USA", "Thriller"));
}
@Test
public void getAllSeries() throws Exception {
mockMvc.perform(get(BASE_URI)
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(content().contentType("application/json;charset=UTF-8"))
.andExpect(jsonPath("$", hasSize(2)))
.andExpect(jsonPath("$[0].id", is(1)))
.andExpect(jsonPath("$[0].name", is("The walking dead")))
.andExpect(jsonPath("$[0].country", is("USA")))
.andExpect(jsonPath("$[0].genre", is("Thriller")))
.andExpect(jsonPath("$[1].id", is(2)))
.andExpect(jsonPath("$[1].name", is("Homeland")))
.andExpect(jsonPath("$[1].country", is("USA")))
.andExpect(jsonPath("$[1].genre", is("Drama")));
verify(seriesService, times(1)).getAllSeries();
verifyZeroInteractions(seriesService);
}
@Test
public void getJsonSeries() throws Exception {
mockMvc.perform(get(BASE_URI + "/{seriesId}", 1L)
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(content().contentType("application/json;charset=UTF-8"))
.andExpect(jsonPath("$.id", is(1)))
.andExpect(jsonPath("$.name", is("Fringe")))
.andExpect(jsonPath("$.country", is("USA")))
.andExpect(jsonPath("$.genre", is("Thriller")));
verify(seriesService, times(1)).getSeries(1L);
verifyZeroInteractions(seriesService);
}
@Test
public void getXmlSeries() throws Exception {
mockMvc.perform(get(BASE_URI + "/{seriesId}", 1L)
.accept(MediaType.APPLICATION_XML))
.andExpect(status().isOk())
.andExpect(content().contentType(MediaType.APPLICATION_XML))
.andExpect(xpath("/series/id").string("1"))
.andExpect(xpath("/series/name").string("Fringe"))
.andExpect(xpath("/series/country").string("USA"))
.andExpect(xpath("/series/genre").string("Thriller"));
verify(seriesService, times(1)).getSeries(1L);
verifyZeroInteractions(seriesService);
}
}
我正在展示一些已实施的测试。 检查SeriesIntegrationTesting以获得完整的实现。
功能测试
该应用程序使用RestTemplate类包含一些功能测试。 您需要部署Web应用程序才能对此进行测试。
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations={
"classpath:xpadro/spring/web/configuration/root-context.xml",
"classpath:xpadro/spring/web/configuration/app-context.xml"})
public class SeriesFunctionalTesting {
private static final String BASE_URI = "http://localhost:8080/spring-rest-api-v32/spring/series";
private RestTemplate restTemplate = new RestTemplate();
@Autowired
private MongoOperations mongoOps;
@Before
public void setup() {
List<HttpMessageConverter<?>> converters = new ArrayList<HttpMessageConverter<?>>();
converters.add(new StringHttpMessageConverter());
converters.add(new Jaxb2RootElementHttpMessageConverter());
converters.add(new MappingJacksonHttpMessageConverter());
restTemplate.setMessageConverters(converters);
initializeDatabase();
}
private void initializeDatabase() {
try {
mongoOps.dropCollection("series");
mongoOps.insert(new Series(1, "The walking dead", "USA", "Thriller"));
mongoOps.insert(new Series(2, "Homeland", "USA", "Drama"));
} catch (DataAccessResourceFailureException e) {
fail("MongoDB instance is not running");
}
}
@Test
public void getAllSeries() {
Series[] series = restTemplate.getForObject(BASE_URI, Series[].class);
assertNotNull(series);
assertEquals(2, series.length);
assertEquals(1L, series[0].getId());
assertEquals("The walking dead", series[0].getName());
assertEquals("USA", series[0].getCountry());
assertEquals("Thriller", series[0].getGenre());
assertEquals(2L, series[1].getId());
assertEquals("Homeland", series[1].getName());
assertEquals("USA", series[1].getCountry());
assertEquals("Drama", series[1].getGenre());
}
@Test
public void getJsonSeries() {
List<HttpMessageConverter<?>> converters = new ArrayList<HttpMessageConverter<?>>();
converters.add(new MappingJacksonHttpMessageConverter());
restTemplate.setMessageConverters(converters);
String uri = BASE_URI + "/{seriesId}";
ResponseEntity<Series> seriesEntity = restTemplate.getForEntity(uri, Series.class, 1l);
assertNotNull(seriesEntity.getBody());
assertEquals(1l, seriesEntity.getBody().getId());
assertEquals("The walking dead", seriesEntity.getBody().getName());
assertEquals("USA", seriesEntity.getBody().getCountry());
assertEquals("Thriller", seriesEntity.getBody().getGenre());
assertEquals(MediaType.parseMediaType("application/json;charset=UTF-8"), seriesEntity.getHeaders().getContentType());
}
@Test
public void getXmlSeries() {
String uri = BASE_URI + "/{seriesId}";
ResponseEntity<Series> seriesEntity = restTemplate.getForEntity(uri, Series.class, 1L);
assertNotNull(seriesEntity.getBody());
assertEquals(1l, seriesEntity.getBody().getId());
assertEquals("The walking dead", seriesEntity.getBody().getName());
assertEquals("USA", seriesEntity.getBody().getCountry());
assertEquals("Thriller", seriesEntity.getBody().getGenre());
assertEquals(MediaType.APPLICATION_XML, seriesEntity.getHeaders().getContentType());
}
}
就是这样,Web应用程序已经过测试并正在运行。 现在是时候迁移到Spring 4了。
3迁移到Spring 4
检查此页面以阅读有关从早期版本的Spring框架进行迁移的信息
3.1更改Maven依赖项
本节说明应修改哪些依赖项。 您可以在此处查看完整的pom.xml。
第一步是将Spring依赖版本从3.2.3.RELEASE更改为4.0.0.RELEASE:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>4.0.0.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>4.0.0.RELEASE</version>
</dependency>
下一步是更新到Servlet 3.0规范。 这一步很重要,因为某些Spring功能基于Servlet 3.0,因此将不可用。 事实上,试图执行SeriesIntegrationTesting将导致一个ClassNotFoundException由于这个原因,这也解释了这里 。
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
</dependency>
3.2更新Spring名称空间
不要忘记更改spring配置文件的名称空间:
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd
http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd
请查看第2节中链接的信息页面,因为有关mvc名称空间的某些更改。
3.3弃用杰克逊库
如果再次检查SeriesFunctionalTesting(设置方法),您会发现杰克逊转换器已被弃用。 如果您尝试运行测试,由于Jackson库中的方法更改,它将抛出NoSuchMethodError:
java.lang.NoSuchMethodError: org.codehaus.jackson.map.ObjectMapper.getTypeFactory()Lorg/codehaus/jackson/map/type/TypeFactory
在Spring4中,已不再支持Jackson 1.x,而支持Jackson v2。 让我们更改旧的依赖项:
<dependency>
<groupId>org.codehaus.jackson</groupId>
<artifactId>jackson-mapper-asl</artifactId>
<version>1.4.2</version>
</dependency>
对于这些:
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.3.0</version>
</dependency>
最后,如果要显式注册消息转换器,则需要为新版本更改不推荐使用的类:
//converters.add(new MappingJacksonHttpMessageConverter());
converters.add(new MappingJackson2HttpMessageConverter());
3.4迁移完成
迁移完成。 现在,您可以运行该应用程序并执行其测试。 下一节将回顾我在本文开头提到的一些改进。
4 Spring 4 Web改进
4.1 @ResponseBody和@RestController
如果您的REST API以JSON或XML格式提供内容,则某些API方法(用@RequestMapping注释)的返回类型将用@ResponseBody注释。 使用此注释,返回类型将包含在响应主体中。 在Spring4中,我们可以通过两种方式简化此过程:
用@ResponseBody注释控制器
现在可以在类型级别添加此注释。 这样,注释就被继承了,我们不必强迫将注释放在每个方法中。
@Controller
@ResponseBody
public class SeriesController {
用@RestController注释控制器
@RestController
public class SeriesController {
此注释甚至进一步简化了控制器。 如果我们检查此注释,我们将看到它本身已使用@Controller和@ResponseBody进行了注释:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Controller
@ResponseBody
public @interface RestController {
包括此注释将不会影响使用@ResponseEntity注释的方法。 处理程序适配器查找返回值处理程序的列表,以确定谁有能力处理响应。 的 在ResponseBody类型之前会先询问负责处理ResponseEntity返回类型的处理程序 ,因此,如果方法中存在ResponseEntity批注,则将使用该处理程序。
4.2异步调用
使用实用程序类RestTemplate调用RESTful服务将阻塞线程,直到它收到响应为止。 Spring 4包含AsyncRestTemplate以便执行异步调用。 现在,您可以拨打电话,继续进行其他计算,并在以后检索响应。
@Test
public void getAllSeriesAsync() throws InterruptedException, ExecutionException {
logger.info("Calling async /series");
Future<ResponseEntity<Series[]>> futureEntity = asyncRestTemplate.getForEntity(BASE_URI, Series[].class);
logger.info("Doing other async stuff...");
logger.info("Blocking to receive response...");
ResponseEntity<Series[]> entity = futureEntity.get();
logger.info("Response received");
Series[] series = entity.getBody();
assertNotNull(series);
assertEquals(2, series.length);
assertEquals(1L, series[0].getId());
assertEquals("The walking dead", series[0].getName());
assertEquals("USA", series[0].getCountry());
assertEquals("Thriller", series[0].getGenre());
assertEquals(2L, series[1].getId());
assertEquals("Homeland", series[1].getName());
assertEquals("USA", series[1].getCountry());
assertEquals("Drama", series[1].getGenre());
}
带回调的异步调用
尽管前面的示例进行了异步调用,但如果尚未发送响应,则尝试使用futureEntity.get()检索响应时,线程将阻塞。
AsyncRestTemplate返回ListenableFuture ,它扩展了Future并允许我们注册一个回调。 以下示例进行异步调用,并继续执行其自己的任务。 当服务返回响应时,它将由回调处理:
@Test
public void getAllSeriesAsyncCallable() throws InterruptedException, ExecutionException {
logger.info("Calling async callable /series");
ListenableFuture<ResponseEntity<Series[]>> futureEntity = asyncRestTemplate.getForEntity(BASE_URI, Series[].class);
futureEntity.addCallback(new ListenableFutureCallback<ResponseEntity<Series[]>>() {
@Override
public void onSuccess(ResponseEntity<Series[]> entity) {
logger.info("Response received (async callable)");
Series[] series = entity.getBody();
validateList(series);
}
@Override
public void onFailure(Throwable t) {
fail();
}
});
logger.info("Doing other async callable stuff ...");
Thread.sleep(6000); //waits for the service to send the response
}
5结论
我们使用了Spring 3.2.x Web应用程序,并将其迁移到Spring 4.0.0的新版本中。 我们还回顾了可以应用于Spring 4 Web应用程序的一些改进。
我正在Google Plus和Twitter上发布我的新帖子。 如果您要更新新内容,请关注我。
参考:在XavierPadró的Blog博客上,从我们的JCG合作伙伴 Xavier Padro将Spring MVC RESTful Web服务迁移到Spring 4 。
翻译自: https://www.javacodegeeks.com/2014/02/migrating-spring-mvc-restful-web-services-to-spring-4.html