1. ProblemDetail as a unified way of returning error response

Almost all APIs have some way of handling problems and errors. Some minimal level of reporting errors are done using HTTP’s status codes. However, these codes are generic and there is often a need to be more specific. ProblemDetail is a newly added class in the Spring ecosystem that satisfies RFC 7807. It is now a consistent way to represent errors out of the box.

@RestController
class SimpleController {
	@GetMapping("/{num}")
	String moreThanZero(@PathVariable int num) {
		if (num <= 0) {
			throw new IllegalArgumentException("num must be more zero");
		}
		return "hello world";
	}
}

@ControllerAdvice
class SimpleControllerAdvice {
	@ExceptionHandler
	public ProblemDetail handleException(IllegalArgumentException ex) {
		return ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, ex.getMessage());
	}
}

GET http://localhost:8080/hello/0

{
  "type": "about:blank",
  "title": "Bad Request",
  "status": 400,
  "detail": "num must be more zero",
  "instance": "/0"
}

2. Observation interface consolidates tracing and metrics

Micrometer is the metrics collection facility included in Spring Boot’s Actuator. Micrometer now supports both tracing and metrics through a unified API called Observation. The Observation APIs in micrometer consolidates both tracing and metrics so they can be used to talk to tracing servers like Zipkin or time-series platforms like Grafana, etc.

@RestController
@RequiredArgsConstructor
class SimpleController {

	private final ObservationRegistry registry;

	@GetMapping("/{num}")
	String moreThanZero(@PathVariable int num) {
		if (num <= 0) {
			throw new IllegalArgumentException("num must be more zero");
		}
		return Observation.createNotStarted("more.than.zero", registry)
				.observe(() -> "hello world");
	}
}

GET http://localhost:8080/actuator/metrics/more.than.zero

{
  "name": "more.than.zero",
  "description": null,
  "baseUnit": "seconds",
  "measurements": [
    {
      "statistic": "COUNT",
      "value": 2.0
    },
    {
      "statistic": "TOTAL_TIME",
      "value": 2.70708E-4
    },
    {
      "statistic": "MAX",
      "value": 1.94458E-4
    }
  ],
  "availableTags": [
    {
      "tag": "error",
      "values": [
        "none"
      ]
    }
  ]
}

3. Declarative Web Client

Similar to Spring Cloud OpenFeign, Spring Web comes with a declarative client. Hopefully by the time Spring Boot 3 is released, the configuration required to get things going will be simpler.


interface SimpleClient {
    @GetExchange("/hello/{num}")
    String hello(@PathVariable int num);
}

@SpringBootApplication
@Slf4j
public class SpringBoot3DemoApplication {

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

    @Bean
    HttpServiceProxyFactory httpServiceProxyFactory(WebClient.Builder builder) {
        var webClient = builder.baseUrl("http://localhost:8080").build();
        return HttpServiceProxyFactory.builder()
                .clientAdapter(WebClientAdapter.forClient(webClient))
                .build();
    }

    @Bean
    SimpleClient simpleClient(HttpServiceProxyFactory httpServiceProxyFactory) {
        return httpServiceProxyFactory.createClient(SimpleClient.class);
    }

    @Bean
    ApplicationListener<ApplicationReadyEvent> ready(SimpleClient simpleClient) {
        return args -> log.info(simpleClient.hello(42));
    }
}