本文主要依據《Spring實戰》第五章內容進行總結
Spring MVC框架是基於模型-視圖-控制器(Model-View-Controller,MVC)模式實現,它能夠構建像Spring框架那樣靈活和鬆耦合的Web應用。
1、Spring MVC起步
1.1、Spring MVC如何處理客戶端請求
Spring MVC處理客戶端請求的過程可以參考如下所示的圖示:
具體步驟如下:
- 客戶端請求離開瀏覽器時①,會帶有用戶請求內容的信息,至少會包含請求的URL,但是還可能帶有其他的信息,例如用戶提交的表單信息;
- 請求會傳遞給Spring的DispatcherServlet,DispatcherServlet是一個前端控制器,主要負責將請求委託給應用程序的其他組件來執行實際的處理;
- DispatcherServlet要將請求發送給Spring MVC控制器(controller),控制器是一個用於處理請求的Spring組件,DispatcherServlet要確定將請求發送給哪個控制器,所以DispatcherServlet會查詢一個或多個處理器映射(handler mapping)②,處理器映射根據請求的URL信息進行決策;
- 一旦選擇了合適的控制器,DispatcherServlet會將請求發送給選中的控制器③,由控制器進行業務邏輯的處理;
- 控制器在完成邏輯處理後,通常會產生一些信息,這些信息需要返回給用戶並在瀏覽器上顯示,這些信息被稱爲模型(model),控制器要將模型數據打包,並且標示出用於渲染輸出的視圖名,然後將模型及視圖名發送回DispatcherServlet④;
- 傳遞給DispatcherServlet的視圖名不一定是真實對應的視圖名稱,可能是一個邏輯視圖名,DispatcherServlet將會使用視圖解析器(view resolver)⑤來將邏輯試圖名匹配一個特定的試圖實現;
- 請求最後到達真實視圖⑥,在這裏它交付模型數據,請求的任務也就完成了;
- 視圖將使用模型數據渲染輸出,這個輸出會通過響應對象傳遞給客戶端⑦。
1.2、搭建Spring MVC
1.2.1、配置DispatcherServlet
DispatcherServlet是Spring MVC的核心,它主要負責將請求路由到其它的組件之中,所以配置Spring MVC的第一步就是配置DispathcerServlet。
按照傳統的Web框架,Servlet一般都是在web.xml中進行配置的,但在Servlet 3規範之後,我們可以通過Java代碼的方式配置,只需要將Java類實現javax.servlet.ServletContainerInitializer接口即可,在Servlet 3.0環境中,容器會在類路徑中查找實現javax.servlet.ServletContainerInitializer接口的類,如果能發現的話,就會用它來配置Servlet容器。Spring提供了這個接口的實現SpringServletContainerInitializer,這個類又會反過來查找實現WebApplicationInitializer的類。在這裏我們創建一個配置類SpringMvcInitializer:
public class SpringMvcInitializer 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[] {"/"};
}
}
在這裏SpringMvcInitializer類擴展了AbstractAnnotationConfigDispatcherServletInitializer,而AbstractAnnotationConfigDispatcherServletInitializer實現了WebApplicationIntializer,所以部署到Servlet 3.0容器中的時候,容器就會自動發現它,並用它來配置Servlet上下文,在後面的章節,我們將會介紹如何使用web.xml配置DispathcerServlet。
我們可以看到,SpringMvcInitializer重寫了三個方法,其中getServletMappings()方法會將一個或多個路徑映射到DispatcherServlet上,在這裏,它映射的是“/”,這表示它會是應用的默認Servlet,它會處理進入應用的所有請求。
1.2.2、兩個應用上下文
我們可以看到,上面的SpringMvcInitializer配置類中除了getServletMappings()方法之外還有兩個方法,這兩個方法都是配置Spring應用上下文的。
當DispathcherServlet啓動的時候,它會創建Spring應用上下文,並加載配置文件或配置類中聲明的bean,在getServletConfigClasses()方法中,我們要求DispatcherServlet加載應用上下文時,使用定義在WebConfig配置類中的bean。
但是在Spring Web應用中,通常還會有另外一個應用上下文,另外的這個應用上下文就是由ContextLoaderListener創建的。
實際上,AbstractAnnotationConfigDispatcherServletInitializer會同時創建DispatcherServlet和ContextLoaderListener,getServletConfigClasses()方法返回的帶有@Configuration註解的類將會用來定義DispatcherServlet應用上下文中的bean,getRootConfigClasses()方法返回的帶有@Configuration註解的類將會用來配置ContextLoaderListener創建的應用上下文的bean。
通常情況下,DispathcerServlet加載包含Web組件的bean,如控制器、視圖解析器以及處理器映射,而ContextLoaderListener要加載應用中的其他bean,這些bean通常是驅動應用後端的中間層和數據層組件。
1.2.3、啓用Spring MVC
配置好DispatcherServlet之後,我們需要創建Spring MVC的配置WebConfig,下面示例是最簡單的Spring MVC配置:
@Configuration
@EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter{
}
最簡單的Spring MVC配置就是一個帶有@EnableWebMVC註解的類,它可以啓用Spring MVC,但是它還有不少的問題需要解決:
- 沒有配置視圖解析器,這樣的話,Spring會默認使用BeanNameViewResolver;
- 沒有啓用組件掃描,這樣的話,Spring只能找到顯式聲明在配置類中的控制器;
- Dispatcher會映射爲應用的默認Servlet,它會處理所有的請求,包括靜態資源的請求。
所以我們需要稍微調整一下上面的配置:
@Configuration
@EnableWebMvc
@ComponentScan("web")
public class WebConfig extends WebMvcConfigurerAdapter{
@Bean
public ViewResolver viewResolver() {
InternalResourceViewResolver resolver = new InternalResourceViewResolver();
resolver.setPrefix("/WEB-INF/views/");
resolver.setSuffix(".jsp");
resolver.setExposeContextBeansAsAttributes(true);
return resolver;
}
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configure) {
configure.enable();
}
}
可以看到,在這個配置類中,我們使用@EnableWebMvc啓用Spring MVC,接着我們使用@ComponentScan啓用組件掃描,它會查找web包下的組件,然後我們添加了一個ViewResolver視圖解析器,在這裏,我們使用的是InternalResourceViewResolver,它會查找JSP文件,查找的時候,它會在視圖名稱上加一個特定的前綴和後綴,例如,名爲home的視圖將會解析爲/WEB-INF/views/home.jsp。最後,WebConfig擴展了WebMvcConfigurerAdapter,通過調用DefaultServletHandlerConfigurer的enable()方法,我們要求DispatcherServlet將對靜態資源的請求轉發到Servlet容器默認的Servlet上,而不是DispatcherServlet本身來處理此類請求。
2、控制器
2.1、一個簡單的控制器
在Spring MVC中,控制器只是方法上添加了@RequestMapping註解的類,這個註解聲明瞭它們所要處理的請求。下面這個例子就是一個最簡單的控制器:
@Controller
public class HomeController {
@RequestMapping(value="/",method=RequestMethod.GET)
public String home() {
return "home";
}
}
可以看到,HomeController帶有@Controller註解,@Controller註解是用來聲明控制器的,通過查看它的源碼:
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Controller {
/**
* The value may indicate a suggestion for a logical component name,
* to be turned into a Spring bean in case of an autodetected component.
* @return the suggested component name, if any
*/
String value() default "";
}
我們發現,它是基於@Component註解的,它的目的就是輔助實現組件掃描,因爲HomeController帶有@Controller註解,組件掃描會自動發現它並將它聲明爲一個Spring應用上下文中的bean。
我們在home()方法上使用了@RequestMapping註解,它的value屬性指定了這個方法所要處理的請求路徑,method屬性指定了它所處理的HTTP方法,在這裏,當收到對“/”的HTTP GET請求時,就會調用home()方法。
另外home()方法返回一個String類型的”home”,這個String會被Spring MVC解讀爲要渲染的視圖名稱,DispatcherServlet會要求視圖解析器將這個邏輯名稱解析爲實際的視圖,在這裏,根據配置,邏輯視圖名會被解析爲“/WEB-INF/views/home.jsp”。
我們可以定義一個簡單的home.jsp:
<%@ page language="java" contentType="text/html; charset=utf-8"
pageEncoding="utf-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
<title>主頁</title>
</head>
<body>
<h1>這是主頁</h1>
</body>
</html>
接下來,我們對主頁進行訪問:
可以看到,我們在瀏覽器中訪問的請求路徑是http://localhost:8080/spring-mvc/,而實際上返回的是home.jsp的內容。
上面這個Controller中,我們是將@RequestMapping註解放在處理方法home()上面的,實際上@RequestMapping註解也可以定義在類級別上,我們修改一下HomeController:
@Controller
@RequestMapping(value="/")
public class HomeController {
@RequestMapping(method=RequestMethod.GET)
public String home() {
return "home";
}
}
其實上面的HomeController定義和之前定義的HomeController執行效果都是一樣的。但是,當控制器在類級別上添加@RequestMapping註解的時候,這個註解會應用到控制器所有處理方法上,處理方法上的@RequestMapping註解會對類級別上的@RequestMapping的聲明進行補充。
另外@RequestMapping的value屬性能夠接受一個String類型的數組,也就是說,它可以處理多個路徑的請求,我們修改這個Controller:
@Controller
@RequestMapping(value={"/","/homepage"})
public class HomeController {
@RequestMapping(method=RequestMethod.GET)
public String home() {
return "home";
}
}
這樣的話,我們訪問http://localhost:8080/spring-mvc/和http://localhost:8080/spring-mvc/homepage效果都是一樣的。
2.2、傳遞模型數據到視圖中
上面介紹的例子實現的功能很簡單,只是訪問一個頁面,頁面返回指定的輸出,而實際應用中,經常需要訪問頁面時,頁面能夠根據後臺業務邏輯計算的結果返回相應的輸出,這就需要一個新的方法來處理這個頁面,例如,我們需要知道每個班學生信息,就需要在訪問頁面時去數據庫中查找相應的學生信息,然後將查詢到的數據返回給頁面顯示,在這裏學生信息就是模型數據,那我們該如何處理呢?我們新定義一個控制器StudentInfoController:
@Controller
public class StudentInfoController {
@RequestMapping(value="/listStudentInfo",method=RequestMethod.GET)
public String listStudentInfo(Model model) {
Student s = new Student();
model.addAttribute(s.getStudentList());
return "studentInfo";
}
}
可以看到,listStudentInfo()方法中給定了一個Model作爲參數,這樣,listStudentInfo()方法就能將Student中獲取到的學生列表信息填充到模型中了。Model實際上就是一個Map(也就是key-value對的集合),它會傳遞給視圖,這樣數據就能渲染到客戶端了。在本例中,我們調用Model的addAttribute()方法時並沒有指定key,那麼key會根據值的對象類型推斷確定。在本例中,因爲值的對象類型爲List< Student >,因此,key會推斷爲studentList。當然我們也可以顯式地聲明模型的key:
@Controller
public class StudentInfoController {
@RequestMapping(value="/listStudentInfo",method=RequestMethod.GET)
public String listStudentInfo(Model model) {
Student s = new Student();
model.addAttribute("studentList", s.getStudentList());
return "studentInfo";
}
}
如果希望使用非Spring類型的話,我們可以使用java.util.Map來代替Model:
@Controller
public class StudentInfoController {
@RequestMapping(value="/listStudentInfo",method=RequestMethod.GET)
public String listStudentInfo(Map model) {
Student s = new Student();
model.put("studentList", s.getStudentList());
return "studentInfo";
}
}
我們還可以這樣改寫這個方法以實現同樣的效果:
@RequestMapping(value="/studentInfo",method=RequestMethod.GET)
public List<Student> studentInfo(){
Student s = new Student();
return s.getStudentList();
}
在這裏,我們既沒有返回視圖名稱,也沒有顯式地設定模型,這個方法返回的是一個Student列表,當處理方法像這樣返回對象或集合時,這個值會放到模型中,模型的key會根據其類型推斷得出,在這裏也就是studentList。而邏輯視圖的名稱將會根據請求路徑推斷得出,因爲這個方法處理針對“/studentInfo”的GET請求,因而視圖名稱將會是studentInfo。
當視圖是JSP的時候,模型數據會作爲請求屬性放到請求之中,這樣在JSP中就可以通過JSTL獲取到學生信息:
<%@ page language="java" contentType="text/html; charset=utf-8"
pageEncoding="utf-8" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Insert title here</title>
</head>
<body>
<h1>學生信息</h1>
<c:forEach items="${studentList }" var="student">
ID:${student.id }<br/>
姓名:${student.name }<br/>
性別:${student.sex }<br/>
</c:forEach>
</body>
</html>
我們再寫一個Student類,用於模擬去數據庫中查詢學生信息:
public class Student {
private int id;
private String name;
private String sex;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
public Student(int id, String name, String sex) {
this.id = id;
this.name = name;
this.sex = sex;
}
public Student() {
}
public List<Student> getStudentList() {
List<Student> studentList = new ArrayList<Student>();
Student s1 = new Student(1, "張三", "男");
Student s2 = new Student(2, "李四", "女");
Student s3 = new Student(3, "王五", "男");
studentList.add(s1);
studentList.add(s2);
studentList.add(s3);
return studentList;
}
}
這樣,我們訪問相應的控制器時,可以看到頁面:
3、接受請求的輸入
對於Web應用,客戶端除了可以從服務器讀取數據之外,還可以允許用戶輸入,將數據發送到服務器上。Spring MVC允許以多種方式將客戶端中的數據傳送到控制器的處理器方法中,包括:
- 查詢參數
- 表單參數
- 路徑變量
3.1、處理查詢參數
還是上面的獲取學生信息的例子,假如我們需要通過ID查詢指定學生信息,我們可以將學生ID作爲查詢參數傳遞給處理方法:
@RequestMapping(value="/queryStudentInfo",method=RequestMethod.GET)
public String queryStudentInfo(@RequestParam("id") int id, Model model) {
Student s = new Student().getStudentById(id);
if(null != s) {
model.addAttribute("student", s);
}
return "student";
}
可以看到,在queryStudentInfo()方法中接收了一個int類型的參數id,這個參數使用了@RequestParam註解進行標註,這個註解表示請求參數中名爲id的參數值將會傳遞給queryStudentInfo()方法的參數id,這樣我們就可以獲取到查詢參數了。我們寫一個簡單的student.jsp作爲頁面顯示:
<%@ page language="java" contentType="text/html; charset=utf-8"
pageEncoding="utf-8" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Insert title here</title>
</head>
<body>
<h1>學生信息</h1>
ID:${student.id }<br/>
姓名:${student.name }<br/>
性別:${student.sex }<br/>
</body>
這樣我們就可以通過查詢參數獲取到學生信息了:
如果請求參數id不存在,我們可以通過@RequestParam的defaultValue屬性設置參數的默認值,這樣queryStudentInfo()方法就既可以處理有參數的查詢也可以處理無參數的查詢了。我們修改一下queryStudentInfo()方法:
@RequestMapping(value="/queryStudentInfo",method=RequestMethod.GET)
public String queryStudentInfo(@RequestParam(value="id",defaultValue="1") int id, Model model) {
Student s = new Student().getStudentById(id);
if(null != s) {
model.addAttribute("student", s);
}
return "student";
}
在這裏,defaultValue接受的是一個String類型的值,而我們需要的id是一個int類型的,Spring會將defaultValue的值轉換爲int類型。這樣修改以後,即使我們不傳遞查詢參數,也可以獲取到一個id默認爲1的學生信息:
3.2、通過路徑參數接受輸入
假設我們的應用程序需要根據學生ID獲取到學生信息,一種方法就是上節介紹的通過@RequestParam註解獲取查詢參數,然後去後臺進行查詢,另一種方式我們可以通過路徑參數獲取。例如,我們要獲取ID爲2的學生信息,如果通過查詢參數,我們需要訪問“/queryStudentInfo?id=2”,而通過路徑參數,我們只需要訪問“/queryStudentInfo/2”即可,那麼我們該如何獲取到查詢參數呢?我們修改上面的控制器方法:
@RequestMapping(value="/queryStudentInfo/{id}",method=RequestMethod.GET)
public String queryStudentInfo(@PathVariable("id") int id, Model model) {
Student s = new Student().getStudentById(id);
if(null != s) {
model.addAttribute("student", s);
}
return "student";
}
在之前介紹的內容中,所有的方法都映射到了靜態定義的路徑上,但是要獲取到路徑參數,@RequestMapping註解中就需要包含變量部分,在這裏我們可以使用佔位符“{}”,路徑中其他部分要與所處理的請求完全匹配,但是佔位符部分可以是任意值。在這裏,queryStudentInfo()方法的id參數上添加了@PathVariable(“id”)註解,這表明在請求路徑中,不管佔位符部分的值是什麼都會傳遞到處理器方法的id參數中。這樣,我們訪問“/queryStudentInfo/2”即可獲取到ID爲2的學生信息:
因爲上面方法的參數名恰巧與佔位符的名稱相同,因此我們可以去掉@PathVariable中的value屬性:
@RequestMapping(value="/queryStudentInfo/{id}",method=RequestMethod.GET)
public String queryStudentInfo(@PathVariable int id, Model model) {
Student s = new Student().getStudentById(id);
if(null != s) {
model.addAttribute("student", s);
}
return "student";
}
需要注意的是,佔位符的名稱必須要與@PathVariable註解的value屬性值相同。如果@PathVariable中沒有value屬性的話,它會假設佔位符的名稱與方法的參數名相同,這能夠讓代碼稍微簡潔一些,因爲不必重複寫佔位符的名稱了,但是需要注意的是,如果想要重命名參數時,必須要同時修改佔位符的名稱,使其互相匹配。
3.3、處理表單參數
很多時候,我們需要用戶在瀏覽器中輸入一些信息,服務器接收到這些信息之後可以進行一系列的邏輯處理,然後將處理結果返回給用戶,通常我們通過表單的方式與用戶進行交互。現在,假設我們需要用戶在頁面錄入學生信息,錄入之後,再將用戶錄入的信息返回展示給用戶,這樣我們就模擬了一個服務器與客戶端交互的過程。我們新寫一個錄入學生信息的表單:
<%@ page language="java" contentType="text/html; charset=utf-8"
pageEncoding="utf-8" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Insert title here</title>
</head>
<body>
<h1>請錄入學生信息</h1>
<form action="addStudent" method="post">
ID:<input type="text" name="id"/><br/>
姓名:<input type="text" name="name"/><br/>
性別:<input type="text" name="sex"/><br/>
<input type="submit" value="提交"/>
</form>
</body>
</html>
接下來我們寫一個處理器方法來接收表單參數:
@RequestMapping(value="/addStudent",method=RequestMethod.POST)
public String addStudent(Student s, Model model) {
studentList.add(s);
return "redirect:showStudent";
}
可以看到,這個方法接收一個Student類型的參數,這個參數的id、name、sex屬性會使用請求中同名的參數進行填充,也就是說,如果處理器方法的參數中包含與表單參數同名的屬性,這些屬性的值將與表單參數進行綁定。
我們還可以看到,這個方法返回的是redirect:showStudent,當InternalResourceViewResolver看到視圖的格式中的“redirect:”前綴時,他就知道要將其解析爲重定向的規則,而不是視圖的名稱,視圖解析器會重定向到“redirect:”指定的控制器的處理方法上,類似的還有“forward:”前綴,請求將會轉發給“forward:”指定路徑的控制器處理方法上。
我們可以運行上面的代碼進行測試:
我們錄入一個ID爲4的學生信息,點擊提交,頁面顯示剛剛新增的學生信息:
從上面的實例我們可以看到,當編寫控制器的處理器方法時,Spring MVC及其靈活,概括來講,如果你的處理器方法需要內容的話,只需將對應的對象作爲參數,而它不需要的內容,則沒有必要出現在參數列表中。