1. For Testing

1.1. @Spy

The @Spy annotation creates a partial mock. Unlike @Mock (which stubs all methods to return defaults), a spy calls the real methods unless you explicitly stub them. This is useful when you want to test a real object but override one or two specific methods.

@ExtendWith(MockitoExtension.class)
class CarServiceTest {

    @Spy
    private CarService carService;

    @Test
    void spyCallsRealMethodByDefault() {
        // Real method is called
        Car car = carService.createDefaultCar();
        assertNotNull(car);
    }

    @Test
    void spyCanStubSpecificMethods() {
        // Override just one method
        doReturn(Optional.of(new Car())).when(carService).findById(1L);

        Optional<Car> result = carService.findById(1L);
        assertTrue(result.isPresent());
    }
}
Use @Mock when you want to isolate completely. Use @Spy when you need the real behaviour but want to override a few methods.

1.2. @Captor

The @Captor annotation creates an ArgumentCaptor to capture arguments passed to a mocked method. This is useful when you want to verify not just that a method was called, but what it was called with.

@ExtendWith(MockitoExtension.class)
class CarServiceTest {

    @Mock
    private CarRepository carRepository;

    @InjectMocks
    private CarService carService;

    @Captor
    private ArgumentCaptor<Car> carCaptor;

    @Test
    void saveShouldPassCarToRepository() {
        Car car = new Car();
        car.setBrand("BMW");

        carService.save(car);

        verify(carRepository).save(carCaptor.capture());

        Car capturedCar = carCaptor.getValue();
        assertEquals("BMW", capturedCar.getBrand());
    }
}

1.3. Further reading on testing annotations

Mockito Annotations

https://www.baeldung.com/mockito-annotations

More on Mockito

https://www.baeldung.com/tag/mockito/

2. For REST

2.1. @PatchMapping

@PatchMapping is an alias for @RequestMapping(method = RequestMethod.PATCH).

The difference between PUT and PATCH:

  • PUT is meant for replacing the entire resource at a given URL

  • PATCH is meant for a partial update — only the properties included in the request body are updated

@PatchMapping("{id}")
public ResponseEntity<Car> partialUpdate(@PathVariable long id, @RequestBody Map<String, Object> updates) {
    Optional<Car> optionalCar = carService.findById(id);
    if (optionalCar.isPresent()) {
        Car car = optionalCar.get();

        if (updates.containsKey("brand")) {
            car.setBrand((String) updates.get("brand"));
        }
        if (updates.containsKey("mileage")) {
            car.setMileage((Integer) updates.get("mileage"));
        }

        return ResponseEntity.ok(carService.save(car));
    }
    return ResponseEntity.notFound().build();
}

2.2. @ControllerAdvice

The @ControllerAdvice annotation allows you to handle exceptions across the whole application, not just in a single controller. Think of it as a global exception interceptor for all @RequestMapping methods.

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(EntityNotFoundException.class)
    public ResponseEntity<String> handleNotFound(EntityNotFoundException ex) {
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ex.getMessage());
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<Map<String, String>> handleValidationErrors(MethodArgumentNotValidException ex) {
        Map<String, String> errors = new HashMap<>();
        ex.getBindingResult().getFieldErrors().forEach(error ->
            errors.put(error.getField(), error.getDefaultMessage())
        );
        return ResponseEntity.badRequest().body(errors);
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<String> handleGeneral(Exception ex) {
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
            .body("An unexpected error occurred");
    }
}
Use @RestControllerAdvice (instead of @ControllerAdvice) for REST APIs — it combines @ControllerAdvice with @ResponseBody so you can return objects directly.

3. Topic: Declarative HTTP Clients

3.1. Introduction

When your Spring Boot application needs to call another REST API, you need an HTTP client. Spring Boot 4 / Spring Framework 7 introduces native declarative HTTP clients using @HttpExchange, which replace the need for external libraries like Feign.

3.2. What you will learn

  • The evolution of HTTP clients in Spring: RestTemplate → RestClient → Declarative HTTP Clients

  • How to use @HttpExchange to create declarative, type-safe HTTP clients

  • When you might still use Spring Cloud OpenFeign

3.3. The landscape of HTTP clients

Approach Status in Spring Boot 4

RestTemplate

In maintenance mode, heading toward deprecation. Still works but not recommended for new code.

RestClient

The recommended synchronous HTTP client (introduced in Spring 6.1). Fluent API, lightweight.

WebClient

The recommended reactive/async HTTP client. Use when you need non-blocking behaviour.

@HttpExchange (Declarative)

Native Spring alternative to Feign. Define an interface, Spring generates the implementation. Built into spring-web, no extra dependencies needed.

Spring Cloud OpenFeign

Still available via Spring Cloud. Useful in existing Spring Cloud microservice architectures with service discovery (Eureka) and load balancing. But for new projects, @HttpExchange is the preferred approach.

3.4. How: Declarative HTTP Clients with @HttpExchange

No extra dependencies needed — this is built into spring-web.

3.4.1. Step 1: Define the client interface

import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.service.annotation.GetExchange;
import org.springframework.web.service.annotation.HttpExchange;
import org.springframework.web.service.annotation.PostExchange;

import java.util.List;

@HttpExchange("/api/books")
public interface BookClient {

    @GetExchange
    List<Book> findAll();

    @GetExchange("/{isbn}")
    Book findByIsbn(@PathVariable String isbn);

    @PostExchange
    Book create(@RequestBody Book book);
}
The annotations (@GetExchange, @PostExchange, etc.) mirror the controller-side annotations (@GetMapping, @PostMapping). This symmetry is intentional.

3.4.2. Step 2: Register the client

In Spring Boot 4, you can use @ImportHttpServices to auto-configure the client:

import org.springframework.boot.http.client.annotation.ImportHttpServices;
import org.springframework.context.annotation.Configuration;

@Configuration
@ImportHttpServices(
    group = "book-service",
    types = BookClient.class
)
public class HttpClientConfig {
}

Then configure the base URL in application.properties:

spring.http.client.group.book-service.url=http://localhost:8081

3.4.3. Step 3: Use the client

@Service
public class BookService {

    private final BookClient bookClient;

    public BookService(BookClient bookClient) {
        this.bookClient = bookClient;
    }

    public List<Book> getAllBooks() {
        return bookClient.findAll();
    }
}

That’s it — Spring generates the implementation at runtime, backed by RestClient under the hood.

3.5. Assignment: Declarative HTTP Client

Target

To learn working with Spring’s native declarative HTTP clients

Roadmap

During this assignment you will create a declarative HTTP client for an existing REST service

Given
When
  • You create an @HttpExchange interface for one of the APIs above

  • Register it with @ImportHttpServices

  • And call it from a controller or service

Then
  • You see the results from the external API returned through your own application

3.6. Bonus Assignment

  • Add a GET by ID method

  • Add error handling (what happens when the external service returns 404?)

  • Try calling your own CarApp’s REST API from a separate Spring Boot project using @HttpExchange

3.7. Legacy: Feign (for reference)

If you encounter Feign in existing codebases, here is a brief overview. For new projects, prefer @HttpExchange instead.

Spring Cloud OpenFeign uses annotations similar to Spring MVC:

@FeignClient(name = "book-service", url = "http://localhost:8081")
public interface BookClient {

    @GetMapping("/api/books/{isbn}")
    Book findByIsbn(@PathVariable String isbn);

    @GetMapping("/api/books")
    List<Book> findAll();

    @PostMapping("/api/books")
    Book create(@RequestBody Book book);
}

This requires the spring-cloud-starter-openfeign dependency and @EnableFeignClients on a configuration class.

4. Lombok

4.1. @Singular

Used with the @Builder annotation to create a singular way to add entries one at a time to a List (or Set, or Map):

@Builder
public class Person {
    private String givenName;
    private String familyName;

    @Singular
    private List<String> interests;
}

We can now build up the list one entry at a time:

Person person = Person.builder()
    .givenName("Aaron")
    .familyName("Aardvark")
    .interest("history")
    .interest("sport")
    .build();

The generated builder creates an unmodifiable list from the individually added items.

@Singular also works with Set and Map fields. For maps, it generates a singular method that takes a key and value.

4.2. Further reading on Lombok

@Singular documentation

https://projectlombok.org/features/Builder

Lombok features overview

https://projectlombok.org/features/