Improve resources caching

Update controllers to add an ETag information so that meta-data is cached
on the client.

Also enables the compression for json, css and html resources.

Closes gh-165
This commit is contained in:
Stephane Nicoll 2015-12-11 11:15:07 +01:00
parent 89330da312
commit 5fb8c84e5e
4 changed files with 50 additions and 7 deletions

View File

@ -6,6 +6,12 @@ info:
spring-boot: spring-boot:
version: 1.3.0.RELEASE version: 1.3.0.RELEASE
server:
compression:
enabled: true
mime-types: application/json,text/css,text/html
min-response-size: 2048
initializr: initializr:
env: env:
boms: boms:

View File

@ -16,6 +16,9 @@
package io.spring.initializr.web package io.spring.initializr.web
import java.nio.charset.StandardCharsets
import java.util.concurrent.TimeUnit
import groovy.util.logging.Slf4j import groovy.util.logging.Slf4j
import io.spring.initializr.generator.CommandLineHelpGenerator import io.spring.initializr.generator.CommandLineHelpGenerator
import io.spring.initializr.mapper.InitializrMetadataJsonMapper import io.spring.initializr.mapper.InitializrMetadataJsonMapper
@ -27,11 +30,13 @@ import io.spring.initializr.generator.ProjectRequest
import io.spring.initializr.metadata.InitializrMetadata import io.spring.initializr.metadata.InitializrMetadata
import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Autowired
import org.springframework.http.CacheControl
import org.springframework.http.HttpHeaders import org.springframework.http.HttpHeaders
import org.springframework.http.HttpStatus import org.springframework.http.HttpStatus
import org.springframework.http.MediaType import org.springframework.http.MediaType
import org.springframework.http.ResponseEntity import org.springframework.http.ResponseEntity
import org.springframework.stereotype.Controller import org.springframework.stereotype.Controller
import org.springframework.util.DigestUtils
import org.springframework.web.bind.annotation.ModelAttribute import org.springframework.web.bind.annotation.ModelAttribute
import org.springframework.web.bind.annotation.RequestHeader import org.springframework.web.bind.annotation.RequestHeader
import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RequestMapping
@ -86,16 +91,20 @@ class MainController extends AbstractInitializrController {
def builder = ResponseEntity.ok().contentType(MediaType.TEXT_PLAIN) def builder = ResponseEntity.ok().contentType(MediaType.TEXT_PLAIN)
if (userAgent) { if (userAgent) {
if (userAgent.startsWith(WebConfig.CURL_USER_AGENT_PREFIX)) { if (userAgent.startsWith(WebConfig.CURL_USER_AGENT_PREFIX)) {
return builder.body(commandLineHelpGenerator.generateCurlCapabilities(metadata, appUrl)) def content = commandLineHelpGenerator.generateCurlCapabilities(metadata, appUrl)
return builder.eTag(createUniqueId(content)).body(content)
} }
if (userAgent.startsWith(WebConfig.HTTPIE_USER_AGENT_PREFIX)) { if (userAgent.startsWith(WebConfig.HTTPIE_USER_AGENT_PREFIX)) {
return builder.body(commandLineHelpGenerator.generateHttpieCapabilities(metadata, appUrl)) def content = commandLineHelpGenerator.generateHttpieCapabilities(metadata, appUrl)
return builder.eTag(createUniqueId(content)).body(content)
} }
if (userAgent.startsWith(WebConfig.SPRING_BOOT_CLI_AGENT_PREFIX)) { if (userAgent.startsWith(WebConfig.SPRING_BOOT_CLI_AGENT_PREFIX)) {
return builder.body(commandLineHelpGenerator.generateSpringBootCliCapabilities(metadata, appUrl)) def content = commandLineHelpGenerator.generateSpringBootCliCapabilities(metadata, appUrl)
return builder.eTag(createUniqueId(content)).body(content)
} }
} }
builder.body(commandLineHelpGenerator.generateGenericCapabilities(metadata, appUrl)) def content = commandLineHelpGenerator.generateGenericCapabilities(metadata, appUrl)
builder.eTag(createUniqueId(content)).body(content)
} }
@RequestMapping(value = "/", produces = ["application/hal+json"]) @RequestMapping(value = "/", produces = ["application/hal+json"])
@ -120,7 +129,8 @@ class MainController extends AbstractInitializrController {
private ResponseEntity<String> serviceCapabilitiesFor(InitializrMetadataVersion version, MediaType contentType) { private ResponseEntity<String> serviceCapabilitiesFor(InitializrMetadataVersion version, MediaType contentType) {
String appUrl = generateAppUrl() String appUrl = generateAppUrl()
def content = getJsonMapper(version).write(metadataProvider.get(), appUrl) def content = getJsonMapper(version).write(metadataProvider.get(), appUrl)
return ResponseEntity.ok().contentType(contentType).body(content) return ResponseEntity.ok().contentType(contentType).eTag(createUniqueId(content))
.cacheControl(CacheControl.maxAge(7, TimeUnit.DAYS)).body(content)
} }
private static InitializrMetadataJsonMapper getJsonMapper(InitializrMetadataVersion version) { private static InitializrMetadataJsonMapper getJsonMapper(InitializrMetadataVersion version) {
@ -220,4 +230,10 @@ class MainController extends AbstractInitializrController {
result result
} }
private String createUniqueId(String content) {
StringBuilder builder = new StringBuilder()
DigestUtils.appendMd5DigestAsHex(content.getBytes(StandardCharsets.UTF_8), builder)
builder.toString()
}
} }

View File

@ -16,6 +16,8 @@
package io.spring.initializr.web package io.spring.initializr.web
import java.nio.charset.StandardCharsets
import groovy.json.JsonBuilder import groovy.json.JsonBuilder
import io.spring.initializr.metadata.Dependency import io.spring.initializr.metadata.Dependency
import io.spring.initializr.metadata.InitializrMetadataProvider import io.spring.initializr.metadata.InitializrMetadataProvider
@ -23,6 +25,9 @@ import io.spring.initializr.util.Version
import io.spring.initializr.util.VersionRange import io.spring.initializr.util.VersionRange
import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Autowired
import org.springframework.http.MediaType
import org.springframework.http.ResponseEntity
import org.springframework.util.DigestUtils
import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController import org.springframework.web.bind.annotation.RestController
@ -40,7 +45,7 @@ class UiController {
protected InitializrMetadataProvider metadataProvider protected InitializrMetadataProvider metadataProvider
@RequestMapping(value = "/ui/dependencies", produces = ["application/json"]) @RequestMapping(value = "/ui/dependencies", produces = ["application/json"])
String dependencies(@RequestParam(required = false) String version) { ResponseEntity<String> dependencies(@RequestParam(required = false) String version) {
def dependencyGroups = metadataProvider.get().dependencies.content def dependencyGroups = metadataProvider.get().dependencies.content
def content = [] def content = []
Version v = version ? Version.parse(version) : null Version v = version ? Version.parse(version) : null
@ -55,7 +60,9 @@ class UiController {
} }
} }
} }
writeDependencies(content) def json = writeDependencies(content)
ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).
eTag(createUniqueId(json)).body(json)
} }
private static String writeDependencies(List<DependencyItem> items) { private static String writeDependencies(List<DependencyItem> items) {
@ -96,4 +103,10 @@ class UiController {
} }
} }
private String createUniqueId(String content) {
StringBuilder builder = new StringBuilder()
DigestUtils.appendMd5DigestAsHex(content.getBytes(StandardCharsets.UTF_8), builder)
builder.toString()
}
} }

View File

@ -24,6 +24,7 @@ import org.junit.Ignore
import org.junit.Test import org.junit.Test
import org.skyscreamer.jsonassert.JSONCompareMode import org.skyscreamer.jsonassert.JSONCompareMode
import org.springframework.http.HttpHeaders
import org.springframework.http.HttpStatus import org.springframework.http.HttpStatus
import org.springframework.http.MediaType import org.springframework.http.MediaType
import org.springframework.http.ResponseEntity import org.springframework.http.ResponseEntity
@ -32,6 +33,7 @@ import org.springframework.web.client.HttpClientErrorException
import static org.hamcrest.CoreMatchers.allOf import static org.hamcrest.CoreMatchers.allOf
import static org.hamcrest.CoreMatchers.containsString import static org.hamcrest.CoreMatchers.containsString
import static org.hamcrest.CoreMatchers.nullValue
import static org.hamcrest.core.IsNot.not import static org.hamcrest.core.IsNot.not
import static org.junit.Assert.assertEquals import static org.junit.Assert.assertEquals
import static org.junit.Assert.assertFalse import static org.junit.Assert.assertFalse
@ -169,6 +171,7 @@ class MainControllerIntegrationTests extends AbstractInitializrControllerIntegra
@Test @Test
void metadataWithCurrentAcceptHeader() { void metadataWithCurrentAcceptHeader() {
ResponseEntity<String> response = invokeHome(null, 'application/vnd.initializr.v2.1+json') ResponseEntity<String> response = invokeHome(null, 'application/vnd.initializr.v2.1+json')
assertThat(response.getHeaders().getFirst(HttpHeaders.ETAG), not(nullValue()))
validateContentType(response, CURRENT_METADATA_MEDIA_TYPE) validateContentType(response, CURRENT_METADATA_MEDIA_TYPE)
validateCurrentMetadata(new JSONObject(response.body)) validateCurrentMetadata(new JSONObject(response.body))
} }
@ -184,6 +187,7 @@ class MainControllerIntegrationTests extends AbstractInitializrControllerIntegra
@Test @Test
void metadataWithHalAcceptHeader() { void metadataWithHalAcceptHeader() {
ResponseEntity<String> response = invokeHome(null, 'application/hal+json') ResponseEntity<String> response = invokeHome(null, 'application/hal+json')
assertThat(response.getHeaders().getFirst(HttpHeaders.ETAG), not(nullValue()))
validateContentType(response, MainController.HAL_JSON_CONTENT_TYPE) validateContentType(response, MainController.HAL_JSON_CONTENT_TYPE)
validateCurrentMetadata(new JSONObject(response.body)) validateCurrentMetadata(new JSONObject(response.body))
} }
@ -283,6 +287,7 @@ class MainControllerIntegrationTests extends AbstractInitializrControllerIntegra
private void validateCurlHelpContent(ResponseEntity<String> response) { private void validateCurlHelpContent(ResponseEntity<String> response) {
validateContentType(response, MediaType.TEXT_PLAIN) validateContentType(response, MediaType.TEXT_PLAIN)
assertThat(response.getHeaders().getFirst(HttpHeaders.ETAG), not(nullValue()))
assertThat(response.body, allOf( assertThat(response.body, allOf(
containsString("Spring Initializr"), containsString("Spring Initializr"),
containsString('Examples:'), containsString('Examples:'),
@ -291,6 +296,7 @@ class MainControllerIntegrationTests extends AbstractInitializrControllerIntegra
private void validateHttpIeHelpContent(ResponseEntity<String> response) { private void validateHttpIeHelpContent(ResponseEntity<String> response) {
validateContentType(response, MediaType.TEXT_PLAIN) validateContentType(response, MediaType.TEXT_PLAIN)
assertThat(response.getHeaders().getFirst(HttpHeaders.ETAG), not(nullValue()))
assertThat(response.body, allOf( assertThat(response.body, allOf(
containsString("Spring Initializr"), containsString("Spring Initializr"),
containsString('Examples:'), containsString('Examples:'),
@ -300,6 +306,7 @@ class MainControllerIntegrationTests extends AbstractInitializrControllerIntegra
private void validateGenericHelpContent(ResponseEntity<String> response) { private void validateGenericHelpContent(ResponseEntity<String> response) {
validateContentType(response, MediaType.TEXT_PLAIN) validateContentType(response, MediaType.TEXT_PLAIN)
assertThat(response.getHeaders().getFirst(HttpHeaders.ETAG), not(nullValue()))
assertThat(response.body, allOf( assertThat(response.body, allOf(
containsString("Spring Initializr"), containsString("Spring Initializr"),
not(containsString('Examples:')), not(containsString('Examples:')),
@ -308,6 +315,7 @@ class MainControllerIntegrationTests extends AbstractInitializrControllerIntegra
private void validateSpringBootHelpContent(ResponseEntity<String> response) { private void validateSpringBootHelpContent(ResponseEntity<String> response) {
validateContentType(response, MediaType.TEXT_PLAIN) validateContentType(response, MediaType.TEXT_PLAIN)
assertThat(response.getHeaders().getFirst(HttpHeaders.ETAG), not(nullValue()))
assertThat(response.body, allOf( assertThat(response.body, allOf(
containsString("Service capabilities"), containsString("Service capabilities"),
containsString("Supported dependencies"), containsString("Supported dependencies"),