1. 程式人生 > >朱曄和你聊Spring系列S1E4:靈活但不算好用的Spring MVC

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

iat ndt css host 4.0 ttr found zip壓縮 return

本文會以一些例子來展現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後得到如下圖的結果:
技術分享圖片

是不是很方便呢?這個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來獲取當前執行的方法):
技術分享圖片

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

https://www.cnblogs.com/lovecindywang/p/9738024.html

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