:::: MENU ::::

Boot, Rest, Security, Swagger y Caché juntos

Introducción

Esta aplicación de tipo Maven está basada en la que se muestra en mi anterior post de Docker. Es una ampliación de la misma así que me basaré en dicho código para comentar de qué manera he añadido las nuevas funcionalidades.

Spring Boot

Puesto que vamos a usar nuevas tecnologías, modificamos el fichero pom.xml añadiendo:

<dependency>
	<groupId>io.springfox</groupId>
	<artifactId>springfox-swagger2</artifactId>
	<version>2.8.0</version>
</dependency>

<dependency>
	<groupId>io.springfox</groupId>
	<artifactId>springfox-swagger-ui</artifactId>
	<version>2.8.0</version>
</dependency>

<dependency>
	<groupId>com.fasterxml.jackson.dataformat</groupId>
	<artifactId>jackson-dataformat-xml</artifactId>
</dependency>

<dependency>
	<groupId>com.fasterxml.jackson.dataformat</groupId>
	<artifactId>jackson-dataformat-csv</artifactId>
</dependency>

<dependency>
	<groupId>net.sf.supercsv</groupId>
	<artifactId>super-csv</artifactId>
	<version>2.4.0</version>
</dependency>
<dependency>
	<groupId>net.sf.supercsv</groupId>
	<artifactId>super-csv-dozer</artifactId>
	<version>2.4.0</version>
</dependency>

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-cache</artifactId>
</dependency>

<dependency>
	<groupId>org.springframework.security</groupId>
	<artifactId>spring-security-web</artifactId>
</dependency>
<dependency>
	<groupId>org.springframework.security</groupId>
	<artifactId>spring-security-config</artifactId>
</dependency>

Rest

Esta nueva aplicación cambia ligeramente el controlador para devolver diferentes tipos de formato de archivo. Esto puede verse en la clase MainController.java, y más concretamente en las siguientes líneas:

	@RequestMapping(value = "/myentity/json", method = RequestMethod.GET, produces = "application/json")
	public ResponseEntity<List<MyEntity>> readAllEntitiesJson() {
		return new ResponseEntity<List<MyEntity>>(myService.readAll(), HttpStatus.OK);
	}

	@RequestMapping(value = "/myentity/xml", method = RequestMethod.GET, produces = "application/xml")
	public ResponseEntity<List<MyEntity>> readAllEntitiesXml() {
		return new ResponseEntity<List<MyEntity>>(myService.readAll(), HttpStatus.OK);
	}

	@RequestMapping(value = "/myentity/csv", method = RequestMethod.GET, produces = "text/csv")
	public void readAllEntitiesCsv(HttpServletResponse response) throws IOException {
		response.setContentType("text/csv");
		String reportName = "CSV_Report_Name.csv";
		response.setHeader("Content-disposition", "attachment;filename=" + reportName);

		List<MyEntity> rows = myService.readAll();

		Iterator<MyEntity> iter = rows.iterator();
		while (iter.hasNext()) {
			MyEntity outputString = (MyEntity) iter.next();
			response.getOutputStream().print(outputString.getMyLong() + ";" + outputString.getMyString() + "\n");
		}
		response.getOutputStream().flush();
	}

Podéis ver como para exportar para CSV es ligeramente más laborioso, mientras que para JSON y XML la librería de Jackson (que la app detecta en el Classpath) transforma el resultado automáticamente en el formato deseado.

Security

Para añadir seguridad al proyecto he creado un componente llamado SecurityConfiguration.java:

package com.luisgomezcaballero.restapidemo.security;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;

@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
	@Bean
	public UserDetailsService userDetailsService() {
		InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
		manager.createUser(User.withUsername("user").password("{noop}password").roles("USER").build());
		return manager;
	}

	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http.authorizeRequests().antMatchers("/swagger-resources/**").permitAll().anyRequest().authenticated().and().httpBasic();
		http.csrf().disable();
	}
}

Con el manager creo un usuario básico (user/password) para que haya alguno con el que autenticarse en la app.

El método de configurar hace que todas las peticiones que se hagan sobre la aplicación tengan que tener un usuario autenticado, salvo para los recursos de Swagger (ahora explicaré esta tecnología) que tienen que ser públicos (lo he programado así intencionadamente).

Swagger

Esta tecnología nos permite hacer una documentación sobre la aplicación que es dinámica: esto es, no hay que editar el código y después un documento externo que tengamos en otro lugar distinto a donde está nuestra app, sino que cada vez que modifiquemos algo, Swagger se encargará de regenerar la documentación.

Creamos la siguiente clase de configuración:

SwaggerConfiguration.java

package com.luisgomezcaballero.restapidemo.configuration;

import java.util.Collections;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

@Configuration
@EnableSwagger2
public class SwaggerConfiguration {
	@Bean
	public Docket docket() {
		return new Docket(DocumentationType.SWAGGER_2).select().apis(RequestHandlerSelectors.any())
				.paths(PathSelectors.any()).build().apiInfo(apiInfo());
	}

	private ApiInfo apiInfo() {
		return new ApiInfo("REST API with Java Spring Boot", "Demonstration of the setup and use of several Java and Spring technologies.", "0.0.1-SNAPSHOT",
				"",
				new Contact("Luis Gómez Caballero", "http://luisgomezcaballero.com", "luis.gomez.caballero@gmail.com"),
				"", "", Collections.emptyList());
	}
}

Y una vez configurada basta con usar anotaciones de Swagger en el código Java, como puede apreciarse en el componente MainController.java

package com.luisgomezcaballero.restapidemo.controller;

import java.io.IOException;
import java.util.Iterator;
import java.util.List;

import javax.servlet.http.HttpServletResponse;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import com.luisgomezcaballero.restapidemo.model.MyEntity;
import com.luisgomezcaballero.restapidemo.service.MyService;

import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;

@RestController
@RequestMapping("/api")
public class MainController {

	@Autowired
	MyService myService;

	@ApiOperation(value = "List all entities (JSON)", nickname = "readAllEntitiesJson")
	@RequestMapping(value = "/myentity/json", method = RequestMethod.GET, produces = "application/json")
	public ResponseEntity<List<MyEntity>> readAllEntitiesJson() {
		return new ResponseEntity<List<MyEntity>>(myService.readAll(), HttpStatus.OK);
	}

	@ApiOperation(value = "List all entities (XML)", nickname = "readAllEntitiesXml")
	@RequestMapping(value = "/myentity/xml", method = RequestMethod.GET, produces = "application/xml")
	public ResponseEntity<List<MyEntity>> readAllEntitiesXml() {
		return new ResponseEntity<List<MyEntity>>(myService.readAll(), HttpStatus.OK);
	}

	@ApiOperation(value = "List all entities (CSV)", nickname = "readAllEntitiesCsv")
	@RequestMapping(value = "/myentity/csv", method = RequestMethod.GET, produces = "text/csv")
	public void readAllEntitiesCsv(HttpServletResponse response) throws IOException {
		response.setContentType("text/csv");
		String reportName = "CSV_Report_Name.csv";
		response.setHeader("Content-disposition", "attachment;filename=" + reportName);

		List<MyEntity> rows = myService.readAll();

		Iterator<MyEntity> iter = rows.iterator();
		while (iter.hasNext()) {
			MyEntity outputString = (MyEntity) iter.next();
			response.getOutputStream().print(outputString.getMyLong() + ";" + outputString.getMyString() + "\n");
		}
		response.getOutputStream().flush();
	}

	@ApiOperation(value = "Read entity (JSON)", nickname = "readEntityByIdJson")
	@ApiImplicitParams({
			@ApiImplicitParam(name = "id", value = "Entity identifier", required = true, dataType = "long", paramType = "query") })
	@RequestMapping(value = "/myentity/json/{id}", method = RequestMethod.GET, produces = "application/json")
	public ResponseEntity<MyEntity> readEntityByIdJson(@PathVariable("id") long id) {
		MyEntity myEntityResult = myService.readById(id);
		return new ResponseEntity<MyEntity>(myEntityResult, HttpStatus.OK);
	}

	@ApiOperation(value = "Read entity (XML)", nickname = "readEntityByIdXml")
	@ApiImplicitParams({
			@ApiImplicitParam(name = "id", value = "Entity identifier", required = true, dataType = "long", paramType = "query") })
	@RequestMapping(value = "/myentity/xml/{id}", method = RequestMethod.GET, produces = "application/xml")
	public ResponseEntity<MyEntity> readEntityByIdXml(@PathVariable("id") long id) {
		MyEntity myEntityResult = myService.readById(id);
		return new ResponseEntity<MyEntity>(myEntityResult, HttpStatus.OK);
	}

	@ApiOperation(value = "Read entity (CSV)", nickname = "readEntityByIdCsv")
	@ApiImplicitParams({
			@ApiImplicitParam(name = "id", value = "Entity identifier", required = true, dataType = "long", paramType = "query") })
	@RequestMapping(value = "/myentity/csv/{id}", method = RequestMethod.GET, produces = "text/csv")
	public void readEntityByIdCsv(HttpServletResponse response, @PathVariable("id") long id) throws IOException {
		response.setContentType("text/csv");
		String reportName = "CSV_Report_Name.csv";
		response.setHeader("Content-disposition", "attachment;filename=" + reportName);

		MyEntity myEntityResult = myService.readById(id);

		response.getOutputStream().print(myEntityResult.getMyLong() + ";" + myEntityResult.getMyString() + "\n");
		response.getOutputStream().flush();
	}

	@ApiOperation(value = "Create entity", nickname = "createEntity")
	@ApiImplicitParams({
			@ApiImplicitParam(name = "myEntity", value = "Entity", required = true, dataType = "MyEntity", paramType = "body") })
	@RequestMapping(value = "/myentity", method = RequestMethod.POST)
	public ResponseEntity<MyEntity> createEntity(@RequestBody MyEntity myEntity,
			@AuthenticationPrincipal final UserDetails userDetails) {
		myService.create(myEntity);
		return new ResponseEntity<MyEntity>(myEntity, HttpStatus.CREATED);
	}

	@ApiOperation(value = "Update entity", nickname = "updateEntity")
	@ApiImplicitParams({
			@ApiImplicitParam(name = "id", value = "Entity identifier", required = true, dataType = "long", paramType = "query"),
			@ApiImplicitParam(name = "myEntity", value = "Entity", required = true, dataType = "MyEntity", paramType = "body") })
	@RequestMapping(value = "/myentity/{id}", method = RequestMethod.PUT)
	public ResponseEntity<MyEntity> updateEntity(@PathVariable("id") long id, @RequestBody MyEntity myEntity) {
		myService.update(id, myEntity);
		return new ResponseEntity<MyEntity>(myEntity, HttpStatus.OK);
	}

	@ApiOperation(value = "Delete entity", nickname = "deleteEntityById")
	@ApiImplicitParams({
			@ApiImplicitParam(name = "id", value = "Entity identifier", required = true, dataType = "long", paramType = "query") })
	@RequestMapping(value = "/myentity/{id}", method = RequestMethod.DELETE)
	public ResponseEntity<MyEntity> deleteEntityById(@PathVariable("id") long id) {
		myService.deleteById(id);
		return new ResponseEntity<MyEntity>(HttpStatus.NO_CONTENT);
	}

	@ApiOperation(value = "Delete all entities", nickname = "deleteAllEntities")
	@RequestMapping(value = "/myentity", method = RequestMethod.DELETE)
	public ResponseEntity<MyEntity> deleteAllEntities() {
		myService.deleteAll();
		return new ResponseEntity<MyEntity>(HttpStatus.NO_CONTENT);

	}
}

Caché

Es interesante usar caché para operaciones que suelen repetirse y que devuelven siempre los mismos resultados. Para probar esta tecnología he usado unos timers que esperan varios segundos, simulando lecturas de base de datos muy lentas.

Para que la aplicación use caché hay que realizar varios pasos:

– Anotar la clase principal con @EnableCaching.
– Anotar cada método que queramos que sea cacheable con @Cacheable, especificando el caché que vamos a usar (en mi caso, “mycache”).

MyServiceImpl.java

package com.luisgomezcaballero.restapidemo.service;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

import com.luisgomezcaballero.restapidemo.model.MyEntity;

@Service
public class MyServiceImpl implements MyService {

	private static final boolean DELAY_ENABLED = true;
	private static final long DELAY_TIME = 3000L;

	private List<MyEntity> myEntityList;

	{
		myEntityList = new ArrayList<MyEntity>();
		myEntityList.add(new MyEntity(1, "Entity1"));
		myEntityList.add(new MyEntity(2, "Entity2"));
		myEntityList.add(new MyEntity(3, "Entity3"));
	}

	@Cacheable("mycache")
	@Override
	public List<MyEntity> readAll() {
		testCache();
		return myEntityList;
	}

	@Cacheable("mycache")
	@Override
	public MyEntity readById(long id) {
		testCache();
		for (MyEntity myEntity : myEntityList) {
			if (myEntity.getMyLong() == id) {
				return myEntity;
			}
		}
		return null;
	}

	@Override
	public MyEntity create(MyEntity myEntity) {
		myEntityList.add(myEntity);
		return myEntity;
	}

	@Override
	public MyEntity update(long id, MyEntity myEntity) {
		for (MyEntity myEntityToModify : myEntityList) {
			if (myEntityToModify.getMyLong() == id) {
				int index = myEntityList.indexOf(myEntityToModify);
				myEntityList.set(index, myEntity);
				break;
			}
		}
		return myEntity;
	}

	@Override
	public void deleteById(long id) {
		for (Iterator<MyEntity> iterator = myEntityList.iterator(); iterator.hasNext();) {
			MyEntity myEntity = iterator.next();
			if (myEntity.getMyLong() == id) {
				iterator.remove();
			}
		}
	}

	@Override
	public void deleteAll() {
		myEntityList.clear();
	}

	private void testCache() {
		if (DELAY_ENABLED) {
			try {
				long time = DELAY_TIME;
				Thread.sleep(time);
			} catch (InterruptedException e) {
				throw new IllegalStateException(e);
			}
		}
	}

}

Demostración: seguridad

Al desplegar la aplicación e intentamos lanzar cualquier operación, siempre tendremos que autenticarnos como user/password.

Demostración: documentación

Una vez que la aplicación está desplegada, podemos ver la documentación de la misma en la ruta http://localhost:8080/restapidemo/swagger-ui.html.

Demostración: caché

Si desplegamos esta aplicación y ejecutamos una operación cacheada, como puede ser “/myentity/json”, lanzará el código del servicio de “readAll()”, y se ejecutará el Thread.sleep(), con lo que tardará unos segundos en devolver la información.

Pero si ahora volvemos a ejecutar la misma operación, veremos cómo ahora los resultados se devuelven instantáneamente, ya que detecta que están cacheados.

Repositorio

El código de este proyecto puede encontrarse en https://github.com/luisgomezcaballero/restapidemo.


So, what do you think ?