← Kursa Dön
📄 Text · 15 min

Java Platform Module System (JPMS)

Java 9'dan önce projeler büyüdükçe bir kaos ortamı oluşuyordu: yüzlerce JAR dosyası classpath'e atılıyor, hangi sınıfın hangi JAR'dan geldiği belli olmuyordu, aynı sınıf birden fazla JAR'da bulununca "hangisini kullanacağım" sorusu cevapsız kalıyordu. Java Platform Module System (JPMS) bu karmaşayı çözmek için Java 9'da tanıtıldı.

Bu derste modül sisteminin neden var olduğunu, nasıl çalıştığını ve projelerimizde nasıl kullanacağımızı öğreneceğiz.


1. Java 9 Öncesi: Classpath Cehennemi

Java 9'dan önce derleme ve çalıştırma sırasında tüm JAR dosyaları tek bir düz listeye — classpath'e — ekleniyordu. JVM bu listedeki her şeyi "büyük bir çorba" gibi görüyordu. İki temel problem vardı.

Birincisi: İsim çakışması. İki farklı kütüphane aynı tam nitelikli sınıf adını (fully qualified name) içerebiliyordu. JVM classpath'te hangisini önce bulursa onu yüklüyor, diğerini sessizce görmezden geliyordu. Bu, çalışma zamanında garip hatalar üretiyordu.

İkincisi: Kapsülleme yoktu. Bir kütüphane sadece kendi iç kullanımı için tasarladığı sınıfları public yapmak zorundaydı — çünkü başka bir paketten erişmek için tek yol buydu. Ama public demek "herkes erişebilir" demekti. Kütüphane yazarı "bu sınıf internal, dokunmayın" dese bile hiçbir garanti yoktu.

classpath cehennemi:
libs/
├── logging-1.0.jar      (com.util.Logger)
├── analytics-2.3.jar    (com.util.Logger)  ← Aynı isim!
├── http-client-4.5.jar
├── json-parser-1.2.jar
└── ...50 JAR daha

JVM: "com.util.Logger hangisi? İlk bulduğumu alıyorum."

Bu duruma "JAR Hell" ya da "Classpath Hell" denir. Büyük projelerde — özellikle kurumsal Java'da — en sık karşılaşılan ve en zor debuglanan sorunlardan biriydi.

🎯 Analoji — Apartman ve Güvenlik:

>

Java 9 öncesini, kapısında kilit olmayan bir apartman gibi düşün. Her daire (JAR) kapısı açık, herkes her odaya girebilir. Kimin ne eşyası olduğu belli değil, iki dairede aynı marka buzdolabı varsa hangisinin kime ait olduğu karışır. JPMS ise her daireye kilit takıp, bir güvenlik sistemi kurmak gibidir — her modül kendi kapısını kontrol eder, sadece izin verdiği şeyleri dışarıya açar.


2. Modül Nedir?

Modül, en basit tanımıyla paketlerin bir üst katmanıdır. Bir modül şunları yapar:

  • İçerdiği paketlerden hangilerinin dış dünyaya açık olduğunu belirler (exports)

  • Hangi diğer modüllere bağımlı olduğunu açıkça bildirir (requires)

  • Reflection erişimine izin vereceği paketleri kontrol eder (opens)

Paketler sınıfları gruplar, modüller ise paketleri gruplar. Ama modüller sadece gruplama yapmaz — erişim kontrolü sağlar.

Modül vs Paket

ÖzellikPaketModül
GruplamaSınıfları gruplarPaketleri gruplar
Erişim kontrolüpublic/protected/default/privateexports/opens ile paket seviyesinde
BağımlılıkYok (import ile sadece kullanım belirtilir)requires ile açıkça tanımlanır
Tanım dosyasıYok (dizin yapısına göre)module-info.java
KapsamTek bir isim alanıBirden fazla paket + meta bilgi

Modüller JVM'e "ben şu modüllere ihtiyaç duyuyorum ve şu paketlerimi dışarıya açıyorum" der. JVM bu bilgiyi derleme zamanında ve çalışma zamanında kontrol eder. Classpath'teki gibi "çalışırken sürpriz" olmaz.


3. module-info.java: Modülün Kimlik Kartı

Her modülün kök dizininde bir module-info.java dosyası bulunur. Bu dosya modülün adını, bağımlılıklarını ve dışa açtığı paketleri tanımlar.

Temel Yapı

module com.myapp.core {
    requires java.sql;           // Bu modüle ihtiyacım var
    requires java.logging;       // Buna da

    exports com.myapp.core.api;  // Bu paketimi dışarıya açıyorum
    exports com.myapp.core.model;

    // Bu paketi reflection için açıyorum (framework'ler için)
    opens com.myapp.core.entity to com.google.gson;
}

Her bir direktifi tek tek inceleyelim.

requires — Bağımlılık Bildirimi

requires ile modülünüzün hangi diğer modüllere ihtiyaç duyduğunu belirtirsiniz. Derleme ve çalışma zamanında bu modüller mevcut olmalıdır.

module com.myapp.web {
    requires java.net.http;   // Java'nın HTTP client modülü
    requires java.sql;        // JDBC modülü
    requires com.myapp.core;  // Kendi yazdığımız core modül
}

java.base modülü otomatik olarak her modüle dahildir — onu requires ile eklemenize gerek yoktur. String, Object, List, Map gibi temel sınıflar java.base içindedir.

exports — Paket Açma

exports ile modülünüzün belirli paketlerini diğer modüllerin kullanımına açarsınız. Export edilmeyen paketler modül dışından kesinlikle erişilemez — sınıflar public bile olsa.

module com.myapp.core {
    exports com.myapp.core.api;     // Herkes kullanabilir
    exports com.myapp.core.model;   // Herkes kullanabilir
    // com.myapp.core.internal → export edilmedi, dışarıdan erişilemez
}

Bu, Java'nın modül öncesi en büyük eksikliğini kapatır. Artık "bu internal bir sınıf, kullanmayın" demek yerine gerçekten erişimi engelleyebilirsiniz.

opens — Reflection Erişimi

opens direktifi, bir paketi runtime'da reflection ile erişime açar. Normal exports derleme zamanında erişim verir ama reflection'ı engellemez — tam tersi, opens özellikle reflection için tasarlanmıştır.

module com.myapp.core {
    exports com.myapp.core.api;

    // Jackson/Gson gibi kütüphaneler private alanlara reflection ile erişsin
    opens com.myapp.core.entity;
}

Framework'ler (Spring, Jackson, Hibernate) genellikle reflection kullanır. Bu yüzden entity veya DTO sınıflarınızın bulunduğu paketleri opens ile açmanız gerekir.

provides / uses — Service Loading

provides ve uses direktifleri Java'nın ServiceLoader mekanizmasıyla çalışır. Bir modül servis arayüzü tanımlar (uses), başka bir modül o servisi implemente eder (provides).

// API modülü — servisi tanımlar
module com.myapp.api {
    exports com.myapp.api;
    uses com.myapp.api.PaymentProcessor;
}

// Implementasyon modülü — servisi sağlar
module com.myapp.stripe {
    requires com.myapp.api;
    provides com.myapp.api.PaymentProcessor
        with com.myapp.stripe.StripeProcessor;
}

Bu pattern, plugin mimarisi veya stratejik soyutlama için çok kullanışlıdır. Uygulama hangi implementasyonun yüklendiğini bilmek zorunda kalmaz.


4. Basit Modül Oluşturma ve Derleme

Teoriyi pratiğe dökelim. İki modüllü basit bir proje oluşturalım: com.myapp.greeter (selamlama servisi) ve com.myapp.main (ana uygulama).

Dizin Yapısı

project/
├── com.myapp.greeter/
│   ├── module-info.java
│   └── com/myapp/greeter/
│       └── Greeter.java
└── com.myapp.main/
    ├── module-info.java
    └── com/myapp/main/
        └── Main.java

Greeter Modülü

// com.myapp.greeter/module-info.java
module com.myapp.greeter {
    exports com.myapp.greeter;
}
// com.myapp.greeter/com/myapp/greeter/Greeter.java
package com.myapp.greeter;

public class Greeter {
    public String greet(String name) {
        return "Merhaba, " + name + "! Hoş geldiniz.";
    }
}

Main Modülü

// com.myapp.main/module-info.java
module com.myapp.main {
    requires com.myapp.greeter;
}
// com.myapp.main/com/myapp/main/Main.java
package com.myapp.main;

import com.myapp.greeter.Greeter;

public class Main {
    public static void main(String[] args) {
        Greeter greeter = new Greeter();
        System.out.println(greeter.greet("Tolgahan"));
    }
}

Derleme ve Çalıştırma

# Greeter modülünü derle
javac -d out/com.myapp.greeter \
    com.myapp.greeter/module-info.java \
    com.myapp.greeter/com/myapp/greeter/Greeter.java

# Main modülünü derle (greeter'a bağımlı)
javac --module-path out \
    -d out/com.myapp.main \
    com.myapp.main/module-info.java \
    com.myapp.main/com/myapp/main/Main.java

# Çalıştır
java --module-path out \
    --module com.myapp.main/com.myapp.main.Main

Çıktı:

Merhaba, Tolgahan! Hoş geldiniz.

Burada --module-path (kısa hali -p) classpath'in yerini alır. --module (kısa hali -m) ise hangi modülün hangi sınıfından başlanacağını belirtir.

⚠️ Dikkat: Modül isimlendirmesi reverse domain convention izler (com.myapp.greeter). Modül adı ile paket adı aynı olmak zorunda değil ama genellikle kök paket adıyla eşleştirilir. Bu, karışıklığı önler.


5. Modüler JDK

Java 9 ile birlikte JDK kendisi de modüler hale geldi. Eskiden rt.jar adında tek bir dev dosya vardı ve JDK'nın tüm sınıfları bu dosyanın içindeydi. Şimdi JDK ~70 modüle bölünmüş durumda.

Temel JDK Modülleri

ModülİçerikOtomatik?
java.baseString, Object, List, Map, Stream, Optional...Evet, her zaman dahil
java.sqlJDBC: Connection, Statement, ResultSet...Hayır, requires gerekir
java.loggingjava.util.logging (JUL)Hayır, requires gerekir
java.net.httpHttpClient (Java 11+)Hayır, requires gerekir
java.desktopAWT, SwingHayır, requires gerekir
java.xmlDOM, SAX, StAX XML parser'larHayır, requires gerekir

Hangi modüllerin var olduğunu görmek için:

java --list-modules

Bu komut sisteminizde yüklü JDK'nın tüm modüllerini listeler. Bir modülün içeriğini incelemek için:

java --describe-module java.sql

Çıktı:

java.sql jar:file:///path/to/jdk/jmods/java.sql.jmod
exports java.sql
exports javax.sql
requires java.base mandated
requires java.xml transitive
requires java.logging transitive

Bu çıktı bize java.sql modülünün java.xml ve java.logging modüllerini transitive olarak gerektirdiğini söylüyor. Peki transitive ne demek?


6. requires transitive — Bağımlılık Geçişkenliği

Normal requires ile bir modülü kullandığınızda, o modülün kendi bağımlılıkları sizin modülünüze geçmez. Yani A → B → C zincirinde A, C'yi göremez.

requires transitive ise bağımlılığı geçişken yapar: A, B'yi requires transitive ile kullanıyorsa, B'ye bağımlı olan herkes otomatik olarak A'ya da erişir.

Örnek: Transitive Olmadan

// logger modülü
module com.myapp.logger {
    requires java.logging;        // java.logging'i kullanıyorum
    exports com.myapp.logger;
}

// web modülü
module com.myapp.web {
    requires com.myapp.logger;    // logger'ı kullanıyorum
    // Ama java.logging'e erişimim YOK — çünkü transitive değil
}

Bu durumda com.myapp.web modülünden java.util.logging.Level gibi bir sınıfa erişmeye çalışırsanız derleme hatası alırsınız.

Örnek: Transitive İle

// logger modülü
module com.myapp.logger {
    requires transitive java.logging;  // geçişken bağımlılık
    exports com.myapp.logger;
}

// web modülü
module com.myapp.web {
    requires com.myapp.logger;
    // Artık java.logging'e de erişimim var!
}

requires transitive ne zaman kullanılır? API'nizin dönüş tipi veya parametre tipi başka bir modülden geliyorsa. Örneğin getLogger() metodunuz java.util.logging.Logger döndürüyorsa, kullanıcıların o tipi görmesi gerekir — dolayısıyla requires transitive java.logging yaparsınız.

💡 İpucu: Genel kural şu: eğer modülünüzün public API'sinde başka modülden bir tip görünüyorsa requires transitive kullanın. Sadece iç implementasyonda kullanıyorsanız normal requires yeterlidir.


7. Qualified Exports ve Opens

Bazen bir paketi herkese değil, sadece belirli modüllere açmak istersiniz. Bu durumda qualified (nitelikli) exports ve opens kullanırsınız.

Qualified Exports

module com.myapp.core {
    // Sadece web modülü bu pakete erişebilir
    exports com.myapp.core.internal to com.myapp.web;

    // Birden fazla modüle açmak
    exports com.myapp.core.spi to com.myapp.web, com.myapp.admin;
}

Bu, internal API'ları kontrollü şekilde paylaşmak için mükemmeldir. Kendi projenizdeki modüller arası iletişimde kullanırsınız ama dış dünyaya kapatırsınız.

Qualified Opens

module com.myapp.core {
    // Sadece Hibernate bu paketi reflection ile görebilir
    opens com.myapp.core.entity to org.hibernate.core;

    // Sadece Jackson bu paketi reflection ile görebilir
    opens com.myapp.core.dto to com.fasterxml.jackson.databind;
}

Framework'lerin reflection ihtiyacını karşılarken erişimi mümkün olduğunca daraltmak güvenlik açısından en iyi pratiktir.

open module — Tümünü Aç

Eğer modülünüzdeki tüm paketleri reflection'a açmak istiyorsanız modül tanımının başına open ekleyebilirsiniz:

open module com.myapp.core {
    requires java.sql;
    exports com.myapp.core.api;
    // Tüm paketler reflection'a açık — opens yazmaya gerek yok
}

Bu yaklaşım hızlı prototipleme veya framework-yoğun projelerde pratiktir ama üretim kodunda mümkünse spesifik opens tercih edilmelidir.


8. Migration: Unnamed ve Automatic Modüller

Gerçek dünyada tüm kütüphaneler modüler değildir. Mevcut projelerin ve kütüphanelerin modül sistemine geçişi için Java iki geçiş mekanizması sağlar.

Unnamed Module (İsimsiz Modül)

Classpath'e (--class-path) koyduğunuz tüm JAR dosyaları otomatik olarak unnamed module içinde yer alır. Bu modülün özellikleri:

  • Tüm paketlerini export eder (eski davranış korunur)

  • Tüm named modülleri okuyabilir

  • Ama named modüller unnamed module'ü requires ile belirtemez

# Eski stil — classpath kullanımı (unnamed module)
java --class-path libs/gson-2.10.jar:out \
    com.myapp.main.Main

Bu sayede eski projeler Java 9+ üzerinde hiçbir değişiklik yapmadan çalışmaya devam eder. Unnamed module bir geçiş köprüsüdür — mümkünse kendi kodunuzu modüler hale getirmelisiniz.

Automatic Module (Otomatik Modül)

Bir JAR dosyasını --module-path'e koyduğunuzda ama o JAR'ın içinde module-info.class yoksa, Java onu automatic module olarak ele alır.

Otomatik modülün adı iki şekilde belirlenir:

  1. JAR'ın MANIFEST.MF dosyasında Automatic-Module-Name attribute'u varsa o kullanılır

  2. Yoksa JAR dosya adından türetilir: gson-2.10.1.jargson modülü

// Gson henüz modüler değil ama module-path'e koyduğumuzda
// otomatik modül olarak kullanabiliriz
module com.myapp.core {
    requires gson;  // otomatik modül adı
}

Otomatik modüller:

  • Tüm paketlerini export eder

  • Tüm diğer modülleri (named ve unnamed dahil) okuyabilir

  • Named modüller tarafından requires ile kullanılabilir

Migration Stratejisi

Büyük bir projeyi modüler hale getirirken önerilen yaklaşım "bottom-up"tır:

1. Bağımlılığı olmayan kütüphanelerle başla
2. module-info.java ekle
3. Yukarı doğru ilerle
4. Üçüncü parti kütüphaneler automatic module olarak kalabilir

     com.myapp.main        ← En son modüler yap
          ↓
     com.myapp.web         ← Sonra bunu
          ↓
     com.myapp.core        ← İlk bunu modüler yap
          ↓
     gson (automatic)      ← Bu zaten otomatik modül olarak çalışır

⚠️ Dikkat: Automatic module'den named module'e geçiş yaparken, Automatic-Module-Name kullanın. Böylece modül adı JAR dosya adından bağımsız olur ve ileride gerçek module-info.java eklediğinizde isim değişikliği yaşanmaz.


9. Modül Sistemi ile Çalışırken Sık Yapılan Hatalar

Hata 1: Export Edilmeyen Pakete Erişim

error: package com.myapp.core.internal is not visible
    (package com.myapp.core.internal is declared in module
     com.myapp.core, which does not export it)

Bu hatayı aldığınızda iki seçeneğiniz var: ya paketi exports ile açarsınız, ya da o paketteki sınıfları kullanan kodu export edilen bir paketin arkasına gizlersiniz (daha iyi pratik).

Hata 2: requires Eksikliği

error: module com.myapp.web does not read module java.sql

Modülünüzde java.sql'den bir sınıf kullanıyorsanız ama requires java.sql yazmamışsanız bu hatayı alırsınız. Çözüm basit — module-info.java'ya requires ekleyin.

Hata 3: Split Package

İki farklı modülün aynı paketi içermesi modül sisteminde yasaktır. Bu, classpath döneminin en büyük sorunlarından biriydi ve modül sistemi bunu sıfır toleransla engeller.

error: module X and module Y both contain package com.util

Çözüm: paketleri yeniden isimlendirin veya tek bir modülde birleştirin.

Hata 4: Reflection Erişimi

java.lang.reflect.InaccessibleObjectException:
    Unable to make field private String name accessible:
    module com.myapp.core does not "opens com.myapp.core.entity"

Framework'ler (Jackson, Hibernate, Spring) private alanlara reflection ile erişmeye çalıştığında bu hatayı alırsınız. Çözüm: ilgili paketi opens ile açın.


10. Gerçek Dünya Örneği: Çok Modüllü Proje

Bir e-ticaret uygulamasının modül yapısını inceleyelim:

// com.shop.model — Veri modelleri
module com.shop.model {
    exports com.shop.model;
    opens com.shop.model to com.fasterxml.jackson.databind;
}
// com.shop.model/com/shop/model/Product.java
package com.shop.model;

public class Product {
    private String id;
    private String name;
    private double price;

    public Product() { }

    public Product(String id, String name, double price) {
        this.id = id;
        this.name = name;
        this.price = price;
    }

    public String getId() { return id; }
    public String getName() { return name; }
    public double getPrice() { return price; }

    @Override
    public String toString() {
        return name + " (" + price + " TL)";
    }
}
// com.shop.repository — Veri erişim katmanı
module com.shop.repository {
    requires transitive com.shop.model;  // API'da Product döndürüyoruz
    exports com.shop.repository;
}
// com.shop.repository/com/shop/repository/ProductRepository.java
package com.shop.repository;

import com.shop.model.Product;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

public class ProductRepository {
    private final List<Product> products = new ArrayList<>();

    public void save(Product product) {
        products.add(product);
    }

    public Optional<Product> findById(String id) {
        return products.stream()
            .filter(p -> p.getId().equals(id))
            .findFirst();
    }

    public List<Product> findAll() {
        return List.copyOf(products);
    }
}
// com.shop.service — İş mantığı
module com.shop.service {
    requires com.shop.repository;
    // com.shop.model'e de erişimimiz var — transitive sayesinde!
    exports com.shop.service;
}
// com.shop.service/com/shop/service/ProductService.java
package com.shop.service;

import com.shop.model.Product;
import com.shop.repository.ProductRepository;
import java.util.List;

public class ProductService {
    private final ProductRepository repository = new ProductRepository();

    public void addProduct(String id, String name, double price) {
        repository.save(new Product(id, name, price));
    }

    public List<Product> getExpensiveProducts(double minPrice) {
        return repository.findAll().stream()
            .filter(p -> p.getPrice() >= minPrice)
            .toList();
    }
}
// com.shop.app — Ana uygulama
module com.shop.app {
    requires com.shop.service;
}
// com.shop.app/com/shop/app/Main.java
package com.shop.app;

import com.shop.model.Product;
import com.shop.service.ProductService;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        ProductService service = new ProductService();
        service.addProduct("P1", "Laptop", 25000);
        service.addProduct("P2", "Mouse", 500);
        service.addProduct("P3", "Monitor", 12000);

        List<Product> expensive = service.getExpensiveProducts(10000);
        System.out.println("Pahalı ürünler:");
        expensive.forEach(p -> System.out.println("  - " + p));
    }
}

Çıktı:

Pahalı ürünler:
  - Laptop (25000.0 TL)
  - Monitor (12000.0 TL)

Bu örnekte dikkat edilmesi gerekenler:

  • com.shop.app doğrudan com.shop.model'e requires yazmamış ama Product sınıfını kullanabiliyor — çünkü com.shop.repository bunu requires transitive ile geçişken yapmış.

  • Her modül sadece kendi API paketini export ediyor.

  • com.shop.model paketini Jackson için opens ile açmış ama derleme zamanında herkes zaten erişebilir (exports ile).


Modüler JDK'nın en güçlü avantajlarından biri jlink aracıdır. Bu araç, uygulamanızın sadece ihtiyaç duyduğu modülleri içeren özel bir JRE oluşturur.

# Sadece ihtiyaç duyulan modüllerle minimal JRE oluştur
jlink --module-path out \
      --add-modules com.myapp.main \
      --output custom-jre \
      --launcher myapp=com.myapp.main/com.myapp.main.Main

# Oluşturulan JRE ile çalıştır
./custom-jre/bin/myapp

Tam JDK ~300 MB iken, jlink ile oluşturulan custom runtime 30-40 MB'a düşebilir. Bu özellikle Docker container'ları ve mikro servisler için büyük avantajdır.

# Custom JRE'nin boyutunu kontrol et
du -sh custom-jre/
# 38M    custom-jre/

# Normal JDK boyutu
du -sh $JAVA_HOME/
# 310M   /usr/lib/jvm/java-21/

💡 İpucu: jlink sadece modüler uygulamalarla çalışır. Eğer classpath kullanıyorsanız jlink'ten yararlanamazsınız. Bu, modüler yapıya geçmenin somut bir motivasyonudur.


Özet

  • JPMS Java 9 ile geldi ve classpath kaosunu modüler yapıyla çözdü — her modül bağımlılıklarını ve dışa açtığı paketleri açıkça bildirir.

  • module-info.java modülün kimlik kartıdır: requires bağımlılık, exports paket paylaşımı, opens reflection erişimi sağlar.

  • requires transitive bağımlılığı geçişken yapar — API'nizde başka modülden tip kullanıyorsanız gereklidir.

  • Qualified exports/opens ile erişimi sadece belirli modüllere kısıtlayabilirsiniz — en az yetki prensibi (least privilege).

  • Automatic module ve unnamed module sayesinde eski kütüphaneler modül sisteminde sorunsuz çalışır — geçiş kademeli yapılabilir.

  • jlink ile sadece gerekli modülleri içeren minimal runtime oluşturabilirsiniz — Docker ve mikro servisler için ideal.