Micronaut

マイクロサービスとサーバーレス向けの軽量Javaフレームワーク。コンパイル時DIとAOTコンパイルにより高効率を実現。

JavaフレームワークMicroservicesDIAOPGraalVMNative

GitHub概要

micronaut-projects/micronaut-core

Micronaut Application Framework

スター6,280
ウォッチ171
フォーク1,118
作成日:2018年3月7日
言語:Java
ライセンス:Apache License 2.0

トピックス

cloudnativegroovyjavakotlinmicroservicesserverless

スター履歴

micronaut-projects/micronaut-core Star History
データ取得日時: 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移行コスト: 既存アプリケーション移行に工数が必要

主要リンク

書き方の例

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);
    }
}