Generación de código para el backend. ¿Qué generar, cómo y por qué?

En este artículo quiero mostrar cómo se genera un código de back-end (y un poco de front-end) en nuestra empresa, por qué es necesario y cómo hacerlo mejor.

Lo que exactamente generaremos no es tan importante.
Es importante que describamos 3 tipos de objetos sobre la base de los cuales generaremos interacción frontend con el backend, y en algunos lugares la implementación del backend será completamente

Estos tipos de objetos:
1. Mensajes : objetos que, al ser serializados en JSON, participan en el intercambio de información entre el frontend y el backend
2. Puntos finales: URI que llama al front-end junto con una descripción del método HTTP, parámetros de solicitud, tipo de cuerpo de solicitud y tipo de respuesta
3. Entidades : estos son mensajes para los que tienen puntos finales estándar para Crear / Actualizar / Listar / Eliminar (tal vez no todos), se almacenan en la base de datos y para ellos hay un Objeto de acceso a datos o un repositorio Spring JPA, generalmente depende de tecnología, pero algún tipo de acceso a la base de datos

No hago front-end en absoluto, pero
1) Sé que está escrito en mecanografiado, por lo que también generamos clases mecanografiadas
2) La mayoría de los requisitos de backend provienen de desarrolladores frontend.

Requisitos del código



Entonces, ¿cuáles son los requisitos del front end?

1. Interfaz de interacción tipo REST
2. Respuestas monótonas: json, carga útil en el campo 'datos'
3. Errores monótonos si se produce una excepción en el backend, también es aconsejable agregar el seguimiento de la pila
4. Códigos HTTP "correctos": 404 si no se encuentra el libro, 400 si la solicitud es incorrecta (por ejemplo, json no válido), etc.

Agregaré requisitos al código de back-end "por mi cuenta":
1. Manejo de errores en un solo lugar
2. Capacidad para detener el flujo en cualquier parte del código y devolver el código HTTP deseado
3. Quiero escribir algo de lógica de negocios como bloqueo, y algo como asíncrono, dependiendo de las bibliotecas utilizadas. Pero todo esto debería funcionar en un marco asincrónico
4. Es aconsejable que los desarrolladores de backend no piensen en las solicitudes y respuestas HTTP, en las rutas de Vertx y en los bajos de eventos, sino que simplemente escriban su lógica de negocios.

Es aconsejable implementar todos los requisitos anteriores con herencia y composición, y solo cuando falle, use la generación de código

También es recomendable generar clases en paralelo para typscript y kotlin para que la interfaz siempre envíe al backend lo que se necesita (y no confíe en los desarrolladores para que no se olviden de agregar un nuevo campo a la clase)

¿Qué vamos a generar?



Por ejemplo, tome una aplicación web hipotética que pueda guardar y editar libros, mostrar su lista y buscar por nombre.

En términos de tecnología, el backend de Kotlin, Vert.x, corutinas. Algo como lo que mostré en el artículo "Tres paradigmas de programación asincrónica en Vertx"

Para hacerlo más interesante, haremos acceso a la base de datos basada en Spring Data JPA.
No digo que necesite mezclar Spring y Vert.x en un proyecto (aunque lo hago yo mismo, lo admito), pero solo tomo Spring, ya que es más fácil para él mostrar generación basada en Entidades.

Estructura del proyecto con generación.



Ahora necesitas hacer un proyecto para la generación.
Tendremos muchos proyectos de gradle. Ahora los haré en un repositorio de git, pero en la vida real cada uno debería sentarse solo, porque cambiarán en diferentes momentos, tendrán sus propias versiones.

Entonces, el primer proyecto es un proyecto con anotaciones que indicarán nuestros enrutadores, métodos HTTP, etc. Llámalo metainfo
Otros dos proyectos dependen de ello:
codegen y api

La API contiene descripciones de enrutadores y mensajes, esas clases que irán de un lado a otro entre el backend y el frontend.
codegen es un proyecto de generación de código (¡pero no un proyecto en el que se genera el código!): contiene la recopilación de información de las clases api y los generadores de código reales.
Los generadores recibirán todos los detalles de la generación en argumentos, de qué paquete tomar la descripción de los enrutadores, a qué directorio generar, qué nombre de la plantilla Velocity para la generación, es decir metainfo y codegen generalmente se pueden usar en proyectos completamente diferentes

Bueno, dos proyectos en los que la generación se llevará a cabo realmente:
generado por frontend en el que generaremos la clase de Script mecanografiado que corresponde a nuestros mensajes de kotlin
y backend : con la aplicación Vertx real.

Para que un proyecto "vea" el resultado de compilar otro, utilizaremos el complemento para publicar artefactos en el repositorio local de Maven.

Proyecto Metafinfo :

Anotaciones con las que marcaremos las fuentes de generación: descripciones de puntos finales, mensajes, entidades:
/* Contains a number of endpoints. We will generate Vert.x router or Spring MVC controller from it*/ annotation class EndpointController(val url:String) /* Endpoint inside a controller. Concrete URI and HTTP method. May be has query param */ annotation class Endpoint(val method: HttpMethodName, val param: String = "") /* For empty constructor generation */ annotation class EmptyConstructorMessage /* Make abstract implementation method for endpoint logic asynchronous */ annotation class AsyncHandler /* All the next annotations are for Entities only:*/ annotation class GenerateCreate annotation class GenerateUpdate annotation class GenerateGetById annotation class GenerateList annotation class GenerateDelete /* Make CRUD implementation abstract, so that we will override it*/ annotation class AbstractImplementation /* Generate search by this field in DAO layer */ annotation class FindBy /* This entity is child of another entity, so generate end point like /parent/$id/child to bring all children of concrete parent instead of /child - bring all entities of this type */ annotation class ChildOf(vararg val parents: KClass<*>) enum class HttpMethodName { POST,PUT,GET,DELETE } 

Para las clases de Tipos de letra, definiremos anotaciones que se pueden colgar en los campos y que se incluirán en la clase de Tipos de letra generada
 annotation class IsString annotation class IsEmail annotation class IsBoolean annotation class MaxLength(val len:Int) 


El código fuente del proyecto metainfo

Proyecto api :

Tenga en cuenta los complementos noArg y jpa en build.gradle para generar constructores sin argumentos

No tengo suficiente fantasía, por lo que crearemos algunas descripciones locas de controladores y entidades para nuestra aplicación:

 @EndpointController("/util") interface SearchRouter { @Endpoint(HttpMethodName.GET, param = "id") fun search(id: String): String @Endpoint(method = HttpMethodName.POST) @AsyncHandler fun search(searchRequest: SearchRequest) // we have no check or response type } data class SearchRequest( @field:IsString val author: String?, @field:IsEmail val someEmail: String, @field:IsString val title: String? ) @GenerateList @GenerateGetById @GenerateUpdate @Entity @AbstractImplementation data class Book( @Id @GeneratedValue(strategy = GenerationType.IDENTITY) var id: Long?, @field:IsBoolean @Column(name = "is_deleted") var hardcover: Boolean, @field:IsString @field:MaxLength(128) @Column(nullable = false, length = 128) val title: String, @field:IsString @field:MaxLength(128) @Column(nullable = false, length = 255) val author: String ) @GenerateList @GenerateGetById @GenerateUpdate @GenerateDelete @GenerateCreate @Entity @ChildOf(Book::class) data class Chapter( @Id @GeneratedValue(strategy = GenerationType.IDENTITY) var id: Long?, @Column(nullable = false, name = "book_id") var bookId: Long?, @field:IsString @field:MaxLength(128) @Column(nullable = false, length = 128) @field:FindBy val name: String, @Column(nullable = false) val page:Int ) 


Código fuente del proyecto api

Proyecto Codegen :

Primero definimos los "descriptores": aquellas clases que completaremos al reflexionar sobre nuestro proyecto de API:

 data class EndPoint( val url: String, val input: String?, val param: String?, val output: String, val method: String, val handler: String, val asyncHandler: Boolean ) data class Router(val name: String, val url: String, val endpoints: List<EndPoint>) class Entity( name: String, val parents: List<String>, val abstractVerticle: Boolean, val crudFeatures: CrudFeatures, fields: List<Field>, var children: List<Entity> ) : Message(name, fields) { fun shouldGenerateRouterAndVerticle(): Boolean { return crudFeatures.generateRouterAndVerticle() } override fun toString(): String { return "Entity(parents=$parents, abstractVerticle=$abstractVerticle, crudFeatures=$crudFeatures, children=$children)" } } data class CrudFeatures( val list: Boolean, val create: Boolean, val update: Boolean, val delete: Boolean, val get: Boolean ) { fun generateRouterAndVerticle(): Boolean { return list || create || update || delete || get } } open class Message(val name: String, val fields: List<Field>) data class Field(val name: String, val type: String, val validators: List<Annotation>, val findBy: Boolean) 


El código que recopila la información se ve así:
 class EntitiesCreator(typeMapper: TypeMapper, frontendAnnoPackage:String) { private val messagesDescriptor = MessagesCreator(typeMapper, frontendAnnoPackage) fun createEntities(entitiesPackage: String): List<Entity> { val reflections = Reflections(entitiesPackage, SubTypesScanner(false)) val types = reflections.getSubTypesOf(Object::class.java) return types.map { createEntity(it) } } fun createEntityRestEndpoints(entity: Entity): List<EndPoint> { val name = entity.name val url = name.toLowerCase() val endpoints: MutableList<EndPoint> = mutableListOf() if (entity.crudFeatures.create) { endpoints.add( EndPoint(url, name, null, name, "post", "handleNew$name", false) ) } if (entity.crudFeatures.get) { endpoints.add( EndPoint( "$url/:id", null, "id", name, "get", "handleGet$name", false ) ) } if (entity.crudFeatures.update) { endpoints.add( EndPoint(url, name, null, name, "put", "handleUpdate$name", false) ) } if (entity.crudFeatures.delete) { endpoints.add( EndPoint( "$url/:id", null, "id", "", "delete", "handleDelete$name", false ) ) } if (entity.crudFeatures.list) { if (entity.parents.isEmpty()) { endpoints.add( EndPoint( url, null, null, "List<$name>", "get", "handleGetAllFor$name", false ) ) } } entity.children.forEach { endpoints.add( EndPoint( "$url/:id/${it.name.toLowerCase()}", null, "id", "List<$name>", "get", "handleGet${it.name}For$name", false ) ) } return endpoints } private fun createEntity(aClass: Class<*>): Entity { return Entity( aClass.simpleName, getParents(aClass), isVerticleAbstract(aClass), shouldGenerateCrud(aClass), messagesDescriptor.createFields(aClass), listOf() ) } private fun isVerticleAbstract(aClass: Class<*>): Boolean { return aClass.getDeclaredAnnotation(AbstractImplementation::class.java) != null } private fun getParents(aClass: Class<*>): List<String> { return aClass.getDeclaredAnnotation(ChildOf::class.java)?.parents?.map { it.simpleName }?.requireNoNulls() ?: listOf() } private fun shouldGenerateCrud(aClass: Class<*>): CrudFeatures { val listAnno = aClass.getDeclaredAnnotation(GenerateList::class.java) val createAnno = aClass.getDeclaredAnnotation(GenerateCreate::class.java) val getAnno = aClass.getDeclaredAnnotation(GenerateGetById::class.java) val updateAnno = aClass.getDeclaredAnnotation(GenerateUpdate::class.java) val deleteAnno = aClass.getDeclaredAnnotation(GenerateDelete::class.java) return CrudFeatures( list = listAnno != null, create = createAnno != null, update = updateAnno != null, delete = deleteAnno != null, get = getAnno != null ) } } class MessagesCreator(private val typeMapper: TypeMapper, private val frontendAnnotationsPackageName: String) { fun createMessages(packageName: String): List<Message> { val reflections = Reflections(packageName, SubTypesScanner(false)) return reflections.allTypes.map { Class.forName(it) }.map { createMessages(it) } } private fun createMessages(aClass: Class<*>): Message { return Message(aClass.simpleName, createFields(aClass)) } fun createFields(aClass: Class<*>): List<Field> { return ReflectionUtils.getAllFields(aClass).map { createField(it) } } private fun createField(field: java.lang.reflect.Field): Field { val annotations = field.declaredAnnotations return Field( field.name, typeMapper.map(field.type), createConstraints(annotations), annotations.map { anno -> anno::annotationClass.get() }.contains(FindBy::class) ) } private fun createConstraints(annotations: Array<out Annotation>): List<Annotation> { return annotations.filter { it.toString().startsWith("@$frontendAnnotationsPackageName") } } } class RoutersCreator(private val typeMapper: TypeMapper, private val endpointsPackage:String ) { fun createRouters(): List<Router> { val reflections = Reflections(endpointsPackage, SubTypesScanner(false)) return reflections.allTypes.map { createRouter( Class.forName( it ) ) } } private fun createRouter(aClass: Class<*>): Router { return Router(aClass.simpleName, getUrl(aClass), ReflectionUtils.getAllMethods(aClass).map { createEndpoint(it) }) } private fun getUrl(aClass: Class<*>): String { return aClass.getAnnotation(EndpointController::class.java).url } private fun getEndPointMethodName(declaredAnnotation: Endpoint?): String { val httpMethodName = declaredAnnotation?.method return (httpMethodName ?: HttpMethodName.GET).name.toLowerCase() } private fun getParamName(declaredAnnotation: Endpoint?): String { val paramName = declaredAnnotation?.param return (paramName ?: "id") } private fun createEndpoint(method: Method): EndPoint { val types = method.parameterTypes val declaredAnnotation: Endpoint? = method.getDeclaredAnnotation(Endpoint::class.java) val methodName = getEndPointMethodName(declaredAnnotation) var url = method.name var input: String? = null var param: String? = null val hasInput = types.isNotEmpty() val handlerName = "$methodName${StringUtils.capitalize(url)}" if (hasInput) { val inputType = types[0] val inputTypeName = typeMapper.map(inputType) val createUrlParameterName = inputType == java.lang.String::class.java if (createUrlParameterName) { param = getParamName(declaredAnnotation) url += "/:$param" } else { input = simpleName(inputTypeName) } } return EndPoint( url, input, param, method.returnType.toString(), methodName, handlerName, isHandlerAsync(method) ) } private fun isHandlerAsync(method: Method): Boolean { val declaredAnnotation: AsyncHandler? = method.getDeclaredAnnotation(AsyncHandler::class.java) return declaredAnnotation != null } private fun simpleName(name: String): String { val index = name.lastIndexOf(".") return if (index >= 0) name.substring(index + 1) else name } } 


Bueno, también hay clases "principales" que reciben argumentos: qué paquetes deben reflejarse, qué plantillas de Velocity usar, etc.
No son tan interesantes, puede ver todo en el repositorio: código fuente

En proyectos generados por frontend y backend , hacemos cosas similares:
1. dependencia de la API en tiempo de compilación
2. dependencia del codegen en la etapa de construcción
3. Las plantillas de generación se encuentran en el directorio buildSrc en el que se necesitan archivos y códigos en gradle, que se necesitan en la etapa de compilación, pero no en la etapa de compilación o tiempo de ejecución. Es decir podemos cambiar el patrón de generación sin recompilar el proyecto codegen
4. Frontend- Geneiled compila el mecanografiado generado y lo publica en el repositorio de paquetes npm
5. En el backend , se generan enrutadores que heredan de un enrutador abstracto no generado que sabe cómo manejar diferentes tipos de solicitudes. También se generan Verticles abstractos que deben heredarse con la implementación de la lógica de negocios en sí. Además, se generan todo tipo de pequeñas cosas en las que yo, como programador, no quiero pensar: registrar códecs y constantes de direcciones en el bajo del evento.
Código fuente generado por frontend y backend

En frontend generado, debe prestar atención al complemento que publica las fuentes generadas en el repositorio npm. Para que esto funcione, debe poner la IP de su repositorio en build.gradle y poner su token de autenticación en .npmrc

Las clases generadas se ven así:
 import { IsString, MaxLength, IsDate, IsArray, } from 'class-validator'; import { Type } from 'class-transformer'; // Entity(parents=[], abstractVerticle=false, crudFeatures=CrudFeatures(list=true, create=true, update=true, delete=true, get=true), children=[]) export class Chapter { // Field(name=bookId, type=number, validators=[], findBy=false) bookId!: number; @IsString() @MaxLength(128) // Field(name=name, type=string, validators=[@com.valapay.test.annotations.frontend.IsString(), @com.valapay.test.annotations.frontend.MaxLength(len=128)], findBy=false) name!: string; // Field(name=id, type=number, validators=[], findBy=false) id!: number; // Field(name=page, type=number, validators=[], findBy=false) page!: number; } 


Presta atención a las anotaciones de validación de clase tc.

En el proyecto de back-end, también se generan repositorios para Spring Data JPA, es posible decir que el proyecto de procesamiento de mensajes en Verticle está bloqueando (y se ejecuta a través de Vertx.executeBlocking) o asíncrono (con corutinas), es posible decir que el Verticle generado para Entity es abstracto y luego existe la posibilidad anular los ganchos que se llaman antes y después de llamar a los métodos generados. El despliegue de Verticles es automático en la interfaz de spring beans, bueno, en resumen, muchas cosas buenas.

Y todo esto es fácil de expandir, por ejemplo, colgar una lista de roles en los Endpoints y generar una verificación del rol del usuario conectado al llamar al endpoint, y mucho más, lo cual es suficiente para la imaginación.

También es fácil generar no Vertx, no Spring, sino algo más, incluso akka-http, es suficiente solo cambiar las plantillas en el proyecto de back-end.

Otra posible dirección de desarrollo es generar más front-end.

Todo el código fuente está aquí .

Gracias a Ildar desde el front-end por ayudarnos a crear la generación en nuestro proyecto y al escribir el artículo.

Source: https://habr.com/ru/post/450100/


All Articles