Spring MVC屬于SpringFrameWork的后續(xù)產(chǎn)品,已經(jīng)融合在Spring Web Flow里面。Spring 框架提供了構(gòu)建 Web 應(yīng)用程序的全功能 MVC 模塊。使用 Spring 可插入的 MVC 架構(gòu),從而在使用Spring進(jìn)行WEB開發(fā)時,可以選擇使用Spring的Spring MVC框架或集成其他MVC開發(fā)框架,如Struts1(現(xiàn)在一般不用),[Struts 2](https://baike.baidu.com/item/Struts 2/2187934)(一般老項目使用)等。
SpringMVC 已經(jīng)成為目前最主流的 MVC 框架之一, 從 Spring3.0 的發(fā)布, 就已全面超越 Struts2,成為最優(yōu)秀的 MVC 框架。它通過一套注解,讓一個簡單的 Java 類成為處理請求的控制器,而無須實現(xiàn)任何接口。同時它還支持RESTful 編程風(fēng)格的請求。
MVC模式(Model-View-Controller)是軟件工程中的一種軟件架構(gòu)模式,把軟件系統(tǒng)分為三個基本部分:模型(Model)、視圖(View)和控制器(Controller)。
控制器(Controller):Servlet,控制器主要處理用戶的請求
視圖(View):HTML, JSP, 前端框架
模型(Model):邏輯業(yè)務(wù)程序(后臺的功能程序), Service, Dao, JavaBean
所有的業(yè)務(wù)邏輯交給jsp單獨(dú)處理完成,一個web項目只存在DB層和JSP層,所有的東西都耦合在一起,對后期的維護(hù)和擴(kuò)展極為不利。
JSP Model2有所改進(jìn),把業(yè)務(wù)邏輯的內(nèi)容放到了JavaBean中,而JSP頁面負(fù)責(zé)顯示以及請求調(diào)度的工作。雖然第二代比第一代好了些,但JSP還是把view和controller的業(yè)務(wù)耦合在一起。依然很不理想。
Model2第二代就是現(xiàn)在大力推廣的和使用的mvc,將一個項目劃分為三個模塊,各司其事互不干擾,既解決了jsp所形成的耦合性,又增加了邏輯性、業(yè)務(wù)性以及復(fù)用性和維護(hù)性
表示層(web層):包含JSP,Servlet等web相關(guān)的內(nèi)容
業(yè)務(wù)邏輯層(Service):處理業(yè)務(wù),不允許出現(xiàn)servlet中的request、response。
數(shù)據(jù)層(Data Access Object):也叫持久層,封裝了對數(shù)據(jù)庫的訪問細(xì)節(jié)。
其中 web層相當(dāng)于mvc中的view+controller,Service層和dao層相當(dāng)于mvc中的model。
MVC模式:(Model-View-Controller):為了解決頁面代碼和后臺代碼的分離.
查看是否生成webapp目錄和maven項目打包方式是否變?yōu)閣ar
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.0.6.RELEASE</version>
</dependency>
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/javaee"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
version="2.5">
<!--spring核心(前端控制器)-->
<servlet>
<servlet-name>dispatcher</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
</servlet>
<servlet-mapping> <!--只有*.form后綴的請求才會進(jìn)入springmvc-->
<servlet-name>dispatcher</servlet-name>
<url-pattern>*.form</url-pattern>
</servlet-mapping>
</web-app>
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd">
<context:component-scan base-package="com.dfbz"/>
<!--開啟springmvc注解支持-->
<mvc:annotation-driven/>
</beans>
在 SpringMVC 的各個組件中,處理器映射器、處理器適配器、視圖解析器稱為 SpringMVC 的三大組件。
在springmvc早期版本中需要我們自己加載springmvc的三大組件(現(xiàn)在我們使用的版本5.0.6會自動加載這三大組件)
**處理器映射器:RequestMappingHandlerMapping **
處理器適配器:RequestMappingHandlerAdapter
處理器解析器:ExceptionHandlerExceptionResolver
在早期的版本中使用 <mvc:annotation-driven> 自動加載這三大組件,但是高版本的不需要<mvc:annotation-driven>來加載
同時它還提供了:數(shù)據(jù)綁定支持,@NumberFormatannotation支持,@DateTimeFormat支持,@Valid支持,讀寫XML的支持(JAXB,讀寫JSON的支持(Jackson)。我們處理響應(yīng)ajax請求時,就使用到了對json的支持(配置之后,在加入了jackson的core和mapper包之后,不寫配置文件也能自動轉(zhuǎn)換成json)
注意:默認(rèn)的Spring配置文件放在WEB-INF下,名為{servlet-name}-servlet.xml
{servlet-name}指的是,核心控制器配置的名字
如:dispatcherServlet-servlet.xml
當(dāng)請求被springmvc處理時,springmvc會去默認(rèn)路徑下加載xxxx-servlet.xml核心配置文件
但是我們在開發(fā)中一般都是把配置文件寫在classes下的,我們可以在web.xml中設(shè)置springmvc配置文件的路徑
<!--spring核心(前端控制器)-->
<servlet>
<servlet-name>dispatcher</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<!--配置springmvc配置文件的位置-->
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:dispatcher-servlet.xml</param-value>
</init-param>
</servlet>
<servlet-mapping> <!--只有*.form后綴的請求才會進(jìn)入springmvc-->
<servlet-name>dispatcher</servlet-name>
<url-pattern>*.form</url-pattern>
</servlet-mapping>
package com.dfbz.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;
@Controller
public class HelloController{
//代表此方法的訪問路徑為/hello.form
@RequestMapping("/hello.form")
public ModelAndView hello(){
ModelAndView mav=new ModelAndView();
mav.addObject("msg","小標(biāo)");
mav.setViewName("/hello.jsp");
return mav;
}
}
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
${msg },歡迎您!
</body>
</html>
1、客戶端發(fā)送請求給前端控制器(DispatcherServlet)
2、dispatcherServlet接收到請求調(diào)用HandlerMapping處理器映射器
3、處理器映射器根據(jù)請求的url找對應(yīng)的處理器,生成處理器對象(handler)返回
4、dispatchServlet將handler傳入處理器適配器執(zhí)行
5、處理器適配器執(zhí)行handler
6、執(zhí)行完成最終封裝一個ModelAndView
7、將ModelAndView返回給前端控制器
8、前端控制器將請求的路徑交給視圖解析器進(jìn)行解析
9、最終封裝一個View對象給dispatcherServlet,此View對象封裝了響應(yīng)參數(shù)
10、JSP頁面渲染數(shù)據(jù)
11、響應(yīng)客戶端
我們知道SpringMVC實質(zhì)上是對servlet進(jìn)行封裝,讓我們的開發(fā)更加簡便
我們知道springmvc在工作開始之前會加載默認(rèn)的處理器映射器、處理器適配器、處理器解析器等
可以在spring-webmvc-5.0.6.RELEASE.jar源碼包下查看DispatcherServlet.properties文件看有哪些處理器是springmvc默認(rèn)加載的
我們發(fā)現(xiàn)DispatcherServlet最終還是繼承與HttpServlet,那么我們就直接找service方法吧!
經(jīng)打斷點(diǎn)發(fā)現(xiàn),最終會走向DispacherServlet的doDispacher方法!
此時請求進(jìn)入DispatcherServlet,按照我們畫圖分析的結(jié)果應(yīng)該是把請求交給處理器映射器HandlerMapping最終返回一個Handler
接下來進(jìn)入處理器適配器HandlerAdapter執(zhí)行handler最終返回一個ModelAndView
然后請求交給視圖解析器進(jìn)行解析最終返回一個View對象
SpringMVC自帶了攔截器請求的核心控制器.所以就可以在請求過來的時候,直接啟動Spring框架
默認(rèn)情況下,Spring容器是在核心控制器DispatcherServlet獲得請求后才啟動的.
能不能網(wǎng)站啟動的時候,Spring容器就立刻啟動.
<servlet>
<servlet-name>dispatcher</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<load-on-startup>1</load-on-startup><!-- 啟動服務(wù)器的時候,Servlet就創(chuàng)建實例 -->
</servlet>
可以返回視圖字符串,解析器會自動解析
@RequestMapping("/demo3_1")
public String demo3_1(){
return "/success.jsp"; //直接返回字符串
}
ModelAndView是SpringMVC幫我們提供的,即"模型和視圖"
@RequestMapping("/demo3_2")
public ModelAndView demo3_2(){ //返回ModelAndView
ModelAndView mav=new ModelAndView();
mav.setViewName("success.jsp");
mav.addObject("username","東方標(biāo)準(zhǔn)");
return mav;
}
一般用于使用原生的Servlet對象或者ajax請求
@RequestMapping("/demo3_3") //返回void(一般用于ajax)
public void demo3_2(HttpServletResponse response) throws IOException {
response.setContentType("text/html;charset=utf8");
response.getWriter().write("東方標(biāo)準(zhǔn)");
}
SpringMVC提供了一個 String 類型返回值之后會自動解析forward、redirect等特殊字符串
視圖解析器配置的前綴和后綴解析不支持forward、redirect
:forward:代表轉(zhuǎn)發(fā)request.getRequestDispatcher(url).forward(request,response)
:redirect:代表重定向response.sendRedirect(url)
@RequestMapping("/demo3_4")
public String demo3_4(Model model) { //轉(zhuǎn)發(fā)
System.out.println("執(zhí)行啦!");
model.addAttribute("username","東方標(biāo)準(zhǔn)");
return "forward:/success.jsp";
}
@RequestMapping("/demo3_5")
public String demo3_5(HttpSession session) { //重定向
session.setAttribute("password","admin");
System.out.println("執(zhí)行啦!");
return "redirect:/success.jsp";
}
注解式處理器映射器,對類中標(biāo)記了@ResquestMapping的方法進(jìn)行映射。根據(jù)@ResquestMapping定義的url匹配@ResquestMapping標(biāo)記的方法,匹配成功返回HandlerMethod對象給前端控制器。HandlerMethod對象中封裝url對應(yīng)的方法Method。
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Mapping
public @interface RequestMapping {
}
按照我們傳統(tǒng)的url寫法不能很好的規(guī)定請求路徑,即不能按照業(yè)務(wù)來區(qū)分請求
例如現(xiàn)在user需要定義一個findById,goods也需要定義一個findById,此時就會有沖突,針對這種現(xiàn)象我們可以在類上定義一個命名空間,即在類上使用@RequestMapping注解,類似于一級目錄,以后訪問此類下的任意資源都需要加上此目錄
請求 URL 的第一級訪問目錄。此處不寫的話,就相當(dāng)于應(yīng)用的根目錄。 寫的話需要以/開頭。它出現(xiàn)的目的是為了使我們的 URL 可以按照模塊化管理:
例如:
user模塊:
/user/register
/user/update
/user/findById
goods模塊:
/goods/add
/goods/update
/goods/findById
映射路徑的有三種:標(biāo)準(zhǔn)的映射路徑,帶通配符的映射路徑,帶路徑變量的映射路徑
請求 URL 的第二級訪問目錄。
params={“userId”},表示請求參數(shù)必須有 userId,區(qū)分大小寫
params={“userId!=20”},表示請求參數(shù)中 id不能是 20。可以不攜帶userId參數(shù),區(qū)分大小寫
headers:用于指定限制請求消息頭的條件。
package com.dfbz.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.ModelAndView;
@Controller
public class HelloController{
public HelloController(){
System.out.println("Hello創(chuàng)建了");
}
/*
代表此方法的訪問路徑為/hello.form,如果攜帶userId參數(shù)那么參數(shù)必須不能等于1
并且提交方式一定要為POST
*/
@RequestMapping(value="/hello.form",params="userId!=1",method=RequestMethod.POST)
public ModelAndView hello(){
ModelAndView mav=new ModelAndView();
mav.addObject("msg","小標(biāo)");
mav.setViewName("/hello.jsp");
return mav;
}
}
小結(jié):
1.參數(shù)必須包括:params={“username”,“password”}
2.參數(shù)不能包括:params={"!userid"}
3參數(shù)值必須是指定的值:params={“username=zhangsan”})
4.參數(shù)值必須不是指定的值:params={“userid!=123”})
所謂的路徑變量,就是將參數(shù)放在路徑里面,而不是放在?的后面
如:原get請求方法 /login.mvc?username=’zhangsan’&pwd=’123456’
路徑變量寫法:
/zhangsan/123456/login.form
REST(英文:Representational State Transfer,簡稱REST)RESTful是一種軟件架構(gòu)風(fēng)格、設(shè)計風(fēng)格,而不是標(biāo)準(zhǔn),只是提供了一組設(shè)計原則和約束條件。它主要用于客戶端和服務(wù)器交互類的軟件。基于這個風(fēng)格設(shè)計的軟件可以更簡潔,更有層次,更易于實現(xiàn)緩存等機(jī)制。
請求方式有幾種?7種
jsp、html,只支持get、post。
http://localhost:8080/SpringMVC_01/user
提交方式: post
http://localhost:8080/SpringMVC_01/user
提交方式:put
http://localhost:8080/SpringMVC_01/user/101
提交方式:delete
http://localhost:8080/SpringMVC_01/user/102
提交方式:get
SpringMVC里面,所謂的數(shù)據(jù)綁定就是將請求帶過來的表單數(shù)據(jù)綁定到執(zhí)行方法的參數(shù)變量中,或?qū)⒎?wù)器數(shù)據(jù)綁定到內(nèi)置對象,傳遞到頁面
HttpServletRequest
HttpServletResponse
HttpSession
Model
ModelMap
ModelAndView
Model和ModelMap默認(rèn)都是存儲了Request請求作用域的數(shù)據(jù)的對象
這個兩個對象的作用是一樣.就將數(shù)據(jù)返回到頁面.
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h3>測試ServletAPI綁定</h3>
<p><a href="/demo1.form"></a></p>
<h3>測試參數(shù)類型綁定</h3>
<p><a href="/demo2.form?username=zhangsan">屬性參數(shù)綁定</a></p>
<h3>測試對象參數(shù)綁定</h3>
<form action="/demo3.form" method="post">
<input type="text" name="username">
<input type="text" name="password">
<input type="text" name="address">
<input type="submit" value="對象參數(shù)綁定">
</form>
<h3>測試數(shù)組類型綁定</h3>
<form action="/demo4.form" method="post">
<input type="checkbox" value="1" name="ids">
<input type="checkbox" value="2" name="ids">
<input type="checkbox" value="3" name="ids">
<input type="submit" value="數(shù)組類型綁定">
</form>
<h3>測試Pojo對象數(shù)據(jù)綁定</h3>
<hr>
<form action="/demo6.form" method="post">
<h3>Pojo封裝數(shù)組</h3>
<input type="checkbox" value="1" name="ids">
<input type="checkbox" value="2" name="ids">
<input type="checkbox" value="3" name="ids">
<br>
<h3>Pojo封裝user</h3>
<input type="text" name="user.username">
<input type="text" name="user.password">
<input type="text" name="user.address">
<br>
<h3>Pojo封裝list</h3>
<input type="text" name="userList[1].name">
<input type="text" name="userList[1].password">
<input type="text" name="userList[1].address">
<input type="text" name="userList[0].name">
<input type="text" name="userList[0].password">
<input type="text" name="userList[0].address">
<br>
<h3>Pojo封裝map</h3>
<input type="text" name="map['username']"><br>
<input type="text" name="map['password']"><br>
<input type="text" name="map['age']"><br>
<input type="text" name="map['address']"><br>
<input type="submit" value="Pojo對象參數(shù)綁定"><br>
</form>
</body>
</html>
package com.dfbz.controller;
import com.dfbz.entity.Pojo;
import com.dfbz.entity.User;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.util.Arrays;
import java.util.List;
@Controller
public class DemoController {
@RequestMapping("/demo1") //自定義屬性綁定
public String demo1(HttpServletRequest request, HttpServletResponse response, HttpSession session){
request.setAttribute("username","zhangsan");
return "/success.jsp";
}
@RequestMapping("/demo2") //屬性參數(shù)綁定
public String demo2(String username){
System.out.println(username);
return "/success.jsp";
}
@RequestMapping("/demo3") //對象參數(shù)綁定
public String demo3(User user){
System.out.println(user);
return "/success.jsp";
}
@RequestMapping("/demo4") //數(shù)組參數(shù)綁定
public String demo4(String[] ids){
System.out.println(Arrays.toString(ids));
return "/success.jsp";
}
@RequestMapping("/demo5") //Pojo對象參數(shù)綁定
public String demo5(Pojo pojo){
System.out.println(pojo);
return "/success.jsp";
}
}
測試GET/POST提交中文數(shù)據(jù)
<h3>測試Post亂碼</h3>
<form action="/demo" method="post">
<input type="text" name="username">
<input type="text" name="password">
<input type="text" name="address">
<input type="submit">
</form>
@RequestMapping("/demo7.form") //Pojo對象參數(shù)綁定
public String demo7(User user){
System.out.println(user);
return "/success.jsp";
}
發(fā)現(xiàn)POST亂碼
解決post提交亂碼我們可以配置spring提供的過濾器
<!--解決Post提交中文亂碼-->
<filter>
<filter-name>characterEncoding</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<!--要使用的字符集,一般我們使用UTF-8(保險起見UTF-8最好)-->
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>characterEncoding</filter-name>
<!--代表所有請求都經(jīng)過編碼處理-->
<url-pattern>/*</url-pattern>
</filter-mapping>
SpringMVC有一些數(shù)據(jù)是不能自動綁定,需要我們使用它提供的注解強(qiáng)制綁定.
遇到需要強(qiáng)制綁定的幾種情況
a. 默認(rèn)參數(shù)綁定的是表單數(shù)據(jù),如果數(shù)據(jù)不是來自表單(如restful),那么必須需要強(qiáng)制綁定
b. 數(shù)據(jù)是來自表單的,但是參數(shù)名不匹配,那么也需要強(qiáng)制綁定
c. 數(shù)據(jù)是來自表單的,但是需要將數(shù)據(jù)綁定在Map對象里面,需要強(qiáng)制綁定
這個注解是綁定路徑參數(shù)的.
/**
* http://localhost:8080/20/zhangsan/demo9.form
* @param id
* @param username
* @return
*/
@RequestMapping("/{id}/{username}/demo9") //@PathVariable綁定restful請求
public String demo9(
@PathVariable("id") Integer id,@PathVariable("username") String username
){
System.out.println(id);
System.out.println(username);
return "/success.jsp";
}
<a href="/20/zhangsan/demo9.form">測試restful風(fēng)個綁定</a>
接收的參數(shù)的變量名與表單的name屬性不一樣
/**
* http://localhost:8080/demo8.form?name=zhangsan
* @param username
* @return
*/
@RequestMapping("/demo8") //@RequestParam強(qiáng)制數(shù)據(jù)類型綁定
public String demo8(@RequestParam("name") String username) {
System.out.println(username);
return "/success.jsp";
}
<a href="/demo8.form?name=zhangsan">測試強(qiáng)制類型綁定</a>
/**
* 獲得JSP 頁面,JSESSIOINID這個Cookie值
* @param cookevalue
*/
@RequestMapping("/demo10")
public void getcookie(@CookieValue(value="JSESSIONID") String cookevalue){
//輸出Cookie
System.out.println(cookevalue);
}
<a href="/demo10.form">測試@CookieValue注解</a>
@RequestMapping("/demo11")
//獲取請求頭host的值封裝到value中
public void demo10(@RequestHeader("host")String value){
System.out.println(value);
}
<a href="/demo11.form">測試@RequestHeader注解</a>
把Model和ModelMap中的指定的key或者指定的屬性的值也存入一份進(jìn)session域
package com.dfbz.controller;
import com.dfbz.entity.User;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.SessionAttribute;
import org.springframework.web.bind.annotation.SessionAttributes;
import javax.servlet.http.HttpServletRequest;
@Controller
/**
* names: 代表此類中model/modelMap的username屬性將會添加到一份進(jìn)入session
* types: 此類中指定的類型會添加一份到session中
*/
@SessionAttributes(names={"username"},types={User.class,String.class,Integer.class} )
public class Demo2Controller {
@RequestMapping("/demo12.form")
public String demo12(Model modelMap) {
modelMap.addAttribute("username","zhangsan");
modelMap.addAttribute("password","admin");
modelMap.addAttribute("age",20);
User user=new User();
user.setUsername("xiaodong");
modelMap.addAttribute("user",user);
return "/success.jsp";
}
}
<a href="/demo12.form">測試@SessionAttribute注解</a>
從session中獲取一個值封裝到參數(shù)中
/**
* @SessionAttribute:從session中獲取一個值
* @param username
* @param user
* @return
*/
@RequestMapping(value="/demo6")
public String demo5(@SessionAttribute("password") String username,@SessionAttribute("user") User user){
System.out.println(username);
return "/success.jsp";
}
SpringMVC之所以能夠幫我們實現(xiàn)自動數(shù)據(jù)類型轉(zhuǎn)換是因為SpringMVC提供了非常多的轉(zhuǎn)換器(Converter)
例如:
發(fā)現(xiàn)他們都實現(xiàn)Converter接口,如果我們需要之定義轉(zhuǎn)換器必須實現(xiàn)Converter接口
實現(xiàn)日期的轉(zhuǎn)換
public class User {
private Integer id;
private String username;
private String password;
private String address;
private Date birthday;
}
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<form action="/testConverter.form">
<input type="text" name="username">
<input type="text" name="password">
<input type="text" name="address">
<input type="text" name="birthday">
<input type="submit">
</form>
</body>
</html>
@RequestMapping("/testConverter.form")
public String testConverter(User user) {
System.out.println(user);
return "/success.jsp";
}
package com.dfbz.converter;
import org.springframework.core.convert.converter.Converter;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* Converter<傳入進(jìn)來的類型,轉(zhuǎn)換之后的類型>
*/
public class MyConverter implements Converter<String,Date> {
@Override
public Date convert(String str) {
SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd");
try {
Date date=sdf.parse(str);
return date;
} catch (ParseException e) {
e.printStackTrace();
}
return null;
}
}
<!--在SpringMVC配置的轉(zhuǎn)換器中添加我們自定義的轉(zhuǎn)換器-->
<bean id="myConverters" class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
<!-- 配置多個轉(zhuǎn)換器 -->
<property name="converters">
<set>
<!-- 配置轉(zhuǎn)換器(自定義轉(zhuǎn)化器類) -->
<bean class="com.dfbz.converter.MyConverter"></bean>
</set>
</property>
</bean>
<!--開啟springmvc注解支持并重新指定轉(zhuǎn)換器-->
<mvc:annotation-driven conversion-service="myConverters"/>
Spring框架默認(rèn)創(chuàng)建的對象是單例.所以業(yè)務(wù)控制器是一個單例對象.
SpringMVC提供了,request,session ,globalsession三個生命周期
每個Session創(chuàng)建一個實例
迎關(guān)注頭條號:java小馬哥
周一至周日早九點(diǎn)半!下午三點(diǎn)半!精品技術(shù)文章準(zhǔn)時送上!!!
精品學(xué)習(xí)資料獲取通道,參見文末
1、回顧servlet 與jsp 執(zhí)行過程
2、Spring MVC請求處理流程
3,mvc 體系結(jié)構(gòu)詳解
URL映射
表單參數(shù)映射
調(diào)用目標(biāo)Control
數(shù)據(jù)模型映射
視圖解析
異常處理
DispatcherServlet
DispatcherServlet它的作用有點(diǎn)兒類似于網(wǎng)關(guān),DispatcherServlet負(fù)責(zé)將請求轉(zhuǎn)發(fā)到不同的Controller上去。
配置DispatcherServlet
/ 后面不能寫成/*否則容易出問題。
編寫Controller,繼承controller類。實現(xiàn)handlerRequest接口。指定ModelAndView。這是一種方式。
第二種方式 繼承 HttpRequestHandler類。那么DispatcherServlet是如何去匹配不同的controller的呢?這就需要了解SpringMVC的體系結(jié)構(gòu)。整個體系結(jié)構(gòu)都搞懂才行。下面就從一個全局的視角去看Spring MVC的體系結(jié)構(gòu)。
SpringMVC的體系結(jié)構(gòu)圖
DispatcherServlet是通過HandlerMapping的一組映射關(guān)系去找到我們的目標(biāo)Controller
通過上面我們知道Controller存在的形式是多種多樣的。可以通過Controller接口的形式,也可以通過@Controller注解的形式存在。等等等。。。有這么多不同的Controller形式DispatcherServlet是如何去執(zhí)行的呢?這里面就涉及到一個HandlerAdapter控制器適配器。找到數(shù)據(jù)模型的一個映射。然后試圖解析是通過ViewResolver這個東東。他支持各種各樣的試圖解析。view用于具體的試圖解析。HandlerExceptionResolver異常攔截解析器。
下面就圍繞這張圖去逐一進(jìn)行源碼分析。
HandlerMapping是一個接口,他有兩個實現(xiàn)類,MatchableHandlerMapping和AbstractHandlerMapping。HandlerMapping在getHandler里面并沒有返回handler(我們的目標(biāo)執(zhí)行器,也就是我們的controller)而是返回一個執(zhí)行鏈條。HandlerExecutionChain。
根據(jù)下圖可知HandlerExecutionChain里面有一個getHandler方法,這里面返回了Handler。但是具體的是哪個一個Handler是不確定的,因為他是Object類型的。所以通過動態(tài)代理的方式是不能實現(xiàn)的。只能通過這個執(zhí)行鏈條去返回目標(biāo)Handler。后續(xù)的文章我們會對執(zhí)行鏈條做深入的講解。
然后看上面的類繼承關(guān)系圖。左面繼承了AbstractUrlHandlerMapping其中AbstractDetectingUrlHandlerMapping起到自動發(fā)現(xiàn)的作用,他是根據(jù)Bean的名稱,而且是必須以/開頭。比如這樣
<bean name="/hello.do" class="com.tuling.control.SimpleControl"/>
然后SimpleUrlHandlerMapping。如果不需要自動發(fā)現(xiàn)功能,則使用這個。SimpleUrlHandlerMapping是自己配置URL和controller之間的一個關(guān)系。所以他們之間的區(qū)別就是一個自動發(fā)現(xiàn),一個手動配置。AbstractHandlerMethodMapping是對方法進(jìn)行映射RequestMappingInfoHandlerMapping這個大家比較熟悉了,就是通過@RequestMapping進(jìn)行映射的。如果配置了SimpleUrlHandlerMapping或者BeanNameUrlHandlerMapping那么默認(rèn)的就會失效。SimpleUrlHandlerMapping和BeanNameUrlHandlerMapping可以同時配置,都可以映射到我們的Controller控制器。
對initApplicationContext()方法進(jìn)行調(diào)用。其中super.initApplicationContext();這里是初始化一些攔截器。比如自動發(fā)現(xiàn)攔截器啊等等等。this.registerHandlers(this.urlMap);開始進(jìn)行Url的匹配點(diǎn)進(jìn)去看一下可知如果urlMap不為空才進(jìn)行匹配。第一步是!url.startsWith("/")如果不是/開頭的。就會把你的URL加上/然后獲取Handler,就是我們的控制器。然后判斷handler是不是String類型的,。如果是進(jìn)行去除空格的處理。然后執(zhí)行this.registerHandler(url, handler);點(diǎn)進(jìn)去發(fā)現(xiàn),將handler轉(zhuǎn)化為Object類型。然后判斷是否是懶加載并且是String類型。然后將Handler轉(zhuǎn)化成String的字符串。然后獲取一個ApplicationContext對象。判斷handler是都是單例。如果是通過ApplicationContext.getBean(handlerName)獲取到一個Object對象。然后獲取handlerMap,判斷是否是空,如果不為空說明你配置了兩個name屬性此時會拋出異常。接著判斷你的url是否等于/如果是設(shè)置一個根目錄的Handler通過localhost:8080/即可訪問我們的handler。然后判斷是否等于/*如果是就設(shè)置一個默認(rèn)的和Handler。也就是說你永遠(yuǎn)都不會擔(dān)心找不到這個url的異常。
最后如果以上情況都不是,則通過this.handlerMap.put(urlPath, resolvedHandler);將url和handler綁定在一起。如果說handler是懶加載那么此時map中存儲的value就是beanName。這就是SimpleUrlHandlerMapping的初始化流程。
我們知道最終執(zhí)行調(diào)用的一定是DispatcherServlet。所以我們從這里開始入手。找到getHandler
首先通過Iterator var2=this.handlerMappings.iterator();或取到所有的Mapping然后遍歷。遍歷一次取出一個handlerMapping然后調(diào)用handlerMapping。點(diǎn)進(jìn)去看看。第一行g(shù)etHandlerInternal這個方法很重要。點(diǎn)進(jìn)去看看,在這個方法里第一行通過getUrlPathHelper().getLookupPathForRequest(request);去解析我們地址欄上的URL。比如或取到的是/hello.do。第二行通過Object handler=lookupHandler(lookupPath, request);去查找對應(yīng)的handler。點(diǎn)進(jìn)去發(fā)現(xiàn)就是從上面put進(jìn)去的handlerMap找到對應(yīng)的handler。在最后return一個buildPathExposingHandler。點(diǎn)進(jìn)去發(fā)現(xiàn)HandlerExecutionChain這里面配置了一個攔截器鏈,那么返回的并不是我們最終的handler。
那么getHandlerInternal方法的第二行也執(zhí)行完了。最終拿到得是一個攔截器鏈,如果說攔截器鏈為空則判斷第一行的url解析器拿到得/hello.do是否等于/如果是Object rawHandler=getRootHandler()設(shè)置為根節(jié)點(diǎn)的handler,然后判斷rawHandler是否等于空,如果是則調(diào)用默認(rèn)的handler。如果不等于空呢,則直接通過beanName從Spring容器找到Handler。 再調(diào)用buildPathExposingHandler。還是獲取一個攔截器鏈。最終,將攔截器鏈返回。getHandlerInternal方法執(zhí)行結(jié)束。返回到getHandler方法里。那么如果說,沒有獲取到攔截器鏈,則獲取默認(rèn)的handler。如果不為空,則直接通過beanName從容器中獲取到handler。然后,去創(chuàng)建一個攔截器鏈條,又加入了一個攔截器。最后進(jìn)一步獲取handler。然后返回HandlerExecutionChain。最后通過HandlerAdapter ha=this.getHandlerAdapter(mappedHandler.getHandler());獲取到的才是我們的handler。SimpleUrlHandlerMapping執(zhí)行流程到此介紹完畢,說實話SpringMVC的代碼寫的跟IOC\aop差著一大塊兒的水平感覺。
Controller 接口:
HttpRequestHandler 接口:
HttpServlet 接口:
@RequestMapping方法注解
可以看出 Handler 沒有統(tǒng)一的接口,當(dāng)dispatchServlet獲取當(dāng)對應(yīng)的Handler之后如何調(diào)用呢?調(diào)用其哪個方法?這里有兩種解決辦法,一是用instanceof 判斷Handler 類型然后調(diào)用相關(guān)方法 。二是通過引入適配器實現(xiàn),每個適配器實現(xiàn)對指定Handler的調(diào)用。spring 采用后者。
HandlerAdapter中有三個接口
1supports接口,主要的作用是傳入一個handler看看是否可以被執(zhí)行。如果可以返回true。
2handle處理接口,處理完返回一個ModelAndView。
3getLastModified,用作緩存處理,獲取最后一次修改時間。
我們知道HandlerAdapter對應(yīng)適配了4種handler關(guān)系圖如下。
舉個例子,如果說我們寫了一個Servlet,然后繼承HttpServlet此時如果不配置SimpleServletHandlerAdapter適配器,那么就會報錯,報500的異常。但是如果配置了這個適配器,那么SpringMVC給我們配置的默認(rèn)適配器SimpleControllerHandlerAdapter就會失效。所以需要手動的配置一下。下面看一下源碼即可驗證這個說法。上面講到返回一個攔截器鏈條。然后下面的代碼就是獲取適配器如下圖。
點(diǎn)進(jìn)getHandlerAdapter方法。這個方法里做了一個do-while循環(huán)。通過support方法判斷是否是可用的適配器。
在下圖的方法里判斷是否是我們配置的適配器,如果是則返回true。 拿到適配器以后就開始了真正的調(diào)用。
此時適配器找到了以后就開始真正的調(diào)用handler也就是我們servlet或者是controller。然后通過一個叫ViewResolver的接口類去解析。其中有一個接口叫resolveViewName。他返回一個View。然后View里面有一個render接口。通過里面的model進(jìn)行處理,封裝HTML。通過response進(jìn)行寫入。所以它是先有ViewResolver才有的View。
然后看看View有哪些實現(xiàn)類如下圖。
第一個AbstractTemplateViewResolver里面有以下兩種視圖支持,比如我們常用的FreeMarkerViewReslover。
第二個BeanNameViewResolver可以通過這種方式實現(xiàn)自定的視圖解析器,生明一個自定義View類。然后實現(xiàn)View接口。在render里面去做返回。然后controller里面通過ModelAndView視圖解析器的構(gòu)造器去加載我們的自定義View類型。
第三個就是InternalResourceViewResolver。這個就是我們最常用的,通過配置前綴,后綴等信息去實現(xiàn)視圖解析。
下圖是通過BeanName的形式做的視圖解析
最后附上一張整體的流程圖
封面圖源網(wǎng)絡(luò),侵權(quán)刪除)
私信頭條號,發(fā)送:“資料”,獲取更多“秘制” 精品學(xué)習(xí)資料
如有收獲,請幫忙轉(zhuǎn)發(fā),您的鼓勵是作者最大的動力,謝謝!
一大波微服務(wù)、分布式、高并發(fā)、高可用的原創(chuàng)系列文章正在路上,
歡迎關(guān)注頭條號:java小馬哥
周一至周日早九點(diǎn)半!下午三點(diǎn)半!精品技術(shù)文章準(zhǔn)時送上!!!
十余年BAT架構(gòu)經(jīng)驗傾囊相授
對于 Web 應(yīng)用程序而言,我們從瀏覽器發(fā)起一個請求,請求經(jīng)過一系列的分發(fā)和處理,最終會進(jìn)入到我們指定的方法之中,這一系列的的具體流程到底是怎么樣的呢?
記得在初入職場的時候,面試前經(jīng)常會背一背 Spring MVC 流程,印象最深的就是一個請求最先會經(jīng)過 DispatcherServlet 進(jìn)行分發(fā)處理,DispatcherServlet 就是我們 Spring MVC 的入口類,下面就是一個請求的大致流轉(zhuǎn)流程(圖片參考自 Spring In Action):
上面就是一個傳統(tǒng)的完整的 Spring MVC 流程,為什么要說這是傳統(tǒng)的流程呢?因為這個流程是用于前后端沒有分離的時候,后臺直接返回頁面給瀏覽器進(jìn)行渲染,而現(xiàn)在大部分應(yīng)用都是前后端分離,后臺直接生成一個 Json 字符串就直接返回前端,不需要經(jīng)過視圖解析器進(jìn)行處理,也就是說前后端分離之后,流程就簡化成了 1-2-3-4-7(其中第四步返回的一般是 Json 格式數(shù)據(jù))。
Spring MVC主要可以分為兩大過程,一是初始化,二就是處理請求。初始化的過程主要就是將我們定義好的 RequestMapping 映射路徑和 Controller 中的方法進(jìn)行一一映射存儲,這樣當(dāng)收到請求之后就可以處理請求調(diào)用對應(yīng)的方法,從而響應(yīng)請求。
初始化過程的入口方法是 DispatchServlet 的 init() 方法,而實際上 DispatchServlet 中并沒有這個方法,所以我們就繼續(xù)尋找父類,會發(fā)現(xiàn) init 方法在其父類(FrameworkServlet)的父類 HttpServletBean 中。
在這個方法中,首先會去家在一些 Servlet 相關(guān)配置(web.xml),然后會調(diào)用 initServletBean() 方法,這個方法是一個空的模板方法,業(yè)務(wù)邏輯由子類 FrameworkServlet 來實現(xiàn)。
這個方法本身沒有什么業(yè)務(wù)邏輯,主要是初始化 WebApplicationContext 對象,WebApplicationContext 繼承自 ApplicationContext,主要是用來處理 web 應(yīng)用的上下文。
initWebApplicationContext() 方法主要就是為了找到一個上下文,找不到就會創(chuàng)建一個上下文,創(chuàng)建之后,最終會調(diào)用方法 configureAndRefreshWebApplicationContext(cwac) 方法,而這個方法最終在設(shè)置一些基本容器標(biāo)識信息之后會去調(diào)用 refresh()方法,也就是初始化 ioc 容器。
當(dāng)調(diào)用 refresh() 方法初始化 ioc 容器之后,最終會調(diào)用方法 onRefresh(),這個方法也是一個模板鉤子方法,由子類實現(xiàn),也就是回到了我們 Spring MVC 的入口類 DispatcherServlet。
onRefresh() 方法就是 Spring MVC 初始化的最后一個步驟,在這個步驟當(dāng)中會初始化 Spring MVC 流程中可能需要使用到的九大組件。
這個組件比較熟悉,主要就是用來處理文件上傳請求,通過將普通的 Request 對象包裝成 MultipartHttpServletRequest 對象來進(jìn)行處理。
LocaleResolver 用于初始化本地語言環(huán)境,其從 Request 對象中解析出當(dāng)前所處的語言環(huán)境,如中國大陸則會解析出 zh-CN 等等,模板解析以及國際化的時候都會用到本地語言環(huán)境。
這個主要是用戶主題解析,在 Spring MVC 中,一套主題對應(yīng)一個 .properties 文件,可以存放和當(dāng)前主題相關(guān)的所有資源,如圖片,css樣式等。
用于查找處理器(Handler),比如我們 Controller 中的方法,這個其實最主要就是用來存儲 url 和 調(diào)用方法的映射關(guān)系,存儲好映射關(guān)系之后,后續(xù)有請求進(jìn)來,就可以知道調(diào)用哪個 Controller 中的哪個方法,以及方法的參數(shù)是哪些。
這是一個適配器,因為 Spring MVC 中支持很多種 Handler,但是最終將請求交給 Servlet 時,只能是 doService(req,resp) 形式,所以 HandlerAdapter 就是用來適配轉(zhuǎn)換格式的。
這個組件主要是用來處理異常,不過看名字也很明顯,這個只會對處理 Handler 時產(chǎn)生的異常進(jìn)行處理,然后會根據(jù)異常設(shè)置對應(yīng)的 ModelAndView,然后交給 Render 渲染成頁面。
這個主鍵主要是從 Request 中獲取到視圖名稱。
這個組件會依賴于 RequestToViewNameTranslator 組件獲取到的視圖名稱,因為視圖名稱是字符串格式,所以這里會將字符串格式的視圖名稱轉(zhuǎn)換成為 View 類型視圖,最終經(jīng)過一系列解析和變量替換等操作返回一個頁面到前端。
這個主鍵主要是用來管理 FlashMap,那么 FlashMap 又有什么用呢?要明白這個那就不得不提到重定向了,有時候我們提交一個請求的時候會需要重定向,那么假如參數(shù)過多或者說我們不想把參數(shù)拼接到 url 上(比如敏感數(shù)據(jù)之類的),這時候怎么辦呢?因為參數(shù)不拼接在 url 上重定向是無法攜帶參數(shù)的。
FlashMap 就是為了解決這個問題,我們可以在請求發(fā)生重定向之前,將參數(shù)寫入 request 的屬性 OUTPUT_FLASH_MAP_ATTRIBUTE 中,這樣在重定向之后的 handler 中,Spring 會自動將其設(shè)置到 Model 中,這樣就可以從 Model 中取到我們傳遞的參數(shù)了。
在九大組件初始化完成之后,Spring MVC 的初始化就完成了,接下來就是接收并處理請求了,那么處理請求的入口在哪里呢?處理請求的入口方法就是 DispatcherServlet 中的 doService 方法,而 doService 方法又會調(diào)用 doDispatch 方法。
這個方法最關(guān)鍵的就是調(diào)用了 getHandler 方法,這個方法就是會獲取到前面九大組件中的 HandlerMapping,然后進(jìn)行反射調(diào)用對應(yīng)的方法完成請求,完成請求之后后續(xù)還會經(jīng)過視圖轉(zhuǎn)換之類的一些操作,最終返回 ModelAndView,不過現(xiàn)在都是前后端分離,基本也不需要用到視圖模型,在這里我們就不分析后續(xù)過程,主要就是分析 HandlerMapping 的初始化和查詢過程。
這個方法里面會遍歷 handllerMappings,這個 handllerMappings 是一個 List 集合,因為 HandlerMapping 有多重實現(xiàn),也就是 HandlerMapping 不止一個實現(xiàn),其最常用的兩個實現(xiàn)為 RequestMappingHandlerMapping 和 BeanNameUrlHandlerMapping。
AbstractHandlerMapping 是一個抽象類,其 getHandlerInternal 這個方法也是一個模板方法:
getHandlerInternal 方法最終其會調(diào)用子類實現(xiàn),而這里的子類實現(xiàn)會有多個,其中最主要的就是 AbstractHandlerMethodMapping 和 AbstractUrlHandlerMapping 兩個抽象類,那么最終到底會調(diào)用哪個實現(xiàn)類呢?
這時候如果拿捏不準(zhǔn)我們就可以看一下類圖,上面我們提到,HandlerMapper 有兩個非常主要的實現(xiàn)類:RequestMappingHandlerMapping 和 BeanNameUrlHandlerMapping。那么我們就分別來看一下這兩個類的類圖關(guān)系:
可以看到,這兩個實現(xiàn)類的抽象父類正好對應(yīng)了 AbstractHandlerMapping 的兩個子類,所以這時候具體看哪個方法,那就看我們想看哪種類型了。
其實除了這兩種 HandlerMapping 之外,Spring 中還有其他一些 HandllerMapping,如 SimpleUrlHandlerMapping 等。
提到的這幾種 HandlerMapping,對我們來說最常用,最熟悉的那肯定就是 RequestMappingHandlerMapping ,在這里我們就以這個為例來進(jìn)行分析,所以我們應(yīng)該
這個方法本身也沒有什么邏輯,其主要的核心查找 Handler 邏輯在 lookupHandlerMethod 方法中,這個方法主要是為了獲取一個 HandlerMethod 對象,前面的方法都是 Object,而到這里變成了 HandlerMethod 類型,這是因為 Handler 有各種類型,目前我們已經(jīng)基本跟到了具體類型之下,所以類型就變成了具體類型,而如果我們看的的另一條分支線,那么返回的就會是其他對象,正是因為支持多種不同類型的 HandlerMapping 對象,所以最終為了統(tǒng)一執(zhí)行,才會需要在獲得 Hanlder 之后,DispatcherServlet 中會再次通過調(diào)用 getHandlerAdapter 方法來進(jìn)一步封裝成 HandlerAdapter 對象,才能進(jìn)行方法的調(diào)用
這個方法主要會從 mappingRegistry 中獲取命中的方法,獲取之后還會經(jīng)過一系列的判斷比較判斷比較,因為有些 url 會對應(yīng)多個方法,而方法的請求類型不同,比如一個 GET 方法,一個 POST 方法,或者其他一些屬性不相同等等,都會導(dǎo)致最終命中到不同的方法,這些邏輯主要都是在 addMatchingMappings 方法去進(jìn)一步實現(xiàn),并最終將命中的結(jié)果加入到 matches 集合內(nèi)。
在這個方法中,有一個對象非常關(guān)鍵,那就是 mappingRegistry,因為最終我們根據(jù) url 到這里獲取到對應(yīng)的 HandlerMtthod,所以這個對象很關(guān)鍵:
看這個對象其實很明顯可以看出來,這個對象其實只是維護(hù)了一些 Map 對象,所以我們可以很容易猜測到,一定在某一個地方,將 url 和 HandlerMapping 或者 HandlerMethod 的映射關(guān)系存進(jìn)來了,這時候其實我們可以根據(jù) getMappingsByUrl 方法來進(jìn)行反推,看看 urlLookup 這個 Map 是什么時候被存入的,結(jié)合上面的類圖關(guān)系,一路反推,很容易就可以找到這個 Map 中的映射關(guān)系是 AbstractHandlerMethodMapping 對象的 afterPropertiesSet 方法實現(xiàn)的(AbstractHandlerMethodMapping 實現(xiàn)了 InitializingBean 接口),也就是當(dāng)這個對象初始化完成之后,我們的 url 和 Handler 映射關(guān)系已經(jīng)存入了 MappingRegistry 對象中的集合 Map 中。
afterPropertiesSet 方法中并沒有任何邏輯,而是直接調(diào)用了 initHandlerMethods。
initHandlerMethods 方法中,首先還是會從 Spring 的上下文中獲取所有的 Bean,然后會進(jìn)一步從帶有 RequestMapping 注解和 Controller 注解中的 Bean 去解析并獲得 HandlerMethod。
這個方法中,其實就是通過反射獲取到 Controller 中的所有方法,然后調(diào)用 registerHandlerMethod 方法將相關(guān)信息注冊到 MappingRegistry 對象中的各種 Map 集合之內(nèi):
registerHandlerMethod 方法中會直接調(diào)用 AbstractHandlerMethodMapping 對象持有的 mappingRegistry 對象中的 regidter方法,這里會對 Controller 中方法上的一些元信息進(jìn)行各種解析,比如參數(shù),路徑,請求方式等等,然后會將各種信息注冊到對應(yīng)的 Map 集合中,最終完成了整個初始化。
本文重點(diǎn)以 RequestMappingHandlerMapping 為例子分析了在 Spring 當(dāng)中如何初始化 HandlerMethod,并最終在調(diào)用的時候又是如何根據(jù) url 獲取到對應(yīng)的方法并進(jìn)行執(zhí)行最終完成整個流程。
*請認(rèn)真填寫需求信息,我們會在24小時內(nèi)與您取得聯(lián)系。