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

Diferentes configuracion de logback en función del Spring profile

Para una aplicación que estoy desarrollando, cuando arranco el profile “dev” me interesa que la salida del log sea por la cónsola. En un entorno de test (profile “test”) me interesa que almacene el log en un fichero. Es importante que el fichero de configuración se llame src/main/resources/logback-spring.xml.

Ejemplo:

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
	<include resource="org/springframework/boot/logging/logback/base.xml" />
	<springProfile name="dev">
		<logger name="com.sourcerebels" level="debug" additivity="false">
			<appender-ref ref="CONSOLE" />
		</logger>
		<root level="warn">
			<appender-ref ref="CONSOLE" />
		</root>
	</springProfile>
	<springProfile name="test">
		<appender name="FILE" class="ch.qos.logback.core.FileAppender">
			<file>/path/to/log/logfile.log</file>
			<encoder>
				<pattern>%d{dd-MM-yyyy HH:mm:ss.SSS} %magenta([%thread]) %highlight(%-5level) %logger{36}.%M - %msg%n</pattern>
			</encoder>
		</appender>
		<logger name="com.sourcerebels" level="info" additivity="false">
			<appender-ref ref="FILE" />
		</logger>
		<root level="warn">
			<appender-ref ref="FILE" />
		</root>
	</springProfile>
</configuration>

Más info: Logging docs.

Obtener los valores de la JVM referentes al tamaño de Heap

Ejecutar:

java -XX:+PrintFlagsFinal -version | grep -i HeapSize

El resultado será similar a este:

    uintx ErgoHeapSizeLimit                         = 0                                   {product}
    uintx HeapSizePerGCThread                       = 87241520                            {product}
    uintx InitialHeapSize                          := 125829120                           {product}
    uintx LargePageHeapSizeThreshold                = 134217728                           {product}
    uintx MaxHeapSize                              := 2006974464                          {product}
java version "1.8.0_141"
Java(TM) SE Runtime Environment (build 1.8.0_141-b15)
Java HotSpot(TM) 64-Bit Server VM (build 25.141-b15, mixed mode)

Spring Boot – Cifrar passwords en bbdd

Información obtenida del siguiente enlace.

@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
    
    private static final Logger logger = LoggerFactory.getLogger(SecurityConfiguration.class);

    @Autowired
    private UserService userService;
    
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    
    @Bean
    public DaoAuthenticationProvider authProvider() {
        DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
        authProvider.setUserDetailsService(userService);
        authProvider.setPasswordEncoder(passwordEncoder());
        return authProvider;
    }
    
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        if (logger.isDebugEnabled()) {
            logger.debug("SecurityConfiguration.configure");
        }
        auth.authenticationProvider(authProvider());
    }
}

Failed to instantiate Pageable: Specified class is an interface

Al añadir los parámetros de paginación a un controlador de spring. Unos tests que estaba haciendo con MockMvc y Mockito me comenzaron a fallar con el siguiente error:

Caused by: org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.data.domain.Pageable]: Specified class is an interface

La solución consiste en proporcionar una clase que resuelva los argumentos en tiempo de test (setCustomArgumentResolvers). Esta clase solo atiende al parámetro de tipo Pageable.

public class TestUtils {
    public static MockMvcBuilder prepareMockMvcForPageableArguments(Object... controllers) {
        return MockMvcBuilders.standaloneSetup(controllers)
                .setCustomArgumentResolvers(
                        new HandlerMethodArgumentResolver() {
                            @Override
                            public boolean supportsParameter(MethodParameter parameter) {
                                return parameter.getParameterType().equals(Pageable.class);
                            }

                            @Override
                            public Object resolveArgument(MethodParameter parameter,
                                    ModelAndViewContainer container,
                                    NativeWebRequest request,
                                    WebDataBinderFactory binderFactory) throws Exception {
                                return new PageRequest(0, 50);
                            }
                        });

    }
}

Para utilizarlo:

MockMvc mvc = TestUtils.prepareMockMvcForPageableArguments(controller).build();

Fuente:

https://github.com/terasolunaorg/terasoluna-tourreservation/blob/master/terasoluna-tourreservation-web/src/test/java/org/terasoluna/tourreservation/app/searchtour/SearchTourControllerTest.java

Paginación con Spring Data

A modo de recordatorio.

  • El controlador debe recibir un objeto de tipo Pageable
@GetMapping("/myentities")
public Page getMyEntities(final String userUuid, Pageable pageable) {
    return myRepository.findAllByUserUuid(userUuid, pageable);
}
  • Esto significa que a la llamada al controlador se le pueden pasar los parámetros page, limit y sort para controlar la página que queremos recuperar y el orden.
page=número de página a mostrar
limit=número de elementos a mostrar por página
sort=campo por el que se quiere ordenar
  •  Si se quiere personalizar el nombre de estos parámetros, se puede hacer modificando las siguientes propiedades en el fichero application.properties.
spring.data.rest.page-param-name=page
spring.data.rest.limit-param-name=limit
spring.data.rest.sort-param-name=sort
  • El repositorio tiene que extender el interfaz de Spring PagingAndSortingRepository. JpaRepository ya extiende de este.
  • El método findXX del repositorio debe devolver un objeto de tipo Page y recibir un parámetro de tipo Pageable.
@Repository
public interface MyRepository extends JpaRepository&lt;MyEntity, Long&gt; {
    public Page findAllByUserUuid(String userUuid, Pageable pageable);
}

Referencias

https://docs.spring.io/spring-data/rest/docs/1.1.x/reference/html/paging-chapter.html

Saludos

Mockito InOrder y ArgumentCaptor

InOrder

Uso de InOrder permite validar el orden en el que se ejecutan las llamadas a los distintos objetos doble (mock).

@Mock
Repository someRepository;

@Mock
Repository someOtherRepository;

@Test
public void testSomething() {

	InOrder order = Mockito.inOrder(someRepository, someOtherRepository);

	order.verify(someRepository).save(anyString());
	order.verify(someOtherRepository).findByName(anyString());
	order.verify(repository).update(anyString());
	order.verifyNoMoreInteractions();
}

ArgumentCaptor

Uso de ArgumentCaptor permite capturar los argumentos que recibe una llamada a una objeto doble (mock).

@Mock
Repository mockRepository;

@Test
public void testSomething() {

	service.useCase("some_string");
	
	ArgumentCaptor<String> argument = ArgumentCaptor.forClass(String.class);
	verify(mockRepository).findByName(argument.capture());
	assertEquals("some_string", argument.getValue());
}

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.

Recomendación SDKMAN

SDKMAN es una herramienta genial que nos permite gestionar diferentes instalaciones de herramientas relacionadas con el mundo Java (JDK, Maven, Gradle, SpringBoot, …) desde la línea de comandos. Lo que es muy práctico es que permite que convivan, de forma muy fácil, diferentes versiones de estas herramientas.

$ sdk current

Using:

gradle: 3.4.1
grails: 3.2.7
groovy: 2.4.10
kotlin: 1.1.1
maven: 3.3.9
sbt: 0.13.13
scala: 2.12.1
springboot: 1.5.2.RELEASE

Si no lo estáis usando ya, os recomiendo que le echéis un ojo.

Saludos