İçeriğe geç
academia.sh

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: GunlukCephesi orada, kaynakta hiç geçmeyen GerceklestirimA ve GerceklestirimB orada değil.
  • Gerçekleştirim koşum sınıf yoluna bir servis bildirim dosyasıyla girer; ServiceLoader bu 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.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat