Java 9 ile Modül Bazlı Yazılım

Bir yazılım sisteminde karmaşaya, bağımlılıklara ve kodun bakım ve geliştirilmesi sürecine hakim olabilmenin bir yolu da komponent ya da modül bazlı yazılım yapmaktan geçmektedir. İdeal şartlarda bir modül tek bir görevi yerine getirir ve tek sorumluluk prensibi göz önünde bulundurularak implemente edilmiştir. Modül iç dünyasını gizli tutar ve kullanımını modül API (application programming interface) olarak isimlendirilen tanımlı giriş, çıkış kanalları ya da başka bir deyişle kullanım arayüzü aracılığı ile sağlar. Kullanım arayüzleri modülün hangi işlemleri gerçekleştirdiğini soyut olarak tanımlarken, bu işlemlerin nasıl gerçekleştirildikleri modül içinde yer alan implementasyonlarda yer almaktadırlar. Kısaca bir modül kullanıcısı için bir kara kutudur. Bu şekilde kullanıcısını etkilemeden, iç implementasyonu değiştirmek mümkündür, çünkü kullanıcı iç implementasyona değil, kullanıcı arayüzüne bağımlıdır. Kullanıcı arayüzleri sahip oldukları yapıyı koruyabildikleri sürece, modül üzerinde yapılan değişiklikler kullanıcıyı etkilemez. Bu şekilde tanımlı kullanıcı arayüzleri aracılığıyla esnek olarak birbirine bağlı olan uygulama parçaları geliştirmek ve bu parçalar üzerinde uygulamanın genelini etkilemeden gerekli değişiklikleri yapmak mümkündür.

Java 9 öncesinde modül bazlı yazılımın nasıl yapıldığını ya da benim bu konuda nasıl çalıştığımı ve karşılaştığım sorunları bir örnek üzerinde göstermek istiyorum. Vereceğim örnekte loglama işlemini gerçekleşiren bir modül yer almaktadır. Bu modülün kullanım arayüzü (API) Logger.java isimli sınıfta tanımlıdır.


package com.kurumsaljava.logger;

public interface Logger {

	void log(LogLevel level, String log);

}

Logger bir interface sınıftır ve modülün yaptığı işlemi tek bir soyut metot olarak ihtiva etmektedir. Bu yapı bize genel olarak modülün hangi tür bir işlemden sorumlu olduğunu tanımlama imkanı sağlamaktadır. Bunu yaparken herhangi bir implementasyon detayı kullanma zorunluluğumuz bulunmamaktadır. Bu şekilde tamamen soyut bir tanımlama yapmış olmaktayız. Oluşturduğunuz API ne sorusuna cevap verirken, nasıl sorunun cevabını iç implementasyona bırakmaktadır.

Loglama işleminin nasıl gercekleştirildiği bir implementasyon detayıdır ve modül dışında yer alanları ilgilendirmez/ilgilendirmemeldir. Modülün ana görevlerinden birisi, bu implementasyon detayını gizlemek olmalıdır. Bu ona implementasyon üzerinde istediği şekilde değişiklik yapma imkanını sunarken, modül kullanıcılarının bu değişikliklerden doğrudan etkilenmelerinin önüne geçen bir unsur olacaktır.

Modülün sorumlu olduğu işlemi su şekilde implemente edebiliriz:

package com.kurumsaljava.logger;

import com.kurumsaljava.logger.LogLevel;
import com.kurumsaljava.logger.Logger;

class StdoutLoggerImpl implements Logger {

	@Override
	public void log(LogLevel level, String log) {
		System.out.println(level.name() + " - " + log);
	}
}

Burada dikkatinizi sınıfın tanımlandığı satıra çekmek istiyorum. Sınıfı public olarak tanımlamadım, çünkü modül dışındaki dünyanın bu sınıfa erişimini engellemek istiyorum. Modülün kara bir kutu olabilmesi için bu gerekli bir adım. Bana kalmış olsa bu sınıfı private olarak tanımlardım. Ama bu ne yazık ki mümkün değil, çünkü bir interface sınıfın tüm metotları ya public ya da package private olmak zorundadır yani implementasyon sınıfı private kullanarak metotların görünürlüğünü daha da daraltamaz. Bu yüzden yukarıda yer alan implementasyon sınıfını en az package private olarak tanımlayabilmekteyim. Bu StdoutLoggerImpl sınıfının sadece aynı paket içinde yer alan sınıflar tarafından erişilebilir olduğu anlamına gelmektedir. Ama reflection kullanarak, bu sorunu aşabiliriz yani sadece paket içinde kullanılabilir olan bir sınıfın reflection aracılığı ile başka bir paket içinde yer alan bir sınıf tarafından kullanılabilir olmasını sağlayabiliriz.

package com.kurumsaljava.application;

import java.lang.reflect.Constructor;

import com.kurumsaljava.logger.LogLevel;
import com.kurumsaljava.logger.Logger;

public class Main {

	public static void main(String[] args) throws Exception {

		Class<Logger> c = (Class<Logger>)Class.forName("com.kurumsaljava.logger.StdoutLoggerImpl");
		
		Constructor<?> constructor = c.getDeclaredConstructor();
		constructor.setAccessible(true);
		Logger logger = (Logger)constructor.newInstance();
		logger.log(LogLevel.DEBUG, "Hello World");
	}
}

Görüldüğü gibi başka bir paket içinde yer alan Main sınıfı aslında modülün iç dünyasının bir parçası olan ve dış dünyanın görmemesi gereken bir implementasyon sınıfını kullanabilmektedir. Buna ne derleyici ne de yorumlayıcı engel olabilmiştir. Bu durum modüler bir yapı oluşturmamıza engel olmakta ve modüllerin birer kara kutu olmaları gerektiği prensibini çiğmemektedir. Modül bir kara kutu olduğunu düşünerek hareket ettiğinde, örneğin StdoutLoggerImpl yerine başka bir sınıf oluşturulduğunda, Main sınıfı çalışmaz hale gelecektir. Oysaki bizim istediğimiz modül kullanım yapısı şu şekilde olmalıdır:

package com.kurumsaljava.application;

import com.kurumsaljava.logger.LogLevel;
import com.kurumsaljava.logger.LoggerFactory;

public class Main {

	public static void main(String[] args) throws Exception {

		LoggerFactory.instance().log(LogLevel.DEBUG, "Hello World");
	}
}

Main sınıfı logger modülene ve sunduğu hizmete erişebilmek için LoggerFactory sınıfını kullanmaktadır. LoggerFactory şu yapıya sahiptir ve iç implementasyonu API aracılığı ile kullanılabilir hale getirmektedir:

package com.kurumsaljava.logger;

public class LoggerFactory {

	public static Logger instance() {
		return new StdoutLoggerImpl();
	}
}

LoggerFactory sınıfı StdoutLoggerImpl sınıfı ile aynı paket içinde bulundugundan, LoggerFactory bu sınıfı kullabilmektedir. İç implementasyonu kullanılabilir hale getirmek için API’nin bir parçası olan böyle bir sınıfı oluşturmamız gerekmektektedir. Bu durumda modülün kullanım arayüzünü oluşturan sınıflar Logger, LogLevel ve LoggerFactory sınıflarıdır yani modül kullanıcıları modülün sunduğu hizmeti sadece bu üç sınıf aracılığı ile kullanabilirler. Ama reflection örneğinde gördüğümüz gibi iç implementasyona erişmek her zaman mümkündür yani Java dilinde gerçek modüller oluşturmak aslında mümkün değildir.

StdoutLoggerImpl sınıfı gizleme imkanımız gerçekten yok mu? Bir de şöyle denesek:

package com.kurumsaljava.logger;

public class LoggerFactory {

	public static Logger instance() {
		return new StdoutLoggerImpl();
	}

	private static class StdoutLoggerImpl implements Logger {

		@Override
		public void log(LogLevel level, String log) {
			System.out.println(level.name() + " - " + log);
		}
	}
}

Java’da bir sınıf bünyesinde başka bir sınıf tanımlamak (inner class) ve sınıfı private olarak deklare etmek mümkün. Modülü yine şu şekilde kullanabiliriz:

package com.kurumsaljava.application;

import com.kurumsaljava.logger.LogLevel;
import com.kurumsaljava.logger.LoggerFactory;

public class Main {

	public static void main(String[] args) throws Exception {

		LoggerFactory.instance().log(LogLevel.DEBUG, "Hello World");
	}
}

Peki reflection? Ne yazık ki bu şekilde bile reflection ile iç sınıflara erişim mümkündür:

public static void main(String[] args) throws Exception {

	Class<Logger> c = (Class<Logger>)Class.forName("com.kurumsaljava.logger.LoggerFactory$StdoutLoggerImpl");
	
	Constructor<?> constructor = c.getDeclaredConstructor();
	constructor.setAccessible(true);
	Logger logger = (Logger)constructor.newInstance();
	logger.log(LogLevel.DEBUG, "Hello World");
}

Sonuç? Java 9 öncesi dilin sahip oldugu özellikleri kullanarak kara kutu gibi çalışan modüller oluşturmak mümkün değil. Biz ne kadar paket bazında sınıf erişimini engellemeye çalışsak da, classpath içinde yer alan her sınıfa erişim bir şekilde mümkündür. Bize burada ne jar dosya yapısı, ne public/private direktifleri ne de tüm sınıfların yer aldığı classpath destek vermektedir. Peki Java dilinde modül nasıl oluşturabiliriz? Bu OSGI (Open Services Gateway initiative) ile mümkün, ama Java 9 ile daha da kolay, çünkü modüller için gerekli yapılar dilin ve derleyicinin bir parçası haline geldi.

Java 9 sürümünün getirdiği yeniliklerin başında yeni modül sistemi geliyor. Şimdi logger modülünün gerçek bir modül olarak Java 9 ile nasıl implemente edilebileceğini yakından inceleyelim.

Öncelikle bir takım sınıfların bir modül teşkil ettiğini tanımlamamız gerekiyor. Bu amaçla modül tanımlayıcısı (module descriptor) olarak isimlendirilen bir sınıf oluşturmamız gerekiyor. Logger modülü için bu sınıfı şu şekilde tanımlayabiliriz:

module logger {
}

Modül tanımlayıcı sınıfının ismi module-info.java ismini taşımaktadır. Yukarıdaki örnekte logger isminde bir modül tanımladık. Modülün yer alacağı dizin yapısı şu şekildedir:

src
 |__logger
       |__module-info.java
       |__com
           |_kurumsaljava
                  |__logger
                      |__Logger.java
                      |__LoggerFactory.java
                      |__LogLevel.java
                      |__StdoutLoggerImpl.java 

Modüle ait tüm parçalar src/logger dizininde yer almaktadır. Modülü tanımlamak için src/logger/module-info.java sınıfını oluşturduk. Modül implementasyonu /src/logger/com/kurumsaljava/logger dizininde yer almaktadır. Şimdi logger modülünü kullanan yeni bir modül oluşturulım. Application ismini taşıyan bu modül, logger modülünde yer alan loglama servisini kullanmaktadır.

src
 |__logger
 |     |__module-info.java
 |     |__com
 |         |_kurumsaljava
 |                |__logger
 |                      |__Logger.java
 |                      |__LoggerFactory.java
 |                      |__LogLevel.java
 |                      |__StdoutLoggerImpl.java 
 |__application
       |__module-info.java
       |__com
           |_kurumsaljava
                  |__application
                        |__Main.java                      

Uygulamamız iki modülden oluşmaktadır ve şu şekilde (windows powershell ile) derlenmektedir:


javac -d classes --module-source-path src $(dir src -r -i "*.java")

Birinci parametre (-d) ile derlenen sınıfların hangi dizinde yer aldığını, ikinci parametre ile (–module-source-path) derlemek istediğimiz modül kodlarının hangi dizinde yer aldığını tanımladık. Bu işlemin ardından ekran çıktısı şu şekilde olacaktır:

PS C:\Users\acar\eclipse-workspace-java9\modules> javac -d classes --module-source-path src $(dir src -r -i "*.java")
C:\Users\acar\eclipse-workspace-java9\modules\src\application\com\kurumsaljava\application\Main.java:3: error: package com.kurumsaljava.com.logger is not visible
import com.kurumsaljava.com.logger.LogLevel;
                           ^
  (package com.kurumsaljava.com.logger is declared in module logger, but module application does not read it)
C:\Users\acar\eclipse-workspace-java9\modules\src\application\com\kurumsaljava\application\Main.java:4: error: package com.kurumsaljava.com.logger is not visible
import com.kurumsaljava.com.logger.LoggerFactory;
                           ^
  (package com.kurumsaljava.com.logger is declared in module logger, but module application does not read it)
2 errors

Hatanın ne olduğunu daha iyi anlayabilmek için tekrar Main sınıfına bir göz atalım:

package com.kurumsaljava.application;

import com.kurumsaljava.logger.LogLevel;
import com.kurumsaljava.logger.LoggerFactory;

public class Main {

	public static void main(String[] args) {
		LoggerFactory.instance().log(LogLevel.INFO, "Hello World");
	}

}

Main sınıfı LogLevel ve LoggerFactory sınıflarını kullanmaktadır. Bunlar logger modülünün kullanım arayüzünü oluşturan sınıflardır. Lakin derleyici bu sınıflar için “not visible” yani görülmeyen sınıflar hatasını vermektedir. Derleyici bu sınıfları bulamamaktadır, çünkü logger modülü tanımlayıcısında (module-info.java) bu sınıflar kullanıma açılmamıştır yani logger modülü bu hali ile tam bir kara kutudur ve kullanımı imkansızdır. Logger modülünü kullanılabilir hale getirmek için kullanım arayüzünü oluşturan sınıfları diğer modüllerin erişebileceği şekilde tanımlamamız gerekmektedir. Bu işlemi şu şekilde gerçekleştirebiliriz:

-- logger/module-info.java

module logger {
	exports com.kurumsaljava.logger;
}

Bu tanımlama ile com.kurumsaljava.logger paketinde yer alan sınıfların hepsini diğer modüller tarafından kullanılabilir hale getirmiş olduk. Logger modülünü kullanmak isteyen her modülün şu şekilde bu isteği dile getirmesi gerekmektedir:

-- application/module-info.java

module application {
	requires logger;
}

Derleme işlemi tamamlandıktan sonra, uygulamayı oluşturan Main sınıfını şu şekilde koşturabiliriz:


java --module-path classes -m application/com.kurumsaljava.application.Main

Bu hali ile bir modülün sahip olması gerektiği yapıya çok yaklaşmış olduk. Modülün iç dünyasını paket bazında kullanıma sunabildigimiz için logger modülünün yapısını aşağıdaki şekilde değiştirmek faydalı olacaktır, aksi taktirde bu konfigürasyon ile application/Main sınıfı doğrudan StdoutLoggerImpl sınıfına erişebilmektedir. Bu dogal olarak modülün bir kara kutu gibi işlev görmesinin önünde bir engel oluşturmaktadır.

src
 |__logger
 |     |__module-info.java
 |     |__com
 |         |_kurumsaljava
 |                |__logger
 |                      |__impl                 
 |                      |     |__StdoutLoggerImpl.java
 |                      |__api
 |                          |__Logger.java 
 |                          |__LoggerFactory.java
 |                          |__LogLevel.java
 
-- logger/module-info.java

module logger {
	exports com.kurumsaljava.logger.api;	
}

Modüler uygulamamızı koştururken –module-path parametresi dikkatinizi çekmiş olabilir. Aynı şekilde modülleri derlerken –module-source-path parametresini kullanmıştık. Java’nın yeni modül sistemi classpath yerine modülü oluşturan sınıfları yüklemek için module-path dizin yapısını kullanmaktadır. Module path bir ya da daha fazla ve derlenen modülleri ihtiva eden dizinlerden oluşmaktadır. Derleme işlemi esnasında modüle ait sınıflar module-source-path ile tanımlanmatadır. Modüler bir uygulama koşturulduğunda, modülün module-path içinde bulunan ilk sürümü kullanılmakta ve diğerleri göz ardı edilmektedir. Module-path ve classpath birlikte kullanılabilmektedir.

Oluşturulan modülleri jar dosyalarında tutmak münkün. Lakin her modül için ayrı bir jar dosyası oluşturulması gerekmektedir. Modüler uygulamamiz için aşağidaki şekilde modül jar dosyaları oluşturabilir ve bu jar dosyaları aracılığı ile uygulamamızı koşturabiliriz.

PS C:\Users\acar\eclipse-workspace-java9\modules> jar --create --file module/application.jar --main-class com.kurumsaljava.application.Main -C classes/application .
PS C:\Users\acar\eclipse-workspace-java9\modules> jar --create --file module/logger.jar -C classes/logger .
PS C:\Users\acar\eclipse-workspace-java9\modules> jar --create --file module/logger.jar -C classes/logger .
PS C:\Users\acar\eclipse-workspace-java9\modules> java --module-path module -m application/com.kurumsaljava.application.Main
INFO - Hello World

Oluşturdugumuz modüller arasındaki bağımlılıkları şu şekilde kontrol edebiliriz:

PS C:\Users\acar\eclipse-workspace-java9\modules> jdeps --module-path modules module/*.jar
application
 [file:///C:/Users/acar/eclipse-workspace-java9/modules/module/application.jar]
   requires mandated java.base (@9.0.4)
   requires logger
application -> java.base
application -> logger
   com.kurumsaljava.application                       -> com.kurumsaljava.logger                            logger
   com.kurumsaljava.application                       -> java.lang                                          java.base
logger
 [file:///C:/Users/acar/eclipse-workspace-java9/modules/module/logger.jar]
   requires mandated java.base (@9.0.4)
logger -> java.base
   com.kurumsaljava.logger                            -> java.io                                            java.base
   com.kurumsaljava.logger                            -> java.lang                                          java.base
   com.kurumsaljava.logger                            -> java.lang.invoke                                   java.base
PS C:\Users\acar\eclipse-workspace-java9\modules>

Görüldügü gibi application.jar dosyasında yer alan modül logger modülü yanı sıra java.base modülüne bağımlıdır. Aynı şekilde logger modülü de java.base modülüne bağımlıdır. Burada Java bünyesinde yer alan sınıfların da artık modüler bir yapıda olduğunu görmekteyiz. Sadece ihtiyaç duyulan sınıfların yer aldığı modülleri kullanarak, Java uygulamalarını daha az hafıza ihtiyacı duyan Java ortamlarında koşturmak mümkün hale gelmiştir. Tüm java modüllerine şu sekilde erişebiliriz:

PS C:\Users\acar\eclipse-workspace-java9\modules> java --list-modules
java.activation@9.0.4
java.base@9.0.4
java.compiler@9.0.4
java.corba@9.0.4
java.datatransfer@9.0.4
java.desktop@9.0.4
java.instrument@9.0.4
java.logging@9.0.4
java.management@9.0.4
java.management.rmi@9.0.4
java.naming@9.0.4
java.prefs@9.0.4
java.rmi@9.0.4
java.scripting@9.0.4
java.se@9.0.4
java.se.ee@9.0.4
java.security.jgss@9.0.4
java.security.sasl@9.0.4
java.smartcardio@9.0.4
java.sql@9.0.4
java.sql.rowset@9.0.4
java.transaction@9.0.4
java.xml@9.0.4
java.xml.bind@9.0.4
java.xml.crypto@9.0.4
java.xml.ws@9.0.4
java.xml.ws.annotation@9.0.4
jdk.accessibility@9.0.4
jdk.attach@9.0.4
jdk.charsets@9.0.4
jdk.compiler@9.0.4
jdk.crypto.cryptoki@9.0.4
jdk.crypto.ec@9.0.4
jdk.crypto.mscapi@9.0.4
jdk.dynalink@9.0.4
jdk.editpad@9.0.4
jdk.hotspot.agent@9.0.4
jdk.httpserver@9.0.4
jdk.incubator.httpclient@9.0.4
jdk.internal.ed@9.0.4
jdk.internal.jvmstat@9.0.4
jdk.internal.le@9.0.4
jdk.internal.opt@9.0.4
jdk.internal.vm.ci@9.0.4
jdk.jartool@9.0.4
jdk.javadoc@9.0.4
jdk.jcmd@9.0.4
jdk.jconsole@9.0.4
jdk.jdeps@9.0.4
jdk.jdi@9.0.4
jdk.jdwp.agent@9.0.4
jdk.jlink@9.0.4
jdk.jshell@9.0.4
jdk.jsobject@9.0.4
jdk.jstatd@9.0.4
jdk.localedata@9.0.4
jdk.management@9.0.4
jdk.management.agent@9.0.4
jdk.naming.dns@9.0.4
jdk.naming.rmi@9.0.4
jdk.net@9.0.4
jdk.pack@9.0.4
jdk.policytool@9.0.4
jdk.rmic@9.0.4
jdk.scripting.nashorn@9.0.4
jdk.scripting.nashorn.shell@9.0.4
jdk.sctp@9.0.4
jdk.security.auth@9.0.4
jdk.security.jgss@9.0.4
jdk.unsupported@9.0.4
jdk.xml.bind@9.0.4
jdk.xml.dom@9.0.4
jdk.xml.ws@9.0.4
jdk.zipfs@9.0.4

Aşağıdaki şekilde sadece uygulamamızı ve ihtiyaç duyduğumuz Java modüllerini ihtiva eden bir Java sürümü oluşturabiliriz:

PS C:\Users\acar\eclipse-workspace-java9\modules> jlink --module-path "%JAVA_HOME%/jmods;module" --add-modules application --output distribution
PS C:\Users\acar\eclipse-workspace-java9\modules> .\distribution\bin\java -m application
INFO - Hello World

Jlink ile oluşturduğumuz Java sürümü sadcee 35MB büyüklügünde olup, uygulamamızı ihtiva etmekle birlikte, onu koşturmak için gerekli tüm yeteneklere sahiptir. Bunu 300MB büyüklügünde olan JDK9 sürümü ile kıyasladığımızda, Java ile modüler yazılımın ve Java’nın sahip olduğu kendi modüler yapının Java 9 ile yapılacak yeni projelerde hafıza kullanımını ne oranda düşürebileceği görülmektedir.

Modül tanımlayıcılarının (module descriptor) içeriğine aşağidaki şekilde ulaşmak mümkündür. Bu kullanmak istediğimiz modüllerin kullanım arayüzleri hakkında fikir edinmemizi kolaylaştıracaktır.

PS C:\Users\acar\eclipse-workspace-java9\modules> jar --describe-module --file=module/logger.jar
logger jar:file:///C:/Users/acar/eclipse-workspace-java9/modules/module/logger.jar/!module-info.class
exports com.kurumsaljava.logger
requires java.base mandated

PS C:\Users\acar\eclipse-workspace-java9\modules> jar --describe-module --file=module/application.jar
application jar:file:///C:/Users/acar/eclipse-workspace-java9/modules/module/application.jar/!module-info.class
requires java.base mandated
requires logger
contains com.kurumsaljava.application
main-class com.kurumsaljava.application.Main

PS C:\Users\acar\eclipse-workspace-java9\modules>

Son olarak servis modüllerinden bahsetmek istiyorum. Şimdiye kadar incelemiş olduğumuz modül sisteminde bir sorun mevcut. Logger modülünün birden fazla implementasyonu düşünülebilir. Örneğin bir RemoteLoggerImpl sınıfı ile logları bir sunucuya gönderebiliriz. Bu durumda ikinci bir modül oluşturarak, Logger sınıfını yeniden implemente etmemiz gerekmektedir. Lakin bu durumda modül kullanıcısının bağımlılık sayısı artacaktır. İncelediğimiz örnekte application modülü logger modülünü kullanmaktadır. İsmi remotelogger olan yeni bir modül oluşturduğumuz taktirde, application modülü bu yeni modülü kullanmak isteğinde, modül tanımlayıcısı şöyle bir yapıya bürünecektir:

-- application/module-info.java

module application {
	requires logger;
	requires remotelogger;
}

Oluşturacağımız her yeni logger implementasyonu için bu liste uzayıp, gidecektir. Modüller arasında bu şekilde bir bağımlılık oluşturmak, modüllerin bakımını ve geliştirilmesini zora somaktadır. Hizmet kullanan (consumer) ve hizmet sunan (provider) modülleri birbirlerine bağımlı olmayacak şekilde ayırt etmemiz gerekmektedir. Aksi taktirde bu bağımlılıkların yönetimi imkansiz hale gelecektir. Bunu sağlamak için servis katalog (service catalog) ya da servis kayıt merkezi (service registry) yapısını kullanabiliriz.

Servis kayıt merkezini hizmet alan ve hizmet sunan modüller arasında bir aracı katman olarak düşünebiliriz. Hizmet sunan modüller servis kayit merkesine başvurarak, sundukları hizmeti kaydettirirler. Aynı şekilde hizmet almak isteyen modüller de servis kayıt merkezine başvurarak, istedikleri servisi edinmeye çalışırlar. Burada ki en önemli nokta, hizmet sunan ve hizmet alan modüllerin birbirlerini tanımamalarıdır ve daha önce bahsetmiş olduğum bağımlılığın oluşmamasıdır. Modüller sadce servis kayıt merkezini tanırlar ve işlemleri onun aracılığı ile gerçekleştirirler.

Şimdi logger modülünü iki değişik implementasyon sunacak şekilde yeniden oluşturalım ve application modülünün bu iki modüle bagımlı olmadan, mevcut logger implementasyonları servis kayıt merkezi aracılığı ile nasıl kullanabileceğini bir örnek üzerinde inceleyelim.

Logger modülünü soyut olarak yani sadece api sınıflarını ihtiva edecek şekilde şu şekilde tanımlayabiliriz:

src
 |__logger
 |     |__module-info.java
 |     |__com
 |         |_kurumsaljava
 |                |__logger
 |                       |__api
 |                          |__Logger.java 
 |                          |__LogLevel.java
 
-- logger/module-info.java

module logger {
	exports com.kurumsaljava.logger.api;	
}

-- Logger.java

package com.kurumsaljava.logger.api;
public interface Logger {
	
	public void log(LogLevel level, String log);

}

Akabinde stdout ve remote logger implementasyonlarını yeni iki modül bünyesinde aşağıdaki şekilde implemente edebiliriz:

src
 |__logger
 |     |__module-info.java
 |     |__com
 |         |_kurumsaljava
 |                |__logger
 |                        |__api
 |                           |__Logger.java 
 |                           |__LogLevel.java
 |__remotelogger
 |     |__module-info.java
 |     |__com
 |          |_kurumsaljava
 |                |__logger
 |			           |__remote
 |                          |__RemoteLoggerImpl.java 
 |__stdoutlogger
 |     |__module-info.java
 |     |__com
 |         |_kurumsaljava
 |                |__logger
 |			            |__stdout
 |                           |__StdoutLoggerImpl.java 
 
-- remotelogger/module-info.java

module remotelogger {
	requires logger; 
	provides com.kurumsaljava.logger.api.Logger with com.kurumsaljava.logger.remote.RemoteLoggerImpl;
}

-- stdoutlogger/module-info.java

module stdoutlogger {
	requires logger; 
	provides com.kurumsaljava.logger.api.Logger with com.kurumsaljava.logger.stdout.StdoutLoggerImpl;
}

Modül tanımlayıcısı bünyesinde provides direktifi ile bir modül başka bir modül tarafından tanımlanmış ve export edilmiş bir api sınıfını implemente edebilmektedir. Provides ile oluşturduğumuz implementasyonlar servis kayıt merkezinde Logger sınıfı için iki servis implementasyonu kaydı oluşturacaktır. Kısaca iki yeni modül “biz bu servisi sağlayabiliyoruz” şeklinde bir açıklamada bulunarak, gerekli servis kayıtlarının oluşturulmasını sağlamaktadırlar. Peki bu modüllere doğrudan bir bağımlılık oluşturmadan, servis implementasyonlarını nasıl kullanabiliriz? Şu şekilde:

src
 |__logger
 |     |__module-info.java
 |     |__com
 |         |_kurumsaljava
 |                |__logger
 |			            |__api
 |                          |__Logger.java 
 |                          |__LogLevel.java
 |__remotelogger
 |     |__module-info.java
 |     |__com
 |         |_kurumsaljava
 |                |__logger
 |		            	|__remote
 |                          |__RemoteLoggerImpl.java 
 |__stdoutlogger
 |     |__module-info.java
 |     |__com
 |         |_kurumsaljava
 |                |__logger
 |			            |__stdout
 |                          |__StdoutLoggerImpl.java 
 |__application
 |     |__module-info.java
 |     |__com
 |         |_kurumsaljava
 |                |__application
 |				         |__Main 

-- application/module-info.java

module application {
	requires logger;
	uses com.kurumsaljava.logger.api.Logger;
}

-- Main

package com.kurumsaljava.application;

import java.util.ServiceLoader;

import com.kurumsaljava.logger.api.Logger;
import com.kurumsaljava.logger.api.LogLevel;

public class Main {

	public static void main(String[] args) {
		Iterable<Logger>  services = ServiceLoader.load(Logger.class);		
		for(Logger logger: services) {
			logger.log(LogLevel.DEBUG, "Hello World");			
		}
	}
}

Application isimli modül modül tanımlayısı bünyesinde uses direktifi ile hangi modülü kullanmak istediğini ifade etmektedir. Bu Logger soyut sınıfını baz alan soyut bir tanımlamadır. Application modülü kesinlikle somut bir sınıf ya da modül implementasyonuna işaret etmemektedir. Bu şekilde modüller arasındaki bağı daha esnek hale getirmek ve modüller arası alışverişi servis kayıt merkezi aracılığıyla yürütmek mümkün hale gelmektedir.

Mevcut servis implementasyonlarına erişim yani servis kayıt merkezine başvuru ServiceLoader sınıfı aracılığı ile gerçekleşmektedir. Main örneginde Logger.class sınıfını implemente eden modüller load() metodu aracılığı ile servis kayıt merkezinden alınmakta ve bu şekilde arzu edilen servis modül bünyesindeki tanımlı kullanıcı arayüzü aracılığı ile koşturulabilmektedir. ServiceLoader örneğinde görüldüğü gibi LoggerFactory gibi bir sınıfın kullanımı gereksiz hale gelmiştir, çünkü factory (bknz. factory tasarım şablonu) görevini servis kayıt merkezi üstlenmektedir.

Java, 9 sürümü ile gerçek anlamda modüler uygulamaların oluşturulması mümkün kılan bir araç haline gelmiştir. Yeni uygulamaların bu avantajı kullanabilmeleri ümidiyle …


EOF (End Of Fun)
Özcan Acar

Share Button
0.00 avg. rating (0% score) - 0 votes

4 Comments

  • Alkan

    04 Şubat 2018

    Teşekürler Hocam

  • Yusuf ÇAKAL

    04 Şubat 2018

    Yine kalite kokan bir yazı olmuş hocam elinize sağlık.

  • Yusuf SEZER

    15 Haziran 2018

    Doyurucu bir makale, programlama dillerinin Node.js den baya etkilendiğini düşünüyorum (.Net core middleware, Java 9 REPL ve modül yapısı)

  • Yücel

    12 Nisan 2021

    Eline emeğine sağlık hocam.

Bir cevap yazın