<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Solon Framework</title>
    <description>The latest articles on DEV Community by Solon Framework (@solonjava).</description>
    <link>https://dev.to/solonjava</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4003833%2F83933c1e-7d66-44d1-9237-16669f6b9a80.png</url>
      <title>DEV Community: Solon Framework</title>
      <link>https://dev.to/solonjava</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/solonjava"/>
    <language>en</language>
    <item>
      <title>The AppContext API: Programmatic Bean Control in Solon</title>
      <dc:creator>Solon Framework</dc:creator>
      <pubDate>Thu, 06 Aug 2026 01:48:31 +0000</pubDate>
      <link>https://dev.to/solonjava/the-appcontext-api-programmatic-bean-control-in-solon-13aj</link>
      <guid>https://dev.to/solonjava/the-appcontext-api-programmatic-bean-control-in-solon-13aj</guid>
      <description>&lt;p&gt;Annotations get you a long way in Solon. &lt;code&gt;@Component&lt;/code&gt;, &lt;code&gt;@Inject&lt;/code&gt;, &lt;code&gt;@Bean&lt;/code&gt; cover the everyday work: declare a bean, wire it, done. But the moment you start writing a plugin, a framework extension, or anything that has to touch the container at runtime, you drop below the annotation layer. That layer is &lt;code&gt;AppContext&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;AppContext&lt;/code&gt; is the core component of Solon. It's where IoC/AOP actually lives, and it's the foundation the framework's hot-plug capability is built on. Its job is simple to state: manage the beans it holds, and register/apply the annotation processors that produce them. This post walks through the programmatic side of it, the API you reach for when annotations aren't enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting hold of an AppContext
&lt;/h2&gt;

&lt;p&gt;There are three ways to get a reference, depending on where you are.&lt;/p&gt;

&lt;p&gt;Global, from anywhere:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.noear.solon.Solon&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DemoClass&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;UserService&lt;/span&gt; &lt;span class="n"&gt;userService&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;demo&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;Solon&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;getBeanAsync&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;UserService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bean&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;userService&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bean&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
        &lt;span class="o"&gt;});&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Injected into a component:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.noear.solon.annotation.Component&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.noear.solon.annotation.Inject&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.noear.solon.core.AppContext&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="nd"&gt;@Component&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DemoComponent&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;@Inject&lt;/span&gt;
    &lt;span class="nc"&gt;AppContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Handed to a plugin at the start of its lifecycle:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.noear.solon.core.AppContext&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.noear.solon.core.Plugin&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DemoPlugin&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;Plugin&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;AppContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;//...&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The plugin variant is the one that matters most for extension work: &lt;code&gt;Plugin.start(AppContext)&lt;/code&gt; is your entry point into the container before the application is fully wired.&lt;/p&gt;

&lt;h2&gt;
  
  
  The container is async-ready, and that changes how you fetch
&lt;/h2&gt;

&lt;p&gt;Notice the first example used &lt;code&gt;getBeanAsync&lt;/code&gt;, not &lt;code&gt;getBean&lt;/code&gt;. That's deliberate.&lt;/p&gt;

&lt;p&gt;Solon builds its container as beans become available, not all at once up front. If you call &lt;code&gt;getBean(SomeType.class)&lt;/code&gt; too early, the bean may simply not be there yet and you get back null. The official docs flag this as a matter of timing. The async fetch methods are the answer: they hand you the bean the moment it's ready, whether that's now or a few beans later.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Fires whenever UserService becomes available, no null surprises&lt;/span&gt;
&lt;span class="nc"&gt;Solon&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;getBeanAsync&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;UserService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bean&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// use bean here&lt;/span&gt;
&lt;span class="o"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two families exist for this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;getBeanAsync(name, callback)&lt;/code&gt; / &lt;code&gt;getBeanAsync(type, callback)&lt;/code&gt; — receive a single bean when ready&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;subBeansOfType(baseType, callback)&lt;/code&gt; — receive every bean of a base type as they arrive&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There's a matching pair at the wrapper level: &lt;code&gt;getWrapAsync(nameOrType, callback)&lt;/code&gt; and &lt;code&gt;subWrapsOfType(baseType, callback)&lt;/code&gt;. More on wrappers below.&lt;/p&gt;

&lt;p&gt;If you know the bean already exists by the time your code runs, the synchronous &lt;code&gt;getBean(name)&lt;/code&gt; and &lt;code&gt;getBean(type)&lt;/code&gt; are fine. The rule of thumb: inside plugin start or during early wiring, prefer async; well after startup, synchronous is fine.&lt;/p&gt;

&lt;h2&gt;
  
  
  baseType vs type: a batch or a single one
&lt;/h2&gt;

&lt;p&gt;Several methods come in two shapes, one that takes a &lt;code&gt;baseType&lt;/code&gt; and one that takes a &lt;code&gt;type&lt;/code&gt;. This isn't redundancy, it's a deliberate trade-off.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;With OfType&lt;/th&gt;
&lt;th&gt;Without OfType&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;getWrapsOfType(baseType) -&amp;gt; List[T]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;getWrap(type) -&amp;gt; T&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;subWrapsOfType(baseType, callback)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;getWrapAsync(type)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;getBeansOfType(baseType) -&amp;gt; List[T]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;getBean(type) -&amp;gt; T&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;subBeansOfType(baseType, callback)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;getBeanAsync(type)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;getBeansMapOfType(baseType)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;/&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;baseType&lt;/strong&gt; matches by base class and returns a &lt;em&gt;batch&lt;/em&gt;. The method names carry an &lt;code&gt;s&lt;/code&gt; (plural). You get every matching bean, at some cost to lookup speed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;type&lt;/strong&gt; matches the concrete type by hash code and returns a &lt;em&gt;single&lt;/em&gt; bean. Faster, but exactly one.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The design follows Solon's restraint principle: pick the one that fits what you actually need, rather than always reaching for the broad query. Need all implementations of an interface? &lt;code&gt;getBeansOfType&lt;/code&gt;. Need one specific bean fast? &lt;code&gt;getBean&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;There's also &lt;code&gt;getBeansMapOfType(baseType)&lt;/code&gt;, which returns a &lt;code&gt;Map[String, T]&lt;/code&gt; keyed by bean name, and &lt;code&gt;getBeanOrNew(type)&lt;/code&gt;, which fetches or creates on the spot.&lt;/p&gt;

&lt;h2&gt;
  
  
  Manual assembly: putting objects into the container at runtime
&lt;/h2&gt;

&lt;p&gt;This is the part annotations can't do. When you build an object yourself and want the container to own it, you wrap it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// wrap and push into the container in one step&lt;/span&gt;
&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;wrapAndPut&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;UserService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;UserServiceImpl&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The wrapping API:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;wrap(type)&lt;/code&gt;, &lt;code&gt;wrap(type, bean)&lt;/code&gt;, &lt;code&gt;wrap(type, bean, typed)&lt;/code&gt; — produce a &lt;code&gt;BeanWrap&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;wrap(name, bean)&lt;/code&gt;, &lt;code&gt;wrap(name, bean, typed)&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;wrap(name, type)&lt;/code&gt; and &lt;code&gt;wrap(name, type, typed)&lt;/code&gt; — supported since v3.0&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;wrapAndPut(type)&lt;/code&gt;, &lt;code&gt;wrapAndPut(type, bean)&lt;/code&gt;, &lt;code&gt;wrapAndPut(type, bean, typed)&lt;/code&gt; — wrap and push into the container&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;wrapAndPut(name, bean)&lt;/code&gt;, &lt;code&gt;wrapAndPut(name, bean, typed)&lt;/code&gt; — supported since v3.0&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;putWrap(name, wrap)&lt;/code&gt;, &lt;code&gt;putWrap(type, wrap)&lt;/code&gt; — push an existing wrap into the bean store&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;hasWrap(nameOrType)&lt;/code&gt; — check whether a wrap exists&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;beanRegister(wrap, name, typed)&lt;/code&gt; — register a wrap (the fuller version of &lt;code&gt;putWrap&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And a critical detail. If you wrap a bean manually and expect type subscribers (&lt;code&gt;subBeansOfType&lt;/code&gt; / &lt;code&gt;subWrapsOfType&lt;/code&gt;) to notice it, they won't automatically. Per the docs:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;By default, beans produced by &lt;code&gt;@Bean&lt;/code&gt; or &lt;code&gt;@Component&lt;/code&gt; are published automatically. Otherwise you must call the publish method (&lt;code&gt;beanPublish&lt;/code&gt;) manually.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So the manual path is: wrap it, then publish it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;BeanWrap&lt;/span&gt; &lt;span class="n"&gt;wrap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;wrap&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;UserService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;UserServiceImpl&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;putWrap&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;UserService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;wrap&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;beanPublish&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;wrap&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// now subWrapsOfType subscribers fire&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two related methods round this out: &lt;code&gt;beanDeliver(wrap)&lt;/code&gt; hands a special-interface bean off to its manager, and &lt;code&gt;beanInject(bean)&lt;/code&gt; runs injection on an object you created yourself so its &lt;code&gt;@Inject&lt;/code&gt; fields get filled.&lt;/p&gt;

&lt;h2&gt;
  
  
  Walking the container
&lt;/h2&gt;

&lt;p&gt;When you need to inspect what's registered, there are iteration and lookup helpers. The docs note these are timing-sensitive, so run them once the container is settled:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;beanForeach((name, wrap) -&amp;gt; {})&lt;/code&gt; and &lt;code&gt;beanForeach((wrap) -&amp;gt; {})&lt;/code&gt; — iterate the wrap store&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;beanFind((name, wrap) -&amp;gt; bool) -&amp;gt; List[BeanWrap]&lt;/code&gt; and &lt;code&gt;beanFind((wrap) -&amp;gt; bool) -&amp;gt; List[BeanWrap]&lt;/code&gt; — find matching wraps&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Registering your own annotation handlers
&lt;/h2&gt;

&lt;p&gt;The other half of AppContext is annotation processing. This is how Solon lets you teach the container about a custom annotation, the same mechanism the framework uses internally.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;beanBuilderAdd(anno, builder)&lt;/code&gt; and &lt;code&gt;beanBuilderAdd(anno, targetClz, builder)&lt;/code&gt; — handle how an annotation &lt;em&gt;builds&lt;/em&gt; a bean&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;beanInjectorAdd(anno, injector)&lt;/code&gt; and &lt;code&gt;beanInjectorAdd(anno, targetClz, injector)&lt;/code&gt; — handle how an annotation &lt;em&gt;injects&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;beanExtractorAdd(anno, extractor)&lt;/code&gt; and &lt;code&gt;beanExtractorHas(anno)&lt;/code&gt; — handle &lt;em&gt;extraction&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;beanInterceptorAdd(anno, interceptor)&lt;/code&gt;, &lt;code&gt;beanInterceptorAdd(anno, interceptor, index)&lt;/code&gt;, &lt;code&gt;beanInterceptorGet(anno)&lt;/code&gt; — attach an interceptor to an annotation (AOP)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Registered inside &lt;code&gt;Plugin.start&lt;/code&gt;, these let a plugin introduce a brand-new annotation that behaves like a first-class citizen of the container. That's the seam a lot of Solon's ecosystem plugins hook into.&lt;/p&gt;

&lt;p&gt;For scanning and construction there's also &lt;code&gt;beanScan(source)&lt;/code&gt; / &lt;code&gt;beanScan(basePackage)&lt;/code&gt; / &lt;code&gt;beanScan(classLoader, basePackage)&lt;/code&gt; to bring a package's beans in, and &lt;code&gt;beanMake(clz)&lt;/code&gt; to construct a single bean with full processing applied.&lt;/p&gt;

&lt;h2&gt;
  
  
  Binding lifecycle
&lt;/h2&gt;

&lt;p&gt;Finally, if a bean needs to react to container lifecycle events, register it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;lifecycle(lifecycleBean)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;lifecycle(index, lifecycleBean)&lt;/code&gt; — with ordering&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the programmatic equivalent of implementing &lt;code&gt;LifecycleBean&lt;/code&gt;, useful when the object you want to hook in wasn't declared as a component.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this fits
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;AppContext&lt;/code&gt; is the layer beneath the annotations, and it's what makes the rest of the kernel's open capabilities possible. The lifecycle timing points, the E-Spi external extensions and H-Spi hot-plug modules, they all lean on this same interface to register, publish, and tear down beans at runtime. If annotations are how you &lt;em&gt;declare&lt;/em&gt; your application, &lt;code&gt;AppContext&lt;/code&gt; is how you &lt;em&gt;program&lt;/em&gt; the container underneath it.&lt;/p&gt;

&lt;p&gt;The full method reference lives in the official docs at solon.noear.org. Everything above is the subset you actually reach for when you step past annotations.&lt;/p&gt;

</description>
      <category>solon</category>
      <category>java</category>
      <category>architecture</category>
      <category>backend</category>
    </item>
    <item>
      <title>Solon Plugins Beyond the Fatjar: External Extension (E-Spi) and Hot-Plug (H-Spi)</title>
      <dc:creator>Solon Framework</dc:creator>
      <pubDate>Wed, 05 Aug 2026 12:12:10 +0000</pubDate>
      <link>https://dev.to/solonjava/solon-plugins-beyond-the-fatjar-external-extension-e-spi-and-hot-plug-h-spi-4nef</link>
      <guid>https://dev.to/solonjava/solon-plugins-beyond-the-fatjar-external-extension-e-spi-and-hot-plug-h-spi-4nef</guid>
      <description>&lt;p&gt;You shipped a Solon service as a single fatjar. Then reality shows up: ops wants to point the datasource at a different host without asking you to rebuild, and the business team wants to take one module offline at 2 a.m. without bouncing the whole process. If your only answer is "repackage and redeploy the whole jar," you feel the friction every time.&lt;/p&gt;

&lt;p&gt;Solon has two mechanisms built exactly for this seam: &lt;strong&gt;E-Spi&lt;/strong&gt; (external extension) and &lt;strong&gt;H-Spi&lt;/strong&gt; (hot-plug). They sit at different points on the same spectrum, and picking the wrong one costs you either flexibility or stability. Here is how each works and when to reach for which. Everything below is on v4.0.4.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shared problem: a fatjar is sealed
&lt;/h2&gt;

&lt;p&gt;A fatjar is convenient to deploy and miserable to amend. Config files, business modules, everything is baked in. E-Spi and H-Spi both crack that seal, but with very different contracts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;E-Spi&lt;/strong&gt; lets you place config files and plugin jars &lt;em&gt;outside&lt;/em&gt; the fatjar, loaded at startup into the same runtime. Simple, no extra dependency, but changes require a restart.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;H-Spi&lt;/strong&gt; gives each plugin its own isolated ClassLoader and lets you start and stop modules while the service keeps running. More power, more responsibility.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  E-Spi: put config and modules beside the jar
&lt;/h2&gt;

&lt;p&gt;E-Spi (external extension) targets the fatjar deployment case directly. You designate an extension directory; at startup Solon scans it and loads what it finds:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;.properties&lt;/code&gt; / &lt;code&gt;.yml&lt;/code&gt; files are loaded as &lt;strong&gt;extension config&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;.jar&lt;/code&gt; / &lt;code&gt;.zip&lt;/code&gt; files are loaded as &lt;strong&gt;plugin packages&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 1 — declare the extension directory
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# extension directory is demo_ext (no error if it doesn't exist)&lt;/span&gt;
&lt;span class="na"&gt;solon.extend&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;demo_ext"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Prefix the value with &lt;code&gt;!&lt;/code&gt; and Solon creates the directory for you:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# extension directory is demo_ext (! means auto-create)&lt;/span&gt;
&lt;span class="na"&gt;solon.extend&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;!demo_ext"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2 — drop files next to the jar
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;demo.jar
demo_ext/_db.properties
demo_ext/demo_user.jar
demo_ext/demo_order.jar
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the datasource config lives in &lt;code&gt;_db.properties&lt;/code&gt; outside the jar, and two business modules ride along as separate plugin jars. Ops can edit that properties file directly; you never touch the fatjar.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3 (optional) — load programmatically
&lt;/h3&gt;

&lt;p&gt;If you'd rather load extras in code, the kernel exposes it directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@SolonMain&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Application&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;throws&lt;/span&gt; &lt;span class="nc"&gt;Exception&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;Solon&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;start&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Application&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// load a package file&lt;/span&gt;
            &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;classLoader&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;addJar&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;File&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/demo.jar"&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;

            &lt;span class="c1"&gt;// load a properties file&lt;/span&gt;
            &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;loadAdd&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;File&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/demo.yml"&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
        &lt;span class="o"&gt;});&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Under the hood, this is &lt;code&gt;AppClassLoader.addJar(URL | File)&lt;/code&gt;. That single detail explains E-Spi's whole personality:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Everything is shared&lt;/strong&gt; — all plugin packages share one ClassLoader, one AppContext, one config tree&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Split or merged, your call&lt;/strong&gt; — package externally, or bundle with the main app; loading timing is the same either way&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Updates need a restart&lt;/strong&gt; — because it all rides one ClassLoader loaded at boot, swapping a jar or editing config only takes effect after restarting the main service&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No extra dependency&lt;/strong&gt; — the kernel provides E-Spi directly&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One packaging note: a plugin jar should either be built as a fatjar itself, or have its dependencies folded into the main app (common dependencies especially belong in the main app's build, with the plugin's own pom marking them optional).&lt;/p&gt;

&lt;p&gt;Official example: &lt;code&gt;demo2002-external_ext&lt;/code&gt; (under &lt;code&gt;2.Solon_Advanced&lt;/code&gt; in the solon-examples repo).&lt;/p&gt;

&lt;h2&gt;
  
  
  H-Spi: isolate and hot-swap without a restart
&lt;/h2&gt;

&lt;p&gt;H-Spi (hot-plug) is the heavier tool. You develop one business module as a self-contained plugin package, and the running service can load and unload it live. The defining difference from E-Spi is isolation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Each plugin gets its &lt;strong&gt;own&lt;/strong&gt; ClassLoader, AppContext, and config — &lt;strong&gt;fully isolated&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Need the main app's global resources? Grab them explicitly via &lt;code&gt;Solon.app()&lt;/code&gt;, &lt;code&gt;Solon.cfg()&lt;/code&gt;, &lt;code&gt;Solon.context()&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Updating a plugin package &lt;strong&gt;does not require restarting&lt;/strong&gt; the main service&lt;/li&gt;
&lt;li&gt;The main app must pull in the &lt;strong&gt;solon-hotplug&lt;/strong&gt; dependency to manage business plugin packages&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The ClassLoader contract
&lt;/h3&gt;

&lt;p&gt;Isolation is the whole point, so the class-visibility rules matter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Parent ClassLoader&lt;/strong&gt; (put common resources here): children can see and use its classes and resources — but anything a child registers must be unregistered in its &lt;code&gt;stop&lt;/code&gt; event&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sibling ClassLoaders&lt;/strong&gt;: cannot use each other's classes or resources. Don't wire explicit type interactions between siblings; talk through the event bus instead, passing data as parent-level entity classes or weakly-typed JSON — treat it like calling a remote API&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Write start(), and write stop() honestly
&lt;/h3&gt;

&lt;p&gt;A hot-pluggable plugin implements &lt;code&gt;Plugin&lt;/code&gt;. &lt;code&gt;start&lt;/code&gt; registers what the module needs; &lt;code&gt;stop&lt;/code&gt; must remove &lt;strong&gt;every&lt;/strong&gt; resource it registered, or you leak on unload:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Plugin1Impl&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;Plugin&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;AppContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="nc"&gt;StaticRepository&lt;/span&gt; &lt;span class="n"&gt;staticRepository&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;AppContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;// add my own config file&lt;/span&gt;
        &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;loadAdd&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"demo1011.plugin1.yml"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="c1"&gt;// scan my own beans&lt;/span&gt;
        &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;beanScan&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Plugin1Impl&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// add my own static file repository (register the classloader)&lt;/span&gt;
        &lt;span class="n"&gt;staticRepository&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ClassPathStaticRepository&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getClassLoader&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="s"&gt;"plugin1_static"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="nc"&gt;StaticMappings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/html/"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;staticRepository&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;stop&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="kd"&gt;throws&lt;/span&gt; &lt;span class="nc"&gt;Throwable&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// remove http handlers (use a prefix to make removal easy)&lt;/span&gt;
        &lt;span class="nc"&gt;Solon&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;router&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;remove&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/user"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// remove scheduled jobs (pick a job impl that supports manual removal)&lt;/span&gt;
        &lt;span class="nc"&gt;JobManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getInstance&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;jobRemove&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"job1"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// remove event subscriptions&lt;/span&gt;
        &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;beanForeach&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bw&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bw&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;raw&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nc"&gt;EventListener&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                &lt;span class="nc"&gt;EventBus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;unsubscribe&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bw&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;raw&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
            &lt;span class="o"&gt;}&lt;/span&gt;
        &lt;span class="o"&gt;});&lt;/span&gt;

        &lt;span class="c1"&gt;// remove the static file repository&lt;/span&gt;
        &lt;span class="nc"&gt;StaticMappings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;remove&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;staticRepository&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;stop&lt;/code&gt; method is the tax for hot-plug. Routes, jobs, event subscriptions, static repositories — if &lt;code&gt;start&lt;/code&gt; added it, &lt;code&gt;stop&lt;/code&gt; removes it. Skip a line and you get ghost routes or leaked listeners after unload.&lt;/p&gt;

&lt;p&gt;Template rendering has one more ClassLoader gotcha — the renderer must be pinned to the right ClassLoader:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BaseController&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;Render&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// account for the classloader where templates live&lt;/span&gt;
    &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;FreemarkerRender&lt;/span&gt; &lt;span class="n"&gt;viewRender&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;FreemarkerRender&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;BaseController&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getClassLoader&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;

    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Object&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Context&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;throws&lt;/span&gt; &lt;span class="nc"&gt;Throwable&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nc"&gt;Throwable&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Throwable&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nc"&gt;ModelAndView&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;viewRender&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;render&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;render&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For cross-module communication, lean on the event bus with weakly-typed payloads (Map / JSON string); DamiBus pairs well here for decoupling. Official example package: &lt;code&gt;demo1011&lt;/code&gt;. Plugin management can be pushed further into a repository or platform via solon-hotplug.&lt;/p&gt;

&lt;h2&gt;
  
  
  Side by side
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;E-Spi&lt;/th&gt;
&lt;th&gt;H-Spi&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ClassLoader / AppContext / config&lt;/td&gt;
&lt;td&gt;shared&lt;/td&gt;
&lt;td&gt;isolated (fully)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Restart after update?&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;no (hot update)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Extra dependency&lt;/td&gt;
&lt;td&gt;none (kernel built-in)&lt;/td&gt;
&lt;td&gt;solon-hotplug&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Focus&lt;/td&gt;
&lt;td&gt;simple external extension / config edits&lt;/td&gt;
&lt;td&gt;isolation + hot-plug + management&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Resource removal&lt;/td&gt;
&lt;td&gt;nothing special&lt;/td&gt;
&lt;td&gt;must manually remove all registered resources in &lt;code&gt;stop&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cross-module comms&lt;/td&gt;
&lt;td&gt;direct sharing&lt;/td&gt;
&lt;td&gt;event bus / weakly-typed data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Underlying mechanism&lt;/td&gt;
&lt;td&gt;&lt;code&gt;AppClassLoader.addJar&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;isolated ClassLoader + &lt;code&gt;Plugin.start/stop&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Which one do you actually need
&lt;/h2&gt;

&lt;p&gt;Start with the question "does this have to change without a restart?"&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No, a restart window is fine.&lt;/strong&gt; Use E-Spi. Externalizing datasource config and shipping business modules as sibling jars covers most "I don't want to rebuild the fatjar" cases, with zero extra dependency and no lifecycle bookkeeping.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Yes, the service must stay up while a module comes and goes.&lt;/strong&gt; Use H-Spi. You get true isolation and live swap, and in return you accept the &lt;code&gt;stop&lt;/code&gt;-cleanup discipline and event-bus-only cross-module contract.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A useful mental model: E-Spi moves &lt;em&gt;files&lt;/em&gt; outside the jar; H-Spi moves &lt;em&gt;modules&lt;/em&gt; into their own runtime bubbles. One is about deployment convenience, the other about operational isolation. Plenty of teams run both — E-Spi for externalized config, H-Spi for the one or two modules that genuinely need hot swap.&lt;/p&gt;

&lt;p&gt;If you're deciding how to structure a Solon service for the long haul, it's worth reading both official pages end to end before you commit — the ClassLoader rules in particular reward a careful first read.&lt;/p&gt;

&lt;p&gt;What does your fatjar most need to shed first: config, or whole modules?&lt;/p&gt;

</description>
      <category>solon</category>
      <category>java</category>
      <category>architecture</category>
      <category>backend</category>
    </item>
    <item>
      <title>SolonCode v2026.8.4: Adjustable UI Fonts, 22 Languages, and Smarter Memory Search</title>
      <dc:creator>Solon Framework</dc:creator>
      <pubDate>Wed, 05 Aug 2026 08:54:00 +0000</pubDate>
      <link>https://dev.to/solonjava/soloncode-v202684-adjustable-ui-fonts-22-languages-and-smarter-memory-search-382i</link>
      <guid>https://dev.to/solonjava/soloncode-v202684-adjustable-ui-fonts-22-languages-and-smarter-memory-search-382i</guid>
      <description>&lt;p&gt;A fully Chinese-capable coding agent that goes to work the moment you open a terminal. This release rounds out three things at once: &lt;strong&gt;read it clearly, use your own language, and actually find what it remembered.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What is SolonCode
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;SolonCode&lt;/strong&gt; is an enterprise-grade &lt;strong&gt;terminal coding agent&lt;/strong&gt; built by Hangzhou Noear Technology — a digital teammate that understands requirements, plans steps, and writes code on its own. It's not picky about models or platforms, and a single &lt;code&gt;curl&lt;/code&gt; line gets you running.&lt;/p&gt;

&lt;p&gt;Repos: &lt;a href="https://gitee.com/opensolon/soloncode" rel="noopener noreferrer"&gt;Gitee&lt;/a&gt; · &lt;a href="https://gitcode.com/opensolon/soloncode" rel="noopener noreferrer"&gt;GitCode&lt;/a&gt; · &lt;a href="https://github.com/opensolon/soloncode" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Install
&lt;/h2&gt;

&lt;p&gt;Runs on Java 8+ (8~26), across macOS / Linux / Windows / Harmony PC.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# macOS / Linux / Harmony PC&lt;/span&gt;
curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://solon.noear.org/soloncode/setup.sh | bash

&lt;span class="c"&gt;# Windows (PowerShell)&lt;/span&gt;
irm https://solon.noear.org/soloncode/setup.ps1 | iex
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the first run, start &lt;code&gt;soloncode web 0&lt;/code&gt; (&lt;code&gt;0&lt;/code&gt; picks a random port; you can also pin one, e.g. &lt;code&gt;soloncode web 1212&lt;/code&gt;), then head to &lt;strong&gt;Settings → Models&lt;/strong&gt; to add a model and test the connection.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three reasons to upgrade
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Adjustable UI fonts — no more squinting through long sessions
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Settings → General → Fonts&lt;/strong&gt; adds three independent controls: &lt;strong&gt;UI Font&lt;/strong&gt; (12 searchable options), &lt;strong&gt;Code Font&lt;/strong&gt; (independent from the UI font), and &lt;strong&gt;Font Scale&lt;/strong&gt; (85%–150%, 5% steps, with a one-click reset).&lt;/p&gt;

&lt;p&gt;Changes apply instantly, no restart needed. The scaling decouples type size from layout — when you enlarge text, fixed-size elements like icons, borders, and control heights don't get stretched out of shape. That's something full-page browser zoom could never do cleanly.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv14qy0cv3r0y6oqi5xtb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv14qy0cv3r0y6oqi5xtb.png" alt="Settings → General → Fonts: UI Font, Code Font, Font Scale" width="800" height="361"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F94x4vhqhoqgqveb2q7za.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F94x4vhqhoqgqveb2q7za.png" alt="UI Font dropdown, searchable font families" width="800" height="361"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1q2o4qfvaqpxeahm97po.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1q2o4qfvaqpxeahm97po.png" alt="Interface at 125% font scale" width="800" height="361"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  2. 22 UI languages — overseas developers land on their native tongue
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Settings → General → Display Language&lt;/strong&gt; offers &lt;strong&gt;Follow System + 22 languages&lt;/strong&gt;, covering Simplified and Traditional Chinese, English, Japanese, Korean, German, French, Spanish, Italian, Russian, and Portuguese — plus less common ones like Arabic, Bengali, Bosnian, and Norwegian. It's not a token English-Japanese-Korean gesture. The default follows your system, so overseas developers land on a native interface right after install.&lt;/p&gt;

&lt;p&gt;Two notes: the UI language is &lt;strong&gt;independent from the model's reply language&lt;/strong&gt; — switching to English only changes the interface text. And in this version the language choice isn't persisted yet (a refresh reverts to Follow System); this has been reported to the core team.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzo708c52lpbvno1s08b8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzo708c52lpbvno1s08b8.png" alt="Display Language: Follow System + 22 languages" width="800" height="361"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Memory — from "can store" to "can find again"
&lt;/h3&gt;

&lt;p&gt;Memory took shape across four releases: v2026.7.27 added the web management panel, v2026.7.28 introduced &lt;strong&gt;scopes&lt;/strong&gt; (shared cross-project knowledge goes to "User Global," project-specific conventions go to "Current Workspace," and switching projects keeps them from interfering), and v2026.8.4 refined the &lt;strong&gt;prompting and search&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Search targets a real pain point: once you have hundreds of entries, "stored but unfindable" is as good as not stored. Now you can browse by importance and time, and use semantic retrieval to pinpoint what you need.&lt;/p&gt;

&lt;h2&gt;
  
  
  While we're at it: engineering touch-ups
&lt;/h2&gt;

&lt;p&gt;The web workbench keeps growing into a serious tool: &lt;strong&gt;sub-agent management&lt;/strong&gt; (v2026.7.24, four built-ins — bash / explore / general / plan, none with write access except general; drawing that boundary in config is more reliable than leaning on prompts), &lt;strong&gt;context-compaction threshold as a ratio&lt;/strong&gt; (v2026.7.26, default 75%, no recalculating when you swap models), a simplified &lt;strong&gt;&lt;code&gt;/goal&lt;/code&gt; command&lt;/strong&gt;, an enhanced &lt;strong&gt;Git review panel&lt;/strong&gt; (diff/stat, rollback, directory detection, garbled-Chinese fix), &lt;strong&gt;config reload&lt;/strong&gt; (no restart across instances), &lt;strong&gt;skins&lt;/strong&gt;, and the continuously iterating &lt;strong&gt;desktop build&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F47qrh30frvtjyjloudhd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F47qrh30frvtjyjloudhd.png" alt="Settings → Agents: built-in bash / explore / general / plan" width="800" height="361"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Appendix: v2026.8.4 changelog
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Added the &lt;code&gt;soloncode web&lt;/code&gt; font adjustment feature&lt;/li&gt;
&lt;li&gt;Improved SolonCode memory prompting and search&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Full history is in the project repo CHANGELOG.&lt;/p&gt;

</description>
      <category>solon</category>
      <category>java</category>
      <category>ai</category>
      <category>tools</category>
    </item>
    <item>
      <title>Solon's Application Lifecycle: 12 Timing Points, LifecycleBean, EventBus and Plugin SPI</title>
      <dc:creator>Solon Framework</dc:creator>
      <pubDate>Wed, 05 Aug 2026 07:10:29 +0000</pubDate>
      <link>https://dev.to/solonjava/solons-application-lifecycle-12-timing-points-lifecyclebean-eventbus-and-plugin-spi-4gmf</link>
      <guid>https://dev.to/solonjava/solons-application-lifecycle-12-timing-points-lifecyclebean-eventbus-and-plugin-spi-4gmf</guid>
      <description>&lt;p&gt;Most framework bugs I have chased in Java web apps were not logic bugs. They were timing bugs: a bean that read config before the config was loaded, a listener that subscribed after the event had already fired, a connection pool that closed while requests were still draining.&lt;/p&gt;

&lt;p&gt;Solon is explicit about this. The docs lay out the whole application lifecycle as a fixed set of timing points, and every extension mechanism in the framework hangs off one of them. Once you can name the points, "when does my code run" stops being guesswork.&lt;/p&gt;

&lt;p&gt;Here is the model, with the four hooks you actually write against.&lt;/p&gt;

&lt;h2&gt;
  
  
  The lifecycle, counted
&lt;/h2&gt;

&lt;p&gt;The official breakdown is one init callback + six application events + three plugin timing points + two container timing points.&lt;/p&gt;

&lt;p&gt;The init callback is the lambda you pass to &lt;code&gt;Solon.start&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.noear.solon.Solon&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.noear.solon.annotation.SolonMain&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="nd"&gt;@SolonMain&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;App&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;Solon&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;start&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;App&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// application init timing point&lt;/span&gt;
        &lt;span class="o"&gt;});&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The six application events, in order:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;th&gt;Note&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;AppInitEndEvent&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;init finished&lt;/td&gt;
&lt;td&gt;manual subscription only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;AppPluginLoadEndEvent&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;plugin loading finished&lt;/td&gt;
&lt;td&gt;manual subscription only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;AppBeanLoadEndEvent&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;bean scan finished&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;AppLoadEndEvent&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;startup finished&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;AppPrestopEndEvent&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;pre-stop&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;AppStopEndEvent&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;stopped&lt;/td&gt;
&lt;td&gt;since v2.1.0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;All of them live in &lt;code&gt;org.noear.solon.core.event&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Two warnings from the docs are worth repeating, because both are easy to trip over:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do not block the startup thread.&lt;/strong&gt; The app only runs normally after startup completes. A blocking call in an init hook will hang the whole boot.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Events before &lt;code&gt;AppBeanLoadEndEvent&lt;/code&gt; must be subscribed before startup.&lt;/strong&gt; By the time class scanning happens, &lt;code&gt;AppInitEndEvent&lt;/code&gt; and &lt;code&gt;AppPluginLoadEndEvent&lt;/code&gt; have already fired. An annotated listener discovered during the scan is simply too late. For those two you subscribe manually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.noear.solon.Solon&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.noear.solon.annotation.SolonMain&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.noear.solon.core.event.AppInitEndEvent&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="nd"&gt;@SolonMain&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;App&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;Solon&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;start&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;App&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;onEvent&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;AppInitEndEvent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                &lt;span class="c1"&gt;//...&lt;/span&gt;
            &lt;span class="o"&gt;});&lt;/span&gt;
        &lt;span class="o"&gt;});&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For anything from &lt;code&gt;AppBeanLoadEndEvent&lt;/code&gt; onward, the annotated form works:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.noear.solon.annotation.Component&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.noear.solon.core.event.AppLoadEndEvent&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.noear.solon.core.event.EventListener&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="nd"&gt;@Component&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AppLoadEndEventListener&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;EventListener&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;AppLoadEndEvent&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;onEvent&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;AppLoadEndEvent&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;throws&lt;/span&gt; &lt;span class="nc"&gt;Throwable&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// event.app();&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That "subscribe early or miss it" rule is the whole reason the event list is worth memorizing. It is not a quirk, it is a consequence of the container scanning classes exactly once.&lt;/p&gt;

&lt;h2&gt;
  
  
  LifecycleBean: four methods, two annotations
&lt;/h2&gt;

&lt;p&gt;For ordinary beans you rarely need raw events. &lt;code&gt;LifecycleBean&lt;/code&gt; binds your bean to the container's start and stop:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Interface method&lt;/th&gt;
&lt;th&gt;Annotation&lt;/th&gt;
&lt;th&gt;Runs at&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;start()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;@Init&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;AppContext::start()&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;postStart()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;same, later half (since v2.9)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;preStop()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;AppContext::preStop()&lt;/code&gt; (since v2.9)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;stop()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;@Destroy&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;AppContext::stop()&lt;/code&gt; (since v2.2.0)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.noear.solon.annotation.Component&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.noear.solon.core.bean.LifecycleBean&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="nd"&gt;@Component&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DemoCom&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;LifecycleBean&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// bean scan is done here, do initialization&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;postStart&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// start remote services; do NOT create new managed beans here&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;preStop&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// deregister from discovery, etc.&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;stop&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// release local resources&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you only need &lt;code&gt;start()&lt;/code&gt;, the annotation is shorter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.noear.solon.annotation.Component&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.noear.solon.annotation.Init&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="nd"&gt;@Component&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Demo&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;@Init&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="c1"&gt;// any no-arg method name&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same for &lt;code&gt;@Destroy&lt;/code&gt; in place of &lt;code&gt;stop()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Three constraints that matter in real code:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The constructor is the wrong place for initialization.&lt;/strong&gt; At &lt;code&gt;::new()&lt;/code&gt; the bean has been constructed but is not yet registered in the container, and injected fields are not populated. Use constructor-parameter injection, or &lt;code&gt;@Init&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;postStart()&lt;/code&gt; cannot create managed beans.&lt;/strong&gt; It is the tail end of startup, meant for kicking off tasks and network listeners.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;LifecycleBean&lt;/code&gt; only applies to singletons.&lt;/strong&gt; For non-singletons, only the first instance produced during the scan is managed; the lifecycle of other instances is on you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ordering without ceremony
&lt;/h2&gt;

&lt;p&gt;Since v2.2.8, &lt;code&gt;LifecycleBean&lt;/code&gt; instances are ordered automatically, and the ordering comes from injection dependencies. If &lt;code&gt;Bean2&lt;/code&gt; injects &lt;code&gt;Bean1&lt;/code&gt;, then &lt;code&gt;Bean1.start()&lt;/code&gt; runs first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Component&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Bean1&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;LifecycleBean&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// db1 init ...&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;func1&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// db1 call&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;@Component&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Bean2&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;LifecycleBean&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;@Inject&lt;/span&gt;
    &lt;span class="nc"&gt;Bean1&lt;/span&gt; &lt;span class="n"&gt;bean1&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;bean1&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;func1&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The docs call out the useful trick here: even when two beans have no natural dependency, adding an injection creates one, and ordering follows for free.&lt;/p&gt;

&lt;p&gt;The flip side is that mutual injection between two &lt;code&gt;LifecycleBean&lt;/code&gt;s makes the sort unsolvable and you get a circular-dependency error. Two ways out: drop the mutual dependency, or pin the order explicitly with &lt;code&gt;@Component(index = 1)&lt;/code&gt; / &lt;code&gt;@Component(index = 2)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If a dependency simply is not ready during &lt;code&gt;start()&lt;/code&gt; — some beans are only produced at &lt;code&gt;AppContext.start()&lt;/code&gt; time — move that work to an &lt;code&gt;AppLoadEndEvent&lt;/code&gt; listener instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  EventBus: strongly typed, synchronous, transaction-friendly
&lt;/h2&gt;

&lt;p&gt;The lifecycle events above ride on the same in-process bus you can use for your own events. Three properties define it: strongly typed events, publish/subscribe, and synchronous dispatch that propagates exceptions — which is what makes transaction rollback work across a publish.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Getter&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;HelloEvent&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;HelloEvent&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Subscribe either way:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.noear.solon.annotation.Component&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.noear.solon.core.event.EventBus&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.noear.solon.core.event.EventListener&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="nd"&gt;@Component&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;HelloEventListener&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;EventListener&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;HelloEvent&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;onEvent&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;HelloEvent&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;throws&lt;/span&gt; &lt;span class="nc"&gt;Throwable&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getName&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// or manually&lt;/span&gt;
&lt;span class="nc"&gt;EventBus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;subscribe&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;HelloEvent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getName&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
&lt;span class="o"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Publish:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Component&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DemoService&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;hello&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;EventBus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;publish&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;HelloEvent&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;           &lt;span class="c1"&gt;// synchronous&lt;/span&gt;
        &lt;span class="c1"&gt;// EventBus.publishAsync(new HelloEvent(name));    // generally not recommended&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The docs are direct about &lt;code&gt;publishAsync&lt;/code&gt;: generally not recommended, because it cannot propagate exceptions and therefore cannot participate in transaction propagation. If you want a topic-based bus rather than a type-based one, the docs point at DamiBus instead of stretching &lt;code&gt;EventBus&lt;/code&gt; to fit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Plugin: the same lifecycle, one level up
&lt;/h2&gt;

&lt;p&gt;A &lt;code&gt;Plugin&lt;/code&gt; is a module-level participant in the application lifecycle. The interface is three methods:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;Plugin&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;AppContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;throws&lt;/span&gt; &lt;span class="nc"&gt;Throwable&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;preStop&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="kd"&gt;throws&lt;/span&gt; &lt;span class="nc"&gt;Throwable&lt;/span&gt; &lt;span class="o"&gt;{}&lt;/span&gt;
    &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;stop&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="kd"&gt;throws&lt;/span&gt; &lt;span class="nc"&gt;Throwable&lt;/span&gt; &lt;span class="o"&gt;{}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Plugin &lt;code&gt;start&lt;/code&gt; runs after application init completes — before bean scanning, which is exactly why plugins can register interceptors and extensions that the scan will then honor. &lt;code&gt;preStop&lt;/code&gt; runs before stop; with safe-stop enabled there is a gap of a few seconds between them. Container &lt;code&gt;start&lt;/code&gt; runs after the scan finishes, and container &lt;code&gt;stop&lt;/code&gt; runs after plugin &lt;code&gt;stop&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Registration is declarative, close in spirit to Spring Factories or Java SPI. Put the implementation in an &lt;code&gt;integration&lt;/code&gt; package, name it &lt;code&gt;XxxSolonPlugin&lt;/code&gt;, and keep it free of injection:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kn"&gt;package&lt;/span&gt; &lt;span class="nn"&gt;demo.integration&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DemoSolonPlugin&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;Plugin&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;AppContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// plugin starting...&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;preStop&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="kd"&gt;throws&lt;/span&gt; &lt;span class="nc"&gt;Throwable&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;stop&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then declare it in a properties file whose name must be globally unique — using the package name is the recommended convention:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="err"&gt;META-INF/solon/{packname}.properties&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;solon.plugin&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;{PluginImpl}&lt;/span&gt;
&lt;span class="py"&gt;solon.plugin.priority&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;priority&lt;/code&gt; is higher-wins, default &lt;code&gt;0&lt;/code&gt;. At startup Solon scans every &lt;code&gt;.properties&lt;/code&gt; under &lt;code&gt;META-INF/solon/&lt;/code&gt;, collects the plugins, and sorts them.&lt;/p&gt;

&lt;p&gt;To drop a plugin that arrived through a transitive dependency, either configure it out:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;solon.plugin.exclude&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{PluginImpl}"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or exclude it in code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;Solon&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;start&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;App&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;pluginExclude&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;PluginImpl&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Naming tells you where a plugin came from: &lt;code&gt;solon-*&lt;/code&gt; is internal architecture, &lt;code&gt;*-solon-plugin&lt;/code&gt; is an external adapter, with &lt;code&gt;*-solon-ai-plugin&lt;/code&gt; and &lt;code&gt;*-solon-cloud-plugin&lt;/code&gt; for the AI and Cloud interface adapters.&lt;/p&gt;

&lt;p&gt;The framework's own &lt;code&gt;solon-data&lt;/code&gt; is the compact example. Implementation at &lt;code&gt;org.noear.solon.data.integration.DataSolonPlugin&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DataSolonPlugin&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;Plugin&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;AppContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Solon&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;enableTransaction&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;beanInterceptorAdd&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Tran&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TranInterceptor&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;declared in &lt;code&gt;META-INF/solon/solon.data.properties&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;solon.plugin&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;org.noear.solon.data.integration.DataSolonPlugin&lt;/span&gt;
&lt;span class="py"&gt;solon.plugin.priority&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;3&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the entire mechanism behind transaction support: one plugin, registered at a known timing point, adding an interceptor before beans are scanned. Beyond this there are two further layers in the same series — E-SPI for out-of-package extension and H-SPI for hot plug/unplug management — worth knowing exist when you get to modular deployments.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I decide which hook to use
&lt;/h2&gt;

&lt;p&gt;Working backwards from the failure mode is faster than memorizing the table:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Need injected fields available? Not the constructor. Use &lt;code&gt;@Init&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Need to start a listener or scheduled task? &lt;code&gt;postStart()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Need to deregister before shutdown drains? &lt;code&gt;preStop()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Need to release local resources? &lt;code&gt;@Destroy&lt;/code&gt; / &lt;code&gt;stop()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Need something to run when &lt;em&gt;everything&lt;/em&gt; is up, including beans created during startup? &lt;code&gt;AppLoadEndEvent&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Need to react before the bean scan? Manual subscription in the &lt;code&gt;Solon.start&lt;/code&gt; lambda.&lt;/li&gt;
&lt;li&gt;Need to ship the behavior as a reusable module across projects? A &lt;code&gt;Plugin&lt;/code&gt; plus one properties file.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The ordering rule is the part I would keep in muscle memory: dependencies define order, and if you need order without a dependency, injection is the cheapest way to declare it.&lt;/p&gt;

&lt;p&gt;Docs: &lt;a href="https://solon.noear.org/article/240" rel="noopener noreferrer"&gt;https://solon.noear.org/article/240&lt;/a&gt; (lifecycle), &lt;a href="https://solon.noear.org/article/480" rel="noopener noreferrer"&gt;https://solon.noear.org/article/480&lt;/a&gt; (LifecycleBean), &lt;a href="https://solon.noear.org/article/264" rel="noopener noreferrer"&gt;https://solon.noear.org/article/264&lt;/a&gt; (EventBus), &lt;a href="https://solon.noear.org/article/58" rel="noopener noreferrer"&gt;https://solon.noear.org/article/58&lt;/a&gt; (Plugin SPI). Version basis: Solon v4.0.4.&lt;/p&gt;

</description>
      <category>solon</category>
      <category>java</category>
      <category>architecture</category>
      <category>backend</category>
    </item>
    <item>
      <title>No JSON Required: Configuring SolonCode's Model from the Web Settings Panel</title>
      <dc:creator>Solon Framework</dc:creator>
      <pubDate>Tue, 04 Aug 2026 00:42:39 +0000</pubDate>
      <link>https://dev.to/solonjava/no-json-required-configuring-soloncodes-model-from-the-web-settings-panel-ceo</link>
      <guid>https://dev.to/solonjava/no-json-required-configuring-soloncodes-model-from-the-web-settings-panel-ceo</guid>
      <description>&lt;p&gt;Most coding agents make you earn your first conversation. You install the binary, then you go hunting for a config file, then you find out the config file wants a provider name you have to spell exactly right, a base URL that may or may not need a trailing path segment, and a key you paste into a text editor and hope you didn't leave a trailing space on.&lt;/p&gt;

&lt;p&gt;SolonCode skips that. You start it in web mode, the browser opens, and you add your model in a settings panel — paste the endpoint, paste the key, hit test, save. The chat area picks up the new model immediately. There is a &lt;code&gt;settings.json&lt;/code&gt; underneath, and you can read it if you're curious, but on the way to your first task you never need to open it.&lt;/p&gt;

&lt;p&gt;Here's the whole path, with the screens you'll actually see.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before you start
&lt;/h2&gt;

&lt;p&gt;Two things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Java 8 or newer.&lt;/strong&gt; SolonCode runs on Java 8 through Java 26, so whatever JDK your project already builds against is almost certainly fine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One reachable model endpoint.&lt;/strong&gt; OpenAI, Anthropic, Gemini, Ollama, or anything speaking a compatible format.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Optional, and only if you want the matching features later: Node.js (for some MCP servers, Skills, and browser-type tools), Git (so the web UI can show you diffs and staging), and a language server (for LSP-backed code understanding). None of these block your first run.&lt;/p&gt;

&lt;p&gt;Install:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# macOS / Linux / Harmony PC&lt;/span&gt;
curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://solon.noear.org/soloncode/setup.sh | bash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Windows PowerShell&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;irm&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;https://solon.noear.org/soloncode/setup.ps1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;iex&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Re-running the same command later is how you update. The installer refreshes program files and does its best to preserve what you've already configured — &lt;code&gt;settings.json&lt;/code&gt;, &lt;code&gt;AGENTS.md&lt;/code&gt;, and friends stay put.&lt;/p&gt;

&lt;p&gt;Confirm it landed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;soloncode version
&lt;span class="c"&gt;# SolonCode v2026.8.4&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Start web mode
&lt;/h2&gt;

&lt;p&gt;Go to the project you actually want to work on. The directory you launch from becomes the workspace, and that matters for scoping later, so &lt;code&gt;cd&lt;/code&gt; first rather than launching from your home directory.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;your-project
soloncode web 0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three ways to pick a port:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;th&gt;Port&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;soloncode web&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Default, 4808&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;soloncode web 0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Random free port&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;soloncode web 1212&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;That specific port&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;web 0&lt;/code&gt; is the one to reach for when you keep several projects open, because you're not going to collide with an instance you forgot about. The browser tab is labeled with the workspace name and path, which is how you tell four open tabs apart.&lt;/p&gt;

&lt;p&gt;The terminal prints the address it picked:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SolonCode v2026.8.4 PID-4131 Model:your-model
/path/to/your-project
Web interface: http://localhost:1212/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The browser opens on its own. If it doesn't — remote box, headless environment, an OS that decided not to cooperate — copy the address from that output.&lt;/p&gt;

&lt;p&gt;What you land on is the welcome screen: an input box in the middle, the workspace file tree on the right, conversation controls on the left, and the current model shown under the input.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe2m8r29hercjz3x03p78.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe2m8r29hercjz3x03p78.png" alt="SolonCode welcome screen" width="800" height="361"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  First stop: set your language
&lt;/h2&gt;

&lt;p&gt;Click &lt;strong&gt;Settings&lt;/strong&gt; in the left rail, and the first card under &lt;strong&gt;General&lt;/strong&gt; is &lt;strong&gt;Display Language&lt;/strong&gt;. It ships as &lt;em&gt;Follow system default&lt;/em&gt;, which means the UI speaks whatever your OS does.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4jpxpyqh63i7z3kk3fhx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4jpxpyqh63i7z3kk3fhx.png" alt="Display language dropdown" width="800" height="361"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Twenty-three locales in that list. Pick yours and the interface re-renders on the spot — no restart, no reload. Menus, section names, buttons, placeholder text, all of it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3ghg3oh3acyn8scjzzm8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3ghg3oh3acyn8scjzzm8.png" alt="Settings panel in English" width="800" height="361"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Two things worth knowing that the dropdown doesn't tell you:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The setting is per-session.&lt;/strong&gt; Selecting a language applies it immediately, but reloading the page drops you back to &lt;em&gt;Follow system default&lt;/em&gt;. If your OS locale already matches what you want, you'll never notice. If it doesn't, expect to re-pick after a reload.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It changes the UI, not the agent.&lt;/strong&gt; Display Language controls chrome. The language your model &lt;em&gt;answers&lt;/em&gt; in comes from the model, and it will happily reply in one language to a prompt written in another. If you want English answers, ask for English answers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Reply in English. &amp;lt;your actual request&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or put it in &lt;code&gt;AGENTS.md&lt;/code&gt; once and stop repeating yourself.&lt;/p&gt;

&lt;p&gt;The rest of General, for when you come back to it: Conversation Strategy, Sandbox Mode, Memory, Retry on Failure, and Appearance.&lt;/p&gt;

&lt;h2&gt;
  
  
  The settings panel
&lt;/h2&gt;

&lt;p&gt;Nine sections down the side:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;General · Agents · Mounts · Skills · Providers · Models · MCP Servers · OpenAPI Servers · LSP Servers&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You want &lt;strong&gt;Models&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;One small thing that will bite you if nobody says it: &lt;strong&gt;Esc closes the whole settings panel&lt;/strong&gt;, not just the form you have open. Use the panel's own &lt;strong&gt;Back&lt;/strong&gt; button when you mean to back out of a form.&lt;/p&gt;

&lt;h2&gt;
  
  
  Add your first model
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Models → + Add Model&lt;/strong&gt;. This opens as a full page inside the panel, not a popup dialog:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz2wb7rn4qqgkovqsyydt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz2wb7rn4qqgkovqsyydt.png" alt="Add model form" width="800" height="361"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Name&lt;/strong&gt; — how it shows up in the model picker. Free text, so name it something you'll recognize at a glance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scope&lt;/strong&gt; — two buttons, &lt;strong&gt;User (Global)&lt;/strong&gt; and &lt;strong&gt;Workspace (Local)&lt;/strong&gt;. More on this below.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;URL mode&lt;/strong&gt; — a dropdown, default &lt;em&gt;Full URL (auto-detect)&lt;/em&gt;. The alternatives are BaseUrl modes for OpenAI Chat Completions, OpenAI Responses, and Anthropic, plus dedicated Gemini and Ollama entries. Auto-detect covers the common cases; reach for a specific mode when your endpoint isn't a full completions URL.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;API URL&lt;/strong&gt; — placeholder shows the shape it wants, e.g. &lt;code&gt;https://api.deepseek.com/v1/chat/completions&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;API Key&lt;/strong&gt; — &lt;code&gt;sk-...&lt;/code&gt;, with a show/hide toggle.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Model&lt;/strong&gt; — the provider's model identifier.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Context length&lt;/strong&gt; — a dropdown of common sizes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Timeout&lt;/strong&gt; — 120s by default.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Extra options&lt;/strong&gt; — a JSON field for things like &lt;code&gt;temperature&lt;/code&gt;, &lt;code&gt;top_p&lt;/code&gt;, or reasoning flags, with a &lt;strong&gt;Format JSON&lt;/strong&gt; button. Empty is a perfectly good answer here.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Set as default model&lt;/strong&gt; — a checkbox.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Filled in against a local Ollama instance:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwgxxtmhh5g1aywh6z7rb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwgxxtmhh5g1aywh6z7rb.png" alt="Add model form, filled" width="800" height="361"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then the part people skip: &lt;strong&gt;press Test Connection before you press Save.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A model that's added but unreachable looks identical to a model that works, right up until your first task dies mid-run and you're reading logs to figure out whether the problem was your key, your URL, or your proxy. The test button is right next to Save. Ten seconds there removes a whole category of confusion.&lt;/p&gt;

&lt;p&gt;After you save, the model list in the chat area refreshes to match — no restart. If you've accumulated a long list, the picker supports search filtering, so you type a fragment instead of scrolling.&lt;/p&gt;

&lt;h2&gt;
  
  
  Global or workspace
&lt;/h2&gt;

&lt;p&gt;The scope buttons in the form decide which file your model lands in:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scope&lt;/th&gt;
&lt;th&gt;File&lt;/th&gt;
&lt;th&gt;Use it for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;User (Global)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;~/.soloncode/settings.json&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Your everyday model, shared across every project&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Workspace (Local)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;.soloncode/settings.json&lt;/code&gt; in the project&lt;/td&gt;
&lt;td&gt;Models specific to this project&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;SolonCode reads the user-level file first, then the workspace file, and workspace config overrides or supplements what it found.&lt;/p&gt;

&lt;p&gt;Workspace scope has a second-order effect worth thinking about for a second. That file sits inside the project directory, which means it can be committed. Sharing endpoint and model choices with your team that way is genuinely useful. Sharing an API key that way is not. Decide which of those you're doing before you &lt;code&gt;git add&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Providers, when one key serves many models
&lt;/h2&gt;

&lt;p&gt;If you're adding a second and third model against the same endpoint, the &lt;strong&gt;Providers&lt;/strong&gt; section is the better place to work. It manages vendor config — API address, keys — as one entry, with add, modify, and enable/disable per provider.&lt;/p&gt;

&lt;p&gt;The reason to care is maintenance rather than setup. When a key rotates, you want one place to change it, not four model entries that each remember their own copy. Same story when you're moving an endpoint. Set the provider up once; point models at it.&lt;/p&gt;

&lt;h2&gt;
  
  
  First conversation
&lt;/h2&gt;

&lt;p&gt;Don't lead with a code change. Lead with a question, because the answer tells you whether the model is actually seeing your repo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Reply in English. What kind of project is this directory?
Check the build file and source layout before answering.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fx5gdeqjyd5lx6vmtie1j.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fx5gdeqjyd5lx6vmtie1j.png" alt="First conversation" width="800" height="361"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Watch the tool trace above the answer: &lt;code&gt;ls&lt;/code&gt;, a glob for &lt;code&gt;pom.xml&lt;/code&gt;, a read of that file, then a recursive listing of &lt;code&gt;src&lt;/code&gt;. It looked before it spoke. And what it reported back was that the skeleton exists — parent POM, one web dependency, standard Maven layout — but there's no source, no &lt;code&gt;app.yml&lt;/code&gt;, no tests, so nothing is actually written yet.&lt;/p&gt;

&lt;p&gt;That's the signal you're checking for. An agent that had described a working application there would be an agent you couldn't trust on a real repo. Note the footer too: token count, elapsed time, and running context usage, so you can see what a turn cost you.&lt;/p&gt;

&lt;p&gt;Once you're connected, give it a foundation to work from:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Generate .soloncode/CODE.md for this project, including build commands,
test commands, and notes on what to be careful about when modifying code.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If that file already exists, SolonCode prefers what's in it. That's the point — it's the engineering contract for the repo, and having it written down means you stop re-explaining your build every session.&lt;/p&gt;

&lt;p&gt;Now a small task, scoped tightly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Check whether the install instructions in README are out of date.
Docs only, no code changes. Tell me what you changed when you're done.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Small and reviewable on the first pass. You're not testing whether the agent can do impressive things yet; you're testing whether you can read and trust its output.&lt;/p&gt;

&lt;p&gt;For anything bigger, four things belong in the prompt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Goal: fix the NPE in the user login endpoint.
Scope: only the user module.
Constraint: do not change the response structure.
Verification: run the related unit tests; if none exist, say so.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Goal, scope, constraint, verification. The last one matters more than it looks — asking for verification is what turns "I made a change" into "I made a change and here's the evidence."&lt;/p&gt;

&lt;h2&gt;
  
  
  What else the panel covers
&lt;/h2&gt;

&lt;p&gt;Model config is one section out of nine. The rest are there when a specific need shows up: permissions and tool allow/deny lists, mount pools that expose external directories as Skills or Agents resources, a Skills marketplace with install support, MCP servers over stdio/SSE/streamable with connection checks and per-tool disabling, OpenAPI servers imported from a JSON/YAML spec, and LSP servers for code intelligence.&lt;/p&gt;

&lt;p&gt;None of that is required today, and the pattern is the same each time: open the section, fill in the entry, test, save.&lt;/p&gt;

&lt;p&gt;Saving generally writes through to the running engine, so most changes don't need a restart. When you're running several instances against the same config, there's a &lt;strong&gt;Reload config from disk&lt;/strong&gt; button at the top of the panel to bring them in sync.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two things to set before you expose it
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Web auth.&lt;/strong&gt; General settings has username and password fields for a login page, and they're empty by default — which is fine for &lt;code&gt;localhost&lt;/code&gt;, and not fine the moment you bind to an address other machines can reach. If you're running SolonCode on a dev box or a shared server, turn this on before anything else. An unauthenticated agent UI is a remote shell with a nicer font.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keys out of Git.&lt;/strong&gt; Global config lives in your home directory, nowhere near your repo. Workspace config lives in &lt;code&gt;.soloncode/settings.json&lt;/code&gt;, inside the project. Check what your &lt;code&gt;.gitignore&lt;/code&gt; does with it. The scan-for-leaked-keys conversation is much less pleasant than the two minutes this takes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Things you'll appreciate later
&lt;/h2&gt;

&lt;p&gt;Web-mode features that don't matter on day one and become daily habits by week two:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Mermaid rendering&lt;/strong&gt; — ask for an architecture diagram and read it as a diagram&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memory management&lt;/strong&gt; — inspect, edit, and delete long-term memory from the UI, with scope switching&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;File preview&lt;/strong&gt; — images, video, and Markdown, including fullscreen&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Conversation management&lt;/strong&gt; — pin, fork (copy), and delete threads&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Voice input&lt;/strong&gt; — hold to talk, release to send&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skins&lt;/strong&gt; — &lt;code&gt;default&lt;/code&gt;, &lt;code&gt;eyecare&lt;/code&gt;, &lt;code&gt;contrast&lt;/code&gt;, plus custom skins you upload as a zip&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And the keyboard, since you'll live here:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Key&lt;/th&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Ctrl + Enter&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Send&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;↑&lt;/code&gt; / &lt;code&gt;↓&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Cycle input history&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;/&lt;/code&gt; then Tab&lt;/td&gt;
&lt;td&gt;Command completion&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;@&lt;/code&gt; then Tab&lt;/td&gt;
&lt;td&gt;Subagent completion&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  When you don't want a UI at all
&lt;/h2&gt;

&lt;p&gt;For scripts, CI, or a one-shot job, skip the interactive console entirely:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;soloncode run &lt;span class="s2"&gt;"Summarize the current project structure"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same engine, same config, no browser. The settings you just made in the web panel apply here too — which is the actual reason the panel is worth using first. You configure once, visually, with a connection test to confirm it, and every other entry point inherits it.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Sources&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Quick start, install to first conversation — &lt;a href="https://solon.noear.org/article/1467" rel="noopener noreferrer"&gt;https://solon.noear.org/article/1467&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;settings.json and the Web settings page — &lt;a href="https://solon.noear.org/article/1465" rel="noopener noreferrer"&gt;https://solon.noear.org/article/1465&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Web interactive mode — &lt;a href="https://solon.noear.org/article/1442" rel="noopener noreferrer"&gt;https://solon.noear.org/article/1442&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;SolonCode CLI index — &lt;a href="https://solon.noear.org/article/soloncode" rel="noopener noreferrer"&gt;https://solon.noear.org/article/soloncode&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Screens captured live on SolonCode v2026.8.4, 2026-08-04, UI set to English&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>java</category>
      <category>ai</category>
      <category>tools</category>
      <category>opensource</category>
    </item>
    <item>
      <title>SolonCode in 15 Minutes: Install, Configure a Model, and Land Your First Reviewable Diff</title>
      <dc:creator>Solon Framework</dc:creator>
      <pubDate>Mon, 03 Aug 2026 16:02:56 +0000</pubDate>
      <link>https://dev.to/solonjava/soloncode-in-15-minutes-install-configure-a-model-and-land-your-first-reviewable-diff-3pcm</link>
      <guid>https://dev.to/solonjava/soloncode-in-15-minutes-install-configure-a-model-and-land-your-first-reviewable-diff-3pcm</guid>
      <description>&lt;p&gt;If your experience with coding agents is "it installed and it chats," you haven't had a first success yet — not the kind you can screenshot and show your team. Most getting-started posts stop at "I talked to it once," which doesn't tell you whether the tool can actually touch a real repository. This guide fixes that with a different yardstick.&lt;/p&gt;

&lt;p&gt;SolonCode is an open-source coding agent written in Java, built on the Solon AI framework, and designed to run on Java 8 through Java 26. One honest caveat up front: its prompt system is built around Chinese-first interaction (the official docs state it's not recommended if you can't work with Chinese prompts) — which makes it an interesting pick for Chinese-speaking teams and for developers who want a model-agnostic agent they can run on their own gateway. Everything below is verified against the official docs as of SolonCode v2026.8.3.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "first success" actually means here
&lt;/h2&gt;

&lt;p&gt;Most short tutorials leave you with three failure modes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The model won't connect, and you can't tell whether that counts as a failure or a misconfiguration.&lt;/li&gt;
&lt;li&gt;You chatted, but the agent never modified a real file, so you still don't trust it.&lt;/li&gt;
&lt;li&gt;Something breaks and you don't know whether to check the API key, the proxy, or the workspace path.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So this guide sets three concrete pass criteria — L1, L2, L3 — and gives you a fixed path: install → configure a model in the Web UI → land a small reviewable change. The goal isn't to learn all of SolonCode in 15 minutes; it's to earn one screenshot-worthy first success that you can explain to a colleague.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites (2-minute self-check)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Requirement&lt;/th&gt;
&lt;th&gt;How to verify&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;JDK&lt;/td&gt;
&lt;td&gt;Java 8 or higher (official support: Java 8 ~ 26)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;java -version&lt;/code&gt; prints a version&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OS&lt;/td&gt;
&lt;td&gt;macOS / Linux / Windows (Harmony PC also supported)&lt;/td&gt;
&lt;td&gt;you can run a shell / PowerShell&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Model API&lt;/td&gt;
&lt;td&gt;any LLM endpoint in OpenAI / Anthropic / Gemini / Ollama-compatible form&lt;/td&gt;
&lt;td&gt;you have &lt;code&gt;apiUrl&lt;/code&gt;, &lt;code&gt;apiKey&lt;/code&gt; (if needed), and a model name&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;SolonCode does not bundle or bind to any vendor — you bring your own model (BYOK) or point it at an internal gateway. Without a working model configuration, the process starts but that's not a first success.&lt;/p&gt;

&lt;p&gt;Nice-to-haves (not blockers): Git (helps you view diffs in the Web UI), a small real project directory (the launch directory becomes the workspace), and stable network / correct proxy settings.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install: one command, then verify the version
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;macOS / Linux / Harmony PC:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://solon.noear.org/soloncode/setup.sh | bash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Windows (PowerShell):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;irm&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;https://solon.noear.org/soloncode/setup.ps1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;iex&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Re-running the same install command updates the program while keeping your config and definition files. Program and user-level config default to &lt;code&gt;~/.soloncode/&lt;/code&gt; (Windows: &lt;code&gt;.soloncode&lt;/code&gt; under your home directory).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Minimum success check:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;soloncode version
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If it prints a version string, you're good to continue. If you get &lt;code&gt;command not found&lt;/code&gt;: re-run the installer, check that &lt;code&gt;soloncode&lt;/code&gt; is on your PATH, and open a new terminal window (PATH changes often require a fresh shell).&lt;/p&gt;

&lt;p&gt;For offline / intranet installs, the official path is: download &lt;code&gt;soloncode-cli-bin-*.tar.gz&lt;/code&gt; from &lt;a href="https://gitee.com/opensolon/soloncode/releases" rel="noopener noreferrer"&gt;Gitee Releases&lt;/a&gt; on a networked machine, copy it over, extract, and run &lt;code&gt;install.sh&lt;/code&gt; / &lt;code&gt;install.ps1&lt;/code&gt;. For your first success, do it online first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Configure your first model in the Web UI
&lt;/h2&gt;

&lt;p&gt;The officially recommended path for new users is the Web settings page — no need to hand-edit config files on day one.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;soloncode web 0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;web&lt;/code&gt; command behavior: &lt;code&gt;soloncode web&lt;/code&gt; uses default port &lt;strong&gt;4808&lt;/strong&gt;; &lt;code&gt;soloncode web 0&lt;/code&gt; picks an available port automatically (handy when 4808 is taken); &lt;code&gt;soloncode web 1212&lt;/code&gt; binds a specific port. The terminal prints something like &lt;code&gt;Web interface: http://localhost:xxxxx/&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Then:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open &lt;strong&gt;Settings → LLM&lt;/strong&gt; (or equivalent) in the Web UI.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add a model&lt;/strong&gt; with at least: API URL (&lt;code&gt;apiUrl&lt;/code&gt;), API key (if the provider needs one), and model name (exactly as shown in your provider console).&lt;/li&gt;
&lt;li&gt;Use the &lt;strong&gt;"Test connection"&lt;/strong&gt; button — the official docs emphasize it works.&lt;/li&gt;
&lt;li&gt;Make sure the model is &lt;strong&gt;enabled&lt;/strong&gt; and appears in the conversation's model list.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Config lands in &lt;code&gt;settings.json&lt;/code&gt;: user-level at &lt;code&gt;~/.soloncode/settings.json&lt;/code&gt; (shared across projects), workspace-level at &lt;code&gt;.soloncode/settings.json&lt;/code&gt; (project-specific). Workspace settings are read after user-level and can override or extend them. Security note: keys are secrets — never commit &lt;code&gt;settings.json&lt;/code&gt; to Git; use a secret manager or per-developer local config for team setups.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Model-configuration success checklist:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Test connection passes&lt;/li&gt;
&lt;li&gt;[ ] A "hello" message gets a normal model reply (not timeout / 401 / empty)&lt;/li&gt;
&lt;li&gt;[ ] The current model name shows near the UI title (e.g. &lt;code&gt;Model:deepseek-v4-flash&lt;/code&gt; — whatever you configured)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Only now are you "installed and talking." That's not yet a coding success.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enter your project (workspace = launch directory)
&lt;/h2&gt;

&lt;p&gt;The official docs recommend starting inside the project root; the launch directory becomes the current workspace.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; /path/to/your-project
soloncode web 0
&lt;span class="c"&gt;# or&lt;/span&gt;
soloncode cli
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No project yet? You can explore conversation and file generation in any empty directory, but L2/L3 strongly benefit from a real repository — otherwise your "demo" has no persuasive power. When the CLI starts, the tips line looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Tips: (esc) interrupt | /(tab) command | $(tab) skill | @(tab) agent
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;esc&lt;/code&gt; interrupts; commands / skills / subagents have tab completion. Natural language is enough for your first success.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three-level task: L1 → L2 → L3
&lt;/h2&gt;

&lt;p&gt;Suggested time budget (roughly 15 minutes total, model speed dependent): L1 ~3 min, L2 5–7 min, L3 5–7 min. If the model is slow or the repo is large, do L2 thoroughly rather than skipping review and rushing L3.&lt;/p&gt;

&lt;h3&gt;
  
  
  L1 — greeting and project scan
&lt;/h3&gt;

&lt;p&gt;Send (in Chinese — this is the product's first-class interaction language):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;请先阅读当前项目结构，告诉我：
1）这是什么技术栈；
2）构建命令和测试命令分别可能是什么；
3）如果存在 .soloncode/CODE.md，请优先参考它。
不要修改任何文件。
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the project has no &lt;code&gt;CODE.md&lt;/code&gt; yet, the official quick start suggests continuing with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;请根据当前项目生成 .soloncode/CODE.md，包含构建命令、测试命令和代码修改注意事项。
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;L1 pass criteria (all must hold):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] The tech-stack description roughly matches the repo (honest "needs further confirmation" is allowed)&lt;/li&gt;
&lt;li&gt;[ ] Build/test entry points are stated or inferred (or honestly reported as not found)&lt;/li&gt;
&lt;li&gt;[ ] When you asked for no file changes, &lt;code&gt;git status&lt;/code&gt; shows no unexpected dirty files&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Common L1 failures: not starting in the project root; model misconfigured; empty repo or unreadable permissions.&lt;/p&gt;

&lt;h3&gt;
  
  
  L2 — a small change + explain the diff (first "demoable" step)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Task A (docs, lowest risk):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;请帮我检查 README 中是否有过时的安装说明，只修改文档，不改业务代码。
完成后：
1）列出改了哪些文件；
2）用简短条目说明每处为什么改；
3）不要提交 Git，交给我人工确认。
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Task B (code, still small):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;请新增一个简单的健康检查接口（或补全已有 health 相关说明），范围尽量小。
限制：
- 不要做大范围重构；
- 不要改无关模块；
- 改完说明如何本地验证；
- 不要 git commit。
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;L2 pass criteria:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Pointable file changes exist (visible in Web Git Diff or &lt;code&gt;git diff&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;[ ] The agent explains in natural language what changed and why&lt;/li&gt;
&lt;li&gt;[ ] You scanned the diff yourself — no keys, no huge unrelated reformatting, nothing deleted by accident&lt;/li&gt;
&lt;li&gt;[ ] You can describe this change to a colleague in one sentence (that's "demoable")&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Human review checklist (mandatory, 30s–2min):&lt;/strong&gt; did the change exceed your allowed scope? Any API keys, intranet addresses, or passwords? Any lockfiles or generated artifacts modified? Can you actually run the verification steps yourself?&lt;/p&gt;

&lt;p&gt;This is where SolonCode's value starts showing: it advances the implementation; you guard the boundary and the merge decision.&lt;/p&gt;

&lt;h3&gt;
  
  
  L3 — a constrained small feature (must be reviewable)
&lt;/h3&gt;

&lt;p&gt;Only after L2 passes. The prompt template includes the four elements the official docs emphasize: &lt;strong&gt;goal, scope, constraints, verification&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;目标：{one sentence describing the deliverable behavior, e.g. 为用户模块增加按邮箱查询的只读接口}
范围：只允许修改 {包名/目录} 下的文件；文档仅在必要时更新 README 一小节。
限制：
- 不要改变既有接口的返回结构；
- 不要升级依赖版本；
- 不要执行删除文件、强制推送、修改 CI 密钥类操作；
- 不要 git commit / push。
验证：
- 修改后运行：{e.g. mvn -pl xxx test 或 npm test 或具体命令}；
- 若无法运行测试，请说明阻塞原因与你已做的静态检查。
交付：
1）变更文件列表；
2）行为说明（给 reviewer 看）；
3）你执行过的命令与结果摘要；
4）残留风险（若有）。
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;L3 pass criteria:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] All four deliverables present (list / explanation / command results / risks)&lt;/li&gt;
&lt;li&gt;[ ] The diff is reviewable by a third person (no mysterious large-scale reshuffling)&lt;/li&gt;
&lt;li&gt;[ ] Verification commands were run, or the blocker is credible&lt;/li&gt;
&lt;li&gt;[ ] You explicitly decide: accept / partially accept / roll back — the decision stays with a human&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;L3 explicitly should NOT be:&lt;/strong&gt; whole-site rewrites, multi-service migration in one shot, "refactoring as a side effect" without tests, production data operations, permission escalation, key rotation — and never treat "one conversation" as "can merge without review."&lt;/p&gt;

&lt;h2&gt;
  
  
  Screenshot checklist (for your team channel)
&lt;/h2&gt;

&lt;p&gt;Four to six screenshots beat a paragraph of adjectives:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;What to capture&lt;/th&gt;
&lt;th&gt;What it proves&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;soloncode version&lt;/code&gt; output&lt;/td&gt;
&lt;td&gt;installed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Web Settings → LLM + test connection success&lt;/td&gt;
&lt;td&gt;model wired up&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;L1's reply about project structure&lt;/td&gt;
&lt;td&gt;reads the workspace&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Git Diff or &lt;code&gt;git diff&lt;/code&gt; snippet (mask sensitive info)&lt;/td&gt;
&lt;td&gt;actually changed code/docs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Agent's change summary + your verification output&lt;/td&gt;
&lt;td&gt;reviewable and verifiable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;(optional) Web file tree + workspace path&lt;/td&gt;
&lt;td&gt;right directory&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Top 10 failures and where to look first
&lt;/h2&gt;

&lt;p&gt;Debugging hint from the official docs: check on-disk logs under the workspace &lt;strong&gt;&lt;code&gt;.soloncode/logs/&lt;/code&gt;&lt;/strong&gt;; docs: &lt;a href="https://solon.noear.org/article/1415" rel="noopener noreferrer"&gt;logs &amp;amp; troubleshooting&lt;/a&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Check first&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;soloncode&lt;/code&gt; command not found&lt;/td&gt;
&lt;td&gt;PATH, installed for current user&lt;/td&gt;
&lt;td&gt;reinstall; new terminal; confirm &lt;code&gt;~/.soloncode/bin&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Java not 8+ / missing&lt;/td&gt;
&lt;td&gt;&lt;code&gt;java -version&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;install any JDK 8–26 release&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Web UI won't open&lt;/td&gt;
&lt;td&gt;terminal URL; port occupied&lt;/td&gt;
&lt;td&gt;use &lt;code&gt;web 0&lt;/code&gt; or another port; open the printed address manually&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Model connection fails&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;apiUrl&lt;/code&gt; / &lt;code&gt;apiKey&lt;/code&gt; / model name&lt;/td&gt;
&lt;td&gt;compare character-by-character with provider console; check proxy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;401 / 403&lt;/td&gt;
&lt;td&gt;key permissions &amp;amp; quota&lt;/td&gt;
&lt;td&gt;rotate key; check plan and IP restrictions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Timeout / spinner forever&lt;/td&gt;
&lt;td&gt;network, proxy, cross-border path&lt;/td&gt;
&lt;td&gt;switch to reachable gateway; increase timeout; try a lighter model&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;Chats but won't touch files&lt;/td&gt;
&lt;td&gt;prompt forbade changes; permission / sandbox&lt;/td&gt;
&lt;td&gt;L2: explicitly allow target paths; check tool permissions in settings&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;Modified wrong directory&lt;/td&gt;
&lt;td&gt;launch dir ≠ repo root&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;cd&lt;/code&gt; to the root containing &lt;code&gt;pom.xml&lt;/code&gt; / &lt;code&gt;package.json&lt;/code&gt; / &lt;code&gt;.git&lt;/code&gt; before starting&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;Ignoring instructions&lt;/td&gt;
&lt;td&gt;language expectations&lt;/td&gt;
&lt;td&gt;the product is Chinese-prompt-driven; write constraints in Chinese&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;Weird behavior / stale state&lt;/td&gt;
&lt;td&gt;long or old session context&lt;/td&gt;
&lt;td&gt;start a new session; complex tasks → check task state under &lt;code&gt;.soloncode/sessions/&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Mental note on safety: HITL (human-in-the-loop) switches like &lt;code&gt;hitlEnabled&lt;/code&gt; exist in settings. During your first success, keep the default conservative posture — don't disable safety-related limits just to go faster on a production repo.&lt;/p&gt;

&lt;h2&gt;
  
  
  After first success: only two compounding steps
&lt;/h2&gt;

&lt;p&gt;Don't dive straight into Loop / multi-agent / IM binding. The smallest next loop is:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Write a 10-line project &lt;code&gt;AGENTS.md&lt;/code&gt;&lt;/strong&gt; at &lt;code&gt;.soloncode/AGENTS.md&lt;/code&gt; (workspace beats user-level &lt;code&gt;~/.soloncode/AGENTS.md&lt;/code&gt;). Minimal copy-paste template:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# 本项目 Agent 规约（精简）&lt;/span&gt;

&lt;span class="gu"&gt;## 必须&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; 改代码前先读相关文件；小步修改
&lt;span class="p"&gt;-&lt;/span&gt; 优先运行项目既有测试命令（见 CODE.md）
&lt;span class="p"&gt;-&lt;/span&gt; 完成后给出：变更列表、验证方式、风险

&lt;span class="gu"&gt;## 禁止&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; 无确认不删文件、不 force push、不改密钥与 CI 密钥
&lt;span class="p"&gt;-&lt;/span&gt; 不引入与任务无关的依赖升级
&lt;span class="p"&gt;-&lt;/span&gt; 不把 API Key 写进仓库

&lt;span class="gu"&gt;## 技术偏好&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; 语言/框架：{填写}
&lt;span class="p"&gt;-&lt;/span&gt; 代码风格：与现有文件保持一致
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The official advice: keep it short so it doesn't eat your context; state identity, boundaries, and workflow clearly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Install exactly one Skill.&lt;/strong&gt; Either via Web &lt;strong&gt;Settings → Skill Market&lt;/strong&gt; (browse, read the description, then install to global or workspace pool), or manually: drop a directory containing &lt;code&gt;SKILL.md&lt;/code&gt; into &lt;code&gt;~/.soloncode/skills/&lt;/code&gt; or &lt;code&gt;.soloncode/skills/&lt;/code&gt;. Trigger it with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;请使用 {技能名} 按它的规约帮我完成 {一件具体事}。
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;CLI skill completion: &lt;code&gt;$(tab)&lt;/code&gt;. Principle: read the &lt;code&gt;SKILL.md&lt;/code&gt; scenario and permissions before installing. More skills ≠ stronger agent; more &lt;strong&gt;relevant&lt;/strong&gt; skills = stronger agent. Official skills doc: &lt;a href="https://solon.noear.org/article/1408" rel="noopener noreferrer"&gt;agent skills&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Boundaries: what is NOT first success, and when to stop
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Not first success yet:&lt;/strong&gt; you only opened the web page and the model test fails; you only chatted and never produced a reviewable diff in a real repo; you have a diff but never looked at it and can't explain it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stop for today if:&lt;/strong&gt; the target is a production main branch with no tests and no one available to review; the ask is architecture-level rewrite; or you have no legitimate model access path (account / gateway / compliance).&lt;/p&gt;

&lt;p&gt;SolonCode is a delegatable coding agent, not an auto-disclaimer machine. &lt;strong&gt;People own direction, boundaries, and merge; the agent advances within the boundary.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The 15-minute path (one page)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. java -version            # 8+
2. curl ...setup.sh | bash  # or irm ...setup.ps1 | iex
3. soloncode version
4. soloncode web 0
5. Settings → LLM → Add model → Test connection → say "你好"
6. cd your-project &amp;amp;&amp;amp; soloncode web 0
7. L1: scan the project (no file changes)
8. L2: small change + you review the diff
9. (optional) L3: constrained small feature + verification commands
10. Write a 10-line .soloncode/AGENTS.md or install one skill
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why this matters
&lt;/h2&gt;

&lt;p&gt;First success isn't about faith in AI; it's about four real things: a real environment (JDK + command on PATH), a real model (test the connection, don't just save the form), a real task (a reviewable small change in the project root), and real review (a human reads the diff and clicks merge).&lt;/p&gt;

&lt;p&gt;When you can repeat L2 reliably, SolonCode stops being "another chat window" and becomes the onboarding day of a digital employee in your workflow. The next steps — model routing &amp;amp; cost (series B2), codifying specs into &lt;code&gt;AGENTS.md&lt;/code&gt; (series D3), or remote delegation via IM (series C2) — all build on the pass criteria above.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Try it now:&lt;/strong&gt; run the first 8 steps of the one-page path and keep the L2 diff screenshot. That's the most honest manual SolonCode can give you.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;References (all official):&lt;/em&gt; &lt;a href="https://github.com/opensolon/soloncode" rel="noopener noreferrer"&gt;Repository&lt;/a&gt; · &lt;a href="https://solon.noear.org/article/soloncode" rel="noopener noreferrer"&gt;Docs entry&lt;/a&gt; · &lt;a href="https://solon.noear.org/article/1467" rel="noopener noreferrer"&gt;Quick start: install to first conversation&lt;/a&gt; · &lt;a href="https://solon.noear.org/article/1404" rel="noopener noreferrer"&gt;Install / update / uninstall&lt;/a&gt; · &lt;a href="https://solon.noear.org/article/1465" rel="noopener noreferrer"&gt;Settings &amp;amp; Web UI&lt;/a&gt; · &lt;a href="https://solon.noear.org/article/1442" rel="noopener noreferrer"&gt;Web interaction mode&lt;/a&gt; · &lt;a href="https://solon.noear.org/article/1411" rel="noopener noreferrer"&gt;Workspace &amp;amp; AGENTS.md / CODE.md&lt;/a&gt; · &lt;a href="https://solon.noear.org/article/1408" rel="noopener noreferrer"&gt;Skills&lt;/a&gt; · &lt;a href="https://solon.noear.org/article/1415" rel="noopener noreferrer"&gt;Logs &amp;amp; troubleshooting&lt;/a&gt; · &lt;a href="https://gitee.com/opensolon/soloncode/releases" rel="noopener noreferrer"&gt;Offline releases (Gitee)&lt;/a&gt;&lt;/p&gt;

</description>
      <category>java</category>
      <category>ai</category>
      <category>opensource</category>
      <category>tools</category>
    </item>
    <item>
      <title>Solon Server Threads: Zero-Config Auto-Tuning by CPU Cores — ioBound, coreThreads, maxThreads</title>
      <dc:creator>Solon Framework</dc:creator>
      <pubDate>Mon, 03 Aug 2026 15:58:02 +0000</pubDate>
      <link>https://dev.to/solonjava/solon-server-threads-zero-config-auto-tuning-by-cpu-cores-iobound-corethreads-maxthreads-153a</link>
      <guid>https://dev.to/solonjava/solon-server-threads-zero-config-auto-tuning-by-cpu-cores-iobound-corethreads-maxthreads-153a</guid>
      <description>&lt;p&gt;It was 2 AM, and the on-call chat was on fire again: the order service was healthy on every dashboard, but throughput had flatlined at ~800 req/s while P99 climbed past 4 seconds. The usual suspect? A thread pool sized by guesswork during a late-night deploy, six months earlier. We'd hand-tuned &lt;code&gt;maxThreads&lt;/code&gt; to "something that felt right," and it wasn't right anymore.&lt;/p&gt;

&lt;p&gt;That's the moment I started appreciating a different default: in Solon, all of those knobs ship as &lt;code&gt;0&lt;/code&gt; — meaning &lt;em&gt;auto&lt;/em&gt;, derived from your machine's actual CPU cores at runtime. You can go months without thinking about a single thread-pool property. This post walks through the five knobs that exist, how the auto-tuning math works, and the three failure modes that tell you it's time to touch them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The five knobs under the hood
&lt;/h2&gt;

&lt;p&gt;Solon exposes these on &lt;code&gt;app.yml&lt;/code&gt; (all values are the documented defaults):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Minimum threads for the http server (0 = auto; also accepts fixed values like 2, or core multiples like x2)&lt;/span&gt;
&lt;span class="na"&gt;server.http.coreThreads&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
&lt;span class="c1"&gt;# Maximum threads for the http server (0 = auto; also accepts fixed values like 32, or core multiples like x32)&lt;/span&gt;
&lt;span class="na"&gt;server.http.maxThreads&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
&lt;span class="c1"&gt;# Idle thread timeout in ms (0 = auto)   # supported since v1.10.13&lt;/span&gt;
&lt;span class="na"&gt;server.http.idleTimeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
&lt;span class="c1"&gt;# Is this an IO-bound service? (default true)   # supported since v1.12.2&lt;/span&gt;
&lt;span class="na"&gt;server.http.ioBound&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

&lt;span class="c1"&gt;# Enable the virtual thread pool (default false)   # supported since v2.7.3&lt;/span&gt;
&lt;span class="na"&gt;solon.threads.virtual.enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice what's missing: no hard-coded defaults for &lt;code&gt;coreThreads&lt;/code&gt; or &lt;code&gt;maxThreads&lt;/code&gt;. &lt;code&gt;0&lt;/code&gt; means "figure it out from the hardware." That single decision removes a whole class of "copy-pasted tuning values" problems — the ones that were right for someone else's 32-core box and wrong for your 2-core container.&lt;/p&gt;

&lt;h2&gt;
  
  
  CPU-bound or IO-bound: the one question that matters
&lt;/h2&gt;

&lt;p&gt;The auto-tuner only needs you to answer one question: is your workload CPU-bound or IO-bound?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CPU-bound&lt;/strong&gt;: the work happens entirely in CPU and memory — think a "hello world" handler that returns a string. Responses are extremely fast. Here, &lt;em&gt;more&lt;/em&gt; threads just means more context-switch overhead; you're paying for nothing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;IO-bound&lt;/strong&gt;: the work touches the network card or disk — a CRUD write to a database, an uploaded file flushed to disk. These requests can take 10+ seconds, and each one occupies a thread while it waits. Threads run out fast.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;code&gt;server.http.ioBound&lt;/code&gt; flag tells the framework which world you live in, and the auto-tuner adjusts accordingly.&lt;/p&gt;

&lt;h2&gt;
  
  
  The auto-tuning math
&lt;/h2&gt;

&lt;p&gt;With &lt;code&gt;server.http.ioBound: true&lt;/code&gt; (the default):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setting&lt;/th&gt;
&lt;th&gt;Formula&lt;/th&gt;
&lt;th&gt;Example: 2c4g box&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;coreThreads&lt;/td&gt;
&lt;td&gt;CPU cores × 2&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;maxThreads&lt;/td&gt;
&lt;td&gt;coreThreads × 32&lt;/td&gt;
&lt;td&gt;128&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;With &lt;code&gt;server.http.ioBound: false&lt;/code&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setting&lt;/th&gt;
&lt;th&gt;Formula&lt;/th&gt;
&lt;th&gt;Example: 2c4g box&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;coreThreads&lt;/td&gt;
&lt;td&gt;CPU cores × 2&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;maxThreads&lt;/td&gt;
&lt;td&gt;coreThreads × 8&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;IO-bound services get 4× the headroom — because each thread spends most of its life &lt;em&gt;waiting&lt;/em&gt; on IO, you need more of them in flight to keep the machine busy. CPU-bound services stay lean, because the CPU is the bottleneck and extra threads only add switching costs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Doing the math yourself, the two-minute version
&lt;/h2&gt;

&lt;p&gt;Before you override anything, it helps to estimate what a setting is worth:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Throughput (QPS).&lt;/strong&gt; If one request takes 0.1 s to respond, one thread can serve ~10 requests per second. 100 threads → ~1000 QPS. Thread count is a direct throughput ceiling.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Memory.&lt;/strong&gt; Each thread costs at least 1–2 MB. 100 threads ≈ 200 MB just sitting there. A 200 KB request payload can exist in ~4 copies during processing (≈800 KB), so at 1000 QPS you're churning ~800 MB/s of request data — and if GC only frees it after ~5 s, that's ~4 GB/s of live pressure. Thread tuning and memory tuning are the same conversation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why coreThreads is the "do not touch" knob
&lt;/h2&gt;

&lt;p&gt;Two reasons, depending on the underlying transport:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;For &lt;strong&gt;BIO&lt;/strong&gt; servers, a large &lt;code&gt;coreThreads&lt;/code&gt; means the pool never shrinks — your "minimum" becomes a permanent resident of RAM.&lt;/li&gt;
&lt;li&gt;For &lt;strong&gt;NIO&lt;/strong&gt; servers, &lt;code&gt;coreThreads&lt;/code&gt; must stay small because the pool is two-staged: &lt;code&gt;coreThreads&lt;/code&gt; is stage one, &lt;code&gt;maxThreads&lt;/code&gt; is stage two. Bloating stage one defeats the whole design.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Default it, and forget it. If you're going to change anything, it's &lt;code&gt;maxThreads&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three ways "threads are exhausted" shows up
&lt;/h2&gt;

&lt;p&gt;When &lt;code&gt;maxThreads&lt;/code&gt; is genuinely too small, the symptoms are distinct — and each one tells you &lt;em&gt;where&lt;/em&gt; the bottleneck is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;It stalls, then recovers.&lt;/strong&gt; The pool rejects a submission and the main thread takes over the work. The main thread can't accept new requests while it's busy, so things pause — then resume once the backlog clears. Annoying, but survivable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The protocol returns an error / refuses service.&lt;/strong&gt; The server actively rejects requests with an error, so the client learns &lt;em&gt;immediately&lt;/em&gt; that the server is overwhelmed. This is the polite refusal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The connection just drops.&lt;/strong&gt; The threads that &lt;em&gt;parse the protocol&lt;/em&gt; are exhausted too — there's no thread left to even craft a proper error response, so the server slams the connection shut.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Symptom 3 is the emergency red line. Symptom 1 is your early warning.&lt;/p&gt;

&lt;h2&gt;
  
  
  When (and how) to actually override maxThreads
&lt;/h2&gt;

&lt;p&gt;"Leave it at the default" is the honest starting point. The framework's auto values handle the vast majority of single-instance deployments. You reach for &lt;code&gt;maxThreads&lt;/code&gt; when you're &lt;strong&gt;single-instance with heavy traffic or slow requests&lt;/strong&gt; — and you size it &lt;em&gt;against your memory budget&lt;/em&gt;, not against vibes.&lt;/p&gt;

&lt;p&gt;Official guidance example: on a &lt;strong&gt;1c2g&lt;/strong&gt; container, you might configure &lt;code&gt;x256&lt;/code&gt; → 512 threads. At ~2 MB per thread, that's ~1 GB of thread memory alone, leaving ~1 GB for business data processing. Tight — and the docs say so plainly: it may be borderline, and it may blow up memory. The point isn't the exact number; it's that you should &lt;em&gt;compute the memory cost&lt;/em&gt; before you type a bigger number.&lt;/p&gt;

&lt;h2&gt;
  
  
  Virtual threads: one flag, when your Java allows it
&lt;/h2&gt;

&lt;p&gt;Since v2.7.3, Solon has a dedicated switch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;solon.threads.virtual.enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;   &lt;span class="c1"&gt;# default: false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One property. If your runtime supports virtual threads (Java 21+), this is the "more concurrency without more native threads" lever — the alternative to hand-inflating &lt;code&gt;maxThreads&lt;/code&gt;. It exists precisely because the answer to "threads not enough" should not always be "increase the pool."&lt;/p&gt;

&lt;h2&gt;
  
  
  Context: how server configs relate
&lt;/h2&gt;

&lt;p&gt;If you're wondering where these keys fit in the wider config map: Solon splits server settings into four series — &lt;code&gt;server.*&lt;/code&gt; (the master config, reused by all signals), &lt;code&gt;server.http.*&lt;/code&gt;, &lt;code&gt;server.socket.*&lt;/code&gt;, and &lt;code&gt;server.websocket.*&lt;/code&gt;. When a signal-specific value is absent, it falls back to the master config. Ports are the special case: http uses the main port, socket defaults to main + 20000, websocket to main + 15000.&lt;/p&gt;

&lt;p&gt;And for the curious: Solon's http signals ship as tiny plugins — &lt;code&gt;solon-server-jdkhttp&lt;/code&gt; (0.1 MB, BIO-based) and &lt;code&gt;solon-server-smarthttp&lt;/code&gt; (0.5 MB, AIO-based). Your &lt;code&gt;server.http.*&lt;/code&gt; knobs tune whichever signal you pulled in. (For context, in the Spring world — general knowledge, not a Solon claim — you'd typically hand-edit something like &lt;code&gt;server.tomcat.threads.max&lt;/code&gt; and pick a fixed number yourself.)&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;Threads aren't "more is better," and they aren't "fewer is better" either — they're a budget you should spend knowingly. Solon's approach is to make the default a &lt;em&gt;computed&lt;/em&gt; number (CPU cores × 2 → × 32 or × 8 depending on &lt;code&gt;ioBound&lt;/code&gt;) instead of a hard-coded guess, so you only think about threads when your workload genuinely asks for it. When it does: size &lt;code&gt;maxThreads&lt;/code&gt; against memory, leave &lt;code&gt;coreThreads&lt;/code&gt; alone, and remember the virtual-thread flag is one line away.&lt;/p&gt;

&lt;p&gt;Next time your on-call chat lights up at 2 AM, the answer might be a single &lt;code&gt;server.http.maxThreads&lt;/code&gt; line — or the discovery that &lt;code&gt;0&lt;/code&gt; was doing fine all along.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Written while exploring Solon (v4.0.4), an Apache-2.0 Java framework. Facts checked against the official docs (solon.noear.org articles 513, 565, 131).&lt;/em&gt;&lt;/p&gt;

</description>
      <category>backend</category>
      <category>java</category>
      <category>performance</category>
    </item>
    <item>
      <title>Solon.cfg() Everything: The Programmatic Config Center Behind SolonProps — Typed Getters, Prefix Groups, and Bean Binding</title>
      <dc:creator>Solon Framework</dc:creator>
      <pubDate>Sun, 02 Aug 2026 10:07:35 +0000</pubDate>
      <link>https://dev.to/solonjava/soloncfg-everything-the-programmatic-config-center-behind-solonprops-typed-getters-prefix-1mig</link>
      <guid>https://dev.to/solonjava/soloncfg-everything-the-programmatic-config-center-behind-solonprops-typed-getters-prefix-1mig</guid>
      <description>&lt;p&gt;Most frameworks make you read config the same way: sprinkle &lt;code&gt;@Value&lt;/code&gt; annotations everywhere, or grab a string from &lt;code&gt;Environment&lt;/code&gt; and parse it yourself every single time. It works, but the code ends up with a hundred tiny, untestable couplings to a file somewhere.&lt;/p&gt;

&lt;p&gt;Solon's answer is different. Config is not scattered across annotations — it lives in one place, and you can reach it from anywhere in your app. That place is &lt;code&gt;SolonProps&lt;/code&gt;, the application's property collection (a.k.a. the config center). You grab it with a single static call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;SolonProps&lt;/span&gt; &lt;span class="n"&gt;props&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Solon&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. No injection, no context lookup. &lt;code&gt;Solon.cfg()&lt;/code&gt; is available anywhere, including in static contexts and utility classes. This post walks through the API surface you'll actually use — typed getters, prefix grouping, bean conversion, expression/template lookup, and dynamic loading.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Typed getters — no more manual parsing
&lt;/h2&gt;

&lt;p&gt;The most common thing you do with config is read a value. SolonProps gives you typed shortcuts so you never hand-parse a string:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"demo.name"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;              &lt;span class="c1"&gt;// String, or null&lt;/span&gt;
&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"demo.port"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"8080"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;      &lt;span class="c1"&gt;// with default&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt;    &lt;span class="n"&gt;max&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getInt&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"demo.max"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;       &lt;span class="c1"&gt;// int&lt;/span&gt;
&lt;span class="kt"&gt;long&lt;/span&gt;   &lt;span class="n"&gt;ttl&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getLong&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"demo.ttl"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3000L&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;    &lt;span class="c1"&gt;// long&lt;/span&gt;
&lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;ratio&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getDouble&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"demo.ratio"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// double&lt;/span&gt;
&lt;span class="kt"&gt;boolean&lt;/span&gt; &lt;span class="n"&gt;on&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getBool&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"demo.on"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;     &lt;span class="c1"&gt;// boolean&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's also &lt;code&gt;getByKeys("demo.url", "demo.url2")&lt;/code&gt; which returns the first non-null value across several keys — handy when you're migrating config keys and want a graceful fallback.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Prefix grouping — get a whole section at once
&lt;/h2&gt;

&lt;p&gt;This is where it gets interesting. Spring has &lt;code&gt;@ConfigurationProperties(prefix=...)&lt;/code&gt;, but Solon does it at runtime without any annotation. Pass a key prefix and you get back a fresh &lt;code&gt;Props&lt;/code&gt; collection with just that section:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app.yml&lt;/span&gt;
&lt;span class="c1"&gt;// demo.db:&lt;/span&gt;
&lt;span class="c1"&gt;//   url: jdbc:mysql://localhost:3306/demo&lt;/span&gt;
&lt;span class="c1"&gt;//   user: root&lt;/span&gt;
&lt;span class="c1"&gt;//   maxPool: 8&lt;/span&gt;

&lt;span class="nc"&gt;Props&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getProp&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"demo.db"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"url"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;pool&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getInt&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"maxPool"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three variants cover different shapes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;getProp(keyStarts)&lt;/code&gt; → a single &lt;code&gt;Props&lt;/code&gt; (one section)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;getGroupedProp(keyStarts)&lt;/code&gt; → &lt;code&gt;Map&amp;lt;String, Props&amp;gt;&lt;/code&gt; (multi-instance sections like &lt;code&gt;demo.db1&lt;/code&gt;, &lt;code&gt;demo.db2&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;getListedProp(keyStarts)&lt;/code&gt; → &lt;code&gt;Map&amp;lt;String, Props&amp;gt;&lt;/code&gt; (list-style indexed sections)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So multi-datasource or multi-client configs don't need custom parsing code. The framework splits them for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Bean conversion — config straight into objects
&lt;/h2&gt;

&lt;p&gt;Instead of reading keys one by one, bind an entire prefix to a POJO:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// demo.mail:&lt;/span&gt;
&lt;span class="c1"&gt;//   host: smtp.example.com&lt;/span&gt;
&lt;span class="c1"&gt;//   port: 587&lt;/span&gt;
&lt;span class="c1"&gt;//   from: no-reply@example.com&lt;/span&gt;

&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MailConfig&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;    &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;MailConfig&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toBean&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"demo.mail"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;MailConfig&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or bind the whole property set to an existing bean instance:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;MailConfig&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;MailConfig&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;bindTo&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// matches keys against bean fields&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;toBean(Class&amp;lt;T&amp;gt;)&lt;/code&gt; converts the entire collection; &lt;code&gt;toBean(prefix, Class&amp;lt;T&amp;gt;)&lt;/code&gt; scopes it to a prefix. &lt;code&gt;bindTo(bean)&lt;/code&gt; mutates an existing instance. No reflection boilerplate on your side.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Expression &amp;amp; template lookup
&lt;/h2&gt;

&lt;p&gt;Config values can reference each other (Solon resolves &lt;code&gt;${...}&lt;/code&gt; inside values at load time), but you can also do it on demand:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// expr:  ${key} | key | ${key:def} | key:def&lt;/span&gt;
&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;v1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getByExpr&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"demo.db.url"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;          &lt;span class="c1"&gt;// plain key&lt;/span&gt;
&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;v2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getByExpr&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"${demo.db.url}"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;       &lt;span class="c1"&gt;// explicit expr&lt;/span&gt;
&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;v3&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getByExpr&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"${demo.db.port:3306}"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// with default&lt;/span&gt;

&lt;span class="c1"&gt;// tmpl:  ${key} | aaa${key}bbb | ${key:def}/ccc&lt;/span&gt;
&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;t1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getByTmpl&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"jdbc:${demo.db.url}/db"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// template&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;getByExpr&lt;/code&gt; is for single expressions, &lt;code&gt;getByTmpl&lt;/code&gt; for embedding values inside larger strings. Both respect &lt;code&gt;:def&lt;/code&gt; defaults.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Loading &amp;amp; extending at runtime
&lt;/h2&gt;

&lt;p&gt;Solon apps load &lt;code&gt;app.yml&lt;/code&gt; automatically, but you can pull in more files at any time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;loadAdd&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"classpath:demo-ext.yml"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;        &lt;span class="c1"&gt;// from classpath&lt;/span&gt;
&lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;loadAdd&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="no"&gt;URL&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"file:/etc/app/extra.yml"&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// from URL&lt;/span&gt;
&lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;loadAdd&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getProperties&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;          &lt;span class="c1"&gt;// merge JVM props&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's also &lt;code&gt;loadAddIfAbsent(...)&lt;/code&gt; for incremental loading that won't clobber existing keys — useful for layering environment overrides. And if you build a plugin that needs to contribute config, &lt;code&gt;loadAdd(Properties)&lt;/code&gt; plus &lt;code&gt;plugs()&lt;/code&gt; (the plugin config collection) are your integration points.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Environment variables — bridge 12-factor style
&lt;/h2&gt;

&lt;p&gt;Kubernetes, Docker, and CI all prefer env vars over files. Pull them into your props with a prefix filter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;loadEnv&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"DEMO_"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;                          &lt;span class="c1"&gt;// all vars starting with DEMO_&lt;/span&gt;
&lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;loadEnv&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;contains&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"SOLON"&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;         &lt;span class="c1"&gt;// custom predicate&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Loaded vars become normal props, readable through all the typed getters above. One line, no third-party dotenv library.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. App &amp;amp; server metadata
&lt;/h2&gt;

&lt;p&gt;The framework exposes its own identity as typed getters — useful in logs, health checks, and admin pages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;appName&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;        &lt;span class="c1"&gt;// solon.app.name&lt;/span&gt;
&lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;appGroup&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;       &lt;span class="c1"&gt;// solon.app.group&lt;/span&gt;
&lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;appNamespace&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;   &lt;span class="c1"&gt;// solon.app.namespace&lt;/span&gt;
&lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;appTitle&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;       &lt;span class="c1"&gt;// solon.app.title&lt;/span&gt;
&lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;appEnabled&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;     &lt;span class="c1"&gt;// solon.app.enabled (health)&lt;/span&gt;
&lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;locale&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;         &lt;span class="c1"&gt;// solon.locale&lt;/span&gt;

&lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;serverPort&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;     &lt;span class="c1"&gt;// server.port&lt;/span&gt;
&lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;serverHost&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;     &lt;span class="c1"&gt;// server.host&lt;/span&gt;
&lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;serverContextPath&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// server.contextPath&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  8. Runtime mode checks
&lt;/h2&gt;

&lt;p&gt;Instead of passing flags around, ask the config center directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isDebugMode&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;      &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* dev-time behavior */&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isSetupMode&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;      &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* first-run setup     */&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isFilesMode&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;      &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* running from files  */&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isDriftMode&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;      &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* solon cloud drift   */&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isWhiteMode&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;      &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* security whitelist  */&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;testing&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;          &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* under unit test     */&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isEnabledVirtualThreads&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* JDK 21+        */&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  9. One subtle gotcha: removing a key
&lt;/h2&gt;

&lt;p&gt;Because props are synced to &lt;code&gt;System.getProperties()&lt;/code&gt; at load time, removing a key requires a double delete:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;remove&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"redis.onOff"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getProperties&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;remove&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"redis.onOff"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Forget the second line and the key can still surface through &lt;code&gt;System.getProperties()&lt;/code&gt; lookups. It's the one place where the two-way sync shows its face.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it fits compared to Spring
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;@Value("${x}")&lt;/code&gt; → &lt;code&gt;props.get("x")&lt;/code&gt; or the typed getters — no annotation needed, works in any context including static methods&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Environment.getProperty("x")&lt;/code&gt; → &lt;code&gt;Solon.cfg().get("x")&lt;/code&gt; — available without injecting &lt;code&gt;Environment&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;@ConfigurationProperties(prefix="x")&lt;/code&gt; → &lt;code&gt;props.getProp("x")&lt;/code&gt; / &lt;code&gt;props.toBean("x", X.class)&lt;/code&gt; — runtime, no compile-time binding&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Spring's approach is declarative and compile-checked; Solon's is a single runtime object with the same power, minus the annotation ceremony. Pick whichever fits your style — but know that in Solon the config center is always one call away.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Solon.cfg()&lt;/code&gt; gives you the whole config center, from anywhere, no injection&lt;/li&gt;
&lt;li&gt;Typed getters (&lt;code&gt;getInt/getLong/getBool/getDouble&lt;/code&gt;) kill manual parsing&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;getProp/getGroupedProp/getListedProp&lt;/code&gt; split config by prefix at runtime&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;toBean/bindTo&lt;/code&gt; convert config sections into POJOs without annotations&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;loadEnv(prefix)&lt;/code&gt; bridges env vars; &lt;code&gt;loadAdd/loadAddIfAbsent&lt;/code&gt; layer extra files&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;getByExpr/getByTmpl&lt;/code&gt; handle expression and template lookup with &lt;code&gt;:def&lt;/code&gt; defaults&lt;/li&gt;
&lt;li&gt;Deleting a key needs a double remove (props + &lt;code&gt;System.getProperties()&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Full API reference: &lt;a href="https://solon.noear.org/article/910" rel="noopener noreferrer"&gt;SolonProps Interface Reference&lt;/a&gt; — part of the official &lt;a href="https://solon.noear.org/article/482" rel="noopener noreferrer"&gt;Solon config guide&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>backend</category>
      <category>java</category>
      <category>softwaredevelopment</category>
    </item>
    <item>
      <title>Solon Config Vault: ENC() Secrets, @VaultInject, and a Runtime Key — No Jasypt, No Config Server</title>
      <dc:creator>Solon Framework</dc:creator>
      <pubDate>Sun, 02 Aug 2026 03:48:39 +0000</pubDate>
      <link>https://dev.to/solonjava/solon-config-vault-enc-secrets-vaultinject-and-a-runtime-key-no-jasypt-no-config-server-5hi5</link>
      <guid>https://dev.to/solonjava/solon-config-vault-enc-secrets-vaultinject-and-a-runtime-key-no-jasypt-no-config-server-5hi5</guid>
      <description>&lt;p&gt;Every team has that one &lt;code&gt;application.yml&lt;/code&gt; with a database password sitting in plain text, committed to a repo that half the company can read. You know it should not be there. But the fix usually means pulling in Jasypt, standing up a config server, or explaining to your ops team why they need to manage another secret store.&lt;/p&gt;

&lt;p&gt;Solon takes a much smaller route. There's a built-in plugin called &lt;code&gt;solon-security-vault&lt;/code&gt; that lets you encrypt sensitive values (database credentials, API keys, whatever) and mark them with an &lt;code&gt;ENC(...)&lt;/code&gt; prefix right in your config file. No external service. No new infrastructure. Just a dependency, a helper method, and one annotation.&lt;/p&gt;

&lt;p&gt;Full disclosure, straight from the official docs: this is "anti-honest-people, not anti-thieves." It keeps secrets from being &lt;em&gt;readily&lt;/em&gt; visible, it does not pretend to be a KMS. If you need defense-in-depth, pair it with a proper config center. But for the very common problem of "don't leave plaintext passwords in Git," it's a clean, pragmatic answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shape of it
&lt;/h2&gt;

&lt;p&gt;You configure one vault password, then any value wrapped in &lt;code&gt;ENC(...)&lt;/code&gt; gets decrypted on injection:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;solon.vault&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;liylU9PhDq63tk1C"&lt;/span&gt;

&lt;span class="na"&gt;test.db1&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;jdbc:mysql://localhost:3306/demo"&lt;/span&gt;
  &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ENC(xo1zJjGXUouQ/CZac55HZA==)"&lt;/span&gt;
  &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ENC(XgRqh3C00JmkjsPi4mPySA==)"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three things to notice:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The vault password itself can come from a JVM arg at runtime instead of living in the file (more on that below).&lt;/li&gt;
&lt;li&gt;The default algorithm expects a 16-character password — mixed case and digits are recommended.&lt;/li&gt;
&lt;li&gt;Only the values wrapped in &lt;code&gt;ENC(...)&lt;/code&gt; are affected. Everything else stays as normal config.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Step 1 — Add the plugin
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;org.noear&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;solon-security-vault&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 2 — Generate ciphertext
&lt;/h2&gt;

&lt;p&gt;You don't hand-roll the encrypted strings. A tiny helper in your app prints them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TestApp&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;throws&lt;/span&gt; &lt;span class="nc"&gt;Exception&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;Solon&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;start&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;TestApp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// prints the encrypted form of "root"&lt;/span&gt;
        &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;VaultUtils&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;encrypt&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"root"&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it once, copy the output into your &lt;code&gt;ENC(...)&lt;/code&gt; values, and delete the helper. (Or keep it behind a flag for rotating secrets later.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3 — Inject with @VaultInject
&lt;/h2&gt;

&lt;p&gt;The idiomatic way is the &lt;code&gt;@VaultInject&lt;/code&gt; annotation. It decrypts on the way in, so the rest of your code never sees the ciphertext:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Configuration&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TestConfig&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;@Bean&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"db2"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;DataSource&lt;/span&gt; &lt;span class="nf"&gt;db2&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;@VaultInject&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"${test.db1}"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nc"&gt;HikariDataSource&lt;/span&gt; &lt;span class="n"&gt;ds&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ds&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The manual route: VaultUtils.guard()
&lt;/h2&gt;

&lt;p&gt;Not everything fits an annotation. When you need to decrypt imperatively, &lt;code&gt;VaultUtils&lt;/code&gt; has you covered — either a whole config block or a single value:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// decrypt a whole config block&lt;/span&gt;
&lt;span class="nc"&gt;Props&lt;/span&gt; &lt;span class="n"&gt;props&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Solon&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;getProp&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"test.db1"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="nc"&gt;VaultUtils&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;guard&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="nc"&gt;HikariDataSource&lt;/span&gt; &lt;span class="n"&gt;ds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getBean&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;HikariDataSource&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// decrypt a single value&lt;/span&gt;
&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;VaultUtils&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;guard&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Solon&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"test.demo.name"&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This composes nicely with &lt;code&gt;Solon.cfg()&lt;/code&gt; — the same property-collection entry point you already use for everything else. No second API to learn.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep the key out of the file
&lt;/h2&gt;

&lt;p&gt;Hardcoding the vault password next to the ciphertext is only marginally better than hardcoding the plaintext. The docs suggest passing it at runtime instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;java &lt;span class="nt"&gt;-Dsolon&lt;/span&gt;.vault.password&lt;span class="o"&gt;=&lt;/span&gt;your-16-char-key &lt;span class="nt"&gt;-jar&lt;/span&gt; demo.jar
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And if you're used to &lt;code&gt;--key=value&lt;/code&gt; style args, Solon treats both forms equivalently — any startup arg containing a dot also becomes an app property. So &lt;code&gt;--solon.vault.password=...&lt;/code&gt; works just the same. That means CI/CD pipelines can inject the key per environment without touching the repo at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bring your own algorithm
&lt;/h2&gt;

&lt;p&gt;The plugin ships with an &lt;code&gt;AesVaultCoder&lt;/code&gt;, and the &lt;code&gt;VaultCoder&lt;/code&gt; interface is your extension point. Either implement it as a component or register an existing implementation as a bean:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Component&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;VaultCoderImpl&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;VaultCoder&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;VaultCoderImpl&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// even the password's config key can be changed&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;password&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Solon&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"solon.vault.password"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="nf"&gt;encrypt&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;str&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;throws&lt;/span&gt; &lt;span class="nc"&gt;Exception&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="nf"&gt;decrypt&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;str&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;throws&lt;/span&gt; &lt;span class="nc"&gt;Exception&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or swap in a ready-made coder:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Configuration&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Config&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;@Bean&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;VaultCoder&lt;/span&gt; &lt;span class="nf"&gt;vaultCoderInit&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;AesVaultCoder&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The honest boundary
&lt;/h2&gt;

&lt;p&gt;Let me quote the official line again, because it matters: this feature is "to prevent the gentleman, not the villain." It stops sensitive values from being &lt;em&gt;directly visible&lt;/em&gt; — a stray screenshot, a careless paste, a config dump in CI logs. It is not encryption at rest for your whole config system, and the vault password itself must be protected.&lt;/p&gt;

&lt;p&gt;So my take, after using it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Use it for&lt;/strong&gt;: keeping DB passwords, tokens, and API keys out of plaintext in shared repos.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't use it as&lt;/strong&gt;: a replacement for a KMS or a proper config center when you operate at that scale.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pair it with&lt;/strong&gt;: a runtime-injected &lt;code&gt;solon.vault.password&lt;/code&gt;, and treat the key like you'd treat any other secret — rotate it, restrict it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you've come from Spring Boot, this is the niche that Jasypt usually fills — except here it's a first-party Solon plugin, the annotation is native, and the whole thing hangs off &lt;code&gt;Solon.cfg()&lt;/code&gt; with zero extra services. For a framework that prides itself on being restrained, it's a surprisingly complete little feature.&lt;/p&gt;

&lt;p&gt;Now go find that plaintext password in your repo. You know the one.&lt;/p&gt;

</description>
      <category>backend</category>
      <category>devops</category>
      <category>java</category>
      <category>security</category>
    </item>
    <item>
      <title>Solon Config Injection: @Inject, @BindProps, and Auto-Refresh — No @Value, No @ConfigurationProperties</title>
      <dc:creator>Solon Framework</dc:creator>
      <pubDate>Sat, 01 Aug 2026 15:45:49 +0000</pubDate>
      <link>https://dev.to/solonjava/solon-config-injection-inject-bindprops-and-auto-refresh-no-value-no-2bfp</link>
      <guid>https://dev.to/solonjava/solon-config-injection-inject-bindprops-and-auto-refresh-no-value-no-2bfp</guid>
      <description>&lt;p&gt;I've been using Solon for a while now, and one of the first things I had to unlearn coming from Spring was how configuration gets into my code. There is no &lt;code&gt;@Value&lt;/code&gt; scatter, no &lt;code&gt;@ConfigurationProperties&lt;/code&gt; class hierarchy — but honestly, after a week I stopped missing both.&lt;/p&gt;

&lt;p&gt;Here's how config injection and binding actually work in Solon, with the exact APIs I use daily.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three expression forms you'll meet
&lt;/h2&gt;

&lt;p&gt;Solon's &lt;code&gt;@Inject&lt;/code&gt; uses a &lt;code&gt;${...}&lt;/code&gt; expression syntax. Three forms cover almost everything:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Inject&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"${track.name}"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;              &lt;span class="c1"&gt;// plain property lookup&lt;/span&gt;
&lt;span class="nd"&gt;@Inject&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"${track.name:demoApi}"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;      &lt;span class="c1"&gt;// with a default value&lt;/span&gt;
&lt;span class="nd"&gt;@Inject&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"${classpath:user.config.yml}"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// inject a whole resource file&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;${xxx:def}&lt;/code&gt; default only works for single values — don't try to default a collection or an entity with it. For those, you inject a whole property block instead (more below).&lt;/p&gt;

&lt;h2&gt;
  
  
  Field injection, the everyday case
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Component&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DemoService&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="c1"&gt;// static field injection, supported since v3.0&lt;/span&gt;
    &lt;span class="c1"&gt;// autoRefreshed = the value updates when config changes (singleton only!)&lt;/span&gt;
    &lt;span class="nd"&gt;@Inject&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"${track.name:demoApi}"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;autoRefreshed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;trackName&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// without a match, the field initial value is kept&lt;/span&gt;
    &lt;span class="nd"&gt;@Inject&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"${track.url}"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;trackUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"http://x.x.x/track"&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// inject a whole config block as Properties&lt;/span&gt;
    &lt;span class="nd"&gt;@Inject&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"${track.db1}"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="nc"&gt;Properties&lt;/span&gt; &lt;span class="n"&gt;trackDbCfg&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// or let Solon build a real bean from the config block&lt;/span&gt;
    &lt;span class="nd"&gt;@Inject&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"${track.db1}"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="nc"&gt;HikariDataSource&lt;/span&gt; &lt;span class="n"&gt;trackDs&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The last two lines are the ones that surprised me most. Give &lt;code&gt;@Inject&lt;/code&gt; a target type and a property prefix, and Solon constructs the object from the config block — &lt;code&gt;HikariDataSource&lt;/code&gt; gets built straight from &lt;code&gt;track.db1.*&lt;/code&gt; keys. No manual &lt;code&gt;setJdbcUrl(...)&lt;/code&gt; wiring.&lt;/p&gt;

&lt;p&gt;One caveat that bit me: &lt;code&gt;autoRefreshed = true&lt;/code&gt; is only safe on &lt;strong&gt;field injection into singleton beans&lt;/strong&gt;. If the bean is prototype-scoped, leave auto-refresh off, or you'll get surprising mid-lifecycle value swaps.&lt;/p&gt;

&lt;h2&gt;
  
  
  Injecting into &lt;a class="mentioned-user" href="https://dev.to/bean"&gt;@bean&lt;/a&gt; method parameters and constructors
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Configuration&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DemoConfig&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="c1"&gt;// inject a datasource built from config, into a @Bean method&lt;/span&gt;
    &lt;span class="nd"&gt;@Bean&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;DataSource&lt;/span&gt; &lt;span class="nf"&gt;db1&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;@Inject&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"${track.db1}"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nc"&gt;HikariDataSource&lt;/span&gt; &lt;span class="n"&gt;ds&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ds&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// conditional bean: only created when the property exists and is true&lt;/span&gt;
    &lt;span class="nd"&gt;@Bean&lt;/span&gt;
    &lt;span class="nd"&gt;@Condition&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;onExpression&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"${cache.enable:false} == true"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;CacheService&lt;/span&gt; &lt;span class="nf"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;@Inject&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"${cache.config}"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nc"&gt;CacheServiceSupplier&lt;/span&gt; &lt;span class="n"&gt;supplier&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;supplier&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Constructor injection works the same way on &lt;code&gt;@Component&lt;/code&gt; classes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Component&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DemoConfig&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;demoName&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;DemoConfig&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;@Inject&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"${demo.name}"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;demoName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Binding a whole config class: @BindProps
&lt;/h2&gt;

&lt;p&gt;If you prefer a typed config object you can reuse (closer to Spring's &lt;code&gt;@ConfigurationProperties&lt;/code&gt;), bind it once and inject it anywhere:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Option A: bind with @Inject + @Configuration&lt;/span&gt;
&lt;span class="nd"&gt;@Inject&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"${user.config}"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="nd"&gt;@Configuration&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserProperties&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Option B: bind with @BindProps (since v3.0.7) — also generates&lt;/span&gt;
&lt;span class="c1"&gt;// config metadata for IDE hints&lt;/span&gt;
&lt;span class="nd"&gt;@BindProps&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"user.config"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="nd"&gt;@Configuration&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserProperties&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Or bind on a @Bean method&lt;/span&gt;
&lt;span class="nd"&gt;@Configuration&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DemoConfig&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;@BindProps&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"user.config"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="nd"&gt;@Bean&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;UserProperties&lt;/span&gt; &lt;span class="nf"&gt;userProperties&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;UserProperties&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then reuse it anywhere with a plain &lt;code&gt;@Inject&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Inject&lt;/span&gt;
&lt;span class="nc"&gt;UserProperties&lt;/span&gt; &lt;span class="n"&gt;userProperties&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A detail I appreciate: &lt;code&gt;@Inject&lt;/code&gt; on classes has &lt;strong&gt;required detection&lt;/strong&gt; — if the config is missing, Solon raises an exception instead of silently injecting nulls. That catches typos in &lt;code&gt;app.yml&lt;/code&gt; at startup, not at 3am in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Manual access when you don't want injection
&lt;/h2&gt;

&lt;p&gt;Sometimes injection is overkill — a util class, a static context, a one-off lookup. &lt;code&gt;Solon.cfg()&lt;/code&gt; is your gateway:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Solon&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"track.name"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"demoApi"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// with default&lt;/span&gt;
&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Solon&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"track.url"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;               &lt;span class="c1"&gt;// plain&lt;/span&gt;

&lt;span class="nc"&gt;Properties&lt;/span&gt; &lt;span class="n"&gt;dbCfg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Solon&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;getProp&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"track.db1"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;       &lt;span class="c1"&gt;// whole block as Properties&lt;/span&gt;
&lt;span class="nc"&gt;HikariDataSource&lt;/span&gt; &lt;span class="n"&gt;ds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Solon&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;getBean&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"track.db1"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;HikariDataSource&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// as bean&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Prefer method-call form for frequently-read values so you always get the freshest state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="nf"&gt;trackName&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Solon&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"track.name"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"demoApi"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Change subscription: two ways
&lt;/h2&gt;

&lt;p&gt;Config can change at runtime (config service push, code updates). Two mechanisms exist:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Auto-refresh on injected fields&lt;/strong&gt; (singletons only):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Inject&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"${track.name:demoApi}"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;autoRefreshed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;trackName&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Manual subscription&lt;/strong&gt; — you get every changed key and filter what you need:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;Solon&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;onChange&lt;/span&gt;&lt;span class="o"&gt;((&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;startsWith&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"track.name"&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// refresh whatever depends on it&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The mental model
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;You want to...&lt;/th&gt;
&lt;th&gt;Spring way&lt;/th&gt;
&lt;th&gt;Solon way&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Inject a scalar&lt;/td&gt;
&lt;td&gt;&lt;code&gt;@Value("${x}")&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;@Inject("${x}")&lt;/code&gt; or &lt;code&gt;@Inject("${x:def}")&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Inject a config block&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;@ConfigurationProperties&lt;/code&gt; + class&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;@Inject("${prefix}")&lt;/code&gt; into &lt;code&gt;Properties&lt;/code&gt; or a typed target&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reusable typed config&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;@ConfigurationProperties&lt;/code&gt; + registration&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;@Inject&lt;/code&gt;/&lt;code&gt;@BindProps&lt;/code&gt; + &lt;code&gt;@Configuration&lt;/code&gt; class, plain &lt;code&gt;@Inject&lt;/code&gt; to reuse&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Build a bean from config&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;@Bean&lt;/code&gt; + manual mapping&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;@Inject("${prefix}") HikariDataSource&lt;/code&gt; or &lt;code&gt;Solon.cfg().getBean(...)&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;React to config change&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;@RefreshScope&lt;/code&gt; (cloud)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;autoRefreshed=true&lt;/code&gt; or &lt;code&gt;Solon.cfg().onChange(...)&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Missing key handling&lt;/td&gt;
&lt;td&gt;silently null / error varies&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;@Inject&lt;/code&gt; throws on missing (required detection)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two things worth calling out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;@Inject&lt;/code&gt; doubles as the bean-injection annotation.&lt;/strong&gt; In Spring, &lt;code&gt;@Autowired&lt;/code&gt; and &lt;code&gt;@Value&lt;/code&gt; are separate concerns. In Solon, one annotation handles bean references (&lt;code&gt;@Inject DataSource&lt;/code&gt;) and config values (&lt;code&gt;@Inject("${...}")&lt;/code&gt;) — and when both are present (&lt;code&gt;@Inject("${track.db1}") HikariDataSource&lt;/code&gt;), it builds a bean &lt;em&gt;from&lt;/em&gt; config. Fewer annotations, one consistent rule.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The config block is a first-class citizen.&lt;/strong&gt; &lt;code&gt;getProp()&lt;/code&gt; / &lt;code&gt;getBean()&lt;/code&gt; on any prefix means config-to-object conversion isn't locked inside a DI annotation — it's available anywhere &lt;code&gt;Solon.cfg()&lt;/code&gt; is.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's the whole surface. No profile classes to register, no &lt;code&gt;@ConfigurationProperties&lt;/code&gt; hierarchy to maintain — a prefix, a target type, and Solon does the rest.&lt;/p&gt;

</description>
      <category>backend</category>
      <category>java</category>
      <category>programming</category>
      <category>softwaredevelopment</category>
    </item>
    <item>
      <title>Solon Multi-Environment Config: Six Loading Tiers, an In-File Segment Trick, and No Profiles API</title>
      <dc:creator>Solon Framework</dc:creator>
      <pubDate>Sat, 01 Aug 2026 15:05:29 +0000</pubDate>
      <link>https://dev.to/solonjava/solon-multi-environment-config-six-loading-tiers-an-in-file-segment-trick-and-no-profiles-api-1id5</link>
      <guid>https://dev.to/solonjava/solon-multi-environment-config-six-loading-tiers-an-in-file-segment-trick-and-no-profiles-api-1id5</guid>
      <description>&lt;p&gt;If you've spent time with Spring Boot, you know the &lt;code&gt;spring.profiles.active&lt;/code&gt; routine — one property, multiple &lt;code&gt;application-{profile}.properties&lt;/code&gt; files, and a creeping suspicion that somewhere a prod config is silently falling back to dev defaults.&lt;/p&gt;

&lt;p&gt;Solon's answer is &lt;code&gt;solon.env&lt;/code&gt;. Same concept, different design choices — and one pattern (&lt;code&gt;solon.env.on&lt;/code&gt; in-file segments) that Spring has no direct equivalent for.&lt;/p&gt;

&lt;p&gt;Here's how it works.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Single Key: &lt;code&gt;solon.env&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Everything in Solon's multi-environment system pivots on one property:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app.yml&lt;/span&gt;
&lt;span class="na"&gt;solon.env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dev&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set this, and Solon automatically loads &lt;code&gt;app-dev.yml&lt;/code&gt; alongside &lt;code&gt;app.yml&lt;/code&gt;. Remove it, and only &lt;code&gt;app.yml&lt;/code&gt; loads. No profiles API, no &lt;code&gt;@ActiveProfiles&lt;/code&gt;, no annotation scanning — just a key.&lt;/p&gt;

&lt;h2&gt;
  
  
  Environment-Specific Files
&lt;/h2&gt;

&lt;p&gt;The naming convention is &lt;code&gt;app-{env}.yml&lt;/code&gt; (or &lt;code&gt;.properties&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="s"&gt;app.yml          ← always loaded&lt;/span&gt;
&lt;span class="s"&gt;app-dev.yml      ← loaded when solon.env=dev&lt;/span&gt;
&lt;span class="s"&gt;app-pro.yml      ← loaded when solon.env=pro&lt;/span&gt;
&lt;span class="s"&gt;app-test.yml     ← loaded when solon.env=test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keys in &lt;code&gt;app-{env}.yml&lt;/code&gt; override the same keys in &lt;code&gt;app.yml&lt;/code&gt;. Everything else in &lt;code&gt;app.yml&lt;/code&gt; is inherited.&lt;/p&gt;

&lt;p&gt;One constraint worth knowing: &lt;code&gt;app-{env}.yml&lt;/code&gt; cannot itself declare another &lt;code&gt;solon.env&lt;/code&gt;. The env chain is intentionally one level deep.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four Ways to Activate an Environment
&lt;/h2&gt;

&lt;p&gt;You don't have to hardcode &lt;code&gt;solon.env&lt;/code&gt; in the file. You can set it at any of four levels, with higher numbers winning:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Priority&lt;/th&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1 (lowest)&lt;/td&gt;
&lt;td&gt;In &lt;code&gt;app.yml&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;solon.env: dev&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;JVM system property&lt;/td&gt;
&lt;td&gt;&lt;code&gt;-Dsolon.env=pro&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Launch argument&lt;/td&gt;
&lt;td&gt;&lt;code&gt;--env=pro&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4 (highest)&lt;/td&gt;
&lt;td&gt;OS environment variable&lt;/td&gt;
&lt;td&gt;&lt;code&gt;export solon.env=pro&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;In practice: set &lt;code&gt;solon.env: dev&lt;/code&gt; in &lt;code&gt;app.yml&lt;/code&gt; as a safe default for local development, then override at deploy time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Jar deployment&lt;/span&gt;
java &lt;span class="nt"&gt;-jar&lt;/span&gt; demo.jar &lt;span class="nt"&gt;--env&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;pro

&lt;span class="c"&gt;# Docker run&lt;/span&gt;
docker run &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s1"&gt;'solon.env=pro'&lt;/span&gt; demo_image

&lt;span class="c"&gt;# Kubernetes pod spec&lt;/span&gt;
&lt;span class="nb"&gt;env&lt;/span&gt;:
  - name: solon.env
    value: pro
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All three launch forms below are equivalent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;java &lt;span class="nt"&gt;-Dsolon&lt;/span&gt;.env&lt;span class="o"&gt;=&lt;/span&gt;pro &lt;span class="nt"&gt;-jar&lt;/span&gt; demo.jar
java &lt;span class="nt"&gt;-jar&lt;/span&gt; demo.jar &lt;span class="nt"&gt;--solon&lt;/span&gt;.env&lt;span class="o"&gt;=&lt;/span&gt;pro
java &lt;span class="nt"&gt;-jar&lt;/span&gt; demo.jar &lt;span class="nt"&gt;--env&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;pro
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Six Loading Tiers
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;solon.env&lt;/code&gt; controls which env file loads, but that's just the first two tiers of a six-tier config loading stack. Later tiers override earlier ones:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tier&lt;/th&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;app.yml&lt;/code&gt; + &lt;code&gt;app-{env}.yml&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Base config, inside the jar&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;solon.config.load&lt;/code&gt; classpath entries&lt;/td&gt;
&lt;td&gt;v2.2.7+, supports &lt;code&gt;${solon.env}&lt;/code&gt; variables; wildcard &lt;code&gt;*&lt;/code&gt; from v2.7.6+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;solon.config.add&lt;/code&gt; external files&lt;/td&gt;
&lt;td&gt;Files placed beside the jar at runtime&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;JVM props / env vars / launch args&lt;/td&gt;
&lt;td&gt;Runtime overrides&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;app.cfg().loadAdd()&lt;/code&gt; in startup code&lt;/td&gt;
&lt;td&gt;Programmatic additions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Solon Cloud Config (e.g., Nacos)&lt;/td&gt;
&lt;td&gt;Remote config center&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The pattern: static defaults live inside the jar, runtime overrides live outside it, remote config sits at the top.&lt;/p&gt;

&lt;h2&gt;
  
  
  Loading More Configs: &lt;code&gt;solon.config.load&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;For apps with per-module configs, &lt;code&gt;solon.config.load&lt;/code&gt; lets you declare additional classpath resources with variable substitution and wildcard support (v2.7.6+):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app.yml&lt;/span&gt;
&lt;span class="na"&gt;solon.env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dev&lt;/span&gt;

&lt;span class="na"&gt;solon.config.load&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;classpath:${solon.env}/jdbc.yml"&lt;/span&gt;        &lt;span class="c1"&gt;# resolves to classpath:dev/jdbc.yml&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;classpath:${solon.env}/*.yml"&lt;/span&gt;           &lt;span class="c1"&gt;# all yml in dev/ folder (v2.7.6+)&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;classpath:app-ds-${solon.env}.yml"&lt;/span&gt;      &lt;span class="c1"&gt;# e.g., app-ds-dev.yml&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;classpath:common/base.yml"&lt;/span&gt;              &lt;span class="c1"&gt;# environment-agnostic shared config&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This lets you split config by concern (database, cache, auth) rather than cramming everything into one file per environment.&lt;/p&gt;

&lt;p&gt;For external files placed beside the jar, use &lt;code&gt;solon.config.add&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;solon.config.add&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;./local-overrides.yml"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or at launch time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;java &lt;span class="nt"&gt;-jar&lt;/span&gt; demo.jar &lt;span class="nt"&gt;--solon&lt;/span&gt;.config.add&lt;span class="o"&gt;=&lt;/span&gt;./local-overrides.yml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The In-File Segment Trick (v2.5.5+)
&lt;/h2&gt;

&lt;p&gt;This is the feature that has no Spring Boot equivalent. Solon lets you embed multiple environment-specific segments inside a single &lt;code&gt;app.yml&lt;/code&gt;, separated by &lt;code&gt;---&lt;/code&gt; and guarded by &lt;code&gt;solon.env.on&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;solon.env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pro&lt;/span&gt;

&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;solon.env.on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pro&lt;/span&gt;
&lt;span class="na"&gt;demo.auth&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;root&lt;/span&gt;
  &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;${AUTH_PASSWORD}"&lt;/span&gt;

&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;solon.env.on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dev|test&lt;/span&gt;
&lt;span class="na"&gt;demo.auth&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;demo&lt;/span&gt;
  &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;demo1234"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only the segment matching the current &lt;code&gt;solon.env&lt;/code&gt; is applied. The &lt;code&gt;|&lt;/code&gt; syntax lets one segment cover multiple environments.&lt;/p&gt;

&lt;p&gt;This pattern suits small services where maintaining separate env files feels heavy, but you still want clear boundaries in the source.&lt;/p&gt;

&lt;h2&gt;
  
  
  Programmatic Loading
&lt;/h2&gt;

&lt;p&gt;Sometimes you need to load config based on conditions known only at startup. Two clean hooks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@SolonMain&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;App&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;Solon&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;start&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;App&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// Tier 5: added before beans initialize, merged into the config stack&lt;/span&gt;
            &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;loadAdd&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"app-jdbc-"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;".yml"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
            &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;loadAdd&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"app-cache-"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;".yml"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="o"&gt;});&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or via &lt;code&gt;@Import&lt;/code&gt; on the startup class:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Import&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;profiles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"classpath:module-defaults.yml"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="nd"&gt;@SolonMain&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;App&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;Solon&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;start&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;App&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For Docker/Kubernetes, &lt;code&gt;loadEnv()&lt;/code&gt; pulls in environment variables by prefix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;Solon&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;start&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;App&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;loadEnv&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"demo."&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// pulls in all env vars starting with "demo."&lt;/span&gt;
&lt;span class="o"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note: Solon automatically syncs &lt;code&gt;solon.*&lt;/code&gt; environment variables into the config store, so &lt;code&gt;solon.env=pro&lt;/code&gt; as a container env var works without &lt;code&gt;loadEnv()&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Injecting Config Values
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;@Inject&lt;/code&gt; with &lt;code&gt;${}&lt;/code&gt; syntax wires config values into beans:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Component&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DataSourceConfig&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="nd"&gt;@Inject&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"${db.url}"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;dbUrl&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="nd"&gt;@Inject&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"${db.username}"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For structured config objects, &lt;code&gt;@BindProps&lt;/code&gt; binds an entire prefix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@BindProps&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"app.mail"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="nd"&gt;@Component&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MailProperties&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Variable references also work inside YAML values themselves:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;db.server&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;10.0.0.1"&lt;/span&gt;
&lt;span class="na"&gt;db.name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;orders"&lt;/span&gt;
&lt;span class="na"&gt;db.url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;jdbc:mysql://${db.server}/${db.name}"&lt;/span&gt;  &lt;span class="c1"&gt;# composed from other keys&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  A Realistic Project Layout
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src/main/resources/
├── app.yml                   # base config + solon.env: dev
├── app-dev.yml               # dev overrides
├── app-pro.yml               # prod overrides
└── config/
    ├── common/
    │   └── base.yml          # shared constants (env-agnostic)
    ├── dev/
    │   └── jdbc.yml          # dev DB connection
    └── pro/
        └── jdbc.yml          # prod DB connection
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;app.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;solon.env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dev&lt;/span&gt;

&lt;span class="na"&gt;solon.config.load&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;classpath:config/common/base.yml"&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;classpath:config/${solon.env}/jdbc.yml"&lt;/span&gt;

&lt;span class="na"&gt;solon.app.name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;order-service"&lt;/span&gt;
&lt;span class="na"&gt;server.port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8080&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;app-dev.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;solon.logging.level&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;debug&lt;/span&gt;
&lt;span class="na"&gt;solon.debug&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;app-pro.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;solon.logging.level&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;warn&lt;/span&gt;
&lt;span class="na"&gt;server.port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;80&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Deploy to prod:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;java &lt;span class="nt"&gt;-jar&lt;/span&gt; order-service.jar &lt;span class="nt"&gt;--env&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;pro
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One flag flips the entire config stack.&lt;/p&gt;

&lt;h2&gt;
  
  
  Migration Reference (from Spring Boot)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Spring Boot&lt;/th&gt;
&lt;th&gt;Solon&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;spring.profiles.active&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;solon.env&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Same concept, different key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;application-{profile}.properties&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;app-{env}.yml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Same convention&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;@Profile("dev")&lt;/code&gt; on beans&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;solon.env.on: dev&lt;/code&gt; in YAML&lt;/td&gt;
&lt;td&gt;Config-layer isolation vs code-layer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@PropertySource&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;solon.config.load&lt;/code&gt; in YAML&lt;/td&gt;
&lt;td&gt;Declarative import&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@ConfigurationProperties(prefix="x")&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;@BindProps(prefix="x")&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Prefix binding&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@Value("${x}")&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;@Inject("${x}")&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Value injection&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The mental model is nearly identical. The key difference: Solon keeps environment logic in config (via &lt;code&gt;solon.env.on&lt;/code&gt; segments or separate files) rather than scattering &lt;code&gt;@Profile&lt;/code&gt; annotations across bean definitions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;solon.env&lt;/code&gt; is the single activation key — set it in the file, at the JVM, or as a container env var&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;app-{env}.yml&lt;/code&gt; is loaded automatically; no registration step required&lt;/li&gt;
&lt;li&gt;Six loading tiers let you layer static defaults with runtime overrides cleanly&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;solon.config.load&lt;/code&gt; with &lt;code&gt;${solon.env}&lt;/code&gt; handles per-module, per-environment config splitting&lt;/li&gt;
&lt;li&gt;In-file &lt;code&gt;---&lt;/code&gt; segments with &lt;code&gt;solon.env.on&lt;/code&gt; (v2.5.5+) are unique to Solon — useful for small services&lt;/li&gt;
&lt;li&gt;Config injection uses &lt;code&gt;@Inject("${...}")&lt;/code&gt; and &lt;code&gt;@BindProps&lt;/code&gt; — both standard Solon IoC&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The loading order is deterministic and documented. Deploy with confidence.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>backend</category>
      <category>java</category>
      <category>software</category>
    </item>
    <item>
      <title>Solon's 10 HTTP Servers: Pick Your Engine With One Dependency Change</title>
      <dc:creator>Solon Framework</dc:creator>
      <pubDate>Fri, 31 Jul 2026 04:34:01 +0000</pubDate>
      <link>https://dev.to/solonjava/solons-10-http-servers-pick-your-engine-with-one-dependency-change-55oi</link>
      <guid>https://dev.to/solonjava/solons-10-http-servers-pick-your-engine-with-one-dependency-change-55oi</guid>
      <description>&lt;p&gt;I've been running Solon apps in production for a while now, and one thing that keeps surprising people is how the server engine works.&lt;/p&gt;

&lt;p&gt;Not "which server does Solon use" — because the answer is: &lt;strong&gt;you choose&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Solon doesn't bundle a hardcoded HTTP server. The framework core is about 0.3MB. The HTTP engine is a separate plugin. You pick one. Or two. Or swap them between environments without touching a single line of application code.&lt;/p&gt;

&lt;p&gt;Here's a quick rundown of the 10 HTTP server options, what they're good for, and how to switch.&lt;/p&gt;

&lt;h2&gt;
  
  
  The One-Dependency Swap
&lt;/h2&gt;

&lt;p&gt;In Solon, changing your HTTP server means changing one dependency in &lt;code&gt;pom.xml&lt;/code&gt;. Nothing else.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- Default: jdkhttp (built-in JDK, 0.3MB) --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;org.noear&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;solon-web&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Want Undertow instead?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;org.noear&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;solon-web&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;exclusions&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;exclusion&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;org.noear&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;solon-server-jdkhttp&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/exclusion&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/exclusions&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;org.noear&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;solon-server-undertow&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. Your &lt;code&gt;@Controller&lt;/code&gt;, &lt;code&gt;@Mapping("/hello")&lt;/code&gt;, filters, interceptors — everything stays the same. The routing layer is framework-owned; the server underneath is a pluggable adapter.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 10 Engines at a Glance
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Plugin&lt;/th&gt;
&lt;th&gt;Size&lt;/th&gt;
&lt;th&gt;Protocols&lt;/th&gt;
&lt;th&gt;Reactive&lt;/th&gt;
&lt;th&gt;JDK&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;jdkhttp&lt;/td&gt;
&lt;td&gt;0.3MB&lt;/td&gt;
&lt;td&gt;http&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;8+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;smarthttp&lt;/td&gt;
&lt;td&gt;0.8MB&lt;/td&gt;
&lt;td&gt;http, ws&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;8+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;grizzly&lt;/td&gt;
&lt;td&gt;1.8MB&lt;/td&gt;
&lt;td&gt;http, ws, http2&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;8+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;vertx&lt;/td&gt;
&lt;td&gt;6.3MB&lt;/td&gt;
&lt;td&gt;http, ws, http2&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;8+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;jetty&lt;/td&gt;
&lt;td&gt;2.7MB&lt;/td&gt;
&lt;td&gt;http, ws&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;8+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;undertow&lt;/td&gt;
&lt;td&gt;4.6MB&lt;/td&gt;
&lt;td&gt;http, ws, http2&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;8+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;tomcat&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;http, ws, http2&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;8+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;jetty-jakarta&lt;/td&gt;
&lt;td&gt;3.9MB&lt;/td&gt;
&lt;td&gt;http, ws, http2&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;17+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;undertow-jakarta&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;http, ws, http2&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;17+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;tomcat-jakarta&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;http, ws, http2&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;17+&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Plus two dedicated WebSocket-only servers (&lt;code&gt;solon-server-websocket&lt;/code&gt; at 0.4MB, &lt;code&gt;solon-server-websocket-netty&lt;/code&gt; at 3.6MB) and one Socket.D server.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Choose
&lt;/h2&gt;

&lt;h3&gt;
  
  
  For microservices and edge services: jdkhttp or smarthttp
&lt;/h3&gt;

&lt;p&gt;If you're building a lightweight API service or a microservice that needs to start in under a second, stick with &lt;code&gt;jdkhttp&lt;/code&gt;. It's 0.3MB, requires zero external dependencies, and supports reactive I/O. The JDK built-in &lt;code&gt;com.sun.net.httpserver&lt;/code&gt; is perfectly adequate for most API workloads.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;smarthttp&lt;/code&gt; (0.8MB, Chinese-contributed) adds WebSocket support on the same port — useful if you need both REST and WebSocket without adding a second server.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@SolonMain&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;App&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;Solon&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;start&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;App&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;enableWebSocket&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="o"&gt;});&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  When you need HTTP/2: grizzly, undertow, vertx, or tomcat
&lt;/h3&gt;

&lt;p&gt;All four support HTTP/2. Enable it with a single line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;onEvent&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;HttpServerConfigure&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;enableHttp2&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// v2.3.8+&lt;/span&gt;
&lt;span class="o"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Undertow is a solid middle ground — 4.6MB, Apache 2.0 license, good community adoption. Vert.x is heavier (6.3MB) but gives you the full Vert.x event-loop ecosystem if you're already using it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Traditional Java shops: Jetty or Tomcat
&lt;/h3&gt;

&lt;p&gt;If your team is migrating from Spring Boot + embedded Tomcat, &lt;code&gt;solon-server-tomcat&lt;/code&gt; is the most natural drop-in. It uses the same Tomcat v9 engine underneath — your ops team's existing tuning knowledge carries over.&lt;/p&gt;

&lt;p&gt;Jetty is a great alternative if you prefer Eclipse ecosystem. It also has explicit JSP support via &lt;code&gt;solon-server-jetty-add-jsp&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Jakarta EE migration: jetty-jakarta, undertow-jakarta, tomcat-jakarta
&lt;/h3&gt;

&lt;p&gt;If you're on JDK 17+ and targeting Jakarta namespace (&lt;code&gt;jakarta.*&lt;/code&gt;), use the Jakarta variants. They're paired with the corresponding Jakarta server versions (Jetty 12, Undertow 2.3, Tomcat 11).&lt;/p&gt;

&lt;h2&gt;
  
  
  The Code That Never Changes
&lt;/h2&gt;

&lt;p&gt;This is the part I like most. No matter which server engine you pick, your application code looks identical:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Controller&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DemoController&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;@Mapping&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/hello"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="nf"&gt;hello&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;"Hello world!"&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@Mapping&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/greet/{name}"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="nf"&gt;greet&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;"Hello, "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;"!"&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same code runs on jdkhttp (0.3MB, no dependencies), Undertow (4.6MB, HTTP/2), or Tomcat (production-hardened). Zero changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Advanced: Multiple Ports, SSL, and Debug Mode
&lt;/h2&gt;

&lt;p&gt;All server adapters share the same configuration surface:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app.yml&lt;/span&gt;
&lt;span class="na"&gt;server.port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8080&lt;/span&gt;
&lt;span class="na"&gt;server.http.port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8080&lt;/span&gt;          &lt;span class="c1"&gt;# alias&lt;/span&gt;
&lt;span class="na"&gt;server.ssl.keyStore&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/data/ca/demo.jks"&lt;/span&gt;
&lt;span class="na"&gt;server.ssl.keyPassword&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;demo"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or programmatically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;Solon&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;start&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;App&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Add a second HTTP port (useful when main port is HTTPS)&lt;/span&gt;
    &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;onEvent&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;HttpServerConfigure&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addHttpPort&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8082&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;enableDebug&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;    &lt;span class="c1"&gt;// debug mode for smarthttp/jdkhttp&lt;/span&gt;
    &lt;span class="o"&gt;});&lt;/span&gt;
&lt;span class="o"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Custom SSLContext (v2.5.9+):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;onEvent&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;HttpServerConfigure&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;SSLContext&lt;/span&gt; &lt;span class="n"&gt;sslContext&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;buildMyCustomSSLContext&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;enableSsl&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sslContext&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What About the "Default" Default?
&lt;/h2&gt;

&lt;p&gt;If you add &lt;code&gt;solon-web&lt;/code&gt; without any explicit server exclusion, Solon uses &lt;code&gt;jdkhttp&lt;/code&gt; — the built-in JDK HTTP server. It's the lightest option (0.3MB), requires zero external JARs, and works for 90% of development scenarios.&lt;/p&gt;

&lt;p&gt;In production, you might want something more tuned. But the beauty is: &lt;strong&gt;you don't decide on day one.&lt;/strong&gt; Start with jdkhttp, benchmark, then swap to Undertow or Tomcat when you need HTTP/2 or WebSocket — all without rewriting a single route.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest Limitations
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tomcat&lt;/strong&gt; (v9) doesn't support WebSocket directly — you need the &lt;code&gt;solon-server-tomcat-add-websocket&lt;/code&gt; sub-plugin.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;smarthttp&lt;/strong&gt; had a file upload size cap (~2.1GB) before v3.0.3 — fixed now, but worth noting if you're on an older version.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vert.x&lt;/strong&gt; is the heaviest at 6.3MB — great for reactive workloads but overkill for a simple CRUD API.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Jakarta variants&lt;/strong&gt; (jetty-jakarta, undertow-jakarta, tomcat-jakarta) require JDK 17+.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Solon's pluggable server architecture is one of those features that seems small on paper but makes a big difference in practice. One dependency change, zero code impact, and a full spectrum of engines from 0.3MB to full-featured production servers.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Need&lt;/th&gt;
&lt;th&gt;Pick&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Lightest API server&lt;/td&gt;
&lt;td&gt;jdkhttp (0.3MB)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;REST + WebSocket on same port&lt;/td&gt;
&lt;td&gt;smarthttp (0.8MB)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HTTP/2 + solid performance&lt;/td&gt;
&lt;td&gt;undertow (4.6MB)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spring Boot migration&lt;/td&gt;
&lt;td&gt;tomcat&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reactive event-loop&lt;/td&gt;
&lt;td&gt;vert.x or grizzly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jakarta EE (JDK 17+)&lt;/td&gt;
&lt;td&gt;jetty-jakarta / undertow-jakarta&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Standalone WebSocket&lt;/td&gt;
&lt;td&gt;solon-server-websocket (0.4MB)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The server is infrastructure. The framework is your code. Solon keeps them separate — and that's a good thing.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>backend</category>
      <category>java</category>
      <category>softwaredevelopment</category>
    </item>
  </channel>
</rss>
