Skip to content

Actividad 6 - Microservicios con Spring Cloud Config

El objetivo de esta actividad es desarrollar una arquitectura de microservicios utilizando Spring Boot con Spring Cloud Config para la configuración centralizada. Se implementan dos microservicios: uno que utiliza JDBC puro y otro que utiliza JPA con relaciones ManyToMany.

El proyecto está formado por los siguientes componentes:

ComponentePuertoTecnología
Config Server8888Spring Cloud Config
Manager Users8081JDBC puro
Manager Students8082JPA + Spring Data

Se utilizan dos contenedores PostgreSQL, uno para cada microservicio:

docker-compose.yml
services:
postgres-users:
image: postgres:18.3-alpine3.23
environment:
POSTGRES_DB: dwsc_users
POSTGRES_USER: estudiante
POSTGRES_PASSWORD: estudiante
ports:
- "5432:5432"
volumes:
- postgres_users_data:/var/lib/postgresql/data
- ./init-users.sql:/docker-entrypoint-initdb.d/init-users.sql
postgres-students:
image: postgres:18.3-alpine3.23
environment:
POSTGRES_DB: dwsc_students
POSTGRES_USER: estudiante
POSTGRES_PASSWORD: estudiante
ports:
- "5433:5432"
volumes:
- postgres_students_data:/var/lib/postgresql/data
- ./init-students.sql:/docker-entrypoint-initdb.d/init-students.sql
volumes:
postgres_users_data:
postgres_students_data:

Se crea un script SQL para inicilizar la base de datos de usuarios con una tabla sampleusers y algunos registros de ejemplo:

init-users.sql
CREATE TABLE IF NOT EXISTS sampleusers (
username VARCHAR(50) PRIMARY KEY,
password VARCHAR(100) NOT NULL,
dni VARCHAR(20) NOT NULL UNIQUE,
name VARCHAR(100) NOT NULL,
surnames VARCHAR(200) NOT NULL,
age INT NOT NULL
);
INSERT INTO sampleusers (username, password, dni, name, surnames, age) VALUES
('juan', 'juanpass', '12345678D', 'Juan', 'Lopez Garcia', 31),
('jose', 'josepass', '12345678B', 'Jose', 'Perez Rodriguez', 33),
('javi', 'javipass', '12345678A', 'Javier', 'Criado Rodriguez', 35);

Se crear un script SQL para inicilizar la base de datos con una tabla students con una relación ManyToMany con una tabla degrees, y algunos registros de ejemplo:

init-students.sql
CREATE TABLE IF NOT EXISTS public.student (
id bigint NOT NULL GENERATED ALWAYS AS IDENTITY (INCREMENT 1 START 1 MINVALUE 1 MAXVALUE 9223372036854775807 CACHE 1),
name text COLLATE pg_catalog."default",
surnames text COLLATE pg_catalog."default",
dni character varying(255) COLLATE pg_catalog."default",
CONSTRAINT student_pkey PRIMARY KEY (id)
) TABLESPACE pg_default;
ALTER TABLE IF EXISTS public.student OWNER TO estudiante;
CREATE TABLE IF NOT EXISTS public.degree (
id bigint NOT NULL GENERATED ALWAYS AS IDENTITY (INCREMENT 1 START 1 MINVALUE 1 MAXVALUE 9223372036854775807 CACHE 1),
code text COLLATE pg_catalog."default",
name text COLLATE pg_catalog."default",
programme text COLLATE pg_catalog."default",
CONSTRAINT degree_pkey PRIMARY KEY (id)
) TABLESPACE pg_default;
ALTER TABLE IF EXISTS public.degree OWNER TO estudiante;
CREATE TABLE IF NOT EXISTS public.student_degree (
student_id bigint NOT NULL,
degree_id bigint NOT NULL,
CONSTRAINT student_degree_pkey PRIMARY KEY (student_id, degree_id),
CONSTRAINT student_degree_ibfk_1 FOREIGN KEY (student_id) REFERENCES public.student (id) MATCH SIMPLE ON UPDATE NO ACTION ON DELETE NO ACTION,
CONSTRAINT student_degree_ibfk_2 FOREIGN KEY (degree_id) REFERENCES public.degree (id) MATCH SIMPLE ON UPDATE NO ACTION ON DELETE NO ACTION
) TABLESPACE pg_default;
ALTER TABLE IF EXISTS public.student_degree OWNER TO estudiante;
INSERT INTO public.student (dni, name, surnames) VALUES
('12345678V', 'Carlos', 'López Jiménez'),
('12345678G', 'César', 'González García');
INSERT INTO public.degree (code, name, programme) VALUES
('GRAD04015', 'Grado en Ingeniería Informática', 'Plan 2015'),
('MASTER7109', 'Máster en Tecnologías y Aplicaciones en Ingeniería Informática', NULL);
INSERT INTO public.student_degree (student_id, degree_id)
SELECT s.id, d.id FROM public.student s, public.degree d
WHERE s.dni = '12345678V' AND d.code = 'GRAD04015';
INSERT INTO public.student_degree (student_id, degree_id)
SELECT s.id, d.id FROM public.student s, public.degree d
WHERE s.dni = '12345678G' AND d.code = 'MASTER7109';

El servidor de configuración centraliza las propiedades de todos los microservicios:

Habilitamos el servidor de configuración con la anotación @EnableConfigServer en la clase principal:

ConfigServerApplication.java
package es.ual.config_server;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.config.server.EnableConfigServer;
@SpringBootApplication
@EnableConfigServer
public class ConfigServerApplication {
public static void main(String[] args) {
SpringApplication.run(ConfigServerApplication.class, args);
}
}

Para la configuración del Config Server, se utiliza un repositorio Git que contiene las propiedades de cada microservicio, donde se almacenan en el repositorio marcado en la rama main en la carpeta actividad6/config-repo:

application.properties
spring.application.name=config-server
server.port=8888
# Git repository (GitHub)
spring.cloud.config.server.git.uri=https://github.com/aek676/dwsc-2026.git
spring.cloud.config.server.git.default-label=main
spring.cloud.config.server.git.search-paths=actividad6/config-repo

Esta sería la configuración para el microservicio de manager-users donde definimos tanto su puerto, conexión a la base de datos con JDBC y los metadatos para la ducumentación OpenAPI:

manager-users.properties
# Configuration for manager-users
server.port=8081
# Database configuration (JDBC hardcoded in service, but can be overridden)
spring.datasource.url=jdbc:postgresql://localhost:5432/dwsc_users
spring.datasource.username=estudiante
spring.datasource.password=estudiante
spring.datasource.driver-class-name=org.postgresql.Driver
# OpenAPI Info
springdoc.api-info.title=Manager Users API
springdoc.api-info.description=API for managing users
springdoc.api-info.version=1.0.0
springdoc.api-info.contact.name=UAL Team
springdoc.api-info.contact.email=anass@ual.es

La configuración para el microservicio de manager-students, donde definimos su puerto, la conexión a la base de datos con JPA y los metadatos para la ducumentación OpenAPI:

manager-students.properties
# Configuration for manager-students
server.port=8082
# Database configuration (JPA)
spring.datasource.url=jdbc:postgresql://localhost:5433/dwsc_students
spring.datasource.username=estudiante
spring.datasource.password=estudiante
spring.datasource.driver-class-name=org.postgresql.Driver
spring.jpa.hibernate.ddl-auto=update
# OpenAPI Info
springdoc.api-info.title=Manager Students API
springdoc.api-info.description=API for managing students (JPA)
springdoc.api-info.version=1.0.0
springdoc.api-info.contact.name=UAL Team
springdoc.api-info.contact.email=anass@ual.es
EndpointDescripción
http://localhost:8888/actuator/healthEstado del servidor
http://localhost:8888/application/defaultConfiguración por defecto
http://localhost:8888/manager-users/defaultConfiguración de manager-users
http://localhost:8888/manager-students/defaultConfiguración de manager-students

Este microservicio utiliza JDBC puro para acceder a la base de datos, sin ORM.

pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>4.0.5</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>es.ual</groupId>
<artifactId>manager-users</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>manager-users</name>
<description>Demo project for Spring Boot</description>
<url/>
<licenses>
<license/>
</licenses>
<developers>
<developer/>
</developers>
<scm>
<connection/>
<developerConnection/>
<tag/>
<url/>
</scm>
<properties>
<java.version>17</java.version>
<spring-cloud.version>2025.1.1</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc</artifactId>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>3.0.2</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<executions>
<execution>
<id>default-compile</id>
<phase>compile</phase>
<goals>
<goal>compile</goal>
</goals>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</path>
</annotationProcessorPaths>
</configuration>
</execution>
<execution>
<id>default-testCompile</id>
<phase>test-compile</phase>
<goals>
<goal>testCompile</goal>
</goals>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</path>
</annotationProcessorPaths>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
User.java
package es.ual.manager_users.domain;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class User {
private String username;
private String password;
private String dni;
private String name;
private String surnames;
private int age;
}
Users.java
package es.ual.manager_users.domain;
import java.util.ArrayList;
public class Users extends ArrayList<User> {
}
UserService.java
package es.ual.manager_users.service;
import es.ual.manager_users.domain.User;
import es.ual.manager_users.domain.Users;
public interface UserService {
public Users getUsersFromDB();
public User getUserFromDB(String username);
public boolean insertUser(User user);
}
UserServiceImpl.java
package es.ual.manager_users.service;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import org.springframework.stereotype.Service;
import es.ual.manager_users.domain.User;
import es.ual.manager_users.domain.Users;
@Service
public class UserServiceImpl implements UserService {
@Override
public Users getUsersFromDB() {
Users users = new Users();
Connection conn = this.connect2DB();
try {
Statement st = conn.createStatement();
ResultSet rs = st.executeQuery("SELECT * FROM sampleusers ORDER BY age");
while (rs.next()) {
User user = new User();
user.setUsername(rs.getString("username"));
user.setPassword(rs.getString("password"));
user.setDni(rs.getString("dni"));
user.setName(rs.getString("name"));
user.setSurnames(rs.getString("surnames"));
user.setAge(rs.getInt("age"));
users.add(user);
}
rs.close();
st.close();
} catch (Exception e) {
System.err.println("[UserService - getUsersFromDB] SQLException while querying the users");
System.err.println(e.getMessage());
}
return users;
}
@Override
public User getUserFromDB(String username) {
User user = null;
Connection conn = this.connect2DB();
try {
PreparedStatement pst = conn.prepareStatement("SELECT * FROM sampleusers WHERE username = ?");
pst.setString(1, username);
ResultSet rs = pst.executeQuery();
if (rs.next()) {
user = new User();
user.setUsername(rs.getString("username"));
user.setPassword(rs.getString("password"));
user.setDni(rs.getString("dni"));
user.setName(rs.getString("name"));
user.setSurnames(rs.getString("surnames"));
user.setAge(rs.getInt("age"));
}
rs.close();
pst.close();
} catch (Exception e) {
System.err.println("[UserService - getUserFromDB] SQLException while querying user: " + username);
System.err.println(e.getMessage());
}
return user;
}
@Override
public boolean insertUser(User user) {
Connection conn = this.connect2DB();
try {
PreparedStatement checkUsername = conn.prepareStatement("SELECT * FROM sampleusers WHERE username = ?");
checkUsername.setString(1, user.getUsername());
ResultSet rsUsername = checkUsername.executeQuery();
boolean usernameExists = rsUsername.next();
rsUsername.close();
checkUsername.close();
if (usernameExists) {
conn.close();
return false;
}
PreparedStatement checkDni = conn.prepareStatement("SELECT * FROM sampleusers WHERE dni = ?");
checkDni.setString(1, user.getDni());
ResultSet rsDni = checkDni.executeQuery();
boolean dniExists = rsDni.next();
rsDni.close();
checkDni.close();
if (dniExists) {
conn.close();
return false;
}
PreparedStatement insert = conn.prepareStatement("INSERT INTO sampleusers (username, password, dni, name, surnames, age) VALUES (?, ?, ?, ?, ?, ?)");
insert.setString(1, user.getUsername());
insert.setString(2, user.getPassword());
insert.setString(3, user.getDni());
insert.setString(4, user.getName());
insert.setString(5, user.getSurnames());
insert.setInt(6, user.getAge());
insert.executeUpdate();
insert.close();
conn.close();
return true;
} catch (Exception e) {
System.err.println("[UserService - insertUser] Error inserting user: " + user.getUsername());
System.err.println(e.getMessage());
return false;
}
}
private Connection connect2DB() {
Connection conn = null;
try {
Class.forName("org.postgresql.Driver");
String url = "jdbc:postgresql://localhost:5432/dwsc_users";
conn = DriverManager.getConnection(url, "estudiante", "estudiante");
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (SQLException e) {
e.printStackTrace();
}
return conn;
}
}
UserController.java
package es.ual.manager_users.controller;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
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.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import es.ual.manager_users.domain.User;
import es.ual.manager_users.domain.Users;
import es.ual.manager_users.service.UserService;
@Controller
public class UserController {
@Autowired
private UserService userService;
@RequestMapping("/userstable")
public String getUsersTab(Map<String, Users> model) {
Users users = userService.getUsersFromDB();
model.put("users", users);
return "usertemplate";
}
@RequestMapping("/users")
public @ResponseBody Users getUsers() {
return userService.getUsersFromDB();
}
@GetMapping("/users/{username}")
public ResponseEntity<User> getUser(@PathVariable String username) {
User user = userService.getUserFromDB(username);
if (user != null) {
return ResponseEntity.ok(user);
} else {
return ResponseEntity.notFound().build();
}
}
@PostMapping("/users")
public ResponseEntity<Map<String, String>> insertUser(@RequestBody User user) {
boolean inserted = userService.insertUser(user);
if (inserted) {
return ResponseEntity.ok(Map.of("message", "User created successfully"));
} else {
return ResponseEntity.badRequest().body(Map.of("error", "Username or DNI already exists"));
}
}
@GetMapping("/usertable/{username}")
public String getUserHtml(@PathVariable String username, Map<String, User> model) {
User user = userService.getUserFromDB(username);
if (user != null) {
model.put("user", user);
return "userview";
}
return "error";
}
}

Documentación añadida a UserController y remplazamos el @Controller por @RestController:

actividad6/manager-users/src/main/java/es/ual/manager_users/controller/UserController.java
package es.ual.manager_users.controller;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
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.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import es.ual.manager_users.domain.User;
import es.ual.manager_users.domain.Users;
import es.ual.manager_users.service.UserService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
@Controller
@RestController
@RequestMapping("/users")
@Tag(name = "Users", description = "API for managing users")
public class UserController {
@Autowired
private UserService userService;
@RequestMapping("/userstable")
public String getUsersTab(Map<String, Users> model) {
Users users = userService.getUsersFromDB();
model.put("users", users);
return "usertemplate";
}
@RequestMapping("/users")
public @ResponseBody Users getUsers() {
@GetMapping
@Operation(summary = "Get all users", description = "Retrieves all users from the database")
public Users getUsers() {
return userService.getUsersFromDB();
}
@GetMapping("/users/{username}")
@GetMapping("/{username}")
@Operation(summary = "Get user by username", description = "Retrieves a user by username")
public ResponseEntity<User> getUser(@PathVariable String username) {
User user = userService.getUserFromDB(username);
if (user != null) {
return ResponseEntity.ok(user);
} else {
return ResponseEntity.notFound().build();
}
}
@PostMapping("/users")
@PostMapping
@Operation(summary = "Create new user", description = "Creates a new user in the database")
public ResponseEntity<Map<String, String>> insertUser(@RequestBody User user) {
boolean inserted = userService.insertUser(user);
if (inserted) {
return ResponseEntity.ok(Map.of("message", "User created successfully"));
} else {
return ResponseEntity.badRequest().body(Map.of("error", "Username or DNI already exists"));
}
}
@GetMapping("/usertable/{username}")
public String getUserHtml(@PathVariable String username, Map<String, User> model) {
User user = userService.getUserFromDB(username);
if (user != null) {
model.put("user", user);
return "userview";
}
return "error";
}
}

La configuración de OpenAPI se realiza en una clase de configuración donde se definen los metadatos del API que se congieran desde el Config Server:

OpenApiConfig.java
package es.ual.manager_users.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info;
@Configuration
public class OpenApiConfig {
@Value("${springdoc.api-info.title:Manager Users API}")
private String apiTitle;
@Value("${springdoc.api-info.description:API for managing users}")
private String apiDescription;
@Value("${springdoc.api-info.version:1.0.0}")
private String apiVersion;
@Value("${springdoc.api-info.contact.name:UAL Team}")
private String contactName;
@Value("${springdoc.api-info.contact.email:ual@ugr.es}")
private String contactEmail;
@Bean
public Info apiInfo() {
return new Info()
.title(apiTitle)
.description(apiDescription)
.version(apiVersion)
.contact(new Contact()
.name(contactName)
.email(contactEmail));
}
}
MétodoEndpointDescripción
GET/usersObtener todos los usuarios
GET/users/{username}Obtener usuario por username
POST/usersCrear nuevo usuario

Entramos a la URL: http://localhost:8081/swagger-ui.html y probamos una API desde alli

Swagger UI - Manager Users

Este microservicio utiliza JPA con Spring Data para acceder a la base de datos, implementando relaciones ManyToMany entre estudiantes y titulaciones.

pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>4.0.5</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>es.ual</groupId>
<artifactId>manager-students</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>manager-students</name>
<description>Demo project for Spring Boot</description>
<url/>
<licenses>
<license/>
</licenses>
<developers>
<developer/>
</developers>
<scm>
<connection/>
<developerConnection/>
<tag/>
<url/>
</scm>
<properties>
<java.version>17</java.version>
<spring-cloud.version>2025.1.1</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc</artifactId>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>3.0.2</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Student.java
package es.ual.manager_students.domain;
import java.util.Set;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.JoinTable;
import jakarta.persistence.ManyToMany;
import lombok.Getter;
import lombok.Setter;
@Entity
@Getter
@Setter
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String dni;
private String name;
private String surnames;
@ManyToMany
@JoinTable(name = "student_degree", joinColumns = { @JoinColumn(name = "student_id") }, inverseJoinColumns = {
@JoinColumn(name = "degree_id") })
private Set<Degree> degrees;
}
Degree.java
package es.ual.manager_students.domain;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.Getter;
import lombok.Setter;
@Entity
@Getter
@Setter
public class Degree {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String code;
private String name;
private String programme;
}
StudentRepository.java
package es.ual.manager_students.repository;
import org.springframework.data.repository.CrudRepository;
import es.ual.manager_students.domain.Student;
public interface StudentRepository extends CrudRepository<Student, Long> {
Student findByName(String name);
Student findByDni(String dni);
}
DegreeRepository.java
package es.ual.manager_students.repository;
import org.springframework.data.repository.CrudRepository;
import es.ual.manager_students.domain.Degree;
public interface DegreeRepository extends CrudRepository<Degree, Long> {
Degree findByCode(String code);
}
StudentController.java
package es.ual.manager_students.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import es.ual.manager_students.domain.Student;
import es.ual.manager_students.repository.StudentRepository;
@RestController
@RequestMapping("/students")
public class StudentController {
@Autowired
StudentRepository studentRepo;
@GetMapping
public ResponseEntity<Iterable<Student>> getStudents() {
return ResponseEntity.ok(studentRepo.findAll());
}
@GetMapping("/{name}")
public ResponseEntity<Student> getStudentByName(@PathVariable String name) {
Student student = studentRepo.findByName(name);
if (student == null) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok(student);
}
@GetMapping("/dni/{dni}")
public ResponseEntity<Student> getStudentByDni(@PathVariable String dni) {
Student student = studentRepo.findByDni(dni);
if (student == null) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok(student);
}
@PutMapping("/{dni}")
public ResponseEntity<Student> updateStudent(@PathVariable String dni, @RequestBody Student student) {
Student existing = studentRepo.findByDni(dni);
if (existing == null) {
return ResponseEntity.notFound().build();
}
existing.setName(student.getName());
existing.setSurnames(student.getSurnames());
return ResponseEntity.ok(studentRepo.save(existing));
}
@DeleteMapping("/{dni}")
public ResponseEntity<Void> deleteStudent(@PathVariable String dni) {
Student student = studentRepo.findByDni(dni);
if (student == null) {
return ResponseEntity.notFound().build();
}
studentRepo.delete(student);
return ResponseEntity.noContent().build();
}
}
DegreeController.java
package es.ual.manager_students.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
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.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import es.ual.manager_students.domain.Degree;
import es.ual.manager_students.repository.DegreeRepository;
@RestController
@RequestMapping("/degrees")
public class DegreeController {
@Autowired
DegreeRepository degreeRepo;
@GetMapping
public ResponseEntity<Iterable<Degree>> getAllDegrees() {
return ResponseEntity.ok(degreeRepo.findAll());
}
@GetMapping("/{code}")
public ResponseEntity<Degree> getDegreeByCode(@PathVariable String code) {
Degree degree = degreeRepo.findByCode(code);
if (degree == null) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok(degree);
}
@PutMapping("/{code}")
public ResponseEntity<Degree> updateDegree(@PathVariable String code, @RequestBody Degree degree) {
Degree existing = degreeRepo.findByCode(code);
if (existing == null) {
return ResponseEntity.notFound().build();
}
existing.setName(degree.getName());
existing.setProgramme(degree.getProgramme());
return ResponseEntity.ok(degreeRepo.save(existing));
}
@DeleteMapping("/{code}")
public ResponseEntity<Void> deleteDegree(@PathVariable String code) {
Degree degree = degreeRepo.findByCode(code);
if (degree == null) {
return ResponseEntity.notFound().build();
}
degreeRepo.delete(degree);
return ResponseEntity.noContent().build();
}
}

Documentación añadida a StudentController:

actividad6/manager-students/src/main/java/es/ual/manager_students/controller/StudentController.java
package es.ual.manager_students.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
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.DeleteMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import es.ual.manager_students.domain.Student;
import es.ual.manager_students.repository.StudentRepository;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
@RestController
@RequestMapping("/students")
@Tag(name = "Students", description = "API for managing students (JPA)")
public class StudentController {
@Autowired
StudentRepository studentRepo;
@GetMapping
@Operation(summary = "Get all students", description = "Retrieves all students from the database")
public ResponseEntity<Iterable<Student>> getStudents() {
return ResponseEntity.ok(studentRepo.findAll());
}
@GetMapping("/{name}")
@Operation(summary = "Get student by name", description = "Retrieves a student by name")
public ResponseEntity<Student> getStudentByName(@PathVariable String name) {
Student student = studentRepo.findByName(name);
if (student == null) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok(student);
}
@GetMapping("/dni/{dni}")
@Operation(summary = "Get student by DNI", description = "Retrieves a student by DNI")
public ResponseEntity<Student> getStudentByDni(@PathVariable String dni) {
Student student = studentRepo.findByDni(dni);
if (student == null) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok(student);
}
@PutMapping("/{dni}")
@Operation(summary = "Update student", description = "Updates an existing student by DNI")
public ResponseEntity<Student> updateStudent(@PathVariable String dni, @RequestBody Student student) {
Student existing = studentRepo.findByDni(dni);
if (existing == null) {
return ResponseEntity.notFound().build();
}
existing.setName(student.getName());
existing.setSurnames(student.getSurnames());
return ResponseEntity.ok(studentRepo.save(existing));
}
@DeleteMapping("/{dni}")
@Operation(summary = "Delete student", description = "Deletes a student by DNI")
public ResponseEntity<Void> deleteStudent(@PathVariable String dni) {
Student student = studentRepo.findByDni(dni);
if (student == null) {
return ResponseEntity.notFound().build();
}
studentRepo.delete(student);
return ResponseEntity.noContent().build();
}
}

Documentación añadida a DegreeController:

actividad6/manager-students/src/main/java/es/ual/manager_students/controller/DegreeController.java
package es.ual.manager_students.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
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.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import es.ual.manager_students.domain.Degree;
import es.ual.manager_students.repository.DegreeRepository;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
@RestController
@RequestMapping("/degrees")
@Tag(name = "Degrees", description = "API for managing degrees (JPA)")
public class DegreeController {
@Autowired
DegreeRepository degreeRepo;
@GetMapping
@Operation(summary = "Get all degrees", description = "Retrieves all degrees from the database")
public ResponseEntity<Iterable<Degree>> getAllDegrees() {
return ResponseEntity.ok(degreeRepo.findAll());
}
@GetMapping("/{code}")
@Operation(summary = "Get degree by code", description = "Retrieves a degree by code")
public ResponseEntity<Degree> getDegreeByCode(@PathVariable String code) {
Degree degree = degreeRepo.findByCode(code);
if (degree == null) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok(degree);
}
@PutMapping("/{code}")
@Operation(summary = "Update degree", description = "Updates an existing degree by code")
public ResponseEntity<Degree> updateDegree(@PathVariable String code, @RequestBody Degree degree) {
Degree existing = degreeRepo.findByCode(code);
if (existing == null) {
return ResponseEntity.notFound().build();
}
existing.setName(degree.getName());
existing.setProgramme(degree.getProgramme());
return ResponseEntity.ok(degreeRepo.save(existing));
}
@DeleteMapping("/{code}")
@Operation(summary = "Delete degree", description = "Deletes a degree by code")
public ResponseEntity<Void> deleteDegree(@PathVariable String code) {
Degree degree = degreeRepo.findByCode(code);
if (degree == null) {
return ResponseEntity.notFound().build();
}
degreeRepo.delete(degree);
return ResponseEntity.noContent().build();
}
}

La configuración de OpenAPI se realiza en una clase de configuración donde se definen los metadatos del API que se congieran desde el Config Server:

OpenApiConfig.java
package es.ual.manager_students.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info;
@Configuration
public class OpenApiConfig {
@Value("${springdoc.api-info.title:Manager Students API}")
private String apiTitle;
@Value("${springdoc.api-info.description:API for managing students (JPA)}")
private String apiDescription;
@Value("${springdoc.api-info.version:1.0.0}")
private String apiVersion;
@Value("${springdoc.api-info.contact.name:UAL Team}")
private String contactName;
@Value("${springdoc.api-info.contact.email:ual@ugr.es}")
private String contactEmail;
@Bean
public Info apiInfo() {
return new Info()
.title(apiTitle)
.description(apiDescription)
.version(apiVersion)
.contact(new Contact()
.name(contactName)
.email(contactEmail));
}
}
MétodoEndpointDescripción
GET/studentsObtener todos los estudiantes
GET/students/{name}Buscar estudiante por nombre
GET/students/dni/{dni}Buscar estudiante por DNI
PUT/students/{dni}Actualizar estudiante
DELETE/students/{dni}Eliminar estudiante
MétodoEndpointDescripción
GET/degreesObtener todas las titulaciones
GET/degrees/{code}Buscar titulación por código
PUT/degrees/{code}Actualizar titulación
DELETE/degrees/{code}Eliminar titulación

Entramos a la URL: http://localhost:8082/swagger-ui.html y probamos una API desde alli

Swagger UI - Manager Students
  1. Iniciar las bases de datos

    Terminal window
    docker-compose up -d
  2. Compilar el Config Server

    Terminal window
    cd config-server
    ./mvnw clean package -DskipTests
  3. Compilar Manager Users

    Terminal window
    cd manager-users
    ./mvnw clean package -DskipTests
  4. Compilar Manager Students

    Terminal window
    cd manager-students
    ./mvnw clean package -DskipTests
  5. Ejecutar los servicios

    Ejecutar en terminals separadas:

    Terminal window
    # Terminal 1: Config Server
    cd config-server
    ./mvnw spring-boot:run
    # Terminal 2: Manager Users
    cd manager-users
    ./mvnw spring-boot:run
    # Terminal 3: Manager Students
    cd manager-students
    ./mvnw spring-boot:run
  6. Verificar los servicios

ServicioPuerto
Config Server8888
Manager Users8081
Manager Students8082
PostgreSQL Users5432
PostgreSQL Students5433