mirror of https://github.com/halo-dev/halo
perf: optimizing request plugin bundle resources (#4639)
#### What type of PR is this? /kind improvement /area core /milestone 2.10.x #### What this PR does / why we need it: 优化插件捆绑资源的查询性能 在插件 js bundle 1.2M 大小的情况下: 优化前: <img width="773" alt="image" src="https://github.com/halo-dev/halo/assets/38999863/e8e3b995-c8e9-44d7-b0ed-29eb82b975c5"> 优化后: <img width="765" alt="image" src="https://github.com/halo-dev/halo/assets/38999863/3860863e-1293-4713-ba6b-b101dec3a1e4"> how to test it? 在插件生产模式下使用此 PR 测试,比对与不使用此 PR 的 API 速度,且检查 js bundle 和 css bundle 的资源是否正确加载。 #### Does this PR introduce a user-facing change? ```release-note 优化插件捆绑资源(JSBundle)的查询性能以提高页面加载速度 ```pull/4669/head
parent
610609656d
commit
ce0e02a167
|
@ -19,11 +19,13 @@ import static run.halo.app.infra.utils.FileUtils.deleteFileSilently;
|
|||
import io.swagger.v3.oas.annotations.enums.ParameterIn;
|
||||
import io.swagger.v3.oas.annotations.media.ArraySchema;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
@ -31,13 +33,21 @@ import java.util.Map;
|
|||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.concurrent.locks.ReadWriteLock;
|
||||
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.function.Supplier;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.reactivestreams.Publisher;
|
||||
import org.springdoc.webflux.core.fn.SpringdocRouteBuilder;
|
||||
import org.springframework.beans.factory.DisposableBean;
|
||||
import org.springframework.core.io.FileSystemResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
import org.springframework.core.io.buffer.DataBufferUtils;
|
||||
import org.springframework.dao.OptimisticLockingFailureException;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.http.CacheControl;
|
||||
|
@ -45,14 +55,19 @@ import org.springframework.http.MediaType;
|
|||
import org.springframework.http.codec.multipart.FilePart;
|
||||
import org.springframework.http.codec.multipart.FormFieldPart;
|
||||
import org.springframework.http.codec.multipart.Part;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.FileSystemUtils;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.reactive.function.BodyInserters;
|
||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||
import org.springframework.web.reactive.function.server.ServerRequest;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.server.ServerWebInputException;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.scheduler.Scheduler;
|
||||
import reactor.core.scheduler.Schedulers;
|
||||
|
@ -82,6 +97,8 @@ public class PluginEndpoint implements CustomEndpoint {
|
|||
|
||||
private final Scheduler scheduler = Schedulers.boundedElastic();
|
||||
|
||||
private final BufferedPluginBundleResource bufferedPluginBundleResource;
|
||||
|
||||
@Override
|
||||
public RouterFunction<ServerResponse> endpoint() {
|
||||
final var tag = "api.console.halo.run/v1alpha1/Plugin";
|
||||
|
@ -240,36 +257,59 @@ public class PluginEndpoint implements CustomEndpoint {
|
|||
|
||||
private Mono<ServerResponse> fetchJsBundle(ServerRequest request) {
|
||||
Optional<String> versionOption = request.queryParam("v");
|
||||
if (versionOption.isEmpty()) {
|
||||
return pluginService.generateJsBundleVersion()
|
||||
return versionOption.map(s ->
|
||||
Mono.defer(() -> bufferedPluginBundleResource
|
||||
.getJsBundle(s, pluginService::uglifyJsBundle)
|
||||
).flatMap(fsRes -> {
|
||||
var bodyBuilder = ServerResponse.ok()
|
||||
.cacheControl(MAX_CACHE_CONTROL)
|
||||
.contentType(MediaType.valueOf("text/javascript"));
|
||||
try {
|
||||
Instant lastModified = Instant.ofEpochMilli(fsRes.lastModified());
|
||||
return request.checkNotModified(lastModified)
|
||||
.switchIfEmpty(Mono.defer(() ->
|
||||
bodyBuilder.lastModified(lastModified)
|
||||
.body(BodyInserters.fromResource(fsRes)))
|
||||
);
|
||||
} catch (IOException e) {
|
||||
return Mono.error(e);
|
||||
}
|
||||
})
|
||||
)
|
||||
.orElseGet(() -> pluginService.generateJsBundleVersion()
|
||||
.flatMap(v -> ServerResponse
|
||||
.temporaryRedirect(buildJsBundleUri("js", v))
|
||||
.build()
|
||||
);
|
||||
}
|
||||
return pluginService.uglifyJsBundle()
|
||||
.defaultIfEmpty("")
|
||||
.flatMap(bundle -> ServerResponse.ok()
|
||||
.cacheControl(MAX_CACHE_CONTROL)
|
||||
.contentType(MediaType.valueOf("text/javascript"))
|
||||
.bodyValue(bundle)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private Mono<ServerResponse> fetchCssBundle(ServerRequest request) {
|
||||
Optional<String> versionOption = request.queryParam("v");
|
||||
if (versionOption.isEmpty()) {
|
||||
return pluginService.generateJsBundleVersion()
|
||||
return versionOption.map(s ->
|
||||
Mono.defer(() -> bufferedPluginBundleResource.getCssBundle(s,
|
||||
pluginService::uglifyCssBundle)
|
||||
).flatMap(fsRes -> {
|
||||
var bodyBuilder = ServerResponse.ok()
|
||||
.cacheControl(MAX_CACHE_CONTROL)
|
||||
.contentType(MediaType.valueOf("text/css"));
|
||||
try {
|
||||
Instant lastModified = Instant.ofEpochMilli(fsRes.lastModified());
|
||||
return request.checkNotModified(lastModified)
|
||||
.switchIfEmpty(Mono.defer(() ->
|
||||
bodyBuilder.lastModified(lastModified)
|
||||
.body(BodyInserters.fromResource(fsRes)))
|
||||
);
|
||||
} catch (IOException e) {
|
||||
return Mono.error(e);
|
||||
}
|
||||
})
|
||||
)
|
||||
.orElseGet(() -> pluginService.generateJsBundleVersion()
|
||||
.flatMap(v -> ServerResponse
|
||||
.temporaryRedirect(buildJsBundleUri("css", v))
|
||||
.build()
|
||||
);
|
||||
}
|
||||
return pluginService.uglifyCssBundle()
|
||||
.flatMap(bundle -> ServerResponse.ok()
|
||||
.cacheControl(MAX_CACHE_CONTROL)
|
||||
.contentType(MediaType.valueOf("text/css"))
|
||||
.bodyValue(bundle)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -353,7 +393,7 @@ public class PluginEndpoint implements CustomEndpoint {
|
|||
if (!configMapName.equals(configMapNameToUpdate)) {
|
||||
throw new ServerWebInputException(
|
||||
"The name from the request body does not match the plugin "
|
||||
+ "configMapName name.");
|
||||
+ "configMapName name.");
|
||||
}
|
||||
})
|
||||
.flatMap(configMapToUpdate -> client.fetch(ConfigMap.class, configMapName)
|
||||
|
@ -482,7 +522,7 @@ public class PluginEndpoint implements CustomEndpoint {
|
|||
@ArraySchema(uniqueItems = true,
|
||||
arraySchema = @Schema(name = "sort",
|
||||
description = "Sort property and direction of the list result. Supported fields: "
|
||||
+ "creationTimestamp"),
|
||||
+ "creationTimestamp"),
|
||||
schema = @Schema(description = "like field,asc or field,desc",
|
||||
implementation = String.class,
|
||||
example = "creationTimestamp,desc"))
|
||||
|
@ -633,4 +673,111 @@ public class PluginEndpoint implements CustomEndpoint {
|
|||
.subscribeOn(this.scheduler);
|
||||
}
|
||||
|
||||
@Component
|
||||
static class BufferedPluginBundleResource implements DisposableBean {
|
||||
|
||||
private final AtomicReference<FileSystemResource> jsBundle = new AtomicReference<>();
|
||||
private final AtomicReference<FileSystemResource> cssBundle = new AtomicReference<>();
|
||||
|
||||
private final ReadWriteLock jsLock = new ReentrantReadWriteLock();
|
||||
private final ReadWriteLock cssLock = new ReentrantReadWriteLock();
|
||||
|
||||
private Path tempDir;
|
||||
|
||||
public Mono<FileSystemResource> getJsBundle(String version,
|
||||
Supplier<Flux<DataBuffer>> jsSupplier) {
|
||||
var fileName = tempFileName(version, ".js");
|
||||
return Mono.defer(() -> {
|
||||
jsLock.readLock().lock();
|
||||
try {
|
||||
var jsBundleResource = jsBundle.get();
|
||||
if (getResourceIfNotChange(fileName, jsBundleResource) != null) {
|
||||
return Mono.just(jsBundleResource);
|
||||
}
|
||||
} finally {
|
||||
jsLock.readLock().unlock();
|
||||
}
|
||||
|
||||
jsLock.writeLock().lock();
|
||||
try {
|
||||
var oldJsBundle = jsBundle.get();
|
||||
return writeBundle(fileName, jsSupplier)
|
||||
.doOnNext(newRes -> jsBundle.compareAndSet(oldJsBundle, newRes));
|
||||
} finally {
|
||||
jsLock.writeLock().unlock();
|
||||
}
|
||||
}).subscribeOn(Schedulers.boundedElastic());
|
||||
}
|
||||
|
||||
public Mono<FileSystemResource> getCssBundle(String version,
|
||||
Supplier<Flux<DataBuffer>> cssSupplier) {
|
||||
var fileName = tempFileName(version, ".css");
|
||||
return Mono.defer(() -> {
|
||||
try {
|
||||
cssLock.readLock().lock();
|
||||
var cssBundleResource = cssBundle.get();
|
||||
if (getResourceIfNotChange(fileName, cssBundleResource) != null) {
|
||||
return Mono.just(cssBundleResource);
|
||||
}
|
||||
} finally {
|
||||
cssLock.readLock().unlock();
|
||||
}
|
||||
|
||||
cssLock.writeLock().lock();
|
||||
try {
|
||||
var oldCssBundle = cssBundle.get();
|
||||
return writeBundle(fileName, cssSupplier)
|
||||
.doOnNext(newRes -> cssBundle.compareAndSet(oldCssBundle, newRes));
|
||||
} finally {
|
||||
cssLock.writeLock().unlock();
|
||||
}
|
||||
}).subscribeOn(Schedulers.boundedElastic());
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private Resource getResourceIfNotChange(String fileName, Resource resource) {
|
||||
if (resource != null && resource.exists() && fileName.equals(resource.getFilename())) {
|
||||
return resource;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private Mono<FileSystemResource> writeBundle(String fileName,
|
||||
Supplier<Flux<DataBuffer>> dataSupplier) {
|
||||
return Mono.defer(
|
||||
() -> {
|
||||
var filePath = createTempFileToStore(fileName);
|
||||
return DataBufferUtils.write(dataSupplier.get(), filePath)
|
||||
.then(Mono.fromSupplier(() -> new FileSystemResource(filePath)));
|
||||
});
|
||||
}
|
||||
|
||||
private Path createTempFileToStore(String fileName) {
|
||||
try {
|
||||
if (tempDir == null || !Files.exists(tempDir)) {
|
||||
this.tempDir = Files.createTempDirectory("halo-plugin-bundle");
|
||||
}
|
||||
var path = tempDir.resolve(fileName);
|
||||
Files.deleteIfExists(path);
|
||||
return Files.createFile(path);
|
||||
} catch (IOException e) {
|
||||
throw new ServerWebInputException("Failed to create temp file.", null, e);
|
||||
}
|
||||
}
|
||||
|
||||
private String tempFileName(String v, String suffix) {
|
||||
Assert.notNull(v, "Version must not be null");
|
||||
Assert.notNull(suffix, "Suffix must not be null");
|
||||
return v + suffix;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() throws Exception {
|
||||
if (tempDir != null && Files.exists(tempDir)) {
|
||||
FileSystemUtils.deleteRecursively(tempDir);
|
||||
}
|
||||
this.jsBundle.set(null);
|
||||
this.cssBundle.set(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package run.halo.app.core.extension.service;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
import org.springframework.web.server.ServerWebInputException;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
@ -46,14 +47,14 @@ public interface PluginService {
|
|||
*
|
||||
* @return uglified js bundle
|
||||
*/
|
||||
Mono<String> uglifyJsBundle();
|
||||
Flux<DataBuffer> uglifyJsBundle();
|
||||
|
||||
/**
|
||||
* Uglify css bundle from all enabled plugins to a single css bundle string.
|
||||
*
|
||||
* @return uglified css bundle
|
||||
*/
|
||||
Mono<String> uglifyCssBundle();
|
||||
Flux<DataBuffer> uglifyCssBundle();
|
||||
|
||||
/**
|
||||
* <p>Generate js bundle version for cache control.</p>
|
||||
|
|
|
@ -9,7 +9,6 @@ import java.nio.charset.StandardCharsets;
|
|||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
@ -21,9 +20,13 @@ import org.apache.commons.lang3.Validate;
|
|||
import org.pf4j.PluginWrapper;
|
||||
import org.pf4j.RuntimeMode;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
import org.springframework.core.io.buffer.DataBufferUtils;
|
||||
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
|
||||
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StreamUtils;
|
||||
import org.springframework.web.server.ServerWebInputException;
|
||||
import reactor.core.Exceptions;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
@ -133,55 +136,52 @@ public class PluginServiceImpl implements PluginService {
|
|||
}
|
||||
|
||||
@Override
|
||||
public Mono<String> uglifyJsBundle() {
|
||||
return Mono.fromSupplier(() -> {
|
||||
StringBuilder jsBundle = new StringBuilder();
|
||||
List<String> pluginNames = new ArrayList<>();
|
||||
for (PluginWrapper pluginWrapper : pluginManager.getStartedPlugins()) {
|
||||
String pluginName = pluginWrapper.getPluginId();
|
||||
pluginNames.add(pluginName);
|
||||
Resource jsBundleResource =
|
||||
BundleResourceUtils.getJsBundleResource(pluginManager, pluginName,
|
||||
BundleResourceUtils.JS_BUNDLE);
|
||||
if (jsBundleResource != null) {
|
||||
try {
|
||||
jsBundle.append(
|
||||
jsBundleResource.getContentAsString(StandardCharsets.UTF_8));
|
||||
jsBundle.append("\n");
|
||||
} catch (IOException e) {
|
||||
log.error("Failed to read js bundle of plugin [{}]", pluginName, e);
|
||||
}
|
||||
public Flux<DataBuffer> uglifyJsBundle() {
|
||||
var startedPlugins = List.copyOf(pluginManager.getStartedPlugins());
|
||||
String plugins = """
|
||||
this.enabledPluginNames = [%s];
|
||||
""".formatted(startedPlugins.stream()
|
||||
.map(PluginWrapper::getPluginId)
|
||||
.collect(Collectors.joining("','", "'", "'")));
|
||||
return Flux.fromIterable(startedPlugins)
|
||||
.mapNotNull(pluginWrapper -> {
|
||||
var pluginName = pluginWrapper.getPluginId();
|
||||
return BundleResourceUtils.getJsBundleResource(pluginManager, pluginName,
|
||||
BundleResourceUtils.JS_BUNDLE);
|
||||
})
|
||||
.flatMap(resource -> {
|
||||
try {
|
||||
// Specifying bufferSize as resource content length is
|
||||
// to append line breaks at the end of each plugin
|
||||
return DataBufferUtils.read(resource, DefaultDataBufferFactory.sharedInstance,
|
||||
(int) resource.contentLength())
|
||||
.doOnNext(dataBuffer -> {
|
||||
// add a new line after each plugin bundle to avoid syntax error
|
||||
dataBuffer.write("\n".getBytes(StandardCharsets.UTF_8));
|
||||
});
|
||||
} catch (IOException e) {
|
||||
log.error("Failed to read plugin bundle resource", e);
|
||||
return Flux.empty();
|
||||
}
|
||||
}
|
||||
|
||||
String plugins = """
|
||||
this.enabledPluginNames = [%s];
|
||||
""".formatted(pluginNames.stream()
|
||||
.collect(Collectors.joining("','", "'", "'")));
|
||||
return jsBundle + plugins;
|
||||
});
|
||||
})
|
||||
.concatWith(Flux.defer(() -> {
|
||||
var dataBuffer = DefaultDataBufferFactory.sharedInstance
|
||||
.wrap(plugins.getBytes(StandardCharsets.UTF_8));
|
||||
return Flux.just(dataBuffer);
|
||||
}));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<String> uglifyCssBundle() {
|
||||
return Mono.fromSupplier(() -> {
|
||||
StringBuilder cssBundle = new StringBuilder();
|
||||
for (PluginWrapper pluginWrapper : pluginManager.getStartedPlugins()) {
|
||||
public Flux<DataBuffer> uglifyCssBundle() {
|
||||
return Flux.fromIterable(pluginManager.getStartedPlugins())
|
||||
.mapNotNull(pluginWrapper -> {
|
||||
String pluginName = pluginWrapper.getPluginId();
|
||||
Resource cssBundleResource =
|
||||
BundleResourceUtils.getJsBundleResource(pluginManager, pluginName,
|
||||
BundleResourceUtils.CSS_BUNDLE);
|
||||
if (cssBundleResource != null) {
|
||||
try {
|
||||
cssBundle.append(
|
||||
cssBundleResource.getContentAsString(StandardCharsets.UTF_8));
|
||||
} catch (IOException e) {
|
||||
log.error("Failed to read css bundle of plugin [{}]", pluginName, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
return cssBundle.toString();
|
||||
});
|
||||
return BundleResourceUtils.getJsBundleResource(pluginManager, pluginName,
|
||||
BundleResourceUtils.CSS_BUNDLE);
|
||||
})
|
||||
.flatMap(resource -> DataBufferUtils.read(resource,
|
||||
DefaultDataBufferFactory.sharedInstance, StreamUtils.BUFFER_SIZE)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -259,7 +259,7 @@ public class PluginServiceImpl implements PluginService {
|
|||
if (!VersionUtils.satisfiesRequires(systemVersion, requires)) {
|
||||
throw new UnsatisfiedAttributeValueException(String.format(
|
||||
"Plugin requires a minimum system version of [%s], but the current version is "
|
||||
+ "[%s].",
|
||||
+ "[%s].",
|
||||
requires, systemVersion),
|
||||
"problemDetail.plugin.version.unsatisfied.requires",
|
||||
new String[] {requires, systemVersion});
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package run.halo.app.core.extension.endpoint;
|
||||
|
||||
import static java.util.Objects.requireNonNull;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.argThat;
|
||||
|
@ -16,6 +17,7 @@ import static org.springframework.web.reactive.function.BodyInserters.fromMultip
|
|||
import com.github.zafarkhaja.semver.Version;
|
||||
import java.io.IOException;
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
|
@ -33,11 +35,15 @@ import org.mockito.InjectMocks;
|
|||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.core.io.FileSystemResource;
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.client.MultipartBodyBuilder;
|
||||
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||
import org.springframework.web.server.ServerWebInputException;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.test.StepVerifier;
|
||||
import run.halo.app.core.extension.Plugin;
|
||||
import run.halo.app.core.extension.Setting;
|
||||
import run.halo.app.core.extension.service.PluginService;
|
||||
|
@ -130,8 +136,8 @@ class PluginEndpointTest {
|
|||
|
||||
verify(client).list(same(Plugin.class), argThat(
|
||||
predicate -> predicate.test(expectPlugin)
|
||||
&& !predicate.test(unexpectedPlugin1)
|
||||
&& !predicate.test(unexpectedPlugin2)),
|
||||
&& !predicate.test(unexpectedPlugin1)
|
||||
&& !predicate.test(unexpectedPlugin2)),
|
||||
any(), anyInt(), anyInt());
|
||||
}
|
||||
|
||||
|
@ -158,8 +164,8 @@ class PluginEndpointTest {
|
|||
|
||||
verify(client).list(same(Plugin.class), argThat(
|
||||
predicate -> predicate.test(expectPlugin)
|
||||
&& !predicate.test(unexpectedPlugin1)
|
||||
&& !predicate.test(unexpectedPlugin2)),
|
||||
&& !predicate.test(unexpectedPlugin1)
|
||||
&& !predicate.test(unexpectedPlugin2)),
|
||||
any(), anyInt(), anyInt());
|
||||
}
|
||||
|
||||
|
@ -382,4 +388,90 @@ class PluginEndpointTest {
|
|||
plugin.setSpec(spec);
|
||||
return plugin;
|
||||
}
|
||||
|
||||
@Nested
|
||||
class BufferedPluginBundleResourceTest {
|
||||
private final PluginEndpoint.BufferedPluginBundleResource bufferedPluginBundleResource =
|
||||
new PluginEndpoint.BufferedPluginBundleResource();
|
||||
|
||||
private static Flux<DataBuffer> getDataBufferFlux(String x) {
|
||||
var buffer = DefaultDataBufferFactory.sharedInstance
|
||||
.wrap(x.getBytes(StandardCharsets.UTF_8));
|
||||
return Flux.just(buffer);
|
||||
}
|
||||
|
||||
@Test
|
||||
void writeAndGetJsResourceTest() {
|
||||
bufferedPluginBundleResource.getJsBundle("1",
|
||||
() -> getDataBufferFlux("first line\nnext line"))
|
||||
.as(StepVerifier::create)
|
||||
.consumeNextWith(resource -> {
|
||||
try {
|
||||
String content = resource.getContentAsString(StandardCharsets.UTF_8);
|
||||
assertThat(content).isEqualTo("first line\nnext line");
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
})
|
||||
.verifyComplete();
|
||||
|
||||
// version is matched, should return cached content
|
||||
bufferedPluginBundleResource.getJsBundle("1",
|
||||
() -> getDataBufferFlux("first line\nnext line-1"))
|
||||
.as(StepVerifier::create)
|
||||
.consumeNextWith(resource -> {
|
||||
try {
|
||||
String content = resource.getContentAsString(StandardCharsets.UTF_8);
|
||||
assertThat(content).isEqualTo("first line\nnext line");
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
})
|
||||
.verifyComplete();
|
||||
|
||||
// new version should return new content
|
||||
bufferedPluginBundleResource.getJsBundle("2",
|
||||
() -> getDataBufferFlux("first line\nnext line-2"))
|
||||
.as(StepVerifier::create)
|
||||
.consumeNextWith(resource -> {
|
||||
try {
|
||||
String content = resource.getContentAsString(StandardCharsets.UTF_8);
|
||||
assertThat(content).isEqualTo("first line\nnext line-2");
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
})
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
void writeAndGetCssResourceTest() {
|
||||
bufferedPluginBundleResource.getCssBundle("1",
|
||||
() -> getDataBufferFlux("first line\nnext line"))
|
||||
.as(StepVerifier::create)
|
||||
.consumeNextWith(resource -> {
|
||||
try {
|
||||
String content = resource.getContentAsString(StandardCharsets.UTF_8);
|
||||
assertThat(content).isEqualTo("first line\nnext line");
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
})
|
||||
.verifyComplete();
|
||||
|
||||
// version is matched, should return cached content
|
||||
bufferedPluginBundleResource.getCssBundle("1",
|
||||
() -> getDataBufferFlux("first line\nnext line-1"))
|
||||
.as(StepVerifier::create)
|
||||
.consumeNextWith(resource -> {
|
||||
try {
|
||||
String content = resource.getContentAsString(StandardCharsets.UTF_8);
|
||||
assertThat(content).isEqualTo("first line\nnext line");
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
})
|
||||
.verifyComplete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue