Java ile Özel Notasyon Geliştirimi

Hüseyin Akdoğan
6 min readDec 23, 2019

JDK 1.5 ve üstü bir sürümle geliştirim yapmış her seviyeden Java geliştiricisi notasyon kullanmıştır. @Inject, @Entity, @Autowired, hiç olmadıysa @Override notasyonunu görmemiş, kullanmamış herhalde yoktur. Java dilinin güçlü yanlarından biri olan notasyonlar, birer dekoratör gibi çalışarak; sınıf, yapılandırıcı, metot, alan gibi program yapılarına meta verisi eklememizi sağlar. Geliştiriciye belirli bir eylem/davranışı adeta markalayıp, uygulamanın ihtiyaç duyulan noktalarına enjekte etme olanağı sağlayan notasyonlar, mükerrer kod yazımını azaltıp, kod okunurluğunu artırırlar.

Notasyonların gücünden, Java geliştiricileri kendi özel notasyonlarını oluşturarak yeterince yararlanmakta mıdır? Başta kendi geliştirim tecrübem olmak üzere kişisel gözlemim ve çeşitli yazı, makale, yayınlanmış anketlerden anladığım; Javacılar notasyon kullanmayı yazmaktan daha çok seviyor. Bu yazıyla amacım, notasyonları daha yakından tanımanıza yardımcı olmak, çalışma mantıklarını göstermek ve basit bir örnek aracılığıyla kendi notasyonunuzu nasıl yazıp kullanabileceğinizi örneklendirerek, özel notasyonların sağladığı avantajlara dikkatinizi çekmek.

Nasıl Çalışıyor?

An annotation is a marker which associates information with a program construct, but has no effect at run time. JLS Section 9.7

Java dil belirtiminde ifade edildiği gibi, notasyonlar kendi başlarına çalışma zamanında herhangi bir etkiye sahip değildir. Daha açık bir ifadeyle, bir notasyon tek başına eklendiği programın çalışma zamanı davranışını değiştirmez. Aslında bir notasyon, uygulandığı noktada elde edilmek istenen davranışı bildiren bir işaretçi(an annotation is a marker - JSL) gibi davranır. Bu sebeple, notasyonların geliştirici tarafından belirlenmiş davranış/eylemi gerçekleştirmesi için çalışma zamanı çerçeveleri(runtime frameworks) veya derleyici tarafından işlenmesi gerekir. Notasyonlar, derleme zamanında Java derleyicisinin bir tür eklentisi sayılabilecek notasyon işleyiciler(annotation processors), çalışma zamanında ise Java Reflection API tarafından işlenir. Bu yazıda, çalışma zamanında Reflection API ile işlenecek bir notasyon örneğini inceleyeceğiz.

Nasıl Tanımlanır?

Bir notasyon tanımlamak, arayüz tanımlamaya benzer. Sözdizimsel fark, interface anahtarı başına @ sembolünün getirilmesinden ibarettir.

public @interface Monitor {}

Bir notasyon oluşturmak için temelde iki bilgiyi sağlamak gerekmektedir. Bunlardan biri Retention politikası, diğeri Target. Retention politikası, notasyona erişim zamanını tanımlarken, target hangi yapıya(sınıf, method, alan) uygulanacağını tanımlamaktadır.

Retention politikası, RetentionPolicy ile tanımlanır. RetentionPolicy enum tipinde bir veri yapısıdır ve java.lang.annotation paketi altında bulunmaktadır.

package java.lang.annotation;/**
* Annotation retention policy. The constants of this enumerated type
* describe the various policies for retaining annotations. They are used
* in conjunction with the {
@link Retention} meta-annotation type to specify
* how long annotations are to be retained.
*
*
@author Joshua Bloch
*
@since 1.5
*/
public enum RetentionPolicy {
/**
* Annotations are to be discarded by the compiler.
*/
SOURCE,
/**
* Annotations are to be recorded in the class file by the compiler
* but need not be retained by the VM at run time. This is the default
* behavior.
*/
CLASS,
/**
* Annotations are to be recorded in the class file by the compiler and
* retained by the VM at run time, so they may be read reflectively.
*
*
@see java.lang.reflect.AnnotatedElement
*/
RUNTIME
}

Görüldüğü üzere 3 retention politikası vardır.

  • SOURCE: Notasyon derleyici tarafından atılır.
  • CLASS: Notasyon derleyici tarafından oluşturulan sınıf dosyasına kaydedilir ve JVM tarafından saklanması gerekmez. Varsayılan davranış biçimidir.
  • RUNTIME: Notasyon sınıf dosyasına derleyici tarafından kaydedilir ve çalışma zamanında JVM tarafından saklanır, böylece reflection ile okunabilir.

Runtime, uygulamaların notasyonlara ve ilişkili verilerine reflection ile erişip kod yürütülmesine izin verdiği için en bilinen ve yaygın olarak kullanılan retention politikasıdır.

Java 9 sonrası, 11 target vardır; bir başka deyişle 11 yapıya notasyon uygulayabiliriz.

  • TYPE: Sınıf, arayüz, notasyon, enum gibi tipleri hedefler
  • FIELD: Sınıf değişkenleri, enum sabitleri gibi alan tiplerini hedefler
  • METHOD: Sınıf metotlarını hedefler
  • MODULE: Java 9 ile gelmiştir, modülleri hedefler
  • PARAMETER: Metot, yapılandırıcı parametrelerini hedefler
  • CONSTRUCTOR: Yapılandırıcıları hedefler
  • LOCAL_VARIABLE: Yerel değişkenleri hedefler
  • ANNOTATION_TYPE: Notasyonları hedefler ve başka bir notasyon ekler
  • PACKAGE: Paketleri hedefler
  • TYPE_PARAMETER: Java 1.8 ile gelmiştir, MyClass<T>’deki T gibi jenerik parametreleri hedefler
  • TYPE_USE: Java 1.8 ile gelmiştir, herhangi bir türün kullanımını(örneğin new ile oluşturulma, arayüz implementasyonu, cast işlemi vb) hedefler

Notasyon Parametreleri ve Notasyon Tipleri

Notasyonlar program yapılarına meta verisini, parametreleri aracılığıyla ekler. Bir notasyonun parametreye sahip olup olmaması tipini belirler. Parametre bildirimi içermeyen notasyonlar işaretçi notasyon(marker annotation type) türünü, tek bir parametre bildirimi içeren notasyonlar tek elemanlı(single element annotation type), birden fazla parametre bildirimi içeren notasyonlar ise kompleks notasyon(complex annotation type) türünü oluşturur. Primitif tipler, String, Class, Enum, bir başka notasyon ve bu anılan tiplerin dizi tipi, bir notasyon parametresi olarak bildirilebilir.

Senaryomuz

Profiling için kodumuzda çalışma sürelerini ölçmek istediğimiz metotlara sahip olduğumuzu düşünelim. Her metodun değil, bazı metotların çalışma süresini ölçmek istiyoruz; üstelik bu metotlara ilerde yenileri eklenebilir. Bunun için bir notasyon yazmak iyi bir fikirdir çünkü en başta belirttiğimiz üzere kod okunurluğunu bozmadan ek olarak aynı davranışı tekrar tekrar dilediğimiz noktalarda(örneğin kodumuza eklenecek yeni metotlarda) elde etmek için notasyonlar biçilmiş kaftandır.

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Monitor
{
MonitorPolicy value();
}

Retention policy ve Target’tan daha önce bahsettiğimiz için, yukarıda görülen notasyonun çalışma zamanında Reflection API ile işlenebileceğini ve ancak metotlara(başka bir yapıya uygulanırsa derleme zamanında hata alınır) uygulanabileceğini kestirmeniz zor değil. Bu noktada Target ile ilgili şu detayı da paylaşalım, birden fazla hedef tanımlayabilirsiniz.

@Target({ElementType.FIELD, ElementType.METHOD})

Notasyonumuzun gövdesinde ise enum tipinde MonitorPolicy parametresinin deklare edildiğini görüyorsunuz. İlgili enum değerine çalışma zamanında value() metodu üzerinden erişeceğimiz gibi, notasyonumuza değer geçirmek için value’yu anahtar olarak da kullanabiliriz.

@Monitor(value = MonitorPolicy.SHORT)

Fakat değer geçirmek için value anahtarını kullanmak zorunlu değildir. Aşağıdaki kullanım ile yukarıdaki eşdeğerdir.

@Monitor(MonitorPolicy.SHORT)

Value dışında farklı bir ismi kullansaydık bunu belirtmemiz gerekirdi çünkü value, notasyonlarda varsayılan anahtar ismidir. MonitorPolicy SHORT ve DETAILED değerlerine sahiptir. Bu değerlerle, çalışma süresini ölçeceğimiz metotlara dair döndürülecek bilginin biçimini belirliyoruz, ayrıntılı veya kısa.

enum MonitorPolicy 
{
SHORT,DETAILED
}

Tanımladığımız parametreye varsayılan bir değer atamak istediğimizde ise default anahtarını kullanırız.

MonitorPolicy value() default MonitorPolicy.SHORT;

Bu durumda aşağıdaki gibi bir kullanımda, monitor policy short olacaktır.

@Monitor
public String getUserInfo(){
//Make HTTP request
}

İşleyicimiz

Daha önce ifade ettiğimiz gibi, notasyonlar kendi başlarına herhangi bir kod çalıştırmazlar. Monitor notasyonunda Retention policy olarak RUNTIME tanımlandığı için, çalışma zamanında Java Reflection API kullanılarak işlenmesi gerekir. Aşağıdaki metot bu vazifeyi gerçekleştirir.

public static String executor(final Object object, final Object... passedParameters) {
checkObjectReference(object);
final StringBuilder result = new StringBuilder();
final Method[] methods = object.getClass().getDeclaredMethods();
for(Method method :methods){
if(method.isAnnotationPresent(Monitor.class)){
if(passedParameters.length > 0){
result.append(invoker(method, object, passedParameters));
} else {
result.append(invoker(method, object));
}
}
}
return result.toString();
}

Executor metodu, biri Monitor notasyonunu kullanan sınıf referansı, diğeri varargs tipinde; ölçüm yapılacak metotların parametreleri olmak üzere 2 argüman alıyor.

Önce object referansı üzerinden, ilgili nesnenin deklare edilmiş metotlarını çekip ardından bir döngü yardımıyla gezilen metotlarda Monitor notasyonunun uygulanıp uygulanmadığını Reflection API’ye ait isAnnotationPresent() metoduyla(metoda notasyon nesnesini geçirdiğimize dikkat edin) kontrol ediyoruz. Monitor notasyonunu uygulayan metod varsa, koşturulması için invoker metodunu çağırıyoruz.

private static String invoker(final Method method, Object object, Object... passedParameters) {

method.setAccessible(true);
final StringBuilder result = new StringBuilder();
final MonitorPolicy policy = method.getAnnotation(Monitor.class).value();
long start = System.currentTimeMillis();

try {
if (passedParameters.length > 0) {
checkTypeMismatch(method.getName(), method.getParameters(), passedParameters);
method.invoke(object, passedParameters);
} else if(method.getGenericParameterTypes().length > 0){
throw new MissingArgumentException("The parameter(s) of the " + method.getName() + " method are missing");
} else {
method.invoke(object);
}

final long end = System.currentTimeMillis();
if(policy.equals(MonitorPolicy.SHORT)){
result.append(end - start).append(" ms");
logger.info( "{} ms", (end - start));
} else {
result.append("Total execution time of ")
.append(method.getName())
.append(" method is ")
.append(end - start)
.append(" ms");
logger.info("Total execution time of {} method is {} ms", method.getName(), (end - start));
}

} catch ...

Public olarak deklare edilmemiş metotlara da erişebilmek için, invoker metodunda ilk olarak method.setAccessible(true); ile erişim kontrolünü yapılandırıyoruz; aksi halde private metotlar için IllegalAccessException istisnasıyla karşılaşırız. Sonraki adımda, çağırıldığı yapıyla(bizim örneğimizde metot) ilişkilendirilmiş, belirtilen tipteki(metoda notasyon nesnesini geçirdiğimize dikkat edin) notasyonu döndüren Reflection API’ye ait getAnnotation metodunu kullanarak, notasyonumuzda tanımlanmış değeri çekiyoruz. Bu değeri, çalışma süresine dair bilgiyi çıktılama biçimizi belirlemede kullanacağız. Bu aşamadan sonra, o anki zaman mührünü milisaniye cinsinden start değişkeninde depoluyoruz; Monitor notasyonunu uygulamış metodu, Reflection API’nin invoke metoduyla koşturduktan sonra elde edilen zaman mührünü ise end değişkeninde. Son aşamada ise, MonitorPolicy’ye bağlı olarak çalışma süresini çıktılıyoruz.

Aşağıda, paylaşılan örnekteki gibi bir kullanımda alacağımız muhtemel çıktıyı görüyorsunuz.

@Monitor(MonitorPolicy.DETAILED)
public String getUserInfo(){
//Make HTTP request
}
Profiler.executor(new User());

Total execution time of getUserInfo method is 1459 ms

Monitor notasyonunu içeren uygulamaya buradan ulaşabilirsiniz.

Sonuç

Java notasyonları sınıf, yapılandırıcı, metot, alan gibi Java program yapılarına meta verisi eklememizi sağlayan, bu şekilde mükerrer kod yazımını azaltıp, kod okunurluğunu artıran Java dilinin güçlü yanlarından biridir. Notasyonlar kendi başlarına herhangi bir kod çalıştırmazlar, bunun yerine derleyici veya ait oldukları çerçeve(framework) için, uygulandığı noktada elde edilmek istenen davranışı bildiren bir işaretçi gibi davranıp, derleme zamanında Java Annotation Processors’ler, çalışma zamanında ise Java Reflection API tarafından işlenirler.

--

--