microsphere-java is a Java utility library — a reusable toolkit that makes everyday Java programming tasks easier. Think of it like a Swiss Army knife: it doesn't do one big thing, it gives you many small, well-crafted tools. It contains ~340 Java source files organized into focused modules and packages.
The project is split into several Maven modules (sub-projects):
| Module | What it does |
|---|---|
microsphere-java-annotations |
Custom Java annotations (labels you attach to code) |
microsphere-java-core |
The main utility library |
microsphere-java-test |
Test utilities |
microsphere-annotation-processor |
Compile-time annotation processing |
microsphere-jdk-tools |
JDK compiler tools |
microsphere-lang-model |
Language model utilities |
What it is: Java annotations are like sticky notes you attach to your code. These custom ones communicate intent to developers.
Key annotations:
@Since("1.0.0")— marks when a class/method was introduced (like a changelog label)@Experimental— warns "this API might change"@Immutable— signals an object won't change after creation@Nullable/@Nonnull— tells readers (and tools) whether a value can benull
// Example: Mark a method as introduced in version 1.0
@Since("1.0.0")
public void myMethod() { ... }
// Warn a caller that null might come back
@Nullable
public String findUser(int id) { ... }What it is: An implementation of the Observer pattern — when something happens (an event), anyone who cares (a listener) gets notified automatically.
Key classes:
Event— the base class for every event; records the timestamp automaticallyEventListener<E>— an interface you implement to react to events; supports priority (lower number = higher priority)EventDispatcher— the hub that registers listeners and fires events; two flavors:DirectEventDispatcher— notifies listeners one-by-one in the same threadParallelEventDispatcher— notifies listeners using a thread pool (in parallel)
AbstractEventDispatcher— the shared logic; caches which listener handles which event type
// 1. Define an event
class UserLoggedIn extends Event {
public UserLoggedIn(Object source) { super(source); }
}
// 2. Define a listener
class WelcomeListener implements EventListener<UserLoggedIn> {
public void onEvent(UserLoggedIn event) {
System.out.println("Welcome! Logged in at: " + event.getTimestamp());
}
}
// 3. Wire them up
EventDispatcher dispatcher = EventDispatcher.newDefault();
dispatcher.addEventListener(new WelcomeListener());
dispatcher.dispatch(new UserLoggedIn("myApp")); // triggers WelcomeListenerWhat it is: Small but powerful interfaces and classes that improve code design.
Prioritized— anything that needs ordering (lower integer = first). Used by event listeners, converters, etc.Wrapper— lets one object "wrap" another and exposeunwrap(MyType.class)to retrieve the inner object. Very common in proxy/decorator patterns (like JDBCConnectionwrappers).DelegatingWrapper— a default wrapper that delegates all calls to the wrapped object.MutableInteger— a mutableintholder (handy in lambdas where variables must be "effectively final").
// Prioritized: higher-priority tasks run first
class HighPriorityTask implements Prioritized {
public int getPriority() { return Prioritized.MAX_PRIORITY; } // runs first
}What it is: Java's standard Function, Consumer, Supplier etc. cannot declare checked exceptions. These classes solve that limitation.
ThrowableFunction<T,R>— likeFunction<T,R>butapply()can throw any exceptionThrowableConsumer<T>— likeConsumer<T>but can throwThrowableSupplier<T>— likeSupplier<T>but can throwThrowableAction— likeRunnablebut can throwStreams/Predicates— stream/filter helpers
// Parse a file path that might throw IOException
ThrowableFunction<String, byte[]> reader = path -> Files.readAllBytes(Paths.get(path));
// Safe execution: wraps checked exception in RuntimeException
byte[] data = reader.execute("/some/file.txt");
// Or with a fallback:
byte[] data = reader.execute("/some/file.txt", (path, ex) -> new byte[0]);What it is: Utility methods and classes on top of Java's built-in collections.
Key utilities:
CollectionUtils—isEmpty(),isNotEmpty(),asIterable(), etc.MapUtils/ListUtils/SetUtils— helpers for Maps, Lists, SetsArrayStack— a stack backed by an ArrayListSingletonIterator/SingletonDeque— collections holding exactly one elementUnmodifiableDeque/ReadOnlyIterator— unmodifiable wrappersDelegatingIterator/DelegatingDeque— decorators that forward calls to an inner object
List<String> list = null;
if (CollectionUtils.isEmpty(list)) {
System.out.println("Nothing here"); // safe — no NullPointerException
}What it is: A flexible strategy for converting values from one type to another (e.g., String → Integer).
Converter<S,T>— functional interface: given a source typeS, produce a target typeTConverters— discovers all registered converters via Java's SPI (Service Provider Interface)- Multiple converters can coexist;
Prioritizeddecides which one wins
// Convert a String to an Integer if a converter is registered
Integer value = Converter.convertIfPossible("42", Integer.class);What it is: A broad set of helper classes for common operations.
Major utilities:
StringUtils— string checks, manipulationsClassUtils— class loading, type checking, hierarchy traversalAnnotationUtils— reading and searching annotations on classes/methodsArrayUtils— array length, containment, etc.ExceptionUtils/ThrowableUtils— wrap/unwrap exceptions cleanlyServiceLoaderUtils— load SPI services with error handlingStopWatch— measure elapsed time for profilingVersion/VersionUtils— parse and compare1.2.3-style version stringsShutdownHookUtils— register cleanup code that runs on JVM shutdown
// Time how long something takes
StopWatch sw = new StopWatch();
sw.start("my task");
// ... do work ...
sw.stop();
System.out.println(sw.prettyPrint());
// Compare versions
Version v1 = Version.of("1.2.3");
Version v2 = Version.of("1.3.0");
System.out.println(v1.compareTo(v2)); // negative: v1 < v2What it is: Java reflection lets you inspect and call classes/methods/fields at runtime. These utilities wrap the boilerplate and handle edge cases.
Key classes:
MethodUtils/FieldUtils/ConstructorUtils— find and invoke methods/fields/constructors safelyTypeUtils— generic type resolution (e.g., "what is theTinList<T>?")ReflectionUtils— common reflection helpersProxyUtils— work with JDK dynamic proxies
// Find a method by name and invoke it
Method m = MethodUtils.findMethod(MyClass.class, "doSomething", String.class);
Object result = MethodUtils.invokeMethod(instance, m, "hello");What it is: Helpers for reading files, scanning classpaths, and filtering resources.
io.scanner— scan classpath entries (JARs, directories) for classes or resourcesio.filter— file/resource filtersio.event— file change events (e.g., watch a directory)
What it is: A thin abstraction over whatever logging framework is on the classpath (SLF4J, JUL, etc.).
Logger logger = LoggerFactory.getLogger(MyClass.class);
logger.info("Starting up...");What it is: Thread-safe helpers and ThreadLocal tools for concurrent programs.
What it is: URL/URI building utilities, classpath URL handlers, and console URL handlers.
What it is: Lightweight JSON parsing and creation (JSONObject, JSONArray, JSONTokener) — a local, dependency-free JSON library similar to org.json.
What it is: Reads and generates Spring-Boot-style additional-spring-configuration-metadata.json configuration property metadata, allowing IDE auto-completion for configuration keys.
| Term | Plain English |
|---|---|
| Interface | A contract: defines what methods must exist, not how |
@FunctionalInterface |
An interface with exactly one method — can be used as a lambda |
Generics (<T>) |
Placeholder for a type that gets filled in later — avoids casts |
| SPI (Service Provider Interface) | A Java mechanism to load implementations by placing a file in META-INF/services/ |
| Observer Pattern | Publisher fires events; subscribers (listeners) react — they're decoupled |
| Wrapper/Decorator Pattern | Wrap an object to add or change behavior without modifying the original |
| Reflection | Inspect or invoke classes/methods at runtime, even if you don't know them at compile time |
- Add custom annotations to your API (use
microsphere-java-annotations) to document stability, nullability, and version history. - Publish/subscribe to events within a JVM (use
EventDispatcher) without a message broker. - Safely call code that throws checked exceptions inside streams or lambdas (use
ThrowableFunction). - Convert types in a pluggable, prioritized way (use
Converter). - Traverse class hierarchies / read generics at runtime (use
TypeUtils,ClassUtils). - Measure performance during debugging (use
StopWatch). - Sort tasks/listeners by priority (implement
Prioritized).
// 1. Define a typed event
class OrderPlaced extends Event {
private final String orderId;
public OrderPlaced(Object source, String orderId) {
super(source);
this.orderId = orderId;
}
public String getOrderId() { return orderId; }
}
// 2. Implement a listener with HIGH priority
class AuditListener implements EventListener<OrderPlaced> {
public void onEvent(OrderPlaced e) {
System.out.println("AUDIT: order " + e.getOrderId() + " at " + e.getTimestamp());
}
public int getPriority() { return Prioritized.MAX_PRIORITY; } // runs first
}
// 3. Wire up and fire
EventDispatcher dispatcher = EventDispatcher.newDefault();
dispatcher.addEventListener(new AuditListener());
dispatcher.dispatch(new OrderPlaced(this, "ORD-001"));
// → AUDIT: order ORD-001 at 1716726766000This library's design keeps individual pieces small, testable, and composable — a great pattern to study when learning professional Java development.
This section assumes solid Java knowledge (generics, lambdas, streams, SPI) and focuses on how to integrate microsphere-java components into real applications.
The Converter<S,T> system is fully open/closed: you add new conversions without touching existing code.
Step 1 — Implement Converter:
package com.example.convert;
import io.microsphere.convert.Converter;
import io.microsphere.lang.Prioritized;
// Generic type parameters are read at runtime via reflection —
// you must provide concrete types, not raw/wildcard types.
public class UUIDToStringConverter implements Converter<UUID, String> {
@Override
public String convert(UUID source) {
return source == null ? null : source.toString();
}
// Override to outrank the built-in ObjectToStringConverter
@Override
public int getPriority() {
return Prioritized.MAX_PRIORITY + 1; // very high priority
}
}Step 2 — Register via SPI:
Create src/main/resources/META-INF/services/io.microsphere.convert.Converter and add one class name per line:
com.example.convert.UUIDToStringConverter
Step 3 — Use it:
// Lookup by type pair (cached after first call)
Converter<UUID, String> c = Converter.getConverter(UUID.class, String.class);
String s = c.convert(UUID.randomUUID());
// Or the one-liner (returns null if no converter found)
String s2 = Converter.convertIfPossible(UUID.randomUUID(), String.class);Priority resolution: multiple converters for the same <S,T> pair are sorted by getPriority(). Lower integer value wins (executes first). Prioritized.MAX_PRIORITY is Integer.MIN_VALUE (highest), Prioritized.NORMAL_PRIORITY is 0, Prioritized.MIN_PRIORITY is Integer.MAX_VALUE.
Sometimes a listener only cares about a subset of events of the same type. Implement ConditionalEventListener to filter at the dispatcher level — onEvent is never called for rejected events.
import io.microsphere.event.ConditionalEventListener;
import io.microsphere.event.Event;
import io.microsphere.event.EventListener;
public class CriticalOrderListener
implements EventListener<OrderPlaced>, ConditionalEventListener {
// Called before onEvent; return false to skip this listener
@Override
public boolean accept(Event event) {
OrderPlaced order = (OrderPlaced) event;
return order.getAmount().compareTo(BigDecimal.valueOf(10_000)) > 0;
}
@Override
public void onEvent(OrderPlaced event) {
// Only called for orders > 10,000
alertRiskTeam(event);
}
}Auto-load listeners via SPI by creating META-INF/services/io.microsphere.event.EventListener:
com.example.CriticalOrderListener
Listeners registered via SPI are loaded automatically by AbstractEventDispatcher during construction.
ThrowableFunction<T,R> supports andThen / compose just like java.util.function.Function, but both sides may throw:
ThrowableFunction<String, Path> toPath = Paths::get;
ThrowableFunction<Path, byte[]> readFile = Files::readAllBytes;
ThrowableFunction<byte[], String> decode = b -> new String(b, StandardCharsets.UTF_8);
// Build a pipeline: String -> Path -> byte[] -> String
ThrowableFunction<String, String> readText = toPath.andThen(readFile).andThen(decode);
// Execute with a fallback instead of a RuntimeException
String content = readText.execute("/etc/hosts", (path, ex) -> {
log.warn("Cannot read {}: {}", path, ex.getMessage());
return "";
});Use ThrowableAction for void operations that can throw, and ThrowableSupplier<T> when there is no input:
ThrowableAction action = connection::close; // Closeable in a lambda
ThrowableSupplier<Config> loader = () -> Config.load("/app.yml");
ThrowableAction.execute(action, ex -> log.error("Close failed", ex));
Config cfg = loader.execute(ex -> Config.defaults());Use Wrapper when you build decorator/proxy chains and callers need to "look through" the chain to get the real underlying object (classic JDBC pattern):
// Implement DelegatingWrapper — only getDelegate() is required
public class MetricsDataSource implements DataSource, DelegatingWrapper {
private final DataSource delegate;
public MetricsDataSource(DataSource delegate) {
this.delegate = delegate;
}
@Override
public Object getDelegate() { return delegate; }
@Override
public Connection getConnection() throws SQLException {
long start = System.nanoTime();
try {
return delegate.getConnection();
} finally {
metrics.record(System.nanoTime() - start);
}
}
// ... rest of DataSource ...
}A caller that holds a MetricsDataSource can transparently reach the real DataSource:
DataSource real = Wrapper.tryUnwrap(metricsDs, HikariDataSource.class);
// real != null if the delegate is a HikariDataSource (or itself implements Wrapper)TypeUtils solves a common reflection problem: "given an instance of MyRepo extends JpaRepository<User, Long>, what are the entity and ID types?"
// 1. Resolve a single type argument by index
Class<?> entityType = TypeUtils.resolveActualTypeArgumentClass(
MyRepo.class, // the concrete class
JpaRepository.class, // the generic base
0); // argument index (0 = first)
// entityType == User.class
// 2. Resolve all type arguments
List<Class> args = TypeUtils.resolveActualTypeArgumentClasses(
MyRepo.class, JpaRepository.class);
// [User.class, Long.class]
// 3. Check at Type level before casting
Type t = TypeUtils.resolveActualTypeArgument(MyRepo.class, JpaRepository.class, 0);
if (TypeUtils.isParameterizedType(t)) { ... }Results are cached in an LRU cache (default size 256, tunable via system property microsphere.reflect.resolved-generic-types.cache.size).
// All interfaces, including inherited ones
List<Class<?>> interfaces = ClassUtils.getAllInterfaces(ArrayList.class);
// All superclasses up to Object
List<Class<?>> supers = ClassUtils.getAllSuperClasses(ArrayList.class);
// Everything together (superclasses + interfaces)
List<Class<?>> hierarchy = ClassUtils.getHierarchy(ArrayList.class);microsphere-java-test provides pre-built model classes for testing generic type resolution and annotation processing:
// Pre-built type models for parameterized-type tests
StringArrayList list = new StringArrayList(); // extends ArrayList<String>
Class<?> t = TypeUtils.resolveActualTypeArgumentClass(
StringArrayList.class, List.class, 0);
// t == String.class ✓
// Base class for annotation-processor unit tests
class MyProcessorTest extends AbstractAnnotationProcessingTest {
@Test
public void testOutput() {
// Compiler is wired automatically; just verify generated files
}
}This section targets architects and library authors who need to extend or build on top of microsphere-java. It covers internal design, thread-safety guarantees, extension points, and system-level tunables.
microsphere-java is built around a small set of cross-cutting patterns:
| Pattern | Where used |
|---|---|
| SPI (Service Provider Interface) | Converter, EventListener, URLStreamHandlerFactory, all service lookups |
| Template Method | AbstractEventDispatcher — subclasses provide only the Executor |
| Strategy | Converter<S,T>, EventDispatcher implementations, URLStreamHandlerFactory |
| Wrapper / Decorator | Wrapper, DelegatingWrapper, DelegatingIterator, UnmodifiableDeque |
| Composite | CompositeURLStreamHandlerFactory — chains multiple factories in priority order |
| Visitor | ConfigurationPropertyJSONElementVisitor — walks the annotation-processor AST |
| Factory Method | EventDispatcher.newDefault(), EventDispatcher.parallel(Executor) |
| Prioritized Ordering | Prioritized + Prioritized.COMPARATOR — used everywhere SPI lists are sorted |
Listener storage:
ConcurrentMap<Class<? extends Event>, List<EventListener>> listenerCache
The key is the event type, not the listener type. The map is populated lazily: when the first event of a given type is dispatched, the dispatcher scans all registered listeners for those whose generic type argument E is assignment-compatible with the event type, then caches that filtered, priority-sorted list.
Mutation path (add/remove listener):
synchronized(mutex) {
modify listeners list
sort(listeners) // O(n log n) on every mutation, not on every dispatch
invalidate listenerCache
}
Because sorting happens at mutation time, dispatch itself is a read-only path with no locking beyond the ConcurrentMap read.
Dispatch path:
executor.execute(() -> {
for (EventListener listener : sortedListeners(event.getClass())) {
if (listener instanceof ConditionalEventListener) {
if (!((ConditionalEventListener) listener).accept(event)) continue;
}
listener.onEvent(event);
}
})
Extending AbstractEventDispatcher:
public class MdcEventDispatcher extends AbstractEventDispatcher {
private final Executor executor;
public MdcEventDispatcher(Executor executor) {
this.executor = executor;
}
@Override
protected Executor getExecutor() {
// Wrap the executor to propagate MDC context
return command -> executor.execute(() -> {
Map<String, String> ctx = MDC.getCopyOfContextMap();
try { command.run(); }
finally { if (ctx != null) MDC.setContextMap(ctx); else MDC.clear(); }
});
}
}Converters maintains a two-level lookup structure:
static ConcurrentMap<Entry<Class<?>, Class<?>>, List<Converter>> converterCache
- Bootstrap: at first access,
ServiceLoaderUtils.loadServicesList(Converter.class)loads and sorts all registered converters. - Lookup for
<S,T>: filters the master list to those whereconverter.accept(S, T)returns true. The defaultacceptimplementation comparesgetSourceType()/getTargetType()(both resolved viaTypeUtils.resolveActualTypeArgumentClass). - Result: the first entry (highest priority) is returned and the filtered list is cached under the
<S,T>key.
Custom converters added after bootstrap will not appear in an already-cached <S,T> entry. If dynamic registration is required, call Converters.invalidate() (if available) or restart the Converters singleton state.
microsphere-java exposes a URLStreamHandler extension mechanism that supports nested / sub-protocol URLs like:
classpath:com/example/config.yml
jar:classpath:lib/my.jar!/META-INF/MANIFEST.MF
Extension SPI:
Implement URLStreamHandlerFactory and register it via:
META-INF/services/java.net.URLStreamHandlerFactory
→ com.example.net.MyProtocolHandlerFactory
ServiceLoaderURLStreamHandlerFactory.attach() installs a global factory that:
- Loads all
URLStreamHandlerFactorySPI implementations (priority-ordered). - Falls back to
ExtendableProtocolURLStreamHandlerfor sub-protocol chaining.
Sub-protocol handler:
public class ClasspathURLStreamHandler extends ExtendableProtocolURLStreamHandler {
@Override
protected URLConnection openSubProtocolConnection(URL u, URLStreamHandler subHandler)
throws IOException {
// resolve 'classpath:' to a jar URL and open it
}
}Register under META-INF/services/io.microsphere.net.ExtendableProtocolURLStreamHandler.
microsphere-annotation-processor generates Spring-Boot-compatible configuration metadata JSON at compile time.
Attach the processor (Maven):
<dependency>
<groupId>io.github.microsphere-projects</groupId>
<artifactId>microsphere-annotation-processor</artifactId>
<scope>provided</scope>
</dependency>Annotate configuration holders:
@ConfigurationProperty(
name = "app.cache.ttl",
defaultValue = "300",
description = "Cache TTL in seconds",
source = ConfigurationProperty.SYSTEM_PROPERTIES_SOURCE
)
public class CacheConfig { ... }At compile time, ConfigurationPropertyAnnotationProcessor walks the annotation tree via ConfigurationPropertyJSONElementVisitor and writes:
META-INF/configuration-property-metadata.json
This file is picked up by Spring Boot's IDE support for auto-completion of application.properties / application.yml keys.
Writing a custom annotation processor that uses the test harness:
public class MyProcessorTest extends AbstractAnnotationProcessingTest {
@Override
protected Processor processor() {
return new MyAnnotationProcessor();
}
@Test
public void testGeneratedOutput() throws Exception {
// compile test sources, processor runs automatically
// assert generated files exist and have expected content
}
}| Component | Thread-safety guarantee |
|---|---|
AbstractEventDispatcher — dispatch |
Lock-free read from ConcurrentMap; safe for concurrent dispatch |
AbstractEventDispatcher — add/remove |
Synchronized on internal mutex; blocks during mutation |
Converters cache |
ConcurrentMap; lazy population is idempotent (same result if races occur) |
ServiceLoaderUtils |
Optional LRU cache guarded internally; disable with microsphere.service-loader.cached=false |
TypeUtils generic cache |
LRU with configurable size; safe for concurrent reads |
ParallelEventDispatcher |
Listener callbacks run concurrently — listeners must be thread-safe |
ParallelEventDispatcher — executor lifecycle:
// Use a managed pool; register shutdown on JVM exit
ExecutorService pool = Executors.newFixedThreadPool(4,
new CustomizedThreadFactory("event-dispatch"));
ExecutorUtils.shutdownOnExit(pool); // registers ShutdownHook
EventDispatcher dispatcher = EventDispatcher.parallel(pool);These system properties control caches and loader behaviour:
| Property | Default | Effect |
|---|---|---|
microsphere.service-loader.cached |
true |
Set to false to disable ServiceLoader result caching |
microsphere.reflect.resolved-generic-types.cache.size |
256 |
LRU cache size for TypeUtils generic resolution |
Set via JVM argument: -Dmicrosphere.service-loader.cached=false
Prioritized is a general-purpose ordering contract. Adopt it in your own SPIs to get automatic priority-based sorting for free:
// Your SPI
public interface DataValidator extends Prioritized {
ValidationResult validate(Object data);
}
// Registration and lookup
List<DataValidator> validators = ServiceLoaderUtils.loadServicesList(DataValidator.class);
// validators are already sorted: lowest getPriority() value first
// Run in order
for (DataValidator v : validators) {
ValidationResult r = v.validate(data);
if (!r.isValid()) break; // short-circuit on first failure
}Built-in Prioritized.COMPARATOR works on any Object (non-Prioritized objects are treated as equal to each other and placed after all Prioritized objects), so it can be used as a general-purpose Comparator for heterogeneous lists.