Ders 04 / 12
Paketleme ve Çalıştırılabilir Çıktı
Aynı arşivleme çağrısı hem kendine yeten bir arşiv hem de dışarıdan bir sınıfa muhtaç bir arşiv üretir: birincisi koşum sınıf yolunda tek başına yeterlidir, ikincisi tek başına NoClassDefFoundError ile düşer ve yalnız eksik dizin eklendiğinde çalışır. Giriş noktası da kaynakta değil arşivin kendi bildiriminde durur: aynı sınıflardan üretilen bildirimsiz bir arşivde bu bilgi hiç yoktur.
İçindekiler
Önceki ders bir modül sınırının derlemedeki karşılığının bir sınıf yolu ve yükleyici kararı olduğunu ölçtü: sınır ne kaynağın dizin yapısına ne paket adına bağlıydı, yalnız tüketen tarafa hangi sınıf dosyalarının verildiğine bağlıydı. Ama bu ölçümün tamamı hâlâ dağınık dizinler üzerinden yürüdü — koşum sınıf yolu, birbirinden ayrı, birer birer oluşturulan dizinlerin bir listesiydi. Bu ders o listeyi taşınabilir tek bir ürüne çeviriyor: derlenmiş sınıflar bir arşive konuyor, ve arşivin kendisi artık koşum sınıf yolunun tek bir öğesi olarak taşınabiliyor.
İlk derste bir bildirimden türeyen evre dizisi dört evreyle sınırlıydı: temizleme, kaynak derleme, derleme sınıf yolunu kurma, koşum sınıf yolunu kurma. Paketleme, bu dizinin doğal bir uzantısı — kaynak derlendikten, iki sınıf yolu kurulduktan sonra, koşum sınıf yolunun bir bölümünü tek bir taşınabilir dosyaya sıkıştıran beşinci bir adım. Bu adımın girdisi hep aynı: daha önce derlenmiş sınıf dosyalarının bulunduğu bir dizin. Çıktısı ise şimdiye kadar hiç görülmemiş bir şey: kendi başına açılıp okunabilen, kendi içinde bir giriş noktası taşıyabilen tek bir dosya — çalıştırılabilir çıktı.
Arşivleme ve Bildirim
Bir arşiv, bir dizindeki sınıf dosyalarının tek bir dosyaya paketlenmiş hâlidir; arşivleyici (standart kitaplığın bir parçası, program içinden çağrılan bir araç sağlayıcısı) bu paketlemeyi gerçekten yapıyor. Arşivleyiciye verilen tek girdi, hangi dizinin paketleneceği ve isteğe bağlı olarak bir giriş noktası adı; arşivleyici bu ikisinden bir arşiv dosyası ve bir bildirim (manifest) üretiyor.
- DR20. Arşivleyici çağrısı, sınıf dosyalarının bulunduğu dizini olduğu gibi paketliyor; hiçbir bağımlılık dizini bu çağrıya eklenmiyor. Paketlenen şey yalnız verilen dizinin içeriği.
Aşağıdaki arsivle yöntemi bu tek derleme görevini üç parametreyle çalıştırıyor: hangi
arşiv dosyasının üretileceği, hangi dizinin paketleneceği, ve isteğe bağlı olarak hangi
sınıfın giriş noktası sayılacağı. Bu üçü dışında hiçbir bilgi arşivleme çağrısına
girmiyor — özellikle hiçbir bağımlılık listesi, hiçbir başka sınıf yolu. Bu kısıtlılık
kasıtlı: bu dersin ikinci yarısı tam olarak bu kısıtlılığın sonucunu ölçüyor.
// PaketYardimci.java — kaynak yazma, derleme ve arsivleme icin ortak islemler
import java.io.*;
import java.net.*;
import java.nio.file.*;
import java.util.*;
import java.util.spi.ToolProvider;
public class PaketYardimci {
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 void arsivle(Path jarYolu, Path siniflarDizini, String anaSinif) throws IOException {
Files.createDirectories(jarYolu.getParent());
List<String> arg = new ArrayList<>(List.of("--create", "--file", jarYolu.toString()));
if (anaSinif != null) { arg.add("--main-class"); arg.add(anaSinif); }
arg.add("-C"); arg.add(siniflarDizini.toString()); arg.add(".");
ToolProvider arsivleyici = ToolProvider.findFirst("jar").orElseThrow();
StringWriter sessiz = new StringWriter();
arsivleyici.run(new PrintWriter(sessiz), new PrintWriter(sessiz), arg.toArray(new String[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, PaketYardimci.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();
}
}
}
arsivle yalnız iki şey yapıyor: verilen dizini paketliyor, ve verilirse bir giriş noktası
adını bildirime yazıyor. kos önceki derslerdeki aynı işlemi yapıyor — tek farkla, sınıf
yolundaki bir öğe artık bir dizin değil, bir arşiv dosyası olabiliyor; URLClassLoader bir
arşiv dosyasının konumunu da bir dizin gibi kabul ediyor.
Bildirim, arşivin içinde ayrı bir kayıt olarak duruyor — sınıf dosyalarının yanında, ama
onlardan biri değil. Bir bildirim birçok alan taşıyabilir; bu ders yalnız birini
kullanıyor: Ana-Sınıf. Bu alan bir sınıf adını (bir dizgiyi) taşıyor, hiçbir çalıştırılabilir
kod içermiyor. Bildirimi okuyan taraf — burada bizim kendi kodumuz, gerçek bir kurulumda
ise bir çalıştırıcı — bu dizgiyi alıyor, o adı taşıyan sınıfı arşivin içinden buluyor ve
onun main yöntemini çağırıyor. Giriş noktasının kendisi hâlâ sıradan bir Java sınıfı;
değişen tek şey, o sınıfın adının nereden okunduğu.
Giriş Noktası Kaynakta Değil, Bildirimde
Bir arşiv onlarca sınıf taşıyabilir, ve bunların yalnız biri (ya da hiçbiri) bir main
yöntemi içerebilir. Arşivin kendisini çalıştırmak isteyen taraf — bir çalıştırıcı, ya da
bu derste bizim kendi kodumuz — hangi sınıftan başlayacağını bilmek zorunda, ama bu
bilgiyi arşivin içindeki sınıf dosyalarını tek tek tarayarak bulmuyor. Aradığı yer sabit:
arşivin bildirimindeki tek bir alan.
- DR21. Giriş noktası bilgisi yalnız bildirimin Ana-Sınıf (
Main-Class) alanına bakılarak okunuyor; kaynak dosyalarının hiçbirinde bu bilgi aranmıyor.
// CalistirilabilirCikti.java — arsivleyici gercekten cagirilir, giris noktasi bildirimde durur, arsiv bagimliliklari icermez
import java.io.*;
import java.net.*;
import java.nio.file.*;
import java.util.*;
import java.util.jar.*;
public class CalistirilabilirCikti {
public static void main(String[] args) throws Exception {
Path kok = Files.createTempDirectory("paket");
Path kaynak = kok.resolve("kaynak");
Path sBagimsiz = PaketYardimci.yaz(kaynak, "Bagimsiz",
"public class Bagimsiz { public static void main(String[] a) { } }");
Path bagimsizD = kok.resolve("bagimsizD");
PaketYardimci.derle(bagimsizD, List.of(), sBagimsiz);
Path bildirimliJar = kok.resolve("bildirimli.jar");
PaketYardimci.arsivle(bildirimliJar, bagimsizD, "Bagimsiz");
Path bildirimsizJar = kok.resolve("bildirimsiz.jar");
PaketYardimci.arsivle(bildirimsizJar, bagimsizD, null);
System.out.println("-- giris noktasi kaynakta degil, arsivin bildiriminde --");
try (JarFile jf = new JarFile(bildirimliJar.toFile())) {
String ana = jf.getManifest().getMainAttributes().getValue("Main-Class");
System.out.println("bildirimli arsivin giris noktasi : " + ana);
}
try (JarFile jf = new JarFile(bildirimsizJar.toFile())) {
Manifest m = jf.getManifest();
String ana = (m == null) ? null : m.getMainAttributes().getValue("Main-Class");
System.out.println("bildirimsiz arsivin giris noktasi : " + ana);
}
System.out.println("bildirimli arsiv, arsivin kendisinden kosuyor mu: "
+ PaketYardimci.kos(List.of(bildirimliJar), "Bagimsiz"));
System.out.println();
System.out.println("-- sinirlayici olcum: arsiv bagimliliklarini icermez --");
Path sYardimci = PaketYardimci.yaz(kaynak, "Yardimci",
"public class Yardimci { public static String yardim() { return \"yardimci\"; } }");
Path sMuhtac = PaketYardimci.yaz(kaynak, "Muhtac",
"public class Muhtac { public static void main(String[] a) { Yardimci.yardim(); } }");
Path muhtacD = kok.resolve("muhtacD");
PaketYardimci.derle(muhtacD, List.of(), sYardimci, sMuhtac);
Path sadeceMuhtacDizini = kok.resolve("sadeceMuhtac");
Files.createDirectories(sadeceMuhtacDizini);
Files.copy(muhtacD.resolve("Muhtac.class"), sadeceMuhtacDizini.resolve("Muhtac.class"));
Path muhtacJar = kok.resolve("muhtac.jar");
PaketYardimci.arsivle(muhtacJar, sadeceMuhtacDizini, "Muhtac");
Path yardimciD = kok.resolve("yardimciD");
Files.createDirectories(yardimciD);
Files.copy(muhtacD.resolve("Yardimci.class"), yardimciD.resolve("Yardimci.class"));
String bagimsizSonuc = PaketYardimci.kos(List.of(bildirimliJar), "Bagimsiz");
String muhtacTekBasina = PaketYardimci.kos(List.of(muhtacJar), "Muhtac");
String muhtacArtiYardimci = PaketYardimci.kos(List.of(muhtacJar, yardimciD), "Muhtac");
System.out.printf("%-48s %s%n", "Bagimsiz arsivi, koşum sınıf yolu yalniz arsiv:", bagimsizSonuc);
System.out.printf("%-48s %s%n", "Muhtac arsivi, koşum sınıf yolu yalniz arsiv:", muhtacTekBasina);
System.out.printf("%-48s %s%n", "Muhtac arsivi, Yardimci dizini eklenince:", muhtacArtiYardimci);
}
}
-- giris noktasi kaynakta degil, arsivin bildiriminde -- bildirimli arsivin giris noktasi : Bagimsiz bildirimsiz arsivin giris noktasi : null bildirimli arsiv, arsivin kendisinden kosuyor mu: tamam -- sinirlayici olcum: arsiv bagimliliklarini icermez -- Bagimsiz arsivi, koşum sınıf yolu yalniz arsiv: tamam Muhtac arsivi, koşum sınıf yolu yalniz arsiv: NoClassDefFoundError Muhtac arsivi, Yardimci dizini eklenince: tamam
İlk üç satır giriş noktasının nerede durduğunu ölçüyor. Aynı Bagimsiz sınıfından iki
arşiv üretiliyor: biri bir giriş noktasıyla (--main-class Bagimsiz), öbürü hiç giriş
noktası verilmeden. Bildirimli arşivin bildirimi okunduğunda Ana-Sınıf alanı Bagimsiz
değerini taşıyor; bildirimsiz arşivin bildiriminde bu alan hiç yok, okunan değer null.
İki arşiv de aynı sınıf dosyasını taşıyor, aynı derlemeden geliyor — aralarındaki tek fark
bildirimdeki bir satır. Kaynak dosyasının kendisinde giriş noktası diye bir bilgi hiç yok;
Bagimsiz.java ne “ben giriş noktasıyım” der ne de böyle bir işaretleyici taşır. Bu bilgi
yalnız arşivleme anında, arşivin kendi bildirimine yazılıyor — ve üçüncü satırın gösterdiği
gibi, arşiv bu bildirimi kullanarak kendi başına koşabiliyor.
Bu, Java Temelleri kursunda kurulan giriş noktası kavramının bir katman ötesi. Orada
giriş noktasının imzası (bir main yönteminin biçimi) sabitti; burada sorulan soru farklı:
birden çok sınıfın bulunduğu bir arşivde, hangi sınıf giriş noktası? Bu sorunun yanıtı
artık kaynakta değil, üzerinde çalışılan arşivin kendi bildiriminde. Bildirimsiz arşiv de
geçerli bir arşiv — sınıf dosyalarını taşıyor, açılabiliyor, içeriği okunabiliyor — ama
“hangi sınıftan başlanacağı” sorusunun yanıtı o arşivde yok. Bu eksiklik yalnız bildirim
okunduğunda görünüyor; arşivin kendisi bunu hiçbir yerde açıkça söylemiyor, null dönen
bir alan olarak duruyor.
Sınırlayıcı Ölçüm: Arşiv Bağımlılıklarını İçermez
- DR22.
Bagimsizarşivi hiçbir dış tipi anmıyor;Muhtacarşivi ise kendi dışındakiYardimcitipine çağrı zamanında ihtiyaç duyuyor. İkisi de aynı arşivleme çağrısıyla üretiliyor. - DR23.
Muhtacarşivi yalnız kendi sınıf dosyasını içeriyor;Yardimci’nin sınıf dosyası bilinçli olarak dışarıda bırakılıyor ki koşum sınıf yolunun geri kalanına duyulan ihtiyaç ölçülebilsin. - DR24. Koşum hatası, arşivin kendisinden değil arşivin tek başına konduğu sınıf yolundan doğuyor; aynı arşiv farklı bir sınıf yoluna konunca aynı hata düşmüyor.
- DR25. Bildirimsiz arşiv de geçerli bir arşivdir ve sınıf dosyalarını taşır; yalnızca giriş noktası bilgisi eksiktir ve bu eksiklik yalnız bildirim okunduğunda görünür.
Son üç satır iki arşivi yan yana koşturuyor. Bagimsiz arşivi tek başına koşum sınıf
yoluna konduğunda sorunsuz çalışıyor — çünkü Bagimsiz.main hiçbir dış tipi
anmıyor, ihtiyaç duyduğu her şey kendi sınıf dosyasında. Muhtac arşivi ise aynı
biçimde, tek başına koşum sınıf yoluna konduğunda NoClassDefFoundError ile düşüyor:
Muhtac.main, Yardimci.yardim()’i çağırıyor, ama Yardimci’nin sınıf dosyası bu
arşivin içinde yok — arşivleme sırasında bilinçli olarak yalnız Muhtac.class
paketlendi. Aynı Muhtac arşivi, koşum sınıf yoluna Yardimci’nin bulunduğu dizin de
eklendiğinde sorunsuz çalışıyor; arşivin kendisi değişmedi, değişen yalnız yanına
konan sınıf yolu.
Bu üç satır tek bir kuralı doğruluyor: arşiv bağımlılıklarını içermez. Arşivleyici
çağrısı yalnız verilen dizini paketler; o dizinin dışındaki hiçbir sınıf dosyası
otomatik olarak eklenmez. Bir arşivin “kendine yeter” olup olmadığı arşivin kendi
içeriğine değil, kaynak kodunun hangi tipleri andığına bağlı — ve bu, derleme anında bilinen
bir şey (Muhtac, Yardimci’yi derleme sınıf yolunda gerektirdi) ile paketleme anında
unutulabilecek bir şey (Yardimci’nin sınıf dosyası arşive hiç girmedi) arasındaki
boşluk. Derlemenin geçmiş olması, arşivin kendine yeteceğinin garantisi değil.
Bu boşluk, Bağımlılık Yönetimi dersinde ölçülen sessiz kusurla aynı ailedendir, ama
tersine dönmüş biçimde. Orada bir tipin koşum sınıf yolunda gerekmemesi onun
kopyalanmış olduğu anlamına geliyordu — sabitin değeri çağrı yerine gömülmüştü ve
kaynak dosyası ortadan kalkabilirdi. Burada ise bir tipin arşive girmemesi onun hâlâ
gerekli olduğu, yalnız paketlemenin onu unuttuğu anlamına geliyor. İki durumda da
hata anında değil, sonraki bir adımda ortaya çıkıyor: orada sabit değiştiğinde, burada
arşiv tek başına koşum sınıf yoluna konduğunda. Aradaki fark, kusurun sınıfı: Muhtac’ın
eksik bağımlılığı sessiz değil, NoClassDefFoundError ile açıkça düşüyor — çünkü
Yardimci’ye yapılan çağrı derleme zamanında gömülemeyecek kadar dinamik, bir yöntem
çağrısı, bir sabit değil.
Bu, arşivleme adımının sorumluluğunu da netleştiriyor: bir arşivin bağımlılıklarını “otomatik olarak” toplaması beklenmemeli. Hangi sınıf dosyalarının bir arşive girdiği, tıpkı hangi bağımlılığın hangi sınıf yoluna girdiği gibi, paketleme çağrısına verilen girdiyle belirlenen bir karardır — ve bu kararın eksik verilmesi, derleme anında hiç görünmeyen bir koşum hatasına dönüşebilir.
Dört ders boyunca ölçülen şeyin özeti burada tamamlanıyor. Bir bildirimden sabit bir evre dizisi türedi; o evrelerden ikisi bildirimdeki her bağımlılığı bir kapsam etiketine göre süzerek iki ayrı sınıf yolu kurdu; bir modülün genel ile iç yüzeyi arasındaki sınır bu iki yoldan birine hangi dizinin girdiğiyle belirlendi; ve şimdi bu yollardan biri — koşum sınıf yolu — tek bir taşınabilir dosyaya paketlendi. Dört adımın hiçbirinde kaynak metnin kendisi değişmedi; değişen hep bildirimdi, hep sınıf yoluydu.
Özet
- Bir arşiv, bir dizindeki sınıf dosyalarının arşivleyici tarafından tek bir dosyaya paketlenmiş hâlidir; koşum sınıf yolunun tek bir öğesi olarak taşınabilir.
- Giriş noktası bilgisi kaynak dosyasında değil, arşivin kendi bildiriminin Ana-Sınıf alanında durur; bildirimsiz bir arşiv geçerlidir ama giriş noktası bilgisi eksiktir.
- Arşiv bağımlılıklarını içermez: arşivleme çağrısı yalnız verilen dizini paketler, o dizinin dışındaki hiçbir sınıf dosyası otomatik eklenmez.
- Kendine yeten bir arşiv (dış tip anmayan) tek başına koşum sınıf yoluna konduğunda çalışır; dışa muhtaç bir arşiv aynı koşulda koşum hatasıyla düşer ve yalnız eksik sınıf yolu tamamlandığında çalışır.
- Derlemenin geçmiş olması bir arşivin kendine yeteceğinin garantisi değildir; derleme sınıf yolunda gerekli olan bir tip, paketleme sırasında arşive hiç girmemiş olabilir.
Sonraki Adım
Bu payda dört ders boyunca tek bir soru izlendi: bir bildirimden, bir bağımlılıktan, bir modül sınırından, bir arşivden — hep aynı ikili sınıf yolu ayrımı çıktı. Koşum sınıf yolu artık taşınabilir bir ürün hâline geldi ve içine ne konduğu bir karardır. Sıradaki konu aynı kararı üçüncü bir yol için veriyor — testin gördüğü yol, üretimin gördüğü yol değildir.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.