feat(ft546): add isolated shadow process

This commit is contained in:
zgw
2026-08-12 14:21:32 +08:00
parent 778444e5ea
commit 364668c939
14 changed files with 668 additions and 0 deletions

View File

@@ -265,6 +265,7 @@
<module>ruoyi-admin</module>
<module>ruoyi-ft546-protocol</module>
<module>ruoyi-wcs</module>
<module>ruoyi-wcs-shadow</module>
<module>ruoyi-framework</module>
<module>ruoyi-system</module>
<module>ruoyi-quartz</module>

68
ruoyi-wcs-shadow/pom.xml Normal file
View File

@@ -0,0 +1,68 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi</artifactId>
<version>3.8.9</version>
</parent>
<artifactId>ruoyi-wcs-shadow</artifactId>
<packaging>jar</packaging>
<description>Independent read-only FT546 shadow process</description>
<dependencies>
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-wcs</artifactId>
<exclusions>
<exclusion>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-common</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-ft546-protocol</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.tngtech.archunit</groupId>
<artifactId>archunit-junit5</artifactId>
<version>1.2.1</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot.version}</version>
<configuration>
<mainClass>com.ruoyi.shadow.Ft546ShadowApplication</mainClass>
</configuration>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,26 @@
package com.ruoyi.shadow;
import com.ruoyi.shadow.config.ShadowKernelConfiguration;
import com.ruoyi.shadow.config.ShadowRuntimeConfiguration;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration;
import org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration;
import org.springframework.context.annotation.Import;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootConfiguration
@EnableAutoConfiguration(exclude = {
DataSourceAutoConfiguration.class,
SecurityAutoConfiguration.class,
UserDetailsServiceAutoConfiguration.class
})
@Import({ShadowRuntimeConfiguration.class, ShadowKernelConfiguration.class})
@EnableScheduling
public class Ft546ShadowApplication {
public static void main(String[] args) {
SpringApplication.run(Ft546ShadowApplication.class, args);
}
}

View File

@@ -0,0 +1,60 @@
package com.ruoyi.shadow.config;
import com.ruoyi.wcs.application.dispatch.DispatchCoordinator;
import com.ruoyi.wcs.domain.dispatch.DefaultVehicleAssignmentPolicy;
import com.ruoyi.wcs.domain.dispatch.VehicleAssignmentPolicy;
import com.ruoyi.wcs.domain.shared.IdGenerator;
import com.ruoyi.wcs.domain.traffic.AStarRoutePlanner;
import com.ruoyi.wcs.domain.traffic.DeadlockGuard;
import com.ruoyi.wcs.domain.traffic.RoutePlanner;
import com.ruoyi.wcs.domain.traffic.WaitForGraph;
import com.ruoyi.wcs.port.out.DispatchPolicy;
import com.ruoyi.wcs.port.out.TrafficStateRepository;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.concurrent.atomic.AtomicLong;
@Configuration(proxyBeanMethods = false)
public class ShadowKernelConfiguration {
@Bean
public RoutePlanner shadowRoutePlanner() {
return new AStarRoutePlanner();
}
@Bean
public VehicleAssignmentPolicy shadowVehicleAssignmentPolicy(
RoutePlanner routePlanner) {
return new DefaultVehicleAssignmentPolicy(routePlanner,
DispatchPolicy.NONE);
}
@Bean
public DeadlockGuard shadowDeadlockGuard(RoutePlanner routePlanner) {
return new DeadlockGuard(new WaitForGraph(), routePlanner);
}
@Bean
public IdGenerator shadowIdGenerator() {
AtomicLong sequence = new AtomicLong();
return () -> "SHADOW-" + sequence.incrementAndGet();
}
@Bean
public TrafficStateRepository disabledShadowTrafficStateRepository() {
return commit -> {
throw new IllegalStateException(
"SHADOW_TRAFFIC_STATE_REPOSITORY_NOT_CONFIGURED");
};
}
@Bean
public DispatchCoordinator shadowDispatchCoordinator(
VehicleAssignmentPolicy assignmentPolicy,
DeadlockGuard deadlockGuard,
TrafficStateRepository trafficStateRepository,
IdGenerator idGenerator) {
return new DispatchCoordinator(assignmentPolicy, deadlockGuard,
trafficStateRepository, idGenerator);
}
}

View File

@@ -0,0 +1,22 @@
package com.ruoyi.shadow.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "shadow")
public class ShadowProperties {
private boolean enabled;
private final JdbcEndpoint source = new JdbcEndpoint();
private final JdbcEndpoint evidence = new JdbcEndpoint();
public boolean isEnabled() { return enabled; }
public void setEnabled(boolean enabled) { this.enabled = enabled; }
public JdbcEndpoint getSource() { return source; }
public JdbcEndpoint getEvidence() { return evidence; }
public static final class JdbcEndpoint {
private String jdbcUrl;
public String getJdbcUrl() { return jdbcUrl; }
public void setJdbcUrl(String jdbcUrl) { this.jdbcUrl = jdbcUrl; }
}
}

View File

@@ -0,0 +1,32 @@
package com.ruoyi.shadow.config;
import com.ruoyi.shadow.startup.ShadowStartupGate;
import com.ruoyi.shadow.startup.ShadowStartupReport;
import com.ruoyi.shadow.web.ShadowStatusController;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(ShadowProperties.class)
public class ShadowRuntimeConfiguration {
@Bean
public ShadowStartupGate shadowStartupGate() {
return new ShadowStartupGate();
}
@Bean
public ShadowStartupReport shadowStartupReport(
ShadowStartupGate gate, ApplicationContext context,
Environment environment, ShadowProperties properties) {
return gate.verify(context, environment, properties);
}
@Bean
public ShadowStatusController shadowStatusController(
ShadowStartupReport report) {
return new ShadowStatusController(report);
}
}

View File

@@ -0,0 +1,104 @@
package com.ruoyi.shadow.startup;
import com.ruoyi.shadow.config.ShadowProperties;
import com.ruoyi.wcs.port.out.DeviceCommandGateway;
import com.ruoyi.wcs.port.out.WmsNotificationGateway;
import org.springframework.beans.factory.BeanIsNotAFactoryException;
import org.springframework.context.ApplicationContext;
import org.springframework.core.env.Environment;
import java.time.Instant;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.Map;
public final class ShadowStartupGate {
public ShadowStartupReport verify(ApplicationContext context,
Environment environment,
ShadowProperties properties) {
if (!properties.isEnabled()) {
throw failure("SHADOW_MUST_BE_EXPLICITLY_ENABLED");
}
if (!Arrays.asList(environment.getActiveProfiles())
.contains("shadow")) {
throw failure("SHADOW_PROFILE_MUST_BE_ACTIVE");
}
requireText(properties.getSource().getJdbcUrl(),
"SHADOW_SOURCE_JDBC_URL_REQUIRED");
requireText(properties.getEvidence().getJdbcUrl(),
"SHADOW_EVIDENCE_JDBC_URL_REQUIRED");
requireLoopback(environment.getProperty("server.address",
"127.0.0.1"));
requireNoBeans(context, DeviceCommandGateway.class,
"DEVICE_COMMAND_GATEWAY_FORBIDDEN_IN_SHADOW");
requireNoBeans(context, WmsNotificationGateway.class,
"WMS_NOTIFICATION_GATEWAY_FORBIDDEN_IN_SHADOW");
requireNoForbiddenBeanPackage(context);
Map<String, String> checks = new LinkedHashMap<String, String>();
checks.put("SHADOW_ENABLED", "PASS");
checks.put("SHADOW_PROFILE_ACTIVE", "PASS");
checks.put("SOURCE_JDBC_CONFIGURED", "PASS");
checks.put("EVIDENCE_JDBC_CONFIGURED", "PASS");
checks.put("STATUS_ADDRESS_LOOPBACK", "PASS");
checks.put("SIDE_EFFECT_BEANS_ABSENT", "PASS");
checks.put("FORBIDDEN_BEAN_PACKAGES_ABSENT", "PASS");
checks.put("SOURCE_PERMISSIONS_VERIFIED", "PENDING");
checks.put("EVIDENCE_DATABASE_VERIFIED", "PENDING");
return ShadowStartupReport.starting(Instant.now(), checks);
}
private static void requireLoopback(String address) {
String normalized = address == null ? "" : address.trim();
if (!"127.0.0.1".equals(normalized)
&& !"localhost".equalsIgnoreCase(normalized)
&& !"::1".equals(normalized)) {
throw failure("SHADOW_STATUS_ADDRESS_MUST_BE_LOOPBACK");
}
}
private static <T> void requireNoBeans(ApplicationContext context,
Class<T> type,
String reasonCode) {
if (!context.getBeansOfType(type).isEmpty()) {
throw failure(reasonCode);
}
}
private static void requireNoForbiddenBeanPackage(
ApplicationContext context) {
for (String beanName : context.getBeanDefinitionNames()) {
requireAllowedBeanType(context.getType(beanName, false));
requireAllowedBeanType(factoryBeanType(context, beanName));
}
}
private static Class<?> factoryBeanType(ApplicationContext context,
String beanName) {
try {
return context.getType("&" + beanName, false);
} catch (BeanIsNotAFactoryException ignored) {
return null;
}
}
private static void requireAllowedBeanType(Class<?> beanType) {
String typeName = beanType == null ? null : beanType.getName();
if (typeName != null
&& (typeName.startsWith("com.ruoyi.schedule.")
|| typeName.startsWith("com.ruoyi.project.")
|| typeName.startsWith("com.ruoyi.api."))) {
throw failure("FORBIDDEN_PRODUCTION_BEAN_IN_SHADOW:"
+ typeName);
}
}
private static void requireText(String value, String reasonCode) {
if (value == null || value.trim().isEmpty()) {
throw failure(reasonCode);
}
}
private static IllegalStateException failure(String reasonCode) {
return new IllegalStateException(reasonCode);
}
}

View File

@@ -0,0 +1,43 @@
package com.ruoyi.shadow.startup;
import java.time.Instant;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
public final class ShadowStartupReport {
public enum Status { STARTING, READY, DEGRADED, STOPPED }
private final Status status;
private final Instant checkedAt;
private final Instant lastCompletedCycle;
private final Long sourceLagMillis;
private final long evidenceBacklog;
private final Map<String, String> startupChecks;
private ShadowStartupReport(Status status, Instant checkedAt,
Instant lastCompletedCycle,
Long sourceLagMillis, long evidenceBacklog,
Map<String, String> startupChecks) {
this.status = status;
this.checkedAt = checkedAt;
this.lastCompletedCycle = lastCompletedCycle;
this.sourceLagMillis = sourceLagMillis;
this.evidenceBacklog = evidenceBacklog;
this.startupChecks = Collections.unmodifiableMap(
new LinkedHashMap<String, String>(startupChecks));
}
public static ShadowStartupReport starting(Instant checkedAt,
Map<String, String> checks) {
return new ShadowStartupReport(Status.STARTING, checkedAt, null, null,
0L, checks);
}
public Status getStatus() { return status; }
public Instant getCheckedAt() { return checkedAt; }
public Instant getLastCompletedCycle() { return lastCompletedCycle; }
public Long getSourceLagMillis() { return sourceLagMillis; }
public long getEvidenceBacklog() { return evidenceBacklog; }
public Map<String, String> getStartupChecks() { return startupChecks; }
}

View File

@@ -0,0 +1,21 @@
package com.ruoyi.shadow.web;
import com.ruoyi.shadow.startup.ShadowStartupReport;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/shadow")
public final class ShadowStatusController {
private final ShadowStartupReport report;
public ShadowStatusController(ShadowStartupReport report) {
this.report = report;
}
@GetMapping("/status")
public ShadowStartupReport status() {
return report;
}
}

View File

@@ -0,0 +1,12 @@
server:
address: 127.0.0.1
port: 18091
spring:
main:
banner-mode: "off"
shadow:
enabled: false
source:
jdbc-url:
evidence:
jdbc-url:

View File

@@ -0,0 +1,20 @@
package com.ruoyi.project.shadowtest;
import org.springframework.beans.factory.FactoryBean;
public final class ForbiddenFactoryBean implements FactoryBean<Object> {
@Override
public Object getObject() {
throw new AssertionError("forbidden factory must never create a bean");
}
@Override
public Class<?> getObjectType() {
return null;
}
@Override
public boolean isSingleton() {
return true;
}
}

View File

@@ -0,0 +1,200 @@
package com.ruoyi.shadow;
import com.ruoyi.project.shadowtest.ForbiddenFactoryBean;
import com.ruoyi.shadow.startup.ShadowStartupReport;
import com.ruoyi.shadow.web.ShadowStatusController;
import com.ruoyi.wcs.application.dispatch.DispatchCoordinator;
import com.ruoyi.wcs.port.out.DeviceCommandGateway;
import com.ruoyi.wcs.port.out.WmsNotificationGateway;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.ConfigDataApplicationContextInitializer;
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
class Ft546ShadowApplicationTest {
private final WebApplicationContextRunner contextRunner =
new WebApplicationContextRunner()
.withInitializer(
new ConfigDataApplicationContextInitializer())
.withUserConfiguration(Ft546ShadowApplication.class);
@Test
void explicitReadOnlyShadowConfigurationStartsTheNarrowContext() {
configured(contextRunner).run(context -> {
assertFalse(context.getStartupFailure() != null);
assertEquals("127.0.0.1",
context.getEnvironment().getProperty("server.address"));
assertEquals("18091",
context.getEnvironment().getProperty("server.port"));
assertEquals(ShadowStartupReport.Status.STARTING,
context.getBean(ShadowStartupReport.class).getStatus());
assertNotNull(context.getBean(ShadowStartupReport.class)
.getCheckedAt());
assertNull(context.getBean(ShadowStartupReport.class)
.getLastCompletedCycle());
assertNull(context.getBean(ShadowStartupReport.class)
.getSourceLagMillis());
assertEquals(0L, context.getBean(ShadowStartupReport.class)
.getEvidenceBacklog());
assertEquals("PASS", context.getBean(ShadowStartupReport.class)
.getStartupChecks().get("SHADOW_ENABLED"));
assertEquals("PASS", context.getBean(ShadowStartupReport.class)
.getStartupChecks().get("SIDE_EFFECT_BEANS_ABSENT"));
assertEquals("PENDING", context.getBean(ShadowStartupReport.class)
.getStartupChecks().get("SOURCE_PERMISSIONS_VERIFIED"));
assertEquals("PENDING", context.getBean(ShadowStartupReport.class)
.getStartupChecks().get("EVIDENCE_DATABASE_VERIFIED"));
assertNotNull(context.getBean(DispatchCoordinator.class));
assertNotNull(context.getBean(ShadowStatusController.class));
assertTrue(context.getBeansOfType(
DeviceCommandGateway.class).isEmpty());
assertTrue(context.getBeansOfType(
WmsNotificationGateway.class).isEmpty());
});
}
@Test
void statusEndpointReturnsTheReadOnlyStartupState() {
configured(contextRunner).run(context -> {
MockMvc mvc = MockMvcBuilders.webAppContextSetup(context).build();
mvc.perform(get("/shadow/status"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.status").value("STARTING"))
.andExpect(jsonPath("$.checkedAt").exists())
.andExpect(jsonPath("$.evidenceBacklog").value(0))
.andExpect(jsonPath("$.startupChecks.SHADOW_ENABLED")
.value("PASS"))
.andExpect(jsonPath(
"$.startupChecks.SIDE_EFFECT_BEANS_ABSENT")
.value("PASS"));
});
}
@Test
void startupFailsClosedUnlessEnabledProfileAndBothJdbcUrlsAreExplicit() {
assertStartupFailure(contextRunner.withPropertyValues(
"spring.profiles.active=shadow",
"shadow.source.jdbc-url=jdbc:h2:mem:source",
"shadow.evidence.jdbc-url=jdbc:h2:mem:evidence"),
"SHADOW_MUST_BE_EXPLICITLY_ENABLED");
assertStartupFailure(contextRunner.withPropertyValues(
"shadow.enabled=true",
"shadow.source.jdbc-url=jdbc:h2:mem:source",
"shadow.evidence.jdbc-url=jdbc:h2:mem:evidence"),
"SHADOW_PROFILE_MUST_BE_ACTIVE");
assertStartupFailure(contextRunner.withPropertyValues(
"spring.profiles.active=shadow",
"shadow.enabled=true",
"shadow.evidence.jdbc-url=jdbc:h2:mem:evidence"),
"SHADOW_SOURCE_JDBC_URL_REQUIRED");
assertStartupFailure(contextRunner.withPropertyValues(
"spring.profiles.active=shadow",
"shadow.enabled=true",
"shadow.source.jdbc-url=jdbc:h2:mem:source"),
"SHADOW_EVIDENCE_JDBC_URL_REQUIRED");
assertStartupFailure(contextRunner.withPropertyValues(
"spring.profiles.active=shadow",
"shadow.enabled=true",
"shadow.source.jdbc-url=jdbc:h2:mem:source",
"shadow.evidence.jdbc-url=jdbc:h2:mem:evidence",
"server.address=0.0.0.0"),
"SHADOW_STATUS_ADDRESS_MUST_BE_LOOPBACK");
}
@Test
void startupRejectsAnyRealCommandGatewayBean() {
assertStartupFailure(configured(contextRunner)
.withUserConfiguration(
ForbiddenGatewayConfiguration.class),
"DEVICE_COMMAND_GATEWAY_FORBIDDEN_IN_SHADOW");
}
@Test
void startupRejectsAnyRealWmsGatewayBean() {
assertStartupFailure(configured(contextRunner)
.withUserConfiguration(
ForbiddenWmsGatewayConfiguration.class),
"WMS_NOTIFICATION_GATEWAY_FORBIDDEN_IN_SHADOW");
}
@Test
void startupRejectsAForbiddenFactoryBeanWithUnknownProductType() {
assertStartupFailure(configured(contextRunner)
.withUserConfiguration(
ForbiddenFactoryBeanConfiguration.class),
"FORBIDDEN_PRODUCTION_BEAN_IN_SHADOW:"
+ ForbiddenFactoryBean.class.getName());
}
private static WebApplicationContextRunner configured(
WebApplicationContextRunner runner) {
return runner.withPropertyValues(
"spring.profiles.active=shadow",
"shadow.enabled=true",
"shadow.source.jdbc-url=jdbc:h2:mem:source",
"shadow.evidence.jdbc-url=jdbc:h2:mem:evidence");
}
private static void assertStartupFailure(
WebApplicationContextRunner runner, String reasonCode) {
runner.run(context -> {
Throwable failure = context.getStartupFailure();
assertNotNull(failure);
assertTrue(messages(failure).contains(reasonCode),
messages(failure));
});
}
private static String messages(Throwable failure) {
StringBuilder messages = new StringBuilder();
Throwable current = failure;
while (current != null) {
if (current.getMessage() != null) {
messages.append(current.getMessage()).append('\n');
}
current = current.getCause();
}
return messages.toString();
}
@Configuration(proxyBeanMethods = false)
static class ForbiddenGatewayConfiguration {
@Bean
DeviceCommandGateway forbiddenDeviceCommandGateway() {
return envelope -> {
throw new AssertionError("must never send in shadow test");
};
}
}
@Configuration(proxyBeanMethods = false)
static class ForbiddenWmsGatewayConfiguration {
@Bean
WmsNotificationGateway forbiddenWmsNotificationGateway() {
return notification -> {
throw new AssertionError("must never notify in shadow test");
};
}
}
@Configuration(proxyBeanMethods = false)
static class ForbiddenFactoryBeanConfiguration {
@Bean
ForbiddenFactoryBean forbiddenFactoryBean() {
return new ForbiddenFactoryBean();
}
}
}

View File

@@ -0,0 +1,50 @@
package com.ruoyi.shadow;
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.lang.ArchRule;
import org.junit.jupiter.api.Test;
import org.springframework.util.ClassUtils;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;
import static org.junit.jupiter.api.Assertions.assertFalse;
class ShadowDependencyBoundaryTest {
@Test
void shadowCodeCannotDependOnAdminPlcOrLegacySchedulerPackages() {
JavaClasses shadowClasses = new ClassFileImporter()
.importUrl(Ft546ShadowApplication.class.getProtectionDomain()
.getCodeSource().getLocation());
ArchRule rule = noClasses()
.that().resideInAPackage("com.ruoyi.shadow..")
.should().dependOnClassesThat().resideInAnyPackage(
"com.ruoyi.project..",
"com.ruoyi.schedule..",
"com.ruoyi.api..",
"HslCommunication..",
"org.apache.plc4x..");
rule.check(shadowClasses);
}
@Test
void productionSideEffectLibrariesAreAbsentFromTheShadowClasspath() {
ClassLoader loader = getClass().getClassLoader();
assertFalse(ClassUtils.isPresent(
"com.ruoyi.schedule.ScheduleThread", loader));
assertFalse(ClassUtils.isPresent(
"HslCommunication.Profinet.Siemens.SiemensS7Net", loader));
assertFalse(ClassUtils.isPresent(
"org.apache.plc4x.java.api.PlcDriverManager", loader));
assertFalse(ClassUtils.isPresent(
"org.springframework.data.redis.connection.RedisConnectionFactory",
loader));
assertFalse(ClassUtils.isPresent(
"org.springframework.security.web.SecurityFilterChain", loader));
assertFalse(ClassUtils.isPresent(
"org.mybatis.spring.SqlSessionFactoryBean", loader));
assertFalse(ClassUtils.isPresent(
"cn.hutool.extra.spring.SpringUtil", loader));
}
}

View File

@@ -0,0 +1,9 @@
spring:
profiles:
active: shadow
shadow:
enabled: true
source:
jdbc-url: jdbc:h2:mem:source
evidence:
jdbc-url: jdbc:h2:mem:evidence