云原生quarkus框架项目实践

写在前面,   不知不觉上篇文章已经是好几年前了,  回到博客园倍感亲切.   总想写点什么,  发现博客园里关于quarkus的文章不多,  故把自己在项目过程中的点滴整理如下,  希望对您有所帮助.

一、quarkus 是什么?为什么要用quarkus

quarkus是Redhat开源的云原生微服务框架,  相比较成熟的SpringCloud, 为什么要用quarkus?

主要有以下几点原因:

  1. Spring系列框架臃肿、复杂, 更像是一个全家桶. 而quarkus 简单、高效, 工具先进
  2. 启动速度, quarkus可以在5秒内启动, 而spring对于一个golang开发者来说, 这个速度直接无法忍受.
  3. quarkus可以热编译, 无需手动编译和重启服务, 而Spring的热编译..
  4. 与其他工具集成, Spring集成了大部分的工具, 但你把DI换成guice试试, quarkus可以很方便的集成工具, 虽然框架本身包含的东西不多
  5. quarkus不依赖tomcat或jetty, 可以编译为原生应用, 性能大幅提高
  6. 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

猜你喜欢

转载自www.cnblogs.com/newmin/p/quarkus-simle-guide.html