refactor: replace Spring Boot with Jooby framework

- Remove Spring Boot dependencies and annotations.
- Implement Jooby MVC controllers and Guice dependency injection.
- Migrate persistence layer to Ebean ORM.
- Configure Flyway migrations and ApiErrorController.
- Update application configuration to HOCON format.
This commit was merged in pull request #1.
This commit is contained in:
2026-03-02 16:38:11 +01:00
parent 83070ccbda
commit eb51b221cf
76 changed files with 1162 additions and 943 deletions

View File

@@ -1,12 +1,22 @@
<project>
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.pablotj</groupId>
<artifactId>portfolio-api</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<artifactId>bootstrap</artifactId>
<properties>
<jooby.version>3.6.1</jooby.version>
<postgresql.version>42.7.2</postgresql.version>
<testcontainers.version>1.19.7</testcontainers.version>
</properties>
<dependencies>
<dependency>
<groupId>com.pablotj</groupId>
@@ -14,40 +24,90 @@
<version>${project.version}</version>
</dependency>
<!-- Drivers DB -->
<dependency>
<groupId>io.jooby</groupId>
<artifactId>jooby-netty</artifactId>
<version>${jooby.version}</version>
</dependency>
<dependency>
<groupId>io.jooby</groupId>
<artifactId>jooby-jackson</artifactId>
<version>${jooby.version}</version>
</dependency>
<dependency>
<groupId>io.jooby</groupId>
<artifactId>jooby-jackson</artifactId>
<version>3.6.1</version>
</dependency>
<dependency>
<groupId>io.jooby</groupId>
<artifactId>jooby-hibernate-validator</artifactId>
<version>3.6.1</version>
</dependency>
<dependency>
<groupId>io.jooby</groupId>
<artifactId>jooby-openapi</artifactId>
<version>${jooby.version}</version>
</dependency>
<dependency>
<groupId>io.jooby</groupId>
<artifactId>jooby-swagger-ui</artifactId>
<version>${jooby.version}</version>
</dependency>
<dependency>
<groupId>io.jooby</groupId>
<artifactId>jooby-logback</artifactId>
<version>${jooby.version}</version>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
<version>${postgresql.version}</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
<version>2.2.224</version>
</dependency>
<!-- Swagger/OpenAPI -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>${springdoc.version}</version>
<groupId>io.jooby</groupId>
<artifactId>jooby-hikari</artifactId>
<version>${jooby.version}</version>
</dependency>
<!-- Actuator -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
<groupId>io.jooby</groupId>
<artifactId>jooby-guice</artifactId>
<version>${jooby.version}</version>
</dependency>
<!-- Test -->
<!--
install(new DotenvModule());
<dependency>
<groupId>io.jooby</groupId>
<artifactId>jooby-dotenv</artifactId>
<version>${jooby.version}</version>
</dependency>-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<groupId>io.jooby</groupId>
<artifactId>jooby-flyway</artifactId>
<version>${jooby.version}</version>
</dependency>
<dependency>
<groupId>io.jooby</groupId>
<artifactId>jooby-test</artifactId>
<version>${jooby.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<version>${testcontainers.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
@@ -55,15 +115,77 @@
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<groupId>io.jooby</groupId>
<artifactId>jooby-maven-plugin</artifactId>
<version>${jooby.version}</version>
<executions>
<execution>
<goals>
<goal>repackage</goal>
<goal>openapi</goal> <!-- Este goal genera openapi.json/yaml -->
</goals>
</execution>
</executions>
<configuration>
<mainClass>com.pablotj.portfolio.bootstrap.PortfolioApplication</mainClass>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.5.1</version>
<executions>
<execution>
<id>jooby-shade</id>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<createDependencyReducedPom>false</createDependencyReducedPom>
<filters>
<filter>
<artifact>*:*</artifact>
<excludes>
<!-- 1. GENÉRICO: Elimina TODO en META-INF excepto servicios y archivos ebean -->
<exclude>META-INF/*.SF</exclude>
<exclude>META-INF/*.DSA</exclude>
<exclude>META-INF/*.RSA</exclude>
<exclude>META-INF/MANIFEST.MF</exclude>
<exclude>META-INF/DEPENDENCIES*</exclude>
<exclude>META-INF/LICENSE*</exclude>
<exclude>META-INF/NOTICE*</exclude>
<exclude>META-INF/*.txt</exclude>
<exclude>META-INF/*.md</exclude>
<!-- 2. ESPECÍFICO: Limpia los avisos de Netty, JSON Schema y Módulos -->
<exclude>META-INF/io.netty.versions.properties</exclude>
<exclude>draftv3/schema</exclude>
<exclude>draftv4/schema</exclude>
<exclude>**/module-info.class</exclude>
<exclude>META-INF/versions/**</exclude>
<!-- 3. GENÉRICO: Archivos de licencia sueltos en la raíz -->
<exclude>LICENSE*</exclude>
<exclude>NOTICE*</exclude>
</excludes>
</filter>
</filters>
<transformers>
<transformer
implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
<transformer
implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
<resource>META-INF/ebean.mf</resource>
</transformer>
<transformer
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>com.pablotj.portfolio.bootstrap.PortfolioApplication</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>

View File

@@ -1,16 +1,67 @@
package com.pablotj.portfolio.bootstrap;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import com.pablotj.portfolio.bootstrap.certification.CertificationApplicationConfig;
import com.pablotj.portfolio.bootstrap.education.EducationApplicationConfig;
import com.pablotj.portfolio.bootstrap.experience.ExperienceApplicationConfig;
import com.pablotj.portfolio.bootstrap.profile.ProfileApplicationConfig;
import com.pablotj.portfolio.bootstrap.project.ProjectApplicationConfig;
import com.pablotj.portfolio.bootstrap.skill.SkillApplicationConfig;
import com.pablotj.portfolio.infrastructure.config.CorsConfig;
import com.pablotj.portfolio.infrastructure.rest.api.ApiErrorController;
import com.pablotj.portfolio.infrastructure.rest.api.ApiRootController;
import com.pablotj.portfolio.infrastructure.rest.certification.CertificationController;
import com.pablotj.portfolio.infrastructure.rest.education.EducationController;
import com.pablotj.portfolio.infrastructure.rest.experience.ExperienceController;
import com.pablotj.portfolio.infrastructure.rest.profile.ProfileController;
import com.pablotj.portfolio.infrastructure.rest.project.ProjectController;
import com.pablotj.portfolio.infrastructure.rest.skill.SkillController;
import io.jooby.Jooby;
import io.jooby.OpenAPIModule;
import io.jooby.ebean.EbeanModule;
import io.jooby.flyway.FlywayModule;
import io.jooby.guice.GuiceModule;
import io.jooby.hibernate.validator.HibernateValidatorModule;
import io.jooby.hikari.HikariModule;
import io.jooby.jackson.JacksonModule;
import io.jooby.netty.NettyServer;
@SpringBootApplication(scanBasePackages = "com.pablotj")
@EnableJpaRepositories(basePackages = {"com.pablotj.portfolio.infrastructure.persistence.*.repo"})
@EntityScan(basePackages = {"com.pablotj.portfolio.infrastructure.persistence.*.entity"})
public class PortfolioApplication {
public class PortfolioApplication extends Jooby {
{
install(new NettyServer());
install(new JacksonModule());
install(new HibernateValidatorModule());
install(new HikariModule());
install(new FlywayModule());
install(new EbeanModule());
install(new CorsConfig());
error(ApiErrorController.getHandler());
install(new ProfileApplicationConfig());
install(new ProjectApplicationConfig());
install(new CertificationApplicationConfig());
install(new EducationApplicationConfig());
install(new ExperienceApplicationConfig());
install(new SkillApplicationConfig());
install(new GuiceModule()); // Esto habilita la resolución de @Inject y @Named
path("/api", () -> {
mvc(ApiRootController.class);
mvc(CertificationController.class);
mvc(EducationController.class);
mvc(ExperienceController.class);
mvc(ProfileController.class);
mvc(ProjectController.class);
mvc(SkillController.class);
});
install(new OpenAPIModule().swaggerUI("/docs"));
public static void main(String[] args) {
SpringApplication.run(PortfolioApplication.class, args);
}
}
public static void main(final String[] args) {
runApp(args, PortfolioApplication::new);
}
}

View File

@@ -3,19 +3,20 @@ package com.pablotj.portfolio.bootstrap.certification;
import com.pablotj.portfolio.application.certification.CreateCertificationUseCase;
import com.pablotj.portfolio.application.certification.GetCertificationUseCase;
import com.pablotj.portfolio.domain.certification.port.CertificationRepositoryPort;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.pablotj.portfolio.infrastructure.persistence.certification.adapter.CertificationRepositoryAdapter;
import io.jooby.Extension;
import io.jooby.Jooby;
import jakarta.annotation.Nonnull;
@Configuration
public class CertificationApplicationConfig {
public class CertificationApplicationConfig implements Extension {
@Bean
public GetCertificationUseCase getCertificationUseCase(CertificationRepositoryPort repo) {
return new GetCertificationUseCase(repo);
}
@Override
public void install(@Nonnull Jooby app) {
CertificationRepositoryAdapter adapter = new CertificationRepositoryAdapter();
@Bean
public CreateCertificationUseCase createCertificationUseCase(CertificationRepositoryPort repo) {
return new CreateCertificationUseCase(repo);
app.getServices().put(CertificationRepositoryPort.class, adapter);
app.getServices().put(GetCertificationUseCase.class, new GetCertificationUseCase(adapter));
app.getServices().put(CreateCertificationUseCase.class, new CreateCertificationUseCase(adapter));
}
}

View File

@@ -3,19 +3,20 @@ package com.pablotj.portfolio.bootstrap.education;
import com.pablotj.portfolio.application.education.CreateEducationUseCase;
import com.pablotj.portfolio.application.education.GetEducationUseCase;
import com.pablotj.portfolio.domain.education.port.EducationRepositoryPort;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.pablotj.portfolio.infrastructure.persistence.education.adapter.EducationRepositoryAdapter;
import io.jooby.Extension;
import io.jooby.Jooby;
import jakarta.annotation.Nonnull;
@Configuration
public class EducationApplicationConfig {
public class EducationApplicationConfig implements Extension {
@Bean
public GetEducationUseCase getEducationUseCase(EducationRepositoryPort repo) {
return new GetEducationUseCase(repo);
}
@Override
public void install(@Nonnull Jooby app) {
EducationRepositoryAdapter adapter = new EducationRepositoryAdapter();
@Bean
public CreateEducationUseCase createEducationUseCase(EducationRepositoryPort repo) {
return new CreateEducationUseCase(repo);
app.getServices().put(EducationRepositoryPort.class, adapter);
app.getServices().put(GetEducationUseCase.class, new GetEducationUseCase(adapter));
app.getServices().put(CreateEducationUseCase.class, new CreateEducationUseCase(adapter));
}
}

View File

@@ -3,19 +3,20 @@ package com.pablotj.portfolio.bootstrap.experience;
import com.pablotj.portfolio.application.experience.CreateExperienceUseCase;
import com.pablotj.portfolio.application.experience.GetExperienceUseCase;
import com.pablotj.portfolio.domain.experience.port.ExperienceRepositoryPort;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.pablotj.portfolio.infrastructure.persistence.experience.adapter.ExperienceRepositoryAdapter;
import io.jooby.Extension;
import io.jooby.Jooby;
import jakarta.annotation.Nonnull;
@Configuration
public class ExperienceApplicationConfig {
public class ExperienceApplicationConfig implements Extension {
@Override
public void install(@Nonnull Jooby app) {
@Bean
public GetExperienceUseCase getExperienceUseCase(ExperienceRepositoryPort repo) {
return new GetExperienceUseCase(repo);
}
ExperienceRepositoryAdapter adapter = new ExperienceRepositoryAdapter();
@Bean
public CreateExperienceUseCase createExperienceUseCase(ExperienceRepositoryPort repo) {
return new CreateExperienceUseCase(repo);
app.getServices().put(ExperienceRepositoryPort.class, adapter);
app.getServices().put(GetExperienceUseCase.class, new GetExperienceUseCase(adapter));
app.getServices().put(CreateExperienceUseCase.class, new CreateExperienceUseCase(adapter));
}
}

View File

@@ -3,19 +3,20 @@ package com.pablotj.portfolio.bootstrap.profile;
import com.pablotj.portfolio.application.profile.CreateProfileUseCase;
import com.pablotj.portfolio.application.profile.GetProfileUseCase;
import com.pablotj.portfolio.domain.profile.port.ProfileRepositoryPort;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.pablotj.portfolio.infrastructure.persistence.profile.adapter.ProfileRepositoryAdapter;
import io.jooby.Extension;
import io.jooby.Jooby;
import jakarta.annotation.Nonnull;
@Configuration
public class ProfileApplicationConfig {
public class ProfileApplicationConfig implements Extension {
@Override
public void install(@Nonnull Jooby app) {
@Bean
public GetProfileUseCase getHomeUseCase(ProfileRepositoryPort repo) {
return new GetProfileUseCase(repo);
}
ProfileRepositoryAdapter adapter = new ProfileRepositoryAdapter();
@Bean
public CreateProfileUseCase createHomeUseCase(ProfileRepositoryPort repo) {
return new CreateProfileUseCase(repo);
app.getServices().put(ProfileRepositoryPort.class, adapter);
app.getServices().put(GetProfileUseCase.class, new GetProfileUseCase(adapter));
app.getServices().put(CreateProfileUseCase.class, new CreateProfileUseCase(adapter));
}
}

View File

@@ -3,19 +3,20 @@ package com.pablotj.portfolio.bootstrap.project;
import com.pablotj.portfolio.application.project.CreateProjectUseCase;
import com.pablotj.portfolio.application.project.GetProjectUseCase;
import com.pablotj.portfolio.domain.project.port.ProjectRepositoryPort;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.pablotj.portfolio.infrastructure.persistence.project.adapter.ProjectRepositoryAdapter;
import io.jooby.Extension;
import io.jooby.Jooby;
import jakarta.annotation.Nonnull;
@Configuration
public class ProjectApplicationConfig {
public class ProjectApplicationConfig implements Extension {
@Override
public void install(@Nonnull Jooby app) {
@Bean
public CreateProjectUseCase createProjectUseCase(ProjectRepositoryPort repo) {
return new CreateProjectUseCase(repo);
}
ProjectRepositoryAdapter adapter = new ProjectRepositoryAdapter();
@Bean
public GetProjectUseCase getProjectUseCase(ProjectRepositoryPort repo) {
return new GetProjectUseCase(repo);
app.getServices().put(ProjectRepositoryPort.class, adapter);
app.getServices().put(CreateProjectUseCase.class, new CreateProjectUseCase(adapter));
app.getServices().put(GetProjectUseCase.class, new GetProjectUseCase(adapter));
}
}

View File

@@ -3,19 +3,20 @@ package com.pablotj.portfolio.bootstrap.skill;
import com.pablotj.portfolio.application.skill.CreateSkillUseCase;
import com.pablotj.portfolio.application.skill.GetSkillUseCase;
import com.pablotj.portfolio.domain.skill.port.SkillRepositoryPort;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.pablotj.portfolio.infrastructure.persistence.skill.adapter.SkillRepositoryAdapter;
import io.jooby.Extension;
import io.jooby.Jooby;
import jakarta.annotation.Nonnull;
@Configuration
public class SkillApplicationConfig {
public class SkillApplicationConfig implements Extension {
@Override
public void install(@Nonnull Jooby app) {
@Bean
public GetSkillUseCase getSkillUseCase(SkillRepositoryPort repo) {
return new GetSkillUseCase(repo);
}
SkillRepositoryAdapter adapter = new SkillRepositoryAdapter();
@Bean
public CreateSkillUseCase createSkillUseCase(SkillRepositoryPort repo) {
return new CreateSkillUseCase(repo);
app.getServices().put(SkillRepositoryPort.class, adapter);
app.getServices().put(GetSkillUseCase.class, new GetSkillUseCase(adapter));
app.getServices().put(CreateSkillUseCase.class, new CreateSkillUseCase(adapter));
}
}

View File

@@ -0,0 +1,61 @@
# Configuración de la App (Sustituye a info y app)
info {
app {
version = "0.0.1-SNAPSHOT" # Maven no rellena esto automáticamente en Jooby sin plugins extra, mejor ponlo fijo
}
}
app {
cors {
allowed-origins = ${?APP_ALLOWED_ORIGINS}
allowed-origins = "http://localhost:8080" # Valor por defecto
}
}
# Configuración del Servidor
server {
port = 8080
# En Jooby, el context-path se define habitualmente en la clase App,
# pero puedes usar esta propiedad si la gestionas manualmente.
}
# Base de Datos (Ebean usa estas propiedades automáticamente)
db {
url = "jdbc:postgresql://"${?DB_HOST}":"${?DB_PORT}"/"${?DB_NAME}
user = ${?DB_USER}
password = ${?DB_PASSWORD}
driverClassName = org.postgresql.Driver
# HikariCP (Configuración de Pool)
hikari {
maximumPoolSize = 3
minimumIdle = 1
idleTimeout = 30000
connectionTimeout = 10000
}
}
flyway {
# Ubicación de tus scripts .sql
locations = ["classpath:db/migration"]
cleanDisabled = true
baselineOnMigrate = true
baselineVersion = ${?FLYWAY_BASELINE_VERSION}
# Si quieres que se ejecute siempre al arrancar
run = [migrate]
}
ebean {
ddl {
generate = false
run = false
}
# Mostrar SQL en consola
debug = true
}
# Jackson
jackson {
indent_output = true
}

View File

@@ -1,54 +0,0 @@
info:
app:
version: @project.version@
app:
cors:
allowed-origins: ${APP_ALLOWED_ORIGINS:http://localhost:8080}
spring:
application:
name: portfolio-api
web:
resources:
add-mappings: false
datasource:
url: jdbc:postgresql://${DB_HOST:localhost}:${DB_PORT:5432}/${DB_NAME:portfolio}
username: ${DB_USER:postgres}
password: ${DB_PASSWORD:postgres}
driver-class-name: org.postgresql.Driver
hikari:
maximum-pool-size: 3
minimum-idle: 1
idle-timeout: 30000
connection-timeout: 10000
leak-detection-threshold: 10000
jpa:
hibernate:
ddl-auto: validate
properties:
hibernate.transaction.jta.platform: org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform
hibernate:
format_sql: true
dialect: org.hibernate.dialect.PostgreSQLDialect
show-sql: true
jackson:
serialization:
INDENT_OUTPUT: true
springdoc:
api-docs:
path: /v3/api-docs
swagger-ui:
path: /swagger-ui
server:
port: 8080
servlet:
context-path: /api
forward-headers-strategy: framework