Skip to content

Instantly share code, notes, and snippets.

@ZapDos7
Last active March 11, 2024 16:18
Show Gist options
  • Save ZapDos7/7ace311e2615289b31e33bb7c6ffe6b7 to your computer and use it in GitHub Desktop.
Save ZapDos7/7ace311e2615289b31e33bb7c6ffe6b7 to your computer and use it in GitHub Desktop.

Spring

Maven

Spring Boot

Spring Cloud

Spring Security

Spring Events

Spring Boot Testing

Observability

Logging

Swagger

Maven

Intro

  • We include a pom.xml (POM = Project Object Model) file which contains data about files & dependencies so it auto handles them when needed)

Maven Artifact

It is something produced or used by the project e.g. JARs, source/binary distr etc)

  • mvn archetype:generate - -DgroupId=X // universally unique identified for project e.g. company name - -DartifactId=Y // directory/name of app - -DarchetypeVersion=W - extension -> type - classifier -> type - The ones above are the artifact ID. Also we can add: - -DinteractiveMode=false

Standard Project Structure

Y
└ pom.xml     <- helps parser, it's based on schema
└ src/
  └ main
     └ java
        └ com
           └ X (my company)
              └ app
                 └ Application.java
  └ test
      └ java
        └ com
           └ X (my company)
              └ app
                 └ TestApplication.java

mvn package

Step in build lifecycle = phase (not goal)

Each level does all the previous ones: mvn compile:

  • validate
  • generate sources
  • process sources
  • generate resources
  • process resources
  • compile

Test

java -cp target/my-app-1.0-SNAPSHOT.jar com.mycompany.app.App

Most Common Phases (Default)

  1. validate (is project correct? is all necessary info available?)
  2. compile
  3. test (using suitable unit testing framework - should not require the code to be packaged/deployed)
  4. package (compiled code: JAR or other distributed format)
  5. integration-test (process & deploy the package if needed in environment: integration tests)
  6. verify (if package is valid and meets quality data)
  7. install (install to local repo: use as dependency in other projects locally)
  8. deploy (done integration/release env copies final package to remote repo)

Two Other Lifecycles

  1. clean (cleans up artifacts created by prior builds)
  2. site (gemerates site documentation for this project in javadoc format)

Terms

  • MOJO (Σ(MOJO) = plugin) is a Java class that implements the org.apache maven plugin Mojo Interface (it's the implementation of a goal)
  • Snapshot: indicates WIP
  • APT: format of documentation
  • XDoc: format of documentation

Dependencies

Need: group ID, artifact ID, version. Optionally: scope, type, optional (they have default values)

Dependency Scope

  • compile: needed to work
  • provided: needed but if already exists in package, not (e.g. Tomcat)
  • test: only for test libs, not for final deployment (e.g. JUnit)
  • runtime: compiler may not need it but it's needed on runtime
  • system
  • import

Version Requirements

  • soft: 1.0 (earlier works too)
  • hard: [1.0] (the only one needed)

Version Order Specifications

Exclusions in dependencies for conflict resolution

e.g. [1.0] exclude this because 1.7 is better:

Directories

  • home/.m2/ where our JARs reside
  • /target/folder/ where compiled works reside

Repositories

They hold the build artifacts & dependencies)

  • local (computer directory where maven runs - all you should do is delete)
  • remote (fully remote or just shared on server in company etc - upload/download)
    • downloaded when missing depedency (pom.xml specify mirrors if needed)

Build offline

mvn -o package

Spring Boot

Intro

  • Spring simplifies Java development
  • 4 strategies:
    • lightweight & minimally invasive development with POJOs (plain old Java object)
    • loose coupling thgouth DI (dependency injection) and interface orientation
    • declarative programming through aspects and common conventions
    • eliminating boilerplate code (repetitive) with aspects templates

DI

Traditionally, each object is responsible for obtaining its own references to the objects it collaborates with (= its dependencies), which leads to highly coulped and hard to test code. With DI, objects are given their dependencies at creation time by some third party that coordinates each object in the system.

+---+  ┌--injected into-- A
| C | <┤ 
+---+  └--injected into-- B

e.g. construction injection (select interface at object constructor => loose coupling (only know interface, not implementation)

  • Mock object framework: Mockito to mock implementation of interfaces for easier testing
  • Wiring: create associations between app components (commonly via xml or Java) => we can change the dependencies without changing the depending classes
  • Application context: loads bean definitions and wires them together and it is fully responsible

Enables us to capture the functionality used throughout the application in reusable components

  • Cross cutting concerns: component with system services needed but aren't their cofre responsibility (these services span some components)
  • This leads to higher complexity, duplicate code between components even if abstracted module, so single method call, that call is duplicated in multiple places)
  • Components littered with code outside their functionality
  • So AOP (POJOs remain plain, components are cohesive and focused on their job and ignorant of any involved system calls)
  • Aspects declared by spring context only

Spring Container

  • creates the objects, wires them, configures and manages their lifecycle
  • uses DI to manage the components that make up an applications (including creating associations between collaborating components)
  • leads to cleaner code
  • easier to understand
  • easier to unit test
  • easier to reuse
  • 2 types of container implementations:
    • bean factory (simplest containers, provide support for DI, often too low level)
    • application contexts (build in the notion of a bean factory by providing application-framework services - resolve textual messages etc)

Bean Lifecycle

  1. Instantiate
    • inject values
  2. Populate properties
    • if implements beanId: setBeanName()
  3. (Aware) setBeanName()
  4. (Aware) setBeanFactory()
    • passes reference to the enclosing app context
  5. (Aware) setApplicationContext()
  6. Pre init bean post procs
  7. AfterPropertiesSet()
  8. Custom init
  9. Post init bean post procs
  10. Bean is ready to use!
  11. ..
  12. destroy() disposable bean
  13. Call custom destroy()
  14. END

Wiring Beans (how they communicate)

3 ways (we can also use more than one):

  • explicit configuration in XML (only for convenient XML namespace without equivalent)
  • explicit configuration in Java (better than XML)
  • implicit bean discovery & automatic wiring (recommended)

Automatic Wiring

These ways can also work together

  • component scanning: automatically discovers beans to be created in the application context (@ComponentScan, @Component)
  • autowiring: automatically satisfies bean dependencies (@Autowired works on constructors & setters and any method in class, works like @Injected: instantiate bean & pass in a bean assignable to corresponding class/interface: Spring tries to satisfy the dependency expressed in methods parameters (1 bean matches so 1 bean will be wired on), if many beans match, ambiguity exception, if no matching beans, throws exception unless required=false (check for NPE in code!))

For Testing, we use SpringJUnit4ClassRunner: automatically a Spring application context is created when the test starts

Tutorial's Point notes

  1. Core Container - core (fundamental parts like IoC & DI) - bean (BeanFactory - factory pattern) - Context (solid base - medium to access any objects defined and configured - application context) - SpEL (expression language for querying & manipulating object graph on runtime)
  2. Data Access/Integration - JDBC (adds abbstraction/layer) - ORM (adds integration layers for object relational mapping APIs like JPA, JDO, Hibernate...)) - OXM (adds abstraction layer for object/XML implementation JAXB, Castor...) - JMS (Java Messaging Service: producing and consuming messages) - Transaction (supports programmatic declarative transaction managment for classes that implement special interfaces & all PJOs)
  3. Web - web (basic web oriented integration features) - web-MVC - web-socket - wev-portlet
  4. Misc - AOP - Aspects (integration with AspectJS-AOP tool) - Instrumentation - Messaging (support for STOMP as the web socket sub protocol) - Testing

Overview

  • Redis: saves values as key0value set
  • Spring: container of singleton beans (only 1 copy is needed) as desired by creator - IoC
  • application.yml: read by app once it is booted (last first)
  • bootstrap.yml: read by app once it is booted (first first)
  • Jetty = lightweight Tomcat
  • mongeez for mongo db management

Sources

  • Spring in Action

Spring Boot with JPA/Hibernate

Properties and columns in entity definition

columnDefinition in @Column when ddl-auto != none

Hibernate Types

Source

Pitfalls

Source

Boost the Performance

Source

What does the property spring.jpa.open-in-view=true do?

Source

What is load balancing?

= The act of balancing the "load" or traffic to an application across multiple instances or servers

GCP (Google Cloud Platform)

  • With GCP, load balancing is a software cloud-based offering to automatically facilitate shift or traffic from one instance to another when traffic or load is increased.

  • Has 8 load balancer offerings:

    A. Global

    1. global external HTTP(S) load balancing (supports applications deployed in multiple regions (even if in the same country) that receives HTTP(s) traffic from the internet.)
    2. external HTTP(S) load balancing (classic)
    3. SSL proxy load balancing (also external)
    4. TCP proxy load balancing (also external - it "proxies" the traffic meaning it does not directly provide source/destination details to the instance, returns the response from the instance to the client, and requires 2 connections: one from the client to the load balancer and another from the load balancer to the instance.)

    B. Regional

    1. regional external HTTP(S) load balancing (external - instances deployed in one region only, receives HTTP traffic from the public internet)
    2. internal HTTP(S) load balancing (HTTP traffic e.g. back end APIs)
    3. internal TCP/UDP load balancing (TCP traffic - no data loss & UDP traffic - time is of the essence)
    4. external TCP/UDP network load balancing (supports TCP, UDP, ESP & ICMP traffic)
  • Categories

    • Flow of traffic (external traffic from internet to application (=> external facing IP open for anyone to send traffic to) or internal traffic between internal instances on same network (intenral-facing IP address))
    • Traffic type (type of protocol used e.g. HTTP, TCP, UDP)
    • Global or regional

Global External HTTP(S) Load Balancer

  • Receives HTTP traffic from the public internet

  • Has instances deployed accross multiple regions

  • GCP Offerings:

    • Classic/Legacy: stable release of global load balancer
    • New & Improved: preview stage load balancer (only for test environments)
  • The new version provides three advances traffic management features:

    • Traffic Policies (configure behavior & algorithms)
    • Traffic Actions (perform actions based on request & response)
    • Traffic Steering (route traffic based on request params)
  • How to setup:

    1. create an instance template
      • it ensures the same configuration files are used each time
      • it is required to set up a managed instance group and a managed instance group is required before we can set up a load balancer (having our instances be managed through a group will allow us to configure our load balancer to shift traffic between instances when needed)
      • GCP console > Computer Engine > Instance Templates > Create instance template (fill in info - you should add a network tag for health checks) Note: Under Management we can define the startup script.
    2. deploy a managed instance group (optionally configured with a health check)
      • GCP console > Computer Engine > Health Checks > Create Health Check
      • Here we define scope (global/regional), protocol (HTTP, TCP etc), we could also define a request path used in our app specifically for smoke tests
      • Health criteria (check interval, timeout, etc)
    3. configure firewall rules to accept health check probes (if needed)
      • VCP Network > Firewall > Create firewall rule.
      • Here we also define the specifics of the firewall; for this case, we need ingress direction of traffic (internal requests). We also need to define the IP addresses google uses, thus allowing traffic only from those systems - Health Check Documentation
    • Managed Instance Groups
      • GCP Console > Computer Engine > Instance Group > Create new Instance group
      • Select between stateful/stateless, define location, autohealing (select one of defined health check)
    1. create a load balancer & configure it to receive external traffic
      • GCP Console > VCP Network > IP Addresses
      • Here we can reserve an external static address (we can select type global/regional).
      • Now, the address is reserved and can be used elsewhere.
      • GCP Console > Network Services > Load Balancer > Create load balancer > select type > Start configuration (internet facing on internal only & global (static or new) or regional).
      • We give a name & define back end service(s) (using the predefined instance groups & health checks)
      • Then we define host & path rules
      • FE configuration
      • Eventually we review the set up & finalize the configuration, which creates the load balancer. Now ths FE's IP address is what the services will reach out to.
    2. test load balancer
      • Using the aforementioned IP we can POST http://ip.address:port/path in order to verify the load balancer is working.
      • In order to verify propetly we need to "kill" some of the instances, thus mimicking loaded conditions for our load balancer.
      • GCP Console > Compute Engines > VM Instances > check IPs, ssh there, kill the proper process & try anew.

Security Layers

  • Hardware & Media
  • Network
  • OS
  • Applications *

Authentication

Authorization

  • determines what the principal can or cannot do
  • it is based on authentication
  • it is also called access control
  • support:
    • web request
    • method invocation
    • domain object access control

Implementation

In memory

<!-- Main -->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- Test -->
<dependency>
  <groupId>org.springframework.security</groupId>
  <artifactId>spring-security-test</artifactId>
</dependency>

Java Config:

package com.frankmoley.lil.adminweb.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter /* !! */ {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .antMatchers("/", "/home").permitAll()
                .anyRequest().authenticated()
                .and()
                .httpBasic();
    }

    @Bean
    @Override
    public UserDetailsService userDetailsService() {
        UserDetails user =
                User.withDefaultPasswordEncoder()
                        .username("user")
                        .password("password")
                        .roles("USER")
                        .build();
        return new InMemoryUserDetailsManager(user); // only for initial testing purposes - not scalable, NEVER in prod
    }

}

JDBC

. Add to schema/db a table users`

CREATE TABLE users
(
    username varchar_ignorecase(50) not null primary key,
    password varchar_ignorecase(500) not null,
    enabled  boolean not null
);

CREATE TABLE authorities
(
    username  varchar_ignorecase(50) not null,
    authority varchar_ignorecase(50) not null,
    constraint fk_authorities_users foreign key (username) references users (username)
);

CREATE UNIQUE INDEX ix_auth_username on authorities (username, authority);

Spring Boot with OAuth with JWT

  • Authorization server: supreme architectural component for web API security. Acts as centralization authorization points that allows apps & HTTP endpoints to identify the features of our app
  • Resource server: application that provides the access token to the clients to access the Resource server HTTP endpoints. It's a collection of libraries {HTTP endpoints, static resources, dynamic webpages}
  • OAuth2: authorization framework thath enables the application web security to access the resources from the client Grant Type (auth code), client ID, client secret
  • JWT: JSON Web Token, represents the claims secured between 2 parties

OAuth 2.0

Provider:

  • responsible for exposing OAuth 2.0 protected resources
  • configuration
    • create OAuth 2.0 clients who can access resources (independently or on behalf of user)
    • manage and evaluate tokens used for access (& where applicable, provider must supply interface for user to confirm granting access - confirmation page)

IoC -> DI for specific lightweight containers with IoC of a UI

Simple dependency example:

ClassName -----> Interface
   |                Λ
   |                |
   └-- creates->  Implementator   

Dependencies for a Dependency Injector

ClassName -----> Interface    <--------------------┐
   Λ                Λ                              |
   |                |                              |
   |           Implementator   <--- creates --- Assembler
   └-----------------------------------------------┘ 

3 Styles of DI

  1. IoC to interface injection
  2. IoC to setter injection
  3. IoC to constructor injection

Another way of DI

Service locator: an object or method that knows how to get hold of all the services that an app might need

   ┌----------> Service Locator <------------------┐
   |                                               |
   |                                               |
ClassName -----> Interface    <--------------------┤
                    Λ                              |
                    |                              |
               Implementator   <--- creates --- Assembler

You don't need the entire service locator: reduce by using a role interface, declare just the bit of interface needed

Service Locator vs DI

  • both provide decoupling but their difference: how the implementation is provided to the app class
  • service locator: asks explicitly by a message
  • DI: no explicit request (service appears to app class)

Flyway falsely indicated duplicate version

Issue: mvn spring-boot:run returns error "Flyway duplicate script version" BUT db does not contain this script under flyway_schema_version. Solution: mvn clean package

Intro - Why use Spring Events?

Observer pattern implemented in Spring basically.

  • introduce loose coupling => flexibility
  • simpler testing process
  • changes in one component do not affect the others
  • provides publush/subscribe capabilities
  • publishers & subscribers are not tied up

It also follows open-closed principle (open for extension, closed for modification)

Components

Application Event

Is a simple pojo

class CustomEvent {
    String name;
    ...
}

Publisher

Constructs & publishes event objects

@Autowired ApplicationEventPublisher publisher;
void method() {
    publisher.publishEvent(new CustomEvent());
}

Listeners

Can be implemented with an annotation:

class MyListener {
    @EventListener public void onEvent(Object o) {}
}
  • Each method will be automatically registered as a new application listener to listen for one or many events, depending on the signature of the method.
  • have non void return type (result is sent as a new event - if returns collection, each element is sent as a new individual event)
  • can define order to be invoked using Order(X) (X = int, order number)

or by implementing ApplicationListener interface:

class MyListener implements ApplicationListener<ApplicationEvent> {
    @Override public void onApplicationEvent(ApplicationEvent e) {}
}
  • With the latter form as long as it is registered as a Spring bean, it will receive events.
  • Spring uses the signature of the listener to determine if it matches that event or not.
  • It can only be used for objects that extend ApplicationEvent class.
  • Can only process one event type.
  • can define order to be invoked using Order(X) (X = int, order number)

All Listeners are automatically registered

Synchronous & Asynchronous events

Synchronous events (default): the publisher thread blocks until all listeners have finished listening the event. Asynchronous events: the event is published in a new thread & the execution is independent from the listener.

Transaction Bound Events

Spring allows to bind an event to a phase of transaction

+================+
| @Transactional |
+================+ --------> BEFORE_COMMIT
| commit         |
+----------------+ --------> AFTER_COMMIT
| rollback       |
+----------------+ --------> AFTER_ROLLBACK
| completion     |
+================+ --------> AFTER_COMPLETION

Filtering events

Spring allows listeners to listen under conditions

Predefined events

Application - tied to application states

  • ApplicationStartingEvent
  • ApplicationEnvironmentPreparedEvent
  • ApplicationContextInitializedEvent
  • ApplicationPreparedEvent
  • ApplicationStartedEvent

Context - tied to application context

  • ContextRefreshedEvent
  • ContextStartededEvent
  • ContextStoppedEvent
  • ContextClosedEvent

If we need listeners before the context is created, we need to add them manually:

public static void main(String[] args) {
    SpringApplication springApp = new SpringApplication(Application.class);
    springApp.addListeners(new MyListener());
    springApp.run(args);
}

Implementation

When creating a publisher, we can simply

// event
@Data
public class CustomerRegisteredEvent
{
    private final Customer customer;
}


// Service - publisher
@Component
@RequiredArgsConstructor
public class CustomerService
{

    private final CustomerRepository customerRepository;

    private final ApplicationEventPublisher publisher;

    public void register(Customer customer)
    {
        customerRepository.save(customer);
        publisher.publishEvent(new CustomerRegisteredEvent(customer));
    }

    public void remove(Customer customer)
    {
        customerRepository.delete(customer);
    }
}

// Service - listener
@Component
@RequiredArgsConstructor
public class EmailListeners
{
    private final EmailService emailService;

    @EventListener
    public void onRegisterEvent(CustomerRegisteredEvent event)
    {
        emailService.sendRegisterEmail(event.getCustomer());
    }
}

Async Events

How to

  • Use @Async on listener needed to be executed asyncronously.
  • Add @EnableAsync on our SpringBootApplication

Limitations

  • Cannot publish event by returning a value (if we need to, we can use ApplicationEventPublisher.publish())
  • Exceptions are not propagated to the caller (but we can implement AsyncUncaughtExceptionHandler interface)

Implementation

@Component
@RequiredArgsConstructor
public class AnalyticsCustomerRegisteredListener
{
    private final AnalyticsService analyticsService;

    @Async //!
    @EventListener //!
    public void onRegisterEvent(CustomerRegisteredEvent event)
    {
        analyticsService.registerNewCustomer(event.getCustomer());

    }
}

Filtering Events

Use

@EventListener(condition="") (condition in SPEL), evaluated if

  • true
  • "true", "on", "yes", "1"
  • default = "" (always handled)

Examples

  • #event to reference an event
  • #event.customer.type eq 'b2c'
  • @myBean.test(#event)

Implementation

// listener
@Component
@RequiredArgsConstructor
public class PromotionListeners
{

    private final PromotionService promotionService;

    @EventListener(condition = "#event.customer.newsletter")
    public void onRegistrationEvent (CustomerRegisteredEvent event)
    {
        promotionService.applyPromotion(event.getCustomer());
    }
}

Transactional Events

Why do we need them

  • Spring allows us to bind an event listener to a phase of the current transaction.
  • In case something goes wrong, rollback => consistent state

Use

  • @Transactional: combines more than one writes on a database as a single atomic operation.
  • @TransactionalEventListener(phase=abc) - event consumption is delayed until a certain transactional outcome (more fine grain control). abc takes values: BEFORE_COMMIT, AFTER_COMMIT (default), AFTER_ROLLBACK, AFTER_COMPLETION.
  • We should opt to test native queries in repo level since they are the most prone to issues

  • We should select the components we wish to test so as to not load the entire application context needlessly & have more specific scope of testing.

  • Two types of testing:

    • integration testing (when >1 teams) - top down
    • else, bottom up
  • TDD - red green testing

  • To test only the service layer: @SpringBootTest(WebEnvironment.NONE)

  • What to test in a controller?

    1. URL mapping
    2. Deserialize input
    3. Validate input
    4. Business logic (only this one is tested in Unit tests)
    5. Serialize output
    6. Translate exceptions

    All 6 can be tested in integration tests

  • @WebMvcTest or WebFlowTest for controller testing, no need for HTTP resever, @MockBean for services/repos

  • @SpringBootTest should not be overused (when the full application context isn't needed)

  • To test HTTP interactions: @MockMvc

  • To simulate slow conditions/chunked responses: OkHttp MockWebServer & WireMock (mocks at HTTP level)

  • If we mock all services:

    • it's fast & easier BUT false assurance If we mock no service:
    • accurate BUT slow and expensive So we use Spring Cloud Contract, which uses stubs, defined contracts between services, gives fast feedback without a need for a full setup.

Unit Testing

package com.frankmoley.lil.api.service;

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

import com.frankmoley.lil.api.data.entity.CustomerEntity;
import com.frankmoley.lil.api.data.repository.CustomerRepository;
import com.frankmoley.lil.api.web.model.Customer;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

@ExtendWith(MockitoExtension.class)
public class CustomerServiceTest {

  @InjectMocks // inject Mocked repo into Service
  CustomerService customerService;

  @Mock
  CustomerRepository customerRepository;

  @Test
  void getAllCustomers(){
    Mockito.doReturn(getMockCustomers(2)).when(customerRepository).findAll();
    List<Customer> customers = this.customerService.getAllCustomers(); // implemented in service layer
    assertEquals(2, customers.size()); // we added 2 customers in repository so the fetching method should return 2
  }

  private Iterable<CustomerEntity> getMockCustomers(int size){
    List<CustomerEntity> customers = new ArrayList<>(size);
    for(int i=0;i<size;i++){ // add dummy data
      customers.add(new CustomerEntity(UUID.randomUUID(), "firstName" + i, "lastName" + i,
          "email"+i+"@test.com", "555-515-1234", "1234 Main Street"));
    }
    return customers;
  }
  
  //
  @Test
  void getCustomer() {
    CustomerEntity entity = getMockCustomerEntity();
    Optional<CustomerEntity> optional = Optional.of(entity);
    Mockito.doReturn(optional).when(customerRepository).findById(entity.getCustomerId());

    Customer customer = service.getCustomer(entity.getCustomerId().toString());
    assertNotNull(customer);
    assertEquals("testFirst", customer.getFirstName());
  }

  @Test
  void getCustomer_notExists() {
    CustomerEntity entity = getMockCustomerEntity();
    Optional<CustomerEntity> optional = Optional.empty();
    Mockito.doReturn(optional).when(customerRepository).findById(entity.getCustomerId());
    assertThrows(NotFoundException.class, ()->service.getCustomer(entity.getCustomerId().toString()), "exception not throw as expected");
    // throw custom
  }


  @Test
  void findByEmailAddress() {
    CustomerEntity entity = getMockCustomerEntity();

    Mockito.doReturn(entity).when(customerRepository).findByEmailAddress(entity.getEmailAddress());
    Customer customer = service.findByEmailAddress(entity.getEmailAddress());
    assertNotNull(customer);
    assertEquals("testFirst", customer.getFirstName());
  }

  @Test
  void addCustomer() {
    CustomerEntity entity = getMockCustomerEntity();
    when(customerRepository.findByEmailAddress(entity.getEmailAddress())).thenReturn(null);
    when(customerRepository.save(any(CustomerEntity.class))).thenReturn(entity); // "any" -> fetch something, does not verify the customer is the one we needed because the ID is not set => we need more than unit tests!!!
    Customer customer = new Customer(entity.getCustomerId().toString(), entity.getFirstName(), entity.getLastName(),
        entity.getEmailAddress(), entity.getPhoneNumber(), entity.getAddress());
    customer = service.addCustomer(customer);
    assertNotNull(customer);
    assertEquals("testLast", customer.getLastName());
  }

  @Test
  void addCustomer_existing() {
    CustomerEntity entity = getMockCustomerEntity();
    when(customerRepository.findByEmailAddress(entity.getEmailAddress())).thenReturn(entity);
    Customer customer = new Customer(entity.getCustomerId().toString(), entity.getFirstName(), entity.getLastName(),
        entity.getEmailAddress(), entity.getPhoneNumber(), entity.getAddress());
    assertThrows(ConflictException.class, () -> service.addCustomer(customer), "should have thrown conflict exception");
  }

  @Test
  void updateCustomer() {
    CustomerEntity entity = getMockCustomerEntity();
    when(customerRepository.save(any(CustomerEntity.class))).thenReturn(entity); // any again
    Customer customer = new Customer(entity.getCustomerId().toString(), entity.getFirstName(), entity.getLastName(),
        entity.getEmailAddress(), entity.getPhoneNumber(), entity.getAddress());
    customer = service.updateCustomer(customer);
    assertNotNull(customer);
    assertEquals("testLast", customer.getLastName());
  }

  @Test
  void deleteCustomer() {
    UUID id = UUID.randomUUID();
    doNothing().when(customerRepository).deleteById(id); // doNothing() because void method
    service.deleteCustomer(id.toString());
  }


  private CustomerEntity getMockCustomerEntity(){
    return new CustomerEntity(UUID.randomUUID(), "testFirst", "testLast", "testemail@test.com", "555-515-1234", "1234 Test Street");
  }

}

Run with Coverage

(For IntelliJ): Properly executed lines are indicated with green, whereas imporerly ones with red. This also works with Surefire or other plugins.

Integration Testing

  1. Introduce resources folder where we introduce the test data (e.g. in sql files)
  2. In pom.xml, add:
<dependency>
  <groupId>com.h2database</groupId>
  <artifactId>h2</artifactId>
  <scope>test</scope>
</dependency>
  1. Code
-- schema
CREATE SCHEMA IF NOT EXISTS tdd;


CREATE TABLE IF NOT EXISTS tdd.CUSTOMERS (
    CUSTOMER_ID  UUID PRIMARY KEY,
    FIRST_NAME VARCHAR,
    LAST_NAME VARCHAR,
    EMAIL VARCHAR UNIQUE,
    PHONE VARCHAR,
    ADDRESS VARCHAR
);

-- data

INSERT INTO tdd.CUSTOMERS (CUSTOMER_ID, FIRST_NAME, LAST_NAME, EMAIL, PHONE, ADDRESS) VALUES ('054b145c-ddbc-4136-a2bd-7bf45ed1bef7','Cally','Reynolds','penatibus.et@lectusa.com','(901) 166-8355','556 Lakewood Park, Bismarck, ND 58505');
INSERT INTO tdd.CUSTOMERS (CUSTOMER_ID, FIRST_NAME, LAST_NAME, EMAIL, PHONE, ADDRESS) VALUES ('9ac775c3-a1d3-4a0e-a2df-3e4ee8b3a49a','Sydney','Bartlett','nibh@ultricesposuere.edu','(982) 231-7357','4829 Badeau Parkway, Chattanooga, TN 37405');
INSERT INTO tdd.CUSTOMERS (CUSTOMER_ID, FIRST_NAME, LAST_NAME, EMAIL, PHONE, ADDRESS) VALUES ('c04ca077-8c40-4437-b77a-41f510f3f185','Hunter','Newton','quam.quis.diam@facilisisfacilisis.org','(831) 996-1240','2 Rockefeller Avenue, Waco, TX 76796');
INSERT INTO tdd.CUSTOMERS (CUSTOMER_ID, FIRST_NAME, LAST_NAME, EMAIL, PHONE, ADDRESS) VALUES ('3b6c3ecc-fad7-49db-a14a-f396ed866e50','Brooke','Perkins','sit@vitaealiquetnec.net','(340) 732-9367','87 Brentwood Park, Dallas, TX 75358');
INSERT INTO tdd.CUSTOMERS (CUSTOMER_ID, FIRST_NAME, LAST_NAME, EMAIL, PHONE, ADDRESS) VALUES ('38124691-9643-4f10-90a0-d980bca0b27d','Nolan','Slater','sociis.natoque.penatibus@pedeCras.co.uk','(540) 487-5928','99 Sage Street, Reno, NV 89505');

and

package com.frankmoley.lil.api.service;

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

import com.frankmoley.lil.api.web.error.ConflictException;
import com.frankmoley.lil.api.web.error.NotFoundException;
import com.frankmoley.lil.api.web.model.Customer;
import org.junit.jupiter.api.Test;
import org.junit.platform.commons.util.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase.Replace;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.List;

@SpringBootTest
@AutoConfigureTestDatabase(replace = Replace.ANY)
public class CustomerServiceIntegrationTest {

  @Autowired
  private CustomerService service;

  @Test
  void getAllCustomers(){
    List<Customer> customers = this.service.getAllCustomers();
    assertEquals(5, customers.size()); // this passes if no changes in db OR if we cleanup after testing!!!
  }

  @Test
  void getCustomer(){
    Customer customer = this.service.getCustomer("054b145c-ddbc-4136-a2bd-7bf45ed1bef7");
    assertNotNull(customer);
    assertEquals("Cally", customer.getFirstName());
  }

  @Test
  void getCustomer_NotFound(){
    assertThrows(NotFoundException.class, () -> this.service.getCustomer("d972b30f-21cc-411f-b374-685ce23cd317"), "should have thrown an exception");
  }

  @Test
  void addCustomer(){
    Customer customer = new Customer("", "John", "Doe", "jdoe@test.com", "555-515-1234", "1234 Main Street; Anytown, KS 66110");
    customer = this.service.addCustomer(customer);
    assertTrue(StringUtils.isNotBlank(customer.getCustomerId()));
    assertEquals("John", customer.getFirstName());
    //cleaning up
    this.service.deleteCustomer(customer.getCustomerId());
  }

  @Test
  void addCustomer_alreadyExists(){
    Customer customer = new Customer("", "John", "Doe", "penatibus.et@lectusa.com", "555-515-1234", "1234 Main Street; Anytown, KS 66110");
    assertThrows(ConflictException.class, () -> this.service.addCustomer(customer));
  }

  @Test
  void updateCustomer(){
    Customer customer = new Customer("", "John", "Doe", "jdoe@test.com", "555-515-1234", "1234 Main Street; Anytown, KS 66110");
    customer = this.service.addCustomer(customer);
    customer.setFirstName("Jane");
    customer = this.service.updateCustomer(customer);
    assertEquals("Jane", customer.getFirstName());
    //cleaning up
    this.service.deleteCustomer(customer.getCustomerId());
  }

}

Web Integration Testing

Test the entire stack!

package com.frankmoley.lil.api.web.controller;

import static org.hamcrest.Matchers.containsString;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;

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

import com.fasterxml.jackson.databind.ObjectMapper;
import com.frankmoley.lil.api.web.model.Customer;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase.Replace;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MockMvc;

@SpringBootTest
@AutoConfigureMockMvc // ---
@AutoConfigureTestDatabase(replace = Replace.ANY)
public class CustomerControllerIntegrationTest {

  @Autowired // ---
  MockMvc mockMvc; //creates a pseudo dispatcher servlet which allows us to test the method

  @Test
  void getAllCustomers() throws Exception { // throwing exceptions is good to do in this point!
    this.mockMvc.perform(get("/customers")).andExpect(status().isOk())
        .andExpect(content().string(containsString("054b145c-ddbc-4136-a2bd-7bf45ed1bef7")))
        .andExpect(content().string(containsString("38124691-9643-4f10-90a0-d980bca0b27d")));
  }

  @Test
  void getCustomer() throws Exception {
    this.mockMvc.perform(get("/customers/054b145c-ddbc-4136-a2bd-7bf45ed1bef7")).andExpect(status().isOk())
        .andExpect(content().string(containsString("054b145c-ddbc-4136-a2bd-7bf45ed1bef7")));
  }

  @Test
  void getCustomer_notFound() throws Exception {
    this.mockMvc.perform(get("/customers/2b31469c-da3d-469f-9900-d00b5e4e352f")).andExpect(status().isNotFound());
  }

  @Test
  void addCustomer() throws Exception {
    Customer customer = new Customer("", "John", "Doe", "jdoe@test.com", "555-515-1234", "1234 Main St, Smallville KS 66083");
    ObjectMapper mapper = new ObjectMapper();
    String jsonString = mapper.writeValueAsString(customer);
    this.mockMvc.perform(post("/customers").content(jsonString).contentType("application/json")).andExpect(status().isCreated())
        .andExpect(content().string(containsString("jdoe@test.com")));
  }

  @Test
  void addCustomer_exists() throws Exception {
    Customer customer = new Customer("","Cally","Reynolds","penatibus.et@lectusa.com","(901) 166-8355","556 Lakewood Park, Bismarck, ND 58505");
    ObjectMapper mapper = new ObjectMapper();
    String jsonString = mapper.writeValueAsString(customer);
    this.mockMvc.perform(post("/customers").content(jsonString).contentType("application/json")).andExpect(status().isConflict());
  }

  @Test
  void updateCustomer() throws Exception {
    Customer customer = new Customer("c04ca077-8c40-4437-b77a-41f510f3f185","Jack","Bower","quam.quis.diam@facilisisfacilisis.org","(831) 996-1240","2 Rockefeller Avenue, Waco, TX 76796");
    ObjectMapper mapper = new ObjectMapper();
    String jsonString = mapper.writeValueAsString(customer);
    this.mockMvc.perform(put("/customers/c04ca077-8c40-4437-b77a-41f510f3f185").content(jsonString).contentType("application/json")).andExpect(status().isOk())
        .andExpect(content().string(containsString("Jack")))
        .andExpect(content().string(containsString("Bower")));
  }

  @Test
  void updateCustomer_badRequest() throws Exception {
    Customer customer = new Customer("c04ca077-8c40-4437-b77a-41f510f3f185","Jack","Bower","quam.quis.diam@facilisisfacilisis.org","(831) 996-1240","2 Rockefeller Avenue, Waco, TX 76796");
    ObjectMapper mapper = new ObjectMapper();
    String jsonString = mapper.writeValueAsString(customer);
    this.mockMvc.perform(put("/customers/2b31469c-da3d-469f-9900-d00b5e4e352f").content(jsonString).contentType("application/json")).andExpect(status().isBadRequest());
  }

  @Test
  void deleteCustomer() throws Exception {
    this.mockMvc.perform(delete("/customers/3b6c3ecc-fad7-49db-a14a-f396ed866e50")).andExpect(status().isResetContent());
  }
}
  • Observability = use {logging, metrics, tracing} in order to maintain an application
  • Centralized logging when distributed systems
  • How change log level within Spring Boot app to troubleshoot an issue without restarting your application: -> by using logback autoscan functionality, or with Spring cloud config library or with Spring Boot actuator and Spring admin
  • Wavefront: easily integrates w/ Spring Boot
  • Distributed monitoring: use Spring Boot actuator & micrometer
  • Spring cloud sleuth = Spring library used to implement distributed tracing

Log4J

Levels

  1. ALL
  2. DEBUG
  3. INFO
  4. WARN
  5. ERROR
  6. FATAL
  7. OFF
  8. TRACE

log.setLevel(level.CHOICE) : CHOICE & lower levels print

Core & Support Objects

Core

  • Logger object: responsible for capturing logging info. They are stored in a namespace hierarchy
  • Layout object: provides objects which are used to format logging info in different styles (support to apprender objects before publishin logging ingormation). They play an important role: publish info in a humanly readable way
  • Apprender object: lower level: responsible for publishing logging info to various destinations
Java Program:
                +--------------+
                | Level obj ** |           +-------------+
                +--------------+           | Destination |
                                           +-------------+
                +----------------+               Λ
                | log manager ** |               |
                +----------------+               |  info is logged
                                                 |
                +--------------+           +-----------------+
logging info -> | logger obj * |     ->    | Apprender Obj * |
                +--------------+           +-----------------+
                                                 Λ
                +---------------+                |
                | Filter obj ** |                | Formatted info
                +---------------+                |
                                            +--------------+
                +-----------------+         | Layout Obj * |
                | Obj renderer ** |         +--------------+
                +-----------------+
                
* core objects
** support objects

One of the best ways is DataSource-Proxy

Source

  1. Add this to maven file:
<dependency>
    <groupId>com.github.gavlyukovskiy</groupId>
    <artifactId>datasource-proxy-spring-boot-starter</artifactId>
    <version>${ds-proxy-spring-boot-starter.version}</version>
</dependency>
  1. application properties:
logging.level.net.ttddyy.dsproxy.listener=debug

These will log SQL statements like so:

Name:dataSource, Connection:5, Time:1, Success:True
Type:Prepared, Batch:False, QuerySize:1, BatchSize:0
Query:["
    SELECT
       u.id AS id1_7_,
       u.email AS email5_7_,
       u.external_id AS external6_7_,
       u.first_name AS first_na7_7_,
       u.last_name AS last_nam9_7_,
    FROM users u
    WHERE u.email = ?
"]
Params:[(
    john.doe@acme.com
)]

Thoughts about layered architecture

Source

Pointcuts determine join points of interest and thus enable us to control when advice runs.

Further reading:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment