quarkus使用/迁移经验

quarkus

introduce and make notes about the issues found during using quarkus

build加速

quarkus.native.builder-image=quay.io/quarkus/ubi9-quarkus-mandrel-builder-image:jdk-21
quarkus.native.container-build=true
quarkus.native.builder-image.pull=missing

在application.properties 中指定image并且不要每次去pullimage 加快编译。

docker build ubi8 vs ubi9

从 quarkus 3.19, 默认使用UBI9 作为native镜像, 对 vm里面的cpu有要求,可能会报错:(参考:url)

Fatal glibc error: CPU does not support x86-64-v2

所以, 使用ubi8:


# uib9
quarkus.native.builder-image=quay.io/quarkus/ubi9-quarkus-mandrel-builder-image:jdk-21

# ubi8
quarkus.native.builder-image=quay.io/quarkus/ubi-quarkus-mandrel-builder-image:jdk-21

Dockerfile修改:

# ubi9
FROM registry.access.redhat.com/ubi9/ubi9-minimal:9.6

# ubi8
FROM registry.access.redhat.com/ubi8-minimal:8.10

native镜像在部分机器无法启动 报错

The current machine does not support all of the following CPU features that are required by the image: [CX8, CMOV, FXSR, MMX, SSE, SSE2, SSE3, SSSE3, SSE4_1, SSE4_2, POPCNT, LZCNT, AVX, AVX2, BMI1, BMI2, FMA].
Please rebuild the executable with an appropriate setting of the -march option.

解决办法:

quarkus.native.additional-build-args=-march=compatibility

docker 自定义镜像

可以在native基础上加上自己的软件 方便调查问题:

FROM registry.access.redhat.com/ubi9/ubi-minimal:9.3

# 设置非交互模式并安装工具
# --releasever=9: 解决某些环境下无法识别版本的问题
# --nodocs: 不安装文档,显著减小体积
# clean all: 清理元数据缓存
RUN microdnf update -y --releasever=9 && \
    microdnf install -y --releasever=9 --nodocs \
        procps-ng \
        net-tools \
        wget \
        vim-minimal && \
    microdnf clean all -y --releasever=9 && \
    rm -rf /var/cache/yum

grpc

常见配置:

quarkus.http.port=8077
quarkus.grpc.server.port=9097
quarkus.grpc.server.host=0.0.0.0
# Reflection (grpc.reflection.v1 / v1alpha) for grpcurl, Postman, etc. Dev mode enables it automatically;
# in prod it is off unless you set GRPC_SERVER_ENABLE_REFLECTION=true (reflection exposes service/schema info).
quarkus.grpc.server.enable-reflection-service=true
quarkus.grpc.server.use-separate-server=true

# Index external JARs so Jandex sees gRPC ImplBase  BindableService; without this,
# prod/native finds zero bindable services and Quarkus skips starting the gRPC server
# (dev mode still wires server support, which hides the issue locally).
quarkus.index-dependency.business-protocol.group-id=cn.sichuancredit.datasource.business
quarkus.index-dependency.business-protocol.artifact-id=business-protocol

# Logging gRPC client (@GrpcClient("logging")) — set LOGGING_GRPC_HOST, LOGGING_GRPC_PORT
quarkus.grpc.clients.logging.host=${LOGGING_GRPC_HOST:192.168.102.224}
quarkus.grpc.clients.logging.port=${LOGGING_GRPC_PORT:6391}
quarkus.grpc.clients.logging.plain-text=true

注意:
(1)如果你实现了某个grpc服务,quarkus-index-dependency 这个需要设置,里面的内容就是你的服务所在的maven group 和 artifcat。 这个只在native模式有影响,不设置的话服务不会正常启动。
(2)打开:quarkus.grpc.server.enable-reflection-service=true 方便的你grpc 客户端可以通过reflection自动获取相关的定义。

移除grpc依赖避免log4j 引入

移除log4j的依赖避免native失败:

问题:

Caused by: com.oracle.graal.pointsto.constraints.UnsupportedFeatureException: Discovered unresolved type during parsing: io.grpc.netty.shaded.io.netty.util.internal.logging.Log4J2Logger. This error is reported at image build time because class io.grpc.netty.shaded.io.netty.util.internal.logging.Log4J2LoggerFactory is registered for linking at image build time by command line and command line. Error encountered while parsing io.grpc.netty.shaded.io.netty.util.internal.logging.InternalLoggerFactory.newDefaultFactory(InternalLoggerFactory.java:42)

解决:

java {
xxxxxx
}

configurations.all {
    exclude group: 'io.grpc', module: 'grpc-netty-shaded'
}

repositories {
yyyyyy
}

db

redis

自定义key:

实现一个这样的CacheKeyGenerator即可:

// 这个是忽略了参数中的第一个参数来组成cachekey:
@RegisterForReflection
public class CacheKeyGeneratorSkipFirstParam implements CacheKeyGenerator {

    public CacheKeyGeneratorSkipFirstParam() {

    }

    @Override
    public Object generate(Method method, Object... methodParams) {
        StringBuilder sb = new StringBuilder();
        sb.append(method.getName()).append('-');
        for (int i = 1; i < methodParams.length; i++) {
            sb.append(methodParams[i]).append('-');
        }
        return sb.toString();
    }
}

注意必须有空的构造函数和注解:@RegisterForReflection 然后就可以:

    /**
     * Cache key matches legacy Spring {@code @Cacheable} (method + idCard + personName semantics via two keys only).
     */
    @CacheResult(cacheName = "zzdtec", keyGenerator = CacheKeyGeneratorSkipFirstParam.class)
    public FetchResult load(AccessLogContext accessLogContext, String idCard, String personName) {
        return httpExecutor.fetch(accessLogContext, idCard, personName);
    }

得到的缓存key就是:cache:zzdtec:load-341224xxxxx-涛yyyy-

配置

# 配置密码相关
quarkus.redis.hosts=${REDIS_HOSTS:redis://192.168.102.221:36379/13}
quarkus.redis.password=${REDIS_PASSWORD:xxxxx}

quarkus.cache.type=redis
# 配置TTL
quarkus.cache.redis.zzdtec.expire-after-write=${REDIS_CACHE_EXPIRE:30d}
# 还需要配置你的缓存的object 不然会失败。 同样的该类需要有相关注解:@RegisterForReflection
quarkus.cache.redis.zzdtec.value-type=cn.sichuancredit.zzdtec.server.api.FetchResult

HTTP CLIENT util

参考实现:

注意事项:【1】不要直接初始化对象 static

【2】application.properties中忽略相关,需要在运行时初始化,避免build时初始化

quarkus.native.additional-build-args=\
    --initialize-at-run-time=com.xx.worker.util.HttpClientUtil,\
    --initialize-at-run-time=com.xx.worker.util.HttpClientUtil$ClientHolder,\
    --enable-http,\
    --enable-https,\

package com.xx.worker.util;

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.Map;

/**
 * HTTP client utility using JDK 21 standard HttpClient.
 * Uses lazy initialization for GraalVM native image compatibility.
 */
public class HttpClientUtil {

    private static final Duration DEFAULT_TIMEOUT = Duration.ofSeconds(30);

    private static class ClientHolder {
        static final HttpClient INSTANCE = HttpClient.newBuilder()
                .version(HttpClient.Version.HTTP_2)
                .connectTimeout(Duration.ofSeconds(10))
                .build();
    }

    private static HttpClient getClient() {
        return ClientHolder.INSTANCE;
    }

    /**
     * Send GET request and return response body as string.
     *
     * @param url target URL
     * @return response body
     */
    public static String get(String url) {
        return get(url, Map.of());
    }

    /**
     * Send GET request with headers and return response body as string.
     *
     * @param url     target URL
     * @param headers request headers
     * @return response body
     */
    public static String get(String url, Map<String, String> headers) {
        try {
            HttpRequest.Builder builder = HttpRequest.newBuilder()
                    .uri(URI.create(url))
                    .timeout(DEFAULT_TIMEOUT)
                    .GET();

            headers.forEach(builder::header);

            HttpRequest request = builder.build();
            HttpResponse<String> response = getClient().send(request, HttpResponse.BodyHandlers.ofString());

            if (response.statusCode() >= 400) {
                throw new RuntimeException("HTTP request failed with status: " + response.statusCode());
            }

            return response.body();
        } catch (Exception e) {
            throw new RuntimeException("Failed to send GET request to " + url, e);
        }
    }

    /**
     * Send POST request with JSON body and return response body as string.
     *
     * @param url  target URL
     * @param json JSON request body
     * @return response body
     */
    public static String postJson(String url, String json) {
        return postJson(url, json, Map.of());
    }

    /**
     * Send POST request with JSON body and headers, return response body as string.
     *
     * @param url     target URL
     * @param json    JSON request body
     * @param headers additional headers
     * @return response body
     */
    public static String postJson(String url, String json, Map<String, String> headers) {
        try {
            HttpRequest.Builder builder = HttpRequest.newBuilder()
                    .uri(URI.create(url))
                    .timeout(DEFAULT_TIMEOUT)
                    .header("Content-Type", "application/json; charset=utf-8")
                    .POST(HttpRequest.BodyPublishers.ofString(json));

            headers.forEach(builder::header);

            HttpRequest request = builder.build();
            HttpResponse<String> response = getClient().send(request, HttpResponse.BodyHandlers.ofString());

            if (response.statusCode() >= 400) {
                throw new RuntimeException("HTTP request failed with status: " + response.statusCode());
            }

            return response.body();
        } catch (Exception e) {
            throw new RuntimeException("Failed to send POST request to " + url, e);
        }
    }

    /**
     * Send POST request with form data and return response body as string.
     *
     * @param url      target URL
     * @param formData form data
     * @return response body
     */
    public static String postForm(String url, Map<String, String> formData) {
        try {
            String body = formData.entrySet().stream()
                    .map(entry -> encode(entry.getKey()) + "=" + encode(entry.getValue()))
                    .reduce((a, b) -> a + "&" + b)
                    .orElse("");

            HttpRequest request = HttpRequest.newBuilder()
                    .uri(URI.create(url))
                    .timeout(DEFAULT_TIMEOUT)
                    .header("Content-Type", "application/x-www-form-urlencoded; charset=utf-8")
                    .POST(HttpRequest.BodyPublishers.ofString(body))
                    .build();

            HttpResponse<String> response = getClient().send(request, HttpResponse.BodyHandlers.ofString());

            if (response.statusCode() >= 400) {
                throw new RuntimeException("HTTP request failed with status: " + response.statusCode());
            }

            return response.body();
        } catch (Exception e) {
            throw new RuntimeException("Failed to send POST form request to " + url, e);
        }
    }

    /**
     * Send PUT request with JSON body and return response body as string.
     *
     * @param url  target URL
     * @param json JSON request body
     * @return response body
     */
    public static String putJson(String url, String json) {
        return putJson(url, json, Map.of());
    }

    /**
     * Send PUT request with JSON body and headers, return response body as string.
     *
     * @param url     target URL
     * @param json    JSON request body
     * @param headers additional headers
     * @return response body
     */
    public static String putJson(String url, String json, Map<String, String> headers) {
        try {
            HttpRequest.Builder builder = HttpRequest.newBuilder()
                    .uri(URI.create(url))
                    .timeout(DEFAULT_TIMEOUT)
                    .header("Content-Type", "application/json; charset=utf-8")
                    .PUT(HttpRequest.BodyPublishers.ofString(json));

            headers.forEach(builder::header);

            HttpRequest request = builder.build();
            HttpResponse<String> response = getClient().send(request, HttpResponse.BodyHandlers.ofString());

            if (response.statusCode() >= 400) {
                throw new RuntimeException("HTTP request failed with status: " + response.statusCode());
            }

            return response.body();
        } catch (Exception e) {
            throw new RuntimeException("Failed to send PUT request to " + url, e);
        }
    }

    /**
     * Send DELETE request and return response body as string.
     *
     * @param url target URL
     * @return response body
     */
    public static String delete(String url) {
        return delete(url, Map.of());
    }

    /**
     * Send DELETE request with headers and return response body as string.
     *
     * @param url     target URL
     * @param headers request headers
     * @return response body
     */
    public static String delete(String url, Map<String, String> headers) {
        try {
            HttpRequest.Builder builder = HttpRequest.newBuilder()
                    .uri(URI.create(url))
                    .timeout(DEFAULT_TIMEOUT)
                    .DELETE();

            headers.forEach(builder::header);

            HttpRequest request = builder.build();
            HttpResponse<String> response = getClient().send(request, HttpResponse.BodyHandlers.ofString());

            if (response.statusCode() >= 400) {
                throw new RuntimeException("HTTP request failed with status: " + response.statusCode());
            }

            return response.body();
        } catch (Exception e) {
            throw new RuntimeException("Failed to send DELETE request to " + url, e);
        }
    }

    private static String encode(String value) {
        return java.net.URLEncoder.encode(value, java.nio.charset.StandardCharsets.UTF_8);
    }
}

参考链接