Study Guide 2023+

spring

Warning: These notes are partial, ongoing, incomplete, and may contain typos/inaccuracies. (They are kept factually accurate, time permitting.)

They are being united from many disparate notes created in the past and the layout/organization will gradually improve with time!

Please view them on a computer as they are not optimized for mobile (although you can still view them on Mobile along with the Flashcards at your own risk)!

Topics and code examples are lazy-loaded and may require two-clicks from the TOC to correctly calculate the updated x,y coordinates (after rendering). Thanks!

Spring: General Concepts

  1. Inversion of Control - preconfigured default values reflecting best practices and commonly used patterns.
  1. https://gitlab.com/Thoughtscript/interview_helper
  2. https://www.baeldung.com/
  3. https://spring.io/
  4. https://mkyong.com/
  5. https://github.com/spring-projects
  6. https://github.com/spring-projects/spring-boot/tree/main/spring-boot-project/spring-boot-starters
  7. https://mvnrepository.com/repos/central

Spring: Project Layout

Typical Project Layout

+- gradle
|  +- ...
|
+- build
|  +- ...
|
+- src
|  +- main
|  |  +- java
|  |  |  +- com
|  |  |     +- example
|  |  |        +- app
|  |  |           +- ...
|  |  |           +- Main.java
|  |  |
|  |  +- resources
|  |  |  +- application.properties
|  |  |  +- ...
|  |  |
|  |  +- webapp
|  |     +- resources
|  |     |  +- scripts
|  |     |     +- ...
|  |     |  +- styles
|  |     |     +- ...
|  |     +- WEB-INF
|  |     |  +- JSP
|  |     |     +- ...
|  |
|  +- test
|     +- java
|        +- com
|           +- example
|              +- app
|                 +- MainTest.java
|   
+- target
|  +- ...
|
+- pom.xml
+- gradlew.bat
+- build.gradle
+- settings.gradle 
+- .gitignore
+- README.md
+- ...

Common Maven Commands

mvn clean
mvn install
mvn spring-boot:run

Comman Gradle Commands

./gradlew clean
./gradlew build
./gradlew run

Java SSL Keytool

keytool -genkey \
  -alias interviewhelper \
  -keystore interviewhelper.p12 \
  -storetype PKCS12 \
  -keyalg RSA \
  -storepass F#4%afdfdsdfdfa*adf \
  -validity 730 \
  -keysize 4096

IntelliJ

Looks like .war artifacts don't populate immediately when a pom.xml has successfully been loaded into IntelliJ anymore.

  1. Click File > Plugins > Type in Maven Helper
  2. The Maven Helper plugin provides additional context menu options that appear to be missing now out of the box.
  3. To correctly build a .war file, right-click on pom.xml > Run Maven > Plugins > maven-war-plugin > war:war
  1. https://gitlab.com/Thoughtscript/java-reactive-pubsub
  2. https://gitlab.com/Thoughtscript/interview_helper/
  3. https://web.archive.org/web/20230128043222/https://x-team.com/blog/react-reactor-passwordless-spring/
  4. https://plugins.jetbrains.com/plugin/7179-maven-helper

Spring: Logging

  1. Slf4j - The Simple Logging Facade for Java serves as an abstraction or interface for an underlying target logging framework or library.
  2. Log4j - Apache, the original default logging library.
  3. Logback - Successor to Log4j.
  4. Log4j 2 - Apache's upgrade for Log4j that provides significant improvements over its predecessor and applies many lessons from Logback.
  5. Lombok - A utility library that provides many helpful annotations. Includes Slf4j logging as an annotation.

Common Combinations

  1. Lombok + Slf4j (included in Lombok) + Logback (included in Spring Boot Starters, the default)
  2. Slf4j + Log4j
  1. https://stackoverflow.com/questions/39562965/what-is-the-difference-between-log4j-slf4j-and-logback
  2. https://krishankantsinghal.medium.com/logback-slf4j-log4j2-understanding-them-and-learn-how-to-use-d33deedd0c46

Code samples:

  1. https://github.com/Thoughtscript/spring_boot_2023 (Spring Boot + Lombok + Slf4j + Logback)

Spring: Annotations

Common Spring-related Decorator-style annotations.

Spring MVC

  1. @PathVariable with @GetMapping("/{product_id}") to specify a URL Path Variable
  2. @RequestBody - the HTTP Request Body data. Can be Form Data. By Key-Value.
  3. @RequestParam - an HTTP Key-Value Request Paramater passed the URL string.
  4. @RestController - annotates @Controller and @ResponseBody.

Jackson, JAX

  1. @JsonNaming - allows snake case and camel case, to be used in a deserialized/serialized field.
  2. @JsonPropertyOrder - specifies the exact serialization/deserialization order to be specified (since Spring will use alphabetical order by default). Sometimes a property must be computed after another regardless of alphabetical order.
  3. @JsonIgnore - specifies that a field should be ignored during serialization. Useful for preventing infinite JSON recursion with One-to-Many, Many-to-Many, and Many-to-One table relationships.
  4. @Transient - no to be confused with the transient keyword, @Transient specifies that a field should be ignored but does not involve Serialization.

Spring

  1. @ComponentScan - specifies that the Application should recursively search for relevant Beans within the specified Package or Classpath.
  2. @SpringBootApplication - annotates @Configuration, @EnableAutoConfiguration, and @ComponentScan.
  3. @Configuration - specifies that the Bean is for configuration, contains configuration settings, Extends or Overrides some default configuration Bean, loads Application Properties, or sets them programmatically.
  4. @EnableWebMvc - Spring (but not Spring Boot) configuration annotation.
  5. @EnableAutoConfiguration - use the default auto-configured settings.
  6. @EnableAsync and @Async - enables Asynchronous programming in Spring and configures the desired Thread Executor arrangment so the @Async annotation can be added to a Bean method automatically wrapping it with an Executor. Typically used with CompletableFutures and/or Futures.
  7. @Transactional - specify that a method should be wrapped in database Transaction.
  8. @EnableRetry, @Retryable, and @Recover - enable specific method invocations to be attempted multiple times (default of three) through method intercepting in the event of a specified Exception. Remember that @Recover is used to handle Exceptions emanating from an @Retryable attempt.

Java EE

  1. @Entity - Javax Persistence annotation specifying that the POJO in question is relevant to Object Relational Mapping.
  2. @Bean - Java EE annotation indicating that the Class is a Bean (should be initialized and used as such) within some configuration Class.
  1. https://thorben-janssen.com/hibernate-tip-difference-between-joincolumn-and-primarykeyjoincolumn/
  2. https://www.baeldung.com/spring-transactional-propagation-isolation
  3. https://www.techferry.com/articles/hibernate-jpa-annotations.html
  4. https://www.baeldung.com/spring-retry
  5. https://github.com/spring-projects/spring-retry

Spring: Hibernate

  1. The Hibernate Naming Strategy divides into two basic strategies: (1) ImplicitNamingStrategy and (2) DefaultNamingStrategy. The default naming strategy can be configured in application.properties or as a configured setting. (It is not set in the Hibernate SQL Dialect.)
  2. Used with Spring Data, JPA, Entity Framework, and Javax Persistence.
  3. Object Relational Mapping framework for converting SQL data into valid Java entities in-memory at runtime.

Relationships

Relations between data structures, Rows, and Tables are specified with a mix of annotations and data-layer constraints.

Note: _fk is sometimes appended to a column below but should not be confused with a Foreign Key (Foreign Key Constraint) proper. I use that convention to make it easy to read what a column is doing - it stands for foreign-key-like or what's sometimes referred as a Foreign Keys Without a Constraint. (Best practices encourage the use of true Foreign Key Constraints in Production, DRY JOIN tables, and removing any additional Foreign Key columns.) Consult: https://ardalis.com/related-data-without-foreign-keys/ for more on the nomenclature.

Lazy Loading

Note that fetch defaults to FetchType.EAGER (associated entities are loaded immediately). FetchType.LAZY will load those entities only when the field is first used.

Generally, Hibernate encourages FetchType.LAZY: "If you forget to JOIN FETCH all EAGER associations, Hibernate is going to issue a secondary select for each and every one of those which, in turn, can lead to N+1 query issues. For this reason, you should prefer LAZY associations."

Refer to: https://docs.jboss.org/hibernate/orm/5.4/userguide/html_single/Hibernate_User_Guide.html#architecture and https://docs.jboss.org/hibernate/orm/5.4/userguide/html_single/Hibernate_User_Guide.html#fetching

One to One

Example: User and UserProfile.

@PrimaryKeyJoinColumn

@Entity
@Table(name = "A")
public class A {
   
  @Id
  @Column(name = "id")
  @GeneratedValue
  private int id;

  //  If a Foreign Key is explicitly defined between A to B.
  @OneToOne(cascade = CascadeType.MERGE)
  @PrimaryKeyJoinColumn
  private B b;
}
@Entity
@Table(name = "B")
public class B {
   
  // @GeneratedValue
  @Id
  @Column(name = "id")
  private int id;
}

@JoinColumn

@Entity
@Table(name = "A")
public class A {
   
  @Id
  @Column(name = "id")
  @GeneratedValue
  private int id;

  @OneToOne(fetch = FetchType.EAGER)
  @MapsId
  @JoinColumn(name = "bId")
  private B b;
}
@Entity
@Table(name = "B")
public class B {

  @Id
  @Column(name = "id")
  @GeneratedValue
  private int id;

  // Optional
  @OneToOne(mappedBy = "b", cascade = CascadeType.ALL)
  private A a;
}
DROP TABLE IF EXISTS A;
CREATE TABLE A (
  id INT(11) NOT NULL AUTO_INCREMENT,
  bId INT(11) NOT NULL
  PRIMARY KEY (id)
);


DROP TABLE IF EXISTS B;
CREATE TABLE B (
  id INT(11) NOT NULL AUTO_INCREMENT,
  PRIMARY KEY (id)
);

One To Many

Example: Owner and Pet (there are many Pets that can be owned by an Owner). And, below many A are mapped to a single B.

@Entity
@Table(name = "A")
public class A {

  @Id
  @Column(name = "id")
  @GeneratedValue
  private int id;
}
@Entity
@Table(name = "B")
public class B {

  @Id
  @Column(name = "id")
  @GeneratedValue
  private int id;

  @OneToMany(cascade = CascadeType.ALL)
  @JoinTable(name = "B_A", joinColumns = { @JoinColumn(name = "bId") }, inverseJoinColumns = { @JoinColumn(name = "aId") })
  // Alternatively, if a Foreign Key or column is used
  // instead of DRY-JOIN table.
  // @OneToMany(fetch = FetchType.LAZY, mappedBy="a_fk")
  private Set<A> manyA;
}
DROP TABLE IF EXISTS A;
CREATE TABLE A (
  id INT(11) NOT NULL AUTO_INCREMENT,
  PRIMARY KEY (id)
);


DROP TABLE IF EXISTS B;
CREATE TABLE B (
  id INT(11) NOT NULL AUTO_INCREMENT,
  PRIMARY KEY (id)
);

-- Make sure to verify the field constraints when 
----using a DRY-JOIN table that's also managed by Hibernate
DROP TABLE IF EXISTS B_A;
CREATE TABLE B_A (
  id INT(11),
  bId INT(11),
  aId INT(11)
);

Many to One

Example: Owner and Pet (there are many Pets that can be owned by an Owner). And, below many B are mapped to a single A.

Assuming an FK exists:

@Entity
@Table(name = "A")
public class A {

  @Id
  @Column(name = "A_id")
  @GeneratedValue
  private int id;
}
@Entity
@Table(name = "B")
public class B {

  @Id
  @Column(name = "id")
  @GeneratedValue
  private int id;
 
  @ManyToOne
  @JoinColumn(name="A_id",foreignKey=@ForeignKey(name="A_id"))
  // Alternatively, if no Foreign Key Constraints exist.
  // @JoinColumn(name="A_id")
  private A a;
}
DROP TABLE IF EXISTS A;
CREATE TABLE A (
  A_id INT(11) NOT NULL AUTO_INCREMENT,
  PRIMARY KEY (A_id)
);


DROP TABLE IF EXISTS B;
CREATE TABLE B (
  id INT(11) NOT NULL AUTO_INCREMENT,
  A_id INT(11),
  PRIMARY KEY (id)
);

Many to Many

Example: Books and Authors.

Assuming an FK exists:

@Entity
@Table(name = "A")
public class A {

  @Id
  @Column(name = "id")
  @GeneratedValue
  private int id;

  @ManyToMany
  @JoinTable(name= "A_B", 
    joinColumns = @JoinColumn(name = "aId"),
    inverseJoinColumns = @JoinColumn(name = "bId"))
  private Set<B> manyB;
}
@Entity
@Table(name = "B")
public class B {

  @Id
  @Column(name = "id")
  @GeneratedValue
  private int id;

  @ManyToMany(mappedBy="manyB")
  private Set<A> manyA;
}
DROP TABLE IF EXISTS A;
CREATE TABLE A (
  id INT(11) NOT NULL AUTO_INCREMENT
  PRIMARY KEY (id)
);


DROP TABLE IF EXISTS B;
CREATE TABLE B (
  id INT(11) NOT NULL AUTO_INCREMENT,
  PRIMARY KEY (id)
);

-- Make sure to verify the field constraints when 
----using a DRY-JOIN table that's also managed by Hibernate
DROP TABLE IF EXISTS A_B;
CREATE TABLE A_B (
  id INT(11),
  aId INT(11),
  bId INT(11)
);

Jackson JSON Serialization

To avoid infinite recursion:

  1. Use @JsonIgnore to on side side of the infinite recursion.
  2. Use a custom serializer.
  3. Use @JsonView(Views.Internal.class).

Refer to: https://www.baeldung.com/jackson-bidirectional-relationships-and-infinite-recursion

Best Practices

  1. Use fetch = FetchType.LAZY when configuring Many-to-Many, Many-to-One, and One-to-Many relationships.
  2. Prefer Foreign Key Constraints over a column that refers to a Primary Key or other unique identifier.
  3. Use some annotation like @JsonIgnore to prevent infinite JSON serialization when using Jackson.
  1. https://www.baeldung.com/hibernate-naming-strategy
  2. https://dev.to/jhonifaber/hibernate-onetoone-onetomany-manytoone-and-manytomany-8ba
  3. https://www.baeldung.com/hibernate-one-to-many
  4. https://www.baeldung.com/jackson-bidirectional-relationships-and-infinite-recursion
  5. https://www.baeldung.com/jpa-one-to-one
  6. https://docs.jboss.org/hibernate/orm/5.4/userguide/html_single/Hibernate_User_Guide.html#architecture
  7. https://docs.jboss.org/hibernate/orm/5.4/userguide/html_single/Hibernate_User_Guide.html#fetching

Code samples:

  1. https://github.com/Thoughtscript/spring_boot_2023/tree/main/server/src/main/java/io/thoughtscript/bootexample/domain
  2. https://github.com/Thoughtscript/java_stuff

Spring: Tests

  1. Spring Integration Tests are foremost characterized by the injection of the WebApplicationContext:

    @Autowired
    private WebApplicationContext wac;
    
    • WebApplicationContext spins up a test Application Context so Services, Controllers, etc. are called as they are (not in isolation from each other).
    • Spring Mocks will also be used in Integration Tests.
  2. Spring Unit Tests don't spin up a test WebApplicationContext and rely heavily on Spring Mocks to achieve test isolation.

Example Integration Tests

import com.thoughtscript.springunit.config.AppConfig;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;

import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = { AppConfig.class })
@WebAppConfiguration
public class ExampleControllerIntegrationTest {

    @Autowired
    private WebApplicationContext wac;

    private MockMvc mockMvc;

    @Before
    public void preTest() throws Exception {
        mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build();
    }

    @Test
    public void test() {
        try {
            // Actually calls the endpoint
            mockMvc.perform(get("/test/fetch"))
                    .andDo(print())
                    .andExpect(status().isOk())
                    .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8))
                    .andExpect(jsonPath("$.text").value("Hello You!"));

        } catch (Exception e) {
            System.out.println("Exception: " + e);
        }
    }

    @After
    public void postTest() {
        mockMvc = null;
    }
}

Example Unit Tests

import static org.mockito.Mockito.*;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

public class ExampleControllerUnitTest {

    private MockMvc mockMvc;

    @Mock
    private ExampleService exampleService;

    @InjectMocks
    private ExampleController exampleController;


    @Before
    public void preTest() {
        MockitoAnnotations.initMocks(this);
        mockMvc = MockMvcBuilders.standaloneSetup(exampleController).build();
    }

    @Test
    public void test() {
        try {
            // Mocks the endpoint and service
            when(exampleService.helloText()).thenReturn("Hello You!");
            
            mockMvc.perform(get("/test/fetch"))
                    .andDo(print())
                    .andExpect(status().isOk())
                    .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8))
                    .andExpect(jsonPath("$.text").value("Hello You!"));

            verify(exampleService, times(1)).helloText();
            verifyNoMoreInteractions(exampleService);

        } catch (Exception e) {
            System.out.println("Exception: " + e);
        }
    }

    @After
    public void postTest() {
        mockMvc = null;
    }
}

Spring: Jupiter Tests

<?xml version="1.0"?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"
         xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
    <modelVersion>4.0.0</modelVersion>
    <groupId>io.thoughtscript.example</groupId>
    <artifactId>spring-cucumber</artifactId>
    <packaging>jar</packaging>
    <version>1.0.0</version>
    <name>spring-cucumber</name>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.1</version>
    </parent>

    <properties>
        <!-- Java Version Related -->
        <java.version>18</java.version>
        <maven.compiler.target>${java.version}</maven.compiler.target>
        <maven.compiler.source>${java.version}</maven.compiler.source>

        <!-- Dependency Versions -->
        <spring.boot.version>3.2.1</spring.boot.version>
        <lombok.version>1.18.30</lombok.version>
        <cucumber.version>7.15.0</cucumber.version>
        <junit-jupiter.version>5.10.1</junit-jupiter.version>
        <junit-platform-suite.version>1.10.1</junit-platform-suite.version>
    </properties>

    <dependencies>

        <!-- Spring Starter Dependencies -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <version>${spring.boot.version}</version>
        </dependency>

        <!-- Lombok Logging Dependencies -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>${lombok.version}</version>
            <scope>provided</scope>
        </dependency>

        <!-- JUnit 5 (Jupiter) Dependencies -->
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter</artifactId>
            <version>${junit-jupiter.version}</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.junit.platform</groupId>
            <artifactId>junit-platform-suite</artifactId>
            <version>${junit-platform-suite.version}</version>
            <scope>test</scope>
        </dependency>

        <!-- Cucumber Dependencies -->
        <dependency>
            <groupId>io.cucumber</groupId>
            <artifactId>cucumber-java</artifactId>
            <version>${cucumber.version}</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>io.cucumber</groupId>
            <artifactId>cucumber-spring</artifactId>
            <version>${cucumber.version}</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>io.cucumber</groupId>
            <artifactId>cucumber-junit-platform-engine</artifactId>
            <version>${cucumber.version}</version>
            <scope>test</scope>
        </dependency>

        <!-- Spring WebMvc Testing -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <!-- Inject Mocks Testing -->
        <dependency>
            <groupId>org.mockito</groupId>
            <artifactId>mockito-junit-jupiter</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>

            <!-- Required for mvn spring-boot:run command -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <configuration>
                    <archive>
                        <manifest>
                            <addClasspath>true</addClasspath>
                            <classpathPrefix>lib/</classpathPrefix>
                            <mainClass>io.thoughtscript.example</mainClass>
                        </manifest>
                    </archive>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

Cucumber Acceptance Testing

package io.thoughtscript.example.acceptance;

import io.cucumber.spring.CucumberContextConfiguration;
import org.junit.platform.suite.api.ConfigurationParameter;
import org.junit.platform.suite.api.IncludeEngines;
import org.junit.platform.suite.api.SelectClasspathResource;
import org.junit.platform.suite.api.Suite;
import org.springframework.boot.test.context.SpringBootTest;

import static io.cucumber.junit.platform.engine.Constants.GLUE_PROPERTY_NAME;

// Looks like this assumes the root dir test/resources/...
@SelectClasspathResource("features")
// This the test package containing the actual Java Step Definition Classes
@ConfigurationParameter(key = GLUE_PROPERTY_NAME, value = "io.thoughtscript.example.acceptance")

// These are required for Cucumber to get picked up by Jupiter during maven-sure-fire.
@Suite
@IncludeEngines("cucumber")

// These are required by Spring
@CucumberContextConfiguration
@SpringBootTest()
public class CucumberAcceptanceTests {
}
package io.thoughtscript.example.acceptance;

import io.cucumber.java.en.Given;
import io.cucumber.java.en.Then;
import io.cucumber.java.en.When;
import lombok.extern.slf4j.Slf4j;

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

//These all have to be public visibility
@Slf4j
public class StepDefinitions {

    private int actual;

    //These all have to be public visibility
    @Given("some number crunching")
    public void setup() {
        log.info("prepping...");
    }

    @When("I multiply {int} and {int}")
    public void multiply(Integer x, Integer y) {
        log.debug("Multiplying {} and {}", x, y);
        actual = x * y;
    }

    @When("I add {int} {int} and {int}")
    public void tripleAddition(Integer x, Integer y, Integer z) {
        log.debug("Adding {} {} and {}", x, y, z);
        actual = x + y + z;
    }

    @Then("the result is {int}")
    public void the_result_is(Integer expected) {
        log.info("Result: {} (expected {})", actual, expected);
        assertEquals(expected, actual);
    }
}
Feature: Cucumber Spring Example

  Background: A Basic Example
    Given some number crunching

  Scenario: Multiplication
    When I multiply 4 and 5
    Then the result is 20

  Scenario: Triple Addition
    When I add 1 2 and 3
    Then the result is 6

Refer to: https://github.com/Thoughtscript/spring_cucumber/tree/main/src/test/java/io/thoughtscript/example/acceptance

Controller Tests

package io.thoughtscript.example.controllers;

import io.thoughtscript.example.services.ExampleService;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit.jupiter.SpringExtension;

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

@ExtendWith(SpringExtension.class)
@AutoConfigureMockMvc
@SpringBootTest()
class ExampleRestControllerIntegrationTest {

    private final String testString = "OK";

    @Autowired
    ExampleService exampleService;

    @Test
    void testA() {
        assertEquals(testString, exampleService.example());
    }
}
package io.thoughtscript.example.controllers;

import io.thoughtscript.example.services.ExampleService;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit.jupiter.SpringExtension;

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

@ExtendWith(SpringExtension.class)
@AutoConfigureMockMvc
@SpringBootTest()
class ExampleRestControllerIntegrationTest {

    private final String testString = "OK";

    @Autowired
    ExampleService exampleService;

    @Test
    void testA() {
        assertEquals(testString, exampleService.example());
    }
}
package io.thoughtscript.example.controllers;

import io.thoughtscript.example.services.ExampleService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;

import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@WebMvcTest(ExampleRestController.class)
class ExampleRestControllerWebMvcTest {

    //This has to be present and will be injected automatically into the ExampleRestController.
    @MockBean
    ExampleService exampleService;

    @Autowired
    private MockMvc mvc;

    @Test
    void testA() throws Exception {
        mvc.perform(MockMvcRequestBuilders
                        .get("/api/example")
                        .accept(MediaType.APPLICATION_JSON))
                .andDo(print())
                .andExpect(status().isOk());
    }

}

Refer to: https://github.com/Thoughtscript/spring_cucumber/tree/main/src/test/java/io/thoughtscript/example/controllers

Basic Tests

package io.thoughtscript.example.helpers;

import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;

@Slf4j
class StaticHelpersTest {

    private final String EXPECTED = "invoked";

    @BeforeAll
    // This has to be a static method
    static void init() {
        log.info("JUnit 5 Jupiter tests initializing...");
    }

    @BeforeEach
    void eachInit() {
        log.info("Running before each time...");
    }

    @Test
    // These cannot be private visibility apparently
    void testA() {
            assertEquals(EXPECTED, StaticHelpers.invoke());
    }

    @Test
    void testB() {
        assertNotNull(StaticHelpers.invoke());
    }

    @Test
    void testC() {
        assertEquals(EXPECTED.length(), StaticHelpers.invoke().length());
        assertNotEquals("incorrectString", StaticHelpers.invoke());
    }

    @AfterEach
    void eachAfter() {
        log.info("Running after each time...");
    }


    @AfterAll
    // This has to be a static method
    static void shutdown() {
        log.info("JUnit 5 Jupiter tests completed...");
    }
}

Refer to: https://github.com/Thoughtscript/spring_cucumber/tree/main/src/test/java/io/thoughtscript/example/helpers

Service Tests

package io.thoughtscript.example.services;

import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import static org.junit.jupiter.api.Assertions.*;

@Slf4j
@SpringBootTest
/*
 Apparently auto-wiring into a "basic" Jupiter test
 also requires this annotation now.
*/
public class ExampleServiceWithAutoWiringTest {
    private final String EXPECTED = "OK";

    @Autowired
    ExampleService testService;

    @Test
    void testA() {
        assertEquals(EXPECTED, testService.example());
    }
}
package io.thoughtscript.example.services;

import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;

@Slf4j
class ExampleServiceWithoutAutoWiringTest {

    private final String EXPECTED = "OK";

    private ExampleService testService = new ExampleService();

    @Test
    void testA() {
        assertEquals(EXPECTED, testService.example());
    }
}

Refer to: https://github.com/Thoughtscript/spring_cucumber/tree/main/src/test/java/io/thoughtscript/example/services

  1. https://github.com/Thoughtscript/spring_cucumber/

Spring: Serverless

// Recommend Typescript, JavaScript, or ECS Fargate (which deploys a complete Spring app) due to performance concerns around Spring Serverless...
  1. https://www.baeldung.com/java-aws-lambda-hibernate
  2. https://www.baeldung.com/spring-cloud-function
  3. https://www.rowellbelen.com/serverless-microservices-with-spring-boot-and-spring-data/
  4. https://github.com/eugenp/tutorials/tree/master/aws-modules/aws-lambda/lambda/src/main/java/com/baeldung/lambda

Spring: WebFlux

The Reactive paradigm attempts to address shortcomings with blocking, highly-concurrent, systems at scale.

Reactive programming introduces Back-Pressure as the gradual degradation in performance as throughput increases throughout a web service and as information moves through dependencies and internal resources (they use a "water pipe" metaphor - it can get clogged, as water throughput increases pressure increases on the whole system, etc.). Prior paradigms don't handle Back-Pressure very efficiently.

Reactive Programming Principles

  1. A preference for Functional Programming.
  2. Asynchronous Programming from the beginning, not as an afterthought.
  3. Message driven - in line with other coterminous attempts to address the concerns above (Actor-Based systems like Akka, Event Stream, and Messaging systems like Kafka, etc.).
  4. Elasticity - resources are efficiently allocated based on Back-Pressure to reduce performance degradation.

WebFlux Features

  1. Functional Routers - API endpoints that are implemented using Functional Programming.
  2. Streams API oriented.
  3. Mono and Flux as first-class citizens. Promise-like entities available right out of the box.
  4. Backing Reactor daemons to provide Multi-Threaded event loops.

Better performance in highly concurrent use cases.

Prohibitions

  1. One cannot use .block() within any reactive context. Use .subscribe() instead (it's non-blocking but will return a value as an observer).

MongoDB DocumentReferences

  1. Mongo DBRefs aren't supported in Reactive Spring Mongo DB. (One can combine the results of multiple Reactive Streams but that's tedious and unreadable.)
  2. So, use the standard Spring Mongo DB dependencies for managed nesting using the @DBRef annotation.

Refer to: https://docs.spring.io/spring-data/mongodb/docs/3.3.0-M2/reference/html/#mapping-usage.document-references and https://github.com/spring-projects/spring-data-mongodb/issues/3808

  1. https://www.baeldung.com/spring-webflux-concurrency
  2. https://www.baeldung.com/spring-mongodb-dbref-annotation
  3. https://docs.spring.io/spring-data/mongodb/docs/3.3.0-M2/reference/html/#mapping-usage.document-references
  4. https://github.com/spring-projects/spring-data-mongodb/issues/3808

Code samples:

  1. https://github.com/Thoughtscript/spring_2023

Spring: Threads

Spring Threads

  1. Spring Threading manages Threads for the entire web application.
  2. Spring Threads are utilized in processing the actual Business Logic.
  3. Spring Threads use TaskExecutor and TaskScheduler which are implementations of and wrappers for the underlying native Java Executor, ThreadPoolExecutor, etc. (Spring Executors can be used in lieu of the native Java Executors above.)
  4. Spring Executors also execute implementations of Runnable.

Enable Asynchronous task execution via:

@Configuration
@EnableAsync
@EnableScheduling
public class AppConfig {
}

Set the Spring Task Execution Pool defaults:

spring:
  task:
    execution:
      pool:
        core-size: 2
        max-size: 4

Refer to: https://docs.spring.io/spring-framework/docs/4.2.x/spring-framework-reference/html/scheduling.html

Tomcat Threads

Tomcat Threading manages inbound and outgoing web requests, connections, etc. Tomcat Threads are allocated at the "periphery" of the application - so-called Server Thread Pools that continue to be pooled/allocated even when an application itself is terminated.

Remember that many .war files might live within the same Tomcat deployment - Tomcat Threads are shared by all such deployed applications with the web container.

The configuration below (application.yml) specifies the minimum and maximum number of Threads extant in the Tomcat Thread Pool:

server:
  port: 8080
  tomcat:
    max-connections: 200
    # Tomcat thread pool
    threads:
      min: 2
      max: 4

Refer to: https://docs.spring.io/spring-boot/docs/current/reference/html/application-properties.html

  1. https://www.baeldung.com/java-web-thread-pool-config
  2. https://docs.spring.io/spring-boot/docs/current/reference/html/application-properties.html
  3. https://docs.spring.io/spring-framework/docs/4.2.x/spring-framework-reference/html/scheduling.html

Code samples:

  1. https://github.com/Thoughtscript/spring_2023/blob/main/_spring/src/main/java/io/thoughtscript/example/controllers/PasswordlessRestController.java

Spring: Asynchronous Programming

Java Spring accomplishes Asynchronous programming in three primary ways:

  1. CompletableFutures and Futures
  2. Executors, Threading, and the @Async keyword
  3. Asynchronous Messaging

@EnableAsync

Enables Spring support for Asynchronous programming:

@Configuration
@EnableAsync
public class AppConfig implements AsyncConfigurer {

    @Override
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(7);
        executor.setMaxPoolSize(42);
        executor.setQueueCapacity(11);
        executor.setThreadNamePrefix("MyExecutor-");
        executor.initialize();
        return executor;
    }

    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        return new MyAsyncUncaughtExceptionHandler();
    }
 }

@Async

After the above is configured, one can use the @Async keyword to automatically wrap a Bean method with an Executor:

@Async
public void asyncMethodWithVoidReturnType() {
    System.out.println("Execute method asynchronously. " + Thread.currentThread().getName());
}

@Async
public Future<String> asyncMethodWithReturnType() {
    System.out.println("Execute method asynchronously - " + Thread.currentThread().getName());
    try {
        Thread.sleep(5000);
        return new AsyncResult<String>("hello world !!!!");
    } catch (InterruptedException e) {
        //
    }

    return null;
}

The Bean method should be:

  1. Public visibility so that Spring can proxy (an interface for) the Asynchronous method. (Consequently, the method can't be invoked within the same Class since doing so would bypass the proxy.)
  2. Have avoid, Future, or CompletableFuture return type.
  1. https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/scheduling/annotation/EnableAsync.html
  2. https://www.baeldung.com/spring-async
  3. https://spring.io/guides/gs/async-method/

Spring: Techniques

Spring Data Mongo

Remember that for two connections, one should use distinct Mongo configuration techniques like so:

package io.thoughtscript.example.configurations;

import com.mongodb.ConnectionString;
import io.thoughtscript.example.Constants;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import org.springframework.data.mongodb.MongoDatabaseFactory;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.SimpleMongoClientDatabaseFactory;
import org.springframework.data.mongodb.repository.config.EnableMongoRepositories;

@Configuration
@EnableMongoRepositories(basePackages = "io.thoughtscript.example.repositories")
public class MongoConfiguration {

    @Bean
    public MongoDatabaseFactory mongoDatabaseFactory() {
        return new SimpleMongoClientDatabaseFactory(new ConnectionString("mongodb://localhost:27017/" + Constants.MONGO_DB_NAME));
    }

    @Bean
    public MongoTemplate mongoTemplate() throws Exception {
        return new MongoTemplate(mongoDatabaseFactory());
    }
}
package io.thoughtscript.example.configurations;

import com.mongodb.reactivestreams.client.MongoClient;
import com.mongodb.reactivestreams.client.MongoClients;
import io.thoughtscript.example.Constants;
import io.thoughtscript.example.repositories.LanguageMongoReactiveRepository;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.mongodb.config.AbstractReactiveMongoConfiguration;
import org.springframework.data.mongodb.core.ReactiveMongoTemplate;
import org.springframework.data.mongodb.repository.config.EnableReactiveMongoRepositories;

@Configuration
@EnableReactiveMongoRepositories(basePackageClasses = {LanguageMongoReactiveRepository.class})
@ComponentScan(basePackages = "io.thoughtscript.example.repositories")
class ReactiveMongoConfiguration extends AbstractReactiveMongoConfiguration {

  @Value("${spring.data.mongodb.host}")
  private String host;
  @Value("${spring.data.mongodb.port}")
  private Integer port;

  @Override
  protected String getDatabaseName() {
    return Constants.MONGO_DB_NAME;
  }

  @Override
  public MongoClient reactiveMongoClient() {
    return MongoClients.create();
  }

  @Bean
  public ReactiveMongoTemplate reactiveMongoTemplate() {
    return new ReactiveMongoTemplate(reactiveMongoClient(), getDatabaseName());
  }

}
  1. Reason: two configuration Classess that have overlapping Beans (say AbstractMongoClientConfiguration and AbstractReactiveMongoConfiguration which both initialize MappingMongoConverter mappingMongoConverter) will fail to initialize correctly in the Application Context.

Refer to: https://www.baeldung.com/spring-data-mongodb-tutorial

Mapping Domain to Tables

Many frameworks have in-built conventions that dictate which Classes are mapped to what Tables. While usually helpful, one will often want to override these default conventions.

There are two common ways of doing that. The first is encountered when using a full ORM (Hibernate) to map, populate, and serialize the Columns of a Row into a POJO 1-1:

@Entity(name = "MyTable")
@Table(name = "MyTable")

In some stacks, JPA Entity framework is used by itself and one can point SQL queries to another Table using @Entity alone (without @Table):

@Entity(name = "MyTable")
//@Table(name = "MyTable")

Refer to: https://www.baeldung.com/jpa-entity-table-names#jpqlTableName

Also: https://www.baeldung.com/hibernate-naming-strategy#1-manual-escaping-using-double-quotes

  1. https://www.baeldung.com/spring-data-mongodb-tutorial
  2. https://baeldung.com/jpa-entity-table-names#jpqlTableName
  3. https://www.baeldung.com/hibernate-naming-strategy#1-manual-escaping-using-double-quotes