Quarkus
クラウドネイティブJavaのためのスーパーソニック・サブアトミックフレームワーク。高速起動とネイティブイメージ最適化が特徴。
GitHub概要
quarkusio/quarkus
Quarkus: Supersonic Subatomic Java.
トピックス
スター履歴
フレームワーク
Quarkus
概要
QuarkusはKubernetesネイティブJavaフレームワークで、高速起動と低メモリ消費を実現するマイクロサービス開発プラットフォームです。
詳細
Quarkus(クアルカス)は、Red Hatによって開発されたクラウドネイティブ、マイクロサービス向けのJavaフレームワークです。2019年にリリースされて以来、「Supersonic Subatomic Java」をスローガンに、従来のJavaアプリケーションの課題であった起動時間の長さとメモリ消費量の多さを根本的に解決することを目指しています。
最大の特徴は、GraalVM Native Imageとコンパイル時最適化による超高速起動と超低メモリ消費です。従来のJVMアプリケーションが数秒から数十秒の起動時間を必要とするのに対し、Quarkusネイティブアプリケーションは100ms以下での起動が可能で、Spring Bootと比較して90%以上の起動時間短縮を実現します。メモリ使用量においても75%の削減を達成し、最小64MBからの動作が可能です。
**Developer Joy(開発者体験の向上)**に重点を置き、ライブリロード機能により、コード変更が即座に反映され、開発効率が大幅に向上します。また、コンパイル時依存性注入とAOT(Ahead-of-Time)コンパイルにより、ランタイムではなくビルド時に多くの処理を実行し、実行時のオーバーヘッドを最小化します。
サーバーレス環境(AWS Lambda、Azure Functions等)やコンテナ環境(Kubernetes、Docker)での実行に最適化されており、コールドスタート問題を解決することで、真のクラウドネイティブJavaアプリケーション開発を可能にします。現在は300以上のエクステンション(拡張機能)を提供し、Spring系開発者の移行を支援する豊富なAPIセットも用意されています。
メリット・デメリット
メリット
- 超高速起動: 100ms以下でのアプリケーション起動を実現
- 超低メモリ消費: 最小64MBからの動作、従来比75%削減
- 優れたスループット: ネイティブ実行時の高いパフォーマンス
- Kubernetes最適化: クラウドネイティブ環境での卓越した性能
- サーバーレス対応: Lambda等での高速コールドスタート
- Developer Joy: ライブリロード機能による高い開発効率
- GraalVM統合: ネイティブコンパイルによる最適化
- 豊富なエクステンション: 300以上の機能拡張
デメリット
- 学習コスト: 新しい概念とQuarkus固有の仕組みの習得が必要
- ビルド時間: ネイティブコンパイルに長時間を要する
- エコシステム: Spring Bootと比較してサードパーティライブラリが限定的
- デバッグの困難さ: ネイティブ実行時のデバッグが複雑
- リフレクション制限: GraalVMでのリフレクション使用に制約
- 成熟度: 比較的新しいフレームワークで事例が限定的
- 実行時制約: ネイティブ実行時の機能制限
主要リンク
書き方の例
Hello World(基本的なWebアプリケーション)
package org.acme;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
@Path("/hello")
public class GreetingResource {
@GET
@Produces(MediaType.TEXT_PLAIN)
public String hello() {
return "Hello from Quarkus!";
}
@GET
@Path("/status")
@Produces(MediaType.TEXT_PLAIN)
public String status() {
return "Service is running with Quarkus";
}
}
// application.propertiesでの設定
// quarkus.http.port=8080
// quarkus.application.name=quarkus-demo
REST API開発(CDI使用)
package org.acme.model;
import io.quarkus.hibernate.orm.panache.PanacheEntity;
import jakarta.persistence.Entity;
import java.util.List;
@Entity
public class Product extends PanacheEntity {
public String name;
public String description;
public Double price;
public Integer quantity;
// パンケーキエンティティパターンによる簡潔な実装
public static Product findByName(String name) {
return find("name", name).firstResult();
}
public static List<Product> findByPriceRange(Double minPrice, Double maxPrice) {
return find("price >= ?1 and price <= ?2", minPrice, maxPrice).list();
}
}
// REST リソース
package org.acme.resource;
import org.acme.model.Product;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.transaction.Transactional;
import java.util.List;
@Path("/api/products")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class ProductResource {
@GET
public List<Product> getAllProducts() {
return Product.listAll();
}
@GET
@Path("/{id}")
public Product getProduct(@PathParam("id") Long id) {
Product product = Product.findById(id);
if (product == null) {
throw new WebApplicationException("Product not found", 404);
}
return product;
}
@POST
@Transactional
public Response createProduct(Product product) {
if (product.name == null || product.name.trim().isEmpty()) {
return Response.status(400).entity("Product name is required").build();
}
product.persist();
return Response.status(201).entity(product).build();
}
@PUT
@Path("/{id}")
@Transactional
public Product updateProduct(@PathParam("id") Long id, Product updatedProduct) {
Product product = Product.findById(id);
if (product == null) {
throw new WebApplicationException("Product not found", 404);
}
product.name = updatedProduct.name;
product.description = updatedProduct.description;
product.price = updatedProduct.price;
product.quantity = updatedProduct.quantity;
return product;
}
@DELETE
@Path("/{id}")
@Transactional
public Response deleteProduct(@PathParam("id") Long id) {
Product product = Product.findById(id);
if (product == null) {
throw new WebApplicationException("Product not found", 404);
}
product.delete();
return Response.noContent().build();
}
@GET
@Path("/search")
public List<Product> searchByPriceRange(
@QueryParam("min") Double minPrice,
@QueryParam("max") Double maxPrice) {
return Product.findByPriceRange(minPrice, maxPrice);
}
}
リアクティブプログラミング(Mutiny使用)
package org.acme.reactive;
import io.smallrye.mutiny.Uni;
import io.smallrye.mutiny.Multi;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import java.time.Duration;
import java.util.Random;
@Path("/api/reactive")
public class ReactiveResource {
private final Random random = new Random();
@GET
@Path("/async")
@Produces(MediaType.TEXT_PLAIN)
public Uni<String> asyncEndpoint() {
return Uni.createFrom().item(() -> {
// 非同期処理をシミュレート
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return "非同期処理完了: " + System.currentTimeMillis();
});
}
@GET
@Path("/stream")
@Produces(MediaType.SERVER_SENT_EVENTS)
public Multi<String> streamData() {
return Multi.createFrom().ticks().every(Duration.ofSeconds(1))
.map(tick -> "ストリームデータ: " + tick + " - " + System.currentTimeMillis())
.take(10);
}
@GET
@Path("/weather/{city}")
@Produces(MediaType.APPLICATION_JSON)
public Uni<WeatherInfo> getWeatherInfo(@PathParam("city") String city) {
return Uni.createFrom().item(() -> {
// 外部API呼び出しをシミュレート
return new WeatherInfo(city, random.nextInt(40), "晴れ");
})
.onItem().delayIt().by(Duration.ofMillis(500));
}
@GET
@Path("/combined")
@Produces(MediaType.APPLICATION_JSON)
public Uni<CombinedResult> getCombinedData() {
Uni<String> service1 = Uni.createFrom().item("Service1データ")
.onItem().delayIt().by(Duration.ofMillis(300));
Uni<String> service2 = Uni.createFrom().item("Service2データ")
.onItem().delayIt().by(Duration.ofMillis(500));
return Uni.combine().all().unis(service1, service2)
.asTuple()
.map(tuple -> new CombinedResult(tuple.getItem1(), tuple.getItem2()));
}
public static class WeatherInfo {
public String city;
public int temperature;
public String condition;
public WeatherInfo(String city, int temperature, String condition) {
this.city = city;
this.temperature = temperature;
this.condition = condition;
}
}
public static class CombinedResult {
public String result1;
public String result2;
public long timestamp;
public CombinedResult(String result1, String result2) {
this.result1 = result1;
this.result2 = result2;
this.timestamp = System.currentTimeMillis();
}
}
}
データベース操作(Hibernate Reactive)
package org.acme.reactive.model;
import io.quarkus.hibernate.reactive.panache.PanacheEntity;
import io.smallrye.mutiny.Uni;
import jakarta.persistence.Entity;
import java.util.List;
@Entity
public class User extends PanacheEntity {
public String username;
public String email;
public String firstName;
public String lastName;
public static Uni<User> findByUsername(String username) {
return find("username", username).firstResult();
}
public static Uni<List<User>> findByEmailDomain(String domain) {
return find("email like ?1", "%" + domain).list();
}
}
// リアクティブサービス
package org.acme.reactive.service;
import org.acme.reactive.model.User;
import io.smallrye.mutiny.Uni;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.transaction.Transactional;
import java.util.List;
@ApplicationScoped
public class UserService {
public Uni<List<User>> getAllUsers() {
return User.listAll();
}
public Uni<User> getUserById(Long id) {
return User.findById(id);
}
@Transactional
public Uni<User> createUser(User user) {
return user.persist().map(v -> user);
}
@Transactional
public Uni<User> updateUser(Long id, User updatedUser) {
return User.findById(id)
.onItem().ifNotNull().transform(user -> {
user.username = updatedUser.username;
user.email = updatedUser.email;
user.firstName = updatedUser.firstName;
user.lastName = updatedUser.lastName;
return user;
});
}
@Transactional
public Uni<Boolean> deleteUser(Long id) {
return User.deleteById(id);
}
public Uni<User> findByUsername(String username) {
return User.findByUsername(username);
}
}
// リアクティブREST リソース
package org.acme.reactive.resource;
import org.acme.reactive.model.User;
import org.acme.reactive.service.UserService;
import io.smallrye.mutiny.Uni;
import jakarta.inject.Inject;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import java.util.List;
@Path("/api/reactive/users")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class ReactiveUserResource {
@Inject
UserService userService;
@GET
public Uni<List<User>> getAllUsers() {
return userService.getAllUsers();
}
@GET
@Path("/{id}")
public Uni<Response> getUser(@PathParam("id") Long id) {
return userService.getUserById(id)
.onItem().ifNotNull().transform(user -> Response.ok(user).build())
.onItem().ifNull().continueWith(Response.status(404).build());
}
@POST
public Uni<Response> createUser(User user) {
return userService.createUser(user)
.onItem().transform(created -> Response.status(201).entity(created).build());
}
@PUT
@Path("/{id}")
public Uni<Response> updateUser(@PathParam("id") Long id, User user) {
return userService.updateUser(id, user)
.onItem().ifNotNull().transform(updated -> Response.ok(updated).build())
.onItem().ifNull().continueWith(Response.status(404).build());
}
@DELETE
@Path("/{id}")
public Uni<Response> deleteUser(@PathParam("id") Long id) {
return userService.deleteUser(id)
.onItem().transform(deleted -> deleted ?
Response.noContent().build() :
Response.status(404).build());
}
@GET
@Path("/username/{username}")
public Uni<Response> getUserByUsername(@PathParam("username") String username) {
return userService.findByUsername(username)
.onItem().ifNotNull().transform(user -> Response.ok(user).build())
.onItem().ifNull().continueWith(Response.status(404).build());
}
}
ネイティブ設定とプロファイル
# application.properties
# 基本設定
quarkus.application.name=quarkus-native-demo
quarkus.http.port=8080
# データベース設定
quarkus.datasource.db-kind=postgresql
quarkus.datasource.username=user
quarkus.datasource.password=password
quarkus.datasource.reactive.url=postgresql://localhost:5432/testdb
# Hibernate設定
quarkus.hibernate-orm.database.generation=drop-and-create
quarkus.hibernate-orm.sql-load-script=import.sql
# 開発環境設定
%dev.quarkus.log.level=DEBUG
%dev.quarkus.datasource.reactive.url=postgresql://localhost:5432/testdb_dev
# 本番環境設定
%prod.quarkus.log.level=INFO
%prod.quarkus.datasource.reactive.url=${DATABASE_URL}
# ネイティブ設定
quarkus.native.additional-build-args=--verbose,--no-fallback
quarkus.native.enable-https-url-handler=true
テスト(Quarkus Test Framework)
package org.acme;
import io.quarkus.test.junit.QuarkusTest;
import io.restassured.http.ContentType;
import org.junit.jupiter.api.Test;
import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.*;
@QuarkusTest
public class ProductResourceTest {
@Test
public void testGetAllProducts() {
given()
.when().get("/api/products")
.then()
.statusCode(200)
.contentType(ContentType.JSON)
.body("size()", greaterThanOrEqualTo(0));
}
@Test
public void testCreateProduct() {
String product = """
{
"name": "Test Product",
"description": "Test Description",
"price": 99.99,
"quantity": 10
}
""";
given()
.contentType(ContentType.JSON)
.body(product)
.when().post("/api/products")
.then()
.statusCode(201)
.body("name", equalTo("Test Product"))
.body("price", equalTo(99.99f));
}
@Test
public void testGetProductById() {
given()
.when().get("/api/products/1")
.then()
.statusCode(anyOf(equalTo(200), equalTo(404)));
}
@Test
public void testSearchByPriceRange() {
given()
.queryParam("min", 10.0)
.queryParam("max", 100.0)
.when().get("/api/products/search")
.then()
.statusCode(200)
.contentType(ContentType.JSON);
}
}
// リアクティブテスト
package org.acme.reactive;
import io.quarkus.test.junit.QuarkusTest;
import io.restassured.http.ContentType;
import org.junit.jupiter.api.Test;
import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.*;
@QuarkusTest
public class ReactiveResourceTest {
@Test
public void testAsyncEndpoint() {
given()
.when().get("/api/reactive/async")
.then()
.statusCode(200)
.contentType(ContentType.TEXT)
.body(containsString("非同期処理完了"));
}
@Test
public void testWeatherEndpoint() {
given()
.when().get("/api/reactive/weather/Tokyo")
.then()
.statusCode(200)
.contentType(ContentType.JSON)
.body("city", equalTo("Tokyo"))
.body("temperature", instanceOf(Integer.class))
.body("condition", notNullValue());
}
@Test
public void testCombinedEndpoint() {
given()
.when().get("/api/reactive/combined")
.then()
.statusCode(200)
.contentType(ContentType.JSON)
.body("result1", equalTo("Service1データ"))
.body("result2", equalTo("Service2データ"))
.body("timestamp", instanceOf(Long.class));
}
}
ネイティブビルドとコンテナ化
# JVMモードでの開発・実行
./mvnw compile quarkus:dev
# ネイティブイメージビルド(GraalVM使用)
./mvnw package -Pnative
# Dockerコンテナ内でのネイティブビルド
./mvnw package -Pnative -Dquarkus.native.container-build=true
# 実行ファイルの起動(超高速)
./target/quarkus-demo-1.0-runner
# Dockerfile.native
FROM registry.access.redhat.com/ubi8/ubi-minimal:8.6
WORKDIR /work/
RUN chown 1001 /work \
&& chmod "g+rwX" /work \
&& chown 1001:root /work
COPY --chown=1001:root target/*-runner /work/application
EXPOSE 8080
USER 1001
CMD ["./application", "-Dquarkus.http.host=0.0.0.0"]