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 |
|
More on 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
@HttpExchangeto 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 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, |
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
To learn working with Spring’s native declarative HTTP clients
During this assignment you will create a declarative HTTP client for an existing REST service
-
The code above
-
You create an
@HttpExchangeinterface for one of the APIs above -
Register it with
@ImportHttpServices -
And call it from a controller or service
-
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.
3.8. Further reading
Spring Framework HTTP Interface Clients |
https://docs.spring.io/spring-framework/reference/integration/rest-clients.html#rest-http-interface |
Spring Cloud OpenFeign Reference |
https://docs.spring.io/spring-cloud-openfeign/reference/spring-cloud-openfeign.html |
Intro to Feign - Baeldung |
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 |
|
Lombok features overview |