SpringBoot Reference
Concepts
Loose Coupling
Loose coupling refers to designing a system where components are minimally dependent on each other.
- This allows for easier modification, testing, and maintenance because changes in one component have little to no impact on others.
- By Autowiring, we achieve loose coupling.
By using
new
keyword (instantiating an obejct), we tightly couple the dependency which is not good
-
Tightly Coupled:
public class UserService { private final UserRepository userRepository = new UserRepository(); // Directly creating a dependency // Methods using userRepository }
-
Loosely Coupled (using Dependency Injection):
public class UserService { private final UserRepository userRepository; // Constructor Injection public UserService(UserRepository userRepository) { this.userRepository = userRepository; } // Methods using userRepository }
Here, UserService
is loosely coupled with UserRepository
because it doesn’t
create the dependency itself.
Instead, UserRepository
is injected into UserService
, making it easier to
swap out UserRepository
implementations.
Inversion of Control
Instead of the Class taking responsibility of creating the object, the framework manages it for you
- the control of object creation and dependency management is inverted from the application code to a framework.
- The framework manages the lifecycle and interactions of objects.
Dependency Injection
Dependency Injection (DI) is a design pattern used to implement IoC ( Inversion of Control).
- In DI, the framework (like Spring) handles the creation and injection of dependencies or bean instantiation beans and wiring dependencies, rather than the classes managing their own dependencies.
Stereotype Annotations
Basic philosophy of Spring Boot : Conventions over configurations
@Component
- Purpose: Marks a class as a Spring component or bean.
- Commonly Used in: Utility classes, business logic classes, and other non-specialized components.
@Controller
- Purpose: Marks a class as a Spring MVC controller.
- Use Case: Used for classes that handle HTTP requests in a Spring MVC web application.
- Commonly Used in: Classes that define request mappings, handle user input, and return views or data to the client.
@Service
- Purpose: Marks a class as a service or business logic component.
- Use Case: Typically used for classes that contain the business logic of the application.
- Commonly Used in: Service layer classes that encapsulate business rules, data processing, and interactions with repositories or other services.
@Repository
- Purpose: Marks a class as a Spring Data repository.
- Use Case: Used for classes that interact with a database or external data source.
- Commonly Used in: Data access objects (DAOs) that perform CRUD (Create, Read, Update, Delete) operations on entities.
Config
Set a desired Port
server.port=8089
Application Yaml settings
Good Practice : Design application configuration using
@ConfigurationProperties
to ensure Type Safety
myConfig:
flag: true
message: "From YAML"
number: 100
Type Safety can be ensured with this
@Component
@ConfigurationProperties("myConfig")
public class MyConfiguration {
private boolean flag;
private String message;
private int number;
Banner
For Ascii banner, put the ASCII Art in banner.txt in and it will be taken Sample file
to turn off the banner
spring:
main:
banner-mode: "off"
For image banner, put the logo.png file and
spring:
banner:
image:
location: logo.png
Springboot Startup process
https://nitinkc.github.io/spring/microservices/springboot-startup/
Initial Data Setup
keep the sql script in the resources folder by the name data.sql
Scans
Component Scan
By default, the package containing the main method is scanned.
-
@ComponentScan
is used to specify the packages that Spring should scan to discover Spring-managed components like beans, controllers, services, etc.
@ComponentScan(basePackages = {"com.spring5.concepts",
"com.spring5.services"
"com.test.animals",
"com.flowers"
})
Entity Scan
@EntityScan
is specific to Spring Data JPA.
- It’s used to specify the packages where JPA entities are located.
- This is important because Spring Data JPA needs to know where the entity classes are in order to create repositories and perform CRUD operations.
@EntityScan(basePackages = {"com.learningJPA.dSpringDataRepository"
,"com.learningJPA.eTest.model"
//,"com.learningJPA.hibernate.*"
})
SpringBootApplication ScanBasePackages
@SpringBootApplication
is a meta-annotation that combines several annotations,
including @ComponentScan
.
-
scanBasePackages
within@SpringBootApplication
allows you to specify the base packages to scan for Spring components. - used in the main application class.
- It also scans the default package where the main application class is located.
- exclude argument is used to exclude specific auto-configurations, which means that Spring Boot won’t automatically configure the classes mentioned.
@SpringBootApplication(
scanBasePackages = {
"com.test.package1",
"com.test.service.security"
},
exclude = { JmxAutoConfiguration.class })
@Autowired & Dependency Injection
https://nitinkc.github.io/spring/microservices/dependency-injection-concepts/
Autowiring
- byType
- byName
- constructor - similar to byType, but through constuctor
https://nitinkc.github.io/spring/microservices/dependency-injection-concepts/#Autowiring
Eliminates the need to create a new object and hence the need of constructors
from the components
StudentService studentService = new StudentService();
@Autowired
StudentService studentService;//Free to use studentService object within the class anywhere
@Autowired
used for automatic dependency injection.
- Spring should find the matching bean and wire the dependency
Dependency injection is a design pattern in which objects are provided with their dependencies (i.e., the objects they need to collaborate with) rather than creating those dependencies themselves.
Automatic Injection: When you annotate a field, setter method, or
constructor with @Autowired
, Spring will
automatically inject the required dependency (another Spring bean) at runtime.
Constructor vs Setter Injection
- Constructor Injection for Mandatory Dependencies
- Setter Injection for Optional Dependencies
Lombok
To use the @RequiredArgsConstructor
annotation. Mark the fields you want to
include in the constructor as final
or annotate them with @NonNull
.
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
Sequence of execution
Postman/browser/client -> Controller -> Service -> Repository -> Service -> Controller
REST APIs
- Retrieve all Users - GET
/users
- Create a User - POST
/users
- Retrieve one User - GET
/user/{id}
->/user/1
- Delete a User - DELETE
/user/{id}
->/user/1
- Retrieve all posts for a User - GET
/user/{id}/posts
- Create a posts for a User - POST
/user/{id}/posts
- Retrieve details of a post - GET
/user/{id}/posts/{post_id}
Best Practices
- Use Plurals
https://nitinkc.github.io/microservices/Idempotence-HTTP-methods/#designing-restful-uris
Controller Vs RestController
@Controller
on a Controller class needs @ResponseBody
with method name
@Controller
@RequestMapping(method= RequestMethod.GET, path = "/health",
produces = { "application/json", MediaType.APPLICATION_XML_VALUE})
public class HealthCheckController {
@RequestMapping(path = "/check", method = RequestMethod.GET)
public @ResponseBody String hello(){// ResponseBody Annotation is compulsory
return "Health is Ok";
}
}
GET Request
https://nitinkc.github.io/spring/microservices/GET-rest-calls/
@RequestMapping(method = RequestMethod.GET,
path = "/student/{studentId}")
OR
@GetMapping(value = "/student/{studentId}",
path = "/student/{studentId}",
produces = { "application/json", MediaType.APPLICATION_XML_VALUE})
Between value
and path
attribute, value is commonly used to describe the
path
Shortened GetMapping
@GetMapping(path="/getMapping")
GetMapping returning a bean (JSON Response)
@GetMapping(path="/getBean")
public HelloWorldReturnBean helloWorldReturnBean() {
return new HelloWorldReturnBean("Hello World - From HelloWorldReturnBean");
}
Path Variable vs Request Param
Validation
Request Validation
- At Class level, add
@Validated
annotation and at the@Valid
at the parameter level - Check the
required
anddefaultValue
arguments of RequestParam Annotation
@RestController
@RequestMapping("/test")
@Validated
public class ValidationController {
@GetMapping("/email")
public String testEmail(@Valid @Email(message = "Please provide a valid email address")
@RequestParam(value = "email") String email ,
@RequestParam(value = "greet", required = false, defaultValue = "No Val from Request")
String greet,
@RequestParam(value = "count", required = false, defaultValue = "-1") Integer count) {
StringBuilder sb= new StringBuilder();
sb.append(email).append(" email OK").append("\nCount is ").append(count).append("\n").append(greet);
return sb.toString();
}
}
Response Validation
In the DTO Class (Using Lombok)
@Data
public class StudentRequestBody {
private int count;
@JsonProperty("studentIds")//If the name in the request body differs from variable name
private List<String> studentIdList;
@Email(message = "Incorrect EmailID received from DB")
private String emailId;
}
POST Request
@RequestMapping(method = RequestMethod.POST)
//OR
@PostMapping("/students")
With Map as Request Body
if request body is like below, a map can be used
Curl Request
curl --location 'localhost:8090/student/db/studentIdsByMap' \
--header 'Content-Type: application/json' \
--data '{
"values": ["10","12.5","50","100"]
}'
Read the Request Body using @RequestBody
in the method parameter into either a
Map, for simple structures or a class for complex
@PostMapping(path = "/studentIdsByMap",
consumes = {MediaType.APPLICATION_JSON_VALUE},
produces = {"application/json"})
public List<StudentDto> getStudentByIdsByMap(@RequestBody Map<String,List<Integer>> mapStudentIds){
...
}
With Class as Request Body
Curl Request
curl --location 'localhost:8090/student/db/studentIdsByClassName' \
--header 'Content-Type: application/json' \
--data '{
"greeting":"Hi from postman.",
"count":5,
"studentIds": ["1","2","3","4","5"]
}'
Corresponding Java class to catch the request Body
public class StudentRequestBody {
private int count;
@JsonProperty("studentIds")//If the name in the request body differs from variable name
private List<String> studentIdList;
private String greeting;
}
Controller with @RequestBody
@PostMapping(path = "/studentIdsByClassName",
consumes = {MediaType.APPLICATION_JSON_VALUE},
produces = {"application/json"})
public StudentDtoClass getStudentByIdsRequestBody(@RequestBody StudentRequestBody studentRequestBody){
...
}
Service
For a single student Id, JPA’s findById method can be utilized. It returns an Optional, so if in case the return is a null Optional Class findById can be utilized.
Return an Object
Optional<Student> studentById = studentRepository.findById(studentId);//Method from JPA Repo, returns Optional
Student student = studentById.orElseGet(Student::new);//Return empty constructor if no data/Null
The supplier in orElseGet can be written in whichever way feels intuitive.
Student student = studentById.orElseGet(Student::new);//Return empty constructor if no data/Null
//student = studentById.orElseGet(() -> new Student());
//student = studentById.orElseGet(() -> Student.builder().build());
Simple Mapper
If the class structure of DAO Class is different from the DTO Class, then separate mappers or convertors can be written.
StudentDto studentDto = studentMapper.convert(student);//Convertor/Mapper/Transformer
The convert method takes in a DAO Object and returns a DTO object
@Component
public class StudentMapper {
public StudentDto convert(Student studentById) {
return StudentDto.builder()
.fullName(studentById.getFirstName() + " " + studentById.getLastName())
.city(studentById.getCityOfBirth())
.sex(studentById.getGender())
.university(studentById.getUniversity())
.emailId(studentById.getEmail())//TODO: The email validation.
.build();
}
}
Over all the service class with method to return a single student object
@Service
public class StudentServiceWithDb {
@Autowired StudentRepository studentRepository;
@Autowired StudentMapper studentMapper;
public StudentDto getStudentById(int studentId) {
Optional<Student> studentById = studentRepository.findById(studentId);//Method from JPA Repo, returns Optional
Student student = studentById.orElseGet(Student::new);//Return empty constructor if no data/Null
//student = studentById.orElseGet(() -> new Student());
//student = studentById.orElseGet(() -> Student.builder().build());
StudentDto studentDto = studentMapper.convert(student);//Convertor/Mapper/Transformer
return studentDto;
}
}
Return a List of Object
The method that returns a List of objects, based on the multiple student id’s
passed can be written using the
findAllByIds
method of JpaRepository Interface.
List<Student> studentDetailsList = studentRepository.findAllById(studentIdList);
In order to convert the list of DAO objects to a list of DTO objects, the intuition could be of for loop
//Intuitive way
List<StudentDto> studentDtoList = new ArrayList<>();//Initialize the return array
for(Student s:studentDetailsList){
StudentDto singleStudentDto = studentMapper.convert(s);
studentDtoList.add(singleStudentDto);
}
But a better way of achieving this is the use of functional style of programming
//Java 8
List<StudentDto> studentDtoList = studentDetailsList.stream()
//.filter(Objects::nonNull)//Remove any null rows if needed
.map(studentMapper::convert)
.collect(Collectors.toList());
Finally, the overall method would be
public List<StudentDto> getStudentByIds(List<Integer> studentIdList) {
List<Student> studentDetailsList = studentRepository.findAllById(studentIdList);
List<StudentDto> studentDtoList = studentDetailsList.stream()
.map(studentMapper::convert)
.collect(Collectors.toList());
return studentDtoList;
}
Mapping for DTO
Jackson Mapper
Map Struct
The simple one is Jackson mapper, with lot of control
@JsonProperty("studentIds")//If the name in the request body differs from variable name
private List<String> studentIdList;
Control other aspects of the DTO
- if empty values in the array is not needed
@JsonInclude(JsonInclude.Include.NON_NULL)
@Data
public class Filters {
@JsonProperty("treatmentDay")
public String treatmentDay;
}
Repository
Spring Data JPA is an implementation of Java Persistence API
Custom Exceptions
Use @ControllerAdvice
or @RestControllerAdvice
for Global exception
handling.
Difference between @ControllerAdvice
& @RestControllerAdvice
The primary difference is in the type of responses they handle.
- @RestControllerAdvice is geared toward RESTful services, where responses are typically data-centric (e.g., JSON or XML),
- while @ControllerAdvice is used in traditional web applications, where responses often include both views and data.
HikariCP
HikariCP, often referred to simply as Hikari, is a popular and high-performance connection pool library for Java applications.
Connection pooling is a technique used to efficiently manage and reuse database connections in applications that interact with a relational
spring.jpa.properties.hibernate.default_schema="chinookMusic"
spring.datasource.url = jdbc:postgresql://localhost:5432/mydb
# or combined
spring.datasource.url=jdbc:postgresql://postgres:5432/mydb?currentSchema=test
Read Environment properties
Use the Environment dependency
@Autowired
private Environment environment;
int portNumber = Integer.parseInt(environment.getProperty("server.port"));
Another method by @Value Annotation
@Value("${custom.value}")
private String customVal;
Rest Template
Refer the following page for details Rest Template
CommandLineRunner
https://nitinkc.github.io/spring/microservices/CommandLineRunner/
@Component
@Slf4j
@RequiredArgsConstructor
@Order(value = 1)
@ConditionalOnExpression("${dateExpiration:false}")
public class RutWhileBooting implements CommandLineRunner {
private final LicenseService licenseService;
@Override
public void run(String... args) throws Exception {
log.info("Starting Runner : ExpiryDate");
//Do processing
licenseService.runJob();
}
}
Scheduling a Job
https://nitinkc.github.io/spring/microservices/spring-scheduler/
use @EnableScheduling
on the application main class
@Component
@AllArgsConstructor
public class DailyTaskScheduler {
private final MyService myService;
@Scheduled(cron = "0 0 0 * * *") // Executes at midnight every day
//@Scheduled(fixedRate = 5000) // Executes every 5 seconds (5000 milliseconds) for testing
public void runDailyTask() {
myService.runJob();
}
Spring Boot Actuator
Monitoring
- /env, /metrics, /trace, /dump
- /beans, / autoconfig, /configprops, /mappings
Metric logging - Prometheus and micrometer
Design Patterns in Spring
- Front Controller - Dispatcher Servlet
- Prototype - Beans
- Dependency Injection
- Factory Pattern - Bean Factory & Application Context
- Template Method - org.springframework.web.servlet.mvc.AbstractController
Aspect Oriented Programming - AOP
https://nitinkc.github.io/spring/microservices/spring-aop/
Bean Scope
https://nitinkc.github.io/microservices/spring-beans/#bean-scope
Spring Security
Spring Security is a powerful and highly customizable authentication and access-control framework. It is the de-facto standard for securing Spring-based applications.
Key Concepts
- Authentication: The process of verifying the identity of a user, device, or system. It answers the question, “Who are you?”.
- Authorization: The process of determining whether an authenticated user has permission to access a specific resource or perform a particular action. It answers the question, “What are you allowed to do?”.
-
Principal: The currently authenticated user. It can be represented as an object within Spring Security’s
SecurityContext
. -
GrantedAuthority: Represents a permission granted to the principal. It is typically expressed as a role (e.g.,
ROLE_ADMIN
,ROLE_USER
). -
SecurityContextHolder: Provides access to the
SecurityContext
, which holds theAuthentication
object and other security-related information.
Configuration
Spring Security can be configured using a SecurityFilterChain
bean. This is the most common way to configure security in a Spring Boot application.
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/public/**").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.formLogin(withDefaults());
return http.build();
}
@Bean
public UserDetailsService userDetailsService() {
UserDetails user = User.withDefaultPasswordEncoder()
.username("user")
.password("password")
.roles("USER")
.build();
UserDetails admin = User.withDefaultPasswordEncoder()
.username("admin")
.password("password")
.roles("ADMIN")
.build();
return new InMemoryUserDetailsManager(user, admin);
}
}
In this example:
- Requests to
/public/**
are permitted for everyone. - Requests to
/admin/**
are only allowed for users with theADMIN
role. - All other requests require authentication.
- A form-based login is enabled with default settings.
- An in-memory user store is configured with two users:
user
andadmin
.
Annotations
-
@EnableWebSecurity
: Enables Spring Security’s web security support and provides the Spring MVC integration. -
@EnableMethodSecurity
: Enables method-level security. -
@PreAuthorize
: Used to secure methods with a SpEL (Spring Expression Language) expression. The method will only be invoked if the expression evaluates totrue
. -
@PostAuthorize
: Allows for authorization logic to be executed after the method has been invoked. -
@Secured
: A simpler annotation for role-based security. For example,@Secured("ROLE_ADMIN")
.
Reactive Programming (Spring WebFlux)
Reactive Programming with Spring WebFlux
Spring WebFlux is a fully non-blocking, reactive web framework for building modern, scalable applications. It is an alternative to Spring MVC and is built on top of Project Reactor.
Key Concepts
-
Reactive Streams: A standard for asynchronous stream processing with non-blocking backpressure. Key interfaces are
Publisher
,Subscriber
,Subscription
, andProcessor
. -
Mono: A
Publisher
that emits 0 or 1 element. Represents a single, asynchronous value or an empty result.Mono<User> findUserById(String id);
-
Flux: A
Publisher
that emits 0 to N elements. Represents a sequence of asynchronous values.Flux<User> findAllUsers();
-
Backpressure: A mechanism that allows a
Subscriber
to control the rate at which aPublisher
produces data, preventing theSubscriber
from being overwhelmed.
WebFlux vs. Spring MVC
Feature | Spring MVC (Blocking) | Spring WebFlux (Non-Blocking) |
---|---|---|
Thread Model | Thread-per-request | Event-loop model (few threads handle many requests) |
Dependencies | spring-boot-starter-web |
spring-boot-starter-webflux |
API Style | Imperative, synchronous (User , List<User> ) |
Functional, reactive (Mono<User> , Flux<User> ) |
Advanced Testing
Advanced Testing in Spring Boot
Spring Boot provides a rich set of testing utilities to write comprehensive unit, integration, and end-to-end tests.
Test Slices
Test slices allow you to test a specific layer or “slice” of your application in isolation. This is faster than loading the entire application context.
-
@WebMvcTest
: For testing the web layer (controllers) without the full application context. It auto-configuresMockMvc
.@WebMvcTest(UserController.class) public class UserControllerTest { @Autowired private MockMvc mockMvc; @MockBean private UserService userService; @Test void shouldReturnUser() throws Exception { given(userService.getUserById("1")).willReturn(new User("Nitin", "nitin@test.com")); mockMvc.perform(get("/users/1")) .andExpect(status().isOk()) .andExpect(jsonPath("$.name").value("Nitin")); } }
-
@DataJpaTest
: For testing the persistence layer (JPA repositories). It uses an in-memory database by default and rolls back transactions after each test.@DataJpaTest public class UserRepositoryTest { @Autowired private TestEntityManager entityManager; @Autowired private UserRepository userRepository; @Test void shouldFindUserByUsername() { User user = new User("Nitin", "nitin@test.com"); entityManager.persist(user); entityManager.flush(); Optional<User> found = userRepository.findByUsername("Nitin"); assertThat(found).isPresent(); assertThat(found.get().getEmail()).isEqualTo("nitin@test.com"); } }
-
@JsonTest
: For testing JSON serialization and deserialization.
Testcontainers
Testcontainers is a Java library that provides lightweight, throwaway instances of common databases, Selenium web browsers, or anything else that can run in a Docker container. This is ideal for true integration testing.
-
Dependency: Add
org.testcontainers:junit-jupiter
and the specific container module (e.g.,postgresql
). -
Usage:
@SpringBootTest @Testcontainers class UserServiceIntegrationTest { @Container static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:13-alpine"); @DynamicPropertySource static void configureProperties(DynamicPropertyRegistry registry) { registry.add("spring.datasource.url", postgres::getJdbcUrl); registry.add("spring.datasource.username", postgres::getUsername); registry.add("spring.datasource.password", postgres::getPassword); } @Autowired private UserService userService; @Test void testWithRealDatabase() { // Your integration test logic here } }
Resilience and Fault Tolerance
In distributed systems, services can fail. Resilience patterns help your application gracefully handle such failures. Resilience4J is a lightweight, easy-to-use fault tolerance library inspired by Netflix Hystrix.
Key Patterns
-
Circuit Breaker: Prevents repeated calls to a failing service. After a certain number of failures, the circuit “opens,” and all subsequent calls fail immediately (or are redirected to a fallback) for a configured duration. This gives the failing service time to recover.
-
States:
CLOSED
(calls allowed),OPEN
(calls fail-fast),HALF_OPEN
(limited calls to check recovery).
-
States:
- Retry: Automatically re-invokes a failed operation. Useful for transient errors like temporary network glitches.
- Bulkhead: Limits the number of concurrent calls to a specific service, preventing one slow service from exhausting all resources and causing cascading failures.
- Rate Limiter: Controls the rate of requests to a service (e.g., 100 requests per second).
- Time Limiter: Sets a timeout for asynchronous operations.
Example with Circuit Breaker
-
Dependencies: Add
spring-cloud-starter-circuitbreaker-resilience4j
. -
Configuration (
application.yml
):resilience4j.circuitbreaker: instances: myApiService: registerHealthIndicator: true slidingWindowSize: 10 minimumNumberOfCalls: 5 permittedNumberOfCallsInHalfOpenState: 3 automaticTransitionFromOpenToHalfOpenEnabled: true waitDurationInOpenState: 5s failureRateThreshold: 50 eventConsumerBufferSize: 10
-
Usage in Code:
@Service public class MyApiService { @CircuitBreaker(name = "myApiService", fallbackMethod = "fallback") public String fetchData() { // Call to an external, potentially failing service return restTemplate.getForObject("http://external-api/data", String.class); } public String fallback(Throwable t) { // Return a default value or a cached response return "Fallback data"; } }
Database Migration
Database Migration with Flyway
Database migration tools like Flyway and Liquibase help you version-control your database schema, making it easy to evolve your database structure in a consistent and automated way.
Why Use It?
- Version Control for DB: Treat your schema changes like code.
- Automation: Apply schema changes automatically on application startup.
- Consistency: Ensures all environments (dev, test, prod) are using the same schema version.
- Rollbacks: Simplifies the process of reverting to a previous schema state (more supported in Liquibase).
Flyway
Flyway is a popular open-source database migration tool that favors simplicity and convention over configuration.
-
Dependency: Add
org.flywaydb:flyway-core
. -
SQL Scripts: Create SQL migration scripts in
src/main/resources/db/migration
. The naming convention is crucial:V<VERSION>__<DESCRIPTION>.sql
.V1__create_user_table.sql
-
V2__add_email_to_user.sql
-- V1__create_user_table.sql CREATE TABLE users ( id BIGINT PRIMARY KEY, username VARCHAR(255) NOT NULL );
-- V2__add_email_to_user.sql ALTER TABLE users ADD COLUMN email VARCHAR(255);
-
Execution: On startup, Spring Boot will automatically detect Flyway and run any new migration scripts. Flyway uses a
flyway_schema_history
table in your database to track which migrations have already been applied.
Liquibase
Liquibase is another powerful migration tool that uses XML, YAML, or JSON changelogs instead of pure SQL, which can make it more database-agnostic.
- Changelog File: You define changesets in a master changelog file.
-
Changesets: Each changeset is an atomic unit of change, identified by an
id
andauthor
.
Containerization & Cloud-Native
Containerizing Spring Boot with Docker
Containerization, particularly with Docker, is the standard for packaging and deploying modern applications. Cloud-native practices enable applications to be scalable, resilient, and manageable in dynamic environments like Kubernetes.
Docker
Docker allows you to package your application and its dependencies into a standardized unit called a container.
Dockerfile
A Dockerfile
is a script containing instructions to build a Docker image.
# Use an official OpenJDK runtime as a parent image
FROM openjdk:17-jdk-slim
# Set the working directory in the container
WORKDIR /app
# Copy the fat jar into the container at /app
COPY target/my-app-0.0.1-SNAPSHOT.jar app.jar
# Make port 8080 available to the world outside this container
EXPOSE 8080
# Run the jar file
ENTRYPOINT ["java", "-jar", "app.jar"]
Multi-Stage Builds
A multi-stage build is a best practice that helps keep your final image small and secure by separating the build environment from the runtime environment.
# --- Build Stage ---
FROM maven:3.8.5-openjdk-17 AS build
WORKDIR /source
COPY . .
RUN mvn clean package -DskipTests
# --- Runtime Stage ---
FROM openjdk:17-jdk-slim
WORKDIR /app
COPY --from=build /source/target/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
Cloud-Native Best Practices
- Configuration Management: Externalize configuration using ConfigMaps in Kubernetes or a dedicated config server (like Spring Cloud Config) instead of baking it into the image.
-
Health Checks: Implement liveness and readiness probes (
/actuator/health/liveness
,/actuator/health/readiness
). Kubernetes uses these to know if your application is running correctly and ready to receive traffic. -
Graceful Shutdown: Ensure your application handles
SIGTERM
signals to shut down gracefully, finishing in-flight requests and releasing resources. Spring Boot does this by default. - Stateless Services: Design your services to be stateless. State should be stored in an external database or cache (like Redis or a distributed database). This allows you to scale your application horizontally with ease.
- Distributed Tracing: Use tools like Zipkin or Jaeger to trace requests as they travel across multiple microservices, which is essential for debugging in a distributed system.
-
Changesets: Each changeset is an atomic unit of change, identified by an
id
andauthor
. return “Fallback data”; } }// Your integration test logic here } }
| **API Style** | Imperative, synchronous (
User
,List<User>
) | Functional, reactive (Mono<User>
,Flux<User>
) | -
@Secured
: A simpler annotation for role-based security. For example,@Secured("ROLE_ADMIN")
.