Spring Boot. Personalizar el json de error 404 y Swagger

En el proyecto en el que estoy trabajando ahora tenemos una API de servicios Rest hecha con Spring Boot y documentada usando Swagger. Existe una clase anotada con @RestControllerAdvice para personalizar la respuesta de error. He observado que cuando sucede un error 404 no se está ejecutando el método correspondiente por lo que, el JSON de error retornado, tiene el formato por defecto de Spring. Para que esto no suceda.

En el fichero application.properties

spring.mvc.throw-exception-if-no-handler-found=true
spring.resources.add-mappings=false

Implementar @RestControllerAdvice

@RestControllerAdvice
public class MyControllerAdvice {

    @ExceptionHandler(NoHandlerFoundException.class)
    public ResponseEntity<?> handleControllerException(HttpServletRequest request,
                                                       NoHandlerFoundException ex) {
        LOGGER.debug("handleControllerException. message: {}", ex.getMessage());
        // Return custom response entity
    }
}

Con esto ya responde con el formato de JSON que esperábamos.

Hacer funcionar Swagger UI

Al añadir la propiedad spring.resources.add-mappings esta solución vemos que deja de funciona Swagger UI. Da este error:

org.springframework.web.HttpMediaTypeNotAcceptableException: Could not find acceptable representation

El problema es que el autoconfigurador mvc de spring mapea recursos por defecto que son los que utiliza Swagger para mostrar su página web.

Finalmente copio el código del autoconfigurador pero hago que en lugar de aplicar a todas las url "/**" aplique sólo a "/swagger-ui.hml" tal y como se puede ver en el siguiente código:

@Configuration
@EnableWebMvc
public class MyWebMvcConfigurerAdapter extends WebMvcConfigurerAdapter {

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/webjars/**")
                .addResourceLocations("classpath:/META-INF/resources/webjars/");

        registry.addResourceHandler("/swagger-ui.html")
                .addResourceLocations(getStaticLocations());
    }

    private String[] getStaticLocations() {
        return new String[]{
                "/",
                "classpath:/META-INF/resources/",
                "classpath:/resources/",
                "classpath:/static/",
                "classpath:/public/"
        };
    }
}

Fuente

Ocultar método soportado por endpoint de terceros. Spring y Swagger.

A veces, queremos ocultar que uno de los métodos soportados por un endpoint que no controlamos no aparezca en Swagger.

No he visto que exista ninguna función para hacer esto.

Este es el Predicate que he implementado.

import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

import com.google.common.base.Predicate;

import springfox.documentation.RequestHandler;

public class IsRequestMethodPredicate implements Predicate<RequestHandler> {

	private final String path;
	private final RequestMethod method;

	public IsRequestMethodPredicate(String path, RequestMethod method) {
		this.path = path;
		this.method = method;
	}

	public static IsRequestMethodPredicate isRequestMethod(String path, RequestMethod method) {
		return new IsRequestMethodPredicate(path, method);
	}

	public static Predicate<RequestHandler> isNotRequestMethod(String path, RequestMethod method) {
		return not(isRequestMethod(path, method));
	}

	@Override
	public boolean apply(RequestHandler input) {
		RequestMapping mapping = AnnotationUtils
				.findAnnotation(input.getHandlerMethod().getMethod(), RequestMapping.class);
		if (mapping != null && mapping.path().length > 0) {
			return hasPath(mapping, path) && hasMethod(mapping, method);
		}
		return false;
	}

	private static boolean hasPath(RequestMapping mapping, String path) {
		for (String mappingPath : mapping.path()) {
			if (mappingPath != null && mappingPath.equals(path)) {
				return true;
			}
		}
		return false;
	}

	private static boolean hasMethod(RequestMapping mapping, RequestMethod method) {
		for (RequestMethod mappingMethod : mapping.method()) {
			if (mappingMethod == method) {
				return true;
			}
		}
		return false;
	}
}

Para utilizarlo:

import static IsRequestMethodPredicate.isNotRequestMethod;

@Bean
public Docket api() {
	return new Docket(DocumentationType.SWAGGER_2)
			.select()
			.apis(isNotRequestMethod("/some/third/party/endpoint", RequestMethod.GET))
			.paths(paths())
			.build();
}

Ocultar endpoints Spring Swagger 2

Es posible que en alguna ocasión no queramos que Swagger genere la documentación de algún endpoint por algún motivo concreto. Estos son los pasos a seguir:

1. Definir una anotación con la que decoraremos todos los métodos de los controladores que se quieran ocultar.

import java.lang.annotation.Target;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface HideApiDocumentation {

}

2. Modificar la configuración de Swagger2 para que ignore los métodos anotados.

import static springfox.documentation.builders.PathSelectors.*;
import static com.google.common.base.Predicates.*;
import static springfox.documentation.builders.RequestHandlerSelectors.withMethodAnnotation;

...
...

@Configuration
@EnableSwagger2
public class SwaggerConfiguration {

	@Bean
	public Docket api() {
		return new Docket(DocumentationType.SWAGGER_2)
			.select()
			.apis(not(withMethodAnnotation(HideApiDocumentation.class)))
			.paths(PathSelectors.any())
			.build();
	}
}

3. Utilizar la anotación en los métodos de los controladores

@RestController
public class SomeController {

	@HideApiDocumentation
	@PostMapping("/")
	public String post(@RequestBody String param) {
		...
	}

}

4. Opcional. Ocultar endpoints de librerías de terceros.

Si queremos ocultar también los enpoints generados por librerías de terceros (p.ej oauth), tendremos que indicar manualmente las rutas (método “paths”).

@Bean
public Docket api() {
	return new Docket(DocumentationType.SWAGGER_2)
		.select()
		.apis(not(withMethodAnnotation(HideApiDocumentation.class)))
		.paths(paths())
		.build();
}

private Predicate<String> paths() {
	return not(or(
	regex("/oauth/token.*"),
	regex("/oauth/revoke.*")));
}

Espero que sea de utilidad.