Quarkus框架-反应式编程-实现简单登录功能

Quarkus框架-反应式编程-实现简单登录功能

简介

在使用JAVA开发微服务时,可以选择的框架有很多,当前主流的框架是Spring和SpringBoot,在Spring5.3x最后一个版本中,增加了对GraalVM的支持, 弥补了短板,使得Spring框架更加适用于开发云原生应用
Quarkus框架是Kubernetes原生的JAVA应用开发框架,针对OpenJDK HotSpot虚拟机和GraalVM进行了优化,它的启动速度非常快,并且消耗的资源也更少,同时支持阻塞时和反应式开发,对于开发人员来说也相对友好

个人说明

以下代码和图示仅仅研究分享所得,项目是可以正常运行和启动的,前提是需要搭建Consul,和MySQL数据库一主二从,如果有对Quarkus框架初学和感兴趣的,想要有更深层印象的可以下载项目源码: https://gitee.com/li-yusen/quarkus-shopping-manager.git

项目依赖(pom文件)

 <?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>pers.online.gateway</groupId>
    <artifactId>quarkus-gateway</artifactId>
    <version>2.0-SNAPSHOT</version>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.0.0</version>
        <relativePath /> <!-- lookup parent from repository -->
    </parent>

    <properties>
        <dockerHost></dockerHost>
        <compiler-plugin.version>3.12.1</compiler-plugin.version>
        <maven.compiler.release>17</maven.compiler.release>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <quarkus.platform.artifact-id>quarkus-bom</quarkus.platform.artifact-id>
        <quarkus.platform.group-id>io.quarkus.platform</quarkus.platform.group-id>
        <quarkus.platform.version>3.9.4</quarkus.platform.version>
        <skipITs>true</skipITs>
        <surefire-plugin.version>3.2.5</surefire-plugin.version>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>${quarkus.platform.group-id}</groupId>
                <artifactId>${quarkus.platform.artifact-id}</artifactId>
                <version>${quarkus.platform.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webflux</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-consul-discovery</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-loadbalancer</artifactId>
        </dependency>


    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>com.spotify</groupId>
                <artifactId>docker-maven-plugin</artifactId>
                <version>1.2.2</version>
                <executions>
                    <execution>
                        <id>build-image</id>
                        <phase>package</phase>
                        <goals>
                            <goal>build</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                    <dockerHost>${dockerHost}</dockerHost>
                    <imageName>${project.artifactId}</imageName>
                    <dockerDirectory>${project.basedir}</dockerDirectory>
                    <resources>
                        <resource>
                            <targetPath>/</targetPath>
                            <directory>${project.build.directory}</directory>
                            <include>${project.build.finalName}.jar</include>
                        </resource>
                    </resources>
                </configuration>
            </plugin>

            <!--            <plugin>-->
<!--                <groupId>${quarkus.platform.group-id}</groupId>-->
<!--                <artifactId>quarkus-maven-plugin</artifactId>-->
<!--                <version>${quarkus.platform.version}</version>-->
<!--                <extensions>true</extensions>-->
<!--                <executions>-->
<!--                    <execution>-->
<!--                        <goals>-->
<!--                            <goal>build</goal>-->
<!--                            <goal>generate-code</goal>-->
<!--                            <goal>generate-code-tests</goal>-->
<!--                        </goals>-->
<!--                    </execution>-->
<!--                </executions>-->
<!--            </plugin>-->
<!--            <plugin>-->
<!--                <artifactId>maven-compiler-plugin</artifactId>-->
<!--                <version>${compiler-plugin.version}</version>-->
<!--                <configuration>-->
<!--                    <compilerArgs>-->
<!--                        <arg>-parameters</arg>-->
<!--                    </compilerArgs>-->
<!--                </configuration>-->
<!--            </plugin>-->
<!--            <plugin>-->
<!--                <artifactId>maven-surefire-plugin</artifactId>-->
<!--                <version>${surefire-plugin.version}</version>-->
<!--                <configuration>-->
<!--                    <systemPropertyVariables>-->
<!--                        <java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>-->
<!--                        <maven.home>${maven.home}</maven.home>-->
<!--                    </systemPropertyVariables>-->
<!--                </configuration>-->
<!--            </plugin>-->
<!--            <plugin>-->
<!--                <artifactId>maven-failsafe-plugin</artifactId>-->
<!--                <version>${surefire-plugin.version}</version>-->
<!--                <executions>-->
<!--                    <execution>-->
<!--                        <goals>-->
<!--                            <goal>integration-test</goal>-->
<!--                            <goal>verify</goal>-->
<!--                        </goals>-->
<!--                    </execution>-->
<!--                </executions>-->
<!--                <configuration>-->
<!--                    <systemPropertyVariables>-->
<!--                        <native.image.path>${project.build.directory}/${project.build.finalName}-runner</native.image.path>-->
<!--                        <java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>-->
<!--                        <maven.home>${maven.home}</maven.home>-->
<!--                    </systemPropertyVariables>-->
<!--                </configuration>-->
<!--            </plugin>-->
        </plugins>
    </build>

    <profiles>
        <profile>
            <id>native</id>
            <activation>
                <property>
                    <name>native</name>
                </property>
            </activation>
            <properties>
                <skipITs>false</skipITs>
                <quarkus.package.type>native</quarkus.package.type>
            </properties>
        </profile>
    </profiles>
</project>

项目结构

在这里插入图片描述

项目说明

本项目采用Quarkus框架和mutiny反应式框架,利用反应式编程实现简单的登录功能,由于项目是按照微服务搭建的,服务注册中心使用Consul,网关使用GateWay(需要单独搭建SpringCloud GateWay + Consul,也可以先不搭建),Stork,数据库使用MySQL搭建一主二从,具体yml配置信息如下:

quarkus:

  live-reload:
    enabled: false
  log:
    console:
      level: DEBUG
    category:
      "WebApplicationException":
        level: DEBUG
      "org.jboss.resteasy.reactive.common.core.AbstractResteasyReactiveContext":
        level: DEBUG

  http:
    host: 192.168.1.2
#    host: 192.168.57.166
    port: 18888
    cors:
      enabled: true
      origins: "*"
      methods: "GET,POST,PUT,DELETE,OPTIONS"
      headers: "Content-Type,Authorization,X-Requested-With,Accept,Origin"
      exposed-headers: "Access-Control-Allow-Origin,Content-Type,Authorization,X-Requested-With,Accept"
      max-age: 1800
  application:
    name: shopping-manager
  datasource:
    db-kind: mysql
    username: root
    password: root
    reactive:
      url: vertx-reactive:mysql://192.168.56.124:3306/quarkus-shopping-cloud?useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&useSSL=false
      #url: vertx-reactive:mysql://192.168.57.166:3306/quarkus-shopping-cloud?useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&useSSL=false
      max-size: 10
    "slave1":
      db-kind: mysql
      username: root
      password: root
      reactive:
        #url: vertx-reactive:mysql://192.168.66.124:3306/quarkus-shopping-cloud?useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&useSSL=false
        url: vertx-reactive:mysql://192.168.66.124:3307/quarkus-shopping-cloud?useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&useSSL=false
    "slave2":
      db-kind: mysql
      username: root
      password: root
      reactive:
        #url: vertx-reactive:mysql://192.168.66.124:3306/quarkus-shopping-cloud?useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&useSSL=false
        url: vertx-reactive:mysql://192.168.66.124:3308/quarkus-shopping-cloud?useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&useSSL=false
  hibernate-orm:
    log:
      sql: true
    database:
      generation: update
  stork:
    shopping-manager:
      load-balancer:
        type: round-robin
      service-discovery:
        consul-host: 192.168.56.124
        consul-port: 8500
        use-health-checks: true
        type: consul
      service-registrar:
        type: consul
        consul-host: 192.168.56.124
        consul-port: 8500
  smallrye-openapi:
    info-title: "管理端"
    info-version: "1.0"
    info-description: "Quarkus框架开发shopping-manager接口文档"
    info-terms-of-service: "https://gitee.com/li-yusen/quarkus-shopping-manager"
    info-contact:
      email: "l88289634@163.com"
      name: "sen"
      url: ""
  swagger-ui:
    #访问地址:http://localhost:18888/q/swagger-ui;http://localhost:18888/q/openapi
    # 生产环境使用
    always-include: true

database:
  up: true


Consule服务注册发现

package per.sen.shopping.infrastructure.consul;

import io.quarkus.runtime.StartupEvent;
import io.vertx.ext.consul.CheckOptions;
import io.vertx.ext.consul.CheckStatus;
import io.vertx.ext.consul.ConsulClientOptions;
import io.vertx.ext.consul.ServiceOptions;
import io.vertx.mutiny.core.Vertx;
import io.vertx.mutiny.ext.consul.ConsulClient;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.event.Observes;
import org.eclipse.microprofile.config.inject.ConfigProperty;


/**
 * @Descibe: 注册服务consul
 * @Author: LYS
 * @Date: 2024/4/26 17:24
 */

@ApplicationScoped
public class Registration {
    @ConfigProperty(name="quarkus.application.name")
    String applicationName;

    @ConfigProperty(name = "quarkus.stork.shopping-manager.service-discovery.consul-host")
    String host;
    @ConfigProperty(name = "quarkus.stork.shopping-manager.service-discovery.consul-port")
    int port;
    @ConfigProperty(name = "quarkus.http.port")
    int servicePort;
    @ConfigProperty(name = "quarkus.http.host")
    String serviceHost;

    public void init(@Observes StartupEvent ev, Vertx vertx) {
        CheckOptions checkOptions = new CheckOptions();
        checkOptions.setInterval("30s");
        checkOptions.setStatus(CheckStatus.PASSING);

        checkOptions.setHttp("http://"+ serviceHost + ":" + servicePort + "/q/health/live");
        ConsulClientOptions consulClientOptions = new ConsulClientOptions().setHost(host).setPort(port);

        ConsulClient client = ConsulClient.create(vertx, consulClientOptions);

        try {
        client.registerServiceAndAwait(
                new ServiceOptions().
                        setPort(servicePort).
                        setAddress(serviceHost).
                        setName(applicationName).
                        setCheckOptions(checkOptions));
    } catch (Throwable throwable){
            client.close();
            throw new RuntimeException(throwable.getMessage());
        }
    }

}

package per.sen.shopping.infrastructure.consul;

import io.smallrye.health.api.AsyncHealthCheck;
import io.smallrye.mutiny.Uni;
import jakarta.enterprise.context.ApplicationScoped;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.eclipse.microprofile.health.HealthCheckResponse;
import org.eclipse.microprofile.health.Liveness;

import java.time.Duration;

/**
 * @Descibe: 健康检测:@Liveness应用程序已启动并运行。
 * @Author: LYS
 * @Date: 2024/5/30 10:07
 */

@Liveness
@ApplicationScoped
public class LivenessAsync  implements AsyncHealthCheck {
    @ConfigProperty(name="quarkus.application.name")
    String applicationName;
    @ConfigProperty(name = "database.up", defaultValue = "false")
    private boolean databaseUp;
    @Override
    public Uni<HealthCheckResponse> call() {

        try {
            simulateDatabaseConnectionVerification();
            return Uni.createFrom().item(HealthCheckResponse.up(applicationName))
                    .onItem().delayIt().by(Duration.ofMillis(10));
        } catch (IllegalStateException e) {
            // cannot access the database
            return Uni.createFrom().item(HealthCheckResponse.down(applicationName))
                    .onItem().delayIt().by(Duration.ofMillis(10));
        }
    }


    private void simulateDatabaseConnectionVerification() {
        if (!databaseUp) {
            throw new IllegalStateException("Cannot contact database");
        }
    }
}

全局异常处理器

package per.sen.shopping.infrastructure.common.exception;


import io.quarkus.hibernate.validator.runtime.jaxrs.ResteasyReactiveViolationException;
import io.smallrye.mutiny.Uni;
import jakarta.annotation.Priority;
import jakarta.el.MethodNotFoundException;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.context.Dependent;
import jakarta.ws.rs.NotAllowedException;
import jakarta.ws.rs.NotFoundException;
import jakarta.ws.rs.NotSupportedException;
import jakarta.ws.rs.Priorities;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.ext.Provider;
import org.jboss.resteasy.reactive.RestResponse;
import org.jboss.resteasy.reactive.server.ServerExceptionMapper;
import per.sen.shopping.infrastructure.common.response.ApiResponse;
import per.sen.shopping.infrastructure.common.response.CustomCode;

import per.sen.shopping.infrastructure.common.response.HttpStatus;

import java.net.BindException;

/**
 * @Describe: 全局异常拦截器
 * @Author: LYS
 * @Date: 2024/6/1 16:53
 */
@Provider
public class ExceptionMappers   {
    /**
     * @Describe: 基础异常
     * @Author: LYS
     * @Date: 2024/6/1 17:32
     */
   
    @ServerExceptionMapper(BaseException.class)
    public RestResponse<ApiResponse> mapException  (BaseException baseException) {
        return RestResponse.status(Response.Status.INTERNAL_SERVER_ERROR, ApiResponse.error(baseException.getCode(),baseException.getModule()+":"+baseException.getDefaultMessage()));
    }

    /**
     * @Describe: 业务异常
     * @Author: LYS
     * @Date: 2024/6/1 17:27
     */
  
    @ServerExceptionMapper(CustomException.class)
    public RestResponse<ApiResponse> mapException  (CustomException customException) {
        return RestResponse.status(Response.Status.BAD_REQUEST, ApiResponse.error(customException.getCode(),customException.getErrorMsg()));
    }

    /**
     * @Describe: 参数校验异常
     * @Author: LYS
     * @Date: 2024/6/1 17:27
     */
   
    @ServerExceptionMapper(ResteasyReactiveViolationException.class)
    public Uni<RestResponse<ApiResponse>> mapException (ResteasyReactiveViolationException  x) {
        return  Uni.createFrom().item(RestResponse.status(Response.Status.BAD_REQUEST, ApiResponse.error(CustomCode.ILLEGAL_PARAM_EXCEPTION.getCode(), x.getMessage().split(":")[1].trim())));
    }

    /**
     * @Describe: 请求路径异常
     * @Author: LYS
     * @Date: 2024/6/1 18:01
     */
    @ServerExceptionMapper(NotFoundException.class)
    public Uni<RestResponse<ApiResponse>> mapException (NotFoundException  notFoundException) {
        return  Uni.createFrom().item(RestResponse.status(Response.Status.NOT_FOUND, ApiResponse.error(HttpStatus.NOT_FOUND.getCode(),HttpStatus.NOT_FOUND.getMsg())));
    }

    /**
     * @Describe: 请求方式异常
     * @Author: LYS
     * @Date: 2024/6/1 18:01
     */
    @ServerExceptionMapper(NotAllowedException.class)
    public Uni<RestResponse<ApiResponse>> mapException (NotAllowedException notAllowedException) {
        return Uni.createFrom().item(RestResponse.status(Response.Status.METHOD_NOT_ALLOWED, ApiResponse.error(HttpStatus.METHOD_NOT_ALLOWED.getCode(),HttpStatus.METHOD_NOT_ALLOWED.getMsg())));
    }



}

统一响应体

package per.sen.shopping.infrastructure.common.response;

import lombok.Data;
import java.util.LinkedHashMap;


/**
 * @Describe: 统一响应体
 * @Author: LYS
 * @DateTime: 2023/11/26 18:59
 */
@Data
@SuppressWarnings("unused")
public class ApiResponse extends LinkedHashMap<String,Object> {
    private final static String CODE_TAG = "code";
    private final static String MSG_TAG = "msg";
    private final static String DATA_TAG = "data";


    public ApiResponse (Integer code ,String msg,Object data) {
        super(3);
        super.put(CODE_TAG,code);
        super.put(MSG_TAG,msg);
        super.put(DATA_TAG,data);
    }


    public static  ApiResponse success(Object data) {
       return  new ApiResponse(HttpStatus.SUCCESS.getCode(),HttpStatus.SUCCESS.getMsg(),data);
    }
    public static  ApiResponse error(String msg,Object data) {
        return new ApiResponse(HttpStatus.INTERNAL_SERVER_ERROR.getCode(), msg,data);
    }

    public static  ApiResponse error(Integer code, String msg) {
        return new ApiResponse(code, msg,null);
    }

}

统一响应编码

package per.sen.shopping.infrastructure.common.response;


/**
 * @Describe: 统一响应状态码
 * @Author: LYS
 * @DateTime: 2023/11/26 18:58
 */
public enum HttpStatus {
    SUCCESS(200,"操作成功"),
    BAD_REQUEST(400,"业务异常"),
    UNAUTHORIZED(401,"未授权"),
    FORBIDDEN(403,"禁止访问"),
    NOT_FOUND(404,"路径不存在,请检查路径是否正确"),
    METHOD_NOT_ALLOWED(405,"请求方法不允许,请检查请求方法是否正确"),
    REQUEST_TIMEOUT(408,"请求超时"),
    CONFLICT(409,"资源冲突"),
    GONE(410,"资源已过期"),
    UNSUPPORTED_MEDIA_TYPE(415,"不支持的媒体类型"),
    TOO_MANY_REQUESTS(429,"请求过多"),
    INTERNAL_SERVER_ERROR(500,"服务器内部错误");
   

    private final Integer code;
    private final String msg;


    HttpStatus(Integer code, String msg) {
        this.code = code;
        this.msg = msg;

    }

    public int getCode() {
        return code;
    }

    public String getMsg() {
        return msg;
    }



}

业务异常code

在这里插入图片描述

接口层

在这里插入图片描述

应用层

这里应用层起到了一个防腐层的作用,只进行的业务的编排,而不实现业务的具体逻辑,具体的业务逻辑由领域服务提供
在这里插入图片描述

领域服务

领域服务的划分很难一概而论,此处尽可能采用DDD的思想,进行领域服务的划分如下:
在这里插入图片描述在这里插入图片描述
在这里插入图片描述

数据交互

由于使用的MySQL主从架构,此处模拟一写多读的情况下,读操作轮询选择数据源.
这样做的是因为Quarkus框架整合反应式编程,从代码编写到数据库连接全部都使用的反应式非阻塞式,所以数据库连接使用的是响应式客户端,因此常规的Sharding-jdbc等这些三方架构都是无法使用的,这是我目前想到的最好的主从架构,多读情况下访问不同读库的方法,仅限于研究层面提供参考思路.
反应式编程以及响应式的数据库客户端目前支持的是Hibernate,Mybatis框架仍然是阻塞式的并且不支持xml配置文件的形式,所以此处为了整体采用反应式编程,使用了quarkus-reactive-mysql-client和quarkus-hibernate-reactive-panache依赖
反应式编程的查询大家可以看看参考下哈
在这里插入图片描述其中对响应结果,我封装了工具类来将结果转化为具体的实体类
在这里插入图片描述

package per.sen.shopping.infrastructure.utils;

import com.github.dozermapper.core.DozerBeanMapperBuilder;
import com.github.dozermapper.core.Mapper;
import io.vertx.mutiny.sqlclient.Row;
import per.sen.shopping.infrastructure.common.exception.BaseException;
import per.sen.shopping.infrastructure.common.response.HttpStatus;

import java.lang.reflect.Field;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Date;

/**
 * @Describe: RowSet转化实体工具类(数据库查询结果转化实体)
 * @Author: LYS
 * @Date: 2024/6/15 23:09
 */
public class RowToClazzUtil {
    private static final Mapper dozerMapper = DozerBeanMapperBuilder.buildDefault();
    private static final String  SERIALVERSIONUID = "serialVersionUID";

    public static <T> T convert(Row row ,Class<T> entityType) throws BaseException {
        T entityInstance;
        try {
            entityInstance  = entityType.getDeclaredConstructor().newInstance();
            Class<?> declaringClass = entityType.getDeclaringClass();
            Field[] declaredFields = entityType.getDeclaredFields();
            for (Field field : declaredFields) {

                String fieldName = field.getName();
                if(SERIALVERSIONUID.equals(fieldName) || fieldName.contains("$$_hibernate")){
                    continue;
                }
                Class<?> fieldType = field.getType();
                Object o;
                if(fieldType.getName().equals("java.util.Date")){
                    LocalDateTime localDateTime = row.getLocalDateTime(fieldName);
                    if(localDateTime != null){
                        o = Date.from(localDateTime.atZone(ZoneId.systemDefault()).toInstant());
                    }else{
                        o = null;
                    }
                }else{
                     o = row.get(fieldType, fieldName);
                }
                Field declaredField = entityType.getDeclaredField(fieldName);
                declaredField.setAccessible(true);
                declaredField.set(entityInstance,o);
            }
        } catch (Exception e) {
            throw new BaseException(HttpStatus.INTERNAL_SERVER_ERROR.getCode(),null,"manager",e.getMessage());
        }
        return entityInstance;
    }

}

读库选择器,如果有多读库的情况下,轮询访问不同节点
在这里插入图片描述
接口调用情况
成功:
在这里插入图片描述业务异常:
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

雨落纠纷

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值