Adapt spring.web.resources.cache configuration (#3130)

#### What type of PR is this?

/kind improvement
/area core

#### What this PR does / why we need it:

This PR adapt web properties to control cache of static resources.

Users who want to disable cache can configure like this:

```yaml
spring:
  web:
    resources:
      cache:
        cachecontrol:
          no-cache: true
        use-last-modified: false
```

By default, we have disabled cache in development environment.

#### Which issue(s) this PR fixes:

Fixes https://github.com/halo-dev/halo/issues/3127

#### Does this PR introduce a user-facing change?

```release-note
None
```
pull/3145/head
John Niang 2023-01-11 10:38:40 +08:00 committed by GitHub
parent 92e57b056b
commit 2a70d59350
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 51 additions and 23 deletions

View File

@ -7,10 +7,10 @@ import static run.halo.app.infra.utils.FileUtils.checkDirectoryTraversal;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import java.net.URI; import java.net.URI;
import java.time.Duration;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.web.WebProperties;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
@ -45,12 +45,18 @@ public class WebFluxConfig implements WebFluxConfigurer {
private final HaloProperties haloProp; private final HaloProperties haloProp;
private final WebProperties.Resources resourceProperties;
private final ApplicationContext applicationContext; private final ApplicationContext applicationContext;
public WebFluxConfig(ObjectMapper objectMapper, HaloProperties haloProp, public WebFluxConfig(ObjectMapper objectMapper,
HaloProperties haloProp,
WebProperties webProperties,
ApplicationContext applicationContext) { ApplicationContext applicationContext) {
this.objectMapper = objectMapper; this.objectMapper = objectMapper;
this.haloProp = haloProp; this.haloProp = haloProp;
this.resourceProperties = webProperties.getResources();
this.applicationContext = applicationContext; this.applicationContext = applicationContext;
} }
@ -118,19 +124,22 @@ public class WebFluxConfig implements WebFluxConfigurer {
@Override @Override
public void addResourceHandlers(ResourceHandlerRegistry registry) { public void addResourceHandlers(ResourceHandlerRegistry registry) {
var attachmentsRoot = haloProp.getWorkDir().resolve("attachments"); var attachmentsRoot = haloProp.getWorkDir().resolve("attachments");
var cacheControl = CacheControl.maxAge(Duration.ofDays(365 / 2)); final var cacheControl = resourceProperties.getCache()
.getCachecontrol()
.toHttpCacheControl();
final var useLastModified = resourceProperties.getCache().isUseLastModified();
// Mandatory resource mapping // Mandatory resource mapping
var uploadRegistration = registry.addResourceHandler("/upload/**") var uploadRegistration = registry.addResourceHandler("/upload/**")
.addResourceLocations(FILE_URL_PREFIX + attachmentsRoot.resolve("upload") + "/") .addResourceLocations(FILE_URL_PREFIX + attachmentsRoot.resolve("upload") + "/")
.setUseLastModified(true) .setUseLastModified(useLastModified)
.setCacheControl(cacheControl); .setCacheControl(cacheControl);
// For console project // For console project
registry.addResourceHandler("/console/**") registry.addResourceHandler("/console/**")
.addResourceLocations(haloProp.getConsole().getLocation()) .addResourceLocations(haloProp.getConsole().getLocation())
.setCacheControl(cacheControl) .setCacheControl(cacheControl)
.setUseLastModified(true) .setUseLastModified(useLastModified)
.resourceChain(true) .resourceChain(true)
.addResolver(new EncodedResourceResolver()) .addResolver(new EncodedResourceResolver())
.addResolver(new PathResourceResolver()); .addResolver(new PathResourceResolver());
@ -149,7 +158,7 @@ public class WebFluxConfig implements WebFluxConfigurer {
checkDirectoryTraversal(attachmentsRoot, path); checkDirectoryTraversal(attachmentsRoot, path);
registration.addResourceLocations(FILE_URL_PREFIX + path + "/") registration.addResourceLocations(FILE_URL_PREFIX + path + "/")
.setCacheControl(cacheControl) .setCacheControl(cacheControl)
.setUseLastModified(true); .setUseLastModified(useLastModified);
}); });
}); });

View File

@ -6,7 +6,6 @@ import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.lang.reflect.Constructor; import java.lang.reflect.Constructor;
import java.nio.file.Path; import java.nio.file.Path;
import java.time.Duration;
import java.time.Instant; import java.time.Instant;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.pf4j.ClassLoadingStrategy; import org.pf4j.ClassLoadingStrategy;
@ -24,10 +23,10 @@ import org.pf4j.PluginRepository;
import org.pf4j.PluginStatusProvider; import org.pf4j.PluginStatusProvider;
import org.pf4j.RuntimeMode; import org.pf4j.RuntimeMode;
import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.web.WebProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.http.CacheControl;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import org.springframework.web.reactive.accept.RequestedContentTypeResolver; import org.springframework.web.reactive.accept.RequestedContentTypeResolver;
import org.springframework.web.reactive.function.BodyInserters; import org.springframework.web.reactive.function.BodyInserters;
@ -176,7 +175,9 @@ public class PluginAutoConfiguration {
} }
@Bean @Bean
public RouterFunction<ServerResponse> pluginJsBundleRoute(HaloPluginManager haloPluginManager) { public RouterFunction<ServerResponse> pluginJsBundleRoute(HaloPluginManager haloPluginManager,
WebProperties webProperties) {
var cacheProperties = webProperties.getResources().getCache();
return RouterFunctions.route() return RouterFunctions.route()
.GET("/plugins/{name}/assets/console/{*resource}", request -> { .GET("/plugins/{name}/assets/console/{*resource}", request -> {
String pluginName = request.pathVariable("name"); String pluginName = request.pathVariable("name");
@ -186,13 +187,17 @@ public class PluginAutoConfiguration {
if (jsBundle == null || !jsBundle.exists()) { if (jsBundle == null || !jsBundle.exists()) {
return ServerResponse.notFound().build(); return ServerResponse.notFound().build();
} }
var useLastModified = cacheProperties.isUseLastModified();
var bodyBuilder = ServerResponse.ok()
.cacheControl(cacheProperties.getCachecontrol().toHttpCacheControl());
try { try {
if (useLastModified) {
var lastModified = Instant.ofEpochMilli(jsBundle.lastModified()); var lastModified = Instant.ofEpochMilli(jsBundle.lastModified());
return request.checkNotModified(lastModified) return request.checkNotModified(lastModified)
.switchIfEmpty(Mono.defer(() -> ServerResponse.ok() .switchIfEmpty(Mono.defer(() -> bodyBuilder.lastModified(lastModified)
.cacheControl(CacheControl.maxAge(Duration.ofDays(365 / 2)))
.lastModified(lastModified)
.body(BodyInserters.fromResource(jsBundle)))); .body(BodyInserters.fromResource(jsBundle))));
}
return bodyBuilder.body(BodyInserters.fromResource(jsBundle));
} catch (IOException e) { } catch (IOException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }

View File

@ -6,12 +6,11 @@ import static org.springframework.web.reactive.function.server.RouterFunctions.r
import java.io.IOException; import java.io.IOException;
import java.nio.file.Path; import java.nio.file.Path;
import java.time.Duration;
import java.time.Instant; import java.time.Instant;
import org.springframework.boot.autoconfigure.web.WebProperties;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.FileSystemResource; import org.springframework.core.io.FileSystemResource;
import org.springframework.http.CacheControl;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.BodyInserters; import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.server.RouterFunction; import org.springframework.web.reactive.function.server.RouterFunction;
@ -34,20 +33,24 @@ public class ThemeConfiguration {
} }
@Bean @Bean
public RouterFunction<ServerResponse> themeAssets() { public RouterFunction<ServerResponse> themeAssets(WebProperties webProperties) {
var cacheProperties = webProperties.getResources().getCache();
return route( return route(
GET("/themes/{themeName}/assets/{*resource}").and(accept(MediaType.TEXT_PLAIN)), GET("/themes/{themeName}/assets/{*resource}").and(accept(MediaType.TEXT_PLAIN)),
request -> { request -> {
var themeName = request.pathVariable("themeName"); var themeName = request.pathVariable("themeName");
var resource = request.pathVariable("resource"); var resource = request.pathVariable("resource");
var fsRes = new FileSystemResource(getThemeAssetsPath(themeName, resource)); var fsRes = new FileSystemResource(getThemeAssetsPath(themeName, resource));
var bodyBuilder = ServerResponse.ok()
.cacheControl(cacheProperties.getCachecontrol().toHttpCacheControl());
try { try {
if (cacheProperties.isUseLastModified()) {
var lastModified = Instant.ofEpochMilli(fsRes.lastModified()); var lastModified = Instant.ofEpochMilli(fsRes.lastModified());
return request.checkNotModified(lastModified) return request.checkNotModified(lastModified)
.switchIfEmpty(Mono.defer(() -> ServerResponse.ok() .switchIfEmpty(Mono.defer(() -> bodyBuilder.lastModified(lastModified)
.cacheControl(CacheControl.maxAge(Duration.ofDays(356 / 2)))
.lastModified(lastModified)
.body(BodyInserters.fromResource(fsRes)))); .body(BodyInserters.fromResource(fsRes))));
}
return bodyBuilder.body(BodyInserters.fromResource(fsRes));
} catch (IOException e) { } catch (IOException e) {
return Mono.error(e); return Mono.error(e);
} }

View File

@ -7,6 +7,12 @@ spring:
enabled: always enabled: always
thymeleaf: thymeleaf:
cache: false cache: false
web:
resources:
cache:
cachecontrol:
no-cache: true
use-last-modified: false
halo: halo:
console: console:

View File

@ -21,6 +21,11 @@ spring:
max-in-memory-size: 10MB max-in-memory-size: 10MB
messages: messages:
basename: config.i18n.messages basename: config.i18n.messages
web:
resources:
cache:
cachecontrol:
max-age: 365d
halo: halo:
external-url: "http://${server.address:localhost}:${server.port}" external-url: "http://${server.address:localhost}:${server.port}"