8 jun 2023

Java Streams - Parte I



Hace ya bastante tiempo que JDK 8 está en la calle, pero muchas veces surgen dudas de las nuevas funcionalidades que podemos encontrarnos en cada nueva versión.

Sobre todo para aquellos desarrolladores y compañeros de profesión que solo ven nuevas versiones, como nuevas opciones que hacen funcionar las cosas por arte de magia, pero nada más lejos de la realidad.

Por tanto, dentro de este artículo vamos a analizar la funcionalidad del Stream API en Java 8. También veremos cómo crear y utilizar streams en acción. Y además la diferencia de rendimiento que se produce con su uso, algo que cuando hablamos en un mundo gobernado por millones de datos, se hace más que interesante.

Vamos!

Entorno

Para la realización del ejercicio se ha utilizado el siguiente entorno:

- Hardware: Portatil MSI Prestige 15 (Intel Core 11th Gen 4 CPU 3 GHz, 32 GB Ram)

- SO: Windows 11 Pro

- Entorno de desarollo: VSCode

- Wsl2 con Ubuntu 20.04.6 LTS (Focal Fossa)

- Java 17

- Maven 3.9.x

¿Qué son los Streams?

Como hemos comentado el API de Stream en Java se agregó en JDK 8 para proporcionar un enfoque funcional para procesar una colección de objetos. El Stream de Java no almacena datos y no es una estructura de datos. Además, el origen de datos subyacente no se modifica.

El Stream de Java utiliza interfaces funcionales y admite operaciones en estilo funcional en flujos de elementos mediante el uso de expresiones lambda.

Los Streams de Java 8 son un envoltorío alrededor de un origen de datos (Array, List, etc.) que nos permite operar con la fuente de datos y realizar un procesamiento en bloque de manera rápida y conveniente.

Es un flujo de datos que procesa los datos desde la fuente original y envía los datos procesados a la fuente de datos de destino.

Definición de la API


Configuración del entorno

Creamos un proyecto maven.

Para este objetivo sencillo usamos el propio arquetipo que nos proporciona Maven, descrito aqui:

Maven 5 min


Gist Maven




Para facilitarnos la vida a la hora de ejecutarlo, vamos a instalar el plugin de codehaus.mojo y Lombok.




De esta forma podremos ejecutarlo:
$ mvn exec:java 



Manos la obra

Para no cansarnos antes de empezar, vamos a crear un Stream rápido y así podemos ver de qué estamos hablando.



Como podemos ver es algo sencillo, no deja de ser una colección de objetos. No almacenan info, la reserva de memoria se hace en el Array strArr, y las operaciones que ejecutes sobre el Stream no afectarán al origen de los datos.

Espera, ¿Operaciones? De qué estamos hablando ....

Operaciones


Las operaciones que tenemos para trabajar con Streams, las vamos a dividir en 2:
  1. Intermediate operations. Aquellas que devuelven un new Stream. Son operaciones que son enviadas a la siguiente operacion normalmente. Ejemplos:
    • filter()
    • map()
    • flatMap()
    • distinct()
    • sorted()
    • peek()
    • limit()
    • skip()
  2. Terminal operations. Aquellas que NO devuelven un new Stream. Son operaciones que una vez que las llamamos, el Stream se consume y por tanto no pueden ser enviadas a la siguiente operacion. Ejemplos:
    1. toArray()
    2. collect()
    3. count()
    4. reduce()
    5. forEach()
    6. forEachOrdered()
    7. min()
    8. max()
    9. anyMatch()
    10. allMatch()
    11. noneMatch()
    12. findAny()
    13. findFirst()

Si lo vemos por código:

Creamos un Service para que nos devuelva 2 tipos de Streams diferentes:



Y ahora algunas operaciones para ver como se comporta:


Rendimiento


Hasta ahora nada nuevo que no hiciéramos antes con unas cuantas líneas más de código. Pero no creo que la gente que evoluciona un JDK piense solo en ahorrarnos 2 líneas de código, que también, sino que esto realmente ¿Será más óptimo?

Vamos a verlo

En un pensamiento clásico, podríamos creer que para filtrar un elemento, vamos a recorrer toda la lista, y desde ahí hacemos el filtrado y búsqueda.



Podemos ver, que usando PEEK, como operación que veíamos anteriormente, esto se ejecuta UNA sola vez. ¿Cómooooo? ¿No hemos recorrido la lista N veces? pues no, porque la primera iteración ya me cumple la condición.


Resumen


En una forma clásica, habríamos ejecutado la operación N veces. Al principio es difícil de entender pero básicamente los Streams diseñan un flujo de trabajo que se ejecuta de forma unitaria item a item, así que cuando cumplo la condición, simplemente paro, el flujo termina.

Como hemos podido repasar, las mejoras en las diferentes versiones, sean JDKs, Frameworks, etc ... debemos repasarlas, pero no por que sea más "moderno" sino porque traen mejoras, corrigen errores, mejoran el rendimiento etc.

Os dejo el repo en GitHub por si queréis ampliarlo:


Read More

17 oct 2021

Gestiona tu tiempo y vive!



Sí vive! porque hoy en día, lo creamos o no, con el mundo hiperconectado, la alta competencia que tenemos, y demasiado sobrecargado de responsabilidades, o te gestionas o mueres, y lo peor, te puedes perder lo mejor de tu vida, de tus hijos, de tu salud. 

Por eso en este post voy a hablaros de cómo me organizo yo, padre de dos niños maravillosos, pero niños al fin y al cabo, con un trabajo apasionante, una familia, una casa, y un montón de responsabilidades.

No es una entrada que quiera que te tomes como un dogma, sino simplemente un montón de años de experiencia, acumulados en una carrera universitaria, no pocas empresas, y montones de proyectos.

Origen

Pues os voy a ser sincero, tampoco era la persona más ordenada del mundo, para que vamos a engañarnos. En el instituto y en la carrera, pues ya sabes, estudiar el día de antes, y como mucho llevar al día todo lo que se podía.

Tuve compañeros que sí que es verdad que llenaban hojas y hojas de agendas, con las cosas que tenían que hacer, mientras que yo me organizaba con mi sencilla carpeta clasificadora, en la que eso sí, cuidaba cada día de poner en orden y repasaba el fin de semana.

Ahí es quizá donde mi mente empezó a darse cuenta, que había que hacer una pausa cada X tiempo, el que tu decidas, pero que ese momento es solo para ordenarte mentalmente, y vaciar tu cerebro de esas tareas que no te aportan.

Universidad

Como digo, no fui muy ordenado nunca, pero el primer año de universidad me puso en mi sitio rápidamente. Me las dieron todas en el mismo sitio, clases, mañana y tarde, prácticas, exámenes, etc. Conclusión: muy bajo rendimiento y poca productividad, nivel de estrés por las nubes, desesperación, desanimo, etc.

Algo tenía que hacer, pero con apenas 18 años ¿Qué quieres? suficiente que llegaba a tiempo a clase.

Pero en los primeros años, y luego hasta que acabé, me enseñaron sobre todo a sobrellevar una carga tremenda, no menos de 10 o 15 asignaturas, talleres, prácticas, trabajos, y claro, eres joven y hay que salir y disfrutar, e incluso me animé a trabajar mientras estudiaba.

Para poder superar esa carga inicial, me basé en 2 conceptos claros:

1. Mirar temporalmente a lo lejos (Trimestres) con objetivos claros (Exámenes) y decisiones claras para acometerlos (presentarse a todo era una locura), priorizar.

2. Una vez he decidido lo que quería hacer, era cuestión de organizarse y marcar 3 puntos claves:

a. Ratos muertos, clases que te saltas etc. No las desperdicies, si estaba cansado dormía, si necesitaba repasar a la biblioteca, si estaba leyendo un libro apasionante, lo hacía, pero no desperdiciaba el tiempo.

b. Era concienzudo y práctico. Si decía que eran 6 asignaturas las que me sacaba, a por ellas! Las demás no es que no las mirara, iba a clase y eso, pero mis objetivos eran claros, no generaba desperdicios futuros.

c. Repaso diario, semanal y mensual. Es lo más importante. Debía ajustarme a lo marcado, pero si no lo repasas, pierdes el rumbo. Debes estar atento siempre, porque ese repaso, es algo que te lleva muy poco tiempo para revisar un apunte que se te olvidó y tienes que pedir, una práctica que debías entregar, o un examen que debes preparar en alguna tutoría.

Nivel Profesional

Cuando llegas al trabajo, ah amigo! eso sí que es sufrir, ahí si que te las vas a llevar todas en el mismo sitio.

Los comienzos siempre son difíciles, y aunque creas que vas preparado, quieres y necesitas demostrar. Y eso te lleva a un nivel de estrés, que quizá con la edad mejoras, pero en esos primeros años es tremendo. No te das cuenta que en ese momento no puedes igualar años de experiencia de gente y compañeros tuyos.

Para esos años, me gusta mucho el libro de Isra García: Ultraproductividad: Trabajar menos, producir más, vivir mejor.



Destacaría sobre todo los trucos que Isra nos da para por ejemplo saber decir NO, para "desconectar" (rrss, mail, móvil, etc) y saber concentrarte.

Los primeros años de trabajo sobre todo es para que te formes, para que moldees tu carrera profesional (no significa que luego no puedas cambiarla, yo lo he hecho no pocas veces).

Cuando avanzas en tu carrera profesional, normalmente la personal suele crecer también, novia, novio, perro, gato, coche, casa, responsabilidades al fin y al cabo, sin contar que igual todavía no te han llegado los niños ;), o sí.

En esos momentos, ya no tienes tareas únicas, ya te empiezan a dar responsabilidad, dentro y fuera del trabajo. Dentro, pues empiezas con la multitarea, los multiproyectos y la cabeza en mil sitios. En mi caso tuve 2 puntos de apoyo:

1. Una herramienta de TODO List. Ahora hay miles, elije la que mejor se adapte a tus necesidades, pero apunta todo lo que tengas que hacer, porque sino, lo olvidarás.

Alguna que me ha resultado útil a mi es Microsoft TODO. No es que sea un fan de los chicos de Redmond y me atraigan especialmente, pero tengo que reconocer que su paquete ofimático es inmejorable.

Si a eso le sumas un buen gestor de correo, lo tienes dominado.

2. Pomodoro. Esta técnica es increíblemente útil para esos momentos en los que te entran las notificaciones de mil sitios, te interrumpen cientos de veces más, y tienes mil cosas más que hacer. Si la aplicas bien, no solo terminarás tareas completas, sino que empezarás a tener el control de estas.


Puede que la vida te lleve más allá, tengas críos, el trabajo se complique, te saquen de tu zona de confort, te despidan y tengas que reinventarte, te caigas y tengas que levantarte, no una sino mil veces. Ahí te quiero ver! 

Pues sí, es cuando necesitas ayuda de verdad, pero resulta que el sol sale todos los días, y uno de esos días, es cuando te decides y cambias.

¡Cómo? me dirás, pues yo tuve que leer varios libros, y asistir a no pocos seminarios, escuchar a no pocos sabios, y al final lo que os puedo dejar y espero que os ayude:

  1. Aprende a decir NO. Ya os lo comenté anteriormente con el libro de Isra García, ni podemos llegar a todo ni nos pueden exigir hacerlo, solo tenemos 2 manos, hoy mañana y pasado.
  2. Reserva tareas completas de trabajo, bien en pomodoros o como quieras, pero estar concentrado en ese momento es lo que te permite terminar tareas y no dejarlas a medias.
  3. No te líes con las herramientas, yo uso OneNote, Outlook y Calendar, y ya! Me sobra todo lo demás.
  4. Como diría Francisco Rábano (LinkedIn), mantén una lista de tareas ordenadas, repásala y límpiala.
  5. Aprende antes de lanzarte, no quieras correr antes de andar, te evitará dar muchas vueltas innecesarias. ¿Haces una tarta sin receta? normalmente no, el mero hecho de tenerlo apuntado y aprender de otro, libera tu cerebro de estresarse con algo que ya tiene escrito por otro que sabe el resultado.
  6. Sé meticuloso con tu orden, contigo mismo, sí contigo mismo, si tu estás sano, pensarás mejor, así que empieza por ti mismo, ya habrá tiempo para los demás.
  7. No tengas miedo de reconocer tus limitaciones, y solicitar ayuda, estudia, lee, compra material, asiste a seminarios y cursos, lo agradecerás.

Os contaré una anécdota, yo hasta no hace mucho había mejorado mi productividad un 100% con webinars, seminarios, charlas, cursos y libros como el método Fase 

pero no fue hasta que llegué a una entrevista, en la que por cierto me rechazaron, pero yo siempre procuro sacar lo positivo de todo, ya sea un rechazo, un despido, lo que sea, pues en ese caso, me recomendó el método GTD. Ni idea le dije a esa persona, pero nunca de dejo de dudar de todo, y seguir aprendiendo, es otro nivel más, y ya os puedo asegurar que sí, Organízate con eficacia de David Allen, me parece de las mejores obras que puedas conseguir:



Te aconsejo que lo compres porque son de esos libros que gustan tener.


Resumen

Al fina nuestro cerebro desde que somos seres humanos, está preparado para saber actuar en miles de situaciones, pero no significa que lo saturemos. Lo importante es saber liberar esa carga, y para eso, las listas, las notas, y el repaso periódico de las mismas, hará que te liberes.

¿Lo demás? Como dice David Allen, todo el mundo sin darse cuenta aplica una planificación natural:

  1. Principios
  2. Visualizar el resultado
  3. Lluvia de ideas
  4. Ordenación
  5. Tareas


Espero no haber sido demasiado pesado 😉, y si te he podido ayudar, para mi más que suficiente.

Sed felices! 😘


Read More

8 jun 2020

TDD Spring Boot (I)

Contenido

Parte I

  • Proyecto Maven SPBoot
  • Configuracion POM.XML
    • Mockito
    • JaCoCo
  • Elegir Junit4 VS Junit5
  • Simple Example
    • TDD Controller
    • TDD Service
    • TDD Repository
    • TDD Controller segunda aproximación

Parte II

  • Junit5 en profundidad
  • Diferentes anotaciones para TDD en SpringBoot
    • @SpringBootTest - SpringBootTest loads complete application and injects all the beans which is can be slow
    • @WebMvcTest - for testing the controller layer
    • @JsonTest - for testing the JSON marshalling and unmarshalling
    • @DataJpaTest - for testing the repository layer
    • @RestClientTests - for testing REST clients
  • Mockito en profundidad
  • References

Proyecto Maven SPBoot

Creamos un proyecto nuevo y nos aseguramos que tenga la dependencia:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>

Por defecto ya la incluye.

Configuraciones POM.XML

Vamos a necesitar 2 herramientas para poder realizar los TESTs, la primera es para generar los mocks y se llama Mockito, y la segunda es para medir el coverage o cobertura, y es JaCoCo.

JaCoCo

<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.7.7.201606060606</version>
<executions>
<execution>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>report</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
</executions>
</plugin>
La configuracion más importante es decirle que haga los tests y al mismo tiempo genere el informe de coverage:
<phase>test</phase>
Se encargaría de esto.
Cuando ejecutemos
$ mvn test
tendremos en la carpeta:
target/site/jacoco/index.html
El archivo con el informe de cobertura.

Mockito

Es el framework que usaremos para realizar los mocks necesarios.
* Al incorporar las dependencias de Junit5 en SpringBoot deberíamos tenerlas incorporadas. (revisar este punto)

Elegir Junit4 VS Junit5

Aunque Junit5 ya lleva varios años, la mayor parte de la documentación está para Junit4.
Las pples cosas a tener en cuenta para aplicar Junit5
POM.XML
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</exclusion>
</exclusions>
</dependency>
Añadimos las dependencias de Jupiter (Junit5 para diferenciarlo de Junit4)
Debemos EXCLUIR la versión anterior que viene con Spring de Junit4.

Los tests también se arrancan de otra forma, las pples diferencias a tener en cuenta serían:
  1. La anotación @RunWith al Test ahora es reemplazada por @ExtendWith, y hay una nueva clase de soporte para Spring.
  2. Las anotaciones @Test ahora están en un paquete diferente.
  3. Los asserts ahora están en otro paquete, y usan un api diferente.
  4. Las excepciones ahora se tratan directamente dentro del cuerpo del método de Test (y no con un atributo en la anotación).
  5. Las anotaciones @Before y @After ahora son @BeforeEach y @AfterEach

Esta sería la forma de arrancar un Test con Junit5
@SpringBootTest
@ExtendWith(SpringExtension.class)

Simple Example

¿Como sería una programación TDD con SpringBoot?
El enfoque es sencillo, deberíamos empezar con los Tests. Para ello ¿Cómo realizaríamos nuestro planteamiento? Imaginemos que tenemos el siguiente esquema

CONTROLLER

Lo primero es generar el ENDPOINT para lo cual vamos a crear nuestro Test que permita tener ese ENDPOINT.
@WebMvcTest
@ExtendWith(SpringExtension.class)
public class HelloControllerTest {
}

Como vemos usamos @WebMvcTest y necesitamos dos cosas más, algo que nos permita hacer la llamada al endpoint y un MOCK que nos responda como el Service de la siguiente capa. Como queremos hacer test unitarios y que cada capa se testee de manera independiente, generamos ese mock.   
public class HelloControllerTest {
@Autowired
MockMvc mockMvc;
@MockBean
private HelloService helloSvc;

Llegados a este punto vemos que no compila. Lógicamente nos falta ese HelloService   que estamos inyectando.
Vamos a crearlo:

@Service
public class HelloServiceImpl implements HelloService {
}

Ahora ya sí, no hay problema de compilación.

Nuestro ENDPOINT solo debe saludarnos, por lo que vamos a ello:
@Test
void getHello() throws Exception {
when(helloSvc.get()).thenReturn("Hello JUnit 5");
mockMvc.perform(MockMvcRequestBuilders.get("/hello").contentType(MediaType.ALL))
.andDo(MockMvcResultHandlers.print())
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.content().string("Hello JUnit 5"));
}

Pero no compila, vemos el código:
helloSvc.get()
Ese método no existe, así necesitamos crearlo.
@Override
public String get() {
return "";
}

Ahora sí, ya compila, pero el Test ha fallado. O quizá es eso lo que debiera hacer para ahora empezar a generar código poco a poco y que vaya siendo testeable.

SERVICE

Vamos a la siguiente capa.
Queremos testear el servicio, que debería traerse de la Base de Datos un objeto, en nuestro caso por simplicidad hemos optado por un String, y posteriormente devolverlo.
Vamos a ello
@Service
public class HelloServiceImpl implements HelloService {

@Autowired
HelloRepository helloRepository;

@Override
public String get() {
return helloRepository.get();
}

}


Otra vez error de compilación. Vemos el código y se queja de que no tenemos HelloRepository.

Por tanto vamos a crearlo
@Repository
public class HelloRepositoryImpl implements HelloRepository {
@Override
public String get() {
return "Hello JUnit 5";
}
}


Con esto ya tenemos las piezas necesarias, vemos que según vamos haciendo el Test nos va solicitando lo que necesitamos, y por tanto vamos haciendo un código más robusto.

Service Test

@SpringBootTest
public class HelloServiceTest {
@Mock
private HelloRepository helloRepository;

@InjectMocks // auto inject helloRepository
private HelloService helloService = new HelloServiceImpl();

@BeforeEach
void setMockOutput() {
when(helloRepository.get()).thenReturn("Hello Mockito From Responsitory");
}

@DisplayName("Test Mock helloService + helloRepository")
@Test
void testGet() {
assertEquals("Hello Mockito From Responsitory", helloService.get());
}
}
Analicemos el código:
@Mock
private HelloRepository helloRepository;

Esta parte ya la entendemos, estamos creando un MOCK del Repository para evitar la llamada al mismo y poder testear cada capa.
Pero de nada sirve si en la siguiente capa que lo necesita no se lo inyectamos, ¿como?
@InjectMocks // auto inject helloRepository
private HelloService helloService = new HelloServiceImpl();

Por ultimo para cada petición vamos a simular la respuesta:
    @BeforeEach
    void setMockOutput() {
        when(helloRepository.get()).thenReturn("Hello Mockito From Responsitory");
    }

Así pues cuando ejecutes
assertEquals("Hello Mockito From Responsitory", helloService.get());

Realmente estás llamando al mock autoinyectado anteriormente.

TDD Controller segunda aproximación

En este ultimo caso tendríamos un aproximación más orientada a un test de integración. Ya que estaríamos haciendo una llamada que invocaría todas las capas, si nos quisiéramos quedar en la primera simplemente es hacer un mock del Service.
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
public class MainControllerTest {
// bind the above RANDOM_PORT
@LocalServerPort
private int port;
@Autowired
private TestRestTemplate restTemplate;
@Test
public void getHello() throws Exception {
ResponseEntity<String> response = restTemplate.getForEntity(
new URL("http://localhost:" + port + "/hello").toString(), String.class);
assertEquals("Hello JUnit 5", response.getBody());
}
}

REPOSITORY

La anotación @Repository es la que marca que la class sea descubierta y registrada con el application context. La anotación es de propósito general y se puede aplicar sobre las clases DAO y los repositorios de estilo DDD.
¿Cómo lo vamos a Testear?
Haremos uso de la anotación @DataJpaTest que se centra solamente en los componentes JPA.

Normalmente haremos uso de una base de datos en memoria H2, para realizar dichos tests.

Esta parte profundizaremos en una segunda parte de este mismo manual.

Referencias

Este manual es una recopilación de otros, los cuales sin animo de plagio, están aquí recogidos:

Read More

27 jun 2019

Securizando APIs (II) [Roles]: SpringBoot + Security + JWT + Auth en Base de datos

Introduccion


Vamos a continuar/ampliar el anterior post que os dejo aqui.
En la anterior entrada vimos la manera, de todas las posibles soluciones que puedes tener con Spring Security, de poder tener una arquitectura de microservicios, haciendo que uno de esos microservicios sea unod e Authentication, mediante una conexión en BD y posterior validación de las peticiones REST que se generen a través de un filter que haga precisamente eso, un filtro que extraiga la cabecera, recupere el token y lo valide.
Ahora ampliamos esa primera entrega, con la introdución a la autorización (¿A qué tenemos persmiso para acceder?) y lo resolvemos con los ROLES.
Como siempre os dejo el acceso al código de la aplicación desde mi GitHub.

Algo de teoría


Aunque no es mi fuerte, y no me gusta, pero algo hay que tener claro antes de empezar.
Spring Security nos permite proteger nuestros endpoints con las etiquetas @PreAuthorize y @Secured indicando los roles que tienen acceso.
Podéis suponer que no son lo mismo, aunque comparten cosas:

  • @PreAuthorize es una anotación más nueva que @Secured también es más flexible.
  • @PreAuthorize soporta Spring Expression Language (SpEL) pudiendo utilizar expresiones como hasRole, permitAl. 
    • Con @Secured solo podemos referencia los ROLES permitidos.
  • @Secured cuando se tienen varios roles en la lista que le pasemos se va a comportar como un OR. Con @PreAuthorize, admite expresiones (pudiendo usar AND, OR, NOT). Ejemplo:
    • @Secured({«ROLE_ADMIN», «ROLE_USER»}) Acceso a todos usuarios con rol admin O user.
    • @PreAuthorize(«hasRole(‘ROLE_ADMIN’) AND hasRole(‘ROLE_USER’)») Acceso a todos usuarios con rol admin Y user.
    • @Secured({«ROLE_ADMIN»}) y @PreAuthorize(«hasRole(‘ROLE_ADMIN’)») son equivalentes.


A codificar!

Controller securizado


Vamos a añadir un controller (a modo de prueba) para saber cómo se debe securizar con lo explicado antes.

/**
 * UserController
 */
@RestController
public class UserController {
    
    @PreAuthorize("hasRole('ROLE_USER') OR hasRole('ROLE_ADMIN')")
    @GetMapping("/userSecured")
    public String listUsersSecured() {

        return "listUsersSecured User or Admin";

    }

    @PreAuthorize("hasRole('ROLE_ADMIN')")
    @GetMapping("/userSecuredAdmin")
    public String listUsersSecuredAdmin() {

        return "listUsersSecured Admin";

    }

    @GetMapping("/users")
    public String listUsers() {

        return "listUsers";

    }

    @PreAuthorize("hasRole('ROLE_SVC')")
    @GetMapping("/userService")
    public String listUsersService() {

        return "listUsersService";

    }

}


Podemos ver 3 Endpoints, uno dice que puedes acceder si tienes el ROLE_USER o ROLE_ADMIN. El segundo solo para ROLE_ADMIN y el ultimo es para una supuesta llamada interna entre microservicios ROLE_SVC.

Cambios en el modelo


Ahora vamos a tener que hacer algunos cambios para que la tabla USER y una futura tabla ROLES las relacionemos. Esa relacion es N a N, porque un usuario podrá tener N roles, pero esos roles pueden estar presentes en N usuarios.
Por lo que nos sale una tercera tabla para relacionarlo, pero por suerte eso lo hace spring por nosotros.
* No voy a profundizar en JPA de cómo sería hacerlo a bajo nivel.


ROLES


@Entity
public class Role {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;

    @Column
    private String name;

    @Column
    private String description;

    public long getId() {
        return id;
    }

    public void setId(long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    public Role(String name, String description) {
        this.name = name;
        this.description = description;
    }

    public Role() {
    }
}


USERS
@Entity
public class Usuario {
    @Id
    @GeneratedValue
    private long id;
    public String username;
    public String password;

    @ManyToMany(fetch = FetchType.EAGER, cascade = { CascadeType.MERGE, CascadeType.REFRESH })
    @JoinTable(name = "USER_ROLES", joinColumns = { @JoinColumn(name = "USER_ID") }, inverseJoinColumns = {
            @JoinColumn(name = "ROLE_ID") })
    private Set roles;

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    @Override
    public String toString() {
        return "Usuario [id=" + id + ", password=" + password + ", username=" + username + "]";
    }

    public Usuario() {
    }

    public long getId() {
        return id;
    }

    public void setId(long id) {
        this.id = id;
    }

    public Usuario(String username, String password) {
        this.username = username;
        this.password = password;
    }

    public Set getRoles() {
        return roles;
    }

    public void setRoles(Set roles) {
        this.roles = roles;
    }

    public Usuario(String username, String password, Set roles) {
        this.username = username;
        this.password = password;
        this.roles = roles;
    }

}


Podemos ver el cambio desde la tabla de usuario, para poder obtener de un usuario los ROLES asociados que tiene, que es lo que verdaderamente nos importa.
Luego por ejemplo para dar de alta un usuario haremos algo del estilo


   Role role1 = new Role("ROLE_ADMIN", "Admin role");
   roleRepo.save(role1);
   Role role2 = new Role("ROLE_USER", "User role");
   roleRepo.save(role2);
   Role role3 = new Role("ROLE_SVC", "Service role");
   roleRepo.save(role3);

   Set roles = new HashSet<>();

   roles.add(role2);
   Usuario usuario1 = new Usuario("usuario", bCryptPasswordEncoder.encode("usuario"), roles); // Solo user
   userRepo.save(usuario1);

   roles.add(role1);
   Usuario usuario2 = new Usuario("admin", bCryptPasswordEncoder.encode("admin"),roles); // User & Admin
   userRepo.save(usuario2);

   roles.clear();
   roles.add(role3);
   Usuario usuario3 = new Usuario("service", bCryptPasswordEncoder.encode("service"),roles);
   userRepo.save(usuario3);


Configuramos


La parte de la configuración es sencilla, realmente la podríamos haber tenido activada sin que por ello la aplicación no fuese 100% funcional.

@Configuration
// @EnableGlobalMethodSecurity(securedEnabled = true) // Este es el método 1 de
// securización a usar: @Secured
@EnableGlobalMethodSecurity(prePostEnabled = true) // Este es el método 2 de securización a usar: @PreAuthorize
public class SecurityConfig extends WebSecurityConfigurerAdapter {

.....


}


Modificamos el Token


Para que con el token viaje el ROL debemos hacer un pequeño cambio. Aquí la variedad de soluciones es amplia, pero vamos a modificar la interfaz de UserDetails, para una vez obtenido el usuario de la BBDD podamos añadir la información correspondiente a los permisos.

Vamos a cambiar esta parte:

@Override
 public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
  Usuario usuario = usuarioRepository.findByUsername(username);
  if (usuario == null) {
   throw new UsernameNotFoundException(username);
  }
  return UserDetailsMapper.build(usuario);
  }


Vemos que aparece un UserDetailsMapper.
/**
 * UserDetailsMapper
 */
public class UserDetailsMapper {
    public static UserDetails build(Usuario user) {
        return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(),
                getAuthorities(user));
    }

    private static Set getAuthorities(Usuario retrievedUser) {
        Set roles = retrievedUser.getRoles();
        Set authorities = new HashSet<>();
        roles.forEach(role -> authorities.add(new SimpleGrantedAuthority( role.getName())));
        return authorities;
    }
}


Como vemos la parte importante está en :
return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(),getAuthorities(user));

Ese ultimo parámetro antes lo mandabamos vacío, porque todos tenían el mismo ROL, y ahora ya no, por lo que debemos sacar la lista de ROLES.


¿Y qué añadimos al TOKEN?


Pues tampoco tanto.
 @Override
 protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
   Authentication auth) throws IOException, ServletException {

  final String authorities = auth.getAuthorities().stream().map(GrantedAuthority::getAuthority)
    .collect(Collectors.joining(","));

  String token = Jwts.builder()
  .setIssuedAt(new Date())
  .setIssuer(SecurityConstants.ISSUER_INFO)
  .setSubject(((User) auth.getPrincipal()).getUsername())
    .claim(SecurityConstants.AUTHORITIES_KEY, authorities)
  .setExpiration(new Date(System.currentTimeMillis() + SecurityConstants.TOKEN_EXPIRATION_TIME))
  .signWith(SignatureAlgorithm.HS512, SecurityConstants.SUPER_SECRET_KEY)
  .compact();

  response.addHeader(SecurityConstants.HEADER_AUTHORIZACION_KEY,
    SecurityConstants.TOKEN_BEARER_PREFIX + " " + token);

  response.setContentType("application/json");
  response.setCharacterEncoding("UTF-8");
  response.getWriter().write("{\"" + SecurityConstants.TOKEN_PREFIX + "\":\"" + token + "\"}");
 }

Vemos como hemos añadido la parte de  .claim(SecurityConstants.AUTHORITIES_KEY, authorities)
Con la authorities que ahora sí, las hemos obtenido desde nuestro usuario.


¿Y para validarlo?


recordamos que teníamos otro filtro, que hacía eso precisamente. Cuando nos llegue un TOKEN, lo "ABRIMOS", buscamos la parte donde viaja el ROL y a security debemos pasarle los authorities. Pues añadimos esta parte:

   if (user != null) {

    final Collection authorities = Arrays
      .stream(claims.get(SecurityConstants.AUTHORITIES_KEY).toString().split(",")).map(SimpleGrantedAuthority::new)
      .collect(Collectors.toList());
    return new UsernamePasswordAuthenticationToken(user, "", authorities);
   }




Resumen

Como hemos visto, lo que le interesa a Security para poder authorizar son los authorities (permisos) que tiene ese asignados esa petición de recurso y que hemos decidido que viaje en el token.
Se podría por ejemplo que viaje solo el username, por ejemplo, se valida el token y si es válido, en otro servicio se le podrían buscar los permisos y que no viaje en el token...

Las configuraciones son variadas. Aquí solo expongo una de las tantas formas que me parece más clara para poder tenerlo en una arquitectura de microservicios.


Espero que sirva de ayuda y aclare alguna que otra duda.

Read More

20 jun 2019

Securizando APIs: SpringBoot + Security + JWT + Auth en Base de datos

1. Introducción

Dentro de la cantidad de "sobreinformación" que existe actualmente, voy a intentar dar una píldoras resumidas de los pasos a seguir para securizar vuestra API creada con SringBoot.
Vamos a usar JSON Web Tokens (JWT), activaremos Spring Security con la configuración adecuada, y vamos a securizarlo usando una Base de datos H2 como ejemplo de uso de JPA (A modo de ejemplo debería bastar)
Las configuraciones en detalle de cada uno de las piezas, las dejo a discreción del lector en profundizar en ellas.

Os dejo el código fuente de este tutorial en github para vuestra consulta.

2. JWT

Cogiendo la definición de la wikipedia, no vamos a repetirla, destacar solamente:
La mayor ventaja de los tokens sobre otros sistemas es el hecho de que no tenga estado (stateless). El backend no necesita mantener un registro de los tokens. Cada token es compacto y auto contenido. Contiene todos los datos necesarios para comprobar su validez, así como la información del usuario para las diferentes peticiones.

3. Vamos a por el código

La estructura del proyecto lo he dividido en la parte que pega al modelo (la cual está asociada con JPA a un ejemplo con H2 y se debería cambiar por el sistema que tenga cada uno) y la otra parte de Security que es la que sería común a cualquier microservicio, incluídos aquellos que tengan que hacer uso del JWT cuando quedan expuestos.

En la parte de Security no vamos a hacer uso de ningún controller, realmente no nos hace falta, si no que a través de los Filters que nos proporciona el propio framework de Spring y de Security podemos ser capaces de  tener las piezas necesarias para devolver y analizar un JWT.

Por ultimo tenemos una serie de ficheros de configuración (appication.properties, SecurityConfig y SecurityConstants)

4. A programar!


4.1 Autenticación

Lo primero que vamos a analizar es la Authenticación (Respondemos a la pregunta de ¿quién? ).
public class JWTAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

 private AuthenticationManager authenticationManager;

 public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {
  this.authenticationManager = authenticationManager;

  // Sin esta parte la url que atendería sería la de /login
  setFilterProcessesUrl(SecurityConstants.SIGNIN_URL);

 }

 @Override
 public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
   throws AuthenticationException {
  try {
   Usuario credenciales = new ObjectMapper().readValue(request.getInputStream(), Usuario.class);

   return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(
     credenciales.getUsername(), credenciales.getPassword(), new ArrayList<>()));
  } catch (IOException e) {
   throw new RuntimeException(e);
  }
 }

 @Override
 protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
   Authentication auth) throws IOException, ServletException {

  String token = Jwts.builder().setIssuedAt(new Date()).setIssuer(SecurityConstants.ISSUER_INFO)
    .setSubject(((User) auth.getPrincipal()).getUsername()).claim("roles", "user") 
    .setExpiration(new Date(System.currentTimeMillis() + SecurityConstants.TOKEN_EXPIRATION_TIME))
    .signWith(SignatureAlgorithm.HS512, SecurityConstants.SUPER_SECRET_KEY).compact();

  response.addHeader(SecurityConstants.HEADER_AUTHORIZACION_KEY,
    SecurityConstants.TOKEN_BEARER_PREFIX + " " + token);

  response.setContentType("application/json");
  response.setCharacterEncoding("UTF-8");
  response.getWriter().write("{\"" + SecurityConstants.TOKEN_PREFIX + "\":\"" + token + "\"}");
 }
}



Vamos a resaltar las siguientes partes:
// Sin esta parte la url que atendería sería la de /login
setFilterProcessesUrl(SecurityConstants.SIGNIN_URL);

Por otro lado nos fijamos que en el proceso de authenticate le pasamos como queremos hacer la autenticación:
new UsernamePasswordAuthenticationToken()

Y en caso de tener éxito generamos el token. Este se devolverá en la cabecera y además en el body (no es necesario pero es más cómodo).

4.2 Autorización

Ahora vamos a responder ¿Para qué tengo permiso?.
public class JWTAuthorizationFilter extends BasicAuthenticationFilter {

 public JWTAuthorizationFilter(AuthenticationManager authManager) {
  super(authManager);
 }

 @Override
 protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain)
   throws IOException, ServletException {
  String header = req.getHeader(SecurityConstants.HEADER_AUTHORIZACION_KEY);
  if (header == null || !header.startsWith(SecurityConstants.TOKEN_BEARER_PREFIX)) {
   chain.doFilter(req, res);
   return;
  }
  UsernamePasswordAuthenticationToken authentication = getAuthentication(req);
  SecurityContextHolder.getContext().setAuthentication(authentication);
  chain.doFilter(req, res);
 }

 private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
  String token = request.getHeader(SecurityConstants.HEADER_AUTHORIZACION_KEY);
  if (token != null) {
   // Se procesa el token y se recupera el usuario.
   String user = Jwts.parser().setSigningKey(SecurityConstants.SUPER_SECRET_KEY)
     .parseClaimsJws(token.replace(SecurityConstants.TOKEN_BEARER_PREFIX, "")).getBody().getSubject();

   if (user != null) {
    return new UsernamePasswordAuthenticationToken(user, null, new ArrayList<>());
   }
   return null;
  }
  return null;
 }
}


*Este filtro podría cambiarse por OnePerRequest, o por un Generic, etc ...

Como vemos no es muy complejo de entender, simplemente buscamos en la cabecera el token. Si lo encontramos lo validamos, y si todo es correcto devolvemos UsernamePasswordAuthenticationToken(), pero en este caso sin la PASS porque ya no nos hace falta.

4.3 La "magia" de Spring

Una de las bondades de Spring es que muchas cosas ya las da intrínsecamente el propio framework. Es algo que está muy bien, aunque en mi caso me suele poner de los nervios y prefiero tener el control, pero que en la mayoría de los caso nos ahorra mucho trabajo.

@Service
public class UsuarioDetailsServiceImpl implements UserDetailsService {

 private UsuarioRespository usuarioRepository;

 public UsuarioDetailsServiceImpl(UsuarioRespository usuarioRepository) {
  this.usuarioRepository = usuarioRepository;
 }

 @Override
 public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
  Usuario usuario = usuarioRepository.findByUsername(username);
  if (usuario == null) {
   throw new UsernameNotFoundException(username);
  }
  return new User(usuario.getUsername(), usuario.getPassword(), emptyList());
 }
}


El mero hecho de implementar esta Interfaz, le da a Security la capacidad de saber cómo debe hacer para recuperar un usuario de la Base de Datos.

4.4 Configuración


El último paso es la parte de configuración de los filtros y de security. La que aquí pongo es muy elemental y se debe se bastante más exquisito en las cuestiones a añadir.
@Configuration
// @EnableGlobalMethodSecurity(securedEnabled = true) // Este es el método 1 de
// securización a usar: @Secured
@EnableGlobalMethodSecurity(prePostEnabled = true) // Este es el método 2 de securización a usar: @PreAuthorize
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    protected UserDetailsService userDetailsService;

    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        http.cors().and().csrf().disable().authorizeRequests().antMatchers("/h2-console/**").permitAll().anyRequest()
                .authenticated().and().addFilter(new JWTAuthenticationFilter(authenticationManager()))
                .addFilter(new JWTAuthorizationFilter(authenticationManager())).sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS);

        http.headers().frameOptions().disable();

    }

    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        // Se define la clase que recupera los usuarios y el algoritmo para procesar las
        // passwords
        // Se podría usar un método custom en vez de bCryptPasswordEncoder()

        auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
    }

    @Bean
    CorsConfigurationSource corsConfigurationSource() {
        final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", new CorsConfiguration().applyPermitDefaultValues());
        return source;
    }

}



Como podemos ver, se ha usado un  método para desencriptar la contraseña (cada implementación debería sobreescribir este método y adaptarlo a sus necesidades.

Y por ultimo enlazamos "la magia":
        auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());


5. Resumiendo


No he pretendido ser muy exquisito con todo el código aquí explicado, pero sí dejar "subrayado" aquellas partes que en otros lenguajes o frameworks (como Node) es más fácil de ver y quizá en Spring está algo más oculto.



Read More

9 jun 2019

Microservicios y Swagger (I)

Microservicios hoy


Todos tenemos claro que las aplicaciones monolíticas, como las habíamos conocido hasta ahora, digamos que ... "han cambiado".
Este tipo de aplicaciones son (fueron) buenas soluciones en determinados momentos, pero cuando estas han querido evolucionar, escalar, se han encontrado ante lo que los que llevamos ya tiempo en el sector, hemos ido sufriendo: codigo spaguetti, falta de documentación, mala organización del codigo, falta de aplicar patrones, refactorización difícil, alto acoplamiento, rendimiento que se degrada en cada versión, etc.
No voy a detallar las bondades que seguramente todos conocemos de las arquitecturas orientadas a microservicios, y tampoco creo ser la persona más entendida para hacerlo, pero sí que todos entendemos que este tipo de arquitecturas nacieron para mejorar precisamente el tener esas aplicaciones monolíticas infumables, que parece no querer morir nunca y que cada nuevo desarrollo se intenta pegar como un chicle pegajoso para seguir engordando la bola.
Pero amigo, los microservicios no son la panacea, si no ponemos un poco de orden, no será muy diferente la situación.

SWAGGER

Resultado de imagen de swagger
Swagger nos ayuda un poco al respecto, como dicen en su web: "Diseñe, describa y documente su API en el primer editor de código abierto completamente dedicado a las API basadas en OpenAPI".
Y eso es lo importante, tener un contrato con el que la otra parte, la que consume, sepa que esperar. Esto es muy cool, porque ambas partes (quien genera el servicio y quien lo consume - Front y Back por ejemplo) pueden desarrollar en paralelo siempre que  se respete ese contrato.

Swagger se puede integrar con las API REST de las siguientes maneras:
  1. Un enfoque de arriba hacia abajo: primero la especificación de API y luego generación de código 
  2. Un enfoque sencillo: primero el código de la API y luego la integración Swagger. Esto es bastante familiar y sobre todo útil cuando ya hay una API REST existente incorporada y la documentación de Swagger debe integrarse.

Ejemplo practico (caso 2)


Tienes el código en mi github


Configuracion del pom.xml



    <properties>
        <java.version>1.8</java.version>
        <springfox.version>2.4.0</springfox.version>
    </properties>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>${springfox.version}</version>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>${springfox.version}</version>
        </dependency>



Configuración de Swagger

Con la anotacion @EnableSwagger2 en nuestra aplicacion SpringBoot ya deberíamos tener todo lo necesario.

@SpringBootApplication
@EnableSwagger2
public class Application {

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

}

Si navegamos a la url de nuestro microservicio deberíamos poder verlo:
http://localhost:9000/swagger-ui.html


Configuracion ad-hoc de Swagger

Podemos crear una configuracion particular, porque no queramos tener todo lo que nos añade swagger de la manera más genérica.
Para ello debemos añadir cada "feature" a la configuracion.


package com.mrknight.jpaexample.topic.config;

import static springfox.documentation.builders.PathSelectors.regex;

import com.google.common.base.Predicate;

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

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

@EnableSwagger2
@Configuration
public class SwaggerConfiguration {

/**
* Publish a bean to generate swagger2 endpoints
*
* @return a swagger configuration bean
*/
@Bean
public Docket topicsApi() {
return new Docket(DocumentationType.SWAGGER_2).apiInfo(topicsApiInfo()).select().paths(topicPaths())
.apis(RequestHandlerSelectors.any()).build().useDefaultResponseMessages(false);
}

/**
* Api info
*
* @return ApiInfo
*/
private ApiInfo topicsApiInfo() {
return new ApiInfoBuilder().title("Service Topic").version("2.0").license("Apache License Version 2.0").build();
}

/**
* Config paths.
*
* @return the predicate
*/
private Predicate<String> topicPaths() {
return regex("/topic.*");
}
}


Configuracion del modelo


Modificamos el modelo para añadir la configuracion.
Añadimos las anotaciones  @ApiModel y @ApiModelProperty.

package com.mrknight.jpaexample.topic;

import javax.persistence.Entity;
import javax.persistence.Id;

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;

@Entity
@ApiModel("Model Topic")
public class Topic {

@Id
@ApiModelProperty(value = "the topic's id", required = true)
private String id;
@ApiModelProperty(value = "the topic's name", required = false)
private String name;
@ApiModelProperty(value = "the topic's descr", required = false)
private String descr;

public String getId() {
return id;
}

public void setId(String id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public String getDescr() {
return descr;
}

public void setDescr(String descr) {
this.descr = descr;
}

@Override
public String toString() {
return "Topic [descr=" + descr + ", id=" + id + ", name=" + name + "]";
}

public Topic(String id, String name, String descr) {
this.id = id;
this.name = name;
this.descr = descr;
}

public Topic() {

}

}


Configuracion del Controller


Por ultimo añadimos la config del controller.
Tenemos las anotaciones @Api y @ApiOperation


package com.mrknight.jpaexample.topic;

import java.util.List;
import java.util.Optional;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;

@RestController
@Api(value = "Topics microservice", description = "This API has a CRUD for topics")
public class TopicController {

@Autowired
private TopicService topicsvc;

@GetMapping("topics")
@ApiOperation(value = "Find all topics", notes = "Return a list")
public List<Topic> getAllTopics() {
return topicsvc.getAllTopics();
}

@PostMapping("topics")
@ApiOperation(value = "Save a topic", notes = "Topic included in body")
public void addTopics(@RequestBody Topic topic) {
topicsvc.addTopic(topic);

}

@GetMapping("topics/{id}")
@ApiOperation(value = "Find a topic by Id", notes = "Return topic")
public Optional<Topic> getTopic(@PathVariable("id") String id) {
return topicsvc.getTopic(id);

}

@PutMapping("topics/{id}")
@ApiOperation(value = "Update topic by Id", notes = "Update topic in body")
public void updateTopic(@PathVariable("id") String id, @RequestBody Topic t) {
topicsvc.updateTopic(t, id);
}

@DeleteMapping("topics/{id}")
@ApiOperation(value = "Delete topic by Id", notes = "Del topic by id")
public void delTopic(@PathVariable("id") String id) {
topicsvc.deleteTopic(id);
}
}



Visualizando


navegamos a la url anterior:
http://localhost:9000/swagger-ui.html

Y ahora vemos como ha cambiado ;) El primer controller sería el que nops genera por defecto y el segundo el que hemos generado nosotros.



Hasta aquí la primera parte de como hacer funcionar vuestra API con swagger.
¿Qué sería lo siguiente? Usar las bondades de swagger para autogenerar ese contrato y que ese código podamos importarlo en nuestro proyecto.


Read More