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
| Özellik | Paket | Modül |
|---|---|---|
| Gruplama | Sınıfları gruplar | Paketleri gruplar |
| Erişim kontrolü | public/protected/default/private | exports/opens ile paket seviyesinde |
| Bağımlılık | Yok (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 |
| Kapsam | Tek 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.javaGreeter 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 | İçerik | Otomatik? |
|---|---|---|
java.base | String, Object, List, Map, Stream, Optional... | Evet, her zaman dahil |
java.sql | JDBC: Connection, Statement, ResultSet... | Hayır, requires gerekir |
java.logging | java.util.logging (JUL) | Hayır, requires gerekir |
java.net.http | HttpClient (Java 11+) | Hayır, requires gerekir |
java.desktop | AWT, Swing | Hayır, requires gerekir |
java.xml | DOM, SAX, StAX XML parser'lar | Hayır, requires gerekir |
Hangi modüllerin var olduğunu görmek için:
java --list-modulesBu 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 transitiveBu çı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'ü
requiresile belirtemez
# Eski stil — classpath kullanımı (unnamed module)
java --class-path libs/gson-2.10.jar:out \
com.myapp.main.MainBu 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:
JAR'ın
MANIFEST.MFdosyasındaAutomatic-Module-Nameattribute'u varsa o kullanılırYoksa JAR dosya adından türetilir:
gson-2.10.1.jar→gsonmodü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
requiresile 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.sqlModü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.appdoğrudancom.shop.model'erequiresyazmamış amaProductsınıfını kullanabiliyor — çünkücom.shop.repositorybunurequires transitiveile geçişken yapmış.Her modül sadece kendi API paketini export ediyor.
com.shop.modelpaketini Jackson içinopensile açmış ama derleme zamanında herkes zaten erişebilir (exportsile).
11. jlink ile Custom Runtime Image
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/myappTam 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:
requiresbağımlılık,exportspaket paylaşımı,opensreflection 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.
AI Asistan
Sorularını yanıtlamaya hazır