Micronaut
マイクロサービスとサーバーレス向けの軽量Javaフレームワーク。コンパイル時DIとAOTコンパイルにより高効率を実現。
GitHub概要
micronaut-projects/micronaut-core
Micronaut Application Framework
スター6,280
ウォッチ171
フォーク1,118
作成日:2018年3月7日
言語:Java
ライセンス:Apache License 2.0
トピックス
cloudnativegroovyjavakotlinmicroservicesserverless
スター履歴
データ取得日時: 2025/8/13 01:43
フレームワーク
Micronaut
概要
Micronautは、マイクロサービス向けに最適化された次世代のJVMフレームワークです。
詳細
Micronaut(マイクロノート)は、モダンなJVMベースのフルスタックフレームワークで、マイクロサービスやサーバーレスアプリケーションの構築に特化しています。2018年にObject Computing社によって開発され、従来のフレームワークとは根本的に異なるアプローチを採用しています。
最大の特徴は、**コンパイル時依存性注入(DI)とアスペクト指向プログラミング(AOP)**です。Spring Bootなどの従来フレームワークがランタイム時にリフレクションを使用するのに対し、Micronautはアノテーションプロセッサーを使用してコンパイル時に必要なメタデータを生成します。これにより、ランタイムでのリフレクション処理が不要となり、大幅な高速化と低メモリ使用量を実現しています。
GraalVM Native Imageとの組み合わせでは、10ms以下の起動時間と最小64MBでの動作が可能で、AWS Lambdaなどのサーバーレス環境で90%のコールドスタート時間短縮、75%のメモリ削減を実現した事例があります。Java 17以上をサポートし、Java、Groovy、Kotlinでの開発が可能です。クラウドネイティブアプリケーション開発に必要な機能(設定管理、サービスディスカバリ、分散トレーシング)を標準装備し、Kubernetes環境での運用に最適化されています。
メリット・デメリット
メリット
- 高速起動: コンパイル時DIにより従来比10倍以上の起動速度
- 低メモリ消費: リフレクション不使用で80MB程度から動作可能
- GraalVM Native対応: ネイティブイメージによりサブ秒起動を実現
- マイクロサービス最適化: 分散システム開発に必要な機能を標準搭載
- サーバーレス親和性: Lambda、Azure Functions等で高いパフォーマンス
- コンパイル時安全性: DIエラーをコンパイル時に検出
- 多言語サポート: Java、Groovy、Kotlin対応
- クラウドネイティブ: Kubernetes、Service Mesh対応
デメリット
- 学習コストの高さ: 従来Spring経験者も新しい考え方が必要
- エコシステムの限定性: Springと比較してライブラリやツールが少ない
- デバッグの複雑さ: コンパイル時生成コードのデバッグが困難
- ビルド時間の増加: アノテーション処理により初回ビルドが重い
- 新しい技術: 採用事例が限定的で情報が少ない
- Java依存: JVM言語に限定される
- Spring Boot移行コスト: 既存アプリケーション移行に工数が必要
主要リンク
- Micronaut公式サイト
- Micronaut公式ドキュメント
- Micronaut GitHub
- Micronaut Launch
- Micronaut Guides
- Micronaut Examples
書き方の例
Hello World(基本的なWebアプリケーション)
package com.example;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.micronaut.runtime.Micronaut;
@Controller
public class Application {
@Get("/")
public String index() {
return "Hello Micronaut!";
}
@Get("/api/status")
public String status() {
return "Service is running";
}
public static void main(String[] args) {
Micronaut.run(Application.class, args);
}
}
依存性注入(コンパイル時DI)
package com.example.service;
import jakarta.inject.Singleton;
// エンジンインターフェース
public interface Engine {
String start();
}
// V8エンジン実装
@Singleton
public class V8Engine implements Engine {
@Override
public String start() {
return "V8 Engine started";
}
}
// 車クラス(コンストラクタ注入)
package com.example.model;
import jakarta.inject.Inject;
import jakarta.inject.Singleton;
@Singleton
public class Vehicle {
private final Engine engine;
@Inject
public Vehicle(Engine engine) {
this.engine = engine;
}
public String startVehicle() {
return engine.start();
}
}
// 使用例
package com.example.controller;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import com.example.model.Vehicle;
@Controller("/api/vehicle")
public class VehicleController {
private final Vehicle vehicle;
public VehicleController(Vehicle vehicle) {
this.vehicle = vehicle;
}
@Get("/start")
public String startVehicle() {
return vehicle.startVehicle();
}
}
REST API開発
package com.example.controller;
import io.micronaut.http.annotation.*;
import io.micronaut.http.HttpStatus;
import io.micronaut.validation.Validated;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Email;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.Map;
@Controller("/api/users")
@Validated
public class UserController {
private final Map<Long, User> users = new ConcurrentHashMap<>();
private Long nextId = 1L;
@Get
public List<User> getAllUsers() {
return users.values().stream().toList();
}
@Get("/{id}")
public User getUserById(@PathVariable Long id) {
User user = users.get(id);
if (user == null) {
throw new UserNotFoundException("User not found: " + id);
}
return user;
}
@Post
@Status(HttpStatus.CREATED)
public User createUser(@Body @Valid User user) {
user.setId(nextId++);
users.put(user.getId(), user);
return user;
}
@Put("/{id}")
public User updateUser(@PathVariable Long id, @Body @Valid User updatedUser) {
if (!users.containsKey(id)) {
throw new UserNotFoundException("User not found: " + id);
}
updatedUser.setId(id);
users.put(id, updatedUser);
return updatedUser;
}
@Delete("/{id}")
@Status(HttpStatus.NO_CONTENT)
public void deleteUser(@PathVariable Long id) {
if (!users.containsKey(id)) {
throw new UserNotFoundException("User not found: " + id);
}
users.remove(id);
}
}
// Userエンティティクラス
package com.example.model;
import io.micronaut.core.annotation.Introspected;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
@Introspected
public class User {
private Long id;
@NotBlank
private String name;
@Email
@NotBlank
private String email;
// コンストラクタ
public User() {}
public User(String name, String email) {
this.name = name;
this.email = email;
}
// ゲッター・セッター
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
}
// カスタム例外
package com.example.exception;
public class UserNotFoundException extends RuntimeException {
public UserNotFoundException(String message) {
super(message);
}
}
データベース操作(Micronaut Data)
package com.example.entity;
import io.micronaut.data.annotation.GeneratedValue;
import io.micronaut.data.annotation.Id;
import io.micronaut.data.annotation.MappedEntity;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Positive;
@MappedEntity("products")
public class Product {
@Id
@GeneratedValue
private Long id;
@NotNull
private String name;
private String description;
@NotNull
@Positive
private Double price;
// コンストラクタ
public Product() {}
public Product(String name, String description, Double price) {
this.name = name;
this.description = description;
this.price = price;
}
// ゲッター・セッター
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
public Double getPrice() { return price; }
public void setPrice(Double price) { this.price = price; }
}
// Repository
package com.example.repository;
import io.micronaut.data.annotation.Repository;
import io.micronaut.data.repository.CrudRepository;
import io.micronaut.data.annotation.Query;
import com.example.entity.Product;
import java.util.List;
@Repository
public interface ProductRepository extends CrudRepository<Product, Long> {
List<Product> findByNameContaining(String name);
List<Product> findByPriceBetween(Double minPrice, Double maxPrice);
@Query("SELECT p FROM Product p WHERE p.price > :price")
List<Product> findExpensiveProducts(Double price);
List<Product> findByNameContainingOrderByPriceAsc(String name);
}
// Service
package com.example.service;
import jakarta.inject.Singleton;
import com.example.entity.Product;
import com.example.repository.ProductRepository;
import java.util.List;
import java.util.Optional;
@Singleton
public class ProductService {
private final ProductRepository productRepository;
public ProductService(ProductRepository productRepository) {
this.productRepository = productRepository;
}
public List<Product> getAllProducts() {
return (List<Product>) productRepository.findAll();
}
public Optional<Product> getProductById(Long id) {
return productRepository.findById(id);
}
public Product saveProduct(Product product) {
return productRepository.save(product);
}
public void deleteProduct(Long id) {
productRepository.deleteById(id);
}
public List<Product> searchProductsByName(String name) {
return productRepository.findByNameContaining(name);
}
public List<Product> getProductsInPriceRange(Double minPrice, Double maxPrice) {
return productRepository.findByPriceBetween(minPrice, maxPrice);
}
}
リアクティブプログラミング
package com.example.controller;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.reactivex.Single;
import io.reactivex.Observable;
import reactor.core.publisher.Mono;
import reactor.core.publisher.Flux;
import java.time.Duration;
@Controller("/api/reactive")
public class ReactiveController {
// RxJava Single
@Get("/single")
public Single<String> getSingle() {
return Single.fromCallable(() -> {
// 重い処理をシミュレート
Thread.sleep(1000);
return "Single response";
});
}
// RxJava Observable
@Get("/stream")
public Observable<String> getStream() {
return Observable.interval(1, java.util.concurrent.TimeUnit.SECONDS)
.map(i -> "Item " + i)
.take(5);
}
// Reactor Mono
@Get("/mono")
public Mono<String> getMono() {
return Mono.fromCallable(() -> "Mono response")
.delayElement(Duration.ofSeconds(1));
}
// Reactor Flux
@Get("/flux")
public Flux<String> getFlux() {
return Flux.interval(Duration.ofSeconds(1))
.map(i -> "Flux item " + i)
.take(3);
}
}
// リアクティブHTTPクライアント
package com.example.client;
import io.micronaut.http.annotation.Get;
import io.micronaut.http.client.annotation.Client;
import io.reactivex.Single;
import reactor.core.publisher.Mono;
@Client("https://api.github.com")
public interface GitHubClient {
@Get("/users/{username}")
Single<String> getUser(String username);
@Get("/users/{username}/repos")
Mono<String> getUserRepos(String username);
}
AOPとインターセプター
package com.example.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Timed {
String description() default "";
}
// インターセプター実装
package com.example.interceptor;
import io.micronaut.aop.MethodInterceptor;
import io.micronaut.aop.MethodInvocationContext;
import io.micronaut.context.annotation.Type;
import jakarta.inject.Singleton;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Singleton
@Type(Timed.class)
public class TimedInterceptor implements MethodInterceptor<Object, Object> {
private static final Logger LOG = LoggerFactory.getLogger(TimedInterceptor.class);
@Override
public Object intercept(MethodInvocationContext<Object, Object> context) {
long start = System.currentTimeMillis();
String description = context.getAnnotation(Timed.class)
.stringValue("description")
.orElse("");
try {
Object result = context.proceed();
long duration = System.currentTimeMillis() - start;
LOG.info("Method {} {} executed in {}ms",
context.getMethodName(), description, duration);
return result;
} catch (Exception e) {
long duration = System.currentTimeMillis() - start;
LOG.error("Method {} {} failed after {}ms",
context.getMethodName(), description, duration, e);
throw e;
}
}
}
// AOPの使用例
package com.example.service;
import jakarta.inject.Singleton;
import com.example.annotation.Timed;
@Singleton
public class BusinessService {
@Timed(description = "heavy computation")
public String performHeavyTask() {
try {
Thread.sleep(2000); // 重い処理をシミュレート
return "Task completed";
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("Task interrupted", e);
}
}
@Timed
public String quickTask() {
return "Quick task done";
}
}
テスト
package com.example.controller;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.HttpStatus;
import io.micronaut.http.client.HttpClient;
import io.micronaut.http.client.annotation.Client;
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import org.junit.jupiter.api.Test;
import jakarta.inject.Inject;
import static org.junit.jupiter.api.Assertions.assertEquals;
@MicronautTest
public class UserControllerTest {
@Inject
@Client("/")
HttpClient client;
@Test
public void testGetAllUsers() {
var response = client.toBlocking().exchange(
HttpRequest.GET("/api/users"),
String.class
);
assertEquals(HttpStatus.OK, response.getStatus());
}
@Test
public void testCreateUser() {
String userJson = """
{
"name": "John Doe",
"email": "[email protected]"
}
""";
var response = client.toBlocking().exchange(
HttpRequest.POST("/api/users", userJson),
String.class
);
assertEquals(HttpStatus.CREATED, response.getStatus());
}
}
// サービステスト
package com.example.service;
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import jakarta.inject.Inject;
import com.example.repository.ProductRepository;
import com.example.entity.Product;
import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;
@MicronautTest
public class ProductServiceTest {
@Inject
ProductService productService;
@Mock
ProductRepository productRepository;
@Test
public void testSaveProduct() {
// Given
Product product = new Product("Test Product", "Description", 99.99);
when(productRepository.save(any(Product.class))).thenReturn(product);
// When
Product result = productService.saveProduct(product);
// Then
assertNotNull(result);
assertEquals("Test Product", result.getName());
verify(productRepository).save(product);
}
}