DEV Community

Solon Framework
Solon Framework

Posted on

The AppContext API: Programmatic Bean Control in Solon

Annotations get you a long way in Solon. @Component, @Inject, @Bean 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 AppContext.

AppContext 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.

Getting hold of an AppContext

There are three ways to get a reference, depending on where you are.

Global, from anywhere:

import org.noear.solon.Solon;

public class DemoClass {
    UserService userService;

    public void demo() {
        Solon.context().getBeanAsync(UserService.class, bean -> {
            userService = bean;
        });
    }
}
Enter fullscreen mode Exit fullscreen mode

Injected into a component:

import org.noear.solon.annotation.Component;
import org.noear.solon.annotation.Inject;
import org.noear.solon.core.AppContext;

@Component
public class DemoComponent {
    @Inject
    AppContext context;
}
Enter fullscreen mode Exit fullscreen mode

Handed to a plugin at the start of its lifecycle:

import org.noear.solon.core.AppContext;
import org.noear.solon.core.Plugin;

public class DemoPlugin implements Plugin {
    @Override
    public void start(AppContext context) {
        //...
    }
}
Enter fullscreen mode Exit fullscreen mode

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

The container is async-ready, and that changes how you fetch

Notice the first example used getBeanAsync, not getBean. That's deliberate.

Solon builds its container as beans become available, not all at once up front. If you call getBean(SomeType.class) 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.

// Fires whenever UserService becomes available, no null surprises
Solon.context().getBeanAsync(UserService.class, bean -> {
    // use bean here
});
Enter fullscreen mode Exit fullscreen mode

Two families exist for this:

  • getBeanAsync(name, callback) / getBeanAsync(type, callback) — receive a single bean when ready
  • subBeansOfType(baseType, callback) — receive every bean of a base type as they arrive

There's a matching pair at the wrapper level: getWrapAsync(nameOrType, callback) and subWrapsOfType(baseType, callback). More on wrappers below.

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

baseType vs type: a batch or a single one

Several methods come in two shapes, one that takes a baseType and one that takes a type. This isn't redundancy, it's a deliberate trade-off.

With OfType Without OfType
getWrapsOfType(baseType) -> List[T] getWrap(type) -> T
subWrapsOfType(baseType, callback) getWrapAsync(type)
getBeansOfType(baseType) -> List[T] getBean(type) -> T
subBeansOfType(baseType, callback) getBeanAsync(type)
getBeansMapOfType(baseType) /
  • baseType matches by base class and returns a batch. The method names carry an s (plural). You get every matching bean, at some cost to lookup speed.
  • type matches the concrete type by hash code and returns a single bean. Faster, but exactly one.

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? getBeansOfType. Need one specific bean fast? getBean.

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

Manual assembly: putting objects into the container at runtime

This is the part annotations can't do. When you build an object yourself and want the container to own it, you wrap it.

// wrap and push into the container in one step
context.wrapAndPut(UserService.class, new UserServiceImpl());
Enter fullscreen mode Exit fullscreen mode

The wrapping API:

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

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

By default, beans produced by @Bean or @Component are published automatically. Otherwise you must call the publish method (beanPublish) manually.

So the manual path is: wrap it, then publish it.

BeanWrap wrap = context.wrap(UserService.class, new UserServiceImpl());
context.putWrap(UserService.class, wrap);
context.beanPublish(wrap);  // now subWrapsOfType subscribers fire
Enter fullscreen mode Exit fullscreen mode

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

Walking the container

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:

  • beanForeach((name, wrap) -> {}) and beanForeach((wrap) -> {}) — iterate the wrap store
  • beanFind((name, wrap) -> bool) -> List[BeanWrap] and beanFind((wrap) -> bool) -> List[BeanWrap] — find matching wraps

Registering your own annotation handlers

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.

  • beanBuilderAdd(anno, builder) and beanBuilderAdd(anno, targetClz, builder) — handle how an annotation builds a bean
  • beanInjectorAdd(anno, injector) and beanInjectorAdd(anno, targetClz, injector) — handle how an annotation injects
  • beanExtractorAdd(anno, extractor) and beanExtractorHas(anno) — handle extraction
  • beanInterceptorAdd(anno, interceptor), beanInterceptorAdd(anno, interceptor, index), beanInterceptorGet(anno) — attach an interceptor to an annotation (AOP)

Registered inside Plugin.start, 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.

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

Binding lifecycle

Finally, if a bean needs to react to container lifecycle events, register it:

  • lifecycle(lifecycleBean)
  • lifecycle(index, lifecycleBean) — with ordering

This is the programmatic equivalent of implementing LifecycleBean, useful when the object you want to hook in wasn't declared as a component.

Where this fits

AppContext 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 declare your application, AppContext is how you program the container underneath it.

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.

Top comments (0)