Ders 10 / 12
Günlükleme Cephesi ve Gerçekleştirim
Çağıranın sınıf dosyasının cepheyi andığı ama gerçekleştirimi anmadığı sabit havuzdan okunarak gösterilir; gerçekleştirim koşum sınıf yoluna bir servis bildirimiyle girer ve iki gerçekleştirim bulunduğunda seçimi yolun sırası belirler. Sınırlayıcı ölçüm, gerçekleştirim hiç bulunmadığında çağıranın yine de derlenip koştuğunu ama hiçbir kayıt üretmediğini gösterir.
İçindekiler
Testin gördüğü yolu biz kurduk ve içine neyi koyacağımızı biliyorduk: hangi sahte tipin hangi gerçek tipin yerine geçtiğini, hangi kurulumun test sınıf yoluna hangi bağımlılığı eklediğini, adım adım biz yazdık. Bu derste yazan taraf değişiyor. Çağıran bir arayüze karşı derlenir, o arayüzün arkasında duracak sınıfın adını hiç yazmaz — ne içe aktarır, ne örnekler, ne bir dizgi olarak anar. Yine de program koştuğunda o adsız sınıf bulunur ve çağrılır. Soru şu: bir ad, kaynakta hiç yazılmadan, koşum sınıf yoluna nasıl giriyor, ve orada birden fazla aday varsa hangisinin kazanacağına ne karar veriyor?
Bu konu üç dersle günlüklemeyi ele alıyor, ama bu ilk ders günlük tutmanın kendisiyle değil, günlükleme cephesinin kurduğu bağımlılık yönüyle ilgileniyor. Bir cephe — burada tek yöntemli bir arayüz — çağıranın konuştuğu tek tip oluyor; arkasındaki gerçekleştirim çağırana hiç görünmüyor. Aşağıdaki ölçüm bunu dört adımda kuruyor: önce derlemenin gerçekleştirimi hiç görmeden geçtiğini ve sınıf dosyasının onu anmadığını, sonra gerçekleştirimin koşum sınıf yoluna nasıl bir bildirimle girdiğini, sonra birden fazla gerçekleştirim olduğunda kazananı neyin belirlediğini, son olarak da hiç gerçekleştirim yokken neyin olduğunu.
Derleme Sınıf Yolunda Yalnız Cephe Var
GL1. Çağıran kaynak metninde yalnız cephe arayüzünün adını yazar; gerçekleştirim sınıflarının adı kaynakta hiç geçmez, bu yüzden derleyicinin onları görmesine gerek yoktur. GL2. Sınıf dosyasının sabit havuzu, yalnız kaynakta adı geçen tipleri taşır; kaynakta hiç yazılmayan bir ad sabit havuzda da bulunmaz.
Ölçümün geri kalanında tekrar eden dört iş var: bir kaynağı diske yazmak, onu belirli bir sınıf yoluna karşı derlemek, derlenmiş bir sınıfı belirli bir sınıf yoluyla koşturmak, ve bir sınıf dosyasının sabit havuzunda hangi tiplerin adı geçtiğini okumak. Bu dördü aşağıdaki yardımcı sınıfta toplanıyor; konunun kalan iki dersi de aynı dördü kullanıyor.
// Arac.java — kaynak yazma, derleme, kosum ve sinif dosyasi okuma icin ortak yordamlar (yardimci sinif, main yok)
import java.io.*;
import java.lang.classfile.*;
import java.lang.classfile.constantpool.*;
import java.net.*;
import java.nio.file.*;
import java.util.*;
import java.util.spi.ToolProvider;
class Arac {
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, List<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 sessizCikti = new StringWriter();
int sonuc = derleyici.run(new PrintWriter(sessizCikti), new PrintWriter(sessizCikti),
arg.toArray(new String[0]));
return sonuc == 0;
}
static void calistir(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, Arac.class.getClassLoader().getParent())) {
Class<?> sinif = Class.forName(anaSinif, true, yukleyici);
sinif.getMethod("main", String[].class).invoke(null, (Object) new String[0]);
}
}
static Object alan(List<Path> sinifYolu, String anaSinif, String alanAdi) 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, Arac.class.getClassLoader().getParent())) {
Class<?> sinif = Class.forName(anaSinif, true, yukleyici);
sinif.getMethod("main", String[].class).invoke(null, (Object) new String[0]);
return sinif.getField(alanAdi).get(null);
}
}
static Set<String> atiflar(Path sinifDosyasi) throws IOException {
Set<String> adlar = new TreeSet<>();
ClassModel model = ClassFile.of().parse(sinifDosyasi);
for (PoolEntry e : model.constantPool()) {
if (e instanceof ClassEntry ce) adlar.add(ce.asInternalName().replace('/', '.'));
}
return adlar;
}
static void goster(String etiket, Object deger) {
System.out.printf("%-62s: %s%n", etiket, deger);
}
static void bildir(Path dizin, String servis, String saglayici) throws IOException {
Path d = dizin.resolve("META-INF").resolve("services");
Files.createDirectories(d);
Files.writeString(d.resolve(servis), saglayici + "\n");
}
static String cepheKaynagi() {
return "public interface GunlukCephesi { void yaz(String ileti); int uretilenKayitSayisi(); }";
}
static String cagiranKaynagi() {
return "import java.util.ServiceLoader;\n"
+ "public class Cagiran {\n"
+ " static final class Sessiz implements GunlukCephesi {\n"
+ " public void yaz(String ileti) { }\n"
+ " public int uretilenKayitSayisi() { return 0; }\n"
+ " }\n"
+ " public static String secilenSinif = \"yok\";\n"
+ " public static int uretilenKayit = -1;\n"
+ " static GunlukCephesi bul() {\n"
+ " for (GunlukCephesi g : ServiceLoader.load(GunlukCephesi.class, Cagiran.class.getClassLoader())) return g;\n"
+ " return new Sessiz();\n"
+ " }\n"
+ " public static void main (String[] a) {\n"
+ " GunlukCephesi g = bul();\n"
+ " secilenSinif = g.getClass().getSimpleName();\n"
+ " g.yaz(\"baslangic\"); g.yaz(\"ilerleme\"); g.yaz(\"bitis\");\n"
+ " uretilenKayit = g.uretilenKayitSayisi();\n"
+ " }\n"
+ "}";
}
static String gerceklestirimKaynagi(String ad) {
return "public class " + ad + " implements GunlukCephesi {\n"
+ " private int sayac = 0;\n"
+ " public void yaz(String ileti) { sayac++; }\n"
+ " public int uretilenKayitSayisi() { return sayac; }\n"
+ "}";
}
}
cagiranKaynagi() çağıranın kaynağını üretiyor: Cagiran yalnız GunlukCephesi arayüzünü ve
ServiceLoader’ı içe aktarıyor, hiçbir gerçekleştirim sınıfının adını yazmıyor. Bulamazsa
kendi içinde tanımladığı Sessiz iç sınıfına düşüyor — bu da bir gerçekleştirim, ama
çağıranın kendi kaynağında duruyor, dışarıdan gelmiyor.
// SadeceCephe.java — derleme sinif yolunda yalniz cephe varken cagiran derleniyor mu, sinif dosyasi kimi aniyor
import java.nio.file.*;
import java.util.*;
public class SadeceCephe {
public static void main(String[] args) throws Exception {
Path kok = Files.createTempDirectory("gl1");
Path kaynak = kok.resolve("kaynak");
Path sCephe = Arac.yaz(kaynak, "GunlukCephesi", Arac.cepheKaynagi());
Path sCagiran = Arac.yaz(kaynak, "Cagiran", Arac.cagiranKaynagi());
Path sA = Arac.yaz(kaynak, "GerceklestirimA", Arac.gerceklestirimKaynagi("GerceklestirimA"));
Path sB = Arac.yaz(kaynak, "GerceklestirimB", Arac.gerceklestirimKaynagi("GerceklestirimB"));
Path cepheD = kok.resolve("cephe");
Path cagiranD = kok.resolve("cagiran");
Path aD = kok.resolve("kayitA");
Path bD = kok.resolve("kayitB");
Arac.derle(cepheD, List.of(), List.of(sCephe));
Arac.derle(aD, List.of(cepheD), List.of(sA));
Arac.derle(bD, List.of(cepheD), List.of(sB));
boolean cagiranDerlendi = Arac.derle(cagiranD, List.of(cepheD), List.of(sCagiran));
Set<String> atif = Arac.atiflar(cagiranD.resolve("Cagiran.class"));
Arac.goster("gerceklestirim adi derleme sinif yolunda hic olmadan cagiran derlendi mi", cagiranDerlendi);
Arac.goster("cagiranin sinif dosyasi GunlukCephesi'ni aniyor mu", atif.contains("GunlukCephesi"));
Arac.goster("cagiranin sinif dosyasi GerceklestirimA'yi aniyor mu", atif.contains("GerceklestirimA"));
Arac.goster("cagiranin sinif dosyasi GerceklestirimB'yi aniyor mu", atif.contains("GerceklestirimB"));
}
}
gerceklestirim adi derleme sinif yolunda hic olmadan cagiran derlendi mi: true cagiranin sinif dosyasi GunlukCephesi'ni aniyor mu : true cagiranin sinif dosyasi GerceklestirimA'yi aniyor mu : false cagiranin sinif dosyasi GerceklestirimB'yi aniyor mu : false
GerceklestirimA ve GerceklestirimB bu ölçümde gerçekten var: ikisi de derlenmiş, ikisinin
de sınıf dosyası diskte duruyor. cagiranD’nin derleme sınıf yolu bile onları içermiyor —
yalnız cepheD var — ve Cagiran yine de derleniyor (GL1). Sabit havuz okunduğunda
GunlukCephesi orada, GerceklestirimA ve GerceklestirimB orada değil (GL2). Bu, bir
tahmin değil; kaynakta hiç yazılmayan bir ad, derleyicinin üretebileceği hiçbir komutta da
geçmiyor, çünkü sınıf dosyası kaynağın çevirisidir, kaynağın bilmediği bir şeyi icat
etmez. Cephenin bütün gücü tam olarak burada: çağıran, arkada duracak sınıfın var olup
olmadığını bile derleme anında bilmek zorunda değil.
Gerçekleştirimin Yola Bildirimle Girmesi
GL3. Gerçekleştirim koşum sınıf yoluna bir servis bildirimi dosyasıyla girer:
META-INF/services/<cephe-arayüzünün-tam-adı> adlı dosya, sağlayıcı sınıfın adını satır
olarak taşır. GL4. ServiceLoader.load(...) bu dosyayı okuyarak sağlayıcıyı bulur ve
örnekler; çağıran sağlayıcının kurucusunu hiçbir yerde çağırmaz.
// ServisBildirimi.java — gerceklestirim yola bir servis bildirimiyle giriyor, cagiran onu ServiceLoader ile buluyor
import java.nio.file.*;
import java.util.*;
public class ServisBildirimi {
public static void main(String[] args) throws Exception {
Path kok = Files.createTempDirectory("gl2");
Path kaynak = kok.resolve("kaynak");
Path sCephe = Arac.yaz(kaynak, "GunlukCephesi", Arac.cepheKaynagi());
Path sCagiran = Arac.yaz(kaynak, "Cagiran", Arac.cagiranKaynagi());
Path sA = Arac.yaz(kaynak, "GerceklestirimA", Arac.gerceklestirimKaynagi("GerceklestirimA"));
Path cepheD = kok.resolve("cephe");
Path cagiranD = kok.resolve("cagiran");
Path aD = kok.resolve("kayitA");
Arac.derle(cepheD, List.of(), List.of(sCephe));
Arac.derle(cagiranD, List.of(cepheD), List.of(sCagiran));
Arac.derle(aD, List.of(cepheD), List.of(sA));
Arac.bildir(aD, "GunlukCephesi", "GerceklestirimA");
Object secilenOncesi = Arac.alan(List.of(cagiranD, cepheD), "Cagiran", "secilenSinif");
Arac.goster("gerceklestirim kosum yolunda yokken dusulen sinif", secilenOncesi);
List<Path> kosumYoluSonrasi = List.of(cagiranD, cepheD, aD);
Object secilenSonrasi = Arac.alan(kosumYoluSonrasi, "Cagiran", "secilenSinif");
Object kayitSonrasi = Arac.alan(kosumYoluSonrasi, "Cagiran", "uretilenKayit");
Arac.goster("gerceklestirim yola bildirimle girince secilen sinif", secilenSonrasi);
Arac.goster("uretilen kayit sayisi", kayitSonrasi);
}
}
gerceklestirim kosum yolunda yokken dusulen sinif : Sessiz gerceklestirim yola bildirimle girince secilen sinif : GerceklestirimA uretilen kayit sayisi : 3
aD dizini iki şey taşıyor: GerceklestirimA.class ve META-INF/services/GunlukCephesi
adlı, içinde tek satır GerceklestirimA yazan bir bildirim dosyası. Koşum sınıf yoluna aD
eklendiğinde Cagiran içindeki ServiceLoader.load çağrısı bu dosyayı okuyor,
GerceklestirimA’yı yüklüyor ve örnekliyor — kod tarafında hiçbir satır new GerceklestirimA() yazmadan (GL3, GL4). Üç yaz çağrısının üçü de bu örneğe ulaşıyor ve
sayaç üçe çıkıyor. aD koşum sınıf yolundan çıkarılınca aynı Cagiran.class, hiç yeniden
derlenmeden, kendi Sessiz düşüşüne geri dönüyor. Değişen tek şey koşum sınıf yolunun
içeriği; Cagiran.class dosyası ikisinde de birebir aynı bayt dizisi.
İki Gerçekleştirim Olunca Sırayı Yol Belirliyor
GL5. Koşum sınıf yolunda aynı servis için birden fazla sağlayıcı bildirildiğinde
ServiceLoader’ın döndürdüğü ilk sağlayıcı, sınıf yolu girişlerinin sırasına bağlıdır.
GL6. Sıra değişince seçilen gerçekleştirim değişir, ama ne derleme ne koşum hatası
oluşur — ikisi de “tamam” sonucuyla biter.
// SiraKazanani.java — kosum sinif yolunda iki gerceklestirim varken kazanani yolun sirasi belirliyor
import java.nio.file.*;
import java.util.*;
public class SiraKazanani {
public static void main(String[] args) throws Exception {
Path kok = Files.createTempDirectory("gl3");
Path kaynak = kok.resolve("kaynak");
Path sCephe = Arac.yaz(kaynak, "GunlukCephesi", Arac.cepheKaynagi());
Path sCagiran = Arac.yaz(kaynak, "Cagiran", Arac.cagiranKaynagi());
Path sA = Arac.yaz(kaynak, "GerceklestirimA", Arac.gerceklestirimKaynagi("GerceklestirimA"));
Path sB = Arac.yaz(kaynak, "GerceklestirimB", Arac.gerceklestirimKaynagi("GerceklestirimB"));
Path cepheD = kok.resolve("cephe");
Path cagiranD = kok.resolve("cagiran");
Path aD = kok.resolve("kayitA");
Path bD = kok.resolve("kayitB");
Arac.derle(cepheD, List.of(), List.of(sCephe));
Arac.derle(cagiranD, List.of(cepheD), List.of(sCagiran));
Arac.derle(aD, List.of(cepheD), List.of(sA));
Arac.derle(bD, List.of(cepheD), List.of(sB));
Arac.bildir(aD, "GunlukCephesi", "GerceklestirimA");
Arac.bildir(bD, "GunlukCephesi", "GerceklestirimB");
Object aOnce = Arac.alan(List.of(cagiranD, cepheD, aD, bD), "Cagiran", "secilenSinif");
Object bOnce = Arac.alan(List.of(cagiranD, cepheD, bD, aD), "Cagiran", "secilenSinif");
Arac.goster("A once verilen kosum sinif yolunda secilen", aOnce);
Arac.goster("B once verilen kosum sinif yolunda secilen", bOnce);
Arac.goster("ikisi de hatasiz kosup yalniz sirayla farklilasti mi", !aOnce.equals(bOnce));
}
}
A once verilen kosum sinif yolunda secilen : GerceklestirimA B once verilen kosum sinif yolunda secilen : GerceklestirimB ikisi de hatasiz kosup yalniz sirayla farklilasti mi : true
aD ve bD birbirinden habersiz iki dizin; her biri kendi sınıfını ve kendi servis
bildirimini taşıyor. Koşum sınıf yolu [cagiranD, cepheD, aD, bD] sırasıyla kurulduğunda
ServiceLoader önce aD’nin bildirimine rastlıyor ve GerceklestirimA‘yı döndürüyor; aynı
iki dizin bD, aD sırasıyla verildiğinde kazanan GerceklestirimB oluyor (GL5). Cagiran
kaynağı, GunlukCephesi arayüzü, iki gerçekleştirimin kendisi — hiçbiri iki koşum arasında
değişmedi; değişen yalnız listenin sırası. Kod tarafında hiçbir yerde bir çakışma uyarısı ya
da hata yok (GL6): iki sağlayıcı bulunması, K05’in ortak tanımındaki “koşum sınıf yolunda iki
gerçekleştirim bulunduğunda seçim yolun sırasına göre değişir, hata yok” gözlemiyle aynı
kalıba giriyor, burada arayüz düzeyinde bir kez daha görülüyor.
Gerçekleştirim Hiç Bulunmazsa
GL7. Koşum sınıf yolunda hiçbir sağlayıcı bulunmazsa ServiceLoader.load(...) boş bir
yineleyici döner; çağıran bunu bir istisna olarak değil, kendi tanımladığı bir varsayılanla
karşılar. GL8. Bu durumda program hatasız çalışır ama hiçbir gerçekleştirim
uretilenKayitSayisi()’nı artırmadığı için üretilen kayıt sıfırdır.
// GerceklestirimYok.java — gerceklestirim kosum yolunda hic yokken cagiran hatasiz kosuyor ama hicbir kayit uretmiyor
import java.nio.file.*;
import java.util.*;
public class GerceklestirimYok {
public static void main(String[] args) throws Exception {
Path kok = Files.createTempDirectory("gl4");
Path kaynak = kok.resolve("kaynak");
Path sCephe = Arac.yaz(kaynak, "GunlukCephesi", Arac.cepheKaynagi());
Path sCagiran = Arac.yaz(kaynak, "Cagiran", Arac.cagiranKaynagi());
Path cepheD = kok.resolve("cephe");
Path cagiranD = kok.resolve("cagiran");
Arac.derle(cepheD, List.of(), List.of(sCephe));
Arac.derle(cagiranD, List.of(cepheD), List.of(sCagiran));
List<Path> kosumYolu = List.of(cagiranD, cepheD);
boolean hataAlmadanKostu;
try {
Arac.calistir(kosumYolu, "Cagiran");
hataAlmadanKostu = true;
} catch (Exception e) {
hataAlmadanKostu = false;
}
Object secilen = Arac.alan(kosumYolu, "Cagiran", "secilenSinif");
Object kayit = Arac.alan(kosumYolu, "Cagiran", "uretilenKayit");
Arac.goster("gerceklestirim kosum yolunda hic yokken hata almadan kosuyor mu", hataAlmadanKostu);
Arac.goster("dusulen sinif", secilen);
Arac.goster("uretilen kayit sayisi", kayit);
}
}
gerceklestirim kosum yolunda hic yokken hata almadan kosuyor mu: true dusulen sinif : Sessiz uretilen kayit sayisi : 0
Bu ölçümün koşum sınıf yolunda GerceklestirimA da GerceklestirimB da yok — yalnız cephe
ve çağıran var. Program yine de tamamlanıyor, hiçbir istisna fırlatmıyor (GL7), ve
uretilenKayit sıfırda kalıyor (GL8). Bu, dersin sınırlayıcı ölçümü: cephe, çağıranla
gerçekleştirim arasındaki bağımlılığı yok etmiyor, yönünü çeviriyor. Gerçekleştirim
hiç yoksa çağıran bunu bir hata olarak değil bir boşluk olarak görüyor ve kendi
Sessiz düşüşüyle dolduruyor. Üretilen kayıt sayısının sıfır olması burada bir kusurun
işareti değil; çağıranın kendi kaynağında yazdığı bir kuralın sonucu. Bir günlükleme
cephesinin gerçek dünyadaki gerçekleştirimleri de bu sözü taşır: gerçekleştirim
bağlanmamışsa program çökmez, yalnız konuşmaz. Bu, cephe düzeneğinin çağırana verdiği asıl
güvencedir — çağıranın kendisi hangi gerçekleştirimin koşum sınıf yoluna gireceğini
bilmeden yazılabilir, çünkü en kötü ihtimalle davranış “hiç kayıt yok” olur, “program
çalışmıyor” değil. Bu güvence bir bedel de taşır: koşum sınıf yolu yanlış kurulduğunda
ortada hiçbir hata mesajı olmadığı için eksik kalan kayıtların fark edilmesi, derleyicinin
reddettiği bir satırı fark etmekten çok daha zordur.
Özet
- Çağıran kaynak metninde yalnız cephe arayüzünün adını yazar; derleme sınıf yolunda gerçekleştirim hiç bulunmasa da çağıran derlenir.
- Sınıf dosyasının sabit havuzu kaynakta yazılan tipleri taşır:
GunlukCephesiorada, kaynakta hiç geçmeyenGerceklestirimAveGerceklestirimBorada değil. - Gerçekleştirim koşum sınıf yoluna bir servis bildirim dosyasıyla girer;
ServiceLoaderbu bildirimi okuyarak sağlayıcıyı çağıranın hiçbir satırında adı geçmeden bulur ve örnekler. - Koşum sınıf yolunda birden fazla sağlayıcı bildirildiğinde kazananı sınıf yolu girişlerinin sırası belirler; sıra değişince seçim değişir ama hata oluşmaz.
- Sınırlayıcı ölçüm: gerçekleştirim hiç bulunmadığında çağıran yine derlenip hatasız koşar, ama hiçbir kayıt üretmez — cephe bağımlılığı yok etmiyor, yönünü çeviriyor.
Sonraki Adım
Bu derste ölçülen şey bir adın yola nasıl girdiğiydi: bildirimle, sırayla, ya da hiç girmeden. Sıradaki ders aynı çağrı kümesini sabit tutup değişen tek şeyi bir eşik yapıyor — aynı üç çağrı iki ayrı eşikle koşturulduğunda kaç tanesi gerçekten bir kayda dönüşüyor, ve eşiğin altında kalan bir çağrının hâlâ bir bedeli var mı?
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.