diff --git a/.jules/bolt.md b/.jules/bolt.md
new file mode 100644
index 00000000..b8fc7701
--- /dev/null
+++ b/.jules/bolt.md
@@ -0,0 +1,3 @@
+## 2025-02-12 - Reusable HexFormat optimization
+**Learning:** Found multiple instances of byte-to-hex conversion using `String.format("%02x", b)` in a loop or calling `HexFormat.of()` repeatedly. This creates unnecessary intermediate `String` and `Object[]` allocations, decreasing performance.
+**Action:** Use a static final reusable `java.util.HexFormat.of()` instance for byte array to hex string conversions in the codebase to eliminate redundant memory allocations and reduce GC overhead.
diff --git a/pom.xml b/pom.xml
index 6b7e40df..2ada4be2 100644
--- a/pom.xml
+++ b/pom.xml
@@ -7,7 +7,7 @@
org.springframework.boot
spring-boot-starter-parent
- 3.5.0
+ 3.5.14
diff --git a/src/main/java/com/clearfolio/viewer/artifact/ArtifactLinkService.java b/src/main/java/com/clearfolio/viewer/artifact/ArtifactLinkService.java
index 454b0448..1977d7b0 100644
--- a/src/main/java/com/clearfolio/viewer/artifact/ArtifactLinkService.java
+++ b/src/main/java/com/clearfolio/viewer/artifact/ArtifactLinkService.java
@@ -12,6 +12,7 @@
import java.util.List;
import java.util.Optional;
import java.util.UUID;
+import java.util.HexFormat;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
@@ -57,6 +58,7 @@ public class ArtifactLinkService {
private static final int TOKEN_FIELD_COUNT = 10;
private static final Base64.Encoder URL_ENCODER = Base64.getUrlEncoder().withoutPadding();
private static final Base64.Decoder URL_DECODER = Base64.getUrlDecoder();
+ private static final HexFormat HEX_FORMAT = HexFormat.of();
private final ArtifactStore artifactStore;
private final ArtifactLinkLedger artifactLinkLedger;
@@ -423,11 +425,7 @@ private static String sha256Hex(byte[] bytes) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] raw = digest.digest(bytes);
- StringBuilder hex = new StringBuilder(raw.length * 2);
- for (byte b : raw) {
- hex.append(String.format("%02x", b));
- }
- return hex.toString();
+ return HEX_FORMAT.formatHex(raw);
} catch (GeneralSecurityException ex) {
throw new IllegalStateException("SHA-256 digest unavailable", ex);
}
diff --git a/src/main/java/com/clearfolio/viewer/service/DefaultDocumentConversionService.java b/src/main/java/com/clearfolio/viewer/service/DefaultDocumentConversionService.java
index b8787f35..7a729522 100644
--- a/src/main/java/com/clearfolio/viewer/service/DefaultDocumentConversionService.java
+++ b/src/main/java/com/clearfolio/viewer/service/DefaultDocumentConversionService.java
@@ -13,6 +13,8 @@
import org.springframework.web.multipart.MultipartFile;
import com.clearfolio.viewer.auth.TenantContext;
+import java.util.HexFormat;
+
import com.clearfolio.viewer.model.ConversionJob;
import com.clearfolio.viewer.repository.ConversionJobRepository;
import com.clearfolio.viewer.repository.ConversionJobStateStore;
@@ -26,6 +28,8 @@
@Service
public class DefaultDocumentConversionService implements DocumentConversionService {
+ private static final HexFormat HEX_FORMAT = HexFormat.of();
+
private final ConversionJobRepository repository;
private final ConversionJobStateStore stateStore;
private final DocumentValidationService validationService;
@@ -160,12 +164,7 @@ private String contentHash(MultipartFile file) {
}
byte[] raw = digest.digest();
- StringBuilder hex = new StringBuilder(raw.length * 2);
- for (byte b : raw) {
- hex.append(String.format("%02x", b));
- }
-
- return hex.toString();
+ return HEX_FORMAT.formatHex(raw);
} catch (NoSuchAlgorithmException ex) {
throw new IllegalStateException("SHA-256 digest unavailable", ex);
} catch (IOException ex) {
diff --git a/src/main/java/com/clearfolio/viewer/service/DefaultDocumentValidationService.java b/src/main/java/com/clearfolio/viewer/service/DefaultDocumentValidationService.java
index b898f7ac..b4975ed4 100644
--- a/src/main/java/com/clearfolio/viewer/service/DefaultDocumentValidationService.java
+++ b/src/main/java/com/clearfolio/viewer/service/DefaultDocumentValidationService.java
@@ -23,6 +23,7 @@ public class DefaultDocumentValidationService implements DocumentValidationServi
private static final Logger LOGGER = LoggerFactory.getLogger(DefaultDocumentValidationService.class);
private static final int FINGERPRINT_TRUNCATE_BYTES = 8;
+ private static final HexFormat HEX_FORMAT = HexFormat.of();
private final Set blockedExtensions;
private final long maxUploadSizeBytes;
@@ -140,7 +141,7 @@ private String tokenFingerprint(String approvalToken) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hashed = digest.digest(approvalToken.getBytes(StandardCharsets.UTF_8));
- return HexFormat.of().formatHex(hashed, 0, FINGERPRINT_TRUNCATE_BYTES);
+ return HEX_FORMAT.formatHex(hashed, 0, FINGERPRINT_TRUNCATE_BYTES);
} catch (NoSuchAlgorithmException ex) {
throw new IllegalStateException("SHA-256 digest unavailable", ex);
}