Ders 03 / 12
Çoklu Modül Projeler
Üç sınır kurulumundan biri derlemede, biri koşumda yakalanır, biri hiç yakalanmadan sınırı sessizce aşar. Sınırlayıcı ölçüm bunun neden böyle olduğunu gösterir: Genel ve Dahili aynı dizine derlenir, ve sınırı gerçekten kuran şey dizin değil bir yükleyicinin izin listesidir — aynı dizindeki iki sınıf dosyasından biri yüklenirken öbürü ClassNotFoundException ile reddedilir.
İçindekiler
Önceki ders altı tekil tipin her birinin hangi sınıf yolunda gerekli olduğunu ölçtü — ama her tip kendi başınaydı, tek bir sınıfın ya da arayüzün sınırları içinde kaldı. Gerçek bir proje tek dosyalık sınıflardan değil, birbirine bağımlı modüllerden kurulur, ve bir modülün “başkasına açık” saydığı yüzey ile “kendi içinde” tuttuğu yüzey arasındaki çizgi bir dil kuralı değil, bir derleme kararıdır. Bu ders o çizginin derlemedeki karşılığını ölçüyor: modül sınırı aşıldığında ne oluyor, ve bu yakalama nereden geliyor?
Modül sınırı burada bir çoklu modül projenin en küçük hâliyle kuruluyor: iki taraf,
biri “sağlayan” biri “tüketen”. Sağlayan tarafın kendi içinde bir genel yüzeyi (Genel)
ve bir iç uygulaması (Dahili) var; tüketen taraf yalnız genel yüzeyi kullanmalı,
iç uygulamaya hiç dokunmamalı. Gerçek bir çoklu modül projede bu ayrım genellikle ayrı
dizinlere, ayrı derleme çıktılarına yansır — ama derlemenin kendisi bu ayrımı neden
uyguladığını hiç söylemez. Bu ders o nedeni ölçerek çıkarıyor.
Modül Sınırının Derlemedeki Karşılığı
Bir modülün genel yüzeyi ile iç uygulaması aynı kaynak dizininden, aynı derleme çağrısıyla,
aynı çıktı dizinine derlenebilir — Java’nın kendisi bunları ayırmaz. Aşağıdaki Genel ve
Dahili sınıfları tam olarak böyle: ikisi de tek bir derleme çağrısında, tek bir çıktı
dizinine (aTumOut) yazılıyor. Genel dışarıya açık sayılan tarafı, Dahili yalnız
modülün kendi içinde kalması gereken tarafı temsil ediyor — ama bu ayrım şu ana kadar
yalnız bir isimlendirme kararı, derlemenin kendisi henüz hiçbir şey uygulamıyor.
- DR14. Üç sınır kurulumu da aynı iç tip (
Dahili.gizli) üzerinden ölçülüyor; farklı olan yalnız erişim biçimi (doğrudan referans / yansıma) ve koşum sınıf yoluna hangi dizinin konduğu. - DR15.
GenelveDahiliaynı kaynak dizininden aynı derleme çağrısıyla, aynı çıktı dizinine derleniyor; ikisi arasında fiziksel hiçbir ayrım yok.
Sınırı “gerçek” kılan adım, çıktının bir alt kümesini ayrı bir dizine kopyalamak: yalnız
Genel.class, aGenelSadece adlı seçilmiş bir dizine kopyalanıyor. Bir modülün
tüketicisine verilen sınıf yolu bu seçilmiş dizini gösteriyor, Dahili’nin de bulunduğu
tam çıktıyı değil.
// ModulYardimci.java — bir kaynagi derleyip sinif yolu listesine karsi kosturmak icin ortak islemler
import java.io.*;
import java.net.*;
import java.nio.file.*;
import java.util.*;
import java.util.spi.ToolProvider;
public class ModulYardimci {
static Path yaz(Path dizin, String ad, String kaynak) throws IOException {
Files.createDirectories(dizin);
Path p = dizin.resolve(ad + ".java");
Files.writeString(p, kaynak);
return p;
}
static boolean derle(Path cikti, List<Path> sinifYolu, Path... kaynaklar) throws IOException {
Files.createDirectories(cikti);
List<String> arg = new ArrayList<>(List.of("-d", cikti.toString()));
if (!sinifYolu.isEmpty()) {
StringJoiner sj = new StringJoiner(File.pathSeparator);
for (Path p : sinifYolu) sj.add(p.toString());
arg.add("-classpath");
arg.add(sj.toString());
}
for (Path k : kaynaklar) arg.add(k.toString());
ToolProvider derleyici = ToolProvider.findFirst("javac").orElseThrow();
StringWriter sessiz = new StringWriter();
int sonuc = derleyici.run(new PrintWriter(sessiz), new PrintWriter(sessiz), arg.toArray(new String[0]));
return sonuc == 0;
}
static String kos(List<Path> sinifYolu, String anaSinif) throws Exception {
URL[] url = new URL[sinifYolu.size()];
for (int i = 0; i < url.length; i++) url[i] = sinifYolu.get(i).toUri().toURL();
try (URLClassLoader yukleyici = new URLClassLoader(url, ModulYardimci.class.getClassLoader().getParent())) {
Class<?> sinif = Class.forName(anaSinif, true, yukleyici);
sinif.getMethod("main", String[].class).invoke(null, (Object) new String[0]);
return "tamam";
} catch (java.lang.reflect.InvocationTargetException e) {
return e.getCause().getClass().getSimpleName();
} catch (Throwable t) {
return t.getClass().getSimpleName();
}
}
}
derle, önceki derslerdeki aynı işlemi yapıyor: kaynakları verilen bir sınıf yoluna karşı
derliyor. kos derlenmiş sınıfı ayrı bir sınıf yoluna karşı çalıştırıyor ve fırlayan
istisnanın sınıf adını döndürüyor — “tamam” değilse, sınırın çalışma zamanında yakalandığı
anlamına geliyor.
Bu iki yöntemin dışında ModulYardimci hiçbir şey bilmiyor — ne “genel” ne “iç” kavramını
tanıyor, ne bir modülün ne olduğunu. Sınırın kendisi bu sınıfın dışında, çağıran kodun
hangi dizinleri hangi sınıf yoluna kattığı kararında kuruluyor. Bu, bu dersin temel
gözlemini önceden özetliyor: modül sınırı bir dil özelliği değil, bir kullanım kararı.
Üç Sınır Kurulumu
Dahili.gizli bir modülün iç uygulamasını temsil ediyor: dışarıdan çağrılması
“sınır aşımı”. Aşağıdaki üç kurulum bu aşımı üç farklı biçimde deniyor; hepsi aynı Dahili
kaynağını, hepsi aynı aGenelSadece sınıf yolu tuzağını kullanıyor, ama biri kaynak
metninde bir tip adı yazarak, ikisi bir dizgi vererek erişiyor.
- DR16. “Yakalanmadı” sonucu yalnız kurulum 1’in derleme başarısızlığıyla karşılaştırılan bir referans değil; kurulum 3’te program hatasız çalışıyor ve iç tipin değeri okunuyor.
- DR17. Sınırlı yükleyici (
SinirliYukleyici), JVM, Eşzamanlılık ve Başarım kursunun Sınıf Yükleme dersinde kurulan özel yükleyici düzeneğiyle aynı ilkeyi kullanıyor; yalnız bir izin listesini kendi bildirimine ekliyor. - DR18. İzin listesi sınıf adına bakıyor, dizine bakmıyor; aynı dizindeki iki sınıf
dosyasından biri yüklenebiliyor, öbürü
ClassNotFoundExceptionile reddediliyor. - DR19.
hesapla()yöntemi, sınırlı yükleyicinin ölçümündeDahilitipini hiç anmıyor; bu yüzdenGenel’in yüklenip çalışması,Dahili’nin erişilebilir olup olmamasından bağımsız ölçülüyor.
// SinirliYukleyici.java — ayni dizindeki sinif dosyalarindan yalniz izin verilen adlari tanimlayan yukleyici
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Set;
class SinirliYukleyici extends ClassLoader {
private final Path dizin;
private final Set<String> izinli;
SinirliYukleyici(Path dizin, Set<String> izinli) {
super(SinirliYukleyici.class.getClassLoader().getParent());
this.dizin = dizin;
this.izinli = izinli;
}
protected Class<?> findClass(String ad) throws ClassNotFoundException {
if (!izinli.contains(ad)) throw new ClassNotFoundException(ad);
try {
byte[] bayt = Files.readAllBytes(dizin.resolve(ad + ".class"));
return defineClass(ad, bayt, 0, bayt.length);
} catch (IOException e) {
throw new ClassNotFoundException(ad);
}
}
}
// ModulSiniri.java — modul sinirinin derlemedeki karsiligi: sinir bir sinif yolu ve yukleyici karari
import java.io.*;
import java.nio.file.*;
import java.util.*;
public class ModulSiniri {
public static void main(String[] args) throws Exception {
Path kok = Files.createTempDirectory("modul");
Path kaynak = kok.resolve("kaynak");
Path sDahili = ModulYardimci.yaz(kaynak, "Dahili",
"public class Dahili { public static int gizli() { return 99; } }");
Path sGenel = ModulYardimci.yaz(kaynak, "Genel",
"public class Genel { public static int hesapla() { return 41; } }");
Path aTumOut = kok.resolve("aTumOut");
ModulYardimci.derle(aTumOut, List.of(), sDahili, sGenel);
Path aGenelSadece = kok.resolve("aGenelSadece");
Files.createDirectories(aGenelSadece);
Files.copy(aTumOut.resolve("Genel.class"), aGenelSadece.resolve("Genel.class"));
System.out.println("-- uc sinir kurulumu, kaci derlemede kaci kosumda yakalaniyor --");
Path sB1 = ModulYardimci.yaz(kok.resolve("b1"), "B1",
"public class B1 { public static void main(String[] a) { System.out.println(Dahili.gizli()); } }");
boolean b1Derlendi = ModulYardimci.derle(kok.resolve("b1Cikti"), List.of(aGenelSadece), sB1);
System.out.println("kurulum 1 - dogrudan referans, ic dizin sinif yolunda yok: derlendi mi = " + b1Derlendi
+ " -> " + (b1Derlendi ? "yakalanmadi" : "derlemede yakalandi"));
Path sB2 = ModulYardimci.yaz(kok.resolve("b2"), "B2",
"public class B2 { public static void main(String[] a) throws Exception {\n"
+ " Class<?> c = Class.forName(\"Dahili\");\n"
+ " c.getMethod(\"gizli\").invoke(null); } }");
Path b2Cikti = kok.resolve("b2Cikti");
boolean b2Derlendi = ModulYardimci.derle(b2Cikti, List.of(aGenelSadece), sB2);
System.out.println("kurulum 2 - yansimayla erisim, ic dizin sinif yolunda yok: derlendi mi = " + b2Derlendi);
String kurulum2Kosum = ModulYardimci.kos(List.of(b2Cikti, aGenelSadece), "B2");
System.out.println("kurulum 2 kosum sonucu: " + kurulum2Kosum
+ " -> " + (kurulum2Kosum.equals("tamam") ? "yakalanmadi" : "kosumda yakalandi"));
String kurulum3Kosum = ModulYardimci.kos(List.of(b2Cikti, aTumOut), "B2");
System.out.println("kurulum 3 - ayni yansimali sinif, kosum sinif yoluna ic dizinin tamami eklendi: sonuc = "
+ kurulum3Kosum + " -> " + (kurulum3Kosum.equals("tamam") ? "hic yakalanmadi, sinir sessizce asildi" : "yakalandi"));
System.out.println();
System.out.println("-- sinirlayici olcum: sinir bir dizin ayrimi degil --");
System.out.println("Genel ve Dahili ayni dizinde (aTumOut): " + Files.exists(aTumOut.resolve("Genel.class"))
+ ", " + Files.exists(aTumOut.resolve("Dahili.class")));
SinirliYukleyici sinirli = new SinirliYukleyici(aTumOut, Set.of("Genel"));
Class<?> genelSinif = Class.forName("Genel", true, sinirli);
System.out.println("sinirli yukleyici Genel'i yukledi mi: true, hesapla() = "
+ genelSinif.getMethod("hesapla").invoke(null));
String dahiliSonuc;
try {
Class.forName("Dahili", true, sinirli);
dahiliSonuc = "yuklendi";
} catch (ClassNotFoundException e) {
dahiliSonuc = "ClassNotFoundException";
}
System.out.println("ayni sinirli yukleyici Dahili'yi yukleyebildi mi (ayni dizinde olmasina ragmen): " + dahiliSonuc);
}
}
-- uc sinir kurulumu, kaci derlemede kaci kosumda yakalaniyor -- kurulum 1 - dogrudan referans, ic dizin sinif yolunda yok: derlendi mi = false -> derlemede yakalandi kurulum 2 - yansimayla erisim, ic dizin sinif yolunda yok: derlendi mi = true kurulum 2 kosum sonucu: ClassNotFoundException -> kosumda yakalandi kurulum 3 - ayni yansimali sinif, kosum sinif yoluna ic dizinin tamami eklendi: sonuc = tamam -> hic yakalanmadi, sinir sessizce asildi -- sinirlayici olcum: sinir bir dizin ayrimi degil -- Genel ve Dahili ayni dizinde (aTumOut): true, true sinirli yukleyici Genel'i yukledi mi: true, hesapla() = 41 ayni sinirli yukleyici Dahili'yi yukleyebildi mi (ayni dizinde olmasina ragmen): ClassNotFoundException
İlk kurulum sınırı en katı biçimde ölçüyor: B1, Dahili.gizli’yi doğrudan tip
referansıyla çağırıyor, ve sınıf yolunda Dahili hiç yok. Derleyici sembolü çözemiyor,
derleme çöküyor — sınır derlemede yakalanıyor. İkinci kurulum aynı çağrıyı yansımayla
yapıyor: B2 kaynağında Dahili adı hiçbir yerde doğrudan geçmiyor, bu yüzden derleme
sorunsuz tamamlanıyor. Ama koşum sınıf yolu da Dahili’yi içermiyor, ve Class.forName
çalışma zamanında ClassNotFoundException fırlatıyor — sınır bu kez koşumda
yakalanıyor.
Üçüncü kurulum aynı derlenmiş B2 sınıfını değiştirmeden, yalnız koşum sınıf yoluna farklı
bir dizin ekleyerek çalıştırıyor: aGenelSadece yerine aTumOut — modülün tüm iç çıktısı.
Bu, gerçek bir projede sık rastlanan bir hata: birileri “yalnız genel arayüzü” değil, tüm
derleme çıktısını koşum sınıf yoluna koyuyor. Sonuç: program hatasız çalışıyor, Dahili
bulunuyor ve çağrılıyor — sınır hiç yakalanmıyor, sessizce aşılıyor.
Üç sınır kurulumundan biri derlemede, biri koşumda yakalanıyor, biri hiç yakalanmıyor.
Yakalamanın hangisi olduğunu belirleyen şey B1/B2 kaynağının yazılışı değil, koşum
sınıf yoluna hangi dizinin konduğu.
Bu üçlünün sırası da anlamlı. En sıkı yakalama (derleme) en erken düşen yakalama —
derleyici sembolü çözemediği an duruyor, ekstra bir adım gerekmiyor. Orta sıkılıktaki
yakalama (koşum) bir adım daha esnek: kaynak derleniyor, program başlıyor, hata ancak
Dahili’ye gerçekten erişilmeye çalışıldığı satırda düşüyor — program o satıra hiç
uğramasaydı hata da hiç görünmezdi. En gevşek durum ise hiç yakalama değil: üçüncü
kurulumda kod hem derleniyor hem çalışıyor, ve Dahili’nin çağrılması hiçbir uyarı
üretmiyor. Modül sınırının ne kadar sıkı olduğu, bu üç kurulumun hangisine benzediğine
bağlı — ve bu bir dil garantisi değil, sınıf yoluna ne konduğuyla ilgili bir işletim
kararı.
Sınırlayıcı Ölçüm: Sınır Bir Dizin Ayrımı Değildir
Yukarıdaki üç kurulum, sınırı ayrı dizinlere kopyalayarak kurdu — bu, gerçek bir yapı sürecinin de yaptığı şey: yalnız genel yüzeyi ayrı bir çıktıya taşımak. Ama bu yönteme güvenmek, sınırın neden işlediğini yanlış anlatır: sanki sınır fiziksel bir dizin farkıymış gibi görünür. Son ölçüm bunun yanlış olduğunu gösteriyor.
Genel ve Dahili bu ölçümde hâlâ aynı dizinde duruyor — aTumOut, ikisinin de
sınıf dosyasını taşıyor, kopyalama yok. Sınırı burada kuran şey SinirliYukleyici’nin
kendi izin listesi: Set.of("Genel"). Bu yükleyici Geneli istendiğinde diskten okuyup
tanımlıyor — hesapla() çağrılabiliyor, 41 dönüyor. Ama aynı yükleyiciden Dahili
istendiğinde, findClass izin listesinde olmadığını görüp ClassNotFoundException
fırlatıyor; Dahili.class dosyası tam olarak aynı dizinde, birkaç bayt ötede duruyor,
ama yükleyici ona hiç bakmıyor bile.
Bu, önceki üç kurulumun ölçtüğü şeyi tersinden doğruluyor: modül sınırı bir dizin ayrımı değil, bir sınıf yolu ve yükleyici kararıdır. Aynı dizinden derlenen iki tip farklı sınıf yollarına (ya da farklı izin listelerine) konularak ayrı erişim düzeylerine sahip olabiliyor, ve bu ayrım o zaman da işliyor — hiçbir fiziksel dizin ayrımı olmadan. Kurulum 3’ün gösterdiği kaçış da bunun aynadaki yansıması: dizin ayrımı yeterli bir güvence değil, çünkü sınırı taşıyan gerçek mekanizma her zaman hangi sınıf yolunun (ya da hangi yükleyicinin) hangi adı görmesine izin verildiğidir.
Bu iki bulgu birlikte okunduğunda bir çoklu modül projenin sınırının nereye yerleştirildiği ortaya çıkıyor: sınır ne kaynak dosyasının hangi klasörde durduğuna, ne de sınıfın hangi paket adını taşıdığına bağlı. Sınır, bir sağlayan modülün derleme çıktısından tüketen modüle hangi alt kümenin — hangi dizinin, hangi arşivin, hangi izin listesinin — verildiğine bağlı. İki modül aynı depoda, aynı derleme çağrısıyla, hatta aynı çıktı dizininde üretilebilir; sınırın gerçekten işlemesi için gereken tek şey, tüketen tarafa verilen sınıf yolunun neyi içerip neyi içermediğidir.
Özet
- Modül sınırının derlemedeki karşılığı bir sınıf yolu kararıdır: aynı kaynak dizininden derlenen “genel” ve “iç” tipler, tüketiciye farklı sınıf yolları verilerek ayrılır.
- Üç sınır kurulumundan biri derlemede yakalanır (doğrudan referans, tip sınıf yolunda yok), biri koşumda yakalanır (yansıma, tip koşum sınıf yolunda yok), biri hiç yakalanmaz (yansıma, ama koşum sınıf yoluna iç çıktının tamamı yanlışlıkla eklenmiş).
- Bir sınıf yükleyicinin izin listesi sınıf adına bakar, dizine bakmaz: aynı dizindeki iki
sınıf dosyasından biri yüklenip çalıştırılabilirken öbürü
ClassNotFoundExceptionile reddedilebilir. - Sınırlayıcı ölçüm:
GenelveDahilifiziksel olarak aynı dizinde durmaya devam eder; sınırı kuran şey dizin değil, yükleyicinin kendi kararıdır.
Sonraki Adım
Bu derste bir modülün sınırı, koşum sınıf yoluna hangi dizinin ya da hangi yükleyicinin konduğu üzerinden ölçüldü — sınırı biz kurduk, kararı biz verdik. Ama bu bildirim henüz tek bir çalışan programa dönüşmedi; koşum sınıf yolu şu ana kadar hep dağınık dizinlerin bir listesiydi. Sıradaki ders bu listeyi taşınabilir tek bir ürüne çeviriyor: derlenmiş sınıflar bir arşive konuyor, giriş noktası kaynakta değil arşivin kendi bildiriminde duruyor, ve arşivin bağımlılıklarını içerip içermediği ayrı bir soru olarak kalıyor.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.