ClassLoadery

Wpis dodany 04 października 2009 o godz. 14:34:14 w kategorii Java, OSGI, Techblog.

ClassLoader to obiekt, który jest odpowiedzialny za ładowanie klas. Tak twierdzi javadoc w opisie do abstrakcyjnej klasy ClassLoader i z pewnością ma rację. W pisaniu prostych programów jakakolwiek wiedza na temat ładowania klas nie jest nam specjalnie potrzebna, nie ma jednak co ukrywać, że Java nie została stworzona do prostych programów, ale do szeroko rozumianego "enterpri(s|c)e". A tam tego typu wiedza jest nie tylko przydatna, ale wręcz konieczna.

Zróbmy więc małą powtórkę z tego, jak ładowanie klas działa i dlaczego, jeśli w ogóle, jest fajne. Otóż powodem powstania koncepcji classloaderów w Javie były aplety, które w swoim czasie miały podbić świat. Wyszło jak wyszło, ale classloadery zostały. Liczba mnoga jest jak najbardziej uzasadniona, gdyż do czynienia mamy co najmniej z trzema, są to bootstrap, extension i system.

Typ "bootrstrap" ładuje klasy najważniejsze, systemowe, zazwyczaj te z rt.jar, czyli choćby java.lang.String. Wywołując getClassLoader() dla String.class dostaniemy null. Typ "extension" szuka klas w jre/lib/ext, a dopiero typ "system" ładuje klasy naszej aplikacji. Oczywiście w najprostszym przypadku, bo mamy przecież serwery aplikacyjne i OSGI, ale o tym następnym razem (i o samym OSGI, i o ładowaniu klas w takim wypadku).

public class ClassloaderFun {
    public static void main(String[] args) {
        // dostajemy null
        out.println(String.class.getClassLoader());
        // dostajemy sun.misc.Launcher$AppClassLoader@7d772e
        out.println(ClassloaderFun.class.getClassLoader());
        out.println(Thread.currentThread().getContextClassLoader());
    }
}

Idźmy dalej, bowiem ładowanie klas nierozłącznie powiązane jest z hierarchią. Każdy classloader (poza typem "bootstrap") posiada jakiegoś przodka. Zazwyczaj zanim nasz classloader spróbuje załadować jakąś klasę, odpytuje najpierw właśnie owego przodka (wedle tzw. podejścia "parent first"). Każdy wątek posiada dostęp do kontekstowego classloadera, czyli tego, który aktualnie jest w użyciu. Zawsze możemy go podmienić za pomocą metody setContextClassLoader. Taka możliwość przydaje się choćby wtedy, kiedy chcemy, aby nowo tworzony wątek miał dostęp do klas ładowanych dynamicznie np. poprzez URLClassLoader. Warto jeszcze wspomnieć, że nie ma problemu z załadowaniem kilku różnych klas o tej samej nazwie zarówno samej klasy, jak i pakietu. Funkcję przestrzeni nazw pełnią tutaj właśnie classloadery.

        URL url = new URL("file:///tmp/plugin.jar");
        URLClassLoader pluginLoader = new URLClassLoader(new URL[] { url });
        Class clazz = pluginLoader.loadClass("net.miracki.classloader.plugin.PluginImpl");
        out.println("Loaded class: " + clazz + ", classloader " + clazz.getClassLoader());
        ((Plugin) clazz.newInstance()).doSomething();

Z tematem ładowania klas powiązany jest wybór implementacji dla różnych API. Istnieje mechanizm SPI, czyli Service Provider Interface, który dzięki odpowiednim znacznikom w strukturze pliku *.jar pozwala poinformować maszynę wirtualną, że implementacją chociażby javax.xml.parsers.SAXParserFactory jest w przypadku Xercesa org.apache.xerces.jaxp.SAXParserFactoryImpl. Dokładnie odbywa się to dzięki umieszczeniu w katalogu /META-INF/services/ pliku o nazwie interfejsu, a którego treścią jest nazwa implementacji.

Z SPI można korzystać albo i nie, na przykład bardzo popularna fasada do logowania, czyli commons-logging używa tego mechanizmu, ale robi też o wiele więcej, co niesie ze sobą różne skutki. Otóż domyślna fabryka (do wyboru implementacji której używa się właśnie SPI) do tworzenia loggerów, czyli klasa org.apache.commons.logging.impl.LogFactoryImpl dynamicznie (tak, to słowo klucz, a kawałki kodu poniżej) szuka odpowiedniego loggera. W szczegóły wgłębiać się nie będę, zwłaszcza, że zrobił to już świetnie w swoim artykule o problemach związanych z ładowaniem klas w commons-logging Ceki Gülcü, twórca m.in. Log4j. Problemy te zaowocowały stworzeniem nowej fasady do logowania, czyli SLF4J oraz następcy Log4j, czyli Logbacka. Poniżej wycinki z kodu commons-logging:

private static final String[] classesToDiscover = {
    LOGGING_IMPL_LOG4J_LOGGER,
    "org.apache.commons.logging.impl.Jdk14Logger",
    "org.apache.commons.logging.impl.Jdk13LumberjackLogger",
    "org.apache.commons.logging.impl.SimpleLog"
};
for(int i=0; (i<classesToDiscover.length) && (result == null); ++i) {
    result = createLogFromClass(classesToDiscover[i], logCategory, true);
}
                Class c = null;
                try {
                    c = Class.forName(logAdapterClassName, true, currentCL);
                } catch (ClassNotFoundException originalClassNotFoundException) {
                    // tu trochę kodu obsługującego wyjątek
                }

                constructor = c.getConstructor(logConstructorSignature);
                Object o = constructor.newInstance(params);

                if (o instanceof Log) {
                    logAdapterClass = c;
                    logAdapter = (Log) o;
                    break;
                }

To, na co trzeba zwrócić uwagę, to właśnie konsekwencje dynamicznego ładowania klas, które opisał Ceki Gülcü. Właściwie chociaż jego artykuł dotyczy commons-logging i wyboru implementacji loggerów, to jest świetnym źródłem wiedzy na temat ładowania klas. Dobitnie pokazuje, dlaczego nie warto kombinować tam, gdzie to jest zupełnie niepotrzebne. Zresztą nowa fasada Cekiego do logowania, czyli wspomniany już SLF4J, nie wyczynia cudów z classloaderami, wystarczy przejrzeć manual.

A jeśli już jesteśmy przy różnych issues w ładowaniu klas, to niedawno chwalony przeze mnie Stripes Framework też miał problem z classloaderami, gdy próbowano go użyć w środowisku OSGI. Wniosek jest więc taki, że Java i jej wirtualna maszyna pozwalają na wiele, tylko czasem warto przemyśleć konsekwencje swoich decyzji, na pierwszy rzut oka dynamiczne odnajdywanie loggerów też wydawało się być super.