| Artifact | io.github.microsphere-projects:microsphere-java-core (0.3.20-SNAPSHOT, release branch) |
| Package | io.microsphere.metadata — 9 main-source types |
| Model type | io.microsphere.beans.ConfigurationProperty + nested ConfigurationProperty.Metadata |
| JDK level | Compiled at Java 8; CI-tested on JDK 8 / 11 / 17 / 21 / 25 |
Depends on io.microsphere.util |
ServiceLoaderUtils.loadServicesList, ClassLoaderUtils.nullSafeClassLoader, ClassUtils.getTypeName, Assert.assertNotEmpty / assertNotNull |
Other core packages used |
io.microsphere.json (JSONUtils.readValues / append / appendName / writeBeanAsString), io.microsphere.beans, io.microsphere.collection, io.microsphere.constants (ResourceConstants, SymbolConstants), io.microsphere.io.IOUtils, io.microsphere.nio.charset.CharsetUtils, io.microsphere.logging, io.microsphere.lang.Prioritized, io.microsphere.lang.function.ThrowableSupplier, io.microsphere.annotation.* (from microsphere-java-annotations) |
| External dependencies | none — the JSON reader/writer is the in-repo io.microsphere.json model, not Jackson or Gson |
Configuration metadata is the machine-readable description of the properties an application exposes: name, type,
default value, whether it is required, a human description, and where it is declared. Spring IDE reads an equivalent
document to offer autocompletion and documentation popups on application.properties; Microsphere ships the same idea
under META-INF/microsphere/configuration-properties.json, and the io.microsphere.metadata package is the runtime
counterpart of that file — it reads it, loads it from the classpath (possibly from many jars), and generates
it. The compile-time half lives in the sibling module
microsphere-annotation-processor, which is documented separately.
| Contract | Kind | One-line responsibility | Shipped implementations |
|---|---|---|---|
ConfigurationPropertyReader |
SPI interface | parse metadata content into List<ConfigurationProperty> |
DefaultConfigurationPropertyReader (JSON) |
ConfigurationPropertyLoader |
SPI interface | obtain metadata from a source and merge everything found | MetadataResourceConfigurationPropertyLoader, AdditionalMetadataResourceConfigurationPropertyLoader, abstract ClassPathResourceConfigurationPropertyLoader |
ConfigurationPropertyGenerator |
SPI interface | render one ConfigurationProperty as JSON text |
DefaultConfigurationPropertyGenerator, ReflectiveConfigurationPropertyGenerator |
All nine types in the package, and the ConfigurationProperty model they operate on, record Javadoc @since 1.0.0
(metadata/ConfigurationPropertyReader.java:39, metadata/ConfigurationPropertyLoader.java:45,
metadata/ConfigurationPropertyGenerator.java:54, beans/ConfigurationProperty.java:61), so the contract has been
stable across the whole 0.x line.
| GroupId | io.github.microsphere-projects (pom.xml:12) |
| Reactor version | ${revision} = 0.3.20-SNAPSHOT (pom.xml:54); tag 0.3.20 is published |
| BOM | microsphere-java-parent/pom.xml imports microsphere-all-bom:0.3.13 (:22, :33-34) |
| Spring | spring-framework-bom is imported (microsphere-java-parent/pom.xml:40-46); 7.0.9 by default (:23), 5.3.39 under profile java8-16 activated by <jdk>[1.8,17)</jdk> (:54-62, activation :57) |
| JDK matrix | Temurin 8, 11, 17, 21, 25 (.github/workflows/maven-build.yml:25, distribution: 'temurin' :33) |
Note
There is no maven.compiler.* or <release> property anywhere in this reactor. The bytecode level is inherited
from the external parent io.github.microsphere-projects:microsphere-build:0.3.16 (pom.xml:6-10). The package
itself only uses JDK 8 APIs (ClassLoader.getResourceAsStream, Enumeration<URL>, try-with-resources), so it is
usable from Java 8 upwards without a multi-release jar.
Spring is listed above for completeness only: nothing in io.microsphere.metadata imports Spring. The JSON document
format is defined by the in-repo io.microsphere.json model, which the reader and the generators call directly — see
JSON support rather than assuming a third-party mapper.
Add the dependency (the annotation processor is the compile-time companion and is usually provided):
<dependency>
<groupId>io.github.microsphere-projects</groupId>
<artifactId>microsphere-java-core</artifactId>
<version>0.3.20</version>
</dependency>
<dependency>
<groupId>io.github.microsphere-projects</groupId>
<artifactId>microsphere-annotation-processor</artifactId>
<version>0.3.20</version>
<scope>provided</scope>
</dependency>| Item | Value | Evidence |
|---|---|---|
| Documented line | release, ${revision} = 0.3.20-SNAPSHOT |
root pom.xml:54 |
| Newest published release | 0.3.20, tagged 2026-10-16 |
the 0.3.20 tag resolves to chore: merge main into release [skip ci], committed 2026-10-16T03:43:20Z |
| Previous release | 0.3.19, tagged 2026-08-30 |
the 0.3.19 tag, committed 2026-08-30T12:51:37Z |
| Changelog source | release-notes.md at the repository root |
The project publishes no CHANGELOG.md; the v0.3.16–v0.3.20 blocks of release-notes.md read Release notes generation failed and carry raw commit lists, so every entry below is quoted as a commit subject and checked against the release sources. |
0.3.20 adds no new API surface for this area — the 0.3.19...0.3.20 range is a hardening release. What it does change, and how far that reaches the sources this page documents:
- Nullability contracts. 4 of 9 main sources in the scope of this page declare
@Nonnull/@Nullable. Subjects in the range that name this area:2b0989d3 Allow nullable configuration descriptions/e3a84c77 Add nullability annotations to resolvers. - No other change in this range touches the area this page covers.
Note
Reading the commit list: 2b0989d3 Allow nullable configuration descriptions concerns the description member of @ConfigurationProperty, whose generating side lives in the annotation-processor module, so its effect is split between this page and that module's page. The range also carries da6586f1 Assert resource processing does not throw, but no test file on this line matches it, so it is left unattributed.
The model lives in io.microsphere.beans (beans/ConfigurationProperty.java:63) because it is a JavaBean, but it is
the vocabulary of this package and is described here.
| Member | Declaration | Notes |
|---|---|---|
getName() |
final String name, :68-69, :127 |
immutable; asserted non-null in the constructor (:120) |
getType() |
String type, :74-75, :132 |
a type name, not a Class — see pitfall 8 |
setType(Class) / setType(String) |
:136-139, :141-144 |
the Class overload stores ClassUtils.getTypeName(type) |
getValue() / getDefaultValue() |
Object, :80-81, :86-87 |
@Nullable, untyped |
isRequired() |
boolean required, :92, :164 |
always serialised, never omitted |
getDescription() |
String, :97-98, :101 |
@Nullable |
getMetadata() |
final Metadata metadata, :112-113, :173 |
created eagerly in the constructor (:123) |
Metadata.getSources() / getTargets() |
:258-263, :282-287 |
lazily initialised LinkedHashSet<String>(2) — insertion-ordered and de-duplicated |
Metadata.getDeclaredClass() / getDeclaredField() |
:294-295, :304-305 |
@Nullable; the declaring field the IDE should jump to |
Two constructors exist: ConfigurationProperty(String name) defaults the type to String
(beans/ConfigurationProperty.java:115-117), and ConfigurationProperty(String name, Class<?> type) (:119-124).
equals/hashCode compare all seven members including Metadata (:178-202), which is what makes the
"merge across jars" question in section 4 observable: two properties with the same name but different descriptions are
not equal.
ConfigurationProperty property = new ConfigurationProperty("server.port", Integer.class);
property.setValue(8080);
property.setDefaultValue(8080);
property.setRequired(true);
property.setDescription("The port number for the server");
ConfigurationProperty.Metadata metadata = property.getMetadata();
metadata.getSources().add("application.properties");
metadata.getTargets().add("server");
metadata.setDeclaredClass("com.example.ServerConfig");
metadata.setDeclaredField("port");Source: microsphere-java-core/src/test/java/io/microsphere/JSONTestUtils.java:51-64
The canonical source of a property name is the annotation io.microsphere.annotation.ConfigurationProperty
(microsphere-java-annotations/src/main/java/io/microsphere/annotation/ConfigurationProperty.java:60-88), whose
name/type/defaultValue/required/description members map one-to-one onto the model, and whose three source
constants — system-properties, environment-variables, application (:43, :48, :53) — are the values you will
see in Metadata.sources.
ConfigurationPropertyReader (metadata/ConfigurationPropertyReader.java:41) extends Prioritized and exposes three
read overloads that cascade into one another:
| Method | Source | Delegates to |
|---|---|---|
List<ConfigurationProperty> read(InputStream) |
:66-68 (default) |
wraps in InputStreamReader with CharsetUtils.DEFAULT_CHARSET |
List<ConfigurationProperty> read(Reader) |
:94-96 (default) |
IOUtils.copyToString(reader) then read(String) |
List<ConfigurationProperty> read(String content) |
:119 (abstract) |
the actual parsing, format-specific |
All three are @Nonnull @Immutable and declare throws Throwable — a checked, deliberately untyped failure channel
(see pitfall 9).
DefaultConfigurationPropertyReader (metadata/DefaultConfigurationPropertyReader.java:39) is the only shipped
implementation and the only supported format. It parses the content as a JSON array of objects
(:43 — JSONUtils.readValues(content, List.class, Map.class)) and maps six keys plus a nested metadata object per
entry (:59-89):
@Override
public List<ConfigurationProperty> read(String content) throws Throwable {
List<Map<String, Object>> configurationPropertiesMaps = readValues(content, List.class, Map.class);
...
return unmodifiableList(configurationProperties);
}Reading from a resource is the usual entry point:
ClassLoader classLoader = getClass().getClassLoader();
try (InputStream inputStream = classLoader.getResourceAsStream(ADDITIONAL_CONFIGURATION_PROPERTY_METADATA_RESOURCE)) {
List<ConfigurationProperty> configurationProperties = new DefaultConfigurationPropertyReader().read(inputStream);
assertEquals(2, configurationProperties.size());
}Source: metadata/DefaultConfigurationPropertyReaderTest.java:47-55
The same reader pointed at the file the annotation processor produces yields the module's seven documented properties:
private static final String JSON_RESOURCE_NAME = "META-INF/microsphere/configuration-properties.json";
ClassLoader classLoader = getClassLoader(getClass());
try (InputStream inputStream = classLoader.getResourceAsStream(JSON_RESOURCE_NAME)) {
List<ConfigurationProperty> configurationProperties = reader.read(inputStream);
assertEquals(7, configurationProperties.size());
}Source: metadata/ConfigurationPropertiesJSONReaderTest.java:39, :48-55
Important
DefaultConfigurationPropertyReader.getPriority() returns MIN_PRIORITY
(metadata/DefaultConfigurationPropertyReader.java:54-57), i.e. the lowest precedence — remember that
Prioritized.MIN_PRIORITY is Integer.MAX_VALUE and MAX_PRIORITY is Integer.MIN_VALUE
(lang/Prioritized.java:86, :81). Your own reader, with any smaller numeric priority, wins the sort.
The two resource locations are constants, not conventions you have to remember:
| Constant | Value | Declared at |
|---|---|---|
ResourceConstants.MICROSPHERE_METADATA_RESOURCE |
META-INF/microsphere/ |
constants/ResourceConstants.java:38 |
CONFIGURATION_PROPERTY_METADATA_RESOURCE |
META-INF/microsphere/configuration-properties.json |
:54 (file name :43) |
ADDITIONAL_CONFIGURATION_PROPERTY_METADATA_RESOURCE |
META-INF/microsphere/additional-configuration-properties.json |
:60 (file name :48) |
ConfigurationPropertyLoader.loadAll() (metadata/ConfigurationPropertyLoader.java:91-106) is the aggregate entry
point. It discovers every registered loader through Java SPI —
ServiceLoaderUtils.loadServicesList(ConfigurationPropertyLoader.class) at :93, which reads
META-INF/services/io.microsphere.metadata.ConfigurationPropertyLoader and sorts the instances with
Prioritized.COMPARATOR (util/ServiceLoaderUtils.java:890) — then calls load() on each in that order and appends
the results into one unmodifiable list (:94-105). A loader that returns null or an empty list contributes nothing
(isNotEmpty guard at :98); a loader that throws is logged at error level and skipped (:101-103).
| Loader | Resource | Priority | Registered in META-INF/services? |
|---|---|---|---|
MetadataResourceConfigurationPropertyLoader |
configuration-properties.json |
MIN_PRIORITY = Integer.MAX_VALUE (:40-42) |
no (see the warning below) |
AdditionalMetadataResourceConfigurationPropertyLoader |
additional-configuration-properties.json |
MIN_PRIORITY + 9 (:40-42) — overflows, see pitfall 7 |
yes |
ClassPathResourceConfigurationPropertyLoader (abstract) |
whatever the constructor names | inherited | no — extend it |
test-only DefaultConfigurationPropertyLoader / Empty… / Null… / Error… |
in-memory | default NORMAL_PRIORITY |
test scope only |
ClassPathResourceConfigurationPropertyLoader (metadata/ClassPathResourceConfigurationPropertyLoader.java:47) is the
base you extend. Its load() (:80-92) has exactly two behaviours, selected by the loadedAll flag:
loadedAll == false→classLoader.getResourceAsStream(resourceName)(:89) — the first copy on the classpath only. Every other jar contributing the same path is silently ignored.loadedAll == true→classLoader.getResources(resourceName)(:83) and onereadper URL (:84-87) — every jar's copy is parsed and appended.
Either way an unopenable stream is not an error: load(list, streamSupplier) (:94-104) logs at trace and returns
(:96-101). The class loader is never null — the constructor funnels it through
ClassLoaderUtils.nullSafeClassLoader (:74, implemented at util/ClassLoaderUtils.java:218-220), and a blank
resource name is rejected with IllegalArgumentException (:72).
ClassPathResourceConfigurationPropertyLoader loader =
new ClassPathResourceConfigurationPropertyLoader(ADDITIONAL_CONFIGURATION_PROPERTY_METADATA_RESOURCE) {
};
List<ConfigurationProperty> configurationProperties = loader.load();
assertEquals(2, configurationProperties.size());Source: metadata/ClassPathResourceConfigurationPropertyLoaderTest.java:41-46, :93-96
The loadedAll variants — (String, boolean) and (String, ClassLoader, boolean) — are exercised by the same suite
(:56-60, :76-85), and both missing-resource cases assert an empty list rather than an exception (:62-74). The
reader is hard-wired inside the base class (new DefaultConfigurationPropertyReader(), :76), so a custom reader does
not reach this path automatically (pitfall 6).
Aggregation, in the shape the tests pin down:
List<ConfigurationProperty> configurationProperties = loadAll();
assertEquals(3, configurationProperties.size()); // 2 (additional) + 1 (test loader)
assertEquals(newConfigurationProperty(), configurationProperties.get(2));Source: metadata/ConfigurationPropertyLoaderTest.java:44-49
Warning
Two loader implementations are effectively unavailable in 0.3.20 and you should not rely on them being invoked:
MetadataResourceConfigurationPropertyLoaderis not registered inmicrosphere-java-core/src/main/resources/META-INF/services/io.microsphere.metadata.ConfigurationPropertyLoader— that file contains onlyio.microsphere.metadata.AdditionalMetadataResourceConfigurationPropertyLoader. ConsequentlyloadAll()never readsMETA-INF/microsphere/configuration-properties.jsonon its own; instantiate the loader directly if you need it.loadAll()documents "an empty list if no loaders are found" (metadata/ConfigurationPropertyLoader.java:84), but the discovery call at:93sits outside thetry/catchat:96-103, andServiceLoaderUtilsthrowsIllegalArgumentExceptionwhen no implementation is declared (util/ServiceLoaderUtils.java:882-886). If the service file is stripped (shading, or amodule-infomigration),loadAll()throws instead of degrading.
ConfigurationPropertyGenerator (metadata/ConfigurationPropertyGenerator.java:56) is the smallest contract:
String generate(ConfigurationProperty) returning JSON text, @Nonnull, throwing
IllegalArgumentException on a null argument (:66). It is what the annotation processor calls to append
loader-supplied properties to the generated file
(microsphere-annotation-processor/src/main/java/io/microsphere/annotation/processor/ConfigurationPropertyAnnotationProcessor.java:143-146),
resolved with loadFirstService(ConfigurationPropertyGenerator.class)
(ConfigurationPropertyJSONElementVisitor.java:65).
| Implementation | Mechanism | getPriority() |
Registered? |
|---|---|---|---|
DefaultConfigurationPropertyGenerator |
hand-built StringBuilder, JSONUtils.append |
MIN_PRIORITY (:161-164) |
yes — the only registration |
ReflectiveConfigurationPropertyGenerator |
JSONUtils.writeBeanAsString(property) (:78) |
inherits NORMAL_PRIORITY (lang/Prioritized.java:98-100) |
no |
Both reject null up front (DefaultConfigurationPropertyGenerator.java:82,
ReflectiveConfigurationPropertyGenerator.java:77).
ConfigurationProperty configurationProperty = new ConfigurationProperty("server.port", Integer.class);
String json = generator.generate(configurationProperty);
JSONObject jsonObject = new JSONObject(json);
assertEquals("server.port", jsonObject.getString("name"));
assertEquals("java.lang.Integer", jsonObject.getString("type"));
configurationProperty.setValue(8080);
json = generator.generate(configurationProperty);
jsonObject = new JSONObject(json);
assertEquals(8080, jsonObject.getInt("value"));Source: metadata/DefaultConfigurationPropertyGeneratorTest.java:59-72
The default generator emits name and type unconditionally, required always, and omits value,
defaultValue, description when they are null (:95, :101, :110); metadata is always emitted (:116-117),
and within it sources, targets, declaredClass and declaredField are each omitted when empty or null, with the
trailing comma trimmed (:123-159). An empty Metadata therefore renders as {}:
Metadata metadata = new Metadata();
String json = generator.generate(metadata);
assertEquals("{}", json);
assertNull(jsonObject.opt("sources"));
assertNull(jsonObject.opt("targets"));Source: metadata/DefaultConfigurationPropertyGeneratorTest.java:102-111
The reflective variant is the one to compare against when you need the bean's full property set, and it is the same call the tests use to assert the round-trip shape:
ConfigurationProperty configurationProperty = newConfigurationProperty();
String json = generator.generate(configurationProperty); // writeBeanAsString(...)
assertConfigurationPropertyJSON(json);Source: metadata/ReflectiveConfigurationPropertyGeneratorTest.java:46-51
Tip
To confirm that the metadata you expect really reached the file, watch the build output: on the final round the
processor prints the complete generated JSON through printNote(...)
(ConfigurationPropertyAnnotationProcessor.java:154), loader-contributed entries included.
- A missing metadata resource is not an error.
ClassPathResourceConfigurationPropertyLoader.load(...)(:94-104) treats anullInputStreamas "nothing to do", logs attrace, and returns an empty list. Nothing in the chain complains if you expectedconfiguration-properties.jsonbut never generated it. Assert on the content, or check thatmicrosphere-annotation-processoris on the annotation-processor path (META-INF/services/javax.annotation.processing.Processor). - Merging and de-duplication do not happen by property name.
loadAll()concatenates (metadata/ConfigurationPropertyLoader.java:99) and the base loader concatenates per URL (:86). The same property declared in two jars arrives twice. De-duplication only happens inside one property, wheresourcesandtargetsareLinkedHashSets (beans/ConfigurationProperty.java:258-263,:282-287). If you need "one entry per name", do it yourself —ConfigurationProperty.equalscovers all seven fields (:178-190), so aSetwill not collapse near-duplicates either. loadedAll=falseisclasspath:,loadedAll=trueisclasspath*:. The all-hit branch usesClassLoader.getResources(:83), which walks the parent class-loader chain and yields one URL per jar entry — the JDK equivalent of Spring'sclasspath*:. The single-hit branch usesgetResourceAsStream(:89) and stops at the first winner, so withconfiguration-properties.jsonpresent in ten jars you read exactly one. Both built-in loaders use the single-hit constructors (MetadataResourceConfigurationPropertyLoader.java:36,AdditionalMetadataResourceConfigurationPropertyLoader.java:36), so neither merges across jars by default.getResourcesmatches an exact resource path — no**patterns, no file-name globs, no directory scanning (that isio.microsphere.io.scanner, see I/O and scanning).nulland empty are tolerated; exceptions are swallowed — but not all of them.@Nullable load()(metadata/ConfigurationPropertyLoader.java:67-68) explicitly permitsnull, andloadAll()filters it (:98). A throwing loader is caught asThrowableand logged (:101-103) — your metadata can silently shrink to an empty list with only oneerrorline as evidence. The exception to the degradation policy is discovery itself: no service file ⇒IllegalArgumentExceptionescapes (see the warning in section 4.1).- The reader is strict about the keys it assumes are present.
readConfigurationPropertyunboxes(Boolean) map.get("required")(metadata/DefaultConfigurationPropertyReader.java:64), so a JSON entry withoutrequiredraisesNullPointerException;initMetadatadereferences the nested object without a null check (:79-80), so an entry withoutmetadataalso raisesNullPointerException; andconfigurationProperty.setType(type)rejects a missing/emptytypewithIllegalArgumentException(beans/ConfigurationProperty.java:141-144). These contradict the package's silent-degradation policy and are tracked in the project's NPE-hardening audit. - The reader SPI is registered but never consulted.
META-INF/services/io.microsphere.metadata.ConfigurationPropertyReadernamesDefaultConfigurationPropertyReader, yet nothing in the reactor callsloadServicesList(ConfigurationPropertyReader.class); the loader base constructs the default reader directly (metadata/ClassPathResourceConfigurationPropertyLoader.java:76). To change the format, subclass the loader and use the package-visibleload(List, ThrowableSupplier)hook (:94) yourself. MIN_PRIORITY + 9overflows.AdditionalMetadataResourceConfigurationPropertyLoader.getPriority()returnsMIN_PRIORITY + 9(:41) andMIN_PRIORITYisInteger.MAX_VALUE(lang/Prioritized.java:86), so the value wraps to-2147483640— nearMAX_PRIORITY, i.e. the loader sorts first, not last. The accompanying test compares against the same overflowing expression (AdditionalMetadataResourceConfigurationPropertyLoaderTest.java:51-54), so the defect is invisible there. OrderloadAll()output by your own explicit priorities.typeis a string, and this package never resolves it.ConfigurationProperty.getType()returns the recorded type name (beans/ConfigurationProperty.java:132); the reader passes the raw JSON string straight through (metadata/DefaultConfigurationPropertyReader.java:61,:67). Resolvingjava.lang.String[]into aClassand convertingdefaultValueinto that type is the job ofio.microsphere.reflectandio.microsphere.convert, which the JSON layer already uses underneath (json/JSONUtils.java:63,:66-67,:1427-1442). See Type conversion and Reflection and beans.throws Throwableis part of the contract. Both reader methods (metadata/ConfigurationPropertyReader.java:66,:94,:119) andConfigurationPropertyLoader.load()(:68) declarethrows Throwable, so acatch (Exception)block does not satisfy the compiler.loadAll()is the onlythrows-free API (:91).- Do not hand-edit the generated file.
META-INF/microsphere/configuration-properties.jsonis rewritten by the annotation processor on every compile (ConfigurationPropertyAnnotationProcessor.java:149-156); the seven entries currently shipped inmicrosphere-java-core/src/main/resources/META-INF/microsphere/configuration-properties.jsonare generated output. Extra properties belong inadditional-configuration-properties.json, contributed by aConfigurationPropertyLoader.
- Type conversion — turning
getType()/getDefaultValue()into usable values - Reflection and beans —
ConfigurationPropertyas a JavaBean,BeanUtils.resolvePropertiesAsMap - Utilities and constants —
ServiceLoaderUtils,ClassLoaderUtils,ResourceConstants - JSON support — the
io.microsphere.jsonmodel behind the reader and both generators - Annotation processor — module page — the compile-time half that writes these files
- Developer-facing deep dive: Configuration Property Metadata — Developer Guide