跳至主要內容

16.- Servlet Web 应用程序开发(Spring MVC)

安图新大约 12 分钟

16.- Servlet Web 应用程序开发(Spring MVC)

前言

Spring Boot 非常适合 web 应用程序开发。您可以使用嵌入的TomcatJettyUndertowNetty创建一个自包含的 HTTP 服务器。大多数web应用程序使用spring-boot-starter-web模块来快速启动和运行。

如果想构建基于servletweb应用程序,您可以利用 Spring Boot 对Spring MVC的自动配置。

一、Spring Web MVC

Spring Web MVC 框架(通常被称为“Spring MVC”)是一个“模型-视图-控制器”Web 框架。Spring MVC 使用注解@Controller@RestController bean 来处理传入的HTTP请求。控制器中的方法通过使用@RequestMapping注释映射到HTTP。详细可以参考Spring MVC 官方文档open in new window

1.示例

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

代码如下(示例):

@RestController
@RequestMapping("/hello")
public class HelloWordController {



    @GetMapping("/get")
    public ResponseEntity httpGet() {


        return ResponseEntity.ok("http get");
    }

    @PostMapping("/post")
    public ResponseEntity postGet() {


        return ResponseEntity.ok("http post");
    }

    @DeleteMapping("/delete")
    public ResponseEntity deleteGet() {


        return ResponseEntity.ok("http delete");
    }

    @PutMapping("/put")
    public ResponseEntity putGet() {


        return ResponseEntity.ok("http put");
    }
}

Spring Web MVC 包括WebMvc.Fn,一种轻量级函数式编程模型,其中函数用于路由和处理请求。它是基于注解的编程模型的替代方案,但在其他方面运行在相同的DispatcherServlet参考官网open in new window

代码如下(示例):

@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();
    }

}

@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();
    }

}

你可以定义任意数量的 RouterFunction bean 来模块化路由器的定义。如果需要应用优先级,可以对 bean 进行排序。

2.Spring MVC 自动配置

Spring Boot 为 Spring MVC 提供了可以很好地与大多数应用程序一起工作的自动配置。 自动配置在 Spring 默认配置的基础上增加了以下特性:

1、 包含ContentNegotiatingViewResolverBeanNameViewResolverbean;

ContentNegotiatingViewResolver视图解析器,同样的内容数据来呈现不同的View
BeanNameViewResolver视图解析器,用于返回自定义的视图。

1、 支持对静态资源的处理,包括对webjar的支持;
2、 自动注册ConverterGenericConverterFormatterbean;
3、 支持HttpMessageConverters
4、 自动注册MessageCodesResolver
5、 静态index.html的支持;
6、 自动使用ConfigurableWebBindingInitializerbean;

3.HttpMessageConverters

Spring MVC 使用 HttpMessageConverter 接口来转换 HTTP 请求和响应。默认情况下是开箱即用,例如,对象可以自动转换为 JSON(通过使用 Jackson 库)或 XML(如果可用,则使用 Jackson XML 扩展;如果 Jackson XML 扩展不可用,则使用 JAXB),默认情况下,字符串以 UTF-8 编码。

Spring Boot 自带的 JSON 格式转换,HttpMessageConverter 实现有如下几种:

MappingJackson2HttpMessageConverter(默认)
JsonbHttpMessageConverter
GsonHttpMessageConverter

可以使用属性spring.mvc.converters.preferred-json-mapper选择具体的josn(jackson,gson,jsonb)转换方式。

如果你需要添加或自定义转换器,你可以使用 Spring Boot 的HttpMessageConverters类,如下所示:

@Configuration(proxyBeanMethods = false)
public class MyHttpMessageConvertersConfiguration {



    @Bean
    public HttpMessageConverters customConverters() {


        HttpMessageConverter<?> additional = new AdditionalHttpMessageConverter();
        HttpMessageConverter<?> another = new AnotherHttpMessageConverter();
        return new HttpMessageConverters(additional, another);
    }

}

4.JSON 序列化和反序列化

如果你使用Jackson来序列化和反序列化JSON数据, 你可能想写自己的JsonSerializerJsonDeserializer类。自定义序列化器通常通过模块注册到Jackson。但是 Spring Boot 提供了一个替代的@JsonComponent注解参考序列化器注册到 Jacksonopen in new window,可以更容易地直接注册 Spring bean。
你可以直接在JsonSerializerJsonDeserializerKeyDeserializer实现上使用@JsonComponent注解。你也可以在包含序列化器/反序列化器作为内部类的类上使用它,如下面的例子所示:

@JsonComponent
public class MyJsonComponent {



    public static class Serializer extends JsonSerializer<MyObject> {



        @Override
        public void serialize(MyObject value, JsonGenerator jgen, SerializerProvider serializers) throws IOException {


            jgen.writeStartObject();
            jgen.writeStringField("name", value.getName());
            jgen.writeNumberField("age", value.getAge());
            jgen.writeEndObject();
        }

    }

    public static class Deserializer extends JsonDeserializer<MyObject> {



        @Override
        public MyObject deserialize(JsonParser jsonParser, DeserializationContext ctxt) throws IOException {


            ObjectCodec codec = jsonParser.getCodec();
            JsonNode tree = codec.readTree(jsonParser);
            String name = tree.get("name").textValue();
            int age = tree.get("age").intValue();
            return new MyObject(name, age);
        }

    }

}

ApplicationContext中的所有@JsonComponent bean 都会自动注册到Jackson。因为@JsonComponent是用@Component元注解的,所以通常的组件扫描规则也适用。Spring Boot 还提供了JsonObjectSerializerJsonObjectDeserializer基类,它们在序列化对象时提供了标准Jackson版本的替代方案。
如下代码所示:


@JsonComponent
public class MyJsonComponent {



    public static class Serializer extends JsonObjectSerializer<MyObject> {



        @Override
        protected void serializeObject(MyObject value, JsonGenerator jgen, SerializerProvider provider)
                throws IOException {


            jgen.writeStringField("name", value.getName());
            jgen.writeNumberField("age", value.getAge());
        }

    }

    public static class Deserializer extends JsonObjectDeserializer<MyObject> {



        @Override
        protected MyObject deserializeObject(JsonParser jsonParser, DeserializationContext context, ObjectCodec codec,
                JsonNode tree) throws IOException {


            String name = nullSafeValue(tree.get("name"), String.class);
            int age = nullSafeValue(tree.get("age"), Integer.class);
            return new MyObject(name, age);
        }

    }

}

5.MessageCodesResolver

Spring MVC 有一个从绑定错误(BindingResult)中生成错误代码来渲染错误消息的策略:MessageCodesResolver。如果你设置了spring.mvc.message-codes-resolver-format属性为PREFIX_ERROR_CODEPOSTFIX_ERROR_CODE。Spring Boot 会创建一个 DefaultMessageCodesResolver.Format

DefaultMessageCodesResolverMessageCodesResolver接口的默认实现。

将为对象错误创建两个消息代码, 按照如下顺序:

1.: code + "." + object name
2.: code

将为字段规范创建四个消息代码, 按照如下顺序:

1.: code + "." + object name + "." + field
2.: code + "." + field
3.: code + "." + field type
4.: code

例如,对于错误代码 “typeMismatch”, 对象名 “user”, 字段 “age”:

1. try "typeMismatch.user.age"
2. try "typeMismatch.age"
3. try "typeMismatch.int"
4. try "typeMismatch"

因此,这个解析算法可以用来显示绑定错误的特定消息,比如"required"和"typeMismatch":
在 obejct+field 级别("age"字段,仅在"user"对象上)
在 field 级别(全部的"age"字段,无论对象名称是什么)
或在一般级别(所有字段,在任何对象上)。

对于数组、List 或 Map 属性,将生成特定元素和整个集合的代码。假设在对象“user”中有一个数组“groups”的字段“name”:

1. try "typeMismatch.user.groups[0].name"
2. try "typeMismatch.user.groups.name"
3. try "typeMismatch.groups[0].name"
4. try "typeMismatch.groups.name"
5. try "typeMismatch.name"
6. try "typeMismatch.java.lang.String"
7. try "typeMismatch"

6.静态资源

默认情况下,Spring Boot 在classpath中从一个名为/static (或 /public/resources/META-INF/resources)或者ServletContext根目录获取静态内容。

它使用了 Spring MVC 中的ResourceHttpRequestHandler,这样你就可以通过添加自己的WebMvcConfigurer和覆盖addResourceHandlers方法来修改该行为。

默认情况下,资源映射在/**上,但是你可以使用spring.mvc.static-path-pattern 属性来调优。
例如,将所有资源重新定位到/resources/**,可以实现如下操作:

spring:
  mvc:
    static-path-pattern: "/resources/**"

你也可以使用 spring.web.resources.static-locations 自定义静态资源路径(替换默认的位置),根 servlet 上下文路径“/”也会自动添加为一个位置。

除了前面提到的“标准”静态资源位置之外,还有一种针对Webjars内容的特殊情况。
任何路径在/webjars/**的资源,如果是以webjars格式打包的,都是从jar文件中提供的。什么是 webjarsopen in new window

如果你的应用被打包成一个 jar,不要使用src/main/webapp目录。尽管该目录是一个通用标准,但它只适用于war打包,而且如果你生成一个jar,大多数构建工具都会静默地忽略它。

Spring Boot 还支持 Spring MVC 提供的高级资源处理特性,允许使用像缓存破坏静态资源或为webjar使用版本无关url这样的特性。
要为webjar使用版本无关的url,请添加webjar -locator-core依赖项。然后声明你的webjar
以 jQuery 为例, 声明 /webjars/jquery/jquery.min.js 得到/webjars/jquery/x.y.z/jquery.min.js,x.y.z 就是 webjar 版本。

7.欢迎页

Spring Boot 支持静态和模板欢迎页面。它首先在配置的静态内容位置中查找index.html文件。如果没有找到,则查找索引模板。如果找到其中一个,它将自动用作应用程序的欢迎页面。

8.路径匹配和内容协商

Spring MVC 可以通过查看请求路径并将其与应用程序中定义的映射进行匹配,从而将传入的HTTP请求映射到处理程序(例如,Controller方法上的@GetMapping注解)。

默认情况下,Spring Boot 选择禁用后缀模式匹配,这意味着像“GET /projects/spring-boot.json”这样的请求将不匹配@GetMapping("/projects/spring-boot")的映射。 这个特性主要用于过去 HTTP 客户端没有发送正确的“Accept”请求头; 我们需要确保向客户端发送正确的内容类型。如今,内容协商更加可靠。

代替使用后缀匹配,我们可以使用查询参数来确保像“GET /projects/spring-boot?format=json"将被映射到@GetMapping("/projects/spring-boot"):

spring:
  mvc:
    contentnegotiation:
      favor-parameter: true

或者如果你喜欢使用不同的参数名:

spring:
  mvc:
    contentnegotiation:
      favor-parameter: true
      parameter-name: "myparam"

GET /projects/spring-boot?myparam=json

大多数标准媒体类型都是开箱即用的,但你也可以定义新的类型:

spring:
  mvc:
    contentnegotiation:
      media-types:
        markdown: "text/markdown"

后缀模式匹配已弃用,并将在未来的版本中删除。如果你理解了这些注意事项,并且仍然希望你的应用程序使用后缀模式匹配,下面的配置是必需的:

spring:
  mvc:
    contentnegotiation:
      favor-path-extension: true
    pathmatch:
      use-suffix-pattern: true

或者,与其打开所有后缀模式,还不如只支持已注册后缀模式:

spring:
  mvc:
    contentnegotiation:
      favor-path-extension: true
    pathmatch:
      use-registered-suffix-pattern: true

从 Spring Framework 5.3 开始,Spring MVC 支持几种将请求路径与控制器处理程序匹配的实现策略。它以前只支持AntPathMatcher(默认))策略,但现在也提供了PathPatternParser。Spring Boot 现在提供了一个配置属性来选择和选择新的策略:

spring:
  mvc:
    pathmatch:
      matching-strategy: "path-pattern-parser"

 
PathPatternParser是一个优化的实现,但是限制了一些路径模式变体的使用,并且与后缀模式匹配不兼容(spring.mvc.pathmatch.use-suffix-pattern, spring.mvc.pathmatch.use-registered-suffix-pattern)或者用 servlet 前缀映射 DispatcherServlet(spring.mvc.servlet.path)

9.ConfigurableWebBindingInitializer

Spring MVC 使用WebBindingInitializer为特定的请求初始化WebDataBinder。如果你创建了自己的ConfigurableWebBindingInitializer @Bean, Spring Boot 会自动配置 Spring MVC 来使用它。

10.模版引擎

除了 REST web 服务之外,您还可以使用 Spring MVC 来提供动态 HTML 内容。Spring MVC 支持各种模板技术,包括ThymeleafFreeMarkerjsp。此外,许多其他模板引擎也包含它们自己的 Spring MVC 集成。
Spring Boot 包括对以下模板引擎的自动配置支持:

1、 FreeMarker
2、 Groovy
3、 Thymeleaf
4、 Mustache

当你使用这些带有默认配置的模板引擎之一时,你的模板将自动从 src/main/resources/templates 中获得。

11.错误处理

默认情况下,Spring Boot 提供了一个/error映射,以一种合理的方式处理所有错误,它被注册为servlet容器中的“全局”错误页。
对于机器客户端(client),它生成一个 JSON 响应,其中包含错误、HTTP 状态和异常消息的详细信息。
对于浏览器客户端,有一个“whitelabel”错误视图,它以 HTML 格式呈现相同的数据(要定制它,请添加一个解析错误的视图)。

server.error 属性可以设置自定义默认错误处理行为。
 
完全替换默认行为,你可以实现ErrorController并注册该类型的bean定义,或者添加ErrorAttributes类型的bean以使用现有机制,替换其内容。

BasicErrorController可以用作自定义ErrorController的基类,如果你想为新的content-type添加处理程序(默认是专门处text/html,并为其他所有内容提供回退),这个非常有用。为此,扩展BasicErrorController,添加一个带有@RequestMapping的公共方法,该方法具有一个produces属性,并创建新类型的 bean。

下面例子新增content-typetext/markdown异常处理。

public class CustomErrorController extends BasicErrorController {


    private ErrorAttributes errorAttributes;

    public CustomErrorController(ErrorAttributes errorAttributes, ErrorProperties errorProperties) {


        super(errorAttributes, errorProperties);
        this.errorAttributes = errorAttributes;
    }

    @RequestMapping(value = "/error", produces = "text/markdown")
    @ResponseBody
    public Map<String, Object> errorPageHandler(HttpServletRequest request, HttpServletResponse response) {


        ServletWebRequest requestAttributes = new ServletWebRequest(request);
        Map<String, Object> errorMap = this.errorAttributes.getErrorAttributes(requestAttributes, ErrorAttributeOptions.defaults());
        return errorMap;
    }
}

你也可以定义一个带@ControllerAdvice注解的类来定制JSON内容,以返回特定的控制器和/或异常类型,如下所示:

@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在同一个包中定义的控制器抛出YourException,则使用 CustomErrorType POJO 的 JSON 表示,而不是ErrorAttributes表示。

自定义错误页面

如果您想要显示给定状态码的自定义HTML错误页面,您可以将文件添加到/error目录。错误页面可以是静态HTML(即添加在任何静态资源目录下),也可以使用模板构建。文件的名称应该是确切的状态码或序列掩码。

例如,要将 404 映射到一个静态 HTML 文件,你的目录结构应该如下:

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,如下面的例子所示:

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

}

你也可以使用常规的 Spring MVC 特性,比如@ExceptionHandler方法和@ControllerAdvice。然后ErrorController获取任何未处理的异常。

在 Spring MVC 之外映射错误页面

对于不使用 Spring MVC 的应用程序,可以使用ErrorPageRegistrar接口直接注册ErrorPages。这种抽象直接与底层的嵌入式 servlet 容器一起工作,即使您没有 Spring MVC DispatcherServlet也可以工作。

@Configuration(proxyBeanMethods = false)
public class MyErrorPagesConfiguration {



    @Bean
    public ErrorPageRegistrar errorPageRegistrar() {


        return this::registerErrorPages;
    }

    private void registerErrorPages(ErrorPageRegistry registry) {


        registry.addErrorPages(new ErrorPage(HttpStatus.BAD_REQUEST, "/400"));
    }

}

12.跨域

跨源资源共享(CORS)是一个由大多数浏览器实现的 W3C 规范,它允许您以一种灵活的方式指定对哪种类型的跨域请求进行授权,而不是使用一些不那么安全、功能也不那么强大的方法,如 IFRAME 或 JSONP。

从 4.2 版开始,Spring MVC 支持CORS。在 Spring Boot 应用程序中使用带有@CrossOrigin注释的控制器CORS配置不需要任何特定的配置。全局CORS配置可以通过使用自定义的addCorsMappings (CorsRegistry)方法注册WebMvcConfigurer bean 来定义,如下面的例子所示:

@Configuration(proxyBeanMethods = false)
public class MyCorsConfiguration {



    @Bean
    public WebMvcConfigurer corsConfigurer() {


        return new WebMvcConfigurer() {



            @Override
            public void addCorsMappings(CorsRegistry registry) {


                registry.addMapping("/api/**");
            }

        };
    }

}


总结

本文主要内容是 Spring Boot 对 Spring MVC 自动配置的支持,以及一些个性化特性的实现。