本文會以一些例子來展示Spring MVC的常見功能和一些擴展點,而後咱們來討論一下Spring MVC好用很差用。html
基於以前的parent模塊,咱們來建立一個新的模塊:java
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>me.josephzhu</groupId>
<artifactId>spring101-webmvc</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>spring101-webmvc</name>
<description></description>
<parent>
<groupId>me.josephzhu</groupId>
<artifactId>spring101</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
複製代碼
使用web來啓用Spring MVC,使用thymeleaf來啓用thymeleaf模板引擎。Thymeleaf是一個強大的Java模板引擎,能夠脫離於Web單獨使用,自己就有很是多的可配置可擴展的點,這裏不展開討論,詳見官網。 接下去咱們建立主程序:android
package me.josephzhu.spring101webmvc;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Spring101WebmvcApplication {
public static void main(String[] args) {
SpringApplication.run(Spring101WebmvcApplication.class, args);
}
}
以及一個測試Controller:
package me.josephzhu.spring101webmvc;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.servlet.ModelAndView;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
@Controller
public class MyController {
@GetMapping("shop")
public ModelAndView shop() {
ModelAndView modelAndView = new ModelAndView();
modelAndView.setViewName("shop");
modelAndView.addObject("items",
IntStream.range(1, 5)
.mapToObj(i -> new MyItem("item" + i, i * 100))
.collect(Collectors.toList()));
return modelAndView;
}
}
複製代碼
這裏使用到了一個自定義的類:web
package me.josephzhu.spring101webmvc;
import lombok.AllArgsConstructor;
import lombok.Data;
@AllArgsConstructor
@Data
public class MyItem {
private String name;
private Integer price;
}
複製代碼
最後咱們須要在resources目錄下建立一個templates目錄,在目錄下再建立一個shop.html模板文件:spring
<!DOCTYPE html SYSTEM "http://www.thymeleaf.org/dtd/xhtml1-strict-thymeleaf-4.dtd">
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Hello Shop</title>
</head>
<body>
Hello Shop
<table>
<tr th:each="item : ${items}">
<td th:text="${item.name}">...</td>
<td th:text="${item.price}">...</td>
</tr>
</table>
</body>
</html>
複製代碼
咱們看到有了SpringBoot,建立一個Spring MVC程序整個過程很是簡單: 1 引入starter 2 建立@Controller,設置@RequestMapping 3 建立模板文件 沒有任何配置工做,一切都是starter自動配置。apache
幾乎全部Spring MVC的擴展點都集成在了接口中,要進行擴展很簡單,實現這個接口,加上@Configuration和@EnableWebMvc註解,實現須要的方法便可。 咱們先用它來快速配置一些ViewController:api
package me.josephzhu.spring101webmvc;
import org.springframework.context.annotation.Configuration;
import org.springframework.format.FormatterRegistry;
import org.springframework.http.HttpStatus;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.*;
import org.springframework.web.servlet.resource.GzipResourceResolver;
import org.springframework.web.servlet.resource.VersionResourceResolver;
import java.util.List;
@EnableWebMvc
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/hello").setViewName("helloworld");
registry.addRedirectViewController("/", "/hello");
registry.addStatusController("/user", HttpStatus.BAD_REQUEST);
}
}
複製代碼
代碼中多貼了一些後面會用到的import在這裏能夠忽略。這裏咱們配置了三套策略: 1 訪問/會跳轉到/hello 2 訪問/hello會訪問helloworld這個view 3 訪問/user會給出400的錯誤代碼 這裏咱們在templats目錄再添加一個空白的helloworld.html:緩存
<!DOCTYPE html SYSTEM "http://www.thymeleaf.org/dtd/xhtml1-strict-thymeleaf-4.dtd">
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Hello World</title>
</head>
<body>
Hello World
</body>
</html>
複製代碼
這種配置方式能夠省一些代碼量,可是我我的認爲在這裏作配置可讀性通常。bash
咱們還能夠實現路徑匹配策略的定製:mvc
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
configurer.setUseTrailingSlashMatch(false);
}
複製代碼
好比這樣就關閉告終尾爲/的匹配(默認開啓)。試着訪問http://localhost:8080/shop/獲得以下錯誤:
2018-10-02 18:58:16.581 WARN 20264 --- [nio-8080-exec-1] o.s.web.servlet.PageNotFound : No mapping found for HTTP request with URI [/shop/] in DispatcherServlet with name 'dispatcherServlet'
複製代碼
這個方法能夠針對路徑匹配進行至關多的配置,具體請參見文檔,這裏只列出了其中的一個功能。
在配置類加上下面的代碼:
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/static/**")
.addResourceLocations("classpath:/static/")
.resourceChain(true)
.addResolver(new GzipResourceResolver())
.addResolver(new VersionResourceResolver()
.addFixedVersionStrategy("1.0.0", "/**"));
}
複製代碼
這就實現了靜態資源路由到static目錄,而且爲靜態資源啓用了Gzip壓縮和基於版本號的緩存。配置後咱們在resources目錄下建立一個static目錄,而後隨便建立一個a.html文件,試試訪問這個文件,測試能夠發現:http://localhost:8080/static/1.0.0/a.html和http://localhost:8080/static/a.html均可以訪問到這個文件。
HandlerMethodArgumentResolver接口這是一個很是很是重要經常使用的擴展點。經過這個接口,咱們能夠實現通用方法來裝配HandlerMethod上的自定義參數,咱們如今來定義一個MyDevice類型,而後咱們但願框架能夠在全部出現MyDevice參數的時候自動爲咱們從Header裏獲取相應的設備信息構成MyDevice對象(若是咱們API的使用者是客戶端應用程序,這是否是一個挺常見的需求)。
package me.josephzhu.spring101webmvc;
import lombok.Data;
@Data
public class MyDevice {
private String type;
private String version;
private String screen;
}
複製代碼
而後是自定義的HandlerMethodArgumentResolver實現:
package me.josephzhu.spring101webmvc;
import org.springframework.core.MethodParameter;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
public class DeviceHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver {
@Override
public boolean supportsParameter(MethodParameter methodParameter) {
return methodParameter.getParameterType().equals(MyDevice.class);
}
@Override
public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer modelAndViewContainer, NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory) throws Exception {
MyDevice myDevice = new MyDevice();
myDevice.setType(nativeWebRequest.getHeader("device.type"));
myDevice.setVersion(nativeWebRequest.getHeader("device.version"));
myDevice.setScreen(nativeWebRequest.getHeader("device.screen"));
return myDevice;
}
}
複製代碼
實現分兩部分,第一部分告訴框架,咱們這個ArgumentResolver支持解析怎麼樣的參數。這裏咱們的實現是根據參數類型,還有不少時候能夠經過檢查是否參數上有額外的自定義註解來實現(後面也會有例子)。第二部分就是真正的實現了,實現很是簡單,從請求頭裏獲取相應的信息構成咱們的MyDevice對象。 要讓這個Resolver被MVC框架識別到,咱們須要繼續擴展剛纔的WebConfig類,加入下面的代碼:
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new DeviceHandlerMethodArgumentResolver());
}
複製代碼
而後,咱們寫一個例子來測試一下:
package me.josephzhu.spring101webmvc;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
@RestController
@Slf4j
@RequestMapping("api")
public class MyRestController {
@RequestMapping(value = "items", method = RequestMethod.GET)
public List<MyItem> getItems(MyDevice device) {
log.debug("Device : " + device);
List<MyItem> myItems = new ArrayList<>();
myItems.add(new MyItem("aa", 10));
myItems.add(new MyItem("bb", 20));
return myItems;
}
}
複製代碼
這裏由於用了debug,因此須要在配置文件中打開debug日誌級別:
logging.level.me.josephzhu.spring101webmvc=DEBUG
複製代碼
測試一下:
curl -X GET \
http://localhost:8080/api/items \
-H 'device.screen: 1280*800' \
-H 'device.type: android' \
-H 'device.version: 1.1'
複製代碼
能夠在控制檯看到這樣的日誌:
2018-10-02 19:10:56.667 DEBUG 20325 --- [nio-8080-exec-9] m.j.spring101webmvc.MyRestController : Device : MyDevice(type=android, version=1.1, screen=1280*800)
複製代碼
能夠證實咱們方法中定義的MyDevice的確是從請求中獲取到了正確的結果。你們能夠發揮一下想象,ArgumentResolver不但能夠作相似參數自動裝配(從各個地方獲取必要的數據)的工做,並且還能夠作驗證工做。你們能夠仔細看一下resolveArgument方法的參數,是否是至關於要啥有啥了(當前參數定義、當前請求、Model容器以及綁定工廠)。
在剛纔的實現中,咱們直接返回了List數據,對於API來講,咱們通常會定義一套API的結果對象,包含API的數據、成功與否結果、錯誤消息、簽名等等內容,這樣客戶端能夠作簽名驗證,而後是根據成功與否來決定是要解析數據仍是直接提示錯誤,好比:
package me.josephzhu.spring101webmvc;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
public class APIResponse<T> {
T data;
boolean success;
String message;
String sign;
}
複製代碼
若是咱們在每一個API方法中去返回這樣的APIResponse固然能夠實現這個效果,還有一種通用的實現方式是使用ResponseBodyAdvice:
package me.josephzhu.spring101webmvc;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
@ControllerAdvice
public class APIResponseBodyAdvice implements ResponseBodyAdvice<Object> {
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
return !returnType.getParameterType().equals(APIResponse.class);
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
String sign = "";
Sign signAnnotation = returnType.getMethod().getAnnotation(Sign.class);
if (signAnnotation != null)
sign = "abcd";
return new APIResponse(body, true, "", sign);
}
}
複製代碼
經過定義@ControllerAdvice註解來啓用這個Advice。在實現上也是兩部分,第一部分告訴框架咱們這個Advice支持的是非APIResponse類型(若是返回的對象已是APIResponse了,咱們固然就不須要再包裝一次了)。第二部分是實現,這裏的實現很簡單,咱們先檢查一下方法上是否有Sign這個註解,若是有的話進行簽名(這裏的邏輯是寫死的簽名),而後把獲得的body塞入APIResponse後返回。 這裏補上Sign註解的實現:
package me.josephzhu.spring101webmvc;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Sign {
}
複製代碼
這是一個空註解,沒啥能夠說的,下面咱們來測試一下這個ResponseBodyAdvice:
@RequestMapping(value = "item/{id}", method = RequestMethod.GET)
public MyItem getItem(@PathVariable("id") String id) {
Integer i = null;
try {
i = Integer.parseInt(id);
} catch (NumberFormatException ex) {
}
if (i == null || i < 1)
throw new IllegalArgumentException("不合法的商品ID");
return new MyItem("item" + id, 10);
}
複製代碼
訪問http://localhost:8080/api/item/23後獲得以下圖的結果:
是否是很方便呢?這個API包裝的過程能夠由框架進行,無需每次手動來作。
若是咱們訪問http://localhost:8080/api/item/0會看到錯誤白頁,針對錯誤處理,咱們但願:
package me.josephzhu.spring101webmvc;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.method.HandlerMethod;
import javax.servlet.http.HttpServletRequest;
@ControllerAdvice(annotations = RestController.class)
@Slf4j
public class MyRestExceptionHandler {
@ExceptionHandler
@ResponseBody
public APIResponse handle(HttpServletRequest req, HandlerMethod method, Exception ex) {
log.error(String.format("訪問 %s -> %s 出錯了!", req.getRequestURI(), method.toString()), ex);
return new APIResponse(null, false, ex.getMessage(), "");
}
}
複製代碼
注意幾點:
訪問地址http://localhost:8080/api/item/sd能夠看到以下輸出:
(注意,處理簽名的ResponseBodyAdvice並不會針對這個返回進行處理,由於以前實現的時候咱們就判斷了返回內容不是APIResponse纔去處理,在本身正式的實現中你能夠實現的更合理,讓簽名的處理邏輯同時適用出現異常的狀況)日誌中也出現了錯誤信息:
2018-10-02 19:48:41.450 ERROR 20422 --- [nio-8080-exec-6] m.j.s.MyRestExceptionHandler : 訪問 /api/item/sd -> public me.josephzhu.spring101webmvc.MyItem me.josephzhu.spring101webmvc.MyRestController.getItem(java.lang.String) 出錯了!
java.lang.IllegalArgumentException: 不合法的商品ID
at me.josephzhu.spring101webmvc.MyRestController.getItem(MyRestController.java:34) ~[classes/:na]
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_161]
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_161]
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_161]
at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_161]
at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:209) ~[spring-web-5.0.9.RELEASE.jar:5.0.9.RELEASE]
複製代碼
好比有這麼一個需求,咱們但願能夠接受自定義的枚舉做爲參數,並且枚舉的名字不必定須要和請求的參數徹底大小寫匹配,這個時候咱們須要實現本身的轉換器:
package me.josephzhu.spring101webmvc;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.converter.ConverterFactory;
import java.util.Arrays;
public class MyConverterFactory implements ConverterFactory<String, Enum> {
@Override
public <T extends Enum> Converter<String, T> getConverter(Class<T> targetType) {
return new String2EnumConverter(targetType);
}
class String2EnumConverter<T extends Enum<T>> implements Converter<String, T> {
private Class<T> enumType;
private String2EnumConverter(Class<T> enumType) {
this.enumType = enumType;
}
@Override
public T convert(String source) {
return Arrays.stream(enumType.getEnumConstants())
.filter(e -> e.name().equalsIgnoreCase(source))
.findAny().orElse(null);
}
}
}
複製代碼
這裏實現了一個從字符串到自定義枚舉的轉換,在搜索枚舉名字的時候咱們忽略了大小寫。 接下去咱們經過WebConfig來註冊這個轉換器工廠:
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverterFactory(new MyConverterFactory());
}
複製代碼
來寫一段代碼測試一下:
@GetMapping("search")
public List<MyItem> search(@RequestParam("type") ItemTypeEnum itemTypeEnum) {
return IntStream.range(1, 5)
.mapToObj(i -> new MyItem(itemTypeEnum.name() + i, i * 100))
.collect(Collectors.toList());
}
複製代碼
這是一個Get請求的API,接受一個type參數,參數是一個自定義枚舉:
package me.josephzhu.spring101webmvc;
public enum ItemTypeEnum {
BOOK, TOY, TOOL
}
複製代碼
很明顯枚舉的名字都是大寫的,咱們來訪問一下地址http://localhost:8080/api/search?type=TOy 測試一下程序是否能夠正確匹配:
TOy的搜索參數匹配到了TOY枚舉,結果符合咱們的預期。
最後,咱們來看看Spring MVC最通用的擴展點,也就是攔截器。
這個圖清晰展示了攔截器幾個重要方法事件節點。在這個例子中,咱們利用preHandle和postHandle兩個方法實現能夠統計請求執行耗時的攔截器:
package me.josephzhu.spring101webmvc;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Slf4j
public class ExecutionTimeHandlerInterceptor extends HandlerInterceptorAdapter {
private static final String START_TIME_ATTR_NAME = "startTime";
private static final String EXECUTION_TIME_ATTR_NAME = "executionTime";
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
long startTime = System.currentTimeMillis();
request.setAttribute(START_TIME_ATTR_NAME, startTime);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
long startTime = (Long) request.getAttribute(START_TIME_ATTR_NAME);
long endTime = System.currentTimeMillis();
long executionTime = endTime - startTime;
String time = "[" + handler + "] executeTime : " + executionTime + "ms";
if (modelAndView != null) {
modelAndView.addObject(EXECUTION_TIME_ATTR_NAME, time);
}
log.debug(time);
}
}
複製代碼
在實現的時候,咱們不只僅把執行時間輸出到了日誌,並且還經過修改ModelAndView對象把這個信息加入到了視圖模型內,這樣頁面也能夠展示這個時間。要啓用攔截器,咱們還須要配置WebConfig:
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new ExecutionTimeHandlerInterceptor());
}
複製代碼
接下去咱們運行剛纔那個例子,能夠看到以下的日誌輸出:
2018-10-02 19:58:22.189 DEBUG 20422 --- [nio-8080-exec-9] m.j.s.ExecutionTimeHandlerInterceptor : [public java.util.List<me.josephzhu.spring101webmvc.MyItem> me.josephzhu.spring101webmvc.MyRestController.search(me.josephzhu.spring101webmvc.ItemTypeEnum)] executeTime : 22ms
複製代碼
頁面上也能夠引用到咱們添加進去的對象:
<!DOCTYPE html SYSTEM "http://www.thymeleaf.org/dtd/xhtml1-strict-thymeleaf-4.dtd">
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Hello World</title>
</head>
<body>
Hello World
<div th:text="${executionTime}"></div>
</body>
</html>
複製代碼
攔截器是很是通用的一個擴展,能夠全局實現權限控制、緩存、動態修改結果等等功能。
本文咱們經過一個一個例子展示了Spring MVC的一些重要擴展點:
其實Spring MVC還有不少擴展點,好比模型參數綁定和校驗、容許咱們實現動態的RequestMapping甚至是DispatcherServlet進行擴展,你能夠繼續自行研究。 最後,我想說說我對Spring MVC的見解,整體上我以爲Spring MVC實現很靈活,擴展點不少,幾乎每個組件都是鬆耦合,容許咱們本身定義和替換。可是我以爲它的實現有點過於鬆散。ASP.NET MVC的實現我就挺喜歡,相比Spring MVC,ASP.NET MVC的兩個ActionFilter和ActionResult的實現是亮點: