Data Engineering: Work with DBs? Build data pipelines? Or maybe you're exploring AI-driven data capabilities? We want to hear your insights.
Enterprise Security: Now is the time to ensure your systems are secure. Expand your org's tactics and put future attackers in their place.
A framework is a collection of code that is leveraged in the development process by providing ready-made components. Through the use of frameworks, architectural patterns and structures are created, which help speed up the development process. This Zone contains helpful resources for developers to learn about and further explore popular frameworks such as the Spring framework, Drupal, Angular, Eclipse, and more.
Transforming Software Development With Low-Code and No-Code Integration
The Role of AI in Low- and No-Code Development
In today's security landscape, OAuth2 has become a standard for securing APIs, providing a more robust and flexible approach than basic authentication. My journey into this domain began with a critical solution architecture decision: migrating from basic authentication to OAuth2 client credentials for obtaining access tokens. While Spring Security offers strong support for both authentication methods, I encountered a significant challenge. I could not find a declarative approach that seamlessly integrated basic authentication and JWT authentication within the same application. This gap in functionality motivated me to explore and develop a solution that not only meets the authentication requirements but also supports comprehensive integration testing. This article shares my findings and provides a detailed guide on setting up Keycloak, integrating it with Spring Security and Spring Boot, and utilizing the Spock Framework for repeatable integration tests. By the end of this article, you will clearly understand how to configure and test your authentication mechanisms effectively with Keycloak as an identity provider, ensuring a smooth transition to OAuth2 while maintaining the flexibility to support basic authentication where necessary. Prerequisites Before you begin, ensure you have met the following requirements: You have installed Java 21. You have a basic understanding of Maven and Java. This is the parent project for the taptech-code-accelerator modules. It manages common dependencies and configurations for all the child modules. You can get it from here taptech-code-accelerator. Building taptech-code-accelerator To build the taptech-code-accelerator project, follow these steps: git clone the project from the repository: git clone https://github.com/glawson6/taptech-code-accelerator.git Open a terminal and change the current directory to the root directory of the taptech-code-accelerator project. cd path/to/taptech-code-accelerator Run the following command to build the project: ./build.sh This command cleans the project, compiles the source code, runs any tests, packages the compiled code into a JAR or WAR file, and installs the packaged code in your local Maven repository. It also builds the local Docker image that will be used to run later. Please ensure you have the necessary permissions to execute these commands. Keycloak Initial Setup Setting up Keycloak for integration testing involves several steps. This guide will walk you through creating a local environment configuration, starting Keycloak with Docker, configuring realms and clients, verifying the setup, and preparing a PostgreSQL dump for your integration tests. Step 1: Create a local.env File First, navigate to the taptech-common/src/test/resources/docker directory. Create a local.env file to store environment variables needed for the Keycloak service. Here's an example of what the local.env file might look like: POSTGRES_DB=keycloak POSTGRES_USER=keycloak POSTGRES_PASSWORD=admin KEYCLOAK_ADMIN=admin KEYCLOAK_ADMIN_PASSWORD=admin KC_DB_USERNAME=keycloak KC_DB_PASSWORD=keycloak SPRING_PROFILES_ACTIVE=secure-jwk KEYCLOAK_ADMIN_CLIENT_SECRET=DCRkkqpUv3XlQnosjtf8jHleP7tuduTa IDP_PROVIDER_JWKSET_URI=http://172.28.1.90:8080/realms/offices/protocol/openid-connect/certs Step 2: Start the Keycloak Service Next, start the Keycloak service using the provided docker-compose.yml file and the ./start-services.sh script. The docker-compose.yml file should define the Keycloak and PostgreSQL services. version: '3.8' services: postgres: image: postgres volumes: - postgres_data:/var/lib/postgresql/data #- ./dump:/docker-entrypoint-initdb.d environment: POSTGRES_DB: keycloak POSTGRES_USER: ${KC_DB_USERNAME} POSTGRES_PASSWORD: ${KC_DB_PASSWORD} networks: node_net: ipv4_address: 172.28.1.31 keycloak: image: quay.io/keycloak/keycloak:23.0.6 command: start #--import-realm environment: KC_HOSTNAME: localhost KC_HOSTNAME_PORT: 8080 KC_HOSTNAME_STRICT_BACKCHANNEL: false KC_HTTP_ENABLED: true KC_HOSTNAME_STRICT_HTTPS: false KC_HEALTH_ENABLED: true KEYCLOAK_ADMIN: ${KEYCLOAK_ADMIN} KEYCLOAK_ADMIN_PASSWORD: ${KEYCLOAK_ADMIN_PASSWORD} KC_DB: postgres KC_DB_URL: jdbc:postgresql://172.28.1.31/keycloak KC_DB_USERNAME: ${KC_DB_USERNAME} KC_DB_PASSWORD: ${KC_DB_PASSWORD} ports: - 8080:8080 volumes: - ./realms:/opt/keycloak/data/import restart: always depends_on: - postgres networks: node_net: ipv4_address: 172.28.1.90 volumes: postgres_data: driver: local networks: node_net: ipam: driver: default config: - subnet: 172.28.0.0/16 Then, use the ./start-services.sh script to start the services: Step 3: Access Keycloak Admin Console Once Keycloak has started, log in to the admin console at http://localhost:8080 using the configured admin username and password (default is admin/admin). Step 4: Create a Realm and Client Create a Realm: Log in to the Keycloak admin console. In the left-hand menu, click on "Add Realm". Enter the name of the realm (e.g., offices) and click "Create". Create a Client: Select your newly created realm from the left-hand menu. Click on "Clients" in the left-hand menu. Click on "Create" in the right-hand corner. Enter the client ID (e.g., offices), choose openid-connect as the client protocol, and click "Save." Click "Save." Extract the admin-cli Client Secret: Follow directions in the doc EXTRACTING-ADMIN-CLI-CLIENT-SECRET.md to extract the admin-cli client secret. Save the client secret for later use. Step 5: Verify the Setup With HTTP Requests To verify the setup, you can use HTTP requests to obtain tokens. Get access token: http -a admin-cli:[client secret] --form POST http://localhost:8080/realms/master/protocol/openid-connect/token grant_type=password username=admin password=Pa55w0rd Step 6: Create a PostgreSQL Dump After verifying the setup, create a PostgreSQL dump of the Keycloak database to use for seeding the database during integration tests. Create the dump: docker exec -i docker-postgres-1 /bin/bash -c "PGPASSWORD=keycloak pg_dump --username keycloak keycloak" > dump/keycloak-dump.sql Save the file: Save the keycloak-dump.sql file locally. This file will be used to seed the database for integration tests. Following these steps, you will have a Keycloak instance configured and ready for integration testing with Spring Security and the Spock Framework. Spring Security and Keycloak Integration Tests This section will set up integration tests for Spring Security and Keycloak using Spock and Testcontainers. This involves configuring dependencies, setting up Testcontainers for Keycloak and PostgreSQL, and creating a base class to hold the necessary configurations. Step 1: Add Dependencies First, add the necessary dependencies to your pom.xml file. Ensure that Spock, Testcontainers for Keycloak and PostgreSQL, and other required libraries are included (check here). Step 2: Create the Base Test Class Create a base class to hold the configuration for your integration tests. package com.taptech.common.security.keycloak import com.taptech.common.security.user.InMemoryUserContextPermissionsService import com.fasterxml.jackson.databind.ObjectMapper import dasniko.testcontainers.keycloak.KeycloakContainer import org.keycloak.admin.client.Keycloak import org.slf4j.Logger import org.slf4j.LoggerFactory import org.springframework.beans.factory.annotation.Autowired import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.testcontainers.containers.Network import org.testcontainers.containers.PostgreSQLContainer import org.testcontainers.containers.output.Slf4jLogConsumer import org.testcontainers.containers.wait.strategy.ShellStrategy import org.testcontainers.utility.DockerImageName import org.testcontainers.utility.MountableFile import spock.lang.Shared import spock.lang.Specification import spock.mock.DetachedMockFactory import java.time.Duration import java.time.temporal.ChronoUnit class BaseKeyCloakInfraStructure extends Specification { private static final Logger logger = LoggerFactory.getLogger(BaseKeyCloakInfraStructure.class); static String jdbcUrlFormat = "jdbc:postgresql://%s:%s/%s" static String keycloakBaseUrlFormat = "http://%s:%s" public static final String OFFICES = "offices"; public static final String POSTGRES_NETWORK_ALIAS = "postgres"; @Shared static Network network = Network.newNetwork(); @Shared static PostgreSQLContainer<?> postgres = createPostgresqlContainer() protected static PostgreSQLContainer createPostgresqlContainer() { PostgreSQLContainer container = new PostgreSQLContainer<>("postgres") .withNetwork(network) .withNetworkAliases(POSTGRES_NETWORK_ALIAS) .withCopyFileToContainer(MountableFile.forClasspathResource("postgres/keycloak-dump.sql"), "/docker-entrypoint-initdb.d/keycloak-dump.sql") .withUsername("keycloak") .withPassword("keycloak") .withDatabaseName("keycloak") .withLogConsumer(new Slf4jLogConsumer(logger)) .waitingFor(new ShellStrategy() .withCommand( "psql -q -o /dev/null -c \"SELECT 1\" -d keycloak -U keycloak") .withStartupTimeout(Duration.of(60, ChronoUnit.SECONDS))) return container } public static final DockerImageName KEYCLOAK_IMAGE = DockerImageName.parse("bitnami/keycloak:23.0.5"); @Shared public static KeycloakContainer keycloakContainer; @Shared static String adminCC = "[email protected]" def setup() { } // run before every feature method def cleanup() {} // run after every feature method def setupSpec() { postgres.start() String jdbcUrl = String.format(jdbcUrlFormat, POSTGRES_NETWORK_ALIAS, 5432, postgres.getDatabaseName()); keycloakContainer = new KeycloakContainer("quay.io/keycloak/keycloak:23.0.6") .withNetwork(network) .withExposedPorts(8080) .withEnv("KC_HOSTNAME", "localhost") .withEnv("KC_HOSTNAME_PORT", "8080") .withEnv("KC_HOSTNAME_STRICT_BACKCHANNEL", "false") .withEnv("KC_HTTP_ENABLED", "true") .withEnv("KC_HOSTNAME_STRICT_HTTPS", "false") .withEnv("KC_HEALTH_ENABLED", "true") .withEnv("KEYCLOAK_ADMIN", "admin") .withEnv("KEYCLOAK_ADMIN_PASSWORD", "admin") .withEnv("KC_DB", "postgres") .withEnv("KC_DB_URL", jdbcUrl) .withEnv("KC_DB_USERNAME", "keycloak") .withEnv("KC_DB_PASSWORD", "keycloak") keycloakContainer.start() String authServerUrl = keycloakContainer.getAuthServerUrl(); String adminUsername = keycloakContainer.getAdminUsername(); String adminPassword = keycloakContainer.getAdminPassword(); logger.info("Keycloak getExposedPorts: {}", keycloakContainer.getExposedPorts()) String keycloakBaseUrl = String.format(keycloakBaseUrlFormat, keycloakContainer.getHost(), keycloakContainer.getMappedPort(8080)); //String keycloakBaseUrl = "http://localhost:8080" logger.info("Keycloak authServerUrl: {}", authServerUrl) logger.info("Keycloak URL: {}", keycloakBaseUrl) logger.info("Keycloak adminUsername: {}", adminUsername) logger.info("Keycloak adminPassword: {}", adminPassword) logger.info("JDBC URL: {}", jdbcUrl) System.setProperty("spring.datasource.url", jdbcUrl) System.setProperty("spring.datasource.username", postgres.getUsername()) System.setProperty("spring.datasource.password", postgres.getPassword()) System.setProperty("spring.datasource.driverClassName", "org.postgresql.Driver"); System.setProperty("POSTGRES_URL", jdbcUrl) System.setProperty("POSRGRES_USER", postgres.getUsername()) System.setProperty("POSRGRES_PASSWORD", postgres.getPassword()); System.setProperty("idp.provider.keycloak.base-url", authServerUrl) System.setProperty("idp.provider.keycloak.admin-client-secret", "DCRkkqpUv3XlQnosjtf8jHleP7tuduTa") System.setProperty("idp.provider.keycloak.admin-client-id", KeyCloakConstants.ADMIN_CLI) System.setProperty("idp.provider.keycloak.admin-username", adminUsername) System.setProperty("idp.provider.keycloak.admin-password", adminPassword) System.setProperty("idp.provider.keycloak.default-context-id", OFFICES) System.setProperty("idp.provider.keycloak.client-secret", "x9RIGyc7rh8A4w4sMl8U5rF3HuNm2wOC3WOD") System.setProperty("idp.provider.keycloak.client-id", OFFICES) System.setProperty("idp.provider.keycloak.token-uri", "/realms/offices/protocol/openid-connect/token") System.setProperty("idp.provider.keycloak.jwkset-uri", authServerUrl + "/realms/offices/protocol/openid-connect/certs") System.setProperty("idp.provider.keycloak.issuer-url", authServerUrl + "/realms/offices") System.setProperty("idp.provider.keycloak.admin-token-uri", "/realms/master/protocol/openid-connect/token") System.setProperty("idp.provider.keycloak.user-uri", "/admin/realms/{realm}/users") System.setProperty("idp.provider.keycloak.use-strict-jwt-validators", "false") } // run before the first feature method def cleanupSpec() { keycloakContainer.stop() postgres.stop() } // run after @Autowired Keycloak keycloak @Autowired KeyCloakAuthenticationManager keyCloakAuthenticationManager @Autowired InMemoryUserContextPermissionsService userContextPermissionsService @Autowired KeyCloakManagementService keyCloakService @Autowired KeyCloakIdpProperties keyCloakIdpProperties @Autowired KeyCloakJwtDecoderFactory keyCloakJwtDecoderFactory def test_config() { expect: keycloak != null keyCloakAuthenticationManager != null keyCloakService != null } static String basicAuthCredsFrom(String s1, String s2) { return "Basic " + toBasicAuthCreds(s1, s2); } static toBasicAuthCreds(String s1, String s2) { return Base64.getEncoder().encodeToString((s1 + ":" + s2).getBytes()); } @Configuration @EnableKeyCloak public static class TestConfig { @Bean ObjectMapper objectMapper() { return new ObjectMapper(); } DetachedMockFactory mockFactory = new DetachedMockFactory() } } In the BaseKeyCloakInfraStructure class, a method named createPostgresqlContainer() is used to set up a PostgreSQL test container. This method configures the container with various settings, including network settings, username, password, and database name. This class sets up the entire Postgresql and Keycloak env. One of the key steps in this method is the use of a PostgreSQL dump file to populate the database with initial data. This is done using the withCopyFileToContainer() method, which copies a file from the classpath to a specified location within the container. If you have problems starting, you might need to restart the Docker Compose file and extract the client secret. This is explained in EXTRACTING-ADMIN-CLI-CLIENT-SECRET. The code snippet for this is: .withCopyFileToContainer(MountableFile.forClasspathResource("postgres/keycloak-dump.sql"), "/docker-entrypoint-initdb.d/keycloak-dump.sql") Step 3: Extend the Base Class End Run Your Tests package com.taptech.common.security.token import com.taptech.common.EnableCommonConfig import com.taptech.common.security.keycloak.BaseKeyCloakInfraStructure import com.taptech.common.security.keycloak.EnableKeyCloak import com.taptech.common.security.keycloak.KeyCloakAuthenticationManager import com.taptech.common.security.user.UserContextPermissions import com.taptech.common.security.utils.SecurityUtils import com.fasterxml.jackson.databind.ObjectMapper import org.slf4j.Logger import org.slf4j.LoggerFactory import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.security.oauth2.client.registration.InMemoryReactiveClientRegistrationRepository import org.springframework.test.context.ContextConfiguration import org.springframework.test.web.reactive.server.EntityExchangeResult import org.springframework.test.web.reactive.server.WebTestClient import spock.mock.DetachedMockFactory import org.springframework.boot.autoconfigure.security.reactive.ReactiveSecurityAutoConfiguration @ContextConfiguration(classes = [TestApiControllerConfig.class]) @WebFluxTest(/*controllers = [TokenApiController.class],*/ properties = [ "spring.main.allow-bean-definition-overriding=true", "openapi.token.base-path=/", "idp.provider.keycloak.initialize-on-startup=true", "idp.provider.keycloak.initialize-realms-on-startup=false", "idp.provider.keycloak.initialize-users-on-startup=true", "spring.test.webtestclient.base-url=http://localhost:8888" ], excludeAutoConfiguration = ReactiveSecurityAutoConfiguration.class) class TokenApiControllerTest extends BaseKeyCloakInfraStructure { private static final Logger logger = LoggerFactory.getLogger(TokenApiControllerTest.class); /* ./mvnw clean test -Dtest=TokenApiControllerTest ./mvnw clean test -Dtest=TokenApiControllerTest#test_public_validate */ @Autowired TokenApiApiDelegate tokenApiDelegate @Autowired KeyCloakAuthenticationManager keyCloakAuthenticationManager @Autowired private WebTestClient webTestClient @Autowired TokenApiController tokenApiController InMemoryReactiveClientRegistrationRepository clientRegistrationRepository def test_configureToken() { expect: tokenApiDelegate } def test_public_jwkkeys() { expect: webTestClient.get().uri("/public/jwkKeys") .exchange() .expectStatus().isOk() .expectBody() } def test_public_login() { expect: webTestClient.get().uri("/public/login") .headers(headers -> { headers.setBasicAuth(BaseKeyCloakInfraStructure.adminCC, "admin") }) .exchange() .expectStatus().isOk() .expectBody() .jsonPath(".access_token").isNotEmpty() .jsonPath(".refresh_token").isNotEmpty() } def test_public_login_401() { expect: webTestClient.get().uri("/public/login") .headers(headers -> { headers.setBasicAuth(BaseKeyCloakInfraStructure.adminCC, "bad") }) .exchange() .expectStatus().isUnauthorized() } def test_public_refresh_token() { given: def results = keyCloakAuthenticationManager.passwordGrantLoginMap(BaseKeyCloakInfraStructure.adminCC, "admin", OFFICES).toFuture().join() def refreshToken = results.get("refresh_token") expect: webTestClient.get().uri("/public/refresh") .headers(headers -> { headers.set("Authorization", SecurityUtils.toBearerHeaderFromToken(refreshToken)) headers.set("contextId", OFFICES) }) .exchange() .expectStatus().isOk() .expectBody() .jsonPath(".access_token").isNotEmpty() .jsonPath(".refresh_token").isNotEmpty() } def test_public_validate() { given: def results = keyCloakAuthenticationManager.passwordGrantLoginMap(BaseKeyCloakInfraStructure.adminCC, "admin", OFFICES).toFuture().join() def accessToken = results.get("access_token") expect: EntityExchangeResult<UserContextPermissions> entityExchangeResult = webTestClient.get().uri("/public/validate") .headers(headers -> { headers.set("Authorization", SecurityUtils.toBearerHeaderFromToken(accessToken)) }) .exchange() .expectStatus().isOk() .expectBody(UserContextPermissions.class) .returnResult() logger.info("entityExchangeResult: {}", entityExchangeResult.getResponseBody()) } @Configuration @EnableCommonConfig @EnableKeyCloak @EnableTokenApi public static class TestApiControllerConfig { @Bean ObjectMapper objectMapper() { return new ObjectMapper(); } DetachedMockFactory mockFactory = new DetachedMockFactory() } } Conclusion With this setup, you have configured Testcontainers to run Keycloak and PostgreSQL within a Docker network, seeded the PostgreSQL database with a dump file, and created a base test class to manage the lifecycle of these containers. You can now write your integration tests extending this base class to ensure your Spring Security configuration works correctly with Keycloak.
Every day, developers are pushed to evaluate and use different tools, cloud provider services, and follow complex inner-development loops. In this article, we look at how the open-source Dapr project can help Spring Boot developers build more resilient and environment-agnostic applications. At the same time, they keep their inner development loop intact. Meeting Developers Where They Are A couple of weeks ago at Spring I/O, we had the chance to meet the Spring community face-to-face in the beautiful city of Barcelona, Spain. At this conference, the Spring framework maintainers, core contributors, and end users meet yearly to discuss the framework's latest additions, news, upgrades, and future initiatives. While I’ve seen many presentations covering topics such as Kubernetes, containers, and deploying Spring Boot applications to different cloud providers, these topics are always covered in a way that makes sense for Spring developers. Most tools presented in the cloud-native space involve using new tools and changing the tasks performed by developers, sometimes including complex configurations and remote environments. Tools like the Dapr project, which can be installed on a Kubernetes cluster, push developers to add Kubernetes as part of their inner-development loop tasks. While some developers might be comfortable with extending their tasks to include Kubernetes for local development, some teams prefer to keep things simple and use tools like Testcontainers to create ephemeral environments where they can test their code changes for local development purposes. With Dapr, developers can rely on consistent APIs across programming languages. Dapr provides a set of building blocks (state management, publish/subscribe, service Invocation, actors, and workflows, among others) that developers can use to code their application features. Instead of spending too much time describing what Dapr is, in this article, we cover how the Dapr project and its integration with the Spring Boot framework can simplify the development experience for Dapr-enabled applications that can be run, tested, and debugged locally without the need to run inside a Kubernetes cluster. Today, Kubernetes, and Cloud-Native Runtimes Today, if you want to work with the Dapr project, no matter the programming language you are using, the easiest way is to install Dapr into a Kubernetes cluster. Kubernetes and container runtimes are the most common runtimes for our Java applications today. Asking Java developers to work and run their applications on a Kubernetes cluster for their day-to-day tasks might be way out of their comfort zone. Training a large team of developers on using Kubernetes can take a while, and they will need to learn how to install tools like Dapr on their clusters. If you are a Spring Boot developer, you probably want to code, run, debug, and test your Spring Boot applications locally. For this reason, we created a local development experience for Dapr, teaming up with the Testcontainers folks, now part of Docker. As a Spring Boot developer, you can use the Dapr APIs without a Kubernetes cluster or needing to learn how Dapr works in the context of Kubernetes. This test shows how Testcontainers provisions the Dapr runtime by using the @ClassRule annotation, which is in charge of bootstrapping the Dapr runtime so your application code can use the Dapr APIs to save/retrieve state, exchange asynchronous messages, retrieve configurations, create workflows, and use the Dapr actor model. How does this compare to a typical Spring Boot application? Let’s say you have a distributed application that uses Redis, PostgreSQL, and RabbitMQ to persist and read state and Kafka to exchange asynchronous messages. You can find the code for this application here (under the java/ directory, you can find all the Java implementations). Your Spring Boot applications will need to have not only the Redis client but also the PostgreSQL JDBC driver and the RabbitMQ client as dependencies. On top of that, it is pretty standard to use Spring Boot abstractions, such as Spring Data KeyValue for Redis, Spring Data JDBC for PostgreSQL, and Spring Boot Messaging RabbitMQ. These abstractions and libraries elevate the basic Redis, relational database, and RabbitMQ client experiences to the Spring Boot programming model. Spring Boot will do more than just call the underlying clients. It will manage the underlying client lifecycle and help developers implement common use cases while promoting best practices under the covers. If we look back at the test that showed how Spring Boot developers can use the Dapr APIs, the interactions will look like this: In the second diagram, the Spring Boot application only depends on the Dapr APIs. In both the unit test using the Dapr APIs shown above and the previous diagram, instead of connecting to the Dapr APIs directly using HTTP or gRPC requests, we have chosen to use the Dapr Java SDK. No RabbitMQ, Redis clients, or JDBC drivers were included in the application classpath. This approach of using Dapr has several advantages: The application has fewer dependencies, so it doesn’t need to include the Redis or RabbitMQ client. The application size is not only smaller but less dependent on concrete infrastructure components that are specific to the environment where the application is being deployed. Remember that these clients’ versions must match the component instance running on a given environment. With more and more Spring Boot applications deployed to cloud providers, it is pretty standard not to have control over which versions of components like databases and message brokers will be available across environments. Developers will likely run a local version of these components using containers, causing version mismatches with environments where the applications run in front of our customers. The application doesn’t create connections to Redis, RabbitMQ, or PostgreSQL. Because the configuration of connection pools and other details closely relate to the infrastructure and these components are pushed away from the application code, the application is simplified. All these concerns are now moved out of the application and consolidated behind the Dapr APIs. A new application developer doesn’t need to learn how RabbitMQ, PostgreSQL, or Redis works. The Dapr APIs are self-explanatory: if you want to save the application’s state, use the saveState() method. If you publish an event, use the publishEvent() method. Developers using an IDE can easily check which APIs are available for them to use. The teams configuring the cloud-native runtime can use their favorite tools to configure the available infrastructure. If they move from a self-managed Redis instance to a Google Cloud In-Memory Store, they can swap their Redis instance without changing the application code. If they want to swap a self-managed Kafka instance for Google PubSub or Amazon SQS/SNS, they can shift Dapr configurations. But, you ask, what about those APIs, saveState/getState and publishEvent? What about subscriptions? How do you consume an event? Can we elevate these API calls to work better with Spring Boot so developers don’t need to learn new APIs? Tomorrow, a Unified Cross-Runtime Experience In contrast with most technical articles, the answer here is not, “It depends." Of course, the answer is YES. We can follow the Spring Data and Messaging approach to provide a richer Dapr experience that integrates seamlessly with Spring Boot. This, combined with a local development experience (using Testcontainers), can help teams design and code applications that can run quickly and without changes across environments (local, Kubernetes, cloud provider). If you are already working with Redis, PostgreSQL, and/or RabbitMQ, you are most likely using Spring Boot abstractions Spring Data and Spring RabbitMQ/Kafka/Pulsar for asynchronous messaging. For Spring Data KeyValue, check the post A Guide to Spring Data Key Value for more details. To find an Employee by ID: For asynchronous messaging, we can take a look at Spring Kafka, Spring Pulsar, and Spring AMQP (RabbitMQ) (see also Messaging with RabbitMQ ), which all provide a way to produce and consume messages. Producing messages with Kafka is this simple: Consuming Kafka messages is extremely straightforward too: For RabbitMQ, we can do pretty much the same: And then to send a message: To consume a message from RabbitMQ, you can do: Elevating Dapr to the Spring Boot Developer Experience Now let’s take a look at how it would look with the new Dapr Spring Boot starters: Let’s take a look at the DaprKeyValueTemplate: Let’s now store our Vote object using the KeyValueTemplate. Let’s find all the stored votes by creating a query to the KeyValue store: Now, why does this matter? The DaprKeyValueTemplate, implements the KeyValueOperations interfaces provided by Spring Data KeyValue, which is implemented by tools like Redis, MongoDB, Memcached, PostgreSQL, and MySQL, among others. The big difference is that this implementation connects to the Dapr APIs and does not require any specific client. The same code can store data in Redis, PostgreSQL, MongoDB, and cloud provider-managed services such as AWS DynamoDB and Google Cloud Firestore. Over 30 data stores are supported in Dapr, and no changes to the application or its dependencies are needed. Similarly, let’s take a look at the DaprMessagingTemplate. Let’s publish a message/event now: To consume messages/events, we can use the annotation approach similar to the Kafka example: An important thing to notice is that out-of-the-box Dapr uses CloudEvents to exchange events (other formats are also supported), regardless of the underlying implementations. Using the @Topic annotation, our application subscribes to listen to all events happening in a specific Dapr PubSub component in a specified Topic. Once again, this code will work for all supported Dapr PubSub component implementations such as Kafka, RabbitMQ, Apache Pulsar, and cloud provider-managed services such as Azure Event Hub, Google Cloud PubSub, and AWS SNS/SQS (see Dapr Pub/sub brokers documentation). Combining the DaprKeyValueTemplate and DaprMessagingTemplate gives developers access to data manipulation and asynchronous messaging under a unified API, which doesn’t add application dependencies, and it is portable across environments, as you can run the same code against different cloud provider services. While this looks much more like Spring Boot, more work is required. On top of Spring Data KeyValue, the Spring Repository interface can be implemented to provide a CRUDRepository experience. There are also some rough edges for testing, and documentation is needed to ensure developers can get started with these APIs quickly. Advantages and Trade-Offs As with any new framework, project, or tool you add to the mix of technologies you are using, understanding trade-offs is crucial in measuring how a new tool will work specifically for you. One way that helped me understand the value of Dapr is to use the 80% vs 20% rule. Which goes as follows: 80% of the time, applications do simple operations against infrastructure components such as message brokers, key/value stores, configuration servers, etc. The application will need to store and retrieve state and emit and consume asynchronous messages just to implement application logic. For these scenarios, you can get the most value out of Dapr. 20% of the time, you need to build more advanced features that require deeper expertise on the specific message broker that you are using or to write a very performant query to compose a complex data structure. For these scenarios, it is okay not to use the Dapr APIs, as you probably require access to specific underlying infrastructure features from your application code. It is common when we look at a new tool to generalize it to fit as many use cases as we can. With Dapr, we should focus on helping developers when the Dapr APIs fit their use cases. When the Dapr APIs don’t fit or require specific APIs, using provider-specific SDKs/clients is okay. By having a clear understanding of when the Dapr APIs might be enough to build a feature, a team can design and plan in advance what skills are needed to implement a feature. For example, do you need a RabbitMQ/Kafka or an SQL and domain expert to build some advanced queries? Another mistake we should avoid is not considering the impact of tools on our delivery practices. If we can have the right tools to reduce friction between environments and if we can enable developers to create applications that can run locally using the same APIs and dependencies required when running on a cloud provider. With these points in mind let’s look at the advantage and trade-offs: Advantages Concise APIs to tackle cross-cutting concerns and access to common behavior required by distributed applications. This enables developers to delegate to Dapr concerns such as resiliency (retry and circuit breaker mechanisms), observability (using OpenTelemetry, logs, traces and metrics), and security (certificates and mTLS). With the new Spring Boot integration, developers can use the existing programming model to access functionality With the Dapr and Testcontainers integration, developers don’t need to worry about running or configuring Dapr, or learning other tools that are external to their existing inner development loops. The Dapr APIs will be available for developers to build, test, and debug their features locally. The Dapr APIs can help developers save time when interacting with infrastructure. For example, instead of pushing every developer to learn about how Kafka/Pulsar/RabbitMQ works, they just need to learn how to publish and consume events using the Dapr APIs. Dapr enables portability across cloud-native environments, allowing your application to run against local or cloud-managed infrastructure without any code changes. Dapr provides a clear separation of concerns to enable operations/platform teams to wire infrastructure across a wide range of supported components. Trade-Offs Introducing abstraction layers, such as the Dapr APIs, always comes with some trade-offs. Dapr might not be the best fit for all scenarios. For those cases, nothing stops developers from separating more complex functionality that requires specific clients/drivers into separate modules or services. Dapr will be required in the target environment where the application will run. Your applications will depend on Dapr to be present and the infrastructure needed by the application wired up correctly for your application to work. If your operation/platform team is already using Kubernetes, Dapr should be easy to adopt as it is a quite mature CNCF project with over 3,000 contributors. Troubleshooting with an extra abstraction between our application and infrastructure components can become more challenging. The quality of the Spring Boot integration can be measured by how well errors are propagated to developers when things go wrong. I know that advantages and trade-offs depend on your specific context and background, feel free to reach out if you see something missing in this list. Summary and Next Steps Covering the Dapr Statestore (KeyValue) and PubSub (Messaging) is just the first step, as adding more advanced Dapr features into the Spring Boot programming model can help developers access more functionality required to create robust distributed applications. On our TODO list, Dapr Workflows for durable executions is coming next, as providing a seamless experience to develop complex, long-running orchestration across services is a common requirement. One of the reasons why I was so eager to work on the Spring Boot and Dapr integration is that I know that the Java community has worked hard to polish their developer experiences focusing on productivity and consistent interfaces. I strongly believe that all this accumulated knowledge in the Java community can be used to take the Dapr APIs to the next level. By validating which use cases are covered by the existing APIs and finding gaps, we can build better integrations and automatically improve developers’ experiences across languages. You can find all the source code for the example we presented at Spring I/O linked in the "Today, Kubernetes, and Cloud-Native Runtimes" section of this article. We expect to merge the Spring Boot and Dapr integration code to the Dapr Java SDK to make this experience the default Dapr experience when working with Spring Boot. Documentation will come next. If you want to contribute or collaborate with these projects and help us make Dapr even more integrated with Spring Boot, please contact us.
In this article, I am going to show you how to build Message Publisher using Apache Kafka and Spring Boot. First, we will talk about what Apache Kafka is. Apache Kafka is an open-source, distributed streaming platform designed for real-time event processing. It provides a reliable, scalable, and fault-tolerant way to handle large volumes of data streams. Kafka allows you to publish and subscribe to data topics, making it ideal for building event-driven applications, log aggregation, and data pipelines. Prerequisites Apache Kafka Java Apache Maven Any IDE (Intellij or STS or Eclipse) Project Structure In this project, we will expose an endpoint to create a user and we will publish UserCreatedEvent to Kafka Topic. application.yml file YAML spring: application: name: message-publisher kafka: producer: bootstrap-servers: localhost:9092 key-serializer: org.apache.kafka.common.serialization.StringSerializer value-serializer: org.springframework.kafka.support.serializer.JsonSerializer app: topic_name: users-topic server: port: 8089 spring.application.name is used to define the application name. bootstrap-servers specifies the hostname and port number of Kafka. Serializer specifies which serializer needs to be used to convert Java object to bytes before sending it to Kafka. Based on key type we can use StringSerializer or IntegerSerializer. (Example: org.apache.kafka.common.serialization.StringSerializer) key-serializer is used in a scenario when the same keys should go to the same partition. value-serializer specifies which serializer needs to be used to convert Java objects to bytes before sending Kafka. If we are using a custom java class as value, then we can use JSONSerializer as value-serializer. pom.xml XML <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.3.0</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.lights5.com</groupId> <artifactId>message-publisher</artifactId> <version>0.0.1-SNAPSHOT</version> <name>message-publisher</name> <description>Demo project for Kafka Producer using Spring Boot</description> <properties> <java.version>17</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.kafka</groupId> <artifactId>spring-kafka</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <excludes> <exclude> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </exclude> </excludes> </configuration> </plugin> </plugins> </build> </project> spring web, spring kafka are required dependencies. ApplicationConfiguration class Java package com.lights5.com.message.publisher; import lombok.Getter; import lombok.Setter; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Configuration; @Getter @Setter @Configuration @ConfigurationProperties(prefix = "app") public class AppConfig { private String topicName; } This class is used to bind configuration values from application.yml file to the respective fields. Application class Java package com.lights5.com.message.publisher; import lombok.RequiredArgsConstructor; import org.apache.kafka.clients.admin.NewTopic; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import org.springframework.kafka.config.TopicBuilder; @SpringBootApplication @RequiredArgsConstructor public class Application { private final AppConfig appConfig; public static void main(String[] args) { SpringApplication.run(Application.class, args); } @Bean NewTopic usersTopic() { return TopicBuilder.name(appConfig.getTopicName()) .partitions(3) .replicas(2) .build(); } } NewTopic Bean is used to create a topic if the topic doesn’t exist already on the Kafka broker. We can configure the required number of partitions and replicas as we need. Model Classes User class Java package com.lights5.com.message.publisher; import java.time.LocalDateTime; record User ( String firstName, String lastName, String email, Long phoneNumber, Address address, LocalDateTime createdAt) { record Address ( String city, String country, String zipcode) { } } EventType enum Java package com.lights5.com.message.publisher; enum EventType { USER_CREATED_EVENT; } EventPayload class Java package com.lights5.com.message.publisher; record EventPayload ( EventType eventType, String payload) { } Endpoint to Create User (UserController class) Java package com.lights5.com.message.publisher; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.*; import static com.lights5.com.message.publisher.EventType.USER_CREATED_EVENT; @RestController @RequiredArgsConstructor @RequestMapping("/v1/users") class UsersController { private final UsersService usersService; @PostMapping @ResponseStatus(HttpStatus.CREATED) public void createUser(@RequestBody User user) { usersService.publishMessage(user, USER_CREATED_EVENT); } } UsersController class exposes the POST method to create a user, which in turn calls a method in the UsersService class. UsersService class Java package com.lights5.com.message.publisher; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.kafka.common.serialization.StringSerializer; import org.springframework.kafka.core.KafkaTemplate; import org.springframework.stereotype.Service; @Slf4j @Service @RequiredArgsConstructor class UsersService { private final AppConfig appConfig; private final ObjectMapper objectMapper; private final KafkaTemplate<String, EventPayload> kafkaTemplate; public void publishMessage(User user, EventType eventType) { try { var userCreatedEventPayload = objectMapper.writeValueAsString(user); var eventPayload = new EventPayload(eventType, userCreatedEventPayload); kafkaTemplate.send(appConfig.getTopicName(), eventPayload); } catch (JsonProcessingException ex) { log.error("Exception occurred in processing JSON {}", ex.getMessage()); } } } KafkaTemplate is used to send messages to Kafka. Spring Boot autoconfigures KafkaTemplate and injects to the required class. KafkaTemplate<K, V> is of this form. Here K is the key type and V is the value type. In our case key is String type and V is EventPayload class type. So we need to use StringSerializer for the key and JsonSerializer (EventPayload is the custom Java class type) for values. kafkaTemplate.send() method takes topicName as 1st parameter and data to be published as 2nd argument. Running Kafka in Local To run this application locally, first, we need to run Kafka locally and then start the Spring Boot application. Please use this docker-compose file to run Kafka locally. YAML version: '2.1' services: zoo1: image: confluentinc/cp-zookeeper:7.3.2 hostname: zoo1 container_name: zoo1 ports: - "2181:2181" environment: ZOOKEEPER_CLIENT_PORT: 2181 ZOOKEEPER_SERVER_ID: 1 ZOOKEEPER_SERVERS: zoo1:2888:3888 kafka1: image: confluentinc/cp-kafka:7.3.2 hostname: kafka1 container_name: kafka1 ports: - "9092:9092" - "29092:29092" environment: KAFKA_ADVERTISED_LISTENERS: INTERNAL://kafka1:19092,EXTERNAL://${DOCKER_HOST_IP:-127.0.0.1}:9092,DOCKER://host.docker.internal:29092 KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: INTERNAL:PLAINTEXT,EXTERNAL:PLAINTEXT,DOCKER:PLAINTEXT KAFKA_INTER_BROKER_LISTENER_NAME: INTERNAL KAFKA_ZOOKEEPER_CONNECT: "zoo1:2181" KAFKA_BROKER_ID: 5 KAFKA_LOG4J_LOGGERS: "kafka.controller=INFO,kafka.producer.async.DefaultEventHandler=INFO,state.change.logger=INFO" depends_on: - zoo1 kafka2: image: confluentinc/cp-kafka:7.3.2 hostname: kafka2 container_name: kafka2 ports: - "9093:9093" - "29093:29093" environment: KAFKA_ADVERTISED_LISTENERS: INTERNAL://kafka2:19093,EXTERNAL://${DOCKER_HOST_IP:-127.0.0.1}:9093,DOCKER://host.docker.internal:29093 KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: INTERNAL:PLAINTEXT,EXTERNAL:PLAINTEXT,DOCKER:PLAINTEXT KAFKA_INTER_BROKER_LISTENER_NAME: INTERNAL KAFKA_ZOOKEEPER_CONNECT: "zoo1:2181" KAFKA_BROKER_ID: 6 KAFKA_LOG4J_LOGGERS: "kafka.controller=INFO,kafka.producer.async.DefaultEventHandler=INFO,state.change.logger=INFO" depends_on: - zoo1 kafka3: image: confluentinc/cp-kafka:7.3.2 hostname: kafka3 container_name: kafka3 ports: - "9094:9094" - "29094:29094" environment: KAFKA_ADVERTISED_LISTENERS: INTERNAL://kafka3:19094,EXTERNAL://${DOCKER_HOST_IP:-127.0.0.1}:9094,DOCKER://host.docker.internal:29094 KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: INTERNAL:PLAINTEXT,EXTERNAL:PLAINTEXT,DOCKER:PLAINTEXT KAFKA_INTER_BROKER_LISTENER_NAME: INTERNAL KAFKA_ZOOKEEPER_CONNECT: "zoo1:2181" KAFKA_BROKER_ID: 7 KAFKA_LOG4J_LOGGERS: "kafka.controller=INFO,kafka.producer.async.DefaultEventHandler=INFO,state.change.logger=INFO" depends_on: - zoo1 docker-compose -f up . Run this command in the directory where the compose file is located. The above command starts the Kafka locally. Testing Using Postman Endpoint: (POST method) Payload JSON { "firstName": "John", "lastName": "Albert", "email": "[email protected]", "phoneNumber": "9999999999", "address": { "city": "NewYork", "country": "USA", "zipcode": "111111" }, "createdAt": "2024-06-06T16:46:00" } You can verify using kafka-console-consumer command whether the data is published or not. Source Code. Conclusion Spring Boot provides easy integration with Kafka and helps us create pub sub-model applications easily with minimal configurations. We can develop Microservices event-driven applications easily with Spring Boot and Kafka.
In this tutorial, we’ll review an example application that is written in the Groovy programming language and demonstrate how to use the H2 relational database (H2 DB/H2) with Spring Boot. The benefit of using the Groovy Programming Language, in this case, is that it allows an example to ship exactly one file which contains everything we need to run the application. The H2 Database Engine is a powerful open-source relational database that is written in the Java programming language and is used with some frequency in software development projects — especially when it comes to testing applications. Using H2 with the Spring Framework and with Spring Boot, in particular, is the use case that we’ll demonstrate here. In the next section, we’ll take a look at the example and dissect, in detail, what happens in each step. H2 With Spring Boot Example on GitHub Included here is a link to the GitHub gist pertaining to the example used to demonstrate connecting the H2 Relational Database with Spring Boot. I’ve also added the full example later in this article, which you should be able to paste into the groovyConsole and run as-is. Maven Dependencies The following dependencies are used in this example: Spring Boot Spring Boot AutoConfigure Spring JDBC H2 Database Engine Javax Annotation API SLF4J Simple Provider The Groovy Grape dependency management system should find these dependencies automatically when the script is executed however for reference purposes I’ve included these here. An Example Pertaining To Using the H2 Database Engine With Spring Boot In this section, we’ll take a look at the script in closer detail and go over what’s happening in each step. Prerequisites In order to run this example, you will need the following: Java version 22.0.1 (required) — see OpenJDK on java.net and Oracle Java SE Development Kit 22.0.1 (JDK 22.0.1) Groovy 4.0.17 (required) groovyConsole (optional) This script can be executed using Groovy alone; hence, the groovyConsole is optional. The script uses the Groovy Adaptable Packaging Engine (Groovy Grape) to pull in dependencies from Maven Central hence a connection to the Internet is required as well. I’ve included an example of what the output should look like when running this script from the command line here. H2 DB with Spring Boot Successful CRUD Example Output The red arrow points to the command used to run the script, and orange arrow points to the log statement that indicates the script is starting, and the blue arrow points to the log statement that indicates that the script has finished running. In this example, the script runs a Spring Boot application that creates a table in the H2 DB, executes several CRUD (create, read, update, delete) operations on that table, and then drops the table. The Groovy script runs to completion successfully and then exits. Step 1: Declare a Package When we define the Spring Boot Application, we’ll include the scanBasePackages setting, which requires a package name so we set that here. Step 2: Add the Groovy Grape GrabConfig Annotation In step two we need to add the Groovy Grape GrabConfig annotation and also set the systemClassLoader property to true — if we do not have this an exception will be thrown when the script is executed. Step 3: Grab Dependencies and Import Required Classes In step three we need to grab the dependencies necessary to run this example as well as import required classes — this includes the Spring Boot, H2 Database, and other supporting classes. Note that we’re using the Hikari database driver in this example. See the section in this article for complete details. Step 4: Obtain a Reference to an SLF4J Logger We’re using the SLF4J log delegation framework in this example and we’ll send messages to console output so we can watch what’s happening as the script executes. The HikariCP dependency is one other framework that we’re using that also uses SLF4J and we’ve included this high-performance connection pooling implementation in this example. Step 5: Configure H2 Database Datasource and JdbcTemplate Beans In the fifth step, we’ll configure the H2 Database datasource which utilizes the HikariCP high-performance connection pool dependency as the datasource type. Since this example demonstrates some simple CRUD operations executed against the H2 Database from a Spring Boot application, we’ll also configure an instance of JdbcTemplate here which uses this data source. Note that we’re assigning the HikariDataSource class as the datasource type. The H2 DB instance configured in this example will reside in memory only — if we want to persist this information to disk, then we need to change the URL. Step 6: Create a Repository Class In this step, we implement a repository that contains the CRUD operations that we can execute on the H2 Database instance via the JdbcTemplate, which is auto-wired in this example by Spring Boot. Step 7: Implement a Service Bean In this step, we implement a transactional service bean that has stop-and-start lifecycle methods along with convenience methods that delegate to the repository bean. The start method creates the example table in H2 when Spring Boot initializes the beans that the container is managing, and the stop method drops the example table before the container stops. Other methods defined in the ExampleService deliver convenience and hide implementation details. Using a service aids in reuse and is also helpful when testing our code. As the image has been truncated, refer to the full example below or see the GitHub Gist. Step 8: Implement the Spring Boot CommandLineRunner Interface In this step, we implement the Spring Boot CommandLineRunner specification. Our implementation includes executing CRUD operations via the service created in step seven against the H2 Database. We log some information along the way so we can see what happens as each CRUD operation completes. Step 9: Configure Spring Boot Application for Component Scanning The code in the snippet defines a Spring Boot application and specifies the base package for component scanning. Step Ten: Configure and Then Run the Spring Boot Application The code in this snippet configures and then runs the Spring Boot application with the following configuration: Initialize SpringApplicationBuilder: Creates a builder for the Spring Boot application using H2SpringBootExampleApplication Set profiles and web application type: Configures the application to use the default profile and disables the web environment (this is not a web application so we don’t need this) Set parent context: Specifies the BeanConfiguration, ExampleRepository, ExampleService, and ExampleCommandLineRunner classes as components in the parent context Run the application: Execute the application with the provided arguments Close the context: Closes the application context — this step ensures that the stop lifecycle method in the service (see step six) is called before the Spring Boot example application has exited resulting in the names table in the H2 DB being dropped. Finally, the script logs a completion message and then exits. The next section includes the complete Spring Boot with the H2 Database example script. Spring Boot With The H2 Database Engine Complete Example Here, I’ve included a copy of the entire script, which you should be able to run either using the groovyConsole or via the command line with the groovy shell. /* * Precondition: * * - Java version "22.0.1" 2024-04-16 * - Groovy version 4.0.17 */ package com.thospfuller.examples.h2.database.spring.boot @GrabConfig(systemClassLoader=true) @Grab(group='org.springframework.boot', module='spring-boot', version='3.3.0') @Grab(group='org.springframework.boot', module='spring-boot-autoconfigure', version='3.3.0') import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.builder.SpringApplicationBuilder import org.springframework.context.annotation.Configuration import org.springframework.beans.factory.annotation.Autowired import org.springframework.stereotype.Repository import org.springframework.stereotype.Component import org.springframework.boot.jdbc.DataSourceBuilder @Grab(group='org.springframework', module='spring-jdbc', version='6.1.8') import org.springframework.jdbc.core.JdbcTemplate import org.springframework.jdbc.core.ParameterizedPreparedStatementSetter import org.springframework.transaction.annotation.Transactional import org.springframework.context.annotation.Bean import org.springframework.boot.CommandLineRunner import org.springframework.boot.WebApplicationType import org.springframework.stereotype.Service @Grab(group='com.h2database', module='h2', version='2.2.224') @Grab(group='com.zaxxer', module='HikariCP', version='5.1.0') import com.zaxxer.hikari.HikariDataSource import javax.sql.DataSource import java.sql.PreparedStatement @Grab(group='javax.annotation', module='javax.annotation-api', version='1.3.2') import javax.annotation.PostConstruct import javax.annotation.PreDestroy @Grab(group='org.slf4j', module='slf4j-simple', version='2.0.9') import org.slf4j.LoggerFactory def log = LoggerFactory.getLogger(this.class) log.info "H2 Database with Spring Boot example begins; args: $args" @Configuration class BeanConfiguration { @Bean DataSource getDataSource () { return DataSourceBuilder .create() .driverClassName("org.h2.Driver") .type(HikariDataSource) .url("jdbc:h2:mem:example-db;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE") .username("sa") .password("sa") .build() } @Bean JdbcTemplate getJdbcTemplate (DataSource dataSource) { return new JdbcTemplate (dataSource) } } @Repository class ExampleRepository { private static final def log = LoggerFactory.getLogger(ExampleRepository) static final def TABLE_NAME = "NAMES" @Autowired private JdbcTemplate jdbcTemplate void createExampleTable () { log.info "createExampleTable: method begins." jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS ${TABLE_NAME} (id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, name VARCHAR(255));") log.info "createExampleTable: method ends." } def deleteExampleTable () { log.info "deleteExampleTable: method begins." jdbcTemplate.execute("DROP TABLE IF EXISTS ${TABLE_NAME};") log.info "deleteExampleTable: method ends." } def addNames (String... names) { return addNames (names as List<String>) } def addNames (List<String> nameList) { return jdbcTemplate.batchUpdate( "INSERT INTO ${TABLE_NAME} (name) VALUES (?);", nameList, nameList.size (), { PreparedStatement preparedStatement, String name -> preparedStatement.setString(1, name) } as ParameterizedPreparedStatementSetter<String> ) } def updateName (int id, String newName) { return jdbcTemplate.update( "UPDATE ${TABLE_NAME} SET NAME = ? WHERE ID = ?;", newName, id ) } def deleteName (int id) { return jdbcTemplate.update( "DELETE FROM ${TABLE_NAME} WHERE ID = ?;", id ) } def readNames () { return jdbcTemplate.queryForList ("select name from ${TABLE_NAME}") } } @Service @Transactional class ExampleService { private static final def log = LoggerFactory.getLogger(ExampleService) @Autowired def exampleRepository @PostConstruct def start () { log.info "start: method begins." exampleRepository.createExampleTable () log.info "start: method ends." } @PreDestroy def stop () { log.info "stop: method begins." exampleRepository.deleteExampleTable () log.info "stop: method ends." } def addNames (String... nameList) { return exampleRepository.addNames (nameList) } def updateName (int id, String newName) { return exampleRepository.updateName (id, newName) } def deleteName (int id) { return exampleRepository.deleteName (id) } def readNames () { return exampleRepository.readNames () } } @Component class ExampleCommandLineRunner implements CommandLineRunner { private static final def log = LoggerFactory.getLogger(H2SpringBootExampleApplication) @Autowired private def exampleService @Override public void run (String... args) { log.info "run: method begins; args: $args" def namesAdded = exampleService.addNames ("aaa", "bbb", "ccc", "ddd") log.info "namesAdded: $namesAdded" def updateResult = exampleService.updateName (2, "ZZZ") def names = exampleService.readNames () log.info "updateResult: $updateResult, names after update: $names" def deletedNames = exampleService.deleteName (2) names = exampleService.readNames () log.info "deletedNames: $deletedNames, names after deletion: $names" log.info "run: method ends." } } @SpringBootApplication(scanBasePackages = ["com.thospfuller.examples.h2.database.spring.boot"]) class H2SpringBootExampleApplication {} def springApplicationBuilder = new SpringApplicationBuilder(H2SpringBootExampleApplication) def context = springApplicationBuilder .profiles("default") .web(WebApplicationType.NONE) .parent ( BeanConfiguration, ExampleRepository, ExampleService, ExampleCommandLineRunner ) .run(args) context.close () log.info "...done!" return Conclusion I hope that this tutorial has provided adequate guidance as well as a useful example regarding how to use the H2 Database with Spring Boot. If you encounter any problems or have questions about this tutorial, please leave a comment and I’ll try to help.
With Spring Boot 3.2 and Spring Framework 6.1, we get support for Coordinated Restore at Checkpoint (CRaC), a mechanism that enables Java applications to start up faster. With Spring Boot, we can use CRaC in a simplified way, known as Automatic Checkpoint/Restore at startup. Even though not as powerful as the standard way of using CRaC, this blog post will show an example where the Spring Boot applications startup time is decreased by 90%. The sample applications are from chapter 6 in my book on building microservices with Spring Boot. Overview The blog post is divided into the following sections: Introducing CRaC, benefits, and challenges Creating CRaC-based Docker images with a Dockerfile Trying out CRaC with automatic checkpoint/restore Summary Next blog post Let’s start learning about CRaC and its benefits and challenges. 1. Introducing CRaC, Benefits, and Challenges Coordinated Restore at Checkpoint (CRaC) is a feature in OpenJDK, initially developed by Azul, to enhance the startup performance of Java applications by allowing them to restore to a previously saved state quickly. CRaC enables Java applications to save their state at a specific point in time (checkpoint) and then restore from that state at a later time. This is particularly useful for scenarios where fast startup times are crucial, such as serverless environments, microservices, and, in general, applications that must be able to scale up their instances quickly and also support scale-to-zero when not being used. This introduction will first explain a bit about how CRaC works, then discuss some of the challenges and considerations associated with it, and finally, describe how Spring Boot 3.2 integrates with it. The introduction is divided into the following subsections: 1.1. How CRaC Works 1.2. Challenges and Considerations 1.3. Spring Boot 3.2 integration with CRaC 1.1. How CRaC Works Checkpoint Creation At a chosen point during the application’s execution, a checkpoint is created. This involves capturing the entire state of the Java application, including the heap, stack, and all active threads. The state is then serialized and saved to the file system. During the checkpoint process, the application is typically paused to ensure a consistent state is captured. This pause is coordinated to minimize disruption and ensure the application can resume correctly. Before taking the checkpoint, some requests are usually sent to the application to ensure that it is warmed up, i.e., all relevant classes are loaded, and the JVM HotSpot engine has had a chance to optimize the bytecode according to how it is being used in runtime. Commands to perform a checkpoint: Shell java -XX:CRaCCheckpointTo=<some-folder> -jar my_app.jar # Make calls to the app to warm up the JVM... jcmd my_app.jar JDK.checkpoint State Restoration When the application is started from the checkpoint, the previously saved state is deserialized from the file system and loaded back into memory. The application then continues execution from the exact point where the checkpoint was taken, bypassing the usual startup sequence. Command to restore from a checkpoint: Shell java -XX:CRaCRestoreFrom=<some-folder> Restoring from a checkpoint allows applications to skip the initial startup process, including class loading, warmup initialization, and other startup routines, significantly reducing startup times. For more information, see Azul’s documentation: What is CRaC? 1.2. Challenges and Considerations As with any new technology, CRaC comes with a new set of challenges and considerations: State Management Open files and connections to external resources, such as databases, must be closed before the checkpoint is taken. After the restore, they must be reopened. CRaC exposes a Java lifecycle interface that applications can use to handle this, org.crac.Resource, with the callback methods beforeCheckpoint and afterRestore. Sensitive Information Credentials and secrets stored in the JVM’s memory will be serialized into the files created by the checkpoint. Therefore, these files need to be protected. An alternative is to run the checkpoint command against a temporary environment that uses other credentials and replace the credentials on restore. Linux Dependency The checkpoint technique is based on a Linux feature called CRIU, “Checkpoint/Restore In Userspace”. This feature only works on Linux, so the easiest way to test CRaC on a Mac or a Windows PC is to package the application into a Linux Docker image. Linux Privileges Required CRIU requires special Linux privileges, resulting in Docker commands to build Docker images and creating Docker containers also requiring Linux privileges to be able to run. Storage Overhead Storing and managing checkpoint data requires additional storage resources, and the checkpoint size can impact the restoration time. The original jar file is also required to be able to restart a Java application from a checkpoint. I will describe how to handle these challenges in the section on creating Docker images. 1.3. Spring Boot 3.2 Integration With CRaC Spring Boot 3.2 (and the underlying Spring Framework) helps with the processing of closing and reopening connections to external resources. Before the creation of the checkpoint, Spring stops all running beans, giving them a chance to close resources if needed. After a restore, the same beans are restarted, allowing beans to reopen connections to the resources. The only thing that needs to be added to a Spring Boot 3.2-based application is a dependency to the crac-library. Using Gradle, it looks like the following in the gradle.build file: Groovy dependencies { implementation 'org.crac:crac' Note: The normal Spring Boot BOM mechanism takes care of versioning the crac dependency. The automatic closing and reopening of connections handled by Spring Boot usually works. Unfortunately, when this blog post was written, some Spring modules lacked this support. To track the state of CRaC support in the Spring ecosystem, a dedicated test project, Spring Lifecycle Smoke Tests, has been created. The current state can be found on the project’s status page. If required, an application can register callback methods to be called before a checkpoint and after a restore by implementing the above-mentioned Resource interface. The microservices used in this blog post have been extended to register callback methods to demonstrate how they can be used. The code looks like this: Java import org.crac.*; public class MyApplication implements Resource { public MyApplication() { Core.getGlobalContext().register(this); } @Override public void beforeCheckpoint(Context<? extends Resource> context) { LOG.info("CRaC's beforeCheckpoint callback method called..."); } @Override public void afterRestore(Context<? extends Resource> context) { LOG.info("CRaC's afterRestore callback method called..."); } } Spring Boot 3.2 provides a simplified alternative to take a checkpoint compared to the default on-demand alternative described above. It is called automatic checkpoint/restore at startup. It is triggered by adding the JVM system property -Dspring.context.checkpoint=onRefresh to the java -jar command. When set, a checkpoint is created automatically when the application is started. The checkpoint is created after Spring beans have been created but not started, i.e., after most of the initialization work but before that application starts. For details, see Spring Boot docs and Spring Framework docs. With an automatic checkpoint, we don’t get a fully warmed-up application, and the runtime configuration must be specified at build time. This means that the resulting Docker images will be runtime-specific and contain sensitive information from the configuration, like credentials and secrets. Therefore, the Docker images must be stored in a private and protected container registry. Note: If this doesn’t meet your requirements, you can opt for the on-demand checkpoint, which I will describe in the next blog post. With CRaC and Spring Boot 3.2’s support for CRaC covered, let’s see how we can create Docker images for Spring Boot applications that use CRaC. 2. Creating CRaC-Based Docker Images With a Dockerfile While learning how to use CRaC, I studied several blog posts on using CRaC with Spring Boot 3.2 applications. They all use rather complex bash scripts (depending on your bash experience) using Docker commands like docker run, docker exec, and docker commit. Even though they work, it seems like an unnecessarily complex solution compared to producing a Docker image using a Dockerfile. So, I decided to develop a Dockerfile that runs the checkpoint command as a RUN command in the Dockerfile. It turned out to have its own challenges, as described below. I will begin by describing my initial attempt and then explain the problems I stumbled into and how I solved them, one by one until I reach a fully working solution. The walkthrough is divided into the following subsections: 2.1. First attempt 2.2. Problem #1, privileged builds with docker build 2.3. Problem #2, CRaC returns exit status 137, instead of 0 2.4. Problem #3, Runtime configuration 2.5. Problem #4, Spring Data JPA 2.6. The resulting Dockerfile Let’s start with a first attempt and see where it leads us. 2.1. First Attempt My initial assumption was to create a Dockerfile based on a multi-stage build, where the first stage creates the checkpoint using a JDK-based base image, and the second step uses a JRE-based base image for runtime. However, while writing this blog post, I failed to find a base image for a Java 21 JRE supporting CRaC. So I changed my mind to use a regular Dockerfile instead, using a base image from Azul: azul/zulu-openjdk:21.0.3-21.34-jdk-crac Note: BellSoft also provides base images for CraC; see Liberica JDK with CRaC Support as an alternative to Azul. The first version of the Dockerfile looks like this: Dockerfile FROM azul/zulu-openjdk:21.0.3-21.34-jdk-crac ADD build/libs/*.jar app.jar RUN java -Dspring.context.checkpoint=onRefresh -XX:CRaCCheckpointTo=checkpoint -jar app.jar EXPOSE 8080 ENTRYPOINT ["java", "-XX:CRaCRestoreFrom=checkpoint"] This Dockerfile is unfortunately not possible to use since CRaC requires a build to run privileged commands. 2.2. Problem #1, Privileged Builds With Docker Build As mentioned in section 1.2. Challenges and Considerations, CRIU, which CRaC is based on, requires special Linux privileges to perform a checkpoint. The standard docker build command doesn’t allow privileged builds, so it can’t be used to build Docker images using the above Dockerfile. Note: The --privileged - flag that can be used in docker run commands is not supported by docker build. Fortunately, Docker provides an improved builder backend called BuildKit. Using BuildKit, we can create a custom builder that is insecure, meaning it allows a Dockerfile to run privileged commands. To communicate with BuildKit, we can use Docker’s CLI tool buildx. The following command can be used to create an insecure builder named insecure-builder: Shell docker buildx create --name insecure-builder --buildkitd-flags '--allow-insecure-entitlement security.insecure' Note: The builder runs in isolation within a Docker container created by the docker buildx create command. You can run a docker ps command to reveal the container. When the builder is no longer required, it can be removed with the command: docker buildx rm insecure-builder. The insecure builder can be used to build a Docker image with a command like: Shell docker buildx --builder insecure-builder build --allow security.insecure --load . Note: The --load flag loads the built image into the regular local Docker image cache. Since the builder runs in an isolated container, its result will not end up in the regular local Docker image cache by default. RUN commands in a Dockerfile that requires privileges must be suffixed with --security=insecure. The --security-flag is only in preview and must therefore be enabled in the Dockerfile by adding the following line as the first line in the Dockerfile: Dockerfile # syntax=docker/dockerfile:1.3-labs For more details on BuildKit and docker buildx, see Docker Build architecture. We can now perform the build; however, the way the CRaC is implemented stops the build, as we will learn in the next section. 2.3. Problem #2, CRaC Returns Exit Status 137 Instead of 0 On a successful checkpoint, the java -Dspring.context.checkpoint=onRefresh -XX:CRaCCheckpointTo... command is terminated forcefully (like using kill -9) and returns the exit status 137 instead of 0, causing the Docker build command to fail. To prevent the build from stopping, the java command is extended with a test that verifies that 137 is returned and, if so, returns 0 instead. The following is added to the java command: || if [ $? -eq 137 ]; then return 0; else return 1; fi. Note: || means that the command following will be executed if the first command fails. With CRaC working in a Dockerfile, let’s move on and learn about the challenges with runtime configuration and how to handle them. 2.4. Problem #3, Runtime Configuration Using Spring Boot’s automatic checkpoint/restore at startup, there is no way to specify runtime configuration on restore; at least, I haven’t found a way to do it. This means that the runtime configuration has to be specified at build time. Sensitive information from the runtime configuration, such as credentials used for connecting to a database, will written to the checkpoint files. Since the Docker images will contain these checkpoint files they also need to be handled in a secure way. The Spring Framework documentation contains a warning about this, copied from the section Automatic checkpoint/restore at startup: As mentioned above, and especially in use cases where the CRaC files are shipped as part of a deployable artifact (a container image, for example), operate with the assumption that any sensitive data “seen” by the JVM ends up in the CRaC files, and assess carefully the related security implications. So, let’s assume that we can protect the Docker images, for example, in a private registry with proper authorization in place and that we can specify the runtime configuration at build time. In Chapter 6 of the book, the source code specifies the runtime configuration in the configuration files, application.yml, in a Spring profile named docker. The RUN command, which performs the checkpoint, has been extended to include an environment variable that declares what Spring profile to use: SPRING_PROFILES_ACTIVE=docker. Note: If you have the runtime configuration in a separate file, you can add the file to the Docker image and point it out using an environment variable like SPRING_CONFIG_LOCATION=file:runtime-configuration.yml. With the challenges of proper runtime configuration covered, we have only one problem left to handle: Spring Data JPA’s lack of support for CRaC without some extra work. 2.5. Problem #4, Spring Data JPA Spring Data JPA does not work out-of-the-box with CRaC, as documented in the Smoke Tests project; see the section about Prevent early database interaction. This means that auto-creation of database tables when starting up the application, is not possible when using CRaC. Instead, the creation has to be performed outside of the application startup process. Note: This restriction does not apply to embedded SQL databases. For example, the Spring PetClinic application works with CRaC without any modifications since it uses an embedded SQL database by default. To address these deficiencies, the following changes have been made in the source code of Chapter 6: Manual creation of a SQL DDL script, create-tables.sql Since we can no longer rely on the application to create the required database tables, a SQL DDL script has been created. To enable the application to create the script file, a Spring profile create-ddl-script has been added in the review microservice’s configuration file, microservices/review-service/src/main/resources/application.yml. It looks like: YAML spring.config.activate.on-profile: create-ddl-script spring.jpa.properties.jakarta.persistence.schema-generation: create-source: metadata scripts: action: create create-target: crac/sql-scripts/create-tables.sql The SQL DDL file has been created by starting the MySQL database and, next, the application with the new Spring profile. Once connected to the database, the application and database are shut down. Sample commands: Shell docker compose up -d mysql SPRING_PROFILES_ACTIVE=create-ddl-script java -jar microservices/review-service/build/libs/review-service-1.0.0-SNAPSHOT.jar # CTRL/C once "Connected to MySQL: jdbc:mysql://localhost/review-db" is written to the log output docker compose down The resulting SQL DDL script, crac/sql-scripts/create-tables.sql, has been added to Chapter 6’s source code. The Docker Compose file configures MySQL to execute the SQL DDL script at startup. A CraC-specific version of the Docker Compose file has been created, crac/docker-compose-crac.yml. To create the tables when the database is starting up, the SQL DDL script is used as an init script. The SQL DDL script is mapped into the init-folder /docker-entrypoint-initdb.d with the following volume-mapping in the Docker Compose file: Dockerfile volumes: - "./sql-scripts/create-tables.sql:/docker-entrypoint-initdb.d/create-tables.sql" Added a runtime-specific Spring profile in the review microservice’s configuration file. The guidelines in the Smoke Tests project’s JPA section have been followed by adding an extra Spring profile named crac. It looks like the following in the review microservice’s configuration file: YAML spring.config.activate.on-profile: crac spring.jpa.database-platform: org.hibernate.dialect.MySQLDialect spring.jpa.properties.hibernate.temp.use_jdbc_metadata_defaults: false spring.jpa.hibernate.ddl-auto: none spring.sql.init.mode: never spring.datasource.hikari.allow-pool-suspension: true Finally, the Spring profile crac is added to the RUN command in the Dockerfile to activate the configuration when the checkpoint is performed. 2.6. The Resulting Dockerfile Finally, we are done with handling the problems resulting from using a Dockerfile to build a Spring Boot application that can restore quickly using CRaC in a Docker image. The resulting Dockerfile, crac/Dockerfile-crac-automatic, looks like: Dockerfile # syntax=docker/dockerfile:1.3-labs FROM azul/zulu-openjdk:21.0.3-21.34-jdk-crac ADD build/libs/*.jar app.jar RUN --security=insecure \ SPRING_PROFILES_ACTIVE=docker,crac \ java -Dspring.context.checkpoint=onRefresh \ -XX:CRaCCheckpointTo=checkpoint -jar app.jar \ || if [ $? -eq 137 ]; then return 0; else return 1; fi EXPOSE 8080 ENTRYPOINT ["java", "-XX:CRaCRestoreFrom=checkpoint"] Note: One and the same Dockerfile is used by all microservices to create CRaC versions of their Docker images. We are now ready to try it out! 3. Trying Out CRaC With Automatic Checkpoint/Restore To try out CRaC, we will use the microservice system landscape used in Chapter 6 of my book. If you are not familiar with the system landscape, it looks like the following: Chapter 6 uses Docker Compose to manage (build, start, and stop) the system landscape. Note: If you don’t have all the tools used in this blog post installed in your environment, you can look into Chapters 21 and 22 for installation instructions. To try out CRaC, we need to get the source code from GitHub, compile it, and create the Docker images for each microservice using a custom insecure Docker builder. Next, we can use Docker Compose to start up the system landscape and run the end-to-end validation script that comes with the book to ensure that everything works as expected. We will wrap up the try-out section by comparing the startup times of the microservices when they start with and without using CRaC. We will go through each step in the following subsections: 3.1. Getting the source code 3.2. Building the CRaC-based Docker images 3.3. Running end-to-end tests 3.4. Comparing startup times without CRaC 3.1. Getting the Source Code Run the following commands to get the source code from GitHub, jump into the Chapter06 folder, check out the branch SB3.2-crac-automatic, and ensure that a Java 21 JDK is used (Eclipse Temurin is used here): Shell git clone https://github.com/PacktPublishing/Microservices-with-Spring-Boot-and-Spring-Cloud-Third-Edition.git cd Microservices-with-Spring-Boot-and-Spring-Cloud-Third-Edition/Chapter06 git checkout SB3.2-crac-automatic sdk use java 21.0.3-tem 3.2. Building the CRaC-Based Docker Images Start with compiling the microservices source code: Shell ./gradlew build If not already created, create the insecure builder with the command: Shell docker buildx create --name insecure-builder --buildkitd-flags '--allow-insecure-entitlement security.insecure' Now we can build a Docker image, where the build performs a CRaC checkpoint for each of the microservices with the commands: Shell docker buildx --builder insecure-builder build --allow security.insecure -f crac/Dockerfile-crac-automatic -t product-composite-crac --load microservices/product-composite-service docker buildx --builder insecure-builder build --allow security.insecure -f crac/Dockerfile-crac-automatic -t product-crac --load microservices/product-service docker buildx --builder insecure-builder build --allow security.insecure -f crac/Dockerfile-crac-automatic -t recommendation-crac --load microservices/recommendation-service docker buildx --builder insecure-builder build --allow security.insecure -f crac/Dockerfile-crac-automatic -t review-crac --load microservices/review-service 3.3. Running End-To-End Tests To start up the system landscape, we will use Docker Compose. Since CRaC requires special Linux privileges, a CRaC-specific docker-compose file comes with the source code, crac/docker-compose-crac.yml. Each microservice is given the required privilege, CHECKPOINT_RESTORE, by specifying: YAML cap_add: - CHECKPOINT_RESTORE Note: Several blog posts on CRaC suggest using privileged containers, i.e., starting them with run --privleged or adding privileged: true in the Docker Compose file. This is a really bad idea since an attacker who gets control over such a container can easily take control of the host that runs Docker. For more information, see Docker’s documentation on Runtime privilege and Linux capabilities. The final addition to the CRaC-specific Docker Compose file is the volume mapping for MySQL to add the init file described above in section 2.5. Problem #4, Spring Data JPA: Dockerfile volumes: - "./sql-scripts/create-tables.sql:/docker-entrypoint-initdb.d/create-tables.sql" Using this Docker Compose file, we can start up the system landscape and run the end-to-end verification script with the following commands: Shell export COMPOSE_FILE=crac/docker-compose-crac.yml docker compose up -d Let’s start with verifying that the CRaC afterRestore callback methods were called: Shell docker compose logs | grep "CRaC's afterRestore callback method called..." Expect something like: Shell ...ReviewServiceApplication : CRaC's afterRestore callback method called... ...RecommendationServiceApplication : CRaC's afterRestore callback method called... ...ProductServiceApplication : CRaC's afterRestore callback method called... ...ProductCompositeServiceApplication : CRaC's afterRestore callback method called... Now, run the end-to-end verification script: Shell ./test-em-all.bash If the script ends with a log output similar to: Shell End, all tests OK: Fri Jun 28 17:40:43 CEST 2024 …it means all tests run ok, and the microservices behave as expected. Bring the system landscape down with the commands: Shell docker compose down unset COMPOSE_FILE After verifying that the microservices behave correctly when started from a CRaC checkpoint, we can compare their startup times with microservices started without using CRaC. 3.4. Comparing Startup Times Without CRaC Now over to the most interesting part: How much faster does the microservice startup when performing a restore from a checkpoint compared to a regular cold start? The tests have been run on a MacBook Pro M1 with 64 GB memory. Let’s start with measuring startup times without using CRaC. 3.4.1. Startup Times Without CRaC To start the microservices without CRaC, we will use the default Docker Compose file. So, we must ensure that the COMPOSE_FILE environment variable is unset before we build the Docker images for the microservices. After that, we can start the database services, MongoDB and MySQL: Shell unset COMPOSE_FILE docker compose build docker compose up -d mongodb mysql Verify that the databases are reporting healthy with the command: docker compose ps. Repeat the command until both report they are healthy. Expect a response like this: Shell NAME ... STATUS ... chapter06-mongodb-1 ... Up 13 seconds (healthy) ... chapter06-mysql-1 ... Up 13 seconds (healthy) ... Next, start the microservices and look in the logs for the startup time (searching for the word Started). Repeat the logs command until logs are shown for all four microservices: Shell docker compose up -d docker compose logs | grep Started Look for a response like: Shell ...Started ProductCompositeServiceApplication in 1.659 seconds ...Started ProductServiceApplication in 2.219 seconds ...Started RecommendationServiceApplication in 2.203 seconds ...Started ReviewServiceApplication in 3.476 seconds Finally, bring down the system landscape: Shell docker compose down 3.4.2. Startup Times With CRaC First, declare that we will use the CRaC-specific Docker Compose file and start the database services, MongoDB and MySQL: Shell export COMPOSE_FILE=crac/docker-compose-crac.yml docker compose up -d mongodb mysql Verify that the databases are reporting healthy with the command: docker compose ps. Repeat the command until both report they are healthy. Expect a response like this: Shell NAME ... STATUS ... crac-mongodb-1 ... Up 10 seconds (healthy) ... crac-mysql-1 ... Up 10 seconds (healthy) ... Next, start the microservices and look in the logs for the startup time (this time searching for the word Restored). Repeat the logs command until logs are shown for all four microservices: Shell docker compose up -d docker compose logs | grep Restored Look for a response like: Shell ...Restored ProductCompositeServiceApplication in 0.131 seconds ...Restored ProductServiceApplication in 0.225 seconds ...Restored RecommendationServiceApplication in 0.236 seconds ...Restored ReviewServiceApplication in 0.154 seconds Finally, bring down the system landscape: Shell docker compose down unset COMPOSE_FILE Now, we can compare the startup times! 3.4.3. Comparing Startup Times Between JVM and CRaC Here is a summary of the startup times, along with calculations of how many times faster the CRaC-enabled microservice starts and the reduction of startup times in percentage: MICROSERVICE WITHOUT CRAC WITH CRAC CRAC TIMES FASTER CRAC REDUCED STARTUP TIME product-composite 1.659 0.131 12.7 92% product 2.219 0.225 9.9 90% recommendation 2.203 0.236 9.3 89% review 3.476 0.154 22.6 96% Generally, we can see a 10-fold performance improvement in startup times or 90% shorter startup time; that’s a lot! Note: The improvement in the Review microservice is even better since it no longer handles the creation of database tables. However, this improvement is irrelevant when comparing improvements using CRaC, so let’s discard the figures for the Review microservice. 4. Summary Coordinated Restore at Checkpoint (CRaC) is a powerful feature in OpenJDK that improves the startup performance of Java applications by allowing them to resume from a previously saved state, a.k.a., a checkpoint. With Spring Boot 3.2, we also get a simplified way of creating a checkpoint using CRaC, known as automatic checkpoint/restore at startup. The tests in this blog post indicate a 10-fold improvement in startup performance, i.e., a 90% reduction in startup time when using automatic checkpoint/restore at startup. The blog post also explained how Docker images using CRaC can be built using a Dockerfile instead of the complex bash scripts suggested by most blog posts on the subject. This, however, comes with some challenges of its own, like using custom Docker builders for privileged builds, as explained in the blog post. Using Docker images created using automatic checkpoint/restore at startup comes with a price. The Docker images will contain runtime-specific and sensitive information, such as credentials to connect to a database at runtime. Therefore, they must be protected from unauthorized use. The Spring Boot support for CRaC does not fully cover all modules in Spring’s eco-system, forcing some workaround to be applied, e.g., when using Spring Data JPA. Also, when using automatic checkpoint/Restore at startup, the JVM HotSpot engine cannot be warmed up before the checkpoint. If optimal execution time for the first requests being processed is important, automatic checkpoint/restore at startup is probably not the way to go. 5. Next Blog Post In the next blog post, I will show you how to use regular on-demand checkpoints to solve some of the considerations with automatic checkpoint/restore at startup. Specifically, the problems with specifying the runtime configuration at build time, storing sensitive runtime configuration in the Docker images, and how the Java VM can be warmed up before performing the checkpoint.
Building a full-featured Angular browser/mobile user interface requires a very specific skill set. There is a vast array of tools, frameworks, and platforms, and each requires a steep learning curve. An open-source project, ApiLogicServer, has released a technical preview that combines GenAI-powered microservice automation with Ontimize, an Angular UI framework. While the promise of a complete running application is the goal of GenAI-enabled microservice, the developer will still need to interact with the generated components to create a finished web application. Figure 1: Ontimize home landing page GenAI-Enabled API Microservice Features Using VSCode and CoPilot, ApiLogicServer has a long list of features based on existing Python libraries. Python 3.8 and higher SQLALchemy ORM JSON API (OpenAPI - Swagger) KeyCloak single sign-on LogicBank for spreadsheet-like declarative rules Declarative role-based access control Kafka producer and consumer integration React-admin multi-page back-office application Ontimize Angular client (technical preview) Framework to extend and customize API DevOps scripts for Docker and Azure deployment Installation The ApiLogicServer can be installed on Windows, vdMAC OS, or Linux with a Python pip install command (setting up the Python and the virtual environment is described on the documentation page). Shell (venv) pip install ApiLogicServer GenAI Prompting Once installed, the command line opens up tools you can also use such as the genai command to submit a prompt to create, build, and connect to a new database automatically. This feature is great for testing new ideas. The developer can also build a project and connect to one of the many SQL databases (MySQL, PostgreSQL, SQL Server, Oracle, SQLite). Shell als genai --using=genai_demo.prompt Command Line and Database Connectivity Once installed in the virtual environment, a set of command line tools can be used to create a new project and connect to your database (e.g., PostgreSQL, Oracle, SQL Server, MySQL, or SQLite). The authentication provider can use sql (a SQLite database) or KeyCloak for single sign-on. In the nw+ example below, we will use the Northwind sample database running SQLite. Shell als create --project-name=demo --open-with=code --auth-provider-type=sql --app=ontimize --db-url=nw+ Server Automation Features The create command will create a SQLAlchemy ORM model, JSON API (Swagger), a react-admin back-office multi-page application, and the ability to use copilot or ChatGPT to generate spreadsheet-like declarative rules using LogicBank. Once VSCode is open, press F5 to start the ApiLogicServer and use the browser to see a running react-admin application. Open browser http://localhost:5656. Ontimize an Angular Framework Ontimize is an open-source Node.js Angular framework that has been around for more than 7 years and there are hundreds of production applications built using this framework. It is well documented and the playground makes the learning curve extremely fast. Using the Ontimize playground and web version called QuickStart, ApiLogicServer created a suite of TypeScript, HTML, and SCSS templates (building blocks) using the Jinja framework. These templates can be modified or extended and reused to build new forms. The command line app-create installs the Ontimize seed and generates a new app_model.yaml file. This file is then used to generate the forms from the API’s. The npm install command brings in all the Node.js libraries needed to run the generated Ontimize application. From the VSCode Terminal Window: Shell cd ui/ontimize npm install Build Angular From a YAML Source ApiLogicServer uses a YAML file to describe the entities, attributes, and relationships to create a runtime react-admin application. The Ontimize Angular app uses a different YAML file with additional metadata that is used by the command line application builder. Each API endpoint is exposed with a New, Home, and Detail page. This application is similar to the react-admin application. The developer can edit the app_model.yaml to exclude entities or attributes, change labels or attribute templates, and then rebuild all the forms using the new settings. The ui/app_model.yaml can be edited and the app-build command can be re-run multiple times to reflect the edits. From the terminal window in VSCode: Shell als app-build --app=ontimize cd ui/ontimize npm start Then run http://localhost:4299. Ontimize Angular Features The out-of-the-box features of this new Ontimize Angular application include on-demand PDF reports and charts, over 20 different attribute template types (e.g., currency, date, phone, checkbox, email, checkbox, combo, password, etc.) master-detail using relationships, parent key lookups, exports, filters, sorts, maps and more. Ontimize also supports the translation into multiple languages and international currency/date types. Having an instant running application wired to your APIs allows a truly agile approach to getting feedback from stakeholders. Figure 2: Demo Bank Customer Table Figure 3: Tree View Split Pane Detail LifeCycle of API Development The lifecycle of any API project involves change. The database may add or remove tables or columns. The ApiLogicServer command line provides rebuild-from-database which will rebuild the SQLAlchemy ORM model. Another command line tool rebuild-from-model will regenerate the react-admin YAML file. Using the app-create and app-build for a new Ontimize project (als app-create –app=ont_new) then merge the Ontimize app_model.yaml files or forms. LogicBank is integrated into ApiLogicServer and SQLAlchemy ORM to provide spreadsheet-like rules (e.g. sums, counts, formulas, constraints, and events). Using Copilot prompts to generate rules is an interesting exercise for a future article. Once the forms are created and wired to the API, the UX team can modify the style sheet, TypeScript, and form layout to create a highly polished web application. DevOps Containers ApiLogicServer provides a DevOps directory that can be used to build and deploy Docker containers to the cloud. Ontimize can be instructed to build a production environment and deploy to a web container or include it with the ApiLogicServer. Summary There is the ability as a developer to deliver a full-featured application including a Server, API, and user interface without having to write a single line of code. ApiLogicServer has tools to quickly build and deploy to Docker containers and cloud. The GenAI-enabled feature helps with code completion and can generate declarative logic rules from prompts. The Python platform offers the developer access to a large suite of libraries and tools, and the Copilot services make extending server functions a snap. This really is the start of a beautiful friendship.
Java records fit perfectly in Spring Boot applications. Let’s have several scenarios where Java records can help us increase readability and expressiveness by squeezing the homologous code. Using Records in Controllers Typically, a Spring Boot controller operates with simple POJO classes that carry our data back over the wire to the client. For instance, check out this simple controller endpoint returning a list of authors, including their books: Java @GetMapping("/authors") public List<Author> fetchAuthors() { return bookstoreService.fetchAuthors(); } Here, the Author (and Book) can be simple carriers of data written as POJOs. But, they can be replaced by records as well. Here it is: Java public record Book(String title, String isbn) {} public record Author(String name, String genre, List<Book> books) {} That’s all! The Jackson library (which is the default JSON library in Spring Boot) will automatically marshal instances of type Author/Book into JSON. In the bundled code, you can practice the complete example via the localhost:8080/authors endpoint address. Using Records With Templates Thymeleaf is probably the most used templating engine in Spring Boot applications. Thymeleaf pages (HTML pages) are typically populated with data carried by POJO classes, which means that Java records should work as well. Let’s consider the previous Author and Book records, and the following controller endpoint: Java @GetMapping("/bookstore") public String bookstorePage(Model model) { model.addAttribute("authors", bookstoreService.fetchAuthors()); return "bookstore"; } The List<Author> returned via fetchAuthors() is stored in the model under a variable named authors. This variable is used to populate bookstore.html as follows: HTML ... <ul th:each="author : ${authors}"> <li th:text="${author.name} + ' (' + ${author.genre} + ')'" /> <ul th:each="book : ${author.books}"> <li th:text="${book.title}" /> </ul> </ul> ... Done! You can check out the application Java Coding Problems SE. Using Records for Configuration Let’s assume that in application.properties we have the following two properties (they could be expressed in YAML as well): Properties files bookstore.bestseller.author=Joana Nimar bookstore.bestseller.book=Prague history Spring Boot maps such properties to POJO via @ConfigurationProperties. But, a record can be used as well. For instance, these properties can be mapped to the BestSellerConfig record as follows: Java @ConfigurationProperties(prefix = "bookstore.bestseller") public record BestSellerConfig(String author, String book) {} Next, in BookstoreService (a typical Spring Boot service), we can inject BestSellerConfig and call its accessors: Java @Service public class BookstoreService { private final BestSellerConfig bestSeller; public BookstoreService(BestSellerConfig bestSeller) { this.bestSeller = bestSeller; } public String fetchBestSeller() { return bestSeller.author() + " | " + bestSeller.book(); } } In the bundled code, we have added a controller that uses this service as well. Record and Dependency Injection In the previous examples, we have injected the BookstoreService service into BookstoreController using the typical mechanism provided by SpringBoot – dependency injection via constructor (it can be done via @Autowired as well): Java @RestController public class BookstoreController { private final BookstoreService bookstoreService; public BookstoreController(BookstoreService bookstoreService) { this.bookstoreService = bookstoreService; } @GetMapping("/authors") public List<Author> fetchAuthors() { return bookstoreService.fetchAuthors(); } } But, we can compact this class by re-writing it as a record as follows: Java @RestController public record BookstoreController(BookstoreService bookstoreService) { @GetMapping("/authors") public List<Author> fetchAuthors() { return bookstoreService.fetchAuthors(); } } The canonical constructor of this record will be the same as our explicit constructor. The application is available on GitHub. Feel free to challenge yourself to find more use cases of Java records in Spring Boot applications.
Spring Boot provides health indicators to monitor application health and database health with the help of Spring Boot Actuator. Spring Boot Actuator comes with various health indicators for most relational databases and non-relational databases like MongoDB, Redis, ElasticSearch, etc. Spring Boot Actuator also provides health indicators for RabbitMQ, and IBM MQ out of the box. Why Choose Custom Health Checks If we want to monitor the health status of the external services our application connects to, Spring Boot doesn’t provide any health indicators for this scenario out of the box, we need to write custom health indicators. Also, Spring Boot doesn’t provide any Kafka health indicator out of the box as of today, so in this scenario, we need to go for a custom health indicator. Implementation Project Structure Main Class Java package com.lights5.services.orders.api; import com.lights5.services.orders.api.config.EndpointsRegistry; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class OrdersApiApplication { @Autowired private EndpointsRegistry endpointsRegistry; public static void main(String[] args) { SpringApplication.run(OrdersApiApplication.class, args); } } application.yml file YAML spring: application: name: orders-api kafka: producer: bootstrap-servers: localhost:9092 data: mongodb: host: localhost port: 27017 database: orders_db username: ${username} password: ${pwd} authentication-database: orders_db app: client: payments-service: host: https://example.com paths: health-check: /mock-service/health intiate-payment: /payments ApplicationConfig Java package com.lights5.services.orders.api.config; import org.apache.kafka.clients.admin.AdminClient; import org.springframework.boot.actuate.data.mongo.MongoHealthIndicator; import org.springframework.boot.autoconfigure.kafka.KafkaProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.mongodb.core.MongoTemplate; import java.util.Properties; @Configuration public class AppConfig { @Bean MongoHealthIndicator mongoHealthIndicator(MongoTemplate mongoTemplate) { return new CustomMongoHealthIndicator(mongoTemplate); } @Bean AdminClient kafkaAdminClient(KafkaProperties kafkaProperties) { Properties properties = new Properties(); properties.put("bootstrap.servers", kafkaProperties.getBootstrapServers()); properties.put("request.timeout.ms", 3000); properties.put("connections.max.idle.ms", 5000); return AdminClient.create(properties); } } EndpointsRegistry This class registers all external clients that the application connects to. Java package com.lights5.services.orders.api.config; import lombok.Getter; import lombok.Setter; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Configuration; import java.util.Map; @Getter @Setter @Configuration @ConfigurationProperties(prefix = "app") public class EndpointsRegistry { private Map<String, ServiceEndpoint> client; @Getter @Setter static class ServiceEndpoint { private String host; private Map<String, String> paths; } public String getHealthCheckURL(String serviceName) { ServiceEndpoint endpoint = this.client.get(serviceName); if (endpoint != null) { String healthCheckPath = endpoint.getPaths().get("health-check"); String host = endpoint.getHost(); return host + healthCheckPath; } return null; } } PaymentServiceMonitor (External Service Monitor) This class overrides the default behavior of the AbstractHealthIndicator class. It executes a GET request on the health check URL of an external client and updates the status accordingly. Java package com.lights5.services.orders.api.config; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.hc.client5.http.classic.methods.HttpGet; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse; import org.apache.hc.client5.http.impl.classic.HttpClientBuilder; import org.springframework.boot.actuate.health.AbstractHealthIndicator; import org.springframework.boot.actuate.health.Health; import org.springframework.stereotype.Component; import java.io.IOException; @Slf4j @Component @RequiredArgsConstructor public class PaymentServiceHealthIndicator extends AbstractHealthIndicator { private final EndpointsRegistry endpointsRegistry; @Override protected void doHealthCheck(Health.Builder builder) { try { String healthCheckURL = endpointsRegistry.getHealthCheckURL("payment-service"); CloseableHttpClient httpClient = HttpClientBuilder.create().build(); CloseableHttpResponse response = httpClient.execute(new HttpGet(healthCheckURL)); if (response.getCode() >= 500) { log.error("Payment Service is down"); } else { builder.up(); } } catch (IOException ex) { builder.down(); } } } HostAvailability Class This class is used to verify whether the application is able to connect to a given host and port number. Java package com.lights5.services.orders.api.service; import lombok.extern.slf4j.Slf4j; import java.io.IOException; import java.net.*; @Slf4j public class HostAvailability { private HostAvailability() { // to prevent instantiation from other classes. } public static boolean isAvailable(String hostName, int port) { SocketAddress socketAddress = new InetSocketAddress(hostName, port); try (Socket socket = new Socket()) { socket.connect(socketAddress, 5000); } catch (IOException e) { log.error("Application Health Check Failed due to service unavailability {}", e.getMessage()); return false; } return true; } } CustomMongoHealthIndicator This class extends the MongoHealthIndicator (provided by Spring Boot) and overrides the behavior of the health check method. First, we verify whether the app is able to connect to the host and port and then we execute a small command on the database to check database connectivity. If we execute the command directly without verifying server connectivity, it takes more time if the server is not reachable from the application. That’s why we are verifying server connectivity first. Java package com.lights5.services.orders.api.config; import com.lights5.services.orders.api.service.HostAvailability; import lombok.extern.slf4j.Slf4j; import org.bson.Document; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.actuate.data.mongo.MongoHealthIndicator; import org.springframework.boot.actuate.health.Health; import org.springframework.data.mongodb.core.MongoTemplate; @Slf4j public class CustomMongoHealthIndicator extends MongoHealthIndicator { private final MongoTemplate mongoTemplate; @Value("${spring.data.mongodb.host}") private String mongodbHost; @Value("${spring.data.mongodb.port}") private int port; public CustomMongoHealthIndicator(MongoTemplate mongoTemplate) { super(mongoTemplate); this.mongoTemplate = mongoTemplate; } public void doHealthCheck(Health.Builder builder) throws Exception { boolean isServerAvailable = HostAvailability.isAvailable(mongodbHost, port); if (isServerAvailable) { Document result = mongoTemplate.executeCommand("{ isMaster: 1 }"); builder.up().withDetail("maxWireVersion", result.getInteger("maxWireVersion")); } else { log.error("MongoDB Server is down."); builder.down(); } } } KafkaHealthIndicator This class provides implementation to verify Kafka cluster health. It uses KafkaAdminClient to describe the cluster, if it receives the response then the cluster is up else the cluster is down. Note: Doesn’t work if the producer is configured to support transactions. Java package com.lights5.services.orders.api.config; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.kafka.clients.admin.AdminClient; import org.apache.kafka.common.Node; import org.springframework.boot.actuate.health.AbstractHealthIndicator; import org.springframework.boot.actuate.health.Health; import org.springframework.stereotype.Component; import java.util.Collection; import java.util.concurrent.TimeUnit; @Slf4j @Component @RequiredArgsConstructor public class KafkaHealthIndicator extends AbstractHealthIndicator { private final AdminClient kafkaAdminClient; @Override protected void doHealthCheck(Health.Builder builder) throws Exception { Collection<Node> nodes = kafkaAdminClient.describeCluster() .nodes().get(3, TimeUnit.SECONDS); if (!nodes.isEmpty()) { log.error("Kafka Server is up with nodes {}", nodes.size()); builder.up(); } else { log.error("Kafka Server is down"); builder.down(); } } } Working If any of these HealthIndicators (MongoDB, Kafka, Payments Service) is down, then the health check URL of the application returns the status as the following: JSON { "status": "DOWN" } If all these dependent services are up, then the status will be returned as: JSON { "status": "UP" } Endpoint to verify app health. Conclusion Custom health checks are important as they provide greater visibility of external services availability. We can take action immediately thereby decreasing application errors.
Reactive programming has significantly altered how developers tackle modern application development, particularly in environments that demand top-notch performance and scalability. Quarkus, a Kubernetes-native Java framework specifically optimized for GraalVM and HotSpot, fully embraces the principles of reactive programming to craft applications that are responsive, resilient, and elastic. This article comprehensively explores the impact and effectiveness of reactive programming in Quarkus, providing detailed insights and practical examples in Java to illustrate its transformative capabilities. What Is Reactive Programming? Reactive programming is a programming paradigm that focuses on handling asynchronous data streams and the propagation of change. It provides developers with the ability to write code that responds to changes in real time, such as user inputs, data updates, or messages from other services. This approach is particularly well-suited for building applications that require real-time responsiveness and the ability to process continuous streams of data. By leveraging reactive programming, developers can create more interactive and responsive applications that can adapt to changing conditions and events. Key features of reactive programming include: Asynchronous: Non-blocking operations that allow multiple tasks to run concurrently Event-driven: Actions are triggered by events such as user actions or data changes Resilient: Systems remain responsive under load by handling failures gracefully Scalable: Efficient resource usage to handle a high number of requests Why Quarkus for Reactive Programming? Quarkus, a framework designed to harness the advantages of reactive programming, aims to provide a streamlined and efficient environment for developing reactive applications. There are several compelling reasons to consider Quarkus for such applications: Native support for Reactive frameworks: Quarkus seamlessly integrates with popular reactive libraries such as Vert.x, Mutiny, and Reactive Streams. This native support allows developers to leverage the full power of these frameworks within the Quarkus environment. Efficient resource usage: Quarkus's native image generation and efficient runtime result in lower memory consumption and faster startup times. This means that applications built with Quarkus can be more resource-efficient, leading to potential cost savings and improved performance. Developer productivity: Quarkus offers features like live coding, significantly improving the development experience. This means developers can iterate more quickly, leading to faster development cycles and ultimately more productive software development. Getting Started With Reactive Programming in Quarkus Let’s dive into a simple example to demonstrate reactive programming in Quarkus using Java. We’ll create a basic REST API that fetches data asynchronously. Step 1: Setting Up the Project First, create a new Quarkus project: Shell mvn io.quarkus:quarkus-maven-plugin:create \ -DprojectGroupId=com.example \ -DprojectArtifactId=reactive-quarkus \ -DclassName="com.example.GreetingResource" \ -Dpath="/greeting" cd reactive-quarkus Add the necessary dependencies in your pom.xml: XML <dependencies> <dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-resteasy-reactive</artifactId> </dependency> <dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-smallrye-mutiny</artifactId> </dependency> <dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-vertx</artifactId> </dependency> </dependencies> Step 2: Start Coding Now, create a simple REST endpoint using Mutiny, a reactive programming library designed for simplicity and performance: Java import io.smallrye.mutiny.Uni; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; @Path("/greeting") public class GreetingResource { @GET @Produces(MediaType.APPLICATION_JSON) public Uni<Greeting> greeting() { return Uni.createFrom().item(() -> new Greeting("Hello, Reactive World!")) .onItem().delayIt().byMillis(1000); // Simulate delay } public static class Greeting { public String message; public Greeting(String message) { this.message = message; } } } In this example: We define a REST endpoint /greeting that produces JSON. The greeting method returns a Uni<Greeting> which represents a single value or failure, a concept from Mutiny. We simulate a delay using onItem().delayIt().byMillis(1000) to mimic an asynchronous operation Step 3: Running the Application To run the application, use the Quarkus development mode: Shell ./mvnw quarkus:dev Now, visit http://localhost:8080/greeting to see the response: JSON { "message": "Hello, Reactive World!" } Unit Testing Reactive Endpoints When testing reactive endpoints in Quarkus, it's important to verify that the application functions correctly in response to various conditions. Quarkus facilitates seamless integration with JUnit 5, allowing developers to effectively write and execute unit tests to ensure the proper functionality of their applications. Step 1: Adding Test Dependencies Ensure you have the following dependencies in your pom.xml for testing: XML <dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-junit5</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>io.rest-assured</groupId> <artifactId>rest-assured</artifactId> <scope>test</scope> </dependency> Step 2: Writing a Unit Test Create a test class to verify the behavior of the GreetingResource: Java import io.quarkus.test.junit.QuarkusTest; import io.rest-assured.RestAssured; import org.junit.jupiter.api.Test; import static org.hamcrest.CoreMatchers.is; @QuarkusTest public class GreetingResourceTest { @Test public void testGreetingEndpoint() { RestAssured.when().get("/greeting") .then() .statusCode(200) .body("message", is("Hello, Reactive World!")); } } In this test: We use the @QuarkusTest annotation to enable Quarkus testing features. We use RestAssured to send an HTTP GET request to the /greeting endpoint and verify the response status code and body. Step 3: Running the Tests To run the tests, use the Maven test command: Shell ./mvnw test The test will execute and verify that the /greeting endpoint returns the expected response. Advanced Usage: Integrating With Databases Let’s extend the example by integrating a reactive database client. We’ll use the reactive PostgreSQL client provided by Vert.x. Add the dependency for the reactive PostgreSQL client: XML <dependency> <groupId>io.quarkiverse.reactive</groupId> <artifactId>quarkus-reactive-pg-client</artifactId> </dependency> Configure the PostgreSQL client in application.properties: Shell quarkus.datasource.db-kind=postgresql quarkus.datasource.username=your_username quarkus.datasource.password=your_password quarkus.datasource.reactive.url=postgresql://localhost:5432/your_database Create a repository class to handle database operations: Java import io.smallrye.mutiny.Uni; import io.vertx.mutiny.pgclient.PgPool; import io.vertx.mutiny.sqlclient.Row; import io.vertx.mutiny.sqlclient.RowSet; import javax.enterprise.context.ApplicationScoped; import javax.inject.Inject; @ApplicationScoped public class GreetingRepository { @Inject PgPool client; public Uni<String> findGreeting() { return client.query("SELECT message FROM greetings WHERE id = 1") .execute() .onItem().transform(RowSet::iterator) .onItem().transform(iterator -> iterator.hasNext() ? iterator.next().getString("message") : "Hello, default!"); } } Update the GreetingResource to use the repository: Java import io.smallrye.mutiny.Uni; import javax.inject.Inject; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; @Path("/greeting") public class GreetingResource { @Inject GreetingRepository repository; @GET @Produces(MediaType.APPLICATION_JSON) public Uni<Greeting> greeting() { return repository.findGreeting() .onItem().transform(Greeting::new); } public static class Greeting { public String message; public Greeting(String message) { this.message = message; } } } This setup demonstrates how to perform asynchronous database operations using the reactive PostgreSQL client. The findGreeting method queries the database and returns a Uni<String> representing the greeting message. Handling Errors in Reactive Programming Handling errors gracefully is a critical aspect of building resilient reactive applications. Mutiny provides several operators to handle errors effectively. Update the GreetingRepository to include error handling: Java public Uni<String> findGreeting() { return client.query("SELECT message FROM greetings WHERE id = 1") .execute() .onItem().transform(RowSet::iterator) .onItem().transform(iterator -> iterator.hasNext() ? iterator.next().getString("message") : "Hello, default!") .onFailure().recoverWithItem("Hello, fallback!"); } In this updated method: We use onFailure().recoverWithItem("Hello, fallback!") to provide a fallback message in case of any failure during the database query. Reactive Event Bus With Vert.x Quarkus seamlessly integrates with Vert.x, a powerful reactive toolkit, to provide a high-performance event bus for developing sophisticated event-driven applications. This event bus allows various components of your application to communicate asynchronously, facilitating efficient and scalable interaction between different parts of the system. Add the necessary Vert.x dependencies: XML <dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-vertx</artifactId> </dependency> Create a Vert.x consumer to handle events: Java import io.quarkus.vertx.ConsumeEvent; import io.smallrye.mutiny.Uni; import javax.enterprise.context.ApplicationScoped; @ApplicationScoped public class GreetingService { @ConsumeEvent("greeting") public Uni<String> generateGreeting(String name) { return Uni.createFrom().item(() -> "Hello, " + name + "!") .onItem().delayIt().byMillis(500); // Simulate delay } } Now, Update the GreetingResource to send events to the event bus: Java import io.smallrye.mutiny.Uni; import io.vertx.mutiny.core.eventbus.EventBus; import javax.inject.Inject; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.core.MediaType; @Path("/greeting") public class GreetingResource { @Inject EventBus eventBus; @GET @Produces(MediaType.APPLICATION_JSON) public Uni<Greeting> greeting(@QueryParam("name") String name) { return eventBus.<String>request("greeting", name) .onItem().transform(reply -> new Greeting(reply.body())); } public static class Greeting { public String message; public Greeting(String message) { this.message = message; } } } In this example: We define an event consumer GreetingService that listens for greeting events and generates a greeting message. The GreetingResource sends a greeting event to the event bus and waits for the response asynchronously. Comparison: Quarkus vs. Spring in Reactive Capabilities When building reactive applications, Quarkus and Spring offer robust frameworks, each with unique approaches and strengths. 1. Framework Integration Spring Spring Boot leverages Spring WebFlux for reactive programming and seamlessly integrates with the Spring ecosystem, supporting Project Reactor as its reactive library. Quarkus Quarkus utilizes Vert.x and Mutiny for reactive programming, providing native support from the ground up and optimizing for performance and efficiency. 2. Performance and Resource Efficiency Spring While Spring Boot with WebFlux offers good performance for reactive applications, it may be heavier in terms of resource usage compared to Quarkus. Quarkus Quarkus is designed to be lightweight and fast, showcasing lower memory consumption and faster startup times, especially when compiled to a native image with GraalVM. 3. Developer Experience Spring Spring Boot offers a mature ecosystem with extensive documentation and strong community support, making it easy for developers familiar with Spring to adopt reactive programming. Quarkus Quarkus provides an excellent developer experience with features like live coding and quick feedback loops. Its integration with reactive libraries like Mutiny makes it intuitive for developers new to reactive programming. 4. Cloud-Native and Microservices Spring Widely used for building microservices and cloud-native applications, Spring Boot provides a rich set of tools and integrations for deploying applications to the cloud. Quarkus Designed with cloud-native and microservices architectures in mind, Quarkus showcases efficient resource usage and strong support for Kubernetes, making it a compelling choice for cloud deployments. 5. Ecosystem and Community Spring Boasting a vast ecosystem with numerous extensions and integrations, Spring is supported by a large community of developers. Quarkus Rapidly gaining popularity, Quarkus offers a comprehensive set of extensions, and its community is also expanding, contributing to its ecosystem. Conclusion Reactive programming in Quarkus provides a cutting-edge approach to enhancing the performance and scalability of Java applications. By harnessing the capabilities of reactive streams and asynchronous operations, Quarkus empowers developers to build applications that are not only robust and high-performing, but also well-suited for modern cloud-native environments. The efficiency and power of Quarkus, combined with its rich ecosystem of reactive libraries, offer developers the tools they need to handle a wide range of tasks, from simple asynchronous operations to complex data streams, making Quarkus a formidable platform for reactive programming in Java.
In today's rapidly evolving technological landscape, it is crucial for any business or application to efficiently manage and utilize data. NoSQL databases have emerged as an alternative to traditional relational databases, offering flexibility, scalability, and performance advantages. These benefits become even more pronounced when combined with Java, a robust and widely-used programming language. This article explores three key benefits of understanding and learning NoSQL databases with Java, highlighting the polyglot philosophy and its efficiency in software architecture. Enhanced Flexibility and Scalability One significant benefit of NoSQL databases is their capability to handle various data models, such as key-value pairs, documents, wide-column stores, and graph databases. This flexibility enables developers to select the most suitable data model for their use case. When combined with Java, a language renowned for its portability and platform independence, the adaptability of NoSQL databases can be fully utilized. Improved Performance and Efficiency Performance is a crucial aspect of database management, and NoSQL databases excel in this area because of their distributed nature and optimized storage mechanisms. When developers combine these performance-enhancing features with Java, they can create applications that are not only efficient but also high-performing. Embracing the Polyglot Philosophy The polyglot philosophy in software development encourages using multiple languages, frameworks, and databases within a single application to take advantage of each one's strengths. Understanding and learning NoSQL databases with Java perfectly embodies this approach, offering several benefits for modern software architecture. Leveraging Eclipse JNoSQL for Success With NoSQL Databases and Java To fully utilize NoSQL databases with Java, developers can use Eclipse JNoSQL, a framework created to streamline the integration and management of NoSQL databases in Java applications. Eclipse JNoSQL supports over 30 databases and is aligned with Jakarta NoSQL and Jakarta Data specifications, providing a comprehensive solution for modern data handling needs. Eclipse JNoSQL: Bridging Java and NoSQL Databases Eclipse JNoSQL is a framework that simplifies the interaction between Java applications and NoSQL databases. With support for over 30 different NoSQL databases, Eclipse JNoSQL enables developers to work efficiently across various data stores without compromising flexibility or performance. Key features of Eclipse JNoSQL include: Support for Jakarta Data Query Language: This feature enhances the power and flexibility of querying across databases. Cursor pagination: Processes large datasets efficiently by utilizing cursor-based pagination rather than traditional offset-based pagination NoSQLRepository: Simplifies the creation and management of repository interfaces New column and document templates: Simplify data management with predefined templates Jakarta NoSQL and Jakarta Data Specifications Eclipse JNoSQL is designed to support Jakarta NoSQL and Jakarta Data specifications, standardizing and simplifying database interactions in Java applications. Jakarta NoSQL: This comprehensive framework offers a unified API and a set of powerful annotations, making it easier to work with various NoSQL data stores while maintaining flexibility and productivity. Jakarta Data: This specification provides an API for easier data access across different database types, enabling developers to create custom query methods on repository interfaces. Introducing Eclipse JNoSQL 1.1.1 The latest release, Eclipse JNoSQL 1.1.1, includes significant enhancements and new features, making it a valuable tool for Java developers working with NoSQL databases. Key updates include: Support to cursor pagination Support to Jakarta Data Query Fixes several bugs and enhances performance For more details, visit the Eclipse JNoSQL Release 1.1.1 notes. Practical Example: Java SE Application With Oracle NoSQL To illustrate the practical use of Eclipse JNoSQL, let's consider a Java SE application using Oracle NoSQL. This example showcases the effectiveness of cursor pagination and JDQL for querying. The first pagination method we will discuss is Cursor pagination, which offers a more efficient way to handle large datasets than traditional offset-based pagination. Below is a code snippet demonstrating cursor pagination with Oracle NoSQL. Java @Repository public interface BeerRepository extends OracleNoSQLRepository<Beer, String> { @Find @OrderBy("hop") CursoredPage<Beer> style(@By("style") String style, PageRequest pageRequest); @Query("From Beer where style = ?1") List<Beer> jpql(String style); } public class App4 { public static void main(String[] args) { var faker = new Faker(); try (SeContainer container = SeContainerInitializer.newInstance().initialize()) { BeerRepository repository = container.select(BeerRepository.class).get(); for (int index = 0; index < 100; index++) { Beer beer = Beer.of(faker); // repository.save(beer); } PageRequest pageRequest = PageRequest.ofSize(3); var page1 = repository.style("Stout", pageRequest); System.out.println("Page 1"); page1.forEach(System.out::println); PageRequest pageRequest2 = page1.nextPageRequest(); var page2 = repository.style("Stout", pageRequest2); System.out.println("Page 2"); page2.forEach(System.out::println); System.out.println("JDQL query: "); repository.jpql("Stout").forEach(System.out::println); } System.exit(0); } } In this example, BeerRepository efficiently retrieves and paginates data using cursor pagination. The style method employs cursor pagination, while the jpql method demonstrates a JDQL query. API Changes and Compatibility Breaks in Eclipse JNoSQL 1.1.1 The release of Eclipse JNoSQL 1.1.1 includes significant updates and enhancements aimed at improving functionality and aligning with the latest specifications. However, it's important to note that these changes may cause compatibility issues for developers, which need to be understood and addressed in their projects. 1. Annotations Moved to Jakarta NoSQL Specification Annotations like Embeddable and Inheritance were previously included in the Eclipse JNoSQL framework. In the latest version, however, they have been relocated to the Jakarta NoSQL specification to establish a more consistent approach across various NoSQL databases. As a result, developers will need to update their imports and references to these annotations. Java // Old import import org.jnosql.mapping.Embeddable; // New import import jakarta.nosql.Embeddable; The updated annotations can be accessed at the Jakarta NoSQL GitHub repository. 2. Unified Query Packages To simplify and unify the query APIs, SelectQuery and DeleteQuery have been consolidated into a single package. Consequently, specific query classes like DocumentQuery, DocumentDeleteQuery, ColumnQuery, and ColumnDeleteQuery have been removed. Impact: Any code using these removed classes will no longer compile and must be refactored to use the new unified classes. Solution: Refactor your code to use the new query classes in the org.eclipse.jnosql.communication.semistructured package. For example: Java // Old usage DocumentQuery query = DocumentQuery.select().from("collection").where("field").eq("value").build(); // New usage SelectQuery query = SelectQuery.select().from("collection").where("field").eq("value").build(); Similar adjustments will be needed for delete queries. 3. Migration of Templates Templates such as ColumnTemplate, KeyValueTemplate, and DocumentTemplate have been moved from the Jakarta Specification to Eclipse JNoSQL. Java // Old import import jakarta.nosql.document.DocumentTemplate; // New import import org.eclipse.jnosql.mapping.document.DocumentTemplate; 4. Default Query Language: Jakarta Data Query Language (JDQL) Another significant update in Eclipse JNoSQL 1.1.1 is the adoption of Jakarta Data Query Language (JDQL) as the default query language. JDQL provides a standardized way to define queries using annotations, making it simpler and more intuitive for developers. Conclusion The use of a NoSQL database is a powerful asset in modern applications. It allows software architects to employ polyglot persistence, utilizing the best persistence capability in each scenario. Eclipse JNoSQL assists Java developers in implementing these NoSQL capabilities into their applications.
Justin Albano
Software Engineer,
IBM
Thomas Hansen
CTO,
AINIRO.IO