Kotlin, SpringBoot, Docker y DockerCompose III: docker y docker-compose

Finalizamos la serie de artículos de Kotlin y SpringBoot con el presente artículo con título «Kotlin, SpringBoot, Docker y DockerCompose III: docker y docker-compose». El objetivo de la entrada es dockerizar la aplicación para hacer funcionar la aplicación desarrollada en un contenedor Docker. Además, definiremos la aplicación para que funcione en una composición de contenedores con un contenedor con MySQL.

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

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

  1. Docker
    • Propuesta 1
    • Propuesta 2
    • Propuesta 3
  2. DockerCompose

1.- Docker

La aplicación la podemos dockerizar de tres formas posibles. Las diferencia reside en el tamaño de la imagen resultante.

Propuesta 1

La estrategia consiste en copiar el contenido de la aplicación en el interior de la imagen y posteriormente realizar la creación del artefacto. El snippet con el contenido del Dockerfile es el siguiente:

FROM openjdk:8-jdk-alpine
VOLUME /tmp
RUN mkdir /app
COPY . /app
WORKDIR /app
RUN /app/gradlew build
RUN mv /app/build/libs/*.jar /app/app.jar
ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app/app.jar"]

Para crear la imagen ejecutamos el siguiente comando docker:

docker image build -t alvaroms/ejem1kotlindocker:v1 .

Para visualizar las imágenes creadas ejecutamos el siguiente comando:

docker image list

El resultado de la ejecución del comando muestra una imagen con un tamaño aproximado a 686MB.

Propuesta 2

La segunda estrategía consigue reducir el tamaño de la imagen. Para ello, se realiza la creación del artefacto fuera de la imagen y se copia al interior de la imagen. Es decir, respecto a la solución anterior, se elimina el paso de construcción del artefacto. Para realizar la creación del artefacto, es necesario definir una tarea de creación del fichero jar mediante una tarea en Gradle. La tarea Gradle es la siguiente:

tasks.register("buildDocker"){
  dependsOn("build")
  doLast{
    fileList("./build/libs").forEach { file ->
      file.copyTo(File("./ejem1kotlindocker.jar"))
    }
  }
}

Como se observa en el snippet anterior, la tarea «buildDocker» tiene una dependencia con la tarea build; una vez ejecutado build, su resultado, generado en la carpeta «./build/libs» del proyecto, se copia a la carpeta raíz del proyecto. Esta tarea debe de ser ejecutada previamente a la construcción de la imagen.

La definición de la imagen en el Dockerfile es la siguiente:

FROM openjdk:8-jdk-alpine
VOLUME /tmp
RUN mkdir /app
WORKDIR /app
EXPOSE 8080
ADD ./ejem1kotlindocker.jar /app/ejem1kotlindocker.jar
ENTRYPOINT ["java", "-jar", "/app/ejem1kotlindocker.jar"]

Para crear la imagen ejecutamos el siguiente comando docker:

docker image build -t alvaroms/ejem1kotlindocker:v2 .

Para visualizar las imagenes creadas ejecutamos el siguiente comando:

docker image list

El resultado de esta forma implica la creación de una imagen con un tamaño aproximado a 149MB. En relación a la primera forma, la reducción del tamaño es notable.

Propuesta 3

La tercera estrategia consiste en fusionar los objetivos de las anteriores. Para ello, primeramente, realizamos la creación del artefacto en una primera capa; posteriormente,
con el artefacto creado, construimos la imagen. Todo ello en el paso de construcción de la imagen y en elmomento de crear la imagen Docker. El snippet con el contenido del Dockerfile es el siguiente:

FROM openjdk AS build
VOLUME /tmp
RUN mkdir /app
COPY . /app
WORKDIR /app
RUN /app/gradlew build
FROM openjdk:8-jdk-alpine

ARG APP_VERSION=1.0.0-SNAPSHOT
ENV database__client=mysql
ENV database__connection__host=mysql
ENV database__connection__user=root
ENV database__connection__password=password
ENV database__connection__database=prueba
ENV SPRING_PROFILES_ACTIVE=dev
LABEL org.label-schema.version=$APP_VERSION
COPY --from=build /app/build/libs/*.jar /app/app.jar
ENTRYPOINT ["java", "-Djava.security.egd=file:/dev/./urandom", "-jar", "/app/app.jar" ]

Con esta opción, definimos las variables de entorno con la configuración por defecto a la base de datos cuyos valores pueden ser pasados por parámetro en la creación de la imagen. De la misma manera que los argumentos necesarios.

Para crear la imagen ejecutamos el siguiente comando docker:

docker image build -t alvaroms/ejem1kotlindocker:v3 --build-arg APP_VERSION=1.0.3-SNAPSHOT .

Para visualizar las imagenes creadas ejecutamos el siguiente comando:

docker image list

El resultado de esta forma implica la creación de una imagen con un tamaño aproximado a 149MB. En relación a la primera forma, la reducción del tamaño es notable;y, respecto a la segunda, no es necesario realizar ninguna operación intermedia. Así, de las tres definiciones de Dockerfile, esta tercera, es la solución más eficiente.

2.- DockerCompose

DockerCompose permite realizar la composición de contenedores interconectados. La definición de la composición se realiza en el fichero docker-compose.yml, ubicado en la raíz del proyecto. La composición se define en base a dos servicios: el primero, mysql, corresponde con un contenedor de una imagen de MySQL; el segundo, kotlindocker, corresponde con la aplicación desarrollada.

Cada contenedor está definida en dos redes: kotlinnetwork, para el contenedor de la aplicación; y, mysqlnetwork, para el contenedor de la base de datos MySQL. La conexión la realiza el servicio de kotlin al tener conexión a las dos redes. Para los volúmenes, se definen un volumen para cada servicio.

Otro detalle importante, reside en qué imagen de la aplicación seleccionamos, o bien, creamos la imagen a partir del código existente: opción build, creamos la imagen a partir del código; e, image, creamos el contenedor a partir de una imagen definida.

El contenido del fichero docker-compose es el siguiente:

version: '3'
services:
  mysql:
    container_name: mysql
    image: mysql:5.7
    environment:
      - MYSQL_DATABASE=prueba
      - MYSQL_ROOT_PASSWORD=password
    ports:
      - 3306:3306
    volumes:
      - mysqlvolume:/var/lib/mysql
    networks:
      - mysqlnetwork
  kotlindocker:
    depends_on:
      - mysql
    container_name: kotlindocker
      # image: alvaroms/ejem1kotlindocker:v4
      build: ./
    environment:
      - SPRING_PROFILES_ACTIVE=dev
    ports:
      - 8087:8080
    volumes:
      - kotlinvolume:/var/lib/kotlin
    networks:
      - kotlinnetwork
      - mysqlnetwork
    volumes:
      kotlinvolume:
      mysqlvolume:
    networks:
      kotlinnetwork:
        driver_opts:
          com.docker.network.bridge.name: kotlinNetwork
      mysqlnetwork:
        driver_opts:
          com.docker.network.bridge.name: MySQLNetwork

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

Finalizamos con una reflexión sobre la serie sobre kotlin, Spring Boot y Docker: un punto a tener en cuenta es que el código Kotlin presentado es muy mejorable al no haber aplicado patrones funcionales como los que se pueden presentar con la librería Arrow; con ella, la mejora del código es notable, por ejemplo, utilizando contenedores binarios.

Kotlin, SpringBoot, Docker y DockerCompose II: test unitarios

Continuando con el segundo artículo de la serie con título Kotlin «SpringBoot, Docker y DockerCompose II: test unitarios» en el cual me centraré en cómo definir pruebas unitarias. En la anterior entrada, «Kotlin, SpringBoot, Docker y DockerCompose I», realicé una descripción de una aplicación básica de ejemplo.

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

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

  1. Test unitarios de controladores
  2. Test unitarios de servicios
  3. Test unitarios de repositorios

Para la realización de pruebas unitarias es necesario definir un conjunto de dependencias en el fichero de configuración de Gradle build.gradle.kts. Las dependencias necesarias son las siguientes:

testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("org.springframework.security:spring-security-config")
testImplementation("org.springframework.security:spring-security-test")
testImplementation("com.nhaarman.mockitokotlin2:mockito-kotlin:2.1.0")

Para realizar las pruebas unitarias es necesario tener definido el fichero con las propiedades para el entorno de pruebas; en el ejemplo, el contenido necesario del fichero application.properties es el siguiente:

spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.url=jdbc:h2:mem:db;DB_CLOSE_DELAY=-1
spring.datasource.username=sa
spring.datasource.password=
kotlindocker.title=ExampleKotlinDocker
kotlindocker.banner.title=Warning
kotlindocker.banner.content=Kotlin application witth docker.

Las pruebas unitarias consiste en realizar test sobre un componente determinado. Si el componente tiene relaciones de asociación, o bien, usa elementos con una funcionalidad concreta, se puede declarar su resultado para realizar la prueba; estos casos, se emplean objetos mock para dichas pruebas.

1.- Test unitarios de controladores

Para definir una prueba unitaria de un controlador, en nuestro caso AppController, definiremos una clase de Test. La clase se test -AppControllerTest- debe de estar declarada con las siguientes anotaciones: @RunWith, @AutoConfigureMockMvc y @SpringBootTest.

La clase AppControllerTest dede tener definida la inyección de las entidades WebApplicationContext y MockMvc. Además, se definen la declaración de los elementos necesarios para moquear, en nuestro caso, la clase BusinessService mediante la anotación @MockBean.

La definición de un test se realiza definiendo funciones con la anotación @Test. En el cuerpo de la función, se declara el comportamiento del objeto moquedo, se lanza la petición al objeto a probar, en nuestro caso, una petición HTTP al enppoint /business/operation1 con los datos necesarios; y, para finalizar, se realiza el chequeo del resultado con las funciones del objeto MarcherAssert. El snippet del código es el siguiente:

@RunWith(SpringRunner::class)
@AutoConfigureMockMvc
@SpringBootTest
class AppControllerTest {
  @Autowired
  private lateinit var webApplicationContext: WebApplicationContext
  @Autowired
  private lateinit var mockMvc: MockMvc
  @MockBean
  private lateinit var businessService: BusinessService
  @org.junit.Before
  fun setup() {
    mockMvc = MockMvcBuilders
      .webAppContextSetup(webApplicationContext)
      .build()
  }
  @Test
  fun `AppControllerTest operation1`(){
    val responseOperation1 = BusinessServiceResponse(message = "MessageTest")
    whenever(businessService.operationBusiness1( any() )).thenReturn( responseOperation1 )
    val result = mockMvc.perform( MockMvcRequestBuilders.get("/business/operation1").accept(MediaType.APPLICATION_JSON) )
      .andExpect(MockMvcResultMatchers.status().isOk)
      .andExpect(MockMvcResultMatchers.content().contentType(MediaType.APPLICATION_JSON))
      .andReturn()
    MatcherAssert.assertThat(result.response.status, CoreMatchers.`is`(HttpStatus.OK.value()))
    verify(businessService).operationBusiness1( any() )
  }
}

2.- Test unitarios de servicios

La declaración de las pruebas unitarias de un servicio de negocio, sigue la misma línea que el de un controlador. En el ejemplo, definiré una prueba unitaria de la operación definida en el
servicio de negocio.

La diferencia radica en lo siguiente: los elementos que deben de ser moquedos, se declaran con la anotación @Mock; y, para el elemento a probar, se emplea la anotación @InjectMocks. La definición de la clase de test está definida exclusivamente con la anotación @RunWith(MockitoJUnitRunner::class). El snippet de la clase es el siguiente:

@RunWith(MockitoJUnitRunner::class)
class BusinessServiceTest {
  @Mock
  private lateinit var authorRepository: AuthorRepository
  @InjectMocks
  private lateinit var businessService: BusinessService
  @Test
  fun `operation1`(){
    val authorMock = Author(login = "loginTest", firstname = "firstNameTest", lastname = "lastnameTest")
    whenever(authorRepository.findByLogin( any() )).thenReturn(
      authorMock
    )
    val request = BusinessServiceRequest(login = "Param1")
    val result = businessService.operationBusiness1(request)
    MatcherAssert.assertThat(result.message, CoreMatchers.`is`( "Param1-" + authorMock.login ))
    verify(authorRepository).findByLogin( any() )
  }
}

3.- Test unitarios de repositorios

La definición de las clases de test de un repositorio deben de tener la anotación @DataJpaTest y, como atributo, la definición de inyección de dependecia de los repositorios a probar. En nuestro caso, definiremos la referencia al repositorio ArticleRepository y, por otro lado, la referencia al EntityManager de test. La prueba de una operación se define en una función creando los datos de entrada, consulta al repositorio y verificación del resultado. El snippet con el código de ejemplo es el siguiente:

@DataJpaTest
class ArticleRepositoryTest {
  @Autowired
  private lateinit var entityManager: TestEntityManager
  @Autowired
  private lateinit var articleRepository: ArticleRepository
  @Test
  fun `When findByIdOrNull then return Article`() {
    val juergen = Author("loginUserTest", "firstnameTest", "lastnameTest")
    val juergenInserted = entityManager.persist(juergen)
    assertThat(juergenInserted).isNotNull
    val article = Article("titleTest", "headLineTest", "ContentTest", juergen)
    val articleInserted = entityManager.persist(article)
    assertThat(articleInserted).isNotNull
      entityManager.flush()
    val found = articleRepository.findByIdOrNull(article.id!!)
    assertThat(found).isEqualTo(article)
  }
}

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

En la siguiente entrada, «Kotlin, SpringBoot, Docker y DockerCompose III: docker y docker-compose», describiré como realizar test unitarios de los elementos de la aplicación.

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.

Funciones lambda con receptores

Las funciones lambdas son funciones que permiten recibir funciones como parámetros, o bien, retornar una función. Este tipo de función cuya utilización es común en lenguajes con paradigma funcional como son los lenguajes Kotlin, Scala o Haskell. En lengueje Kotlin existe una variante de función lambda la cual permite recibir una referencia al objeto al que se le aplica la función; este tipo de función, se conoce como funciones lambda con receptores.

Para comprender las funciones lambda con receptores, mostraré unos ejemplos de funciones lambda: el primero, es un ejemplo de construcción de una cadena de texto; el segundo, realiza una transformación del tipo de entrada.

Ejemplo 1, función lambda.

Se define una función buildString que recibe como parámetro una función lambda cuyo parámetro de entrada es un elemento de tipo StringBuilder y su salida es un tipo Unit. En el siguiente ejemplo, la función buildString realiza la siguiente funcionalidad: instancia un objeto de la clase StringBuilder identificado como sb, aplicamos la función lambda pasada por parámetros con el objeto sb y, por último, retornamos el String resultante. Para finalizar el ejemplo, se define la invocación de la función y la impresión por consola del resultado.

El snippet del código es el siguiente:

fun buildString(
  builderAction: (StringBuilder) -> Unit
):String{
  val sb = StringBuilder()
  builderAction(sb)
  return sb.toString()
}
val s = buildString {
  it.append("Hello, ")
  it.append("World! ")
}
println("1.- Function Lambda=${s}")

La salida por consola es la siguiente:

1.- Function Lambda=Hello, World!

Ejemplo 2, función lambda

Se define una entidad con nombre Account, se define una función calculate30 la cual recibe como parámetro un elemento de tipo Account y una función lambda que transforma un tipo de entrada Account en otro tipo Account. La función calculate30 realiza las siguientes operaciones: creación de una instancia Account a partir del objeto pasado por parámetro de entrada y, por último, aplicación de la función lambda pasada por parámetro con el objeto Account creado previamente. Para finalizar el ejemplo, se realiza la creación del objeto account1, la invocación a la función calculate30 y la impresión por consola del resultado.

El snippet del código es el siguiente:

data class Account(
  val id: Int,
  val amount: Int,
  val result: Int,
  val status: String
)
fun calculate30(
  init: Account,
  func: (Account) -> Account): Account{
    val account = init.copy(amount = 30)
    return func(account)
}
val account1 = Account(id = 1, amount = 0, result = 0, status = "INIT")
val CTE = 3
val fLambda = calculate30(account1){
  it.copy( result = (it.amount * 6) * CTE, status = "END" )
}
println("1.- Account30 (lambda function)=${fLambda}")

La salida por consola es la siguiente:

1.- Account30 (lambda function)=Account(id=1, amount=30, result=540, status=END)

En los ejemplos anteriores, se puede apreciar que la definición de la función lambda se utiliza la palabre reservada «it» para poder trabajar con el objeto con el que se opera.

Funciones lambda con receptores

Las funciones lambda con receptores en Kotlin son como las funciones lambda a diferencia que la función trabaja con las funciones del tipo de entrada. La declaración de la función, se realiza como si operase con el objeto del tipo de entrada, pudiendo usar el operador this.

Ejemplo 1, función lambda con receptor.

Se define una función buildStringReceiver la cual tiene como parámetro de entrada una función lambda con receptor. La función buildStringReceiver realiza lo siguiente: creación del objeto s de tipo StringBuilder, invocación de la función lambda con el objeto creado y retorno del resultado. Para finalizar, se define la invocación a la función buildStringReceiver con la declaración de la función y la impresión del resultado.

fun buildStringReceiver(
  builderAction: StringBuilder.() -> Unit
): String{
    val s = StringBuilder()
    s.builderAction()
    return s.toString()
}
val r = buildStringReceiver{
  this.append("Hello, ")
  this.append("World! ")
}
println("2.- Function Lambda receiver=${r}")

La salida por consola es la siguiente:

2.- Function Lambda receiver=Hello, World!

Ejemplo 2, función lambda con receptor.

Se define una función calculate30Receiver la cual tiene como parámetro de entrada una función lambda con receptor. La función buildStringReceiver realiza lo siguiente:
creación del objeto de tipo StringBuilder, invocación de la función lambda con el objeto creado y retorno del resultado. Para finalizar, se define la invocación a la función buildStringReceiver con la declaración de la función y la impresión del resultado.

data class Account(
  val id: Int,
  val amount: Int,
  val result: Int,
  val status: String
)
fun calculate30Receiver(
  init: Account,
  func: Account.() -> Account
): Account{
    val account = init.copy(amount = 30)
    return account.func()
}
val fLReceiver = calculate30Receiver(account1){
  this.copy(result = (this.amount * 6) * CTE, status = "END" )
}
println("2.- Account30Receiver(lambda with receiver)=${fLReceiver}")

La salida por consola es la siguiente:

2.- Account30Receiver(lambda with receiver)=Account(id=1, amount=30, result=540, status=END)

Un ejemplo de función lambda con receptor en kotlin pueden ser las funciones apply o use.

Para el lector interesado, el código completo del ejemplo se encuentra en el siguiente enlace

Objetos invocables como funciones en lenguaje Kotlin

En lenguaje Kotlin tenemos la posibilidad de definir una clase la cual funciona como un función; es decir, una vez creado el objeto de la clase, operamos con la instancia como si fuera una función. En la presente entrada, Objetos invocables como funciones en lenguaje Kotlin, mostraré unos ejemplos básicos de uso de esta características de clase.

Los objetos invocables en Kotlin me traen como recuerdo las first class en Scala. Las first class son funciones que se definen como  una variable, un argumento de función, o bien, el resultado de una función. El compilador de Scala, realiza la transformación de la función a una clase transparente al desarrollador. Un ejemplo de first class en Scala es el siguiente:

val multiplyBy2 = (elem: Int) => (elem * 2)

Los ejemplos de objetos invocados que presento en la entrada son tres:

  1. Instanciación de una clase invocable básica.
  2. Instanciación de una clase invovable a partir de un interface.
  3. Definición de un predicado en una clase invocable.

Una clase invocable es aquella clase que define una función específica defina en un método con nombre invoke el cual está definida como un operador. La clase define un constructor y, para la invocación de la función invoke, no es necesario especificar el nombre de la función.

Instancia de una clase invocable básica

En el siguiente ejercicio presento un ejemplo básico. Defino una clase Greeter con un único método invoke que recibe un parámetro; la funcionalidad del método, es la escritura por pantalla del atributo de la clase y el parámetro. El snippet del código es el siguiente:

class Greeter(val greeting: String){
  operator fun invoke(name: String){
    println("Greeting=${greeting} Name=${name}")
  }
}
val obj1 = Greeter("Test1")
obj1("ParamInvoke")

La instancia de la clase Greeter se realiza como cualquier clase típica; pero, la invocación del método, no se especifica sino que se emplea la referencia de la instancia con los parámetros requeridos en la firma del método invoke.

La salida por consola es la siguiente:

Greeting=Test1 Name=ParamInvoke

Instanciación de una clase invovable a partir de un interface

Incrementando el nivel de dificultad, presento una clase SpecialFunction con su método invoke. El método invoke tiene una definición especial porque se utiliza un interface donde se definen los tipos de entrada y de salida de forma genérica; en concreto, se definen dos parámetros de entrada: p1 de tipo X y p2 de tipo Y; y, por último, se define un tipo de salida de tipo Z. El snippet del código es el siguiente:

interface MyFuntion2<in X, in Y, out Z>{
  operator fun invoke(p1:X, p2: Y): Z
}
class SpecialFunction(): MyFuntion2<Int, Int, String>{
  override fun invoke(p1: Int, p2: Int): String {
    return StringBuilder()
      .append(p1)
      .append("+")
      .append(p2)
      .append("=")
      .append((p1+p2).toString())
      .toString()
  }
}
val objectFunctionSum = SpecialFunction()
println("Suma=>${objectFunctionSum(p1 =2,p2=3)}")

De la misma manera que el anterior caso, se realiza la instancia de la clase y, con ésta, realizamos la invocación del método invoke con los parámetros requeridos en su firma.

La salida por consola es la siguiente:

Suma=>2+3=5

Definición de un predicado en una clase invocable

El último caso que presento es una clase que representa un predicado semántico dentro de un contexto funcional. El escenario es el siguiente: dado un sistema que trabaja con proyectos en los cuales se presentan problemas o incidencias representadas por la entidad Issue; la entidad Issue está compuesta por los siguientes campos: un identificador, un nombre de proyecto, un tipo, una prioridad y una descripción del problema. La clase pretende definir lo siguiente: para un proyecto determinado, se quiere determinar si una entidad Issue es de tipo project y si es importante o no. Un elemento Issue es importante si es de tipo tiene valor «BUG» y su prioridad es Critical. El snippet del código es el siguiente:

data class Issue(
  val id:String,
  val project: String,
  val type: String,
  val priority: String,
  val description: String
)
class ImportantIssuesPredicative(val project: String): (Issue) -> Boolean{
  override fun invoke(p1: Issue): Boolean {
    return p1.project == project && p1.isImportant()
  }
  private fun Issue.isImportant(): Boolean{
    return this.type == "BUG" && this.priority == "Critical"
  }
}
val issue1 = Issue(id = "1", project = "project1", type = "PBI", priority = "Medium", description = "description1")
val issue2 = Issue(id = "2", project = "project2", type = "PBI", priority = "High", description = "description1")
val issue3 = Issue(id = "3", project = "project1", type = "BUG", priority = "Critical", description = "description1")
val listIssues = listOf(issue1, issue2, issue3)
val predicate = ImportantIssuesPredicative("project1")
val result = listIssues.filter(predicate)
println("Result->${result}")

La definición y la instancia del predicado se realiza de la misma forma que en los ejemplos anteriores, residiendo la diferencia en la funcionalidad que se añade en el mótodo invoke; en ella, se utiliza una extensión de la clase String definida en la misma clase.

La salida por consola es la siguiente:

Result->[Issue(id=3, project=project1, type=BUG, priority=Critical, description=description1)]

Para finalizar, destacar el parecido con las funciones first class de Scala y la verbosidad de código Kotlin en comparación a Scala.

Al lector interesado, puede acceder al código completo del ejemplo en el siguiente enlace.

Creación de un proyecto Kotlin con Gradle

Las herramientas de gestión de ciclo de vida difieren en cada lenguaje, o bien, hay herramientas que soportan la gestión para diferentes lenguajes; por ejemplo: sbt, permite la creación y gestión de proyectos en Scala y Java; maven, permite la creación de proyectos Java, Scala o Kotlin; y, Gradle, permite la creación y gestión de proyectos en Scala, Java o Kotlin. En la entrada de hoy, Creación de un proyecto Kotlin con Gradle, me centraré en la definición de un proyecto base en lenguaje Kotlin con Gradle.

1.- Gradle

Gradle es una herramienta open source para la automatización y gestión de ciclo de vida software con el cual podemos definir script en lenguaje Groovy o con un DSL Kotlin. Es una herramienta flexible, rápida, customizable y permite la gestión de proyectos de distinto lenguajes, como pueden ser: Java, Android, Scala, Kootlin,…

Para el lector interesado, la versión actual es la 5.4.1 y la la referencia a la documentación es la siguiente.

1.1.- Instalación de Gradle

El proceso de instalación es un proceso característico en función del entorno de trabajo, ya sea Linux, Windows o Mac. La referencia documental de instalación es la siguiente. 

En mi caso, el entorno de trabajo es sobre sistema Linux, en el cual el proceso de instalación consiste en lo siguiente: realizar la descarga de Gradle, descompresión del fichero descargado sobre un carpeta; y, por último, definir la variable de entorno. Para confirmar la instalación, se ejecuta el siguiente comando en una terminal:

gradle -v

La salida del comando tiene el siguiente aspecto:

 ------------------------------------------------------------
Gradle 5.4.1
------------------------------------------------------------

Build time: 2019-04-26 08:14:42 UTC
Revision: 261d171646b36a6a28d5a19a69676cd098a4c19d

Kotlin: 1.3.21
Groovy: 2.5.4
Ant: Apache Ant(TM) version 1.9.13 compiled on July 10 2018
JVM: 1.8.0_201 (Oracle Corporation 25.201-b09)
OS: Linux 4.6.7-040607-generic amd64

 

2.- Creación del proyecto

Una vez que tenemos Gradle instalado en la máquina, el proceso de creación del proyecto es sencillo y parecido a otras herramientas.

El proceso se define en los siguientes pasos:

  1. Creación de una carpeta del proyecto. La creación del directorio lo realizamos creando desde la línea de comandos una carpeta; en el presente caso, se ejecuta el siguiente comando: mkdir ejem2-kotlin. Una vez ejecutado, nos situamos en el interior de la carpeta mediante el comando cd ejem2-kotlin.
  2. Inicialización del proyecto. La inicialización del proyecto consiste en aquel proceso en el cual se realiza la creación de la estructura del proyecto. Dicho proceso, se realiza ejecutando el siguiente comando: gradle init. La consecuencia de este comando es la ejecución de un wizard en el cual se solicita una serie de cuestiones, entre ellos, selección de lenguaje (permite la creación de proyectos en Java, Korlin o Scala), nombre del proyecto y estructura de carpetas.

La estructura de carpetas del proyecto desde el visor de proyectos del IDE IntelliJ es el siguiente:

La estructura de carpetas y ficheros creados a destacar son los siguientes:

  • src.- Carpeta de código fuente con el aspecto típico, contiene la carpeta src/main/kotlin y src/test/kotlin; además, de las carpetas resources.
  • build.- carpeta con el contenido generado por el compilador kotlin.
  • out.- carpeta con el resultado de la compilación del proyecto.
  • gradle.- carpeta con los wrapper de gradle.
  • .gitignore.- fichero git.
  • build.gradle.kts.- fichero gradle con la configuración.
  • settings.gradle.kts.- fichero con propiedades.
  • gradlew.bat y gradlew.sh.- Fichero de ejecución Gradle del proyecto.

3.- Fichero de construcción build.gradle.kts

El fichero build.gradle.kts es aquel fichero en donde definimos la configuración necesaria para trabajar con el proyecto, como por ejemplo: las dependencias, repositorios y la clase principal.

La estructura del fichero está compuesta por los siguientes apartados:

  • plugins.- definición de las referencias de plugins existentes.
  • repositories.- definición de las referencias a los repositorios de artefactos.
  • dependencies.- definición de los artefactos dependientes en el proyecto.
  • application.- definición de la configuración de la aplicación, en concreto, la definición de la clase principal.

Un ejemplo de fichero build.gradle.kts para un proyecto en lenguaje Kotlin con las dependencias de la librería Arrow es la siguiente:

 import org.jetbrains.kotlin.kapt3.base.Kapt.kapt
/*
* This file was generated by the Gradle 'init' task.
*
* This generated file contains a sample Kotlin application project to get you started.
*/
plugins {
  // Apply the Kotlin JVM plugin to add support for Kotlin on the JVM.
  id("org.jetbrains.kotlin.jvm").version("1.3.21")
  kotlin("kapt") version "1.3.31"
  // Apply the application plugin to add support for building a CLI application.
  application
}
repositories {
  // Use jcenter for resolving your dependencies.
  // You can declare any Maven/Ivy/file repository here.
  jcenter()
  mavenCentral()
}
val arrow_version = "0.9.0"
dependencies {
  // Use the Kotlin JDK 8 standard library.
  implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
  // Use the Kotlin test library.
  testImplementation("org.jetbrains.kotlin:kotlin-test")
  // Use the Kotlin JUnit integration.
  testImplementation("org.jetbrains.kotlin:kotlin-test-junit")
  compile( "io.arrow-kt:arrow-core-data:$arrow_version")
  compile( "io.arrow-kt:arrow-core-extensions:$arrow_version" )
  compile( "io.arrow-kt:arrow-syntax:$arrow_version")
  compile( "io.arrow-kt:arrow-typeclasses:$arrow_version")
  compile( "io.arrow-kt:arrow-extras-data:$arrow_version")
  compile( "io.arrow-kt:arrow-extras-extensions:$arrow_version")
  kapt( "io.arrow-kt:arrow-meta:$arrow_version")
  compile( "io.arrow-kt:arrow-query-language:$arrow_version") //optional
  compile( "io.arrow-kt:arrow-free-data:$arrow_version") //optional
  compile( "io.arrow-kt:arrow-free-extensions:$arrow_version") //optional
  compile( "io.arrow-kt:arrow-mtl:$arrow_version") //optional
  compile( "io.arrow-kt:arrow-effects-data:$arrow_version") //optional
  compile( "io.arrow-kt:arrow-effects-extensions:$arrow_version") //optional
  compile( "io.arrow-kt:arrow-effects-io-extensions:$arrow_version") //optional
  compile( "io.arrow-kt:arrow-effects-rx2-data:$arrow_version") //optional
  compile( "io.arrow-kt:arrow-effects-rx2-extensions:$arrow_version") //optional
  compile( "io.arrow-kt:arrow-effects-reactor-data:$arrow_version") //optional
  compile( "io.arrow-kt:arrow-effects-reactor-extensions:$arrow_version") //optional
  compile( "io.arrow-kt:arrow-optics:$arrow_version") //optional
  compile( "io.arrow-kt:arrow-generic:$arrow_version") //optional
  compile( "io.arrow-kt:arrow-recursion-data:$arrow_version") //optional
  compile( "io.arrow-kt:arrow-recursion-extensions:$arrow_version") //optional
  compile( "io.arrow-kt:arrow-query-language:$arrow_version") //optional
  compile( "io.arrow-kt:arrow-integration-retrofit-adapter:$arrow_version") //optional
}
application {
  // Define the main class for the application.
  mainClassName = "es.ams.AppKt"
}

4.- Comandos Gradle

Gradle es una herramienta con la cual se ejecutan tareas con una funcionalidad determinada. En el presente apartado, identificaré un conjunto de comandos para la ejecución de ciertas tareas mínimas.

Un conjunto de tareas básicas son las siguientes:

  • gradle clean.- Eliminación de la carpeta build.
  • gradle build.- Construcción de los componentes binarios del proyecto.
  • gradle tasks.- visualización de las tareas definidas en el proyecto.
  • gradle properties.- visualización de las propiedades definidas en el proyecto.
  • gradle run.– Ejecución del proyecto.
  • gradle projects.- visualización de la información del proyecto.
  • gradle tests.- Ejecución de los test definidos en el proyecto.

Si se utiliza un IDE como IntelliJ, una vez importado y configurado la ubicación de Gradle, se pueden ejecutar desde la propia herramienta. Un aspecto de la estructura, contenido de clases y dependencias es el que se representa en la siguiente imagen:

5.- Conclusiones

La entrada define aspectos básicos y genéricos de Gradle. El objetivo de la entrada es la creación de un proyecto básico de Gradle en lenguaje Kotlin que permita a una persona realizar la creación de un proyecto y empezar a trabajar en dicho entorno.

Si realizamos un proceso comparativo a alto nivel con otros herramientas, el proceso es similar y, en concreto, me ha resultado una forma de trabajo parecida a la herramienta sbt para la gestión de proyectos en lenguaje Scala.