diff --git a/build.gradle b/build.gradle index 21a704c..6420d63 100644 --- a/build.gradle +++ b/build.gradle @@ -32,16 +32,21 @@ jar { } } +ext { + guava = "31.1-jre" +} dependencies { compileOnly 'io.github.guqing:pluggable-suite:0.0.1-SNAPSHOT' compileOnly "io.swagger.core.v3:swagger-core-jakarta:2.2.0" compileOnly 'org.springframework.boot:spring-boot-starter-webflux' + compileOnly "com.google.guava:guava:$guava" compileOnly files("lib/halo-2.0.0-SNAPSHOT-plain.jar") compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' + testImplementation "com.google.guava:guava:$guava" testImplementation 'org.apache.commons:commons-lang3:3.12.0' testImplementation 'org.springframework.boot:spring-boot-starter-webflux' testImplementation 'org.springframework.boot:spring-boot-starter-test' diff --git a/src/main/java/run/halo/sitemap/CachedSitemapGetter.java b/src/main/java/run/halo/sitemap/CachedSitemapGetter.java new file mode 100644 index 0000000..5ae5245 --- /dev/null +++ b/src/main/java/run/halo/sitemap/CachedSitemapGetter.java @@ -0,0 +1,40 @@ +package run.halo.sitemap; + +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import java.time.Duration; +import lombok.AllArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Component; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Schedulers; + +@Component +@AllArgsConstructor +public class CachedSitemapGetter { + + private final Cache cache = CacheBuilder.newBuilder() + .concurrencyLevel(Runtime.getRuntime().availableProcessors()) + .initialCapacity(8) + .maximumSize(8) + .expireAfterWrite(Duration.ofSeconds(30)) + .build(); + + private final DefaultSitemapEntryLister lister; + + public Mono get() { + String cacheKey = "sitemap"; + return Mono.fromCallable(() -> cache.get(cacheKey, () -> lister.list() + .collectList() + .map(entries -> { + String xml = new SitemapBuilder() + .buildSitemapXml(entries); + cache.put(cacheKey, xml); + return xml; + }) + .defaultIfEmpty(StringUtils.EMPTY) + .block() + )) + .subscribeOn(Schedulers.boundedElastic()); + } +} diff --git a/src/main/java/run/halo/sitemap/DefaultSitemapEntryLister.java b/src/main/java/run/halo/sitemap/DefaultSitemapEntryLister.java index f76613b..8ceb93e 100644 --- a/src/main/java/run/halo/sitemap/DefaultSitemapEntryLister.java +++ b/src/main/java/run/halo/sitemap/DefaultSitemapEntryLister.java @@ -1,13 +1,15 @@ package run.halo.sitemap; +import java.time.Instant; +import java.util.Comparator; import java.util.List; import java.util.Objects; +import java.util.function.Function; import lombok.AllArgsConstructor; import lombok.Data; import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Component; import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; import run.halo.app.core.extension.Category; import run.halo.app.core.extension.Post; import run.halo.app.core.extension.SinglePage; @@ -20,7 +22,6 @@ @Component @AllArgsConstructor public class DefaultSitemapEntryLister implements SitemapEntryLister { - private final ReactiveExtensionClient client; private final SitemapGeneratorOptions options; @@ -34,27 +35,45 @@ public Flux list() { } private Flux listPostUrls() { - return client.list(Post.class, post -> post.isPublished() && !post.isDeleted(), null) + return client.list(Post.class, post -> post.isPublished() && !post.isDeleted() + && Post.VisibleEnum.PUBLIC.equals(post.getSpec().getVisible()), + defaultComparator()) .map(post -> post.getStatusOrDefault().getPermalink()); } + Comparator defaultComparator() { + Function createTime = post -> post.getMetadata().getCreationTimestamp(); + Function name = post -> post.getMetadata().getName(); + return Comparator.comparing(createTime).thenComparing(name); + } + private Flux listSinglePageUrls() { return client.list(SinglePage.class, singlePage -> singlePage.isPublished() && Objects.equals(false, singlePage.getSpec().getDeleted()) - && ExtensionOperator.isNotDeleted().test(singlePage), - null) + && ExtensionOperator.isNotDeleted().test(singlePage) + && Post.VisibleEnum.PUBLIC.equals(singlePage.getSpec().getVisible()), + pageDefaultComparator()) .map(post -> post.getStatusOrDefault().getPermalink()); } + Comparator pageDefaultComparator() { + Function createTime = + page -> page.getMetadata().getCreationTimestamp(); + Function name = page -> page.getMetadata().getName(); + return Comparator.comparing(createTime).thenComparing(name); + } + private Flux listCategoryUrls() { return client.list(Category.class, - category -> category.getMetadata().getDeletionTimestamp() == null, null) + category -> category.getMetadata().getDeletionTimestamp() == null, + Comparator.comparing(tag -> tag.getMetadata().getCreationTimestamp())) .map(category -> category.getStatusOrDefault().getPermalink()); } private Flux listTagUrls() { return client.list(Tag.class, - tag -> tag.getMetadata().getDeletionTimestamp() == null, null) + tag -> tag.getMetadata().getDeletionTimestamp() == null, + Comparator.comparing(tag -> tag.getMetadata().getCreationTimestamp())) .map(tag -> tag.getStatusOrDefault().getPermalink()); } diff --git a/src/main/java/run/halo/sitemap/DefaultSitemapXmlSupplier.java b/src/main/java/run/halo/sitemap/DefaultSitemapXmlSupplier.java deleted file mode 100644 index 0f6f7ca..0000000 --- a/src/main/java/run/halo/sitemap/DefaultSitemapXmlSupplier.java +++ /dev/null @@ -1,26 +0,0 @@ -package run.halo.sitemap; - -import java.util.function.Supplier; -import org.springframework.stereotype.Component; -import reactor.core.publisher.Mono; - -/** - * @author guqing - * @since 2.0.0 - */ -@Component -public class DefaultSitemapXmlSupplier implements Supplier> { - private final SitemapEntryLister sitemapEntryLister; - - public DefaultSitemapXmlSupplier(SitemapEntryLister sitemapEntryLister) { - this.sitemapEntryLister = sitemapEntryLister; - } - - @Override - public Mono get() { - return sitemapEntryLister.list() - .collectList() - .map(sitemapEntries -> new SitemapBuilder() - .buildSitemapXml(sitemapEntries)); - } -} diff --git a/src/main/java/run/halo/sitemap/SitemapPluginConfig.java b/src/main/java/run/halo/sitemap/SitemapPluginConfig.java index 521b6f7..8606ee8 100644 --- a/src/main/java/run/halo/sitemap/SitemapPluginConfig.java +++ b/src/main/java/run/halo/sitemap/SitemapPluginConfig.java @@ -39,9 +39,9 @@ public SitemapGeneratorOptions sitemapGeneratorOptions() @Bean RouterFunction sitemapRouterFunction( - DefaultSitemapXmlSupplier sitemapXmlSupplier) { + CachedSitemapGetter cachedSitemapGetter) { return RouterFunctions.route(GET("/sitemap.xml") - .and(accept(MediaType.TEXT_XML)), request -> sitemapXmlSupplier.get() + .and(accept(MediaType.TEXT_XML)), request -> cachedSitemapGetter.get() .flatMap(sitemap -> ServerResponse.ok() .contentType(MediaType.TEXT_XML).bodyValue(sitemap)) ); diff --git a/src/test/java/run/halo/sitemap/CachedSitemapGetterTest.java b/src/test/java/run/halo/sitemap/CachedSitemapGetterTest.java new file mode 100644 index 0000000..b3fefa7 --- /dev/null +++ b/src/test/java/run/halo/sitemap/CachedSitemapGetterTest.java @@ -0,0 +1,41 @@ +package run.halo.sitemap; + +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.concurrent.CountDownLatch; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import reactor.core.publisher.Flux; + +@ExtendWith(MockitoExtension.class) +public class CachedSitemapGetterTest { + @Mock + private DefaultSitemapEntryLister lister; + private CachedSitemapGetter getter; + + @BeforeEach + void setUp() { + when(lister.list()).thenReturn( + Flux.just(SitemapEntry.builder().loc("http://localhost:8090/about").build())); + getter = new CachedSitemapGetter(lister); + } + + @Test + void get() throws InterruptedException { + CountDownLatch countDownLatch = new CountDownLatch(10); + + for (int i = 0; i < countDownLatch.getCount(); i++) { + new Thread(() -> { + getter.get().block(); + countDownLatch.countDown(); + }).start(); + } + countDownLatch.await(); + verify(lister, times(1)).list(); + } +}