朱曄和你聊Spring系列S1E4:靈活但不算好用的SpringMVC

powerzhuye發表於2018-10-02

本文會以一些例子來展現Spring MVC的常見功能和一些擴充套件點,然後我們來討論一下Spring MVC好用不好用。

使用SpringBoot快速開始

基於之前的parent模組,我們來建立一個新的模組:

<?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單獨使用,本身就有非常多的可配置可擴充套件的點,這裡不展開討論,詳見官網。 接下去我們建立主程式:

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;
    }

}
複製程式碼

這裡使用到了一個自定義的類:

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模板檔案:

<!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自動配置。

快速配置ViewController

幾乎所有Spring MVC的擴充套件點都整合在了介面中,要進行擴充套件很簡單,實現這個介面,加上@Configuration和@EnableWebMvc註解,實現需要的方法即可。 我們先用它來快速配置一些ViewController:

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>
複製程式碼

這種配置方式可以省一些程式碼量,但是我個人認為在這裡做配置可讀性一般。

定製路徑匹配

我們還可以實現路徑匹配策略的定製:

@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容器以及繫結工廠)。

自定義ResponseBody後處理

在剛才的實現中,我們直接返回了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後得到如下圖的結果:

朱曄和你聊Spring系列S1E4:靈活但不算好用的SpringMVC

是不是很方便呢?這個API包裝的過程可以由框架進行,無需每次手動來做。

自定義異常處理

如果我們訪問http://localhost:8080/api/item/0會看到錯誤白頁,針對錯誤處理,我們希望:

  1. 可以使用統一的APIResponse方式進行錯誤返回
  2. 可以記錄錯誤資訊以便檢視 實現這個功能非常簡單,我們可以通過@ExceptionHandler實現:
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(), "");
    }
}
複製程式碼

注意幾點:

  1. 我們可以使用@ControllerAdvice的annotations來關聯我們需要攔截的Controller型別
  2. handle方法支援相當多的引數,可謂是要啥有啥,這裡貼下官方文件說明的截圖(在這裡我們使用了ServletRequest來獲取請求地址,使用了HandlerMethod來獲取當前執行的方法):
    朱曄和你聊Spring系列S1E4:靈活但不算好用的SpringMVC

訪問地址http://localhost:8080/api/item/sd可以看到如下輸出:

朱曄和你聊Spring系列S1E4:靈活但不算好用的SpringMVC

(注意,處理簽名的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 測試一下程式是否可以正確匹配:

朱曄和你聊Spring系列S1E4:靈活但不算好用的SpringMVC

TOy的搜尋引數匹配到了TOY列舉,結果符合我們的預期。

自定義攔截器

最後,我們來看看Spring MVC最通用的擴充套件點,也就是攔截器。

朱曄和你聊Spring系列S1E4:靈活但不算好用的SpringMVC

這個圖清晰展現了攔截器幾個重要方法事件節點。在這個例子中,我們利用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的一些重要擴充套件點:

  1. 使用攔截器做執行時間統計
  2. 自定義ResponseBodyAdvice來處理API的包裝
  3. 自定義ExceptionHandler來統計錯誤處理
  4. 自定義ConverterFactory來解析轉換列舉
  5. 自定義ArgumentResolver來組裝裝置資訊引數
  6. 快速實現靜態資源、路徑匹配以及ViewController的配置

其實Spring MVC還有很多擴充套件點,比如模型引數繫結和校驗、允許我們實現動態的RequestMapping甚至是DispatcherServlet進行擴充套件,你可以繼續自行研究。 最後,我想說說我對Spring MVC的看法,總體上我覺得Spring MVC實現很靈活,擴充套件點很多,幾乎每一個元件都是鬆耦合,允許我們自己定義和替換。但是我覺得它的實現有點過於鬆散。ASP.NET MVC的實現我就挺喜歡,相比Spring MVC,ASP.NET MVC的兩個ActionFilter和ActionResult的實現是亮點:

  • ActionFilter機制。Controller裡面的每一個方法稱作Action,我們可以在每一個Action上加上各種註解來啟用ActionFilter,ActionFilter可以針對Action執行前、後、出異常等等情況做回撥處理。ASP.NET MVC的ActionFilter的Filer級別是方法,粒度上比攔截器精細很多,而且配置更直觀。Spring MVC雖然除了攔截器還有ArgumentResolver以及ReturnValueHandler可以分別進行引數處理和返回值處理,但是這兩套擴充套件體系也是基於框架層面的,如果要和方法打通還需要自定義註解來實現。總覺得Spring MVC的這三套擴充套件點相互配合功能上雖然完整,但是有種支離破碎的感覺,如果我們真的要實現很多功能的,話可能會在這裡有相當多的if-else,沒有ActionFilter來得直觀。
  • 方法的返回值可以是ModelAndView,可以是直接輸出到@ResponseBody的自定義型別,這兩種輸出型別的分法可以滿足我們的需求,但是總感覺很彆扭。在ASP.NET MVC中的方法返回抽象為了ActionResult,可以是ViewResult、JsonResult、FileContentResult、RedirectResult、FilePathResult、JavaScriptResult等等,正如其名,看到返回值我們就可以看到方法實際的輸出表現,非常直觀容易理解。 ASP.NET MVC並沒有大量依賴IOC和AOP來實現,而是由框架的整體結構實現了外掛機制,本質上這和Spring的風格就不同,加上Spring MVC從簡化Servlet開始演化,兩者理念上的區別也決定了設計上的區別,因此Spring MVC這樣設計我也能理解。

相關文章