A server that does everything in its controllers turns into classes nobody can test. This chapter covers the alternative Spring made standard: split the work into beans, let each one declare what it depends on, and have something else build them and hand them over. Here that something is the build, which is the difference worth keeping in mind throughout: every decision below is made while the project compiles, and a decision the build can’t make is an error before the server ever starts.
Services and dependency injection
A controller that does everything itself turns into a class nobody can test.
The usual split is Spring’s: controllers translate HTTP, services hold the logic,
repositories hold the SQL, and each one receives what it depends on instead of
building it. The annotations are Spring’s too, under
com.codename1.backend.annotations:
@Service
public class Signups {
private final DataSource db;
@Autowired
private Mailer mailer;
public Signups(DataSource db) {
this.db = db;
}
@PostConstruct
void createTable() throws IOException {
db.execute("CREATE TABLE IF NOT EXISTS signup (email VARCHAR(200))", null);
}
@RestController
public class SignupApi {
private final Signups signups;
public SignupApi(Signups signups) {
this.signups = signups;
}
@PostMapping("/signups")
public String signUp(@RequestParam("email") String email) throws Exception {
signups.register(email);
return "ok";
}
@GetMapping("/signups/count")
public String count() throws Exception {
return String.valueOf(signups.count());
}
}
@Service, @Component and @Repository mark a bean; a @RestController or
@WebSocketMapping class is one too. A class with one constructor gets that
constructor; with several, the one marked @Autowired. Fields and setters are
injected only when marked @Autowired, and a private field is fine.
What makes this different from Spring is when it happens. The build finds every
bean, decides which constructor each gets and which bean fills each injection
point, and writes that down as a class called BackendWiring — a list of new
calls, setter calls and @PostConstruct calls in dependency order. At run time
there is no container, no classpath scan and no reflection, so a server with
twenty beans starts as fast as one with none, and the translator can still drop
every class nothing reaches. A dependency that can’t be satisfied is a build
error that names the injection point, rather than a server that fails its first
request:
constructor parameter 1 of com.example.SignupApi needs a com.example.Signups, and no bean has that type. Annotate the implementing class @Component, @Service or @Repository, or declare a @Bean method returning one.
Two beans of one type are an error too, until one is marked @Primary or the
injection point names one with @Qualifier. Constructors that depend on each
other in a circle are refused; the same circle through @Autowired fields is
fine, because fields are set after every bean exists.
Declaring beans
A bean is a class the build constructs once per server and hands to everything that needs it. These annotations make a class one:
| Annotation | Use it for |
|---|---|
| Anything the application wires together. |
| The same as |
| A class whose |
| A controller or a WebSocket endpoint. Both are beans, and both are always singletons. |
A bean’s name is the simple class name with its first letter lowercased — smtpMailer for SmtpMailer — unless the annotation gives one, as
@Component("invoice") does. Names matter only to @Qualifier, and two beans
with the same name are a build error.
A bean has to be a class the build can construct: concrete, and static if it’s nested. It also has to be public unless it’s in the package the generated entry point lives in, since the generated code has to name it.
Injection points
A bean receives its dependencies in three places, all resolved by type:
The constructor. A class with one constructor gets that one. With several, the build takes the one marked
@Autowired, or failing that the one with no arguments; with neither it can’t choose and says so. Constructor injection is the one to prefer, because the object is complete the moment it exists and a test builds it withnew.A field marked
@Autowired. A private field is fine: the build adds a setter to the compiled class and the generated code calls it, so there is no reflection. Afinalfield can only be set by the constructor, and marking one@Autowiredis a build error that says so.A method marked
@Autowired, called once with its parameters resolved like a constructor’s.
Injection points and lifecycle methods declared in a superclass count too, and are
handled before the subclass’s own, and the same goes for @Scheduled, @McpTool and managed
methods: a job a base class declares runs for every bean that inherits it. A method the subclass overrides follows the
subclass’s annotations, not the superclass’s. The build can only do this for a
superclass compiled from the project’s sources: a base class from a jar that has
injected fields, or lifecycle methods that aren’t public, is a build error telling
you to take those dependencies in the subclass instead.
@Autowired(required = false) leaves the point null when no bean has its type,
where a required point is a build error. A List<T> or Collection<T> receives
every bean of type T:
@Service
public class Checkout {
private final PaymentProvider preferred; // the @Primary one: CardPayments
@Autowired
@Qualifier("invoice")
private PaymentProvider invoice; // the one named "invoice"
@Autowired
private List<PaymentProvider> all; // every PaymentProvider bean
@Autowired(required = false)
private Audit audit; // null when no bean provides one
public Checkout(PaymentProvider preferred) {
this.preferred = preferred;
}
When several beans have the injected type and the point names none, the build
takes the one marked @Primary:
@Component
@Primary
public class CardPayments implements PaymentProvider {
public String name() {
return "card";
}
}
Without one it’s an error listing the candidates, and two @Primary beans of one
type are an error too. @Qualifier("invoice") names the bean a point wants and
overrides @Primary. The single exception is a set of candidates that are all
conditional — a @Profile("dev") bean and a @Profile("!dev") one — where at
most one exists at run time and the generated code picks whichever does.
Some types aren’t beans the application declares, but the server provides them and they’re injected the same way:
| Type | What it receives |
|---|---|
| The server’s configuration, for keys read at run time. |
| The connection pool. A bean that takes one makes a database required, and a server with none configured refuses to start with a message naming the bean. |
| The entity manager over that pool, when the module has entities. |
| The current transaction’s persistence session, described under Backend data access and transactions. |
| The current request. Only a |
| The current session. Only a |
Configuration values
@Value reads a setting, with a fallback after the colon, and converts it to the
field’s type:
@Service
@Profile("!dev")
public class SmtpMailer implements Mailer {
@Value("${mail.host:localhost}")
private String host;
@Value("${mail.port:25}")
private int port;
public void send(String to, String subject, String body) {
// ... talk to host:port ...
}
}
The value comes from the layers described under Configuration and profiles,
so MAIL_HOST in the environment overrides the file. A
key with no fallback that nothing sets stops the server at startup with the key’s
name. @ConfigurationProperties("mail") on a bean does the same for every one of
its setters at once: setHost reads mail.host, and setMaxSize reads
mail.maxSize or mail.max-size.
@Value converts to a String, a primitive or its box, or an enum; a field
of any other type is a build error. A key that has no fallback and isn’t set in
application.properties gets a build warning, since it can still come from the
environment at run time.
@ConfigurationProperties suits a group of related settings better than a
@Value per field, and a bean that holds them is an ordinary bean that others
inject:
@Component
@ConfigurationProperties("mail")
public class MailSettings {
private String host = "localhost"; // kept when mail.host is not set
private int port = 25;
private boolean startTls;
public void setHost(String host) { // mail.host
this.host = host;
}
public void setPort(int port) { // mail.port
this.port = port;
}
public void setStartTls(boolean startTls) { // mail.startTls or mail.start-tls
this.startTls = startTls;
}
Each setter taking a String, a number, a boolean or an enum is bound from the
prefix plus the property name, in camel case or kebab case. A setter whose key
isn’t set is never called, so the field keeps its initial value — which is where
a default belongs. A prefix that binds no setter at all is a build error, since it
means the class and the prefix disagree.
Profiles and factory methods
@Profile and @ConditionalOnProperty make a bean depend on the deployment:
@Component
@Profile("dev")
public class ConsoleMailer implements Mailer {
public void send(String to, String subject, String body) {
System.out.println("mail to " + to + ": " + subject);
}
}
The profile is the one choice the build can’t make for you, so it becomes a
single if in the generated startup code. @ConditionalOnMissingBean is decided
entirely at build time: the bean steps aside when the application declares
another of its type.
A class from a library can’t carry annotations. A @Bean method in a
@Configuration class makes one:
@Configuration
public class Limits {
@Bean
public RateLimiter signupLimiter(@Value("${signups.perMinute:30}") int perMinute) {
return new RateLimiter(perMinute);
}
}
The build calls the method once, directly. There is no proxy around the
configuration class, so one @Bean method calling another gets a second object;
take the other bean as a parameter instead, which the build fills like a
constructor’s.
Conditions in detail
@Profile takes one or more profile names, and the bean exists when any of them
is active. A name starting with ! is negated, so @Profile("!prod") covers
every profile but prod.
@ConditionalOnProperty makes a bean depend on a key:
@Component
@ConditionalOnProperty(value = "search.enabled", havingValue = "true")
public class SearchIndexer {
public void index(String document) {
// ...
}
}
With havingValue, the key must equal that value, compared ignoring case.
Without it, any value but false counts. matchIfMissing = true makes an unset
key count as a match, and several keys in value must all match. prefix is put
in front of each key with a dot.
Both are evaluated once, at start-up, as one if around the bean’s construction.
Which beans are on depends on the deployment’s configuration, so it’s the start that
checks it, as Spring’s does: a bean that requires one whose conditions are all off
under the running configuration stops the server from starting, with an error
naming the injection point. Cover the other case with a bean of the same type under
the opposite condition, make the dependency optional, or give the dependent bean
the same condition.
@ConditionalOnMissingBean is the one condition decided entirely at build time.
It marks a default that steps aside when the application declares another bean it
would compete with, which is how a module offers a fallback:
@Configuration
public class Defaults {
@Bean
@ConditionalOnMissingBean
public Audit audit() {
return new LogAudit(); // used only when the application declares no Audit
}
}
With no arguments, "another bean it would compete with" means one that has any of
the types the default can be injected as, apart from the JDK’s own: its class, its
superclasses and its interfaces. On a class, DefaultMailer implements Mailer
steps aside for any other Mailer. On a @Bean method they’re the declared
return type and the types above it, which is why the method above returns Audit
rather than LogAudit. @ConditionalOnMissingBean(Audit.class) names the types
to compete on explicitly, for a class whose default set is too wide or too narrow.
A @Bean method inherits the conditions of the @Configuration class it’s
declared in, static methods included. A @Profile("prod") configuration class
therefore contributes nothing on any other profile, which is what the annotation on
the class says. The same holds for @ConditionalOnMissingBean on the class: a
configuration class that steps aside takes its @Bean methods with it.
Beans with a lifecycle of their own
A library class often has methods to start and stop it, and no annotations to say
so. @Bean names them:
@Configuration
public class SearchConfig {
@Bean(initMethod = "connect", destroyMethod = "close")
public SearchClient searchClient(@Value("${search.url:http://localhost:9200}") String url) {
return new SearchClient(url);
}
}
initMethod runs after the factory method returns, where a class’s
@PostConstruct would. destroyMethod runs where its @PreDestroy would: at
shutdown for a singleton, at the end of the request for a request-scoped bean, and
when the session ends for a session-scoped one.
Scopes
A bean is a singleton unless it says otherwise. @Scope("prototype") gives each
injection point its own instance. @RequestScope and @SessionScope give one
per HTTP request or session; injected into a singleton, such a bean is reached
through a small generated subclass that finds the current request’s instance on
each call, so the class can’t be final and needs a constructor that takes no
arguments. @Lazy builds a bean the first time it’s used, through the same kind
of subclass. That subclass has to run the bean’s constructor once at startup to
exist at all, so the expensive part of setting a bean up belongs in its
@PostConstruct method, which runs only on the real instance.
| Scope | Instances |
|---|---|
singleton, the default | One per server, built at start-up in dependency order. |
| A new instance for each injection point. Two fields of type |
| One per HTTP request, built the first time the request uses it and destroyed when the request ends. |
| One per HTTP session, kept by the server for as long as the session lives and destroyed when it ends. See Backend sessions. |
| One per server, built the first time something calls it. |
A request-scoped bean looks like any other to the code that uses it:
@Component
@RequestScope
public class RequestClock {
private final long started = System.currentTimeMillis();
public long elapsed() {
return System.currentTimeMillis() - started; // since THIS request's instance
}
}
A controller that injects RequestClock gets the generated stand-in, and each
call on it reaches the instance belonging to the request being served. Two
requests served at once see two instances.
The stand-in extends the bean’s class and overrides its methods, which is where the rules for a scoped or lazy bean come from. The class can’t be final, a method it declares or inherits can’t be final either — a call to it would run on the stand-in instead of the bean — and it needs a constructor with no arguments, which may be protected. Methods inherited from a library class are forwarded too, so the library has to be on the build’s classpath. Inject anything else through fields. Each rule is checked by the build.
@Lazy suits a bean that’s expensive to prepare and not always needed:
@Component
@Lazy
public class ExpensiveIndex {
private Map<String, String> index;
public ExpensiveIndex() {
// Runs at start-up for the generated stand-in too: keep it cheap.
}
@PostConstruct
void load() {
// Runs once, on the real instance, the first time something calls it.
index = new HashMap<String, String>();
}
public String lookup(String key) {
return index.get(key);
}
}
A WebSocket endpoint must be a singleton, since it serves the whole life of the
server rather than one request. A bean with @Scheduled methods can’t be
request- or session-scoped, because a job runs outside any request. A
prototype-scoped one only draws a warning, as in Spring: one instance is built to
run its jobs and is never destroyed, so make it a singleton if it needs
@PreDestroy.
Lifecycle
Beans are built in dependency order, and a bean’s @PostConstruct method runs
once it and everything it depends on has been built and injected. At shutdown,
@PreDestroy methods run in the reverse order, so a bean’s dependencies are still
there when its own method runs. Within one bean the same rule holds for its class
hierarchy: a superclass’s @PostConstruct runs before the subclass’s, and a
subclass’s @PreDestroy runs before the superclass’s:
@Repository
public class Outbox {
private final DataSource db;
private final List<String> pending = new ArrayList<String>();
public Outbox(DataSource db) {
this.db = db;
}
@PostConstruct
void createTable() throws IOException {
// Every dependency is built, injected and initialized by now.
db.execute("CREATE TABLE IF NOT EXISTS outbox (message VARCHAR(500))", null);
}
public synchronized void add(String message) {
pending.add(message);
}
@PreDestroy
synchronized void flush() throws IOException {
// After the last request has finished, before the pool closes.
for (String message : pending) {
db.execute("INSERT INTO outbox (message) VALUES (?)", new Object[] {message});
}
pending.clear();
}
}
Both methods take no arguments and may have any visibility; the build adds a
public bridge to reach a private one. Stopping a server goes in this order, which
is what makes the flush above safe:
Scheduled jobs stop starting, so none begins while the server drains.
The server stops accepting and waits, up to
cn1.server.shutdownTimeoutMillis, for the requests in flight.@Asynccalls and scheduled runs that haven’t finished get the same grace. A task still waiting in a queue when that time is up is dropped rather than started against beans about to be destroyed, and the threads of the ones still running are interrupted.@PreDestroyanddestroyMethodrun, in reverse dependency order.The connection pool closes.
The last metrics and spans are exported.
A start-up that fails partway runs the destroy methods of the beans already built, so a server a supervisor restarts doesn’t leak what their constructors opened.
What the build refuses
Everything a container would discover at start-up is checked while the module compiles, and each refusal names the class and member at fault:
| Message fragment | What to do |
|---|---|
| Annotate the implementation |
| Mark one |
| Inject one side through an |
| Take the dependency as a constructor parameter. |
| Mark the constructor the build should call. |
| A scoped or lazy bean: make the class and its methods non-final and give it a constructor with no arguments. |
| Only a |
| Give one a name of its own in its annotation. |
How this differs from Spring
The annotations mean what they mean in Spring, and most code reads the same. The differences all follow from doing the work at build time:
No scanning. A bean is a class in the module, or the result of a
@Beanmethod. A jar on the class path contributes nothing unless a@Beanmethod builds something from it.No proxies for aspects.
@Transactional,@Async,@Timedand@Countedrewrite the method itself, so they apply to a call throughthis, to a private method, and to an object built withnew. In Spring each of those skips the aspect, and nothing says so.No proxy around
@Configuration. One@Beanmethod calling another gets a second object. Take the other bean as a parameter instead.Errors at build time. A missing or ambiguous dependency, a constructor cycle, a scoped bean that can’t be subclassed and a malformed cron expression all fail the build.
No
ApplicationContext. Nothing looks a bean up by name or type at run time. A class that needs a bean declares it as a dependency.
Testing beans and the wired server
Constructor injection is a constructor, so a unit test builds the object with
fakes and needs nothing else. For the wired server — what @SpringBootTest
does in Spring — a test in the backend module starts the generated wiring on a
free port:
Properties settings = new Properties();
settings.setProperty("cn1.server.port", "0"); // any free port
Backend server = Backend.builder(Config.of(settings, "test"))
.quiet()
.application(wiring)
.start();
try {
int port = server.getServer().getPort();
// ... send requests to http://127.0.0.1:<port> and assert on the answers ...
} finally {
server.stop();
}