Skip to content

Commit 7d95aa8

Browse files
makingclaude
andcommitted
Add Zstandard compression support
Adds a third compression algorithm alongside Brotli and Gzip via the zstd-jni library. Zstd is enabled by default at level 22 (maximum). Spring Framework's EncodedResourceResolver does not yet serve .zst files (spring-projects/spring-framework#36647), so users serving via Spring's resource chain should set zstdEnabled=false until that change is released. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 5b80d98 commit 7d95aa8

12 files changed

Lines changed: 276 additions & 3 deletions

File tree

README.md

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
# Compression Maven Plugin
22

3-
A Maven plugin that compresses static resources using [Brotli](https://github.com/google/brotli) and Gzip.
3+
A Maven plugin that compresses static resources using [Brotli](https://github.com/google/brotli), Gzip, and [Zstandard](https://facebook.github.io/zstd/).
44

55
## When to use this plugin
66

7-
If your web application serves static resources (JavaScript, CSS, SVG, etc.) and your web server supports serving pre-compressed files, this plugin generates `.br` and `.gz` variants at build time so the server can serve them directly without compressing on every request.
7+
If your web application serves static resources (JavaScript, CSS, SVG, etc.) and your web server supports serving pre-compressed files, this plugin generates `.br`, `.gz`, and `.zst` variants at build time so the server can serve them directly without compressing on every request.
88

99
This plugin is designed to work with Spring Framework's `ResourceResolver` chain. The default resource directories and file extensions match Spring Boot's conventions out of the box. See [Spring Boot integration](#spring-boot-integration) for setup details.
1010

@@ -31,6 +31,9 @@ By default, this compresses all text-based static resource files under Spring Bo
3131

3232
- `<filename>.br` -- Brotli compressed (quality 11)
3333
- `<filename>.gz` -- Gzip compressed
34+
- `<filename>.zst` -- Zstandard compressed (level 22)
35+
36+
> Note: Spring Framework's `EncodedResourceResolver` does not yet serve pre-compressed `.zst` files. Support is pending in [spring-projects/spring-framework#36647](https://github.com/spring-projects/spring-framework/pull/36647). If you serve assets via Spring's resource chain, set `<zstdEnabled>false</zstdEnabled>` until that change ships in a Spring Framework release.
3437
3538
### Example: Custom extensions and directories
3639

@@ -66,6 +69,22 @@ By default, this compresses all text-based static resource files under Spring Bo
6669
</configuration>
6770
```
6871

72+
### Example: Disable Zstandard
73+
74+
```xml
75+
<configuration>
76+
<zstdEnabled>false</zstdEnabled>
77+
</configuration>
78+
```
79+
80+
### Example: Lower Zstandard level for faster builds
81+
82+
```xml
83+
<configuration>
84+
<zstdLevel>3</zstdLevel>
85+
</configuration>
86+
```
87+
6988
### Example: Lower Brotli quality for faster builds
7089

7190
```xml
@@ -94,6 +113,8 @@ All configuration parameters can be set either in the plugin `<configuration>` b
94113
| `brotliQuality` | `compression.brotli.quality` | `11` | Brotli compression quality (0-11) |
95114
| `brotliEnabled` | `compression.brotli.enabled` | `true` | Enable Brotli compression |
96115
| `gzipEnabled` | `compression.gzip.enabled` | `true` | Enable Gzip compression |
116+
| `zstdEnabled` | `compression.zstd.enabled` | `true` | Enable Zstandard compression (see Spring Framework note above) |
117+
| `zstdLevel` | `compression.zstd.level` | `22` | Zstandard compression level (practical range 1-22) |
97118
| `skip` | `compression.skip` | `false` | Skip the plugin |
98119

99120
### Default file extensions
@@ -116,6 +137,19 @@ The `brotliQuality` parameter controls the trade-off between compression ratio a
116137

117138
The default value of 11 gives the best compression. For faster builds (e.g., during development), a lower value like 4-6 may be preferable.
118139

140+
### Zstandard level
141+
142+
The `zstdLevel` parameter controls the trade-off between compression ratio and build time:
143+
144+
| Level | Speed | Compression ratio |
145+
|---|---|---|
146+
| 1-3 | Fast | Lower |
147+
| 4-15 | Moderate | Good |
148+
| 16-19 | Slow | Better |
149+
| 20-22 | Very slow ("ultra" mode) | Best |
150+
151+
The default value of 22 gives the best compression. Level 3 matches libzstd's own default and produces noticeably faster builds at the cost of a modest reduction in compression ratio. Note that zstd's decompression speed is, by design, largely independent of the compression level used, so a higher level does not slow down clients that serve the resulting `.zst` files.
152+
119153
## Spring Boot integration
120154

121155
This plugin generates pre-compressed files that Spring Framework's `EncodedResourceResolver` can serve automatically. To enable this, add the following property to your `application.properties`:
@@ -127,6 +161,8 @@ spring.web.resources.chain.compressed=true
127161

128162
When this property is enabled, Spring Boot will look for `.br` or `.gz` variants of the requested resource and serve the compressed version if the client supports it (via the `Accept-Encoding` header).
129163

164+
`.zst` support in `EncodedResourceResolver` is pending upstream (see [spring-projects/spring-framework#36647](https://github.com/spring-projects/spring-framework/pull/36647)). Until that change is released, set `<zstdEnabled>false</zstdEnabled>` if you are serving assets via Spring's resource chain.
165+
130166
For more details, see the Spring documentation:
131167

132168
- [Spring MVC - Static Resources](https://docs.spring.io/spring-framework/reference/web/webmvc/mvc-config/static-resources.html)

pom.xml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
<version>0.3.0-SNAPSHOT</version>
1010
<packaging>maven-plugin</packaging>
1111
<name>compression-maven-plugin</name>
12-
<description>A Maven plugin that compresses static resources using Brotli and Gzip</description>
12+
<description>A Maven plugin that compresses static resources using Brotli, Gzip, and Zstandard</description>
1313

1414
<properties>
1515
<java.version>17</java.version>
@@ -19,6 +19,7 @@
1919
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
2020
<maven.version>3.9.14</maven.version>
2121
<brotli4j.version>1.22.0</brotli4j.version>
22+
<zstd-jni.version>1.5.7-7</zstd-jni.version>
2223
<spring-boot.version>4.0.5</spring-boot.version>
2324
</properties>
2425
<licenses>
@@ -72,6 +73,11 @@
7273
<artifactId>brotli4j</artifactId>
7374
<version>${brotli4j.version}</version>
7475
</dependency>
76+
<dependency>
77+
<groupId>com.github.luben</groupId>
78+
<artifactId>zstd-jni</artifactId>
79+
<version>${zstd-jni.version}</version>
80+
</dependency>
7581
<dependency>
7682
<groupId>org.junit.jupiter</groupId>
7783
<artifactId>junit-jupiter-api</artifactId>

src/it/basic-compress/verify.groovy

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,11 @@ assert buildLog.contains("Compressed icon.svg") : "Should log compression of ico
66
def staticDir = new File(basedir, "target/classes/static")
77
assert new File(staticDir, "app.js.br").exists() : "Brotli file should exist for app.js"
88
assert new File(staticDir, "app.js.gz").exists() : "Gzip file should exist for app.js"
9+
assert new File(staticDir, "app.js.zst").exists() : "Zstd file should exist for app.js"
910
assert new File(staticDir, "style.css.br").exists() : "Brotli file should exist for style.css"
1011
assert new File(staticDir, "style.css.gz").exists() : "Gzip file should exist for style.css"
12+
assert new File(staticDir, "style.css.zst").exists() : "Zstd file should exist for style.css"
1113
assert new File(staticDir, "icon.svg.br").exists() : "Brotli file should exist for icon.svg"
1214
assert new File(staticDir, "icon.svg.gz").exists() : "Gzip file should exist for icon.svg"
15+
assert new File(staticDir, "icon.svg.zst").exists() : "Zstd file should exist for icon.svg"
1316
assert buildLog.contains("BUILD SUCCESS")

src/it/custom-extensions/verify.groovy

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,11 @@ def staticDir = new File(basedir, "target/classes/static")
33

44
assert new File(staticDir, "index.html.br").exists() : "Brotli file should exist for index.html"
55
assert new File(staticDir, "index.html.gz").exists() : "Gzip file should exist for index.html"
6+
assert new File(staticDir, "index.html.zst").exists() : "Zstd file should exist for index.html"
67
assert new File(staticDir, "data.json.br").exists() : "Brotli file should exist for data.json"
78
assert new File(staticDir, "data.json.gz").exists() : "Gzip file should exist for data.json"
9+
assert new File(staticDir, "data.json.zst").exists() : "Zstd file should exist for data.json"
810
assert !new File(staticDir, "app.js.br").exists() : "JS should not be compressed with custom extensions"
911
assert !new File(staticDir, "app.js.gz").exists() : "JS should not be compressed with custom extensions"
12+
assert !new File(staticDir, "app.js.zst").exists() : "JS should not be compressed with custom extensions"
1013
assert buildLog.contains("BUILD SUCCESS")
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
invoker.goals=clean compile

src/it/zstd-enabled/pom.xml

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
5+
<modelVersion>4.0.0</modelVersion>
6+
7+
<groupId>com.example</groupId>
8+
<artifactId>zstd-enabled</artifactId>
9+
<version>1.0-SNAPSHOT</version>
10+
11+
<properties>
12+
<maven.compiler.release>17</maven.compiler.release>
13+
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
14+
</properties>
15+
16+
<build>
17+
<plugins>
18+
<plugin>
19+
<groupId>am.ik.maven</groupId>
20+
<artifactId>compression-maven-plugin</artifactId>
21+
<version>@project.version@</version>
22+
<executions>
23+
<execution>
24+
<goals>
25+
<goal>compress</goal>
26+
</goals>
27+
</execution>
28+
</executions>
29+
<configuration>
30+
<zstdLevel>3</zstdLevel>
31+
</configuration>
32+
</plugin>
33+
</plugins>
34+
</build>
35+
</project>
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
console.log("hello world");

src/it/zstd-enabled/verify.groovy

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
def buildLog = new File(basedir, "build.log").text
2+
assert buildLog.contains("Compressed app.js") : "Should log compression of app.js"
3+
4+
def staticDir = new File(basedir, "target/classes/static")
5+
assert new File(staticDir, "app.js.zst").exists() : "Zstd file should exist for app.js"
6+
assert new File(staticDir, "app.js.br").exists() : "Brotli file should still exist for app.js"
7+
assert new File(staticDir, "app.js.gz").exists() : "Gzip file should still exist for app.js"
8+
assert buildLog =~ /Compressed app\.js:.*zst:/ : "Build log should include zst size entry"
9+
assert buildLog.contains("BUILD SUCCESS")

src/main/java/am/ik/maven/compression/CompressMojo.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,19 @@ public class CompressMojo extends AbstractMojo {
8080
@Parameter(property = "compression.gzip.enabled", defaultValue = "true")
8181
private boolean gzipEnabled;
8282

83+
/**
84+
* Whether to enable Zstandard compression.
85+
*/
86+
@Parameter(property = "compression.zstd.enabled", defaultValue = "true")
87+
private boolean zstdEnabled;
88+
89+
/**
90+
* Zstandard compression level. The practical range is 1-22, where 22 is the maximum
91+
* ("ultra" mode). Higher values produce smaller files but take longer.
92+
*/
93+
@Parameter(property = "compression.zstd.level", defaultValue = "22")
94+
private int zstdLevel;
95+
8396
@Override
8497
public void execute() throws MojoExecutionException {
8598
if (this.skip) {
@@ -99,6 +112,9 @@ public void execute() throws MojoExecutionException {
99112
if (this.gzipEnabled) {
100113
compressorBuilder.addCompressor(new GzipCompressor());
101114
}
115+
if (this.zstdEnabled) {
116+
compressorBuilder.addCompressor(new ZstdCompressor(this.zstdLevel));
117+
}
102118
StaticResourceCompressor compressor = compressorBuilder.build();
103119

104120
Path basePath = this.outputDirectory.toPath();
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/*
2+
* Copyright 2026 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package am.ik.maven.compression;
17+
18+
import java.io.IOException;
19+
import java.nio.file.Files;
20+
import java.nio.file.Path;
21+
22+
import com.github.luben.zstd.Zstd;
23+
24+
/**
25+
* {@link ResourceCompressor} implementation using Zstandard compression.
26+
*/
27+
public final class ZstdCompressor implements ResourceCompressor {
28+
29+
private final int level;
30+
31+
/**
32+
* Creates a new Zstandard compressor with the specified compression level.
33+
* @param level the Zstandard compression level. The practical range is 1-22, where 22
34+
* is the maximum and levels 20-22 enable "ultra" mode (slower and more memory
35+
* intensive but produces the smallest output).
36+
*/
37+
public ZstdCompressor(int level) {
38+
this.level = level;
39+
}
40+
41+
@Override
42+
public String suffix() {
43+
return ".zst";
44+
}
45+
46+
@Override
47+
public String label() {
48+
return "zst";
49+
}
50+
51+
@Override
52+
public long compress(Path source, Path target) throws IOException {
53+
byte[] input = Files.readAllBytes(source);
54+
byte[] compressed = Zstd.compress(input, this.level);
55+
Files.write(target, compressed);
56+
return compressed.length;
57+
}
58+
59+
}

0 commit comments

Comments
 (0)