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;
}
}
读库选择器,如果有多读库的情况下,轮询访问不同节点
接口调用情况
成功:
业务异常: