Add chunked transfer support for rendering templates (#6580)

#### What type of PR is this?

/kind improvement
/area core
/milestone 2.20.x

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

This PR adds chunked transfer support for rendering templates, which means that the max memory used by rendering template will be max chunk size instead of size of rendering result.

Users can define the max chunk size like below:

```yaml
spring:
  thymeleaf:
    reactive:
      maxChunkSize: 8KB # Setting to 0 will disable the chunked response.
```

#### Special notes for your reviewer:

1. Try to start Halo instance
2. Execute the command like below and see if the response headers contain `transfer-encoding: chunked`:
		
    ```bash
	http http://localhost:8090/ -p h
	HTTP/1.1 200 OK
	Cache-Control: no-cache, no-store, max-age=0, must-revalidate
	Content-Language: en-CN
	Content-Type: text/html
	Expires: 0
	Pragma: no-cache
	Referrer-Policy: strict-origin-when-cross-origin
	Vary: Origin
	Vary: Access-Control-Request-Method
	Vary: Access-Control-Request-Headers
	X-Content-Type-Options: nosniff
	X-Frame-Options: SAMEORIGIN
	X-XSS-Protection: 0
	content-encoding: gzip
	set-cookie: XSRF-TOKEN=1e677724-ce82-4b63-911c-f78b22cd9169; Path=/
	transfer-encoding: chunked
	```

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

```release-note
优化模板渲染时所需的内存
```
pull/6686/head
John Niang 2024-09-19 18:16:55 +08:00 committed by GitHub
parent 1c31917778
commit fb9aff00ca
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 56 additions and 5 deletions

View File

@ -6,11 +6,16 @@ import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import org.attoparser.ParseException;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.thymeleaf.ThymeleafProperties;
import org.springframework.boot.context.properties.PropertyMapper;
import org.springframework.context.ApplicationContext;
import org.springframework.core.Ordered;
import org.springframework.http.MediaType;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;
import org.springframework.util.unit.DataSize;
import org.springframework.web.ErrorResponse;
import org.springframework.web.reactive.result.view.View;
import org.springframework.web.server.ServerWebExchange;
@ -24,13 +29,16 @@ import run.halo.app.theme.finders.FinderRegistry;
import run.halo.app.theme.router.ModelConst;
@Component("thymeleafReactiveViewResolver")
public class HaloViewResolver extends ThymeleafReactiveViewResolver {
public class HaloViewResolver extends ThymeleafReactiveViewResolver implements InitializingBean {
private final FinderRegistry finderRegistry;
public HaloViewResolver(FinderRegistry finderRegistry) {
setViewClass(HaloView.class);
private final ThymeleafProperties thymeleafProperties;
public HaloViewResolver(FinderRegistry finderRegistry,
ThymeleafProperties thymeleafProperties) {
this.finderRegistry = finderRegistry;
this.thymeleafProperties = thymeleafProperties;
}
@Override
@ -44,6 +52,37 @@ public class HaloViewResolver extends ThymeleafReactiveViewResolver {
});
}
@Override
public void afterPropertiesSet() throws Exception {
setViewClass(HaloView.class);
var map = PropertyMapper.get();
map.from(thymeleafProperties::getEncoding)
.whenNonNull()
.to(this::setDefaultCharset);
map.from(thymeleafProperties::getExcludedViewNames)
.whenNonNull()
.to(this::setExcludedViewNames);
map.from(thymeleafProperties::getViewNames)
.whenNonNull()
.to(this::setViewNames);
var reactive = thymeleafProperties.getReactive();
map.from(reactive::getMediaTypes)
.whenNonNull()
.to(this::setSupportedMediaTypes);
map.from(reactive::getFullModeViewNames)
.whenNonNull()
.to(this::setFullModeViewNames);
map.from(reactive::getChunkedModeViewNames)
.whenNonNull()
.to(this::setChunkedModeViewNames);
map.from(reactive::getMaxChunkSize)
.asInt(DataSize::toBytes)
.when(size -> size > 0)
.to(this::setResponseMaxChunkSizeBytes);
setOrder(Ordered.LOWEST_PRECEDENCE - 5);
}
public static class HaloView extends ThymeleafReactiveView {
@Autowired

View File

@ -42,10 +42,19 @@ public class HaloTemplateEngine extends SpringWebFluxTemplateEngine {
// We have to subscribe on blocking thread, because some blocking operations will be present
// while processing.
if (publisher instanceof Mono<DataBuffer> mono) {
return mono.subscribeOn(Schedulers.boundedElastic());
return mono.subscribeOn(Schedulers.boundedElastic())
// We should switch back to non-blocking thread.
// See https://github.com/spring-projects/spring-framework/issues/26958
// for more details.
.publishOn(Schedulers.parallel());
}
if (publisher instanceof Flux<DataBuffer> flux) {
return flux.subscribeOn(Schedulers.boundedElastic());
return flux
.subscribeOn(Schedulers.boundedElastic())
// We should switch back to non-blocking thread.
// See https://github.com/spring-projects/spring-framework/issues/26958
// for more details.
.publishOn(Schedulers.parallel());
}
return publisher;
}

View File

@ -27,6 +27,9 @@ spring:
cache:
cachecontrol:
max-age: 365d
thymeleaf:
reactive:
maxChunkSize: 8KB
cache:
type: caffeine
caffeine: