8.1.1 The "Spring Web MVC Framework"(Spring Boot 3.1.5 Reference 翻译)

287 阅读12分钟

8.1 Servlet Web Applications

如果想要开发基于servlet的web应用,可以很方便使用SpringBoot的auto-configuration来开发Spring MVC或Jersey。

8.1.1 The "Spring Web MVC Framework"

Spring Web MVC framework 经常被称为“Spring MVC”,是一个"model view controller"web 框架,Spring MVC使用@Controller@RestControllerbeans来处理HTTP请求。controller中被@RequestMapping注解的方法被用来匹配请求处理。

下面的代码展示了一个返回Json数据的@RestController的典型用法:

import java.util.List;

import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/users")
public class MyRestController {

    private final UserRepository userRepository;

    private final CustomerRepository customerRepository;

    public MyRestController(UserRepository userRepository, CustomerRepository customerRepository) {
        this.userRepository = userRepository;
        this.customerRepository = customerRepository;
    }

    @GetMapping("/{userId}")
    public User getUser(@PathVariable Long userId) {
        return this.userRepository.findById(userId).get();
    }

    @GetMapping("/{userId}/customers")
    public List<Customer> getUserCustomers(@PathVariable Long userId) {
        return this.userRepository.findById(userId).map(this.customerRepository::findByUser).get();
    }

    @DeleteMapping("/{userId}")
    public void deleteUser(@PathVariable Long userId) {
        this.userRepository.deleteById(userId);
    }
}

“WebMvc.fn”,将路由的配置与如何实际处理请求分开,如下所示

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.web.servlet.function.RequestPredicate;
import org.springframework.web.servlet.function.RouterFunction;
import org.springframework.web.servlet.function.ServerResponse;

import static org.springframework.web.servlet.function.RequestPredicates.accept;
import static org.springframework.web.servlet.function.RouterFunctions.route;

@Configuration(proxyBeanMethods = false)
public class MyRoutingConfiguration {

    private static final RequestPredicate ACCEPT_JSON = accept(MediaType.APPLICATION_JSON);

    @Bean
    public RouterFunction<ServerResponse> routerFunction(MyUserHandler userHandler) {
        return route()
                .GET("/{user}", ACCEPT_JSON, userHandler::getUser)
                .GET("/{user}/customers", ACCEPT_JSON, userHandler::getUserCustomers)
                .DELETE("/{user}", ACCEPT_JSON, userHandler::deleteUser)
                .build();
    }
}
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.function.ServerRequest;
import org.springframework.web.servlet.function.ServerResponse;

@Component
public class MyUserHandler {

    public ServerResponse getUser(ServerRequest request) {
        ...
        return ServerResponse.ok().build();
    }

    public ServerResponse getUserCustomers(ServerRequest request) {
        ...
        return ServerResponse.ok().build();
    }

    public ServerResponse deleteUser(ServerRequest request) {
        ...
        return ServerResponse.ok().build();
    }
}

Spring MVC是Spring Framework core的一部分,其详细信息可查看 reference documentation。同样有一些Spring Mvc相关的指导可在spring.io/guides查看。

Tip

你可以定义许多的RouterFunctionbean,你可以使用该特性实现模块化。

Spring MVC Auto-configuration

SpringBoot为Spring MVC提供了auto-configuration,它取代了@EnableWebMvc而且两者不能同时使用。SpringBoot所提供的的auto-configuration还提供了下列的features

  • 包含了ContentNegotiatingViewResolverBeanNameViewResolverbean。
  • 为静态资源提供服务,包含对WebJars的支持(稍后介绍 covered later in this document
  • Converter,GenericConverter,Formatter bean的自动注册
  • HttpMessageConverters提供支持(稍后介绍 covered later in this document
  • 静态index.html的支持
  • ConfigurableWebBindingInitializerbean的自动使用(稍后介绍 covered later in this document

如果你想对SpringBoot MVC进行自定义,定义一些interceptors, formatters, view controllers, and other features。你可以定义自己WebMvcConfigurer继承类并被@Configuration注解,但注意不用使用@EnableWebMvc注解。

如果你想定义RequestMappingHandlerMapping, RequestMappingHandlerAdapter, ExceptionHandlerExceptionResolver这些类自己的实例,你可以声明WebMvcRegistrations类型的bean来提供这些component的自定义实例。

如果你不想使用SpringBoot为SpringMvc提供的auto-configuration,要自己对SpringMvc进行完全的控制,那么请添加自己的@Configuration类并添加注解@EnableWebMvc。或者,像@EnableWebMvc的Javadoc中描述的一样,添加一个你自己被@Configuration注解的DelegatingWebMvcConfiguration

Spring MVC Conversion Service

Spring MVC使用的ConversionService来转换值,与用来从application.properties,application.yaml转换值的ConversionService不同。这意味着Period,Duration,DataSize的转换器在MVC不可用,并且@DurationUnit and @DataSizeUnit这样的注解将被忽略。

如果你想自定义SpringMvc所使用的ConversionService,你可以添加一个WebMvcConfigurerbean,并定义addFormatters方法,在该方法中你可以随意注册任何的converter。或者委托给ApplicationConversionService的静态方法。

值的转换还可以使用 spring.mvc.format.*configuration properties来进行自定义

PropertyDateTimeFormatter
spring.mvc.format.dateofLocalizedDate(FormatStyle.SHORT)
spring.mvc.format.timeofLocalizedTime(FormatStyle.SHORT)
spring.mvc.format.date-timeofLocalizedDateTime(FormatStyle.SHORT)

HttpMessageConverters

Spring MVC使用HttpMessageConverter接口来转换HTTP请求和响应,SpringBoot提供了一些合理好用的默认转换器。例如,对象可以被自动转换为JSON(使用Jackson库实现)或者XML(使用Jackson XML扩展实现或者用过JAXB如果Jackson Xml扩展未提供)。默认情况下,字符串编码方式为UTF-8

如果想要添加你自己的转换器,你可以使用SpringBoot的HttpMessageConverters

import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;

@Configuration(proxyBeanMethods = false)
public class MyHttpMessageConvertersConfiguration {

    @Bean
    public HttpMessageConverters customConverters() {
        HttpMessageConverter<?> additional = new AdditionalHttpMessageConverter();
        HttpMessageConverter<?> another = new AnotherHttpMessageConverter();
        return new HttpMessageConverters(additional, another);
    }
}

任何注册到context的HttpMessageConverterbean都会添加到转换器的集合。你可以以相同的方式覆盖默认的转换器。

MessageCodesResolver

SpringMvc使用类MessageCodesResolver生成error code的策略,用于渲染错误信息。如果你设置了spring.mvc.message-codes-resolver-format配置项PREFIX_ERROR_CODE,POSTFIX_ERROR_CODE,那么SpringBoot为为你创建一个(请查阅DefaultMessageCodesResolver.Format

Static Content

默认情况下,Spring会为下列文件夹下的静态内容提供服务,classpath下的/static (or /public or /resources or /META-INF/resources)文件夹或者ServletContext的根目录。这个特性由ResourceHttpRequestHandler类提供,所以你可以通过添加你自己的WebMvcConfigurer或者覆盖addResourceHandlers方法来修改这个行为。

对于独立的web应用,容器的默认servlet并未启用,可以通过配置项server.servlet.register-default-servlet来启用。

(?)默认的servlet起到一个备用的角色,如果Spring决定不处理它时,会为ServletContext的root提供服务。大多情况下,这并不会发生(除非你修改了默认的MVC配置),因为Spring总是通过DispatcherServlet处理请求。

默认情况下,resource被匹配到/**,但是你可以使用配置项spring.mvc.static-path-pattern修改,例如,将所有资源重新定位到 /resources/**

spring.mvc.static-path-pattern=/resources/**

你还可以使用配置项spring.web.resources.static-locations对静态资源的位置进行自定义(使用文件夹位置来代替默认值)。“/”也会自动被添加为一个位置。

除了上面提到的“标准”静态资源位置,对 Webjars content有一些特殊。默认情况下,如果一个jar被打包成Webjars格式,那么其资源都可通过/webjars/**进行访问。当然该path可以使用配置项spring.mvc.webjars-path-pattern进行自定义

SpringBoot还支持Spring Mvc所提供的一些高些特性,例如,缓存破坏和使用版本无关的URL对于WebJars。

为了对于WebJars使用版本无关的URL,添加webjars-locator-core依赖。以jQuery作为示例,"/webjars/jquery/jquery.min.js"会访问"/webjars/jquery/x.y.z/jquery.min.js"x.y.z就是Webjar的版本。

Note

如果你使用JBoss,那么需要添加webjars-locator-jboss-vfs依赖而不是webjars-locator-core,否则,所有的Webjar都会是404。

为了使用缓存破坏,可按照下面配置,它对所有的静态资源配置了一种缓存破坏的解决方式。有效的在URL上添加内容hash,例如<link href="/css/spring-2a2d595e6ed9a0b24f027f2b63b134d6.css"/>

spring.web.resources.chain.strategy.content.enabled=true
spring.web.resources.chain.strategy.content.paths=/**

Note

SpringBoot由于对Thymeleaf和FreeMarker自动配置了ResourceUrlEncodingFilter,因此模板中资源的链接会在运行时进行重写。但如果你使用的JSP,那么你需要手动配置。其他的模板引擎目前还不能进行自动配置,但是可通过自定义模板宏/助手和使用ResourceUrlProvider实现。

当使用JavaScript module loader动态加载资源时,不可以对其进行重命名。这就是为什么还是支持其他的策略并且可以结合使用的原因。策略"fixed"会在URL上添加一个静态版本字符串,这并不会改变文件的名称,如下所示

spring.web.resources.chain.strategy.content.enabled=true
spring.web.resources.chain.strategy.content.paths=/**
spring.web.resources.chain.strategy.fixed.enabled=true
spring.web.resources.chain.strategy.fixed.paths=/js/lib/
spring.web.resources.chain.strategy.fixed.version=v12

当使用该配置时,在"/js/lib/"下的JavaScript module会使用固定版本策略("/v12/js/lib/mymodule.js"),同时,其他资源继续使用内容hash策略(<link href="/css/spring-2a2d595e6ed9a0b24f027f2b63b134d6.css"/>)。

更多信息请查阅WebProperties.Resources

Tip

有专门的blog(blog post)已经详尽的介绍了该feature,在Spring 框架Reference中同样也有介绍。

Welcome Page

SpringBoot同时支持静态和模板化的欢迎页,会首先在所配置的静态资源位置搜索index.html,如果未找到,就会去搜索index模板,如果两者之一被找到,就会当做应用的欢迎页。

Custom Favicon

和其他静态资源一样,SpringBoot会在所配置的静态资源位置搜索favicon.ico,如果资源存在,就会就会被作为应用的favicon。

Path Matching and Content Negotiation

SpringMVC通过将请求的path和在应用中定义的mappings(例如,@GetMapping注解)进行匹配来将HTTP请求交到handler处理。

默认情况下,SpringBoot会禁用尾模式匹配,这意味着类似 "GET /projects/spring-boot.json"的请求将不会被匹配到@GetMapping("/projects/spring-boot")。这是一种SpringMVC应用的最佳实践( best practice for Spring MVC applications )。该feature对于哪些不会发送正确"Accept"请求头的老HTTP客户端来说非常有用;我们需要确保发送给客户端正确的Content Type。而现在,内容协商是更可靠的。

除了禁用尾匹配模式之外,还有其他的方式对这些没有正确Accept请求头的请求进行处理,我们可以添加参数例如"GET /projects/spring-boot?format=json"会被@GetMapping("/projects/spring-boot"处理。

spring.mvc.contentnegotiation.favor-parameter=true

或者你可以使用不同的参数名称

spring.mvc.contentnegotiation.favor-parameter=true
spring.mvc.contentnegotiation.parameter-name=myparam

大多数标准的media type都是开箱即用的,但是你也可以自己再定义新的

spring.mvc.contentnegotiation.media-types.markdown=text/markdown

从Spring 5.3开始,Spring Mvc支持两种策略来将请求匹配大controllers。默认情况下,SpringBoot使用PathPatternParser策略。PathPatternParser是一个优化后的实现( optimized implementation),但是和AntPathMatcher相比有一些严格的限制。PathPatternParser限制了一些path pattern 的变种(some path pattern variants),他与配置了path prefix(spring.mvc.servlet.path)的DispatcherServlet也不兼容。

可以通过配置项spring.mvc.pathmatch.matching-strategy进行配置,如下所示

spring.mvc.pathmatch.matching-strategy=ant-path-matcher

默认情况下,如果没有为请求找到相应handler,SpringMvc会返回404。可以通过配置spring.mvc.throw-exception-if-no-handler-found=true来抛出NoHandlerFoundException而不是404。请注意,默认情况下,静态内容会被匹配到/** 这相当于为所有请求提供了一个handler,因此想要NoHandlerFoundException被抛出,你需要把spring.mvc.static-path-pattern配置为一个更准确的值,如/resources/**或者设置spring.web.resources.add-mappings为false 这样就不会为静态资源提供服务。

ConfigurableWebBindingInitializer

SpringMvc使用WebBindingInitializer来为请求初始化WebDataBinder,你可以创建自己的ConfigurableWebBindingInitializerbean,这样SpringMvc将会使用你自定义的。

Template Engines

除了提供Rest web service,你还可以使用Spring Mvc来提供动态HTML服务。Spring Mvc支持很多模板技术,包含Thymeleaf FreeMarker以及JSP。除此之外,许多模板引擎都会包含它们自己的SpringMvc集成方式。

SpringBoot为下列的模板引擎提供auto-configuration支持

Tip

尽量不要使用JSP,因为子内嵌式servlet容器中使用JSP会有一些限制(known limitations

默认配置下,当你使用上述的模板引擎时,将从src/main/resources/templates下查找你的模板

Tip

根据你运行应用的方式,你的IDE可能会以不同的顺序排序classpath。当你在IDE中通过Maven或Gradle或从打包好的jar运行你的应用时,可能会导致不同的结果。这会导致SpringBoot找不到预期的模板。如果你有这种问题,可以在IDE中重新排序你的类和资源。

Error Handling

默认情况下,SpringBoot提供了一个/error,会合理地处理所有的错误,它会被servlet容器注册为一个“全局”的错误页面。对于机器客户端来说,它会返回一个带有错误详细信息,HTTP状态和exception message的JSON。对于浏览器客户端来说,会将上面信息渲染到HTML页面并返回(要想定义该行为,添加一个用于处理errorView)

如果你想自定义默认的错误处理行为,可以通过设置SpringBoot所提供的的很多server.error配置来实现。详情请查阅Server Properties

为了完全替换这个默认的行为,你可以实现ErrorController接口,并把它注册为bean,或者添加一个ErrorAttributes的bean来仅仅替换错误的内容。

Tip

当你想为一个新的content type添加一个handler时,可以使用BasicErrorController作为基类来自定义ErrorController会非常方便,在其中添加public方法并使用@RequestMapping注解,并添加produces参数。

从Spring 6.0开始,对RFC 7807 Problem Details提供了支持,SpringMvc可以使用application/problem+json来返回自定义的错误信息,可以通过spring.mvc.problemdetails.enabled=true来设置

{
  "type": "https://example.org/problems/unknown-project",
  "title": "Unknown project",
  "status": 404,
  "detail": "No project found for id 'spring-unknown'",
  "instance": "/projects/spring-unknown"
}

你还可以定义一个被@ControllerAdvice注解的类来针对指定的controller and/or 异常类型返回自定义的JSON,例如

import jakarta.servlet.RequestDispatcher;
import jakarta.servlet.http.HttpServletRequest;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
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.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;

@ControllerAdvice(basePackageClasses = SomeController.class)
public class MyControllerAdvice extends ResponseEntityExceptionHandler {

    @ResponseBody
    @ExceptionHandler(MyException.class)
    public ResponseEntity<?> handleControllerException(HttpServletRequest request, Throwable ex) {
        HttpStatus status = getStatus(request);
        return new ResponseEntity<>(new MyErrorBody(status.value(), ex.getMessage()), status);
    }

    private HttpStatus getStatus(HttpServletRequest request) {
        Integer code = (Integer) request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
        HttpStatus status = HttpStatus.resolve(code);
        return (status != null) ? status : HttpStatus.INTERNAL_SERVER_ERROR;
    }
}

在上面的实例中,如果一个和SomeController所在包一直的controller抛出了MyException异常,那么MyErrorBody会被作为JSON内容而不是ErrorAttributes

一些情况下,controller层面上被处理的错误不会被metrics infrastructure统计到。你可以通过把已处理的异常设置进request attribute,这样应用可以保证这类异常被正确统计。

import jakarta.servlet.http.HttpServletRequest;

import org.springframework.boot.web.servlet.error.ErrorAttributes;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ExceptionHandler;

@Controller
public class MyController {

    @ExceptionHandler(CustomException.class)
    String handleCustomException(HttpServletRequest request, CustomException ex) {
        request.setAttribute(ErrorAttributes.ERROR_ATTRIBUTE, ex);
        return "errorView";
    }
}

Custom Error Pages

如果想为特定的状态码返回一个自定义的HTML错误页,你可以将该页添加到/error文件夹。错误页可以使静态网页(即添加在任何静态文件夹下)或者模板文件。但是文件的名字应该是这个状态码或者一系列掩码。

例如,为404状态码响应一个静态网页,你的文件夹结构应该如下所示

src/
 +- main/
     +- java/
     |   + <source code>
     +- resources/
         +- public/
             +- error/
             |   +- 404.html
             +- <other public assets>

要使用FreeMarker匹配所有的5xx状态码,你的文件夹结构应如下所示

src/
 +- main/
     +- java/
     |   + <source code>
     +- resources/
         +- templates/
             +- error/
             |   +- 5xx.ftlh
             +- <other templates>

如果要进行更复杂配置,你可以继承ErrorViewResolver并注册为bean。如下所示

import java.util.Map;

import jakarta.servlet.http.HttpServletRequest;

import org.springframework.boot.autoconfigure.web.servlet.error.ErrorViewResolver;
import org.springframework.http.HttpStatus;
import org.springframework.web.servlet.ModelAndView;

public class MyErrorViewResolver implements ErrorViewResolver {

    @Override
    public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) {
        // Use the request or status to optionally return a ModelAndView
        if (status == HttpStatus.INSUFFICIENT_STORAGE) {
            // We could add custom model values here
            new ModelAndView("myview");
        }
        return null;
    }
}

你还可以使用正常的SpringMvc feature来实现,比如定义@ExceptionHandler methods@ControllerAdvice。然后ErrorController会处理其他未被处理的异常。

Mapping Error Pages Outside of Spring MVC

pass (该章节介绍不使用SpringMvc的情况下)

Error handling in a WAR Deployment

当部署到servlet容器中时,SpringBoot使用它的error page fileter来将带有error starus的请求转到对应的error page。这是因为servlet规范并没有提供注册error page的api。根据你war包部署容器的不同以及你应用中使用的技术不同,一些额外的配置需要进行配置。

error page filter只有在Response还没有提交的时候正常工作。默认情况下,WebSphere Application Server 8.0及以后版本会在servlet方法成功完成后提交Response。你应该通过设置com.ibm.ws.webcontainer.invokeFlushAfterService=false来禁用掉该行为。