Pada artikel ini saya ingin menunjukkan bagaimana kode backend (dan sedikit front-end) dihasilkan di perusahaan kami, mengapa diperlukan sama sekali dan bagaimana melakukannya dengan lebih baik.
Apa yang akan kita hasilkan sebenarnya tidak begitu penting.
Adalah penting bahwa kita menggambarkan 3 jenis objek berdasarkan yang kita akan menghasilkan interaksi frontend dengan backend, dan di beberapa tempat implementasi backend akan sepenuhnya
Jenis-jenis objek:
1.
Pesan - objek yang, bersambung dalam JSON, berpartisipasi dalam pertukaran informasi antara frontend dan backend
2.
Endpoint - URI yang memanggil front-end bersama dengan deskripsi metode HTTP, parameter permintaan, tipe Badan Permintaan, dan tipe responden
3.
Entitas - Ini adalah pesan yang memiliki titik akhir standar untuk Buat / Perbarui / Daftar / Hapus (mungkin tidak semua), mereka disimpan dalam database dan bagi mereka ada Objek Akses Data, atau repositori Spring JPA - umumnya tergantung pada teknologi, tetapi semacam akses ke database
Saya tidak melakukan front-end sama sekali, tetapi
1) Saya tahu bahwa ini ditulis dalam naskah, jadi kami juga menghasilkan kelas naskah
2) Sebagian besar persyaratan backend berasal dari pengembang frontend.
Persyaratan kode
Jadi, apa saja persyaratan dari ujung depan?
1. Antarmuka interaksi seperti REST
2. Respons yang monoton - json, muatan di bidang 'data'
3. Kesalahan monoton jika pengecualian terjadi pada backend, disarankan juga menambahkan stack trace
4. Kode HTTP "benar" - 404 jika buku tidak ditemukan, 400 jika permintaannya buruk (katakanlah, json tidak valid), dll.
Saya akan menambahkan persyaratan ke kode backend "sendiri":
1. Kesalahan penanganan di satu tempat
2. Kemampuan untuk menghentikan aliran di mana saja dalam kode dan mengembalikan kode HTTP yang diinginkan
3. Saya ingin menulis beberapa logika bisnis sebagai pemblokiran, dan beberapa sebagai asinkron, tergantung pada perpustakaan yang digunakan. Tetapi semua ini harus bekerja dalam satu kerangka asinkron
4. Dianjurkan agar pengembang backend tidak memikirkan permintaan dan tanggapan HTTP, tentang rute Vertx dan bass acara, tetapi cukup tulis logika bisnis mereka.
Dianjurkan untuk menerapkan semua persyaratan di atas dengan pewarisan dan komposisi, dan hanya jika gagal, gunakan pembuatan kode
Juga disarankan untuk membuat kelas secara paralel untuk typscript dan kotlin sehingga frontend selalu mengirimkan backend apa yang dibutuhkan (dan tidak bergantung pada pengembang sehingga mereka tidak akan lupa menambahkan bidang baru ke kelas)
Apa yang akan kita hasilkan
Misalnya, ambil aplikasi web hipotetis yang dapat menyimpan dan mengedit buku, menampilkan daftar mereka dan mencari berdasarkan nama.
Dalam hal teknologi, backend Kotlin, Vert.x, coroutine. Sesuatu seperti apa yang saya perlihatkan dalam artikel
“Tiga Paradigma Pemrograman Asinkron di Vertx”Untuk membuatnya lebih menarik, kami akan membuat akses ke database berdasarkan Spring Data JPA.
Saya tidak mengatakan bahwa Anda perlu mencampur Spring dan Vert.x dalam satu proyek (walaupun saya melakukannya sendiri, saya akui), tetapi saya hanya mengambil Spring karena paling mudah baginya untuk menunjukkan generasi berdasarkan Entitas.
Struktur proyek dengan generasi
Sekarang Anda perlu membuat proyek untuk generasi.
Kami akan memiliki banyak proyek gradle. Sekarang saya akan membuatnya dalam satu repositori git, tetapi dalam kehidupan nyata setiap orang harus duduk di tempatnya sendiri, karena mereka akan berubah pada waktu yang berbeda, mereka akan memiliki versinya sendiri.
Jadi, proyek pertama adalah proyek dengan anotasi yang akan menunjukkan router kami, metode HTTP, dll. Sebut saja
metainfoDua proyek lain bergantung padanya:
codegen dan
apiApi berisi deskripsi router dan pesan - kelas-kelas yang akan bolak-balik antara backend dan frontend
codegen adalah proyek pembuatan kode (tetapi bukan proyek di mana kode dihasilkan!) - ini berisi kumpulan informasi dari kelas
api dan generator kode aktual.
Generator akan menerima semua detail dari generasi dalam argumen - dari paket mana untuk mengambil deskripsi router, ke direktori mana untuk menghasilkan, apa nama templat Velocity untuk generasi - yaitu.
metainfo dan
codegen umumnya dapat digunakan dalam proyek yang sama sekali berbeda
Nah, dua proyek di mana generasi sebenarnya akan dilakukan:
frontend-generate di mana kita akan menghasilkan kelas Filescript yang sesuai dengan pesan kotlin kita
dan
backend - dengan aplikasi Vertx yang sebenarnya.
Agar satu proyek dapat "melihat" hasil kompilasi yang lain, kami akan menggunakan plugin untuk menerbitkan artefak ke repositori Maven lokal.
Proyek Metafinfo :
Anotasi yang dengannya kami akan menandai sumber generasi - deskripsi endpoins, pesan, entitas:
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 }
Untuk kelas naskah, kami akan mendefinisikan anotasi yang dapat digantung di bidang dan yang akan jatuh ke dalam kelas naskah yang dihasilkan
annotation class IsString annotation class IsEmail annotation class IsBoolean annotation class MaxLength(val len:Int)
Kode sumber proyek metainfoProyek api :Perhatikan plugin noArg dan jpa di build.gradle untuk menghasilkan konstruktor tanpa argumen
Saya tidak punya cukup fantasi, jadi kami akan membuat beberapa deskripsi gila tentang pengontrol dan Entitas untuk aplikasi kami:
@EndpointController("/util") interface SearchRouter { @Endpoint(HttpMethodName.GET, param = "id") fun search(id: String): String @Endpoint(method = HttpMethodName.POST) @AsyncHandler fun search(searchRequest: SearchRequest)
Kode sumber proyek apiProyek Codegen :Pertama kita mendefinisikan "deskriptor" - kelas-kelas yang akan kita isi dengan melalui refleksi pada proyek api kita:
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)
Kode yang mengumpulkan informasi terlihat seperti ini:
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 } }
Yah, ada juga kelas "utama" yang menerima argumen - paket mana yang harus direfleksikan, yang digunakan templat Velocity, dll.
Mereka tidak begitu tertarik, Anda dapat melihat segala sesuatu di repositori:
Kode sumberDalam proyek yang dihasilkan frontend dan backend , kami melakukan hal serupa:
1. ketergantungan pada
api pada waktu kompilasi
2. ketergantungan pada
codegen pada tahap build
3. Templat generasi terletak di direktori
buildSrc di mana file dan kode dibutuhkan secara bertahap, yang diperlukan pada tahap build, tetapi tidak pada tahap kompilasi atau runtime. Yaitu kita dapat mengubah pola pembuatan tanpa mengkompilasi ulang proyek
codegen4.
frontend-generate mengkompilasi Script yang dihasilkan dan menerbitkannya ke repositori paket npm
5. Di
backend , router dihasilkan yang mewarisi dari router abstrak yang tidak dibuat yang tahu bagaimana menangani berbagai jenis permintaan. Abstrak Verticle juga dihasilkan yang harus diwariskan dengan implementasi logika bisnis itu sendiri. Selain itu, segala macam hal kecil dihasilkan yang saya sebagai programmer tidak ingin pikirkan - mendaftarkan codec dan alamat konstanta di bass acara.
Kode sumber yang
dihasilkan frontend dan
backendDi
frontend-generate, Anda perlu memperhatikan plugin yang menerbitkan sumber yang dihasilkan di repositori npm. Agar ini berfungsi, Anda perlu memasukkan IP repositori Anda di build.gradle dan memasukkan token otentikasi Anda ke .npmrc
Kelas yang dihasilkan terlihat seperti ini:
import { IsString, MaxLength, IsDate, IsArray, } from 'class-validator'; import { Type } from 'class-transformer';
Perhatikan anotasi validator kelas tc.
Di proyek backend, repositori untuk Spring Data JPA juga dihasilkan, dimungkinkan untuk mengatakan bahwa proyek pemrosesan pesan di Verticle memblokir (dan dijalankan melalui Vertx.executeBlocking) atau asynchronous (dengan coroutine), dimungkinkan untuk mengatakan bahwa Vertikel yang dihasilkan untuk Entitas adalah abstrak dan kemudian ada kemungkinan menimpa kait yang dipanggil sebelum dan sesudah memanggil metode yang dihasilkan. Penyebaran vertikular otomatis pada antarmuka kacang pegas - yah, singkatnya, banyak barang.
Dan semua ini mudah diperluas - misalnya, menggantung daftar peran pada Endpoint dan menghasilkan pemeriksaan peran pengguna yang masuk saat memanggil titik akhir dan banyak lagi - itu cukup untuk itu.
Ini juga mudah untuk menghasilkan bukan Vertx, bukan Spring, tetapi sesuatu yang lain - bahkan akka-http, cukup untuk hanya mengubah template di proyek backend.
Arah pengembangan lain yang mungkin adalah menghasilkan lebih banyak front-end.
Semua kode sumber ada di
sini .
Terima kasih kepada Ildar dari front-end untuk bantuan dalam menciptakan generasi dalam proyek kami dan dalam menulis artikel