fix: not clearing the template engine cache after upgrading the theme (#2970)

#### What type of PR is this?

/kind improvement

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

通过在模板引擎管理器里添加clearCache方法,在升级主题后进行缓存刷新,让新模板内容生效。

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

Fixes #2953 

#### Special notes for your reviewer:

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

```release-note
NONE
```
pull/3003/head
will 2022-12-19 10:24:10 +08:00 committed by GitHub
parent 64550d235f
commit efc940df99
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 52 additions and 2 deletions

View File

@ -37,6 +37,7 @@ import run.halo.app.extension.ReactiveExtensionClient;
import run.halo.app.extension.router.IListRequest; import run.halo.app.extension.router.IListRequest;
import run.halo.app.extension.router.QueryParamBuildUtil; import run.halo.app.extension.router.QueryParamBuildUtil;
import run.halo.app.infra.ThemeRootGetter; import run.halo.app.infra.ThemeRootGetter;
import run.halo.app.theme.TemplateEngineManager;
/** /**
* Endpoint for managing themes. * Endpoint for managing themes.
@ -54,11 +55,14 @@ public class ThemeEndpoint implements CustomEndpoint {
private final ThemeService themeService; private final ThemeService themeService;
private final TemplateEngineManager templateEngineManager;
public ThemeEndpoint(ReactiveExtensionClient client, ThemeRootGetter themeRoot, public ThemeEndpoint(ReactiveExtensionClient client, ThemeRootGetter themeRoot,
ThemeService themeService) { ThemeService themeService, TemplateEngineManager templateEngineManager) {
this.client = client; this.client = client;
this.themeRoot = themeRoot; this.themeRoot = themeRoot;
this.themeService = themeService; this.themeService = themeService;
this.templateEngineManager = templateEngineManager;
} }
@Override @Override
@ -180,6 +184,9 @@ public class ThemeEndpoint implements CustomEndpoint {
return Mono.error(e); return Mono.error(e);
} }
}) })
.flatMap((updatedTheme) -> templateEngineManager.clearCache(
updatedTheme.getMetadata().getName())
.thenReturn(updatedTheme))
.flatMap(updatedTheme -> ServerResponse.ok() .flatMap(updatedTheme -> ServerResponse.ok()
.bodyValue(updatedTheme)); .bodyValue(updatedTheme));
} }

View File

@ -7,12 +7,14 @@ import org.springframework.boot.autoconfigure.thymeleaf.ThymeleafProperties;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.util.ConcurrentLruCache; import org.springframework.util.ConcurrentLruCache;
import org.springframework.util.ResourceUtils; import org.springframework.util.ResourceUtils;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.dialect.IDialect; import org.thymeleaf.dialect.IDialect;
import org.thymeleaf.spring6.ISpringWebFluxTemplateEngine; import org.thymeleaf.spring6.ISpringWebFluxTemplateEngine;
import org.thymeleaf.spring6.dialect.SpringStandardDialect; import org.thymeleaf.spring6.dialect.SpringStandardDialect;
import org.thymeleaf.standard.expression.IStandardVariableExpressionEvaluator; import org.thymeleaf.standard.expression.IStandardVariableExpressionEvaluator;
import org.thymeleaf.templateresolver.FileTemplateResolver; import org.thymeleaf.templateresolver.FileTemplateResolver;
import org.thymeleaf.templateresolver.ITemplateResolver; import org.thymeleaf.templateresolver.ITemplateResolver;
import reactor.core.publisher.Mono;
import run.halo.app.infra.ExternalUrlSupplier; import run.halo.app.infra.ExternalUrlSupplier;
import run.halo.app.infra.exception.NotFoundException; import run.halo.app.infra.exception.NotFoundException;
import run.halo.app.theme.dialect.HaloProcessorDialect; import run.halo.app.theme.dialect.HaloProcessorDialect;
@ -47,14 +49,17 @@ public class TemplateEngineManager {
private final ObjectProvider<IDialect> dialects; private final ObjectProvider<IDialect> dialects;
private final ThemeResolver themeResolver;
public TemplateEngineManager(ThymeleafProperties thymeleafProperties, public TemplateEngineManager(ThymeleafProperties thymeleafProperties,
ExternalUrlSupplier externalUrlSupplier, ExternalUrlSupplier externalUrlSupplier,
ObjectProvider<ITemplateResolver> templateResolvers, ObjectProvider<ITemplateResolver> templateResolvers,
ObjectProvider<IDialect> dialects) { ObjectProvider<IDialect> dialects, ThemeResolver themeResolver) {
this.thymeleafProperties = thymeleafProperties; this.thymeleafProperties = thymeleafProperties;
this.externalUrlSupplier = externalUrlSupplier; this.externalUrlSupplier = externalUrlSupplier;
this.templateResolvers = templateResolvers; this.templateResolvers = templateResolvers;
this.dialects = dialects; this.dialects = dialects;
this.themeResolver = themeResolver;
engineCache = new ConcurrentLruCache<>(CACHE_SIZE_LIMIT, this::templateEngineGenerator); engineCache = new ConcurrentLruCache<>(CACHE_SIZE_LIMIT, this::templateEngineGenerator);
} }
@ -77,6 +82,16 @@ public class TemplateEngineManager {
} }
} }
public Mono<Void> clearCache(String themeName) {
return themeResolver.getThemeContext(themeName)
.doOnNext(themeContext -> {
TemplateEngine templateEngine =
(TemplateEngine) engineCache.get(themeContext);
templateEngine.clearTemplateCache();
})
.then();
}
private ISpringWebFluxTemplateEngine templateEngineGenerator(ThemeContext theme) { private ISpringWebFluxTemplateEngine templateEngineGenerator(ThemeContext theme) {
var engine = new SpringWebFluxTemplateEngine(); var engine = new SpringWebFluxTemplateEngine();
engine.setEnableSpringELCompiler(thymeleafProperties.isEnableSpringElCompiler()); engine.setEnableSpringELCompiler(thymeleafProperties.isEnableSpringElCompiler());

View File

@ -6,6 +6,7 @@ import org.apache.commons.lang3.StringUtils;
import org.springframework.boot.autoconfigure.thymeleaf.ThymeleafProperties; import org.springframework.boot.autoconfigure.thymeleaf.ThymeleafProperties;
import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import run.halo.app.infra.SystemConfigurableEnvironmentFetcher; import run.halo.app.infra.SystemConfigurableEnvironmentFetcher;
import run.halo.app.infra.SystemSetting.Theme; import run.halo.app.infra.SystemSetting.Theme;
@ -26,6 +27,22 @@ public class ThemeResolver {
private final ThymeleafProperties thymeleafProperties; private final ThymeleafProperties thymeleafProperties;
public Mono<ThemeContext> getThemeContext(String themeName) {
Assert.hasText(themeName, "Theme name cannot be empty");
var path = FilePathUtils.combinePath(haloProperties.getWorkDir().toString(),
THEME_WORK_DIR, themeName);
return Mono.just(ThemeContext.builder().name(themeName).path(path))
.flatMap(builder -> environmentFetcher.fetch(Theme.GROUP, Theme.class)
.mapNotNull(Theme::getActive)
.map(activatedTheme -> {
boolean active = StringUtils.equals(activatedTheme, themeName);
return builder.active(active);
})
.defaultIfEmpty(builder.active(false))
)
.map(ThemeContext.ThemeContextBuilder::build);
}
public Mono<ThemeContext> getTheme(ServerHttpRequest request) { public Mono<ThemeContext> getTheme(ServerHttpRequest request) {
return environmentFetcher.fetch(Theme.GROUP, Theme.class) return environmentFetcher.fetch(Theme.GROUP, Theme.class)
.map(Theme::getActive) .map(Theme::getActive)

View File

@ -51,6 +51,7 @@ class ThemeReconcilerTest {
@Mock @Mock
private HaloProperties haloProperties; private HaloProperties haloProperties;
@Mock
private File defaultTheme; private File defaultTheme;
private Path tempDirectory; private Path tempDirectory;

View File

@ -4,6 +4,7 @@ import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isA; import static org.mockito.ArgumentMatchers.isA;
import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import static org.springframework.web.reactive.function.BodyInserters.fromMultipartData; import static org.springframework.web.reactive.function.BodyInserters.fromMultipartData;
@ -32,6 +33,7 @@ import reactor.core.publisher.Mono;
import run.halo.app.core.extension.Theme; import run.halo.app.core.extension.Theme;
import run.halo.app.extension.Metadata; import run.halo.app.extension.Metadata;
import run.halo.app.infra.ThemeRootGetter; import run.halo.app.infra.ThemeRootGetter;
import run.halo.app.theme.TemplateEngineManager;
/** /**
* Tests for {@link ThemeEndpoint}. * Tests for {@link ThemeEndpoint}.
@ -48,6 +50,9 @@ class ThemeEndpointTest {
@Mock @Mock
ThemeService themeService; ThemeService themeService;
@Mock
TemplateEngineManager templateEngineManager;
@InjectMocks @InjectMocks
ThemeEndpoint themeEndpoint; ThemeEndpoint themeEndpoint;
@ -108,6 +113,9 @@ class ThemeEndpointTest {
when(themeService.upgrade(eq("default"), isA(InputStream.class))) when(themeService.upgrade(eq("default"), isA(InputStream.class)))
.thenReturn(Mono.just(newTheme)); .thenReturn(Mono.just(newTheme));
when(templateEngineManager.clearCache(eq("default")))
.thenReturn(Mono.empty());
webTestClient.post() webTestClient.post()
.uri("/themes/default/upgrade") .uri("/themes/default/upgrade")
.body(fromMultipartData(bodyBuilder.build())) .body(fromMultipartData(bodyBuilder.build()))
@ -115,6 +123,8 @@ class ThemeEndpointTest {
.expectStatus().isOk(); .expectStatus().isOk();
verify(themeService).upgrade(eq("default"), isA(InputStream.class)); verify(themeService).upgrade(eq("default"), isA(InputStream.class));
verify(templateEngineManager, times(1)).clearCache(eq("default"));
} }
} }