翻译自 http://spring.io/guides/tutorials/web/3/。
首发于 http://my.oschina.net/u/179755/blog/260918。
现在我们已经编写和测试了一个Web控制器,并很自豪地添加到我们的救生圈,如下图所示。现在是时候将整个应用程序搭建起来了。
第3步:配置一个基本的应用程序
此刻我们已经做好了如下准备:
1. 配置我们的应用程序核心
2. 配置我们的Web组件
3. 初始化我们的基础结构来创建可以工作的WAR文件
4. 在Web容器中执行我们的Web应用程序
为了完成这些工作,我们需要一个新域,配置域。
使用Spring JavaConfig为我们应用程序核心和持久域创建配置
Yummy面馆应用程序包含了一个核心组件集(域类和服务)。它也包含了一个跟核心整合在一起的内存持久存储。
我们可以为这些组件创建配置;但是,就如前面步骤所做的,我们使用测试驱动方法来构建我们的配置。
测试我们的核心和持久配置
首先,构建集成测试如下。
src/test/java/com/yummynoodlebar/config/CoreDomainIntegraionTest.java
package com.yummynoodlebar.config;
import com.yummynoodlebar.core.services.MenuService; import com.yummynoodlebar.core.services.OrderService; import com.yummynoodlebar.events.menu.AllMenuItemsEvent; import com.yummynoodlebar.events.menu.RequestAllMenuItemsEvent; import com.yummynoodlebar.events.orders.AllOrdersEvent; import com.yummynoodlebar.events.orders.CreateOrderEvent; import com.yummynoodlebar.events.orders.OrderDetails; import com.yummynoodlebar.events.orders.RequestAllOrdersEvent; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import static junit.framework.TestCase.assertEquals;
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = {PersistenceConfig.class, CoreConfig.class}) public class CoreDomainIntegrationTest {
@Autowired MenuService menuService;
@Autowired OrderService orderService;
@Test public void thatAllMenuItemsReturned() {
AllMenuItemsEvent allMenuItems = menuService.requestAllMenuItems(new RequestAllMenuItemsEvent());
assertEquals(3, allMenuItems.getMenuItemDetails().size());
}
@Test public void addANewOrderToTheSystem() {
CreateOrderEvent ev = new CreateOrderEvent(new OrderDetails());
orderService.createOrder(ev);
AllOrdersEvent allOrders = orderService.requestAllOrders(new RequestAllOrdersEvent());
assertEquals(1, allOrders.getOrdersDetails().size()); }
}
|
这个集成测试使用JavaConfig通过标注@ContextConfiguration构建了一个ApplicationContext。核心域配置使用CoreConfig创建。持久域配置使用PersistenceConfig创建。
当ApplicationContext构建后,这个测试可以自动注入MenuService和OrderService,为测试方法做好准备。
最后,我们有两个测试方法来断言menuServie和orderService依赖已经被提供,并且正确工作。
接下来,我们来创建核心和持久域配置。
实现我们的核心域配置
Yummy面馆应用程序的核心域配置只包含两个服务。它依赖即将配置的持久域来提供依赖。
下面的代码显示了完整的配置类:
src/main/java/com/yummynoodlebar/config/CoreConfig.java
package com.yummynoodlebar.config;
import com.yummynoodlebar.core.services.OrderEventHandler; import com.yummynoodlebar.core.services.OrderService; import com.yummynoodlebar.persistence.services.OrderPersistenceService; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration;
import com.yummynoodlebar.core.services.MenuEventHandler; import com.yummynoodlebar.core.services.MenuService; import com.yummynoodlebar.persistence.services.MenuPersistenceService;
@Configuration public class CoreConfig { @Bean public MenuService menuService(MenuPersistenceService menuPersistenceService) { return new MenuEventHandler(menuPersistenceService); } @Bean public OrderService orderService(OrderPersistenceService orderPersistenceService) { return new OrderEventHandler(orderPersistenceService); }
}
|
核心事件处理器将分发事件给持久域来进行真实的持久化。目前我们使用了内存库包装的HashMaps。
src/main/java/com/yummynoodlebar/config/PersistenceConfig.java
package com.yummynoodlebar.config;
import com.yummynoodlebar.persistence.domain.MenuItem; import com.yummynoodlebar.persistence.domain.Order; import com.yummynoodlebar.persistence.repository.*; import com.yummynoodlebar.persistence.services.MenuPersistenceEventHandler; import com.yummynoodlebar.persistence.services.MenuPersistenceService; import com.yummynoodlebar.persistence.services.OrderPersistenceEventHandler; import com.yummynoodlebar.persistence.services.OrderPersistenceService; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration;
import java.math.BigDecimal; import java.util.HashMap; import java.util.Map; import java.util.UUID;
@Configuration public class PersistenceConfig {
@Bean public OrdersRepository ordersRepo() { return new OrdersMemoryRepository(new HashMap<UUID, Order>()); }
@Bean public OrderStatusRepository orderStatusRepo() { return new OrderStatusMemoryRepository(); }
@Bean public OrderPersistenceService orderPersistenceService() { return new OrderPersistenceEventHandler(ordersRepo(), orderStatusRepo()); }
@Bean public MenuItemRepository menuItemRepository() { return new MenuItemMemoryRepository(defaultMenu()); }
@Bean public MenuPersistenceService menuPersistenceService(MenuItemRepository menuItemRepository) { return new MenuPersistenceEventHandler(menuItemRepository); }
private Map<String, MenuItem> defaultMenu() { Map<String, MenuItem> items = new HashMap<String, MenuItem>(); items.put("YM1", menuItem("YM1", new BigDecimal("1.99"), 11, "Yummy Noodles")); items.put("YM2", menuItem("YM2", new BigDecimal("2.99"), 12, "Special Yummy Noodles")); items.put("YM3", menuItem("YM3", new BigDecimal("3.99"), 13, "Low cal Yummy Noodles")); return items; }
private MenuItem menuItem(String id, BigDecimal cost, int minutesToPrepare, String name) { MenuItem item = new MenuItem(); item.setId(id); item.setCost(cost); item.setMinutesToPrepare(minutesToPrepare); item.setName(name); return item; }
}
|
Spring JavaConfig将会检查每一个@Bean 注解的方法作为产生Spring Bean的方法。
执行com.yummynoodlebar.config测试包中的CoreDomainIntegrationTest,将验证我们的核心域配置是可以工作的。
为我们的Web组件创建配置
配置我们新的控制器是相当直白的,因为我们已经在每个控制器类上使用@Controller 。
为了初始化我们的Web域组件,我们只需要打开组件扫描,这样Spring就可以找到和初始化这些Spring Bean。
实现我们的Web域配置
我们可以创建如下Spring JavaConfig来执行组件扫描。
src/main/java/com/yummynoodlebar/config/WebCofig.java
package com.yummynoodlebar.config;
import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.EnableWebMvc;
@Configuration @EnableWebMvc @ComponentScan(basePackages = {"com.yummynoodlebar.web.controller"}) public class WebConfig {
}
|
JavaConfig中的@ComponentScan属性指明我们的组件可以在com.yummynoodlebar.web.controllers下找到。
注意:当我们定义组件扫描的位置时,越具体越好,这样我们才不会意外初始化我们不需要的组件。
测试我们的Web域配置
只有通过相关的测试,我们才可以相信这些配置。下面的测试保证了Web配置正确输出。
src/test/java/com/yummynoodlebar/config/WebDomainIntegrationTest.java
package com.yummynoodlebar.config;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; import static org.hamcrest.Matchers.*;
import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext;
@RunWith(SpringJUnit4ClassRunner.class) @WebAppConfiguration @ContextConfiguration(classes = { PersistenceConfig.class, CoreConfig.class, WebConfig.class }) public class WebDomainIntegrationTest {
private static final String STANDARD = "Yummy Noodles"; private static final String CHEF_SPECIAL = "Special Yummy Noodles"; private static final String LOW_CAL = "Low cal Yummy Noodles";
private MockMvc mockMvc;
@Autowired WebApplicationContext webApplicationContext;
@Before public void setup() { mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build(); }
@Test public void thatTextReturned() throws Exception { mockMvc.perform(get("/")) .andDo(print()) .andExpect(content().string(containsString(STANDARD))) .andExpect(content().string(containsString(CHEF_SPECIAL))) .andExpect(content().string(containsString(LOW_CAL)));
}
}
|
我们已经断言了控制器和核心域的服务组件之间的协作的正确性。
这个测试确保一旦所有的对象被注入后,WebConfig的注入正确,对应的控制器就位。
这个测试通过模拟执行处理器映射的请求处理来验证WebConfig。完整的答复也确认正确。更多的测试可以执行,最重要的是我们在前面的步骤已经断言控制器可以正常运行。这个测试简单的表明我们已经正确地通过Spring JavaConfig配置了这些组件。
初始化Web服务网页基础结构
从Spring3.2开始,如果我们使用了支持Servlet3的容器,例如Tomcat7+,我们可以不用写哪怕是一行xml来初始化整个我网页基础结构。
我们准备用WebApplicationInitializer来设置我们的应用程序的Web应用上下文参数,快速搭建我们的应用程序网页基础结构。
首先我们通过类com.yummynoodlebar.config.WebAppInitializer创建一段配置,这个类从AbstractAnnotationConfigDispatcherServletInitializer。
src/main/java/com/yummynoodlebar/config/WebAppInitializer.java
public class WebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
|
接下来我们重载getRootConfigClasses方法来提供一类Spring配置类来构建根应用上下文。这个上下文将被整个应用程序共享,包括Servlets,Filters和Context Listeners。它包含了我们的主要组件,包括核心和持久域。
src/main/java/com/yummynoodlebar/config/WebAppInitializer.java
@Override protected Class<?>[] getRootConfigClasses() { return new Class<?>[] { PersistenceConfig.class, CoreConfig.class }; }
|
在根应用上下文初始化后,重载getServletConfigClasses。同样返回一系列Spring配置类,这里只返回一个。
src/main/java/com/yummynoodlebar/config/WebAppInitializer.java
@Override protected Class<?>[] getServletConfigClasses() { return new Class<?>[] { WebConfig.class }; }
|
最后,增加一些额外的配置来映射Servlet URL context和增加一个标准的filter。
src/main/java/com/yummynoodlebar/config/WebAppInitializer.java
@Override protected String[] getServletMappings() { return new String[] { "/" }; }
@Override protected Filter[] getServletFilters() {
CharacterEncodingFilter characterEncodingFilter = new CharacterEncodingFilter(); characterEncodingFilter.setEncoding("UTF-8"); return new Filter[] { characterEncodingFilter}; }
|
AbstractAnnotationConfigDispatcherServletInitializer完成Spring DispatcherServlet和ContextLoader的建立,这些是Spring Web应用程序的标准部分。
DispatcherServlet是前控制器Servlet,它接收所有请求,这些请求将被多个注册的控制器处理。它还负责将这些请求派给适当的控制器方法来处理。
整个WebAppInitializer源代码如下:
src/main/java/com/yummynoodlebar/config/WebAppInitializer.java
package com.yummynoodlebar.config;
import org.springframework.web.filter.CharacterEncodingFilter; import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;
import javax.servlet.Filter;
public class WebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override protected Class<?>[] getRootConfigClasses() { return new Class<?>[] { PersistenceConfig.class, CoreConfig.class }; }
@Override protected Class<?>[] getServletConfigClasses() { return new Class<?>[] { WebConfig.class }; }
@Override protected String[] getServletMappings() { return new String[] { "/" }; }
@Override protected Filter[] getServletFilters() {
CharacterEncodingFilter characterEncodingFilter = new CharacterEncodingFilter(); characterEncodingFilter.setEncoding("UTF-8"); return new Filter[] { characterEncodingFilter}; } }
|
在Web容器中运行我们的Web服务
我们能否执行我们的Web应用?
首先告诉Gradle我们将使用Tomcat。更新我们的build.gradle文件。
build.gradle
apply plugin: 'war' apply plugin: 'tomcat' apply plugin: 'java' apply plugin: 'propdeps' apply plugin: 'propdeps-maven' apply plugin: 'propdeps-idea' apply plugin: 'propdeps-eclipse' apply plugin: 'eclipse-wtp' apply plugin: 'idea'
println "PROJECT=" + project.name
buildscript { repositories { mavenCentral() maven { url "http://download.java.net/maven/2" } maven { url 'http://repo.spring.io/plugins-release' } }
dependencies { classpath 'org.gradle.api.plugins:gradle-tomcat-plugin:0.9.8' classpath 'org.springframework.build.gradle:propdeps-plugin:0.0.1' } }
repositories { mavenCentral() }
dependencies { def tomcatVersion = '7.0.42' tomcat "org.apache.tomcat.embed:tomcat-embed-core:${tomcatVersion}", "org.apache.tomcat.embed:tomcat-embed-logging-juli:${tomcatVersion}" tomcat("org.apache.tomcat.embed:tomcat-embed-jasper:${tomcatVersion}") { exclude group: 'org.eclipse.jdt.core.compiler', module: 'ecj' }
compile 'org.springframework:spring-core:3.2.3.RELEASE' compile 'org.springframework:spring-webmvc:3.2.3.RELEASE'
compile 'org.slf4j:slf4j-api:1.7.5' runtime 'org.slf4j:slf4j-log4j12:1.7.5'
testCompile 'org.springframework:spring-test:3.2.3.RELEASE'
testCompile 'junit:junit:4.11' testCompile "org.mockito:mockito-all:1.9.5" testCompile "org.hamcrest:hamcrest-library:1.3"
provided 'javax.servlet:javax.servlet-api:3.0.1' }
test { testLogging { // Show that tests are run in the command-line output events 'started', 'passed' } }
task wrapper(type: Wrapper) { gradleVersion = '1.6' }
tomcatRunWar.contextPath = ''
|
我们也许留意到build文件的最后一行保证应用在根上下文运行。
tomcatRunWar.contextPath = ''
|
现在我们可以运行网页了,端口默认8080.
$ ./gradlew tomcatRunWar
|
如果我们访问http://localhost:8080/,我们将得到文本回应,这个回应正是PersitenceConfig中返回的初始菜单。
Yummy Noodles,Special Yummy Noodles,Low cal Yummy Noodles
|
如果我们想运行于不同的端口或者其他配置,可以参考https://github.com/bmuschko/gradle-tomcat-plugin/。
总结
我们走过了很长的路,现在我们得到了一个完整配置的Web前端,它运行在Tomcat,可以打包成War来分发。
我们在配置域中增加了三个新组建,CoreConfig,PersistenceConfig和WebConfig:
整体看起来:
我们的Web前端并不美,甚至没有什么功能。在后面的教程,我们将美化它,增加更多的功能。
下一步:用Thymeleaf创建好看的HTML视图。