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
5 changes: 2 additions & 3 deletions docs/modules/dictionary.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,15 @@
```yaml
opensabre:
governance:
registration-token: ENC(...)
dictionary:
enabled: true
registration-enabled: true
registration-token: ${DICTIONARY_REGISTRATION_TOKEN:${GOVERNANCE_REGISTRATION_TOKEN:${ERROR_CATALOG_REGISTRATION_TOKEN:}}}
preload-codes: [order_status]
```

`registration-enabled` 默认开启;没有声明 `DictionaryProvider` 的应用不会发起注册请求。
字典与错误码上报默认共享 `GOVERNANCE_REGISTRATION_TOKEN`,也可通过
`DICTIONARY_REGISTRATION_TOKEN` 单独覆盖。`ERROR_CATALOG_REGISTRATION_TOKEN` 仅作为旧部署兼容回退。
字典与错误码上报统一读取 Nacos 公共配置 `opensabre.governance.registration-token`,该值支持 Jasypt `ENC(...)` 密文。
注册任务使用[治理注册运行时](governance-registration.md)执行有限重试并暴露运行状态。

## 声明与读取
Expand Down
3 changes: 1 addition & 2 deletions docs/modules/error-catalog.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@ ErrorCatalogProvider orderErrorCatalogProvider() {
}
```

所有参与上报的应用与 `base-sysadmin` 默认共享 `GOVERNANCE_REGISTRATION_TOKEN`。
如需为错误码能力单独设置凭据,可用 `ERROR_CATALOG_REGISTRATION_TOKEN` 覆盖公共凭据。
所有参与上报的应用与 `base-sysadmin` 统一读取 Nacos 公共配置 `opensabre.governance.registration-token`,该值支持 Jasypt `ENC(...)` 密文。
目录端拒绝空凭据、无效凭据和被其他应用占用的错误码。

Framework 在发送快照前会补齐定义归属:业务 Provider 的错误码归当前应用所有,
Expand Down
11 changes: 0 additions & 11 deletions opensabre-starter-boot/src/main/resources/bootstrap.yaml

This file was deleted.

3 changes: 1 addition & 2 deletions opensabre-starter-boot/src/main/resources/logback-spring.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
<contextName>opensabre</contextName>
<!-- name的值是变量的名称,value的值时变量定义的值。通过定义的值会被插入到logger上下文中。定义后,可以使“${}”来使用变量。 -->
<springProperty scope="opensabre" name="log.path" source="logging.file.path" defaultValue="logs"/>
/>
<!--0. 日志格式和颜色渲染 -->
<!-- 彩色日志依赖的渲染类 -->
<conversionRule conversionWord="clr" class="org.springframework.boot.logging.logback.ColorConverter"/>
Expand Down Expand Up @@ -75,4 +74,4 @@
<appender-ref ref="CONSOLE"/>
<appender-ref ref="ROLLING_FILE"/>
</root>
</configuration>
</configuration>
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ public class GovernanceProperties {

private boolean enabled = true;

/** Shared credential for governance registration and reporting capabilities. */
private String registrationToken = "";

private Sysadmin sysadmin = new Sysadmin();

private Audit audit = new Audit();
Expand Down Expand Up @@ -59,14 +62,17 @@ public static class Usage {
@Data
public static class ErrorCatalog {
private boolean enabled = true;
/** Shared internal credential for error catalog registration. */
/** @deprecated Use {@code opensabre.governance.registration-token}. */
@Deprecated(forRemoval = true)
private String registrationToken = "";
}

@Data
public static class Dictionary {
private boolean enabled = true;
private boolean registrationEnabled = false;
/** @deprecated Use {@code opensabre.governance.registration-token}. */
@Deprecated(forRemoval = true)
private String registrationToken = "";
private java.util.List<String> preloadCodes = java.util.List.of();
/** 自动扫描标准字典枚举的包;为空时使用 Spring Boot 应用自动配置包。 */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ private void registerOnce() {
return;
}
String application = environment.getProperty("spring.application.name", "unknown-application");
String token = environment.getProperty("opensabre.governance.dictionary.registration-token", "");
String token = environment.getProperty("opensabre.governance.registration-token", "");
clientProvider.getObject().registerDictionaries(
DictionarySnapshot.from(application, List.copyOf(definitions.values())), token);
log.info("Registered {} dictionaries for {}", definitions.size(), application);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ private void registerOnce() {
}
if (entriesByCode.isEmpty()) return;
String application = environment.getProperty("spring.application.name", "unknown-application");
String token = environment.getProperty("opensabre.governance.error-catalog.registration-token", "");
String token = environment.getProperty("opensabre.governance.registration-token", "");
List<ErrorCatalogEntry> resolvedEntries = entriesByCode.values().stream()
.map(entry -> entry.resolveOwnership(application))
.toList();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,8 @@ opensabre:
fail-open: true
error-catalog:
enabled: true
registration-token: ${ERROR_CATALOG_REGISTRATION_TOKEN:${GOVERNANCE_REGISTRATION_TOKEN:}}
dictionary:
enabled: true
registration-enabled: true
registration-token: ${DICTIONARY_REGISTRATION_TOKEN:${GOVERNANCE_REGISTRATION_TOKEN:${ERROR_CATALOG_REGISTRATION_TOKEN:}}}
preload-codes: []
scan-packages: []
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import org.springframework.boot.env.YamlPropertySourceLoader;
import org.springframework.core.env.MapPropertySource;
import org.springframework.core.env.StandardEnvironment;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.core.io.ClassPathResource;

import java.io.IOException;
Expand All @@ -18,15 +19,18 @@ class GovernanceDefaultPropertiesTest {
void sharesGovernanceTokenAndEnablesDictionaryRegistration() throws IOException {
StandardEnvironment environment = new StandardEnvironment();
environment.getPropertySources().addFirst(new MapPropertySource(
"governance-token", Map.of("GOVERNANCE_REGISTRATION_TOKEN", "shared-token")));
"governance-token", Map.of(
"opensabre.governance.registration-token", "shared-token")));
new YamlPropertySourceLoader().load(
"opensabre-governance", new ClassPathResource("opensabre-governance.yml"))
.forEach(environment.getPropertySources()::addLast);

assertEquals("shared-token", environment.getProperty(
"opensabre.governance.error-catalog.registration-token"));
assertEquals("shared-token", environment.getProperty(
"opensabre.governance.dictionary.registration-token"));
"opensabre.governance.registration-token"));
assertEquals("shared-token", Binder.get(environment)
.bind("opensabre.governance", GovernanceProperties.class)
.orElseThrow(() -> new IllegalStateException("governance properties were not bound"))
.getRegistrationToken());
assertEquals(Boolean.TRUE, environment.getProperty(
"opensabre.governance.dictionary.registration-enabled", Boolean.class));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ public SysadminGovernanceClient getObject() {
true, false, null));
MockEnvironment environment = new MockEnvironment()
.withProperty("spring.application.name", "base-demo")
.withProperty("opensabre.governance.error-catalog.registration-token",
.withProperty("opensabre.governance.registration-token",
"registration-secret");
scheduler = new ThreadPoolTaskScheduler();
scheduler.setPoolSize(1);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,26 @@
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.MethodParameter;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.ResourceRegion;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.converter.ByteArrayHttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.ResourceHttpMessageConverter;
import org.springframework.http.converter.ResourceRegionHttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitter;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;

import java.io.InputStream;
import java.nio.ByteBuffer;
import java.util.Objects;

/**
Expand All @@ -28,12 +39,12 @@ public class RestResponseBodyAdvice implements ResponseBodyAdvice<Object> {
* 框架中不需要包装为Result对象的包名
*/
@Value("${opensabre.rest.result.framework.excludes:org.springdoc}")
private String excludeFrameworkPackageStr;
private String excludeFrameworkPackageStr = "org.springdoc";
/**
* 应用中不需要包装为Result对象的包名
*/
@Value("${opensabre.rest.result.excludes:}")
private String excludePackageStr;
private String excludePackageStr = "";

@Override
public boolean supports(@NotNull MethodParameter returnType,
Expand All @@ -44,7 +55,8 @@ public boolean supports(@NotNull MethodParameter returnType,
returnType.hasMethodAnnotation(ResponseBody.class);
boolean hasRestController = AnnotatedElementUtils.hasAnnotation(returnTypeContainingClass, RestController.class) ||
returnType.hasMethodAnnotation(RestController.class);
return hasResponseBody && hasRestController && isNeedWrap(returnTypeContainingClass.getPackageName());
return hasResponseBody && hasRestController && isNeedWrap(returnTypeContainingClass.getPackageName())
&& !isPassthroughType(returnType.getParameterType(), converterType);
}

/**
Expand All @@ -59,6 +71,35 @@ private boolean isNeedWrap(String packageName) {
return !StringUtils.startsWithAny(packageName, StringUtils.split(allExcludePackageStr, delimiter));
}

/**
* Binary, file and streaming responses must be written by their original message handler.
*/
private boolean isPassthroughType(Class<?> returnType,
Class<? extends HttpMessageConverter<?>> converterType) {
return HttpEntity.class.isAssignableFrom(returnType)
|| HttpHeaders.class.isAssignableFrom(returnType)
|| Resource.class.isAssignableFrom(returnType)
|| ResourceRegion.class.isAssignableFrom(returnType)
|| InputStream.class.isAssignableFrom(returnType)
|| ByteBuffer.class.isAssignableFrom(returnType)
|| returnType == byte[].class
|| StreamingResponseBody.class.isAssignableFrom(returnType)
|| ResponseBodyEmitter.class.isAssignableFrom(returnType)
|| ByteArrayHttpMessageConverter.class.isAssignableFrom(converterType)
|| ResourceHttpMessageConverter.class.isAssignableFrom(converterType)
|| ResourceRegionHttpMessageConverter.class.isAssignableFrom(converterType);
}

private boolean isPassthroughBody(Object body) {
return body instanceof Resource
|| body instanceof ResourceRegion
|| body instanceof InputStream
|| body instanceof ByteBuffer
|| body instanceof byte[]
|| body instanceof StreamingResponseBody
|| body instanceof ResponseBodyEmitter;
}

@SneakyThrows
@Override
public Object beforeBodyWrite(Object body,
Expand All @@ -71,6 +112,9 @@ public Object beforeBodyWrite(Object body,
if (body instanceof Result) {
return body;
}
if (isPassthroughBody(body)) {
return body;
}
// 如果是String,将Result转为json string
if (Objects.isNull(body) || body instanceof String) {
return new ObjectMapper().writeValueAsString(Result.success(body));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package io.github.opensabre.webmvc.rest;

import static org.assertj.core.api.Assertions.assertThat;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.util.Map;

import org.junit.jupiter.api.Test;
import org.springframework.core.MethodParameter;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.http.ResponseEntity;
import org.springframework.http.MediaType;
import org.springframework.http.converter.ByteArrayHttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;

class RestResponseBodyAdviceTest {

private final RestResponseBodyAdvice advice = new RestResponseBodyAdvice();

@Test
void keepsJsonResponsesWrapped() throws Exception {
MethodParameter returnType = parameter("json");

assertThat(advice.supports(returnType, StringHttpMessageConverter.class)).isTrue();
assertThat(advice.beforeBodyWrite(Map.of("value", "body"), returnType, MediaType.APPLICATION_JSON,
StringHttpMessageConverter.class, request(), response()))
.isInstanceOf(io.github.opensabre.common.core.entity.vo.Result.class);
}

@Test
void doesNotWrapFileAndStreamResponses() throws Exception {
MethodParameter resource = parameter("resource");
MethodParameter entity = parameter("entity");
MethodParameter bytes = parameter("bytes");
MethodParameter stream = parameter("stream");

assertThat(advice.supports(resource, StringHttpMessageConverter.class)).isFalse();
assertThat(advice.supports(entity, StringHttpMessageConverter.class)).isFalse();
assertThat(advice.supports(bytes, StringHttpMessageConverter.class)).isFalse();
assertThat(advice.supports(stream, StringHttpMessageConverter.class)).isFalse();
assertThat(advice.supports(parameter("json"), ByteArrayHttpMessageConverter.class)).isFalse();

byte[] payload = {1, 2, 3};
InputStream input = new ByteArrayInputStream(payload);
assertThat(advice.beforeBodyWrite(payload, bytes, MediaType.APPLICATION_OCTET_STREAM,
ByteArrayHttpMessageConverter.class, request(), response())).isSameAs(payload);
assertThat(advice.beforeBodyWrite(input, parameter("input"), MediaType.APPLICATION_OCTET_STREAM,
ByteArrayHttpMessageConverter.class, request(), response())).isSameAs(input);
assertThat(advice.beforeBodyWrite(new ByteArrayResource(payload), resource, MediaType.APPLICATION_OCTET_STREAM,
StringHttpMessageConverter.class, request(), response())).isInstanceOf(ByteArrayResource.class);
}

private MethodParameter parameter(String methodName) throws Exception {
Method method = TestController.class.getDeclaredMethod(methodName);
return new MethodParameter(method, -1);
}

private ServletServerHttpRequest request() {
return new ServletServerHttpRequest(new MockHttpServletRequest());
}

private ServletServerHttpResponse response() {
return new ServletServerHttpResponse(new MockHttpServletResponse());
}

@RestController
private static class TestController {
@GetMapping("/json")
Object json() { return Map.of("value", "json"); }

@GetMapping("/resource")
ByteArrayResource resource() { return new ByteArrayResource(new byte[] {1}); }

@GetMapping("/entity")
ResponseEntity<ByteArrayResource> entity() { return ResponseEntity.ok(new ByteArrayResource(new byte[] {1})); }

@GetMapping("/bytes")
byte[] bytes() { return new byte[] {1}; }

@GetMapping("/stream")
StreamingResponseBody stream() { return output -> { }; }

@GetMapping("/input")
InputStream input() { return InputStream.nullInputStream(); }
}
}
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@
<central-publishing-maven-plugin.version>0.9.0</central-publishing-maven-plugin.version>
<lombok.version>1.18.42</lombok.version>

<revision>1.1.1</revision>
<revision>1.1.2</revision>
</properties>

<!-- 发布配置 -->
Expand Down
Loading