diff --git a/docs/modules/dictionary.md b/docs/modules/dictionary.md index a3e0b2b..6794cff 100644 --- a/docs/modules/dictionary.md +++ b/docs/modules/dictionary.md @@ -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)执行有限重试并暴露运行状态。 ## 声明与读取 diff --git a/docs/modules/error-catalog.md b/docs/modules/error-catalog.md index 4a34f39..90983c1 100644 --- a/docs/modules/error-catalog.md +++ b/docs/modules/error-catalog.md @@ -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 的错误码归当前应用所有, diff --git a/opensabre-starter-boot/src/main/resources/bootstrap.yaml b/opensabre-starter-boot/src/main/resources/bootstrap.yaml deleted file mode 100644 index 4394136..0000000 --- a/opensabre-starter-boot/src/main/resources/bootstrap.yaml +++ /dev/null @@ -1,11 +0,0 @@ -#==============日志===============# -logging: - level: - io.github.opensabre: info - logback: - rollingpolicy: - max-file-size: 1GB - file: - path: logs - pattern: - level: '%5p [${spring.application.name:},%X{traceId:-},%X{spanId:-},%X{parentSpanId:-}]' diff --git a/opensabre-starter-boot/src/main/resources/logback-spring.xml b/opensabre-starter-boot/src/main/resources/logback-spring.xml index 1cca8f4..a4e975d 100644 --- a/opensabre-starter-boot/src/main/resources/logback-spring.xml +++ b/opensabre-starter-boot/src/main/resources/logback-spring.xml @@ -7,7 +7,6 @@ opensabre - /> @@ -75,4 +74,4 @@ - \ No newline at end of file + diff --git a/opensabre-starter-governance/src/main/java/io/github/opensabre/governance/config/GovernanceProperties.java b/opensabre-starter-governance/src/main/java/io/github/opensabre/governance/config/GovernanceProperties.java index c60cfc9..2a2782c 100644 --- a/opensabre-starter-governance/src/main/java/io/github/opensabre/governance/config/GovernanceProperties.java +++ b/opensabre-starter-governance/src/main/java/io/github/opensabre/governance/config/GovernanceProperties.java @@ -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(); @@ -59,7 +62,8 @@ 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 = ""; } @@ -67,6 +71,8 @@ public static class ErrorCatalog { 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 preloadCodes = java.util.List.of(); /** 自动扫描标准字典枚举的包;为空时使用 Spring Boot 应用自动配置包。 */ diff --git a/opensabre-starter-governance/src/main/java/io/github/opensabre/governance/dictionary/DictionaryRegistrationListener.java b/opensabre-starter-governance/src/main/java/io/github/opensabre/governance/dictionary/DictionaryRegistrationListener.java index 567d2fe..4b14f8c 100644 --- a/opensabre-starter-governance/src/main/java/io/github/opensabre/governance/dictionary/DictionaryRegistrationListener.java +++ b/opensabre-starter-governance/src/main/java/io/github/opensabre/governance/dictionary/DictionaryRegistrationListener.java @@ -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); diff --git a/opensabre-starter-governance/src/main/java/io/github/opensabre/governance/errorcatalog/ErrorCatalogRegistrationListener.java b/opensabre-starter-governance/src/main/java/io/github/opensabre/governance/errorcatalog/ErrorCatalogRegistrationListener.java index 8ea2540..c8b9e54 100644 --- a/opensabre-starter-governance/src/main/java/io/github/opensabre/governance/errorcatalog/ErrorCatalogRegistrationListener.java +++ b/opensabre-starter-governance/src/main/java/io/github/opensabre/governance/errorcatalog/ErrorCatalogRegistrationListener.java @@ -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 resolvedEntries = entriesByCode.values().stream() .map(entry -> entry.resolveOwnership(application)) .toList(); diff --git a/opensabre-starter-governance/src/main/resources/opensabre-governance.yml b/opensabre-starter-governance/src/main/resources/opensabre-governance.yml index 2633310..3d54b27 100644 --- a/opensabre-starter-governance/src/main/resources/opensabre-governance.yml +++ b/opensabre-starter-governance/src/main/resources/opensabre-governance.yml @@ -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: [] diff --git a/opensabre-starter-governance/src/test/java/io/github/opensabre/governance/config/GovernanceDefaultPropertiesTest.java b/opensabre-starter-governance/src/test/java/io/github/opensabre/governance/config/GovernanceDefaultPropertiesTest.java index 856950f..19b49d2 100644 --- a/opensabre-starter-governance/src/test/java/io/github/opensabre/governance/config/GovernanceDefaultPropertiesTest.java +++ b/opensabre-starter-governance/src/test/java/io/github/opensabre/governance/config/GovernanceDefaultPropertiesTest.java @@ -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; @@ -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)); } diff --git a/opensabre-starter-governance/src/test/java/io/github/opensabre/governance/errorcatalog/ErrorCatalogRegistrationListenerTest.java b/opensabre-starter-governance/src/test/java/io/github/opensabre/governance/errorcatalog/ErrorCatalogRegistrationListenerTest.java index 1d54328..9009640 100644 --- a/opensabre-starter-governance/src/test/java/io/github/opensabre/governance/errorcatalog/ErrorCatalogRegistrationListenerTest.java +++ b/opensabre-starter-governance/src/test/java/io/github/opensabre/governance/errorcatalog/ErrorCatalogRegistrationListenerTest.java @@ -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); diff --git a/opensabre-starter-webmvc/src/main/java/io/github/opensabre/webmvc/rest/RestResponseBodyAdvice.java b/opensabre-starter-webmvc/src/main/java/io/github/opensabre/webmvc/rest/RestResponseBodyAdvice.java index 0c6ef1c..71aaeca 100644 --- a/opensabre-starter-webmvc/src/main/java/io/github/opensabre/webmvc/rest/RestResponseBodyAdvice.java +++ b/opensabre-starter-webmvc/src/main/java/io/github/opensabre/webmvc/rest/RestResponseBodyAdvice.java @@ -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; /** @@ -28,12 +39,12 @@ public class RestResponseBodyAdvice implements ResponseBodyAdvice { * 框架中不需要包装为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, @@ -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); } /** @@ -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> 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, @@ -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)); diff --git a/opensabre-starter-webmvc/src/test/java/io/github/opensabre/webmvc/rest/RestResponseBodyAdviceTest.java b/opensabre-starter-webmvc/src/test/java/io/github/opensabre/webmvc/rest/RestResponseBodyAdviceTest.java new file mode 100644 index 0000000..228d4d8 --- /dev/null +++ b/opensabre-starter-webmvc/src/test/java/io/github/opensabre/webmvc/rest/RestResponseBodyAdviceTest.java @@ -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 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(); } + } +} diff --git a/pom.xml b/pom.xml index 2d58e70..961cc73 100644 --- a/pom.xml +++ b/pom.xml @@ -68,7 +68,7 @@ 0.9.0 1.18.42 - 1.1.1 + 1.1.2