Komponensek együttműködése webalkalmazás környezetben Jónás Richárd Debreceni Egyetem, Informatiai Intézet T-Soft Mérnökiroda KFT richard.jonas@tsoft.hu Kivonat Napjainkban az élet valamennyi szereplőjének előnye származik abból, ha az interneten tud közölni és fogadni információkat, majd ezen információkat jól szervezetten képes felhasználni. Ehhez elengedhetlen egy szoftver-infrastruktúra kidolgozása, amelyet ma a web-alkalmazások valósítanak meg. Ebből fakadóan a web-alkalmazások gyors és rövid életciklusokkal rendelkező szoftverfejlesztési folyamata elengedhetetlen ahhoz, hogy időben reagáljunk a követelményekre. A web-alkalmazások ilyen fejlesztését támogatja a komponens-alapú szoftverfejlesztés, amelynek számos ága van. Egyesek a komponensek általános reprezentációjával foglalkoznak, mások a komponensek felhasználásával elért üzleti profitot tartják szem előtt, emellett léteznek a módosíthatóságra kihegyezett komponens-architektúrák, stb. Cikkemben a komponensek együttműködését szeretném bemutatni címszavakban, majd megvizsgálni, hogy hogyan lehet az együttműködésből fakadó függéseket úgy finomítani, hogy az ne menjen a módosíthatóság rovására. Továbbá példát láthatunk arra, hogy hogyan lehet szeparálni egymástól a komponens felelősségét és a rendszer funkcionalitásait, ha szükség van rá. Ezt az aspektus-orientált programozás segítségével fogom bemutatni, példákon keresztül. 1. Bevezetés Napjainkban a társadalom informatizálása révén a szoftverfejlesztés még nagyobb jelentőséggel bír, mint eddig, hiszen az élet minden területén szükség van biztonságosan működő, dokumentált, fejleszthető szoftverre. Az objektum-orientált programozás olyan szoftverfejlesztési paradigma, amely megoldást adott a nagyméretű alkalmazások kezelhetőségét illetően, de nem oldott meg számos olyan problémát, amelyek az alkalmazás funkcionalitására vonatkoznak. Az objektumok a valós világ entitásait reprezentálják, tehát az objektum-orientált programozás az entitások felelősségét emeli ki. Egy alkalmazás viszont funkcionalitások összességeként 1
fogható fel, és legtöbb esetben egy objektum azért rendelkezik bizonyos metódussokkal, tulajdonságokkal, mert egy funkciót ez segít megvalósítani. Az alkalmazás életciklusa során előfordul, hogy az alkalmazást új funkciókkal kell kiterjeszteni, vagy meglévő funkciókat kell módosítani. Az alkalmazásban megjelenő funkciók több objektum, komponens, modul együttműködésének eredményeképpen jönnek létre, tehát ha módosítani kell egy funkciót, akkor legrosszabb esetben az együttműködésben résztvevő összes komponenst módosítani kell. Ez természetesen megnehezíti egy alkalmazás több fejlesztő által végzett párhuzamos fejlesztését. Cikkemben szeretnék bemutatni egy technológiát, amellyel a komponensek közötti kommunikáció összefogható, annak érdekében, hogy az könnyebben módosítható legyen. Továbbá megvizsgálom, hogy hogyan lehet a kommunikációt egységesíteni, így egyszerűbbé tenni. 2. Komponensek A komponensek jól elkülöníthető kódrészletek, amelyek önállóan fejleszthetők, dokumentálhatók, üzembehelyezhetők. Cikkemben elsősorban forráskódú komponensekkel (azon belül is Java nyelvű komponensekkel) foglalkozom, de vizsgálatra kerül a nem forráskodú komponensek közötti kommunikáció egyszerűsítése is. Ahhoz, hogy megértsük cikkemben mit értek a kommunikáció alatt, nézzünk meg egy példát. Tegyük fel, hogy van egy fejlesztendő szoftverünk, amelyben számos olyan funkciót valósítunk meg, amelyek az üzembe helyezett szoftver esetében nem kell, hogy működjenek. Ilyen például a naplózás (bizonyos esetekben az üzembehelyezett rendszernél is fontos ez a funkció), az úgynevezett elő- és utófeltétel vizsgálatok metódusok végrehajtása előtt és után, stb. Világos, hogy ezek a fejlesztés során fontosak és egy teszt után, egy működő rendszer esetén nem szükségesek, csak lassítanák a szoftvert. Nem nehéz belátni, hogy a naplózással foglalkozó hívások a teljes rendszer forrását áthatnák, így elég nagy energia lenne kitörölni ezeket a hívásokat a kódból, de ha még egy programozó ezt a feladatot ellátná, akkor a következő rendszerfrissítés esetén szintén meg kell oldania ezt a problémát. 2.1. Aspektus-orientált programozás Az ilyen esetek megoldására fejlesztették ki az aspektus-orientált programozást ([1]). Az AOP középpontjában az aspektus áll, amelyet a rendszer egy funkciójaként, dolgaként foghatunk fel. Azért jött létre ez a paradigma, mert észrevették, hogy a rendszerben megtalálhatók olyan funkciók is, amelyek komponensektől függetlenül jelennek meg, így az öröklődési- és interfészhierarchia segítségével ezeket a problémákat nem lehet megoldani. Az AspectJ ([2]) egy olyan nyelv, amely a Java aspektus-orientált kiterjesztéseként jött létre. Az AspectJ-ben írt kód byte-kód kompatibilis a Java nyelvű osztályokkal, így egy AspectJ program futtatható a hagyományos JVM-eken is. Ez nagyban elősegíti az AspectJ nyelv terjedését, hiszen megtehetjük, hogy a rendszerünk egy részét írjuk csak át aspektus-orientált 2
módon, majd üzembehelyezzük, és fokozatosan átgondoljuk a kritikus részeket. A szakirodalom ez refactoring-nak nevezi ([3][4]) és olyan esetekben kell alkalmazni, ha aspektusokkal szeretnénk összefogni az ismétlődő kommunikációkat már meglévő rendszerünkben. Az AspectJ fogalomrendszere a következő elemekből áll: joinpoint: jól meghatározott kódrészletek, például metódusok, konstruktorok, tulajdonságok, osztályok, stb. pointcut: feltételeket alapján kódrészleteket választhatunk ki vele, például: egy joinpoint segítéségével kijelölt metódus hívása, tulajdonság értékének módosítása, stb. Mitöbb a pointcut-okat logikai összekötőjelekkel kombinálva, összetett feltételeket adhatunk meg, például: olyan metódusok hívása, amely neve add szóval kezdődik, de a metódus maga nem paraméternélküli, stb. advice: egy kódrészlet, amely egy pointcut esetén lefut. aspect: az előbbi három összessége, beleértve a struktúrális módosításokat is. 2.2. Kommunikációs minták Webalkalmazások esetén akkor használhatjuk sikerrel az AOP-t, ha megtaláljuk és kiemeljük a kommunikáció során felmerül ismétlődő mintákat. Ilyenek például a bean-ek kommunikációja a JSP lappal, illetve az adatbázissal, stb. Ezeket a problémákat bizonyos nyelvek, keretrendszerek inherens módon kezelik, de ilyen keretrendszereket használva meg kell tanulnunk egy speciális szkriptnyelvet. Új keretrendszerre áttérve egy újabb nyelvet kell megtanulnunk és így tovább. Ráadásul ezek a nyelvek nem általános célúak, így a fejlesztés során könnyen belebotlunk olyan problémába, amely megoldását nem támogatják. 3. Minták AspectJ-vel Nézzük meg hogyan lehet az előbb említett két gyakran felmerülő, sok egyforma kódot tartalmazó mintát implementálni az AspectJ segítségével. 3.1. Bean-JSP kommunikáció Tegyük fel, hogy a van egy bean-ünk, amely egy számla adataival foglalkozik. A bean a JSP oldalról megkapja a számla azonosítóját (invoideid), majd lekérdezi az adatbázisból a számla leíró attribútumait. Ekkor a bean a következőképpen nézhet ki: public class Bean { private String parameterinvoiceid; 3
public void execute(){... ResultSet r = statement.executequery("select * from" + "invoice where id = " + parameterinvoiceid + " ");... A parameterinvoiceid változó értéke az InvoiceId kérési paraméter értéke kell legyen. Ezt természetesen beleírhattuk volna a bean-be, de ha több 10-20 ilyen értéket kell átadni, akkor a lényegi kód eltűnik a kommunikációs kódok között. Definiáljunk egy RequestGetter interfészt, amely jelzi, hogy egy bean rendelkezik egy kérés objektummal. interface RequestGetter { public ServletRequest getrequest(); public void setrequest(servletrequest request); A következő kód egy ParameterGetter nevű aspektust definiál, amely funkciója a következő: ha egy bean definiálja a RequestGetter interfészt, akkor bármely parameter előtaggal rendelkező attribútum értékére való hivatkozáskor, nem a tényleges értéket fogjuk megkapni, hanem a parameter utáni (esetünkben InvoiceId) névvel rendelkező kérési paraméter értékét. aspect ParameterGetter { declare parents: Bean implements RequestGetter; // (1) private ServletRequest RequestGetter._rq = null; // (2) public ServletRequest RequestGetter.getRequest(){ return _rq; public void Request.setRequest(ServletRequest r){ _rq = r; after(bean b, HttpServletRequest request): // (3) execution(public Bean.new(..)) && this(b) && 4
){ cflow( execution(public void *.HttpJspBase._jspService( HttpServletRequest, HttpServletResponse) ) && args(request) b.setrequest(request); String around(requestgetter rg): // (4) get(string RequestGetter+.parameter*) && this(rg) { String attributename = thisjoinpointstaticpart.getsignature().getname(); String parametername = attributename.substring(9); return rg.getrequest().getparameter(parametername); Az (1)-gyel jelölt kód azt jelenti, hogy a Bean osztály definiálja a RequestGetter interfészt. A (2)-vel kezdődő kódrészlet egy úgynevezett típusok közötti deklaráció (inter-type declaration), amely azt mondja, hogy ha egy osztály implementálja a RequestGetter interfészt, akkor rendelkezni fog egy rq nevű attribútummal, egy getrequest és egy setrequest metódussal, amely implementációját az aspektus adja meg. Tehát az aspektus megoldást ad arra is, hogy hogyan lehet egy interfészt automatikusan implementálni 1. A (3)-mal jelzett kód egy tanács (advice), amely a Bean osztály bármely konstruktorának végrehajtódása után hívódik meg, akkor ha ez a végrehajtódás a HttpJspBase osztály (minden JSP lap őse) jspservice metódusának végrehajtása közben történik 2. A this(b) hatására a szóban forgó objektumot a b azonosítóval érhetjük el, az args(request) kifejezéssel pedig egy hívás/végrehajtás argumentumait lehet nevesíteni, így a request a paraméterben átadott HttpServletRequest értéke lesz. A tanács törzse beállítja a bean kérés objektumát. Ezzel láthattunk egy példát egy komponens autmatikus környezet-detektálására. A (4)-gyel jelzett kód egy másik tanács, amely szerint ha hivatkozás történik egy parameter előtaggal rendelkező változó értékére egy olyan osztály esetében, amelyik implementálja a RequestGetter interfészt, akkor ehelyett hajtódjon végre a következő kód. A kód környezetéről a thisjoinpointstaticpart hordoz strukturális információt, így megtudhatjuk a szóban forgó attribútum nevét. A kilencedik karaktertől a kérési paraméter nevét kaphatjuk meg, amely 1 Erre a Java nyelvben is láthatunk példát a Serializable interfész esetében, hiszen ott is van a szerializált osztály mögött beépített funkcionalitás. 2 A cflow a control flow röviditése. 5
értékét elkérhetjük, hiszen ismerjük a kérést (request). Ha szeretnénk, hogy a Bean ne így működjön, akkor az (1)-sel jelzett sort kell kitörölni, illetve ha szeretnénk, hogy más osztályok is így viselkedjenek, akkor ugyanitt vesszővel elválasztva, és/vagy wildcard karakterek segítségével több osztály nevét is megadhatjuk. 3.2. Bean-adatbázis kommunikáció Az interaktív webalkalmazások fejlesztésekor gyakran felmerül egy adatbázistábla bővítése, módosítása, vagy az abból való törlés. Ez a feladat a tábla ismeretében elég jól algoritmizálható probléma, így joggal gondolhatjuk, hogy kódgenerátorral elkészíthetők a táblákat manipuláló kódok. Elkészíthetők, de utána a generált kódok karbantartása lehetetlen (újrageneráljuk, vagy belejavítsunk a generált kódba). Hogyan segít ezen a problémán az aspektus-orientált programozás? Definiáljuk a következő interfészt: public interface DataModifier { Azon bean-ek, amelyek manipulálni szeretnék az adatbázist, jelezzék azzal, hogy implementálják a DataModifier interfészt. Ezután ha bővíteni szeretné a bean az invoice táblát, definiálja a következő üres törzsű metódust. public void add_invoice(string _invoice_id, Timestamp create_date){ Az add előtag jelzi, hogy bővíteni szeretnénk egy táblát, majd a tábla nevét tartalmazza a metódus. Formális paramétereiben pedig a tábla mezőinek a nevét kell felsorolni úgy, hogy az elsődleges kulcs neve elé egy aláhúzás jel kerüljön (a módosításnál és a törlésnél tudnunk kell mely mezők részei a kulcsnak). import java.sql.*; import org.aspectj.lang.reflect.*; public aspect Database { void around(): execution(public void DataModifier+.add_*(..)){ try { Class.forName( "org.gjt.mm.mysql.driver" ); Connection conn = DriverManager.getConnection( "jdbc:mysql://127.0.0.1:3306/test" ); 6
Object [] args = thisjoinpoint.getargs(); // (1) MethodSignature ms = (MethodSignature) thisjoinpointstaticpart.getsignature(); String methodname = ms.getname(); String tablename = methodname.substring( 4 ); // (2) String [] pnames = ms.getparameternames(); Class [] ptypes = ms.getparametertypes(); StringBuffer sql = new StringBuffer(); sql.append( "insert into " ).append( tablename ).append( " (" ); for ( int i = 0; i < pnames.length; i++ ){ // (3) sql.append( pnames[i] ); if ( i < pnames.length - 1 ) sql.append( ", " ); sql.append( ") values (" ); for ( int i = 0; i < pnames.length; i++ ){ // (4) sql.append( "?" ); if ( i < pnames.length - 1 ) sql.append( ", " ); sql.append( ")" ); // (5) PreparedStatement st = conn.preparestatement( sql.tostring() ); for ( int i = 0; i < ptypes.length; i++ ){ if ( ptypes[ i ].equals( String.class ) ) st.setstring( i + 1, ( String ) args[ i ] ); if ( ptypes[ i ].equals( Integer.TYPE ) ) st.setint( i + 1, ( ( Number ) args[ i ] ).intvalue() ); st.execute(); st.close(); conn.close(); catch ( Exception e ){ 7
e.printstacktrace( System.err ); A Database aspektus megvalósítja a fent vázoltakat, nézzük meg hogyan! Az around-advice jelzi, hogy a DataModifier-t implementáló osztályok add -sal kezdődő metódusainak hívásakor az eredeti kód helyett a tanács törzsét hajtsuk végre. Miután csatlakoztunk az adatbázishoz, az (1)-sel jelzett kód a metódushíváskor átadott argumentumok értékét tudja meg. Majd (2)-sel jelzett kódban a metódus szignatúrájának lekérdezése után meg tudjuk határozni a tábla nevét, hiszen az a 4. karaktertől kezdődik a metódus nevében. Ezután a metódus paramétereinek nevét és típusát tudjuk lekérdezni. Sajnos más tulajdonságot nem (final-e vagy sem), ezért kell a kulcsrész attribútumok neveit aláhúzással kezdeni. Ezt a Java 1.5 ([5]) verziójában kielégített JSR-175-ös kérés ([6]) metadata eszköze képes megoldani. Ezután az SQL utasításnak a szövegét kell összeállítani, a paraméterek nevei a táblában szereplő mezők nevei lesznek (3), majd a paraméterek értékeit előkészített SQL utasítással fogjuk behelyettesíteni (4). A paraméterek típusát ismervén tudjuk, hogy a paraméter értékét mely JDBC típusba kell konvertálni és átadni. Az (5)-sel jelzett kód szöveges és egész értékű paraméterek értékeit tudja átadni az előkészített utasításnak. Ezután végrehajthatjuk az adatmanipulációs utasítást. 4. Konklúzió A cikkben vázolt módszerrel elvégezhetjük a komponensek közötti olyan kommunikációt, amelyek ismétlődőek, opcionálisak. Ehhez természetesen fel kell fedeznünk, hogy a megoldandó probléma milyen aspektusokat tartalmaz. Ezt a legegyszerűbben use-casek, felhasználói esetek és a kollabarációs diagrammok elemzésével végezhetjük el. Ezután meg kell vizsgálni, hogy mely objektumok, komponensek vesznek részt az aspektusban, amely mint látjuk fordítási időben történik. Így az aspektus statikus lesz, de ez a legtöbb esetben nem probléma, hiszen az üzembehelyezett rendszereknél a rendszer egészét, vagy egy jól meghatározott modult frissítenek, de nem aspektusokat. Érzésem szerint az aspektus-orientált programozást sikerrel lehet alkalmazni, ha jól meghatározott eseményekre ismétlődő kódokat kell végrehajtani. Így elkerülhetjük a kódgenerálást az eseményvezért programozás megfelelő használatával. A nyelvi támogatás még nem tökéletes, hiszen az aspektusok írásakor a Java 1.5-ös verziója még nem volt elérhető, így fölöslegesen készített, metódusnélküli interfészekkel kellett minősíteni az osztályokat. Az aspektus-orientált programozás eszközeivel megoldódni látszanak olyan komponensalapú programozásban megjelenő problémák, amelyeket eddig nehézkes volt kezelni. Ilyen a komponensek környezetének detektálása, onnan információk átvétele, illetve a hiba jelzése, 8
ha a környezet nem megfelelő (komponens beépíthetősége). A komponens tud környezetétől függően viselkedni, hiszen a cflow segítségével megadhatjuk, hogy valamilyen hívás hatása alatt vagyunk vagy sem. A komponenseket aspektusok kötik össze, ők valósítják meg a kommunikációt, így a kommunikációval foglalkozó kódokat nagyságrendekkel könnyebben lehet változtatni mint eddig. Az aspektus-orientált programozás még nem fejlődött ki teljesen, de látszik, hogy mindenképpen egy paradigmaváltás előtt állunk. Hivatkozások [1] Aspect-Oriented Software Development Community, http://aosd.net [2] AspectJ Project, http://eclipse.org/aspectj [3] Aspect-Oriented Refactoring: Part 1 Overview and Process, Ramnivas Laddad, http://www.theserverside.com/articles/article.jsp?l=aspectoriented RefactoringPart1 [4] Aspect-Oriented Refactoring: Part 2 The Techniques of the Trade, Ramnivas Laddad, http://www.theserverside.com/articles/article.jsp?l=aspectoriented RefactoringPart2 [5] Java 2 Standart Editon, Sun Microsystems, http://java.sun.com/j2se/1.5.0/ [6] Java Specification Request: A Metadata Facility for the JavaTM Programming Language, www.jcp.org/jsr/detail/175.jsp 9