• Latest
WebFlux and Spring Data Example for Java Records

WebFlux and Spring Data Example for Java Records

December 14, 2021
What does Apple have planned for iPhone, 15 years after the original one?

What does Apple have planned for iPhone, 15 years after the original one?

June 29, 2022
Blizzard Entertainment Acquires Spellbreak Developer Proletariat

Blizzard Entertainment Acquires Spellbreak Developer Proletariat

June 29, 2022
The Multiplayer Online Pirate Game Skull & Bones Release Date And Pre-Order Incentives May Have Leaked

The Multiplayer Online Pirate Game Skull & Bones Release Date And Pre-Order Incentives May Have Leaked

June 29, 2022
CreativeLive acquires Wildist.co   – Photofocus

CreativeLive acquires Wildist.co   – Photofocus

June 29, 2022
GoldenEra – A Celebratory Examination Of GoldenEye 007’s Creation And Impact

GoldenEra – A Celebratory Examination Of GoldenEye 007’s Creation And Impact

June 29, 2022
Sony introduces two Inzone gaming monitors (4K 144Hz and FHD 240Hz),  three headphones too

Sony introduces two Inzone gaming monitors (4K 144Hz and FHD 240Hz),  three headphones too

June 29, 2022
Creators Behind Star Wars: Galaxy Of Heroes Announce New Combat RPG, Legions & Legends

Creators Behind Star Wars: Galaxy Of Heroes Announce New Combat RPG, Legions & Legends

June 29, 2022
Return to Monkey Island Will Be the ‘Conclusion’ for the Series as a Whole

Return to Monkey Island Will Be the ‘Conclusion’ for the Series as a Whole

June 29, 2022
Nikon announces creator-centric Z 30, 400mm super-telephoto prime lens

Nikon announces creator-centric Z 30, 400mm super-telephoto prime lens

June 29, 2022
Snapchat Plus paid subscription is official, costs $3.99/month

Snapchat Plus paid subscription is official, costs $3.99/month

June 29, 2022
Blizzard Acquires Spellbreak Developer Proletariat To Work On World Of Warcraft

Blizzard Acquires Spellbreak Developer Proletariat To Work On World Of Warcraft

June 29, 2022
Monster Hunter Rise: Sunbreak Review

Monster Hunter Rise: Sunbreak Review

June 29, 2022
Advertise with us
Wednesday, June 29, 2022
Bookmarks
  • Login
  • Register
GetUpdated
  • Home
  • Game Updates
    • Mobile Gaming
    • Playstation News
    • Xbox News
    • Switch News
    • MMORPG
    • Game News
    • IGN
    • Retro Gaming
  • Tech News
    • Apple Updates
    • Jailbreak News
    • Mobile News
  • Software Development
  • Photography
  • Contact
    • Advertise With Us
    • About
No Result
View All Result
GetUpdated
No Result
View All Result
GetUpdated
No Result
View All Result
ADVERTISEMENT

WebFlux and Spring Data Example for Java Records

December 14, 2021
in Software Development
Reading Time:23 mins read
0 0
0
Share on FacebookShare on WhatsAppShare on Twitter


Traditionally, Java developers have depended on constructors, accessors, equals(), hashCode(), and toString() to define classes for basic aggregation of values. However,  this is an error-prone approach that adds little value and diverts attention away from modeling immutable data. Java records were initially offered as a preview in JDK 14 to simplify writing data carrier classes. The second preview was released in JDK 15, and the final version in JDK 16. The JDK Enhancement Proposal JEP 395 has a summary of this history.

While code generators can help reduce boilerplate code, the Java record proposals focus on the semantics of the code. Let’s look at the characteristics and benefits of Java records and how to use them to develop a REST API and query a database.

Prerequisites:

The record Keyword

The record is a new type of declaration, a restricted form of class that acts as a transparent carrier for immutable data. Let’s start the exploration by defining a simple data type EndOfGame as a record:

import java.time.LocalDate;
import java.time.LocalTime;

public record EndOfGame(String id, LocalDate date, LocalTime timeOfDay,
                        String mentalState, Integer damageTaken,
                        Integer damageToPlayers, Integer damageToStructures) {
}

As you can see in the example above, the record has a name, EndOfGame in this case. What looks like a constructor signature is the state description or header, declaring the components of the record.

The following members are acquired automatically with the declaration:

  • A private final field and a public read accessor for each component of the state description
  • A public canonical constructor with the same signature as the state description, which initializes each field from the corresponding argument
  • Implementation of equals(), hashCode() and toString()

The tests in the EndOfGameTest class below verify that the automatic members are indeed available:

package com.okta.developer.records.domain;

import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.time.LocalDate;
import java.time.LocalTime;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

public class EndOfGameTest {

    private static final Logger logger = LoggerFactory.getLogger(EndOfGameTest.class);

    private EndOfGame createEndOfGame() {

        return new EndOfGame("1", LocalDate.of(2018, 12, 12),
                LocalTime.of(15, 15), "sober",
                10, 10, 10);
    }

    @Test
    public void equalsTest() {
        EndOfGame eog1 = createEndOfGame();
        EndOfGame eog2 = createEndOfGame();

        assertTrue(eog1.equals(eog2));
        assertEquals(eog1, eog2);
        assertEquals(eog1.hashCode(), eog2.hashCode());
    }

    @Test
    public void toStringTest() {
        EndOfGame eog = createEndOfGame();
        logger.info(eog.toString());

        assertEquals("EndOfGame[id=1, date=2018-12-12, timeOfDay=15:15, mentalState=sober, " +
                        "damageTaken=10, damageToPlayers=10, damageToStructures=10]",
                eog.toString());
    }

    @Test
    public void accessorTest() {
        EndOfGame eog = createEndOfGame();
        assertEquals("sober", eog.mentalState());
    }

}

The automatic canonical constructor is used for creating a sample EndOfGame in the method createEndOfGame(). In the equalsTest() above, eog1 has the same state as eog2, so eog1.equals(eog2) is true. This also implies both instances have the same hashCode. Automatic read accessors have the same name and return type as the component. Note there is no get* prefix in the read accessor name, which is the same name as the component, as illustrated in the accessorTest().

Java Record Restrictions and Rules

While record provides a more concise syntax and semantics designed to help to model data aggregates, as stated before, a record is a restricted form of a class. Let’s have a brief look at those restrictions.

Inheritance, Extensibility, and Immutability

A record is implicitly final, and cannot be abstract. You cannot enhance it later by extension, as the compiler will output the following error:

Cannot inherit from final 'com.okta.developer.records.EndOfGame'

A record cannot extend any class, not even its implicit superclass Record. But it can implement interfaces. Using the extends with records clause will cause the following error:

No extends clause allowed for record

A record does not have write accessors and the implicitly declared fields are final, and not modifiable via reflection. Moreover, a record cannot declare instance fields outside the record header. Records embody an immutable by default policy, usually applied to data carrier classes.

Instance field is not allowed in record
Cannot assign a value to final variable 'id'

Read accessors can be declared explicitly, but should never silently alter the record state. Review record semantics before making explicit declarations of automatic members.

A record without any constructor declaration will be given a canonical constructor with the same signature as the header, which will assign all the private fields from the arguments. The canonical constructor can be declared explicitly in the standard syntax, or in a compact form:

public EndOfGame {
    Objects.requireNonNull(date);
    Objects.requireNonNull(timeOfDay);
    Objects.requireNonNull(mentalState);
    Objects.requireNonNull(damageTaken);
    Objects.requireNonNull(damageToPlayers);
    Objects.requireNonNull(damageToStructures);
}

In the compact form, the parameters are implicit, the private fields cannot be assigned inside the body, and are automatically assigned at the end. This enables a focus on validation, making defensive copies of mutable values, or some other value processing.

Serialization, Encoding, and Mapping

Record instances can extend Serializable, but the serialization and deserialization processes cannot be customized. Serialization and deserialization methods like writeObject, readObject can be implemented, but will be ignored.

As spring-web module provides Jackson with JSON encoders and decoders, and Java record support was added to Jackson in release 2.12, records can be used for REST API request and response mapping.

Records cannot be used as entities with JPA/Hibernate. JPA entities must have a no-args constructor, and must not be final, two requirements that record will not support. There are additional issues with JPA and records too.

Spring Data modules that do not use the object mapping of the underlying data store (like JPA) support records, as persistence construction detection works as with other classes. For Spring Data MongoDB, the general recommendation is to stick to immutable objects, because they are straightforward to materialize by calling the constructor. An all-args constructor, allows materialization to skip property population for optimal performance. This too is recommended, and Java record semantics align with these guidelines.

Use Java Records With Spring WebFlux and Spring Data

While searching for a dataset for this tutorial, I came across a collection of 87 end game statistics from a single player, for a popular game. As the author included his mental state in the game play data, I decided to use Java records for building a basic average query and finding out if the player’s performance was significantly altered when sober vs when intoxicated.

Let’s jump ahead and build a secured REST Api using the EndOfGame record, Spring Boot, MongoDB, and Okta authentication. With the help of Spring Initializr create a WebFlux application, from the web UI or with HTTPie:

https -d start.spring.io/starter.zip bootVersion==2.5.6 
  baseDir==java-records 
  groupId==com.okta.developer.records 
  artifactId==records-demo 
  name==java-records 
  packageName==com.okta.developer.records 
  javaVersion==17 
  dependencies==webflux,okta,data-mongodb-reactive

Note: Although Java records have been available since release 14, the Spring Initializr Web UI only lets you select Java Long Term Support (LTS) releases.

Extract the Maven project, and edit pom.xml to add two more required dependencies for this tutorial, MongoDB Testcontainers Module for testing database access, and spring-security-test:

<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>mongodb</artifactId>
    <version>1.15.3</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-test</artifactId>
    <version>5.5.1</version>
    <scope>test</scope>
</dependency>

Before you begin, you’ll need a free Okta developer account. Install the Okta CLI and run okta register to sign up for a new account. If you already have an account, run okta login. Then, run okta apps create. Select the default app name, or change it as you see fit. Choose Web and press Enter.

Select Okta Spring Boot Starter. Accept the default Redirect URI values provided for you. That is, a Login Redirect of http://localhost:8080/login/oauth2/code/okta and a Logout Redirect of http://localhost:8080.

What does the Okta CLI do?

Rename application.properties to application.yml, and add the following content:

okta:
  oauth2:
    issuer: https://{yourOktaDomain}/oauth2/default
    client-id: {clientId}
    client-secret: {clientSecret}

spring:
  data:
    mongodb:
      port: 27017
      database: fortnite

logging:
  level:
    org.springframework.data.mongodb: TRACE

Add an EndOfGame record under the package com.okta.developer.records.domain. Annotate the record with @Document(collection = "stats") to let MongoDB map EndOfGame to the stats collection. As you can see, a record class can be annotated. The class should look like this:

package com.okta.developer.records.domain;

import org.springframework.data.mongodb.core.mapping.Document;

import java.time.LocalDate;
import java.time.LocalTime;
import java.util.Objects;

@Document(collection = "stats")
public record EndOfGame(String id, LocalDate date,  LocalTime timeOfDay,
                        String mentalState, Integer damageTaken,
                        Integer damageToPlayers, Integer damageToStructures) {

    public EndOfGame {
        Objects.requireNonNull(date);
        Objects.requireNonNull(timeOfDay);
        Objects.requireNonNull(mentalState);
        Objects.requireNonNull(damageTaken);
        Objects.requireNonNull(damageToPlayers);
        Objects.requireNonNull(damageToStructures);
    }

}

Add a new record for the mental state query MentalStateDamage:

package com.okta.developer.records.domain;

public record MentalStateDamage(String mentalState,
                                Double damageToPlayers,
                                Double damageToStructures,
                                Double damageTaken) {
}

Create the package com.okta.developer.records.repository and add a MentalStateStatsRepository interface:

package com.okta.developer.records.repository;

import com.okta.developer.records.domain.MentalStateDamage;
import reactor.core.publisher.Flux;

public interface MentalStateStatsRepository {

    Flux<MentalStateDamage> queryMentalStateAverageDamage();

}

Add the implementation MentalStateStatsRepositoryImpl to retrieve the average damage in each category, for each mental state:

package com.okta.developer.records.repository;

import com.okta.developer.records.domain.EndOfGame;
import com.okta.developer.records.domain.MentalStateDamage;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.mongodb.core.ReactiveMongoTemplate;
import org.springframework.data.mongodb.core.aggregation.Aggregation;
import reactor.core.publisher.Flux;

import static org.springframework.data.mongodb.core.aggregation.Aggregation.*;

public class MentalStateStatsRepositoryImpl implements MentalStateStatsRepository {

    private final ReactiveMongoTemplate mongoTemplate;

    @Autowired
    public MentalStateStatsRepositoryImpl(ReactiveMongoTemplate mongoTemplate) {
        this.mongoTemplate = mongoTemplate;
    }

    @Override
    public Flux<MentalStateDamage> queryMentalStateAverageDamage() {
        Aggregation aggregation = newAggregation(
                group("mentalState")
                        .first("mentalState").as("mentalState")
                        .avg("damageToPlayers").as("damageToPlayers")
                        .avg("damageTaken").as("damageTaken")
                        .avg("damageToStructures").as("damageToStructures"),
                project("mentalState", "damageToPlayers", "damageTaken", "damageToStructures")
                );
        return mongoTemplate.aggregate(aggregation, EndOfGame.class, MentalStateDamage.class);
    }

}

Note: The Impl suffix is required for customizing individual repositories with Spring Data

Create the StatsRepository interface, extending the MentalStateStatsRepository:

package com.okta.developer.records.repository;

import com.okta.developer.records.domain.EndOfGame;
import org.springframework.data.repository.reactive.ReactiveSortingRepository;

public interface StatsRepository extends ReactiveSortingRepository<EndOfGame, String>, MentalStateStatsRepository {

}

After the import,the sample dataset, will create strings for the date and time values, in a custom format. Create the following converters to map String to LocalDate and LocalTime:

package com.okta.developer.records.repository;

import org.springframework.core.convert.converter.Converter;

import java.time.LocalDate;

public class LocalDateConverter implements Converter<String, LocalDate> {

    @Override
    public LocalDate convert(String s) {
        return LocalDate.parse(s);
    }
}

package com.okta.developer.records.repository;

import org.springframework.core.convert.converter.Converter;

import java.time.LocalTime;

public class LocalTimeConverter implements Converter<String, LocalTime> {

    @Override
    public LocalTime convert(String s) {
        return LocalTime.parse(s);
    }
}

Add a MongoConfiguration class in the package com.okta.developer.records.configuration, to register the converters:

package com.okta.developer.records.configuration;

import com.okta.developer.records.repository.LocalDateConverter;
import com.okta.developer.records.repository.LocalTimeConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.mongodb.core.convert.MongoCustomConversions;

import java.util.Arrays;

@Configuration
public class MongoConfiguration {

    @Bean
    public MongoCustomConversions mongoCustomConversions() {

        return new MongoCustomConversions(
            Arrays.asList(
                    new LocalDateConverter(),
                    new LocalTimeConverter()));
    }
}

Add a StatsService interface in the com.okta.developer.records.service package:

package com.okta.developer.records.service;

import com.okta.developer.records.domain.EndOfGame;
import com.okta.developer.records.domain.MentalStateDamage;
import reactor.core.publisher.Flux;

public interface StatsService {

    Flux<MentalStateDamage> queryMentalStateAverageDamage();

    Flux<EndOfGame> getAll();
}

Add a DefaultStatsService class for the implementation in the com.okta.developer.records.service package:

package com.okta.developer.records.service;

import com.okta.developer.records.domain.EndOfGame;
import com.okta.developer.records.domain.MentalStateDamage;
import com.okta.developer.records.repository.StatsRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;

@Service
public class DefaultStatsService implements StatsService {

    @Autowired
    private StatsRepository statsRepository;

    @Override
    public Flux<MentalStateDamage> queryMentalStateAverageDamage() {
        return statsRepository.queryMentalStateAverageDamage();
    }

    @Override
    public Flux<EndOfGame> getAll() {
        return statsRepository.findAll();
    }
}

Add a StatsController class in the com.okta.developer.records.controller package:

package com.okta.developer.records.controller;

import com.okta.developer.records.domain.EndOfGame;
import com.okta.developer.records.domain.MentalStateDamage;
import com.okta.developer.records.service.StatsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;

@RestController
public class StatsController {

    @Autowired
    private StatsService statsService;

    @GetMapping("/endOfGame")
    public Flux<EndOfGame> getAllEndOfGame(){
        return statsService.getAll();
    }

    @GetMapping("/mentalStateAverageDamage")
    public Flux<MentalStateDamage> getMentalStateAverageDamage(){
        return statsService.queryMentalStateAverageDamage();
    }

}

The controller enables the /endOfGame endpoint to get all entries, and the /mentalStateAverageDamage endpoint, that returns the damage in each category, as in average by mental state.

Test Java Records in the Web Layer

Create a StatsControllerTest class in the package com.okta.developer.records.controller under the src/test folder, to verify the endpoints’ basic functionality with a web test. In this test, only the web slice is verified:

package com.okta.developer.records.controller;

import com.okta.developer.records.domain.EndOfGame;
import com.okta.developer.records.domain.MentalStateDamage;
import com.okta.developer.records.service.StatsService;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.web.reactive.server.WebTestClient;
import reactor.core.publisher.Flux;

import java.time.LocalDate;
import java.time.LocalTime;

import static org.mockito.BDDMockito.given;
import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.mockOidcLogin;

@WebFluxTest
public class StatsControllerTest {

    private static Logger logger = LoggerFactory.getLogger(StatsControllerTest.class);

    @MockBean
    private StatsService statsService;

    @Autowired
    private WebTestClient webTestClient;

    @Test
    public void testGet_noAuth_returnsNotAuthorized(){

        webTestClient
                .get().uri("/endofgame")
                .exchange()
                .expectStatus().is3xxRedirection();
    }

    @Test
    public void testGet_withOidcLogin_returnsOk(){

        EndOfGame endOfGame = new EndOfGame("1", LocalDate.now(), LocalTime.now(), "happy", 1, 1, 1);

        given(statsService.getAll()).willReturn(Flux.just(endOfGame));


        webTestClient.mutateWith(mockOidcLogin())
                .get().uri("/endOfGame")
                .exchange()
                .expectStatus().is2xxSuccessful()
                .expectBody()
                    .jsonPath("$.length()").isNumber()
                    .jsonPath("$.length()").isEqualTo("1")
                    .jsonPath("$[0].mentalState").isEqualTo("happy")
                    .jsonPath("$[0].damageTaken").isNumber()
                    .jsonPath("$[0].damageToPlayers").isNumber()
                    .jsonPath("$[0].damageToStructures").isNumber()
                    .jsonPath("$[0].date").isNotEmpty()
                    .jsonPath("$[0].timeOfDay").isNotEmpty()
                    .consumeWith(response -> logger.info(response.toString()));

    }

    @Test
    public void testGetMentalStateAverageDamage_withOidcLogin_returnsOk(){

        MentalStateDamage mentalStateDamage = new MentalStateDamage("happy", 0.0, 0.0, 0.0);

        given(statsService.queryMentalStateAverageDamage()).willReturn(Flux.just(mentalStateDamage));

        webTestClient
                .mutateWith(mockOidcLogin())
                .get().uri("/mentalStateAverageDamage")
                .exchange()
                .expectStatus().is2xxSuccessful()
                .expectBody()
                .jsonPath("$.length()").isEqualTo("1")
                .jsonPath("$.[0].mentalState").isEqualTo("happy")
                .consumeWith(response -> logger.info(response.toString()));
    }

}

Run the tests with the following Maven command:

./mvnw test -Dtest=StatsControllerTest

The /mentalStateAverageDamage test above also verifies that the MentalStateDamage record type is correctly handled when used as a response body. For the /endOfGame you should see response logs similar to this:

[
   {
      "id":"1",
      "date":"2021-10-21",
      "timeOfDay":"12:02:34.233944363",
      "mentalState":"happy",
      "damageTaken":1,
      "damageToPlayers":1,
      "damageToStructures":1
   }
]

Test Java Records in the Database Layer

Download the test dataset from GitHub with HTTPie, and copy it to src/test/resources/stats.json:

https -d raw.githubusercontent.com/oktadev/okta-java-records-example/9a60f81349cdffebbe001719256b0883493f987d/src/test/resources/stats.json
mkdir src/test/resources
mv stats.json src/test/resources/.

Create the StatsRepositoryTest class in the package com.okta.developer.records.repository under the src/test folder, to verify the database slice:

package com.okta.developer.records.repository;

import com.okta.developer.records.configuration.MongoConfiguration;
import com.okta.developer.records.domain.EndOfGame;
import com.okta.developer.records.domain.MentalStateDamage;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.mongo.embedded.EmbeddedMongoAutoConfiguration;
import org.springframework.boot.test.autoconfigure.data.mongo.DataMongoTest;
import org.springframework.context.annotation.Import;
import org.testcontainers.containers.Container;
import org.testcontainers.containers.MongoDBContainer;
import org.testcontainers.utility.DockerImageName;
import org.testcontainers.utility.MountableFile;
import reactor.core.publisher.Flux;

import java.io.IOException;
import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;

@DataMongoTest(excludeAutoConfiguration = EmbeddedMongoAutoConfiguration.class)
@Import(MongoConfiguration.class)
public class StatsRepositoryTest {

    private static final Logger logger = LoggerFactory.getLogger(StatsRepositoryTest.class);

    @Autowired
    private StatsRepository statsRepository;

    private static final MongoDBContainer mongoDBContainer = new MongoDBContainer(DockerImageName.parse("mongo:bionic"))
            .withExposedPorts(27017)
            .withCopyFileToContainer(MountableFile.forClasspathResource("stats.json"),
                    "/stats.json")
            .withEnv("MONGO_INIT_DATABASE", "fortnite");

    @BeforeAll
    public static void setUp() throws IOException, InterruptedException {
        mongoDBContainer.setPortBindings(List.of("27017:27017"));
        mongoDBContainer.start();

        Container.ExecResult result = mongoDBContainer.execInContainer("mongoimport",
                "--verbose", "--db=fortnite", "--collection=stats", "--file=/stats.json", "--jsonArray");
        logger.info(result.getStdout());
        logger.info(result.getStderr());
        logger.info("exit code={}", result.getExitCode());
    }


    @Test
    public void testGetAll(){
        Flux<EndOfGame> stats = statsRepository.findAll();
        List<EndOfGame> result = stats.collectList().block();

        assertThat(result).isNotEmpty();
        assertThat(result).size().isEqualTo(87);
    }

    @Test
    public void testQueryMentalStateAverageDamage(){
        Flux<MentalStateDamage> stats = statsRepository.queryMentalStateAverageDamage();
        List<MentalStateDamage> result = stats.collectList().block();

        assertThat(result).isNotEmpty();
        assertThat(result).size().isEqualTo(2);
        assertThat(result.get(0).mentalState()).isIn("sober", "high");
        assertThat(result.get(1).mentalState()).isIn("sober", "high");

        logger.info(result.get(0).toString());
        logger.info(result.get(1).toString());
    }

    @AfterAll
    public static void tearDown(){
        mongoDBContainer.stop();
    }

}

@DataMongoTest configures the data layer for testing. For this test, I evaluated using repository populator, but decided to go for a Testcontainers MongoDB instance instead, to reuse the import process and data.json file later with Docker Compose. Using a container MongoDB instance requires disabling the EmbeddedMongoAutoConfiguration.

In the setUp() above, the mongoimport tool is executed in the test container, initializing the stats collection with the sample dataset.

Run the tests with the following Maven command:

./mvnw test -Dtest=StatsRepositoryTest

If the import runs successfully, the following line should appear in the test logs:

87 document(s) imported successfully. 0 document(s) failed to import.

Also, you can inspect the response in the logs for the average damage test:

MentalStateDamage[mentalState=sober, damageToPlayers=604.3777777777777, damageToStructures=3373.511111111111, damageTaken=246.46666666666667]
MentalStateDamage[mentalState=high, damageToPlayers=557.547619047619, damageToStructures=2953.8571428571427, damageTaken=241.71428571428572]

For this single-player dataset, the damage taken or inflicted was not orders of magnitude different when sober than when high.

Run the Application

Create a docker folder in the root of the project, and add the following docker-compose.yml file there:

version: "3.1"

services:
  mongodb:
    image: mongo:bionic
    environment:
      - MONGO_INIT_DATABASE=fortnite
    ports:
      - "27017:27017"
    volumes:
      - ../src/test/resources/stats.json:/seed/stats.json
      - ./initdb.sh:/docker-entrypoint-initdb.d/initdb.sh
  demo:
    image: records-demo:0.0.1-SNAPSHOT
    ports:
      - "8080:8080"
    environment:
      - SPRING_DATA_MONGODB_HOST=mongodb
    depends_on:
      - mongodb

Add also the initdb.sh script in the docker folder, to import the test data into MongoDB, with the following content:

mongoimport --verbose --db=fortnite --collection=stats --file=/seed/stats.json --jsonArray

In the project root, generate the application container image with the following Maven command:

./mvnw spring-boot:build-image

Run the application with Docker Compose:

cd docker
docker-compose up

Once the services are up, go to http://localhost:8080/mentalStateAverageDamage, and you should see the Okta login page:

Sign in with your Okta credentials, and if successful, it will redirect to the /mentalStateAverageDamage endpoint, and you should see a response body like the following:

[
   {
      "mentalState":"sober",
      "damageToPlayers":604.3777777777777,
      "damageToStructures":3373.511111111111,
      "damageTaken":246.46666666666667
   },
   {
      "mentalState":"high",
      "damageToPlayers":557.547619047619,
      "damageToStructures":2953.8571428571427,
      "damageTaken":241.71428571428572
   }
]

Java Records Advantages and Limitations

While Java record is more concise for declaring data carrier classes, the “war on boilerplate” is not a goal of this construct, Records are not meant to add features like properties or annotation-driven code generation, as Project Lombok does. Record semantics provide benefits for modeling an immutable state data type. No hidden state is allowed, as no instance fields can be defined outside the header, hence the transparent claim. Compiler generated equals() and hashCode() avoid error-prone coding. Serialization and deserialization into JSON are straightforward thanks to its canonical constructor. Summarizing some of the Java Record features discussed in this post:

Advantages

  • Concise syntax
  • Immutable state
  • Compiler generated equals() and hashCode()
  • Straightforward JSON serialization and deserialization

Limitations

  • Immutable state
  • Cannot be used as JPA/Hibernate entities
  • Cannot be extended or inherit a class

Conclusion

I hope you enjoyed this tutorial and learned about Java record semantics, its benefits, and its limitations. Before choosing this feature, make sure to find out if your favorite frameworks support it. Fortunately, Spring Boot support for Java Records was recently added in 2.5.x releases through Jackson 2.12.x. I was not able to find comments about records in Spring Data documentation.

You can find the completed code for this tutorial on GitHub in the oktadev/okta-java-records-example repository.



Source link

ShareSendTweet
Previous Post

GTO SNOW

Next Post

Call Of Duty: Vanguard Was Best-Selling Game In U.S. In November, With Battlefield 2042 Behind It In Second Place

Related Posts

Performance Tuning Strategies for SQL Server Index

June 29, 2022
0
0
Performance Tuning Strategies for SQL Server Index
Software Development

An optimized approach to indexing is important if you are keen to keep the performance of an SQL Server instance...

Read more

Reactive Kafka With Streaming in Spring Boot

June 29, 2022
0
0
Reactive Kafka With Streaming in Spring Boot
Software Development

The AngularAndSpring project uses Kafka for the distributed sign-in of new users and the distributed token revocation for logged-out users....

Read more
Next Post
Call Of Duty: Vanguard Was Best-Selling Game In U.S. In November, With Battlefield 2042 Behind It In Second Place

Call Of Duty: Vanguard Was Best-Selling Game In U.S. In November, With Battlefield 2042 Behind It In Second Place

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

© 2021 GetUpdated – MW.

  • About
  • Advertise
  • Privacy & Policy
  • Terms & Conditions
  • Contact

No Result
View All Result
  • Home
  • Game Updates
    • Mobile Gaming
    • Playstation News
    • Xbox News
    • Switch News
    • MMORPG
    • Game News
    • IGN
    • Retro Gaming
  • Tech News
    • Apple Updates
    • Jailbreak News
    • Mobile News
  • Software Development
  • Photography
  • Contact
    • Advertise With Us
    • About

Welcome Back!

Login to your account below

Forgotten Password? Sign Up

Create New Account!

Fill the forms bellow to register

All fields are required. Log In

Retrieve your password

Please enter your username or email address to reset your password.

Log In
Are you sure want to unlock this post?
Unlock left : 0
Are you sure want to cancel subscription?