← Kursa Dön
📄 Text · 18 min

Java Reflection API

Normal kod yazarken hangi sınıfı kullanacağını, hangi metodu çağıracağını önceden bilirsin — her şey derleme zamanında bellidir. Ama bazen program çalışırken, hangi sınıfla karşılaşacağını bilemezsin. Bir framework yazdığını düşün: kullanıcı hangi sınıfı verecek? Hangi alanları olacak? Bunu önceden bilemezsin ama yine de o sınıfın alanlarına erişmen, metodlarını çağırman gerekir.

İşte Reflection API tam olarak bu ihtiyaca cevap verir: bir Java programının çalışma zamanında kendi yapısını incelemesi ve değiştirmesi.


1. Reflection Nedir?

Reflection kelimesi "yansıma" demektir. Tıpkı bir aynanın karşısına geçip kendine bakman gibi, bir Java programı da Reflection ile kendi yapısına bakabilir.

🎯 Analoji — Ayna:

>

Bir insanın aynaya bakmasını düşün. Ayna olmadan saçının dağınık olduğunu, gömleğinin ters olduğunu göremezsin. Reflection, Java programı için bu ayna işlevini görür: program kendi sınıflarını, alanlarını, metodlarını "görebilir" ve hatta değiştirebilir. Normal kod yazarken her şey önceden bellidir (derleme zamanı) — ama Reflection ile program çalışırken kendini keşfeder. Tıpkı aynaya bakıp "hmm, bu alan private'mış ama ben yine de erişebilirim" demek gibi.

Reflection iki temel yetenek sağlar:

  • Introspection (İç gözlem): Sınıfın yapısını inceleme — hangi alanları var, hangi metodları var, hangi annotation'ları taşıyor?

  • Manipulation (Müdahale): Alanların değerlerini okuma/yazma, metodları çağırma, yeni nesneler oluşturma — hatta private olanlara bile erişme.

Ne Zaman Kullanılır?

  • Framework geliştirme: Spring, Hibernate, Jackson gibi framework'ler kullanıcı sınıflarını önceden bilmez — reflection ile keşfeder

  • Test araçları: JUnit test metodlarını @Test annotation'ına bakarak bulur

  • Serialization: Jackson, JSON'a dönüştüreceği sınıfın alanlarını reflection ile okur

  • Dependency Injection: Spring, constructor parametrelerini reflection ile inceleyip uygun bean'leri enjekte eder


2. Class Sınıfı — Her Şeyin Başlangıcı

Reflection'ın merkezi java.lang.Class<T> sınıfıdır. JVM'e yüklenen her sınıf için bir Class nesnesi oluşturulur. Bu nesne o sınıfın "meta bilgisi"ni — adını, alanlarını, metodlarını, constructor'larını — içerir.

Class Nesnesine Erişim (3 Yol)

// Yol 1: .class literal — Derleme zamanında tip biliniyorsa
Class<String> clazz1 = String.class;

// Yol 2: .getClass() — Elimizde nesne varsa
String text = "Merhaba";
Class<?> clazz2 = text.getClass();

// Yol 3: Class.forName() — Sınıf adı String olarak geliyorsa
Class<?> clazz3 = Class.forName("java.lang.String");

System.out.println(clazz1 == clazz2);  // true
System.out.println(clazz2 == clazz3);  // true — hepsi aynı Class nesnesi

Her üç yol da aynı Class nesnesini döndürür — çünkü JVM her sınıf için tek bir Class nesnesi tutar.

Class'tan Temel Bilgiler

Class<?> clazz = ArrayList.class;

System.out.println(clazz.getName());           // java.util.ArrayList
System.out.println(clazz.getSimpleName());     // ArrayList
System.out.println(clazz.getPackageName());    // java.util
System.out.println(clazz.getSuperclass());     // class java.util.AbstractList
System.out.println(clazz.isInterface());       // false
System.out.println(clazz.isEnum());            // false

// Implement ettiği interface'ler
for (Class<?> iface : clazz.getInterfaces()) {
    System.out.println("Interface: " + iface.getSimpleName());
}
// Interface: List
// Interface: RandomAccess
// Interface: Cloneable
// Interface: Serializable

Class.forName() en güçlü yoldur çünkü sınıf adı runtime'da belirlenebilir — konfigürasyon dosyasından, veritabanından veya kullanıcı girdisinden okunabilir.


3. Field Erişimi — Alanları Okuma ve Yazma

Reflection ile bir sınıfın alanlarına — hatta private olanlara bile — erişebilirsiniz.

Alanları Keşfetme

class User {
    private String name;
    private int age;
    public String email;

    User(String name, int age, String email) {
        this.name = name;
        this.age = age;
        this.email = email;
    }
}
Class<?> clazz = User.class;

// Sadece public alanlar (miras dahil)
Field[] publicFields = clazz.getFields();

// Tüm alanlar — private dahil (sadece bu sınıfta tanımlananlar)
Field[] allFields = clazz.getDeclaredFields();

for (Field field : allFields) {
    System.out.printf("%-10s %-8s %s%n",
        field.getName(),
        field.getType().getSimpleName(),
        Modifier.isPrivate(field.getModifiers()) ? "(private)" : "(public)");
}
// name       String   (private)
// age        int      (private)
// email      String   (public)

getFields() ve getDeclaredFields() farkını iyi anlamak gerekir. getFields() sadece public alanları döndürür ama miras zincirindeki tüm sınıfları tarar. getDeclaredFields() ise o sınıfta tanımlanan tüm alanları (private dahil) döndürür ama miras alınanları göstermez.

Değer Okuma ve Yazma

User user = new User("Ahmet", 25, "ahmet@mail.com");
Class<?> clazz = user.getClass();

// Private alana erişim
Field nameField = clazz.getDeclaredField("name");
nameField.setAccessible(true);  // private erişim engelini kaldır

// Okuma
String name = (String) nameField.get(user);
System.out.println("Mevcut isim: " + name);  // Ahmet

// Yazma
nameField.set(user, "Mehmet");
String newName = (String) nameField.get(user);
System.out.println("Yeni isim: " + newName);  // Mehmet

⚠️ Dikkat: setAccessible(true) çağrısı Java'nın erişim kontrolünü devre dışı bırakır. Bu güçlü bir silahtır — framework'ler için gereklidir ama uygulama kodunda kullanmaktan kaçının. Modüler projede (JPMS), setAccessible(true) çalışması için ilgili paketin opens ile açılmış olması gerekir.


4. Method Erişimi — Metodları Çağırma

Reflection ile bir nesnenin metodlarını runtime'da keşfedip çağırabilirsiniz.

Metodları Keşfetme

class Calculator {
    public int add(int a, int b) {
        return a + b;
    }

    private int multiply(int a, int b) {
        return a * b;
    }

    public String format(double value) {
        return String.format("%.2f", value);
    }
}
Class<?> clazz = Calculator.class;

for (Method method : clazz.getDeclaredMethods()) {
    String params = Arrays.stream(method.getParameterTypes())
        .map(Class::getSimpleName)
        .collect(Collectors.joining(", "));

    System.out.printf("%s %s(%s)%n",
        method.getReturnType().getSimpleName(),
        method.getName(),
        params);
}
// int add(int, int)
// int multiply(int, int)
// String format(double)

Metod Çağırma (invoke)

Calculator calc = new Calculator();
Class<?> clazz = calc.getClass();

// Public metod çağırma
Method addMethod = clazz.getDeclaredMethod("add", int.class, int.class);
int result = (int) addMethod.invoke(calc, 10, 20);
System.out.println("10 + 20 = " + result);  // 30

// Private metod çağırma
Method multiplyMethod = clazz.getDeclaredMethod("multiply", int.class, int.class);
multiplyMethod.setAccessible(true);
int product = (int) multiplyMethod.invoke(calc, 5, 6);
System.out.println("5 * 6 = " + product);  // 30

getDeclaredMethod() iki parametre alır: metod adı ve parametre tipleri. Parametre tipleri metod overloading durumunda hangi metodu kastettiğinizi belirler. invoke() ise ilk parametre olarak hedef nesneyi, sonrasında metodun parametrelerini alır.

Static Metod Çağırma

Static metodları çağırırken invoke()'un ilk parametresi null olur — çünkü static metodlar bir nesneye bağlı değildir.

Method valueOfMethod = Integer.class.getDeclaredMethod("valueOf", int.class);
Integer wrapped = (Integer) valueOfMethod.invoke(null, 42);
System.out.println(wrapped);  // 42

5. Constructor Erişimi — Nesne Oluşturma

Reflection ile bir sınıfın constructor'larını keşfedip yeni nesneler oluşturabilirsiniz.

class Product {
    private String name;
    private double price;

    public Product() {
        this.name = "Bilinmiyor";
        this.price = 0;
    }

    public Product(String name, double price) {
        this.name = name;
        this.price = price;
    }

    @Override
    public String toString() {
        return name + " - " + price + " TL";
    }
}
Class<?> clazz = Product.class;

// No-arg constructor ile nesne oluşturma
Constructor<?> noArgCtor = clazz.getDeclaredConstructor();
Product p1 = (Product) noArgCtor.newInstance();
System.out.println(p1);  // Bilinmiyor - 0.0 TL

// Parametreli constructor ile nesne oluşturma
Constructor<?> paramCtor = clazz.getDeclaredConstructor(String.class, double.class);
Product p2 = (Product) paramCtor.newInstance("Laptop", 25000.0);
System.out.println(p2);  // Laptop - 25000.0 TL

Constructor'lar da getDeclaredConstructors() ile listelenebilir ve private constructor'lara setAccessible(true) ile erişilebilir. Singleton pattern'i test ederken veya framework'lerde bu yaygındır.

// Tüm constructor'ları listeleme
for (Constructor<?> ctor : clazz.getDeclaredConstructors()) {
    String params = Arrays.stream(ctor.getParameterTypes())
        .map(Class::getSimpleName)
        .collect(Collectors.joining(", "));
    System.out.println("Constructor(" + params + ")");
}
// Constructor()
// Constructor(String, double)

6. Annotation Okuma

Reflection'ın en yaygın kullanım alanlarından biri annotation'ları okumaktır. Framework'ler genellikle sınıflara, alanlara veya metodlara annotation ekler ve runtime'da bunları reflection ile okur.

Custom Annotation Tanımlama

import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)  // Runtime'da erişilebilir olsun
@Target(ElementType.FIELD)            // Sadece alanlara uygulanabilir
@interface JsonField {
    String name() default "";
}

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)             // Sadece sınıflara uygulanabilir
@interface Entity {
    String table();
}

@Retention(RetentionPolicy.RUNTIME) kritik önemdedir. Bu olmadan annotation derleme sonrası silinir ve reflection ile okunamaz.

Annotation Okuma

@Entity(table = "users")
class User {
    @JsonField(name = "user_name")
    private String name;

    @JsonField
    private int age;

    private String internalId;  // Bu alanda annotation yok

    User(String name, int age) {
        this.name = name;
        this.age = age;
        this.internalId = "INT-" + System.nanoTime();
    }
}
Class<?> clazz = User.class;

// Sınıf seviyesinde annotation kontrolü
if (clazz.isAnnotationPresent(Entity.class)) {
    Entity entity = clazz.getAnnotation(Entity.class);
    System.out.println("Tablo adı: " + entity.table());  // users
}

// Alan seviyesinde annotation okuma
for (Field field : clazz.getDeclaredFields()) {
    if (field.isAnnotationPresent(JsonField.class)) {
        JsonField jsonField = field.getAnnotation(JsonField.class);
        String jsonName = jsonField.name().isEmpty()
            ? field.getName()  // annotation'da isim yoksa alan adını kullan
            : jsonField.name();

        System.out.printf("Alan: %s → JSON: %s%n", field.getName(), jsonName);
    }
}
// Alan: name → JSON: user_name
// Alan: age → JSON: age

Bu pattern, Jackson'ın @JsonProperty veya JPA'nın @Column annotation'larının çalışma prensibinin temelidir. Framework sınıfın alanlarını tarar, annotation'ları okur ve ona göre davranır.


7. Mini Framework Örneği: Basit JSON Serializer

Şimdiye kadar öğrendiklerimizi birleştirerek basit bir JSON serializer yazalım. Bu örnek, Jackson veya Gson gibi kütüphanelerin perde arkasında neler yaptığının küçük bir demosu.

import java.lang.annotation.*;
import java.lang.reflect.*;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@interface JsonField {
    String name() default "";
}

class SimpleJsonSerializer {

    static String toJson(Object obj) throws Exception {
        Class<?> clazz = obj.getClass();
        StringBuilder json = new StringBuilder("{");
        boolean first = true;

        for (Field field : clazz.getDeclaredFields()) {
            if (!field.isAnnotationPresent(JsonField.class)) continue;

            field.setAccessible(true);

            JsonField annotation = field.getAnnotation(JsonField.class);
            String key = annotation.name().isEmpty()
                ? field.getName()
                : annotation.name();

            Object value = field.get(obj);

            if (!first) json.append(", ");
            first = false;

            json.append("\"").append(key).append("\": ");

            if (value instanceof String) {
                json.append("\"").append(value).append("\"");
            } else {
                json.append(value);
            }
        }

        json.append("}");
        return json.toString();
    }
}
class Employee {
    @JsonField(name = "full_name")
    private String name;

    @JsonField
    private int age;

    @JsonField(name = "department")
    private String dept;

    private double secretSalary;  // annotation yok — serialize edilmeyecek

    Employee(String name, int age, String dept, double salary) {
        this.name = name;
        this.age = age;
        this.dept = dept;
        this.secretSalary = salary;
    }
}

// Kullanım
Employee emp = new Employee("Ayşe Yılmaz", 30, "Mühendislik", 45000);
String json = SimpleJsonSerializer.toJson(emp);
System.out.println(json);
// {"full_name": "Ayşe Yılmaz", "age": 30, "department": "Mühendislik"}

secretSalary alanı @JsonField annotation'ına sahip olmadığı için JSON çıktısında yer almaz. İşte gerçek JSON kütüphaneleri de temelde bu mantıkla çalışır — tabii çok daha fazla edge case, performans optimizasyonu ve tip desteğiyle.


8. Framework'ler Neden Reflection Kullanır?

Spring, Hibernate, Jackson, JUnit — Java ekosisteminin neredeyse tüm büyük framework'leri reflection'a dayanır. Bunun temel sebebi framework'ün kullanıcı kodunu önceden bilmemesidir.

Spring — Dependency Injection

@Service
class UserService {
    private final UserRepository repository;

    @Autowired
    UserService(UserRepository repository) {  // Spring bunu reflection ile bulur
        this.repository = repository;
    }
}

Spring başlatıldığında şunları yapar:

  1. Classpath'i tarar, @Service, @Component gibi annotation'lı sınıfları bulur

  2. Her sınıfın constructor'ını reflection ile inceler

  3. Constructor parametrelerinin tiplerini belirler

  4. Uygun bean'leri bulup newInstance() ile nesne oluşturur

JPA/Hibernate — ORM Mapping

@Entity
@Table(name = "products")
class Product {
    @Id
    @GeneratedValue
    private Long id;

    @Column(name = "product_name")
    private String name;
}

Hibernate, Product sınıfını reflection ile inceler: @Entity annotation'ını görür, alanları tarar, @Column annotation'larını okur ve SQL sorguları oluşturur. Private alanları setAccessible(true) ile okur/yazar.

Jackson — JSON Serialization

class ApiResponse {
    @JsonProperty("status_code")
    private int statusCode;

    @JsonIgnore
    private String internalToken;
}

Jackson, sınıfın alanlarını reflection ile tarar, annotation'ları okur ve JSON dönüşümünü buna göre yapar. @JsonIgnore olan alanı atlar, @JsonProperty ile isim değişikliği uygular.


9. Performans ve Güvenlik

Reflection güçlüdür ama bedavaya gelmez.

Performans Maliyeti

Reflection ile yapılan işlemler doğrudan kod çağrısından önemli ölçüde yavaştır. Bunun birkaç sebebi var:

  • JVM'in erişim kontrolü yapması ve devre dışı bırakması gerekir

  • Metod çağrıları JIT derleyici tarafından optimize edilemez

  • Parametre tipleri runtime'da kontrol edilir (boxing/unboxing dahil)

// Direkt çağrı — JIT optimize eder, nanosaniyeler
calculator.add(10, 20);

// Reflection — her çağrıda tip kontrolü, erişim kontrolü
Method m = Calculator.class.getDeclaredMethod("add", int.class, int.class);
m.invoke(calculator, 10, 20);  // ~10-50x daha yavaş

Bu yüzden framework'ler genellikle reflection'ı başlangıçta bir kez kullanır: sınıf yapısını inceler, meta bilgiyi cache'ler ve sonraki işlemlerde cache'ten okur. Spring'in başlangıç süresinin uzun olmasının bir sebebi de budur — ama bir kez başladıktan sonra reflection maliyeti minimize edilir.

Güvenlik Endişeleri

setAccessible(true) Java'nın temel güvenlik mekanizmalarından olan erişim kontrolünü devre dışı bırakır. Bu, kötü amaçlı kodun private alanlara erişmesine olanak tanır.

Java 9'dan itibaren JPMS bu sorunu kısmen çözer: modüler projede setAccessible(true) sadece opens ile açılmış paketlerde çalışır. Bu ek güvenlik katmanı, reflection'ın kontrolsüz kullanımını engeller.

// JPMS ile — modül opens yapmamışsa
field.setAccessible(true);
// InaccessibleObjectException: module X does not opens package Y

💡 İpucu: Uygulama kodunda reflection'dan mümkün olduğunca kaçının. Interface, abstract class ve polimorfizm çoğu durumda reflection'a ihtiyaç duymadan aynı esnekliği sağlar. Reflection, framework ve kütüphane geliştiricileri için bir araçtır.


10. Dynamic Proxy — Çalışma Zamanında Interface Implementasyonu

java.lang.reflect.Proxy sınıfı, çalışma zamanında bir interface'i implemente eden yeni bir sınıf oluşturmanıza olanak tanır. Bu, AOP (Aspect-Oriented Programming), loglama, caching ve transaction yönetimi gibi cross-cutting concern'ler için kullanılır.

Temel Kullanım

import java.lang.reflect.*;

interface Greeter {
    String greet(String name);
    String farewell(String name);
}
// InvocationHandler — tüm metod çağrılarını yakalar
InvocationHandler handler = (proxy, method, args) -> {
    System.out.println("[LOG] " + method.getName() + " çağrıldı");

    return switch (method.getName()) {
        case "greet"    -> "Merhaba, " + args[0] + "!";
        case "farewell" -> "Güle güle, " + args[0] + "!";
        default         -> throw new UnsupportedOperationException();
    };
};

// Proxy oluştur
Greeter greeter = (Greeter) Proxy.newProxyInstance(
    Greeter.class.getClassLoader(),
    new Class<?>[] { Greeter.class },
    handler
);

System.out.println(greeter.greet("Ayşe"));
System.out.println(greeter.farewell("Mehmet"));

Çıktı:

[LOG] greet çağrıldı
Merhaba, Ayşe!
[LOG] farewell çağrıldı
Güle güle, Mehmet!

Gerçekçi Örnek: Loglama Proxy'si

Pratikte dynamic proxy genellikle mevcut bir implementasyonu sarmalar (wrap eder):

interface UserService {
    String findUser(int id);
    void deleteUser(int id);
}

class UserServiceImpl implements UserService {
    public String findUser(int id) {
        return "User-" + id;
    }

    public void deleteUser(int id) {
        System.out.println("Kullanıcı " + id + " silindi");
    }
}
class LoggingProxy implements InvocationHandler {
    private final Object target;

    LoggingProxy(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        long start = System.nanoTime();
        System.out.printf("[BEFORE] %s(%s)%n", method.getName(), Arrays.toString(args));

        Object result = method.invoke(target, args);  // Gerçek metodu çağır

        long elapsed = (System.nanoTime() - start) / 1_000_000;
        System.out.printf("[AFTER] %s → %dms%n", method.getName(), elapsed);

        return result;
    }

    @SuppressWarnings("unchecked")
    static <T> T create(T target, Class<T> iface) {
        return (T) Proxy.newProxyInstance(
            iface.getClassLoader(),
            new Class<?>[] { iface },
            new LoggingProxy(target)
        );
    }
}
UserService service = LoggingProxy.create(new UserServiceImpl(), UserService.class);

service.findUser(42);
// [BEFORE] findUser([42])
// [AFTER] findUser → 0ms

service.deleteUser(7);
// [BEFORE] deleteUser([7])
// Kullanıcı 7 silindi
// [AFTER] deleteUser → 0ms

Bu pattern'in gücü, loglama kodunun UserServiceImpl içinde olmamasıdır. Aynı LoggingProxy'yi herhangi bir interface implementasyonuna uygulayabilirsiniz — kod tekrarı sıfır. Spring AOP'nin @Transactional, @Cacheable gibi annotation'ları perde arkasında benzer proxy mekanizmaları kullanır.

⚠️ Dikkat: java.lang.reflect.Proxy sadece interface'ler için çalışır, concrete sınıflar için değil. Sınıf bazlı proxy'ler için CGLIB veya ByteBuddy gibi kütüphaneler kullanılır — Spring bunları tercih eder.


11. Reflection ile Yapılan Yaygın Hatalar

Hata 1: NoSuchMethodException

// Yanlış — parametre tipleri eşleşmiyor
Method m = clazz.getDeclaredMethod("add", Integer.class, Integer.class);
// NoSuchMethodException! Metod int alıyor, Integer değil.

// Doğru — primitive tipler için .class kullan
Method m = clazz.getDeclaredMethod("add", int.class, int.class);

Primitive tipler (int, double, boolean) ile wrapper tipler (Integer, Double, Boolean) reflection'da farklı tiplerdir. Metod int alıyorsa int.class kullanmalısınız.

Hata 2: setAccessible Unutma

Field field = clazz.getDeclaredField("name");
// field.setAccessible(true);  ← Bunu unuttun
String value = (String) field.get(obj);
// IllegalAccessException: class Main cannot access private field

Hata 3: Static ve Instance Karışıklığı

// Static metod — invoke'un ilk parametresi null
Method staticMethod = Math.class.getMethod("abs", int.class);
staticMethod.invoke(null, -5);  // 5

// Instance metod — invoke'un ilk parametresi nesne
Method instanceMethod = String.class.getMethod("length");
instanceMethod.invoke("hello");  // 5
instanceMethod.invoke(null);     // NullPointerException!

Özet

  • Reflection bir Java programının çalışma zamanında kendi yapısını incelemesi ve değiştirmesidir — Class, Field, Method, Constructor sınıfları temel araçlarıdır.

  • `Class` nesnesine .class, .getClass() veya Class.forName() ile erişilir — her sınıf için JVM'de tek bir Class nesnesi bulunur.

  • `setAccessible(true)` private üyelere erişimi açar ama güvenlik risklidir — JPMS'te opens direktifi olmadan çalışmaz.

  • Annotation okuma reflection'ın en yaygın kullanım alanıdır — framework'ler (@Autowired, @Entity, @JsonProperty) bu sayede çalışır.

  • Dynamic Proxy ile runtime'da interface implementasyonları oluşturulur — AOP, loglama ve transaction yönetimi için temeldir.

  • Performans maliyeti yüksektir (10-50x yavaş) — framework'ler bunu başlangıçta bir kez yapıp cache'leyerek minimize eder; uygulama kodunda reflection'dan kaçının.