DEV Community

Solon Framework
Solon Framework

Posted on

Solon's Hidden Toolkit: Core Utility Classes Worth Using in Your Own Code

Solon keeps its kernel small — the core is around 0.3 MB — and most of that is beans, routing, and the plugin system. But nested inside is a package you may never have opened: org.noear.solon.core.util. The official docs describe these classes as "mainly for framework-internal development" and even suggest using external libraries when possible. Fair enough. Still, a handful of them are genuinely useful in application code — zero-dependency, AOT-friendly (they're built for GraalVM), and always in sync with the framework you're already running.

Here are the ones I actually reach for, with APIs verified against the Solon source (v4.0.5).

1. ResourceUtil — load, find, and scan resources

ResourceUtil is the class I use most. The three examples from the official docs cover the common cases:

// Get a single resource as a URL
URL one = ResourceUtil.getResource("demo.json");

// Get a resource and read it as a String
String rst = ResourceUtil.getResourceAsString("demo.json");

// Scan a batch of resources, supporting ** and * wildcards
Collection<String> list = ResourceUtil.scanResources("classpath:demo/**/*.json");
Enter fullscreen mode Exit fullscreen mode

The findResource family adds schema awareness — it understands classpath: and file: prefixes and falls back sensibly:

// Tries "file:" first when no prefix is given
URL a = ResourceUtil.findResource("./config/app.yml");
// Classpath explicitly
URL b = ResourceUtil.findResource("classpath:app.yml");
// External first, then internal (useful for override mechanisms)
URL c = ResourceUtil.findResourceOrFile("app.yml");
Enter fullscreen mode Exit fullscreen mode

And findResourceAsString does the same in one shot. Note that getResourceAsString throws IOException and returns null when the resource is missing — both behaviors are worth knowing before you wrap it.

2. ClassUtil — optional dependencies, safe loading, and class scanning

Class loading is where Java apps usually blow up at runtime. ClassUtil gives you null-safe variants that make optional features much easier to write.

// Check whether an optional class exists — no ClassNotFoundException to catch
if (ClassUtil.hasClass(() -> org.postgresql.Driver.class)) {
    // register the Postgres-specific handler
}

// Load a class by name; returns null instead of throwing
Class<?> clz = ClassUtil.loadClass("com.example.maybe.Missing");
if (clz != null) { ... }

// Instantiate by class name; returns null if the class isn't there
SomePlugin plugin = ClassUtil.tryInstance("com.example.plugins.Extra");
Enter fullscreen mode Exit fullscreen mode

The real gem is scanClasses, which scans using an import-style expression — the same mechanism the framework itself uses for component discovery:

// All classes in a package
Collection<Class<?>> all = ClassUtil.scanClasses("com.example.handlers.*");

// Nested packages, with a suffix filter — this is where it shines
Collection<Class<?>> mappers = ClassUtil.scanClasses("com.example.**.dao.*Mapper");

// Or stream them with a consumer (since 3.7)
ClassUtil.scanClasses("com.example.jobs.*", clz -> {
    if (clz.isAnnotationPresent(Job.class)) { ... }
});
Enter fullscreen mode Exit fullscreen mode

Expression rules worth remembering: a trailing lowercase segment means "package" (com.example.handlers → all classes in it), a trailing uppercase segment means "class", and you can end with .class for a single class. tryInstance uses the no-arg constructor (or a Properties constructor if you pass properties); the strict newInstance throws ConstructionException instead of returning null.

3. Assert — defensive checks without pulling in Spring

If you like org.springframework.util.Assert but don't want Spring on the classpath, Assert (since 3.1) covers the same ground. The one difference to remember: notNull throws NullPointerException, while notEmpty/notBlank throw IllegalArgumentException.

public void createOrder(Order order, String customerId) {
    Assert.notNull(order, "order must not be null");
    Assert.notBlank(customerId, "customerId must not be blank");

    // type checks that read well in conditions
    if (Assert.isNumber(customerId)) { ... }
}
Enter fullscreen mode Exit fullscreen mode

Beyond the checks, isBlank, isDigits, isBoolean, isInteger, and isNumber (integer, decimal, or negative) are handy predicate-style helpers that don't throw.

4. ThreadsUtil — virtual thread executors, named

Since 2.7, ThreadsUtil has exposed JDK 21 virtual threads through a reflective bridge, so the call site stays the same even on older JDKs (the reflection call just fails there — it's JDK 21+ only):

// A per-task virtual thread executor with a readable thread name prefix
ExecutorService pool = ThreadsUtil.newVirtualThreadPerTaskExecutor("my-worker-");

pool.submit(() -> {
    // blocking I/O here is cheap on virtual threads
});
Enter fullscreen mode Exit fullscreen mode

If you only need a ThreadFactory, newVirtualThreadFactory("prefix-") returns one directly. This pairs nicely with Solon's virtual-thread story (solon.threads.virtual.enabled, and solon-java25 on newer JDKs) — same model, one consistent utility.

5. PathMatcher — route-style matching for your own URLs

PathMatcher is what the router itself uses, and it's public and cached, so you can reuse the same matching semantics in filters, permissions, or any URL-rule feature:

// Path variables
PathMatcher userMatcher = PathMatcher.get("/user/{id}");
userMatcher.matches("/user/42");          // true
userMatcher.matches("/user/42/profile");  // false — {id} matches one segment

// Wildcards: * = one segment, ** = many segments
PathMatcher apiMatcher = PathMatcher.get("/api/**");
apiMatcher.matches("/api/v1/orders/123"); // true

// Grab the captured value
Matcher m = userMatcher.matcher("/user/42");
if (m.find()) {
    String id = m.group(1); // "42"
}
Enter fullscreen mode Exit fullscreen mode

PathMatcher.get(...) is a static factory with an internal cache, so repeated calls with the same expression don't recompile a regex. Matching is case-sensitive by default; PathMatcher.setCaseSensitive(false) flips it globally. The {name} syntax captures ([^/]+); a trailing underscore form {name_} captures greedily across segments.

6. MultiMap — multi-value map, case-insensitive keys

MultiMap is a multi-value dictionary whose keys ignore case by default — useful for headers, query params, or any "one key, several values" structure:

MultiMap<String> headers = new MultiMap<>();
headers.add("X-Trace-Id", "abc");
headers.add("x-trace-id", "def"); // same key, second value

headers.getAll("X-TRACE-ID"); // [abc, def]
headers.get("x-trace-id");    // abc (first value)

Map<String, String> single = headers.toValueMap();
Map<String, List<String>> multi = headers.toValuesMap();
Enter fullscreen mode Exit fullscreen mode

The surprising one is the static parser: MultiMap.from(String[] args) parses command-line style arguments (optimized in v4.0.5 to follow picocli's parsing strategy):

String[] args = {"--name", "solon", "-p=8080", "--verbose"};
MultiMap<String> opt = MultiMap.from(args);

opt.get("name");     // "solon"
opt.get("p");        // "8080"
opt.containsKey("verbose"); // true — boolean flags are stored as keys
opt.flags();          // ["verbose"]
Enter fullscreen mode Exit fullscreen mode

MultiMap.from handles --name value, -x=value, boolean flags (via flags()), the -- terminator, and positional arguments — so it's a compact CLI parser if you ever need one inside a Solon app.

7. Utils — the everyday misc box

org.noear.solon.Utils is a grab bag you already import transitively. A few that keep showing up in real code:

String id = Utils.uuid();                 // random UUID string
String digest = Utils.md5("secret");      // md5 hex

// Load a properties file from classpath or disk
Props props = Utils.loadProps("classpath:config/app.properties");

// Bind a map onto an object's fields
Utils.bindTo(sourceMap, configBean);

// String helpers
String camel = Utils.snakeToCamel("user_name"); // userName
String first = Utils.valueOr(null, "fallback");
Enter fullscreen mode Exit fullscreen mode

The honest trade-off

The docs are upfront: these utilities exist mainly for the framework itself, and if you have a favorite external library (Hutool, Guava, Apache Commons), you should keep using it. Where these classes win is when you want zero extra dependencies, want behavior that stays in lockstep with Solon (including its AOT/GraalVM handling — scanClasses and scanResources write index files on native image builds), or just want to avoid a second assertion/CLI/URL-matching library for one small feature.

Next time you find yourself hand-rolling a classpath scan or a URL pattern check, take a look inside org.noear.solon.core.util first — the framework's own toolkit is more usable than it lets on.

This article is based on Solon v4.0.5. Reference: official docs, "几个内核工具类" (article/516), verified against the Solon source.

Top comments (0)