Complete Learning Guide

Java Mastery —
Zero to Spring Boot

A structured, production-minded course covering Java fundamentals, OOP, Collections, Spring Boot REST APIs, JDBC, Hibernate, and a full capstone project.

6Chapters
45+Code Examples
1Full Project
JDK 17+Version
01
Java Basics
JVM internals, syntax, data types, control flow, methods, and arrays
Introduction to Java & the JVM

Java is a statically typed, object-oriented, platform-independent programming language released by Sun Microsystems in 1995. Its "Write Once, Run Anywhere" promise comes from the Java Virtual Machine (JVM).

JDK — Java Development Kit

The full toolkit for developers: compiler (javac), JRE, debugger, and standard libraries. Install this to write Java.

JVM — Java Virtual Machine

Executes Java bytecode. Platform-specific but Java code is not — the JVM handles the translation automatically.

JRE — Java Runtime Environment

JVM + standard libraries. Only needed to run Java programs, not develop them.

Industry Use Cases

Enterprise backends · Android · Big data (Hadoop, Spark) · Financial systems · Cloud microservices

📘 How Java Runs
You write .java → compiler produces .class (bytecode) → JVM interprets bytecode on any OS. This is why Java is platform-independent at the source level.
Installation & First Program

Install JDK 17 (LTS) from adoptium.net or Oracle, then use VS Code or IntelliJ IDEA.

  • 1Install JDK 17: Download from adoptium.net, run the installer, and verify with java --version in your terminal.
  • 2Install IntelliJ IDEA Community: Free IDE with first-class Java support, auto-import, refactoring, and debugger built in.
  • 3Install Maven or Gradle: Build tools that manage dependencies. Spring Boot projects use Maven by default.
  • 4Write and compile: Create Main.java, write your code, run with javac Main.java then java Main.
Main.java
JAVA
// Every Java program starts with a class matching the filename
public class Main {

    // The entry point — JVM calls this first
    public static void main(String[] args) {

        // Print a message to the console
        System.out.println("Hello, Java World!");

        // Formatted output (like printf)
        String name = "Alice";
        System.out.printf("Welcome, %s!%n", name);
    }
}
💡 Naming Convention
Java files must be named exactly as the public class inside them: Main.java contains public class Main. This is a hard requirement of the compiler.
Variables & Data Types

Java is statically typed — you must declare the type of every variable. Types fall into two categories: primitives (stored by value) and reference types (stored as memory addresses).

DataTypes.java
JAVA
public class DataTypes {
    public static void main(String[] args) {

        // ── PRIMITIVE TYPES ──────────────────────────────
        int     age      = 25;           // 32-bit integer
        long    bigNum   = 9_876_543_210L; // 64-bit (note L suffix)
        double  price    = 19.99;        // 64-bit decimal
        float   rate     = 0.18f;        // 32-bit decimal (note f)
        char    grade    = 'A';           // single character
        boolean active   = true;         // true / false only
        byte    small    = 127;          // -128 to 127

        // ── REFERENCE TYPES ──────────────────────────────
        String  name     = "Alice Mugisha"; // immutable text
        int[]   scores   = {90, 85, 92};   // array

        // var keyword (Java 10+) — type inferred by compiler
        var city = "Kigali";  // compiler infers String

        // ── TYPE CONVERSION ──────────────────────────────
        int x = 42;
        double d = x;          // implicit widening (safe)
        int y = (int) 9.99;   // explicit casting (truncates)

        // String ↔ number conversion
        String numStr = String.valueOf(42);
        int    parsed = Integer.parseInt("100");

        System.out.printf("Name: %s, Age: %d, Price: %.2f%n",
                           name, age, price);
    }
}
TypeSizeRange / NotesDefault
byte8 bit-128 to 1270
int32 bit±2.1 billion0
long64 bit±9.2 quintillion — use L suffix0L
double64 bitDecimal — preferred over float0.0
char16 bitUnicode — single quotes only'\u0000'
boolean1 bittrue / falsefalse
StringrefImmutable sequence of charsnull
Operators
Operators.java
JAVA
public class Operators {
    public static void main(String[] args) {

        // ── ARITHMETIC ────────────────────────────────────
        int a = 10, b = 3;
        System.out.println(a + b);    // 13 — addition
        System.out.println(a - b);    // 7  — subtraction
        System.out.println(a * b);    // 30 — multiplication
        System.out.println(a / b);    // 3  — integer division
        System.out.println(a % b);    // 1  — modulus
        System.out.println((double) a / b); // 3.33 — cast first

        // Compound assignment
        a += 5;  // a = a + 5
        a++;     // a = a + 1 (post-increment)

        // ── RELATIONAL ────────────────────────────────────
        System.out.println(a == b);   // false — equal
        System.out.println(a != b);   // true  — not equal
        System.out.println(a > b);    // true
        System.out.println(a <= b);   // false

        // ── LOGICAL ──────────────────────────────────────
        boolean hasId     = true;
        boolean isOver18  = true;
        System.out.println(hasId && isOver18);  // true — AND
        System.out.println(hasId || isOver18);  // true — OR
        System.out.println(!hasId);              // false — NOT

        // ── TERNARY ──────────────────────────────────────
        int score = 75;
        String result = (score >= 50) ? "Pass" : "Fail";
        System.out.println(result);  // Pass
    }
}
Control Flow — if, switch & Loops
ControlFlow.java
JAVA
public class ControlFlow {
    public static void main(String[] args) {

        // ── IF / ELSE IF / ELSE ───────────────────────────
        int score = 78;
        if (score >= 90)       System.out.println("A");
        else if (score >= 80) System.out.println("B");
        else if (score >= 70) System.out.println("C");
        else                   System.out.println("F");

        // ── SWITCH EXPRESSION (Java 14+) ──────────────────
        String day = "MONDAY";
        String type = switch (day) {
            case "SATURDAY", "SUNDAY" -> "Weekend";
            default                       -> "Weekday";
        };
        System.out.println(day + ": " + type);

        // ── FOR LOOP ──────────────────────────────────────
        for (int i = 1; i <= 5; i++) {
            System.out.print(i + " ");
        }
        System.out.println();  // newline

        // ── ENHANCED FOR (for-each) ───────────────────────
        String[] students = {"Alice", "Bob", "Claire"};
        for (String student : students) {
            System.out.println("Hello, " + student);
        }

        // ── WHILE LOOP ────────────────────────────────────
        int count = 0;
        while (count < 3) {
            System.out.println("Count: " + count);
            count++;
        }

        // ── DO-WHILE: runs at least once ─────────────────
        int n = 10;
        do {
            System.out.println("n = " + n);
            n++;
        } while (n < 10);  // runs once even though false
    }
}
Methods

Methods are named blocks of reusable code. They can accept parameters, return values, and be overloaded with different parameter signatures.

Methods.java
JAVA
public class Methods {

    // ── BASIC METHOD ─────────────────────────────────────
    public static double calculateTax(double amount, double rate) {
        return amount * rate;
    }

    // ── VOID METHOD (no return value) ────────────────────
    public static void printLine(String label, int value) {
        System.out.printf("%-15s: %d%n", label, value);
    }

    // ── METHOD OVERLOADING: same name, different params ───
    public static int    add(int a, int b)          { return a + b; }
    public static double add(double a, double b)    { return a + b; }
    public static int    add(int a, int b, int c)  { return a + b + c; }

    // ── VARARGS: variable number of arguments ─────────────
    public static int sum(int... numbers) {
        int total = 0;
        for (int n : numbers) total += n;
        return total;
    }

    public static void main(String[] args) {
        System.out.println(calculateTax(10_000, 0.18)); // 1800.0
        System.out.println(add(3, 4));           // 7   (int)
        System.out.println(add(1.5, 2.5));      // 4.0 (double)
        System.out.println(sum(1, 2, 3, 4, 5));  // 15
    }
}
Arrays & Strings
ArraysAndStrings.java
JAVA
import java.util.Arrays;

public class ArraysAndStrings {
    public static void main(String[] args) {

        // ── ARRAYS ───────────────────────────────────────────
        int[] scores = new int[5];       // declare, size 5
        int[] grades = {92, 85, 78, 96}; // declare + initialise

        grades[0] = 95;                   // update index 0
        System.out.println(grades.length); // 4

        Arrays.sort(grades);               // sort in place
        System.out.println(Arrays.toString(grades)); // [78, 85, 95, 96]

        // 2D array (matrix)
        int[][] matrix = {{1,2},{3,4},{5,6}};
        System.out.println(matrix[1][0]);  // 3

        // ── STRINGS ──────────────────────────────────────────
        String name = "Alice Mugisha";

        System.out.println(name.length());          // 13
        System.out.println(name.toUpperCase());     // ALICE MUGISHA
        System.out.println(name.substring(6));      // Mugisha
        System.out.println(name.contains("Alice")); // true
        System.out.println(name.replace("Alice", "Bob")); // Bob Mugisha
        System.out.println(name.trim());             // removes whitespace

        String[] parts = name.split(" ");
        System.out.println(parts[0]);  // Alice

        // String comparison — always use .equals(), not ==
        String a = "hello", b = "hello";
        System.out.println(a.equals(b));             // true ✅
        System.out.println(a.equalsIgnoreCase("HELLO")); // true

        // StringBuilder — mutable, efficient for concatenation
        StringBuilder sb = new StringBuilder();
        sb.append("Java ").append("is ").append("great!");
        System.out.println(sb.toString());  // Java is great!
    }
}
02
Object-Oriented Programming
Classes, encapsulation, inheritance, polymorphism, and abstraction
Classes & Objects

A class is a blueprint. An object is an instance of a class — a specific entity created from that blueprint with its own data.

Student.java
JAVA
/**
 * Represents a student in the management system.
 * Demonstrates class structure, fields, methods, and toString.
 */
public class Student {

    // ── FIELDS (instance variables) ─────────────────────
    private int    id;
    private String name;
    private int    age;
    private String grade;

    // Static field — shared by ALL instances
    private static String schoolName = "Tech Academy";

    // ── CONSTRUCTOR ─────────────────────────────────────
    public Student(int id, String name, int age, String grade) {
        this.id    = id;
        this.name  = name;
        this.age   = age;
        this.grade = grade;
    }

    // ── INSTANCE METHOD ──────────────────────────────────
    public boolean isPassing() {
        return !grade.equals("F") && !grade.equals("D");
    }

    // ── toString — called by println() ───────────────────
    @Override
    public String toString() {
        return String.format("Student{id=%d, name='%s', grade='%s'}",
                              id, name, grade);
    }

    // Getters and setters (see Encapsulation lesson)
    public String getName()  { return name; }
    public int    getAge()   { return age; }
    public String getGrade() { return grade; }
    public void   setGrade(String grade) { this.grade = grade; }
}
Main.java — using the Student class
JAVA
public class Main {
    public static void main(String[] args) {
        // Create objects using 'new'
        Student alice = new Student(1, "Alice", 20, "A");
        Student bob   = new Student(2, "Bob",   22, "C");

        System.out.println(alice);             // calls toString()
        System.out.println(alice.isPassing()); // true
        alice.setGrade("A+");
        System.out.println(alice.getGrade());  // A+
    }
}
Constructors

Constructors initialise a new object. You can have multiple constructors (constructor overloading) for flexible object creation.

Product.java
JAVA
public class Product {
    private String name;
    private double price;
    private int    stock;

    // Constructor 1: all fields
    public Product(String name, double price, int stock) {
        this.name  = name;
        this.price = price;
        this.stock = stock;
    }

    // Constructor 2: default stock = 0
    public Product(String name, double price) {
        this(name, price, 0);  // delegate to Constructor 1
    }

    // Copy constructor
    public Product(Product other) {
        this(other.name, other.price, other.stock);
    }
}
Encapsulation & Access Modifiers

Encapsulation means hiding internal state and providing controlled access through methods. The four access modifiers control visibility.

ModifierSame ClassSame PackageSubclassAnywhere
private
(default)
protected
public
BankAccount.java
JAVA
public class BankAccount {

    private String owner;
    private double balance;  // hidden — access only via methods

    public BankAccount(String owner, double initialBalance) {
        this.owner   = owner;
        this.balance = Math.max(0, initialBalance); // guard
    }

    public void deposit(double amount) {
        if (amount <= 0) throw new IllegalArgumentException("Amount must be positive");
        balance += amount;
    }

    public void withdraw(double amount) {
        if (amount > balance) throw new IllegalStateException("Insufficient funds");
        balance -= amount;
    }

    // Read-only access — no setter for balance
    public double getBalance() { return balance; }
    public String getOwner()   { return owner; }
}
Inheritance

Inheritance lets a child class reuse and extend a parent class. Java supports single inheritance for classes but multiple inheritance through interfaces.

Inheritance.java
JAVA
// ── PARENT CLASS ─────────────────────────────────────
public class Person {
    protected String name;
    protected int    age;

    public Person(String name, int age) {
        this.name = name;
        this.age  = age;
    }

    public String introduce() {
        return String.format("Hi, I'm %s, age %d.", name, age);
    }
}

// ── CHILD CLASS — extends inherits everything ─────────
public class Student extends Person {
    private String major;

    public Student(String name, int age, String major) {
        super(name, age);  // must call parent constructor first
        this.major = major;
    }

    @Override  // override parent's introduce()
    public String introduce() {
        return super.introduce() + " I study " + major + ".";
    }
}

// ── GRANDCHILD CLASS ──────────────────────────────────
public class GraduateStudent extends Student {
    private String thesisTopic;

    public GraduateStudent(String name, int age,
                           String major, String thesis) {
        super(name, age, major);
        this.thesisTopic = thesis;
    }

    @Override
    public String introduce() {
        return super.introduce() + " Thesis: " + thesisTopic;
    }
}
Polymorphism

Polymorphism means "many forms". The same method call on a parent type dispatches to the correct child implementation at runtime.

Polymorphism.java
JAVA
import java.util.List;

public class PolymorphismDemo {
    public static void main(String[] args) {

        // Parent type references can hold child objects
        Person alice = new Student("Alice", 20, "CS");
        Person bob   = new GraduateStudent("Bob", 25, "Math", "AI Ethics");
        Person claire = new Person("Claire", 30);

        // Polymorphic call — correct introduce() is called at runtime
        List<Person> people = List.of(alice, bob, claire);
        for (Person p : people) {
            System.out.println(p.introduce()); // dynamic dispatch
        }

        // instanceof check before downcasting
        if (alice instanceof Student s) {  // Java 16+ pattern
            System.out.println("Major: " + s.getMajor());
        }
    }
}
Abstraction — Interfaces & Abstract Classes
Abstraction.java
JAVA
// ── INTERFACE: contract with no state ────────────────
public interface Payable {
    double calculatePay();          // abstract by default
    default String paymentSummary() { // default implementation
        return String.format("Pay: RWF %.2f", calculatePay());
    }
}

public interface Describable {
    String describe();
}

// ── ABSTRACT CLASS: partial implementation ────────────
public abstract class Employee implements Payable, Describable {
    protected String name;
    protected double baseSalary;

    public Employee(String name, double baseSalary) {
        this.name       = name;
        this.baseSalary = baseSalary;
    }

    // Subclasses MUST implement this
    public abstract double calculateBonus();

    @Override
    public String describe() {
        return name + " earns " + paymentSummary();
    }
}

// ── CONCRETE IMPLEMENTATION ───────────────────────────
public class FullTimeEmployee extends Employee {
    public FullTimeEmployee(String name, double salary) {
        super(name, salary);
    }

    @Override
    public double calculatePay()   { return baseSalary; }

    @Override
    public double calculateBonus() { return baseSalary * 0.10; }
}
03
Intermediate Java
Exceptions, Collections Framework, File I/O, and Streams API
Exception Handling

Java uses a structured exception model. Checked exceptions must be handled at compile time; unchecked (RuntimeExceptions) are caught at runtime.

ExceptionHandling.java
JAVA
import java.io.IOException;

// ── CUSTOM EXCEPTION ─────────────────────────────────
public class InsufficientFundsException extends RuntimeException {
    private final double shortfall;

    public InsufficientFundsException(double shortfall) {
        super(String.format("Short by RWF %.2f", shortfall));
        this.shortfall = shortfall;
    }

    public double getShortfall() { return shortfall; }
}

// ── EXCEPTION HANDLING PATTERNS ──────────────────────
public class ExceptionDemo {

    public static int parseAge(String input) {
        try {
            int age = Integer.parseInt(input);
            if (age < 0) throw new IllegalArgumentException("Age cannot be negative");
            return age;
        } catch (NumberFormatException e) {
            System.err.println("Not a number: " + input);
            return -1;
        } finally {
            System.out.println("parseAge() completed");  // always runs
        }
    }

    // Multi-catch (Java 7+)
    public static void processFile(String path) {
        try {
            // read file operations...
        } catch (IOException | SecurityException e) {
            System.err.println("File error: " + e.getMessage());
        }
    }

    // try-with-resources — auto closes Closeable resources
    public static void readFile(String path) {
        try (var reader = new java.io.BufferedReader(
                         new java.io.FileReader(path))) {
            String line;
            while ((line = reader.readLine()) != null) {
                System.out.println(line);
            }
        } catch (IOException e) {
            System.err.println("Cannot read file: " + e.getMessage());
        }
    }
}
Collections Framework

The Java Collections Framework provides dynamic data structures far more powerful than arrays. Key interfaces: List, Set, Map.

Collections.java
JAVA
import java.util.*;

public class CollectionsDemo {
    public static void main(String[] args) {

        // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
        // ARRAYLIST — ordered, resizable, allows duplicates
        // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
        List<String> courses = new ArrayList<>();
        courses.add("Java");
        courses.add("Spring");
        courses.add("SQL");
        courses.add(1, "OOP");          // insert at index
        courses.remove("SQL");           // remove by value
        System.out.println(courses.get(0));  // Java
        System.out.println(courses.size());  // 3

        Collections.sort(courses);
        courses.forEach(System.out::println);  // method reference

        // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
        // HASHMAP — key→value pairs, O(1) lookup
        // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
        Map<String, Integer> scores = new HashMap<>();
        scores.put("Alice", 95);
        scores.put("Bob",   82);
        scores.put("Claire", 88);

        System.out.println(scores.get("Alice"));        // 95
        System.out.println(scores.getOrDefault("Dave", 0)); // 0
        System.out.println(scores.containsKey("Bob"));   // true

        for (Map.Entry<String, Integer> entry : scores.entrySet()) {
            System.out.printf("%s → %d%n", entry.getKey(), entry.getValue());
        }

        // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
        // HASHSET — unique elements, no guaranteed order
        // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
        Set<String> tags = new HashSet<>();
        tags.add("java"); tags.add("spring"); tags.add("java");
        System.out.println(tags.size());  // 2 — no duplicates
        System.out.println(tags.contains("spring"));  // true
    }
}
File Handling
FileIO.java
JAVA
import java.io.*;
import java.nio.file.*;
import java.util.List;

public class FileIO {
    public static void main(String[] args) throws IOException {

        Path file = Path.of("students.txt");

        // ── WRITE ────────────────────────────────────────────
        List<String> lines = List.of("Alice,A", "Bob,C", "Claire,A");
        Files.write(file, lines);

        // ── READ ALL LINES ────────────────────────────────────
        List<String> read = Files.readAllLines(file);
        read.forEach(System.out::println);

        // ── APPEND to existing file ───────────────────────────
        Files.writeString(file, "\nDavid,B", StandardOpenOption.APPEND);

        // ── BufferedWriter — efficient for many writes ────────
        try (BufferedWriter bw = Files.newBufferedWriter(
                Path.of("report.txt"))) {
            bw.write("=== Student Report ===");
            bw.newLine();
            bw.write("Total: 4 students");
        }

        // ── Check if file exists ──────────────────────────────
        if (Files.exists(file)) {
            System.out.println("Size: " + Files.size(file) + " bytes");
        }
    }
}
Java Streams API

Streams (Java 8+) enable functional-style data processing on collections — filter, transform, and aggregate data in a declarative, readable way.

StreamsAPI.java
JAVA
import java.util.*;
import java.util.stream.*;

public class StreamsDemo {
    public static void main(String[] args) {

        List<String> names = List.of("Alice", "Bob", "Claire", "Anna", "Brian");

        // Filter + map + collect
        List<String> aNames = names.stream()
            .filter(n -> n.startsWith("A"))    // keep A-names
            .map(String::toUpperCase)             // transform
            .sorted()                             // alphabetical
            .collect(Collectors.toList());
        System.out.println(aNames);  // [ALICE, ANNA]

        // Statistics on a list of scores
        List<Integer> scores = List.of(85, 92, 78, 96, 71);

        OptionalDouble avg = scores.stream()
            .mapToInt(Integer::intValue)
            .average();
        avg.ifPresent(a -> System.out.printf("Avg: %.1f%n", a));

        int topScore = scores.stream()
            .mapToInt(Integer::intValue)
            .max().orElse(0);
        System.out.println("Top: " + topScore);  // 96

        long passing = scores.stream()
            .filter(s -> s >= 80)
            .count();
        System.out.println("Passing: " + passing);  // 3

        // Grouping with Collectors.groupingBy
        Map<Boolean, List<Integer>> grouped = scores.stream()
            .collect(Collectors.partitioningBy(s -> s >= 80));
        System.out.println("Pass: "  + grouped.get(true));
        System.out.println("Fail: "  + grouped.get(false));
    }
}
04
Java for Web Development
Spring Boot, REST APIs, controllers, routing, and JSON handling
Web Concepts & Spring Boot Overview
HTTP Methods

GET (read) · POST (create) · PUT (update) · DELETE (remove) — the four verbs of REST

Spring Boot

Opinionated framework that auto-configures Spring. Start in minutes with embedded Tomcat — no XML config.

MVC Pattern

Model (data) · View (response) · Controller (routing). Spring Boot REST uses Model + Controller only.

Dependency Injection

Spring manages object creation and wiring. You declare what you need; Spring provides it via @Autowired or constructors.

Spring Boot Project Setup
  • 1Generate project: Go to start.spring.io → Select Maven, Java 17, add dependencies: Spring Web, Spring Data JPA, MySQL Driver.
  • 2Download & open: Extract the zip, open in IntelliJ IDEA — it auto-detects Maven and downloads dependencies.
  • 3Configure application.properties: Set DB URL, username, and password for your environment.
  • 4Run: Click the Run button or run mvn spring-boot:run. Access at http://localhost:8080.
src/main/resources/application.properties
PROPS
# Server
server.port=8080

# Database (MySQL)
spring.datasource.url=jdbc:mysql://localhost:3306/school_db
spring.datasource.username=root
spring.datasource.password=yourpassword
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

# JPA / Hibernate
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.database-platform=org.hibernate.dialect.MySQLDialect
pom.xml — key dependencies
XML
<dependencies>
    <!-- Spring Web (MVC + embedded Tomcat) -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!-- Spring Data JPA (Hibernate ORM) -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>

    <!-- MySQL driver -->
    <dependency>
        <groupId>com.mysql</groupId>
        <artifactId>mysql-connector-j</artifactId>
        <scope>runtime</scope>
    </dependency>

    <!-- Lombok — reduces boilerplate -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
</dependencies>
REST Controller & Routing
StudentController.java — full REST API
JAVA
package com.school.controller;

import com.school.model.Student;
import com.school.service.StudentService;
import org.springframework.http.*;
import org.springframework.web.bind.annotation.*;
import java.util.List;

@RestController          // @Controller + @ResponseBody: returns JSON
@RequestMapping("/api/students")  // base path for all routes
public class StudentController {

    private final StudentService studentService;

    // Constructor injection (preferred over @Autowired on field)
    public StudentController(StudentService studentService) {
        this.studentService = studentService;
    }

    // ── GET /api/students — list all ────────────────────
    @GetMapping
    public ResponseEntity<List<Student>> getAllStudents() {
        return ResponseEntity.ok(studentService.findAll());
    }

    // ── GET /api/students/1 — get one ───────────────────
    @GetMapping("/{id}")
    public ResponseEntity<Student> getStudent(@PathVariable Long id) {
        return studentService.findById(id)
            .map(ResponseEntity::ok)
            .orElse(ResponseEntity.notFound().build());
    }

    // ── POST /api/students — create ─────────────────────
    @PostMapping
    public ResponseEntity<Student> createStudent(
            @RequestBody Student student) {
        Student saved = studentService.save(student);
        return ResponseEntity
            .status(HttpStatus.CREATED)  // 201
            .body(saved);
    }

    // ── PUT /api/students/1 — update ────────────────────
    @PutMapping("/{id}")
    public ResponseEntity<Student> updateStudent(
            @PathVariable Long id,
            @RequestBody  Student updates) {
        return studentService.update(id, updates)
            .map(ResponseEntity::ok)
            .orElse(ResponseEntity.notFound().build());
    }

    // ── DELETE /api/students/1 ──────────────────────────
    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deleteStudent(@PathVariable Long id) {
        studentService.delete(id);
        return ResponseEntity.noContent().build();  // 204
    }

    // ── GET /api/students/search?name=alice ─────────────
    @GetMapping("/search")
    public ResponseEntity<List<Student>> search(
            @RequestParam("name") String name) {
        return ResponseEntity.ok(studentService.findByName(name));
    }
}
Dependency Injection & Service Layer
StudentService.java
JAVA
package com.school.service;

import com.school.model.Student;
import com.school.repository.StudentRepository;
import org.springframework.stereotype.Service;
import java.util.*;

@Service  // registers as a Spring bean (injectable)
public class StudentService {

    private final StudentRepository repo;

    // Spring injects StudentRepository automatically
    public StudentService(StudentRepository repo) {
        this.repo = repo;
    }

    public List<Student> findAll()                { return repo.findAll(); }
    public Optional<Student> findById(Long id)   { return repo.findById(id); }
    public Student save(Student s)                 { return repo.save(s); }
    public void delete(Long id)                   { repo.deleteById(id); }

    public List<Student> findByName(String name) {
        return repo.findByNameContainingIgnoreCase(name);
    }

    public Optional<Student> update(Long id, Student updates) {
        return repo.findById(id).map(existing -> {
            existing.setName(updates.getName());
            existing.setEmail(updates.getEmail());
            existing.setGrade(updates.getGrade());
            return repo.save(existing);
        });
    }
}
05
Database Integration
JDBC, CRUD, PreparedStatements, Hibernate ORM & JPA
JDBC — Java Database Connectivity

JDBC is the low-level API for connecting Java to any SQL database. It provides raw control — useful to understand before using higher-level ORMs.

JdbcConnection.java
JAVA
import java.sql.*;

public class JdbcConnection {

    private static final String URL  = "jdbc:mysql://localhost:3306/school_db";
    private static final String USER = "root";
    private static final String PASS = "yourpassword";

    // Reusable method to get a connection
    public static Connection getConnection() throws SQLException {
        return DriverManager.getConnection(URL, USER, PASS);
    }

    public static void main(String[] args) {
        try (Connection conn = getConnection()) {
            System.out.println("Connected: " + conn.getMetaData().getDatabaseProductName());

            // Create table if not exists
            String createSQL = """
                CREATE TABLE IF NOT EXISTS students (
                    id    BIGINT AUTO_INCREMENT PRIMARY KEY,
                    name  VARCHAR(100) NOT NULL,
                    email VARCHAR(150) UNIQUE NOT NULL,
                    grade VARCHAR(5)  DEFAULT 'N/A'
                )""";
            conn.createStatement().execute(createSQL);
            System.out.println("Table ready.");

        } catch (SQLException e) {
            System.err.println("DB Error: " + e.getMessage());
        }
    }
}
CRUD Operations via JDBC
StudentDAO.java — Data Access Object pattern
JAVA
import java.sql.*;
import java.util.*;

public class StudentDAO {

    // ── CREATE (INSERT) ───────────────────────────────────
    public void insert(String name, String email, String grade) {
        String sql = "INSERT INTO students (name, email, grade) VALUES (?, ?, ?)";
        try (Connection conn = JdbcConnection.getConnection();
             PreparedStatement ps = conn.prepareStatement(sql,
                 Statement.RETURN_GENERATED_KEYS)) {

            ps.setString(1, name);
            ps.setString(2, email);
            ps.setString(3, grade);
            ps.executeUpdate();

            ResultSet keys = ps.getGeneratedKeys();
            if (keys.next()) System.out.println("New ID: " + keys.getLong(1));

        } catch (SQLException e) { e.printStackTrace(); }
    }

    // ── READ (SELECT ALL) ─────────────────────────────────
    public List<String> findAll() {
        List<String> students = new ArrayList<>();
        String sql = "SELECT id, name, email, grade FROM students ORDER BY name";
        try (Connection conn = JdbcConnection.getConnection();
             PreparedStatement ps = conn.prepareStatement(sql);
             ResultSet rs = ps.executeQuery()) {

            while (rs.next()) {
                students.add(String.format("%d | %-15s | %s | %s",
                    rs.getLong("id"),
                    rs.getString("name"),
                    rs.getString("email"),
                    rs.getString("grade")));
            }
        } catch (SQLException e) { e.printStackTrace(); }
        return students;
    }

    // ── UPDATE ────────────────────────────────────────────
    public int updateGrade(long id, String newGrade) {
        String sql = "UPDATE students SET grade = ? WHERE id = ?";
        try (Connection conn = JdbcConnection.getConnection();
             PreparedStatement ps = conn.prepareStatement(sql)) {
            ps.setString(1, newGrade);
            ps.setLong(2, id);
            return ps.executeUpdate();  // returns rows affected
        } catch (SQLException e) { e.printStackTrace(); return 0; }
    }

    // ── DELETE ────────────────────────────────────────────
    public int delete(long id) {
        String sql = "DELETE FROM students WHERE id = ?";
        try (Connection conn = JdbcConnection.getConnection();
             PreparedStatement ps = conn.prepareStatement(sql)) {
            ps.setLong(1, id);
            return ps.executeUpdate();
        } catch (SQLException e) { e.printStackTrace(); return 0; }
    }
}
PreparedStatements & SQL Injection Prevention
SqlSecurity.java
JAVA
// ❌ DANGEROUS — never concatenate user input into SQL
String userInput = "' OR '1'='1";  // attacker input
String sql = "SELECT * FROM students WHERE name = '" + userInput + "'";
// Becomes: SELECT * FROM students WHERE name = '' OR '1'='1'
// Returns ALL rows — massive security breach!

// ✅ SAFE — PreparedStatement with placeholders
String safeSql = "SELECT * FROM students WHERE name = ?";
try (PreparedStatement ps = conn.prepareStatement(safeSql)) {
    ps.setString(1, userInput);  // JDBC escapes this safely
    ResultSet rs = ps.executeQuery();
    // Returns 0 rows — attack failed ✅
}

// ✅ SAFE — JPA/Spring Data (always parameterised)
// studentRepository.findByName(userInput);  // no injection possible

// ── BATCH OPERATIONS for performance ─────────────────
String insertSql = "INSERT INTO students (name, email) VALUES (?, ?)";
try (PreparedStatement ps = conn.prepareStatement(insertSql)) {
    conn.setAutoCommit(false);  // manual transaction
    for (String[] student : studentData) {
        ps.setString(1, student[0]);
        ps.setString(2, student[1]);
        ps.addBatch();  // buffer the statement
    }
    ps.executeBatch();    // send all at once
    conn.commit();        // commit transaction
}
🔐 Security Rule #1
Never build SQL strings with user-controlled data. Always use PreparedStatement with ? placeholders, or use Spring Data JPA / Hibernate which parameterise automatically.
Hibernate / JPA ORM

JPA (Java Persistence API) with Hibernate lets you map Java classes to database tables using annotations — no SQL required for standard CRUD.

Student.java — JPA Entity
JAVA
package com.school.model;

import jakarta.persistence.*;
import lombok.*;

@Entity                          // marks this as a JPA entity (DB table)
@Table(name = "students")       // maps to the "students" table
@Data                            // Lombok: generates getters/setters/toString
@NoArgsConstructor               // Lombok: generates no-arg constructor
@AllArgsConstructor              // Lombok: generates all-args constructor
public class Student {

    @Id                          // primary key
    @GeneratedValue(strategy = GenerationType.IDENTITY)  // AUTO_INCREMENT
    private Long id;

    @Column(nullable = false, length = 100)
    private String name;

    @Column(nullable = false, unique = true, length = 150)
    private String email;

    @Column(length = 5, columnDefinition = "VARCHAR(5) DEFAULT 'N/A'")
    private String grade = "N/A";

    @Column(name = "created_at", updatable = false)
    @CreationTimestamp
    private java.time.LocalDateTime createdAt;
}
StudentRepository.java — Spring Data JPA
JAVA
package com.school.repository;

import com.school.model.Student;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
import java.util.List;

@Repository
public interface StudentRepository extends JpaRepository<Student, Long> {

    // Spring Data generates SQL from method names!
    List<Student> findByGrade(String grade);
    List<Student> findByNameContainingIgnoreCase(String name);
    boolean existsByEmail(String email);

    // Custom JPQL query
    @Query("SELECT s FROM Student s WHERE s.grade IN :grades ORDER BY s.name")
    List<Student> findTopStudents(@Param("grades") List<String> grades);

    // Custom native SQL
    @Query(value = "SELECT COUNT(*) FROM students WHERE grade = ?1",
           nativeQuery = true)
    long countByGrade(String grade);
}
// JpaRepository provides: findAll, findById, save, deleteById,
// count, existsById — all for FREE, no implementation needed!
06
Mini Project
Student Management System — Spring Boot + MySQL + REST API
🎯 Capstone Project

Student Management System

A production-structured Spring Boot application with layered architecture (Controller → Service → Repository), MySQL persistence, full CRUD REST API, and global error handling.

Project Structure
Maven project layout
TREE
student-management/
├── src/main/
│   ├── java/com/school/
│   │   ├── StudentManagementApplication.java  // @SpringBootApplication
│   │   ├── model/
│   │   │   ├── Student.java                   // @Entity
│   │   │   └── ApiResponse.java               // standard JSON wrapper
│   │   ├── repository/
│   │   │   └── StudentRepository.java         // JpaRepository
│   │   ├── service/
│   │   │   ├── StudentService.java            // interface
│   │   │   └── StudentServiceImpl.java        // @Service implementation
│   │   ├── controller/
│   │   │   └── StudentController.java         // @RestController
│   │   └── exception/
│   │       ├── ResourceNotFoundException.java
│   │       └── GlobalExceptionHandler.java    // @ControllerAdvice
│   └── resources/
│       └── application.properties
└── pom.xml
Entity & Repository Layer
model/ApiResponse.java — standardised JSON response
JAVA
package com.school.model;

import lombok.*;
import java.time.LocalDateTime;

@Data @AllArgsConstructor @NoArgsConstructor
public class ApiResponse<T> {
    private boolean success;
    private String  message;
    private T       data;
    private LocalDateTime timestamp = LocalDateTime.now();

    public static <T> ApiResponse<T> success(String msg, T data) {
        return new ApiResponse<>(true, msg, data, LocalDateTime.now());
    }

    public static <T> ApiResponse<T> error(String msg) {
        return new ApiResponse<>(false, msg, null, LocalDateTime.now());
    }
}
Service Layer with Business Logic
exception/GlobalExceptionHandler.java
JAVA
package com.school.exception;

import com.school.model.ApiResponse;
import org.springframework.http.*;
import org.springframework.web.bind.annotation.*;

@ControllerAdvice  // handles exceptions across ALL controllers
public class GlobalExceptionHandler {

    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<ApiResponse<?>> handleNotFound(
            ResourceNotFoundException ex) {
        return ResponseEntity
            .status(HttpStatus.NOT_FOUND)
            .body(ApiResponse.error(ex.getMessage()));
    }

    @ExceptionHandler(IllegalArgumentException.class)
    public ResponseEntity<ApiResponse<?>> handleBadRequest(
            IllegalArgumentException ex) {
        return ResponseEntity
            .status(HttpStatus.BAD_REQUEST)
            .body(ApiResponse.error(ex.getMessage()));
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<ApiResponse<?>> handleAll(Exception ex) {
        return ResponseEntity
            .status(HttpStatus.INTERNAL_SERVER_ERROR)
            .body(ApiResponse.error("An unexpected error occurred"));
    }
}
Full Controller — Putting It All Together
controller/StudentController.java — production-ready
JAVA
package com.school.controller;

import com.school.model.*;
import com.school.service.StudentService;
import jakarta.validation.Valid;
import org.springframework.http.*;
import org.springframework.web.bind.annotation.*;
import java.util.List;

@RestController
@RequestMapping("/api/v1/students")
public class StudentController {

    private final StudentService service;

    public StudentController(StudentService service) {
        this.service = service;
    }

    @GetMapping
    public ResponseEntity<ApiResponse<List<Student>>> getAll() {
        List<Student> students = service.findAll();
        return ResponseEntity.ok(
            ApiResponse.success("Found " + students.size() + " students", students)
        );
    }

    @GetMapping("/{id}")
    public ResponseEntity<ApiResponse<Student>> getById(@PathVariable Long id) {
        Student student = service.findByIdOrThrow(id);
        return ResponseEntity.ok(ApiResponse.success("Student found", student));
    }

    @PostMapping
    public ResponseEntity<ApiResponse<Student>> create(
            @Valid @RequestBody Student student) {
        Student saved = service.create(student);
        return ResponseEntity
            .status(HttpStatus.CREATED)
            .body(ApiResponse.success("Student created", saved));
    }

    @PutMapping("/{id}")
    public ResponseEntity<ApiResponse<Student>> update(
            @PathVariable Long id,
            @Valid @RequestBody Student student) {
        Student updated = service.update(id, student);
        return ResponseEntity.ok(ApiResponse.success("Student updated", updated));
    }

    @DeleteMapping("/{id}")
    public ResponseEntity<ApiResponse<Void>> delete(@PathVariable Long id) {
        service.delete(id);
        return ResponseEntity.ok(ApiResponse.success("Student deleted", null));
    }
}
/*
  Sample JSON responses:

  GET /api/v1/students/1
  {
    "success": true,
    "message": "Student found",
    "data": { "id": 1, "name": "Alice", "email": "alice@school.rw", "grade": "A" },
    "timestamp": "2024-03-15T10:22:05"
  }

  POST /api/v1/students  body: {"name":"Bob","email":"bob@school.rw","grade":"B"}
  → 201 Created
*/
▶ Running the Project
mvn spring-boot:run → open http://localhost:8080/api/v1/students in Postman or any REST client. The database tables are created automatically by Hibernate on first run.
📋 Testing with Postman
Import the base URL http://localhost:8080/api/v1/students. Use GET (list/single), POST with JSON body (create), PUT with ID + body (update), DELETE with ID. All responses follow the standardised ApiResponse wrapper.