中文 · 1.x line · Developer Guide index
Branch
release· version line0.2.x· latest release0.2.26, head0.2.27-SNAPSHOT· Java 17 · Spring Boot 3.0.x–4.0.x · Spring Cloud 2022.0.x–2025.1.x · upstreammicrosphere-spring-boot0.2.34. Analysed sources:microsphere-spring-cloud-commons/src/main/java/io/microsphere/spring/cloud/**(58 production classes),src/main/resources/META-INF/**,src/test/java/**(52 test classes).
For contributors: the build layering is in parent / dependencies;
this document covers architecture, source-level behaviour, design patterns and extension points of the
commons jar.
| Aspect | Value |
|---|---|
| Coordinates | io.github.microsphere-projects:microsphere-spring-cloud-commons:${revision} |
| Packaging | jar, parent microsphere-spring-cloud-parent |
| Root package | io.microsphere.spring.cloud |
| Auto-configurations registered | 10, via META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports |
| Compile-scope (non-optional) dependencies | microsphere-spring-boot-core, -webmvc, -webflux, -actuator, org.aspectj:aspectjweaver |
| Optional dependencies | spring-cloud-commons, spring-cloud-context, spring-cloud-loadbalancer, eureka client, Nacos/Zookeeper/Consul discovery starters, Boot web/actuator starters |
| Test-only | junit-jupiter, spring-boot-starter-test, microsphere-spring-boot-test, testcontainers-junit-jupiter |
Package (under io.microsphere.spring.cloud) |
Responsibility |
|---|---|
client.actuator |
Feature catalogue → HasFeatures singletons, /actuator/features integration |
client.condition |
@ConditionalOnFeaturesEnabled / @ConditionalOnFeaturesAvailable |
client.discovery |
UnionDiscoveryClient, ReactiveDiscoveryClientAdapter, DiscoveryUtils |
client.event |
ServiceInstancesChangedEvent |
client.service.registry |
Registration/registry model: Multiple*, Simple*, InMemoryServiceRegistry, RegistrationMetaData, events, aspect, endpoints |
client.service.util |
ServiceInstanceUtils — metadata encoding/decoding, URI building |
commons.condition · commons.constants |
@ConditionalOnUtilEnabled, property-name constants |
context.named |
SpecificationCustomizer + SpecificationBeanPostProcessor (extension seam used by openfeign) |
fault.tolerance |
Weighted round-robin primitives + dynamic Tomcat reconfiguration |
loadbalancer.condition |
@ConditionalOnLoadBalancerEnabled |
graph TD
PARENT["microsphere-spring-cloud-parent<br/>pom"]
COMMONS["microsphere-spring-cloud-commons<br/>jar"]
MSB_CORE["microsphere-spring-boot-core"]
MSB_MVC["microsphere-spring-boot-webmvc"]
MSB_FLUX["microsphere-spring-boot-webflux"]
MSB_ACT["microsphere-spring-boot-actuator"]
AJ["aspectjweaver"]
SCC["spring-cloud-commons<br/>optional"]
SCCX["spring-cloud-context<br/>optional"]
SCLB["spring-cloud-loadbalancer<br/>optional"]
REG["registry starters<br/>eureka · nacos · zookeeper · consul<br/>optional"]
OPENFEIGN["microsphere-spring-cloud-openfeign"]
PARENT --> COMMONS
MSB_CORE --> COMMONS
MSB_MVC --> COMMONS
MSB_FLUX --> COMMONS
MSB_ACT --> COMMONS
AJ --> COMMONS
SCC -.-> COMMONS
SCCX -.-> COMMONS
SCLB -.-> COMMONS
REG -.-> COMMONS
COMMONS --> OPENFEIGN
SCCX -.-> OPENFEIGN
Mandatory edges (compile) supply the Microsphere abstractions — io.microsphere.logging.Logger,
io.microsphere.json.JSONObject, io.microsphere.util.Configurer, WebEndpointMapping,
GenericBeanPostProcessorAdapter. Optional edges are guarded at runtime by class conditions, which is
the module's central degradation strategy.
Registration is imports-file based. META-INF/spring.factories survives only as a
SpringFactoriesLoader data file (see §5.2); it no longer registers auto-configurations — that was
the mechanism of the 0.1.x line, which targeted Boot 2.3–2.6.
spring.factories (auto-configuration list) -> removed on this branch
META-INF/spring/...AutoConfiguration.imports -> authoritative, 10 lines
The registered set, in file order:
SpecificationAutoConfiguration
DiscoveryClientAutoConfiguration
ReactiveDiscoveryClientAutoConfiguration
ServiceRegistryAutoConfiguration
WebMvcServiceRegistryAutoConfiguration
WebFluxServiceRegistryAutoConfiguration
SimpleAutoServiceRegistrationAutoConfiguration
ServiceRegistrationEndpointAutoConfiguration
TomcatFaultToleranceAutoConfiguration
ConfigurationPropertyHasFeaturesAutoConfiguration
File order is not evaluation order. Boot sorts by @AutoConfigureBefore / @AutoConfigureAfter,
and this module expresses ordering through class-name variants so that a missing optional artifact
never causes a ClassNotFoundException:
| Auto-configuration | Before | After |
|---|---|---|
DiscoveryClientAutoConfiguration |
o.s.c.client.CommonsClientAutoConfiguration |
— |
ReactiveDiscoveryClientAutoConfiguration |
o.s.c.client.ReactiveCommonsClientAutoConfiguration |
SimpleReactiveDiscoveryClientAutoConfiguration, ReactiveCompositeDiscoveryClientAutoConfiguration |
SpecificationAutoConfiguration |
— | LoadBalancerAutoConfiguration, o.s.c.openfeign.FeignAutoConfiguration |
WebMvc/WebFluxServiceRegistryAutoConfiguration |
— | ServiceRegistryAutoConfiguration, SimpleAutoServiceRegistrationAutoConfiguration, io.microsphere.spring.boot.webmvc.autoconfigure.WebMvcAutoConfiguration (resp. webflux…WebFluxAutoConfiguration) |
SimpleAutoServiceRegistrationAutoConfiguration |
AutoServiceRegistrationAutoConfiguration |
UtilAutoConfiguration, AutoServiceRegistrationConfiguration, SimpleDiscoveryClientAutoConfiguration |
ServiceRegistrationEndpointAutoConfiguration |
— | the five vendor registry auto-configurations by name (Spring Cloud ServiceRegistryAutoConfiguration, Zookeeper, Consul, Kubernetes, Nacos) |
TomcatFaultToleranceAutoConfiguration |
— | TomcatWebServer, EmbeddedWebServerFactoryCustomizerAutoConfiguration, ConfigurationPropertiesRebinderAutoConfiguration |
ConfigurationPropertyHasFeaturesAutoConfiguration |
CommonsClientAutoConfiguration |
— |
FeignClientAutoRefreshAutoConfiguration (openfeign module) |
— | SpecificationAutoConfiguration — the reason commons must load first |
Two cooperating idioms:
- Class-name conditions only.
@ConditionalOnClass(name = …)with string constants fromDiscoveryClientConstants,ServiceRegistryConstants,FeaturesConstants(e.g.REGISTRATION_CLASS_NAME,FEATURES_ENDPOINT_CLASS_NAME). A registry client that is absent silently switches whole feature groups off. - Composed meta-annotations.
@ConditionalOnAutoServiceRegistrationAvailable= class present +@ConditionalOnBean(type = REGISTRATION_CLASS_NAME)+@ConditionalOnAutoServiceRegistrationEnabled. This is howWebMvcServiceRegistryAutoConfigurationrefuses to load in an app with noRegistrationbean.
ConditionalOnMultipleRegistrationEnabled and the @ConditionalOnProperty in
SimpleAutoServiceRegistrationAutoConfiguration deliberately omit matchIfMissing, making those two
features opt-in; the Tomcat listener and the features endpoint are opt-out
(matchIfMissing = true).
| Provided by upstream | Consumed here by |
|---|---|
Default-properties mechanism loading META-INF/config/default/* as lowest-priority properties (DefaultPropertiesApplicationListener, SpringApplicationDefaultPropertiesPostProcessor) |
endpoints.properties, features.yaml |
ConditionalOnWebMvcAvailable, ConditionalOnWebFluxAvailable, ConditionalOnActuatorEndpointPresent |
Web registry auto-configs, ServiceRegistrationEndpointAutoConfiguration |
WebEndpointMapping, WebEndpointMappingsReadyEvent (microsphere-spring-web) |
WebServiceRegistryAutoConfiguration, ServiceInstanceUtils |
GenericBeanPostProcessorAdapter, BeanUtils.getSortedBeans, ValueHolder, Configurer, Assert, Logger, JSONObject |
SpecificationBeanPostProcessor, UnionDiscoveryClient, TomcatDynamicConfigurationListener, … |
io.microsphere.annotation.ConfigurationProperty + microsphere-annotation-processor |
compile-time configuration metadata for every property constant |
microsphere-spring-boot-test (AutoConfigurationTest, WebAutoConfigurationTest, ReactiveWebAutoConfigurationTest) |
all auto-configuration tests |
@Configuration(proxyBeanMethods = false)
@ConditionalOnAutoServiceRegistrationEnabled
@ConditionalOnClass(name = {"org.aspectj.lang.annotation.Aspect", REGISTRATION_CLASS_NAME})
@Import({EventPublishingRegistrationAspect.class, ServiceRegistryAutoConfiguration.MultipleConfiguration.class})Two independent duties: (a) always install the event-publishing aspect; (b) conditionally install the
composite beans through the package-private nested MultipleConfiguration, gated by
@ConditionalOnMultipleRegistrationEnabled + @ConditionalOnBean({Registration.class, ServiceRegistry.class}).
All three of its beans are @Primary @Bean @ConditionalOnMissingBean, and
multipleAutoServiceRegistration(...) additionally requires an AutoServiceRegistrationProperties bean.
flowchart TD
A["microsphere.spring.cloud.multiple-registration.enabled"] --> B{"property present and not false"}
B -- no --> Z["no composite beans<br/>Single registry path"]
B -- yes --> C{"Registration bean exists"}
C -- no --> Z
C -- yes --> D{"ServiceRegistry bean exists"}
D -- no --> Z
D -- yes --> E["multipleRegistration - Collection of Registration"]
E --> F["multipleServiceRegistry - Map of bean name to ServiceRegistry"]
F --> G{"AutoServiceRegistrationProperties present"}
G -- no --> H["two composites, no auto-registration"]
G -- yes --> I["multipleAutoServiceRegistration - Primary"]
Pointcut expressions (verbatim, src/main/java/.../aspect/EventPublishingRegistrationAspect.java:51,56):
public static final String REGISTER_POINTCUT_EXPRESSION =
"execution(* org.springframework.cloud.client.serviceregistry.ServiceRegistry.register(*)) && target(registry) && args(registration)";
public static final String DEREGISTER_POINTCUT_EXPRESSION =
"execution(* org.springframework.cloud.client.serviceregistry.ServiceRegistry.deregister(*)) && target(registry) && args(registration)";Four advices, all with argNames = "registry, registration":
| Advice | Method | Publishes | Extra behaviour |
|---|---|---|---|
@Before REGISTER |
beforeRegister |
RegistrationPreRegisteredEvent |
then iterates registrationCustomizers.forEach(c -> c.customize(registration)) |
@After REGISTER |
afterRegister |
RegistrationRegisteredEvent |
— |
@Before DEREGISTER |
beforeDeregister |
RegistrationPreDeregisteredEvent |
— |
@After DEREGISTER |
afterDeregister |
RegistrationDeregisteredEvent |
— |
Every advice begins with if (isIgnored(registry)) return; where
isIgnored is MultipleServiceRegistry.class.isAssignableFrom(registry.getClass()) (source comment:
// Remove redundant deregister). Because MultipleServiceRegistry.register(...) fans out to the
individual registries, the join point matches twice — once on the composite, once on each delegate.
Ignoring the composite keeps exactly one event pair per real registry.
setApplicationContext caches applicationContext.getBeanProvider(RegistrationCustomizer.class), so
customizers are resolved lazily and @Order is honoured.
sequenceDiagram
participant AS as AbstractAutoServiceRegistration
participant MSR as MultipleServiceRegistry
participant ASP as EventPublishingRegistrationAspect
participant NR as NacosServiceRegistry
participant CTX as ApplicationContext
participant CZ as RegistrationCustomizer
AS->>MSR: register multipleRegistration
MSR->>ASP: join point on composite
ASP->>ASP: isIgnored registry -> true
ASP-->>MSR: return without publishing
MSR->>NR: special registration type resolved via registrationMap
NR->>ASP: join point on delegate
ASP->>CTX: publishEvent RegistrationPreRegisteredEvent
ASP->>CZ: customize registration
CZ-->>ASP: metadata mutated in place
ASP-->>NR: proceed
NR->>NR: actual registry call
NR->>ASP: after returning
ASP->>CTX: publishEvent RegistrationRegisteredEvent
CTX-->>AS: listeners invoked
Constructor contract: assertNotEmpty(registrations), then for every Registration:
Class<? extends Registration> clazz = (Class) ultimateTargetClass(registration); // AOP-proxy aware
List<Class<? extends Registration>> classes = (List) findAllClasses(clazz,
type -> isAssignableFrom(Registration.class, type) && !Registration.class.equals(type));
classes.forEach(type -> registrationMap.put(type, registration));
this.defaultRegistration = registration; // last winsSo registrationMap holds every supertype of the concrete class except Registration itself
(EurekaRegistration maps under EurekaRegistration, its interfaces, OrderedRegistration, …), and the
last element of the injected collection becomes the default — iteration order of the injected
Collection<Registration> therefore decides which registry is "primary".
getMetadata() returns a single shared RegistrationMetaData(registrations) built over the whole collection.
special(Class<T>) returns this when the requested class is exactly Registration, else
registrationMap.getOrDefault(clazz, null) — a null here is meaningful and is filtered by the caller.
Constructor resolves, per bean name, getRegistrationClass(ultimateTargetClass(serviceRegistry)):
Class<?> registrationClass = forClass(serviceRegistryClass).as(ServiceRegistry.class).getGeneric(0).resolve();
if (Registration.class.equals(registrationClass)) {
List<String> registrationClassNames = loadFactoryNames(serviceRegistryClass, classLoader);
for (String name : registrationClassNames) registrationClass = resolveClassName(name, classLoader);
}The if branch is the Nacos trick: NacosServiceRegistry is declared as ServiceRegistry<Registration>
in some versions, so the generic argument degrades to the plain interface and the pairing is recovered
from META-INF/spring.factories:
com.alibaba.cloud.nacos.registry.NacosServiceRegistry = \
com.alibaba.cloud.nacos.registry.NacosRegistrationThat two-line file is the module's only remaining spring.factories and exists purely as
SpringFactoriesLoader data — do not delete it while multi-registration support is documented.
Behaviour: register / deregister / setStatus all delegate to
iterate(registration, (reg, registry) -> …), which looks up beanNameToRegistrationTypesMap, calls
registration.special(type) and skips null; close() iterates every registry; getStatus(...)
reads only the default registry (the last map entry).
implements Map<String, String> over applicationMetaData (a ConcurrentHashMap) plus the
Collection<Registration>; writes are guarded by synchronized (lock):
| Operation | Fan-out | Read source |
|---|---|---|
put, remove, putAll, clear |
applied to every underlying registration's metadata first, then to applicationMetaData |
— |
get, size, isEmpty, containsKey, containsValue |
none | applicationMetaData |
keySet, values |
none | Collections.unmodifiable* views |
entrySet |
none | raw, modifiable view of applicationMetaData |
The constructor also handles the Zookeeper special case: when
ultimateTargetClass(registration).getName() equals
org.springframework.cloud.zookeeper.serviceregistry.ServiceInstanceRegistration, it reflectively calls
getServiceInstance() (GET_SERVICE_INSTANCE_METHOD_NAME) so the lazily built
ServiceInstance<ZookeeperInstance> exists before metadata is written into it.
SimpleAutoServiceRegistrationAutoConfiguration is opt-in via
microsphere.spring.cloud.service-registry.auto-registration.simple.enabled and @Imports
SimpleAutoServiceRegistration. It provides:
registration(...)→DefaultRegistration extends DefaultServiceInstance implements Registration(empty subclass), instanceIdhost + ":" + port, host frominetUtils.findFirstNonLoopbackHostInfo().getIpAddress(), port fromServerProperties;serviceRegistry()→InMemoryServiceRegistry(@ConditionalOnMissingBean), a rawServiceRegistryoverConcurrentMap<String, Registration> storagekeyed by instance id;deregisteruses the two-argremove(id, registration),close()clears, and status lives in metadata under the private key_status_.
SimpleServiceRegistry is the sibling used against SimpleDiscoveryProperties /
SimpleReactiveDiscoveryProperties: three constructors funnel into a
Map<String, List<DefaultServiceInstance>> and register/deregister mutate the per-service list
(computeIfAbsent(..., newLinkedList)); STATUS_KEY = "_status_" is public static final here.
SimpleAutoServiceRegistration extends AbstractAutoServiceRegistration<Registration> and returns the
same instance from getRegistration() and getManagementRegistration(), getConfiguration() → the
AutoServiceRegistrationProperties (contrast with MultipleAutoServiceRegistration, which returns null).
flowchart TD
A["WebEndpointMappingsReadyEvent<br/>published by microsphere-spring-web"] --> B["WebServiceRegistryAutoConfiguration.onApplicationEvent - final"]
B --> C["context.getBeanProvider Registration"]
C --> D["for each Registration"]
D --> E["newHashSet event mappings"]
E --> F["excludeMappings"]
F --> G{"isExcludedMapping"}
G -- "WebMvc: built-in spring Filter or DispatcherServlet mapping" --> X["remove"]
G -- "WebFlux: always false" --> H
G -- no --> H{"isActuatorWebEndpointMapping"}
H -- "pattern startsWith management.endpoints.web.base-path" --> X
H -- no --> I["keep"]
I --> J["ServiceInstanceUtils.attachMetadata getContextPath, registration, mappings"]
J --> K["metadata web.context-path"]
J --> L["metadata web.mappings - URL-encoded JSON array"]
The abstract base declares onApplicationEvent final and exposes three hooks:
protected abstract String getContextPath(), protected abstract boolean isExcludedMapping(WebEndpointMapping, String[]),
and the overridable protected boolean isActuatorWebEndpointMapping(WebEndpointMapping, String[]).
WebMvcServiceRegistryAutoConfiguration injects ObjectProvider<FilterRegistrationBean> and
ObjectProvider<DispatcherServletRegistrationBean> and treats /* as the default URL mapping
(DEFAULT_URL_MAPPINGS); WebFluxServiceRegistryAutoConfiguration returns "" / false.
Neither registers a @Bean — the configuration is the listener.
ServiceInstanceUtils.attachMetadata serialises each mapping with WebEndpointMapping::toJSON, joins
with "," + LINE_SEPARATOR inside [ … ], strips line separators, URL-encodes, then writes
web.context-path and web.mappings. The inverse getWebEndpointMappings(ServiceInstance) decodes and
rebuilds through WebEndpointMapping.of(Kind.valueOf(kind)), reading keys kind, id, negated,
patterns, methods, params, headers, consumes, produces and applying .negate() when needed.
setProperties(ServiceInstance, DefaultServiceInstance) copies the secure flag (this is the
0.2.x behaviour; 0.1.x copied getUri() — see the 1.x guide).
UnionDiscoveryClient(final,DiscoveryClient,ApplicationContextAware,SmartInitializingSingleton,DisposableBean):getOrder()→HIGHEST_PRECEDENCE;getInstancesconcatenates non-empty lists preserving duplicates;getServicesdeduplicates through aLinkedHashSet;getDiscoveryClients()cachesgetSortedBeans(context, DiscoveryClient.class)while excludingCompositeDiscoveryClientby class name andthis.afterSingletonsInstantiated()warms the cache,destroy()clears it.ReactiveDiscoveryClientAdapter: pure delegation fordescription,probe,getOrder, and blocking collection for the twoFlux-based reads throughstatic <T> List<T> toList(Flux<T>), which usesmono.toFuture().get()insideThrowableSupplier.execute(...)whenSchedulers.isInNonBlockingThread()andmono.block()otherwise — Reactor's non-blocking-thread guard is the reason the branch exists.DiscoveryUtilsconvertsSimpleDiscoveryProperties⇄SimpleReactiveDiscoveryProperties; the reactive side'sgetInstancesis reached viainvokeMethod(true, properties, "getInstances")because that accessor is not public API in every Spring Cloud version.
SpecificationAutoConfiguration registers SpecificationBeanPostProcessor under
@ConditionalOnBean(Specification.class) + @ConditionalOnMissingBean. The post-processor is a
GenericBeanPostProcessorAdapter<Specification> that loads getSortedBeans(beanFactory, SpecificationCustomizer.class)
in setBeanFactory and runs every customizer in processAfterInitialization(specification, beanName).
With no customizer bean the post-processor is not created at all — that is what keeps the feature inert
in applications that never register one.
flowchart TD
A["ConditionalOnFeaturesAvailable"] --> B{"spring.cloud.features.enabled not false"}
B -- no --> Z["feature disabled"]
B -- yes --> C{"FeaturesEndpoint class present"}
C -- no --> Z
C -- yes --> D{"endpoint features available"}
D -- no --> Z
D -- yes --> E["EnableConfigurationProperties FeaturesProperties"]
E --> F["afterPropertiesSet"]
F --> G["walk featuresProperties abstract map"]
F --> H["walk featuresProperties named map"]
G --> I{"resolveClass found"}
I -- no --> J["logger.warn with exact property name, skip"]
I -- yes --> K{"BeanSource.BEAN_FACTORY getBeanTypes featureClass"}
K -- "empty" --> L["add as abstract feature"]
K -- "non empty" --> M["one named feature per bean type, simple name"]
H --> N["NamedFeature moduleName colon featureName"]
L --> O["ModuleFeatures per module"]
M --> O
N --> O
O --> P["registerSingleton moduleName dot features"]
P --> Q["FeaturesEndpoint renders present and absent features"]
FeaturesProperties binds prefix microsphere.spring.cloud.features with two maps —
_abstract (Map<String, List<String>>, accessors getAbstract/setAbstract, the underscore dodging the
Java keyword) and named (Map<String, Map<String, String>>). FeaturesUtils centralises the name
algebra: getAbstractFeaturePropertyName("jdbc") → microsphere.spring.cloud.features.abstract.jdbc,
getNamedFeaturePropertyName("web","rest-template") → …named.web.rest-template,
getHasFeaturesBeanName("jdbc") → jdbc.features, getQualifierFeatureName("web","rest-template") → web:rest-template.
ModuleFeatures keeps a LinkedHashSet of abstract classes and a TreeSet<NamedFeature> ordered by
NamedFeatureComparator.INSTANCE (name, then type name); toHasFeatures() logs at INFO and returns
new HasFeatures(ofList(abstract), ofList(named)). Missing classes never fail startup — they are
warned and skipped, with the offending property name quoted.
The shipped catalogue META-INF/config/default/features.yaml declares the module's own abstract features
(UnionDiscoveryClient, ReactiveDiscoveryClientAdapter, MultipleRegistration, MultipleServiceRegistry,
MultipleAutoServiceRegistration, EventPublishingRegistrationAspect, SimpleServiceRegistry,
SimpleAutoServiceRegistration, InMemoryServiceRegistry, ServiceRegistrationEndpoint,
ServiceDeregistrationEndpoint, SpecificationBeanPostProcessor, SpecificationCustomizer,
TomcatDynamicConfigurationListener) plus entries for the upstream microsphere-spring-* modules.
AbstractServiceRegistrationEndpoint implements SmartInitializingSingleton +
ApplicationListener<WebServerInitializedEvent>, resolving ObjectProviders for Registration,
ServiceRegistry and AbstractAutoServiceRegistration in afterSingletonsInstantiated()
(getIfAvailable() may yield null), storing the port on WebServerInitializedEvent, and computing
running through static boolean detectRunning(...) into a protected static boolean running field —
shared JVM-wide between the two endpoints. ServiceRegistrationEndpoint (@Endpoint(id = "serviceRegistration"))
offers @ReadOperation Map<String,Object> metadata() and @WriteOperation boolean start() (returns the
previous state); ServiceDeregistrationEndpoint offers @WriteOperation boolean stop(). Reflection is
used for isEnabled, getPhase, getOrder, getManagementRegistration, getConfiguration, each guarded
to null on error. META-INF/config/default/endpoints.properties ships both endpoints disabled and
path-mapped to microsphere/service/registration and microsphere/service/deregistration.
TomcatFaultToleranceAutoConfiguration has no beans: on WebServerInitializedEvent it checks
ConfigurableWebServerApplicationContext → ObjectProvider<ServerProperties>.ifAvailable →
webServer instanceof TomcatWebServer, and only then context.addApplicationListener(new TomcatDynamicConfigurationListener(...)).
The listener's onApplicationEvent pipeline: isSourceFrom(event) (identity against the stored context,
trace log otherwise) → filterServerPropertyNames (keys starting with server) → configureTomcatIfChanged.
The "refreshable" ServerProperties source is chosen by constructor-computed
configurationPropertiesRebinderPresent = isBeanPresent(context, ConfigurationPropertiesRebinder.class):
when a rebinder exists the live bean is trusted; otherwise only the changed keys are re-bound
(bind(getProperties(environment, names), "server", ServerProperties.class)). After applying,
currentServerProperties is re-snapshotted from getSubProperties(environment, "server") bound to
ServerProperties — that snapshot is the "compare" side of every change.
Each setting goes through the fluent Configurer:
configure("Tomcat connection timeout(ms)")
.value(refreshableTomcatProperties::getConnectionTimeout)
.compare(currentTomcatProperties::getConnectionTimeout)
.as(this::toIntMillis)
.on(this::isPositive)
.apply(protocol::setConnectionTimeout);i.e. write only when changed, convertible and positive (isPositive(int) { return value > 0; },
toIntMillis(Duration), toIntBytes(DataSize)).
| Handler | Setting | Source | Target |
|---|---|---|---|
AbstractProtocol |
core threads | Tomcat.getThreads().getMinSpare() |
setMinSpareThreads |
AbstractProtocol |
max threads | Tomcat.getThreads().getMax() |
setMaxThreads |
AbstractProtocol |
accept count | Tomcat::getAcceptCount |
setAcceptCount |
AbstractProtocol |
connection timeout | Tomcat::getConnectionTimeout |
setConnectionTimeout |
AbstractProtocol |
max connections | Tomcat::getMaxConnections |
setMaxConnections |
AbstractHttp11Protocol |
max HTTP request header size | ServerProperties::getMaxHttpRequestHeaderSize |
setMaxHttpHeaderSize |
AbstractHttp11Protocol |
max swallow size | Tomcat::getMaxSwallowSize |
setMaxSwallowSize |
AbstractHttp11Protocol |
max form POST size | Tomcat::getMaxHttpFormPostSize |
connector::setMaxPostSize |
port, ssl.* and the context path are not in the list — they cannot be applied to a live connector.
The accessor name here is Boot 3-specific (getMaxHttpRequestHeaderSize), one of the three real deltas
between 0.2.x and 0.1.x.
WeightedRoundRobin (String id, volatile int weight, LongAdder current, volatile long lastUpdate;
increaseCurrent() adds the weight, sel(total) subtracts it, setWeight(int) resets current) and
LoadBalancerUtils.calculateWarmupWeight(uptime, warmup, weight) = round(pow(uptime/warmup, 2) * weight)
clamped to [1, weight] are library code only: no @Configuration in this module wires them, and
microsphere.spring.cloud.fault-tolerance.warmup-time (default 600000) / …weight (default 100) are
not read here. @ConditionalOnLoadBalancerEnabled and @ConditionalOnUtilEnabled are likewise declared but
unused in this module — they exist for downstream consumers such as openfeign.
| Pattern | Classes | Notes |
|---|---|---|
| Decorator | ReactiveDiscoveryClientAdapter, MultipleRegistration |
Wraps a delegate while keeping the Spring Cloud interface contract |
| Composite | MultipleRegistration, MultipleServiceRegistry, RegistrationMetaData, CompositedRequestInterceptor (openfeign) |
Uniform Registration / ServiceRegistry façade over N delegates |
| Observer / event-driven | 4 Registration*Event classes + EventPublishingRegistrationAspect, ServiceInstancesChangedEvent, WebEndpointMappingsReadyEvent, EnvironmentChangeEvent listeners |
AOP is the producer; the context is the bus |
| Template Method | WebServiceRegistryAutoConfiguration (onApplicationEvent final; getContextPath / isExcludedMapping abstract), AbstractServiceRegistrationEndpoint |
Subclasses fill only the web-stack-specific hooks |
| Null Object | NoOpRequestInterceptor (openfeign), SimpleServiceRegistry.close() no-op, MultipleAutoServiceRegistration.getConfiguration() → null |
Neutral values instead of exceptions |
| Registry | InMemoryServiceRegistry, SimpleServiceRegistry, FeignComponentRegistry (openfeign) |
Instance-id or client-name keyed stores |
| Strategy | RegistrationCustomizer, SpecificationCustomizer, ComponentType resolution in Decorated* |
Pluggable per-bean behaviour |
| Post-Processor | SpecificationBeanPostProcessor over GenericBeanPostProcessorAdapter |
Cross-cutting init hook without bean modification |
| Marker-class gating | EnableFeignAutoRefresh.Marker + @ConditionalOnBean; ConditionalOnClass(name = …) constants |
Feature toggles without attributes |
| Fluent command / guarded write | io.microsphere.util.Configurer in TomcatDynamicConfigurationListener |
value → compare → as → on → apply |
| Factory method (reflective) | DecoratedFeignComponent.instantiate, WebEndpointMapping.of usage in ServiceInstanceUtils |
Type-safe construction with runtime class resolution |
| Property | Type | Default | Consumed by |
|---|---|---|---|
spring.cloud.service-registry.auto-registration.enabled |
boolean | true |
ConditionalOnAutoServiceRegistrationEnabled |
spring.cloud.features.enabled |
boolean | true |
ConditionalOnFeaturesEnabled |
spring.cloud.loadbalancer.enabled |
boolean | true |
ConditionalOnLoadBalancerEnabled (declared, unused here) |
spring.cloud.util.enabled |
boolean | true |
ConditionalOnUtilEnabled (declared, unused here) |
microsphere.spring.cloud.multiple-registration.enabled |
boolean | opt-in | ConditionalOnMultipleRegistrationEnabled |
microsphere.spring.cloud.service-registry.auto-registration.simple.enabled |
boolean | opt-in | SimpleAutoServiceRegistrationAutoConfiguration |
microsphere.spring.cloud.client.discovery.mode |
String | unset, needs union |
DiscoveryClientAutoConfiguration.UnionConfiguration |
microsphere.spring.cloud.fault-tolerance.tomcat.enabled |
boolean | true |
TomcatFaultToleranceAutoConfiguration |
microsphere.spring.cloud.features.abstract.* / .named.* |
lists / maps | from features.yaml |
FeaturesProperties |
microsphere.spring.cloud.default-registration.type · .default-service-registry.type |
Class | unset | constants only — declared and used as test properties, not read in main code |
microsphere.spring.cloud.composite-registration.enabled |
boolean | false |
constant only |
management.endpoints.web.base-path |
String | /actuator |
WebServiceRegistryAutoConfiguration |
server.servlet.context-path |
String | empty | WebMvcServiceRegistryAutoConfiguration |
management.endpoint.serviceRegistration/serviceDeregistration.enabled |
boolean | false |
shipped defaults in endpoints.properties |
# whole reactor, default train
./mvnw -pl microsphere-spring-cloud-commons -am clean test
# against another release train
./mvnw -pl microsphere-spring-cloud-commons -am -P spring-cloud-2022 test
# enable the Testcontainers integration tests (Nacos + Eureka via docker compose)
./mvnw -pl microsphere-spring-cloud-commons -am -P test,coverage,testcontainers test
# one test class
./mvnw -pl microsphere-spring-cloud-commons test -Dtest=MultipleRegistrationTestTest infrastructure in use:
AutoConfigurationTest<A>/WebAutoConfigurationTest<A>/ReactiveWebAutoConfigurationTest<A>frommicrosphere-spring-boot-test. Subclasses overrideconfigureAutoConfiguredClasses(Set<Class<?>>)(expected beans),configureGlobalDisabledPropertyValues(Set<String>)(the property that must disable the auto-configuration) andconfigureGlobalMissingClasses(Set<Class<?>>)(classes whose absence must disable it). SeeServiceRegistryAutoConfigurationTest, which expectsEventPublishingRegistrationAspectand disables onspring.cloud.service-registry.auto-registration.enabled=falseor a missingAspect/Registration.*IntegrationTestclasses boot a real context (@SpringBootTest/@ExtendWith(SpringExtension.class)+@EnableAutoConfiguration); registry-backed ones are double-gated with@EnabledIfSystemProperty(named = "testcontainers.enabled", matches = "true")and@EnabledIfDockerAvailable, and startComposeContainerfromsrc/test/resources/META-INF/docker/service-registry-servers.yml.- Naming is enforced by the inherited surefire config:
**/*Test.java,**/*Tests.javaare executed,**/Abstract*.javais excluded — henceBaseServiceRegistrationEndpointTestis a shared base, not a test.
Source constraints when contributing: the bytecode target is 17 but the style is Java 8 — no records,
no var, no pattern-matching instanceof, no text blocks. Non-JSL logging and JSON go through
io.microsphere.logging.Logger and io.microsphere.json.*. The module's only Servlet import is
jakarta.servlet.Filter in WebMvcServiceRegistryAutoConfiguration.
| Seam | How to use | Ordering |
|---|---|---|
RegistrationCustomizer#customize(Registration) |
Register a bean; runs in the @Before register advice, before the registry call |
@Order / Ordered honoured by getBeanProvider |
SpecificationCustomizer#customize(Specification, String) |
Register a bean to mutate every NamedContextFactory.Specification — the mechanism openfeign uses |
getSortedBeans, executed by SpecificationBeanPostProcessor |
ServiceInstancesChangedEvent |
Publish/observe per-service instance changes; processed() marks idempotency |
plain event bus |
| Feature catalogue | Add lines to META-INF/config/default/features.yaml under microsphere.spring.cloud.features.abstract.<module> |
bound by FeaturesProperties |
| Registry pairing | Add a spring.factories entry <ServiceRegistry impl> = <Registration impl> when generics cannot express it |
read by MultipleServiceRegistry.getRegistrationClass |
| New optional feature | New auto-configuration + a .imports line + @ConditionalOnClass(name = …) + an entry in features.yaml + an AutoConfigurationTest subclass |
Boot sorting via AutoConfigure* |
| Caveat | Consequence |
|---|---|
| "last bean wins" | MultipleRegistration.defaultRegistration and MultipleServiceRegistry.defaultServiceRegistry depend on bean iteration order; deterministic behaviour requires explicit ordering |
static running flag in AbstractServiceRegistrationEndpoint |
Shared across both endpoints and across contexts in one JVM |
RegistrationMetaData.entrySet() is modifiable while keySet()/values() are not |
Callers mutating entrySet() bypass the synchronized fan-out |
Javadoc of ConditionalOnFeaturesAvailable mentions microsphere.spring.cloud.features.enabled |
The bound key is spring.cloud.features.enabled; fix the comment, never the constant |
microsphere.spring.cloud.default-registration.type is not read |
Multi-registration ordering is positional today; implementing it is an open task |
Aspect ignores only MultipleServiceRegistry |
A custom composite registry would publish duplicate events |