Skip to main content

Chapter 4: Basic Annotations Spring Boot

Introduction​

Annotations are at the heart of Spring Boot. They allow configuring the application declaratively, without XML, making the code more readable and maintainable.

Chapter Objectives​

By the end of this chapter, you will be able to:

  • Understand the role of the main Spring Boot annotations
  • Use @RestController, @Service, @Repository
  • Master HTTP mapping annotations
  • Inject dependencies with @Autowired
  • Create Spring beans

What is an Annotation?​

Definition​

An annotation is a form of metadata that provides information about the code without being part of it directly.

Format:

@AnnotationName
@AnnotationName(parameter=value)

Example:

@RestController
@RequestMapping("/api")
public class UserController {
// ...
}

Why Annotations?​

Advantages:

  • More concise code
  • Configuration close to the code
  • Type-safe (compile-time checking)
  • Facilitates refactoring

@SpringBootApplication​

The Master Annotation​

package com.example.greeting;

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

@SpringBootApplication
public class GreetingApplication {

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

Composition of @SpringBootApplication​

@SpringBootApplication is a meta-annotation that combines three annotations:

@SpringBootApplication
=
@SpringBootConfiguration
+ @EnableAutoConfiguration
+ @ComponentScan

Detail of Each Annotation​

1. @SpringBootConfiguration​

Role: Marks the class as a source of bean definitions

Equivalent to:

@Configuration

Allows defining beans:

@SpringBootApplication
public class GreetingApplication {

@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}

2. @EnableAutoConfiguration​

Role: Enables Spring Boot's auto-configuration

Example: If Spring Boot detects:

  • spring-boot-starter-web → Configures Tomcat, Spring MVC
  • spring-boot-starter-data-jpa → Configures Hibernate, DataSource
  • PostgreSQL Driver → Configures PostgreSQL

Auto-configuration:

3. @ComponentScan​

Role: Scans the package and sub-packages for Spring components

Scanned package:

package com.example.greeting;  // Base package

// Spring will scan:
// - com.example.greeting
// - com.example.greeting.controller
// - com.example.greeting.service
// - etc.

Detected components:

  • @Component
  • @Controller / @RestController
  • @Service
  • @Repository
  • @Configuration

Stereotype Annotations​

Component Hierarchy​

@Component​

Role: Generic annotation for any Spring component

Usage:

import org.springframework.stereotype.Component;

@Component
public class EmailValidator {

public boolean isValid(String email) {
return email.contains("@");
}
}

When to use: For utility classes that do not fit into other categories

@Controller​

Role: Marks a class as an MVC controller (returns views)

Usage:

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class HomeController {

@GetMapping("/")
public String home(Model model) {
model.addAttribute("message", "Welcome!");
return "home"; // Returns the view "home.html"
}
}

Architecture:

@RestController​

Role: REST controller (returns JSON/XML data)

Equivalent to:

@RestController
=
@Controller + @ResponseBody

Usage:

import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.GetMapping;

@RestController
public class GreetingController {

@GetMapping("/greeting")
public Greeting greeting() {
return new Greeting(1L, "Hello, World!");
}
}

Architecture:

Difference @Controller vs @RestController:

Aspect@Controller@RestController
ReturnView name (String)Data (JSON/XML)
UsageTraditional web applicationsREST APIs
SerializationNoAutomatic (Jackson)
@ResponseBodyRequired on each methodImplicit

@Service​

Role: Marks a class containing business logic

Usage:

import org.springframework.stereotype.Service;
import java.util.concurrent.atomic.AtomicLong;

@Service
public class GreetingService {

private final AtomicLong counter = new AtomicLong();
private static final String template = "Hello, %s!";

public Greeting createGreeting(String name) {
return new Greeting(
counter.incrementAndGet(),
String.format(template, name)
);
}
}

Responsibilities:

  • Business logic
  • Complex calculations
  • Orchestration of repositories
  • Transactions

@Repository​

Role: Marks a data access class

Usage with JPA:

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface UserRepository extends JpaRepository<User, Long> {
// Spring Data JPA automatically generates the implementation
}

Responsibilities:

  • Database queries
  • CRUD operations
  • Custom queries

Advantage: Automatic translation of SQL exceptions into Spring exceptions


HTTP Mapping Annotations​

Overview​

@RequestMapping​

Role: Generic annotation for mapping HTTP requests

Syntax:

@RequestMapping(value = "/path", method = RequestMethod.GET)

Usage at the class level:

@RestController
@RequestMapping("/api/users")
public class UserController {

@RequestMapping(value = "/{id}", method = RequestMethod.GET)
public User getUser(@PathVariable Long id) {
// GET /api/users/123
return userService.findById(id);
}
}

@GetMapping​

Role: Shortcut for GET requests

Equivalent to:

@GetMapping("/path")
=
@RequestMapping(value = "/path", method = RequestMethod.GET)

Examples:

@RestController
@RequestMapping("/api")
public class ProductController {

// GET /api/products
@GetMapping("/products")
public List<Product> getAllProducts() {
return productService.findAll();
}

// GET /api/products/123
@GetMapping("/products/{id}")
public Product getProduct(@PathVariable Long id) {
return productService.findById(id);
}

// GET /api/products?category=books
@GetMapping("/products")
public List<Product> getProductsByCategory(@RequestParam String category) {
return productService.findByCategory(category);
}
}

@PostMapping​

Role: Shortcut for POST requests (creation)

Usage:

@RestController
@RequestMapping("/api/users")
public class UserController {

@PostMapping
public User createUser(@RequestBody User user) {
return userService.save(user);
}
}

Request:

POST /api/users
Content-Type: application/json

{
"name": "John Doe",
"email": "john@example.com"
}

@PutMapping​

Role: Shortcut for PUT requests (full update)

Usage:

@PutMapping("/{id}")
public User updateUser(@PathVariable Long id, @RequestBody User user) {
return userService.update(id, user);
}

Request:

PUT /api/users/123
Content-Type: application/json

{
"name": "John Doe Updated",
"email": "john.updated@example.com"
}

@DeleteMapping​

Role: Shortcut for DELETE requests (deletion)

Usage:

@DeleteMapping("/{id}")
public void deleteUser(@PathVariable Long id) {
userService.delete(id);
}

Request:

DELETE /api/users/123

@PatchMapping​

Role: Shortcut for PATCH requests (partial update)

Usage:

@PatchMapping("/{id}")
public User partialUpdate(@PathVariable Long id, @RequestBody Map<String, Object> updates) {
return userService.partialUpdate(id, updates);
}

Request:

PATCH /api/users/123
Content-Type: application/json

{
"email": "newemail@example.com"
}

Summary Table​

HTTP MethodAnnotationActionIdempotent
GET@GetMappingRead✅
POST@PostMappingCreate❌
PUT@PutMappingUpdate (full)✅
PATCH@PatchMappingUpdate (partial)❌
DELETE@DeleteMappingDelete✅

Parameter Annotations​

@PathVariable​

Role: Extract a value from the URL

Syntax:

@GetMapping("/users/{id}")
public User getUser(@PathVariable Long id) {
// URL: /users/123
// id = 123
return userService.findById(id);
}

Multiple variables:

@GetMapping("/users/{userId}/orders/{orderId}")
public Order getOrder(
@PathVariable Long userId,
@PathVariable Long orderId
) {
return orderService.findByUserAndId(userId, orderId);
}

Different name:

@GetMapping("/users/{id}")
public User getUser(@PathVariable("id") Long userId) {
return userService.findById(userId);
}

@RequestParam​

Role: Extract query string parameters

Syntax:

@GetMapping("/users")
public List<User> getUsers(@RequestParam String name) {
// URL: /users?name=John
// name = "John"
return userService.findByName(name);
}

Optional parameter:

@GetMapping("/users")
public List<User> getUsers(
@RequestParam(required = false) String name
) {
if (name != null) {
return userService.findByName(name);
}
return userService.findAll();
}

Default value:

@GetMapping("/users")
public List<User> getUsers(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size
) {
return userService.findAll(page, size);
}

Multiple parameters:

@GetMapping("/users")
public List<User> searchUsers(
@RequestParam String name,
@RequestParam int age,
@RequestParam String city
) {
// URL: /users?name=John&age=30&city=Montreal
return userService.search(name, age, city);
}

@RequestBody​

Role: Map the HTTP request body to a Java object

Usage:

@PostMapping("/users")
public User createUser(@RequestBody User user) {
return userService.save(user);
}

HTTP Request:

POST /users
Content-Type: application/json

{
"name": "John Doe",
"email": "john@example.com",
"age": 30
}

Received Java Object:

User user = new User();
user.setName("John Doe");
user.setEmail("john@example.com");
user.setAge(30);

@RequestHeader​

Role: Extract an HTTP header

Usage:

@GetMapping("/secure")
public String secureEndpoint(
@RequestHeader("Authorization") String token
) {
// token = "Bearer eyJhbGciOiJIUzI1NiIs..."
return "Authenticated!";
}

Comparison Table​

AnnotationSourceExample URLUsage
@PathVariableURL path/users/123Resource identifiers
@RequestParamQuery string/users?name=JohnFilters, pagination
@RequestBodyHTTP bodyPOST/PUT bodyData submission
@RequestHeaderHTTP headersAuthorization: Bearer tokenAuthentication

Dependency Injection​

@Autowired​

Role: Automatically injects a dependency

Types of Injection​

@RestController
public class UserController {

private final UserService userService;

@Autowired
public UserController(UserService userService) {
this.userService = userService;
}

@GetMapping("/users")
public List<User> getUsers() {
return userService.findAll();
}
}

Since Spring 4.3: @Autowired is optional if a single constructor

@RestController
public class UserController {

private final UserService userService;

// @Autowired not necessary
public UserController(UserService userService) {
this.userService = userService;
}
}

Advantages:

  • Immutability (final)
  • Easier testing
  • Mandatory (no NullPointerException)

2. Setter Injection​

@RestController
public class UserController {

private UserService userService;

@Autowired
public void setUserService(UserService userService) {
this.userService = userService;
}
}

Advantages:

  • Optional dependencies
  • Possible reconfiguration

Disadvantages:

  • Mutable
  • Risk of NullPointerException
@RestController
public class UserController {

@Autowired
private UserService userService;
}

Disadvantages:

  • Hard to test
  • Strong coupling
  • Cannot create an immutable object

Recommendation: Always use constructor injection

Complete Architecture Example​

// Entity
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String email;
// Getters/Setters
}

// Repository
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
}

// Service
@Service
public class UserService {

private final UserRepository userRepository;

public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}

public List<User> findAll() {
return userRepository.findAll();
}

public User save(User user) {
return userRepository.save(user);
}
}

// Controller
@RestController
@RequestMapping("/api/users")
public class UserController {

private final UserService userService;

public UserController(UserService userService) {
this.userService = userService;
}

@GetMapping
public List<User> getAllUsers() {
return userService.findAll();
}

@PostMapping
public User createUser(@RequestBody User user) {
return userService.save(user);
}
}

Dependency Flow:


Practical Exercises​

Exercise 1: Create a REST Controller​

Objective: Use @RestController and @GetMapping

Instructions:

  1. Create a class HelloController
  2. Annotate with @RestController
  3. Create a method sayHello() that returns "Hello, Spring Boot!"
  4. Annotate with @GetMapping("/hello")
  5. Test with the browser

Exercise 2: Use @PathVariable​

Objective: Extract URL parameters

Instructions:

  1. Create an endpoint /greet/\{name\}
  2. Return "Hello, {name}!"
  3. Test with /greet/John

Exercise 3: Create a Service​

Objective: Separate business logic

Instructions:

  1. Create a class CalculatorService with @Service
  2. Add a method add(int a, int b)
  3. Create a CalculatorController
  4. Inject the service via constructor
  5. Create an endpoint /add?a=5&b=3

Exercise 4: Complete Architecture​

Objective: Create a 3-layer architecture

Instructions:

  1. Create a class Product (model)
  2. Create ProductService with an in-memory list
  3. Create ProductController
  4. Implement:
    • GET /products (all)
    • GET /products/{id}
    • POST /products

Review Questions​

  1. What annotations make up @SpringBootApplication?

  2. What is the difference between @Controller and @RestController?

  3. When to use @Service vs @Component?

  4. What is the difference between @PathVariable and @RequestParam?

  5. Why is constructor injection recommended?

  6. What does the @RequestBody annotation do?

  7. Which HTTP method corresponds to @PostMapping?

  8. What is the purpose of @Repository?


Chapter Summary​

Key Points​

  • @SpringBootApplication combines configuration, auto-configuration, and scanning
  • @RestController = @Controller + @ResponseBody
  • @Service for business logic, @Repository for data access
  • Mapping annotations: @GetMapping, @PostMapping, etc.
  • @PathVariable for URL, @RequestParam for query strings
  • Constructor injection recommended

Best Practices​

  • Use specialized annotations (@Service, @Repository) rather than @Component
  • Constructor injection with final
  • One controller per REST resource
  • Clearly separate responsibilities (Controller → Service → Repository)
  • Name endpoints consistently (REST conventions)

Next Steps​

Congratulations! You have completed Module 2.

In Module 3 (coming soon), you will delve into:

  • Inversion of Control in detail
  • Bean lifecycle
  • Scopes
  • Advanced configuration

Put into Practice​

Before moving to the next module, make sure to:


Back to Module 2