Advanced Java

Spring Boot

Module 4

First expose REST services, then wire Hibernate — Spring Boot ties it all together

Where We Are

Across the previous modules we built a small quiz project as a multi-module Maven build:

calculator — build tools, TDD and our first taste of the Spring container (Modules 1–2)
quiz-core — the Student domain and a hand-written Hibernate DAO (Module 3)
quiz-rest — empty so far. This module turns it into a running web app.

Goal: expose the quiz over HTTP, then back it with a real database — the Spring Boot way.

The Problem: Wiring Everything by Hand

To use Hibernate with plain Spring we had to configure a DataSource, a SessionFactory and its properties ourselves:

@Configuration
public class Config {

    @Bean
    public DataSource dataSource() {
        return new DriverManagerDataSource("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1");
    }

    @Bean
    public SessionFactory sessionFactory(DataSource ds, Properties hibernateProperties) {
        LocalSessionFactoryBean factory = new LocalSessionFactoryBean();
        factory.setDataSource(ds);
        factory.setHibernateProperties(hibernateProperties);
        return factory.getObject();
    }

    @Bean
    public Properties hibernateProperties() {
        Properties props = new Properties();
        props.put("hibernate.dialect", "org.hibernate.dialect.H2Dialect");
        props.put("hibernate.hbm2ddl.auto", "validate");
        props.put("hibernate.show_sql", "true");
        return props;
    }
}

...and we still have no web server, no JSON, no endpoints.

What is Spring Boot?

Spring Boot is an opinionated layer on top of the Spring container you already know. It does not replace Spring — it configures it for you.


It gives you

  • Auto-configuration — beans created from what's on the classpath
  • Starters — curated dependency bundles
  • Embedded server — Tomcat, just run the main
  • Sensible defaults over application.properties

So you stop writing

  • Manual DataSource beans
  • Manual SessionFactory / EntityManagerFactory
  • Server bootstrapping and JSON plumbing
  • Most of Config.java

The Plan for This Module

Get something on screen first, persist it second.

1 Expose REST services — make quiz-rest a Spring Boot web app and serve the quiz over HTTP (data in memory for now)
2 Wire Hibernate — let Spring Boot auto-configure the datasource & Hibernate so those endpoints read and write a real database

PART 1

Expose REST Services

Turn quiz-rest into a running Spring Boot web app

Step 1: Make quiz-rest a Spring Boot App

Inherit the Boot parent (it manages versions) and add the web starter. Keep depending on quiz-core:

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

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
        <groupId>fr.epita</groupId>
        <artifactId>quiz-core</artifactId>
        <version>1.0-SNAPSHOT</version>
    </dependency>
</dependencies>

starter-web brings Spring MVC, Jackson (JSON) and an embedded Tomcat.

Step 2: One Annotation to Boot It

Replace the empty Launcher with a Spring Boot application:

package fr.epita.quiz.rest;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Launcher {
    public static void main(String[] args) {
        SpringApplication.run(Launcher.class, args);
    }
}

@SpringBootApplication = @Configuration + @ComponentScan + @EnableAutoConfiguration. Run it — an embedded Tomcat starts on http://localhost:8080.

REST in 30 Seconds

Resources are addressed by URL; the HTTP verb says what to do with them:

MethodOn /api/studentsMeaning
GET/api/studentsList students
GET/api/students/{id}One student
POST/api/studentsCreate a student
PUT/api/students/{id}Update a student
DELETE/api/students/{id}Delete a student

Common status codes: 200 OK, 201 Created, 204 No Content, 404 Not Found, 400 Bad Request.

Step 3: A REST Controller for Students

For now we hold data in memory — the point is to get HTTP + JSON working:

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

    private final Map<String, Student> store = new ConcurrentHashMap<>();

    @GetMapping
    public Collection<Student> findAll() {
        return store.values();
    }

    @GetMapping("/{id}")
    public Student findById(@PathVariable String id) {
        return store.get(id);
    }

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public Student create(@RequestBody Student student) {
        store.put(student.getId(), student);
        return student;
    }
}

@RestController serializes returned objects to JSON automatically.

Step 4: Run It and Call It

mvn spring-boot:run

# create a student
curl -X POST localhost:8080/api/students \
     -H "Content-Type: application/json" \
     -d '{"id":"42","name":"Ada Lovelace","address":"London"}'

# list them
curl localhost:8080/api/students
# => [{"id":"42","name":"Ada Lovelace","address":"London"}]

It works — but the data lives in a Map. Restart the app and it's gone. Time to wire a real database.

Bonus: Control the HTTP Response

Return ResponseEntity when you need the right status code and headers:

@GetMapping("/{id}")
public ResponseEntity<Student> findById(@PathVariable String id) {
    Student student = store.get(id);
    return (student != null)
        ? ResponseEntity.ok(student)
        : ResponseEntity.notFound().build();   // 404 instead of null
}

Map domain errors to status codes globally with @RestControllerAdvice:

@RestControllerAdvice
public class ApiExceptionHandler {

    @ExceptionHandler(StudentNotFoundException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public String handleNotFound(StudentNotFoundException ex) {
        return ex.getMessage();
    }
}

PART 2

Wire Hibernate

Back those endpoints with a real, persisted database

Step 5: Add the Persistence Starter

Two dependencies pull in JPA, Hibernate (the default provider) and an in-memory H2 database:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>runtime</scope>
</dependency>

Just by being on the classpath, Boot now auto-configures a DataSource and a Hibernate EntityManagerFactory.

Step 6: Properties Replace Config.java

Everything we wired by hand becomes a few lines of application.properties (under src/main/resources):

spring.datasource.url=jdbc:h2:mem:test;DB_CLOSE_DELAY=-1

spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
# dialect is detected automatically from the H2 driver
Before: 3–4 hand-written @Bean methods in Config.java
After: 4 properties — Boot builds the same beans for you

Delete Config.java: its DataSource, SessionFactory and Properties beans are now auto-configured.

Step 7: The Entity Is Already Done

Our Student from quiz-core is already a JPA entity — Boot finds and maps it:

package fr.epita.quiz.datamodel;

import jakarta.persistence.*;

@Entity
@Table(name = "STUDENTS")
public class Student {

    @Id
    @Column(name = "ID", nullable = false)
    private String id;

    private String name;
    private String address;

    // constructors, getters, setters
}

Same @Entity we wrote in the Hibernate module — nothing to change.

Step 8: A Repository Instead of a Hand-Written DAO

In Module 3 we wrote HibernateStudentDAO against the SessionFactory by hand. Spring Data JPA generates that for us from an interface:

package fr.epita.quiz.rest;

import fr.epita.quiz.datamodel.Student;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;

public interface StudentRepository extends JpaRepository<Student, String> {

    // derived query - Spring writes the JPQL for you
    List<Student> findByNameContainingIgnoreCase(String fragment);
}

You get save, findById, findAll, delete… with zero implementation.

Step 9: Point the Controller at the Database

Inject the repository (constructor injection — Module 2) and the in-memory Map disappears:

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

    private final StudentRepository repository;

    public StudentController(StudentRepository repository) {
        this.repository = repository;          // injected by Spring
    }

    @GetMapping
    public List<Student> findAll() {
        return repository.findAll();           // SELECT ... FROM STUDENTS
    }

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public Student create(@RequestBody Student student) {
        return repository.save(student);       // INSERT INTO STUDENTS
    }

    @DeleteMapping("/{id}")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public void delete(@PathVariable String id) {
        repository.deleteById(id);
    }
}

Step 10: Hibernate Is Doing the Work

With spring.jpa.show-sql=true, the same POST as before now hits the database. The log shows Hibernate generating SQL:

Hibernate:
    insert into STUDENTS (address, name, ID)
    values (?, ?, ?)

Hibernate:
    select s1_0.ID, s1_0.address, s1_0.name
    from STUDENTS s1_0

Restart the app and the data survives (with a file-based or external DB). The REST layer never changed — only what sits behind it.

What Spring Boot Bought Us

Plain Spring (Module 2–3)

  • Hand-written DataSource bean
  • Hand-written SessionFactory via LocalSessionFactoryBean
  • HibernateStudentDAO opening sessions by hand
  • No web server, no JSON

Spring Boot (this module)

  • DataSource + Hibernate auto-configured
  • A few lines in application.properties
  • JpaRepository interface — no implementation
  • Embedded Tomcat + JSON out of the box

Same Hibernate underneath — Spring Boot just wires it for you so you focus on the controller and the domain.

Note: Your Hibernate DAO Still Works

Spring Data JPA is the idiomatic path, but you are not forced to drop the Module 3 DAO. Boot exposes the underlying Hibernate SessionFactory, so a hand-written DAO can still be a bean:

@Bean
public IStudentDAO studentDAO(EntityManagerFactory emf) {
    // unwrap the Hibernate SessionFactory Boot already configured
    return new HibernateStudentDAO(emf.unwrap(SessionFactory.class));
}

Use it when you have complex, hand-tuned queries; reach for JpaRepository for everything else.

Good Practice: DTOs at the Edge

Returning entities directly couples your API to your database schema. Expose a DTO instead:

public record StudentDTO(String id, String name) { }   // no address exposed

@GetMapping("/{id}")
public ResponseEntity<StudentDTO> findById(@PathVariable String id) {
    return repository.findById(id)
        .map(s -> new StudentDTO(s.getId(), s.getName()))
        .map(ResponseEntity::ok)
        .orElse(ResponseEntity.notFound().build());
}

DTOs let the API and the persistence model evolve independently.

Testing the Boot App

Spin up the web layer with MockMvc — no real server, no real port:

@SpringBootTest
@AutoConfigureMockMvc
class StudentControllerTest {

    @Autowired MockMvc mockMvc;

    @Test
    void createsAStudent() throws Exception {
        mockMvc.perform(post("/api/students")
                .contentType(MediaType.APPLICATION_JSON)
                .content("""
                    {"id":"42","name":"Ada","address":"London"}"""))
            .andExpect(status().isCreated())
            .andExpect(jsonPath("$.name").value("Ada"));
    }
}

Use @DataJpaTest to test the repository against an in-memory database in isolation.

Exercise 1: Expose the Quiz

Task: Add a second resource to quiz-rest.


Requirements:

  1. Create a Question entity (id, statement, answer) in quiz-core
  2. Add a QuestionRepository extends JpaRepository<Question, String>
  3. Expose full CRUD under /api/questions
  4. Return 201 Created on POST and 404 for an unknown id
  5. Verify the generated SQL with spring.jpa.show-sql=true

Exercise 2: Search & DTOs

Task: Make the API friendlier.


Requirements:

  1. Add GET /api/students?name=... backed by a derived query
  2. Return a StudentDTO record instead of the entity
  3. Add a global @RestControllerAdvice mapping a not-found exception to 404
  4. Write a MockMvc test for the search endpoint
  5. Add a @DataJpaTest test for the derived query

Summary

In this module, you turned the empty quiz-rest into a real web app:

Key idea: Spring Boot didn't add new Hibernate or Spring concepts — it wired the ones you already know.

Resources

Slide Overview