Elosztott objektumorientált alkalmazásmodellek

Paller Gábor

1. Elosztott alkalmazásarchitektúrák

Manapság nem lehet olyan alkalmazást írni, ami ne igényelne valamilyen hálózati kommunikációt. A kommunikációt sokféle szinten lehet végezni. A magas szintű kommunikációs keretrendszerekben általában a következő elemek találhatók:
A Jáva a következő magas szintű kommunikációs keretrendszereket támogatja.
Ebben a fejezetben az első ponttal foglalkozunk. Mint később látni fogjuk, az EJB még egy ennél is magasabb absztrakció, ami még azt is eltakarja, hogy az alkalmazás esetleg több gépen szétszórva lehet. Az EJB szinte biztosan felhasznál elosztott objektumorientált megoldást.

Az alábbi képen az elosztott objektumorientált modellek általános elemei láthatók.
Elosztott objektumorientált modellek általános elemei

A következőkben a Jáva által kínált négy elosztott objektumorientált alkalmazásmodellt tekintünk át.

2. Remote Method Invocation (RMI)

Az RMI a Jáva első elosztott objektumorientált modellje. A JDK 1.1 óta része minden sztenderd Jáva környezetnek, kicsi és programozni egyszerű. Általános elterjedtsége és egyszerű használata miatt ez a legnépszerűbb modell jelenleg.

Az RMI egy saját protokollt használ, amelyik a CORBA GIOP-ra hasonlít. A protokollüzenetben a meghívandó objektum szerveroldali csonkjának referenciája, a meghívandó metódus neve és a szerializált paraméterek vannak.A szerializáció az java.io.ObjectInputStream és java.io.ObjectOutputStream formátumát használja. A szerializált paraméterek magukkal viszik a típusukat (osztályukat) is és az osztály annotálva lehet azzal az URL-lel, ahonnan az osztály letölthető. Ez lehetővé teszi a paraméterekhez tartozó osztályok dinamikus letöltését: ha egy paraméter típusa pl. example.MyClass, az RMI üzenet fogadójának szüksége van erre a .class fájlra. Ha ez a lokális CLASSPATH-ről nem elérhető, megnézi az URL-t az osztály annotációjában található URL-t és letölti onnan. Ez a dinamikus RMI osztályletöltés. Az osztály birtokában aztán a paraméterobjektum helyreállítható a távoli gépen (szerver a kérésnél, kliens a válasznál). A dinamikus osztályletöltés biztonsági problémákat vethet fel, mert pl. egy rosszindulatú kliens olyan paramétertípust adhat át, amelynek letöltendő osztályába gonosz dolgokat művelő konstruktort tehet. Amikor a szerver létrehozza a paraméterobjektumot, a konstruktor letöltődik és lefut a szerveroldalon. Nyilván ilyen biankó csekket nem akarunk adni. A dinamikus osztályletöltés csak akkor engedélyezett, ha az RMISecurityManager aktív. A programozó leszármaztathat az RMISecurityManager-től, hogy saját biztonsági szabályait megvalósíthassa.

Az RMI szolgáltatásleírásra magát a Jávát használja. Az RMI interfészeket Jáva interfészosztályok írják le. Habár az RMI elvileg alkalmas nem Jáva kliensekkel/szerverekkel való együttműködésre, valójában egy nem Jáva kliens/szerver implementálása nagyon nehéz lenne. Én nem tudok esetről, amikor az RMI-t nem Jávás kommunikációs partnerrel együtt használták volna.

Az RMI egy nagyon egyszerű katalógust használ, ezt registry-nek hívják. Az RMI registry-ben URL-hez lehet regisztrálni távoli objektum referenciákat. A registry használója pl. megszerezheti a "/merohely/15"  névhez tartozó távoli objektum referenciát.

Nézzünk most egy egyszerű példát. Először a távoli objektum Jáva interfészét kell megvalósítsuk.

Adder.java:

import java.rmi.Remote;
import java.rmi.RemoteException;

public interface Adder extends Remote {
  int add( int i1, int i2 ) throws RemoteException;
}


A távoli objektumnak egy metódusa van. Ennek RemoteException-t kell dobnia, mert a távoli metódus hívása esetleg hibás is lehet (pl. nem sikeres a kommunikáció a szerverrel). A távoli  interfészt implementálni kell.

AdderImpl.java:

import java.rmi.Naming;
import java.rmi.RemoteException;
import java.rmi.RMISecurityManager;
import java.rmi.server.UnicastRemoteObject;
   
public class AdderImpl extends UnicastRemoteObject implements Adder {
   
public AdderImpl() throws RemoteException {
  super(); // Inicializáld az UnicastRemoteObject-et
}
   
public int add( int i1, int i2 ) {
  return i1+i2;
}
   
public static void main( String args[] ) {

// RMISecurityManager-t beállítja, különben a dinamikus osztálybetöltés
// nem működik.
  if( System.getSecurityManager() == null ) {
    System.setSecurityManager( new RMISecurityManager() );
  }
   
  try {
// Ez maga a szerveroldali objektumegyed
    AdderImpl obj = new AdderImpl();
// A registry-ben a /Adder névhez rendeljük
    Naming.rebind( "/Adder", obj );
    System.out.println( "AdderServer bound in registry" );
  } catch (Exception e) {
    System.out.println( "AdderImpl err: " + e.getMessage() );
    e.printStackTrace();
  }
// A program tovább fut, mert az UnicastRemoteObject szálakat indított
}

}


Ez az objektum definiálja magát a távolról elérhető metódust. Érdekes, hogy ez a metódus nem dob RemoteException-t, hiszen ez nem végez hálózati kommunikációt. RemoteException-t a csonkok dobhatnak.

Ezek után nézzük a klienst, ami használja a távoli objektumot.

import java.rmi.Naming;
import java.rmi.RemoteException;

public class AdderClient {

public static void main( String args[] ) {
// Távoli objektum referencia, igazabol a csonkra mutat
  Adder obj = null;

  try {
// A távoli objektum referenciáját a registry-ből vesszük
    obj = (Adder)Naming.lookup( "/Adder" );
    int result = obj.add( 23,34 );
    System.out.println( "Result: "+result );
  } catch (Exception e) {
     System.out.println( "AdderClient exception: " + e.getMessage() );
     e.printStackTrace();
  }
}

}

A Jáva források lefordítása után a csonkokat is generálnunk kell az rmic programmal, amit az objektum implementációján futtatunk (rmic AdderImpl). Ezek után a szerver elindítható.

java -Djava.rmi.server.codebase=file:///mydir/rmi/  -Djava.security.policy=file:///mydir/rmi/rmi.policy AdderImpl

Minthogy a dinamikus osztálybetöltést engedélyeztük (RMISecurityManager), meg kell adnunk a letöltési kódbázist és a policy-t is. Nincs időnk most kitérni a Jáva policy megoldására, a legegyszerűbb policy a következő (mindenkinek mindent megenged).

grant {
  permission java.security.AllPermission;
};


Az RMI nagyon egyszerű, de eléggé korlátozott. Az RMI szerver teljesítménye viszonylag kicsi és nem skálázódik (RMI szerverekből nem lehet szervercsoportot (cluster) szervezni). RMI praktikusan csak Jáva kliens/szerver között használható.

3. Java-IDL

Az legnépszerűbb elosztott objektumorientált alkalmazásmodel a CORBA. A CORBA különböző programozási nyelveken írt objektumokat képes összekötni. Ehhez két eszköze van:
CORBA architektúra
Az ORB és az objektumimplementáció közötti interfészt szabványosítani kell, különben egyik ORB-ra írt programok nem fognak futni egy másik ORB-on. Ezt az interfészt minden nyelvre megcsinálják. A Jáva leképezést Java-IDL-nek hívják. A Java-IDL tehát CORBA Jávából való használatának szabványos módja.

Az első lépés a távoli interfész megírása IDL nyelven. Példa:

AdderInterface.idl:
// IDL interface for the adder object
interface AdderInterface {
    long add( in long n1, in long n2 );
};

Megjegyzendő, hogy az IDL long típusa a 32 bites előjeles egész számot jelenti, ami a Jávában int-nek felel meg. Az IDL-t az ORB-hoz tartozó eszköz dolgozza fel, amely az ORB-hez illeszkedő csonkokat és egyéb segítő osztályokat gyárt belőle. Ezeknek nem kell szabványosaknak lennie, hiszen egy másik ORB-hez újragenerálhatók az IDL-ből.

Nézzük most a szerver kódot!

import org.omg.CosNaming.*;
import org.omg.CosNaming.NamingContextPackage.*;
import org.omg.CORBA.*;
import org.omg.PortableServer.*;
import org.omg.PortableServer.POA;

// Servant az objektum, ami a távoli interfészt implementálja.
// CORBA szerver annyi servant-et gyárt, amennyire szüksége van
// Az AdderInterfacePOA interface-t az IDL fordító generálta
class AdderServant extends AdderInterfacePOA {
// Methods from the IDL
  public int add(int i1, int i2)  {
    return i1 + i2;
  }
}

// Ez az alkalmazás, ami létrehozza és regisztálja az objektumot az ORB-nál
public class AdderServer {
    public static void main(String args[]) {
        try {   
            ORB orb = ORB.init( args, null);
            POA rootpoa = POAHelper.narrow( orb.resolve_initial_references( "RootPOA" ) );
            rootpoa.the_POAManager().activate();

// Létrehozza a servant-et és regisztrálja az ORB-nál
            AdderServant adderimpl = new AdderServant();

            org.omg.CORBA.Object ref = rootpoa.servant_to_reference( adderimpl );
            AdderInterface href = AdderInterfaceHelper.narrow(ref);
     
// Regisztrálja az objektumot a katalógusban. Először lekéri a katalógus gyökeréhez
// tartozó objektumot, aztán regisztrálja az objektumot.
            org.omg.CORBA.Object objRef = orb.resolve_initial_references("NameService");
            NamingContextExt ncRef = NamingContextExtHelper.narrow( objRef );

// Az "Adder" névhez köti a távoli objektumot
            String name = "Adder";
            NameComponent path[] = ncRef.to_name( name );
            ncRef.rebind(path, href);

            System.out.println("AdderServer ready and waiting ...");

// Végtelen ciklus, kérésekre vár
            orb.run();
        } catch ( Exception e ) {
          System.err.println("ERROR: " + e);
          e.printStackTrace(System.out);
        }
        System.out.println("AdderServer Exiting ...");
    }
}

Az RMI-vel összehasonlítva feltűnhet, hogy a CORBA felkészült arra, hogy a távoli objektumot több példányban, esetleg több gépen kell létrehozni. Ez is azt mutatja, hogy a CORBA-t sokkal magasabb követelmények kielégítésére tervezték, mint az RMI-t. Természetesen a valódi minőségi jellemzők az ORB-tól függenek, a JDK-ba egy nagyon egyszerű ORB van beépítve.

A CORBA-nak nincs az RMI dinamikus osztályletöltéshez hasonlító metódusa. Erre az egységesített CORBA típusrendszer miatt nincs szükség, mert a CORBA absztrakt típusokból a paraméterek és visszatérési értékek típusa generálható, a típusok ekvivalensek lesznek a hívó és hívott oldalán, még ha esetleg nem is pontosan ugyanazok. Az ORB-vel kapcsolatot tartó osztályokat sem lehet letölteni, mert ezek ORB-ként különbözhetnek.

A CORBA sztenderd katalógusszolgáltatását CosNaming-nek hívják, ami maga is egy CORBA objektum. Az RMI registry-hez képest a legnagyobb különbség, hogy a CosNaming hierarchikus névstruktúrával rendelkezik, hasonlatosan pl. egy fájlrendszer könyvtáraihoz és a fájlaihoz.

Lássuk most a klienst!

import org.omg.CosNaming.*;
import org.omg.CosNaming.NamingContextPackage.*;
import org.omg.CORBA.*;

public class AdderClient {

  public static void main(String args[]) {
      try{
// Az ORB létrehozása és inicializálása
        ORB orb = ORB.init(args, null);

// CosNaming objektum megszerzése
        org.omg.CORBA.Object objRef =   orb.resolve_initial_references( "NameService" );
        NamingContextExt ncRef = NamingContextExtHelper.narrow(objRef);
 
// Megszerzi az távoli objektum kliensoldali csonkját a katalógusból
        String name = "Adder";
        AdderInterface adderimpl = AdderInterfaceHelper.narrow(ncRef.resolve_str(name));

        System.out.println("Obtained a handle on server object: " + adderimpl);
        System.out.println( "add: "+adderimpl.add( 1,2 ) );
    } catch (Exception e) {
        System.out.println("ERROR : " + e) ;
        e.printStackTrace(System.out);
    }
  }
}

A Java-IDL nyilvánvalóan a bonyolultabb rendszer. A legnagyobb előnye egyben a legnagyobb hátránya is: a fejlesztőknek új nyelvet (IDL) kell megtanulniuk és bonyolult adattípusok IDL-re való leképezése nem egyszerű feladat. Cserébe a Java-IDL jóval nagyobb skálázhatóságot és stabilitást ad és lehetővé teszi, hogy a kommunikáló feleket különböző nyelveken implementáljuk. A sokféle ORB implementáció közül mindenféle igényt kielégítőt lehet találni és maga a JDK is tartalmaz egy kis ORB-t.

4. RMI-IIOP

A Java-IDL-ben IDL-lel kell dolgozni és ez jelentősen csökkenti a vonzerejét. A CORBA stabilitását és skálázhatóságát és az RMI egyszerűségét az RMI-IIOP-ben ötvözték. Az RMI-IIOP az RMI programozási modellt ülteti a CORBA tetejére és igyekszik a CORBA-ból minnél több részletet elrejteni.

A távoli interfészt az RMI-hez hasonlóan definiáljuk, az IDL-lel nem szükséges foglalkozni.

AdderInterface.java
import java.rmi.Remote;
public interface AdderInterface extends java.rmi.Remote {
public int add( int i1, int i2 ) throws java.rmi.RemoteException;
}
A távoli objektum implementációja valójában a CORBA servant.

AdderImpl.java:
import javax.rmi.PortableRemoteObject;

public class AdderImpl extends PortableRemoteObject implements AdderInterface {
   public AdderImpl() throws java.rmi.RemoteException {
       super();     // Inicializálja a PortableRemoteObject-et
   }

   public int add( int in1, int in2 ) throws java.rmi.RemoteException {
      return in1+in2;
   }
}

Továbbra is szükségünk van egy szerveralkalmazásra, amely a távoli objektumot létrehozza és bejegyzi a katalógusban. Az RMI-IIOP elfedi a CosNaming katalógust a jól ismert JNDI API-jal.

AdderServer.java:
import javax.naming.InitialContext;
import javax.naming.Context;

public class AdderServer {
    public static void main(String[] args) {
        try {
  // Létrehozza a servant-et
            HelloImpl helloRef = new HelloImpl();

 // A JNDI-t használja az objektum regisztrálására       
            Context initialNamingContext = new InitialContext();
            initialNamingContext.rebind("AdderService", helloRef );
            System.out.println("Adder server: Ready...");
         } catch (Exception e) {
            System.out.println("Trouble: " + e);
            e.printStackTrace();
         }
     }
}

A kliens is a JNDI-t használja, amikor megszerzi a távoli objektum referenciáját.

AdderClient.java:
import java.rmi.RemoteException;
import java.net.MalformedURLException;
import java.rmi.NotBoundException;
import javax.rmi.*;
import java.util.Vector;
import javax.naming.NamingException;
import javax.naming.InitialContext;
import javax.naming.Context;

public class AdderClient {

    public static void  main( String args[] ) {
        Context ic;
        Object objref;
        AdderInterface ai;

        try {
            ic = new InitialContext();
        
// JNDI-t használja a távoli objektum csonkjának megszerzésére
            objref = ic.lookup("AdderService");
            System.out.println("Client: Obtained a ref. to Adder server.");

// Megfelelő típusúra alakítja a referenciát és meghívja a metódust
            ai = (AdderInterface) PortableRemoteObject.narrow(
                objref, AdderInterface.class);
            int result = ai.add( 3,7 );
            System.out.println( "Result: "+result );
        } catch( Exception e ) {
            System.err.println( "Exception " + e + "Caught" );
            e.printStackTrace( );
            return;
        }
    }
}

Az RMI-IIOP előnye, hogy a CORBA infrastruktúrát úgy használja fel, hogy közben annak bonyolultságát nagyban elrejti. Ugyan az RMI-IIOP CORBA típusleképezést végez, nem lehet garantálni, hogy egy létező CORBA interfésszel megegyező IDL-t generáljon. Az rmic eszköz viszont megkérhető, hogy IDL-t generáljon egy új interfészből, tehát nem Jáva rendszerrel való kapcsolat is megoldható.

5. JAX-RPC

Az Internet szükségessé tette az Internet infrastruktúrán hatékonyan futtatható, interoperábilis elosztott alkalmazásmodel kifejlesztését. Különböző, csak részben műszaki okok miatt egy teljesen új keretrendszert fejlesztettek erre, ez a web services architektúra (a webszolgáltatás nem túl jó név, jobb híján az angol nevet fogom használni). A web services modell három kulcstechnológiából áll.

Ezen írás kereteit meghaladja a web services protokollok ismertetése. Itt csak egy egyszerű SOAP RPC példát mutatok be. Ez jó barátunk, az add szolgáltatás hívását mutatja be. A párbeszéd HTTP tetején zajlik.

Íme a kérés:
<?xml version="1.0" encoding="UTF-8"?>
<soapenv: Envelope xmlns:soapenv=http://schemas.xmlsoap.org/soap/envelope/
  xmlns:xsd=http://www.w3.org/2001/XMLSchema
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <soapenv:Body>
    <ns1:add soapenv:encodingStyle=http://schemas.xmlsoap.org/soap/encoding/ xmlns:ns1="urn:Calculator">
      <ns1:arg0 xsi:type="xsd:int">3</ns1:arg0>
      <ns1:arg1 xsi:type="xsd:int">4</ns1:arg1>
    </ns1:add>
  </soapenv:Body>
</soapenv:Envelope>

És a válasz:
<?xml version="1.0" encoding="UTF-8"?>
<soapenv:Envelope xmlns:soapenv=http://schemas.xmlsoap.org/soap/envelope/
  xmlns:xsd=http://www.w3.org/2001/XMLSchema

  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <soapenv:Body>
    <ns1:addResponse soapenv:encodingStyle=http://schemas.xmlsoap.org/soap/encoding/ xmlns:ns1="urn:Calculator">
      <ns1:addReturn xsi:type="xsd:int">7</ns1:addReturn>
    </ns1:addResponse>
  </soapenv:Body>
</soapenv:Envelope>

A Jáva több web services-szel kapcsolatos API-t nyújt. SOAP with Attachments (SAAJ) dokumentum stílusú SOAP üzenetek feldolgozására való, a Java API for XML Registries (JAXR) pedig a katalógus elérésére való. A lecke témájához igazodva itt csak a kérés-válasz API-t mutatom be, ez a JAX-RPC.

A JAX-RPC modellben ugyanazt kell tenni, mint az eddigi modellekben: távoli interfészt deklarálni, megvalósítani annak implementációját, majd meghívni azt a kliensből. Az egyetlen (habár drámai) kivétel, hogy a JAX-RPC jelenlegi implementációi nem képesek a katalógust használni. Ez azért van, mert a WSDL fájlokat a JAX-RPC implementációk kapásból nem képesek felhasználni, csak előfordítás után, Jáva interfészeket pedig nem lehet az UDDI katalógusban tárolni. Ez sajnos azzal jár, hogy a JAX-RPC kliensekben általában benne van a szerver címe (vagy legalábbis nem a katalógusból veszik).

A távoli interfész és az implementáció nem tartogat meglepetéseket. A távoli interfész:

import java.rmi.Remote;
import java.rmi.RemoteException;

public interface AdderIF extends Remote {
    public int add( int in1, int in2 ) throws RemoteException;

}


És az implementáció:

public class AdderImpl implements AdderIF {
    public int add( int in1, in2 ) {
       return in1+in2;
    }
}

Ezek után az implementációtól függő eszközökkel "összehozzák" ezt a két osztályt egy szervlet-képes webszerverrel. Ez változatos módokon történhet, az Apache Axis esetén egyszerűen odaadják a Jáva forrást a JAX-RPC keretrendszernek és az legyártja a szükséges interfészosztályokat. A Sun JAX-RPC megoldása WAR fájlt gyárt, amelyet bármely szervlet konténerre telepíteni lehet. Ugyanezek az eszközök legyártják a kliens részére is a szükséges illesztőosztályokat. Mint előbb láthattuk, maga a JAX-RPC nem biztosítja ezen osztályok dinamikus letöltését, ezeket be kell építeni a kliensbe. A kliens ezeket használja fel, amikor meghívja a szolgáltatást.

public class AdderClient {
    private String endpointAddress;

    public static void main(String[] args) {
// A szerver címe indítási paraméter
        System.out.println( "Szerver cím = " + args[0]);
        try {
            Stub stub = createProxy();
            stub._setProperty
              (javax.xml.rpc.Stub.ENDPOINT_ADDRESS_PROPERTY, args[0]);
            AdderIF ai = (AdderIF)stub;
             int result = ai.add( 5,6 );
            System.out.println( "Result: "+return );
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }   

    private static Stub createProxy() {
        return (Stub)( new MyAdderService_Impl().getAdderIFPort() );
    }
}

A web services megoldás mögött felsorakozott az egész iparág, gyakorlatilag az egyetlen megoldás, ami pl. Jáva és Microsoft termékek összekötésére alkalmas. Ugyanakkor a SOAP meglehetősen bőbeszédű, ugyanaz az alkalmazás SOAP-ban 4-5-ször nagyobb forgalmat generálhat, mint CORBA-ban. A web services katalógusszolgáltatásának használata még nem terjedt el. A web services alkalmazásokhoz webszerver is kell, ez nagy forgalom esetén elég drága mulatság lehet. Ebben a pillanatban a web services megoldásokat különböző platformokon futó megoldások integrálására használják. Idő kell, hogy kiderüljön, képes-e beváltani az ígéreteit.