<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Hantsy on Medium]]></title>
        <description><![CDATA[Stories by Hantsy on Medium]]></description>
        <link>https://medium.com/@hantsy?source=rss-bb3f4f6eaf51------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/1*hvpS8wQrCuh4tXDq2Yy08g.png</url>
            <title>Stories by Hantsy on Medium</title>
            <link>https://medium.com/@hantsy?source=rss-bb3f4f6eaf51------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Tue, 12 May 2026 02:38:39 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@hantsy/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com" rel="hub"/>
        <item>
            <title><![CDATA[An Introduction to Spring JmsClient API]]></title>
            <link>https://itnext.io/an-introduction-to-spring-jmsclient-api-02a5137d30c4?source=rss-bb3f4f6eaf51------2</link>
            <guid isPermaLink="false">https://medium.com/p/02a5137d30c4</guid>
            <category><![CDATA[activemq-artemis]]></category>
            <category><![CDATA[spring-boot]]></category>
            <category><![CDATA[jms]]></category>
            <category><![CDATA[activemq]]></category>
            <category><![CDATA[spring]]></category>
            <dc:creator><![CDATA[Hantsy]]></dc:creator>
            <pubDate>Sun, 04 Jan 2026 15:35:09 GMT</pubDate>
            <atom:updated>2026-01-04T23:08:12.541Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*WDqXGqExrTqq2g100bgyZQ.jpeg" /><figcaption>Songshan Lake, Dongguan City, China</figcaption></figure><p>In previous posts, we discussed the new <a href="https://itnext.io/an-introduction-to-spring-jdbcclient-api-20e833d7b0f3">JdbcClient</a> and <a href="https://medium.com/itnext/an-introduction-to-spring-restclient-api-22bfafcf9405">RestClient</a> which used to replace the existing JdbcTemplate and RestTemplate. We are all impressed by these developer-friendly APIs.</p><p>Spring 7 introduces <a href="https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/jms/core/JmsClient.html">JmsClient</a>, a more modern and fluent API to interact with JMS(Jakarta Message Service) brokers, which is intended to replace the existing <a href="https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/jms/core/JmsTemplate.html">JmsTemplate</a> and <a href="https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/jms/core/JmsMessagingTemplate.html">JmsMessagingTemplate</a>.</p><h3>Getting Started</h3><p>Start by creating a Maven project and including the spring-core, spring-context, spring-context-support dependencies to provide the basic Spring container support.</p><p>Add the following dependencies to your pom.xml file:</p><pre>// add spring core, context, context support dependencies if not already present<br>&lt;dependency&gt;<br>    &lt;groupId&gt;org.springframework&lt;/groupId&gt;<br>    &lt;artifactId&gt;spring-jms&lt;/artifactId&gt;<br>&lt;/dependency&gt;<br><br>&lt;dependency&gt;<br>    &lt;groupId&gt;org.apache.activemq&lt;/groupId&gt;<br>    &lt;artifactId&gt;artemis-jakarta-client&lt;/artifactId&gt;<br>    &lt;version&gt;2.44.0&lt;/version&gt;<br>&lt;/dependency&gt;</pre><p>Create a configuration class and declare a JMS ConnectionFactory, MessageConverter and JmsListenerContainerFactory bean to enable JMS support:</p><pre>@Configuration<br>@EnableJms<br>public class JmsConfig{<br><br>    @Autowired<br>    Environment environment;<br><br>    @Bean<br>    public CachingConnectionFactory connectionFactory() {<br>        return new CachingConnectionFactory(<br>                new ActiveMQConnectionFactory(environment.getProperty(&quot;activemq.brokerUrl&quot;), &quot;user&quot;, &quot;password&quot;)<br>        );<br>    }<br><br>    // The legacy messageconveter is from: org.springframework.jms.support.converter<br>    @Bean<br>    public MessageConverter messageConverter() {<br>        JacksonJsonMessageConverter messageConverter = new JacksonJsonMessageConverter();<br>        messageConverter.setTypeIdPropertyName(&quot;_type&quot;);<br>        return messageConverter;<br>    }<br><br>    @Bean<br>    public DefaultJmsListenerContainerFactory jmsListenerContainerFactory() {<br>        DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory();<br>        factory.setConnectionFactory(connectionFactory());<br>        factory.setMessageConverter(messageConverter());<br>        return factory;<br>    }<br>}</pre><p>The MessageConverter bean here is used to convert between POJO objects and JMS messages.</p><p>Declare a JmsTemplate bean directly with the ConnectionFactory bean.</p><pre>@Bean<br>public JmsTemplate jmsTemplate() {<br>    JmsTemplate jmsTemplate = new JmsTemplate(connectionFactory());<br>    jmsTemplate.setMessageConverter(messageConverter());<br>    return jmsTemplate;<br>}</pre><p>With the messageConverter property set, you can use jmsTemplate to send a POJO object, as well as a text message.</p><p>Spring JMS also embraces the Spring Messaging abstraction, which provides a JmsMessagingTemplate which is based on Spring Messaging API.</p><p>Declare a JmsMessagingTemplate in the configuration class, and configure the messageConverter property with Spring Messaging specific MessageConverter.</p><pre>// The spring messaging specific message converter is from org.springframework.messaging.<br>import org.springframework.messaging.converter.JacksonJsonMessageConverter;<br>import org.springframework.messaging.converter.MessageConverter;<br>//...<br><br>@Bean<br>public MessageConverter messagingMessageConverter() {<br>    return new JacksonJsonMessageConverter();<br>}<br><br>@Bean<br>public JmsMessagingTemplate jmsMessagingTemplate() {<br>    JmsMessagingTemplate jmsMessagingTemplate = new JmsMessagingTemplate(connectionFactory());<br>    jmsMessagingTemplate.setMessageConverter(messagingMessageConverter());<br>    return jmsMessagingTemplate;<br>}</pre><p>You can create JmsClient from the existing ConnectionFactory bean or JmsOperations (eg. JmsTemplate) bean.</p><pre>@Bean<br>public JmsClient jmsClient() {<br>    return JmsClient.create(jmsTemplate());<br>}</pre><p>Alternatively, declare JmsClient with JmsClient.Builder to customize with a global MessageConverter and MessagePostProcessor.</p><pre>public JmsClient jmsClient() {<br>    return JmsClient.builder(jmsTemplate())<br>        .messageConverter(...)<br>        .messagePostProcessor(...)<br>        .build();<br>}</pre><h3>Sending and Receiving Messages</h3><p>To bootstrap an ActiveMQ Artimes server at runtime, add the following testcontainers dependencies in your pom.xml file:</p><pre>&lt;dependency&gt;<br>    &lt;groupId&gt;org.testcontainers&lt;/groupId&gt;<br>    &lt;artifactId&gt;testcontainers-junit-jupiter&lt;/artifactId&gt;<br>    &lt;version&gt;${testcontainers.version}&lt;/version&gt;<br>    &lt;scope&gt;test&lt;/scope&gt;<br>&lt;/dependency&gt;<br>&lt;dependency&gt;<br>    &lt;groupId&gt;org.testcontainers&lt;/groupId&gt;<br>    &lt;artifactId&gt;testcontainers-activemq&lt;/artifactId&gt;<br>    &lt;version&gt;${testcontainers.version}&lt;/version&gt;<br>    &lt;scope&gt;test&lt;/scope&gt;<br>&lt;/dependency&gt;</pre><p>Create a ApplicationContextInitializer to start the Artimes container before Spring context is initialized:</p><pre>class ArtemisContainerInitializer implements ApplicationContextInitializer&lt;@NotNull ConfigurableApplicationContext&gt; {<br>    private static final Logger log = LoggerFactory.getLogger(ArtemisContainerInitializer.class);<br>    final static String DOCKER_IMAGE_NAME = &quot;apache/activemq-artemis:latest-alpine&quot;;<br>    final static Integer DEFAULT_EXPOSED_PORT = 61616;<br>    final ArtemisContainer container = new ArtemisContainer(DOCKER_IMAGE_NAME)<br>            .withUser(&quot;user&quot;)<br>            .withPassword(&quot;password&quot;)<br>            .withExposedPorts(DEFAULT_EXPOSED_PORT);<br><br><br>    @Override<br>    public void initialize(ConfigurableApplicationContext applicationContext) {<br>        container.start();<br>        applicationContext.addApplicationListener((e) -&gt; {<br>            if (e instanceof ContextClosedEvent) {<br>                container.stop();<br>            }<br>        });<br><br>        var brokerUrlFormat = &quot;tcp://%s:%d&quot;;<br>        var brokerUrl = brokerUrlFormat.formatted(container.getHost(), container.getFirstMappedPort());<br>        log.debug(&quot;connection url is {}&quot;, brokerUrl);<br><br>        applicationContext.getEnvironment()<br>                .getPropertySources()<br>                .addLast(<br>                        new MapPropertySource(&quot;activemqProps&quot;,<br>                                Map.of(&quot;activemq.brokerUrl&quot;, brokerUrl)<br>                        )<br>                );<br>    }<br>}</pre><p>Create a test to verify sending and receiving messages with the classic JmsTemplate.</p><pre>@SpringJUnitConfig(value = {JmsTemplateTest.TestConfig.class})<br>@ContextConfiguration(initializers = {ArtemisContainerInitializer.class})<br>public class JmsTemplateTest {<br>    private final static Logger log = LoggerFactory.getLogger(JmsTemplateTest.class);<br><br>    @Configuration<br>    @Import(value = {JmsConfig.class})<br>    static class TestConfig {<br>    }<br><br>    @Autowired<br>    JmsTemplate jmsTemplate;<br><br>    @Test<br>    public void testSendAndReceive() {<br>        jmsTemplate.convertAndSend(&quot;test&quot;, &quot;hello&quot;);<br><br>        // wait to verify.<br>        await().atMost(Duration.ofMillis(1_500))<br>                .untilAsserted(() -&gt; assertThat(jmsTemplate.receiveAndConvert(&quot;test&quot;)).isEqualTo(&quot;hello&quot;));<br>    }<br><br>    @Test<br>    public void testSendAndReceive_GreetingObject() {<br>        jmsTemplate.convertAndSend(&quot;testObject&quot;, new Greeting(&quot;Hello&quot;, Instant.now()));<br><br>        // wait to verify.<br>        await().atMost(Duration.ofMillis(1_500))<br>                .untilAsserted(() -&gt; {<br>                    Object receivedObject = jmsTemplate.receiveAndConvert(&quot;testObject&quot;);<br>                    log.debug(&quot;received object: {}&quot;, receivedObject);<br><br>                    var greetingObject = (Greeting) receivedObject;<br>                    assertThat(greetingObject.body()).isEqualTo(&quot;Hello&quot;);<br>                });<br>    }<br>}</pre><p>Alternatively, you can send and receive messages with Spring Messaging specific JmsMessagingTemplate.</p><pre>@Autowired<br>JmsMessagingTemplate jmsMessagingTemplate;<br><br>@Test<br>public void testSendAndReceive() {<br>    jmsMessagingTemplate.convertAndSend(&quot;test&quot;, &quot;hello&quot;);<br><br>    // wait to verify.<br>    await().atMost(Duration.ofMillis(1_500))<br>            .untilAsserted(() -&gt; assertThat(jmsMessagingTemplate.receiveAndConvert(&quot;test&quot;, String.class)).isEqualTo(&quot;hello&quot;));<br>}<br><br>@Test<br>public void testSendAndReceive_GreetingObject() {<br>    jmsMessagingTemplate.convertAndSend(&quot;testObject&quot;, new Greeting(&quot;Hello JmsClient!&quot;, Instant.now()));<br><br>    // wait one second to verify.<br>    await().atMost(Duration.ofMillis(1_500))<br>            .untilAsserted(() -&gt; {<br>                var receivedMessage = jmsMessagingTemplate.receiveAndConvert(&quot;testObject&quot;, Greeting.class);<br>                assertThat(receivedMessage).isNotNull();<br>                log.info(&quot;Greeting messages received via JmsMessagingTemplate: {}&quot;, receivedMessage);<br>                assertThat(receivedMessage.body()).isEqualTo(&quot;Hello JmsClient!&quot;);<br>            });<br>}</pre><p>As you see, the jmsMessagingTemplate improved payload type resolves when receiving messages.</p><p>The following is the example testing code using the new JmsClient:</p><pre>@Test<br>public void testSendAndReceive() {<br>    jmsClient.destination(&quot;test&quot;).send( &quot;Hello&quot;);<br><br>    // wait to verify.<br>    await().atMost(Duration.ofMillis(1_500))<br>            .untilAsserted(() -&gt; {<br>                Optional&lt;String&gt; received = jmsClient.destination(&quot;test&quot;).receive(String.class);<br>                assertThat(received.isPresent()).isTrue();<br><br>                log.debug(&quot;Received message: {}&quot;, received.get());<br>                assertThat(received.get()).isEqualTo(&quot;Hello&quot;);<br>            });<br>}<br><br>@Test<br>public void testSendAndReceive_GreetingObject() {<br>    jmsClient.destination(&quot;testObject&quot;)<br>            .withTimeToLive(2_000)<br>            .withReceiveTimeout(1_000)<br>            .withPriority(1)<br>            .withDeliveryDelay(100)<br>            .withDeliveryPersistent(false)<br>            .send(new Greeting(&quot;Hello JmsClient!&quot;, Instant.now()));<br><br>    // wait to verify.<br>    await().atMost(Duration.ofMillis(1_500))<br>            .untilAsserted(() -&gt; {<br>                var received = jmsClient.destination(&quot;testObject&quot;)<br>                        .receive(Greeting.class);<br>                assertThat(received).isPresent();<br>                <br>                log.info(&quot;Greeting messages received: {}&quot;, received.get());<br>                assertThat(received.get().body()).isEqualTo(&quot;Hello JmsClient!&quot;);<br>            });<br>}</pre><p>In the JmsClient example, the traditional method parameters are replaced with intuitive fluent methods. You can configure the destination properties using the withXXX methods before sending operations.</p><p>With jmsListenerContainerFactory bean, both JmsTemplate and JmsMessagingTamplate work well with the listener methods, which are annotated with @JmsListener. The new JmsClient also works seamlessly with the listener methods.</p><p>For example, create a GreetingListener to consume messages with a payload type Greeting.</p><pre>@Component<br>public class GreetingListener {<br>    private final Logger log = LoggerFactory.getLogger(GreetingListener.class);<br><br>    public List&lt;Greeting&gt; received = new ArrayList&lt;&gt;();<br><br>    @JmsListener(destination = &quot;greeting&quot;)<br>    public void onMessage(Greeting message) {<br>        log.debug(&quot;receiving body: {}&quot;, message);<br>        received.add(message);<br>    }<br>}</pre><p>Use jmsClient to send a Greeting message to the destination greeting, then check the received messages in the GreetingListener.</p><pre>@Autowired<br>JmsClient jmsClient;<br><br>@Autowired<br>GreetingListener receiver;<br><br>@Test<br>public void testGreetingListener() {<br>    jmsClient.destination(&quot;greeting&quot;).send(new Greeting(&quot;Hello&quot;, Instant.now()));<br><br>    // wait to verify.<br>    await().atMost(Duration.ofMillis(1_500))<br>            .untilAsserted(() -&gt; {<br>                List&lt;Greeting&gt; received = receiver.received;<br>                log.debug(&quot;&gt;&gt;&gt; received: {}&quot;, received);<br><br>                assertThat(received.size()).isEqualTo(1);<br>                assertThat(received.getFirst().body()).isEqualTo(&quot;Hello&quot;);<br>            });<br>}</pre><h3>Spring Boot</h3><p>Generate your project skeleton from <a href="https://start.spring.io">https://start.spring.io</a>, add Spring for Apache ActiveMQ Artemis and Testcontainers in the <em>Dependencies</em>, it will include the following dependencies in your pom.xml to enable JMS autoconfiguration support with ActiveMQ Artemis.</p><pre>&lt;dependencies&gt;<br>    &lt;dependency&gt;<br>        &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;<br>        &lt;artifactId&gt;spring-boot-starter-artemis&lt;/artifactId&gt;<br>    &lt;/dependency&gt;<br>    &lt;dependency&gt;<br>        &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;<br>        &lt;artifactId&gt;spring-boot-starter-artemis-test&lt;/artifactId&gt;<br>        &lt;scope&gt;test&lt;/scope&gt;<br>    &lt;/dependency&gt;<br>&lt;/dependencies&gt;</pre><p>Additionally, it will add a Testcontainers configuration to start an Artemis server in Docker for development and test environments. The transitive dependency tree includes ativemq-artemis-client which is responsible for connecting to the running Artemis server.</p><p>Now you can inject JmsTempalate, JmsMessagingTemplate and JmsClient freely in your Spring components.</p><p>Add a JmsTemplate compatible MessageConverter to send and receive POJO messages.</p><pre>// from org.springframework.jms.support.converter<br>@Bean<br>JacksonJsonMessageConverter jacksonMessageConverter() {<br>    JacksonJsonMessageConverter messageConverter = new JacksonJsonMessageConverter();<br>    messageConverter.setTypeIdPropertyName(&quot;_type&quot;);<br>    return messageConverter;<br>}</pre><p>Do not forget to add spring-boot-starter-jackson and spring-boot-starter-jackson-test to the project dependencies to enable Jackson autoconfiguration.</p><p>Check the example code for demonstrating <a href="https://github.com/hantsy/spring7-sandbox/tree/master/jms">JmsTemplate</a>, <a href="https://github.com/hantsy/spring7-sandbox/tree/master/jms-messaging">JmsMessagingTemplate</a>, <a href="https://github.com/hantsy/spring7-sandbox/tree/master/jms-client">JmsClient</a>, and <a href="https://github.com/hantsy/spring7-sandbox/tree/master/boot-jms">all-in-one Spring Boot</a> from my Github account.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=02a5137d30c4" width="1" height="1" alt=""><hr><p><a href="https://itnext.io/an-introduction-to-spring-jmsclient-api-02a5137d30c4">An Introduction to Spring JmsClient API</a> was originally published in <a href="https://itnext.io">ITNEXT</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[An Introduction to New Spring RabbitMQ Client]]></title>
            <link>https://itnext.io/an-introduction-to-new-spring-rabbitmq-client-cbe7a83d7aa3?source=rss-bb3f4f6eaf51------2</link>
            <guid isPermaLink="false">https://medium.com/p/cbe7a83d7aa3</guid>
            <category><![CDATA[rabbitmq]]></category>
            <category><![CDATA[spring]]></category>
            <category><![CDATA[spring-boot]]></category>
            <category><![CDATA[amqp]]></category>
            <category><![CDATA[spring-amqp]]></category>
            <dc:creator><![CDATA[Hantsy]]></dc:creator>
            <pubDate>Sat, 03 Jan 2026 08:17:01 GMT</pubDate>
            <atom:updated>2026-01-03T09:19:35.360Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*fqilurw4N3PzPiMfAkXKBA.jpeg" /><figcaption>Songshan Lake, Dongguan City, China</figcaption></figure><p>Spring AMQP 4.0 brings a new module spring-rabbitmq-client which is based on the new RabbitMQ official Java client com.rabbitmq.client:amqp-client, which is aligned with AMQP 1.0 protocol. When using spring-rabbitmq-client in your projects, it is better to upgrade to RabbitMQ 4.0 to get native AMQP 1.0 support.</p><blockquote>Note</blockquote><blockquote>AMQP 0.9.1 and AMQP 1.0 are two different protocols, and RabbitMQ 3.x supports both protocols, but enabling AMQP 1.0 support requires installing an extra plugin rabbitmq_amqp1_0, RabbitMQ 4.0 switches to use AMQP 1.0 as the core protocol by default. The previous Spring RabbitMQ module spring-rabbit is still based on AMQP 0.9.1 protocol. Spring AMQP 4.1 will introduce a new generic spring-amqp-client to implement AMQP 1.0 protocol, see: <a href="https://github.com/spring-projects/spring-amqp/issues/3271">spring-amqp#3271</a></blockquote><h3>Getting Started</h3><p>Create a new simple Maven project with the basic spring-core and spring-context as dependencies, or generate a simple Spring Boot project via <a href="https://start.spring.io">https://start.spring.io</a> as <a href="https://github.com/hantsy/spring7-sandbox/blob/master/docs/jackson.md">the previous post</a>.</p><blockquote>Note</blockquote><blockquote>To date, Spring Boot 4.0 does not include a starter for autoconfiguring the Spring RabbitMQ Client; you need to add the dependency and configuration manually.</blockquote><p>Then add the following dependencies to your pom.xml file:</p><pre>&lt;dependencyManagement&gt;<br>    &lt;dependencies&gt;<br>        ...<br>        &lt;dependency&gt;<br>            &lt;groupId&gt;org.springframework.amqp&lt;/groupId&gt;<br>            &lt;artifactId&gt;spring-amqp-bom&lt;/artifactId&gt;<br>            &lt;version&gt;${spring-amqp.version}&lt;/version&gt;<br>            &lt;type&gt;pom&lt;/type&gt;<br>            &lt;scope&gt;import&lt;/scope&gt;<br>        &lt;/dependency&gt;<br>    &lt;/dependencies&gt;<br>&lt;/dependencyManagement&gt;<br>&lt;dependencies&gt;<br>    ...<br>    &lt;dependency&gt;<br>        &lt;groupId&gt;org.springframework.amqp&lt;/groupId&gt;<br>        &lt;artifactId&gt;spring-rabbitmq-client&lt;/artifactId&gt;<br>    &lt;/dependency&gt;<br>&lt;/dependencies&gt;</pre><p>Next let’s create a simple configuration class:</p><pre>@Configuration<br>@EnableRabbit<br>public class RabbitClientConfig {<br><br>    @Value(&quot;${rabbitmq.port:5672}&quot;)<br>    private int port;<br><br>    @Bean<br>    Environment environment() {<br>        return new AmqpEnvironmentBuilder()<br>                .connectionSettings()<br>                .port(port)<br>                .environmentBuilder()<br>                .build();<br>    }<br><br>    @Bean<br>    AmqpConnectionFactory amqpConnectionFactory(Environment environment) {<br>        return new SingleAmqpConnectionFactory(environment);<br>    }<br><br>    @Bean<br>    RabbitAmqpAdmin rabbitAmqpAdmin(AmqpConnectionFactory connectionFactory) {<br>        return new RabbitAmqpAdmin(connectionFactory);<br>    }<br>    <br>    @Bean<br>    RabbitAmqpTemplate rabbitAmqpTemplate(AmqpConnectionFactory connectionFactory) {<br>        RabbitAmqpTemplate rabbitAmqpTemplate = new RabbitAmqpTemplate(connectionFactory);<br>        return rabbitAmqpTemplate;<br>    }<br><br>    @Bean(RabbitListenerAnnotationBeanPostProcessor.DEFAULT_RABBIT_LISTENER_CONTAINER_FACTORY_BEAN_NAME)<br>    RabbitAmqpListenerContainerFactory rabbitAmqpListenerContainerFactory(AmqpConnectionFactory connectionFactory) {<br>        RabbitAmqpListenerContainerFactory factory = new RabbitAmqpListenerContainerFactory(connectionFactory);<br>        return factory;<br>    }<br>}</pre><p>In the above configuration class, we create the enssential beans, eg. AmqpConnectionFactory, RabbitAmqpAdmin, RabbitAmqpTemplate, and RabbitAmqpListenerContainerFactory for Spring RabbitMQ client. Make sure the Environment and AmqpEnvironmentBuilder is from package com.rabbitmq.client.amqp.</p><p>With RabbitAmqpAdmin, you can declare exchanges, queues, and bindings as usual:</p><pre>public class RabbitClientConfig {<br>    public final static String HELLO_EXCHANGE_NAME = &quot;e1&quot;;<br>    public final static String HELLO_QUEUE_NAME = &quot;q1&quot;;<br>    public final static String HELLO_ROUTING_KEY = &quot;k1&quot;;<br>    //...<br>    @Bean<br>    DirectExchange helloExchange() {<br>        return ExchangeBuilder<br>                .directExchange(HELLO_EXCHANGE_NAME)<br>                .durable(true)<br>                .build();<br>    }<br><br>    @Bean<br>    Queue helloQueue() {<br>        return QueueBuilder.durable(HELLO_QUEUE_NAME)<br>                .deadLetterExchange(&quot;dlx1&quot;)<br>                .build();<br>    }<br><br>    @Bean<br>    Binding helloBinding() {<br>        return BindingBuilder<br>                .bind(helloQueue())<br>                .to(helloExchange())<br>                .with(HELLO_ROUTING_KEY);<br>    }<br>}</pre><p>To boostrap a RabbitMQ server at runtime, add the following testcontainers dependencies in your pom.xml:</p><pre>&lt;dependency&gt;<br>    &lt;groupId&gt;org.testcontainers&lt;/groupId&gt;<br>    &lt;artifactId&gt;testcontainers-junit-jupiter&lt;/artifactId&gt;<br>    &lt;version&gt;${testcontainers.version}&lt;/version&gt;<br>    &lt;scope&gt;test&lt;/scope&gt;<br>&lt;/dependency&gt;<br>&lt;dependency&gt;<br>    &lt;groupId&gt;org.testcontainers&lt;/groupId&gt;<br>    &lt;artifactId&gt;testcontainers-rabbitmq&lt;/artifactId&gt;<br>    &lt;version&gt;${testcontainers.version}&lt;/version&gt;<br>    &lt;scope&gt;test&lt;/scope&gt;<br>&lt;/dependency&gt;</pre><p>Create a ApplicationContextInitializer to start the RabbitMQ container before Spring context is initialized:</p><pre>class RabbitContainerInitializer implements ApplicationContextInitializer&lt;@NotNull ConfigurableApplicationContext&gt; {<br>    private static final Logger log = LoggerFactory.getLogger(RabbitContainerInitializer.class);<br>    final static String DOCKER_IMAGE_NAME = &quot;rabbitmq:4-management-alpine&quot;;<br>    final RabbitMQContainer container = new RabbitMQContainer(DockerImageName.parse(DOCKER_IMAGE_NAME))<br>            .withExposedPorts(5672, 15672, 5552);<br><br>    @Override<br>    public void initialize(ConfigurableApplicationContext applicationContext) {<br>        container.start();<br>        applicationContext.addApplicationListener(e -&gt; {<br>            if (e instanceof ContextClosedEvent) {<br>                container.stop();<br>            }<br>        });<br>        log.debug(&quot;RabbitMQ container exposed ports:&quot; + container.getFirstMappedPort());<br>        applicationContext.getEnvironment()<br>                .getPropertySources()<br>                .addLast(<br>                        new MapPropertySource(&quot;rabbitProps&quot;,<br>                                Map.of(&quot;rabbitmq.port&quot;, container.getFirstMappedPort())<br>                        )<br>                );<br>    }<br>}</pre><blockquote>Note</blockquote><blockquote>Make sure you are using RabittMQ 4.x image to get AMQP 1.0 protocol support by default.</blockquote><p>Now create a test to verify sending and receiving messages:</p><pre>@SpringJUnitConfig(classes = {<br>        RabbitAmqpTemplateTest.TestConfig.class<br>})<br>@ContextConfiguration(initializers = {RabbitContainerInitializer.class})<br>public class RabbitAmqpTemplateTest {<br><br>    @Configuration<br>    @Import({RabbitClientConfig.class})<br>    static class TestConfig {<br>    }<br><br>    @Autowired<br>    RabbitAmqpTemplate rabbitAmqpTemplate;<br><br>    @Test<br>    void testSendAndReceive() throws Exception {<br>        assertThat(this.rabbitAmqpTemplate.convertAndSend(HELLO_EXCHANGE_NAME, HELLO_ROUTING_KEY, &quot;test1&quot;))<br>                .succeedsWithin(Duration.ofSeconds(10));<br><br>        assertThat(this.rabbitAmqpTemplate.receiveAndConvert(HELLO_QUEUE_NAME))<br>                .succeedsWithin(Duration.ofSeconds(10))<br>                .isEqualTo(&quot;test1&quot;);<br>    }<br>}</pre><p>The RabbitAmqpTemplate imeplemnets AsyncAmqpTemplate, so all send and receive operations are asynchronous and return CompletableFuture.</p><p>The RabbitAmqpTempalte also supports RPC-style messaging via convertSendAndReceive method:</p><pre>@Test<br>void verifyRpc() {<br>    String testRequest = &quot;rpc-request&quot;;<br>    String testReply = &quot;rpc-reply&quot;;<br><br>    CompletableFuture&lt;Object&gt; rpcClientResult = this.rabbitAmqpTemplate.convertSendAndReceive(&quot;e1&quot;, &quot;k1&quot;, testRequest);<br><br>    AtomicReference&lt;String&gt; receivedRequest = new AtomicReference&lt;&gt;();<br>    CompletableFuture&lt;Boolean&gt; rpcServerResult =<br>            this.rabbitAmqpTemplate.&lt;String, String&gt;receiveAndReply(&quot;q1&quot;,<br>                    payload -&gt; {<br>                        receivedRequest.set(payload);<br>                        return testReply;<br>                    });<br><br>    assertThat(rpcServerResult).succeedsWithin(Duration.ofSeconds(10)).isEqualTo(true);<br>    assertThat(rpcClientResult).succeedsWithin(Duration.ofSeconds(10)).isEqualTo(testReply);<br>    assertThat(receivedRequest.get()).isEqualTo(testRequest);<br>}</pre><p>We have configured a listener container bean RabbitAmqpListenerContainerFactory in the configuration class, now you can use @RabbitListener to consume messages as usual:</p><pre>@Component<br>class HelloListener {<br>    private static final Logger log = LoggerFactory.getLogger(HelloListener.class);<br>    final List&lt;String&gt; received = Collections.synchronizedList(new ArrayList&lt;&gt;());<br><br>    @RabbitListener(queues = {HELLO_QUEUE_NAME}, id = &quot;testHelloListener&quot;)<br>    void processHello(String data) {<br>        log.debug(&quot;:: received data: [{}]&quot;, data);<br>        this.received.add(data);<br>    }<br>}</pre><p>The HelloListener component will receive messages sent to the HELLO_QUEUE_NAME queue, and save them in the received list.</p><p>Let’s write a test to verify the listener works as expected:</p><pre>@SpringJUnitConfig(classes = {<br>        HelloListenerContainerTest.TestConfig.class<br>})<br>@ContextConfiguration(initializers = {RabbitContainerInitializer.class})<br>public class HelloListenerContainerTest {<br>    private final Logger log = LoggerFactory.getLogger(HelloListenerContainerTest.class);<br><br>    @Configuration<br>    @Import({<br>            RabbitClientConfig.class,<br>            HelloListener.class<br>    })<br>    static class TestConfig {<br>    }<br><br>    @Autowired<br>    RabbitAmqpTemplate rabbitAmqpTemplate;<br><br>    @Autowired<br>    HelloListener listener;<br><br>    @Test<br>    void testHello() {<br>        log.debug(&quot;Start sending message...&quot;);<br>        var initialFuture = CompletableFuture.completedFuture(true);<br>        var words = List.of(<br>                &quot;the&quot;,<br>                &quot;quick&quot;,<br>                &quot;dog&quot;,<br>                &quot;jumped&quot;,<br>                &quot;over&quot;,<br>                &quot;the&quot;,<br>                &quot;lazy&quot;,<br>                &quot;fox&quot;);<br>        for (String word : words) {<br>            initialFuture = initialFuture<br>                    .thenComposeAsync(_ -&gt; rabbitAmqpTemplate<br>                            .convertAndSend(HELLO_EXCHANGE_NAME, HELLO_ROUTING_KEY, word)<br>                            .whenComplete((res, ex) -&gt; log.debug(&quot;sent: [{}]&quot;, word))<br>                    );<br>        }<br><br>        initialFuture.join();<br><br>        Awaitility.await().atMost(Duration.ofMillis(1_000))<br>                .untilAsserted(() -&gt; {<br>                    List&lt;String&gt; received = this.listener.received;<br>                    log.debug(&quot;&gt;&gt;&gt; ackListener received: {}&quot;, received);<br><br>                    Map&lt;String, Long&gt; wordCount = received.stream().collect(Collectors.groupingBy(Function.identity(), Collectors.counting()));<br>                    log.debug(&quot;word count: {}&quot;, wordCount);<br><br>                    assertThat(wordCount.get(&quot;the&quot;)).isEqualTo(2);<br>                });<br><br>    }<br>}</pre><p>By default, the message is sent and acknowledged automatically. You can add fine-grained acknowledgement control in your listener method.</p><h3>Manual Acknowledgement</h3><p>You can set the @RabbitListener attribute ackMode to MANUAL, and customize the acknowledgement mode with listener method parameters: AmqpAcknowledgment and Consumer.Context.</p><pre>@Component<br>class AckListener {<br>    private static final Logger log = LoggerFactory.getLogger(AckListener.class);<br>    final List&lt;String&gt; received = Collections.synchronizedList(new ArrayList&lt;&gt;());<br><br>    CountDownLatch consumeIsDone = new CountDownLatch(11);<br><br>    @RabbitListener(queues = {HELLO_QUEUE_NAME},<br>              ackMode = &quot;#{T(org.springframework.amqp.core.AcknowledgeMode).MANUAL}&quot;,<br>            concurrency = &quot;2&quot;,<br>            id = &quot;testAmqpListener&quot;)<br>    void processAckManually(String data, AmqpAcknowledgment acknowledgment, Consumer.Context context) {<br>        log.debug(&quot;:: received data: [{}]&quot;, data);<br>        try {<br>            if (&quot;discard&quot;.equals(data)) {<br>                if (!this.received.contains(data)) {<br>                    log.debug(&quot;:: ack with discard&quot;);<br>                    context.discard();<br>                } else {<br>                    log.debug(&quot;:: throw new MessageConversionException&quot;);<br>                    throw new MessageConversionException(&quot;Test message is rejected&quot;);<br>                }<br>            } else if (&quot;requeue&quot;.equals(data) &amp;&amp; !this.received.contains(data)) {<br>                log.debug(&quot;:: ack with requeue&quot;);<br>                acknowledgment.acknowledge(AmqpAcknowledgment.Status.REQUEUE);<br>            } else {<br>                log.debug(&quot;:: ack with accept&quot;);<br>                acknowledgment.acknowledge();<br>            }<br>            this.received.add(data);<br>            log.debug(&quot;:: current received:{}&quot;, this.received);<br>        } finally {<br>            this.consumeIsDone.countDown();<br>        }<br>    }<br>}</pre><p>When the message payload is discard, the message is discarded without requeueing; when the payload is requeue, the message is requeued for redelivery; otherwise the message is accepted.</p><p>You can write a test to verify the manual acknowledgment works as expected:</p><pre>@SpringJUnitConfig(classes = {<br>        AckListenerContainerTest.TestConfig.class<br>})<br>@ContextConfiguration(initializers = {RabbitContainerInitializer.class})<br>public class AckListenerContainerTest {<br>    private final Logger log = LoggerFactory.getLogger(AckListenerContainerTest.class);<br><br>    @Configuration<br>    @Import({<br>            RabbitClientConfig.class,<br>            AckListener.class<br>    })<br>    static class TestConfig {<br>    }<br><br>    @Autowired<br>    RabbitAmqpTemplate rabbitAmqpTemplate;<br><br>    @Autowired<br>    AckListener ackListener;<br><br>    @Test<br>    void testAck() {<br>        log.debug(&quot;Start sending message...&quot;);<br>        var initialFuture = CompletableFuture.completedFuture(true);<br>        var words = List.of(<br>                &quot;the&quot;,<br>                &quot;discard&quot;,<br>                &quot;quick&quot;,<br>                &quot;dog&quot;,<br>                &quot;jumped&quot;,<br>                &quot;over&quot;,<br>                &quot;the&quot;,<br>                &quot;requeue&quot;,<br>                &quot;lazy&quot;,<br>                &quot;discard&quot;,<br>                &quot;fox&quot;);<br>        for (String word : words) {<br>            initialFuture = initialFuture<br>                    .thenComposeAsync(_ -&gt; rabbitAmqpTemplate<br>                            .convertAndSend(HELLO_EXCHANGE_NAME, HELLO_ROUTING_KEY, word)<br>                            .whenComplete((res, ex) -&gt; log.debug(&quot;sent: [{}]&quot;, word))<br>                    );<br>        }<br><br>        initialFuture.join();<br><br>        Awaitility.await().atMost(Duration.ofMillis(1_5000))<br>                .untilAsserted(() -&gt; {<br>                    List&lt;String&gt; received = this.ackListener.received;<br>                    log.debug(&quot;&gt;&gt;&gt; ackListener received: {}&quot;, received);<br><br>                    Map&lt;String, Long&gt; wordCount = received.stream().collect(Collectors.groupingBy(Function.identity(), Collectors.counting()));<br>                    log.debug(&quot;word count: {}&quot;, wordCount);<br><br>                    assertThat(wordCount.get(&quot;discard&quot;)).isEqualTo(1);<br>                    assertThat(wordCount.get(&quot;the&quot;)).isEqualTo(2);<br>                });<br><br>    }<br>}</pre><p>The test similarly sends a list of words to the queue, and verifies that the discard message is only received once, and the second discard message is rejected and caused an exception.</p><h3>Messaging Conversion</h3><p>Finally, to use a POJO class as the message payload, you can configure a JSON message converter with Jackson to convert the message payload between JSON strings and type-safe objects.</p><p>Add the following dependencies to your pom.xml:</p><pre>&lt;dependencies&gt;<br>    ...<br>    &lt;dependency&gt;<br>        &lt;groupId&gt;tools.jackson.core&lt;/groupId&gt;<br>        &lt;artifactId&gt;jackson-databind&lt;/artifactId&gt;<br>    &lt;/dependency&gt;<br>&lt;/dependencies&gt;</pre><p>Then delcare a JsonMapper bean in your configuration class:</p><pre>@Configuration(proxyBeanMethods = false)<br>class JacksonJsonMapperConfig {<br><br>    @Bean<br>    JsonMapper jacksonJsonMapper() {<br>        var builder = JsonMapper.builder();<br><br>        builder.changeDefaultPropertyInclusion(include -&gt; include.withValueInclusion(JsonInclude.Include.NON_NULL))<br>                .disable(SerializationFeature.FAIL_ON_EMPTY_BEANS)<br>                .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES,<br>                        DeserializationFeature.FAIL_ON_IGNORED_PROPERTIES)<br>                .enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY)<br>                .findAndAddModules();<br><br>        return builder.build();<br>    }<br>}</pre><p>Then configure a JacksonJsonMessageConverter bean and set it to the RabbitAmqpTemplate:</p><pre>public class RabbitClientConfig {<br>    //...<br>    @Bean<br>    public MessageConverter jsonMessageConverter(JsonMapper jsonMapper) {<br>        return new JacksonJsonMessageConverter(jsonMapper);<br>    }<br><br>    @Bean<br>    RabbitAmqpTemplate rabbitAmqpTemplate(AmqpConnectionFactory connectionFactory,<br>                                          MessageConverter jsonMessageConverter) {<br>        //...<br>        rabbitAmqpTemplate.setMessageConverter(jsonMessageConverter);<br>        return rabbitAmqpTemplate;<br>    }<br>}</pre><p>Now create a simple record class to present the message payload:</p><pre>public record Greeting(String body, Instant sentAt) {<br>}</pre><p>Create a test to verify sending and receiving JSON messages:</p><pre>@SpringJUnitConfig(classes = {<br>        RabbitAmqpTemplateTest.TestConfig.class<br>})<br>@ContextConfiguration(initializers = {RabbitContainerInitializer.class})<br>public class RabbitAmqpTemplateTest {<br><br>    @Configuration<br>    @Import({<br>            JacksonJsonMapperConfig.class,<br>            RabbitClientConfig.class<br>    })<br>    static class TestConfig {<br>    }<br><br>    @Autowired<br>    RabbitAmqpTemplate rabbitAmqpTemplate;<br><br>    @Test<br>    void testSendAndReceive_JSON() throws Exception {<br>        assertThat(this.rabbitAmqpTemplate.convertAndSend(HELLO_EXCHANGE_NAME, HELLO_ROUTING_KEY, new Greeting(&quot;test&quot;, Instant.now())))<br>                .succeedsWithin(Duration.ofSeconds(10));<br><br>        assertThat(this.rabbitAmqpTemplate.receiveAndConvert(HELLO_QUEUE_NAME, ParameterizedTypeReference.&lt;Greeting&gt;forType(Greeting.class)))<br>                .succeedsWithin(Duration.ofSeconds(10))<br>                .matches(it -&gt; it.body().equals(&quot;test&quot;));<br>    }<br>}</pre><p>You can also create a listener to receive JSON messages:</p><pre>@Component<br>class GreetingListener {<br>    private static final Logger log = LoggerFactory.getLogger(GreetingListener.class);<br>    final List&lt;Greeting&gt; received = new ArrayList&lt;&gt;();<br><br>    @RabbitListener(queues = RabbitClientConfig.HELLO_QUEUE_NAME,<br>            concurrency = &quot;2&quot;,<br>            id = &quot;helloListener&quot;,<br>            messageConverter = &quot;jsonMessageConverter&quot;<br>    )<br>    void handleGreeting(/*Message data*/ Greeting greeting) {<br>        log.info(&quot;Converted message payload: {}&quot;, greeting);<br>        received.add(greeting);<br>    }<br>}</pre><p>In the @RabbitListener, you have to set the messageConverter attribute to refer to the JacksonJsonMessageConverter bean. Then the payload will be converted to a Greeting object and available for automatic injection as method parameters. Otherwise, you would have to use the generic Message object and manually extract the payload data.</p><blockquote>Note</blockquote><blockquote>Unlike other rabbit listener containers, there is no messageConverter property in the RabbitAmqpListenerContainerFactory bean to apply the message conversion globally, see: <a href="https://github.com/spring-projects/spring-amqp/issues/3274">spring-projects/spring-amqp#3274</a></blockquote><p>Create a test to verify the JSON listener works as expected:</p><pre>@SpringJUnitConfig(classes = {<br>        RabbitAmqpListenerContainerTest.TestConfig.class<br>})<br>@ContextConfiguration(initializers = {RabbitContainerInitializer.class})<br>public class RabbitAmqpListenerContainerTest {<br>    private final Logger log = LoggerFactory.getLogger(RabbitAmqpListenerContainerTest.class);<br><br>    @Configuration<br>    @Import({JacksonJsonMapperConfig.class,<br>            RabbitClientConfig.class,<br>            GreetingListener.class<br>    })<br>    static class TestConfig {<br>    }<br><br>    @Autowired<br>    RabbitAmqpTemplate rabbitAmqpTemplate;<br><br>    @Autowired<br>    GreetingListener greetingListener;<br><br>    @Test<br>    void testGreetingListener() {<br>        log.debug(&quot;Start sending message...&quot;);<br>        rabbitAmqpTemplate.convertAndSend(HELLO_EXCHANGE_NAME, HELLO_ROUTING_KEY, new Greeting(&quot;Hello&quot;, Instant.now()))<br>                .whenComplete((aBoolean, throwable) -&gt; log.debug(&quot;Sending message is completed!!!&quot;));<br><br>        Awaitility.await().atMost(Duration.ofMillis(1_000))<br>                .untilAsserted(() -&gt; {<br>                    List&lt;Greeting&gt; received = greetingListener.received;<br>                    assertThat(received.size()).isEqualTo(1);<br>                    assertThat(received.getFirst().body()).isEqualTo(&quot;Hello&quot;);<br>                });<br>    }<br>}</pre><p>Grab the example code from the <a href="https://github.com/hantsy/spring7-sandbox/tree/master/rabbit-client">spring7-sandbox/rabbit-client</a> and <a href="https://github.com/hantsy/spring7-sandbox/tree/master/rabbit-client-json">spring7-sandbox/rabbit-client-json</a> projects on GitHub.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=cbe7a83d7aa3" width="1" height="1" alt=""><hr><p><a href="https://itnext.io/an-introduction-to-new-spring-rabbitmq-client-cbe7a83d7aa3">An Introduction to New Spring RabbitMQ Client</a> was originally published in <a href="https://itnext.io">ITNEXT</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[An Introduction to Jackson 3 in Spring 7 and Spring Boot 4]]></title>
            <link>https://itnext.io/an-introduction-to-jackson-3-in-spring-7-and-spring-boot-4-cba114aa36b1?source=rss-bb3f4f6eaf51------2</link>
            <guid isPermaLink="false">https://medium.com/p/cba114aa36b1</guid>
            <category><![CDATA[jackson]]></category>
            <category><![CDATA[json]]></category>
            <category><![CDATA[spring-boot]]></category>
            <category><![CDATA[spring]]></category>
            <dc:creator><![CDATA[Hantsy]]></dc:creator>
            <pubDate>Tue, 30 Dec 2025 07:41:29 GMT</pubDate>
            <atom:updated>2025-12-30T15:07:31.189Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*szOblA0AkoSWYPZPn68uKg.jpeg" /><figcaption>Songshan Lake, Dongguan City, China</figcaption></figure><p>Jackson is the defacto standard for JSON processing in Spring applications. With Spring 7 and Spring Boot 4, Jackson 3 is now the default — it modernizes the codebase for Java 17+ and also brings some breaking changes. Let’s walk through what changed and what you need to do when come to Spring 7 and Spring Boot 4.</p><blockquote>Note</blockquote><blockquote>More details about the changes in Jackson 3, refer to the official Jackson 3 <a href="https://github.com/FasterXML/jackson/wiki/Jackson-Release-3.0">Release Notes</a> and the <a href="https://github.com/FasterXML/jackson/blob/main/jackson3/MIGRATING_TO_JACKSON_3.md">Migration Guide</a>.</blockquote><p>If you’re migrating from Spring 6 to Spring 7, you’ll probably need to update your Jackson configuration — here’s one example of Jackson 2 configuration in Spring 6:</p><pre>@Configuration<br>public class Jackson2ObjectMapperConfig {<br><br>    @Bean<br>    public ObjectMapper objectMapper() {<br><br>        var builder = Jackson2ObjectMapperBuilder.json();<br>        builder.serializationInclusion(JsonInclude.Include.NON_EMPTY);<br>        builder.featuresToDisable(<br>                SerializationFeature.WRITE_DATES_AS_TIMESTAMPS,<br>                SerializationFeature.FAIL_ON_EMPTY_BEANS,<br>                DeserializationFeature.FAIL_ON_IGNORED_PROPERTIES,<br>                DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);<br>        builder.featuresToEnable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY);<br>        builder.modulesToInstall(JavaTimeModule.class)<br><br>        return builder.build();<br>    }<br>}</pre><p>The above code can be replaced with the following code using Jackson 3 in Spring 7:</p><pre>@Configuration(proxyBeanMethods = false)<br>class JacksonJsonMapperConfig {<br><br>    @Bean<br>    JsonMapper jacksonJsonMapper() {<br>        var builder = JsonMapper.builder();<br><br>        builder.changeDefaultPropertyInclusion(include -&gt; include.withValueInclusion(JsonInclude.Include.NON_NULL))<br>                .disable(SerializationFeature.FAIL_ON_EMPTY_BEANS)<br>                .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES,<br>                        DeserializationFeature.FAIL_ON_IGNORED_PROPERTIES)<br>                .enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY)<br>                .findAndAddModules();<br><br>        return builder.build();<br>    }<br>}</pre><p>A few notes on the example:</p><ul><li>In Spring 7 you’ll use the Jackson Builder to build up the configuration.</li><li>Jackson 3 reworks ObjectMapper and introduces JsonMapper for JSON work. There are also specialized mappers like YamlMapper and XmlMapper for other formats.</li><li>Dates and times are serialized as ISO‑8601 strings by default, so you don’t need to turn off WRITE_DATES_AS_TIMESTAMPS.</li></ul><blockquote>Note</blockquote><blockquote>Although Jackson 3 uses tools.jackson, it still shares the <a href="https://github.com/FasterXML/jackson-annotations">jackson-annotations</a> module with Jackson 2, which uses the legacy namespace com.fasterxml.jackson. It looks odd, but this is intentional for backward compatibility.</blockquote><p>Like the Jackson 2 Jackson2ObjectMapperBuilderCustomizer in Spring Boot 3.x, Spring Boot 4 gives you a JsonMapperBuilderCustomizer hook to tweak Jackson 3&#39;s configuration.</p><p>Let’s kick off a simple Spring Boot 4 project to try out Jackson 3.</p><p>Start a new project at <a href="https://start.spring.io/">Spring Initializr</a> with these selections:</p><ul><li>Project: Maven</li><li>Language: Java</li><li>Spring Boot: 4.0.0</li><li>Project Metadata/Java: 25</li><li>Leave the other fields as-is.</li></ul><p>Download and unzip the project, then open it in your favorite IDE.</p><p>Add spring-boot-starter-jackson and spring-boot-starter-jackson-test dependencies to your pom.xml:</p><pre>&lt;dependency&gt;<br>    &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;<br>    &lt;artifactId&gt;spring-boot-starter-jackson&lt;/artifactId&gt;    <br>&lt;/dependency&gt;<br>&lt;dependency&gt;<br>    &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;<br>    &lt;artifactId&gt;spring-boot-starter-jackson-test&lt;/artifactId&gt;<br>    &lt;scope&gt;test&lt;/scope&gt; <br>&lt;/dependency&gt;</pre><p>The spring-boot-starter-jackson gives you a Jackson 3 JsonMapper with default settings. If you want to change those defaults, create a JsonMapperBuilderCustomizer bean.</p><pre>@Bean<br>JsonMapperBuilderCustomizer jsonMapperBuilderCustomizer() {<br>    return builder -&gt; builder<br>            .changeDefaultPropertyInclusion(value -&gt; value.withContentInclusion(Include.NON_NULL))<br>            .enable(SerializationFeature.INDENT_OUTPUT)<br>            .build();<br>}</pre><p>Alternatively, configure these settings in application.properties</p><pre>spring.jackson.default-property-inclusion=non_null<br>spring.jackson.serialization.indent-output=true</pre><p>The spring-boot-starter-jackson-test sets up a minimal JSON test context for use with @JsonTest. Inject JacksonTester into your tests to assert JSON serialization and deserialization.</p><p>Here’s a simple Person example class will be used in our tests.</p><pre>record Person(<br>        String name,<br>        LocalDate birthDate,<br>        Gender gender<br>) {<br>}<br><br>enum Gender {<br>    MALE,<br>    FEMALE<br>}<br><br>@JacksonComponent<br>class CustomLocalDateSerializer {<br><br>    static DateTimeFormatter LOCAL_DATE_FORMAT = DateTimeFormatter.ofPattern(&quot;dd/MM/yyyy&quot;);<br><br>    static class LocalDateSerializer extends ValueSerializer&lt;LocalDate&gt; {<br><br>        @Override<br>        public void serialize(LocalDate value, JsonGenerator gen, SerializationContext ctxt) throws JacksonException {<br>            gen.writeString(value.format(LOCAL_DATE_FORMAT));<br>        }<br>    }<br><br>    static class LocalDateDeserializer extends ValueDeserializer&lt;LocalDate&gt; {<br><br>        @Override<br>        public LocalDate deserialize(JsonParser p, DeserializationContext ctxt) throws JacksonException {<br>            return LocalDate.parse(ctxt.readValue(p, String.class), LOCAL_DATE_FORMAT);<br>        }<br>    }<br>}<br><br>@JacksonMixin(Person.class)<br>record MyPerson(@JsonProperty(&quot;fullName&quot;) String name) {<br>}</pre><p>The example defines a custom serializer and deserializer for LocalDate and a mixin that maps the name property to fullName in JSON.</p><p>Now let’s create a test class to verify serialization and deserialization of Person.</p><pre>@JsonTest<br>class JacksonTests {<br>    private static final Logger log = LoggerFactory.getLogger(JacksonTests.class);<br><br>    @Autowired<br>    JacksonTester&lt;Person&gt; tester;<br><br>    @Autowired<br>    JsonMapper jsonMapper;<br><br>    @Test<br>    void testPersonSerialAndDeserial() throws IOException {<br>        var data = new Person(<br>                &quot;Hantsy Bai&quot;,<br>                LocalDate.of(1970, 1, 1),<br>                Gender.MALE<br>        );<br><br>        var jsonData = jsonMapper.writeValueAsString(data);<br>        log.debug(&quot;serialized json string: {} &quot;, jsonData);<br><br>        var testContents = tester.parse(jsonData);<br>        testContents.assertThat()<br>                .matches(person -&gt; person.name().equals(&quot;Hantsy Bai&quot;));<br>    }<br>}</pre><p>The test serializes a Person to JSON string with JsonMapper, and parses it with JacksonTester, and then asserts the name property.</p><p>The subsequent test verifies that the mixin is applied during deserialization and that the name field is populated as expected.</p><pre>@Test<br>void deserializedWithFullName() throws IOException {<br>    var jsonData = &quot;&quot;&quot;<br>            {<br>                &quot;fullName&quot;:&quot;Hantsy Bai&quot;,<br>                &quot;birthDate&quot;:&quot;01/01/1970&quot;,<br>                &quot;gender&quot;:&quot;MALE&quot;<br>            }<br>            &quot;&quot;&quot;.trim();<br><br>    tester.parse(jsonData)<br>            .assertThat()<br>            .hasFieldOrProperty(&quot;name&quot;)<br>            .matches(person -&gt; person.name().equals(&quot;Hantsy Bai&quot;));<br><br>}</pre><p>If you still need Jackson 2, that’s fine — add the Jackson 2 modules (jackson-module-parameter-names, jackson-datatype-jdk8, jackson-datatype-jsr310) and set spring.http.converters.preferred-json-mapper=jackson2 (Web MVC) or spring.http.codecs.preferred-json-mapper=jackson2 (WebFlux) to return back to use Jackson 2 for JSON serialization and deserialization.</p><p>One issue I encountered during the migration of my projects: Locale serialization changed. For example, Locale.CHINA used to serialize as zh_CN in Jackson 2 but is zh-CN in Jackson 3 — Jackson 3 now uses the LanguageTag format, so be aware of the difference when you parse or compare locale values.</p><p>Grab the complete example on GitHub: <a href="https://github.com/hantsy/spring7-sandbox/tree/master/boot-jackson">spring7-sandbox/boot-jackson</a>.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=cba114aa36b1" width="1" height="1" alt=""><hr><p><a href="https://itnext.io/an-introduction-to-jackson-3-in-spring-7-and-spring-boot-4-cba114aa36b1">An Introduction to Jackson 3 in Spring 7 and Spring Boot 4</a> was originally published in <a href="https://itnext.io">ITNEXT</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[First-Class Kotlin Serialization Support in Spring Boot 4]]></title>
            <link>https://itnext.io/first-class-kotlin-serialization-support-in-spring-boot-4-54a8e930c60b?source=rss-bb3f4f6eaf51------2</link>
            <guid isPermaLink="false">https://medium.com/p/54a8e930c60b</guid>
            <category><![CDATA[kotlin]]></category>
            <category><![CDATA[spring-boot]]></category>
            <category><![CDATA[spring]]></category>
            <dc:creator><![CDATA[Hantsy]]></dc:creator>
            <pubDate>Fri, 26 Dec 2025 14:03:09 GMT</pubDate>
            <atom:updated>2025-12-26T23:08:41.298Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*OCD_RI29kfMrwmT6BTfk3A.jpeg" /><figcaption>Songshan Lake, Dongguan City, China</figcaption></figure><p>Before Spring Boot 4, Spring Boot provided JSON serialization and deserialization support via Jackson, Gson, and Jakarta JSON-B; these libraries were auto-configured and worked out of the box. Kotlinx Serialization JSON was an exception — Spring Boot did not provide comparable auto-configuration. Kotlinx Serialization was only enabled when Kotlin classes were annotated with @Serializable and the kotlinx-serialization-json dependency was present on the classpath. When Jackson and Kotlinx Serialization were both present, Spring Boot generally used Jackson as the default JSON processor; however, for certain types (for example, enums or collections of enums) processing could unexpectedly switch to Kotlinx Serialization (see: <a href="https://github.com/spring-projects/spring-boot/issues/24238">spring-boot#24238</a>).</p><p>With Spring Boot 4, Kotlinx Serialization JSON is now a first-class citizen. As with Jackson, Gson, and Jakarta JSON-B, <a href="https://spring.io/blog/2025/10/28/modularizing-spring-boot">the modularized Spring Boot distribution</a> provides a standalone starter spring-boot-starter-kotlinx-serialization-json. Including this starter in a Kotlin project causes Spring Boot to enable Kotlinx Serialization support and expose the kotlinx.serialization.json.Json bean with default settings.</p><p>Let’s create a simple Spring Boot 4 Kotlin project to demonstrate Kotlinx Serialization support.</p><p>Create a Spring Boot project via <a href="https://start.spring.io/">https://start.spring.io/</a> with the following options:</p><ul><li>Language: Kotlin</li><li>Spring Boot: 4.0.1</li><li>Project type: Maven</li><li>Project metadata:</li><li>Java version: 25</li><li>Keep the other fields at their default values.</li></ul><p>Generate the project, extract the downloaded ZIP, and import it into your IDE.</p><p>Next, add the spring-boot-starter-kotlinx-serialization-json and spring-boot-starter-kotlinx-serialization-json-test to your pom.xml:</p><pre>&lt;dependency&gt;<br>    &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;<br>    &lt;artifactId&gt;spring-boot-starter-kotlinx-serialization-json&lt;/artifactId&gt;<br>&lt;/dependency&gt;<br>&lt;dependency&gt;<br>    &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;<br>    &lt;artifactId&gt;spring-boot-starter-kotlinx-serialization-json-test&lt;/artifactId&gt;<br>    &lt;scope&gt;test&lt;/scope&gt;<br>&lt;/dependency&gt;</pre><p>The spring-boot-starter-kotlinx-serialization-json starter includes an auto-configuration class that registers a Json bean preconfigured with sensible defaults.</p><p>In your pom.xml, find the kotlin-maven-plugin and add the kotlinx-serialization plugin to its configuration section, then add the org.jetbrains.kotlin:kotlin-maven-serialization dependency to the plugin&#39;s dependencies section:</p><pre>&lt;plugin&gt;<br>    &lt;groupId&gt;org.jetbrains.kotlin&lt;/groupId&gt;<br>    &lt;artifactId&gt;kotlin-maven-plugin&lt;/artifactId&gt;<br>    &lt;version&gt;${kotlin.version}&lt;/version&gt;<br>    &lt;configuration&gt;<br>        &lt;compilerPlugins&gt;<br>            ...<br>            &lt;plugin&gt;kotlinx-serialization&lt;/plugin&gt;<br>        &lt;/compilerPlugins&gt;<br>    &lt;/configuration&gt;<br>    &lt;dependencies&gt;<br>        ...<br>        &lt;dependency&gt;<br>            &lt;groupId&gt;org.jetbrains.kotlin&lt;/groupId&gt;<br>            &lt;artifactId&gt;kotlin-maven-serialization&lt;/artifactId&gt;<br>            &lt;version&gt;${kotlin.version}&lt;/version&gt;<br>        &lt;/dependency&gt;<br>    &lt;/dependencies&gt;<br>&lt;/plugin&gt;</pre><p>With this plugin, the Kotlin compiler processes @Serializable annotations at compile time.</p><blockquote>Note</blockquote><blockquote>For Gradle users, refer to the <a href="https://github.com/Kotlin/kotlinx.serialization?tab=readme-ov-file#gradle">Kotlinx Serialization documentation</a> to configure the corresponding Gradle plugin.</blockquote><p>Now, let’s create a simple data class annotated with @Serializable:</p><pre>@Serializable<br>data class Message(val body: String)</pre><p>Build the project (on Unix-like systems):</p><pre>./mvnw clean compile</pre><p>On Windows, use mvnw.cmd clean compile.</p><p>Check the compiled class for Message, open it in your IDE, or using javap command to see the contents:</p><pre>@kotlinx.serialization.Serializable public final data class Message public constructor(body: kotlin.String) {<br>    public companion object {<br>        public final fun serializer(): kotlinx.serialization.KSerializer&lt;com.example.demo.Message&gt; { /* compiled code */ }<br>    }<br>    //...<br><br>    @kotlin.Deprecated public object `$serializer` : kotlinx.serialization.internal.GeneratedSerializer&lt;com.example.demo.Message&gt; {<br>        public final val descriptor: kotlinx.serialization.descriptors.SerialDescriptor /* compiled code */<br><br>        public final fun childSerializers(): kotlin.Array&lt;kotlinx.serialization.KSerializer&lt;*&gt;&gt; { /* compiled code */ }<br><br>        public final fun deserialize(decoder: kotlinx.serialization.encoding.Decoder): com.example.demo.Message { /* compiled code */ }\\<br><br>        public final fun serialize(encoder: kotlinx.serialization.encoding.Encoder, value: com.example.demo.Message): kotlin.Unit { /* compiled code */ }<br>    }<br>}</pre><p>As shown, the Kotlin compiler generates a companion object with a serializer() method and a $serializer object that implements KSerializer. These artifacts handle serialization and deserialization for the Message class.</p><p>Next, let’s create a simple test to use Json bean to serialize and deserialize the Message class:</p><pre>@SpringBootTest<br>class DemoApplicationTests {<br><br>    @Autowired<br>    lateinit var json: Json<br><br>    @Test<br>    fun testMessage() {<br>        val encodedMessage = json.encodeToString(Message(&quot;hello world&quot;))<br>        println(&quot;encoded message: $encodedMessage&quot;)<br>        assertEquals(<br>            &quot;&quot;&quot;<br>            {<br>                &quot;body&quot;: &quot;hello world&quot;<br>            }<br>        &quot;&quot;&quot;.trimIndent(),<br>            encodedMessage<br>        )<br><br>        val decodedMessage = json.decodeFromString&lt;Message&gt;(encodedMessage)<br>        println(&quot;decoded message: $decodedMessage&quot;)<br>    }<br>}</pre><p>Spring Boot also provides a KotlinxSerializationJsonBuilderCustomizer interface to customize kotlinx.serialization.json.JsonBuilder, which is used to construct the Json bean.</p><p>For example, you can create a KotlinxSerializationJsonBuilderCustomizer bean like this:</p><pre>@OptIn(ExperimentalSerializationApi::class)<br>@Configuration(proxyBeanMethods = false)<br>class CustomConfig {<br><br>    @Bean<br>    fun kotlinxSerializationJsonBuilderCustomizer(): KotlinxSerializationJsonBuilderCustomizer {<br>        return KotlinxSerializationJsonBuilderCustomizer { builder -&gt;<br>            builder.apply {<br>                namingStrategy = JsonNamingStrategy.SnakeCase<br>                prettyPrint = true<br>                explicitNulls = false<br>                encodeDefaults = true<br>                ignoreUnknownKeys = false<br>            }<br>        }<br>    }<br><br>}</pre><p>In this example, the Json bean is configured to use a snake_case naming strategy, enable pretty printing, and set other serialization options.</p><p>Alternatively, you can also set these properties via application.properties or application.yml:</p><pre>spring.kotlinx.serialization.json.naming-strategy=snake_case<br>spring.kotlinx.serialization.json.pretty-print=true<br>spring.kotlinx.serialization.json.explicit-nulls=false<br>spring.kotlinx.serialization.json.encode-defaults=true<br>spring.kotlinx.serialization.json.ignore-unknown-keys=false</pre><blockquote>Note</blockquote><blockquote>All available configuration properties can be found in the <a href="https://docs.spring.io/spring-boot/api/java/org/springframework/boot/kotlinx/serialization/json/autoconfigure/KotlinxSerializationJsonProperties.html">KotlinxSerializationJsonProperties</a> class.</blockquote><p>Let’s have a look at another example.</p><pre>@OptIn(ExperimentalSerializationApi::class)<br>@Serializable<br>data class Person(<br>    @JsonNames(&quot;full_name&quot;) val name: String,<br><br>    @Serializable(LocalDateSerializer::class)<br>    val birthDate: LocalDate,<br><br>    val gender: Gender = Gender.MALE<br>)<br><br>enum class Gender {<br>    MALE, FEMALE;<br>}<br><br>class LocalDateSerializer : KSerializer&lt;LocalDate&gt; {<br>    private val formatter = DateTimeFormatter.ISO_LOCAL_DATE<br><br>    override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor(&quot;LocalDate&quot;, PrimitiveKind.STRING)<br>   <br>    override fun deserialize(decoder: Decoder): LocalDate {<br>        val string = decoder.decodeString()<br>        return LocalDate.parse(string, formatter)<br>    }<br><br>    override fun serialize(encoder: Encoder, value: LocalDate) {<br>        val result = value.format(formatter)<br>        encoder.encodeString(result)<br>    }<br>}</pre><p>In this example we define a Person data class, the birthDate type is LocalDate, which is from the Java Date and Time API and not supported by Kotlinx Serialization. Here we create a custom LocalDateSerializer to overcome this issue. While @SerialName renames a property in the JSON output, @JsonNames(&quot;full_name&quot;) specifies alternative field names to accept during deserialization.</p><blockquote>Note</blockquote><blockquote>Kotlinx Serialization is designed for Kotlin multiplatform, it has built-in support for <a href="https://github.com/Kotlin/kotlinx-datetime">Kotlin/kotlinx-datetime</a>.</blockquote><p>Create a test to verify Person serialization and deserialization:</p><pre>@Test<br>fun testKotlinxSerializationJson() {<br>    val encodedPerson = json.encodeToString(Person(&quot;Hantsy Bai&quot;, LocalDate.of(1970, 1, 1)))<br>    println(&quot;encoded person: $encodedPerson&quot;)<br><br>    assertEquals(<br>        &quot;&quot;&quot;<br>        {<br>            &quot;name&quot;: &quot;Hantsy Bai&quot;,<br>            &quot;birth_date&quot;: &quot;1970-01-01&quot;,<br>            &quot;gender&quot;: &quot;MALE&quot;<br>        }<br>    &quot;&quot;&quot;.trimIndent(), encodedPerson<br>    )<br><br>    val decodedPerson = json.decodeFromString&lt;Person&gt;(encodedPerson)<br>    println(&quot;decoded person: $decodedPerson&quot;)<br>    assertEquals(Gender.MALE, decodedPerson.gender)<br>}</pre><p>The following test verifies deserialization when alternative field names are used and default values are applied:</p><pre>@Test<br>fun testWithFullnameAndDefault() {<br><br>    val jsonPerson = &quot;&quot;&quot;<br>         {<br>            &quot;full_name&quot;: &quot;Hantsy Bai&quot;,<br>            &quot;birth_date&quot;: &quot;1970-01-01&quot;<br>        }<br>    &quot;&quot;&quot;.trimIndent()<br><br>    val decodedPerson2 = json.decodeFromString&lt;Person&gt;(jsonPerson)<br>    println(&quot;decoded person: $decodedPerson2&quot;)<br>    assertEquals(&quot;Hantsy Bai&quot;, decodedPerson2.name)<br>    assertEquals(Gender.MALE, decodedPerson2.gender)<br>}</pre><p>In this test we provide full_name instead of name and omit gender to verify that the default is applied during deserialization.</p><p>In Spring Boot 3.x Web MVC and WebFlux applications, mixing multiple JSON libraries sometimes required explicitly selecting the preferred mapper via spring.http.converters.preferred-json-mapper (for MVC) or spring.http.codecs.preferred-json-mapper (for WebFlux). Spring Boot 4&#39;s modular starters largely resolve these conflicts: unless the corresponding starter is present on the classpath, its auto-configuration will not be imported.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=54a8e930c60b" width="1" height="1" alt=""><hr><p><a href="https://itnext.io/first-class-kotlin-serialization-support-in-spring-boot-4-54a8e930c60b">First-Class Kotlin Serialization Support in Spring Boot 4</a> was originally published in <a href="https://itnext.io">ITNEXT</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Programmatic Bean Registration with BeanRegistrar]]></title>
            <link>https://itnext.io/programmatic-bean-registration-with-beanregistrar-7c5d7f0896e3?source=rss-bb3f4f6eaf51------2</link>
            <guid isPermaLink="false">https://medium.com/p/7c5d7f0896e3</guid>
            <category><![CDATA[spring]]></category>
            <category><![CDATA[jakarta-ee]]></category>
            <dc:creator><![CDATA[Hantsy]]></dc:creator>
            <pubDate>Thu, 25 Dec 2025 08:03:57 GMT</pubDate>
            <atom:updated>2025-12-25T14:43:40.612Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Ghjboktg0chJ8UMPo9iuvw.jpeg" /><figcaption>Guangzhou City, China</figcaption></figure><p>Prior to Spring Framework 7, beans could be registered programmatically in several ways — for example, by implementing BeanDefinitionRegistryPostProcessor or ImportBeanDefinitionRegistrar, or by manipulating the ApplicationContext directly.</p><p>A custom BeanDefinitionRegistryPostProcessor might look like this:</p><pre>public class MyBeanRegistryPostProcessor implements BeanDefinitionRegistryPostProcessor {<br><br>    @Override<br>    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {<br>        BeanDefinitionBuilder postRepositoryBeanDef = BeanDefinitionBuilder.genericBeanDefinition(PostRepository.class);<br>        registry.registerBeanDefinition(&quot;posts&quot;, postRepositoryBeanDef.getBeanDefinition());<br>    }<br>}</pre><p>This approach is somewhat verbose and requires familiarity with Spring internals.</p><p>Another example is to register beans directly with an AnnotationConfigApplicationContext which is introduced in Spring 5:</p><pre>AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();<br>PostRepository posts = new PostRepository();<br>PostHandler postHandler = new PostHandler(posts);<br>Routes routesBean = new Routes(postHandler);<br><br>context.registerBean(PostRepository.class, () -&gt; posts);<br>context.registerBean(PostHandler.class, () -&gt; postHandler);<br>context.registerBean(Routes.class, () -&gt; routesBean);<br>context.registerBean(WebHandler.class, () -&gt; RouterFunctions.toWebHandler(routesBean.routes(), HandlerStrategies.builder().build()));<br>context.refresh();</pre><p>Here is the complete project of the above code snippet, see <a href="https://github.com/hantsy/spring-reactive-sample/tree/master/register-bean">spring-reactive-sample/register-bean</a>.</p><p>However, this technique also entails considerable boilerplate.</p><p>Spring Framework 7 introduces a more concise API for programmatic bean registration via a BeanRegistrar interface. For example, you might have two implementations of a PostRepository interface — one backed by R2DBC and another using an in-memory alternative.</p><p>The following example demonstrates conditional registration based on the active profile using BeanRegistrar:</p><pre>class MyBeanRegistrar implements BeanRegistrar {<br><br>    @Override<br>    public void register(BeanRegistry registry, Environment env) {<br>        if (env.matchesProfiles(&quot;h2&quot;)) {<br>            // registry.registerBean(R2dbcConfig.class);<br>            registry.registerBean(<br>                    PostRepository.class,<br>                    (BeanRegistry.Spec&lt;PostRepository&gt; spec) -&gt; spec<br>                            .supplier(ctx -&gt; new H2PostRepository(ctx.bean(DatabaseClient.class)))<br>            );<br>        } else {<br>            registry.registerBean(<br>                    PostRepository.class,<br>                    (BeanRegistry.Spec&lt;PostRepository&gt; spec) -&gt; spec<br>                            .supplier(ctx -&gt; new InMemoryPostRepository())<br>            );<br>        }<br><br>        registry.registerBean(PostHandler.class,<br>                (BeanRegistry.Spec&lt;PostHandler&gt; spec) -&gt; spec<br>                        .supplier(ctx -&gt; new PostHandler(ctx.bean(PostRepository.class)))<br>        );<br><br>        registry.registerBean(Routes.class,<br>                (BeanRegistry.Spec&lt;Routes&gt; spec) -&gt; spec<br>                        .supplier(ctx -&gt; new Routes(ctx.bean(PostHandler.class))));<br><br>        registry.registerBean(&quot;webHandler&quot;, WebHandler.class,<br>                (BeanRegistry.Spec&lt;WebHandler&gt; spec) -&gt; spec<br>                        .prototype()<br>                        .supplier(ctx -&gt; RouterFunctions.toWebHandler(ctx.bean(Routes.class).routes(), HandlerStrategies.builder().build()))<br>        );<br>    }<br>}</pre><p>Import this custom MyBeanRegistrar into a configuration class to activate it:</p><pre>@Configuration<br>@Import(MyBeanRegistrar.class)<br>public class CustomConfig {<br>}</pre><p>BeanRegistrar provides fine-grained control over bean registration, including scope (singleton, prototype, etc.), lazy initialization, and custom suppliers for instantiation. It is also compatible with native-image compilation using GraalVM.</p><p>Annotating the test class with @ActiveProfiles(&quot;h2&quot;) activates the h2 profile, causing the H2PostRepository implementation to be registered and injected.</p><pre>@SpringJUnitConfig(classes = {CustomConfig.class, R2dbcConfig.class})<br>@ActiveProfiles(&quot;h2&quot;)<br>class H2ApplicationTest {<br><br>    @Autowired<br>    PostRepository postRepository;<br><br>    @Test<br>    void testPostRepository() {<br>        assertThat(this.postRepository).isInstanceOf(H2PostRepository.class);<br>    }<br><br>    @Test<br>    void testCurdOperations() {<br>        Post post = new Post(null, &quot;Test Title&quot;, &quot;Test Content&quot;);<br>        Mono&lt;UUID&gt; savedPostId = this.postRepository.save(post);<br><br>        savedPostId.flatMap(id -&gt; this.postRepository.findById(id))<br>                .as(StepVerifier::create)<br>                .consumeNextWith(p -&gt; {<br>                    assertThat(p.title()).isEqualTo(&quot;Test Title&quot;);<br>                    assertThat(p.content()).isEqualTo(&quot;Test Content&quot;);<br>                })<br>                .verifyComplete();<br><br>        savedPostId.flatMap(id -&gt;<br>                        this.postRepository.update(id, new Post(null, &quot;Updated Title&quot;, &quot;Updated Content&quot;))<br>                )<br>                .subscribe(updatedCount -&gt; System.out.println(&quot;Updated rows: &quot; + updatedCount));<br><br>        this.postRepository.findAll().subscribe(System.out::println);<br><br>        savedPostId.flatMap(id -&gt; this.postRepository.deleteById(id))<br>                .subscribe(System.out::println);<br><br>        this.postRepository.findAll().subscribe(System.out::println);<br>    }<br>}</pre><p>Elsewhere, the InMemoryPostRepository will be used instead.</p><pre>@SpringJUnitConfig(CustomConfig.class)<br>class ApplicationTest {<br><br>    @Autowired<br>    PostRepository postRepository;<br><br>    @Test<br>    void testPostRepository() {<br>        assertThat(this.postRepository).isInstanceOf(InMemoryPostRepository.class);<br>    }<br>}<br><br></pre><p>Spring 7 additionally offers BeanRegistrarDsl, a Kotlin extension that enables a more fluent registration style with Kotlin DSL.</p><p>Here is an example written in Kotlin and utilizing BeanRegistrarDsl:</p><pre>@Configuration<br>@Import(MyBeanRegistrar::class)<br>class CustomConfig<br><br>class MyBeanRegistrar : BeanRegistrarDsl({<br>    registerBean { InMemoryPostRepository() }<br>    registerBean { PostHandler(bean()) }<br>    registerBean { Routes(bean()) }<br><br>    registerBean(<br>        name = &quot;webHandler&quot;,<br>        backgroundInit = true,<br>        prototype = false,<br>        supplier = {<br>            RouterFunctions.toWebHandler(bean&lt;Routes&gt;().routes(), HandlerStrategies.builder().build())<br>        }<br>    )<br>    register(FoobarRegistrar())<br>})<br><br>class FoobarRegistrar : BeanRegistrarDsl({<br>    profile(&quot;foo&quot;) {<br>        registerBean&lt;Bar&gt; { Bar(bean&lt;Foo&gt;()) }<br>        registerBean { Foo() }<br>    }<br>})<br><br>class Bar(foo: Foo)<br>class Foo</pre><p>See the repository examples for complete samples:</p><ul><li><a href="https://github.com/hantsylabs/spring7-sandbox/tree/main/bean-registrar">https://github.com/hantsylabs/spring7-sandbox/tree/main/bean-registrar</a></li><li><a href="https://github.com/hantsylabs/spring7-sandbox/tree/main/bean-registrar-kotlin-dsl">https://github.com/hantsylabs/spring7-sandbox/tree/main/bean-registrar-kotlin-dsl</a></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=7c5d7f0896e3" width="1" height="1" alt=""><hr><p><a href="https://itnext.io/programmatic-bean-registration-with-beanregistrar-7c5d7f0896e3">Programmatic Bean Registration with BeanRegistrar</a> was originally published in <a href="https://itnext.io">ITNEXT</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Integrating Jakarta Data with Spring: Rinse and Repeat]]></title>
            <link>https://itnext.io/integrating-jakarta-data-with-spring-rinse-and-repeat-08a913299c13?source=rss-bb3f4f6eaf51------2</link>
            <guid isPermaLink="false">https://medium.com/p/08a913299c13</guid>
            <category><![CDATA[spring]]></category>
            <category><![CDATA[jakarta-data]]></category>
            <category><![CDATA[jakarta-ee]]></category>
            <category><![CDATA[hibernate]]></category>
            <dc:creator><![CDATA[Hantsy]]></dc:creator>
            <pubDate>Thu, 25 Dec 2025 03:51:18 GMT</pubDate>
            <atom:updated>2025-12-25T14:42:32.020Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*y-NT7lrrElUM1BzYu6sp3Q.jpeg" /><figcaption>Guangzhou City, China</figcaption></figure><p>In a previous post — <a href="https://itnext.io/integrating-jakarta-data-with-spring-0beb5c215f5f">Integrating Jakarta Data with Spring</a> — we discussed how to integrate Jakarta Data with the Spring Framework. In that post, we extracted Hibernate’s StatelessSession from Spring&#39;s LocalContainerEntityManagerFactoryBean and registered it as a Spring bean. That approach worked, but it didn&#39;t allow us to use Spring&#39;s transaction manager to control transactions. We also noted a few ad-hoc workarounds, for example, this <a href="https://gist.github.com/jelies/5181262">gist</a> and <a href="https://vladmihalcea.com/jakarta-data-spring-hibernate/">Vlad Mihalcea&#39;s writeup — How to integrate Jakarta Data with Spring and Hibernate</a>.</p><p>In Spring 7, the long-awaited support for Hibernate’s StatelessSession (see issue <a href="https://github.com/spring-projects/spring-framework/issues/7184">spring-projects/spring-framework#7184</a>) has been resovled finally. This means we can now use Spring&#39;s transaction manager to manage Jakarta Data transactions seamlessly.</p><blockquote>The code snippets below target Spring 7.0 and Hibernate 7.2. See the complete example project at <a href="https://github.com/hantsy/spring7-sandbox/tree/master/hibernate-jakarta-data">spring7-sandbox/hibernate-jakarta-data</a>.</blockquote><p>Add the following Hibernate configuration to your Spring project:</p><pre>@Configuration<br>@EnableTransactionManagement<br>@PropertySource(value = &quot;classpath:/hibernate.properties&quot;, ignoreResourceNotFound = true)<br>public class JakartaDataConfig {<br><br>    public static final Logger log = LoggerFactory.getLogger(JakartaDataConfig.class);<br><br>    private static final String ENV_HIBERNATE_DIALECT = &quot;hibernate.dialect&quot;;<br>    private static final String ENV_HIBERNATE_HBM2DDL_AUTO = &quot;hibernate.hbm2ddl.auto&quot;;<br>    private static final String ENV_HIBERNATE_SHOW_SQL = &quot;hibernate.show_sql&quot;;<br>    private static final String ENV_HIBERNATE_FORMAT_SQL = &quot;hibernate.format_sql&quot;;<br><br>    @Autowired<br>    Environment env;<br><br>    @Bean<br>    LocalSessionFactoryBean sessionFactoryBean(DataSource dataSource) {<br>        LocalSessionFactoryBean sessionFactory = new LocalSessionFactoryBean();<br>        sessionFactory.setDataSource(dataSource);<br>        sessionFactory.setPackagesToScan(&quot;com.example.demo&quot;);<br>        sessionFactory.setHibernateProperties(hibernateProperties());<br>        return sessionFactory;<br>    }<br><br>    private Properties hibernateProperties() {<br>        Properties extraProperties = new Properties();<br>        extraProperties.put(ENV_HIBERNATE_FORMAT_SQL, env.getProperty(ENV_HIBERNATE_FORMAT_SQL));<br>        extraProperties.put(ENV_HIBERNATE_SHOW_SQL, env.getProperty(ENV_HIBERNATE_SHOW_SQL));<br>        extraProperties.put(ENV_HIBERNATE_HBM2DDL_AUTO, env.getProperty(ENV_HIBERNATE_HBM2DDL_AUTO));<br><br>        if (env.getProperty(ENV_HIBERNATE_DIALECT) != null) {<br>            log.debug(&quot;Hibernate Dialect: {}&quot;, env.getProperty(ENV_HIBERNATE_DIALECT));<br>            extraProperties.put(ENV_HIBERNATE_DIALECT, env.getProperty(ENV_HIBERNATE_DIALECT));<br>        }<br><br>        return extraProperties;<br>    }<br><br>    @Bean<br>    public HibernateTransactionManager hibernateTransactionManager(SessionFactory sessionFactory) {<br>        return new HibernateTransactionManager(sessionFactory);<br>    }<br><br>}</pre><p>In the configuration above we declare a LocalSessionFactoryBean and a HibernateTransactionManager, which is the same pattern used for standard Hibernate integration with Spring. Behind the scenes, LocalSessionFactoryBean provides both Session and StatelessSession. The @EnableTransactionManagement annotation together with HibernateTransactionManager enables Spring&#39;s transaction management support.</p><p>Now create a Jakarta Data repository interface as follows:</p><pre>@jakarta.data.repository.Repository<br>@Transactional<br>public interface JakartaDataPostRepository {<br><br>    @Delete<br>    int deleteAll();<br><br>    @Save<br>    Post save(Post post);<br><br>    @Transactional(readOnly = true)<br>    @Find<br>    List&lt;Post&gt; findAll();<br><br>    @Transactional(readOnly = true)<br>    @Query(&quot;from Post p where p.title like :s and p.status=:status&quot;)<br>    List&lt;Post&gt; findByKeyword(@Param(&quot;s&quot;) String s, @Param(&quot;status&quot;) Status status, Limit limit);<br><br>    @Transactional(readOnly = true)<br>    @Find<br>    Optional&lt;Post&gt; findById(UUID id);<br>}</pre><p>We add Jakarta Data’s @Repository to mark the interface as a repository and use Spring&#39;s @Transactional to enable transaction management. We do not extend any Jakarta Data base interface; instead we use Jakarta Data&#39;s lifecycle-aware annotations on the methods.</p><blockquote>Note</blockquote><blockquote>Adding @Transactional at the repository interface level binds an active session to the current transaction. Alternatively, in a web application you can enable &quot;Open Session In View&quot; globally to keep the session open for the duration of the request.</blockquote><p>When the interface is compiled, the generated implementation looks like this:</p><pre>@Component<br>public class JakartaDataPostRepository_ implements JakartaDataPostRepository {<br>	/**<br>	 * @see #findByKeyword(String,Status,Limit)<br>	 **/<br>	static final String FIND_BY_KEYWORD_String_Status = &quot;from Post p where p.title like :s and p.status=:status&quot;;<br><br>	protected ObjectProvider&lt;StatelessSession&gt; session;<br>	<br>	public JakartaDataPostRepository_(ObjectProvider&lt;StatelessSession&gt; session) {<br>		this.session = session;<br>	}<br>	<br>	public StatelessSession session() {<br>		return session.getObject();<br>	}<br>	<br>	@Override<br>	public Post save(Post post) {<br>		requireNonNull(post, &quot;Null post&quot;);<br>		try {<br>			if (session.getObject().getIdentifier(post) == null)<br>				session.getObject().insert(post);<br>			else<br>				session.getObject().upsert(post);<br>		}<br>		catch (StaleStateException _ex) {<br>			throw new OptimisticLockingFailureException(_ex.getMessage(), _ex);<br>		}<br>		catch (PersistenceException _ex) {<br>			throw new DataException(_ex.getMessage(), _ex);<br>		}<br>		return post;<br>	}<br>//... other methods<br>}</pre><p>The generated implementation JakartaDataPostRepository_ is annotated with Spring&#39;s @Component, so it will be discovered and registered as a bean. The StatelessSession is injected via Spring&#39;s ObjectProvider, which supplies the current active session.</p><blockquote>Note</blockquote><blockquote>Don’t forget to add hibernate-processor to the compiler&#39;s annotation-processor path to enable code generation.</blockquote><p>The following test uses Spring Test and Testcontainers to verify the integration:</p><pre>@SpringJUnitConfig(classes = {<br>        JakartaDataPostRepositoryWithTestcontainersTest.TestConfig.class<br>})<br>@ContextConfiguration(initializers = PostgresContainerInitializer.class)<br>public class JakartaDataPostRepositoryWithTestcontainersTest {<br>    private final static Logger log = LoggerFactory.getLogger(JakartaDataPostRepositoryWithTestcontainersTest.class);<br><br>    @Autowired<br>    JakartaDataPostRepository posts;<br><br>    @BeforeEach<br>    public void setup() {<br>        var deleted = this.posts.deleteAll();<br>        log.debug(&quot;deleted posts: {}&quot;, deleted);<br>    }<br><br>    @Test<br>    public void testSaveAll() {<br>        var data = List.of(<br>                Post.of(&quot;test&quot;, &quot;content&quot;, Status.PENDING_MODERATION),<br>                Post.of(&quot;test1&quot;, &quot;content1&quot;, Status.DRAFT)<br>        );<br>        data.forEach(this.posts::save);<br><br>        var results = posts.findAll();<br>        assertThat(results.size()).isEqualTo(2);<br><br>        var resultsByKeyword = posts.findByKeyword(&quot;%&quot;, Status.PENDING_MODERATION, new Limit(10, 1));<br>        assertThat(resultsByKeyword.size()).isEqualTo(1);<br>    }<br><br>    @Test<br>    public void testInsertAndQuery() {<br>        var data = Post.of(&quot;test1&quot;, &quot;content1&quot;, Status.DRAFT);<br>        var saved = this.posts.save(data);<br><br>        var byId = this.posts.findById(saved.getId());<br>        assertThat(byId.isPresent()).isTrue();<br><br>        var p = byId.get();<br>        assertThat(p.getStatus()).isEqualTo(Status.DRAFT);<br>    }<br><br>    @Configuration<br>    @ComponentScan(basePackageClasses = JakartaDataPostRepository.class)<br>    @Import({DataSourceConfig.class, JakartaDataConfig.class})<br>    static class TestConfig {<br>    }<br><br>}</pre><p>In the test class we use @SpringJUnitConfig to load the Spring context and @ContextConfiguration to initialize the PostgreSQL Testcontainer. The tests verify the repository&#39;s basic CRUD operations.</p><p>For brevity we omitted the Post entity and the DataSourceConfig class, and we didn&#39;t list project dependencies. For the full example, see <a href="https://github.com/hantsylabs/spring7-sandbox/tree/main/hibernate-jakarta-data">spring7-sandbox/hibernate-jakarta-data</a>.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=08a913299c13" width="1" height="1" alt=""><hr><p><a href="https://itnext.io/integrating-jakarta-data-with-spring-rinse-and-repeat-08a913299c13">Integrating Jakarta Data with Spring: Rinse and Repeat</a> was originally published in <a href="https://itnext.io">ITNEXT</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Meet Jakarta Data: The Newest Member of the Jakarta EE 11 Ecosystem]]></title>
            <link>https://itnext.io/meet-jakarta-data-the-newest-member-of-the-jakarta-ee-11-ecosystem-16175bf50287?source=rss-bb3f4f6eaf51------2</link>
            <guid isPermaLink="false">https://medium.com/p/16175bf50287</guid>
            <category><![CDATA[glassfish]]></category>
            <category><![CDATA[wildfly]]></category>
            <category><![CDATA[jakarta-ee]]></category>
            <category><![CDATA[jakarta-data]]></category>
            <category><![CDATA[hibernate]]></category>
            <dc:creator><![CDATA[Hantsy]]></dc:creator>
            <pubDate>Sat, 29 Nov 2025 09:58:31 GMT</pubDate>
            <atom:updated>2025-11-29T20:44:01.036Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*OKUAVuva5iwzrhBX97kGJQ.jpeg" /><figcaption>huolushan Mountain, Guangzhou City, China</figcaption></figure><p><a href="https://jakarta.ee/specifications/data/1.0/">Jakarta Data</a> is a new specification in Jakarta EE 11 that simplifies data access across different storage technologies. It is storage-agnostic — not limited to Jakarta Persistence or relational databases — and is designed so providers can adapt it to a variety of backends. The <a href="https://www.jnosql.org/">Eclipse JNoSQL project</a> also implements this specification, bringing the same API surface to the NoSQL ecosystem.</p><h3>Exploring Jakarta Data</h3><p>Similar to <a href="https://github.com/spring-projects/spring-data-commons">Spring Data Commons</a>, <a href="https://github.com/micronaut-projects/micronaut-data">Micronaut Data</a>, and <a href="https://quarkus.io/guides/hibernate-orm-panache">Quarkus Panache</a>, Jakarta Data introduces a Repository abstraction. This includes a basic <a href="https://jakarta.ee/specifications/data/1.0/apidocs/jakarta.data/jakarta/data/repository/datarepository">DataRepository</a> interface to indicate a repository, as well as two interfaces, <a href="https://jakarta.ee/specifications/data/1.0/apidocs/jakarta.data/jakarta/data/repository/basicrepository">BasicRepository</a> and <a href="https://jakarta.ee/specifications/data/1.0/apidocs/jakarta.data/jakarta/data/repository/crudrepository">CrudRepository</a>, which provide common CRUD operations for underlying data storage. It also introduces a new annotation, <a href="https://jakarta.ee/specifications/data/1.0/apidocs/jakarta.data/jakarta/data/repository/repository">@Repository</a>, to mark an interface as a repository, whether it is derived from the common interfaces or is a pure interface that does not extend any existing interface.</p><p>For example, the following PostRepository interface is for the entity Post.</p><pre>@Repository<br>public interface PostRepository extends CrudRepository&lt;Post, UUID&gt; {<br>}</pre><p>In addition, Jakarta Data supports derived queries by method name conventions, pagination, and custom queries using @Query annotations. If you have experience with Spring Data or Micronaut, you will be familiar with these features.</p><pre>@Repository<br>public interface PostRepository extends CrudRepository&lt;Post, UUID&gt; {<br>    Optional&lt;Post&gt; findByTitle(String title);<br><br>    Page&lt;Post&gt; findByTitleLike(String titleLike, PageRequest pageRequest);<br><br>    @Query(&quot;where title like :title&quot;)<br>    @OrderBy(&quot;title&quot;)<br>    Post[] byTitleLike(@Param(&quot;title&quot;) String title);<br>}</pre><p>Additionally, Jakarta Data provides a collection of lifecycle annotations (<a href="https://jakarta.ee/specifications/data/1.0/apidocs/jakarta.data/jakarta/data/repository/find">Find</a>, <a href="https://jakarta.ee/specifications/data/1.0/apidocs/jakarta.data/jakarta/data/repository/insert">Insert</a>, <a href="https://jakarta.ee/specifications/data/1.0/apidocs/jakarta.data/jakarta/data/repository/update">Update</a>, <a href="https://jakarta.ee/specifications/data/1.0/apidocs/jakarta.data/jakarta/data/repository/delete">Delete</a>) that allow you to write operation methods more freely in your own interfaces. The entity type can be determined from the method parameters or the return type.</p><pre>@Repository<br>public interface Blogger {<br>    @Query(&quot;&quot;&quot;<br>            SELECT p.id, p.title FROM Post AS p<br>            WHERE p.title LIKE :title<br>            ORDER BY p.createdAt DESC<br>            &quot;&quot;&quot;)<br>    Page&lt;PostSummary&gt; allPosts(@Param(&quot;title&quot;) String title, PageRequest page);<br><br>    @Find<br>    @OrderBy(&quot;createdAt&quot;)<br>    List&lt;Post&gt; byStatus(Status status, Order&lt;Post&gt; order, Limit limit);<br><br>    @Find<br>    Optional&lt;Post&gt; byId(UUID id);<br><br>    @Insert<br>    Post insert(Post post);<br><br>    @Update<br>    Post update(Post post);<br><br>    @Delete<br>    void delete(Post post);<br>}</pre><p>Currently, Quarkus and Micronaut have already integrated Jakarta Data as an alternative persistence solution for developers. I have written articles introducing <a href="https://itnext.io/integrating-jakarta-data-with-quarkus-0d18365a86fe">the integration of Jakarta Data with Quarkus</a> and <a href="https://itnext.io/seamless-data-access-micronaut-data-embraces-jakarta-data-2f16f5a64c9e">Micronaut</a>. Spring and Spring Data have no plans to integrate Jakarta Data, but that does not mean integrating Jakarta Data with Spring is difficult. I also wrote a post about <a href="https://itnext.io/integrating-jakarta-data-with-spring-0beb5c215f5f">integrating Hibernate Data Repositories with Spring</a>.</p><p>Unlike Jakarta Persistence, Spring Data, and Micronaut Data, Jakarta Data 1.0 does not provide specific annotations to define entity types. As a result, it relies heavily on each provider’s implementation details. For example, Micronaut Data reuses Jakarta Persistence annotations as well as its own data annotations, both of which work seamlessly with Jakarta Data. Quarkus and WildFly integrate Jakarta Data via Hibernate Data repositories, so in these environments, Jakarta Persistence entities are used to represent Jakarta Data entities.</p><p>Currently, open-source Jakarta EE implementors such as GlassFish, WildFly, and Open Liberty are working on their own Jakarta Data implementations, typically leveraging entities defined with Jakarta Persistence. However, their approaches vary. WildFly (with Hibernate) translates Jakarta Data queries into Java code and generates repository implementations at compile time. In contrast, GlassFish reuses the effort from Eclipse JNoSQL and dynamically processes queries at runtime.</p><p>In this post, we’ll focus on demonstrating Jakarta Data features on standard Jakarta EE-compatible application servers, such as GlassFish, WildFly, and others.</p><p>You can <a href="https://github.com/hantsy/jakartaee11-sandbox/tree/master/data">get the example project</a> from my GitHub and explore it yourself.</p><h3>WildFly</h3><p>WildFly has provided Jakarta Data as a preview feature since version 34. In the latest WildFly 37 preview, Jakarta Data support has been updated to align with Hibernate 7 and Jakarta Persistence 3.2.</p><p>To use Jakarta Data in WildFly, configure the hibernate-processor in your Maven compiler plugin. This processes your Repository interfaces and generates implementation classes at compile time.</p><pre>&lt;plugin&gt;<br>    &lt;groupId&gt;org.apache.maven.plugins&lt;/groupId&gt;<br>    &lt;artifactId&gt;maven-compiler-plugin&lt;/artifactId&gt;<br>    &lt;version&gt;${maven-compiler-plugin.version}&lt;/version&gt;<br>    &lt;configuration&gt;<br>        &lt;annotationProcessorPaths&gt;<br>            &lt;annotationProcessorPath&gt;<br>                &lt;groupId&gt;org.projectlombok&lt;/groupId&gt;<br>                &lt;artifactId&gt;lombok&lt;/artifactId&gt;<br>                &lt;version&gt;${lombok.version}&lt;/version&gt;<br>            &lt;/annotationProcessorPath&gt;<br>            &lt;annotationProcessorPath&gt;<br>                &lt;groupId&gt;org.hibernate.orm&lt;/groupId&gt;<br>                &lt;artifactId&gt;hibernate-processor&lt;/artifactId&gt;<br>                &lt;version&gt;${hibernate.version}&lt;/version&gt;<br>            &lt;/annotationProcessorPath&gt;<br>        &lt;/annotationProcessorPaths&gt;<br>    &lt;/configuration&gt;<br>&lt;/plugin&gt;</pre><p>Open a terminal window, navigate to the project root folder, run the following command to compile the project. This will generate repository source code using the configured <em>Hibernate Processor</em>:</p><pre>mvn clean compile -Pwildfly</pre><p>The generated repository implementation classes use Hibernate’s StatelessSession to implement all methods. For example, the generated target/generated-sources/annotations/com/example/repository/PostRepository_.java file looks like this:</p><pre>/**<br> * Implements Jakarta Data repository {@link com.example.repository.PostRepository}<br> **/<br>@Dependent<br>@Generated(&quot;org.hibernate.processor.HibernateProcessor&quot;)<br>public class PostRepository_ implements PostRepository {<br>    protected @Nonnull StatelessSession session;<br><br>    public PostRepository_(@Nonnull StatelessSession session) {<br>        this.session = session;<br>    }<br><br>    public @Nonnull StatelessSession session() {<br>        return session;<br>    }<br><br>    @PersistenceUnit<br>    private EntityManagerFactory sessionFactory;<br><br>    @PostConstruct<br>    private void openSession() {<br>        session = sessionFactory.unwrap(SessionFactory.class).openStatelessSession();<br>    }<br><br>    @PreDestroy<br>    private void closeSession() {<br>        session.close();<br>    }<br><br>    @Inject<br>    PostRepository_() {<br>    }<br><br>    //... other methods<br>    /**<br>     * Find {@link Post}.<br>     *<br>     * @see com.example.repository.PostRepository#findAll(PageRequest,Order)<br>     **/<br>    @Override<br>    public Page&lt;Post&gt; findAll(PageRequest pageRequest, Order&lt;Post&gt; sortBy) {<br>        var _builder = session.getCriteriaBuilder();<br>        var _query = _builder.createQuery(Post.class);<br>        var _entity = _query.from(Post.class);<br>        _query.where(<br>        );<br>        var _spec = SelectionSpecification.create(_query);<br>        for (var _sort : sortBy.sorts()) {<br>            _spec.sort(asc(Post.class, _sort.property())<br>                        .reversedIf(_sort.isDescending())<br>                        .ignoringCaseIf(_sort.ignoreCase()));<br>        }<br>        try {<br>            long _totalResults =<br>                    pageRequest.requestTotal()<br>                            ? _spec.createQuery(session)<br>                                    .getResultCount()<br>                            : -1;<br>            var _results = _spec.createQuery(session)<br>                .setFirstResult((int) (pageRequest.page()-1) * pageRequest.size())<br>                .setMaxResults(pageRequest.size())<br>                .getResultList();<br>            return new PageRecord&lt;&gt;(pageRequest, _results, _totalResults);<br>        }<br>        catch (PersistenceException _ex) {<br>            throw new DataException(_ex.getMessage(), _ex);<br>        }<br>    }<br>}</pre><p>To run the project on a managed WildFly server, execute the following command:</p><pre>mvn clean package wildfly:run -Pwildfly</pre><p>You can find Jakarta Data usage examples in the <a href="https://github.com/hantsy/jakartaee11-sandbox/tree/master/data/src/test">testing codes</a>.</p><p>The tests are written with <a href="https://www.arquillian.org">Arquillian</a> and JUnit 5, to run the tests on the managed WildFly with the Arquillian WildFly adapter:</p><pre>mvn clean verify -Parq-wildfly-managed</pre><h3>GlassFish</h3><p>Since GlassFish 8.0.0-M14, initial Jakarta Data support has been included. But unfortunately, I still encountered some issues running these examples on GlassFish.</p><p>Firstly, we used a query result projection to a Record class for Blogger.allPosts, which is a preview feature of the future Jakarta Data 1.1. It is not available in GlassFish. We have to change it to use an entity class in the return type or wrapped type.</p><pre>@Repository<br>public interface Blogger {<br>    @Query(&quot;&quot;&quot;<br>            SELECT p FROM Post AS p<br>            WHERE p.title LIKE :title<br>            ORDER BY p.createdAt DESC<br>            &quot;&quot;&quot;)<br>    Page&lt;Post&gt; allPosts(@Param(&quot;title&quot;) String title, PageRequest page);<br>}</pre><p>Unlike WildFly, which generates implementation code at compile time via the Hibernate processor, the Jakarta Data implementation on GlassFish is provided by the JNoSQL extension <a href="https://github.com/eclipse-jnosql/jnosql-extensions/tree/main/jnosql-jakarta-persistence">JNoSQL Jakarta Persistence</a>, which handles Jakarta Data facilities at runtime.</p><p>To run the project on a managed GlassFish server, execute the following command:</p><pre>mvn clean package cargo:run -Pglassfish</pre><p>To run the integration tests on a managed GlassFish server with the Arquillian GlassFish adapter, execute the following command:</p><pre>mvn clean verify -Parq-glassfish-managed</pre><blockquote>Note</blockquote><blockquote>Currently, several tests are still failing because some fixes from the upstream JNoSQL Jakarta Persistence project have not been applied to the GlassFish repository. I have created some GitHub issues for the GlassFish project to track future updates.</blockquote><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=16175bf50287" width="1" height="1" alt=""><hr><p><a href="https://itnext.io/meet-jakarta-data-the-newest-member-of-the-jakarta-ee-11-ecosystem-16175bf50287">Meet Jakarta Data: The Newest Member of the Jakarta EE 11 Ecosystem</a> was originally published in <a href="https://itnext.io">ITNEXT</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[What’s New in Jakarta Concurrency 3.1?]]></title>
            <link>https://itnext.io/whats-new-in-jakarta-concurrency-3-1-51988b52880e?source=rss-bb3f4f6eaf51------2</link>
            <guid isPermaLink="false">https://medium.com/p/51988b52880e</guid>
            <category><![CDATA[jakarta-concurrency]]></category>
            <category><![CDATA[jakarta-ee]]></category>
            <category><![CDATA[reactive-streams]]></category>
            <dc:creator><![CDATA[Hantsy]]></dc:creator>
            <pubDate>Sat, 29 Nov 2025 09:49:37 GMT</pubDate>
            <atom:updated>2025-11-30T10:59:46.542Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*9CWNgQAWN49Pm5UKZDtFLQ.jpeg" /><figcaption>Huolushan Mountain, Guangzhou City, China</figcaption></figure><p>Jakarta Concurrency provides a standard API for managing concurrent tasks in Jakarta EE applications. It exposes managed executor services, thread factories, and context propagation helpers so that concurrent work runs with container-managed concurrency resources.</p><p><a href="https://jakarta.ee/specifications/concurrency/3.1/">Jakarta Concurrency 3.1</a> introduces several notable improvements:</p><ul><li>Integration with Java 21 virtual threads</li><li>Improved CDI support for injecting concurrency resources</li><li>A new @Schedule annotation for task scheduling</li><li>Java 9 Flow / Reactive Streams support</li></ul><p>We covered virtual threads in an earlier post — <a href="https://github.com/hantsy/jakartaee11-sandbox/blob/master/docs/vt.md">Virtual Thread Support in Jakarta EE 11</a> — and showed how to define concurrency resources with CDI @Qualifiers so they can be injected like regular CDI beans.</p><p>In this post, we’ll skip those topics and focus on the new @Schedule annotation and the Reactive Streams support.</p><h3>New @Schedule Annotation</h3><p>Legacy task scheduling has long been tied to the EJB container. Porting EJB functionalities to CDI-compatible APIs has been a long-standing effort (<a href="https://github.com/jakartaee/concurrency/issues/252">see discussion</a>). The new <a href="https://jakarta.ee/specifications/platform/11/apidocs/jakarta/enterprise/concurrent/schedule">@Schedule</a> annotation aims to replace the <a href="https://jakarta.ee/specifications/platform/11/apidocs/jakarta/ejb/schedule">EJB scheduling annotation</a> and provide a more portable, CDI-friendly mechanism.</p><p>The example below demonstrates a simple usage of @Schedule.</p><p>Suppose we need to notify team members about a recurring project meeting. The bean below uses @Schedule to trigger those notifications.</p><pre>@ApplicationScoped<br>public class StandUpMeeting {<br>    private static final Logger LOGGER = Logger.getLogger(StandUpMeeting.class.getName());<br><br>    private static final Map&lt;String, String&gt; members = Map.of(<br>            &quot;jack&quot;, &quot;jack@example.com&quot;,<br>            &quot;ross&quot;, &quot;ross@example.com&quot;<br>    );<br><br>    @Inject<br>    ManagedThreadFactory threadFactory;<br><br>    @Inject<br>    NotificationService notificationService;<br><br>    @PostConstruct<br>    public void init() {<br>        LOGGER.log(Level.ALL, &quot;init from scheduled tasks...&quot;);<br>    }<br><br>    @Asynchronous(<br>            runAt = {<br>                    @Schedule(<br>                            daysOfWeek = {<br>                                    DayOfWeek.MONDAY,<br>                                    DayOfWeek.TUESDAY,<br>                                    DayOfWeek.WEDNESDAY,<br>                                    DayOfWeek.THURSDAY,<br>                                    DayOfWeek.FRIDAY<br>                            },<br>                            hours = 8<br>                    ), // daily standup<br>                    @Schedule(daysOfMonth = {1}, hours = {12}), // monthly meeting<br>                    @Schedule(cron = &quot;*/5 * * * * *&quot;) // every 5 seconds (test)<br>            }<br>    )<br>    void sendInviteNotifications() {<br>        LOGGER.log(Level.ALL, &quot;running scheduled tasks...&quot;);<br>        try (ForkJoinPool pool = new ForkJoinPool(<br>                Runtime.getRuntime().availableProcessors(),<br>                threadFactory,<br>                (t, e) -&gt; LOGGER.log(Level.INFO, &quot;Thread: {0}, error: {1}&quot;, new Object[]{t.getName(), e.getMessage()}),<br>                true<br>        )) {<br><br>            var callables = members.keySet().stream()<br>                    .map(<br>                            name -&gt; (Callable&lt;Void&gt;) () -&gt; {<br>                                LOGGER.info(&quot;calling invite:&quot; + name);<br>                                notificationService.send(name, members.get(name));<br>                                return null;<br>                            }<br>                    )<br>                    .toList();<br><br>            var futures = pool.invokeAll(callables)<br>                    .stream()<br>                    .map(<br>                            r -&gt; {<br>                                try {<br>                                    return CompletableFuture.completedFuture(r.get(100, TimeUnit.MILLISECONDS));<br>                                } catch (InterruptedException | ExecutionException | TimeoutException e) {<br>                                    throw new CompletionException(e);<br>                                }<br>                            }<br>                    )<br>                    .toList();<br><br>            var result = CompletableFuture.allOf(futures.toArray(CompletableFuture[]::new));<br>            result.join();<br>        }<br>    }<br>}</pre><p>As you can see, the new @Schedule is currently declared inside the @Asynchronous(runAt = ...) attribute, which some developers find awkward.</p><p>The NotificationService below is a simple test service used in the example.</p><pre>@ApplicationScoped<br>public class NotificationService {<br>    private static final Logger LOGGER = Logger.getLogger(NotificationService.class.getName());<br>    private final List&lt;String&gt; names = new CopyOnWriteArrayList&lt;&gt;();<br><br>    public void send(String name, String email) {<br>        LOGGER.log(Level.INFO, &quot;sending invite to:[{0}] via {1}&quot;, new Object[]{name, email});<br>        this.names.add(name);<br>    }<br><br>    public List&lt;String&gt; getNames() {<br>        return Collections.unmodifiableList(names);<br>    }<br><br>}</pre><p>Create a REST resource that triggers the scheduled tasks:</p><pre>@RequestScoped<br>@Path(&quot;schedule&quot;)<br>public class ScheduleResources {<br><br>    @Inject<br>    NotificationService notificationService;<br><br>    @Inject<br>    StandUpMeeting meeting;<br><br>    @POST<br>    public Response invite() {<br>        meeting.sendInviteNotifications();<br>        return Response.ok().build();<br>    }<br><br>    @GET<br>    @Produces(MediaType.APPLICATION_JSON)<br>    public List&lt;String&gt; getInvitedNames() {<br>        return notificationService.getNames();<br>    }<br>}</pre><p>After deployment, you can trigger notifications with POST /schedule and view invited names with GET /schedule.</p><p>See the test ScheduleTest for a runnable example: <a href="https://github.com/hantsy/jakartaee11-sandbox/blob/master/concurrency/src/test/java/com/example/it/ScheduleTest.java">ScheduleTest</a>.</p><p>Unfortunately, the current @Schedule design has a few rough edges:</p><ul><li>It requires an external invocation to trigger scheduled tasks. That means it can not start automatically. see: <a href="https://github.com/jakartaee/concurrency/issues/624">jakartaee/concurrency#624</a></li><li>It is expressed as a nested runAt attribute inside @Asynchronous, which some find unintuitive.</li><li>Its attributes are not aligned with modern equivalents in frameworks such as Quarkus and Spring.</li><li>There is no clear replacement for the legacy timeout callback pattern for handling schedule timeouts.</li></ul><p>A cleaner, top-level scheduling annotation that adopts community best practices would be preferable. See the proposal for a standalone @Scheduled annotation: <a href="https://github.com/jakartaee/concurrency/issues/684">jakartaee/concurrency#684</a></p><h3>Reactive Streams Support</h3><p>Jakarta Concurrency 3.1 adds first-class support for the <a href="https://docs.oracle.com/javase/9/docs/api/java/util/concurrent/Flow.html">Java 9 </a><a href="https://docs.oracle.com/javase/9/docs/api/java/util/concurrent/Flow.html">Flow</a> (<a href="https://www.reactive-streams.org/">Reactive Streams</a>) API, making it easier to build asynchronous, back-pressured pipelines that interoperate with other reactive libraries.</p><p>The ContextService contains two helper methods such as <a href="https://jakarta.ee/specifications/platform/11/apidocs/jakarta/enterprise/concurrent/contextservice#contextualSubscriber(java.util.concurrent.Flow.Subscriber)">contextualSubscriber</a> and <a href="https://jakarta.ee/specifications/platform/11/apidocs/jakarta/enterprise/concurrent/contextservice#contextualProcessor(java.util.concurrent.Flow.Processor)">contextualProcessor</a>. They are used to wrap standard Flow Subscriber and Processor implementations so they execute with proper Jakarta EE context propagation (CDI, JTA, Security).</p><p>The example below demonstrates these concepts with a simple chat application that uses CDI events and Server-Sent Events (SSE) to publish messages and Redis as a backing store. A contextual subscriber is used to asynchronously process and count messages.</p><p>First, define the sample subscriber — RequestCountSubscriber:</p><pre>@ApplicationScoped<br>public class RequestCountSubscriber implements Flow.Subscriber&lt;Long&gt; {<br>    private Logger LOGGER = Logger.getLogger(RequestCountSubscriber.class.getName());<br>    final public static int MAX_REQUESTS = 2;<br><br>    Flow.Subscription subscription;<br>    int requestCount = 0;<br><br>    @Override<br>    public void onSubscribe(Flow.Subscription subscription) {<br>        LOGGER.info(&quot;onSubscribe:&quot; + subscription);<br>        this.subscription = subscription;<br>        this.subscription.request(1);<br>        this.requestCount++;<br>    }<br><br>    @Override<br>    public void onNext(Long item) {<br>        LOGGER.info(&quot;onNext:&quot; + item);<br>        if (requestCount % MAX_REQUESTS == 0) {<br>            this.subscription.request(MAX_REQUESTS);<br>        }<br>        requestCount++;<br>    }<br><br>    @Override<br>    public void onError(Throwable throwable) {<br>        LOGGER.info(&quot;onError:&quot; + throwable.getMessage());<br>        this.subscription.cancel();<br>    }<br><br>    @Override<br>    public void onComplete() {<br>        LOGGER.log(Level.INFO, &quot;onComplete: request count:{0}&quot;, new Object[]{this.requestCount});<br>    }<br>}</pre><p>Next, create a REST resource to publish messages and subscribe the message via SSE:</p><pre>@ApplicationScoped<br>@Path(&quot;chat&quot;)<br>public class ChatResource {<br><br>    @Inject<br>    ChatService chatService;<br><br>    @Context<br>    Sse sse;<br><br>    @PostConstruct<br>    public void init() {<br>        chatService.setSse(this.sse);<br>    }<br><br>    @GET<br>    @Produces(MediaType.SERVER_SENT_EVENTS)<br>    public void join(@Context SseEventSink sink) {<br>        var userId = UUID.randomUUID();<br>        chatService.register(userId, sink);<br>    }<br><br>    @DELETE<br>    @Path(&quot;{id}&quot;)<br>    public void quit(@PathParam(&quot;id&quot;) UUID id) {<br>        chatService.deregister(id);<br>    }<br><br>    @POST<br>    @Consumes(MediaType.APPLICATION_JSON)<br>    public void send(@Valid NewMessageCommand message) {<br>        chatService.send(ChatMessage.of(message.body()));<br>    }<br><br>    @GET<br>    @Path(&quot;sync&quot;)<br>    @Produces(MediaType.APPLICATION_JSON)<br>    public Response latestMessages() {<br>        return Response.ok(chatService.latest10Messages()).build();<br>    }<br><br>    @GET<br>    @Path(&quot;async&quot;)<br>    @Produces(MediaType.APPLICATION_JSON)<br>    public Response latestMessagesAsync() {<br>        return Response.ok(chatService.latest10MessagesFuture()).build();<br>    }<br><br>    @GET<br>    @Path(&quot;flow&quot;)<br>    @Produces(MediaType.APPLICATION_JSON)<br>    public Flow.Publisher&lt;ChatMessage&gt; latestMessagesFlow() {<br>        return chatService.latest10MessagesFlowPublisher();<br>    }<br>}</pre><p>Finally, implement the ChatService to handle incoming messages, store them in Redis, and use the contextual subscriber to subscribe to them. It is also responsible for sending SSE events to connected clients.</p><pre>@ApplicationScoped<br>public class ChatService {<br>    @Inject<br>    private ManagedExecutorService executor;<br><br>    @Inject<br>    private ContextService contextService;<br><br>    @Inject<br>    StatefulRedisConnection&lt;String, String&gt; redisConnection;<br><br>    @Inject<br>    Jsonb jsonb;<br><br>    @Inject<br>    Logger LOG;<br><br>    @Inject<br>    RequestCountSubscriber requestCountSubscriber;<br><br>    @Inject<br>    Event&lt;ChatMessage&gt; chatMessageEvent;<br><br>    private Sse sse;<br><br>    private final Map&lt;UUID, SseEventSink&gt; sinks = new ConcurrentHashMap&lt;&gt;();<br><br>    public void register(UUID id, SseEventSink request) {<br>        LOG.log(Level.FINEST, &quot;register request:{0}&quot;, id);<br>        sinks.put(id, request);<br>    }<br><br>    public void deregister(UUID uuid) {<br>        LOG.log(Level.FINEST, &quot;deregister request:{0}&quot;, uuid);<br>        SseEventSink eventSink = sinks.remove(uuid);<br>        try {<br>            eventSink.close();<br>            LOG.log(Level.FINEST, &quot;closing sink: {0}&quot;, eventSink);<br>        } catch (Exception e) {<br>            throw new RuntimeException(e);<br>        } finally {<br>            LOG.log(Level.ALL, &quot;closed SSE event sink&quot;);<br>        }<br>    }<br><br>    public void send(ChatMessage message) {<br>        RedisReactiveCommands&lt;String, String&gt; commands = redisConnection.reactive();<br>        commands.lpush(&quot;chat&quot;, jsonb.toJson(message))<br>                .doOnSuccess(<br>                        inserted -&gt; {<br>                            LOG.log(Level.FINEST, &quot;inserted items into redis:&quot; + inserted);<br>                            chatMessageEvent.fire(message);<br>                        }<br>                )<br>                .subscribe(<br>                        FlowAdapters.toSubscriber(<br>                                contextService.contextualSubscriber(requestCountSubscriber)<br>                        )<br>                );<br>    }<br><br>    public void onMessage(@Observes ChatMessage msg) {<br>        sinks.values()<br>                .forEach(sink -&gt; {<br>                            OutboundSseEvent outboundSseEvent = this.sse.newEventBuilder()<br>                                    .mediaType(MediaType.APPLICATION_JSON_TYPE)<br>                                    .id(UUID.randomUUID().toString())<br>                                    .name(&quot;message from cdi&quot;)<br>                                    .data(msg)<br>                                    .build();<br>                            sink.send(outboundSseEvent);<br>                        }<br>                );<br>    }<br><br>    public List&lt;ChatMessage&gt; latest10Messages() {<br>        RedisCommands&lt;String, String&gt; commands = redisConnection.sync();<br><br>        return commands.lpop(&quot;chat&quot;, 10)<br>                .stream()<br>                .map(it -&gt; jsonb.fromJson(it, ChatMessage.class))<br>                .toList();<br>    }<br><br>    public CompletableFuture&lt;List&lt;ChatMessage&gt;&gt; latest10MessagesFuture() {<br>        RedisAsyncCommands&lt;String, String&gt; commands = redisConnection.async();<br><br>        return commands.lpop(&quot;chat&quot;, 10)<br>                .thenApplyAsync(<br>                        msg -&gt; msg.stream()<br>                                .map(it -&gt; jsonb.fromJson(it, ChatMessage.class))<br>                                .toList(),<br>                        executor<br>                )<br>                .toCompletableFuture();<br>    }<br><br>    public Flow.Publisher&lt;ChatMessage&gt; latest10MessagesFlowPublisher() {<br>        RedisReactiveCommands&lt;String, String&gt; commands = redisConnection.reactive();<br><br>        Flux&lt;ChatMessage&gt; messageFlux = commands.lpop(&quot;chat&quot;, 10)<br>                .map(it -&gt; jsonb.fromJson(it, ChatMessage.class))<br>                .subscribeOn(Schedulers.fromExecutor(executor));<br><br>        return FlowAdapters.toFlowPublisher(messageFlux);<br>    }<br><br>    public void setSse(Sse sse) {<br>        this.sse = sse;<br>    }<br>}</pre><p>The RedisConnection bean is defined as follows:</p><pre>@ApplicationScoped<br>public class RedisClientProducer {<br>    private static final Logger LOGGER = Logger.getLogger(RedisClientProducer.class.getName());<br><br>    // Producer method for RedisClient<br>    @Produces<br>    @ApplicationScoped<br>    public RedisClient createRedisClient() {<br>        return RedisClient.create(&quot;redis://localhost:6379&quot;);<br>    }<br><br>    // Disposer method to close the RedisClient<br>    public void closeRedisClient(@Disposes RedisClient redisClient) {<br>        LOGGER.finest(&quot;shutdown redis client...&quot;);<br>        redisClient.shutdown();<br>    }<br><br>    @Produces<br>    @ApplicationScoped<br>    public StatefulRedisConnection&lt;String, String&gt; redisConnection(RedisClient redisClient) {<br>        return redisClient.connect();<br>    }<br><br>    public void closeConnection(@Disposes StatefulRedisConnection&lt;String, String&gt; redisConnection) {<br>        LOGGER.finest(&quot;closing redis connection...&quot;);<br>        redisConnection.close();<br>    }<br>}</pre><p>The NewMessageCommand and ChatMessage classes are simple POJOs:</p><pre>public record NewMessageCommand(<br>        @NotBlank String body<br>) {<br>}</pre><pre>public record ChatMessage(String body, LocalDateTime sentAt) {<br>    static ChatMessage of(String body) {<br>        return new ChatMessage(body, LocalDateTime.now());<br>    }<br>}</pre><p>With this setup, messages sent to the chat service are stored in Redis and broadcast to connected clients via SSE. The RequestCountSubscriber processes messages asynchronously, demonstrating Jakarta Concurrency&#39;s Reactive Streams support.</p><p>After deployment, you can interact with the service using the REST endpoints: e.g., GET /chat to join a chat conversation and track the new messages via SSE, POST /chat to send new messages, and GET /chat/sync or GET /chat/async to retrieve the latest 10 messages.</p><blockquote>Warning</blockquote><blockquote>Jakarta REST does not yet provide native Flow/Reactive Streams support, so GET /chat/flow may not work reliably on some application servers.</blockquote><p>See the complete example in this test class: <a href="https://github.com/hantsy/jakartaee11-sandbox/blob/master/concurrency/src/test/java/com/example/it/ChatResourceTest.java">ChatResourceTest</a>.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=51988b52880e" width="1" height="1" alt=""><hr><p><a href="https://itnext.io/whats-new-in-jakarta-concurrency-3-1-51988b52880e">What’s New in Jakarta Concurrency 3.1?</a> was originally published in <a href="https://itnext.io">ITNEXT</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[What’s New in Jakarta Security 4.0?]]></title>
            <link>https://itnext.io/whats-new-in-jakarta-security-4-0-7845ffd81dff?source=rss-bb3f4f6eaf51------2</link>
            <guid isPermaLink="false">https://medium.com/p/7845ffd81dff</guid>
            <category><![CDATA[glassfish]]></category>
            <category><![CDATA[jakarta-ee]]></category>
            <category><![CDATA[java]]></category>
            <dc:creator><![CDATA[Hantsy]]></dc:creator>
            <pubDate>Wed, 24 Sep 2025 15:36:37 GMT</pubDate>
            <atom:updated>2025-09-24T21:50:42.712Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*K977CwZq9MpJDAtWVaPGJA.jpeg" /><figcaption>Huizhou City, Guangdong Province, China</figcaption></figure><p>The <a href="https://jakarta.ee/specifications/security/">Jakarta Security</a> specification offers a set of user-friendly APIs for managing authentication and authorization in Jakarta EE applications. It builds upon <a href="https://jakarta.ee/specifications/authentication/">Jakarta Authentication</a> and <a href="https://jakarta.ee/specifications/authorization/">Jakarta Authorization</a>, while providing developers with enhanced control and flexibility.</p><p>Version 4.0 brings several notable improvements, including:</p><ul><li>A standardized in-memory IdentityStore</li><li>CDI qualifiers for built-in authentication mechanisms</li><li>A handler for processing multiple authentication mechanisms</li></ul><h3>In-Memory IdentityStore</h3><p>Earlier versions of the Jakarta Security implementation — <a href="https://github.com/eclipse-ee4j/soteria">Eclipse Soteria</a> already included an in-memory identity store. With version 4.0, this feature is now officially standardized as part of the Jakarta Security specification.</p><p>Here’s how you can use the in-memory IdentityStore:</p><pre>@InMemoryIdentityStoreDefinition(<br>    value = {<br>        @Credentials(callerName = &quot;admin&quot;, password = &quot;password&quot;, groups = {&quot;web&quot;, &quot;rest&quot;}),<br>        @Credentials(callerName = &quot;webuser&quot;, password = &quot;password&quot;, groups = {&quot;web&quot;}),<br>        @Credentials(callerName = &quot;restuser&quot;, password = &quot;password&quot;, groups = {&quot;rest&quot;})<br>    }<br>)<br>@DeclareRoles({&quot;web&quot;, &quot;rest&quot;})<br>@ApplicationScoped<br>public class SecurityConfig {<br>}</pre><p>In this example, two roles — web and rest are defined, along with three users: admin, webuser, and restuser. All credentials are stored in memory.</p><blockquote>Note</blockquote><blockquote>The in-memory IdentityStore is intended for testing only, as it keeps user credentials in memory. For production, it’s recommended to use a database-backed IdentityStore to securely persist user credentials.</blockquote><h3>CDI Qualifiers for Built-in Authentication Mechanisms</h3><p>To enhance CDI integration, Jakarta Security 4.0 introduces a qualifiers attribute to the built-in XXXAuthenticationMechanismDefinition annotations. This allows you to identify authentication mechanism resources as CDI beans and inject them using custom qualifiers.</p><p>For example, if you want to use both the basic authentication mechanism and a custom form-based authentication mechanism in your CDI beans, start by creating two custom qualifiers: WebAuthenticationQualifier and RestAuthenticationQualifier.</p><pre>// WebAuthenticationQualifier.java<br>@Documented<br>@Target({ElementType.TYPE, ElementType.FIELD})<br>@Retention(RetentionPolicy.RUNTIME)<br>@Qualifier<br>public @interface WebAuthenticationQualifier {<br>}<br><br>// RestAuthenticationQualifier.java<br>@Documented<br>@Target({ElementType.TYPE, ElementType.FIELD})<br>@Retention(RetentionPolicy.RUNTIME)<br>@Qualifier<br>public @interface RestAuthenticationQualifier {<br>}</pre><p>Next, declare both @BasicAuthenticationMechanismDefinition and @CustomFormAuthenticationMechanismDefinition in your SecurityConfig class, setting the qualifiers attribute to the custom qualifiers you defined:</p><pre>@BasicAuthenticationMechanismDefinition(<br>    realmName = &quot;BasicAuth&quot;,<br>    qualifiers = {RestAuthenticationQualifier.class}<br>)<br>@CustomFormAuthenticationMechanismDefinition(<br>    loginToContinue = @LoginToContinue(<br>        loginPage = &quot;/login.faces&quot;,<br>        errorPage = &quot;/login.faces?error&quot;,<br>        useForwardToLogin = false // use redirect<br>    ),<br>    qualifiers = {WebAuthenticationQualifier.class}<br>)<br>// ...<br>@ApplicationScoped<br>public class SecurityConfig {<br>}</pre><p>Now, you can inject the declared XXXAuthenticationMechanism as qualified CDI beans in your CDI beans:</p><pre>@ApplicationScoped<br>public class MultipleHttpAuthenticationMechanismHandler implements HttpAuthenticationMechanismHandler {<br>    @Inject<br>    @RestAuthenticationQualifier<br>    private HttpAuthenticationMechanism restAuthenticationMechanism;<br><br>    @Inject<br>    @WebAuthenticationQualifier<br>    private HttpAuthenticationMechanism webAuthenticationMechanism;<br>    // ...<br>}</pre><h3>Handling Multiple Authentication Mechanisms</h3><p>Previously, combining different authentication mechanisms in a Jakarta EE application was challenging using the Jakarta Security APIs. Version 4.0 introduces a new API, HttpAuthenticationMechanismHandler, which lets you handle incoming requests more flexibly.</p><pre>@Alternative<br>@Priority(APPLICATION)<br>@ApplicationScoped<br>public class MultipleHttpAuthenticationMechanismHandler implements HttpAuthenticationMechanismHandler {<br>    private static final Logger LOGGER = Logger.getLogger(MultipleHttpAuthenticationMechanismHandler.class.getName());<br><br>    @Inject<br>    @RestAuthenticationQualifier<br>    private HttpAuthenticationMechanism restAuthenticationMechanism;<br><br>    @Inject<br>    @WebAuthenticationQualifier<br>    private HttpAuthenticationMechanism webAuthenticationMechanism;<br><br>    @Override<br>    public AuthenticationStatus validateRequest(HttpServletRequest request, HttpServletResponse response, HttpMessageContext httpMessageContext) throws AuthenticationException {<br>        String path = request.getRequestURI().substring(request.getContextPath().length());<br>        LOGGER.log(Level.INFO, &quot;Request path (without context path): {0}&quot;, path);<br>        if (path.startsWith(&quot;/api&quot;)) {<br>            LOGGER.log(Level.INFO, &quot;Handling authentication using RestAuthenticationQualifier HttpAuthenticationMechanism...&quot;);<br>            return restAuthenticationMechanism.validateRequest(request, response, httpMessageContext);<br>        }<br><br>        LOGGER.log(Level.INFO, &quot;Handling authentication using WebAuthenticationQualifier HttpAuthenticationMechanism...&quot;);<br>        return webAuthenticationMechanism.validateRequest(request, response, httpMessageContext);<br>    }<br>}</pre><p>In this example, authentication handling is delegated to the injected HttpAuthenticationMechanism instances. Basic authentication is applied to URIs starting with /api, while form-based authentication is used for other web pages.</p><p>The @Alternative annotation indicates that this bean is an alternative of the built-in bean HttpAuthenticationMechanismHandler provided by the Jakarta EE container, and can replace the existing one at runtime.</p><p>To activate the MultipleHttpAuthenticationMechanismHandler bean at runtime, you can use the @Priority(APPLICATION) annotation as shown above, or configure it in the CDI <em>beans.xml</em> file as follow:</p><pre>&lt;beans ...&gt;<br>    &lt;!-- ... --&gt;<br>    &lt;alternatives&gt;<br>        &lt;class&gt;com.example.MultipleHttpAuthenticationMechanismHandler&lt;/class&gt;<br>    &lt;/alternatives&gt;<br>&lt;/beans&gt;</pre><h3>Example Project</h3><p>The example project demonstrates all the samples covered in this article and also includes additional code snippets, such as @Authenticated and @Authorized(roles), to illustrate real-world class and method level security protection.</p><p>To build and run the example project on GlassFish, use the following command:</p><pre>mvn clean package cargo:run -Pglassfish</pre><p>To test the web pages, open your browser and navigate to <a href="http://localhost:8080/security-examples/test-servlet">http://localhost:8080/security-examples/test-servlet</a>.</p><p>You’ll be redirected to the /login page. After logging in with either webuser or admin (as defined earlier), you’ll be taken back to the protected page.</p><p>To verify that REST API protection works as expected, open a terminal and run:</p><pre>curl -X GET http://localhost:8080/security-examples/api/hello</pre><p>You should receive a 401 Unauthorized error.</p><p>Then, try with the predefined restuser/password credentials:</p><pre>curl -X GET http://localhost:8080/security-examples/api/hello -u &quot;restuser:password&quot;</pre><p>You should see a 200 status code and the response from the /hello endpoint.</p><p>The example project also includes <a href="https://github.com/hantsy/jakartaee11-sandbox/blob/master/security/src/test/java/com/example/it/SecurityTest.java">a simple test written in JUnit 5 and Arquillian</a>.</p><p>Open a terminal and run the following command to execute the test:</p><pre>mvn clean verify -P&quot;arq-glassfish-managed&quot;</pre><p>You can find <a href="https://github.com/hantsy/jakartaee11-sandbox/blob/master/security">the complete example project</a> on my GitHub and explore the code locally.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=7845ffd81dff" width="1" height="1" alt=""><hr><p><a href="https://itnext.io/whats-new-in-jakarta-security-4-0-7845ffd81dff">What’s New in Jakarta Security 4.0?</a> was originally published in <a href="https://itnext.io">ITNEXT</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[What’s New in Jakarta REST 4.0]]></title>
            <link>https://itnext.io/whats-new-in-jakarta-rest-4-0-ba979d9c0cd7?source=rss-bb3f4f6eaf51------2</link>
            <guid isPermaLink="false">https://medium.com/p/ba979d9c0cd7</guid>
            <category><![CDATA[jax-rs]]></category>
            <category><![CDATA[jakarta-ee]]></category>
            <category><![CDATA[json-patch]]></category>
            <category><![CDATA[rest-api]]></category>
            <dc:creator><![CDATA[Hantsy]]></dc:creator>
            <pubDate>Thu, 31 Jul 2025 01:32:52 GMT</pubDate>
            <atom:updated>2025-07-31T14:47:50.412Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ASRcuIMlC6m4ABsjP9rG6Q.jpeg" /><figcaption>Moliugong Mountain, GuangDong, China</figcaption></figure><p>Jakarta REST 4.0 is a major update in Jakarta EE 11, with much of the work focused on housekeeping. For example, there has been significant effort to modernize the Jakarta REST TCK. Additionally, support for the ManagedBean and JAXB specifications has been removed.</p><p>For developers, there are a few notable API changes:</p><ul><li>New convenient methods for checking a header value, especially which contains a token-separated list, including <a href="https://jakarta.ee/specifications/restful-ws/4.0/apidocs/jakarta.ws.rs/jakarta/ws/rs/core/httpheaders#containsHeaderString(java.lang.String,java.util.function.Predicate)">HttpHeaders#containsHeaderString</a>, <a href="https://jakarta.ee/specifications/restful-ws/4.0/apidocs/jakarta.ws.rs/jakarta/ws/rs/client/clientrequestcontext#containsHeaderString(java.lang.String,java.util.function.Predicate)">ClientRequestContext#containsHeaderString</a>, <a href="https://jakarta.ee/specifications/restful-ws/4.0/apidocs/jakarta.ws.rs/jakarta/ws/rs/client/clientresponsecontext#containsHeaderString(java.lang.String,java.util.function.Predicate)">ClientResponseContext#containsHeaderString</a>, <a href="https://jakarta.ee/specifications/restful-ws/4.0/apidocs/jakarta.ws.rs/jakarta/ws/rs/container/containerrequestcontext#containsHeaderString(java.lang.String,java.util.function.Predicate)">ContainerRequestContext#containsHeaderString</a>, and <a href="https://jakarta.ee/specifications/restful-ws/4.0/apidocs/jakarta.ws.rs/jakarta/ws/rs/container/containerresponsecontext#containsHeaderString(java.lang.String,java.util.function.Predicate)">ContainerResponseContext#containsHeaderString</a>.</li><li>A new method, <a href="https://jakarta.ee/specifications/restful-ws/4.0/apidocs/jakarta.ws.rs/jakarta/ws/rs/core/uriinfo#getMatchedResourceTemplate()">UriInfo#getMatchedResourceTemplate</a>, to retrieve the URI template for all paths of the current request.</li><li>Added support for JSON Merge Patch.</li></ul><p>The first two are minor improvements. Let’s take a closer look at JSON Merge Patch.</p><h3>An Introduction to JSON Merge Patch</h3><p>JSON Merge Patch is defined in <a href="https://datatracker.ietf.org/doc/html/rfc7386">RFC 7386</a> as follows:</p><blockquote><em>This specification defines the JSON merge patch format and processing rules. The merge patch format is primarily intended for use with the HTTP PATCH method as a means of describing a set of modifications to a target resource’s content.</em></blockquote><p>Consider the following example JSON document:</p><pre>{<br>    &quot;title&quot;: &quot;My second article&quot;,<br>    &quot;author&quot;: {<br>        &quot;givenName&quot;: &quot;Hantsy&quot;,<br>        &quot;familyName&quot;: &quot;Bai&quot;<br>    },<br>    &quot;tags&quot;: [&quot;second&quot;, &quot;article&quot;],<br>    &quot;content&quot;: &quot;The content of my second article&quot;<br>}</pre><p>Suppose you want to update the tags to &quot;JAX-RS&quot;, &quot;RESTEasy&quot;, &quot;Jersey&quot; and change the author to &quot;Jack&quot;, &quot;Ma&quot;. You would send a request like this:</p><pre>PATCH /articles/2 HTTP/1.1<br>Host: localhost<br>Content-Type: application/merge-patch+json<br><br>{<br>    &quot;author&quot;: {<br>        &quot;givenName&quot;: &quot;Jack&quot;,<br>        &quot;familyName&quot;: &quot;Ma&quot;<br>    },<br>    &quot;tags&quot;: [&quot;JAX-RS&quot;, &quot;RESTEasy&quot;, &quot;Jersey&quot;]<br>}</pre><p>The resulting JSON document would be:</p><pre>{<br>    &quot;title&quot;: &quot;My second article&quot;,<br>    &quot;author&quot;: {<br>        &quot;givenName&quot;: &quot;Jack&quot;,<br>        &quot;familyName&quot;: &quot;Ma&quot;<br>    },<br>    &quot;tags&quot;: [&quot;JAX-RS&quot;, &quot;RESTEasy&quot;, &quot;Jersey&quot;],<br>    &quot;content&quot;: &quot;The content of my second article&quot;<br>}</pre><p>Let’s walk through a simple REST resource example to demonstrate this process in code.</p><h3>Example Project</h3><p>Assume we need to manage a collection of articles, represented by an Article class:</p><pre>// Article.java<br>public record Article(<br>        Integer id,<br>        String title,<br>        Author author,<br>        String content,<br>        List&lt;String&gt; tags,<br>        LocalDateTime publishedAt<br>) {<br>    public Article withId(int id) {<br>        return new Article(id, title, author, content, tags, publishedAt);<br>    }<br><br>    public Article withTags(List&lt;String&gt; tags) {<br>        return new Article(id, title, author, content, tags, publishedAt);<br>    }<br><br>    public Article withAuthor(Author author) {<br>        return new Article(id, title, author, content, tags, publishedAt);<br>    }<br>}<br><br>// Author.java<br>public record Author(String givenName, String familyName) {<br>}</pre><p>As mentioned in <a href="https://github.com/hantsy/jakartaee11-sandbox/blob/master/docs/record.md">Java SE Record support in Jakarta EE 11</a>, although JSON-B did not fully align with Record support in Jakarta EE 11, Eclipse Yasson already supports serialization and deserialization of records.</p><p>The ArticleRepository is a simple in-memory repository:</p><pre>@ApplicationScoped<br>public class ArticleRepository {<br>    private static final ConcurrentHashMap&lt;Integer, Article&gt; articles = new ConcurrentHashMap&lt;&gt;();<br>    private static final AtomicInteger ID_GEN = new AtomicInteger(1);<br><br>    static {<br>        var id1 = ID_GEN.getAndIncrement();<br>        articles.put(<br>            id1,<br>            new Article(id1, &quot;My first article&quot;,<br>                new Author(&quot;Hantsy&quot;, &quot;Bai&quot;),<br>                &quot;This is my first article&quot;,<br>                List.of(&quot;first&quot;, &quot;article&quot;),<br>                LocalDateTime.now())<br>        );<br>        var id2 = ID_GEN.getAndIncrement();<br>        articles.put(id2,<br>            new Article(id2, &quot;My second article&quot;,<br>                new Author(&quot;Hantsy&quot;, &quot;Bai&quot;),<br>                &quot;This is my second article&quot;,<br>                List.of(&quot;second&quot;, &quot;article&quot;),<br>                LocalDateTime.now())<br>        );<br>    }<br><br>    public List&lt;Article&gt; findAll() {<br>        return List.copyOf(articles.values());<br>    }<br><br>    public Article findById(int id) {<br>        return articles.get(id);<br>    }<br><br>    public Article save(Article article) {<br>        if (article.id() == null) {<br>            var id = ID_GEN.getAndIncrement();<br>            article = article.withId(id);<br>        }<br>        articles.put(article.id(), article);<br>        return article;<br>    }<br>}</pre><p>Now, let’s look at the ArticleResource:</p><pre>@Path(&quot;articles&quot;)<br>@RequestScoped<br>public class ArticleResource {<br><br>    @Inject<br>    ArticleRepository repository;<br><br>    Jsonb jsonb;<br><br>    @PostConstruct<br>    public void init() {<br>        jsonb = JsonbBuilder.create();<br>    }<br><br>    @GET<br>    public Response getArticles() {<br>        return Response.ok(repository.findAll()).build();<br>    }<br><br>    @GET<br>    @Path(&quot;{id}&quot;)<br>    public Response getArticle(@PathParam(&quot;id&quot;) Integer id) {<br>        return Response.ok(repository.findById(id)).build();<br>    }<br><br>    @POST<br>    @Consumes(MediaType.APPLICATION_JSON)<br>    public Response createArticle(Article article) {<br>        var saved = repository.save(article);<br>        return Response.created(URI.create(&quot;/articles/&quot; + saved.id())).build();<br>    }<br><br>    @PATCH<br>    @Consumes(MediaType.APPLICATION_JSON_PATCH_JSON)<br>    public Response saveOrUpdateAllArticles(JsonArray patch) {<br>        var all = repository.findAll();<br>        var result = Json.createPatch(patch)<br>            .apply(Json.createReader(new StringReader(jsonb.toJson(all))).readArray());<br>        List&lt;Article&gt; articles = jsonb.fromJson(<br>            jsonb.toJson(result),<br>            new ArrayList&lt;Article&gt;() {}.getClass().getGenericSuperclass()<br>        );<br>        articles.forEach(repository::save);<br><br>        return Response.noContent().build();<br>    }<br><br>    @PATCH<br>    @Path(&quot;{id}&quot;)<br>    @Consumes(MediaType.APPLICATION_JSON_PATCH_JSON)<br>    public Response updateArticle(@PathParam(&quot;id&quot;) Integer id, JsonArray patch) {<br>        var target = repository.findById(id);<br>        var patchedResult = Json.createPatch(patch)<br>            .apply(Json.createReader(new StringReader(jsonb.toJson(target))).readObject());<br>        var article = jsonb.fromJson(jsonb.toJson(patchedResult), Article.class);<br>        repository.save(article);<br><br>        return Response.noContent().build();<br>    }<br><br>    @PATCH<br>    @Path(&quot;{id}&quot;)<br>    //@Consumes(MediaType.APPLICATION_MERGE_PATCH_JSON) // added in 4.0<br>    @Consumes(&quot;application/merge-patch+json&quot;)<br>    public Response mergeArticle(@PathParam(&quot;id&quot;) Integer id, JsonObject patch) {<br>        var targetArticle = repository.findById(id);<br>        var mergedResult = Json.createMergePatch(patch)<br>            .apply(Json.createReader(new StringReader(jsonb.toJson(targetArticle))).readObject());<br>        var article = jsonb.fromJson(jsonb.toJson(mergedResult), Article.class);<br>        repository.save(article);<br><br>        return Response.noContent().build();<br>    }<br>}</pre><p>For comparison, we also include two JSON Patch (defined by <a href="https://datatracker.ietf.org/doc/html/rfc6902">RFC 6902</a> and implemented in Java EE 8/JAX-RS 2.1) example endpoints: one for processing an array of operations, and another for handling a single resource entity.</p><p>Let’s create an Arquillian test to verify the functionality:</p><pre>@ExtendWith(ArquillianExtension.class)<br>@TestMethodOrder(MethodOrderer.OrderAnnotation.class)<br>public class ArticleResourceTest {<br><br>    private static final Logger LOGGER = Logger.getLogger(ArticleResourceTest.class.getName());<br><br>    @Deployment(testable = false)<br>    public static WebArchive createDeployment() {<br>        File[] extraJars = Maven<br>            .resolver()<br>            .loadPomFromFile(&quot;pom.xml&quot;)<br>            .importCompileAndRuntimeDependencies()<br>            .resolve(&quot;org.assertj:assertj-core&quot;)<br>            .withTransitivity()<br>            .asFile();<br>        var war = ShrinkWrap.create(WebArchive.class, &quot;test.war&quot;)<br>            .addAsLibraries(extraJars)<br>            .addClasses(<br>                ArticleResource.class,<br>                Article.class,<br>                Author.class,<br>                ArticleRepository.class,<br>                // jaxrs config<br>                JsonbContextResolver.class,<br>                RestActivator.class<br>            )<br>            .addAsWebInfResource(EmptyAsset.INSTANCE, &quot;beans.xml&quot;);<br>        LOGGER.log(Level.INFO, &quot;war deployment: {0}&quot;, war.toString(true));<br>        return war;<br>    }<br><br>    @ArquillianResource<br>    private URL baseUrl;<br><br>    Client client;<br><br>    private Jsonb jsonb = JsonbBuilder.create();<br><br>    @BeforeEach<br>    public void before() {<br>        LOGGER.log(Level.INFO, &quot;baseURL: {0}&quot;, baseUrl.toExternalForm());<br>        client = ClientBuilder.newClient();<br>        client.register(JsonbContextResolver.class);<br>    }<br><br>    @AfterEach<br>    public void after() {<br>        client.close();<br>    }<br><br>    @Test<br>    @RunAsClient<br>    @Order(1)<br>    public void testGetArticles() {<br>        var target = client.target(URI.create(baseUrl.toExternalForm() + &quot;api/articles&quot;));<br>        List&lt;Article&gt; articleList;<br>        try (Response r = target.request().accept(MediaType.APPLICATION_JSON_TYPE).get()) {<br>            LOGGER.log(Level.INFO, &quot;Get response status: {0}&quot;, r.getStatus());<br>            assertEquals(200, r.getStatus());<br>            articleList = r.readEntity(new GenericType&lt;&gt;() {});<br>            LOGGER.log(Level.INFO, &quot;all articles: {0}&quot;, articleList);<br>            assertThat(articleList.size()).isEqualTo(2);<br>        }<br><br>        // Apply JSON Patch<br>        var patch = Json.createPatchBuilder()<br>            .replace(&quot;/1/content&quot;, &quot;Updated by JsonPatch&quot;)<br>            .remove(&quot;/1/author/familyName&quot;)<br>            .add(&quot;/1/tags/1&quot;, &quot;JAX-RS&quot;)<br>            .build().toJsonArray();<br><br>        var target2 = client<br>            .property(HttpUrlConnectorProvider.SET_METHOD_WORKAROUND, true)<br>            .target(URI.create(baseUrl.toExternalForm() + &quot;api/articles&quot;));<br>        try (Response r2 = target2<br>            .request()<br>            .method(&quot;PATCH&quot;, Entity.entity(patch, MediaType.APPLICATION_JSON_PATCH_JSON_TYPE))) {<br>            LOGGER.log(Level.INFO, &quot;patch response status: {0}&quot;, r2.getStatus());<br>            assertEquals(204, r2.getStatus());<br>        }<br><br>        // Verify the patched result<br>        try (Response r = target.request().accept(MediaType.APPLICATION_JSON_TYPE).get()) {<br>            LOGGER.log(Level.INFO, &quot;Get response status after applying patch: {0}&quot;, r.getStatus());<br>            assertEquals(200, r.getStatus());<br>            articleList = r.readEntity(new GenericType&lt;&gt;() {});<br>            LOGGER.log(Level.INFO, &quot;all articles after applying patch: {0}&quot;, articleList);<br>            assertThat(articleList.size()).isEqualTo(2);<br>        }<br>    }<br><br>    @Test<br>    @RunAsClient<br>    @Order(2)<br>    public void testGetArticleById() {<br>        var target = client.target(URI.create(baseUrl.toExternalForm() + &quot;api/articles/1&quot;));<br>        try (Response r = target.request().accept(MediaType.APPLICATION_JSON_TYPE).get()) {<br>            LOGGER.log(Level.INFO, &quot;Get response status: {0}&quot;, r.getStatus());<br>            assertEquals(200, r.getStatus());<br>            Article article = r.readEntity(Article.class);<br>            LOGGER.log(Level.INFO, &quot;get article by id: {0}&quot;, article);<br>            assertThat(article.title()).isEqualTo(&quot;My first article&quot;);<br>        }<br><br>        var patch = Json.createPatchBuilder()<br>            .replace(&quot;/title&quot;, &quot;My title updated by JsonPatch&quot;)<br>            .build().toJsonArray();<br><br>        var target2 = client<br>            .property(HttpUrlConnectorProvider.SET_METHOD_WORKAROUND, true)<br>            .target(URI.create(baseUrl.toExternalForm() + &quot;api/articles/1&quot;));<br>        try (Response r2 = target2<br>            .request()<br>            .method(&quot;PATCH&quot;, Entity.entity(patch, MediaType.APPLICATION_JSON_PATCH_JSON_TYPE))) {<br>            LOGGER.log(Level.INFO, &quot;patch response status: {0}&quot;, r2.getStatus());<br>            assertEquals(204, r2.getStatus());<br>        }<br><br>        // Verify the patched result<br>        try (Response r = target.request().accept(MediaType.APPLICATION_JSON_TYPE).get()) {<br>            LOGGER.log(Level.INFO, &quot;Get response status after applying patch: {0}&quot;, r.getStatus());<br>            assertEquals(200, r.getStatus());<br>            Article article = r.readEntity(Article.class);<br>            LOGGER.log(Level.INFO, &quot;get article by id after applying patch: {0}&quot;, article);<br>            assertThat(article.title()).isEqualTo(&quot;My title updated by JsonPatch&quot;);<br>        }<br>    }<br><br>    @Test<br>    @RunAsClient<br>    @Order(3)<br>    public void testGetArticleByIdAndMergePatch() {<br>        var target = client.target(URI.create(baseUrl.toExternalForm() + &quot;api/articles/2&quot;));<br>        Article article = null;<br>        try (Response r = target.request().accept(MediaType.APPLICATION_JSON_TYPE).get()) {<br>            LOGGER.log(Level.INFO, &quot;Get response status: {0}&quot;, r.getStatus());<br>            assertEquals(200, r.getStatus());<br>            article = r.readEntity(Article.class);<br>            LOGGER.log(Level.INFO, &quot;get article by id: {0}&quot;, article);<br>            assertThat(article.title()).isEqualTo(&quot;My second article&quot;);<br>        }<br><br>        var updated = article.withTags(List.of(&quot;JAX-RS&quot;, &quot;RESTEasy&quot;, &quot;Jersey&quot;))<br>            .withAuthor(new Author(&quot;Jack&quot;, &quot;Ma&quot;));<br>        var patch = Json.createMergeDiff(<br>            Json.createReader(new StringReader(jsonb.toJson(article))).readObject(),<br>            Json.createReader(new StringReader(jsonb.toJson(updated))).readObject()<br>        ).toJsonValue();<br><br>        var target2 = client<br>            .property(HttpUrlConnectorProvider.SET_METHOD_WORKAROUND, true)<br>            .target(URI.create(baseUrl.toExternalForm() + &quot;api/articles/2&quot;));<br>        try (Response r2 = target2<br>            .request()<br>            .method(&quot;PATCH&quot;, Entity.entity(patch, &quot;application/merge-patch+json&quot;))) {<br>            LOGGER.log(Level.INFO, &quot;patch response status: {0}&quot;, r2.getStatus());<br>            assertEquals(204, r2.getStatus());<br>        }<br><br>        // Verify the patched result<br>        try (Response r = target.request().accept(MediaType.APPLICATION_JSON_TYPE).get()) {<br>            LOGGER.log(Level.INFO, &quot;Get response status after applying patch: {0}&quot;, r.getStatus());<br>            assertEquals(200, r.getStatus());<br>            article = r.readEntity(Article.class);<br>            LOGGER.log(Level.INFO, &quot;get article by id after applying patch: {0}&quot;, article);<br>            assertThat(article.title()).isEqualTo(&quot;My second article&quot;);<br>            assertThat(article.tags()).isEqualTo(List.of(&quot;JAX-RS&quot;, &quot;RESTEasy&quot;, &quot;Jersey&quot;));<br>            assertThat(article.author()).isEqualTo(new Author(&quot;Jack&quot;, &quot;Ma&quot;));<br>        }<br>    }<br>}</pre><p>In the above test:</p><ul><li>The deployment is marked as <em>testable</em>, meaning the test runs as a client and interacts with the service deployed in the test archive.</li><li>After deployment, the @ArquillianResource-annotated URL provides the application&#39;s base URL, including the ApplicationPath defined in the Application class, ending with a /.</li><li>We set .property(HttpUrlConnectorProvider.SET_METHOD_WORKAROUND, true) to ensure the custom PATCH method works correctly with the current Jakarta REST Client API.</li></ul><p>Let’s focus on the testGetArticleByIdAndMergePatch test method, which demonstrates the JSON Merge Patch functionality:</p><ul><li>First, retrieve the resource.</li><li>Modify it and use Json.createMergeDiff to create a patch JsonObject.</li><li>Apply the patch to the remote resource.</li><li>Finally, retrieve the resource again to verify that the patch was applied successfully.</li></ul><blockquote>Warning</blockquote><blockquote>The Jakarta REST Client API does not provide a patch() method, similar to the existing get() or post(). See the related discussion: <a href="https://github.com/jakartaee/rest/issues/1276">jakartaee/rest#1276</a>.</blockquote><p>Get the <a href="https://github.com/hantsy/jakartaee11-sandbox/tree/master/rest">complete example project</a> from my GitHub repository.</p><h3>Final Thoughts</h3><p>Over the past decade, I have developed many backend RESTful API applications. However, I have noticed a growing trend: more customers are choosing Spring WebMvc or WebFlux as their preferred frameworks over Jakarta REST. While libraries and frameworks like RESTEasy and Quarkus help fill some gaps, Jakarta REST itself has evolved slowly. Features like JSON Patch and the new JSON Merge Patch introduced in this version are rarely used in real-world RESTful API development. Even Spring once incubated a project called <strong>Spring Sync</strong> to address similar needs, but it has since been abandoned.</p><p>In my view, since version 2.1, Jakarta REST has not delivered significant features that boost developer productivity. The following is my wishlist for the next generation of Jakarta REST.</p><ul><li>Deprecating Resource/Context injection in favor of CDI @Inject (<a href="https://github.com/jakartaee/rest/issues/951">jakartaee/rest#951</a>, <a href="https://github.com/jakartaee/rest/issues/569">jakartaee/rest#569</a>), and replacing @Provider with CDI @Produces or programmatic configuration in the Application class.</li><li>Supporting async/reactive return types natively, as has been available in Quarkus for years, and moving @Suspended AsyncResponse handling to background concurrency and context propagation (<a href="https://github.com/jakartaee/rest/issues/1281">jakartaee/rest#1281</a>).</li><li>Providing default values for query, form, and path parameter names (<a href="https://github.com/jakartaee/rest/issues/579">jakartaee/rest#579</a>).</li><li>Adding support for Problem Details (<a href="https://github.com/jakartaee/rest/issues/1150">jakartaee/rest#1150</a>).</li><li>Adding support for API Versioning (<a href="https://github.com/jakartaee/rest/issues/1317">jakartaee/rest#1317</a>).</li><li>Adding support for Hypermedia, eg, HAL, HAL Form, etc. (<a href="https://github.com/jakartaee/rest/issues/1323">jakartaee/rest#1323</a>).</li><li>Supporting Java records in FormBeans and related areas (<a href="https://github.com/jakartaee/rest/issues/955">jakartaee/rest#955</a>, <a href="https://github.com/jakartaee/rest/issues/913">jakartaee/rest#913</a>), especially since records are a major feature in EE 11.</li><li>Enabling functional programming styles for both client and server code (<a href="https://github.com/jakartaee/rest/issues/1301">jakartaee/rest#1301</a>).</li><li>Defining HTTP service interfaces as contracts between client and server (<a href="https://github.com/jakartaee/rest/issues/1294">jakartaee/rest#1294</a>).</li><li>Modernizing the client API to use Java 8+ syntax and making the HTTP client engine easily switchable (<a href="https://github.com/jakartaee/rest/issues/1282">jakartaee/rest#1282</a>).</li><li>…</li></ul><p>I hope the Jakarta REST expert group will focus more on features that improve developer productivity and address real-world needs.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=ba979d9c0cd7" width="1" height="1" alt=""><hr><p><a href="https://itnext.io/whats-new-in-jakarta-rest-4-0-ba979d9c0cd7">What’s New in Jakarta REST 4.0</a> was originally published in <a href="https://itnext.io">ITNEXT</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
    </channel>
</rss>