前言
學習了
Spring的注解、AOP后,接着學習Spring Web,對於Web應用開發,Spring提供了Web框架。
Web應用
Spring MVC初探
MVC為(Model-View-Control),當用戶在瀏覽器中點擊鏈接或提交表單時,請求經歷的流程大致如下。

Spring MVC所有的請求都會通過一個前端控制器(front controller servlet),也即是DispatcherServlet,DispatcherServlet用於將請求發送給Spring MVC控制器,而處理器映射會根據url信息確定將請求發送給哪個控制器。- 當請求發送給控制器后,請求會等待控制器處理信息,更好的設計是控制器將請求交給服務對象處理。
- 控制器處理完后,會產生信息,該信息需要返回給用戶並在瀏覽器上顯示,這些信息稱為
模型(Model)。同時,這些信息需要以用戶友好的方式進行格式化,此時需要將信息發送給一個視圖(View)進行處理。 DispatcherServlet拿到模型及邏輯視圖名后會使用視圖解析器進行解析,將其轉化為特定視圖實現。- 指定視圖實現后,需要將模型數據數據交付,視圖將使用模型數據渲染輸出。
配置DispatcherServlet
DispatcherServlet是Spring MVC的核心,其負責將請求路由到其他組件中。可通過將Servlet配置在web.xml中或者使用Java顯示編碼方式將DispatcherServlet配置在Servlet容器中,本例中設置的SpittrWebAppInitializer
package ch5;
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;
public class SpitterWebInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class<?>[] { RootConfig.class };
}
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class<?>[] { WebConfig.class };
}
@Override
protected String[] getServletMappings() {
return new String[] { "/" };
}
}
其中
getServletMappings方法會將一個或多個路徑映射到DispatcherServlet上,/表示它是默認的Servlet,將會處理所有進入應用的請求。
當DispatcherServlet啟動時,會創建Spring應用上下文,並加載配置文件或配置類中所聲明的bean,如上述getServletConfigClasses方法中返回的帶有@Configuration注解的所有定義在WebConfig中的bean。與此同時,在Spring Web應用中,還會有另一個由ContextLoaderListener創建的應用上下文,如getRootConfigClasses方法中返回的帶有@Configuration注解的所有定義在RootConfig中的bean。AbstractAnnotationConfigDispatcherServletInitializer會同時創建DispatcherServlet和ContextLoaderListener兩個應用上下文,DispatcherServlet應用上下文加載Web組件的bean,如控制器、視圖解析器、處理映射器;ContextLoaderListener應用上下文加載其他bean,如驅動應用后端的中間層和數據層組件。
配置Web組件並啟動Spring MVC
可使用
<mvc:annotation-driven>啟動注解啟動的Spring MVC,也可使用@EnableWebMvc注解啟動。WebConfig對應Web組件配置,其代碼如下。
package ch5;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.springframework.web.servlet.view.InternalResourceViewResolver;
@Configuration
@EnableWebMvc
@ComponentScan("ch5")
public class WebConfig extends WebMvcConfigurerAdapter {
@Bean
public ViewResolver viewResolver() {
InternalResourceViewResolver resolver = new InternalResourceViewResolver();
resolver.setPrefix("/WEB-INF/views/");
resolver.setSuffix(".jsp");
return resolver;
}
@Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
configurer.enable();
}
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
super.addResourceHandlers(registry);
}
}
其中配置了
JSP視圖解析器,也配置靜態資源處理器(對靜態資源的請求轉發到Servlet容器中默認的Servlet,而不是使用DispatcherServlet處理)。
配置非Web組件
RootConfig對應非Web組件配置,其源碼如下。
package ch5;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.ComponentScan.Filter;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
@Configuration
@ComponentScan(basePackages = {"ch5"},
excludeFilters = {
@Filter(type = FilterType.ANNOTATION, value = EnableWebMvc.class)
})
public class RootConfig {
}
通過
RootConfig的ComponentScan注解可以添加很多非Web組件,如驅動應用后端的中間層和數據層組件等。
控制器
控制器只是在方法上添加了
@RequestMapping注解的類,該注解聲明了它們所要處理的請求,如HomeController用來處理對/的請求,其源碼如下。
package ch5;
import static org.springframework.web.bind.annotation.RequestMethod.*;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
public class HomeController {
@RequestMapping(value = "/", method = GET)
public String home() {
return "home";
}
}
至此,已經完成所有編碼,可以啟動該項目,可正確在瀏覽器中顯示頁面。
測試控制器
Spring提供了mock Spring MVC來測試控制器HTTP請求,這樣測試時就不用啟動瀏覽器了。HomeControllerTest源碼如下。
package ch5;
import org.junit.Test;
import org.springframework.test.web.servlet.MockMvc;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view;
import static org.springframework.test.web.servlet.setup.MockMvcBuilders.standaloneSetup;
public class HomeControllerTest {
@Test
public void testHomePage() throws Exception {
HomeController controller = new HomeController();
MockMvc mockMvc = standaloneSetup(controller).build();
mockMvc.perform(get("/")).andExpect(view().name("home"));
}
}
運行可順利通過測試。
定義類級別的請求處理
在前面的
HomeController中,對於處理的請求路徑是直接配置在方法上的,更好的處理是將其配置在類上,如下所示。
package ch5;
import static org.springframework.web.bind.annotation.RequestMethod.*;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
@RequestMapping("/")
public class HomeController {
@RequestMapping(method = GET)
public String home() {
return "home";
}
}
上述配置在類上與前面配置在方法上的效果相同,還可配置多個路徑,如下所示。
package ch5;
import static org.springframework.web.bind.annotation.RequestMethod.*;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
@RequestMapping({"/", "/homepage"})
public class HomeController {
@RequestMapping(method = GET)
public String home() {
return "home";
}
}
上述代碼中除了配置處理路徑
/外,還可處理/homepage路徑。
傳遞模型數據至視圖
一般情況下,需要傳遞模型數據至視圖中進行渲染,此時,定義接口
SpittleRepository。
package ch5;
import java.util.List;
public interface SpittleRepository {
List<Spittle> findSpittles(long max, int count);
}
定義接口
SpittleRepository的實現子類SpittoleRepositoryImp。
package ch5;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
@Component
public class SpittleRepositoryImp implements SpittleRepository {
public List<Spittle> findSpittles(long max, int count) {
List<Spittle> spittles = new ArrayList<Spittle>();
for (int i = 0; i < count; i++) {
spittles.add(new Spittle("Spittle " + i, new Date()));
}
return spittles;
}
}
定義
POJO類Spittle。
package ch5;
import java.util.Date;
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
public class Spittle {
private final Long id;
private final String message;
private final Date time;
private Double latitude;
private Double longitude;
public Spittle(String message, Date time) {
this(null, message, time, null, null);
}
public Spittle(Long id, String message, Date time, Double longitude, Double latitude) {
this.id = id;
this.message = message;
this.time = time;
this.longitude = longitude;
this.latitude = latitude;
}
public long getId() {
return id;
}
public String getMessage() {
return message;
}
public Date getTime() {
return time;
}
public Double getLongitude() {
return longitude;
}
public Double getLatitude() {
return latitude;
}
@Override
public boolean equals(Object that) {
return EqualsBuilder.reflectionEquals(this, that, "id", "time");
}
@Override
public int hashCode() {
return HashCodeBuilder.reflectionHashCode(this, "id", "time");
}
}
在
/WEB-INF/views目錄下創建spittles.jsp文件,內容如下。
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib prefix="s" uri="http://www.springframework.org/tags"%>
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
<%@ page isELIgnored="false" %>
<html>
<head>
<title>Spitter</title>
<link rel="stylesheet" type="text/css" href="<c:url value="/resources/style.css" />" >
</head>
<body>
<div class="listTitle">
<h1>Recent Spittles</h1>
<ul class="spittleList">
<c:forEach items="${spittleList}" var="spittle" >
<li id="spittle_<c:out value="spittle.id"/>">
<div class="spittleMessage"><c:out value="${spittle.message}" /></div>
<div>
<span class="spittleTime"><c:out value="${spittle.time}" /></span>
<span class="spittleLocation">(<c:out value="${spittle.latitude}" />, <c:out value="${spittle.longitude}" />)</span>
</div>
</li>
</c:forEach>
</ul>
</div>
</body>
</html>
添加
SpittleController控制器。
package ch5;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
@Controller
@RequestMapping("/spittles")
public class SpittleController {
private SpittleRepository spittleRepository;
@Autowired
public SpittleController(SpittleRepository spittleRepository) {
this.spittleRepository = spittleRepository;
}
@RequestMapping(method = RequestMethod.GET)
public String spittles(Model model) {
model.addAttribute("spittleList", spittleRepository.findSpittles(Long.MAX_VALUE, 20));
return "spittles";
}
}
運行,可正確顯示20個
Spittle實例信息。
接受請求的參數
Spring MVC允許以多種方式將客戶端的數據傳送到控制器的處理器方法中,包括查詢參數、表單參數、路徑變量。
處理查詢參數
可讓用戶指定
findSpittles方法中的max和count兩個參數,並且在未指定時使用缺省值,修改SpittleController如下。
package ch5;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import java.util.List;
@Controller
@RequestMapping("/spittles")
public class SpittleController {
private static final String MAX_LONG_AS_STRING = "9223372036854775807";
private SpittleRepository spittleRepository;
@Autowired
public SpittleController(SpittleRepository spittleRepository) {
this.spittleRepository = spittleRepository;
}
@RequestMapping(method = RequestMethod.GET)
public List<Spittle> spittles(
@RequestParam(value = "max", defaultValue = MAX_LONG_AS_STRING) long max,
@RequestParam(value = "count", defaultValue = "20") int count) {
return spittleRepository.findSpittles(max, count);
}
}
值得注意的是,此時並沒有指定視圖,但是啟動后仍然可以正確顯示結果,這是由於視圖未指定情況下與
@RequestMapping("/spittles")的spittles相同,若換成其他路徑,如/spittles_test則報無法找到**/spittles_test.jsp的錯誤。
處理路徑參數
使用
/spittles?show?spittle_id=123的方式可以傳遞參數,但是更好的一種方法是使用/spittles/123方式請求,該方式優於前種方式。修改SpittleController如下。
package ch5;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
@Controller
@RequestMapping("/spittles")
public class SpittleController {
private SpittleRepository spittleRepository;
@Autowired
public SpittleController(SpittleRepository spittleRepository) {
this.spittleRepository = spittleRepository;
}
@RequestMapping(value = "/{spittleId}", method = RequestMethod.GET)
public String spittles(
@PathVariable("spittleId") long spittleId, Model model) {
System.out.println("spittleId = " + spittleId);
System.out.println(spittleRepository.findOne(spittleId).getMessage());
model.addAttribute(spittleRepository.findOne(spittleId));
return "spittle";
}
}
再添加
spittle.jsp頁面,其源碼如下。
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib prefix="s" uri="http://www.springframework.org/tags"%>
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
<%@ page isELIgnored="false" %>
<html>
<head>
<title>Spitter</title>
<link rel="stylesheet"
type="text/css"
href="<c:url value="/resources/style.css" />">
</head>
<body>
<div class="spittleView">
<div class="spittleMessage"><c:out value="${spittle.message}"/></div>
<div>
<span class="spittleTime"><c:out value="${spittle.time}"/></span>
</div>
</div>
</body>
</html>
運行即可得到正確結果。
處理表單
Web應用需要通過表單與用戶進行交互,需要展示表單數據和處理用戶通過表單提交的數據。對於表單的展示。
- 添加
SpitterController如下
package ch5;
import static org.springframework.web.bind.annotation.RequestMethod.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
@RequestMapping("/spitter")
public class SpitterController {
private SpitterRepository spitterRepository;
@Autowired
public SpitterController(SpitterRepository spitterRepository) {
this.spitterRepository = spitterRepository;
}
@RequestMapping(value = "/register", method = GET)
public String showRegistrationForm() {
return "registerForm";
}
@RequestMapping(value = "/register", method = POST)
public String processRegistration(Spitter spitter) {
spitterRepository.save(spitter);
return "redirect:/spitter/" + spitter.getUsername();
}
@RequestMapping(value = "/{username}", method = GET)
public String showSpitterProfile(@PathVariable String username, Model model) {
Spitter spitter = spitterRepository.findByUsername(username);
model.addAttribute(spitter);
return "profile";
}
}
- 添加
SpitterRepository如下
package ch5;
public interface SpitterRepository {
Spitter findByUsername(String username);
void save(Spitter spitter);
}
- 添加
SpitterRepositoryImp,源碼如下。
package ch5;
import org.springframework.stereotype.Component;
import java.util.*;
@Component
public class SpitterRepositoryImp implements SpitterRepository {
Map<String, Spitter> spitters = new HashMap<String, Spitter>();
public void save(Spitter spitter) {
spitters.put(spitter.getUsername(), spitter);
}
public Spitter findByUsername(String username) {
return spitters.get(username);
}
}
- 添加
registerForm.jsp文件,內容如下。
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ page isELIgnored="false" %>
<html>
<head>
<title>Spitter</title>
<link rel="stylesheet" type="text/css"
href="<c:url value="/resources/style.css" />" >
</head>
<body>
<h1>Register</h1>
<form method="POST">
First Name: <input type="text" name="firstName" /><br/>
Last Name: <input type="text" name="lastName" /><br/>
Email: <input type="email" name="email" /><br/>
Username: <input type="text" name="username" /><br/>
Password: <input type="password" name="password" /><br/>
<input type="submit" value="Register" />
</form>
</body>
</html>
運行后,訪問
http://localhost:8080/spitter/register即可正常顯示表單。
處理表單控制器
當成注冊表單的
POST請求時,控制器需要接受表單數據並將表單數據保存為Spitter對象,在注冊完成后重定向至用戶的基本信息頁面,修改SpitterController如下
package ch5;
import static org.springframework.web.bind.annotation.RequestMethod.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
@RequestMapping("/spitter")
public class SpitterController {
private SpitterRepository spitterRepository;
@Autowired
public SpitterController(SpitterRepository spitterRepository) {
this.spitterRepository = spitterRepository;
}
@RequestMapping(value = "/register", method = GET)
public String showRegistrationForm() {
return "registerForm";
}
@RequestMapping(value = "/register", method = POST)
public String processRegistration(Spitter spitter) {
spitterRepository.save(spitter);
return "redirect:/spitter/" + spitter.getUsername();
}
@RequestMapping(value = "/{username}", method = GET)
public String showSpitterProfile(@PathVariable String username, Model model) {
Spitter spitter = spitterRepository.findByUsername(username);
model.addAttribute(spitter);
return "profile";
}
}
添加
profile.jsp文件,內容如下
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ page isELIgnored="false" %>
<html>
<head>
<title>Spitter</title>
<link rel="stylesheet" type="text/css" href="<c:url value="/resources/style.css" />">
</head>
<body>
<h1>Your Profile</h1>
<c:out value="${spitter.username}"/><br/>
<c:out value="${spitter.firstName}"/> <c:out value="${spitter.lastName}"/><br/>
<c:out value="${spitter.email}"/>
</body>
</html>
運行后,訪問
http://localhost:8080/spitter/register完成注冊后會成功返回到用戶信息頁面。
總結
本篇博文講解了
Spring Web相關的知識點,其核心是DispatcherServlet來派發請求,借助框架,可以快速開發Web應用。
