Acelera tu desarrollo en Spring Boot con GitHub Copilot

0
1772

Este artículo esta escrito a medias con Juan Manuel Pérez Toro.

En el anterior artículo Potenciando el Potencial: Cómo la IA puede mejorar la eficiencia de un programador. se nos comenta como usar OpenAI Codex para generar una api con spring boot y se hace una introducción al prompt engineering.

A su vez Luis Merino Ulizarna nos decía como usar copilot en el artículo Github Copilot ¿Nos reemplazará esta IA? Aprende a usarla.

En esta ocasión usaremos el asistente Github Copilot a modo de pair programming con la IA para intentar aumentar la productividad en un proyecto Spring Boot, y veremos como de productivo es.

0. Índice de contenidos.

  1. Introducción.
  2. Entorno.
  3. Primeros pasos.
  4. Creación de clases.
    4.1 Creación del modelo.
    4.2 Creación del repositorio.
    4.3 Creación del servicio.
    4.4 Creación del controlador.
  5. Configuración de la aplicación.
  6. Conclusiones.
  7. Referencias.

1. Introducción

Un resumen de que es Github Copilot seria:

Github Copilot es una herramienta de inteligencia artificial que funciona como un programador de pares. Ofrece sugerencias de autocompletado y estilo mientras se escribe código, ya sea escribiendo el código directamente o a través de comentarios en lenguaje natural que describen lo que se quiere hacer. Copilot analiza el contexto del archivo que se está editando y sugiere código relevante en el editor de texto. Utiliza la tecnología de OpenAI Codex, un sistema de inteligencia artificial desarrollado por OpenAI. Copilot está entrenado en varios lenguajes de programación, y la calidad de las sugerencias puede variar según el volumen y la diversidad de los datos de entrenamiento. La herramienta está disponible como extensión en Visual Studio Code, Visual Studio, Neovim y el conjunto de IDE de JetBrains.

Con todo esto, queremos ver como de productivo puede ser usar Github Copilot en un proyecto Spring Boot, y si es capaz de ayudarnos a aumentar la productividad.

2. Entorno.

El tutorial está escrito usando el siguiente entorno:

  • Hardware: Portátil MacBook Pro 16′ (2,3 GHz 8-Core Intel Core i9 16 GB 2667 MHz DDR4).
  • Sistema Operativo: Mac OS Ventura 13.3.1
  • IntelliJ IDEA 2023.1.1 (Ultimate Edition)

3. Primeros pasos.

Este tutorial se va a realizar sobre IntelliJ usando el plug-in de copilot que podemos ver como instalarlo en el tutorial de Luis Merino Ulizarna.

Github Copilot no es capaz de generar un proyecto, por lo que previamente necesitamos tener un proyecto Spring Boot creado el cual podemos crear con spring initializr.
Vamos a contar con las siguientes dependencias:

  • lombok
  • spring-boot-starter-data-jpa
  • h2
  • spring-boot-starter-web

Siendo todo parte de la última versión hasta la fecha de Spring Boot 3 (3.0.6)

El pom.xml quedaría asi:

<?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>3.0.6</version>
    <relativePath/> <!-- lookup parent from repository -->
  </parent>
  <groupId>com.example</groupId>
  <artifactId>copilot</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <name>copilot</name>
  <description>Demo project for Spring Boot</description>
  <properties>
    <java.version>17</java.version>
  </properties>
  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>

    <dependency>
      <groupId>com.h2database</groupId>
      <artifactId>h2</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-test</artifactId>
      <scope>test</scope>
    </dependency>
  </dependencies>

  <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>
    </plugins>
  </build>

</project>

4. Creación de clases.

4.1 Creación del modelo.

Empezaremos creando el modelo de nuestro proyecto, de esta forma Copilot tendrá contexto del proyecto.

Lo primero que vamos a hacer es crear una clase Student dentro del paquete com.example.copilot.model.

Dentro de la propia clase escribimos el siguiente prompt:

// create a jpa entity class called Student with lombok annotations

Una vista de como se ve sería esta:

ejemplo plugin copilot en intellij
ejemplo plugin copilot en intellij

Vemos que según hemos puesto el prompt él ya nos sugiere abajo otro comentario, a la derecha vemos el plug-in de copilot con diferentes sugerencias (tenemos que pulsar refresh la primera vez que abrimos el plug-in como veremos en una imagen posterior)

Cabe destacar que no siempre da los mismos resultados por lo que puede que no se vean los mismos resultados. Esto tiene una parte de ir probando y de que el asistente vaya conociéndonos.

Si cambiamos el cursor, o pinchamos en otro sitio, nos dice que ha cambiado la entrada.

editor modificado plugin copilot intellij
editor modificado plugin copilot intellij

Al pedir nuevas sugerencias vemos lo siguiente.

nuevos resultados plugin copilot intellij
nuevos resultados plugin copilot intellij

Esto nos da diferentes sugerencias, las podríamos agrupar en varias categorías.

Sugerencias que nos devuelven solo comentarios, que como vemos no es ni siquiera código que podamos ejecutar.

// create a jpa entity class called Student with lombok annotations
// add the
// create a jpa entity class called Student with lombok annotations
// add the attributes id, studentName, studentEmail, studentAge
// add the annotations for the attributes
// add the annotations for the class
// add the annotations for the constructor
// add the annotations for the getters and setters
.....
// create a jpa entity class called Student with lombok annotations
// add a primary key with auto increment
// add a column for first name, last name, email and phone
// generate a constructor with all fields except id
....

Sugerencias incompletas, no nos hacen la clase como tal, pero si nos dan una pista de como hacerla, por lo que vamos a omitir esta sugerencia.
También nos damos cuenta qué usa javax.persistence y en Spring Boot 3 debería usar jakarta, pero no tiene contexto de ello. Que se quede a medio generar puede ser una limitación en cuanto al número de tokens que puede manejar, sobre todo en la sugerencia que veremos tras esta en la que se mete en temas de relaciones entre entidades.

// create a jpa entity class called Student with lombok annotations
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import javax.persistence.*;

@Data
@AllArgsConstructor
@NoArgsConstructor

En esta respuesta pone comentarios sobre el porqué de las cosas y nos indica en qué ruta debería estar cada fichero, lo cual es bastante útil.

// create a jpa entity class called Student with lombok annotations @Data and @NoArgsConstructor
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.*;
import java.util.Set;

@Data
@NoArgsConstructor
@Entity
public class Student {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private String email;

    // add a one-to-many relationship between Student and Course
    // the mappedBy attribute specifies that the Student entity owns the relationship
    // the cascade attribute specifies that all operations should propagate to the related entity
    @OneToMany(mappedBy = "student", cascade = CascadeType.ALL)
    private Set courses;

    // add a one-to-many relationship between Student and Grade
    // the mappedBy attribute specifies that the Student entity owns the relationship
    // the cascade attribute specifies that all operations should propagate to the related entity
    @OneToMany(mappedBy = "student", cascade = CascadeType.ALL)
    private Set grades;
}

// Path: src/main/java/com/example/copilot/model/Grade.java
package com.example.copilot.model;
// create a jpa entity class called Grade with lombok annotations @Data and @NoArgsConstructor
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.*;

@Data
@NoArgsConstructor
@Entity
public class Grade {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private double value;

    // add a many-to-one relationship between Grade and Student
    // the joinColumn attribute specifies the foreign key column in the Grade table
    // the cascade attribute specifies that all operations should propagate to the related entity
    @ManyToOne(cascade = CascadeType.ALL)
    @JoinColumn(name = "student_id", nullable = false)
    private Student student;
}

// Path: src/main/java/com/example/copilot/model/Course.java
package com.example.copilot.model;
// create a jpa entity class called Course with lombok annotations @Data and @NoArgsConstructor
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.*;

@Data
@NoArgsConstructor
@Entity
public class Course {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;

    // add a many-to-one relationship between Course and Student
    // the joinColumn attribute specifies the foreign key column in the Course table
    // the cascade attribute specifies that all operations should propagate to the related entity
    @ManyToOne(cascade

Finalmente, llegamos a una sugerencia que es bastante acertada, pero nos ha puesto unos campos que Github Copilot presupone que son los habituales,
además, seguimos detectando el uso de javax.persistence en vez de jakarta.persistence. Otra cosa que no tiene en cuenta es que @Data ya tiene el @ToString. Pero nos gusta bastante.

// create a jpa entity class called Student with lombok annotations for getters and setters
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;

import javax.persistence.*;

@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
@Entity
public class Student {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;
    private String name;
    private String email;
    private String address;
}

Como podemos observar, en situaciones de poco contexto Github Copilot necesita la supervisión de un desarrollador con conocimientos en la materia,
ya que de las todas sugerencias tan solo 1 es correcta, y aun así no está afinada del todo.

Cabe destacar también, que usando el mismo prompt dos veces nos genera diferentes resultados,
por lo que parece no ser determinista y resulta difícil que sea reproducible.

Por afinar un poco hemos puesto un prompt más específico:

// create a jpa entity class called Student with lombok annotations an id and a name for springboot 3

No vamos a comentar todos los casos como antes, pero sí el más interesante.

// create a jpa entity class called Student with lombok annotations an id and a name for springboot 3
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import javax.persistence.*;

@Data
@AllArgsConstructor
@NoArgsConstructor
@Entity
public class Student {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
}
// create a jpa repository interface called StudentRepository for springboot 3
// Path: src/main/java/com/example/copilot/repository/StudentRepository.java
package com.example.copilot.repository;
// create a jpa repository interface called StudentRepository for springboot 3
import com.example.copilot.model.Student;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface StudentRepository extends JpaRepository {
}
// create a jpa controller called StudentController for springboot 3
// Path: src/main/java/com/example/copilot/controller/StudentController.java
package com.example.copilot.controller;
// create a jpa controller called StudentController for springboot 3
import com.example.copilot.model.Student;
import com.example.copilot.repository.StudentRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/api/v1

En este caso nos ha generado bien la entidad (salvo por el uso de javax.persistence en vez de jakarta.persistence) y además ya nos está sugiriendo el uso de repositorio y controlador.

De su solución, nos vamos a quedar con esto:

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import javax.persistence.*;

@Data
@AllArgsConstructor
@NoArgsConstructor
@Entity
public class Student {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
}

Pero tenemos que hacer unos retoques, ya que ha usado javax.persistence en vez de jakarta.persistence.

El resultado final es este:

package com.example.copilot.model;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
@Entity
public class Student {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
}

4.2 Creación del repositorio.

Ahora continuamos con la creación del repositorio. El repositorio es capaz de generarlo sin pedirle sugerencias. Una vez creado el fichero, lo abrimos y aceptamos la primera opción que nos muestra:

sugerencia plugin copilot intellij
sugerencia plugin copilot intellij

En la imagen podemos ver que según le vamos dando al intro, copilot nos da sugerencias, pulsando tabulador las aceptamos.

El resultado es el siguiente:

package com.example.copilot.repository;

import com.example.copilot.model.Student;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface StudentRepository extends JpaRepository {
}

4.3 Creación del servicio.

La interfaz del servicio nos la ha generado a base de sugerencias como ocurrió con el repositorio, como vemos, es capaz de generar las operaciones básicas para un CRUD

package com.example.copilot.service;

import com.example.copilot.model.Student;

import java.util.List;

public interface StudentService {
    List getAllStudents();
    Student getStudentById(Long id);
    Student createStudent(Student student);
    Student updateStudent(Student student);
    void deleteStudent(Long id);
}

Además de eso, nos ha sugerido la implementación del servicio, pero se ha quedado a medias y siempre en forma de comentario.
También nos dice como llamar el fichero y en qué ruta debería estar con base en eso.

// Compare this snippet from src/main/java/com/example/copilot/service/StudentServiceImpl.java:
// package com.example.copilot.service;
//
// import com.example.copilot.model.Student;
// import com.example.copilot.repository.StudentRepository;
// import org.springframework.stereotype.Service;
//
// import java.util.List;
//
// @Service
// public class StudentServiceImpl implements StudentService {
//     private final StudentRepository studentRepository;
//
//     public StudentServiceImpl(StudentRepository studentRepository) {
//         this.studentRepository = studentRepository;
//     }
//
//     @Override
//     public List getAllStudents() {
//         return studentRepository.findAll();
//     }
//
//     @Override
//     public Student getStudentById(Long id) {

Con esos comentarios puestos en el fichero que ha sugerido, pero sin los comentarios, nos ha generado la implementación:

package com.example.copilot.service;

import com.example.copilot.model.Student;
import com.example.copilot.repository.StudentRepository;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class StudentServiceImpl implements StudentService {
    private final StudentRepository studentRepository;

    public StudentServiceImpl(StudentRepository studentRepository) {
        this.studentRepository = studentRepository;
    }

    @Override
    public List getAllStudents() {
        return studentRepository.findAll();
    }

    @Override
    public Student getStudentById(Long id) {
        return studentRepository.findById(id).orElseThrow();
    }

    @Override
    public Student createStudent(Student student) {
        return studentRepository.save(student);

    }

    @Override
    public Student updateStudent(Student student) {
        return studentRepository.save(student);

    }

    @Override
    public void deleteStudent(Long id) {
        studentRepository.deleteById(id);

    }

}

4.4 Creación del controlador.

En principio, el controlador se ha ido generando, pero, se ha quedado en los RequestsMapping, por lo que, lo completamos con lo que nos sugiere, pero tenemos que terminar nosotros.

package com.example.copilot.controller;

import com.example.copilot.model.Student;
import com.example.copilot.service.StudentService;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/api/v1/students")

Poniendo el prompt siguiente:

// implement a method to get all students

Nos ha generado el siguiente código que excede la petición:

public class StudentController {
    private final StudentService studentService;

    public StudentController(StudentService studentService) {
        this.studentService = studentService;
    }
    @GetMapping
    public List getAllStudents(){
        return studentService.getAllStudents();
    }
    @GetMapping("/{id}")
    public Student getStudentById(@PathVariable Long id){
        return studentService.getStudentById(id);
    }
    @PostMapping
    public Student createStudent(@RequestBody Student student){
        return studentService.createStudent(student);
    }
    @PutMapping
    public Student updateStudent(@RequestBody Student student){
        return studentService.updateStudent(student);
    }
    @DeleteMapping("/{id}")
    public void deleteStudent(@PathVariable Long id){
        studentService.deleteStudent(id);
    }
}

5. Configuración de la aplicación.

Queremos generar un pequeño fichero de application.properties para nuestra aplicación Spring Boot, para ello escribimos el siguiente prompt:

// generate application.properties with h2 database config for spring boot

Pero al escribir la palabra generate, ha creado código Javascript y Java que generaría el fichero, si queremos que nos dé un fichero de configuración en texto,
tendríamos que pasarle el mismo prompt pero sin la palabra generate:

// application.properties with h2 database config for spring boot

Esto nos ha devuelto varias sugerencias, pero también nos ha devuelto trazas de código de ejecuciones en otras máquinas, y aquí vemos uno de los peligros actuales de las IA como le pasó a Samsung.

Traza de error de otra applicación

Siguiendo el autocompletado se puede configurar la base de datos, pero también empieza a configurar hibernate. Quedando la configuración básica así:

spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=password
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect

6. Conclusiones.

  • Copilot es capaz de generar código, pero con bastante supervisión, por lo que necesita de un programador para qué le guie en el proceso.
  • No nos ha ido generando tests para darnos la seguridad de que en caso de refactorizar el código, no se rompa nada. Pero con los prompts adecuados los generaría.
  • No nos ha generado el fichero de configuración de la base de datos, pero si nos ha sugerido como hacerlo.
  • Al ser solo un acompañante, la arquitectura corre de cuanta del programador.
  • Este es un ejemplo sencillo y ayuda a quitar mucho trabajo repetitivo, pero no es capaz de generar una aplicación completa.
  • Al estar limitado su entrenamiento, no usa la última versión de spring boot aunque una vez que usamos las anotaciones de Jakarta si las fue usando.

7. Referencias.

Potenciando el Potencial: Cómo la IA puede mejorar la eficiencia de un programador,
Github Copilot ¿Nos reemplazará esta IA? Aprende a usarla. Luis Merino Ulizarna.
Documentación de copilot.

DEJA UNA RESPUESTA

Por favor ingrese su comentario!

He leído y acepto la política de privacidad

Por favor ingrese su nombre aquí

Información básica acerca de la protección de datos

  • Responsable:
  • Finalidad:
  • Legitimación:
  • Destinatarios:
  • Derechos:
  • Más información: Puedes ampliar información acerca de la protección de datos en el siguiente enlace:política de privacidad