思維導圖
![](https://img.laitimes.com/img/__Qf2AjLwojIjJCLyojI0JCLiIXZ05WZj91YpB3IwczX0xiRGZkRGZ0Xy9GbvNGL2EzXlpXazxSPRR1T1UEVh9GczoVd5cUYsx2MMBjVtJWd0ckW65UbM5WOHJWa5kHT20ESjBjUIF2X0hXZ0xCMx81dvRWYoNHLrdEZwZ1Rh5WNXp1bwNjW1ZUba9VZwlHdssmch1mclRXY39CXldWYtlWPzNXZj9mcw1ycz9WL49zZuBnL5MDO1AjNwAjMwMDOwAjMwIzLc52YucWbp5GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.png)
微信公衆号已開啟:【java技術愛好者】,還沒關注的記得關注哦~
文章已收錄到我的Github精選,歡迎Star:
https://github.com/yehongzhi/learningSummary
概述
SpringMVC再熟悉不過的架構了,因為現在最火的SpringBoot的内置MVC架構就是SpringMVC。我寫這篇文章的動機是想通過回顧總結一下,重新認識SpringMVC,所謂溫故而知新嘛。
為了了解SpringMVC,先看一個流程示意圖:
從流程圖中,我們可以看到:
- 接收前端傳過來Request請求。
- 根據映射路徑找到對應的處理器處理請求,處理完成之後傳回ModelAndView。
- 進行視圖解析,視圖渲染,傳回響應結果。
總結就是:參數接收,定義映射路徑,頁面跳轉,傳回響應結果。
當然這隻是最基本的核心功能,除此之外還可以定義攔截器,全局異常處理,檔案上傳下載下傳等等。
一、搭建項目
在以前的老項目中,因為還沒有SpringBoot,沒有自動配置,是以需要使用web.xml檔案去定義一個DispatcherServlet。現在網際網路應用基本上都使用SpringBoot,是以我就直接使用SpringBoot進行示範。很簡單,引入依賴即可:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
二、定義Controller
使用SpringMVC定義Controller處理器,總共有五種方式。
2.1 實作Controller接口
早期的SpringMVC是通過這種方式定義:
/**
* @author Ye Hongzhi 公衆号:java技術愛好者
* @name DemoController
* @date 2020-08-25 22:28
**/
@org.springframework.stereotype.Controller("/demo/controller")
public class DemoController implements Controller {
@Override
public ModelAndView handleRequest(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws Exception {
//業務處理
return null;
}
}
2.2 實作HttpRequestHandler接口
跟第一種方式差不多,也是通過實作接口的方式:
/**
* @author Ye Hongzhi 公衆号:java技術愛好者
* @name HttpDemoController
* @date 2020-08-25 22:45
**/
@Controller("/http/controller")
public class HttpDemoController implements HttpRequestHandler{
@Override
public void handleRequest(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws ServletException, IOException {
//業務處理
}
}
2.3 實作Servlet接口
這種方式已經不推薦使用了,不過從這裡可以看出SpringMVC的底層使用的還是Servlet。
@Controller("/servlet/controller")
public class ServletDemoController implements Servlet {
//以下是Servlet生命周期方法
@Override
public void init(ServletConfig servletConfig) throws ServletException {
}
@Override
public ServletConfig getServletConfig() {
return null;
}
@Override
public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
}
@Override
public String getServletInfo() {
return null;
}
@Override
public void destroy() {
}
}
因為不推薦使用這種方式,是以預設是不加載這種擴充卡的,需要加上:
@Configuration
@EnableWebMvc
public class WebMvcConfig extends WebMvcConfigurerAdapter {
@Bean
public SimpleServletHandlerAdapter simpleServletHandlerAdapter() {
return new SimpleServletHandlerAdapter();
}
}
2.4 使用@RequestMapping
這種方式是最常用的,因為上面那些方式定義需要使用一個類定義一個路徑,就會導緻産生很多類。使用注解就相對輕量級一些。
@Controller
@RequestMapping("/requestMapping/controller")
public class RequestMappingController {
@RequestMapping("/demo")
public String demo() {
return "HelloWord";
}
}
2.4.1 支援Restful風格
而且支援Restful風格,使用method屬性定義對資源的操作方式:
@RequestMapping(value = "/restful", method = RequestMethod.GET)
public String get() {
//查詢
return "get";
}
@RequestMapping(value = "/restful", method = RequestMethod.POST)
public String post() {
//建立
return "post";
}
@RequestMapping(value = "/restful", method = RequestMethod.PUT)
public String put() {
//更新
return "put";
}
@RequestMapping(value = "/restful", method = RequestMethod.DELETE)
public String del() {
//删除
return "post";
}
2.4.2 支援Ant風格
//比對 /antA 或者 /antB 等URL
@RequestMapping("/ant?")
public String ant() {
return "ant";
}
//比對 /ant/a/create 或者 /ant/b/create 等URL
@RequestMapping("/ant/*/create")
public String antCreate() {
return "antCreate";
}
//比對 /ant/create 或者 /ant/a/b/create 等URL
@RequestMapping("/ant/**/create")
public String antAllCreate() {
return "antAllCreate";
}
2.5 使用HandlerFunction
最後一種是使用HandlerFunction函數式接口,這是
Spring5.0
後引入的方式,主要用于做響應式接口的開發,也就是Webflux的開發。
有興趣的可以網上搜尋相關資料學習,這個講起來可能要很大篇幅,這裡就不贅述了。
三、接收參數
定義完Controller之後,需要接收前端傳入的參數,怎麼接收呢。
3.1 接收普通參數
在@RequestMapping映射方法上寫上接收參數名即可:
@RequestMapping(value = "/restful", method = RequestMethod.POST)
public String post(Integer id, String name, int money) {
System.out.println("id:" + id + ",name:" + name + ",money:" + money);
return "post";
}
3.2 @RequestParam參數名綁定
如果不想使用形參名稱作為參數名稱,可以使用@RequestParam進行參數名稱綁定:
/**
* value: 參數名
* required: 是否request中必須包含此參數,預設是true。
* defaultValue: 預設參數值
*/
@RequestMapping(value = "/restful", method = RequestMethod.GET)
public String get(@RequestParam(value = "userId", required = false, defaultValue = "0") String id) {
System.out.println("id:" + id);
return "get";
}
3.3 @PathVariable路徑參數
通過@PathVariable将URL中的占位符{xxx}參數映射到操作方法的入參。示範代碼如下:
@RequestMapping(value = "/restful/{id}", method = RequestMethod.GET)
public String search(@PathVariable("id") String id) {
System.out.println("id:" + id);
return "search";
}
3.4 @RequestHeader綁定請求頭屬性
擷取請求頭的資訊怎麼擷取呢?
使用@RequestHeader注解,用法和@RequestParam類似:
@RequestMapping("/head")
public String head(@RequestHeader("Accept-Language") String acceptLanguage) {
return acceptLanguage;
}
3.5 @CookieValue綁定請求的Cookie值
擷取Request中Cookie的值:
@RequestMapping("/cookie")
public String cookie(@CookieValue("_ga") String _ga) {
return _ga;
}
3.6 綁定請求參數到POJO對象
定義了一個User實體類:
public class User {
private String id;
private String name;
private Integer age;
//getter、setter方法
}
定義一個@RequestMapping操作方法:
@RequestMapping("/body")
public String body(User user) {
return user.toString();
}
隻要請求參數與屬性名相同自動填充到user對象中:
3.6.1 支援級聯屬性
現在多了一個Address類存儲位址資訊:
public class Address {
private String id;
private String name;
//getter、setter方法
}
在User中加上address屬性:
public class User {
private String id;
private String name;
private Integer age;
private Address address;
//getter、setter方法
}
傳參時隻要傳入address.name、address.id即會自動填充:
3.6.2 @InitBinder解決接收多對象時屬性名沖突
如果有兩個POJO對象擁有相同的屬性名,不就産生沖突了嗎?比如剛剛的user和address,其中他們都有id和name這兩個屬性,如果同時接收,就會沖突:
//user和address都有id和name這兩個屬性
@RequestMapping(value = "/twoBody", method = RequestMethod.POST)
public String twoBody(User user, Address address) {
return user.toString() + "," + address.toString();
}
這時就可以使用@InitBinder綁定參數名稱:
@InitBinder("user")
public void initBindUser(WebDataBinder webDataBinder) {
webDataBinder.setFieldDefaultPrefix("u.");
}
@InitBinder("address")
public void initBindAddress(WebDataBinder webDataBinder) {
webDataBinder.setFieldDefaultPrefix("addr.");
}
3.6.3 @Requestbody自動解析JSON字元串封裝到對象
前端傳入一個json字元串,自動轉換成pojo對象,示範代碼:
@RequestMapping(value = "/requestBody", method = RequestMethod.POST)
public String requestBody(@RequestBody User user) {
return user.toString();
}
注意的是,要使用POST請求,發送端的Content-Type設定為application/json,資料是json字元串:
甚至有一些人喜歡用一個Map接收:
但是千萬不要用Map接收,否則會造成代碼很難維護,後面的老哥估計看不懂你這個Map裡面有什麼資料,是以最好還是定義一個POJO對象。
四、參數類型轉換
實際上,SpringMVC架構本身就内置了很多類型轉換器,比如你傳入字元串的數字,接收的入參定為int,long類型,都會自動幫你轉換。
就在包org.springframework.core.convert.converter下,如圖所示:
有的時候如果内置的類型轉換器不足夠滿足業務需求呢,怎麼擴充呢,很簡單,看我操作。什麼是Java技術愛好者(戰術後仰)。
首先有樣學樣,内置的轉換器實作Converter接口,我也實作:
public class StringToDateConverter implements Converter<String, Date> {
@Override
public Date convert(String source) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
try {
//String轉換成Date類型
return sdf.parse(source);
} catch (Exception e) {
//類型轉換錯誤
e.printStackTrace();
}
return null;
}
}
接着把轉換器注冊到Spring容器中:
@Configuration
public class ConverterConfig extends WebMvcConfigurationSupport {
@Override
protected void addFormatters(FormatterRegistry registry) {
//添加類型轉換器
registry.addConverter(new StringToDateConverter());
}
}
接着看測試,所有的日期字元串,都自動被轉換成Date類型了,非常友善:
五、頁面跳轉
在前後端未分離之前,頁面跳轉的工作都是由後端控制,采用JSP進行展示資料。雖然現在網際網路項目幾乎不會再使用JSP,但是我覺得還是需要學習一下,因為有些舊項目還是會用JSP,或者需要重構。
如果你在RequestMapping方法中直接傳回一個字元串是不會跳轉到指定的JSP頁面的,需要做一些配置。
第一步,加入解析jsp的Maven配置。
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
<version>7.0.59</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
</dependency>
第二步,添加視圖解析器。
@Configuration
public class WebAppConfig extends WebMvcConfigurerAdapter {
@Bean
public InternalResourceViewResolver viewResolver() {
InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
viewResolver.setPrefix("/");
viewResolver.setSuffix(".jsp");
viewResolver.setViewClass(JstlView.class);
return viewResolver;
}
}
第三步,設定IDEA的配置。
第四步,建立jsp頁面。
第五步,建立Controller控制器。
@Controller
@RequestMapping("/view")
public class ViewController {
@RequestMapping("/hello")
public String hello() throws Exception {
return "hello";
}
}
這樣就完成了,啟動項目,通路/view/hello就看到了:
就是這麼簡單,對吧
六、@ResponseBody
如果采用前後端分離,頁面跳轉不需要後端控制了,後端隻需要傳回json即可,怎麼傳回呢?
使用@ResponseBody注解即可,這個注解會把對象自動轉成json資料傳回。
@ResponseBody注解可以放在類或者方法上,源碼如下:
//用在類、方法上
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ResponseBody {
}
示範一下:
@RequestMapping("/userList")
@ResponseBody
public List<User> userList() throws Exception {
List<User> list = new ArrayList<>();
list.add(new User("1","姚大秋",18));
list.add(new User("2","李星星",18));
list.add(new User("3","冬敏",18));
return list;
}
測試一下/view/userList:
七、@ModelAttribute
@ModelAttribute用法比較多,下面一一講解。
7.1 用在無傳回值的方法上
在Controller類中,在執行所有的RequestMapping方法前都會先執行@ModelAttribute注解的方法。
@Controller
@RequestMapping("/modelAttribute")
public class ModelAttributeController {
//先執行這個方法
@ModelAttribute
public void modelAttribute(Model model){
//在request域中放入資料
model.addAttribute("userName","公衆号:java技術愛好者");
}
@RequestMapping("/index")
public String index(){
//跳轉到inex.jsp頁面
return "index";
}
}
index.jsp頁面如下:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>首頁</title>
</head>
<body>
<!-- 擷取到userName屬性值 -->
<h1>${userName}</h1>
</body>
</html>
相當于一個Controller的攔截器一樣,在執行RequestMapping方法前先執行@ModelAttribute注解的方法。是以要慎用。
啟動項目,通路/modelAttribute/index可以看到:
即使在index()方法中沒有放入userName屬性值,jsp頁面也能擷取到,因為在執行index()方法之前的modelAttribute()方法已經放入了。
7.2 放在有傳回值的方法上
其實調用順序是一樣,也是在RequestMapping方法前執行,不同的在于,方法的傳回值直接幫你放入到Request域中。
//放在有參數的方法上
@ModelAttribute
public User userAttribute() {
//相當于model.addAttribute("user",new User("1", "Java技術愛好者", 18));
return new User("1", "Java技術愛好者", 18);
}
@RequestMapping("/user")
public String user() {
return "user";
}
建立一個user.jsp:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>首頁</title>
</head>
<body>
<h1>ID:${user.id}</h1>
<h1>名稱:${user.name}</h1>
<h1>年齡:${user.age}歲</h1>
</body>
</html>
測試一下:
放入Request域中的屬性值預設是類名的首字母小寫駝峰寫法,如果你想自定義呢?很簡單,可以這樣寫:
//自定義屬性名為"u"
@ModelAttribute("u")
public User userAttribute() {
return new User("1", "Java技術愛好者", 18);
}
/**
JSP就要改成這樣寫:
<h1>ID:${u.id}</h1>
<h1>名稱:${u.name}</h1>
<h1>年齡:${u.age}歲</h1>
*/
7.3 放在RequestMapping方法上
@Controller
@RequestMapping("/modelAttribute")
public class ModelAttributeController {
@RequestMapping("/jojo")
@ModelAttribute("attributeName")
public String jojo() {
return "JOJO!我不做人了!";
}
}
這種情況下RequestMapping方法的傳回的值就不是JSP視圖了。而是把傳回值放入Request域中的屬性值,屬性名為attributeName。視圖則是RequestMapping注解上的URL,是以建立一個對應的JSP頁面:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>首頁</title>
</head>
<body>
<h1>${attributeName}</h1>
</body>
</html>
7.4 放在方法入參上
放在入參上,意思是從前面的Model中提取出對應的屬性值,當做入參傳入方法中使用。如下所示:
@ModelAttribute("u")
public User userAttribute() {
return new User("1", "Java技術愛好者", 18);
}
@RequestMapping("/java")
public String user1(@ModelAttribute("u") User user) {
//拿到@ModelAttribute("u")方法傳回的值,列印出來
System.out.println("user:" + user);
return "java";
}
八、攔截器
攔截器算重點内容了,很多時候都要用攔截器,比如登入校驗,權限校驗等等。SpringMVC怎麼添加攔截器呢?
很簡單,實作HandlerInterceptor接口,接口有三個方法需要重寫。
- preHandle():在業務處理器處理請求之前被調用。預處理。
- postHandle():在業務處理器處理請求執行完成後,生成視圖之前執行。後處理。
- afterCompletion():在DispatcherServlet完全處理完請求後被調用,可用于清理資源等。傳回處理(已經渲染了頁面);
自定義的攔截器,實作的接口HandlerInterceptor:
public class DemoInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//預處理,傳回true則繼續執行。如果需要登入校驗,校驗不通過傳回false即可,通過則傳回true。
System.out.println("執行preHandle()方法");
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
//後處理
System.out.println("執行postHandle()方法");
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//在DispatcherServlet完全處理完請求後被調用
System.out.println("執行afterCompletion()方法");
}
}
然後把攔截器添加到Spring容器中:
@Configuration
public class ConverterConfig extends WebMvcConfigurationSupport {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new DemoInterceptor()).addPathPatterns("/**");
}
}
/**
代表所有路徑,測試一下:
九、全局異常處理
SpringMVC本身就對一些異常進行了全局處理,是以有内置的異常處理器,在哪裡呢?
看
HandlerExceptionResolver
接口的類圖就知道了:
從類圖可以看出有四種異常處理器:
-
,預設的異常處理器。根據各個不同類型的異常,傳回不同的異常視圖。DefaultHandlerExceptionResolver
-
,簡單映射異常處理器。通過配置異常類和view的關系來解析異常。SimpleMappingExceptionResolver
-
,狀态碼異常處理器。解析帶有ResponseStatusExceptionResolver
注釋類型的異常。@ResponseStatus
-
,注解形式的異常處理器。對ExceptionHandlerExceptionResolver
注解的方法進行異常解析。@ExceptionHandler
第一個預設的異常處理器是内置的異常處理器,對一些常見的異常處理,一般來說不用管它。後面的三個才是需要注意的,是用來擴充的。
9.1 SimpleMappingExceptionResolver
翻譯過來就是簡單映射異常處理器。用途是,我們可以指定某種異常,當抛出這種異常之後跳轉到指定的頁面。請看示範。
第一步,添加spring-config.xml檔案,放在resources目錄下,檔案名見文知意即可:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
<!-- 定義預設的異常處理頁面 -->
<property name="defaultErrorView" value="err"/>
<!-- 定義異常處理頁面用來擷取異常資訊的屬性名,預設名為exception -->
<property name="exceptionAttribute" value="ex"/>
<!-- 定義需要特殊處理的異常,用類名或完全路徑名作為key,異常也頁名作為值 -->
<property name="exceptionMappings">
<props>
<!-- 異常,err表示err.jsp頁面 -->
<prop key="java.lang.Exception">err</prop>
<!-- 可配置多個prop -->
</props>
</property>
</bean>
</beans>
第二步,在啟動類加載xml檔案:
@SpringBootApplication
@ImportResource("classpath:spring-config.xml")
public class SpringmvcApplication {
public static void main(String[] args) {
SpringApplication.run(SpringmvcApplication.class, args);
}
}
第三步,在webapp目錄下建立一個err.jsp頁面:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>異常頁面</title>
</head>
<body>
<h1>出現異常,這是一張500頁面</h1>
<br>
<%-- 列印異常到頁面上 --%>
<% Exception ex = (Exception)request.getAttribute("ex"); %>
<br>
<div><%=ex.getMessage()%></div>
<% ex.printStackTrace(new java.io.PrintWriter(out)); %>
</body>
</html>
這樣就完成了,寫一個接口測試一下:
@Controller
@RequestMapping("/exception")
public class ExceptionController {
@RequestMapping("/index")
public String index(String msg) throws Exception {
if ("null".equals(msg)) {
//抛出空指針異常
throw new NullPointerException();
}
return "index";
}
}
效果如下:
這種異常處理器,在現在前後端分離的項目中幾乎已經看不到了。
9.2 ResponseStatusExceptionResolver
這種異常處理器主要用于處理帶有
@ResponseStatus
注釋的異常。請看示範代碼:
自定義一個異常類,并且使用
@ResponseStatus
注解修飾:
//HttpStatus枚舉有所有的狀态碼,這裡傳回一個400的響應碼
@ResponseStatus(value = HttpStatus.BAD_REQUEST)
public class DefinedException extends Exception{
}
寫一個Controller接口進行測試:
@RequestMapping("/defined")
public String defined(String msg) throws Exception {
if ("defined".equals(msg)) {
throw new DefinedException();
}
return "index";
}
啟動項目,測試一下,效果如下:
9.3 ExceptionHandlerExceptionResolver
注解形式的異常處理器,這是用得最多的。使用起來非常簡單友善。
第一步,定義自定義異常BaseException:
public class BaseException extends Exception {
public BaseException(String message) {
super(message);
}
}
第二步,定義一個錯誤提示實體類ErrorInfo:
public class ErrorInfo {
public static final Integer OK = 0;
public static final Integer ERROR = -1;
private Integer code;
private String message;
private String url;
//getter、setter
}
第三步,定義全局異常處理類GlobalExceptionHandler:
//這裡使用了RestControllerAdvice,是@ResponseBody和@ControllerAdvice的結合
//會把實體類轉成JSON格式的提示傳回,符合前後端分離的架構
@RestControllerAdvice
public class GlobalExceptionHandler {
//這裡自定義了一個BaseException,當抛出BaseException異常就會被此方法處理
@ExceptionHandler(BaseException.class)
public ErrorInfo errorHandler(HttpServletRequest req, BaseException e) throws Exception {
ErrorInfo r = new ErrorInfo();
r.setMessage(e.getMessage());
r.setCode(ErrorInfo.ERROR);
r.setUrl(req.getRequestURL().toString());
return r;
}
}
完成之後,寫一個測試接口:
@RequestMapping("/base")
public String base(String msg) throws Exception {
if ("base".equals(msg)) {
throw new BaseException("測試抛出BaseException異常,歐耶!");
}
return "index";
}
啟動項目,測試:
絮叨
SpringMVC的功能實際上肯定還不止我寫的這些,不過學會上面這些之後,基本上已經可以應對日常的工作了。
如果要再深入一些,最好是看看SpringMVC源碼,我之前寫過三篇,責任鍊模式與SpringMVC攔截器,擴充卡模式與SpringMVC,全局異常處理源碼分析。有興趣可以關注公衆号看看我的曆史文章。
微信公衆号已開啟:【java技術愛好者】,沒關注的同學記得關注哦~
堅持原創,持續輸出兼具廣度和深度的技術文章。
上面所有例子的代碼都上傳Github了:
https://github.com/yehongzhi/mall
覺得有用就點個贊吧,你的點贊是我創作的最大動力~
拒絕做一條鹹魚,我是一個努力讓大家記住的程式員。我們下期再見!!!
能力有限,如果有什麼錯誤或者不當之處,請大家批評指正,一起學習交流!