Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

CDI: beans, scopes and injection

Keeping data and logic right in a resource is awkward: a resource should only accept HTTP requests and return responses. Working with posts is better moved into a separate service class. To give the resource a ready service object without creating it with new, we use CDI (Contexts and Dependency Injection), the dependency injection standard. In Quarkus it is implemented by the ArC container from the quarkus-arc extension.

Terms

  • A bean is an object that the container creates and keeps.
  • A scope is how long a bean lives: the whole run of the application or one request.
  • Injection means the container itself puts the needed bean into a field or a constructor.

The post service

Create the service file. The @ApplicationScoped annotation makes the class a bean with a single instance for the whole application:

// src/main/java/com/example/blog/PostService.java
package com.example.blog;
 
import jakarta.enterprise.context.ApplicationScoped;
 
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
 
@ApplicationScoped
public class PostService {
 
    private final Map<Long, Post> posts = new ConcurrentHashMap<>();
    private final AtomicLong nextId = new AtomicLong(1);
 
    public PostService() {
        create("Hello, Quarkus", "The first post of the blog.");
        create("Dev mode", "Change the code and refresh the page.");
    }
 
    public List<Post> findAll() {
        return posts.values().stream()
                .sorted((a, b) -> Long.compare(a.id(), b.id()))
                .toList();
    }
 
    public Optional<Post> findById(long id) {
        return Optional.ofNullable(posts.get(id));
    }
 
    public Post create(String title, String body) {
        long id = nextId.getAndIncrement();
        Post post = new Post(id, title, body);
        posts.put(id, post);
        return post;
    }
 
    public boolean delete(long id) {
        return posts.remove(id) != null;
    }
}

The service stores posts and can find, create and delete them. It knows nothing about HTTP.

Injecting it into the resource

The resource gets the service through @Inject, and the posts and nextId fields are removed from it:

// src/main/java/com/example/blog/PostResource.java
@Path("/posts")
public class PostResource {
 
    @Inject
    PostService postService;
 
    @GET
    public List<Post> list(@QueryParam("search") String search,
                           @QueryParam("limit") @DefaultValue("10") int limit) {
        return postService.findAll().stream()
                .filter(p -> search == null || p.title().toLowerCase().contains(search.toLowerCase()))
                .limit(limit)
                .toList();
    }
 
    @GET
    @Path("/{id}")
    public Post get(@PathParam("id") long id) {
        return postService.findById(id)
                .orElseThrow(NotFoundException::new);
    }
 
    @POST
    public Response create(NewPost newPost) {
        Post post = postService.create(newPost.title(), newPost.body());
        return Response.created(URI.create("/posts/" + post.id()))
                .entity(post)
                .build();
    }
}

The annotation import is jakarta.inject.Inject. The delete method is rewritten the same way, with postService.delete(id). Let's check the search from the previous lessons:

curl "http://localhost:8080/posts?search=dev"
[{"id":2,"title":"Dev mode","body":"Change the code and refresh the page."}]

A field without private is the usual Quarkus style: the container then needs no reflection. You can also inject through a constructor — if it is the only one, @Inject is optional:

// src/main/java/com/example/blog/PostResource.java
private final PostService postService;
 
public PostResource(PostService postService) {
    this.postService = postService;
}

Scopes

AnnotationHow long the bean lives
@ApplicationScopedOne per application, created on first use
@SingletonOne per application, but without a proxy wrapper
@RequestScopedA separate instance for every HTTP request
@DependentA new instance for every injection point

For services @ApplicationScoped is almost always the right choice.

Common mistake

If you forget the scope annotation, the class does not become a bean and the application does not start:

UnsatisfiedResolutionException: Unsatisfied dependency for type com.example.blog.PostService and qualifiers [@Default]
	- injection target: com.example.blog.PostResource#postService
	The following classes match by type, but have been skipped during discovery:
	- com.example.blog.PostService has no bean defining annotation (scope, stereotype, etc.)

The last line names the cause directly: the class has no bean defining annotation. Add @ApplicationScoped.

Thanks for reading!

Donate to the site — we work hard for you.