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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章