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.
1. Arquitectura del Proyecto
Section titled “1. Arquitectura del Proyecto”El proyecto está formado por los siguientes componentes:
| Componente | Puerto | Tecnología |
|---|---|---|
| Config Server | 8888 | Spring Cloud Config |
| Manager Users | 8081 | JDBC puro |
| Manager Students | 8082 | JPA + Spring Data |
2. Docker Compose
Section titled “2. Docker Compose”Se utilizan dos contenedores PostgreSQL, uno para cada microservicio:
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:2.1. Base de datos de usuarios
Section titled “2.1. Base de datos de usuarios”Se crea un script SQL para inicilizar la base de datos
de usuarios con una tabla sampleusers y algunos registros de ejemplo:
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);2.2. Base de datos de estudiantes
Section titled “2.2. Base de datos de estudiantes”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:
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';3. Config Server
Section titled “3. Config Server”El servidor de configuración centraliza las propiedades de todos los microservicios:
3.1. Aplicación principal
Section titled “3.1. Aplicación principal”Habilitamos el servidor de configuración con la anotación @EnableConfigServer en la clase principal:
package es.ual.config_server;
import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.cloud.config.server.EnableConfigServer;
@SpringBootApplication@EnableConfigServerpublic class ConfigServerApplication {
public static void main(String[] args) { SpringApplication.run(ConfigServerApplication.class, args); }
}3.2. Configuración
Section titled “3.2. Configuración”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:
spring.application.name=config-serverserver.port=8888
# Git repository (GitHub)spring.cloud.config.server.git.uri=https://github.com/aek676/dwsc-2026.gitspring.cloud.config.server.git.default-label=mainspring.cloud.config.server.git.search-paths=actividad6/config-repo3.3. Propiedades externas
Section titled “3.3. Propiedades externas”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:
# Configuration for manager-usersserver.port=8081
# Database configuration (JDBC hardcoded in service, but can be overridden)spring.datasource.url=jdbc:postgresql://localhost:5432/dwsc_usersspring.datasource.username=estudiantespring.datasource.password=estudiantespring.datasource.driver-class-name=org.postgresql.Driver
# OpenAPI Infospringdoc.api-info.title=Manager Users APIspringdoc.api-info.description=API for managing usersspringdoc.api-info.version=1.0.0springdoc.api-info.contact.name=UAL Teamspringdoc.api-info.contact.email=anass@ual.esLa 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:
# Configuration for manager-studentsserver.port=8082
# Database configuration (JPA)spring.datasource.url=jdbc:postgresql://localhost:5433/dwsc_studentsspring.datasource.username=estudiantespring.datasource.password=estudiantespring.datasource.driver-class-name=org.postgresql.Driver
spring.jpa.hibernate.ddl-auto=update
# OpenAPI Infospringdoc.api-info.title=Manager Students APIspringdoc.api-info.description=API for managing students (JPA)springdoc.api-info.version=1.0.0springdoc.api-info.contact.name=UAL Teamspringdoc.api-info.contact.email=anass@ual.es3.4. Endpoints del Config Server
Section titled “3.4. Endpoints del Config Server”| Endpoint | Descripción |
|---|---|
http://localhost:8888/actuator/health | Estado del servidor |
http://localhost:8888/application/default | Configuración por defecto |
http://localhost:8888/manager-users/default | Configuración de manager-users |
http://localhost:8888/manager-students/default | Configuración de manager-students |
4. Manager Users (JDBC)
Section titled “4. Manager Users (JDBC)”Este microservicio utiliza JDBC puro para acceder a la base de datos, sin ORM.
4.1. Dependencias Maven
Section titled “4.1. Dependencias Maven”<?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>4.2. Dominio
Section titled “4.2. Dominio”package es.ual.manager_users.domain;
import lombok.Getter;import lombok.Setter;
@Getter@Setterpublic class User {
private String username; private String password; private String dni; private String name; private String surnames; private int age;}package es.ual.manager_users.domain;
import java.util.ArrayList;
public class Users extends ArrayList<User> {
}4.3. Servicio
Section titled “4.3. Servicio”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);}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;
@Servicepublic 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; }}4.4. Controlador
Section titled “4.4. Controlador”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;
@Controllerpublic 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"; }}4.5. Documentación OpenAPI
Section titled “4.5. Documentación OpenAPI”Documentación añadida a UserController y remplazamos el @Controller por @RestController:
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:
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;
@Configurationpublic 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)); }}4.6. Endpoints
Section titled “4.6. Endpoints”| Método | Endpoint | Descripción |
|---|---|---|
| GET | /users | Obtener todos los usuarios |
| GET | /users/{username} | Obtener usuario por username |
| POST | /users | Crear nuevo usuario |
4.7. Swagger UI
Section titled “4.7. Swagger UI”Entramos a la URL: http://localhost:8081/swagger-ui.html
y probamos una API desde alli
5. Manager Students (JPA)
Section titled “5. Manager Students (JPA)”Este microservicio utiliza JPA con Spring Data para acceder a la base de datos, implementando relaciones ManyToMany entre estudiantes y titulaciones.
5.1. Dependencias Maven
Section titled “5.1. Dependencias Maven”<?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>5.2. Dominio
Section titled “5.2. Dominio”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@Setterpublic 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;
}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@Setterpublic class Degree {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id;
private String code; private String name; private String programme;}5.3. Repositorio
Section titled “5.3. Repositorio”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);}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);}5.4. Controlador
Section titled “5.4. Controlador”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(); }
}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(); }
}5.5. Documentación OpenAPI
Section titled “5.5. Documentación OpenAPI”Documentación añadida a StudentController:
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:
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:
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;
@Configurationpublic 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)); }}5.6. Endpoints
Section titled “5.6. Endpoints”Students
Section titled “Students”| Método | Endpoint | Descripción |
|---|---|---|
| GET | /students | Obtener 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étodo | Endpoint | Descripción |
|---|---|---|
| GET | /degrees | Obtener todas las titulaciones |
| GET | /degrees/{code} | Buscar titulación por código |
| PUT | /degrees/{code} | Actualizar titulación |
| DELETE | /degrees/{code} | Eliminar titulación |
5.7. Swagger UI
Section titled “5.7. Swagger UI”Entramos a la URL: http://localhost:8082/swagger-ui.html
y probamos una API desde alli
6. Ejecución del proyecto
Section titled “6. Ejecución del proyecto”-
Iniciar las bases de datos
Terminal window docker-compose up -d -
Compilar el Config Server
Terminal window cd config-server./mvnw clean package -DskipTests -
Compilar Manager Users
Terminal window cd manager-users./mvnw clean package -DskipTests -
Compilar Manager Students
Terminal window cd manager-students./mvnw clean package -DskipTests -
Ejecutar los servicios
Ejecutar en terminals separadas:
Terminal window # Terminal 1: Config Servercd config-server./mvnw spring-boot:run# Terminal 2: Manager Userscd manager-users./mvnw spring-boot:run# Terminal 3: Manager Studentscd manager-students./mvnw spring-boot:run -
Verificar los servicios
- Config Server:
http://localhost:8888/actuator/health - Manager Users:
http://localhost:8081/swagger-ui.html - Manager Students:
http://localhost:8082/swagger-ui.html
- Config Server:
7. Resumen de puertos
Section titled “7. Resumen de puertos”| Servicio | Puerto |
|---|---|
| Config Server | 8888 |
| Manager Users | 8081 |
| Manager Students | 8082 |
| PostgreSQL Users | 5432 |
| PostgreSQL Students | 5433 |