This repository documents my structured learning journey in Java Backend Development.
It brings together my hands-on practice, examples, and mini-projects covering core backend concepts and modern Java frameworks.
The goal of this repository is to:
- Track my backend learning progress in a single place
- Practice concepts with real code
- Showcase my growth from fundamentals to advanced topics
java-backend-journey/ # Central repository showcasing my overall backend development journey and learning activities
├── core_java/ # Contains core Java practice code to strengthen fundamental concepts
├── design_patterns/ # Contains implementations of essential design patterns learned in Java
└── task_management_system # Spring Boot–based application where backend concepts are applied to build a task management system
📚 Covered Topics - Core Java
In Java, every application begins with a class, and class name must match the filename.
📄 File: Main.java
public class Main {
public static void main(String[] args) {
System.out.println("Hello World");
}
}⚙️ Compile and Run
# Compile the program
javac Main.java
# Run the program
java Main- Class names should start with an uppercase letter
- Java is case-sensitive
MyClass≠myclass
- Single-line comments:
// - Multi-line comments:
/* ... */
-
Primitive Types
byte,short,int,longfloat,doublebooleanchar
-
Non-Primitive Types
String•Arrays•Classes&Objects
-
Type Casting in Java
-
Widening Casting (automatic) - converting a smaller type to a larger type size
byte->short->char->int->long->float->double -
Narrowing Casting (manual) - converting a larger type to a smaller type size
double->float->long->int->char->short->byte
-
- Declaring Variables
type variableName = value;
- Example
// Student data int studentID = 15; float studentFee = 75.25f; char studentGrade = 'B'; String studentName = "John Doe"; final int BIRTHYEAR = 1980;
- Array
//Method 1: String[] cars = new String[5]; //Method 2: String[] cars = {"Volvo", "BMW", "Ford", "Mazda"}; System.out.println(cars[0]); //access the elements of an array cars[0] = "Opel"; //change an array element System.out.println(cars.length); //To find out how many elements an array has, use the length property.
- The
Scannerclass is used to get user input, and it is found in thejava.utilpackage. - To use the
Scannerclass, create an object of the class and use any of the available methods found in the Scanner class documentation.Scanner myObj = new Scanner(System.in); // Create a Scanner object System.out.println("Enter username"); String userName = myObj.nextLine(); // Read user input System.out.println("Username is: " + userName); // Output user input
- Input Types:
In the example above, we used the nextLine() method, which is used to read Strings. To read other types, look at the table below:nextBoolean() # Reads a boolean value from the user nextByte() # Reads a byte value from the user nextDouble() # Reads a double value from the user nextFloat() # Reads a float value from the user nextInt() # Reads a int value from the user nextLine() # Reads a String value from the user nextLong() # Reads a long value from the user nextShort() # Reads a short value from the user
- You can use the
println(),print()method to output values or print text in Java.- System.out.println("Hello World!");
- System.out.print("Hello World! ");
Conditions and if statements let you control the flow of your program - deciding which code runs, and which code is skipped.
- if-else
int temperature = 30; if (temperature < 0) { System.out.println("It's freezing!"); } else if (temperature < 20) { System.out.println("It's cool."); } else { System.out.println("It's warm."); }
- switch
Instead of writing many if..else statements, you can use the switch statement.
int day = 4; switch (day) { case 6: System.out.println("Today is Saturday"); break; case 7: System.out.println("Today is Sunday"); break; default: System.out.println("Looking forward to the Weekend"); }
- while Loop
while (condition) { // code block to be executed }
- do-while Loop
do { // code block to be executed } while (condition);
- for Loop
for (statement 1; statement 2; statement 3) { // code block to be executed }
- for-each Loop
String[] cars = {"Volvo", "BMW", "Ford", "Mazda"}; for (String car : cars) { System.out.println(car); }
public class Main {
static void myMethod(String fname) {
System.out.println(fname + " Refsnes");
}
public static void main(String[] args) {
myMethod("Liam");
myMethod("Jenny");
myMethod("Anja");
}
}
// Liam Refsnes
// Jenny Refsnes
// Anja Refsnes- Syntax
try { // Block of code to try } catch(Exception e) { // Block of code to handle errors } finally { // The finally statement lets you execute code, after try...catch }
- The
throwkeyword- The
throwstatement allows you to create a custom error. - The
throwstatement is used together with an exception type. There are many exception types available in Java:ArithmeticException,FileNotFoundException,ArrayIndexOutOfBoundsException,SecurityExceptionetc.
public void checkAge(int age) { if (age < 18) { throw new ArithmeticException("Access denied - You must be at least 18 years old."); } else { System.out.println("Access granted - You are old enough!"); } }
- The
- Multi-Catch
try { int result = 10 / 0; int[] numbers = {1, 2, 3}; System.out.println(numbers[10]); } catch (ArithmeticException | ArrayIndexOutOfBoundsException e) { System.out.println("Math error or array error occurred."); }
- try-with-resources
Why use try-with-resources?
public class Main { public static void main(String[] args) { // resource is opened inside try() try (FileOutputStream output = new FileOutputStream("filename.txt")) { output.write("Hello".getBytes()); // no need to call close() here System.out.println("Successfully wrote to the file."); } catch (IOException e) { System.out.println("Error writing file."); } } }
- Safer - resources are always closed, even if an exception occurs.
- Cleaner - no need to write close() calls.
- Shorter code - less boilerplate, easier to read.
The Java Collections Framework provides a set of interfaces (like List, Set, and Map) and a set of classes (ArrayList, HashSet, HashMap, etc.) that implement those interfaces.
All of these are part of the java.util package.
| Class | Interface | Description |
|---|---|---|
ArrayList |
List | Resizable array that maintains order and allows duplicates |
LinkedList |
List | List with fast insert and remove operations |
HashSet |
Set | Unordered collection of unique elements |
TreeSet |
Set | Sorted set of unique elements (natural order) |
LinkedHashSet |
Set | Maintains the order in which elements were inserted |
HashMap |
Map | Stores key/value pairs with no specific order |
TreeMap |
Map | Sorted map based on the natural order of keys |
LinkedHashMap |
Map | Maintains the order in which keys were inserted |
When & What to Use in Java Collections:
- Use
Listclasses when you care about order, you may have duplicates, and want to access elements by index. - Use
Setclasses when you need to store unique values only. - Use
Mapclasses when you need to store pairs of keys and values, like a name and its phone number.
The List interface is part of the Java Collections Framework and represents an ordered collection of elements.
- You can access elements by their index, add duplicates, and maintain the insertion order.
- Since
Listis an interface, you cannot create a List object directly. - Instead, you use a class that implements the List interface, such as:
ArrayList- like a resizable array with fast random accessLinkedList- like a train of cars you can easily attach or remove
List<String> cars = new ArrayList<String>();
List<String> cars = new LinkedList<String>();| Method | Description |
|---|---|
| add() | Adds an element to the end of the list |
| get() | Returns the element at the specified position |
| set() | Replaces the element at the specified position |
| remove() | Removes the element at the specified position |
| size() | Returns the number of elements in the list |
Sort a List:
Collections.sort(cars); // Ascending order
Collections.sort(cars, Collections.reverseOrder()); // Descending orderThe Set interface is part of the Java Collections Framework and is used to store a collection of unique elements.
Unlike a List, a Set does not allow duplicates, and it does not preserve the order of elements (unless you're using TreeSet or LinkedHashSet).
Common classes that implement Set:
HashSet- fast and unorderedTreeSet- sorted setLinkedHashSet- ordered by insertion
Set<String> cars = new HashSet<String>();
TreeSet<String> cars = new TreeSet<>();
Set<String> cars = new LinkedHashSet<>();
// Loop Through a XSet
for (String i : cars) {
System.out.println(i);
}| Method | Description |
|---|---|
| add() | Adds an element if it's not already in the set |
| remove() | Removes the element from the set |
| contains() | Checks if the set contains the element |
| size() | Returns the number of elements |
| clear() | Removes all elements |
The Map interface is a part of the Java Collections Framework and is used to store key-value pairs. Each key must be unique, but values can be duplicated.
Common classes that implement Map:
HashMap- fast and unorderedTreeMap- sorted by keyLinkedHashMap- ordered by insertion
Map<String, String> capitalCities = new HashMap<>();
Map<String, String> capitalCities = new TreeMap<>();
Map<String, String> capitalCities = new LinkedHashMap<>();
// Print keys
for (String i : capitalCities.keySet()) {
System.out.println(i);
}
// Print values
for (String i : capitalCities.values()) {
System.out.println(i);
}| Method | Description |
|---|---|
| put() | Adds or updates a key-value pair |
| get() | Returns the value for a given key |
| remove() | Removes the key and its value |
| containsKey() | Checks if the map contains the key |
| keySet() | Returns a set of all keys |
- An
iteratoris a way to loop through elements in a data structure. - It is called an iterator because iterating is the technical term for looping.
// Create an ArrayList of Strings ArrayList<String> cars = new ArrayList<String>(); cars.add("Volvo"); cars.add("BMW"); // Get an iterator for the ArrayList Iterator<String> it = cars.iterator(); // Iterate through the list using the iterator while(it.hasNext()) { System.out.println(it.next()); }
Algorithms are used to solve problems by sorting, searching, and manipulating data structures.
In Java, many useful algorithms are already built into the Collections class (found in the java.util package), so you don't have to write them from scratch.
// Searching & Sorting
Collections.sort(names); // must be sorted first
int index = Collections.binarySearch(names, "Angie");
System.out.println("Angie is at index: " + index);
// Reverse order sorting
Collections.sort(list, Collections.reverseOrder())The Collections class contains many more algorithms, such as:
Collections.max()- find the largest elementCollections.min()- find the smallest elementCollections.shuffle()- randomly shuffle elementsCollections.frequency()- count how many times an element appearsCollections.swap()- swap two elements in a list
Everything in Java is associated with classes and objects, along with its attributes and methods. For example: in real life, a car is an object. The car has attributes, such as weight and color, and methods, such as drive and brake.
Class Vs Object
- Class -> Blueprint
- Object -> Instance of a class
Accessing Attributes: You can access attributes by creating an object of the class, and by using the dot syntax (.).
// Create a Main class
public class Main {
int x; // Create a class attribute
// Create a class constructor for the Main class
public Main() {
x = 5; // Set the initial value for the class attribute x
}
// Constructor with parameter
public Main(int x) {
this.x = x;
}
public static void main(String[] args) {
Main myObj = new Main(); // Create an object of class Main (This will call the constructor)
System.out.println(myObj.x); // Print the value of x
}
}
// Outputs 5Modifiers define access level and behavior of classes, methods, and variables.
Types of Modifiers:
- Access Modifiers - controls the access level
- Non-Access Modifiers - do not control access level, but provides other functionality
-
Access Modifiers
For classes:public: The class is accessible by any other classdefault: The class is only accessible by classes in the same package.
For attributes, methods and constructors:
public: The code is accessible for all classesprivate: The code is only accessible within the declared classdefault: The code is only accessible in the same package. This is used when you don't specify a modifier.protected: The code is accessible in the same package and subclasses.
-
Non-Access Modifiers
Non-access modifiers do not control visibility (like public or private), but instead add other features to classes, methods, and attributes. The most commonly used non-access modifiers arefinal,static, andabstract.final: If you don't want the ability to override existing attribute values, declare attributes as final.static: Astaticmethod belongs to the class, not to any specific object. This means you can call it without creating an object of the class.abstract: An abstract method belongs to an abstract class, and it does not have a body. The body is provided by the subclass.synchronized: Methods can only be accessed by one thread at a time
The meaning of Encapsulation, is to make sure that "sensitive" data is hidden from users. To achieve this, you must:
- declare class variables/attributes as
private - provide public get and set methods to access and update the value of a
privatevariable
public class Person {
private String name; // private = restricted access
// Getter
public String getName() {
return name;
}
// Setter
public void setName(String newName) {
this.name = newName;
}
}To inherit from a class, use the extends keyword.
class Vehicle {
}
class Car extends Vehicle {
}Java does not support multiple inheritance with classes (to avoid the diamond problem), but it does support multiple inheritance through interfaces.
Data abstraction is the process of hiding certain details and showing only essential information to the user.
Abstraction can be achieved with either abstract classes or interfaces
The abstract keyword is a non-access modifier, used for classes and methods:
- Abstract class: is a restricted class that cannot be used to create objects (to access it, it must be inherited from another class).
- Abstract method: can only be used in an abstract class, and it does not have a body. The body is provided by the subclass (inherited from).
An abstract class can have both abstract and regular methods:
abstract class Animal {
public abstract void animalSound();
public void sleep() {
System.out.println("Zzz");
}
}Another way to achieve abstraction in Java, is with interfaces.
An interface is a completely "abstract class" that is used to group related methods with empty bodies:
// interface
interface Animal {
public void animalSound(); // interface method (does not have a body)
public void run(); // interface method (does not have a body)
}To access the interface methods, the interface must be "implemented" (kinda like inherited) by another class with the implements keyword (instead of extends). The body of the interface method is provided by the "implement" class:
// Interface
interface Animal {
public void animalSound(); // interface method (does not have a body)
public void sleep(); // interface method (does not have a body)
}
// Pig "implements" the Animal interface
class Pig implements Animal {
public void animalSound() {
// The body of animalSound() is provided here
System.out.println("The pig says: wee wee");
}
public void sleep() {
// The body of sleep() is provided here
System.out.println("Zzz");
}
}
class Main {
public static void main(String[] args) {
Pig myPig = new Pig(); // Create a Pig object
myPig.animalSound();
myPig.sleep();
}
}Note on Interfaces:
- Interface methods do not have a body - the body is provided by the "implement" class
- On implementation of an interface, you must override all of its methods.
- Interface methods are by default
abstractandpublic - Interface attributes are by default
public,staticandfinal. - An interface cannot contain a constructor (as it cannot be used to create objects)
An anonymous class is a class without a name. It is created and used at the same time.
You often use anonymous classes to override methods of an existing class or interface, without writing a separate class file.
// Normal class
class Animal {
public void makeSound() {
System.out.println("Animal sound");
}
}
public class Main {
public static void main(String[] args) {
// Anonymous class that overrides makeSound()
Animal myAnimal = new Animal() {
public void makeSound() {
System.out.println("Woof woof");
}
}; // semicolon is required to end the line of code that creates the object
myAnimal.makeSound();
}
}An enum is a special "class" that represents a group of constants (unchangeable variables, like final variables).
// enum class
enum Level {
LOW,
MEDIUM,
HIGH
}Loop Through an Enum
for (Level myVar : Level.values()) {
System.out.println(myVar);
}Enum Constructor
enum Level {
// Enum constants (each has its own description)
LOW("Low level"),
MEDIUM("Medium level"),
HIGH("High level");
// Field (variable) to store the description text
private String description;
// Constructor (runs once for each constant above)
private Level(String description) {
this.description = description;
}
// Getter method to read the description
public String getDescription() {
return description;
}
}
public class Main {
public static void main(String[] args) {
Level myVar = Level.MEDIUM; // Pick one enum constant
System.out.println(myVar.getDescription()); // Prints "Medium level"
}
}Loop Through Enum with Constructor
for (Level myVar : Level.values()) {
System.out.println(myVar + ": " + myVar.getDescription());
}Wrapper classes provide a way to use primitive data types (int, boolean, etc..) as objects.
The table below shows the primitive type and the equivalent wrapper class:
| Primitive Data Type | Wrapper Class |
|---|---|
| byte | Byte |
| short | Short |
| int | Integer |
| long | Long |
| float | Float |
| double | Double |
| boolean | Boolean |
| char | Character |
When working with Collection objects, such as ArrayList, where primitive types cannot be used (the list can only store objects):
ArrayList<int> myNumbers = new ArrayList<int>(); // Invalid
ArrayList<Integer> myNumbers = new ArrayList<Integer>(); // ValidSince you're now working with objects, you can use certain methods to get information about the specific object.
public class Main {
public static void main(String[] args) {
Integer myInt = 5;
Double myDouble = 5.99;
Character myChar = 'A';
System.out.println(myInt.intValue());
System.out.println(myDouble.doubleValue());
System.out.println(myChar.charValue());
}
}Generics allow you to write classes, interfaces, and methods that work with different data types, without having to specify the exact type in advance.
- Generic Class
class Box<T> { T value; // T is a placeholder for any data type void set(T value) { this.value = value; } T get() { return value; } } public class Main { public static void main(String[] args) { // Create a Box to hold a String Box<String> stringBox = new Box<>(); stringBox.set("Hello"); System.out.println("Value: " + stringBox.get()); // Create a Box to hold an Integer Box<Integer> intBox = new Box<>(); intBox.set(50); System.out.println("Value: " + intBox.get()); } }
- Generic Method Example
public class Main { // Generic method: works with any type T public static <T> void printArray(T[] array) { for (T item : array) { System.out.println(item); } } public static void main(String[] args) { // Array of Strings String[] names = {"Jenny", "Liam"}; // Array of Integers Integer[] numbers = {1, 2, 3}; // Call the generic method with both arrays printArray(names); printArray(numbers); } }
Java Collections like ArrayList and HashMap use generics internally:
Annotations are special notes you add to your Java code. They start with the @ symbol.
They don't change how your program runs, but they give extra information to the compiler or tools.
Java includes several built-in annotations. Here are some of the most commonly used:
@Override: Indicates that a method overrides a method in a superclass.@Deprecated: Marks a method or class as outdated or discouraged from use.@SuppressWarnings: Tells the compiler to ignore certain warnings.
class Animal {
void makeSound() {
System.out.println("Animal sound");
}
}
class Dog extends Animal {
@Override // Compiler check that a method really overrides a method from a superclass.
void makesound() { // Typo here!
System.out.println("Woof!");
}
@Deprecated // Warns developers not to use a method because it may be removed or replaced in the future
static void oldMethod() {
System.out.println("This method is outdated.");
}
}
public class Main {
@SuppressWarnings("unchecked") // Tells the compiler to ignore specific warnings, like "unchecked" or "deprecation"
public static void main(String[] args) {
Animal myDog = new Dog();
myDog.makeSound();
}
}Threads allows a program to operate more efficiently by doing multiple things at the same time. Threads can be used to perform complicated tasks in the background without interrupting the main program.
public class Threads extends Thread {
public static void main(String[] args) {
Threads thread = new Threads();
thread.start();
for(int i = 0; i < 50; i++) {
System.out.println("Outside: " + i);
}
}
public void run() {
for(int i = 0; i < 50; i++) {
System.out.println("Inside: " + i);
}
}
}Java does not have a built-in Regular Expression class, but we can import the java.util.regex package to work with regular expressions. The package includes the following classes:
PatternClass - Defines a pattern (to be used in a search)MatcherClass - Used to search for the patternPatternSyntaxExceptionClass - Indicates syntax error in a regular expression pattern
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class Main {
public static void main(String[] args) {
Pattern pattern = Pattern.compile("w3schools", Pattern.CASE_INSENSITIVE);
Matcher matcher = pattern.matcher("Visit W3Schools!");
boolean matchFound = matcher.find();
if(matchFound) {
System.out.println("Match found");
} else {
System.out.println("Match not found");
}
}
}
// Outputs Match found| Method | Cursor Behavior | Notes |
|---|---|---|
matches() |
Moves to end of string | Full match only |
find() |
Moves to end of last match | Can be called repeatedly |
lookingAt() |
Moves to end of match | Starts at current cursor |
reset() |
Cursor → 0 | Useful for repeated searches |
Flags
Flags in the compile() method change how the search is performed. Here are a few of them:
Pattern.CASE_INSENSITIVE- The case of letters will be ignored when performing a search.Pattern.LITERAL- Special characters in the pattern will not have any special meaning and will be treated as ordinary characters when performing a search.Pattern.UNICODE_CASE- Use it together with the CASE_INSENSITIVE flag to also ignore the case of letters outside of the English alphabet
📚 Covered Topics - Spring Boot
Spring boot is an extension of the Spring-Framework that simplifies the creation of stand-alone, production-grade Spring-based applications. It provides a fast and efficient way to develop Spring applications with minimal configuration.
To work with Spring Boot, it is important to understand the following core principles:
1️⃣ Dependency Injection (DI) - DependencyInjection.java
Dependency Injection (DI) is a design pattern used in object-oriented programming where an object receives its required dependencies from an external source rather than creating them itself. It helps manage how objects are constructed and how they obtain the resources they need. By separating dependency creation from usage, DI improves code flexibility. This results in systems that are easier to modify, test, and maintain.
- Reduces tight coupling between classes
- Improves code reusability and flexibility
- Makes unit testing easier by allowing mock dependencies
- Enhances maintainability and scalability of the system
2️⃣ Inversion of Control (IoC) Container - IocContainer.java
Inversion of Control Principle (it’s a principle, not a design pattern) is basically about inverting the control flow of a program.
what does “Inversion of Control” even mean?
Normally your code controls object creation and flow, With IoC the framework controls object creation and flow
👉 The control is inverted
From you → to the container
That’s it. That’s IoC.
The Spring framework provides an Inversion of Control (IoC) container that manages the creation and lifecycle of objects (beans) and their dependencies.
Spring provides two main types of IoC containers:
-
BeanFactory: This is the most basic type of IoC container in Spring. It provides the essential features needed to manage objects (called Beans) in your application. BeanFactory is lightweight and perfect for simple applications where you only need basic dependency injection.
-
Application Context: This is a more advanced type of IoC container that extends the capabilities of BeanFactory. In addition to the basic features, ApplicationContext offers more robust options like event propagation, declarative mechanisms to create a Bean, and a more extensive lifecycle management. It's typically the go-to choice for most Spring applications because of its powerful features.
Example of IOC in Spring
- Engine.java
public class Engine { public Engine() { System.out.println("Engine bean created"); } public void start() { System.out.println("Engine started"); } }
- Car.java
public class Car { private Engine engine; public Car(Engine engine) { System.out.println("Car bean created"); this.engine = engine; } public void drive() { engine.start(); System.out.println("Car is driving"); } }
- AppConfig.java
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class AppConfig { @Bean public Engine engine() { return new Engine(); } @Bean public Car car() { return new Car(engine()); } }
- ApplicationContext Example(Eager Initialization)
import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext; public class ApplicationContextExample { public static void main(String[] args) { System.out.println("Before context initialization"); ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class); System.out.println("After context initialization"); Car car = context.getBean(Car.class); car.drive(); } }
- BeanFactory Example(Lazy Initialization)
import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.context.annotation.AnnotatedBeanDefinitionReader; public class BeanFactoryExample { public static void main(String[] args) { System.out.println("Before BeanFactory setup"); DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); AnnotatedBeanDefinitionReader reader = new AnnotatedBeanDefinitionReader(factory); reader.register(AppConfig.class); System.out.println("After BeanFactory setup"); Car car = factory.getBean(Car.class); car.drive(); } }
3️⃣ Beans in Spring Boot - Spring Beans
Spring Beans are Java objects managed by the Spring Container, allowing for dependency injection and efficient application configuration.
Key Characteristics of Beans:
- Signleton by Default
- Configurable
- Managed by the Container
There are three primary ways to define beans in Spring:
- XML-Based Configuration
- Annotation-Based Configuration
- Java-based Configuration
Spring provides multiple ways to define Beans, each with its own purpose and use case:
-
@Component(Most Common)
This is the most basic way to define a Bean in Spring. It's typically used for generic components.
➡ Where to use?- Utility classes
- General services
- Simple business logic components
@Component public class MyService { public String getServiceName() { return "This is MyService"; } } -
@Service(Specialized Component for Business Logic)
A specialized version of @Component, used specifically for service classes.
➡ Where to use?- Business logic implementation
- Service layer handling complex operations
@Service public class UserService { public String getUser() { return "Fetching user details..."; } } -
@Repository(DAO Layer for Data Access)
This is a specialized@Componentused in the data access layer. It also provides exception translation.
➡ Where to use?- Database interaction
- DAO (Data Access Object) layer
@Repository public class UserRepository { public List<String> findAllUsers() { return List.of("John", "Jane", "Doe"); } } -
@Controller(For Handling HTTP Requests)
Used in Spring MVC to handle incoming web requests.
➡ Where to use?- Exposing APIs
- Handling HTTP requests
@RestController @RequestMapping("/api") public class UserController { @GetMapping("/users") public List<String> getUsers() { return List.of("Alice", "Bob"); } }
-
@Bean(Manual Bean Definition) Used inside a@Configurationclass to define a Bean manually.
➡ Where to use?- When you need more control over Bean creation
- Third-party library Beans
@Configuration public class AppConfig { @Bean public MyService myService() { return new MyService(); } }
The Spring Context is the core component of the Spring Framework, representing the Spring IoC (Inversion of Control) container. It is responsible for managing the lifecycle of beans, including their creation, configuration, and destruction. The Spring Context acts as a container that holds the beans and provides them to the application whenever required.
Types of ApplicationContext
Spring provides several implementations of the ApplicationContext, each suitable for different use cases:
ClassPathXmlApplicationContext:Loads the context definition from an XML file located in the classpath.FileSystemXmlApplicationContext:Loads the context definition from an XML file in the file system.AnnotationConfigApplicationContext:Loads the context definition from Java-based configuration classes using annotations.WebApplicationContext:A specialized version ofApplicationContextused in web applications.
-
Core Spring Boot Annotations
@SpringBootApplication– Combines@Configuration,@EnableAutoConfiguration, and@ComponentScanto bootstrap the application.@Configuration– Marks a class as a source of bean definitions.@Bean– Declares a method that returns a Spring-managed bean.@ComponentScan– Scans the specified package for Spring components.@EnableAutoConfiguration– Automatically configures Spring based on dependencies.
-
Stereotype Annotations
@Component– Marks a class as a Spring-managed component.@Service– Indicates that a class contains business logic.@Repository– Marks a class as a data access component and enables exception translation.@Controller– Marks a class as a web controller.@RestController– Combines@Controllerand@ResponseBodyfor REST APIs.
-
Dependency Injection
@Autowired– Injects a dependency automatically by type.@Qualifier– Specifies which bean to inject when multiple candidates exist.@Primary– Marks a bean as the default choice when multiple beans are available.@Value– Injects values from properties files.
-
Web Layer Annotations
@RequestMapping– Maps HTTP requests to handler methods.@GetMapping– Handles HTTP GET requests.@PostMapping– Handles HTTP POST requests.@PutMapping– Handles HTTP PUT requests.@DeleteMapping– Handles HTTP DELETE requests.@PathVariable– Binds URL path variables to method parameters.@RequestParam– Binds query parameters to method parameters.@RequestBody– Binds HTTP request body to a Java object.@ResponseBody– Returns data directly as HTTP response body.
-
JPA & Database Annotations
@Entity– Marks a class as a JPA entity.@Table– Specifies the database table name.@Id– Marks the primary key of an entity.@GeneratedValue– Specifies how the primary key is generated.@Column– Maps a field to a database column.@OneToOne– Defines a one-to-one relationship.@OneToMany– Defines a one-to-many relationship.@ManyToOne– Defines a many-to-one relationship.@ManyToMany– Defines a many-to-many relationship.@JoinColumn– Specifies the foreign key column.
-
Validation Annotations
@Valid– Triggers validation on request body objects.@NotNull– Ensures a field is not null.@NotBlank– Ensures a string is not empty or whitespace.@Size– Validates the size of a string or collection.@Email– Validates email format.@Min– Sets minimum numeric value.@Max– Sets maximum numeric value.
-
Security Annotations
@EnableWebSecurity– Enables Spring Security configuration.@PreAuthorize– Authorizes method access before execution.@PostAuthorize– Authorizes method access after execution.@Secured– Restricts access based on roles.
-
Transaction & Utility
@Transactional– Manages database transactions automatically.@Slf4j– Creates a logger instance (Lombok).@Data– Generates getters, setters, equals, hashCode, and toString (Lombok).@Builder– Implements the builder pattern (Lombok).@AllArgsConstructor– Generates constructor with all fields.@NoArgsConstructor– Generates no-argument constructor.
The Spring MVC architecture is designed to streamline the development of web applications by providing a clear separation of concerns. It follows the MVC design pattern, where:
Modelrepresents the application data and business logic.Viewis responsible for rendering the user interface.Controllerhandles user requests and coordinates between the Model and the View.
In Spring MVC, the architecture revolves around a central component called the DispatcherServlet, which acts as the front controller. It delegates requests to appropriate controllers, based on the configured HandlerMapping, and returns a ModelAndView object to be rendered by a ViewResolver.
DispatcherServlet performs the following steps:
- Receives the Request: DispatcherServlet intercepts the incoming request.
- Finds the Handler: It uses HandlerMapping to determine the appropriate handler (controller) for the request.
- Executes the Handler: Once the handler is determined, DispatcherServlet calls the corresponding controller method.
- Returns the Model and View: The controller returns a ModelAndView object, which contains the model data and the view name.
- Renders the View: DispatcherServlet uses ViewResolver to render the view based on the ModelAndView object.
@Controller // Marks this class as a Spring MVC controller
public class HomeController { // Defines the controller class
@RequestMapping("/home") // Maps the URL "/home" to this method
public ModelAndView home() { // Controller method that handles the request
ModelAndView mav = new ModelAndView(); // Creates a ModelAndView object
mav.setViewName("home"); // Sets the logical view name to "home"
mav.addObject("message", "Welcome to Spring MVC!"); // Adds model data to send to the view
return mav; // Returns the ModelAndView to DispatcherServlet
}
}REST(Representational State Transfer) is a stateless architecture, where each request from the client to the server must contain all the information needed to understand and process the request.
RESTful APIs follow the following key principles:
- Statelessness: Each request is independent, and the server doesn't retain client state.
- Uniform Interface: APIs use standard HTTP methods like GET, POST, PUT, and DELETE.
- Client-Server Separation: The client and server are independent and can evolve separately.
Data binding is the process of transferring data between a user interface and the server-side application. In the context of web applications, this means mapping form data from an HTML form to a Java object on the server. Spring Boot simplifies this process through the use of @ModelAttribute.
Exception handling in Spring Boot allows you to manage errors that occur during application execution. By default, Spring Boot provides basic error handling, but you can customize it according to your requirements.
The @ExceptionHandler annotation allows you to handle exceptions at the controller level, while @ControllerAdvice enables global exception handling across the entire application.
-
Create a controller
@RestController public class MyController { @GetMapping("/divide/{a}divideBy{b}") public int divide(@PathVariable int a, @PathVariable int b) { return a / b; // May throw ArithmeticException when b = 0 } @GetMapping("/custom") public String custom() { throw new CustomException("This is a custom exception"); } }
-
Create Custom Exception Class
This allow you to define application-specific error scenarios with more meaningful or context-specific messages.class CustomException extends RuntimeException { public CustomException(String message) { super(message); } }
-
Handling Multiple Exceptions Globally Using the
@ExceptionHandlerAnnotation@ControllerAdvice class GlobalExceptionHandler { @ExceptionHandler(ArithmeticException.class) @ResponseStatus(HttpStatus.BAD_REQUEST) public String handleMultipleExceptions(ArithmeticException ex) { return "An error occurred: " + ex.getMessage(); } @ExceptionHandler(CustomException.class) @ResponseStatus(HttpStatus.BAD_REQUEST) public String handleCustomException(CustomException ex) { return "An error occurred: " + ex.getMessage(); } @ExceptionHandler(Exception.class) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public String handleInternalServerError(Exception ex) { return "An internal server error occurred: " + ex.getMessage(); } }
Interceptors are a powerful feature in Spring Boot that allow you to perform operations before and after the execution of a request. They are particularly useful for tasks such as logging, authentication, or modifying the request and response.
Key Uses of Interceptors:
- Logging: Log request details and response statuses.
- Authentication and Authorization: Check user credentials and permissions.
- Performance Monitoring: Measure request processing times.
- Modification of Requests/Responses: Add headers or modify request parameters.
How Interceptors Works:
Interceptors are similar to filters but provide more specific control over request handling. They are part of the Spring MVC framework and are implemented using the HandlerInterceptor interface.
Here's a step-by-step guide to creating a custom interceptor in Spring Boot.
-
Define the Custom Interceptor
Create a class that implements theHandlerInterceptorinterface. This interface has three key methods that you can override:preHandle(HttpServletRequest request, HttpServletResponse response, Object handler): Called before the request is processed by a controller.postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView): Called after the request has been processed but before the view is rendered.afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex): Called after the request has been completed, including view rendering.
Example of a custom interceptor that logs request and response details
@Component public class CustomInterceptor implements HandlerInterceptor { private static final Logger logger = Logger.getLogger(CustomInterceptor.class.getName()); @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { logger.info("Pre Handle method is Calling"); logger.info("Request URL: " + request.getRequestURL()); logger.info("Request Method: " + request.getMethod()); return true; // Continue with the request } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { logger.info("Post Handle method is Calling"); logger.info("Response Status: " + response.getStatus()); } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { logger.info("After Completion method is Calling"); if (ex != null) { logger.severe("Exception: " + ex.getMessage()); } } }
-
Register the interceptor
To make your interceptor functional, you need to register it with Spring Boot. This is done by creating a configuration class that implementsWebMvcConfigurerand overrides theaddInterceptorsmethod.Here's an example of how to register the interceptor
@Configuration public class WebConfig implements WebMvcConfigurer { @Autowired private CustomInterceptor customInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(customInterceptor); } }
-
Test the Interceptor
Start your Spring Boot application and make a few requests to test your interceptor. You should see logs in the console indicating that the interceptor's methods are being called at different stages of request processing.Interceptro logs
: Pre Handle method is Calling : Request URL: http://localhost:8081/hello : Request Method: GET : Post Handle method is Calling : Response Status: 200 : After Completion method is Calling
Spring Data JPA is a powerful abstraction in Spring that simplifies working with databases using the Java Persistence API (JPA). It provides a way to handle database operations efficiently without requiring developers to write boilerplate code.
Spring Data JPA internally uses JPA, the Java standard for Object-Relational Mapping (ORM). With ORM, Java objects are automatically mapped to database tables, making it easier to interact with the database in an object-oriented way.
An entity in JPA represents a table in your database. Each instance of the entity corresponds to a row in the table. Here’s an example of a simple Book entity:
@Entity // Marks the class as a JPA entity.
public class Book {
@Id // Specifies the primary key of the entity.
@GeneratedValue(strategy = GenerationType.IDENTITY) // Auto-generates the ID for each new entity.
private Long id;
private String title;
private String author;
private double price;
}Repositories are interfaces that handle database operations, such as saving, updating, or retrieving data. You can create a repository by extending JpaRepository or CrudRepository. For instance:
@Repository
public interface BookRepository extends JpaRepository<Book, Long> {
}This interface now provides built-in CRUD methods (save, findById, delete, etc.) without needing to write SQL queries.
Spring Data JPA allows you to define query methods directly in your repository by following a specific naming convention. For example:
List<Book> findByTitle(String title);
List<Book> findByAuthor(String author);Spring will automatically generate the required SQL queries based on the method names!
Spring Data JPA automatically translates these method names into SQL queries. You can also use the @Query annotation to write custom JPQL (Java Persistence Query Language) queries:
@Query("SELECT b FROM Book b WHERE b.price > :price")
List<Book> findBooksCostlierThan(@Param("price") double price);Entity mapping is the process of mapping a Java object (an entity) to a corresponding database table using annotations. Each field in the entity class is mapped to a column in the database, and relationships between entities are mapped to reflect foreign keys, join tables, etc.
JPA provides a set of annotations that simplify this mapping process, eliminating the need to write SQL queries manually.
Common JPA Annotation Explained
-
@Entity: Once the class is annotated with@Entity, JPA automatically maps it to a table in the database. If no table name is specified, the class name will be used by default. -
@Id: The @Id annotation marks a field as the primary key for the entity. Every entity must have an @Id field. -
@Column: The @Column annotation is used to specify the mapping between a field and a database column. It allows customization of column properties such as name, length, nullability, etc. -
@Table: The @Table annotation is used to specify the table name for the entity. If the table name differs from the entity class name, you can use this annotation.
Defining Relationships Between Entities
-
@OneToOne: A OneToOne relationship maps one entity to another entity. This is used when an entity has a single related entity. -
@OneToMany: A OneToManyrelationship indicates that one entity can be associated with multiple related entities. For example, one Author can write many Books. -
@ManyToOne: A ManyToOne relationship is the inverse of @OneToMany. In this case, many Books can belong to a single Author. -
@ManyToMany: A ManyToMany relationship represents a relationship where multiple entities can be associated with multiple other entities. For example, multipleBookscan have multipleCategories.
Example Code for Entity Mapping and Relationships
Let’s take a practical example with three entities: Author, Book, and Category.@Entity
public class Author {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
@OneToMany(mappedBy = "author", cascade = CascadeType.ALL)
private List<Book> books;
}
@Entity
public class Book {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String title;
@ManyToOne
@JoinColumn(name = "author_id")
private Author author;
@ManyToMany
@JoinTable(
name = "book_category",
joinColumns = @JoinColumn(name = "book_id"),
inverseJoinColumns = @JoinColumn(name = "category_id"))
private Set<Category> categories;
}
@Entity
public class Category {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
@ManyToMany(mappedBy = "categories")
private Set<Book> books;
}- The
Authorentity is linked to theBookentity with a@OneToManyrelationship. - The
Bookentity has a@ManyToOnerelationship withAuthorand a@ManyToManyrelationship withCategory. - The
Categoryentity has a@ManyToManyrelationship withBook.
This setup allows each author to write multiple books, each book to belong to multiple categories, and each category to have multiple books.
One of the key features of the JPA is Cascading Operations, which allow the propagation of state changes from one entity to the related entities of the JPA application.
Different types of cascade options in JPA:
PERSIST: When the parent is persisted (saved), the related entities are also persisted.MERGE: When the parent is updated, the related entities are updated.REMOVE: When the parent is deleted, the related entities are deleted.REFRESH: If the parent is refreshed from the database, the related entities are refreshed.DETACH: When the parent is detached from the persistence context, the related entities are also detached.ALL: All of the above cascade operations are applied.
Expand to view the example
@Entity
public class Author {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
@OneToMany(mappedBy = "author", cascade = CascadeType.ALL)
private List<Book> books;
}In this example:
- The
Authorentity has a list ofBookentities. - The
cascade = CascadeType.ALLtells JPA to automatically apply certain operations (like saving or deleting) to theBookentities when performed on theAuthor.
Fetching strategies in JPA determine how and when related entities (associations) are loaded from the database when querying an entity. These strategies are essential for managing performance, particularly when dealing with large datasets and complex relationships.
JPA provides two main fetching strategies:
- Eager Fetching: Loads related entities immediately along with the parent entity.
- Lazy Fetching: Loads related entities on-demand, when they are accessed for the first time.
Choosing the Right Fetching Strategy
- Use Eager Fetching when you always need the associated entities along with the parent entity. For example, in a OneToOne relationship where you frequently access both sides together.
- Use Lazy Fetching when associated entities are not always required and should only be fetched when accessed. This helps in optimizing performance, especially in large collections like OneToMany. However, beware of the N+1 Select Problem in lazy fetching. This occurs when lazy-loaded entities trigger multiple queries, significantly increasing the number of database calls.
Examples of Fetching Strategies
Example 1: Lazy Fetching in OneToMany
@Entity
public class Department {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@OneToMany(fetch = FetchType.LAZY)
private List<Employee> employees;
// Getter and Setter methods
}Here, Department has a OneToMany relationship with Employee. Employees will only be fetched from the database when you access the employees field in a department object.
Example 2: Eager Fetching in ManyToOne
@Entity
public class Employee {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@ManyToOne(fetch = FetchType.EAGER)
private Department department;
// Getter and Setter methods
}In this case, when you load an Employee entity, its associated Department is fetched eagerly.
Pagination is the process of breaking down a large dataset into smaller, manageable pages. Instead of loading all the data in one go, we retrieve one page at a time. This is especially useful for handling large datasets in a memory-efficient way.
Sorting allows data to be ordered in ascending or descending order based on one or more fields.
Spring Data JPA provides the following interfaces for pagination and sorting:
- Pageable: This interface is used to create pagination information, such as the page number and the number of records per page.
- Sort: This interface helps to define the sorting logic, such as sorting by a field in ascending or descending order.
Both of these interfaces can be passed as parameters to repository methods.
Example: Paginating Through Books
@Autowired
private BookRepository bookRepository;
public void getPaginatedBooks(int page, int size) {
Pageable pageable = PageRequest.of(page, size);
Page<Book> bookPage = bookRepository.findAll(pageable);
List<Book> books = bookPage.getContent(); // Get the list of books on the current page
int totalPages = bookPage.getTotalPages(); // Get total number of pages
long totalElements = bookPage.getTotalElements(); // Get total number of elements
System.out.println("Total Pages: " + totalPages);
System.out.println("Total Elements: " + totalElements);
books.forEach(System.out::println); // Print the books on the current page
}In this example:
- PageRequest.of(page, size) creates a Pageable object for the specified page number and page size.
- The Page object contains the books for that page, along with additional information like total pages and total elements.
Example: Sorting Books by Title
@Autowired
private BookRepository bookRepository;
public void getSortedBooks() {
List<Book> books = bookRepository.findAll(Sort.by("title").ascending());
books.forEach(System.out::println); // Print the sorted books
}This will sort the books in ascending order by their title field. You can also sort in descending order by using .descending().
Combining Pagination and Sorting
public void getPaginatedAndSortedBooks(int page, int size) {
Pageable pageable = PageRequest.of(page, size, Sort.by("title").ascending());
Page<Book> bookPage = bookRepository.findAll(pageable);
List<Book> books = bookPage.getContent();
books.forEach(System.out::println);
}Locking mechanisms in databases are used to prevent dirty reads, non-repeatable reads, and phantom reads by managing access to data when multiple transactions occur. Locking ensures data consistency and integrity, especially in concurrent environments.
Types of Locking in JPA
- Optimistic Locking:
- Definition: Optimistic locking assumes that multiple transactions can complete without affecting each other, and it only checks for conflicts at the time of committing.
- Usage: Best suited for scenarios where data conflicts are rare.
- How It Works: Entities are checked for modifications using a version field (usually a column like
@Version). If the version doesn't match during a commit, it indicates a conflict, and an exception is thrown. Annotations:@Version
- Pessimistic Locking:
- Definition: Pessimistic locking assumes that conflicts will occur, and it locks the data from the moment a transaction starts until it completes.
Usage: Best for cases where there's a high likelihood of conflicts.
How It Works: A row is locked in the database until the transaction completes, ensuring that no other transactions can modify the data.
Annotations:
LockModeType.PESSIMISTIC_READ,LockModeType.PESSIMISTIC_WRITE
- Definition: Pessimistic locking assumes that conflicts will occur, and it locks the data from the moment a transaction starts until it completes.
Usage: Best for cases where there's a high likelihood of conflicts.
How It Works: A row is locked in the database until the transaction completes, ensuring that no other transactions can modify the data.
Annotations:
When to Use Optimistic vs. Pessimistic Locking
- Optimistic Locking
- Use when the likelihood of concurrent data conflicts is low.
- Best for systems with high read-to-write ratios.
- Example: A ticket booking system where many users are querying availability but only a few are confirming purchases at the same time.
- Pessimistic Locking
- Use when the likelihood of conflicts is high.
- Best for systems where data consistency is critical.
- Example: A financial system where multiple transactions on the same account need to be processed in a strictly serialized manner.
In any real-world application, especially when interacting with databases, managing data integrity is crucial. Transactions allow us to ensure that a group of operations are executed in an "all-or-nothing" fashion. This means that if one operation fails, all changes made by other operations in the transaction should be rolled back, leaving the system in a consistent state.
Key Points
Use @Transactional at the service layer where business logic and database operations are managed.
By default, @Transactional rolls back on RuntimeExceptions or unchecked exceptions.
Transactions ensure data consistency and integrity, making them crucial in systems that handle financial operations, order processing, and more.
Spring Security is a robust framework that enhances Java EE applications by adding essential security features. It acts as a collection of filters that manage authentication, authorization, and protection. This library ensures that applications are secure, user identities are verified, access is properly controlled, and vulnerabilities are mitigated effectively.
Authentications: To verify the identity of users.
Authorizations: To control the access to resources based on permissions.
Protections: To protect applications from frequent and well-known security threats.
To enable Spring Security support, we need to add the spring-boot-starter-security dependency in our Spring MVC application.
In a Spring Boot application, SecurityFilterAutoConfiguration automatically registers the DelegatingFilterProxy filter with the name springSecurityFilterChain. Once the request reaches to DelegatingFilterProxy, Spring delegates the processing to FilterChainProxy bean that utilizes the SecurityFilterChain to execute the list of all filters to be invoked for the current request.
SecurityFilterAutoConfiguration: Automatically registers theDelegatingFilterProxyfilter under the name springSecurityFilterChain.DelegatingFilterProxy: This filter intercepts incoming HTTP requests and delegates their processing to theFilterChainProxy.FilterChainProxy: TheFilterChainProxybean manages a list of security filters defined in theSecurityFilterChain. It determines which filters should be applied to the current request.SecurityFilterChain: This component contains the filters that will be executed in sequence for the request, handling various security aspects like authentication, authorization, etc.
Imagine if This Situation:
Your Spring Boot application is like a mall. The mall has various sections, like stores, a food court, and a VIP lounge. Some areas are open to everyone, while others (like the VIP lounge) require special access.
- In this mall analogy, Spring Security’s
DelegatingFilterProxyis the main security gate. - This gate ensures that every visitor is routed through the appropriate security checkpoints (filters) managed by the
FilterChainProxy. - Each checkpoint in the
SecurityFilterChainensures that only authorized and authenticated visitors access restricted areas. - If you clear all the necessary checkpoints, you can freely explore the mall, including the restricted VIP areas.
Core Spring Security components are used throughout a Spring Boot application to manage authentication, authorization, and overall security. Here’s where and how these components are typically used:
-
UserDetails Interface:
- The UserDetails interface represents a user in the Spring Security framework. It provides methods to get user information such as username, password, and authorities.
- Purpose: To encapsulate user information, including authentication and authorization details.
- Implementation: You can use it to extend your User Entity.
-
UserDetailsService Interface:
- The UserDetailsService interface is a core component in Spring Security that is used to retrieve user-related data. It has a single method: loadUserByUsername().
- Purpose: To fetch user details from a datasource (e.g., database) based on the username.
- Implementation: You typically implement this interface to load user details, such as username, password, and roles, from your own user repository.
-
InMemoryUserDetailsManager Interface:
- The InMemoryUserDetailsManager is a Spring Security provided implementation of UserDetailsService that stores user information in memory.
- **Purpose:**To store user details in memory, typically for testing or small applications. You define users directly in the configuration.
-
PasswordEncoder Interface:
- The PasswordEncoder interface is used for encoding and validating passwords. It has methods for encoding raw passwords and matching encoded passwords.
- Purpose: To securely hash passwords before storing them and to verify hashed passwords during authentication.
- Common Implementations:
- BCryptPasswordEncoder
- Pbkdf2PasswordEncoder
- SCryptPasswordEncoder
JWT stands for JSON Web Token. A JSON Web Token is a digitally signed token used to securely transmit information between parties in a compact format. It’s like a digital passport that allows users to access different parts of a web application without having to repeatedly log in. The token itself contains all the necessary information, and its signature ensures that the data has not been tampered with. This makes JWT a powerful tool for enabling stateless authentication, where the server doesn’t need to remember who you are, but can still trust the information you provide each time you interact with it.
- JWT Creation
Think of JWT as a digitally signed message. It consists of three parts: a header, payload(which contains the data), and a signature (which ensures that the message hasn’t been tampered with)-
Sample Token:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c -
JWT Header: Contains metadata about the token, such as the signing algorithm (
HS256) and token type (JWT). -
JWT Payload: Contains the claims (e.g., user information, expiration time).
-
Secret Key: Used to sign the token, ensuring its integrity.
-
Signature Generation:
- The encoded header and payload are concatenated with a period (
.) in between. - This concatenated string is hashed using the HMAC-SHA256 algorithm, along with the secret key.
- The result is base64url encoded to produce the encoded signature.
- The encoded header and payload are concatenated with a period (
-
Final Token: The token is the concatenation of the encoded header, payload, and signature, separated by periods (.).
-
Spring Boot DevTools is a development toolset designed to enhance the productivity of developers by providing features like automatic restart, live reload, and property overrides. It simplifies the process of testing and tweaking applications during development by automatically applying changes without requiring a manual restart.
// Installing DevTools
// Add DevTools Dependency:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>Logging is the process of recording events, errors, and informational messages from an application. It helps developers and system administrators track the application's behavior and troubleshoot issues.
Many programming languages have libraries and frameworks (like Log4j, SLF4J, or Python’s logging module) that simplify the logging process and offer advanced features like formatting and log rotation.
Elements of Logging Framework:
Every logging framework comes with three elements.
- Logger — capture the messages
- Formatter — formats the messages captured by the logger
- Handler — Dispatches the messages by printing them on the console , or storing them in a file , sending an email, etc.
Log Levels
The messages logged can be of various security levels . Spring Boot supports five log levels which are
- FATAL – fatal error crashing the system
- ERROR — runtime errors
- WARN — warning
- INFO — events occurring at the run time
- DEBUG — Information about the flow of the system
- TRACE — more detailed information about the flow of the system
Each level serves a specific purpose, helping developers prioritize and filter log messages based on their severity or importance.
Example: Logging using SLF4J
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
@Service
public class EmployeeClient {
private static final Logger logger =
LoggerFactory.getLogger(EmployeeClient.class);
public EmployeeDto createNewEmployee(EmployeeDto input) {
logger.trace("TRACE log");
logger.debug("DEBUG log");
logger.info("Creating new employee");
logger.warn("Sample warning log");
logger.error("Sample error log");
try {
// business logic
} catch (Exception e) {
logger.error("Error while creating employee", e);
}
return null;
}
}Log Formatters
Customize console log output in application.properties.
#properties:
logging.pattern.console=%d [%level] %c{2.} [%t] %m%n| Pattern | Meaning |
|---|---|
%d |
Date & time |
%level |
Log level |
%c |
Class name |
%t |
Thread name |
%m |
Log message |
%n |
New line |
Log File Configuration
# Save logs into a file.
logging.file.name=error.log # Spring Boot will automatically create the log file.
# File Log Pattern
logging.pattern.file=%d{yyyy-MM-dd HH:mm:ss.SSS} [%level] %c{2.} [%t] %m%n
# Enable Color in the Log level
spring.output.ansi.enabled=ALWAYS
logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss} %clr(%-5level) %clr(%c{2.}){cyan} - %msg%n-
Generic HTTP client
RestTemplate,WebClient, andRestClientare powerful HTTP clients in Java used for more than just third-party API calls.RestTemplate: Still in use for legacy systems, but it’s deprecated and should be avoided for new development.WebClient: The preferred choice for modern Spring applications, especially those utilizing reactive programming and requiring asynchronous processing.RestClient: A newer, synchronous client that offers a modern API, expected to replace RestTemplate for developers who need blocking calls but want a more updated and flexible tool.
Rest Template Library
RestTemplateis a powerful synchronous client for handling HTTP communication in Spring Boot applications. It internally uses an HTTP client library i.e. java.net.HttpURLConnection, simplifying the process of making RESTful requests to external services and APIs, including convenience, along with integration, and flexibility for various HTTP communication.
All the packages/libraries for RestTemplate are present in spring-boot-starter-web.// Bean Configuration for RestTemplate @Bean public RestTemplate restTemplate(){ return new RestTemplate(); } // Example: Weather API Client @Service @RequiredArgsConstructor public class WeatherClient { private final RestTemplate restTemplate; private final String apiKey = System.getenv("OPEN_WEATHER_API_KEY"); public WeatherResponse getWeather(String city) { String url = "https://api.openweathermap.org/data/2.5/weather?q=" + city + "&appid=" + apiKey + "&units=metric"; return restTemplate.getForObject(url, WeatherResponse.class); } }
Operation RestTemplate Method Description Create postForObjectSend POST request, receive response body Read getForObject/getForEntityFetch resource by URL Update put/exchangeSend PUT request to update resource Delete delete/exchangeSend DELETE request -
SendGrid Integration
SendGrid provides an official Java SDK. Instead of manually building HTTP requests, you use their prebuilt library.
See the the details Documentation: https://www.twilio.com/en-us/products/email-api
All applications that get deployed to production should use an automated approach to migrate their database. It also ensures you don’t miss any changes during the update process. That’s, of course, also true if you develop your application based on Spring Boot.
Flyway and Liquibase are two of the most popular Java libraries for version-based database migration. The general idea of this approach is to use a similar versioning and migration tactic as you did for your application code. For each version, you provide the required steps to migrate your database from the previous to the new version. Using Flyway or Liquibase, you can then automatically apply the required migration steps to update your database from any version to the current version.
Using Liquibase and Spring Boot
To integrate Liquibase with Spring Boot, you only need to add the required dependency. Spring Boot will automatically execute the migration scripts during application startup.
// Add Dependency
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-liquibase</artifactId>
</dependency>
// Project Folder Structure
src/main/resources
└───db
└───changelog
│ db.changelog-master.xml
└───changes
V1__file_name.sql
V2__file_name.sql
// Configure db.changelog-master.xml
// Each changeSet represents a single database migration step.
<changeSet id="1" author="yeasin">
<sqlFile path="db/changelog/changes/V1__file_name.sql"/>
</changeSet>
<changeSet id="2" author="yeasin">
<sqlFile path="db/changelog/changes/V2__file_name.sql"/>
</changeSet>
// Configure application.properties
spring.liquibase.enabled: true
spring.liquibase.change-log: classpath:db/changelog/db.changelog-master.xml
spring.liquibase.contexts=dev
spring.liquibase.default-schema=publicNote: Liquibase supports multiple file formats for defining migrations:
- XML
- YAML
- JSON
- SQL
This flexibility allows developers to choose the format that best fits their project.
Stateful architecture maintains client session data on servers, while stateless architecture treats each request independently.
1. Task Management System - Console Application
- A console-based Java application for Simple Task Management System
- Focuses on OOP concepts, core Java APIs, and real-world problem-solving
2. Task Management System - Spring Boot Application
- Spring Boot application with CRUD operations, authentication, authorization, and JWT-based security for multi-user access.
- It supports multi-user access using JWT-based security and follows clean, scalable backend design practices.