云原生quarkus框架项目实践
写在前面, 不知不觉上篇文章已经是好几年前了, 回到博客园倍感亲切. 总想写点什么, 发现博客园里关于quarkus的文章不多, 故把自己在项目过程中的点滴整理如下, 希望对您有所帮助.
一、quarkus 是什么?为什么要用quarkus
quarkus是Redhat开源的云原生微服务框架, 相比较成熟的SpringCloud, 为什么要用quarkus?
主要有以下几点原因:
- Spring系列框架臃肿、复杂, 更像是一个全家桶. 而quarkus 简单、高效, 工具先进
- 启动速度, quarkus可以在5秒内启动, 而spring对于一个golang开发者来说, 这个速度直接无法忍受.
- quarkus可以热编译, 无需手动编译和重启服务, 而Spring的热编译..
- 与其他工具集成, Spring集成了大部分的工具, 但你把DI换成guice试试, quarkus可以很方便的集成工具, 虽然框架本身包含的东西不多
- quarkus不依赖tomcat或jetty, 可以编译为原生应用, 性能大幅提高
- quarkus耦合低, 项目结构干净, 适合使用代码生成器.
二、创建一个quarkus项目
您可以使用maven或gradle来快速创建一个quarkus项目, 具体方法见quarkus网站, quarkus 只需要创建一个Resource类, 就可以启动服务. 零配置.
另外:quarkus 对Kotlin支持极为友好, 本文将创建一个使用Kotlin+Gradle的项目. 项目的配置文件: build.gradle.kts内容如下:
plugins{ java kotlin("jvm") version ("1.3.72") kotlin("plugin.allopen") version ("1.3.72") id("io.quarkus") version("1.4.2.Final") } allOpen { annotation("javax.enterprise.context.ApplicationScoped") annotation("javax.enterprise.context.RequestScoped") } repositories { maven("http://maven.aliyun.com/nexus/content/groups/public/") mavenCentral() } dependencies { implementation(kotlin("stdlib")) implementation("io.quarkus:quarkus-kotlin:1.4.2.Final") implementation("io.quarkus:quarkus-resteasy:1.4.2.Final") implementation("io.quarkus:quarkus-resteasy-jsonb:1.4.2.Final") testImplementation("io.quarkus:quarkus-junit5:1.4.2.Final") } tasks.withType<Test> { useJUnitPlatform() }
// 代码生成器 tasks.create("generate").doFirst { exec{ workingDir("./tto") commandLine("sh","-c","./tto.sh") } } tasks.withType<JavaCompile>().configureEach { options.encoding="utf-8" options.compilerArgs = listOf("-Xdoclint:none", "-Xlint:none", "-nowarn") }
三、配置并启动项目
您可以创建一个类, 并添加注解:@ApplicationScoped , 作为系统启动类, 代码如下:
@ApplicationScoped class Application { fun onStart(@Observes event: StartupEvent?) { println("app started..") } }
这并不是必须的, 因为上文提到了, 可能需要集成其他工具. 接着我们创建一个服务如下:
import javax.ws.rs.GET import javax.ws.rs.Path import javax.ws.rs.Produces import javax.ws.rs.core.MediaType
@Path("/hello") class HelloResource { @GET@Path("/{name}") @Produces(MediaType.APPLICATION_JSON) fun hello(@PathParam("name") name:String): String { return "hello ${name}" } }
运行命令启动服务
gradle quarkusDev
访问服务
curl http://localhost:8080/hello/jarrysix
> hello jarrysix
三、使用数据源
通过上面的步骤, 我们已能运行quarkus, 接下来我们通过极为简单的方式来完成数据源的访问.
首先, 我们需要添加配置:
quarkus.datasource.db-kind=h2 quarkus.datasource.username=username-default quarkus.datasource.jdbc.url=jdbc:h2:tcp://localhost/mem:default quarkus.datasource.jdbc.min-size=3 quarkus.datasource.jdbc.max-size=13
创建实体类
@Entity public class Gift { @Id @GeneratedValue(strategy = GenerationType.SEQUENCE, generator="giftSeq") private Long id; public Long getId() { return id; } public void setId(Long id) { this.id = id; } private String name; public String getName() { return name; } }
创建Panachec仓储类
@ApplicationScoped public class PersonRepository implements PanacheRepository<Person> { // put your custom logic here as instance methodspublic void deletePerson(name:String){ delete("name",name); } }
在资源类中调用仓储类
@Path("/person") class HelloResource { @Inject private lateinit var repo:PersonRepository @DELETE@Path("/{name}") fun delete(@PathParam("name") name:String): String { this.repo.deletePerson(name); return "success" } }
当然在实际项目中不建议直接调用仓储, 就这样我们完成人员删除的服务.
三:使用docker打包镜像
quarkus可以通过GraalVM打包成原生镜像, 以在生产环境中得到更低的CPU和内存占用. 如果您不想本地打包, 可以使用docker镜像打包为原生应用.
本文为了简化, 依然使用JVM来运行quarkus, 镜像构建配置文件如下:
# Quarkus docker image demo # Version 1.0 # Author : jarrysix(homepage: http://fze.net) # Date : 2018-04-13 14:40 FROM adoptopenjdk/openjdk14-openj9:alpine-jre MAINTAINER jarrysix WORKDIR /data WORKDIR /app COPY build/*.jar ./ COPY build/lib ./lib RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories && \ apk add tzdata fontconfig ttf-dejavu && cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
EXPOSE 8080 ENTRYPOINT ["java","-jar *-runner.jar"]
四:使用代码生成器
因为quarkus的项目结构及对框架和工具依赖较低, 甚至仔细观察, 项目代码里大多引用的就是JAVA自带的工具集. 这样对我们使用代码生成器来生成一些格式重复的代码是相当有利的.
我在生产环境中, 就用生成器来生成quarkus和vue.js的代码. 极大的减少了工作量. 接下来我们一步一步的创建代码模板并生成代码.
注: 文中使用的是go编写的代码生成器:tto , 项目主页: http://github.com/ixre/tto ; 其他工具也可以达到效果
1. 数据实体代码模板: pojo.java
#!target:java/{{.global.Pkg}}/pojo/{{.table.Title}}Entity.java package {{pkg "java" .global.Pkg}}.pojo; import javax.persistence.Basic; import javax.persistence.Id; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.Table; import javax.persistence.GenerationType; import javax.persistence.GeneratedValue; /** {{.table.Comment}} */ @Entity @Table(name = "{{.table.Name}}", schema = "{{.table.Schema}}") public class {{.table.Title}}Entity { {{range $i,$c := .columns}}{{$type := type "java" $c.Type}} {{if $c.IsPk}}\ @Id @GeneratedValue(strategy = GenerationType.IDENTITY){{else}} @Basic{{end}} @Column(name = "{{$c.Name}}"{{if not $c.NotNull}}, nullable = true{{end}} {{if ne $c.Length 0}},length = {{$c.Length}}{{end}}) private {{$type}} {{$c.Name}}; /** {{$c.Comment}} */ public {{$type}} get{{$c.Prop}}() { return this.{{$c.Name}}; } public void set{{$c.Prop}}({{$type}} {{$c.Name}}){ this.{{$c.Name}} = {{$c.Name}}; } {{end}} /** 拷贝数据 */ public {{.table.Title}}Entity copy({{.table.Title}}Entity src){ {{.table.Title}}Entity dst = new {{.table.Title}}Entity(); {{range $i,$c := .columns}} dst.set{{$c.Prop}}(src.get{{$c.Prop}}());{{end}} return dst; } }
2. 仓储代码模板: quarkus_repo.kt
#!target:kotlin/{{.global.Pkg}}/repo/{{.table.Title}}JpaRepository.kt.gen package {{pkg "java" .global.Pkg}}.repo; import {{pkg "kotlin" .global.Pkg}}.pojo.{{.table.Title}}Entity import io.quarkus.hibernate.orm.panache.PanacheRepository import javax.enterprise.context.ApplicationScoped {{$pkType := type "kotlin" .table.PkType}} /** {{.table.Comment}}仓储 */ @ApplicationScoped class {{.table.Title}}JpaRepository : PanacheRepository<{{.table.Title}}Entity> { }
3. 服务代码模板:quarkus_service.kt
#!target:kotlin/{{.global.Pkg}}/service/{{.table.Title}}Service.kt.gen package {{pkg "java" .global.Pkg}}.service import {{pkg "java" .global.Pkg}}.pojo.{{.table.Title}}Entity import {{pkg "java" .global.Pkg}}.repo.{{.table.Title}}JpaRepository import javax.inject.Inject import javax.enterprise.inject.Default import javax.enterprise.context.ApplicationScoped import net.fze.util.catch import net.fze.commons.std.Types import net.fze.commons.std.TypesConv import net.fze.util.value import javax.transaction.Transactional {{$tableTitle := .table.Title}} {{$pkName := .table.Pk}} {{$pkProp := lower_title .table.PkProp}} {{$pkType := type "kotlin" .table.PkType}} /** {{.table.Comment}}服务 */ @ApplicationScoped class {{.table.Title}}Service { @Inject@field:Default private lateinit var repo: {{$tableTitle}}JpaRepository fun parseId(id:Any):Long{return TypesConv.toLong(id)} /** 根据ID查找{{.table.Comment}} */ fun findByIdOrNull(id:{{$pkType}}):{{$tableTitle}}Entity?{ return this.repo.findByIdOptional(this.parseId(id)) } /** 保存{{.table.Comment}} */ @Transactional fun save{{$tableTitle}}(e: {{$tableTitle}}Entity):Error? { return catch { var dst: {{$tableTitle}}Entity if (e.{{$pkProp}} > 0) { dst = this.repo.findById(this.parseId(e.{{$pkProp}}))!! } else { dst = {{$tableTitle}}Entity() {{$c := try_get .columns "create_time"}}\ {{if ne $c nil}}dst.createTime = Types.time.unix().toLong(){{end}} } {{range $i,$c := exclude .columns $pkName "create_time" "update_time"}} dst.{{lower_title $c.Prop}} = e.{{lower_title $c.Prop}}{{end}}\ {{$c := try_get .columns "update_time"}} {{if ne $c nil}}dst.updateTime = Types.time.unix().toLong(){{end}} this.repo.persistAndFlush(dst) null }.error() } /** 批量保存{{.table.Comment}} */ @Transactional fun saveAll{{$tableTitle}}(entities:Iterable<{{$tableTitle}}Entity>){ this.repo.persist(entities) this.repo.flush() } /** 删除{{.table.Comment}} */ @Transactional fun deleteById(id:{{$pkType}}):Error? { return catch { this.repo.deleteById(this.parseId(id)) }.error() } }
4. 资源类代码模板:restful_resource.kt
#!target:kotlin/{{.global.Pkg}}/resources/{{.table.Title}}Resource.kt.gen package {{pkg "java" .global.Pkg}}.resources import {{pkg "java" .global.Pkg}}.pojo.{{.table.Title}}Entity import {{pkg "java" .global.Pkg}}.service.{{.table.Title}}Service import {{pkg "java" .global.Pkg}}.component.TinyQueryComponent import net.fze.commons.std.Result import net.fze.component.report.DataResult import javax.inject.Inject import javax.ws.rs.* import javax.ws.rs.core.MediaType import javax.enterprise.context.RequestScoped import javax.annotation.security.PermitAll {{$tableTitle := .table.Title}} {{$pkType := type "kotlin" .table.PkType}} /* {{.table.Comment}}资源 */ @Path("/{{.table.Name}}") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) @RequestScoped class {{.table.Title}}Resource { @Inject private lateinit var service:{{.table.Title}}Service @Inject private lateinit var queryComponent: TinyQueryComponent /** 获取{{.table.Comment}} */ @GET@Path("/{id}") @PermitAll fun get(@PathParam("id") id:{{$pkType}}): {{.table.Title}}Entity? { return service.findByIdOrNull(id) } /** 创建{{.table.Comment}} */ @POST @PermitAll fun create(entity: {{.table.Title}}Entity):Result { val err = this.service.save{{.table.Title}}(entity) if(err != null)return Result.create(1,err.message) return Result.OK } /** 更新{{.table.Comment}} */ @PUT@Path("/{id}") @PermitAll fun save(@PathParam("id") id:{{$pkType}},entity: {{.table.Title}}Entity):Result { entity.{{lower_title .table.PkProp}} = id val err = this.service.save{{.table.Title}}(entity) if(err != null)return Result.create(1,err.message) return Result.OK } /** 删除{{.table.Comment}} */ @DELETE@Path("/{id}") @PermitAll fun delete(@PathParam("id") id:{{$pkType}}):Result { val err = this.service.deleteById(id) if(err != null)return Result.create(1,err.message) return Result.OK } /** {{.table.Comment}}列表 */ @GET @PermitAll fun list(): List<{{.table.Title}}Entity> { return mutableListOf() } /** {{.table.Comment}}分页数据 */ @GET@Path("/paging") @PermitAll fun paging(@QueryParam("params") params:String, @QueryParam("page") page:String, @QueryParam("rows") rows:String ): DataResult { return this.queryComponent.fetchData("default", "{{.table.Title}}List", params, page, rows) } }
5. VUE接口文件代码模板:api.ts
#!lang:ts#!name:API和定义文件 #!target:ts/feature/{{.table.Prefix}}/{{.table.Name}}/api.ts import request from '@/utils/request' // {{.table.Comment}}对象 export interface I{{.table.Title}} { {{range $i,$c := .columns}}// {{$c.Comment}} {{lower_title $c.Prop}}:{{type "ts" $c.Type}} {{end}} } export const default{{.table.Title}}:()=>I{{.table.Title}}=()=>{ return { {{range $i,$c := .columns}} {{lower_title $c.Prop}}:{{default "ts" $c.Type}},{{end}} }; } export const get{{.table.Title}} = (id: any, params: any = {}) => request({ url: `/{{.table.Name}}/${id}`, method: 'get', params:{...params} }) export const get{{.table.Title}}List = (params: any = {}) => request({ url: '/{{.table.Name}}', method: 'get', params:{...params} }) export const create{{.table.Title}} = (data: any) => request({ url: '/{{.table.Name}}', method: 'post', data }) export const update{{.table.Title}} = (id: any, data: any) => request({ url: `/{{.table.Name}}/${id}`, method: 'put', data }) export const delete{{.table.Title}} = (id: any) => request({ url: `/{{.table.Name}}/${id}`, method: 'delete' }); export const batchDelete{{.table.Title}} = (arr: any[]) => request({ url: '/{{.table.Name}}', method: 'delete', data:arr }); export const getPaging{{.table.Title}} = (page:number,rows:number,params: any) => request({ url: '/{{.table.Name}}/paging', method: 'get', params:{page,rows,params} })
运行命令将代码生成到指定位置
gradle generate
五:写在最后
因作者写作水平有限, 文中以最精简的方法介绍了quarkus的应用, 包括生成代码等骚操作.
示例代码打包下载地址:quarkus-kotlin-gradle-demo-feature.zip