Integrating Vert.x Application with Weld/CDI
In the last post, we introduced a simple approach to integrate Vert.x applications with Spring framework. In this post, we will try to integrate Vert.x application with CDI to replace Spring.
CDI is a Dependency Injection specification introduced in Java EE 6, now Jakarta EE maintained by the Eclipse Foundation. Weld is a compatible provider of the Jakarta CDI specification.
Project Setup
This module is built with:
- Vert.x 5.1.3 (via
vertx-stack-depchainBOM) - Java 25 (
maven.compiler.release=25) - Weld SE 6.0.4.Final (
weld-se-shaded) - Jandex 3.5.3 (class indexing)
- Jackson 2.22.0 (via
jackson-bom) - JUnit 6.0.2, AssertJ 3.27.7
Add the following dependencies.
<dependency>
<groupId>org.jboss.weld.se</groupId>
<artifactId>weld-se-shaded</artifactId>
<version>${weld.version}</version>
</dependency>
<dependency>
<groupId>io.smallrye</groupId>
<artifactId>jandex</artifactId>
<version>${jandex.version}</version>
</dependency>
weld-se-shadedprovides a CDI runtime environment for Java SE.jandexindexes the application classes and speeds up bean discovery.
Add a beans.xml configuration in main/resources/META-INF to enable CDI support in a Java SE application.
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/beans_2_0.xsd"
bean-discovery-mode="annotated">
</beans>
In Jakarta EE, CDI is enabled by default since Java EE 7, and
beans.xmlis optional.
DemoApplication
Similar to the Spring version, add a DemoApplication to start the application.
public class DemoApplication {
private static final Logger LOGGER = Logger.getLogger(DemoApplication.class.getName());
public static void main(String[] args) {
var weld = new Weld();
WeldContainer container = weld.initialize();
Vertx vertx = container.select(Vertx.class).get();
VerticleFactory factory = container.select(VerticleFactory.class).get();
LOGGER.info("vertx clazz:" + vertx.getClass().getName());
LOGGER.info("factory clazz:" + factory.getClass().getName());
vertx.deployVerticle(factory.prefix() + ":" + MainVerticle.class.getName());
}
}
Here we use weld.initialize() to initialize the CDI container, then retrieve the Vertx bean and VerticleFactory bean, and deploy the MainVerticle.
CdiAwareVerticleFactory
Similar to SpringAwareVerticleFactory, create a CDI-aware VerticleFactory.
@ApplicationScoped
public class CdiAwareVerticleFactory implements VerticleFactory {
@Inject
private Instance<Object> instance;
@Override
public String prefix() {
return "cdi";
}
@Override
public void createVerticle2(String verticleName, ClassLoader classLoader, Promise<Callable<? extends Deployable>> promise) {
String clazz = VerticleFactory.removePrefix(verticleName);
promise.complete(() -> (VerticleBase) instance.select(Class.forName(clazz)).get());
}
}
Note: In Vert.x 5, the method signature uses createVerticle2 with Promise<Callable<? extends Deployable>> and returns VerticleBase.
Resources
Create a Resources class to expose the Vertx and Pool beans.
@ApplicationScoped
public class Resources {
private final static Logger LOGGER = Logger.getLogger(Resources.class.getName());
@Produces
@Singleton
public Vertx vertx(VerticleFactory verticleFactory) {
Vertx vertx = Vertx.vertx();
vertx.registerVerticleFactory(verticleFactory);
return vertx;
}
@Produces
public Pool pgPool(Vertx vertx) {
PgConnectOptions connectOptions = new PgConnectOptions()
.setPort(5432)
.setHost("localhost")
.setDatabase("blogdb")
.setUser("user")
.setPassword("password");
PoolOptions poolOptions = new PoolOptions().setMaxSize(5);
return PgBuilder.pool()
.with(poolOptions)
.connectingTo(connectOptions)
.using(vertx)
.build();
}
public void disposesPgPool(@Disposes Pool pgPool) {
LOGGER.info("disposing PgPool...");
pgPool.close().onSuccess(v -> LOGGER.info("PgPool is closed successfully."));
}
}
Key changes from Vert.x 4:
- Uses PgBuilder.pool() builder pattern instead of PgPool.pool(vertx, connectOptions, poolOptions)
- Returns Pool (from io.vertx.sqlclient.Pool) instead of PgPool
We annotate the
Vertxbean with@Singleton(not@ApplicationScoped) because Weld does not create proxy objects for@Singletonbeans. Without this, there would be a class casting error at startup.
MainVerticle
MainVerticle extends VerticleBase (replacing AbstractVerticle from Vert.x 4) and uses Future<?> start().
@Dependent
public class MainVerticle extends VerticleBase {
private static final Logger LOGGER = Logger.getLogger(MainVerticle.class.getName());
static {
var objectMapper = DatabindCodec.mapper();
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
objectMapper.disable(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS);
objectMapper.disable(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS);
JavaTimeModule module = new JavaTimeModule();
objectMapper.registerModule(module);
}
private PostsHandler postsHandler;
public MainVerticle() {}
@Inject
public MainVerticle(PostsHandler postHandlers) {
this.postsHandler = postHandlers;
}
@Override
public Future<?> start() throws Exception {
LOGGER.log(Level.INFO, "Starting HTTP server...");
var router = routes(postsHandler);
return vertx.createHttpServer()
.requestHandler(router)
.listen(8888)
.onSuccess(server -> LOGGER.log(Level.INFO, "HTTP server started on port " + server.actualPort()))
.onFailure(event -> LOGGER.log(Level.SEVERE, "Failed to start HTTP server:" + event.getMessage()));
}
private Router routes(PostsHandler handlers) {
Router router = Router.router(vertx);
router.get("/posts").produces("application/json").handler(handlers::all);
router.post("/posts").consumes("application/json").handler(BodyHandler.create()).handler(handlers::save);
router.get("/posts/:id").produces("application/json").handler(handlers::get).failureHandler(frc -> frc.response().setStatusCode(404).end());
router.put("/posts/:id").consumes("application/json").handler(BodyHandler.create()).handler(handlers::update);
router.delete("/posts/:id").handler(handlers::delete);
router.get("/hello").handler(rc -> rc.response().end("Hello from my route"));
return router;
}
}
Other Beans
Other beans use CDI @ApplicationScoped to replace Spring's @Component.
@ApplicationScoped
class PostsHandler {
private PostRepository posts;
public PostsHandler() {}
@Inject
PostsHandler(PostRepository posts) {
this.posts = posts;
}
public void all(RoutingContext rc) {
this.posts.findAll()
.onSuccess(data -> rc.response().end(Json.encode(data)));
}
public void get(RoutingContext rc) {
var id = rc.pathParams().get("id");
this.posts.findById(UUID.fromString(id))
.onSuccess(post -> rc.response().end(Json.encode(post)))
.onFailure(throwable -> rc.fail(404, throwable));
}
public void save(RoutingContext rc) {
var body = rc.body().asJsonObject();
var form = body.mapTo(CreatePostCommand.class);
this.posts.save(new Post(null, form.title(), form.content(), null))
.onSuccess(savedId -> rc.response()
.putHeader("Location", "/posts/" + savedId)
.setStatusCode(201)
.end());
}
public void update(RoutingContext rc) {
var id = rc.pathParams().get("id");
var body = rc.body().asJsonObject();
var form = body.mapTo(CreatePostCommand.class);
this.posts.findById(UUID.fromString(id))
.compose(post -> {
var toUpdated = new Post(post.id(), form.title(), form.content(), null);
return this.posts.update(toUpdated);
})
.onSuccess(data -> rc.response().setStatusCode(204).end())
.onFailure(throwable -> rc.fail(404, throwable));
}
public void delete(RoutingContext rc) {
var uuid = UUID.fromString(rc.pathParams().get("id"));
this.posts.findById(uuid)
.compose(post -> this.posts.deleteById(uuid))
.onSuccess(data -> rc.response().setStatusCode(204).end())
.onFailure(throwable -> rc.fail(404, throwable));
}
}
@ApplicationScoped
public class PostRepository {
private Pool client;
public PostRepository() {}
@Inject
public PostRepository(Pool client) {
this.client = client;
}
public Future<List<Post>> findAll() {
return client.query("SELECT * FROM posts ORDER BY id ASC")
.execute()
.map(rs -> StreamSupport.stream(rs.spliterator(), false)
.map(MAPPER)
.collect(toList()));
}
// ... see source for findById, save, update, deleteAll, deleteById
}
@ApplicationScoped
public class DataInitializer {
private Pool client;
public DataInitializer() {}
@Inject
public DataInitializer(Pool client) {
this.client = client;
}
public void run(@Observes @Initialized(ApplicationScoped.class) Object o) throws InterruptedException {
// seeds sample data using client.withTransaction()
}
}
Testing
Add the Weld JUnit5 dependency for testing.
<dependency>
<groupId>org.jboss.weld</groupId>
<artifactId>weld-junit5</artifactId>
<version>5.0.3.Final</version>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
</exclusion>
<exclusion>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
@EnableAutoWeld
@ExplicitParamInjection
@AddPackages(DemoApplication.class)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@ExtendWith(VertxExtension.class)
public class TestMainVerticle {
@Inject
Instance<Object> context;
Vertx vertx;
@BeforeAll
public void setupAll(VertxTestContext testContext) {
vertx = context.select(Vertx.class).get();
var factory = context.select(VerticleFactory.class).get();
vertx.deployVerticle(factory.prefix() + ":" + MainVerticle.class.getName())
.onSuccess(id -> {
LOGGER.info("deployed:" + id);
testContext.completeNow();
})
.onFailure(testContext::failNow);
}
@Test
void testGetAll(VertxTestContext testContext) {
var client = vertx.createHttpClient(new HttpClientOptions().setDefaultPort(8888));
client.request(HttpMethod.GET, "/posts")
.flatMap(req -> req.send().flatMap(HttpClientResponse::body))
.onSuccess(buffer -> testContext.verify(() -> {
assertThat(buffer.toJsonArray().size()).isGreaterThan(0);
testContext.completeNow();
}))
.onFailure(e -> LOGGER.log(Level.ALL, "error: {0}", e.getMessage()));
}
}
Get the example codes from my GitHub.