Spring Boot
Module 4
First expose REST services, then wire Hibernate — Spring Boot ties it all together
Across the previous modules we built a small quiz project as a multi-module Maven build:
Student domain and a hand-written Hibernate DAO (Module 3)
Goal: expose the quiz over HTTP, then back it with a real database — the Spring Boot way.
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.
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.
mainapplication.propertiesDataSource beansSessionFactory / EntityManagerFactoryConfig.javaGet something on screen first, persist it second.
quiz-rest a Spring Boot web app and serve the quiz over HTTP (data in memory for now)
PART 1
Turn quiz-rest into a running Spring Boot web 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.
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.
Resources are addressed by URL; the HTTP verb says what to do with them:
| Method | On /api/students | Meaning |
|---|---|---|
| GET | /api/students | List students |
| GET | /api/students/{id} | One student |
| POST | /api/students | Create 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.
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.
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.
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
Back those endpoints with a real, persisted database
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.
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
@Bean methods in Config.javaDelete Config.java: its DataSource, SessionFactory and Properties beans are now auto-configured.
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.
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.
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);
}
}
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.
DataSource beanSessionFactory via LocalSessionFactoryBeanHibernateStudentDAO opening sessions by handapplication.propertiesJpaRepository interface — no implementationSame Hibernate underneath — Spring Boot just wires it for you so you focus on the controller and the domain.
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.
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.
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.
Task: Add a second resource to quiz-rest.
Question entity (id, statement, answer) in quiz-coreQuestionRepository extends JpaRepository<Question, String>/api/questions201 Created on POST and 404 for an unknown idspring.jpa.show-sql=trueTask: Make the API friendlier.
GET /api/students?name=... backed by a derived queryStudentDTO record instead of the entity@RestControllerAdvice mapping a not-found exception to 404MockMvc test for the search endpoint@DataJpaTest test for the derived queryIn this module, you turned the empty quiz-rest into a real web app:
@SpringBootApplication + @RestController@RequestBody/@PathVariable, ResponseEntityspring-boot-starter-data-jpa and application.propertiesJpaRepositoryMockMvc and @DataJpaTestKey idea: Spring Boot didn't add new Hibernate or Spring concepts — it wired the ones you already know.