Spring——Spring MVC(一)
本文主要依據《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及其靈活,概括來講,如果你的處理器方法需要內容的話,只需將對應的物件作為引數,而它不需要的內容,則沒有必要出現在引數列表中。