Kotlin, SpringBoot, Docker y DockerCompose I

Inicio una serie de entradas cuya finalidad es la creación de una aplicación sencilla con SpringBoot desarrollada en lenguaje Kotlin, además, pretendo dockerizar la aplicación con Docker y Docker DockerCompose.

La serie está compuesta de tres entradas ordenadas de forma cronológica:

La aplicación define un servicio REST basada en un dominio para la gestión de artículos y sus autores. Se define una operación ficticia para integrar los componentes de la arquitectura software.

La estructura del artículo está compuesto por los siguientes puntos:

  1. Definición de dependencias y creación del proyecto
  2. Modelo de entidad
  3. Capa de persistencia
  4. Capa de servicio
  5. Capa controladora
  6. Aplicación
  7. Configuración de base de datos
  8. Arranque

1.-Definición de dependencias y creación del proyecto

La gestión de dependencias del proyecto la realiza Gradle. Las dependencias más importantes son: spring-boot-starter-data-jpa, spring-boot-starter-web, spring-boot-devtools, spring-boot-configuration-processor, h2 y mysql-connector-java

La versión de SpringBoot es la 2.2.7.RELEASE y el dependency-manager es la 1.0.9.RELEASE

2.-Modelo de entidad

La aplicación define dos entidades: Artículos y Autor. Un Autor está compuesto de los siguientes atributos: login, firstname, lastname. Un Artículo está compuesto por los siguientes atributos: title, headline, content, autor, slug y addedAt. Un Artículo está escrito por un Autor y, éste, puede escribir varios Artículos.

El snippet con las definición de las entidades es el siguiente:

@Entity
class Author(
  var login: String,
  var firstname: String,
  var lastname: String,
@Id @GeneratedValue var id: Long? = null)
@Entity
class Article(
  var title: String,
  var headline: String,
  var content: String,
  @ManyToOne var author: Author,
  var slug: String = title.toSlug(),
  var addedAt: LocalDateTime = LocalDateTime.now(),
  @Id @GeneratedValue var id: Long? = null)

La definición de las relaciones entre entidades se utilizan las siguientes anotaciones: @Entity, para definir las clases que definen una entidad; @ManyToOne, para definir las relaciones uno a muchos; y, @Id y @GeneratedValue, para definir un identificador único.

3.- Capa de persistencia

Los componentes de persistencia están basados en el interfaz CrudRepository de Spring JPA. Defino dos repositorios: ArticleRepository, para las operaciones sobre la entidad Article; y,
AuthorRepository, para las operaciones sobre la entidad Author. El snippet con los repositorios es el siguiente:

import com.example.ejem1kotlindocker.entity.Article
import com.example.ejem1kotlindocker.entity.Author
import org.springframework.data.repository.CrudRepository
interface ArticleRepository: CrudRepository<Article, Long> {
  fun findBySlug(slug: String): Article?
  fun findAllByOrderByAddedAtDesc(): Iterable<Article>
}
interface AuthorRepository: CrudRepository<Author, Long> {
  fun findByLogin(login: String): Author?
}

Las operaciones sobre las entidades son operaciones de búsqueda basadas en los atributos de cada entidad.

4.- Capa de servicio

La capa de servicios es aquella que define los componentes con las operaciones de negocio las cuáles utilizan los repositorios o cualquier otro componente. En el ejemplo, se define un único servicio con una operación y los DTO para la funcionalidad requerida. El snippet del servicio es el siguiente:

@Service
class BusinessService {
  companion object {
    @Suppress("JAVA_CLASS_ON_COMPANION")
    private val logger = LoggerFactory.getLogger(javaClass.enclosingClass)
  }

  @Autowired
  private lateinit var authorRepository: AuthorRepository

  @Transactional
  fun operationBusiness1(request: BusinessServiceRequest): BusinessServiceResponse {
    logger.info("[**] BusinessService.operation1. Request=${request}")
    val loginRequest = authorRepository.findByLogin(request.login)
    val stringResponse = StringBuilder()
    stringResponse.append(request.login)
    stringResponse.append("-")
    stringResponse.append(loginRequest!!.login)
    val result = BusinessServiceResponse(message = stringResponse.toString())
    logger.info("[**] BusinessService.operation1. result=${result}")
    return result
  }
}
data class BusinessServiceRequest(val login: String)
data class BusinessServiceResponse(val message: String)

En el snippet anterior, se muestra el servicio BusinessService que define una inyección de dependencia con el repositorio AuthorRepository, empleando la anotación @Autowired; se define una función de negocio operationBusiness1 transaccional al emplear la anotación @Transactional; se define un elemento de logger y los DTO de entrada y salida de de la operación de servicio operationBusiness1.

5.- Capa controladora

La capa controladora es aquella capa en la cual definimos los controladores REST de la aplicación. La capa controladora debe de definir una inyección de dependencia del servicio necesario para la ejecución de la operación de negocio. En nuestro caso, se define el controlador AppController con una dependecia al servicio BusinessService.

El snippet del controlador AppController es el siguiente:

import com.example.ejem1kotlindocker.service.BusinessService
import com.example.ejem1kotlindocker.service.BusinessServiceRequest
import com.example.ejem1kotlindocker.service.BusinessServiceResponse
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
@RestController
@RequestMapping("business")
class AppController {
  companion object {
    @Suppress("JAVA_CLASS_ON_COMPANION")
    private val logger = LoggerFactory.getLogger(javaClass.enclosingClass)
  }

  @Autowired
  private lateinit var businessService: BusinessService

  @GetMapping("operation1")
  fun operation1(): BusinessServiceResponse {
    logger.info("[*] AppController.operation1.")
    val request = BusinessServiceRequest("ParamTest")
    return businessService.operationBusiness1(request)
  }
}

El controlador REST queda definido en la clase AppController por las anotaciones @RestController y @RequestMapping con el String “business”. La funcionalidad del servicio de negocio se define con la anotación @GetMapping con la cual definimos una operación REST de tipo GET. Además, la clase define el atributo businessService con la inyección de dependencia utilizando la anotación @Autowired. Para finalizar e igual que el servicio, se utiliza un elemento para escribir trazas de log.

6.- Aplicación

La aplicación SpringBoot queda definida por la clase Ejem1kotlindockerApplication la cual queda definida con la apnotación @SpringBootApplication. La anotación @EnableConfigurationProperties define la relación de la clase con las propiedades de la misma, referenciando a la clase Ejem1kotlindockerProperties. La clase Ejem1kotlindockerProperties realiza la carga de las propiedades del fichero application.properties. El snippet de la clase es la siguiente:

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.boot.runApplication

@SpringBootApplication
@EnableConfigurationProperties(Ejem1kotlindockerProperties::class)
class Ejem1kotlindockerApplication{}
fun main(args: Array<String>) {
  runApplication<Ejem1kotlindockerApplication>(*args){}
}

7.- Configuración de base de datos

La configuración de base de datos la realizamos en la clase Ejem1kotlindockerConfiguration. La clase define la carga de los datos de configuración desde el fichero application.properties y la definición de inicialización de unos datos iniciales en la base de datos en la función databaseInitializer.

Por otra parte, se definen dos Profile de trabajo: local, para la ejecución de la aplicación con una base de datos MySQL;y, dev, para definir la configuración a una base de datos al trabajar con el docker-compose. El snippet de la clase configuración es el siguiente:

@Configuration
@ConfigurationProperties("spring.datasource")
@SuppressWarnings("unused")
class Ejem1kotlindockerConfiguration{
  companion object {
    @Suppress("JAVA_CLASS_ON_COMPANION")
    private val logger = LoggerFactory.getLogger(javaClass.enclosingClass)
  }
  @Value("\${spring.datasource.driver-class-name}")
  private lateinit var driverClassName: String
  @Value("\${spring.datasource.url}")
  private lateinit var url: String
  @Value("\${spring.datasource.username}")
  private lateinit var username: String
  @Value("\${spring.datasource.password}")
  private lateinit var password: String
  @Profile("local")
  @Bean
  fun localDatabaseConnection(): String {
    println("DB connection form Local - MySQL")
    logger.info("[*] DriverClassName=${driverClassName}")
    logger.info("[*] URL=${url}")
    logger.info("[*] UserName=${username}")
    return "DB Connection for Local - MySQL"
  }
  @Profile("dev")
  @Bean
  fun devDatabaseConnection(): String {
    println("DB connection form Local - MySQL")
    logger.info("[*] DriverClassName=${driverClassName}")
    logger.info("[*] URL=${url}")
    logger.info("[*] UserName=${username}")
    return "DB Connection for Local - MySQL"
  }
  @Bean
  fun databaseInitializer(authorRepository: AuthorRepository,
    articleRepository: ArticleRepository) = ApplicationRunner {
    val smaldini = authorRepository.save(Author("ParamTest", "AuthorTest", "lastNameTest"))
    articleRepository.save(Article(
      title = "Articule1",
      headline = "Headline1",
      content = "Content1",
      author = smaldini
   ))
   articleRepository.save(Article(
     title = "Articule2",
     headline = "Headline2",
     content = "Content3",
     author = smaldini
   ))
  }
}

Los datos iniciales que se crean en la base de datos corresponden con dos elementos de la entidad Article.

8.- Arranque

Para arrancar la aplicación en un entorno local, es decir utilizando una base de datos H2, se ejecuta el siguiente comando ubicados en
la carpeta principal del proyecto:

./gradlew bootRun

Si queremos ejecutar la aplicación utilizando la configuración existente en el profile local, ejecutamos
el siguiente comando unicados en la carpeta principal del proyecto:

SPRING_PROFILES_ACTIVE=local ./gradlew bootRun

Para verificar el correcto funcionamiento del servicio, ejecutamos el siguiente comando curl:

curl http://localhost:8080/kotlindocker/business/operation1

La salida pos consola es la siguiente:

{"message":"ParamTest-ParamTest"}

 

Para el lector interesado en el código del proyeceto puede acceder al siguiente enlace

En la siguiente entrada, Kotlin, SpringBoot, Docker y DockerCompose II: test unitarios, describiré como realizar test unitarios de los elementos de la aplicación.

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión /  Cambiar )

Google photo

Estás comentando usando tu cuenta de Google. Cerrar sesión /  Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión /  Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión /  Cambiar )

Conectando a %s