neuron-explorer
Qwen3.5‑4B 전용 조향형 뉴런 탐색기, 모든 MLP 뉴런을 밀어 결과를 보관·탐색
docuconf는 앱이 선언한 설정을 타입 계약으로 바꾸어 쿠버네티스가 배포 전 검증하고, 앱도 부팅 시 재검증합니다
docuconf turns the config your app already declares into a typed contract that your Kubernetes platform checks before anything deploys, and your app checks again at boot.
개발 도구Java · CUEGitHub @Docuconf온라인운영자 미확인
써 보셨어요? 한 번만 눌러 주세요. 로그인은 필요 없어요.
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기docuconf for Java, on Spring Boot configuration properties.
저장소 README에서 가져옴
README 전체를 불러오는 중입니다.
README 전문 보기 ↗Documentation: docuconf.dev · Java guide
Typed configuration contracts for Spring Boot @ConfigurationProperties. Your properties classes, with the Bean
Validation annotations you already use, become a CUE contract that your Kubernetes platform checks before
deploy, and that your app checks again at startup. It covers environment variables, the
application*.yml files in your jar, and file inputs: TLS key pairs, CA bundles, keystores, structured config
files, licence files and binary data.
Part of docuconf. See the specification.
Status:
0.1.0-SNAPSHOT. Nothing is on Maven Central yet, so you build the SDK from source first (step 1). The contract format is a draft (v1alpha1) and the API may change. The groupIddev.docuconfassumes thedocuconf.devdomain, which is not yet verified on Maven Central (see RELEASING.md).
The steps below follow examples/orders/, a small Spring Boot web service that is set up
exactly as this README says. CI builds it, runs these commands and compiles every snippet on this page.
| Artifact | What it is |
|---|---|
dev.docuconf:docuconf-bom | One version for all of the below. |
dev.docuconf:docuconf-spring | Spring Boot auto-configuration: validates everything at startup. Spring Boot 3.5+ and 4.x, Java 17+. |
dev.docuconf:docuconf-processor | Annotation processor: writes META-INF/docuconf/contract.cue at compile time. |
dev.docuconf:docuconf-maven-plugin | Keeps the contract in step with application.yml, exports it and checks it in CI. |
dev.docuconf:docuconf-core | Annotations, file handles (TlsKeyPair, CaBundle, Keystore), contract model, contract-first mode. JDK only. |
Build the SDK and install 0.1.0-SNAPSHOT into your local Maven repository (~/.m2), with Java 17+ and Maven:
git clone https://github.com/docuconf/docuconf-java.git
cd docuconf-java
mvn install -DskipTests
Once 0.1.0 is on Maven Central, skip this step and use version 0.1.0 below.
Import the BOM, then add the runtime and Bean Validation:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>dev.docuconf</groupId>
<artifactId>docuconf-bom</artifactId>
<version>0.1.0-SNAPSHOT</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependency>
<groupId>dev.docuconf</groupId>
<artifactId>docuconf-spring</artifactId>
</dependency>
<!-- Bean Validation at runtime -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
Add the annotation processor. Listing annotationProcessorPaths turns off javac's discovery of processors on the
class path, so keep every processor you use in the list (Spring's configuration processor, which gives your IDE
application.yml completion, and Lombok, shown here); on JDK 23 and later this list is how processors run anyway:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<!-- Listing annotationProcessorPaths turns off processor discovery on the class path, so list every
processor you use. With annotationProcessorPathsUseDepMgmt, the versions come from your dependency
management (Spring Boot's and docuconf-bom). -->
<annotationProcessorPathsUseDepMgmt>true</annotationProcessorPathsUseDepMgmt>
<annotationProcessorPaths>
<path>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
</path>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</path>
<path>
<groupId>dev.docuconf</groupId>
<artifactId>docuconf-processor</artifactId>
</path>
</annotationProcessorPaths>
<compilerArgs>
<arg>-Adocuconf.appVersion=${project.version}</arg>
</compilerArgs>
</configuration>
</plugin>
And the Maven plugin, which keeps the contract up to date when only application.yml changes and checks the
committed copy in mvn verify (see step 6):
<plugin>
<groupId>dev.docuconf</groupId>
<artifactId>docuconf-maven-plugin</artifactId>
<version>0.1.0-SNAPSHOT</version>
<executions>
<execution>
<goals>
<!-- recompile when only application*.yml changed, so the contract never goes stale -->
<goal>refresh</goal>
<!-- mvn verify fails when the committed contract.cue differs from the exported one -->
<goal>check</goal>
</goals>
</execution>
</executions>
</plugin>
mavenLocal() finds the SDK you installed in step 1:
repositories {
mavenLocal()
mavenCentral()
}
dependencies {
implementation(platform("dev.docuconf:docuconf-bom:0.1.0-SNAPSHOT"))
implementation(platform("org.springframework.boot:spring-boot-dependencies:$springBootVersion"))
implementation("dev.docuconf:docuconf-spring")
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-validation")
annotationProcessor(platform("dev.docuconf:docuconf-bom:0.1.0-SNAPSHOT"))
annotationProcessor(platform("org.springframework.boot:spring-boot-dependencies:$springBootVersion"))
annotationProcessor("dev.docuconf:docuconf-processor")
annotationProcessor("org.springframework.boot:spring-boot-configuration-processor")
}
With the Spring Boot Gradle plugin, its dependency management replaces the two platform(...) lines for Spring
Boot. Gradle has no discovery to turn off: list Lombok in annotationProcessor as usual. Add the contract tasks
from step 6 too.
Ordinary Spring Boot configuration properties, marked @Docuconf. This is
OrdersProperties:
/**
* Settings of the orders service. Each property is the environment variable Spring binds to it: {@code port} is
* {@code ORDERS_PORT}, {@code databaseUrl} is {@code ORDERS_DATABASEURL}. The first sentence of each property's
* Javadoc is the contract's description, and the rest is its details, longer docs for {@code docuconf docs}.
*
* @param port HTTP listen port
* @param logLevel Minimum level of the log lines the service writes
* @param databaseUrl Postgres connection URL for the orders database
* @param allowedOrigins Origins allowed to call the API from a browser
* @param requestTimeout Time allowed to answer one request
* @param workerCount Background workers that process new orders
* <p>Each worker holds one connection from the pool of {@code databaseUrl}, so keep this below the
* database's connection limit.
* <ul>
* <li>Raise it when the order queue backs up.</li>
* <li>Lower it when the database is the bottleneck.</li>
* </ul>
*/
@Docuconf(service = "orders", enumCase = EnumCase.LOWER)
@Validated
@ConfigurationProperties("orders")
public record OrdersProperties(
@Min(1) @Max(65535) @DefaultValue("8080") int port,
@DefaultValue("INFO") LogLevel logLevel,
// @Secret: the platform must supply it from a Secret, and docuconf never prints it. @MaxLength bounds the
// URL in characters; a longer one fails startup with out_of_range.
@NotNull @Secret @UrlSchemes("postgres") @MaxLength(2048) URI databaseUrl,
@NotEmpty @DefaultValue("http://localhost:3000") List<String> allowedOrigins,
@DurationMin(seconds = 1) @DurationMax(minutes = 5) @DefaultValue("30s") Duration requestTimeout,
@Min(1) @Max(64) @DefaultValue("4") int workerCount) {
/** Log levels. The contract spells them in lower case (enumCase); the app accepts any case, as Spring does. */
public enum LogLevel { DEBUG, INFO, WARN, ERROR }
/** Prints the secret as [redacted]; a record's generated toString() would print it. */
@Override
public String toString() {
return Redacted.toString(this);
}
}
@param, the field, or the getter): its first sentence, as Spring's
configuration metadata takes it. @Description("...") overrides it. Every input needs one of at least five
characters. The rest of the Javadoc is the input's details (see Descriptions and details).@DefaultValue, field initializers (JavaBeans work too) and application.yml.@Secret values never print: a record holding one must override toString() as above (the build tells you), and
Lombok classes must mark the field @ToString.Exclude.@UrlSchemes on an int), a @Secret with a default.Each @Docuconf class of the service goes into the same contract, and its prefix names its variables. The
example's WebhookProperties
is WEBHOOK_KEYS, a key set: one or two keys of 32 to 256 characters each, all valid at once, which lets a key
be rotated without downtime (SPEC section 6.1).
Declare it as a KeySet, which exports as type: "keySet" and is always secret:
@Docuconf(service = "orders")
@Validated
@ConfigurationProperties("webhook")
public record WebhookProperties(
// A KeySet is always secret: from a Secret only, never printed, not even by this record's toString().
// 1 or 2 keys (the default), so one can be rotated; each key 32 to 256 characters, so an empty or
// truncated key fails startup with out_of_range.
@KeySetLimits(keyMinLength = 32, keyMaxLength = 256) KeySet keys) {
}
Verify with the helper, which runs your check against every key without stopping at the first match, so the time
taken does not say which key matched
(Webhooks):
// Every key is tried; MessageDigest.isEqual compares in constant time.
return keys.anyMatch(key -> MessageDigest.isEqual(hmac(key, body), got));
For an inbound API key, contains is a constant-time lookup:
boolean known = apiKeys.contains(header);
keys() lists the keys in the order the platform gave them (old,new during a rotation). A KeySet prints as
KeySet[2 keys, [redacted]], so a record holding one needs no toString() override. The number of keys outside
minKeys..maxKeys (default 1..2) is too_few_items or too_many_items; a key outside
keyMinLength..keyMaxLength, or an empty key (a stray comma), is out_of_range. Keys are never trimmed. The
generated docs print the rotation steps for every key set.
mvn package
ORDERS_DATABASEURL='postgres://orders:secret@localhost:5432/orders' java -jar target/orders.jar
Nothing to call: the auto-configuration checks every contract on the class path before any
@ConfigurationProperties bean is bound, reading values through Spring's own Binder, so relaxed names, profiles
and application.yml apply exactly as for your beans. A variable that is set but not declared, and within two
edits of one that is, gets a warning: docuconf: ORDERS_PROT is set but not declared; did you mean ORDERS_PORT?.
ORDERS_PORT=0 java -jar target/orders.jar
***************************
APPLICATION FAILED TO START
***************************
Description:
docuconf: 2 configuration problems:
[missing_required] ORDERS_DATABASEURL: is required (orders.database-url)
[out_of_range] ORDERS_PORT: is below min 1 (got 0)
Action:
Set or fix the environment variables and files listed above. Codes are defined in the docuconf spec (section 11.2). For local runs, DOCUCONF_FILE_ROOT=./dev reads file inputs from ./dev; docuconf.enabled=false skips the check (for tests and build-time tasks).
Every problem at once, each with its SPEC section 11.2 code, and exit status 1, without a stack trace. Secret
values never appear, not even a wrongly supplied URL's credentials. The same lines go to /dev/termination-log (or
$DOCUCONF_TERMINATION_LOG), so kubectl describe pod shows them.
DocuconfTester runs the startup check against an environment map: it never reads or changes the process
environment and starts no threads, and Spring Boot loads application*.yml and profiles as at startup. From the
example's OrdersConfigTest:
class OrdersConfigTest {
private static final String DB = "postgres://orders:[email protected]:5432/orders";
@Test
void aValidEnvironmentPasses() {
var result = DocuconfTester.env(Map.of("ORDERS_DATABASEURL", DB, "ORDERS_LOGLEVEL", "warn")).check();
assertTrue(result.ok(), result.violations().toString());
}
@Test
void everyProblemIsReported() {
var result = DocuconfTester.env(Map.of("ORDERS_PORT", "0", "ORDERS_REQUESTTIMEOUT", "1m30s")).check();
assertEquals(List.of(
"invalid_type ORDERS_REQUESTTIMEOUT",
"missing_required ORDERS_DATABASEURL",
"out_of_range ORDERS_PORT"), result.codes());
}
@Test
void aTypoGetsAHint() {
var result = DocuconfTester.env(Map.of("ORDERS_DATABASEURL", DB, "ORDERS_PROT", "9090")).check();
assertEquals(List.of("ORDERS_PROT is set but not declared; did you mean ORDERS_PORT?"), result.warnings());
}
}
.fileRoot(dir) reads file inputs under a test directory, .properties(Map.of("spring.profiles.active", "prod"))
selects a profile. For ApplicationContextRunner and slice tests, DocuconfTestEnvironment.of(map) is an
initializer that swaps in the map as the environment variables. To switch the check off in a @SpringBootTest,
set docuconf.enabled=false.
The processor writes META-INF/docuconf/contract.cue on every compile. Commit a copy next to your build file;
the platform deploys against it.
mvn docuconf:export
mvn verify
docuconf:export compiles and copies the contract to contract.cue. In mvn verify, the check goal fails when
the committed file differs from the exported one and names the first different line, so CI needs nothing else. It
ignores only metadata.generator.version (the SDK version), so upgrading the SDK does not by itself make the file
stale. The
refresh goal recompiles when only application*.yml changed, which javac alone would not notice.
With Gradle, add these tasks; ./gradlew docuconfExport writes contract.cue, and ./gradlew check fails when it
is out of date:
// application*.yml feeds the contract: make it an input of compileJava, and have the processor read it from the
// sources rather than from build/resources, which compileJava may see before processResources refreshes it.
val springResources = sourceSets.main.get().resources.sourceDirectories.filter { it.isDirectory }
tasks.compileJava {
inputs.files(springResources.asFileTree.matching { include("application*.*", "config/application*.*") })
.withPropertyName("docuconfSpringFiles").withPathSensitivity(PathSensitivity.RELATIVE)
options.compilerArgs.add("-Adocuconf.resources=${springResources.first()}")
}
val exportedContract = tasks.compileJava.flatMap { it.destinationDirectory.file("META-INF/docuconf/contract.cue") }
// ./gradlew docuconfExport writes contract.cue next to the build file, to commit.
val docuconfExport by tasks.registering {
val exported = exportedContract
val committed = layout.projectDirectory.file("contract.cue")
inputs.file(exported)
outputs.file(committed)
doLast { exported.get().asFile.copyTo(committed.asFile, overwrite = true) }
}
// ./gradlew check fails when the committed contract.cue is not the exported one. It ignores only the value of
// metadata.generator.version, the SDK version, so upgrading the SDK does not by itself make the contract stale.
val docuconfCheck by tasks.registering {
val exported = exportedContract
val committed = layout.projectDirectory.file("contract.cue")
inputs.file(exported)
doLast {
val sdkVersion = Regex("""(generator: \{[^{}]*?\bversion: )"[^"]*"""")
fun normalized(cue: String) = sdkVersion.replace(cue, "\$1\"\"")
val now = normalized(exported.get().asFile.readText())
if (!committed.asFile.exists() || normalized(committed.asFile.readText()) != now) {
throw GradleException("docuconf: contract.cue is out of date; run ./gradlew docuconfExport and commit it")
}
}
}
tasks.check { dependsOn(docuconfCheck) }
As a last guard, the contract records a hash of each application*.yml it was exported from. A packaged app whose
application.yml differs from its contract's fails to start and says to rebuild; a development run only warns.
docuconf.stale-contract (auto, fail, warn, ignore) changes that.
Part of the exported contract.cue:
ORDERS_LOGLEVEL: {
type: "enum"
description: "Minimum level of the log lines the service writes"
configKey: "orders.log-level"
values: ["debug", "info", "warn", "error"]
default: "info"
}
The platform team deploys against contract.cue, not your Java code. Before a rollout they check their values
and files with docuconf vet -contract contract.cue -values values.yaml and produce the container's environment
with docuconf render (both from docuconf-go); a Helm-based platform
uses the docuconf Helm chart, which turns the same
contract into a values.schema.json. A missing database URL or an out-of-range port is caught before deploy, and
the startup check catches whatever still gets through.
| docuconf annotation | Input |
|---|---|
@Docuconf(service = ...) | Marks the class. The service name may also come from spring.application.name in application.yml. enumCase and envNames are below. |
@Secret | Must come from a Kubernetes Secret; no default anywhere; never printed. |
@KeySetLimits(minKeys, maxKeys, keyMinLength, keyMaxLength) on KeySet | A key set's bounds: how many keys (default 1 to 2), and each key's length in characters. A KeySet is always secret. |
@Deprecated, @DeprecatedConfigurationProperty(reason, replacement) | deprecated: {message, replacedBy}. The reason must not be blank and is at most 500 characters; a required property cannot be deprecated (the build fails). At startup a deprecated input that is set logs a warning naming it and the reason, never its value. |
@UrlSchemes({...}) | A url with allowed schemes (on String, URI or URL). |
@EnumValues(EnumCase.LOWER) | How this enum property's values are spelled in the contract (AS_DECLARED, LOWER, KEBAB); @Docuconf(enumCase = ...) sets it for the class. |
@Json | One variable holding JSON, bound with Jackson; the contract carries a JSON Schema of the type (with a top-level @Size/@NotEmpty as minItems/maxItems). |
@MaxLength(n) | maxLength on a url or @Json variable, for apps that store the value in a fixed-width field. A url is measured as it is; a json value as received, whitespace included, before it is parsed (as compact JSON when it comes from application*.yml or an overlay as nested keys). |
@TlsFile(dir) on TlsKeyPair | tls.crt, tls.key (PKCS#8, PKCS#1 or SEC1 PEM), optional ca.crt. Checked for key match, validity, minRemaining, dnsNames (SANs; a wildcard covers one label), keyAlgorithms, and the PKIX chain to with . and build JSSE objects. |
Every file annotation takes name, pathEnv, reload (RESTART or WATCH) and maxSize. Mark the property
@NotNull to make the input required. An annotation on a type it does not fit fails the build.
Every input has a description: what it is, in one phrase of plain text. It is the first sentence of the
property's Javadoc (the record component's @param, the field, or the getter), which ends at the first period
followed by white space, or at the first <p> or other block element, as for the javadoc tool; @Description("...")
overrides it. An input may also have details: CommonMark on why it exists and when to change it, at most 4000
characters (Unicode code points). The details are the rest of the Javadoc, read by docuconf's annotation processor
at compile time; they go into the contract for generated docs only and are never read at runtime.
Javadoc HTML becomes CommonMark: <p> starts a paragraph, <ul>/<ol> with <li> are lists, <pre>{@code ...}</pre>
is a fenced code block, {@code x}, {@literal x}, {@link X}, {@value X} and <code> are code spans, <a href> is
a link, <b>/<strong> and <i>/<em> are emphasis and <br> a line break; other tags are dropped, keeping their
text. Block tags (@see, @since, @deprecated) are not part of the details. Details over 4000 characters fail
the build, as a short description does.
docuconf docs in the docuconf CLI generates CONFIG.md and
CONFIG.agents.md from the exported contract: docuconf docs contract.cue -o CONFIG.md, and
--format agents -o CONFIG.agents.md.
| Java | Contract |
|---|---|
Property orders.database-url | Variable ORDERS_DATABASEURL, the name Spring's relaxed binding documents for environment variables (configKey keeps the property name). Nested classes add segments: ORDERS_DB_POOLSIZE. @Docuconf(envNames = EnvNames.UNDERSCORED) exports ORDERS_DATABASE_URL instead, which Spring binds too; the build fails if two properties would share a name. An empty prefix binds at the root, as Spring allows: port is PORT. |
String, Path, Locale, ... | string. @Size → minLength/maxLength; @NotBlank → required, minLength: 1, pattern: "\\S". |
@Pattern(regexp = "p") | pattern: "^(?:p)$": @Pattern matches the whole value, contract patterns match anywhere (SPEC section 4.3). A pattern already anchored at both ends, with no top-level | (^[a-z]+$), is exported as it is. Patterns using Java-only regex features fail the build. |
int, long, Integer, ... | int, with @Min/@Max/@Range/@Positive... A type narrower than 64 bits also exports its own range (int: min: -2147483648, max: 2147483647). An int without a default is 0 when unset, so @Min(1) int needs @DefaultValue or @NotNull Integer; the build says so. |
double, BigDecimal, ... | float, with inclusive @DecimalMin/@DecimalMax. Exclusive bounds (@Positive) have no contract form and are checked only at startup. |
Duration | duration with encoding: "iso8601"; @DurationMin/@DurationMax → min/max. In the environment the value is ISO 8601 (PT1M30S), as the platform renders it; Spring's simple format () works in and , but not in an environment variable (SPEC section 5), where the error says . |
Lengths (minLength, maxLength, itemMinLength, itemMaxLength) count characters, meaning Unicode code points
(codePointCount), never bytes or UTF-16 units (SPEC section 4.3): 日本 is 2 and ZÜ01 fits @Size(max = 4). An emoji
is one character but two Java chars, and Bean Validation's own @Size counts chars, so near the limit
docuconf's startup check and Bean Validation can disagree on astral characters. A value above a limit is
out_of_range; a too-long secret reports its length, never its value.
Processor options: -Adocuconf.name=, -Adocuconf.appVersion=, -Adocuconf.resources=<dir with application.yml>.
Where the contract is in your jar: Spring Boot's repackaging (Maven and Gradle) moves META-INF/ to the root, so
unzip -p target/orders.jar META-INF/docuconf/contract.cue (Gradle: build/libs/<app>.jar). The startup check
finds it either way. Prefer the exported contract.cue from step 6.
string (SPEC section 5): @DefaultValue and initializers
apply.ORDERS_PORT=" 8080" is invalid_type, as the platform says.bool is true or false in any case, never
yes, on or 1. An int is decimal digits with an optional sign: 0x10 and 1e3 are invalid_type, and
010 is 10, never octal 8. A float has a digit on each side of its point (.5 is invalid_type). A duration
is ISO 8601 (PT30S, not 30s). A list item with spaces around it, which Spring would trim, or an empty one,
which it would drop, is invalid_type. Each is reported as invalid_type with the other problems.... (set as ORDERS_WORKER_COUNT).@Email, custom ones) are reported in the same list, with
their own message; for a secret, the value is redacted from it.DOCUCONF_FILE_ROOT=./dev reads /etc/orders/tls from ./dev/etc/orders/tls (also for paths from pathEnv).docuconf.enabled=false skips the check, for slice tests and build-time tasks.@Json variables and @ConfigFile files are read with the Jackson your app has: Jackson 3 on Spring Boot 4,
Jackson 2 on Spring Boot 3 (spring-boot-starter-json), plus jackson-dataformat-yaml or
jackson-dataformat-toml for config files in those formats. If one is missing, startup says which.File inputs reach your properties object when it is bound. For reload = WATCH inputs, docuconf watches the mount
directory in the background (Kubernetes swaps a ..data symlink there). After a change it compares a hash of the
input's files with the content it last checked, re-checks a changed input exactly as at startup, and swaps in the new
value only if it passes. A change that fails is logged by violation and not used: the previous value stays. Your
bean keeps the startup value; read the live one from DocuconfFiles. An unknown input name, or a hook or status on
an input that is not watched, throws and names the inputs there are.
An object built once from a watched value (an SSLContext, an HTTP client, a connection pool) keeps the old value
until you rebuild it, so a renewed certificate is mounted but never served. Either read the value on every use, or
rebuild the object when it changes. docuconf tells you, after an accepted change and never after a rejected one, in
two ways that fire from its watcher thread, without the app reading the value:
DocuconfFileReloadedEvent, for @EventListener methods and ApplicationListener beans, with getInput(),
getValue() (or getValue(Class)) and getGeneration();DocuconfFiles.onChange(input, type, hook), which returns a DocuconfFiles.Subscription; close() it to
unregister.Several hooks and listeners may watch one input. One that throws is logged, by input name and exception type only, and neither stops the others nor undoes the reload.
A TLS server that asks for its context on each new connection:
@Component
class ServerTls {
private final AtomicReference<SSLContext> context = new AtomicReference<>();
ServerTls(OrdersProperties props) throws GeneralSecurityException {
context.set(props.tls().sslContext());
}
@EventListener(condition = "#event.input == 'tls'")
void renewed(DocuconfFileReloadedEvent event) throws GeneralSecurityException {
context.set(event.getValue(TlsKeyPair.class).sslContext());
}
SSLContext current() { // call on each accept, never cache the result
return context.get();
}
}
An HTTP client that trusts a watched CA bundle, rebuilt when the bundle changes:
@Component
class Upstream {
private final AtomicReference<HttpClient> client = new AtomicReference<>();
Upstream(DocuconfFiles files) {
CaBundle ca = files.get("upstreamCa", CaBundle.class).orElseThrow();
client.set(build(ca));
files.onChange("upstreamCa", CaBundle.class, next -> client.set(build(next)));
}
private static HttpClient build(CaBundle ca) {
try {
SSLContext ssl = SSLContext.getInstance("TLS");
ssl.init(null, ca.trustManagerFactory().getTrustManagers(), null);
return HttpClient.newBuilder().sslContext(ssl).build();
} catch (GeneralSecurityException e) {
throw new IllegalStateException(e);
}
}
HttpClient client() { // per request
return client.get();
}
}
TlsKeyPair, CaBundle and Keystore read their files on each call, so calling them on each use also sees a
rotation, but before docuconf has checked it; rebuild in a hook or listener to use only content that passed.
Reload status. files.reloadStatus("tls") returns a ReloadStatus: generation() is 1 after boot (0 for an
optional input absent at boot) plus one per accepted reload, lastReload() is the time of the last accepted reload
(null before the first), and lastRejected() is the last change that failed its checks, as a RejectedReload
with time(), input() and codes() (such as [certificate_expiring]), never content, or null. An accepted
reload clears it. files.reloadStatuses() returns every watched input's status, by name, for a health endpoint or a
metric:
ReloadStatus tls = files.reloadStatus("tls");
Map<String, ReloadStatus> all = files.reloadStatuses(); // for a health endpoint
Keystore passwords. A reload re-opens a watched keystore with the password read at startup, since a process's
environment does not change. Rotating a keystore's password therefore needs a rollout, which also delivers the new
keystore. A changed keystore that does not open with the startup password is rejected as keystore_unreadable, and
the previous one stays.
Spring layers config files, so the platform can supply settings in one more application.yml-style file it mounts
(SPEC section 4.7), instead of one environment variable each. Declare it on a @Docuconf class with
@ConfigOverlay(value = "/etc/orders/overlay/orders.yaml", reload = Reload.WATCH). The platform writes each value
at its variable's configKey (orders.request-timeout), in native YAML types, with durations in ISO 8601. An
EnvironmentPostProcessor loads the file with Spring Boot's own YAML loader right after config data, as a
property source just below systemEnvironment, so the order is application.yml < application-{profile}.yml <
overlay < environment variables < system properties < command line.
file_malformed alongside the other problems.DOCUCONF_FILE_ROOT applies to the overlay path too..yml or .yaml, and its directory must be its own: the build rejects reserved directories
(/app, /etc, ...) and directories shared with a file input.SPRING_PROFILES_ACTIVE in the environment.reload = Reload.WATCH polls the file (docuconf.overlay-poll-interval, default 5s). On a valid change it
rebinds each @Docuconf JavaBean in place and publishes a DocuconfOverlayReloadedEvent; an invalid change is
logged and ignored. Records are immutable, so the build rejects WATCH when one holds a non-secret variable: use
RESTART there.Values that an injector supplies when the container starts (Bank-Vaults vault-env, op run, vals, an operator)
need nothing special: docuconf validates the environment the process starts with. If a secret still holds a
reference at startup (it starts with vault:, op:// or ref+), the injector did not run, and startup fails with
[invalid_type] ORDERS_DATABASEURL: holds an unresolved vault: reference; the injector that should resolve it did not run. The message names the scheme, never the reference.
To validate against a contract written by hand in CUE instead of declared in Java, export it as JSON
(cue export ./contract > contract.json) and load the environment against it with ContractFirst, in
docuconf-core (no Spring needed). The one-liner prints docuconf: N configuration problems: with one line per
problem, writes the termination log and exits 1:
Map<String, Object> config = ContractFirst.bootOrExit(Path.of("contract.json"));
For tests, load any map; nothing is read from the process:
ContractFirst.Result r = ContractFirst.load(json, Map.of("PORT", "8080"));
It parses every wire encoding of SPEC section 5, exactly (the strict rules above), and checks constraints with the
same code the Spring path uses. Values are String, Long, BigDecimal, Boolean, Duration, lists, a KeySet,
and the parsed JSON of json variables. It also reads the contract's file inputs (r.files(): config files in
JSON, YAML and TOML as data, text files as text, TLS key pairs, CA bundles and PKCS#12 keystores checked), profiles
and config-file overlays, in the order default, profile, overlay, environment, all under DOCUCONF_FILE_ROOT. YAML
and TOML are read with Jackson's YAML or TOML module, found at run time (Jackson 3, else Jackson 2), so
docuconf-core keeps no dependency. A deprecated variable that is set loads, with a warning in r.warnings(). The
values are read once, so a contract that declares reload: watch for a file input or an overlay is rejected at load
with an IllegalArgumentException naming each one (SPEC section 11.2 item 8); declare reload: restart, or use the
Spring Boot integration, which reloads watched inputs.
ConformanceTest (in docuconf-core) runs the shared suite from docuconf-go (conformance/cases.json, SPEC
section 12) through the contract-first mode, one JUnit test per case. Each case runs in a new, empty directory
holding its files, which is the case's DOCUCONF_FILE_ROOT. Set DOCUCONF_CONFORMANCE to the file and
DOCUCONF_REQUIRE_CONFORMANCE=1 to fail instead of skipping when it is missing, and to fail when any case is
skipped; without them it looks for docuconf-go/conformance/cases.json in the working directory and its parents.
CI runs it against the pinned docuconf-go commit, and nightly against main.
Capability tags. The runner keeps an allow-list of the tags this SDK supports: int64, json-schema,
key-set, deprecated, strict-parsing, files, profiles and overlays, which is every tag in the suite. No
case is skipped. A case with a tag the runner does not know is skipped, never run (SPEC section 12), and CI fails
on any skipped case.
Export. ExportConformanceTest (in docuconf-processor) declares the suite's export fixture
(conformance/export/fixture.yaml) as a @ConfigurationProperties class,
FixtureProperties, exports it and
runs docuconf conformance export --golden conformance/export/golden.cue on the result (the CLI from DOCUCONF_CLI
or the PATH), which must exit 0 with no differences. GoldenContractTest in docuconf-sample still compares this SDK's own, larger fixture with its golden
file.
ORDERS_DATABASEURL and not ORDERS_DATABASE_URL? The first is the name Spring documents for
orders.database-url. Spring binds both; for the second in the contract, use
@Docuconf(envNames = EnvNames.UNDERSCORED).contract.cue after building in IntelliJ. Enable annotation processing (Settings → Build → Compiler →
Annotation Processors), or delegate builds to Maven or Gradle.application.yml change is not in the contract. Add the Maven plugin's refresh goal, or the Gradle
compileJava input from step 6. Until then, mvn clean compile exports it again.annotationProcessorPaths turned off processor discovery; add
spring-boot-configuration-processor to the list (step 1).@SpringBootTest(properties = "docuconf.enabled=false"), or test the
configuration itself with DocuconfTester (step 5).ORDERS_LOGLEVEL=warn fails on the platform but not in the app. The platform checks the contract's
spelling; export the enum in the case your deployments use with enumCase.mvn verify
Java 17+. Tests vet exported contracts with cue vet -c against the meta-schema at $DOCUCONF_SPEC_CUE, else
../docuconf-go/spec/cue; they skip that step when cue or the meta-schema is missing, unless
DOCUCONF_REQUIRE_VET=1. docuconf-sample is a gateway that uses every variable type and every file type; its
exported contract is the golden file docuconf-sample/src/test/resources/golden/contract.cue (regenerate with
mvn test -pl docuconf-sample -am -Ddocuconf.updateGolden=true). ReadmeSnippetsTest there checks that every
snippet on this page is in a file CI compiles or runs. The examples are separate builds: examples/orders
(quickstart.sh, smoke.sh) and examples/orders-gradle.
Not done yet: Markdown docs generation, multi-profile activation (SPRING_PROFILES_ACTIVE=prod,eu is rejected by
the contract; see SPEC section 13 question 5), and Gradle incremental annotation processing (the processor reads
JavaBean field initializers through the javac tree API, which Gradle's incremental mode hides).
MIT. See LICENSE.
ca.crtrequireCAsslContext()keyStore()@ConfigFile(path) on a class or record | JSON, YAML or TOML, read with Jackson into that type (unknown properties rejected) and validated with Bean Validation through the whole object graph. The contract carries a JSON Schema generated from the type. |
@CaBundleFile(path) on CaBundle | PEM CA certificates, at least minCertificates. |
@KeystoreFile(path, passwordProperty = ...) on Keystore | PKCS#12 or JKS; must open with the @Secret sibling property. |
@TextFile(path, pattern = ...) on String | Licence keys and the like; the property receives the content. |
@BinaryFile(path) on Path | Opaque bytes; size only. |
@ConfigOverlay(path) on the class | A platform-mounted application.yml-style file layered over yours (below). |
@External("vault") | Supplied by a source the platform does not control; left out. |
@Group, @Examples, @Description | Docs metadata. |
90sapplication.yml@DefaultValueexpected an ISO 8601 duration like PT30SURI, URL, or @UrlSchemes | url. @MaxLength, or @Size(max) on a String, → maxLength. |
an enum | enum with the constant names, or as enumCase spells them. The platform checks that spelling exactly; the app accepts any case Spring accepts (warn for WARN), wherever the value comes from. |
KeySet | keySet, always secret, encoding: "csv" (@Delimiter sets separator); @KeySetLimits → minKeys, maxKeys, keyMinLength, keyMaxLength. Bound by docuconf's converter, which splits on the separator without trimming. |
List/Set/array of strings, ints or enums | list, encoding: "csv" (@Delimiter sets separator). @Size/@NotEmpty → minItems/maxItems. List<@Min(0) @Max(1023) Integer> → itemMin/itemMax. On string items, List<@Size(min = 2, max = 4) String> → itemMinLength/itemMaxLength, checked on each item after splitting, so the separator is never counted. @Size on int items fails the build. Spring also reads one variable per item (ORDERS_SHARDS_0, ORDERS_SHARDS__1): items must be numbered from 0 with no gap (SPEC section 5). |
@NotNull/@NotBlank/@NotEmpty without a default | required: true |
Field initializer, @DefaultValue, value in application.yml | default (the yml value wins, as in Spring); a required property with one becomes optional. |
Value in application-{profile}.yml (or a spring.config.activate.on-profile document) | profiles.defaults.{profile}, selected by SPRING_PROFILES_ACTIVE, which is added to the contract as a single profile name. |
Map, lists of objects | Not expressible in v1alpha1: left out with a warning (file-only). |