[JAVA] Major changes related to Spring Framework 5.0 Web MVC

This is the 4th installment of the "Spring Framework 5.0 Major Changes" series, and the main changes (new features and improvements, etc.) related to WebMVC. I would like to introduce.

series

Operation verification version

Base line


Individual


This time, I used Spring Boot 2.0.0.M1 (which depends on Spring Framework 5.0.0.RC1 by default) to verify the operation. The procedure when creating a verification project is briefly introduced below.

Note:

It can also be created using SPRING INITIALIZR and IDE functions.

$ curl -s https://start.spring.io/starter.tgz\
  -d name=spring5-web-demo\
  -d artifactId=spring5-web-demo\
  -d dependencies=web\
  -d baseDir=spring5-web-demo\
  -d bootVersion=2.0.0.M1\
  | tar -xzvf -
$ ./mvnw spring-boot:run
/usr/local/apps/spring5-web-demo
[INFO] Scanning for projects...
[INFO] 
[INFO] ------------------------------------------------------------------------
[INFO] Building spring5-web-demo 0.0.1-SNAPSHOT
[INFO] ------------------------------------------------------------------------
[INFO] 
[INFO] >>> spring-boot-maven-plugin:2.0.0.M1:run (default-cli) > test-compile @ spring5-web-demo >>>
[INFO] 
[INFO] --- maven-resources-plugin:2.6:resources (default-resources) @ spring5-web-demo ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] Copying 1 resource
[INFO] Copying 0 resource
[INFO] 
[INFO] --- maven-compiler-plugin:3.1:compile (default-compile) @ spring5-web-demo ---
[INFO] Changes detected - recompiling the module!
[INFO] Compiling 1 source files to /usr/local/apps/spring5-web-demo/target/classes
[INFO] 
[INFO] --- maven-resources-plugin:2.6:testResources (default-testResources) @ spring5-web-demo ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] skip non existing resourceDirectory /usr/local/apps/spring5-web-demo/src/test/resources
[INFO] 
[INFO] --- maven-compiler-plugin:3.1:testCompile (default-testCompile) @ spring5-web-demo ---
[INFO] Changes detected - recompiling the module!
[INFO] Compiling 1 source files to /usr/local/apps/spring5-web-demo/target/test-classes
[INFO] 
[INFO] <<< spring-boot-maven-plugin:2.0.0.M1:run (default-cli) < test-compile @ spring5-web-demo <<<
[INFO] 
[INFO] 
[INFO] --- spring-boot-maven-plugin:2.0.0.M1:run (default-cli) @ spring5-web-demo ---

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::             (v2.0.0.M1)

2017-05-17 01:19:22.357  INFO 26370 --- [           main] c.e.s.Spring5WebDemoApplication          : Starting Spring5WebDemoApplication on xxx with PID 26370 (/usr/local/apps/spring5-web-demo/target/classes started by xxx in /usr/local/apps/spring5-web-demo)
2017-05-17 01:19:22.360  INFO 26370 --- [           main] c.e.s.Spring5WebDemoApplication          : No active profile set, falling back to default profiles: default
2017-05-17 01:19:22.411  INFO 26370 --- [           main] ConfigServletWebServerApplicationContext : Refreshing org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@25c892fe: startup date [Wed May 17 01:19:22 JST 2017]; root of context hierarchy
2017-05-17 01:19:23.717  INFO 26370 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8080 (http)
2017-05-17 01:19:23.733  INFO 26370 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2017-05-17 01:19:23.735  INFO 26370 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet Engine: Apache Tomcat/8.5.15
2017-05-17 01:19:23.822  INFO 26370 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2017-05-17 01:19:23.822  INFO 26370 --- [ost-startStop-1] o.s.web.context.ContextLoader            : Root WebApplicationContext: initialization completed in 1414 ms
2017-05-17 01:19:23.956  INFO 26370 --- [ost-startStop-1] o.s.b.w.servlet.ServletRegistrationBean  : Mapping servlet: 'dispatcherServlet' to [/]
2017-05-17 01:19:23.961  INFO 26370 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'characterEncodingFilter' to: [/*]
2017-05-17 01:19:23.961  INFO 26370 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'hiddenHttpMethodFilter' to: [/*]
2017-05-17 01:19:23.961  INFO 26370 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'httpPutFormContentFilter' to: [/*]
2017-05-17 01:19:23.961  INFO 26370 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'requestContextFilter' to: [/*]
2017-05-17 01:19:24.206  INFO 26370 --- [           main] s.w.s.m.m.a.RequestMappingHandlerAdapter : Looking for @ControllerAdvice: org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@25c892fe: startup date [Wed May 17 01:19:22 JST 2017]; root of context hierarchy
2017-05-17 01:19:24.292  INFO 26370 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error]}" onto public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController.error(javax.servlet.http.HttpServletRequest)
2017-05-17 01:19:24.293  INFO 26370 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],produces=[text/html]}" onto public org.springframework.web.servlet.ModelAndView org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController.errorHtml(javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse)
2017-05-17 01:19:24.323  INFO 26370 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/webjars/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2017-05-17 01:19:24.323  INFO 26370 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2017-05-17 01:19:24.370  INFO 26370 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**/favicon.ico] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2017-05-17 01:19:24.533  INFO 26370 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Registering beans for JMX exposure on startup
2017-05-17 01:19:24.610  INFO 26370 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http)
2017-05-17 01:19:24.615  INFO 26370 --- [           main] c.e.s.Spring5WebDemoApplication          : Started Spring5WebDemoApplication in 2.996 seconds (JVM running for 6.973)
On the terminal, "Ctrl"+ C」!!

WebMVC related changes

In Spring Framework 5.0, the following changes have been made to the WEB function.

Item number Changes
1 Servlet filter provided by Spring Framework is Servlet 3.It will be an implementation conforming to the API specification of 1. (Actually, I don't know how it complied ...:sweat_smile:) [To details:arrow_right:]
2 Servlet 4 which is a component of Java EE 8 as an argument of Handler method of Spring MVC.Added at 0PushBuilder(HTTP/API for doing Server Push of 2)You will be able to receive it.[To details:arrow_right:]
3 Servlet 3.When an oversize error occurs for the file upload feature supported by 0MaxUploadSizeExceededException(MultipartExceptionSub-exception)Will be thrown.[To details:arrow_right:]

**Note:**However,certainwordsintheerrormessage("size"When"exceed")Because it is judged by whether or not is included, it is the same as before depending on the implementation of the application server.MultipartExceptionがスローされる可能性があるWhenいう点は意識しておいた方がよいでしょう。
4 Unified media type resolution mechanism(MediaTypeFactoryclass)Will be added.[To details:arrow_right:]

**Note:**Withthisresponse,JAF(JavaBeansActivationframework)Dependent code is eliminated.
5 Data binding to immutable objects is supported.[To details:arrow_right:]
6 Jackson 2.9 is supported.[To details:arrow_right:]
7 A component of Java EE 8JSON BindingWillbesupported.

**Note:**ItcanbeusedasanalternativetoJacksonandGSON.[Todetails:arrow_right:]
8 Google Protobuf 3.x is supported.[To details:arrow_right:]
9 Reactive Programing Model(Spring WebFlux described later)With the support of Reactor 3.1 class(Flux, Mono)、RxJava 1.3 and 2.1 class(Observable, Sigle, FlowableSuch)Can be treated as the return value of the Handler method.[To details:arrow_right:]
10 AntPathMatcherAs an alternative toParsingPathMatcherWill be added.[To details:arrow_right:]

**Note:**For Spring WebFlux related components,ParsingPathMatcherIs used by default.
11 @ExceptionHandlerAs a method argumentRedirectAttributesYou will be able to receive it.(=RedirectAttributesYou will be able to link data with the redirect destination via) [To details:arrow_right:]
12 ResponseStatusExceptionHas been added, and you can control the HTTP status by generating and throwing an exception by specifying an arbitrary status code and reason phrase.[To details:arrow_right:]

Note: ResponseStatusExceptionSeems to have been created for Spring WebFlux, but it is also supported for use with Spring MVC.
13 ScriptTemplateView(JSR-View implementation using 223 script engine)Objects required to realize "internationalization of messages" and "fragmentation of templates"(Locale,MessageSourceSuch)Will be handed over to the template engine side.[To details:arrow_right:]

Note: 「国際化」や「テンプレートをフラグメント化」するための実装自体はSpringからは提供されないので、SpringのテストケースSuchを参考に開発者が実装する必要があります。

Is the Servlet filter implementation compliant with the Servlet 3.1 API specification? : thumbsup:

[SPR-14467]: Apparently it's purely (not limited to Servlet filters) an overall API spec-based implementation of Servlet 3.1 (not limited to Servlet filters). = Operation on Servlet 3.0 is not supported)! It seems that. In this regard, up to Spring Framework 4.3, code was implemented to maintain compatibility with the Servlet 2.5 environment, but from Spring Framework 5.0, support for the Servlet 2.5 environment will be discontinued at all ([SPR-13189]. ](Https://jira.spring.io/browse/SPR-13189)).

Servlet 4.0 PushBuilder can be received by Handler method: thumbsup:

[SPR-12674] PushBuilder added in Servlet 4.0, a component of Java EE 8, as an argument to the Handler method of Spring MVC. You will be able to receive (API for HTTP / 2 Server Push).

Let's implement a simple sample and try out HTTP / 2 Server Push. First, create an HTML file with a link in the css file and the css file.

src/main/resources/static/style.css


body {
    background-color: azure;
}

src/main/resources/static/hello.html


<html>
<head>
    <link rel="stylesheet" type="text/css" href="style.css"/> <!--★★★ Link to css file-->
</head>
<body>
<h1>Hello World !!</h1>
</body>
</html>

Next, create a Controller.

package com.example.spring5webdemo;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

import javax.servlet.http.PushBuilder;

@Controller
public class PushBuilderController {

	@GetMapping("/push-builder")
	String get(PushBuilder builder) { //★★★ Declare PushBuilder as an argument
		builder.path("/style.css").push(); //★★★ Set the css file to the path to be pushed and push
		return "forward:/hello.html"; //Return View name for transition to HTML
	}

}

I'd like to say, "Let's start the server and check it!", But I don't think it can be compiled because the API (PushBuilder) of Servlet 4.0 is not found in the first place. So ... I would like to modify the pom.xml file to use an application server that supports Servlet 4.0. At first I thought I would try it with Tomcat 9.0, but Undertow seemed to be easier to convert to HTTP / 2, so here I used Undertow 2.0 (version under development) that supports Servlet 4.0. To check the operation.

Note:

Undertow 2.0.0.Alpha1 has been released to Maven Central, but unfortunately this version is not available. The reason is that the API specification of Servlet 4.0 has changed in the middle and the changes have not been incorporated. So ... In this entry, get the source code from GitHub and install Undertow 2.0.0.Alpha2-SNAPSHOT in your local repository.

$ git clone https://github.com/undertow-io/undertow.git
$ cd undertow
$ mvn -U install -DskipTests=true

Modify the pom.xml file to use Undertow 2.0.0.Alpha2-SNAPSHOT instead of Tomcat.

pom.xml


<!-- ... -->

<properties>
  <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
  <java.version>1.8</java.version>
  <undertow.version>2.0.0.Alpha2-SNAPSHOT</undertow.version> <!-- ★★★ Servlet 4.0 Specify support version(Version installed in the local repository) -->
</properties>

<!-- ... -->

<dependencies>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <!--★★★ Exclude Tomcat-->
    <exclusions>
      <exclusion>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-tomcat</artifactId>
      </exclusion>
    </exclusions>
  </dependency>

  <!-- ... -->

  <!--★★★ Added Undertow-->
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-undertow</artifactId>
  </dependency>

  <!-- ★★★ alpn-Add boot-->
  <!-- 
    ALPN (Application Layer Protocol Negotiation)Settings for downloading the implementation library of.
The downloaded file is a startup option when the application is executed.(-Xbootclasspath)Specified as.
  -->
  <dependency>
    <groupId>org.mortbay.jetty.alpn</groupId>
    <artifactId>alpn-boot</artifactId>
    <version>8.1.11.v20170118</version>
    <scope>provided</scope>
  </dependency>

  <!-- ... -->

</dependencies>

<!-- ... -->

<build>
  <plugins>
    <plugin>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-maven-plugin</artifactId>
      <configuration>
        <!--★★★ Option to launch ALPN implementation library(-Xbootclasspath)Designated to-->
        <jvmArguments>
          -Xbootclasspath/p:${settings.localRepository}/org/mortbay/jetty/alpn/alpn-boot/8.1.11.v20170118/alpn-boot-8.1.11.v20170118.jar
        </jvmArguments>
      </configuration>
    </plugin>
  </plugins>
</build>

<!-- ... -->

In order to communicate with HTTP / 2, the communication between the server and the browser must be SSL / TLS communication using HTTPS, so create a keystore that stores the self-signed certificate.

$ keytool -genkey -storetype PKCS12 -keyalg RSA -keysize 2048 -keystore src/main/resources/keystore.p12 -validity 3650
Please enter the keystore password:  
Please re-enter your new password: 
What is your first and last name?
 [Unknown]:  Sample
What is the organizational unit name?
 [Unknown]:  Sample
What is your organization name?
 [Unknown]:  Sample
What is the city or region name?
 [Unknown]:  Sample
What is the state or state name?
 [Unknown]:  Sample
What is the two-letter country code for this unit?
 [Unknown]:  JP
CN=Sample, OU=Sample, O=Sample, L=Sample, ST=Sample, C=Are you sure you want JP?
 [No]:  Y

Set the keystore that stores the created self-signed certificate to Undertow, and make the server HTTPS.

src/main/resources/application.properties


server.ssl.key-store=classpath:keystore.p12
server.ssl.key-store-password=password

Finally, if you add the following Bean definition, Undertow's HTTP / 2 support is complete.

// ...
import io.undertow.UndertowOptions;
import org.springframework.boot.web.embedded.undertow.UndertowServletWebServerFactory;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
// ...

@SpringBootApplication
public class Spring5WebDemoApplication {
	// ...
	@Bean
	public WebServerFactoryCustomizer<UndertowServletWebServerFactory> webServerFactoryCustomizer() {
		return undertow -> {
			undertow.addBuilderCustomizers(builder -> builder.setServerOption(UndertowOptions.ENABLE_HTTP2, true));
		};
	}
}

Start Spring Boot and check that Undertow is HTTP / 2.

$ ./mvnw spring-boot:run
...
2017-05-18 09:02:40.807  INFO 60554 --- [           main] o.s.b.w.e.u.UndertowServletWebServer     : Undertow started on port(s) 8080 (https)
2017-05-18 09:02:40.813  INFO 60554 --- [           main] c.e.s.Spring5WebDemoApplication          : Started Spring5WebDemoApplication in 2.538 seconds (JVM running for 3.086)
$ curl -s -D - --http2 -k https://localhost:8080/
HTTP/2.0 404
content-type:application/json;charset=UTF-8
date:Thu, 18 May 2017 00:07:16 GMT

{"timestamp":"2017-05-18T00:07:15.967+0000","status":404,"error":"Not Found","message":"Not Found","path":"/"}

Note: The Mac cURL command was not HTTP / 2 compliant, so I ran the following command to install the HTTP / 2 compliant cURL.

$ brew reinstall curl -- --with-nghttp2
$ brew link curl --force
$ hash -r

Since it was confirmed that it was converted to HTTP / 2, I will try to access the path to actually perform HTTP / 2 Server Push using Chrome.

spring50-push-builder.png

It's a little difficult, but you can see that the Initiator of "/style.css" is "Push / push-builder".

Warning:

However ... It seems that Server Push is not enabled after the second time ... + The following log appears in the server log ... So something is wrong May be: cry:

2017-05-18 11:02:02.813 ERROR 60871 --- [  XNIO-2 task-9] io.undertow                              : UT005085: Connection io.undertow.server.protocol.http2.Http2ServerConnection@35f2c3a7 for exchange HttpServerExchange{ GET /style.css request {accept=[text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8], accept-language=[en-US,en;q=0.8,ja;q=0.6], cache-control=[max-age=0], :authority=[localhost:8080], accept-encoding=[gzip, deflate, sdch, br], :path=[/style.css], user-agent=[Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36], :scheme=[https], :method=[GET], Referer=[https://localhost:8080/push-builder], upgrade-insecure-requests=[1], Host=[localhost:8080]} response {Last-Modified=[Wed, 17 May 2017 19:36:35 GMT], Cache-Control=[no-store], Content-Length=[37], Content-Type=[text/css], Accept-Ranges=[bytes], Date=[Thu, 18 May 2017 02:02:02 GMT], :status=[200]}} was not closed cleanly, forcibly closing connection

Servlet 3.0 file upload feature now throws MaxUploadSizeExceededException: thumbsup:

[SPR-9294]: MaxUploadSizeExceededException (MultipartException) when an oversize error occurs in the file upload feature supported by Servlet 3.0 The sub-exception) will now be thrown. However, since it is determined whether the error message contains specific words (" size "and" exceeded "), MultipartException` is thrown as before depending on the implementation of the application server. I think you should be aware that there is a possibility.

Note:

Spring Framework provides a component that works with Commons File Upload, and when a size was exceeded in Commons File Upload, MaxUploadSizeExceededException was thrown.

Now let's actually generate an oversize error. Creating a file upload screen is troublesome, so ... In this entry, upload the file using the cURL command.

First, create a Controller that receives the upload file and start Spring Boot.

package com.example.spring5webdemo;

import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

@RestController
public class UploadRestController {
	@PostMapping("/upload")
	String upload(MultipartFile file) {
		return file.getOriginalFilename() + " is uploaded !";
	}
}

Next, try uploading a file within 1MB (a file smaller than the maximum size) as shown below.

$ pwd
/usr/local/apps/spring5-web-demo
$ curl -s -D - -X POST -F [email protected] http://localhost:8080/upload
HTTP/1.1 100 

HTTP/1.1 200 
Content-Type: text/plain;charset=UTF-8
Content-Length: 21
Date: Tue, 16 May 2017 16:56:48 GMT

pom.xml is uploaded !

Now, let's upload a file of 1MB or more. By the way ... 1MB or more files (target / spring5-web-demo-0.0.1-SNAPSHOT.jar) can be created with ./mvnw package.

$ ./mvnw package
...
$ curl -s -D - -X POST -F file=@target/spring5-web-demo-0.0.1-SNAPSHOT.jar http://localhost:8080/upload
HTTP/1.1 100 

HTTP/1.1 500 
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Tue, 16 May 2017 16:59:21 GMT
Connection: close

{"timestamp":"2017-05-16T16:59:21.255+0000","status":500,"error":"Internal Server Error","message":"Maximum upload size exceeded; nested exception is java.lang.IllegalStateException: org.apache.tomcat.util.http.fileupload.FileUploadBase$SizeLimitExceededException: the request was rejected because its size (14620179) exceeds the configured maximum (10485760)","path":"/upload"}

I got an oversize error, but I don't know if the MaxUploadSizeExceededException occurred, so let's take a look at the server log.

Server log(console)


...
2017-05-17 01:59:21.246 ERROR 26570 --- [nio-8080-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.web.multipart.MaxUploadSizeExceededException: Maximum upload size exceeded; nested exception is java.lang.IllegalStateException: org.apache.tomcat.util.http.fileupload.FileUploadBase$SizeLimitExceededException: the request was rejected because its size (14620179) exceeds the configured maximum (10485760)] with root cause

org.apache.tomcat.util.http.fileupload.FileUploadBase$SizeLimitExceededException: the request was rejected because its size (14620179) exceeds the configured maximum (10485760)
        at org.apache.tomcat.util.http.fileupload.FileUploadBase$FileItemIteratorImpl.<init>(FileUploadBase.java:811) ~[tomcat-embed-core-8.5.15.jar:8.5.15]
...

Looking at the log message, it seems that MaxUploadSizeExceededException has occurred, so let's add an exception handling process for MaxUploadSizeExceededException.

package com.example.spring5webdemo;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MaxUploadSizeExceededException;
import org.springframework.web.multipart.MultipartFile;

@RestController
public class UploadRestController {
	@PostMapping("/upload")
	String upload(MultipartFile file) {
		return file.getOriginalFilename() + " is uploaded !";
	}
	@ExceptionHandler(MaxUploadSizeExceededException.class) //★★★ Addition of exception handling
	@ResponseStatus(HttpStatus.PAYLOAD_TOO_LARGE)
	String handleMaxUploadSizeExceededException() {
		return "Upload file size is too large.";
	}
}

Furthermore ... The analysis of the multipart request is performed (delayed analysis) after the Handler method is determined. This will allow the @ ExceptionHandler method to handle file upload related exceptions.

src/main/resources/application.properties(Enable delay analysis for multipart requests)


spring.servlet.multipart.resolve-lazily=true

Restart Spring Boot and try uploading a file larger than 1MB again.

$ curl -s -D - -X POST -F file=@target/spring5-web-demo-0.0.1-SNAPSHOT.jar http://localhost:8080/upload
HTTP/1.1 100 

HTTP/1.1 413 
Content-Type: text/plain;charset=UTF-8
Content-Length: 30
Date: Tue, 16 May 2017 17:01:38 GMT
Connection: close

Upload file size is too large.

Note:

By the way ... When using Spring Boot, the file upload function of Servlet 3.0 is enabled by default, the maximum size of each file is 1M, and the maximum size of the entire request is limited to 10M. You can change the maximum size in ʻapplication.properties`.

#Maximum size per file
spring.servlet.multipart.max-file-size=2MB
#Maximum size of request
spring.servlet.multipart.max-request-size=20MB

A unified media type resolution mechanism is supported: thumbsup:

[SPR-14484] [SPR-14908]: Unified Media type resolution mechanism (MediaTypeFactory class) is added, and JAF (JavaBeans Activation framework) -dependent code is eliminated. In the new class, resolve media types according to the definition of /org/springframework/http/mime.types (media type and extension mapping file) stored in spring-web. ..

For example, if you create the following Handler method and upload the pom.xml file ...

@PostMapping("/upload")
String upload(MultipartFile file) {
	MediaTypeFactory.getMediaTypes(file.getOriginalFilename())
		.forEach(System.out::println);
	return file.getOriginalFilename() + " is uploaded !";
}

The following media types are output to the console.

application/xml

In addition, you can customize the mapping definition provided by spring-web by creating (copying) /org/springframework/http/mime.types in your project.

src/main/resources/org/springframework/http/mime.types


...(abridgement)
text/xml    xml

If you upload the pom.xml file again ... The following media types will be output to the console.

application/xml
text/xml

Data binding to immutable objects is possible: thumbsup:

[SPR-15199]: Allows data binding of request parameters to immutable objects. If parameter name information exists in the constructor argument (if -d or -parameters is added to the compile option), the request parameter value that matches the parameter name of the constructor argument and the request parameter name is bound. Will be done.

Note:

If there are multiple constructors, the default constructor will be called as before. Also, if you do not want to leave the parameter name information in the constructor argument, you can use @ java.beans.ConstructorProperties provided by Java Beans to map the constructor argument and request parameter.

Now, let's create an immutable class and specify that class as an argument of the Handler method.

package com.example.spring5webdemo;

import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.validation.constraints.NotNull;
import java.time.LocalDate;

@RestController
public class ImmutableObjectRestController {

	@GetMapping("/immutable")
	Query search(@Validated Query query) {
		return query;
	}

	public static class Query {

		@NotNull
		private final String name;
		private final String mail;
		private final String tel;
		private final LocalDate baseDate;

		public Query(String name, String mail, String tel,
				@DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate baseDate) {
			this.name = name;
			this.mail = mail;
			this.tel = tel;
			this.baseDate = baseDate;
		}

		public String getName() {
			return name;
		}

		public String getMail() {
			return mail;
		}

		public String getTel() {
			return tel;
		}

		public LocalDate getBaseDate() {
			return baseDate;
		}

	}

}

In the default state, if LocalDate is set to JSON, readability is very bad, so let's output a value formatted like that.

pom.xml


<dependency>
  <groupId>com.fasterxml.jackson.datatype</groupId>
  <artifactId>jackson-datatype-jsr310</artifactId> <!-- JSR-Added module to support serialization and deserialization of 310 classes-->
</dependency>

src/main/resources/application.properties


spring.jackson.serialization.write-dates-as-timestamps=false

First, try sending a request with all normal values.

$ curl -s -D - http://localhost:8080/immutable?name=kazuki43zoo\&[email protected]\&tel=09012345678\&baseDate=2017-08-01
HTTP/1.1 200 
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Tue, 16 May 2017 17:02:24 GMT

{"name":"kazuki43zoo","mail":"[email protected]","tel":"09012345678","baseDate":"2017-08-01"}

You're bound properly. Next, let's check if validation is performed without specifying name.

$ curl -s -D - http://localhost:8080/[email protected]\&tel=09012345678\&baseDate=2017-08-01
HTTP/1.1 400 
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Tue, 16 May 2017 17:04:25 GMT
Connection: close

{"timestamp":"2017-05-16T17:04:25.515+0000","status":400,"error":"Bad Request","errors":[{"codes":["NotNull.query.name","NotNull.name","NotNull.java.lang.String","NotNull"],"arguments":[{"codes":["query.name","name"],"arguments":null,"defaultMessage":"name","code":"name"}],"defaultMessage":"may not be null","objectName":"query","field":"name","rejectedValue":null,"bindingFailure":false,"code":"NotNull"}],"message":"Validation failed for object='query'. Error count: 1","path":"/immutable"}

This also seems to be working properly. Finally, specify an invalid date for baseDate (LocalDate) and check if a bind error occurs.

$ curl -s -D - http://localhost:8080/immutable?name=kazuki43zoo\&[email protected]\&tel=09012345678\&baseDate=2017-08-32
HTTP/1.1 400 
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Tue, 16 May 2017 17:05:10 GMT
Connection: close

{"timestamp":"2017-05-16T17:05:10.161+0000","status":400,"error":"Bad Request","message":"Failed to convert value of type 'java.lang.String[]' to required type 'java.time.LocalDate'; nested exception is org.springframework.core.convert.ConversionFailedException: Failed to convert from type [java.lang.String] to type [@org.springframework.format.annotation.DateTimeFormat java.time.LocalDate] for value '2017-08-32'; nested exception is java.lang.IllegalArgumentException: Parse attempt failed for value [2017-08-32]","path":"/immutable"}

It seems that this is also working properly.

Warning:

I changed the style to receive the error information at the time of binding error and validation error as BindingResult as follows ... I was able to receive the validation error, but an exception occurred at the time of binding error I couldn't receive it. (Is this a specification!? ...)

@GetMapping("/immutable")
Query search(@Validated Query query, BindingResult bindingResult) {
	// ...
	return query;
}

I wasn't sure if it was a specification or a bug, so I gave Spring JIRA an Issue (SPR-15542).

Jackson 2.9 supported: thumbsup:

[SPR-14925]: Apparently Jackson 2.9 ) Seems to be the required version on Spring Framework 5.0. Jackson 2.9 has not been officially released at this time (May 14, 2017), but it seems that Spring Framework 5.0 will be released around the time of official release. Looking at the changes in the Spring Framework ... From Jackson 2.9, if the error occurs due to a problem with the server-side implementation (POJO implementation), ʻInvalidDefinitionException` will be thrown, so that exception has occurred. It seems that I sometimes fixed it so that it would be a server error (500 Internal Server Error) instead of a client error (400 Bad Request).

JSR-367 JSON Binding supported: thumbsup:

[SPR-14923]: Java EE 8 component JSON Binding It is supported and can be used as an alternative to Jackson and GSON.

First, Spring Boot (spring-boot-starter-web) depends on Jackson, so if you want to use JSON-B, you need to remove Jackson from the dependent libraries and add the library for JSON-B. There is.

pom.xml


<dependencies>
  <!-- ... -->
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <exclusions>
      <exclusion>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId> <!--Exclude Jackson-->
      </exclusion>
    </exclusions>
  </dependency>

  <!-- ... -->

  <!--This entry is the official reference implementation of Yasson(+JSON-P implementation)Add-->
  <dependency>
    <groupId>org.eclipse</groupId>
    <artifactId>yasson</artifactId>
    <version>1.0.0-M2</version>
  </dependency>
  <dependency>
    <groupId>org.glassfish</groupId>
    <artifactId>javax.json</artifactId>
    <version>1.1.0-M2</version>
  </dependency>
</dependencies>

<!-- ... -->
<repositories>
  <!-- ... -->
  <!--Added yasson release repository(To be able to get the milestone version) -->
  <repository>
    <id>yasson-releases</id>
    <name>Yasson Release repository</name>
    <url>https://repo.eclipse.org/content/repositories/yasson-releases/</url>
  </repository>
</repositories>

Next, create the Controller class. There is nothing special to note when creating a Controller class, but it seems that it does not support binding to immutable classes.

package com.example.spring5webdemo;

import java.time.LocalDate;

import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class JsonbRestController {

	@PostMapping("/jsonb")
	Resource post(@RequestBody Resource resource) {
		return resource;
	}

	public static class Resource {

		private String name;
		private String mail;
		private String tel;
		private LocalDate baseDate;

		public String getName() {
			return name;
		}

		public void setName(String name) {
			this.name = name;
		}

		public String getMail() {
			return mail;
		}

		public void setMail(String mail) {
			this.mail = mail;
		}

		public String getTel() {
			return tel;
		}

		public void setTel(String tel) {
			this.tel = tel;
		}

		public LocalDate getBaseDate() {
			return baseDate;
		}

		public void setBaseDate(LocalDate baseDate) {
			this.baseDate = baseDate;
		}

	}

}

When you access the created endpoint, you can see that the requested JSON has been converted (deserialized) to a Java object and that object has been converted (serialized) to JSON.

$ curl -s -D - -X POST -H "Content-Type:application/json" -d '{"name":"kazuki43zoo","mail":"[email protected]","tel":"09012345678","baseDate":"2017-08-01"}' http://localhost:8080/jsonb
HTTP/1.1 200 
Content-Type: application/json;charset=UTF-8
Content-Length: 97
Date: Tue, 16 May 2017 17:08:40 GMT

{"baseDate":"2017-08-01","mail":"[email protected]","name":"kazuki43zoo","tel":"09012345678"}

JSON-B is also available in RestTemplate, a class for HTTP clients. You can also use JSON-B with the test HTTP client (TestRestTemplate) provided by Spring Boot.

package com.example.spring5webdemo;

import java.time.LocalDate;

import org.assertj.core.api.Assertions;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.web.server.LocalServerPort;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.web.client.RestOperations;
import org.springframework.web.client.RestTemplate;

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) //★★★ Start the server using a free port when running the test
public class JsonbTests {

	@LocalServerPort private int port; //★★★ The server startup port is injected

	@Autowired TestRestTemplate testRestTemplate; //★★★ HTTP client is injected to access the server startup port

	@Test
	public void testUsingRestTemplate() {
		LocalDate now = LocalDate.now();

		RestOperations restOperations = new RestTemplate(); //★★★ JSON in the default state-Linked component with B is registered
		Resource request = new Resource();
		request.setName("kazuki43zoo");
		request.setMail("[email protected]");
		request.setTel("09012345678");
		request.setBaseDate(now);

		Resource response = restOperations.postForObject("http://localhost:" + port + "/jsonb", request, Resource.class);

		Assertions.assertThat(response.getName()).isEqualTo("kazuki43zoo");
		Assertions.assertThat(response.getMail()).isEqualTo("[email protected]");
		Assertions.assertThat(response.getTel()).isEqualTo("09012345678");
		Assertions.assertThat(response.getBaseDate()).isEqualTo(now);

	}

	@Test
	public void testUsingTestRestTemplate() {
		LocalDate now = LocalDate.now();

		Resource request = new Resource();
		request.setName("kazuki43zoo");
		request.setMail("[email protected]");
		request.setTel("09012345678");
		request.setBaseDate(now);

		Resource response = testRestTemplate.postForObject("/jsonb", request, Resource.class); //★★★ If you use the HTTP client for testing, you can specify the URL from the path.

		Assertions.assertThat(response.getName()).isEqualTo("kazuki43zoo");
		Assertions.assertThat(response.getMail()).isEqualTo("[email protected]");
		Assertions.assertThat(response.getTel()).isEqualTo("09012345678");
		Assertions.assertThat(response.getBaseDate()).isEqualTo(now);

	}

	public static class Resource {

		private String name;
		private String mail;
		private String tel;
		private LocalDate baseDate;

		public String getName() {
			return name;
		}

		public void setName(String name) {
			this.name = name;
		}

		public String getMail() {
			return mail;
		}

		public void setMail(String mail) {
			this.mail = mail;
		}

		public String getTel() {
			return tel;
		}

		public void setTel(String tel) {
			this.tel = tel;
		}

		public LocalDate getBaseDate() {
			return baseDate;
		}

		public void setBaseDate(LocalDate baseDate) {
			this.baseDate = baseDate;
		}

	}

}

Google Protobuf 3.x supported: thumbs up:

[SPR-13589]: ProtobufHttpMessageConverter now supports Google Protobuf 3.x. Apparently, the default (com.google.protobuf: protobuf-java) supported formats are only" ʻapplication / x-protobuf" and " text / plain ", and other formats ("ʻapplication / For json "" ʻapplication / xml" " text / html "), separate libraries (" com.google.protobuf: protobuf-java-util(official) "and"com.googlecode.protobuf-" It seems that it is necessary to add java-format: protobuf-java-format` (made by 3rd party) ").

Reactor 3.1 and RxJava 1.3 / 2.1 classes can be returned with the Handler method: thumbsup:

[SPR-15365]: In connection with support for Reactive Programming Model (Spring WebFlux described below), Spring MVC also has Reactor 3.1 classes (Flux). You can now treat classes in , Mono), RxJava 1.3 and 2.1 (ʻObservable, Sigle, Flowable`, etc.) as the return value of the Handler method. When these classes (hereinafter referred to as "Reactive class") are returned, Spring MVC is a mechanism to process using asynchronous processing of Spring MVC (linkage function with asynchronous processing supported from Servlet 3.0). It has become.

Previously posted "[Understanding asynchronous requests on Spring MVC (+ Spring Boot)](http://qiita.com/kazuki43zoo/items/ce88dea403c596249e8a#spring-mvc%E3%81%8C%E3%82%" B5% E3% 83% 9D% E3% 83% BC% E3% 83% 88% E3% 81% 97% E3% 81% A6% E3% 81% 84% E3% 82% 8B% E9% 9D% 9E% Spring MVC as introduced in "E5% 90% 8C% E6% 9C% 9F% E5% 87% A6% E7% 90% 86% E3% 81% AE% E6% 96% B9% E5% BC% 8F)" Asynchronous processing of is divided into the following two methods.

Which method is used depends on the combination of Reactive class type and media type to be returned. Specifically, "[Spring Framework Reference -Async Requests with Reactive Types-](http://docs.spring.io/spring/docs/5.0.0.RC1/spring-framework-reference/web.html#mvc -ann-async-reactive-types) ".

Now, let's actually return the Reactive class. Since this entry uses the Reactor class used by Spring WebFlux, first add Reactor to the dependent library.

<dependency>
  <groupId>io.projectreactor</groupId>
  <artifactId>reactor-core</artifactId>
  <version>3.1.0.M1</version>
</dependency>

Next, implement the Handler method that returns the Mono and Flux of the Reactor.

package com.example.spring5webdemo;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;

import java.time.Duration;
import java.util.Random;

@RestController
public class ReactiveRestController {

	@GetMapping("/reactor/mono")
	Mono<Resource> getMono() {
		//When subscribed, return Resource with random number set in id
		//Call subscribeOn so that Subscriber processing is in a separate thread
		//    ->It is not necessary to process this sample code in a separate thread, but
		//    ->Actually, it is assumed that "heavy processing = CPU bound processing" is performed.
		return Mono.fromSupplier(() -> new Resource(new Random().nextInt()))
				.subscribeOn(Schedulers.parallel());
	}

	@GetMapping("/reactor/flux")
	Flux<Resource> getFlux() {
		//When subscribed, 0 is the Resource with id set to 0-4..Return every 5 seconds
		//If interval is used, Subscriber processing will be done in a separate thread.
		return Flux.from(Flux.range(0, 5)).zipWith(Flux.interval(Duration.ofMillis(500)))
				.map(tuple -> new Resource(tuple.getT1()));
	}

	static class Resource {
		private final int id;
		private Resource(int id) {
			this.id = id;
		}
		public int getId() {
			return id;
		}
	}

}

Finally, let's access the Handler method we created.

Mono+json


$ curl -s -D - http://localhost:8080/reactor/mono
HTTP/1.1 200 
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Tue, 16 May 2017 17:10:34 GMT

{"id":129024480}

Flux+event-stream


$ curl -s -D - http://localhost:8080/reactor/flux -H Accept:text/event-stream
HTTP/1.1 200 
Content-Type: text/event-stream;charset=UTF-8
Transfer-Encoding: chunked
Date: Tue, 16 May 2017 17:10:55 GMT

data:{"id":0}

data:{"id":1}

data:{"id":2}

data:{"id":3}

data:{"id":4}

Flux+stream+json


$ curl -s -D - http://localhost:8080/reactor/flux -H Accept:application/stream+json
HTTP/1.1 200 
Content-Type: application/stream+json
Transfer-Encoding: chunked
Date: Tue, 16 May 2017 17:11:22 GMT

{"id":0}
{"id":1}
{"id":2}
{"id":3}
{"id":4}

By the way ... If you return Flux with ʻapplication / json` ...

$ curl -s -D - http://localhost:8080/reactor/flux -H Accept:application/json
HTTP/1.1 200 
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Tue, 16 May 2017 17:11:48 GMT

[{"id":0},{"id":1},{"id":2},{"id":3},{"id":4}]

And the JSON will be replied after all the data has been passed to the Subscriber (about 2.5 seconds later).

ParsingPathMatcher is added: thumbsup:

[SPR-14544]: As an alternative to ʻAntPathMatcher, ParsingPathMatcher is added. Spring MVC uses ʻAntPathMatcher by default, but Spring WebFlux related components seem to use ParsingPathMatcher by default. I haven't seen it properly ...: sweat_smile: ʻIt seems that what could be expressed with AntPathMatcher can also be expressed with ParsingPathMatcher, and ParsingPathMatcheris newly called"{* variable name}` " Expressions are supported.

Taking the following sample as an example ... The path matching specification is the same as "/ users / **", but if you use "{* variable name}", it will be " / The "" part can be treated as a variable. Note that you cannot specify any characters after " { variable name} " such as " / users / { userPaths} .json` ".

package com.example.spring5webdemo;

import org.assertj.core.api.Assertions;
import org.junit.Test;
import org.springframework.util.PathMatcher;
import org.springframework.web.util.ParsingPathMatcher;

public class PathMatcherTests {

	@Test
	public void parsingPathMatcher() {
		PathMatcher pathMatcher = new ParsingPathMatcher();
		String pattern = "/users/{*userPaths}";
		{
			String path = "/users/kazuki43zoo/name";
			Assertions.assertThat(pathMatcher.match(pattern, path));
			Assertions.assertThat(pathMatcher.extractUriTemplateVariables(pattern, path))
				.containsEntry("userPaths", "/kazuki43zoo/name");
		}
		{
			String path = "/users/kazuki43zoo";
			Assertions.assertThat(pathMatcher.match(pattern, path));
			Assertions.assertThat(pathMatcher.extractUriTemplateVariables(pattern, path))
				.containsEntry("userPaths", "/kazuki43zoo");
		}
		{
			String path = "/users/";
			Assertions.assertThat(pathMatcher.match(pattern, path));
			Assertions.assertThat(pathMatcher.extractUriTemplateVariables(pattern, path))
				.containsEntry("userPaths", "/");
		}
		{
			String path = "/users";
			Assertions.assertThat(pathMatcher.match(pattern, path));
			Assertions.assertThat(pathMatcher.extractUriTemplateVariables(pattern, path))
				.containsEntry("userPaths", "");
		}
	}

}

If you want to use ParsingPathMatcher in Spring MVC request matching process, you can do it by defining the following Bean.

@Bean
public WebMvcConfigurer webMvcConfigurer() {
	return new WebMvcConfigurer() {
		@Override
		public void configurePathMatch(PathMatchConfigurer configurer) {
			//★★★ Apply ParsingPathMatcher
			configurer.setPathMatcher(new ParsingPathMatcher());
			//★★★ Disable useSuffixPatternMatch
			//★★★ When useSuffixPatternMatch is enabled, "" at the end of the pattern during request matching processing`.*`Is given and an error occurs during pattern analysis
			configurer.setUseSuffixPatternMatch(false);
		}
	};
}
package com.example.spring5webdemo;

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

@RestController
public class PathMatcherRestController {

	@GetMapping("/path-matcher/{*paths}") // ★★★ {*Variable name}To receive a variable value as a method argument using
	String get(@PathVariable String paths) {
		return paths;
	}

}
$ curl -s -D - http://localhost:8080/path-matcher/a/b/c
HTTP/1.1 200 
Content-Type: text/plain;charset=UTF-8
Content-Length: 6
Date: Tue, 16 May 2017 19:41:41 GMT

/a/b/c

Note:

By the way ... When I used "{* variable name}" in Spring WebFlux where ParsingPathMatcher is applied by default, an error occurred in the request mapping process ...: disappointed_relieved: When using "{* variable name}", is it a bug that must be set to "ʻuseSuffixPatternMatch = false`"? I feel like it, so I created Spring JIRA (SPR-15558).

RedirectAttributes can be received by the @ExceptionHandler method: thumbsup:

[SPR-14651]: You can now receive RedirectAttributes as an argument to the @ExceptionHandler method and redirect to via RedirectAttributes You will be able to link data with.

Note:

Looking at Issue, it is also backported to Spring Framework 4.3 ... I wonder if this is an issue ...

Normally, it is better to create a screen and check it, but ... It is troublesome to create a screen, so check using cURL.

package com.example.spring5webdemo;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

@Controller
public class RedirectAttributesController {

	@GetMapping("/redirect/{id}")
	@ResponseBody
	String getIdWithMessage(@PathVariable int id, @ModelAttribute("message") String message) { //The value set in RedirectAttributes can be obtained as a method argument
		return id + " : " + message;
	}

	@GetMapping("/redirect")
	String get() {
		throw new MyException("error.", 100);
	}

	@ExceptionHandler
	String handleMyException(MyException e, RedirectAttributes redirectAttributes) {
		redirectAttributes.addFlashAttribute("message", e.getMessage()); //★★★ Set message in Flush scope
		redirectAttributes.addAttribute("id", e.getId()); //★★★ Path variable of redirect path "{id}Set the value to be embedded in
		return "redirect:/redirect/{id}";
	}

	private static class MyException extends RuntimeException {
		private final int id;
		public MyException(String message, int id) {
			super(message);
			this.id = id;
		}
		public int getId() {
			return id;
		}
	}

}
$ curl -s -D - -L http://localhost:8080/redirect
HTTP/1.1 302 
Set-Cookie: JSESSIONID=13743780747DC2A73F038F2E0DECFDDD; Path=/; HttpOnly
Location: http://localhost:8080/redirect/100;jsessionid=13743780747DC2A73F038F2E0DECFDDD
Content-Language: ja-JP
Content-Length: 0
Date: Tue, 16 May 2017 21:46:50 GMT

HTTP/1.1 200 
Content-Type: text/plain;charset=UTF-8
Content-Length: 12
Date: Tue, 16 May 2017 21:46:50 GMT

100 : error.

ResponseStatusException is added: thumbsup:

[SPR-14895]: ResponseStatusException created for Spring WebFlux is now available in Spring MVC with status code and reason phrase By specifying and generating / throwing an exception, it becomes possible to determine the HTTP status to respond.

package com.example.spring5webdemo;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ResponseStatusException;

@RestController
public class ResponseStatusExceptionRestController {

	@GetMapping("/response-status")
	void get() {
		throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "request invalid."); //★★★ Throw an exception by specifying an arbitrary status code and reasonable phrase
	}

}
$ curl -s -D - -L http://localhost:8080/response-status
HTTP/1.1 400 
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Tue, 16 May 2017 22:34:21 GMT
Connection: close

{"timestamp":"2017-05-16T22:34:21.909+0000","status":400,"error":"Bad Request","message":"request invalid.","path":"/response-status"}

The objects required for "internationalization" and "template fragmentation" are now linked in ScriptTemplateView: thumbsup:

[SPR-15064]: "Internationalization of messages" in ScriptTemplateView (View implementation using JSR-223's script engine)" And the objects (Locale, MessageSource, etc.) required to realize "template fragmentation" will be passed to the template engine side. The implementation itself for "internationalization" and "fragmenting templates" is not provided by Spring.

In this entry, "nashorn" provided by Java SE is used as a script engine, and "mustache.js" is used as a template engine to "internationalize messages". And "template fragmentation".

First, configure to use ScriptTemplateView.

pom.xml(Install mustachejs)


<dependency>
  <groupId>org.webjars.bower</groupId>
  <artifactId>mustache</artifactId>
  <version>2.3.0</version> <!--Latest version at the time of writing-->
</dependency>
package com.example.spring5webdemo;

import org.springframework.boot.autoconfigure.web.servlet.WebMvcProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewResolverRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.view.script.ScriptTemplateConfigurer;

import java.util.Optional;

@Configuration
public class ScriptTemplateViewConfig implements WebMvcConfigurer {

	private final WebMvcProperties properties;

	public ScriptTemplateViewConfig(WebMvcProperties properties) {
		this.properties = properties;
	}

	@Override
	public void configureViewResolvers(ViewResolverRegistry registry) {
		WebMvcProperties.View view = properties.getView();
		registry.scriptTemplate()
				.prefix(Optional.ofNullable(view.getPrefix()).orElse("classpath:/templates/"))
				.suffix(Optional.ofNullable(view.getSuffix()).orElse(".html"));
	}

	@Bean
	ScriptTemplateConfigurer ScriptTemplateConfigurer() {
		ScriptTemplateConfigurer configurer = new ScriptTemplateConfigurer();
		configurer.setEngineName("nashorn");
		configurer.setScripts("/META-INF/resources/webjars/mustache/2.3.0/mustache.js", "/META-INF/js/render.js");
		configurer.setRenderFunction("render");
		return configurer;
	}
}

Create a JavaScript file to customize the Mustache standard rendering process. I used the term customization, but in reality I just added a custom function to the model and called the Mustache render function.

/main/resources/META-INF/js/render.js


function render(template, model, renderingContext) {
    model["message"] = function () { //★★★ Added custom function for "Internationalization of messages"
        return function (code, render) {
            var messageSource = renderingContext.applicationContext.getBean("messageSource");
            var locale = renderingContext.locale;
            return render(messageSource.getMessage(code.trim(), null, locale));
        }
    };
    model["include"] = function () { //★★★ Added custom function for "template fragmentation"
        return function (viewName, render) {
            return render(renderingContext.templateLoader.apply("templates/" + viewName.trim() + ".html"));
        }
    };
    return Mustache.render(template, model, renderingContext);
}

[RenderingContext](https://github.com/spring-projects/spring-framework/blob/master/spring-webmvc/src/main/java/org/springframework/web/servlet" in the third argument of the rendering method An instance of /view/script/RenderingContext.java) is passed, so get the necessary objects from it and process it.

Now, let's create a template file and Controller and actually run them.

src/main/resources/templates/script-template/home.html


<html>
<head>
    <title>{{title}}</title>
</head>
<body>

<p>{{body}}</p>

<p>{{#message}}welcome{{/message}}</p> <!--★★★ Output a message using a custom function-->

{{#include}}script-template/footer{{/include}} <!--★★★ Include fragments using custom functions-->

</body>
</html>

src/main/resources/templates/script-template/footer.html(Fragment)


<footer>
    Copyright (c) 2017 kazuki43zoo@com
</footer>
package com.example.spring5webdemo;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

import javax.servlet.http.HttpServletResponse;

@Controller
public class ScriptTemplateController {

	@GetMapping("/script-template")
	String home(Model model, HttpServletResponse response) {

		model.addAttribute("title", "Sample title")
			.addAttribute("body", "Sample body");

		return "script-template/home";
	}

}

You also need to create a message definition file.

src/main/resources/messages.properties


welcome=Hello!

src/main/resources/messages_en.properties


welcome=Hello!

When you start Spring Boot and access http: // localhost: 8080 / script-template ...

$ curl -s -D - http://localhost:8080/script-template
HTTP/1.1 200 
Content-Type: text/html;charset=UTF-8
Content-Language: ja-JP
Content-Length: 183
Date: Sat, 20 May 2017 01:51:53 GMT

<html>
<head>
    <title>Sample title</title>
</head>
<body>

<p>Sample body</p>

<p>Hello!</p>

<footer>
    Copyright (c) 2017 kazuki43zoo.com
</footer>

</body>
</html>

If you change ʻAccept-Language to ʻen (English) and access again ...

$ curl -s -D - -H Accept-Language:en http://localhost:8080/script-template
HTTP/1.1 200 
Content-Type: text/html;charset=UTF-8
Content-Language: en
Content-Length: 171
Date: Sat, 20 May 2017 01:53:46 GMT

<html>
<head>
    <title>Sample title</title>
</head>
<body>

<p>Sample body</p>

<p>Hello!</p>

<footer>
    Copyright (c) 2017 kazuki43zoo.com
</footer>

</body>
</html>

You can confirm that "Internationalization of message" and "Include fragment" have been performed.

Summary

This time, we introduced the main changes related to WebMVC. The Reactive Programming Model web framework (Spring WebFlux) has been added from Spring Framework 5.0, and it is attracting attention, but ... Spring MVC is still active (for the time being ... I use Spring MVC for work. I think there will be overwhelmingly more: sweat_smile :). Next time, I will introduce "Major changes related to Test".

Recommended Posts

Major changes related to Spring Framework 5.0 Web MVC
Major changes related to Spring Framework 5.0 Test
Major changes related to Spring Framework 5.0 DI container
Spring Framework 5.0 Summary of major changes
Major changes in Spring Framework 5.0 core functionality
Major changes in Spring Boot 1.5
To receive an empty request with Spring Web MVC @RequestBody
Changes when migrating from Spring Boot 1.5 to Spring Boot 2.0
Transition from Struts2 to Spring MVC (Controller)
Changes when migrating from Spring Boot 2.0 to Spring Boot 2.2
[Spring MVC] How to pass path variables
SameSite cookie in Spring Boot (Spring Web MVC + Tomcat)
I tried to link JavaFX and Spring Framework.
The official name of Spring MVC is Spring Web MVC
Memorandum (Spring Web)
I tried to implement file upload with Spring MVC
How to use Struts2 * Spring Framework (Spring plugin) June 2017 Version