Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
## 📅 CHANGES

## Version 2.7.5 (2026-11-24)

- ServiceError.ref is now sanitized to null if it is blank or empty, preventing the inclusion of empty or whitespace-only strings in error responses. This change enhances the clarity and consistency
of error reporting by ensuring that only meaningful references are included in the ServiceError object.
- REF275-1: Enforce JJWT to use Jackson 3
- RpcDelegate and RpcResult: refactoring

## Version 2.7.4 (2026-10-24)

- 🔒 Security patch: Jackson Databind
Expand Down
2 changes: 1 addition & 1 deletion GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ October 2011, then to GitLab in Dec 2016, and eventually to GitHub in Sep 2021.
<dependency>
<groupId>org.summerboot</groupId>
<artifactId>jexpress</artifactId>
<version>2.7.4</version>
<version>2.7.5</version>
</dependency>
```

Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
- **[Apache Central Repository][2]**
- **[mvnrepository.com][3]**

[1]: https://central.sonatype.com/artifact/org.summerboot/jexpress/2.7.4
[2]: https://repo.maven.apache.org/maven2/org/summerboot/jexpress/2.7.4
[3]: https://mvnrepository.com/artifact/org.summerboot/jexpress/2.7.4
[1]: https://central.sonatype.com/artifact/org.summerboot/jexpress/2.7.5
[2]: https://repo.maven.apache.org/maven2/org/summerboot/jexpress/2.7.5
[3]: https://mvnrepository.com/artifact/org.summerboot/jexpress/2.7.5

[View Changelog (CHANGES)](CHANGES.md)

Expand Down
6 changes: 3 additions & 3 deletions README_中文.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
- **[Maven Central Repository][2]**
- **[mvnrepository.com][3]**

[1]: https://repo.maven.apache.org/maven2/org/summerboot/jexpress/2.7.4
[2]: https://central.sonatype.com/artifact/org.summerboot/jexpress/2.7.4
[3]: https://mvnrepository.com/artifact/org.summerboot/jexpress/2.7.4
[1]: https://repo.maven.apache.org/maven2/org/summerboot/jexpress/2.7.5
[2]: https://central.sonatype.com/artifact/org.summerboot/jexpress/2.7.5
[3]: https://mvnrepository.com/artifact/org.summerboot/jexpress/2.7.5

[View Changelog (CHANGES)](CHANGES.md)

Expand Down
25 changes: 22 additions & 3 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>org.summerboot</groupId>
<artifactId>jexpress</artifactId>
<version>2.7.4</version>
<version>2.7.5-SNAPSHOT</version>
<packaging>jar</packaging>
<name>jExpress ${project.version}</name>
<description>jExpress focuses on addressing non-functional and operational maintainability needs, some of which Spring Boot may not yet provide.</description>
Expand Down Expand Up @@ -217,7 +217,7 @@
<guice.version>7.0.0</guice.version>

<!-- JPA -->
<hibernate.version>7.4.13.Final</hibernate.version>
<hibernate.version>7.4.14.Final</hibernate.version>
<hikari-cp.version>7.1.0</hikari-cp.version>

<!-- JNA -->
Expand All @@ -237,7 +237,7 @@
<!-- PDF - PDFBox -->
<pdfbox.version>3.0.8</pdfbox.version>
<!-- PDF - io.github.openhtmltopdf -->
<openhtml.version>1.1.100</openhtml.version>
<openhtml.version>1.1.105</openhtml.version>
<batik-transcoder.version>1.20</batik-transcoder.version>
<!-- PDF - iText -->
<itext-core.version>9.8.0</itext-core.version>
Expand Down Expand Up @@ -356,17 +356,35 @@
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>${jwt.version}</version>
<exclusions>
<exclusion>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-impl -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>${jwt.version}</version>
<exclusions>
<exclusion>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId> <!-- or jjwt-gson if Gson is preferred -->
<version>${jwt.version}</version>
<exclusions>
<exclusion>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- backward compatibility for jjwt only https://mvnrepository.com/artifact/javax.xml.bind/jaxb-api -->
<!--
Expand All @@ -383,6 +401,7 @@
<scope>compile</scope>
</dependency>


<!-- NIO Netty -->
<!-- https://mvnrepository.com/artifact/io.netty/netty-all Apache 2.0-->
<dependency>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import org.apache.commons.lang3.StringUtils;
import org.summerboot.jexpress.boot.BootConstants;
import org.summerboot.jexpress.util.lang.BeanUtil;
import org.summerboot.jexpress.webserver.jackson.AdditionalJsonFields;
Expand Down Expand Up @@ -48,7 +49,7 @@ public ServiceError() {
}

public ServiceError(String ref) {
this.ref = ref;
this.ref = StringUtils.isBlank(ref) ? null : ref;
}

// public ServiceError(int errorCode, String errorTag, String errorDesc, Throwable ex) {
Expand Down
95 changes: 80 additions & 15 deletions src/main/java/org/summerboot/jexpress/api/rpc/RpcResult.java
Original file line number Diff line number Diff line change
Expand Up @@ -54,25 +54,24 @@ public class RpcResult<T> {
protected T successResponse;
protected final ContentType contentType;
protected final boolean isStreaming;
protected final SessionContext context;

enum ContentType {JSON, XML, OTHER}

protected ObjectMapper httpClientConfiguredObjectMapper = null;

@Deprecated(since = "2.7.4", forRemoval = false)
public RpcResult(HttpRequest originRequest, String originRequestBody, HttpResponse<String> httpResponse, boolean remoteSuccess) {
this(originRequest, originRequestBody, httpResponse, remoteSuccess, null);
this(originRequest, originRequestBody, httpResponse, remoteSuccess, false, null, null);
}

public RpcResult(HttpRequest originRequest, String originRequestBody, HttpResponse<String> httpResponse, boolean remoteSuccess, HttpClientConfig httpClientConfig) {
this(originRequest, originRequestBody, httpResponse, remoteSuccess, false, httpClientConfig);
}

public RpcResult(HttpRequest originRequest, String originRequestBody, HttpResponse<?> httpResponse, boolean remoteSuccess, boolean isStreaming, HttpClientConfig httpClientConfig) {
public RpcResult(HttpRequest originRequest, String originRequestBody, HttpResponse<?> httpResponse, boolean remoteSuccess, boolean isStreaming, HttpClientConfig httpClientConfig, SessionContext context) {
this.originRequest = originRequest;
this.originRequestBody = originRequestBody;
this.httpResponse = httpResponse;
this.rpcResponseBody = httpResponse == null || isStreaming ? null : String.valueOf(httpResponse.body());
this.httpStatusCode = httpResponse == null ? 0 : httpResponse.statusCode();
this.context = context;
this.httpStatus = HttpResponseStatus.valueOf(httpStatusCode);
this.remoteSuccess = remoteSuccess;
this.isStreaming = isStreaming;
Expand Down Expand Up @@ -146,23 +145,87 @@ public ContentType contentType() {
return contentType;
}

/**
*
* @param successResponseClass
* @return
* @deprecated use { @link #deserialize(Class)}
* <p>Sample code:
* <pre>{@code
* if(rpcResult.isRemoteSuccess()){
* MySuccessObj obj = rpcResult.deserialize(MySuccessObj.class);
* } else {
* MyErrorObj err = rpcResult.deserialize(MyErrorObj.class);
* }
* }</pre>
*/
@Deprecated(since = "2.7.4", forRemoval = false)
public RpcResult<T> update(Class<T> successResponseClass) {
return update(successResponseClass, context);
}

/**
*
* @param successResponseClass
* @param context
* @return
* @deprecated use { @link #deserialize(Class)}
* <p>Sample code:
* <pre>{@code
* if(rpcResult.isRemoteSuccess()){
* MySuccessObj obj = rpcResult.deserialize(MySuccessObj.class);
* } else {
* MyErrorObj err = rpcResult.deserialize(MyErrorObj.class);
* }
* }</pre>
*/
@Deprecated(since = "2.7.4", forRemoval = false)
public RpcResult<T> update(Class<T> successResponseClass, final SessionContext context) {
if (remoteSuccess) {
successResponse = deserialize(httpClientConfiguredObjectMapper, null, successResponseClass, true, context);
}
return this;
}

/**
*
* @param responseClass
* @param context
* @param <R>
* @return
* @deprecated use { @link #deserialize(Class)}
* <p>Sample code:
* <pre>{@code
* if(rpcResult.isRemoteSuccess()){
* MySuccessObj obj = rpcResult.deserialize(MySuccessObj.class);
* } else {
* MyErrorObj err = rpcResult.deserialize(MyErrorObj.class);
* }
* }</pre>
*/
@Deprecated(since = "2.7.4", forRemoval = false)
public <R> R deserialize(Class<R> responseClass, final SessionContext context) {
return deserialize(httpClientConfiguredObjectMapper, responseClass, context);
return deserialize(httpClientConfiguredObjectMapper, null, responseClass, true, context);
}

public <R> R deserialize(Class<R> responseClass) {
return deserialize(httpClientConfiguredObjectMapper, responseClass);
}

public <R> R deserialize(JavaType responseType, final SessionContext context) {
return deserialize(httpClientConfiguredObjectMapper, responseType, context);
public <R> R deserialize(JavaType responseType) {
return deserialize(httpClientConfiguredObjectMapper, responseType);
}

public <R> R deserialize(ObjectMapper jacksonMapper, Class<R> responseClass, final SessionContext context) {
public <R> R deserialize(ObjectMapper jacksonMapper, Class<R> responseClass) {
return deserialize(jacksonMapper, null, responseClass, true, context);
}

public <R> R deserialize(ObjectMapper jacksonMapper, JavaType responseType, final SessionContext context) {
public <R> R deserialize(ObjectMapper jacksonMapper, JavaType responseType) {
return deserialize(jacksonMapper, responseType, null, true, context);
}

// Debug 现场还原的最大字符数(通常 4KB~8KB 足够看清出错的 JSON 结构了)

// 💡 for debugging: Maximum number of characters to retrieve for reconstruction (typically 4KB–8KB is sufficient to clearly identify a malformed JSON structure)
private static final int SNAPSHOT_SIZE = 8 * 1024;

protected <R> R deserialize(ObjectMapper jacksonMapper, JavaType responseType, Class<R> responseClass, boolean doValidation, final SessionContext context) {
Expand All @@ -174,10 +237,10 @@ protected <R> R deserialize(ObjectMapper jacksonMapper, JavaType responseType, C
try {
if (isStreaming) {
try (InputStream rawInputStream = (InputStream) httpResponse.body()) {
// 💡 1. 创建一个极小的内存口袋,只用来抄录前 8KB 的数据快照
// 💡 1. Create a tiny memory pocket, used exclusively for recording an 8KB snapshot of the data.
ByteArrayOutputStream debugSnapshot = new ByteArrayOutputStream(SNAPSHOT_SIZE);

// 2. 包装原始网络流:在 Jackson 边读边解的过程中,顺便往快照里写一份
// 💡 2. Wrapping the raw network stream: while Jackson reads and parses the data, simultaneously write a copy to a snapshot.
InputStream wrappedStream = new InputStream() {
private int bytesCopied = 0;

Expand Down Expand Up @@ -211,7 +274,9 @@ public int read(byte[] b, int off, int len) throws java.io.IOException {
: jacksonMapper.readValue(wrappedStream, responseClass);
} catch (JacksonException ex) {
String jsonSnapshotStr = debugSnapshot.toString(StandardCharsets.UTF_8);
context.memo(RpcMemo.MEMO_RPC_RESPONSE_DATA, jsonSnapshotStr);
if (context != null) {
context.memo(RpcMemo.MEMO_RPC_RESPONSE_DATA, jsonSnapshotStr);
}
throw ex;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ static boolean isDebugMode() {
String APP_ID = String.format("%06d", APP_ID_VALUE);

//version
String VERSION = "jExpress 2.7.4";
String VERSION = "jExpress 2.7.5";
String JEXPRESS_PACKAGE_NAME = "org.summerboot.jexpress";

String JSONFILTER_NAME_SERVICEERROR = "ServiceErrorFilter";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ protected <T> RpcResult<T> rpcEx(SessionContext context, HttpRequest httpRequest
Thread.currentThread().interrupt();
Err e = new Err(BootErrorCode.APP_INTERRUPTED, null, "Http Client Interrupted", ex);
context.status(HttpResponseStatus.INTERNAL_SERVER_ERROR).error(e);
RpcResult<T> rpcResult = new RpcResult<>(httpRequest, originRequestBody, null, false, getHttpClientConfig());
RpcResult<T> rpcResult = new RpcResult<>(httpRequest, originRequestBody, null, false, false, getHttpClientConfig(), context);
return rpcResult;
} finally {
context.poi(BootPoi.RPC_END);
Expand All @@ -128,7 +128,7 @@ protected <T> RpcResult<T> rpcEx(SessionContext context, HttpRequest httpRequest
}

//3b. update status
RpcResult<T> rpcResult = new RpcResult<>(httpRequest, originRequestBody, isStreaming ? httpResponse2 : httpResponse1, isRemoteSuccess, isStreaming, getHttpClientConfig());
RpcResult<T> rpcResult = new RpcResult<>(httpRequest, originRequestBody, isStreaming ? httpResponse2 : httpResponse1, isRemoteSuccess, isStreaming, getHttpClientConfig(), context);
String rpcResponseJsonBody = rpcResult.httpResponseBody();
context.memo(RpcMemo.MEMO_RPC_RESPONSE, rpcResult.httpStatusCode() + " " + (isStreaming ? httpResponse2.headers() : httpResponse1.headers()));
context.memo(RpcMemo.MEMO_RPC_RESPONSE_DATA, isStreaming ? "stream data" : rpcResponseJsonBody);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* Copyright 2005-2026 Du Law Office - jExpress, The Summer Boot Framework Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://apache.org
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/

package org.summerboot.jexpress.security.token.jwt;


import io.jsonwebtoken.io.DeserializationException;
import io.jsonwebtoken.io.Deserializer;
import tools.jackson.databind.ObjectMapper;

import java.io.Reader;
import java.util.Map;

/**
* REF275-1: Enforce JJWT to use Jackson 3
*
* @author Changski Tie Zheng Zhang 张铁铮, 魏泽北, 杜旺财, 杜富贵
*/
public class Jackson3Deserializer implements Deserializer<Map<String, ?>> {

// Use Jackson 3 ObjectMapper for deserialization
private final ObjectMapper objectMapper;

public Jackson3Deserializer(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}


@Override
public Map<String, ?> deserialize(byte[] bytes) throws DeserializationException {
return objectMapper.readValue(bytes, Map.class);
}

@Override
public Map<String, ?> deserialize(Reader reader) throws DeserializationException {
return objectMapper.readValue(reader, Map.class);
}
}
Loading
Loading