SpringMVC 的工作流程
一個用戶端請求接來之後呢,會被dispatcherServlet接受,然後dispatcherServlet接受到這個請求之後呢,根據HandlerMapping的映射,将這個請求映射到具體的Handler,同時還包括一個HandlerInterceptor,并且呢這個Handler和HandlerInterceptor是結合起來的,以一個HandlerExecutionChain這樣一個結果傳回給dispatcherServlet,并且這裡的HandlerIntercept至少包括一個,因為它有一個預設的,dispatcherServlet拿到這個Handler之後通過HandlerAdapter來執行具體的Handler業務方法,Handler執行完之後呢,會傳回一個ModelAndView,這樣一個結果給dispatcherServlet,這個結果包括一個邏輯視圖和一個模型資料,dispatcherServlet拿到這個模型資料和邏輯視圖之後呢會通過ViewResolver進行解析,利用視圖解析器将這個邏輯視圖轉換為web工程的實體視圖,并且完成一個模型資料的封裝,最終會傳回一個填充了模型資料的View,響應給用戶端進行渲染,這樣使用者就能看到這個結果了
SpringMVC的元件
1.DispatcherServlet:前端控制器
使用者請求到達前端控制器,它就相當于 mvc 模式中的c,DispatcherServlet 是整個流程控制的中心,相當于是 SpringMVC 的大腦,由它調用其它元件處理使用者的請求,DispatcherServlet 的存在降低了元件之間的耦合性。
2.HandlerMapping:處理器映射器
HandlerMapping 負責根據使用者請求找到 Handler 即處理器(也就是我們所說的 Controller),SpringMVC 提供了不同的映射器實作不同的映射方式,例如:配置檔案方式,實作接口方式,注解方式等,在實際開發中,我們常用的方式是注解方式。
3.Handler:處理器
Handler 是繼 DispatcherServlet 前端控制器的後端控制器,在DispatcherServlet 的控制下 Handler 對具體的使用者請求進行處理。由于 Handler 涉及到具體的使用者業務請求,是以一般情況需要程式員根據業務需求開發 Handler。(這裡所說的 Handler 就是指我們的 Controller)
4.HandlAdapter:處理器擴充卡
通過 HandlerAdapter 對處理器進行執行,這是擴充卡模式的應用,通過擴充擴充卡可以對更多類型的處理器進行執行。
5.ViewResolver:視圖解析器
ViewResolver 負責将處理結果生成 View 視圖,ViewResolver 首先根據邏輯視圖名解析成實體視圖名即具體的頁面位址,再生成 View 視圖對象,最後對 View 進行渲染将處理結果通過頁面展示給使用者。 SpringMVC 架構提供了很多的 View 視圖類型,包括:jstlView、freemarkerView、pdfView 等。一般情況下需要通過頁面标簽或頁面模版技術将模型資料通過頁面展示給使用者,需要由程式員根據業務需求開發具體的頁面。
DispatcherServlet作用
DispatcherServlet 是前端控制器設計模式的實作,提供 Spring Web MVC 的集中通路點,而且負責職責的分派,而且與 Spring IoC 容器無縫內建,進而可以獲得 Spring 的所有好處。DispatcherServlet 主要用作職責排程工作,本身主要用于控制流程,主要職責如下:
- 檔案上傳解析,如果請求類型是 multipart 将通過 MultipartResolver 進行檔案上傳解析;
- 通過 HandlerMapping,将請求映射到處理器(傳回一個 HandlerExecutionChain,它包括一個處理器、多個 HandlerInterceptor 攔截器);
- 通過 HandlerAdapter 支援多種類型的處理器(HandlerExecutionChain 中的處理器);
- 通過 ViewResolver 解析邏輯視圖名到具體視圖實作;
- 本地化解析;
- 渲染具體的視圖等;
- 如果執行過程中遇到異常将交給 HandlerExceptionResolver 來解析
DispathcherServlet配置詳解
<servlet>
<servlet-name>SpringMVC</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring-mvc.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<!-- servlet-mapping -->
<servlet-mapping>
<servlet-name>SpringMVC</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
- load-on-startup:表示啟動容器時初始化該 Servlet;
- url-pattern:表示哪些請求交給 Spring Web MVC 處理, "/" 是用來定義預設 servlet 映射的。也可以如 *.html 表示攔截所有以 html 為擴充名的請求
- contextConfigLocation:表示 SpringMVC 配置檔案的路徑
Spring配置
先添加一個service包,提供一個HelloService類,如下:
@Service
public class HelloService {
public String hello(String name) {
return "hello " + name;
}
}
添加applicationContext.xml
<?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>"
xsi:schemaLocation="<http://www.springframework.org/schema/beans>
<http://www.springframework.org/schema/beans/spring-beans.xsd>
<http://www.springframework.org/schema/context>
<https://www.springframework.org/schema/context/spring-context.xsd>">
<context:component-scan base-package="org.javaboy" use-default-filters="true">
<context:exclude-filter type="annotation"
expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
</beans>
這個配置檔案預設情況下,并不會被自定加載,所有,需要我們在web.xml對其進行配置
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:applicationContext.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
首先通過 context-param 指定 Spring 配置檔案的位置,這個配置檔案也有一些預設規則,它的配置檔案名預設就叫 applicationContext.xml ,并且,如果你将這個配置檔案放在 WEB-INF 目錄下,那麼這裡就可以不用指定配置檔案位置了,隻需要指定監聽器就可以了。這段配置是 Spring 內建 Web 環境的通用配置;一般用于加載除 Web 層的 Bean(如DAO、Service 等),以便于與其他任何Web架構內建。
contextConfigLocation: 表示用于加載Bean的配置檔案
contextClass: 表示用于加載 Bean的 ApplicationContext 實作類,預設 WebApplicationContext。
在MyController中注入HelloService:
@org.springframework.stereotype.Controller("/hello")
public class MyController implements Controller {
@Autowired
HelloService helloService;
/**
* 這就是一個請求處理接口
* * @param req 這就是前端發送來的請求
* * @param resp 這就是服務端給前端的響應
* * @return 傳回值是一個 ModelAndView,Model 相當于是我們的資料模型,
* View 是我們的視圖 * @throws Exception
*/
public ModelAndView handleRequest(HttpServletRequest req, HttpServletResponse resp) throws Exception {
System.out.println(helloService.hello("javaboy"));
ModelAndView mv = new ModelAndView("hello");
mv.addObject("name", "javaboy");
return mv;
}
}
為了在 SpringMVC 容器中能夠掃描到 MyController ,這裡給 MyController 添加了 @Controller 注解,同時,由于我們目前采用的 HandlerMapping 是 BeanNameUrlHandlerMapping(意味着請求位址就是處理器 Bean 的名字),是以,還需要手動指定 MyController 的名字。
最後,修改 SpringMVC 的配置檔案,将 Bean 配置為掃描形式:
<context:component-scan base-package="org.javaboy.helloworld" use-default-filters="false">
<context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
<!--這個是處理器映射器,這種方式,請求位址其實就是一個 Bean 的名字,然後根據這個 bean 的名字查找對應的處理器-->
<bean class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping" id="handlerMapping">
<property name="beanName" value="/hello"/>
</bean>
<bean class="org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter" id="handlerAdapter"/>
<!--視圖解析器-->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver" id="viewResolver">
<property name="prefix" value="/jsp/"/>
<property name="suffix" value=".jsp"/>
</bean>
配置完成後,再次啟動項目,Spring 容器也将會被建立。通路 /hello 接口,HelloService 中的 hello 方法就會自動被調用。
兩個容器
當 Spring 和 SpringMVC 同時出現,我們的項目中将存在兩個容器,一個是 Spring 容器,另一個是 SpringMVC 容器,Spring 容器通過 ContextLoaderListener 來加載,SpringMVC 容器則通過 DispatcherServlet 來加載,這兩個容器不一樣:
從圖中可以看出:
- ContextLoaderListener 初始化的上下文加載的 Bean 是對于整個應用程式共享的,不管是使用什麼表現層技術,一般如 DAO 層、Service 層 Bean;
- DispatcherServlet 初始化的上下文加載的 Bean 是隻對 Spring Web MVC 有效的 Bean,如 Controller、HandlerMapping、HandlerAdapter 等等,該初始化上下文應該隻加載 Web相關元件。
1. 為什麼不在 Spring 容器中掃描所有 Bean?
這個是不可能的。因為請求達到服務端後,找 DispatcherServlet 去處理,隻會去 SpringMVC 容器中找,這就意味着 Controller 必須在 SpringMVC 容器中掃描。
2.為什麼不在 SpringMVC 容器中掃描所有 Bean?
這個是可以的,可以在 SpringMVC 容器中掃描所有 Bean。不寫在一起,有兩個方面的原因:
- 為了友善配置檔案的管理
- 在 Spring+SpringMVC+Hibernate 組合中,實際上也不支援這種寫法
處理器詳情
HandlerMapping
基于XML配置
HandlerMapping 是負責根據 request 請求找到對應的 Handler 處理器及 Interceptor 攔截器,将它們封裝在 HandlerExecutionChain 對象中傳回給前端控制器。
- BeanNameUrlHandlerMapping
BeanNameUrl 處理器映射器,根據請求的url與Spring容器中定義的bean的name進行比對,進而從Spring容器中找到bean執行個體,就是說,請求的url就是處理器bean的名字
這個HandlerMapping配置如下:
<bean class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping" id="handlerMapping">
<property name="beanName" value="/hello"/>
</bean>
這種方式dean id屬性必須要以“/”開頭
- SimpleUrlHandlerMapping
SimpleUrlHandlerMapping 是 BeanNameUrlHandlerMapping 的增強版本,它可以将 url 和處理器 bean 的 id 進行統一映射配置:
<bean class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping" id="handlerMapping">
<property name="mappings">
<props>
<prop key="/hello">myController</prop>
<prop key="/hello2">myController2</prop>
</props>
</property>
</bean>
注意,在props中,可以配置多個請求路徑和處理器執行個體的映射關系。
HandlerAdapter
HandlerAdapter,中文譯作處理器擴充卡。
HandlerAdapter會根據擴充卡接口對後端控制器進行包裝(适配),包裝後即可對處理器進行執行,通過擴充處理器擴充卡可以執行多種類型的處理器,這裡使用了擴充卡設計模式。
在 SpringMVC 中,HandlerAdapter 也有諸多實作類:
- SimpleControllerHandlerAdapter
SimpleControllerHandlerAdapter 簡單控制器處理器擴充卡,所有實作了 org.springframework.web.servlet.mvc.Controller 接口的 Bean 通過此擴充卡進行适配、執行,也就是說,如果我們開發的接口是通過實作 Controller接口來完成的(不是通過注解開發的接口),那麼 HandlerAdapter 必須是 SimpleControllerHandlerAdapter。
<bean class="org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter"
/>
- HttpRequestHandlerAdapter
HttpRequestHandlerAdapter,http 請求處理器擴充卡,所有實作了 org.springframework.web.HttpRequestHandler 接口的 Bean 通過此擴充卡進行适配、執行。
例如存在如下接口:
@Controller
public class MyController2 implements HttpRequestHandler {
public void handleRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("-----MyController2-----");
}
}
<bean class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping" id="handlerMapping">
<property name="mappings">
<props>
<prop key="/hello2">myController2</prop>
</props>
</property>
</bean>
<bean class="org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter" id="handlerAdapter"/>
最佳實踐
各種情況都大概了解了,我們看下項目中的具體實踐。
- 元件自動掃描
web 開發中,我們基本上不再通過 XML 或者 Java 配置來建立一個 Bean 的執行個體,而是直接通過元件掃描來實作 Bean 的配置,如果要掃描多個包,多個包之間用 , 隔開即可:
<context:component-scan base-package="org.sang"/>
正常情況下,我們在項目中使用的是 RequestMappingHandlerMapping,這個是根據處理器中的注解,來比對請求(即 @RequestMapping 注解中的 url 屬性)。因為在上面我們都是通過實作類來開發接口的,相當于還是一個類一個接口,是以,我們可以通過 RequestMappingHandlerMapping 來做處理器映射器,這樣我們可以在一個類中開發出多個接口。
對于上面提到的通過 @RequestMapping 注解所定義出來的接口方法,這些方法的調用都是要通過 RequestMappingHandlerAdapter 這個擴充卡來實作。
例如我們開發一個接口:
@Controller
public class MyController3 {
@RequestMapping("/hello3")
public ModelAndView hello() {
return new ModelAndView("hello3");
}
}
要能夠通路到這個接口,我們需要 RequestMappingHandlerMapping 才能定位到需要執行的方法,需要 RequestMappingHandlerAdapter,才能執行定位到的方法,修改 springmvc 的配置檔案如下:
<?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>"
xsi:schemaLocation="<http://www.springframework.org/schema/beans> <http://www.springframework.org/schema/beans/spring-beans.xsd> <http://www.springframework.org/schema/context> <https://www.springframework.org/schema/context/spring-context.xsd>">
<context:component-scan base-package="org.javaboy.helloworld"/>
<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping" id="handlerMapping"/>
<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter" id="handlerAdapter"/>
<!--視圖解析器-->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver" id="viewResolver">
<property name="prefix" value="/jsp/"/>
<property name="suffix" value=".jsp"/>
</bean>
</beans
然後,啟動項目,通路 /hello3 接口,就可以看到相應的頁面了。
- 繼續優化
由于開發中,我們常用的是 RequestMappingHandlerMapping 和 RequestMappingHandlerAdapter ,這兩個有一個簡化的寫法,如下:
<mvc:annotation-driven>
可以用這一行配置,代替 RequestMappingHandlerMapping 和 RequestMappingHandlerAdapter 的兩行配置。
<?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> <https://www.springframework.org/schema/context/spring-context.xsd> <http://www.springframework.org/schema/mvc> <https://www.springframework.org/schema/mvc/spring-mvc.xsd>">
<context:component-scan base-package="org.javaboy.helloworld"/>
<mvc:annotation-driven/>
<!--視圖解析器-->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver" id="viewResolver">
<property name="prefix" value="/jsp/"/>
<property name="suffix" value=".jsp"/>
</bean>
</beans>
Controller
RequestMapping
1.請求url
标記請求URl很簡單,隻需要在相應的方法上添加注解即可:
@Controller
public class HelloController {
@RequestMapping("/hello")
public ModelAndView hello() {
return new ModelAndView("hello");
}
}
這裡 @RequestMapping(“/hello”) 表示當請求位址為 /hello 的時候,這個方法會被觸發。其中,位址可以是多個,就是可以多個位址映射到同一個方法。
@Controller
public class HelloController {
@RequestMapping({"/hello","/hello2"})
public ModelAndView hello() {
return new ModelAndView("hello");
}
}
這個配置,表示 /hello 和 /hello2 都可以通路到該方法
2.請求窄化
同一個項目中,會存在多個接口,例如訂單相關的接口都是 /order/xxx 格式的,使用者相關的接口都是 /user/xxx 格式的。為了友善處理,這裡的字首(就是 /order、/user)可以統一在 Controller 上面處理。
@Controller
@RequestMapping("/user")
public class HelloController {
@RequestMapping({"/hello","/hello2"})
public ModelAndView hello() {
return new ModelAndView("hello");
}
}
當類上加了 @RequestMapping 注解之後,此時,要想通路到 hello ,位址就應該是 /user/hello 或者 /user/hello2
3.請求方法限定
預設情況下,使用 @RequestMapping 注解定義好的方法,可以被 GET 請求通路到,也可以被 POST 請求通路到,但是 DELETE 請求以及 PUT 請求不可以通路到。
當然,我們也可以指定具體的通路方法:
@Controller
@RequestMapping("/user")
public class HelloController {
@RequestMapping(value = "/hello",method = RequestMethod.GET)
public ModelAndView hello() {
return new ModelAndView("hello");
}
}
通過 @RequestMapping 注解,指定了該接口隻能被 GET 請求通路到,此時,該接口就不可以被 POST 以及請求請求通路到了。強行通路會報如下錯誤:
當然,限定的方法也可以有多個:
@Controller
@RequestMapping("/user")
public class HelloController {
@RequestMapping(value = "/hello",method = {RequestMethod.GET,RequestMethod.POST,RequestMethod.PUT,RequestMethod.DELETE})
public ModelAndView hello() {
return new ModelAndView("hello");
}
}
此時,這個接口就可以被 GET、POST、PUT、以及 DELETE 通路到了。但是,由于 JSP 支支援 GET、POST 以及 HEAD ,是以這個測試,不能使用 JSP 做頁面模闆。可以講視圖換成其他的,或者傳回 JSON,這裡就不影響了。
方法傳回值
1.傳回ModelAndView
如果前後端不分的開發,大部分情況下,我們傳回ModelAndView,即資料模型+視圖:
@Controller
@RequestMapping("/user")
public class HelloController {
@RequestMapping("/hello")
public ModelAndView hello() {
ModelAndView mv = new ModelAndView("hello");
mv.addObject("username", "javaboy");
return mv;
}
}
Model中,放我們的資料,然後在ModelAndView中指定視圖名稱
2.傳回Void
沒有傳回值。沒有傳回值,并不一定真的沒有傳回值,隻是方法的傳回值為 void,我們可以通過其他方式給前端傳回。實際上,這種方式也可以了解為 Servlet 中的那一套方案。
注意,由于預設的 Maven 項目沒有 Servlet,是以這裡需要額外添加一個依賴:
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
</dependency>
通過HttpServletRequest做服務跳轉
@RequestMapping("/hello2")
public void hello2(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
req.getRequestDispatcher("/jsp/hello.jsp").forward(req,resp);//伺服器端跳轉
}
通過HttpServletResponse做重定向
@RequestMapping("/hello3")
public void hello3(HttpServletRequest req, HttpServletResponse resp) throws IOException {
resp.sendRedirect("/hello.jsp");
}
也可以自己手動指定響應頭去實作重定向:
@RequestMapping("/hello3")
public void hello3(HttpServletRequest req, HttpServletResponse resp) throws IOException {
resp.setStatus(302);
resp.addHeader("Location", "/jsp/hello.jsp");
}
通過 HttpServletResponse 給出響應
@RequestMapping("/hello4")
public void hello4(HttpServletRequest req, HttpServletResponse resp) throws IOException {
resp.setContentType("text/html;charset=utf-8");
PrintWriter out = resp.getWriter();
out.write("hello javaboy!");
out.flush();
out.close();
}
這種方式,既可以傳回 JSON,也可以傳回普通字元串。
3.傳回String
- 傳回邏輯視圖名
前面的 ModelAndView 可以拆分為兩部分,Model 和 View,在 SpringMVC 中,Model 我們可以直接在參數中指定,然後傳回值是邏輯視圖名:
@RequestMapping("/hello5")
public String hello5(Model model) {
model.addAttribute("username", "javaboy");//這是資料模型
return "hello";//表示去查找一個名為 hello 的視圖
}
- 服務端跳轉
@RequestMapping("/hello5")
public String hello5() {
return "forward:/jsp/hello.jsp";
}
forward 後面跟上跳轉的路徑。
- 用戶端跳轉
@RequestMapping("/hello5")
public String hello5() {
return "redirect:/user/hello";
}
- 真的傳回一個字元串
上面三個傳回的字元串,都是由特殊含義的,如果一定要傳回一個字元串,需要額外添加一個注意:@ResponseBody ,這個注解表示目前方法的傳回值就是要展示出來傳回值,沒有特殊含義。
@RequestMapping("/hello5")
@ResponseBody
public String hello5() {
return "redirect:/user/hello";
}
上面代碼表示就是想傳回一段内容為 redirect:/user/hello 的字元串,他沒有特殊含義。注意,這裡如果單純的傳回一個中文字元串,是會亂碼的,可以在 @RequestMapping 中添加 produces 屬性來解決:
@RequestMapping(value = "/hello5",produces = "text/html;charset=utf-8")
@ResponseBody
public String hello5() {
return "Java 語言程式設計";
}
參數綁定
預設的參數綁定
預設支援的參數類型,就是可以直接寫在@RequestMapping所注解的方法中的參數類型,一共有四類:
- HttpServletRequest
- HttpServletResponse
- HttpSession
- Model/ModelMap
簡單資料類型
Integer、Boolean、Double 等等簡單資料類型也都是支援的。例如添加一本書:
首先,在 /jsp/ 目錄下建立 add book.jsp 作為圖書添加頁面:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<form action="/doAdd" method="post">
<table>
<tr>
<td>書名:</td>
<td><input type="text" name="name"></td>
</tr>
<tr>
<td>作者:</td>
<td><input type="text" name="author"></td>
</tr>
<tr>
<td>價格:</td>
<td><input type="text" name="price"></td>
</tr>
<tr>
<td>是否上架:</td>
<td>
<input type="radio" value="true" name="ispublic">是
<input type="radio" value="false" name="ispublic">否
</td>
</tr>
<tr>
<td colspan="2">
<input type="submit" value="添加">
</td>
</tr>
</table>
</form>
</body>
</html>
建立控制器,控制器提供兩個功能,一個是通路 jsp 頁面,另一個是提供添加接口:
@Controller
public class BookController {
@RequestMapping("/book")
public String addBook() {
return "addbook";
}
@RequestMapping(value = "/doAdd",method = RequestMethod.POST)
@ResponseBody
public void doAdd(String name,String author,Double price,Boolean ispublic) {
System.out.println(name);
System.out.println(author);
System.out.println(price);
System.out.println(ispublic);
}
}
注意,由于 doAdd 方法确實不想傳回任何值,是以需要給該方法添加 @ResponseBody 注解,表示這個方法到此為止,不用再去查找相關視圖了。另外, POST 請求傳上來的中文會亂碼,是以,我們在 web.xml 中再額外添加一個編碼過濾器:
<filter>
<filter-name>encoding</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
<init-param>
<param-name>forceRequestEncoding</param-name>
<param-value>true</param-value>
</init-param>
<init-param>
<param-name>forceResponseEncoding</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>encoding</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
最後,浏覽器中輸入 http://localhost:8080/book ,就可以執行添加操作,服務端會列印出來相應的日志。
在上面的綁定中,有一個要求,表單中字段的 name 屬性要和接口中的變量名一一對應,才能映射成功,否則服務端接收不到前端傳來的資料。有一些特殊情況,我們的服務端的接口變量名可能和前端不一緻,這個時候我們可以通過 @RequestParam 注解來解決。
- @RequestParam
這個注解的的功能主要有三方面:
- 給變量取别名
- 設定變量是否必填
- 給變量設定預設值
@RequestMapping(value = "/doAdd",method = RequestMethod.POST)
@ResponseBody
public void doAdd(@RequestParam("name") String bookname, String author, Double price, Boolean ispublic) {
System.out.println(bookname);
System.out.println(author);
System.out.println(price);
System.out.println(ispublic);
}
注解中的 “name” 表示給 bookname 這個變量取的别名,也就是說,bookname 将接收前端傳來的 name 這個變量的值。在這個注解中,還可以添加 required 屬性和 defaultValue 屬性,如下:
@RequestMapping(value = "/doAdd",method = RequestMethod.POST)
@ResponseBody
public void doAdd(@RequestParam(value = "name",required = true,defaultValue = "三國演義") String bookname, String author, Double price, Boolean ispublic) {
System.out.println(bookname);
System.out.println(author);
System.out.println(price);
System.out.println(ispublic);
}
required 屬性預設為 true,即隻要添加了 @RequestParam 注解,這個參數預設就是必填的,如果不填,請求無法送出,會報 400 錯誤,如果這個參數不是必填項,可以手動把 required 屬性設定為 false。
但是,如果同時設定了 defaultValue,這個時候,前端不傳該參數到後端,即使 required 屬性為 true,它也不會報錯。
實體類
參數除了是簡單資料類型之外,也可以是實體類。實際上,在開發中,大部分情況下,都是實體類。
還是上面的例子,我們改用一個 Book 對象來接收前端傳來的資料:
public class Book {
private String name;
private String author;
private Double price;
private Boolean ispublic;
...
}
服務端接收資料方式如下:
@RequestMapping(value = "/doAdd",method = RequestMethod.POST)
@ResponseBody
public void doAdd(Book book) {
System.out.println(book);
}
前端頁面傳值的時候和上面的一樣,隻需要寫屬性名就可以了,不需要寫 book 對象名。
當然,對象中可能還有對象。例如如下對象:
public class Book {
private String name;
private Double price;
private Boolean ispublic;
private Author author;
...
}
public class Author {
private String name;
private Integer age;
...
}
Book 對象中,有一個 Author 屬性,如何給 Author 屬性傳值呢?前端寫法如下:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<form action="/doAdd" method="post">
<table>
<tr>
<td>書名:</td>
<td><input type="text" name="name"></td>
</tr>
<tr>
<td>作者姓名:</td>
<td><input type="text" name="author.name"></td>
</tr>
<tr>
<td>作者年齡:</td>
<td><input type="text" name="author.age"></td>
</tr>
<tr>
<td>價格:</td>
<td><input type="text" name="price"></td>
</tr>
<tr>
<td>是否上架:</td>
<td>
<input type="radio" value="true" name="ispublic">是
<input type="radio" value="false" name="ispublic">否
</td>
</tr>
<tr>
<td colspan="2">
<input type="submit" value="添加">
</td>
</tr>
</table>
</form>
</body>
</html>
這樣在後端直接用 Book 對象就可以接收到所有資料了。
自定義參數綁定
前面的轉換,都是系統自動轉換的,這種轉換僅限于基本資料類型。特殊的資料類型,系統無法自動轉換,例如日期。例如前端傳一個日期到後端,後端不是用字元串接收,而是使用一個 Date 對象接收,這個時候就會出現參數類型轉換失敗。這個時候,需要我們手動定義參數類型轉換器,将日期字元串手動轉為一個 Date 對象。
@Component
public class DateConverter implements Converter<String, Date> {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
public Date convert(String source) {
try {
return sdf.parse(source);
} catch (ParseException e) {
e.printStackTrace();
}
return null;
}
}
在自定義的參數類型轉換器中,将一個 String 轉為 Date 對象,同時,将這個轉換器注冊為一個 Bean。
接下來,在 SpringMVC 的配置檔案中,配置該 Bean,使之生效。
<mvc:annotation-driven conversion-service="conversionService"/>
<bean class="org.springframework.format.support.FormattingConversionServiceFactoryBean" id="conversionService">
<property name="converters">
<set>
<ref bean="dateConverter"/>
</set>
</property>
</bean>
配置完成後,在服務端就可以接收前端傳來的日期參數了。
集合類的參數
String 數組
String 數組可以直接用數組去接收,前端傳遞的時候,數組的傳遞其實就多相同的 key,這種一般用在 checkbox 中較多。
例如前端增加興趣愛好一項:
<form action="/doAdd" method="post">
<table>
<tr>
<td>書名:</td>
<td><input type="text" name="name"></td>
</tr>
<tr>
<td>作者姓名:</td>
<td><input type="text" name="author.name"></td>
</tr>
<tr>
<td>作者年齡:</td>
<td><input type="text" name="author.age"></td>
</tr>
<tr>
<td>出生日期:</td>
<td><input type="date" name="author.birthday"></td>
</tr>
<tr>
<td>興趣愛好:</td>
<td>
<input type="checkbox" name="favorites" value="足球">足球
<input type="checkbox" name="favorites" value="籃球">籃球
<input type="checkbox" name="favorites" value="乒乓球">乒乓球
</td>
</tr>
<tr>
<td>價格:</td>
<td><input type="text" name="price"></td>
</tr>
<tr>
<td>是否上架:</td>
<td>
<input type="radio" value="true" name="ispublic">是
<input type="radio" value="false" name="ispublic">否
</td>
</tr>
<tr>
<td colspan="2">
<input type="submit" value="添加">
</td>
</tr>
</table>
</form>
在服務端用一個數組去接收 favorites 對象:
@RequestMapping(value = "/doAdd",method = RequestMethod.POST)
@ResponseBody
public void doAdd(Book book,String[] favorites) {
System.out.println(Arrays.toString(favorites));
System.out.println(book);
}
List 集合
如果需要使用 List 集合接收前端傳來的資料,List 集合本身需要放在一個封裝對象中,這個時候,List 中,可以是基本資料類型,也可以是對象。例如有一個班級類,班級裡邊有學生,學生有多個:
public class MyClass {
private Integer id;
private List<Student> students;
...
}
public class Student {
private Integer id;
private String name;
...
}
添加班級的時候,可以傳遞多個 Student,前端頁面寫法如下:
<form action="/addclass" method="post">
<table>
<tr>
<td>班級編号:</td>
<td><input type="text" name="id"></td>
</tr>
<tr>
<td>學生編号:</td>
<td><input type="text" name="students[0].id"></td>
</tr>
<tr>
<td>學生姓名:</td>
<td><input type="text" name="students[0].name"></td>
</tr>
<tr>
<td>學生編号:</td>
<td><input type="text" name="students[1].id"></td>
</tr>
<tr>
<td>學生姓名:</td>
<td><input type="text" name="students[1].name"></td>
</tr>
<tr>
<td colspan="2">
<input type="submit" value="送出">
</td>
</tr>
</table>
</form>
服務端直接接收資料即可:
@RequestMapping("/addclass")
@ResponseBody
public void addClass(MyClass myClass) {
System.out.println(myClass);
}
Map
相對于實體類而言,Map 是一種比較靈活的方案,但是,Map 可維護性比較差,是以一般不推薦使用。 例如給上面的班級類添加其他屬性資訊:
public class MyClass {
private Integer id;
private List<Student> students;
private Map<String, Object> info;
...
}
在前端,通過如下方式給 info 這個 Map 指派。
<form action="/addclass" method="post">
<table>
<tr>
<td>班級編号:</td>
<td><input type="text" name="id"></td>
</tr>
<tr>
<td>班級名稱:</td>
<td><input type="text" name="info['name']"></td>
</tr>
<tr>
<td>班級位置:</td>
<td><input type="text" name="info['pos']"></td>
</tr>
<tr>
<td>學生編号:</td>
<td><input type="text" name="students[0].id"></td>
</tr>
<tr>
<td>學生姓名:</td>
<td><input type="text" name="students[0].name"></td>
</tr>
<tr>
<td>學生編号:</td>
<td><input type="text" name="students[1].id"></td>
</tr>
<tr>
<td>學生姓名:</td>
<td><input type="text" name="students[1].name"></td>
</tr>
<tr>
<td colspan="2">
<input type="submit" value="送出">
</td>
</tr>
</table>
</form>
檔案上傳
SpringMVC 中對檔案上傳做了封裝,我們可以更加友善的實作檔案上傳。從 Spring3.1 開始,對于檔案上傳,提供了兩個處理器:
- CommonsMultipartResolver
- StandardServletMultipartResolver
第一個處理器相容性較好,可以相容 Servlet3.0 之前的版本,但是它依賴了 commons-fileupload 這個第三方工具,是以如果使用這個,一定要添加 commons-fileupload 依賴。
第二個處理器相容性較差,它适用于 Servlet3.0 之後的版本,它不依賴第三方工具,使用它,可以直接做檔案上傳。
添加依賴
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.4</version>
</dependency>
配置MultipartResolver
<bean class="org.springframework.web.multipart.commons.CommonsMultipartResolver" id="multipartResolver"/>
注意,這個 Bean 一定要有 id,并且 id 必須是 multipartResolver
建立jsp頁面
<form action="/upload" method="post" enctype="multipart/form-data">
<input type="file" name="file">
<input type="submit" value="上傳">
</form>
注意檔案上傳請求是 POST 請求,enctype 一定是 multipart/form-data
開發檔案上傳接口
@Controller
public class FileUploadController {
SimpleDateFormat sdf = new SimpleDateFormat("/yyyy/MM/dd/");
@RequestMapping("/upload")
@ResponseBody
public String upload(MultipartFile file, HttpServletRequest req) {
String format = sdf.format(new Date());
String realPath = req.getServletContext().getRealPath("/img") + format;
File folder = new File(realPath);
if (!folder.exists()) {
folder.mkdirs();
}
String oldName = file.getOriginalFilename();
String newName = UUID.randomUUID().toString() + oldName.substring(oldName.lastIndexOf("."));
try {
file.transferTo(new File(folder, newName));
String url = req.getScheme() + "://" + req.getServerName() + ":" + req.getServerPort() + "/img" + format + newName;
return url;
} catch (IOException e) {
e.printStackTrace();
}
return "failed";
}
}
這個檔案上傳方法中,一共做了四件事:
- 解決檔案儲存路徑,這裡是儲存在項目運作目錄下的 img 目錄下,然後利用日期繼續甯分類
- 處理檔案名問題,使用 UUID 做新的檔案名,用來代替舊的檔案名,可以有效防止檔案名沖突
- 儲存檔案
- 生成檔案通路路徑
這裡還有一個小問題,在 SpringMVC 中,靜态資源預設都是被自動攔截的,無法通路,意味着上傳成功的圖檔無法通路,是以,還需要我們在 SpringMVC 的配置檔案中,再添加如下配置:
<mvc:resources mapping="/**" location="/"/>
完成之後,就可以通路 jsp 頁面,做檔案上傳了。
當然,預設的配置不一定滿足我們的需求,我們還可以自己手動配置檔案上傳大小等:
<bean class="org.springframework.web.multipart.commons.CommonsMultipartResolver" id="multipartResolver">
<!--預設的編碼-->
<property name="defaultEncoding" value="UTF-8"/>
<!--上傳的總檔案大小-->
<property name="maxUploadSize" value="1048576"/>
<!--上傳的單個檔案大小-->
<property name="maxUploadSizePerFile" value="1048576"/>
<!--記憶體中最大的資料量,超過這個資料量,資料就要開始往硬碟中寫了-->
<property name="maxInMemorySize" value="4096"/>
<!--臨時目錄,超過 maxInMemorySize 配置的大小後,資料開始往臨時目錄寫,等全部上傳完成後,再将資料合并到正式的檔案上傳目錄-->
<property name="uploadTempDir" value="file:///E:\\\\tmp"/>
</bean>
這種檔案上傳方式,不需要依賴第三方 jar(主要是不需要添加 commons-fileupload 這個依賴),但是也不支援 Servlet3.0 之前的版本。
使用 StandardServletMultipartResolver ,那我們首先在 SpringMVC 的配置檔案中,配置這個 Bean:
<bean class="org.springframework.web.multipart.support.StandardServletMultipartResolver" id="multipartResolver"></bean>
注意,這裡 Bean 的名字依然叫 multipartResolver
配置完成後,注意,這個 Bean 無法直接配置上傳檔案大小等限制。需要在 web.xml 中進行配置(這裡,即使不需要限制檔案上傳大小,也需要在 web.xml 中配置 multipart-config):
<servlet>
<servlet-name>springmvc</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring-servlet.xml</param-value>
</init-param>
<multipart-config>
<!--檔案儲存的臨時目錄,這個目錄系統不會主動建立-->
<location>E:\\\\temp</location>
<!--上傳的單個檔案大小-->
<max-file-size>1048576</max-file-size>
<!--上傳的總檔案大小-->
<max-request-size>1048576</max-request-size>
<!--這個就是記憶體中儲存的檔案最大大小-->
<file-size-threshold>4096</file-size-threshold>
</multipart-config>
</servlet>
<servlet-mapping>
<servlet-name>springmvc</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
配置完成後,就可以測試檔案上傳了,測試方式和上面一樣。
多檔案上傳
多檔案上傳分為兩種,一種是 key 相同的檔案,另一種是 key 不同的檔案。
1 key 相同的檔案
這種上傳,前端頁面一般如下:
<form action="/upload2" method="post" enctype="multipart/form-data">
<input type="file" name="files" multiple>
<input type="submit" value="上傳">
</form>
主要是 input 節點中多了 multiple 屬性。後端用一個數組來接收檔案即可:
@RequestMapping("/upload2")
@ResponseBody
public void upload2(MultipartFile[] files, HttpServletRequest req) {
String format = sdf.format(new Date());
String realPath = req.getServletContext().getRealPath("/img") + format;
File folder = new File(realPath);
if (!folder.exists()) {
folder.mkdirs();
}
try {
for (MultipartFile file : files) {
String oldName = file.getOriginalFilename();
String newName = UUID.randomUUID().toString() + oldName.substring(oldName.lastIndexOf("."));
file.transferTo(new File(folder, newName));
String url = req.getScheme() + "://" + req.getServerName() + ":" + req.getServerPort() + "/img" + format + newName;
System.out.println(url);
}
} catch (IOException e) {
e.printStackTrace();
}
}
2 key 不同的檔案
key 不同的,一般前端定義如下:
<form action="/upload3" method="post" enctype="multipart/form-data">
<input type="file" name="file1">
<input type="file" name="file2">
<input type="submit" value="上傳">
</form>
這種,在後端用不同的變量來接收就行了:
@RequestMapping("/upload3")
@ResponseBody
public void upload3(MultipartFile file1, MultipartFile file2, HttpServletRequest req) {
String format = sdf.format(new Date());
String realPath = req.getServletContext().getRealPath("/img") + format;
File folder = new File(realPath);
if (!folder.exists()) {
folder.mkdirs();
}
try {
String oldName = file1.getOriginalFilename();
String newName = UUID.randomUUID().toString() + oldName.substring(oldName.lastIndexOf("."));
file1.transferTo(new File(folder, newName));
String url1 = req.getScheme() + "://" + req.getServerName() + ":" + req.getServerPort() + "/img" + format + newName;
System.out.println(url1);
String oldName2 = file2.getOriginalFilename();
String newName2 = UUID.randomUUID().toString() + oldName2.substring(oldName2.lastIndexOf("."));
file2.transferTo(new File(folder, newName2));
String url2 = req.getScheme() + "://" + req.getServerName() + ":" + req.getServerPort() + "/img" + format + newName2;
System.out.println(url2);
} catch (IOException e) {
e.printStackTrace();
}
}
全局異常處理
項目中,可能會抛出多個異常,我們不可以直接将異常的堆棧資訊展示給使用者,有兩個原因:
- 使用者體驗不好
- 非常不安全
是以,針對異常,我們可以自定義異常處理,SpringMVC 中,針對全局異常也提供了相應的解決方案,主要是通過 @ControllerAdvice 和@ExceptionHandler 兩個注解來處理的。
以上傳大小超出限制為例,自定義異常,隻需要提供一個異常處理類即可:
@ControllerAdvice//表示這是一個增強版的 Controller,主要用來做全局資料處理
public class MyException {
@ExceptionHandler(Exception.class)
public ModelAndView fileuploadException(Exception e) {
ModelAndView error = new ModelAndView("error");
error.addObject("error", e.getMessage());
return error;
}
}
在這裡:
- @ControllerAdvice 表示這是一個增強版的 Controller,主要用來做全局資料處理
- @ExceptionHandler 表示這是一個異常處理方法,這個注解的參數,表示需要攔截的異常,參數為 Exception 表示攔截所有異常,這裡也可以具體到某一個異常,如果具體到某一個異常,那麼發生了其他異常則不會被攔截到。
- 異常方法的定義,和 Controller 中方法的定義一樣,可以傳回 ModelAndview,也可以傳回 String 或者 void
例如如下代碼,指揮攔截檔案上傳異常,其他異常和它沒關系,不會進入到自定義異常處理的方法中來。
@ControllerAdvice//表示這是一個增強版的 Controller,主要用來做全局資料處理
public class MyException {
@ExceptionHandler(MaxUploadSizeExceededException.class)
public ModelAndView fileuploadException(MaxUploadSizeExceededException e) {
ModelAndView error = new ModelAndView("error");
error.addObject("error", e.getMessage());
return error;
}
}
服務端資料校驗
B/S 系統中對http 請求資料的校驗多數在用戶端進行,這也是出于簡單及使用者體驗性上考慮,但是在一些安全性要求高的系統中服務端校驗是不可缺少的,實際上,幾乎所有的系統,凡是涉及到資料校驗,都需要在服務端進行二次校驗。為什麼要在服務端進行二次校驗呢?這需要了解用戶端校驗和服務端校驗各自的目的。
- 用戶端校驗,我們主要是為了提高使用者體驗,例如使用者輸入一個郵箱位址,要校驗這個郵箱位址是否合法,沒有必要發送到服務端進行校驗,直接在前端用 js 進行校驗即可。但是大家需要明白的是,前端校驗無法代替後端校驗,前端校驗可以有效的提高使用者體驗,但是無法確定資料完整性,因為在 B/S 架構中,使用者可以友善的拿到請求位址,然後直接發送請求,傳遞非法參數。
- 服務端校驗,雖然使用者體驗不好,但是可以有效的保證資料安全與完整性。
- 綜上,實際項目中,兩個一起用。
Spring 支援JSR-303 驗證架構,JSR-303 是 JAVA EE 6 中的一項子規範,叫做 Bean Validation,官方參考實作是 Hibernate Validator(與Hibernate ORM 沒有關系),JSR-303 用于對 Java Bean 中的字段的值進行驗證。
普通校驗
普通校驗,是這裡最基本的用法。
首先,我們需要加入校驗需要的依賴:
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.1.0.Final</version>
</dependency>
接下來,在 SpringMVC 的配置檔案中配置校驗的 Bean:
<bean class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean" id="validatorFactoryBean">
<property name="providerClass" value="org.hibernate.validator.HibernateValidator"/>
</bean>
<mvc:annotation-driven validator="validatorFactoryBean"/>
配置時,提供一個 LocalValidatorFactoryBean 的執行個體,然後 Bean 的校驗使用 HibernateValidator。
這樣,配置就算完成了。
接下來,我們提供一個添加學生的頁面:
<form action="/addstudent" method="post">
<table>
<tr>
<td>學生編号:</td>
<td><input type="text" name="id"></td>
</tr>
<tr>
<td>學生姓名:</td>
<td><input type="text" name="name"></td>
</tr>
<tr>
<td>學生郵箱:</td>
<td><input type="text" name="email"></td>
</tr>
<tr>
<td>學生年齡:</td>
<td><input type="text" name="age"></td>
</tr>
<tr>
<td colspan="2">
<input type="submit" value="送出">
</td>
</tr>
</table>
</form>
在這裡需要送出的資料中,假設學生編号不能為空,學生姓名長度不能超過 10 且不能為空,郵箱位址要合法,年齡不能超過 150。那麼在定義實體類的時候,就可以加入這個判斷條件了。
public class Student {
@NotNull
private Integer id;
@NotNull
@Size(min = 2,max = 10)
private String name;
@Email
private String email;
@Max(150)
private Integer age;
...
}
- @NotNull 表示這個字段不能為空
- @Size 中描述了這個字元串長度的限制
- @Email 表示這個字段的值必須是一個郵箱位址
- @Max 表示這個字段的最大值
定義完成後,接下來,在 Controller 中定義接口:
@Controller
public class StudentController {
@RequestMapping("/addstudent")
@ResponseBody
public void addStudent(@Validated Student student, BindingResult result) {
if (result != null) {
//校驗未通過,擷取所有的異常資訊并展示出來
List<ObjectError> allErrors = result.getAllErrors();
for (ObjectError allError : allErrors) {
System.out.println(allError.getObjectName()+":"+allError.getDefaultMessage());
}
}
}
}
- @Validated 表示 Student 中定義的校驗規則将會生效
- BindingResult 表示出錯資訊,如果這個變量不為空,表示有錯誤,否則校驗通過。
接下來就可以啟動項目了。通路 jsp 頁面,然後添加 Student,檢視校驗規則是否生效。
預設情況下,列印出來的錯誤資訊時系統預設的錯誤資訊,這個錯誤資訊,我們也可以自定義。自定義方式如下:
由于 properties 檔案中的中文會亂碼,是以需要我們先修改一下 IDEA 配置,點 File–>Settings->Editor–>File Encodings,如下:
然後定義錯誤提示文本,在 resources 目錄下建立一個 MyMessage.properties 檔案,内容如下:
student.id.notnull=id 不能為空
student.name.notnull=name不能為空
student.name.length=name最小長度為 2 ,最大長度為10
student.email.error=email位址非法
student.age.error=年齡不能超過 150
接下來,在 SpringMVC 配置中,加載這個配置檔案:
<bean class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean" id="validatorFactoryBean">
<property name="providerClass" value="org.hibernate.validator.HibernateValidator"/>
<property name="validationMessageSource" ref="bundleMessageSource"/>
</bean>
<bean class="org.springframework.context.support.ReloadableResourceBundleMessageSource" id="bundleMessageSource">
<property name="basenames">
<list>
<value>classpath:MyMessage</value>
</list>
</property>
<property name="defaultEncoding" value="UTF-8"/>
<property name="cacheSeconds" value="300"/>
</bean>
<mvc:annotation-driven validator="validatorFactoryBean"/>
最後,在實體類上,加上校驗出錯時的消息
public class Student {
@NotNull(message = "{student.id.notnull}")
private Integer id;
@NotNull(message = "{student.name.notnull}")
@Size(min = 2,max = 10,message = "{student.name.length}")
private String name;
@Email(message = "{student.email.error}")
private String email;
@Max(value = 150,message = "{student.age.error}")
private Integer age;
...
}
配置完成後,如果校驗再出錯,就會展示我們自己的出錯資訊了。
分組校驗
由于校驗規則都是定義在實體類上面的,但是,在不同的資料送出環境下,校驗規則可能不一樣。例如,使用者的 id 是自增長的,添加的時候,可以不用傳遞使用者 id,但是修改的時候則必須傳遞使用者 id,這種情況下,就需要使用分組校驗。
分組校驗,首先需要定義校驗組,所謂的校驗組,其實就是空接口:
public interface ValidationGroup1 {
}
public interface ValidationGroup2 {
}
然後,在實體類中,指定每一個校驗規則所屬的組:
public class Student {
@NotNull(message = "{student.id.notnull}",groups = ValidationGroup1.class)
private Integer id;
@NotNull(message = "{student.name.notnull}",groups = {ValidationGroup1.class, ValidationGroup2.class})
@Size(min = 2,max = 10,message = "{student.name.length}",groups = {ValidationGroup1.class, ValidationGroup2.class})
private String name;
@Email(message = "{student.email.error}",groups = {ValidationGroup1.class, ValidationGroup2.class})
private String email;
@Max(value = 150,message = "{student.age.error}",groups = {ValidationGroup2.class})
private Integer age;
...
}
在 group 中指定每一個校驗規則所屬的組,一個規則可以屬于一個組,也可以屬于多個組。
最後,在接收參數的地方,指定校驗組:
@Controller
public class StudentController {
@RequestMapping("/addstudent")
@ResponseBody
public void addStudent(@Validated(ValidationGroup2.class) Student student, BindingResult result) {
if (result != null) {
//校驗未通過,擷取所有的異常資訊并展示出來
List<ObjectError> allErrors = result.getAllErrors();
for (ObjectError allError : allErrors) {
System.out.println(allError.getObjectName()+":"+allError.getDefaultMessage());
}
}
}
}
配置完成後,屬于 ValidationGroup2 這個組的校驗規則,才會生效。
校驗注解
@Null 被注解的元素必須為 null
@NotNull 被注解的元素必須不為 null
@AssertTrue 被注解的元素必須為 true
@AssertFalse 被注解的元素必須為 false
@Min(value) 被注解的元素必須是一個數字,其值必須大于等于指定的最小值
@Max(value) 被注解的元素必須是一個數字,其值必須小于等于指定的最大值
@DecimalMin(value) 被注解的元素必須是一個數字,其值必須大于等于指定的最小值
@DecimalMax(value) 被注解的元素必須是一個數字,其值必須小于等于指定的最大值
@Size(max=, min=) 被注解的元素的大小必須在指定的範圍内
@Digits (integer, fraction) 被注解的元素必須是一個數字,其值必須在可接受的範圍内
@Past 被注解的元素必須是一個過去的日期
@Future 被注解的元素必須是一個将來的日期
@Pattern(regex=,flag=) 被注解的元素必須符合指定的正規表達式
@NotBlank(message =) 驗證字元串非 null,且長度必須大于0
@Email 被注解的元素必須是電子郵箱位址
@Length(min=,max=) 被注解的字元串的大小必須在指定的範圍内
@NotEmpty 被注解的字元串的必須非空
@Range(min=,max=,message=) 被注解的元素必須在合适的範圍内
資料回顯
資料回顯就是當使用者資料送出失敗時,自動填充好已經輸入的資料,一般來說,如果是使用Ajax來做資料送出,基本上是沒有資料回顯這個需求的,但是如果通過表單做資料送出,那麼資料回顯就非常必要了。
簡單資料類型資料回顯
簡單資料類型,實際上架構在這裡沒有提供任何形式的支援,就是我們自己手動配置。加入送出的 Student 資料不符合要求,那麼重新回到添加 Student 頁面,并且預設之前已經填好的資料。
首先我們先來改造一下 student.jsp 頁面:
<form action="/addstudent" method="post">
<table>
<tr>
<td>學生編号:</td>
<td><input type="text" name="id" value="${id}"></td>
</tr>
<tr>
<td>學生姓名:</td>
<td><input type="text" name="name" value="${name}"></td>
</tr>
<tr>
<td>學生郵箱:</td>
<td><input type="text" name="email" value="${email}"></td>
</tr>
<tr>
<td>學生年齡:</td>
<td><input type="text" name="age" value="${age}"></td>
</tr>
<tr>
<td colspan="2">
<input type="submit" value="送出">
</td>
</tr>
</table>
</form>
在接收資料時,使用簡單資料類型去接收:
@RequestMapping("/addstudent")
public String addStudent2(Integer id, String name, String email, Integer age, Model model) {
model.addAttribute("id", id);
model.addAttribute("name", name);
model.addAttribute("email", email);
model.addAttribute("age", age);
return "student";
}
這種方式,相當于架構沒有做任何工作,就是我們手動做資料回顯的。此時通路頁面,服務端會再次定位到該頁面,而且資料已經預填好。
實體類資料回顯
簡單資料類型的回顯,實際上非常麻煩,因為需要開發者在服務端一個一個手動設定。如果使用對象的話,就沒有這麼麻煩了,因為 SpringMVC 在頁面跳轉時,會自動将對象填充進傳回的資料中。
<form action="/addstudent" method="post">
<table>
<tr>
<td>學生編号:</td>
<td><input type="text" name="id" value="${student.id}"></td>
</tr>
<tr>
<td>學生姓名:</td>
<td><input type="text" name="name" value="${student.name}"></td>
</tr>
<tr>
<td>學生郵箱:</td>
<td><input type="text" name="email" value="${student.email}"></td>
</tr>
<tr>
<td>學生年齡:</td>
<td><input type="text" name="age" value="${student.age}"></td>
</tr>
<tr>
<td colspan="2">
<input type="submit" value="送出">
</td>
</tr>
</table>
</form>
注意,在預填資料中,多了一個 student. 字首。這 student 就是服務端接收資料的變量名,服務端的變量名和這裡的 student 要保持一直。服務端定義如下:
@RequestMapping("/addstudent")
public String addStudent(@Validated(ValidationGroup2.class) Student student, BindingResult result) {
if (result != null) {
//校驗未通過,擷取所有的異常資訊并展示出來
List<ObjectError> allErrors = result.getAllErrors();
for (ObjectError allError : allErrors) {
System.out.println(allError.getObjectName()+":"+allError.getDefaultMessage());
}
return "student";
}
return "hello";
}
注意,服務端什麼都不用做,就說要傳回的頁面就行了,student 這個變量會被自動填充到傳回的 Model 中。變量名就是填充時候的 key。如果想自定義這個 key,可以在參數中寫出來 Model,然後手動加入 Student 對象,就像簡單資料類型回顯那樣。
另一種定義回顯變量别名的方式,就是使用 @ModelAttribute 注解。
ModelAttribute
@ModelAttribute 這個注解,主要有兩方面的功能:
- 在資料回顯時,給變量定義别名
- 定義全局資料
定義别名
在資料回顯時,給變量定義别名,非常容易,直接加這個注解即可
@RequestMapping("/addstudent")
public String addStudent(@ModelAttribute("s") @Validated(ValidationGroup2.class) Student student, BindingResult result) {
if (result != null) {
//校驗未通過,擷取所有的異常資訊并展示出來
List<ObjectError> allErrors = result.getAllErrors();
for (ObjectError allError : allErrors) {
System.out.println(allError.getObjectName()+":"+allError.getDefaultMessage());
}
return "student";
}
return "hello";
}
這樣定義完成後,在前端再次通路回顯的變量時,變量名稱就不是 student 了,而是 s:
<form action="/addstudent" method="post">
<table>
<tr>
<td>學生編号:</td>
<td><input type="text" name="id" value="${s.id}"></td>
</tr>
<tr>
<td>學生姓名:</td>
<td><input type="text" name="name" value="${s.name}"></td>
</tr>
<tr>
<td>學生郵箱:</td>
<td><input type="text" name="email" value="${s.email}"></td>
</tr>
<tr>
<td>學生年齡:</td>
<td><input type="text" name="age" value="${s.age}"></td>
</tr>
<tr>
<td colspan="2">
<input type="submit" value="送出">
</td>
</tr>
</table>
</form>
假設有一個 Controller 中有很多方法,每個方法都會傳回資料給前端,但是每個方法傳回給前端的資料又不太一樣,雖然不太一樣,但是沒有方法的傳回值又有一些公共的部分。可以将這些公共的部分提取出來單獨封裝成一個方法,用 @ModelAttribute 注解來标記。
例如在一個 Controller 中 ,添加如下代碼:
@ModelAttribute("info")
public Map<String,Object> info() {
Map<String, Object> map = new HashMap<>();
map.put("username", "javaboy");
map.put("address", "www.javaboy.org");
return map;
}
當使用者通路目前 Controller 中的任意一個方法,在傳回資料時,都會将添加了 @ModelAttribute 注解的方法的傳回值,一起傳回給前端。@ModelAttribute 注解中的 info 表示傳回資料的 key。
JSON處理
接收JSON
浏覽器傳來的參數,可以是 key/value 形式的,也可以是一個 JSON 字元串。在 Jsp/Servlet 中,我們接收 key/value 形式的參數,一般是通過 getParameter 方法。如果用戶端商戶傳的是 JSON 資料,我們可以通過如下格式進行解析:
@RequestMapping("/addbook2")
@ResponseBody
public void addBook2(HttpServletRequest req) throws IOException {
ObjectMapper om = new ObjectMapper();
Book book = om.readValue(req.getInputStream(), Book.class);
System.out.println(book);
}
但是這種解析方式有點麻煩,在 SpringMVC 中,我們可以通過一個注解來快速的将一個 JSON 字元串轉為一個對象:
@RequestMapping("/addbook3")
@ResponseBody
public void addBook3(@RequestBody Book book) {
System.out.println(book);
}
這樣就可以直接收到前端傳來的 JSON 字元串了。這也是 HttpMessageConverter 提供的第二個功能。
傳回JSON
目前主流的 JSON 處理工具主要有三種:
jackson
jackson 是一個使用比較多,時間也比較長的 JSON 處理工具,在 SpringMVC 中使用 jackson ,隻需要添加 jackson 的依賴即可:
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.10.1</version>
</dependency>
依賴添加成功後,凡是在接口中直接傳回的對象,集合等等,都會自動轉為 JSON。如下:
public class Book {
private Integer id;
private String name;
private String author;
...
}
@RequestMapping("/book")
@ResponseBody
public Book getBookById() {
Book book = new Book();
book.setId(1);
book.setName("三國演義");
book.setAuthor("羅貫中");
return book;
}
這裡傳回一個對象,但是在前端接收到的則是一個 JSON 字元串,這個對象會通過 HttpMessageConverter 自動轉為 JSON 字元串。
如果想傳回一個 JSON 數組,寫法如下:
@RequestMapping("/books")
@ResponseBody
public List<Book> getAllBooks() {
List<Book> list = new ArrayList<Book>();
for (int i = 0; i < 10; i++) {
Book book = new Book();
book.setId(i);
book.setName("三國演義:" + i);
book.setAuthor("羅貫中:" + i);
list.add(book);
}
return list;
}
添加了 jackson ,就能夠自動傳回 JSON,這個依賴于一個名為 HttpMessageConverter 的類,這本身是一個接口,從名字上就可以看出,它的作用是 Http 消息轉換器,既然是消息轉換器,它提供了兩方面的功能:
- 将傳回的對象轉為 JSON
- 将前端送出上來的 JSON 轉為對象
但是,HttpMessageConverter 隻是一個接口,由各個 JSON 工具提供相應的實作,在 jackson 中,實作的名字叫做 MappingJackson2HttpMessageConverter,而這個東西的初始化,則由 SpringMVC 來完成。除非自己有一些自定義配置的需求,否則一般來說不需要自己提供 MappingJackson2HttpMessageConverter。
舉一個簡單的應用場景,例如每一本書,都有一個出版日期,修改 Book 類如下:
public class Book {
private Integer id;
private String name;
private String author;
private Date publish;
...
}
然後在構造 Book 時添加日期屬性:
@RequestMapping("/book")
@ResponseBody
public Book getBookById() {
Book book = new Book();
book.setId(1);
book.setName("三國演義");
book.setAuthor("羅貫中");
book.setPublish(new Date());
return book;
}
通路 /book 接口,傳回的 json 格式如下:
如果我們想自己定制傳回日期的格式,簡單的辦法,可以通過添加注解來實作:
public class Book {
private Integer id;
private String name;
private String author;
@JsonFormat(pattern = "yyyy-MM-dd",timezone = "Asia/Shanghai")
private Date publish;
注意這裡一定要設定時區。
這樣,就可以定制傳回的日期格式了。
但是,這種方式有一個弊端,這個注解可以加在屬性上,也可以加在類上,也就說,最大可以作用到一個類中的所有日期屬性上。如果項目中有很多實體類都需要做日期格式化,使用這種方式就比較麻煩了,這個時候,我們可以自己提供一個 jackson 的 HttpMesageConverter 執行個體,在這個執行個體中,自己去配置相關屬性,這裡的配置将是一個全局配置。
在 SpringMVC 配置檔案中,添加如下配置:
<mvc:annotation-driven>
<mvc:message-converters>
<ref bean="httpMessageConverter"/>
</mvc:message-converters>
</mvc:annotation-driven>
<bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter" id="httpMessageConverter">
<property name="objectMapper">
<bean class="com.fasterxml.jackson.databind.ObjectMapper">
<property name="dateFormat">
<bean class="java.text.SimpleDateFormat">
<constructor-arg name="pattern" value="yyyy-MM-dd HH:mm:ss"/>
</bean>
</property>
<property name="timeZone" value="Asia/Shanghai"/>
</bean>
</property>
</bean>
添加完成後,去掉 Book 實體類中日期格式化的注解,再進行測試,結果如下:
gson
gson 是 Google 推出的一個 JSON 解析器,主要在 Android 開發中使用較多,不過,Web 開發中也是支援這個的,而且 SpringMVC 還針對 Gson 提供了相關的自動化配置,以緻我們在項目中隻要添加 gson 依賴,就可以直接使用 gson 來做 JSON 解析了。
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.6</version>
</dependency>
如果項目中,同時存在 jackson 和 gson 的話,那麼預設使用的是 jackson,為什麼呢?在 org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter 類的構造方法中,加載順序就是先加載 jackson 的 HttpMessageConverter,後加載 gson 的 HttpMessageConverter。
加完依賴之後,就可以直接傳回 JSON 字元串了。使用 Gson 時,如果想做自定義配置,則需要自定義 HttpMessageConverter。
<mvc:annotation-driven>
<mvc:message-converters>
<ref bean="httpMessageConverter"/>
</mvc:message-converters>
</mvc:annotation-driven>
<bean class="org.springframework.http.converter.json.GsonHttpMessageConverter" id="httpMessageConverter">
<property name="gson">
<bean class="com.google.gson.Gson" factory-bean="gsonBuilder" factory-method="create"/>
</property>
</bean>
<bean class="com.google.gson.GsonBuilder" id="gsonBuilder">
<property name="dateFormat" value="yyyy-MM-dd"/>
</bean>
fastjson
fastjson 号稱最快的 JSON 解析器,但是也是這三個中 BUG 最多的一個。在 SpringMVC 并沒針對 fastjson 提供相應的 HttpMessageConverter,是以,fastjson 在使用時,一定要自己手動配置 HttpMessageConverter(前面兩個如果沒有特殊需要,直接添加依賴就可以了)。
使用 fastjson,我們首先添加 fastjson 依賴:
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.60</version>
</dependency>
然後在 SpringMVC 的配置檔案中配置 HttpMessageConverter:
<mvc:annotation-driven>
<mvc:message-converters>
<ref bean="httpMessageConverter"/>
</mvc:message-converters>
</mvc:annotation-driven>
<bean class="com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter" id="httpMessageConverter">
<property name="fastJsonConfig">
<bean class="com.alibaba.fastjson.support.config.FastJsonConfig">
<property name="dateFormat" value="yyyy-MM-dd"/>
</bean>
</property>
</bean>
fastjson 預設中文亂碼,添加如下配置解決:
<mvc:annotation-driven>
<mvc:message-converters>
<ref bean="httpMessageConverter"/>
</mvc:message-converters>
</mvc:annotation-driven>
<bean class="com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter" id="httpMessageConverter">
<property name="fastJsonConfig">
<bean class="com.alibaba.fastjson.support.config.FastJsonConfig">
<property name="dateFormat" value="yyyy-MM-dd"/>
</bean>
</property>
<property name="supportedMediaTypes">
<list>
<value>application/json;charset=utf-8</value>
</list>
</property>
</bean>
在 SpringMVC 中,對 jackson 和 gson 都提供了相應的支援,就是如果使用這兩個作為 JSON 轉換器,隻需要添加對應的依賴就可以了,傳回的對象和傳回的集合、Map 等都會自動轉為 JSON,但是,如果使用 fastjson,除了添加相應的依賴之外,還需要自己手動配置 HttpMessageConverter 轉換器。其實前兩個也是使用 HttpMessageConverter 轉換器,但是是 SpringMVC 自動提供的,SpringMVC 沒有給 fastjson 提供相應的轉換器。
靜态資源通路
方式一
在 SpringMVC 中,靜态資源,預設都是被攔截的,例如 html、js、css、jpg、png、txt、pdf 等等,都是無法直接通路的。因為所有請求都被攔截了,是以,針對靜态資源,我們要做額外處理,處理方式很簡單,直接在 SpringMVC 的配置檔案中,添加如下内容:
<mvc:resources mapping="/static/html/**" location="/static/html/"/>
mapping 表示映射規則,也是攔截規則,就是說,如果請求位址是 /static/html 這樣的格式的話,那麼對應的資源就去 /static/html/ 這個目錄下查找。
在映射路徑的定義中,最後是兩個 *,這是一種 Ant 風格的路徑比對符号,一共有三個通配符:
統配符 | 含義 |
---|---|
** | 比對多層路徑 |
* | 比對一層路徑 |
? | 比對任意單個字元 |
一個比較原始的配置方式可能如下:
<mvc:resources mapping="/static/html/**" location="/static/html/"/>
<mvc:resources mapping="/static/js/**" location="/static/js/"/>
<mvc:resources mapping="/static/css/**" location="/static/css/"/>
但是,由于 ** 可以表示多級路徑,是以,以上配置,我們可以進行簡化:
<mvc:resources mapping="/**" location="/"/>
方式二
<servlet-mapping>
<servlet-name>default</servlet-name>
<url-pattern>*.jpg</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>default</servlet-name>
<url-pattern>*.js</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>default</servlet-name>
<url-pattern>*.css</url-pattern>
</servlet-mapping>
如果你所用的Web應用伺服器的預設Servlet名稱不是"default",則需要通過default-servlet-name屬性顯示指定:
<mvc:default-servlet-handler default-servlet-name="所使用的Web伺服器預設使用的Servlet名稱" />
方式三
//将靜态資源交給servlet處理
<mvc:default-servlet-handler/> //方式一和三可被攔截器攔截
優雅REST風格的資源URL不希望帶
.html
或
.do
等字尾.由于早期的Spring MVC不能很好地處理靜态資源,是以在
web.xml
中配置
DispatcherServlet
的請求映射,往往使用
.do
、
.html
等方式。這就決定了請求URL必須是一個帶字尾的URL,而無法采用真正的REST風格的URL。
如果将DispatcherServlet請求映射配置為"/",則Spring MVC将捕獲Web容器所有的請求,包括靜态資源的請求,Spring MVC會将它們當成一個普通請求處理,是以找不到對應處理器将導緻錯誤。
如何讓Spring架構能夠捕獲所有URL的請求,同時又将靜态資源的請求轉由Web容器處理,是可将DispatcherServlet的請求映射配置為"/"的前提。由于REST是Spring3.0最重要的功能之一,是以Spring團隊很看重靜态資源處理這項任務,給出了堪稱經典的兩種解決方案。
先調整web.xml中的DispatcherServlet的配置,使其可以捕獲所有的請求:
在springMVC-servlet.xml中配置
<mvc:default-servlet-handler />
後,會在SpringMVC上下文中定義一個
org.springframework.web.servlet.resource.DefaultServletHttpRequestHandler
,它會像一個檢查員,對進入DispatcherServlet的URL進行篩查,如果發現是靜态資源的請求,就将該請求轉由Web應用伺服器預設的Servlet處理,如果不是靜态資源的請求,才由DispatcherServlet繼續處理。
一般Web應用伺服器預設的Servlet名稱是"default",是以
DefaultServletHttpRequestHandler
可以找到它。如果你所有的Web應用伺服器的預設Servlet名稱不是"default",則需要通過default-servlet-name屬性顯示指定:
<mvc:default-servlet-handler default-servlet-name="所使用的Web伺服器預設使用的Servlet名稱" />
攔截器
SpringMVC 中的攔截器,相當于 Jsp/Servlet 中的過濾器,隻不過攔截器的功能更為強大。
攔截器的定義非常容易:
@Component
public class MyInterceptor1 implements HandlerInterceptor {
/**
* 這個是請求預處理的方法,隻有當這個方法傳回值為 true 的時候,後面的方法才會執行
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("MyInterceptor1:preHandle");
return true;
}
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("MyInterceptor1:postHandle");
}
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("MyInterceptor1:afterCompletion");
}
}
@Component
public class MyInterceptor2 implements HandlerInterceptor {
/**
* 這個是請求預處理的方法,隻有當這個方法傳回值為 true 的時候,後面的方法才會執行
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("MyInterceptor2:preHandle");
return true;
}
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("MyInterceptor2:postHandle");
}
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("MyInterceptor2:afterCompletion");
}
}
攔截器定義好之後,需要在 SpringMVC 的配置檔案中進行配置:
<mvc:interceptors>
<mvc:interceptor>
<mvc:mapping path="/**"/>
<ref bean="myInterceptor1"/>
</mvc:interceptor>
<mvc:interceptor>
<mvc:mapping path="/**"/>
<ref bean="myInterceptor2"/>
</mvc:interceptor>
</mvc:interceptors>
如果存在多個攔截器,攔截規則如下:
- preHandle 按攔截器定義順序調用
- postHandler 按攔截器定義逆序調用
- afterCompletion 按攔截器定義逆序調用
- postHandler 在攔截器鍊内所有攔截器返成功調用
- afterCompletion 隻有 preHandle 傳回 true 才調用