1. 程式人生 > >Spring——Spring MVC(一)

Spring——Spring MVC(一)

本文主要依據《Spring實戰》第五章內容進行總結

Spring MVC框架是基於模型-檢視-控制器(Model-View-Controller,MVC)模式實現,它能夠構建像Spring框架那樣靈活和鬆耦合的Web應用。

1、Spring MVC起步

1.1、Spring MVC如何處理客戶端請求

Spring MVC處理客戶端請求的過程可以參考如下所示的圖示:

這裡寫圖片描述

具體步驟如下:

  1. 客戶端請求離開瀏覽器時①,會帶有使用者請求內容的資訊,至少會包含請求的URL,但是還可能帶有其他的資訊,例如使用者提交的表單資訊;
  2. 請求會傳遞給Spring的DispatcherServlet,DispatcherServlet是一個前端控制器,主要負責將請求委託給應用程式的其他元件來執行實際的處理;
  3. DispatcherServlet要將請求傳送給Spring MVC控制器(controller),控制器是一個用於處理請求的Spring元件,DispatcherServlet要確定將請求傳送給哪個控制器,所以DispatcherServlet會查詢一個或多個處理器對映(handler mapping)②,處理器對映根據請求的URL資訊進行決策;
  4. 一旦選擇了合適的控制器,DispatcherServlet會將請求傳送給選中的控制器③,由控制器進行業務邏輯的處理;
  5. 控制器在完成邏輯處理後,通常會產生一些資訊,這些資訊需要返回給使用者並在瀏覽器上顯示,這些資訊被稱為模型(model),控制器要將模型資料打包,並且標示出用於渲染輸出的檢視名,然後將模型及檢視名傳送回DispatcherServlet④;
  6. 傳遞給DispatcherServlet的檢視名不一定是真實對應的檢視名稱,可能是一個邏輯檢視名,DispatcherServlet將會使用檢視解析器(view resolver)⑤來將邏輯試圖名匹配一個特定的試圖實現;
  7. 請求最後到達真實檢視⑥,在這裡它交付模型資料,請求的任務也就完成了;
  8. 檢視將使用模型資料渲染輸出,這個輸出會通過響應物件傳遞給客戶端⑦。

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