后端的代码生成。 生成什么,如何生成以及为什么生成?

在本文中,我想展示如何在我们公司中生成一个后端(和一些前端)代码,为什么根本需要它,以及如何做得更好。

我们究竟将产生什么并不那么重要。
重要的是,我们描述3种类型的对象,并在此基础上生成与后端的前端交互,并且在某些情况下,后端实现将完全

这些类型的对象:
1. 消息 -以JSON序列化的对象,参与前端和后端之间的信息交换
2. 端点 -调用前端的URI以及对HTTP方法,请求参数,请求正文类型和响应者类型的描述
3. 实体 -这些实体的消息具有创建/更新/列表/删除(可能不是全部)的标准端点,它们存储在数据库中,并且它们具有数据访问对象或Spring JPA存储库-通常取决于技术,但是对数据库的某种访问

我根本不做前端,但是
1)我知道它是用Typescript编写的,所以我们也生成Typescript类
2)大多数后端需求来自前端开发人员。

规范要求



那么,从前端有什么要求?

1.类似于REST的交互界面
2.单调响应-json,“数据”字段中的有效负载
3.如果后端发生异常,则出现单调错误,建议也添加堆栈跟踪
4.“正确的” HTTP代码-如果找不到书,则返回404,如果请求不正确(例如,无效的json),则返回400,等等。

我将“自行”将要求添加到后端代码中:
1.一处处理错误
2.能够停止代码中任何地方的流并返回所需的HTTP代码
3.我想写一些业务逻辑作为阻塞,而另一些异步的,这取决于所使用的库。 但这一切都应该在一个异步框架中进行
4.建议后端开发人员不要考虑HTTP请求和响应,不要考虑Vertx路由和事件重低音,而只是编写其业务逻辑。

建议通过继承和组合实现上述所有要求,并且只有在失败时才使用代码生成

还建议为typscript和kotlin并行生成类,以便前端始终向后端发送所需的内容(不要依赖开发人员,他们不会忘记向该类添加新字段)

我们将产生什么



例如,假设一个Web应用程序可以保存和编辑书籍,显示书籍列表并按名称搜索。

在技​​术方面,Kotlin后端Vert.x是协程。 就像我在“ Vertx中的异步编程的三种范例”一文中所展示的一样

为了使它更加有趣,我们将基于Spring Data JPA访问数据库。
我并不是说您需要在一个项目中混合使用Spring和Vert.x(尽管我承认是我自己做的),但我只是选择Spring,因为对他来说,最容易显示基于实体的代。

生成的项目结构



现在,您需要制作一个要生成的项目。
我们将有许多gradle项目。 现在,我将它们放在一个git存储库中,但在现实生活中,每个人都应该坐在自己的位置,因为它们会在不同的时间发生变化,它们将具有自己的版本。

因此,第一个项目是带有注释的项目,该注释将指示我们的路由器,HTTP方法等。 称其为metainfo
其他两个项目依赖于此:
程式码api

该API包含路由器和消息的描述-这些类将在后端和前端之间来回移动
codegen是一个代码生成项目(但不是生成代码的项目!)-它包含来自api类和实际代码生成器的信息集合。
生成器将通过参数接收生成的所有详细信息-从哪个包获取路由器的描述,生成到哪个目录,生成的Velocity模板的名称-即 metainfocodegen通常可以在完全不同的项目中使用

好吧,实际上将在其中进行生成的两个项目:
前端生成,在其中我们将生成与我们的kotlin消息相对应的Typescript类
后端 -使用实际的Vertx应用程序。

为了使一个项目“看到”编译另一个项目的结果,我们将使用该插件将工件发布到本地Maven存储库。

Metafinfo项目

我们将用来标记生成源的注释-端点信息,消息,实体的描述:
/* 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 } 

对于Typescript类,我们将定义可以挂在字段上的注释,它们将属于生成的Typescript类
 annotation class IsString annotation class IsEmail annotation class IsBoolean annotation class MaxLength(val len:Int) 


metainfo项目的源代码

api项目:

请注意build.gradle中的noArg和jpa插件,用于生成不带参数的构造函数

我没有足够的幻想,因此我们将为我们的应用程序创建一些疯狂的控制器和实体描述:

 @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 ) 


api项目源代码

Codegen项目:

首先,我们定义“描述符”-我们将通过对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) 


收集信息的代码如下所示:
 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 } } 


嗯,也有一些接收参数的“主”类-应该反映哪些包,要使用哪些Velocity模板,等等。
他们并不是很感兴趣,您可以查看存储库中的所有内容: 源代码

前端生成后端项目中,我们做类似的事情:
1.编译时对api的依赖
2.在构建阶段对codegen的依赖
3.生成模板位于buildSrc目录中,在gradle中需要将文件和代码放入其中,而在构建阶段则需要这些文件和代码,而在编译或运行时阶段则不需要。 即 我们可以更改生成模式而无需重新编译codegen项目
4. frontend- generation编译生成的Typescript并将其发布到npm软件包存储库
5.在后端 ,将生成路由器,这些路由器继承自知道如何处理不同类型请求的非生成抽象路由器。 还生成了必须与业务逻辑本身的实现一起继承的抽象顶点。 另外,生成了我作为程序员不想考虑的各种小事情-在事件低音中注册编解码器和地址常量。
源代码前端生成后端

frontend-generation中,您需要注意在npm存储库中发布生成的源代码的插件。 为此,您需要将存储库的IP放入build.gradle并将身份验证令牌放入.npmrc

生成的类如下所示:
 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; } 


注意类验证器注释tc。

在后端项目中,还生成了Spring Data JPA的存储库,可以说Verticle中的消息处理项目是阻塞的(并通过Vertx.executeBlocking运行)或异步的(带有协程),可以说为Entity生成的Verticle是抽象的,那么有可能覆盖在调用生成的方法之前和之后调用的钩子。 在Spring bean的界面上,Verticles的部署是自动的-简而言之,很多东西。

所有这些都很容易扩展-例如,在端点上挂起角色列表,并在调用端点时生成已登录用户的角色检查等等,而这一切-足以引起想象。

不仅可以生成Vertx,还可以生成Spring,但是生成其他东西也很容易-即使是akka-http,仅在后端项目中更改模板就足够了。

另一个可能的发展方向是产生更多的前端。

所有源代码都在这里

感谢前端的Ildar在我们的项目中创建世代并撰写文章方面的帮助

Source: https://habr.com/ru/post/zh-CN450100/


All Articles