mirror of
https://github.com/hpd840321/starRiverProperty.git
synced 2026-06-09 16:30:29 +08:00
feat: add service config templates and extraction script
Former-commit-id: 1de24b7eb79676d1aba9d799a58c5a753290cf52
This commit is contained in:
@@ -13,12 +13,13 @@
|
||||
<artifactId>cw-elevator-application-starter</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
<name>cw-elevator-application-starter</name>
|
||||
<description>可执行 Spring Boot 入口:repackage 为单 JAR 发布包(对齐历史 V1 starter 形态)。</description>
|
||||
<description>可执行入口:显式依赖与 V1 嵌套 pom 一致;传递依赖由 cw-elevator-application-web 等子模块提供。</description>
|
||||
|
||||
<properties>
|
||||
<alibaba.eclipse.codestyle.path>${project.basedir}/../../docs/style/alibaba-eclipse-codestyle.xml</alibaba.eclipse.codestyle.path>
|
||||
</properties>
|
||||
|
||||
<!-- 与 cw-elevator-application-V1.0.0.20211103/.../cw-elevator-application-starter/pom.xml 显式依赖集合一致 -->
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
@@ -32,22 +33,14 @@
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||
</dependency>
|
||||
<!-- V1 嵌套 lib 中含 cache / freemarker starter -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-cache</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-aop</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-openfeign</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>cn.cloudwalk</groupId>
|
||||
<artifactId>cloudwalk-device-sdk-protocol-entity</artifactId>
|
||||
<version>2.2.0</version>
|
||||
<artifactId>spring-boot-starter-freemarker</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>cn.cloudwalk.elevator</groupId>
|
||||
@@ -63,10 +56,31 @@
|
||||
|
||||
<build>
|
||||
<finalName>${elevator.release.finalName}</finalName>
|
||||
<resources>
|
||||
<resource>
|
||||
<directory>src/main/resources</directory>
|
||||
</resource>
|
||||
<resource>
|
||||
<directory>${project.basedir}/../deploy/v2-maven</directory>
|
||||
<includes>
|
||||
<include>bootstrap.properties</include>
|
||||
<include>application.properties</include>
|
||||
<include>application-access-control.properties</include>
|
||||
</includes>
|
||||
<filtering>false</filtering>
|
||||
</resource>
|
||||
</resources>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>repackage</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>net.revelc.code.formatter</groupId>
|
||||
|
||||
-5
@@ -1,20 +1,15 @@
|
||||
package cn.cloudwalk.elevator;
|
||||
|
||||
import cn.cloudwalk.event.EnableCloudwalkEvent;
|
||||
import cn.cloudwalk.ribbon.NincaCrkStdRibbonConfiguration;
|
||||
import com.github.pagehelper.autoconfigure.PageHelperAutoConfiguration;
|
||||
import org.mybatis.spring.annotation.MapperScan;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.cache.annotation.EnableCaching;
|
||||
import org.springframework.cloud.netflix.feign.EnableFeignClients;
|
||||
import org.springframework.cloud.netflix.ribbon.RibbonClients;
|
||||
import org.springframework.context.annotation.EnableAspectJAutoProxy;
|
||||
import org.springframework.scheduling.annotation.EnableAsync;
|
||||
|
||||
@RibbonClients({
|
||||
@org.springframework.cloud.netflix.ribbon.RibbonClient(name = "${feign.ninca-crk-std.name:ninca-crk-std}", configuration = NincaCrkStdRibbonConfiguration.class)
|
||||
})
|
||||
@EnableCloudwalkEvent
|
||||
@EnableAsync
|
||||
@EnableCaching
|
||||
|
||||
+66
-72
@@ -1,72 +1,66 @@
|
||||
package cn.cloudwalk.elevator.config;
|
||||
|
||||
import cn.cloudwalk.elevator.integration.davinci.OpenFeignFileStorageManager;
|
||||
import cn.cloudwalk.intelligent.davinci.common.exception.DavinciServiceException;
|
||||
import cn.cloudwalk.intelligent.davinci.storage.bean.part.dto.PartFinishDTO;
|
||||
import cn.cloudwalk.intelligent.davinci.storage.bean.part.dto.PartInitDTO;
|
||||
import cn.cloudwalk.intelligent.davinci.storage.bean.part.dto.PartInitResultDTO;
|
||||
import cn.cloudwalk.intelligent.davinci.storage.manager.FilePartManager;
|
||||
import cn.cloudwalk.intelligent.davinci.storage.manager.FileStorageManager;
|
||||
import cn.cloudwalk.intelligent.davinci.storage.manager.impl.FilePartManagerImpl;
|
||||
import feign.Client;
|
||||
import feign.codec.Decoder;
|
||||
import feign.codec.Encoder;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.cloud.netflix.feign.FeignClientsConfiguration;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@Configuration
|
||||
@Import(FeignClientsConfiguration.class)
|
||||
public class DavinciStorageBeansConfiguration {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(DavinciStorageBeansConfiguration.class);
|
||||
|
||||
@Bean
|
||||
public FileStorageManager fileStorageManager(
|
||||
@Value("${feign.davinci-portal.name:davinci-portal}") String serviceName,
|
||||
Decoder decoder,
|
||||
Encoder encoder,
|
||||
Client client) {
|
||||
return new OpenFeignFileStorageManager(serviceName, decoder, encoder, client);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public FilePartManager filePartManager(
|
||||
@Value("${feign.davinci-portal.name:davinci-portal}") String serviceName,
|
||||
Decoder decoder,
|
||||
Encoder encoder,
|
||||
Client client) {
|
||||
try {
|
||||
return new FilePartManagerImpl(serviceName, decoder, encoder, client);
|
||||
} catch (NoClassDefFoundError e) {
|
||||
LOGGER.warn("Davinci FilePartManager init failed, fallback to noop: {}", e.toString());
|
||||
return new NoopFilePartManager();
|
||||
}
|
||||
}
|
||||
|
||||
private static final class NoopFilePartManager implements FilePartManager {
|
||||
private static final String CODE = "ELEVATOR_DAVINCI_DISABLED";
|
||||
private static final String MSG = "davinci multipart upload is unavailable in current runtime";
|
||||
|
||||
@Override
|
||||
public PartInitResultDTO init(PartInitDTO paramPartInitDTO) throws DavinciServiceException {
|
||||
throw new DavinciServiceException(CODE, MSG);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PartInitResultDTO append(String paramString1, Integer paramInteger, String paramString2,
|
||||
org.springframework.web.multipart.MultipartFile paramMultipartFile)
|
||||
throws DavinciServiceException {
|
||||
throw new DavinciServiceException(CODE, MSG);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String finish(PartFinishDTO paramPartFinishDTO) throws DavinciServiceException {
|
||||
throw new DavinciServiceException(CODE, MSG);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
package cn.cloudwalk.elevator.config;
|
||||
|
||||
import cn.cloudwalk.elevator.integration.davinci.OpenFeignFileStorageManager;
|
||||
import cn.cloudwalk.intelligent.davinci.common.exception.DavinciServiceException;
|
||||
import cn.cloudwalk.intelligent.davinci.storage.bean.part.dto.PartFinishDTO;
|
||||
import cn.cloudwalk.intelligent.davinci.storage.bean.part.dto.PartInitDTO;
|
||||
import cn.cloudwalk.intelligent.davinci.storage.bean.part.dto.PartInitResultDTO;
|
||||
import cn.cloudwalk.intelligent.davinci.storage.manager.FilePartManager;
|
||||
import cn.cloudwalk.intelligent.davinci.storage.manager.FileStorageManager;
|
||||
import cn.cloudwalk.intelligent.davinci.storage.manager.impl.FilePartManagerImpl;
|
||||
import feign.Client;
|
||||
import feign.codec.Decoder;
|
||||
import feign.codec.Encoder;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.cloud.netflix.feign.FeignClientsConfiguration;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@Configuration
|
||||
@Import(FeignClientsConfiguration.class)
|
||||
public class DavinciStorageBeansConfiguration {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(DavinciStorageBeansConfiguration.class);
|
||||
|
||||
@Bean
|
||||
public FileStorageManager fileStorageManager(
|
||||
@Value("${feign.davinci-portal.name:davinci-portal}") String serviceName, Decoder decoder, Encoder encoder,
|
||||
Client client) {
|
||||
return new OpenFeignFileStorageManager(serviceName, decoder, encoder, client);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public FilePartManager filePartManager(@Value("${feign.davinci-portal.name:davinci-portal}") String serviceName,
|
||||
Decoder decoder, Encoder encoder, Client client) {
|
||||
try {
|
||||
return new FilePartManagerImpl(serviceName, decoder, encoder, client);
|
||||
} catch (NoClassDefFoundError e) {
|
||||
LOGGER.warn("Davinci FilePartManager init failed, fallback to noop: {}", e.toString());
|
||||
return new NoopFilePartManager();
|
||||
}
|
||||
}
|
||||
|
||||
private static final class NoopFilePartManager implements FilePartManager {
|
||||
private static final String CODE = "ELEVATOR_DAVINCI_DISABLED";
|
||||
private static final String MSG = "davinci multipart upload is unavailable in current runtime";
|
||||
|
||||
@Override
|
||||
public PartInitResultDTO init(PartInitDTO paramPartInitDTO) throws DavinciServiceException {
|
||||
throw new DavinciServiceException(CODE, MSG);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PartInitResultDTO append(String paramString1, Integer paramInteger, String paramString2,
|
||||
org.springframework.web.multipart.MultipartFile paramMultipartFile) throws DavinciServiceException {
|
||||
throw new DavinciServiceException(CODE, MSG);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String finish(PartFinishDTO paramPartFinishDTO) throws DavinciServiceException {
|
||||
throw new DavinciServiceException(CODE, MSG);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
package cn.cloudwalk.elevator.debug;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.boot.ApplicationArguments;
|
||||
import org.springframework.boot.ApplicationRunner;
|
||||
import org.springframework.core.env.ConfigurableEnvironment;
|
||||
import org.springframework.core.env.PropertySource;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 启动期打印关键配置的命中来源,便于排查配置覆盖链路。始终执行;调度/开关见 {@link ElevatorProbeConstants}(不使用 {@code elevator.config.probe.enabled})。
|
||||
*/
|
||||
@Component
|
||||
public class ConfigSourceProbeRunner implements ApplicationRunner {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(ConfigSourceProbeRunner.class);
|
||||
|
||||
private final ConfigurableEnvironment environment;
|
||||
|
||||
public ConfigSourceProbeRunner(ConfigurableEnvironment environment) {
|
||||
this.environment = environment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run(ApplicationArguments args) {
|
||||
List<String> keys = Arrays.asList("spring.application.name", "spring.profiles.active", "server.port",
|
||||
"spring.cloud.consul.host", "spring.cloud.consul.port", "spring.cloud.consul.enabled",
|
||||
"spring.cloud.consul.discovery.enabled", "spring.cloud.consul.discovery.register",
|
||||
"spring.cloud.consul.discovery.instance-id", "spring.cloud.consul.discovery.ip-address",
|
||||
"spring.cloud.client.ip-address", "feign.cwos-portal.name", "feign.ninca-common.name",
|
||||
"feign.ninca-crk-std.name", "feign.component-organization.name", "feign.mqtt.name",
|
||||
"cwos-portal.ribbon.NIWSServerListClassName", "ninca-common.ribbon.NIWSServerListClassName",
|
||||
"ninca-common-component-organization.ribbon.NIWSServerListClassName",
|
||||
"ninca-crk-std.ribbon.NIWSServerListClassName", "cwos-portal.ribbon.listOfServers",
|
||||
"ninca-common.ribbon.listOfServers", "ninca-common-component-organization.ribbon.listOfServers",
|
||||
"ninca-crk-std.ribbon.listOfServers", "ninca-crk-std.ip");
|
||||
|
||||
LOGGER.debug("===== CONFIG SOURCE PROBE START =====");
|
||||
for (String key : keys) {
|
||||
String value = environment.getProperty(key);
|
||||
LOGGER.debug("probe key={} value={}", key, value);
|
||||
for (PropertySource<?> source : environment.getPropertySources()) {
|
||||
Object candidate = source.getProperty(key);
|
||||
if (candidate != null) {
|
||||
LOGGER.debug("probe key={} source={} sourceValue={}", key, source.getName(), candidate);
|
||||
}
|
||||
}
|
||||
}
|
||||
LOGGER.debug("===== CONFIG SOURCE PROBE END =====");
|
||||
}
|
||||
}
|
||||
+307
@@ -0,0 +1,307 @@
|
||||
package cn.cloudwalk.elevator.debug;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.io.InputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.DisposableBean;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.ApplicationListener;
|
||||
import org.springframework.context.event.ContextRefreshedEvent;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.StreamUtils;
|
||||
|
||||
/**
|
||||
* 启动后延迟请求 Consul {@code /v1/health/service/<name>},逐实例输出注册地址与检查摘要;并与 JVM 内 Ribbon 列表对照。 探针始终启用,延迟见
|
||||
* {@link ElevatorProbeConstants#CONSUL_HTTP_PROBE_DELAY_SECONDS};Consul 地址仍取自 {@code spring.cloud.consul.host|port}。
|
||||
*
|
||||
* <p>
|
||||
* 开始时核对<strong>本进程</strong>({@code spring.application.name})在 Consul 中的登记与健康状态。
|
||||
*/
|
||||
@Component
|
||||
@Order(Ordered.LOWEST_PRECEDENCE)
|
||||
public class ConsulUpstreamHealthProbeRunner implements ApplicationListener<ContextRefreshedEvent>, DisposableBean {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(ConsulUpstreamHealthProbeRunner.class);
|
||||
|
||||
private static final int CONNECT_TIMEOUT_MS = 3000;
|
||||
private static final int READ_TIMEOUT_MS = 8000;
|
||||
|
||||
private static final int CHECKS_SUMMARY_MAX_CHARS = 360;
|
||||
|
||||
private final AtomicBoolean scheduled = new AtomicBoolean(false);
|
||||
|
||||
private volatile ScheduledExecutorService scheduler;
|
||||
|
||||
@Autowired
|
||||
private Environment environment;
|
||||
|
||||
@Autowired(required = false)
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
@Value("${spring.cloud.consul.host:127.0.0.1}")
|
||||
private String consulHost;
|
||||
|
||||
@Value("${spring.cloud.consul.port:8500}")
|
||||
private int consulPort;
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
ScheduledExecutorService s = scheduler;
|
||||
if (s != null) {
|
||||
s.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApplicationEvent(ContextRefreshedEvent event) {
|
||||
if (event.getApplicationContext().getParent() != null) {
|
||||
return;
|
||||
}
|
||||
if (!scheduled.compareAndSet(false, true)) {
|
||||
return;
|
||||
}
|
||||
ScheduledExecutorService exec = Executors.newSingleThreadScheduledExecutor(r -> {
|
||||
Thread t = new Thread(r, "elevator-consul-probe");
|
||||
t.setDaemon(true);
|
||||
return t;
|
||||
});
|
||||
this.scheduler = exec;
|
||||
exec.schedule(this::probeAll, ElevatorProbeConstants.CONSUL_HTTP_PROBE_DELAY_SECONDS, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
private void probeAll() {
|
||||
LOGGER.debug(
|
||||
"===== CONSUL UPSTREAM HEALTH PROBE START base=http://{}:{} (passing=true for upstream list) =====",
|
||||
consulHost, consulPort);
|
||||
ObjectMapper mapper = objectMapper != null ? objectMapper : new ObjectMapper();
|
||||
probeLocalServiceRegistration(mapper);
|
||||
for (String service : ElevatorUpstreamServiceNames.CONSUL_HEALTH_NAMES) {
|
||||
try {
|
||||
String url = String.format("http://%s:%d/v1/health/service/%s?passing=true", consulHost, consulPort,
|
||||
urlEncodePathSegment(service));
|
||||
HttpBodyResult res = httpGet(url);
|
||||
if (res.code >= 400) {
|
||||
LOGGER.warn("consulProbe service={} httpStatus={} bodySnippet={}", service, res.code,
|
||||
abbrevForLog(res.body, 240));
|
||||
continue;
|
||||
}
|
||||
int n = parseAndLogHealthServiceJson(mapper, res.body, "upstream-passing", service);
|
||||
if (n < 0) {
|
||||
LOGGER.warn("consulProbe service={} passingInstances=PARSE_ERROR bodySnippet={}", service,
|
||||
abbrevForLog(res.body, 240));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOGGER.warn("consulProbe service={} failed: {}", service, e.toString());
|
||||
}
|
||||
}
|
||||
LOGGER.debug("===== CONSUL UPSTREAM HEALTH PROBE END =====");
|
||||
}
|
||||
|
||||
/**
|
||||
* 核对当前应用配置的 Consul 地址上,本服务 serviceId(与 {@code spring.application.name} 默认一致)是否可查;并对比 passing 与全量实例。
|
||||
*/
|
||||
private void probeLocalServiceRegistration(ObjectMapper mapper) {
|
||||
String appName = environment.getProperty("spring.application.name", "elevator-app");
|
||||
LOGGER.debug("===== CONSUL LOCAL SERVICE REGISTRATION PROBE START (this serviceId={}) =====", appName);
|
||||
LOGGER.debug(
|
||||
"consulProbe local merged: spring.cloud.consul.enabled={} discovery.enabled={} discovery.register={} "
|
||||
+ "prefer-ip-address={}",
|
||||
environment.getProperty("spring.cloud.consul.enabled"),
|
||||
environment.getProperty("spring.cloud.consul.discovery.enabled"),
|
||||
environment.getProperty("spring.cloud.consul.discovery.register"),
|
||||
environment.getProperty("spring.cloud.consul.discovery.prefer-ip-address"));
|
||||
LOGGER.debug(
|
||||
"consulProbe local merged: server.port={} discovery.instance-id={} discovery.ip-address={} "
|
||||
+ "spring.cloud.client.ip-address={}",
|
||||
environment.getProperty("server.port"),
|
||||
environment.getProperty("spring.cloud.consul.discovery.instance-id"),
|
||||
environment.getProperty("spring.cloud.consul.discovery.ip-address"),
|
||||
environment.getProperty("spring.cloud.client.ip-address"));
|
||||
|
||||
logConsulAgentSelf(mapper);
|
||||
|
||||
int passing = fetchAndLogHealthService(mapper, appName, true, "local-passing");
|
||||
int allChecks = fetchAndLogHealthService(mapper, appName, false, "local-all");
|
||||
|
||||
LOGGER.debug("consulProbe localService={} summary passingEntries={} allEntries={}", appName, passing,
|
||||
allChecks);
|
||||
|
||||
if (passing < 0 || allChecks < 0) {
|
||||
LOGGER.warn("consulProbe localService={} could not read /v1/health/service (see errors above); skip "
|
||||
+ "registration assert.", appName);
|
||||
} else if (allChecks == 0) {
|
||||
LOGGER.warn(
|
||||
"consulProbe localService={} ZERO entries from GET /v1/health/service/{} — not registered under this "
|
||||
+ "name, wrong Consul address (see spring.cloud.consul.host merge), or catalog lag. Compare with "
|
||||
+ "Consul UI service name (exact match).",
|
||||
appName, appName);
|
||||
} else if (passing == 0) {
|
||||
LOGGER.warn("consulProbe localService={} has health entries ({}) but passing=0 — registration exists; "
|
||||
+ "fix health checks or agent connectivity.", appName, allChecks);
|
||||
} else {
|
||||
LOGGER.debug("consulProbe localService={} registration OK: passing={}", appName, passing);
|
||||
}
|
||||
LOGGER.debug("===== CONSUL LOCAL SERVICE REGISTRATION PROBE END =====");
|
||||
}
|
||||
|
||||
private void logConsulAgentSelf(ObjectMapper mapper) {
|
||||
try {
|
||||
String url = String.format("http://%s:%d/v1/agent/self", consulHost, consulPort);
|
||||
HttpBodyResult res = httpGet(url);
|
||||
if (res.code >= 400) {
|
||||
LOGGER.warn("consulProbe agent/self httpStatus={} bodySnippet={}", res.code,
|
||||
abbrevForLog(res.body, 200));
|
||||
return;
|
||||
}
|
||||
JsonNode root = mapper.readTree(res.body);
|
||||
String dc = root.path("Config").path("Datacenter").asText("");
|
||||
String nodeName = root.path("Config").path("NodeName").asText("");
|
||||
String memberAddr = root.path("Member").path("Addr").asText("");
|
||||
String memberName = root.path("Member").path("Name").asText("");
|
||||
LOGGER.debug(
|
||||
"consulProbe agent/self datacenter={} nodeName={} memberAddr={} memberName={} "
|
||||
+ "(compare spring.cloud.consul.host to reach expected Consul agent)",
|
||||
dc, nodeName, memberAddr, memberName);
|
||||
} catch (Exception e) {
|
||||
LOGGER.warn("consulProbe agent/self failed: {}", e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET health/service with optional {@code ?passing=true}; parse body and log each instance; return entry count or
|
||||
* -1.
|
||||
*/
|
||||
private int fetchAndLogHealthService(ObjectMapper mapper, String serviceName, boolean passingOnly, String scope) {
|
||||
try {
|
||||
String path = urlEncodePathSegment(serviceName);
|
||||
String url = String.format("http://%s:%d/v1/health/service/%s", consulHost, consulPort, path);
|
||||
if (passingOnly) {
|
||||
url += "?passing=true";
|
||||
}
|
||||
HttpBodyResult res = httpGet(url);
|
||||
if (res.code >= 400) {
|
||||
LOGGER.warn("consulProbe health/service scope={} service={} passingOnly={} httpStatus={} snippet={}",
|
||||
scope, serviceName, passingOnly, res.code, abbrevForLog(res.body, 200));
|
||||
return -1;
|
||||
}
|
||||
return parseAndLogHealthServiceJson(mapper, res.body, scope, serviceName);
|
||||
} catch (Exception e) {
|
||||
LOGGER.warn("consulProbe health/service scope={} service={} passingOnly={} failed: {}", scope, serviceName,
|
||||
passingOnly, e.toString());
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
private static HttpBodyResult httpGet(String urlString) throws java.io.IOException {
|
||||
HttpURLConnection conn = null;
|
||||
try {
|
||||
conn = (HttpURLConnection)new URL(urlString).openConnection();
|
||||
conn.setRequestMethod("GET");
|
||||
conn.setConnectTimeout(CONNECT_TIMEOUT_MS);
|
||||
conn.setReadTimeout(READ_TIMEOUT_MS);
|
||||
int code = conn.getResponseCode();
|
||||
InputStream in = code >= 400 ? conn.getErrorStream() : conn.getInputStream();
|
||||
String body = in == null ? "" : StreamUtils.copyToString(in, StandardCharsets.UTF_8);
|
||||
return new HttpBodyResult(code, body);
|
||||
} finally {
|
||||
if (conn != null) {
|
||||
conn.disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final class HttpBodyResult {
|
||||
final int code;
|
||||
final String body;
|
||||
|
||||
HttpBodyResult(int code, String body) {
|
||||
this.code = code;
|
||||
this.body = body;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses Consul {@code /v1/health/service/...} JSON array; logs one DEBUG line per entry with Service Address:Port
|
||||
* and Checks summary.
|
||||
*
|
||||
* @return array size, or -1 on parse error
|
||||
*/
|
||||
private int parseAndLogHealthServiceJson(ObjectMapper mapper, String body, String scope, String queriedService) {
|
||||
if (body == null || body.isEmpty()) {
|
||||
LOGGER.debug("consulProbe summary scope={} queriedService={} entryCount=0", scope, queriedService);
|
||||
return 0;
|
||||
}
|
||||
try {
|
||||
JsonNode root = mapper.readTree(body);
|
||||
if (root == null || !root.isArray()) {
|
||||
return -1;
|
||||
}
|
||||
int n = root.size();
|
||||
LOGGER.debug("consulProbe summary scope={} queriedService={} entryCount={}", scope, queriedService, n);
|
||||
for (int i = 0; i < n; i++) {
|
||||
JsonNode item = root.get(i);
|
||||
JsonNode svc = item.path("Service");
|
||||
JsonNode node = item.path("Node");
|
||||
String sid = svc.path("ID").asText("");
|
||||
String sname = svc.path("Service").asText("");
|
||||
String addr = svc.path("Address").asText("");
|
||||
int port = svc.path("Port").asInt(0);
|
||||
String nodeId = node.path("Node").asText("");
|
||||
String checksTxt = summarizeChecks(item.path("Checks"));
|
||||
LOGGER.debug(
|
||||
"consulProbe instance scope={} queriedService={} service={} id={} endpoint={}:{} node={} checks=[{}]",
|
||||
scope, queriedService, sname, sid, addr, port, nodeId, checksTxt);
|
||||
}
|
||||
return n;
|
||||
} catch (Exception e) {
|
||||
LOGGER.warn("consulProbe parse failed scope={} queriedService={} err={}", scope, queriedService,
|
||||
e.toString());
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
private static String summarizeChecks(JsonNode checks) {
|
||||
if (checks == null || !checks.isArray()) {
|
||||
return "";
|
||||
}
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (JsonNode c : checks) {
|
||||
if (sb.length() > 0) {
|
||||
sb.append(';');
|
||||
}
|
||||
sb.append(c.path("CheckID").asText("")).append(':').append(c.path("Status").asText(""));
|
||||
}
|
||||
String t = sb.toString();
|
||||
return t.length() <= CHECKS_SUMMARY_MAX_CHARS ? t : t.substring(0, CHECKS_SUMMARY_MAX_CHARS) + "…";
|
||||
}
|
||||
|
||||
private static String urlEncodePathSegment(String raw) {
|
||||
try {
|
||||
return URLEncoder.encode(raw, StandardCharsets.UTF_8.name()).replace("+", "%20");
|
||||
} catch (java.io.UnsupportedEncodingException e) {
|
||||
return raw;
|
||||
}
|
||||
}
|
||||
|
||||
private static String abbrevForLog(String s, int maxChars) {
|
||||
if (s == null) {
|
||||
return "";
|
||||
}
|
||||
String t = s.replace("\r\n", " ").replace('\n', ' ').trim();
|
||||
return t.length() <= maxChars ? t : t.substring(0, maxChars) + "…";
|
||||
}
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
package cn.cloudwalk.elevator.debug;
|
||||
|
||||
/**
|
||||
* 电梯诊断探针调度常量(写死,不通过 application.properties 配置开关/间隔)。 调整排障节奏时仅改本类,不再使用 {@code elevator.*.probe.*} 属性。
|
||||
*/
|
||||
public final class ElevatorProbeConstants {
|
||||
|
||||
/** 根上下文 {@link org.springframework.context.event.ContextRefreshedEvent} 之后,首次拉取 Consul /v1/health 的延迟。 */
|
||||
public static final int CONSUL_HTTP_PROBE_DELAY_SECONDS = 20;
|
||||
|
||||
/**
|
||||
* Ribbon 各 Feign 客户端 {@link com.netflix.loadbalancer.ILoadBalancer} 快照的延迟;略大于 Consul 探针,减少两路日志交错。
|
||||
*/
|
||||
public static final int RIBBON_LOAD_BALANCER_PROBE_DELAY_SECONDS = 22;
|
||||
|
||||
/** 0 表示仅打一次 delayed-initial 快照;大于 0 时按该间隔秒数重复。 */
|
||||
public static final int RIBBON_PROBE_REPEAT_SECONDS = 0;
|
||||
|
||||
private ElevatorProbeConstants() {}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
package cn.cloudwalk.elevator.debug;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.ApplicationArguments;
|
||||
import org.springframework.boot.ApplicationRunner;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 启动早期打印诊断探针摘要与独立日志文件名模式,便于一次性对照 {@code *-probe.log}。探针固定启用,调度见 {@link ElevatorProbeConstants}。
|
||||
*/
|
||||
@Component
|
||||
@Order(Ordered.HIGHEST_PRECEDENCE)
|
||||
public class ElevatorProbeSummaryRunner implements ApplicationRunner {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(ElevatorProbeSummaryRunner.class);
|
||||
|
||||
@Value("${logging.file:elevator-app}")
|
||||
private String loggingFile;
|
||||
|
||||
@Override
|
||||
public void run(ApplicationArguments args) {
|
||||
LOGGER.debug("===== ELEVATOR DIAGNOSTIC PROBES always on (see {}) cn.cloudwalk.elevator.debug DEBUG + "
|
||||
+ "org.springframework.cloud.consul / com.netflix.loadbalancer / org.springframework.cloud.client.discovery DEBUG "
|
||||
+ "duplicated in <logging.path>/{}-probe.log; consulDelay={}s ribbonDelay={}s ribbonRepeat={}s =====",
|
||||
ElevatorProbeConstants.class.getSimpleName(), loggingFile,
|
||||
ElevatorProbeConstants.CONSUL_HTTP_PROBE_DELAY_SECONDS,
|
||||
ElevatorProbeConstants.RIBBON_LOAD_BALANCER_PROBE_DELAY_SECONDS,
|
||||
ElevatorProbeConstants.RIBBON_PROBE_REPEAT_SECONDS);
|
||||
}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package cn.cloudwalk.elevator.debug;
|
||||
|
||||
/**
|
||||
* 与 Feign/Ribbon、{@code collect_elevator_runtime_evidence.sh}、Consul {@code /v1/health/service/...} 探针对齐的上游 <strong>服务
|
||||
* id</strong>(即在 Consul 中的注册名,也是 Ribbon {@code client} 名,须与 {@code feign.*.name} 解析结果一致)。
|
||||
*
|
||||
* <p>
|
||||
* 前四项与运维脚本中的循环列表一致。第五项为 MQTT 第三方 Feign 默认名({@code feign.mqtt.name},默认 {@code cloudwalk-device-thirdparty}),便于与门户/通用/crk
|
||||
* 主线并列排障;若现场改写 {@code feign.mqtt.name},须保持 Consul 注册名与之一致,否则探针仍可按需改配置或扩展列表。
|
||||
*/
|
||||
public final class ElevatorUpstreamServiceNames {
|
||||
|
||||
private ElevatorUpstreamServiceNames() {}
|
||||
|
||||
public static final String[] CONSUL_HEALTH_NAMES = new String[] {"cwos-portal", "ninca-common",
|
||||
"ninca-common-component-organization", "ninca-crk-std", "cloudwalk-device-thirdparty"};
|
||||
}
|
||||
+174
@@ -0,0 +1,174 @@
|
||||
package cn.cloudwalk.elevator.debug;
|
||||
|
||||
import com.netflix.loadbalancer.ILoadBalancer;
|
||||
import com.netflix.loadbalancer.Server;
|
||||
import com.netflix.loadbalancer.ServerList;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.stream.Collectors;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.DisposableBean;
|
||||
import org.springframework.cloud.client.ServiceInstance;
|
||||
import org.springframework.cloud.client.discovery.DiscoveryClient;
|
||||
import org.springframework.cloud.netflix.ribbon.SpringClientFactory;
|
||||
import org.springframework.context.ApplicationListener;
|
||||
import org.springframework.context.event.ContextRefreshedEvent;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 运行期打印各 Feign/Ribbon 客户端的 {@link ILoadBalancer} 实例与当前 Server 列表,用于定位 「Load balancer does not have available server」:区分
|
||||
* Consul 发现为空、静态 listOfServers 未生效、或尚未触发懒加载。
|
||||
*
|
||||
* <p>
|
||||
* 始终注册;首次快照延迟见 {@link ElevatorProbeConstants#RIBBON_LOAD_BALANCER_PROBE_DELAY_SECONDS},重复间隔见
|
||||
* {@link ElevatorProbeConstants#RIBBON_PROBE_REPEAT_SECONDS}(不再使用 {@code elevator.ribbon.probe.*})。
|
||||
*
|
||||
* <p>
|
||||
* 同延迟窗口输出 {@link ServerList} 实现类与 {@link DiscoveryClient#getInstances},与 Consul HTTP 探针、logback 双写的 Netflix 日志交叉验证。
|
||||
*/
|
||||
@Component
|
||||
@Order(Ordered.LOWEST_PRECEDENCE)
|
||||
public class RibbonLoadBalancerProbeRunner implements ApplicationListener<ContextRefreshedEvent>, DisposableBean {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(RibbonLoadBalancerProbeRunner.class);
|
||||
|
||||
private final AtomicBoolean scheduled = new AtomicBoolean(false);
|
||||
|
||||
private volatile ScheduledExecutorService scheduler;
|
||||
|
||||
@Autowired(required = false)
|
||||
private SpringClientFactory springClientFactory;
|
||||
|
||||
@Autowired(required = false)
|
||||
private DiscoveryClient discoveryClient;
|
||||
|
||||
@Autowired
|
||||
private Environment environment;
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
ScheduledExecutorService s = scheduler;
|
||||
if (s != null) {
|
||||
s.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApplicationEvent(ContextRefreshedEvent event) {
|
||||
if (event.getApplicationContext().getParent() != null) {
|
||||
return;
|
||||
}
|
||||
if (!scheduled.compareAndSet(false, true)) {
|
||||
return;
|
||||
}
|
||||
if (springClientFactory == null) {
|
||||
LOGGER.warn("RIBBON PROBE SKIPPED: SpringClientFactory not available");
|
||||
return;
|
||||
}
|
||||
ScheduledExecutorService exec = Executors.newSingleThreadScheduledExecutor(r -> {
|
||||
Thread t = new Thread(r, "elevator-ribbon-probe");
|
||||
t.setDaemon(true);
|
||||
return t;
|
||||
});
|
||||
this.scheduler = exec;
|
||||
int delay = ElevatorProbeConstants.RIBBON_LOAD_BALANCER_PROBE_DELAY_SECONDS;
|
||||
int repeat = ElevatorProbeConstants.RIBBON_PROBE_REPEAT_SECONDS;
|
||||
exec.schedule(() -> {
|
||||
logRibbonSnapshot("delayed-initial");
|
||||
logDiscoverySnapshot("delayed-initial");
|
||||
if (repeat > 0) {
|
||||
exec.scheduleAtFixedRate(() -> {
|
||||
logRibbonSnapshot("repeat");
|
||||
logDiscoverySnapshot("repeat");
|
||||
}, repeat, repeat, TimeUnit.SECONDS);
|
||||
}
|
||||
}, delay, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
private void logRibbonSnapshot(String phase) {
|
||||
LOGGER.debug("===== RIBBON LOAD BALANCER PROBE START ({}) =====", phase);
|
||||
for (String client : ElevatorUpstreamServiceNames.CONSUL_HEALTH_NAMES) {
|
||||
try {
|
||||
ILoadBalancer lb = springClientFactory.getLoadBalancer(client);
|
||||
if (lb == null) {
|
||||
LOGGER.warn("probe client={} loadBalancer=null", client);
|
||||
continue;
|
||||
}
|
||||
List<Server> servers = lb.getAllServers();
|
||||
int n = servers == null ? 0 : servers.size();
|
||||
String desc = servers == null ? "null"
|
||||
: servers.stream().map(Server::getHostPort).collect(Collectors.joining(","));
|
||||
try {
|
||||
@SuppressWarnings("rawtypes")
|
||||
ServerList sl = springClientFactory.getInstance(client, ServerList.class);
|
||||
LOGGER.debug(
|
||||
"probe client={} serverListClass={} (ConsulServerList vs ConfigurationBasedServerList)", client,
|
||||
sl == null ? "null" : sl.getClass().getName());
|
||||
} catch (RuntimeException ex) {
|
||||
LOGGER.debug("probe client={} serverListClass lookup failed: {}", client, ex.toString());
|
||||
}
|
||||
if (servers != null) {
|
||||
for (Server s : servers) {
|
||||
LOGGER.debug(
|
||||
"ribbonProbe instance client={} host={} port={} zone={} readyToServe={} alive={} "
|
||||
+ "hostPort={}",
|
||||
client, s.getHost(), s.getPort(), s.getZone(), s.isReadyToServe(), s.isAlive(),
|
||||
s.getHostPort());
|
||||
}
|
||||
}
|
||||
LOGGER.debug("probe client={} serverCount={} servers=[{}] lbClass={}", client, n, desc,
|
||||
lb.getClass().getName());
|
||||
} catch (RuntimeException e) {
|
||||
LOGGER.warn("probe client={} failed: {}", client, e.toString());
|
||||
}
|
||||
}
|
||||
LOGGER.debug("===== RIBBON LOAD BALANCER PROBE END ({}) =====", phase);
|
||||
}
|
||||
|
||||
/**
|
||||
* 与 Consul HTTP 探针对照:同一 serviceId 在 {@link DiscoveryClient} 中可见的实例(JVM 内发现视图)。
|
||||
*/
|
||||
private void logDiscoverySnapshot(String phase) {
|
||||
LOGGER.debug("===== DISCOVERY CLIENT PROBE START ({}) =====", phase);
|
||||
if (discoveryClient == null) {
|
||||
LOGGER.debug("discoveryProbe DiscoveryClient=null (skip)");
|
||||
LOGGER.debug("===== DISCOVERY CLIENT PROBE END ({}) =====", phase);
|
||||
return;
|
||||
}
|
||||
LOGGER.debug("discoveryProbe discoveryClientClass={}", discoveryClient.getClass().getName());
|
||||
Set<String> serviceIds = new LinkedHashSet<>();
|
||||
serviceIds.add(environment.getProperty("spring.application.name", "elevator-app"));
|
||||
for (String u : ElevatorUpstreamServiceNames.CONSUL_HEALTH_NAMES) {
|
||||
serviceIds.add(u);
|
||||
}
|
||||
for (String serviceId : serviceIds) {
|
||||
try {
|
||||
List<ServiceInstance> instances = discoveryClient.getInstances(serviceId);
|
||||
int c = instances == null ? 0 : instances.size();
|
||||
LOGGER.debug("discoveryProbe summary phase={} serviceId={} instanceCount={}", phase, serviceId, c);
|
||||
if (instances != null) {
|
||||
for (ServiceInstance inst : instances) {
|
||||
LOGGER.debug(
|
||||
"discoveryProbe instance phase={} serviceId={} host={} port={} uri={} secure={} "
|
||||
+ "metadata={}",
|
||||
phase, serviceId, inst.getHost(), inst.getPort(), inst.getUri(), inst.isSecure(),
|
||||
inst.getMetadata());
|
||||
}
|
||||
}
|
||||
} catch (RuntimeException e) {
|
||||
LOGGER.warn("discoveryProbe serviceId={} failed: {}", serviceId, e.toString());
|
||||
}
|
||||
}
|
||||
LOGGER.debug("===== DISCOVERY CLIENT PROBE END ({}) =====", phase);
|
||||
}
|
||||
}
|
||||
+228
-228
@@ -1,228 +1,228 @@
|
||||
package cn.cloudwalk.elevator.integration.davinci;
|
||||
|
||||
import cn.cloudwalk.intelligent.davinci.common.exception.DavinciServiceException;
|
||||
import cn.cloudwalk.intelligent.davinci.common.result.DavinciResult;
|
||||
import cn.cloudwalk.intelligent.davinci.storage.bean.file.dto.FileRemoveDTO;
|
||||
import cn.cloudwalk.intelligent.davinci.storage.feign.FileManagerFeign;
|
||||
import cn.cloudwalk.intelligent.davinci.storage.feign.OuterCallFeignClient;
|
||||
import cn.cloudwalk.intelligent.davinci.storage.manager.FileStorageManager;
|
||||
import feign.Client;
|
||||
import feign.Feign;
|
||||
import feign.Response;
|
||||
import feign.codec.Decoder;
|
||||
import feign.codec.Encoder;
|
||||
import feign.form.spring.SpringFormEncoder;
|
||||
import feign.okhttp.OkHttpClient;
|
||||
import java.io.FilterInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.InetAddress;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.springframework.cloud.netflix.feign.support.SpringMvcContract;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
/**
|
||||
* 与 davinci-manager-storage 中逻辑一致,但固定使用 {@link SpringMvcContract}(OpenFeign)。
|
||||
* 避免依赖 Nexus 仍带 Netflix 引用的旧 {@code FileStorageManagerImpl} 字节码导致 NoClassDefFoundError。
|
||||
*/
|
||||
public class OpenFeignFileStorageManager implements FileStorageManager {
|
||||
|
||||
private final FileManagerFeign fileManagerFeign;
|
||||
private final FileManagerFeign fileManagerRestFeign;
|
||||
|
||||
public OpenFeignFileStorageManager(String serviceName, Decoder decoder, Encoder encoder, Client client) {
|
||||
String url = "http://" + serviceName + "/portal/fileManager";
|
||||
|
||||
this.fileManagerFeign = Feign.builder().client(client).decode404().encoder(new SpringFormEncoder())
|
||||
.decoder(decoder).contract(new SpringMvcContract()).target(FileManagerFeign.class, url);
|
||||
|
||||
this.fileManagerRestFeign = Feign.builder().client(client).decode404().encoder(encoder).decoder(decoder)
|
||||
.contract(new SpringMvcContract()).target(FileManagerFeign.class, url);
|
||||
}
|
||||
|
||||
static void assertSafeHttpUrl(String urlString) throws DavinciServiceException {
|
||||
if (StringUtils.isEmpty(urlString)) {
|
||||
throw new DavinciServiceException("INVALID_URL", "URL 为空");
|
||||
}
|
||||
URI uri;
|
||||
try {
|
||||
uri = new URI(urlString);
|
||||
} catch (URISyntaxException e) {
|
||||
throw new DavinciServiceException("INVALID_URL", "URL 非法");
|
||||
}
|
||||
String scheme = uri.getScheme();
|
||||
if (scheme == null || (!"http".equalsIgnoreCase(scheme) && !"https".equalsIgnoreCase(scheme))) {
|
||||
throw new DavinciServiceException("INVALID_URL", "仅允许 http 或 https 协议");
|
||||
}
|
||||
String host = uri.getHost();
|
||||
if (StringUtils.isEmpty(host)) {
|
||||
throw new DavinciServiceException("INVALID_URL", "缺少主机名");
|
||||
}
|
||||
String lowerHost = host.toLowerCase(Locale.ROOT);
|
||||
if ("localhost".equals(lowerHost) || lowerHost.endsWith(".local")) {
|
||||
throw new DavinciServiceException("INVALID_URL", "禁止访问该主机");
|
||||
}
|
||||
if ("metadata.google.internal".equalsIgnoreCase(host)) {
|
||||
throw new DavinciServiceException("INVALID_URL", "禁止访问该主机");
|
||||
}
|
||||
try {
|
||||
InetAddress[] all = InetAddress.getAllByName(host);
|
||||
for (InetAddress addr : all) {
|
||||
if (addr.isAnyLocalAddress() || addr.isLoopbackAddress() || addr.isLinkLocalAddress()
|
||||
|| addr.isSiteLocalAddress() || addr.isMulticastAddress()) {
|
||||
throw new DavinciServiceException("INVALID_URL", "禁止访问内网或保留地址");
|
||||
}
|
||||
}
|
||||
} catch (UnknownHostException e) {
|
||||
throw new DavinciServiceException("INVALID_URL", "无法解析主机");
|
||||
}
|
||||
}
|
||||
|
||||
private static void requireDavinciResult(DavinciResult<?> result, String op) throws DavinciServiceException {
|
||||
if (result == null) {
|
||||
throw new DavinciServiceException("NULL_RESULT", "Davinci-portal 返回空结果: " + op);
|
||||
}
|
||||
}
|
||||
|
||||
private static InputStream attachResponseClose(InputStream bodyStream, Response response) {
|
||||
return new FilterInputStream(bodyStream) {
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
try {
|
||||
super.close();
|
||||
} finally {
|
||||
response.close();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public String fileUpload(MultipartFile file) throws DavinciServiceException {
|
||||
DavinciResult<String> result = this.fileManagerFeign.fileUpload(file);
|
||||
requireDavinciResult(result, "fileUpload");
|
||||
if (result.isSuccess()) {
|
||||
return result.getData();
|
||||
}
|
||||
throw new DavinciServiceException(result.getCode(), result.getMessage());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String fileUpload(String moduleCategory, MultipartFile file) throws DavinciServiceException {
|
||||
DavinciResult<String> result = this.fileManagerFeign.fileUpload(moduleCategory, file);
|
||||
requireDavinciResult(result, "fileUpload(module)");
|
||||
if (result.isSuccess()) {
|
||||
return result.getData();
|
||||
}
|
||||
throw new DavinciServiceException(result.getCode(), result.getMessage());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String bigFileUpload(MultipartFile file) throws DavinciServiceException {
|
||||
DavinciResult<String> result = this.fileManagerFeign.bigFileUpload(file);
|
||||
requireDavinciResult(result, "bigFileUpload");
|
||||
if (result.isSuccess()) {
|
||||
return result.getData();
|
||||
}
|
||||
throw new DavinciServiceException(result.getCode(), result.getMessage());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String bigFileUpload(String moduleCategory, MultipartFile file) throws DavinciServiceException {
|
||||
DavinciResult<String> result = this.fileManagerFeign.bigFileUpload(moduleCategory, file);
|
||||
requireDavinciResult(result, "bigFileUpload(module)");
|
||||
if (result.isSuccess()) {
|
||||
return result.getData();
|
||||
}
|
||||
throw new DavinciServiceException(result.getCode(), result.getMessage());
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] fileDownload(String path) throws DavinciServiceException {
|
||||
try (Response response = this.fileManagerFeign.fileDownload(path)) {
|
||||
if (response == null) {
|
||||
return null;
|
||||
}
|
||||
if (response.body() == null) {
|
||||
return null;
|
||||
}
|
||||
try (InputStream inputStream = response.body().asInputStream()) {
|
||||
return IOUtils.toByteArray(inputStream);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new DavinciServiceException("FILE_DOWNLOAD_IO", "调用Davinci-portal服务,获取文件流接口异常");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream fileDownloadStream(String path) throws DavinciServiceException {
|
||||
Response response = this.fileManagerFeign.fileDownload(path);
|
||||
try {
|
||||
if (response == null) {
|
||||
return null;
|
||||
}
|
||||
if (response.body() == null) {
|
||||
response.close();
|
||||
return null;
|
||||
}
|
||||
return attachResponseClose(response.body().asInputStream(), response);
|
||||
} catch (IOException e) {
|
||||
if (response != null) {
|
||||
response.close();
|
||||
}
|
||||
throw new DavinciServiceException("FILE_DOWNLOAD_IO", "调用Davinci-portal服务,获取文件流接口异常");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getFileBase64(String path) throws DavinciServiceException {
|
||||
if (StringUtils.isEmpty(path)) {
|
||||
return "";
|
||||
}
|
||||
DavinciResult<String> result = this.fileManagerFeign.getFileData(path);
|
||||
requireDavinciResult(result, "getFileData");
|
||||
if (result.isSuccess()) {
|
||||
return result.getData();
|
||||
}
|
||||
throw new DavinciServiceException(result.getCode(), result.getMessage());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> remove(FileRemoveDTO dto) throws DavinciServiceException {
|
||||
DavinciResult<List<String>> result = this.fileManagerRestFeign.remove(dto);
|
||||
requireDavinciResult(result, "remove");
|
||||
if (result.isSuccess()) {
|
||||
return result.getData();
|
||||
}
|
||||
throw new DavinciServiceException(result.getCode(), result.getMessage());
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream fileDownLoadWithAbsoluteUrl(String url) throws DavinciServiceException {
|
||||
assertSafeHttpUrl(url);
|
||||
OuterCallFeignClient feignClient = Feign.builder().client(new OkHttpClient()).target(OuterCallFeignClient.class,
|
||||
url);
|
||||
Response response;
|
||||
try {
|
||||
response = feignClient.downLoad();
|
||||
} catch (RuntimeException e) {
|
||||
throw new DavinciServiceException("OUTER_DOWNLOAD", "拉取远程文件失败");
|
||||
}
|
||||
try {
|
||||
if (response.body() == null) {
|
||||
response.close();
|
||||
return null;
|
||||
}
|
||||
return attachResponseClose(response.body().asInputStream(), response);
|
||||
} catch (IOException e) {
|
||||
response.close();
|
||||
throw new DavinciServiceException("OUTER_DOWNLOAD", "读取远程文件流失败");
|
||||
}
|
||||
}
|
||||
}
|
||||
package cn.cloudwalk.elevator.integration.davinci;
|
||||
|
||||
import cn.cloudwalk.intelligent.davinci.common.exception.DavinciServiceException;
|
||||
import cn.cloudwalk.intelligent.davinci.common.result.DavinciResult;
|
||||
import cn.cloudwalk.intelligent.davinci.storage.bean.file.dto.FileRemoveDTO;
|
||||
import cn.cloudwalk.intelligent.davinci.storage.feign.FileManagerFeign;
|
||||
import cn.cloudwalk.intelligent.davinci.storage.feign.OuterCallFeignClient;
|
||||
import cn.cloudwalk.intelligent.davinci.storage.manager.FileStorageManager;
|
||||
import feign.Client;
|
||||
import feign.Feign;
|
||||
import feign.Response;
|
||||
import feign.codec.Decoder;
|
||||
import feign.codec.Encoder;
|
||||
import feign.form.spring.SpringFormEncoder;
|
||||
import feign.okhttp.OkHttpClient;
|
||||
import java.io.FilterInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.InetAddress;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.springframework.cloud.netflix.feign.support.SpringMvcContract;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
/**
|
||||
* 与 davinci-manager-storage 中逻辑一致,但固定使用 {@link SpringMvcContract}(OpenFeign)。 避免依赖 Nexus 仍带 Netflix 引用的旧
|
||||
* {@code FileStorageManagerImpl} 字节码导致 NoClassDefFoundError。
|
||||
*/
|
||||
public class OpenFeignFileStorageManager implements FileStorageManager {
|
||||
|
||||
private final FileManagerFeign fileManagerFeign;
|
||||
private final FileManagerFeign fileManagerRestFeign;
|
||||
|
||||
public OpenFeignFileStorageManager(String serviceName, Decoder decoder, Encoder encoder, Client client) {
|
||||
String url = "http://" + serviceName + "/portal/fileManager";
|
||||
|
||||
this.fileManagerFeign = Feign.builder().client(client).decode404().encoder(new SpringFormEncoder())
|
||||
.decoder(decoder).contract(new SpringMvcContract()).target(FileManagerFeign.class, url);
|
||||
|
||||
this.fileManagerRestFeign = Feign.builder().client(client).decode404().encoder(encoder).decoder(decoder)
|
||||
.contract(new SpringMvcContract()).target(FileManagerFeign.class, url);
|
||||
}
|
||||
|
||||
static void assertSafeHttpUrl(String urlString) throws DavinciServiceException {
|
||||
if (StringUtils.isEmpty(urlString)) {
|
||||
throw new DavinciServiceException("INVALID_URL", "URL 为空");
|
||||
}
|
||||
URI uri;
|
||||
try {
|
||||
uri = new URI(urlString);
|
||||
} catch (URISyntaxException e) {
|
||||
throw new DavinciServiceException("INVALID_URL", "URL 非法");
|
||||
}
|
||||
String scheme = uri.getScheme();
|
||||
if (scheme == null || (!"http".equalsIgnoreCase(scheme) && !"https".equalsIgnoreCase(scheme))) {
|
||||
throw new DavinciServiceException("INVALID_URL", "仅允许 http 或 https 协议");
|
||||
}
|
||||
String host = uri.getHost();
|
||||
if (StringUtils.isEmpty(host)) {
|
||||
throw new DavinciServiceException("INVALID_URL", "缺少主机名");
|
||||
}
|
||||
String lowerHost = host.toLowerCase(Locale.ROOT);
|
||||
if ("localhost".equals(lowerHost) || lowerHost.endsWith(".local")) {
|
||||
throw new DavinciServiceException("INVALID_URL", "禁止访问该主机");
|
||||
}
|
||||
if ("metadata.google.internal".equalsIgnoreCase(host)) {
|
||||
throw new DavinciServiceException("INVALID_URL", "禁止访问该主机");
|
||||
}
|
||||
try {
|
||||
InetAddress[] all = InetAddress.getAllByName(host);
|
||||
for (InetAddress addr : all) {
|
||||
if (addr.isAnyLocalAddress() || addr.isLoopbackAddress() || addr.isLinkLocalAddress()
|
||||
|| addr.isSiteLocalAddress() || addr.isMulticastAddress()) {
|
||||
throw new DavinciServiceException("INVALID_URL", "禁止访问内网或保留地址");
|
||||
}
|
||||
}
|
||||
} catch (UnknownHostException e) {
|
||||
throw new DavinciServiceException("INVALID_URL", "无法解析主机");
|
||||
}
|
||||
}
|
||||
|
||||
private static void requireDavinciResult(DavinciResult<?> result, String op) throws DavinciServiceException {
|
||||
if (result == null) {
|
||||
throw new DavinciServiceException("NULL_RESULT", "Davinci-portal 返回空结果: " + op);
|
||||
}
|
||||
}
|
||||
|
||||
private static InputStream attachResponseClose(InputStream bodyStream, Response response) {
|
||||
return new FilterInputStream(bodyStream) {
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
try {
|
||||
super.close();
|
||||
} finally {
|
||||
response.close();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public String fileUpload(MultipartFile file) throws DavinciServiceException {
|
||||
DavinciResult<String> result = this.fileManagerFeign.fileUpload(file);
|
||||
requireDavinciResult(result, "fileUpload");
|
||||
if (result.isSuccess()) {
|
||||
return result.getData();
|
||||
}
|
||||
throw new DavinciServiceException(result.getCode(), result.getMessage());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String fileUpload(String moduleCategory, MultipartFile file) throws DavinciServiceException {
|
||||
DavinciResult<String> result = this.fileManagerFeign.fileUpload(moduleCategory, file);
|
||||
requireDavinciResult(result, "fileUpload(module)");
|
||||
if (result.isSuccess()) {
|
||||
return result.getData();
|
||||
}
|
||||
throw new DavinciServiceException(result.getCode(), result.getMessage());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String bigFileUpload(MultipartFile file) throws DavinciServiceException {
|
||||
DavinciResult<String> result = this.fileManagerFeign.bigFileUpload(file);
|
||||
requireDavinciResult(result, "bigFileUpload");
|
||||
if (result.isSuccess()) {
|
||||
return result.getData();
|
||||
}
|
||||
throw new DavinciServiceException(result.getCode(), result.getMessage());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String bigFileUpload(String moduleCategory, MultipartFile file) throws DavinciServiceException {
|
||||
DavinciResult<String> result = this.fileManagerFeign.bigFileUpload(moduleCategory, file);
|
||||
requireDavinciResult(result, "bigFileUpload(module)");
|
||||
if (result.isSuccess()) {
|
||||
return result.getData();
|
||||
}
|
||||
throw new DavinciServiceException(result.getCode(), result.getMessage());
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] fileDownload(String path) throws DavinciServiceException {
|
||||
try (Response response = this.fileManagerFeign.fileDownload(path)) {
|
||||
if (response == null) {
|
||||
return null;
|
||||
}
|
||||
if (response.body() == null) {
|
||||
return null;
|
||||
}
|
||||
try (InputStream inputStream = response.body().asInputStream()) {
|
||||
return IOUtils.toByteArray(inputStream);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new DavinciServiceException("FILE_DOWNLOAD_IO", "调用Davinci-portal服务,获取文件流接口异常");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream fileDownloadStream(String path) throws DavinciServiceException {
|
||||
Response response = this.fileManagerFeign.fileDownload(path);
|
||||
try {
|
||||
if (response == null) {
|
||||
return null;
|
||||
}
|
||||
if (response.body() == null) {
|
||||
response.close();
|
||||
return null;
|
||||
}
|
||||
return attachResponseClose(response.body().asInputStream(), response);
|
||||
} catch (IOException e) {
|
||||
if (response != null) {
|
||||
response.close();
|
||||
}
|
||||
throw new DavinciServiceException("FILE_DOWNLOAD_IO", "调用Davinci-portal服务,获取文件流接口异常");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getFileBase64(String path) throws DavinciServiceException {
|
||||
if (StringUtils.isEmpty(path)) {
|
||||
return "";
|
||||
}
|
||||
DavinciResult<String> result = this.fileManagerFeign.getFileData(path);
|
||||
requireDavinciResult(result, "getFileData");
|
||||
if (result.isSuccess()) {
|
||||
return result.getData();
|
||||
}
|
||||
throw new DavinciServiceException(result.getCode(), result.getMessage());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> remove(FileRemoveDTO dto) throws DavinciServiceException {
|
||||
DavinciResult<List<String>> result = this.fileManagerRestFeign.remove(dto);
|
||||
requireDavinciResult(result, "remove");
|
||||
if (result.isSuccess()) {
|
||||
return result.getData();
|
||||
}
|
||||
throw new DavinciServiceException(result.getCode(), result.getMessage());
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream fileDownLoadWithAbsoluteUrl(String url) throws DavinciServiceException {
|
||||
assertSafeHttpUrl(url);
|
||||
OuterCallFeignClient feignClient =
|
||||
Feign.builder().client(new OkHttpClient()).target(OuterCallFeignClient.class, url);
|
||||
Response response;
|
||||
try {
|
||||
response = feignClient.downLoad();
|
||||
} catch (RuntimeException e) {
|
||||
throw new DavinciServiceException("OUTER_DOWNLOAD", "拉取远程文件失败");
|
||||
}
|
||||
try {
|
||||
if (response.body() == null) {
|
||||
response.close();
|
||||
return null;
|
||||
}
|
||||
return attachResponseClose(response.body().asInputStream(), response);
|
||||
} catch (IOException e) {
|
||||
response.close();
|
||||
throw new DavinciServiceException("OUTER_DOWNLOAD", "读取远程文件流失败");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
-23
@@ -1,23 +0,0 @@
|
||||
package cn.cloudwalk.ribbon;
|
||||
|
||||
import com.netflix.client.config.IClientConfig;
|
||||
import com.netflix.loadbalancer.ConfigurationBasedServerList;
|
||||
import com.netflix.loadbalancer.Server;
|
||||
import com.netflix.loadbalancer.ServerList;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* 为 Feign 客户端 {@code ninca-crk-std} 强制使用配置型 ServerList,避免 Consul 发现返回空列表时覆盖
|
||||
* {@code ninca-crk-std.ribbon.listOfServers}(Edgware 下常见「Load balancer does not have available server」)。
|
||||
*/
|
||||
@Configuration
|
||||
public class NincaCrkStdRibbonConfiguration {
|
||||
|
||||
@Bean
|
||||
public ServerList<Server> ribbonServerList(IClientConfig config) {
|
||||
ConfigurationBasedServerList list = new ConfigurationBasedServerList();
|
||||
list.initWithNiwsConfig(config);
|
||||
return list;
|
||||
}
|
||||
}
|
||||
+37
-3
@@ -5,8 +5,8 @@
|
||||
|
||||
<springProperty scope="context" name="fileName" source="logging.file" defaultValue="default"/>
|
||||
|
||||
<!--myibatis log configure-->
|
||||
<logger name="com.apache.ibatis" level="DEBUG"/>
|
||||
<!-- MyBatis 3:logger 名为 org.apache.ibatis.* -->
|
||||
<logger name="org.apache.ibatis" level="DEBUG"/>
|
||||
<logger name="java.sql.Connection" level="DEBUG"/>
|
||||
<logger name="java.sql.Statement" level="DEBUG"/>
|
||||
<logger name="java.sql.PreparedStatement" level="DEBUG"/>
|
||||
@@ -48,7 +48,41 @@
|
||||
<!-- </triggeringPolicy> -->
|
||||
</appender>
|
||||
|
||||
<!-- 日志输出级别 -->
|
||||
<!-- 电梯诊断探针专用文件:与主日志同内容双写(additivity=true:仍走 root→控制台与 ${fileName}.log,并额外写入 ${fileName}-probe.log) -->
|
||||
<appender name="PROBE" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<File>${LOG_PATH}/${fileName}-probe.log</File>
|
||||
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||
<FileNamePattern>${LOG_PATH}/${fileName}-probe.%d{yyyy-MM-dd}.%i.log</FileNamePattern>
|
||||
<MaxHistory>14</MaxHistory>
|
||||
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
|
||||
<maxFileSize>20MB</maxFileSize>
|
||||
</timeBasedFileNamingAndTriggeringPolicy>
|
||||
</rollingPolicy>
|
||||
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
|
||||
<pattern>[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%thread] %-5level %logger{50}:%line - %msg%n</pattern>
|
||||
<charset>UTF-8</charset>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<!-- 探针类:生产环境关闭 debug 输出,提升至 WARN;仍写入 -probe.log 便于异常排障 -->
|
||||
<logger name="cn.cloudwalk.elevator.debug" level="WARN" additivity="true">
|
||||
<appender-ref ref="PROBE"/>
|
||||
</logger>
|
||||
|
||||
<!-- 探针对照框架包统一提升至 WARN,关闭 debug 噪音 -->
|
||||
<logger name="org.springframework.cloud.consul" level="WARN" additivity="true">
|
||||
<appender-ref ref="PROBE"/>
|
||||
</logger>
|
||||
|
||||
<logger name="com.netflix.loadbalancer" level="WARN" additivity="true">
|
||||
<appender-ref ref="PROBE"/>
|
||||
</logger>
|
||||
|
||||
<logger name="org.springframework.cloud.client.discovery" level="WARN" additivity="true">
|
||||
<appender-ref ref="PROBE"/>
|
||||
</logger>
|
||||
|
||||
<!-- 日志输出级别(默认 INFO;部署 application.properties 可再覆盖) -->
|
||||
<root level="INFO">
|
||||
<appender-ref ref="S"/>
|
||||
<appender-ref ref="R"/>
|
||||
|
||||
Reference in New Issue
Block a user