Boot 配置与运行参数实验(U23)
独立新增工程:Liberica JDK21.0.11+11、Boot4.0.9 / Framework7.0.10、Jackson3.1.7、Tomcat11.0.27、HikariCP7.0.2、Connector/J9.7.0。本机macOS27.0.1 arm64;依赖由4.0.9 parent/BOM管理,公开入口真实dependency:tree留在日志。旧Boot3.3.3 / Framework6.1.12机制实验保留在其他工程,不与本次执行混拼。
从仓库根运行完整公开入口:
JAVA_HOME=$(/usr/libexec/java_home -v21) python3 labs/boot-runtime/run.py需要Python3、Maven3.9.9、本机JDK21、keytool,以及本机127.0.0.1:3306可用的MySQL(本次8.4.7,root空密码)。数据库仅执行SELECT 1与连接借用,不建库、不建表、不改变用户服务。MySQL凭据不同则该组如实失败,不猜其他凭据。Hikari实验是客户端池等待,不证明数据库锁/事务/恢复。
入口为所有Maven/Java/keytool子进程显式设置同一JAVA_HOME/PATH并核对主版本21。每个命令180秒上限;服务使用loopback/port0,启动到真实ready后发请求;finally定向终止、wait自有进程,必要强杀会判失败。证书和外部配置只在独有临时目录;不改系统信任库、浏览器、数据库拓扑或其他服务。原始日志在 ~/.cache/software-engineering-study/java-depth-impl/u23/runtime/,短产物为evidence。
EX1:最终值、来源、失败与恢复
包内base和prod Profile是实际编译资源,完整配置对象为 NoticeProperties.java:
| 字段 | 类型/约束/作用 |
|---|---|
lab.notice.message | String / NotBlank;多来源冲突目标,只输出合成白名单值 |
token | String / NotBlank;默认合成值不输出;缺失与显式空白分开验证 |
timeout / limit | Duration / NotNull,默认250ms;DataSize显式默认1KB,输出毫秒/字节 |
workers / nested.region | Min(1) / 嵌套Valid+NotBlank,分别触发约束失败 |
labels / routes | List / Map;Profile列表整体替换,Map逐key合并 |
client-id | String;真实LAB_NOTICE_CLIENTID环境变量验证relaxed命名 |
每个新进程从真实文件/OS环境/JVM系统属性/SPRING_APPLICATION_JSON/命令行读取,不用MapPropertySource冒充环境。RESULT同时输出绑定对象白名单值与ConfigurationPropertySources查得的有效value/source/origin,逐场景检查具体来源;不dump全部Environment或token。
| 输入场景 | 实测message与来源 |
|---|---|
| 包内base / prod | base / profile,分别命中application.properties/application-prod.properties及行号 |
| additional-location外部base / prod | external / external-profile,命中独有外部文件;原包内其他必填配置仍可用 |
| 真实环境变量 / JVM -D | env / system,命中systemEnvironment / systemProperties |
| 再叠SPRING_APPLICATION_JSON / CLI | json / cli,命中spring.application.json / commandLineArgs;CLI组同时携带全部低优先级冲突源 |
| 移除高优先级覆盖 | 恢复external-profile;文件修改不等于自动刷新已创建对象,此处每组重新启动 |
| spring.config.location完整替换文件 | replacement;包内labels/routes消失,timeout=750ms,limit默认1024B |
Profile的labels为[profile-only],routes仍保留base并以profile覆盖shared、增加profile。必需location缺失在environment-event-before-postprocessors→failed阶段,无prepared。监听器明确Order.HIGHEST_PRECEDENCE,故标签表示早于环境后处理的事件观察,不把该观察扩大为通用“事件完全未发”。
类型/约束失败在prepared后、started/runner/ready前:
- 替代配置真正未提供token→BindValidationException,目标属性lab.notice.token、拒绝值null;与
--lab.notice.token=空白覆盖分组记录。 timeout=abc→ConversionFailedException;workers=0、嵌套region空白→BindValidationException。- 同一缺失位置加optional恢复;绑定改合法值恢复。optional只豁免位置不存在,不豁免业务必填约束。
运行矩阵(真实组件)
| 场景 | 实际断言与观察 |
|---|---|
| 平台executor | core=max1、queue1:首任务闩锁阻塞,第二排队,第三真实拒绝,副作用2;只提高max4但默认无界队列时poolSize1/queued2/副作用3 |
| 虚拟executor | SimpleAsyncTaskExecutor,workerVirtual=true;simple.concurrency-limit=1并显式reject,额外任务拒绝;pool.core9不是此分支并发上限 |
| 用户与force | 用户Executor使applicationTaskExecutor退让;mode=force下保留用户Bean并创建应用执行器 |
| scheduler | 默认ThreadPoolTaskScheduler池1;显式spring.task.scheduling.pool.size=3后实际底层core3;虚拟分支为SimpleAsyncTaskScheduler |
| cron配置 | cron+initialDelay在真实上下文初始化期失败;关闭该错误配置后恢复。fixedRate追赶与旧Framework6.1.12中断细节仍由固定源码/既有独立实验解释,不冒充本工程新实测 |
| Hikari+MySQL | 最大池1、minimumIdle0、connectionTimeout300ms;SELECT1成功,持有唯一连接时第二借用SQLTransientConnectionException(最终312ms),释放后恢复SELECT1 |
| Tomcat | 真实connector maxThreads4/minSpare1/maxConnections16/acceptCount2/connectionTimeout2000ms;这是不同等待与资源对象,未测生产吞吐 |
| HTTP JSON | 实际mapper tools.jackson.databind.json.JsonMapper/3.1.7;unknown字段严格模式400,合法输入200;non_null排除optional字段,默认宽松模式接受unknown且保留null |
| 日志JSON | Logstash格式的真实日志行可JSON解析,指定logger提高到ERROR后INFO探针消失;无token泄露。与HTTP JSON、SPRING_APPLICATION_JSON配置源分别验证 |
| multipart | 128B文件200;2048B文件对1KB文件上限精确413且日志有MaxUploadSizeExceededException,controller总调用仍1;未触发/未宣称连接重置 |
| ERROR分派 | sendError500触发REQUEST /lab/error,再ERROR /error original=/lab/error;由真实容器与FilterRegistrationBean观察 |
| TLS | 自有SAN为localhost/127.0.0.1的自签名服务端证书(PKCS12与导出PEM,客户端将其作为本地信任锚);默认不信任失败、显式信任导出的自签名服务端证书后成功200、错误主机名失败;错keystore密码启动失败,修复密码后再次成功;没有-k成功路径 |
| SIGTERM正常 | 确认1200ms慢请求已入handler,4s phase预算下完整200/业务完成;开始关闭后新连接被拒绝,进程143,最终stop1305ms |
| SIGTERM超预算 | 4500ms在途请求、300ms phase预算,观察Graceful shutdown aborted;最终stop406ms,请求RemoteDisconnected、handler未完成;不是持久业务回滚保证 |
实际数值以evidence每条观察为准,时长是本地有界夹具结果,不是性能基准。phase预算不直接等于进程全阶段总停机上限;HTTP关闭不自动证明任意后台任务/外部流量摘除已完成。
只重跑受影响的组
完整入口默认重建并跑全部。已有固定归档且只强化断言时,可按组验证;--skip-build要求归档已存在,源码/POM变化后不得使用。
JAVA_HOME=$(/usr/libexec/java_home -v21) python3 labs/boot-runtime/run.py --group config --skip-build
JAVA_HOME=$(/usr/libexec/java_home -v21) python3 labs/boot-runtime/run.py --group multipart --skip-build
JAVA_HOME=$(/usr/libexec/java_home -v21) python3 labs/boot-runtime/run.py --group scheduler --skip-build其他组为tasks/http/tls/shutdown。分组用于开发期受影响复验:合并结果时保留未变更记录的原调用完成时间与来源,不把旧记录冒充再次运行。日志SHA256明确在证据采集时计算,不能反称为运行当时已采集。开发期全量/分组原始记录备份在同一cache的u23/history;本次交付evidence最终已用默认入口完整重跑(2026-10-26 09:36:07–09:36:39 +08:00),48条记录属于同一Invocation,含实际起止/观察时间、脱敏命令、归档与runner哈希,不将合并记录伪装成一次运行。
完整文件
公开入口与配置:run.py、pom.xml、application.properties、application-prod.properties。Java 实现:RuntimeLab、NoticeProperties、TaskProbe、DatabaseProbe、ServerProbe。文件由站点按原路径发布;使用仓库或下载时保留目录关系。
单一实现
配置绑定、来源观测与失败阶段:
package study.runtime;
import java.time.Duration;
import java.util.List;
import java.util.Map;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.bind.DefaultValue;
import org.springframework.util.unit.DataSize;
import org.springframework.validation.annotation.Validated;
@ConfigurationProperties("lab.notice")
@Validated
public record NoticeProperties(@NotBlank String message, @NotBlank String token,
@NotNull Duration timeout, @Min(1) int workers, @NotNull @Valid Nested nested,
@DefaultValue("1KB") DataSize limit, List<String> labels, Map<String,String> routes,
@DefaultValue("unset") String clientId) {
public record Nested(@NotBlank String region) {}
}package study.runtime;
import java.util.*;
import org.slf4j.LoggerFactory;
import org.springframework.boot.*;
import org.springframework.boot.autoconfigure.*;
import org.springframework.boot.context.event.*;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.context.properties.source.*;
import org.springframework.context.*;
import org.springframework.context.event.ContextClosedEvent;
import org.springframework.core.Ordered;
import org.springframework.core.env.*;
import tools.jackson.databind.json.JsonMapper;
@SpringBootApplication
@EnableConfigurationProperties(NoticeProperties.class)
@org.springframework.scheduling.annotation.EnableScheduling
public class RuntimeLab {
static final List<String> events=new ArrayList<>();
static class Probe implements ApplicationListener<ApplicationEvent>,Ordered {
public int getOrder(){return Ordered.HIGHEST_PRECEDENCE;}
public void onApplicationEvent(ApplicationEvent event){
String phase=null;
if(event instanceof ApplicationEnvironmentPreparedEvent)phase="environment-event-before-postprocessors";
if(event instanceof ApplicationPreparedEvent)phase="prepared";
if(event instanceof ApplicationStartedEvent)phase="started";
if(event instanceof ApplicationReadyEvent)phase="ready";
if(event instanceof ApplicationFailedEvent)phase="failed";
if(event instanceof ContextClosedEvent)phase="closed";
if(phase!=null){events.add(phase);System.out.println("EVENT "+phase);}
}
}
@org.springframework.context.annotation.Bean
ApplicationRunner stageRunner(){return args -> {events.add("runner");System.out.println("EVENT runner");};}
public static void main(String[] args) throws Exception{
events.clear();var app=new SpringApplication(RuntimeLab.class);
boolean server=Arrays.asList(args).contains("--lab.mode=server");
app.setWebApplicationType(server?WebApplicationType.SERVLET:WebApplicationType.NONE);
app.addListeners(new Probe());
ConfigurableApplicationContext context=null;
try{
context=app.run(args);
String mode=context.getEnvironment().getProperty("lab.mode","config");
if(mode.equals("server"))return; // shutdown hook owns a live server; runner harness sends SIGTERM.
Object result=switch(mode){
case "config" -> snapshot(context);
case "tasks" -> TaskProbe.run(context);
case "db" -> DatabaseProbe.run(context);
default -> throw new IllegalArgumentException("unknown mode: "+mode);
};
System.out.println("RESULT "+new JsonMapper().writeValueAsString(result));
}catch(RuntimeException error){
var causes=new ArrayList<String>();for(Throwable t=error;t!=null;t=t.getCause())causes.add(t.getClass().getName());
System.out.println("CAUSES "+causes);System.out.println("EVENTS "+events);throw error;
}finally{
if(context!=null&&!server)context.close();
}
System.out.println("EVENTS "+events);
}
static Map<String,Object> snapshot(ConfigurableApplicationContext context){
var p=context.getBean(NoticeProperties.class);var env=context.getEnvironment();
var effective=new LinkedHashMap<String,Object>();
for(String key:List.of("lab.notice.message","lab.notice.timeout","lab.notice.client-id")){
var name=ConfigurationPropertyName.of(key);
for(var source:ConfigurationPropertySources.get(env)){
var property=source.getConfigurationProperty(name);if(property==null)continue;
Object underlying=source.getUnderlyingSource();String sourceName=underlying instanceof PropertySource<?> ps?ps.getName():String.valueOf(underlying);
effective.put(key,Map.of("value",String.valueOf(property.getValue()),"source",sourceName,"origin",String.valueOf(property.getOrigin())));break;
}
}
var bound=new LinkedHashMap<String,Object>();bound.put("message",p.message());bound.put("timeoutMillis",p.timeout().toMillis());bound.put("workers",p.workers());bound.put("region",p.nested().region());bound.put("limitBytes",p.limit().toBytes());bound.put("labels",p.labels());bound.put("routes",p.routes());bound.put("clientId",p.clientId());
LoggerFactory.getLogger(RuntimeLab.class).info("U23_LOG_PROBE synthetic public message={}",p.message());
return Map.of("bound",bound,"effective",effective,"profiles",env.getActiveProfiles(),"mapper",context.getBean(JsonMapper.class).getClass().getName());
}
}任务和真实池验证:TaskProbe.java、DatabaseProbe.java;HTTP/TLS/在途处理:ServerProbe.java;完整入口与断言:run.py。正文直接引用这些实际编译文件,不复制第二套实现。
未运行:Jetty/Netty/外置容器、生产证书轮换/K8s摘流、生产容量/长期负载、HTTP连接重置分支、持久任务崩溃恢复、MySQL事务/锁。它们不由本票loopback测试结果抵扣。