Neste artigo, quero mostrar como um código de back-end (e um pouco de front-end) é gerado em nossa empresa, por que é necessário e como fazê-lo melhor.
O que exatamente geraremos não é tão importante.
É importante descrevermos três tipos de objetos com base nos quais geraremos interação de front-end com o back-end e, em alguns lugares, a implementação do back-end será completamente
Estes tipos de objetos:
1.
Mensagens - objetos que, sendo serializados no JSON, participam da troca de informações entre o front-end e o back-end
2.
Pontos de extremidade - URI que chama o front-end junto com uma descrição do método HTTP, parâmetros de solicitação, tipo de corpo da solicitação e tipo de resposta
3.
Entidades - são mensagens para as quais eles têm pontos de extremidade padrão para Criar / Atualizar / Lista / Excluir (talvez não todos), são armazenados no banco de dados e para eles existe um Data Access Object, ou repositório Spring JPA - geralmente depende de tecnologia, mas algum tipo de acesso ao banco de dados
Eu não faço front-end, mas
1) Eu sei que ele está escrito em Typecript, então também geramos classes de Typecript
2) A maioria dos requisitos de back-end vem de desenvolvedores de front-end.
Requisitos de código
Então, quais são os requisitos do front end?
1. Interface de interação semelhante a REST
2. Respostas monótonas - json, carga útil no campo 'dados'
3. Erros monótonos se uma exceção ocorreu no back-end, é aconselhável adicionar também rastreamento de pilha
4. códigos HTTP "corretos" - 404 se o livro não for encontrado, 400 se o pedido for ruim (por exemplo, json inválido) etc.
Adicionarei requisitos ao código de back-end "por conta própria":
1. Tratamento de erros em um só lugar
2. Capacidade de interromper o fluxo em qualquer lugar do código e retornar o código HTTP desejado
3. Quero escrever algumas lógicas de negócios como bloqueio e outras como assíncronas, dependendo das bibliotecas usadas. Mas tudo isso deve funcionar em uma estrutura assíncrona
4. É aconselhável que os desenvolvedores de back-end não pensem nas solicitações e respostas HTTP, nas rotas da Vertx e nos baixos de eventos, mas simplesmente escrevam sua lógica de negócios.
É recomendável implementar todos os requisitos acima com herança e composição e, somente onde houver falha, use a geração de código
Também é aconselhável gerar classes em paralelo para typscript e kotlin, para que o front-end sempre envie ao back-end o que é necessário (e não conte com os desenvolvedores para que não esqueçam de adicionar um novo campo à classe)
O que vamos gerar
Por exemplo, pegue um aplicativo da web hipotético que pode salvar e editar livros, exibir sua lista e pesquisar por nome.
Em termos de tecnologia, o back-end da Kotlin, Vert.x, coroutines. Algo parecido com o que mostrei no artigo
"Três paradigmas de programação assíncrona no Vertx"Para torná-lo mais interessante, teremos acesso ao banco de dados com base no Spring Data JPA.
Não estou dizendo que você precisa misturar o Spring e o Vert.x em um projeto (embora eu o faça pessoalmente, admito), mas apenas uso o Spring, pois é mais fácil para ele mostrar a geração baseada em Entidades.
Estrutura do projeto com geração
Agora você precisa fazer um projeto para geração.
Teremos muitos projetos gradle. Agora vou transformá-los em um repositório git, mas na vida real todos devem se sentar sozinhos, porque mudarão em momentos diferentes, terão suas próprias versões.
Portanto, o primeiro projeto é um projeto com anotações que indicarão nossos roteadores, métodos HTTP etc. Chame de
metainfoDois outros projetos dependem disso:
codegen e
apiA API contém descrições de roteadores e mensagens - aquelas classes que vão e voltam entre o back-end e o front-end
codegen é um projeto de geração de código (mas não um projeto no qual o código é gerado!) - contém a coleta de informações das classes
api e dos geradores de código reais.
Os geradores receberão todos os detalhes da geração nos argumentos - de qual pacote levar as descrições dos roteadores, para qual diretório gerar, que nome do modelo Velocity será gerado - ou seja,
metainfo e
codegen geralmente podem ser usados em projetos completamente diferentes
Bem, dois projetos em que a geração será efetivamente realizada:
gerado pela
interface na qual iremos gerar a classe Typescript que corresponde às nossas mensagens kotlin
e
back -
end - com o aplicativo Vertx real.
Para que um projeto “veja” o resultado da compilação de outro, usaremos o plugin para publicar artefatos no repositório local do Maven.
Projeto Metafinfo :
Anotações com as quais marcaremos fontes de geração - descrições de terminais, mensagens, entidades:
annotation class EndpointController(val url:String) annotation class Endpoint(val method: HttpMethodName, val param: String = "") 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 as classes Typescript, definiremos anotações que podem ser penduradas nos campos e que cairão na classe Typescript gerada
annotation class IsString annotation class IsEmail annotation class IsBoolean annotation class MaxLength(val len:Int)
O código fonte do projeto metainfoProjeto API :Observe os plugins noArg e jpa em build.gradle para gerar construtores sem argumentos
Como não tenho fantasia suficiente, criaremos algumas descrições malucas de controladores e entidades para nosso aplicativo:
@EndpointController("/util") interface SearchRouter { @Endpoint(HttpMethodName.GET, param = "id") fun search(id: String): String @Endpoint(method = HttpMethodName.POST) @AsyncHandler fun search(searchRequest: SearchRequest)
Código-fonte do projeto APIProjeto Codegen :Primeiro, definimos os “descritores” - as classes que iremos preencher passando pela reflexão em nosso projeto 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)
O código que coleta as informações fica assim:
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 } }
Bem, também existem classes “principais” que recebem argumentos - em quais pacotes devem ser refletidos, em quais modelos do Velocity usar etc.
Eles não são tão interessantes, você pode ver tudo no repositório:
Código-fonteNos projetos gerados por front- end e back - end , fazemos coisas semelhantes:
1. dependência da
API no momento da compilação
2. dependência de
codegen na fase de construção
3. Os modelos de geração estão localizados no diretório
buildSrc , no qual arquivos e códigos são necessários em gradle, que são necessários no estágio de construção, mas não no estágio de compilação ou tempo de execução. I.e. podemos mudar o padrão de geração sem recompilar o projeto
codegen4.
Frontend-gerado compila o Typescript gerado e o publica no repositório do pacote npm
5. No
back -
end , são gerados roteadores que herdam de um roteador abstrato não gerado que sabe como lidar com diferentes tipos de solicitações. Resumo Também são gerados vértices que devem ser herdados com a implementação da própria lógica de negócios. Além disso, são gerados todos os tipos de pequenas coisas que eu, como programador, não quero pensar - registrar codecs e constantes de endereço no baixo do evento.
Código
- fonte
frontend-gerado e
back - endNo
frontend gerado, você precisa prestar atenção ao plug-in que publica as fontes geradas no repositório npm. Para que isso funcione, você precisa colocar o IP do seu repositório em build.gradle e colocar seu token de autenticação em .npmrc
As classes geradas são assim:
import { IsString, MaxLength, IsDate, IsArray, } from 'class-validator'; import { Type } from 'class-transformer';
Preste atenção às anotações do validador de classe tc.
No projeto de back-end, também são gerados repositórios para o Spring Data JPA, é possível dizer que o projeto de processamento de mensagens no Verticle está bloqueando (e executado através do Vertx.executeBlocking) ou assíncrono (com corotinas), é possível dizer que o Verticle gerado para a Entidade é abstrato e existe a possibilidade substituir ganchos chamados antes e depois de chamar os métodos gerados. A implantação de vértices é automática na interface do spring beans - bem, em suma, muitos brindes.
E tudo isso é fácil de expandir - por exemplo, pendurar uma lista de funções nos pontos de extremidade e gerar uma verificação da função do usuário conectado ao chamar o terminal e muito mais - o que é suficiente para a imaginação.
Também é fácil gerar não o Vertx, nem o Spring, mas outra coisa - mesmo akka-http, basta alterar apenas os modelos no projeto de back-end.
Outra direção de desenvolvimento possível é gerar mais front-end.
Todo o código fonte está
aqui .
Agradecemos a Ildar, do front-end, pela ajuda na criação da geração em nosso projeto e na redação do artigo.