阅读PDF版本

本文会以一些例子来展现Spring MVC的常见功能和一些扩展点,然后我们来讨论一下Spring MVC好用不好用。

使用SpringBoot快速开始

基于之前的parent模块,我们来创建一个新的模块:

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
  3. xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  4. <modelVersion>4.0.0</modelVersion>
  5. <groupId>me.josephzhu</groupId>
  6. <artifactId>spring101-webmvc</artifactId>
  7. <version>0.0.1-SNAPSHOT</version>
  8. <packaging>jar</packaging>
  9. <name>spring101-webmvc</name>
  10. <description></description>
  11. <parent>
  12. <groupId>me.josephzhu</groupId>
  13. <artifactId>spring101</artifactId>
  14. <version>0.0.1-SNAPSHOT</version>
  15. </parent>
  16. <dependencies>
  17. <dependency>
  18. <groupId>org.springframework.boot</groupId>
  19. <artifactId>spring-boot-starter-web</artifactId>
  20. </dependency>
  21. <dependency>
  22. <groupId>org.springframework.boot</groupId>
  23. <artifactId>spring-boot-starter-thymeleaf</artifactId>
  24. </dependency>
  25. </dependencies>
  26. <build>
  27. <plugins>
  28. <plugin>
  29. <groupId>org.springframework.boot</groupId>
  30. <artifactId>spring-boot-maven-plugin</artifactId>
  31. </plugin>
  32. </plugins>
  33. </build>
  34. </project>

使用web来启用Spring MVC,使用thymeleaf来启用thymeleaf模板引擎。Thymeleaf是一个强大的Java模板引擎,可以脱离于Web单独使用,本身就有非常多的可配置可扩展的点,这里不展开讨论,详见官网。

接下去我们创建主程序:

  1. package me.josephzhu.spring101webmvc;
  2. import org.springframework.boot.SpringApplication;
  3. import org.springframework.boot.autoconfigure.SpringBootApplication;
  4. @SpringBootApplication
  5. public class Spring101WebmvcApplication {
  6. public static void main(String[] args) {
  7. SpringApplication.run(Spring101WebmvcApplication.class, args);
  8. }
  9. }
  10. 以及一个测试Controller
  11. package me.josephzhu.spring101webmvc;
  12. import org.springframework.stereotype.Controller;
  13. import org.springframework.web.bind.annotation.GetMapping;
  14. import org.springframework.web.servlet.ModelAndView;
  15. import java.util.stream.Collectors;
  16. import java.util.stream.IntStream;
  17. @Controller
  18. public class MyController {
  19. @GetMapping("shop")
  20. public ModelAndView shop() {
  21. ModelAndView modelAndView = new ModelAndView();
  22. modelAndView.setViewName("shop");
  23. modelAndView.addObject("items",
  24. IntStream.range(1, 5)
  25. .mapToObj(i -> new MyItem("item" + i, i * 100))
  26. .collect(Collectors.toList()));
  27. return modelAndView;
  28. }
  29. }

这里使用到了一个自定义的类:

  1. package me.josephzhu.spring101webmvc;
  2. import lombok.AllArgsConstructor;
  3. import lombok.Data;
  4. @AllArgsConstructor
  5. @Data
  6. public class MyItem {
  7. private String name;
  8. private Integer price;
  9. }

最后我们需要在resources目录下创建一个templates目录,在目录下再创建一个shop.html模板文件:

  1. <!DOCTYPE html SYSTEM "http://www.thymeleaf.org/dtd/xhtml1-strict-thymeleaf-4.dtd">
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>Hello Shop</title>
  6. </head>
  7. <body>
  8. Hello Shop
  9. <table>
  10. <tr th:each="item : ${items}">
  11. <td th:text="${item.name}">...</td>
  12. <td th:text="${item.price}">...</td>
  13. </tr>
  14. </table>
  15. </body>
  16. </html>

我们看到有了SpringBoot,创建一个Spring MVC程序整个过程非常简单:

1 引入starter

2 创建@Controller,设置@RequestMapping

3 创建模板文件

没有任何配置工作,一切都是starter自动配置。

快速配置ViewController

几乎所有Spring MVC的扩展点都集成在了接口中,要进行扩展很简单,实现这个接口,加上@Configuration和@EnableWebMvc注解,实现需要的方法即可。

我们先用它来快速配置一些ViewController:

  1. package me.josephzhu.spring101webmvc;
  2. import org.springframework.context.annotation.Configuration;
  3. import org.springframework.format.FormatterRegistry;
  4. import org.springframework.http.HttpStatus;
  5. import org.springframework.web.method.support.HandlerMethodArgumentResolver;
  6. import org.springframework.web.servlet.config.annotation.*;
  7. import org.springframework.web.servlet.resource.GzipResourceResolver;
  8. import org.springframework.web.servlet.resource.VersionResourceResolver;
  9. import java.util.List;
  10. @EnableWebMvc
  11. @Configuration
  12. public class WebConfig implements WebMvcConfigurer {
  13. @Override
  14. public void addViewControllers(ViewControllerRegistry registry) {
  15. registry.addViewController("/hello").setViewName("helloworld");
  16. registry.addRedirectViewController("/", "/hello");
  17. registry.addStatusController("/user", HttpStatus.BAD_REQUEST);
  18. }
  19. }

代码中多贴了一些后面会用到的import在这里可以忽略。这里我们配置了三套策略:

1 访问/会跳转到/hello

2 访问/hello会访问helloworld这个view

3 访问/user会给出400的错误代码

这里我们在templats目录再添加一个空白的helloworld.html:

  1. <!DOCTYPE html SYSTEM "http://www.thymeleaf.org/dtd/xhtml1-strict-thymeleaf-4.dtd">
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>Hello World</title>
  6. </head>
  7. <body>
  8. Hello World
  9. </body>
  10. </html>

这种配置方式可以省一些代码量,但是我个人认为在这里做配置可读性一般。

定制路径匹配

我们还可以实现路径匹配策略的定制:

  1. @Override
  2. public void configurePathMatch(PathMatchConfigurer configurer) {
  3. configurer.setUseTrailingSlashMatch(false);
  4. }

比如这样就关闭了结尾为/的匹配(默认开启)。试着访问http://localhost:8080/shop/得到如下错误:

  1. 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'

这个方法可以针对路径匹配进行相当多的配置,具体请参见文档,这里只列出了其中的一个功能。

配置静态资源

在配置类加上下面的代码:

  1. @Override
  2. public void addResourceHandlers(ResourceHandlerRegistry registry) {
  3. registry.addResourceHandler("/static/**")
  4. .addResourceLocations("classpath:/static/")
  5. .resourceChain(true)
  6. .addResolver(new GzipResourceResolver())
  7. .addResolver(new VersionResourceResolver()
  8. .addFixedVersionStrategy("1.0.0", "/**"));
  9. }

这就实现了静态资源路由到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的使用者是客户端应用程序,这是不是一个挺常见的需求)。

  1. package me.josephzhu.spring101webmvc;
  2. import lombok.Data;
  3. @Data
  4. public class MyDevice {
  5. private String type;
  6. private String version;
  7. private String screen;
  8. }

然后是自定义的HandlerMethodArgumentResolver实现:

  1. package me.josephzhu.spring101webmvc;
  2. import org.springframework.core.MethodParameter;
  3. import org.springframework.web.bind.support.WebDataBinderFactory;
  4. import org.springframework.web.context.request.NativeWebRequest;
  5. import org.springframework.web.method.support.HandlerMethodArgumentResolver;
  6. import org.springframework.web.method.support.ModelAndViewContainer;
  7. public class DeviceHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver {
  8. @Override
  9. public boolean supportsParameter(MethodParameter methodParameter) {
  10. return methodParameter.getParameterType().equals(MyDevice.class);
  11. }
  12. @Override
  13. public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer modelAndViewContainer, NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory) throws Exception {
  14. MyDevice myDevice = new MyDevice();
  15. myDevice.setType(nativeWebRequest.getHeader("device.type"));
  16. myDevice.setVersion(nativeWebRequest.getHeader("device.version"));
  17. myDevice.setScreen(nativeWebRequest.getHeader("device.screen"));
  18. return myDevice;
  19. }
  20. }

实现分两部分,第一部分告诉框架,我们这个ArgumentResolver支持解析怎么样的参数。这里我们的实现是根据参数类型,还有很多时候可以通过检查是否参数上有额外的自定义注解来实现(后面也会有例子)。第二部分就是真正的实现了,实现非常简单,从请求头里获取相应的信息构成我们的MyDevice对象。

要让这个Resolver被MVC框架识别到,我们需要继续扩展刚才的WebConfig类,加入下面的代码:

  1. @Override
  2. public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
  3. resolvers.add(new DeviceHandlerMethodArgumentResolver());
  4. }

然后,我们写一个例子来测试一下:

  1. package me.josephzhu.spring101webmvc;
  2. import lombok.extern.slf4j.Slf4j;
  3. import org.springframework.web.bind.annotation.*;
  4. import java.util.ArrayList;
  5. import java.util.List;
  6. import java.util.stream.Collectors;
  7. import java.util.stream.IntStream;
  8. @RestController
  9. @Slf4j
  10. @RequestMapping("api")
  11. public class MyRestController {
  12. @RequestMapping(value = "items", method = RequestMethod.GET)
  13. public List<MyItem> getItems(MyDevice device) {
  14. log.debug("Device : " + device);
  15. List<MyItem> myItems = new ArrayList<>();
  16. myItems.add(new MyItem("aa", 10));
  17. myItems.add(new MyItem("bb", 20));
  18. return myItems;
  19. }
  20. }

这里因为用了debug,所以需要在配置文件中打开debug日志级别:

  1. logging.level.me.josephzhu.spring101webmvc=DEBUG

测试一下:

  1. curl -X GET \
  2. http://localhost:8080/api/items \
  3. -H 'device.screen: 1280*800' \
  4. -H 'device.type: android' \
  5. -H 'device.version: 1.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的数据、成功与否结果、错误消息、签名等等内容,这样客户端可以做签名验证,然后是根据成功与否来决定是要解析数据还是直接提示错误,比如:

  1. package me.josephzhu.spring101webmvc;
  2. import lombok.AllArgsConstructor;
  3. import lombok.Data;
  4. @Data
  5. @AllArgsConstructor
  6. public class APIResponse<T> {
  7. T data;
  8. boolean success;
  9. String message;
  10. String sign;
  11. }

如果我们在每个API方法中去返回这样的APIResponse当然可以实现这个效果,还有一种通用的实现方式是使用ResponseBodyAdvice:

  1. package me.josephzhu.spring101webmvc;
  2. import org.springframework.core.MethodParameter;
  3. import org.springframework.http.MediaType;
  4. import org.springframework.http.converter.HttpMessageConverter;
  5. import org.springframework.http.server.ServerHttpRequest;
  6. import org.springframework.http.server.ServerHttpResponse;
  7. import org.springframework.web.bind.annotation.ControllerAdvice;
  8. import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
  9. @ControllerAdvice
  10. public class APIResponseBodyAdvice implements ResponseBodyAdvice<Object> {
  11. @Override
  12. public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
  13. return !returnType.getParameterType().equals(APIResponse.class);
  14. }
  15. @Override
  16. public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
  17. String sign = "";
  18. Sign signAnnotation = returnType.getMethod().getAnnotation(Sign.class);
  19. if (signAnnotation != null)
  20. sign = "abcd";
  21. return new APIResponse(body, true, "", sign);
  22. }
  23. }

通过定义@ControllerAdvice注解来启用这个Advice。在实现上也是两部分,第一部分告诉框架我们这个Advice支持的是非APIResponse类型(如果返回的对象已经是APIResponse了,我们当然就不需要再包装一次了)。第二部分是实现,这里的实现很简单,我们先检查一下方法上是否有Sign这个注解,如果有的话进行签名(这里的逻辑是写死的签名),然后把得到的body塞入APIResponse后返回。

这里补上Sign注解的实现:

  1. package me.josephzhu.spring101webmvc;
  2. import java.lang.annotation.ElementType;
  3. import java.lang.annotation.Retention;
  4. import java.lang.annotation.RetentionPolicy;
  5. import java.lang.annotation.Target;
  6. @Target(ElementType.METHOD)
  7. @Retention(RetentionPolicy.RUNTIME)
  8. public @interface Sign {
  9. }

这是一个空注解,没啥可以说的,下面我们来测试一下这个ResponseBodyAdvice:

  1. @RequestMapping(value = "item/{id}", method = RequestMethod.GET)
  2. public MyItem getItem(@PathVariable("id") String id) {
  3. Integer i = null;
  4. try {
  5. i = Integer.parseInt(id);
  6. } catch (NumberFormatException ex) {
  7. }
  8. if (i == null || i < 1)
  9. throw new IllegalArgumentException("不合法的商品ID");
  10. return new MyItem("item" + id, 10);
  11. }

访问http://localhost:8080/api/item/23后得到如下图的结果:

是不是很方便呢?这个API包装的过程可以由框架进行,无需每次手动来做。

自定义异常处理

如果我们访问http://localhost:8080/api/item/0会看到错误白页,针对错误处理,我们希望:

1 可以使用统一的APIResponse方式进行错误返回

2 可以记录错误信息以便查看

实现这个功能非常简单,我们可以通过@ExceptionHandler实现:

  1. package me.josephzhu.spring101webmvc;
  2. import lombok.extern.slf4j.Slf4j;
  3. import org.springframework.web.bind.annotation.ControllerAdvice;
  4. import org.springframework.web.bind.annotation.ExceptionHandler;
  5. import org.springframework.web.bind.annotation.ResponseBody;
  6. import org.springframework.web.bind.annotation.RestController;
  7. import org.springframework.web.method.HandlerMethod;
  8. import javax.servlet.http.HttpServletRequest;
  9. @ControllerAdvice(annotations = RestController.class)
  10. @Slf4j
  11. public class MyRestExceptionHandler {
  12. @ExceptionHandler
  13. @ResponseBody
  14. public APIResponse handle(HttpServletRequest req, HandlerMethod method, Exception ex) {
  15. log.error(String.format("访问 %s -> %s 出错了!", req.getRequestURI(), method.toString()), ex);
  16. return new APIResponse(null, false, ex.getMessage(), "");
  17. }
  18. }

注意几点:

1 我们可以使用@ControllerAdvice的annotations来关联我们需要拦截的Controller类型

2 handle方法支持相当多的参数,可谓是要啥有啥,这里贴下官方文档说明的截图(在这里我们使用了ServletRequest来获取请求地址,使用了HandlerMethod来获取当前执行的方法):

访问地址http://localhost:8080/api/item/sd可以看到如下输出:

(注意,处理签名的ResponseBodyAdvice并不会针对这个返回进行处理,因为之前实现的时候我们就判断了返回内容不是APIResponse才去处理,在自己正式的实现中你可以实现的更合理,让签名的处理逻辑同时适用出现异常的情况)日志中也出现了错误信息:

  1. 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) 出错了!
  2. java.lang.IllegalArgumentException: 不合法的商品ID
  3. at me.josephzhu.spring101webmvc.MyRestController.getItem(MyRestController.java:34) ~[classes/:na]
  4. at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_161]
  5. at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_161]
  6. at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_161]
  7. at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_161]
  8. at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:209) ~[spring-web-5.0.9.RELEASE.jar:5.0.9.RELEASE]

自动处理参数类型转换

比如有这么一个需求,我们希望可以接受自定义的枚举作为参数,而且枚举的名字不一定需要和请求的参数完全大小写匹配,这个时候我们需要实现自己的转换器:

  1. package me.josephzhu.spring101webmvc;
  2. import org.springframework.core.convert.converter.Converter;
  3. import org.springframework.core.convert.converter.ConverterFactory;
  4. import java.util.Arrays;
  5. public class MyConverterFactory implements ConverterFactory<String, Enum> {
  6. @Override
  7. public <T extends Enum> Converter<String, T> getConverter(Class<T> targetType) {
  8. return new String2EnumConverter(targetType);
  9. }
  10. class String2EnumConverter<T extends Enum<T>> implements Converter<String, T> {
  11. private Class<T> enumType;
  12. private String2EnumConverter(Class<T> enumType) {
  13. this.enumType = enumType;
  14. }
  15. @Override
  16. public T convert(String source) {
  17. return Arrays.stream(enumType.getEnumConstants())
  18. .filter(e -> e.name().equalsIgnoreCase(source))
  19. .findAny().orElse(null);
  20. }
  21. }
  22. }

这里实现了一个从字符串到自定义枚举的转换,在搜索枚举名字的时候我们忽略了大小写。

接下去我们通过WebConfig来注册这个转换器工厂:

  1. @Override
  2. public void addFormatters(FormatterRegistry registry) {
  3. registry.addConverterFactory(new MyConverterFactory());
  4. }

来写一段代码测试一下:

  1. @GetMapping("search")
  2. public List<MyItem> search(@RequestParam("type") ItemTypeEnum itemTypeEnum) {
  3. return IntStream.range(1, 5)
  4. .mapToObj(i -> new MyItem(itemTypeEnum.name() + i, i * 100))
  5. .collect(Collectors.toList());
  6. }

这是一个Get请求的API,接受一个type参数,参数是一个自定义枚举:

  1. package me.josephzhu.spring101webmvc;
  2. public enum ItemTypeEnum {
  3. BOOK, TOY, TOOL
  4. }

很明显枚举的名字都是大写的,我们来访问一下地址http://localhost:8080/api/search?type=TOy 测试一下程序是否可以正确匹配:

TOy的搜索参数匹配到了TOY枚举,结果符合我们的预期。

自定义拦截器

最后,我们来看看Spring MVC最通用的扩展点,也就是拦截器。

这个图清晰展现了拦截器几个重要方法事件节点。在这个例子中,我们利用preHandle和postHandle两个方法实现可以统计请求执行耗时的拦截器:

  1. package me.josephzhu.spring101webmvc;
  2. import lombok.extern.slf4j.Slf4j;
  3. import org.springframework.web.servlet.ModelAndView;
  4. import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
  5. import javax.servlet.http.HttpServletRequest;
  6. import javax.servlet.http.HttpServletResponse;
  7. @Slf4j
  8. public class ExecutionTimeHandlerInterceptor extends HandlerInterceptorAdapter {
  9. private static final String START_TIME_ATTR_NAME = "startTime";
  10. private static final String EXECUTION_TIME_ATTR_NAME = "executionTime";
  11. @Override
  12. public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
  13. long startTime = System.currentTimeMillis();
  14. request.setAttribute(START_TIME_ATTR_NAME, startTime);
  15. return true;
  16. }
  17. @Override
  18. public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
  19. }
  20. @Override
  21. public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
  22. long startTime = (Long) request.getAttribute(START_TIME_ATTR_NAME);
  23. long endTime = System.currentTimeMillis();
  24. long executionTime = endTime - startTime;
  25. String time = "[" + handler + "] executeTime : " + executionTime + "ms";
  26. if (modelAndView != null) {
  27. modelAndView.addObject(EXECUTION_TIME_ATTR_NAME, time);
  28. }
  29. log.debug(time);
  30. }
  31. }

在实现的时候,我们不仅仅把执行时间输出到了日志,而且还通过修改ModelAndView对象把这个信息加入到了视图模型内,这样页面也可以展现这个时间。要启用拦截器,我们还需要配置WebConfig:

  1. @Override
  2. public void addInterceptors(InterceptorRegistry registry) {
  3. registry.addInterceptor(new ExecutionTimeHandlerInterceptor());
  4. }

接下去我们运行刚才那个例子,可以看到如下的日志输出:

  1. 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

页面上也可以引用到我们添加进去的对象:

  1. <!DOCTYPE html SYSTEM "http://www.thymeleaf.org/dtd/xhtml1-strict-thymeleaf-4.dtd">
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>Hello World</title>
  6. </head>
  7. <body>
  8. Hello World
  9. <div th:text="${executionTime}"></div>
  10. </body>
  11. </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这样设计我也能理解。

朱晔和你聊Spring系列S1E4:灵活但不算好用的Spring MVC的更多相关文章

  1. Spring系列之IOC的原理及手动实现

    目录 Spring系列之IOC的原理及手动实现 Spring系列之DI的原理及手动实现 导语 Spring是一个分层的JavaSE/EE full-stack(一站式) 轻量级开源框架.也是几乎所有J ...

  2. 朱晔和你聊Spring系列S1E6:容易犯错的Spring AOP

    阅读PDF版本 标题有点标题党了,这里说的容易犯错不是Spring AOP的错,是指使用的时候容易犯错.本文会以一些例子来展开讨论AOP的使用以及使用过程中容易出错的点. 几句话说清楚AOP 有关必要 ...

  3. 朱晔和你聊Spring系列S1E2:SpringBoot并不神秘

    朱晔和你聊Spring系列S1E2:SpringBoot并不神秘 [编辑器丢失了所有代码的高亮,建议查看PDF格式文档] 文本我们会一步一步做一个例子来看看SpringBoot的自动配置是如何实现的, ...

  4. 朱晔和你聊Spring系列S1E1:聊聊Spring家族的几大件

    朱晔和你聊Spring系列S1E1:聊聊Spring家族的几大件 [下载本文PDF进行阅读] Spring家族很庞大,从最早先出现的服务于企业级程序开发的Core.安全方面的Security.到后来的 ...

  5. 朱晔和你聊Spring系列S1E11:小测Spring Cloud Kubernetes @ 阿里云K8S

    有关Spring Cloud Kubernates(以下简称SCK)详见https://github.com/spring-cloud/spring-cloud-kubernetes,在本文中我们主要 ...

  6. 朱晔和你聊Spring系列S1E8:凑活着用的Spring Cloud(含一个实际业务贯穿所有组件的完整例子)

    本文会以一个简单而完整的业务来阐述Spring Cloud Finchley.RELEASE版本常用组件的使用.如下图所示,本文会覆盖的组件有: Spring Cloud Netflix Zuul网关 ...

  7. 朱晔和你聊Spring系列S1E9:聊聊Spring的那些注解

    本文我们来梳理一下Spring的那些注解,如下图所示,大概从几方面列出了Spring的一些注解: 如果此图看不清楚也没事,请运行下面的代码输出所有的结果. Spring目前的趋势是使用注解结合Java ...

  8. Spring系列之手写一个SpringMVC

    目录 Spring系列之IOC的原理及手动实现 Spring系列之DI的原理及手动实现 Spring系列之AOP的原理及手动实现 Spring系列之手写注解与配置文件的解析 引言 在前面的几个章节中我 ...

  9. Spring系列(零) Spring Framework 文档中文翻译

    Spring 框架文档(核心篇1和2) Version 5.1.3.RELEASE 最新的, 更新的笔记, 支持的版本和其他主题,独立的发布版本等, 是在Github Wiki 项目维护的. 总览 历 ...

随机推荐

  1. HTML+JS+JQuery不可以使用status

    可能是JQuery的内部定义了status的原因!在HTML中的元素如果声明了ID为status的话,脚本中是不能访问这个对象的,会成为一个字符串对象.

  2. 2016-04-25-信息系统实践手记6-JS调用Flex的性能问题一例

    layout: post title: 2016-04-25-信息系统实践手记6-JS调用Flex的性能问题一例 key: 20160425 tags: GIS JS FLEX 技术选型 性能 API ...

  3. Scrapy实现腾讯招聘网信息爬取【Python】

    一.腾讯招聘网 二.代码实现 1.spider爬虫 # -*- coding: utf-8 -*- import scrapy from Tencent.items import TencentIte ...

  4. 【mongoDB高级篇③】综合实战(1): 分析国家地震数据

    数据准备 下载国家地震数据 http://data.earthquake.cn/data/ 通过navicat导入到数据库,方便和mysql语句做对比 shard分片集群配置 # step 1 mkd ...

  5. NVIDIA显卡笔记本安装ubuntu驱动以及分辨率之详解

    随着对ubuntu的了解,突然想在自己的笔记本上装一个双系统.在网上查了安装方法之后,发现因为nvidia显卡的原因会出现一些问题,结果在我自己装了之后发现问题要比看到的多,再看了无数个帖子之后,最终 ...

  6. SqlServer执行Insert命令同时判断目标表中是否存在目标数据

    针对于已查询出数据结果, 且在程序中执行Sql命令, 而非数据库中的存储过程 INSERT INTO TableName (Column1, Column2, Column3, Column4, Co ...

  7. python Django 文件下载示例

    from django.http import StreamingHttpResponse#文件流 def big_file_download(request): # do something... ...

  8. c/c++链队列

    链队列 链队列就是简化了的单链表 nodequeue.h #ifndef __NODEQUEUE__ #define __NODEQUEUE__ #include <stdio.h> #i ...

  9. 4.9Python数据处理篇之Matplotlib系列(九)---子图分布

    目录 目录 前言 (一)subplot()方法 ==1.语法说明== ==2.源代码== ==3.输出效果== (二)subplot2grid方法 ==1.语法说明== ==2.源代码== ==3.展 ...

  10. hashcode相等两个类一定相等吗?equals呢?相反呢?

    hashCode相等,equals也不一定相等, 两个类也不一定相等 equals相同, 说明是同一个对象, 那么hashCode一定相同 哈希表是结合了直接寻址和链式寻址两种方式,所需要的就是将需要 ...