Spring Boot Notes
Annotations
Section titled โAnnotationsโ@Component
Section titled โ@Componentโ@Component annotation marks a class as a Spring-managed bean.
It does not automatically start any logic inside the class unless you explicitly trigger it.
Spring detects a @Component-annotated class during component scanning and adds it to the application context.
To make a @Component run at startup you have many options:
- Implement
CommandLineRunnerinterface and overriderun()method, - Annotate a method with
@PostConstructannotation, - Annotate a method with
@Scheduledannotation (for periodic tasks).
Please Note: @Scheduled annotation over a @Component class method requires the main class to be annotated with @EnableScheduling.
PostConstruct example
Section titled โPostConstruct exampleโimport jakarta.annotation.PostConstruct;import lombok.extern.slf4j.Slf4j;import org.springframework.stereotype.Component;
@Slf4j@Componentpublic class InitTask { @PostConstruct public void init() { log.info(">>> initialized automatically after bean creation."); }}CommandLineRunner example
Section titled โCommandLineRunner exampleโimport lombok.extern.slf4j.Slf4j;import org.springframework.boot.CommandLineRunner;import org.springframework.stereotype.Component;
@Slf4j@Componentpublic class StartupTask implements CommandLineRunner { @Override public void run(String... args) { log.info(">>> running at startup"); }}Periodic Task example
Section titled โPeriodic Task exampleโimport lombok.extern.slf4j.Slf4j;import org.springframework.scheduling.annotation.Scheduled;import org.springframework.stereotype.Component;
@Slf4j@Componentpublic class SchedulerTask { @Scheduled(fixedRate = 5000) public void runEvery5Seconds() { log.info(">>> running every 5 seconds"); }}And the main class:
import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.scheduling.annotation.EnableScheduling;
@EnableScheduling@SpringBootApplicationpublic class RunningPeriodicTaskApplication { public static void main(String[] args) { SpringApplication.run(RunningPeriodicTaskApplication.class, args); }}@Controller
Section titled โ@ControllerโYou can use @Controller annotation for traditional Spring controllers.
@Controller V.S. @RestController
Section titled โ@Controller V.S. @RestControllerโSpring version 4.0 introduced the @RestController annotation in order to simplify the creation of RESTful web services.
It combines @Controller and @ResponseBody annotations, which eliminates the need to annotate every request handling method of the controller class with the @ResponseBody annotation.
Letโs see the same code example first with @Controller annotation and then with @RestController.
@Controller@RequestMapping("users")public class UserController {
@GetMapping("/{id}", produces = "application/json") public @ResponseBody User getUser(@PathVariable int id) { return findUserById(id); }
private Book findUserById(int id) { // perform DB search or whatever is needed }}The same example becomes:
@RestController@RequestMapping("rest-users")public class UserController {
@GetMapping("/{id}", produces = "application/json") public User getUser(@PathVariable int id) { return findUserById(id); }
private Book findUserById(int id) { // perform DB search or whatever is needed }}Serve static HTML page
Section titled โServe static HTML pageโWithin a traditional Spring controller itโs very easy to serve a static HTML page. Given the following project structure:
Directorysrc.main
Directoryjava.com.pietropouzzi.test
Directorycontrollers
- TestController.java
- TestApplication.java
Directoryresources
Directorytemplates
- hello.html
- application.properties
- pom.xml
Here are the files:
package com.pietropoluzzi.user_crud_server.controllers;
import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestMapping;
@Controller@RequestMapping("/test")public class TestController {
@GetMapping("/hello") public String helloPage() { return "hello"; }}<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <title>Hello</title></head><body><h1>Hello, World!</h1></body></html><?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.5.3</version> <relativePath/> </parent>
<!-- all the properties... -->
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency>
<!-- add this dependency to serve static HTML page within @Controller-annotated classes --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> </dependencies> <build> <!-- ... --> </build></project>@RestController
Section titled โ@RestControllerโServe static HTML page with @RestController
Section titled โServe static HTML page with @RestControllerโ@RestController is usually used for returning raw data (e.g., JSON or strings).
You can allow a @RestController to return an HTML view.
Given the following project structure:
Directorysrc.main
Directoryjava.com.pietropouzzi.test
Directorycontrollers
- TestRestController.java
- TestApplication.java
Directoryresources
Directorystatic
- hello.html
- application.properties
- pom.xml
Here are the files:
package com.pietropoluzzi.test.controllers;
import org.springframework.beans.factory.annotation.Autowired;import org.springframework.core.io.Resource;import org.springframework.core.io.ResourceLoader;import org.springframework.http.MediaType;import org.springframework.http.ResponseEntity;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RestController;
import java.io.IOException;import java.nio.charset.StandardCharsets;
@RestControllerpublic class TestRestController {
@Autowired private ResourceLoader resourceLoader;
@GetMapping("/rest-test/hello") public ResponseEntity<String> getHtmlPage() throws IOException { Resource resource = resourceLoader.getResource("classpath:static/hello.html"); String html = new String(resource.getInputStream().readAllBytes(), StandardCharsets.UTF_8); return ResponseEntity.ok() .contentType(MediaType.TEXT_HTML) .body(html); }}<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <title>Hello</title></head><body><h1>Hello, World!</h1></body></html>PostConstruct
Section titled โPostConstructโFrom JDK 6 to JDK 8, the @PostConstruct and @PreDestroy annotations were part of the standard Java libraries under the javax.annotation package.
With JDK 9, the entire javax.annotation package was removed from the core Java modules and fully eliminated in JDK 11.
In Jakarta EE 9, this package has been relocated to jakarta.annotation.
Java EE, known as Jakarta EE, is a platform for developing and deploying enterprise-level Java applications. It provides a set of specifications and APIs that extend Java SE (Standard Edition) to handle the complexities of large-scale, distributed, and networked applications.
@Slf4j allows you to easily use a static Logger instance called log within the annotated class.
Add the following directory within pom.xml file:
<project> <dependencies> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies></project>Then you can annotate the class like so:
@Slf4jpublic class MainClass {
public void someMethod() { log.info("someMethod() invocation"); }}@Slf4j creates under the hood the following log implementation:
private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(MainClass.class);HTTP Response
Section titled โHTTP ResponseโUse Spring ResponseEntity to manipulate HTTP response.
ResponseEntity represents the whole HTTP response: status code, headers, and body.
You can use it to fully configure the HTTP response.
It provides two nested builder interfaces: HeadersBuilder and its sub-interface BodyBuilder.
You can access their capabilities through the static methods of ResponseEntity.
The simplest case is a response with a body and HTTP 200 response code:
@PostMapping("/ok")public ResponseEntity<String> ok() { return ResponseEntity.ok("Hello, World!");}You can access BodyBuilder capabilities through the static methods of ResponseEntity:
@PostMapping("/body-builder-ok")public ResponseEntity<String> bodyBuilderOk() { return ResponseEntity.ok().body("OK request body");}
@PostMapping("/body-builder-bad-request")public ResponseEntity<String> bodyBuilderBadRequest() { return ResponseEntity.badRequest().body("Bad Request body");}
@PostMapping("/body-builder-accepted")public ResponseEntity<String> bodyBuilderSuccess() { return ResponseEntity.status(HttpStatus.ACCEPTED).body("Accepted request body");}You can also create and return a new ResponseEntity instance by passing a type <T> body and an HttpStatusCode implementation:
@PostMapping("/ok-v2")public ResponseEntity<String> ok_v2() { return new ResponseEntity<>("Hello, World!", HttpStatus.OK);}Set custom headers to the response in two different ways:
@PostMapping("/body-builder-custom-headers")public ResponseEntity<String> bodyBuilderCustomHeaders() { return ResponseEntity.ok().header("Custom-Header", "foo").body("ok request body - check the headers");}
@PostMapping("/custom-header")public ResponseEntity<?> customHeader() { HttpHeaders headers = new HttpHeaders(); headers.add("Custom-Header", "foo");
return new ResponseEntity<>("Hello, World!", headers, HttpStatus.OK);}Return different statuses
Section titled โReturn different statusesโLetโs take a more advanced example. You want to perform a POST that adds a new entity (e.g., a user) to a database. Entity properties must meet certain constraints, e.g., age must be an integer greater than zero or name cannot start with a digit.
The project structure looks like:
Directorycom.pietropoluzzi.http_response
Directorycontrollers
- UserController.java
Directorymodels
- UserModel.java
- UserCreationModel.java
Directoryservices
- UserService.java
package com.pietropoluzzi.http_response.models;
record UserModel(Long id, String name, Integer age) {}package com.pietropoluzzi.user_crud_server.models;
import lombok.Getter;import lombok.Setter;
@Getter@Setterpublic class UserCreationResult { private UserModel user; private String errorMsg; private boolean creationSuccess;
public static UserCreationResult success(UserModel user) { UserCreationResult result = new UserCreationResult(); result.creationSuccess = true; result.user = user;
return result; }
public static UserCreationResult failure(String errorMsg) { UserCreationResult result = new UserCreationResult(); result.creationSuccess = false; result.errorMsg = errorMsg;
return result; }}package com.pietropoluzzi.http_response.services;
@Service // org.springframework.stereotype.Servicepublic class UserService {
/** * Check that a string starts with a number * @param str string to check * @return {@code true} if {@code str} starts with a digit, {@code false} otherwise. */ private boolean startsWithDigit(String str) { return str != null && !str.isEmpty() && Character.isDigit(str.charAt(0)); }
public UserCreationResult save(UserModel user) { long nextId = 123;
// check that age is a non-negative number before saving the user if(user.age() < 0) { return UserCreationResult.failure("User age bust be non-negative"); }
if (startsWithDigit(user.name())) { return UserCreationResult.failure("User name must not start with a digit"); }
UserModel newUser = new UserModel(nextId, user.name(), user.age());
// add the user to the database // (code not implemented in this example for readability issues)
return UserCreationResult.success(newUser); }
}package com.pietropoluzzi.http_response.controllers;
@RestController@RequestMapping("/users") // org.springframework.stereotype.Controller;public class UserController {
@PostMapping public ResponseEntity<?> createUser(@RequestBody UserModel user) { UserCreationResult result = service.save(user);
if(result.isCreationSuccess()) { return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(result.getUser()); } else { return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(result.getErrorMsg()); } }}You can now perform the POST in the following ways to test the different scenarios:
Get an HTTP status 200:
Windows PowerShell curl -Method POST http://localhost:8080/users `-Headers @{ "Content-Type" = "application/json" } `-Body '{ "name": "Pit", "age": 24 }'Bash Terminal curl -X POST http://localhost:8080/users \-H "Content-Type: application/json" \-d '{"name": "Pit", "age": 24}'Get an HTTP status 400 because of negative age:
Windows PowerShell curl -Method POST http://localhost:8080/users `-Headers @{ "Content-Type" = "application/json" } `-Body '{ "name": "Pit", "age": -10 }'Bash Terminal curl -X POST http://localhost:8080/users \-H "Content-Type: application/json" \-d '{"name": "Pit", "age": -10}'Get an HTTP status 400 because of name stars with a digit:
Windows PowerShell curl -Method POST http://localhost:8080/users `-Headers @{ "Content-Type" = "application/json" } `-Body '{ "name": "123Pit", "age": 24 }'Bash Terminal curl -X POST http://localhost:8080/users \-H "Content-Type: application/json" \-d '{"name": "123Pit", "age": 24}'
Multi-modules Project
Section titled โMulti-modules ProjectโLetโs take the example of a large project. To simplify developing and debugging, the project can be subdivided into many modules. Modules can interact with each other. They can be full Spring Boot applications or just plain Java code.
You must write a โparentโ pom.xml and many โchildโ pom.xml files (one for each module).
Modules skeleton can be created using Spring initializr web tool.
In the example Iโm going to show you, the โparentโ is called com.pietropoluzzi.spring_notes, and the modules are core and rest_server.
Directorycore
Directorysrc/main
Directoryjava/com/pietropoluzzi/spring_notes/core/ here is the code
- โฆ
Directoryresources
- application.properties
- pom.xml core pom
Directoryrest_server
Directorysrc/main/
- java/com/pietropoluzzi/spring_notes/rest_server here is the code
Directoryresources/
- โฆ
- pom.xml REST server pom
- pom.xml parent pom
Here are the files:
<?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.5.3</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.pietropoluzzi.spring_notes</groupId> <artifactId>spring_notes</artifactId> <version>0.0.1</version> <name>spring_notes</name> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-autoconfigure</artifactId> </dependency> </dependencies> <packaging>pom</packaging>
<description>Spring Notes parent project</description> <url/> <licenses> <license/> </licenses> <developers> <developer/> </developers> <scm> <connection/> <developerConnection/> <tag/> <url/> </scm> <properties> <java.version>21</java.version> <spring.boot.version>3.5.3</spring.boot.version> </properties>
<modules> <module>core</module> <module>rest_server</module> </modules>
<build> <pluginManagement> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </pluginManagement> </build></project><?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>com.pietropoluzzi.spring_notes</groupId> <artifactId>spring_notes</artifactId> <version>0.0.1</version> </parent> <artifactId>core</artifactId> <version>0.0.1</version> <name>core</name> <packaging>jar</packaging> <description>Spring Notes's core module</description> <url/> <licenses> <license/> </licenses> <developers> <developer/> </developers> <scm> <connection/> <developerConnection/> <tag/> <url/> </scm> <properties> <java.version>21</java.version> </properties> <dependencies> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency>
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency>
<!-- JUnit Jupiter (JUnit 5) --> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>jakarta.validation</groupId> <artifactId>jakarta.validation-api</artifactId> </dependency> </dependencies></project><?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>com.pietropoluzzi.spring_notes</groupId> <artifactId>spring_notes</artifactId> <version>0.0.1</version> </parent> <artifactId>rest_server</artifactId> <version>0.0.1</version> <name>rest_server</name> <description>Spring Notes's REST server module</description> <url/> <licenses> <license/> </licenses> <developers> <developer/> </developers> <scm> <connection/> <developerConnection/> <tag/> <url/> </scm> <properties> <java.version>21</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>
<dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency>
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency>
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency>
<dependency> <groupId>org.springdoc</groupId> <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId> <version>2.5.0</version> </dependency>
<dependency> <groupId>com.pietropoluzzi.spring_notes</groupId> <artifactId>core</artifactId> <version>0.0.1</version> <scope>compile</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <excludes> <exclude> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </exclude> </excludes> </configuration> </plugin> </plugins> </build></project>Conclusion
Section titled โConclusionโDocs: