Serialisering av Java-objekter

Bruk av objektorientert programmering får et ekstra "puff" når vi klarer å lagre objektene på disk eller overføre dem i et nettverk. Men dette er ikke enkelt, og vi trenger hjelp av språksystemet for å få det til.

Trykket i Nettverk & Kommunikasjon nr.01/2000
(c) Anders Fongen 2000

Anders Fongen er høgskolelektor og fagsjef ved Den Polytekniske Høgskolen. Web-stedet hans er på www.fongen.no

Objektorientert programmering innebærer at vi strukturerer dataene våre. Vi kan lage objekter som lar dataelementer høre sammen, f.eks. en feilkode sammen med en tekstbeskrivelse. Et telefonnummer kan høre sammen med navn, adresse og andre objekter av alle slag, f.eks. andre telefonnummer i samme gate.

I et objektorientert programmeringspråk er det sentralt at et objekt kan bli brukt av flere. Det oppnår vi ved at de som deler på bruken av et felles objekt ikke har sin egen private kopi inne sine egne private objekter, men kun en henvisning eller peker til dette felles objektet. Disse pekerne kan danne et ganske komplekst nettverk av objekter. Det programtekniske begrepet for et slikt nettverk er en graf, og teorien rundt bruken av grafer er av stor betydning når vi vil lage metoder for å overføre objekter til og fra harddisk eller datanett.

Familie med selvstendige medlemmer

Et objekt av klassen "Familie" kan romme et antall familiemedlemmer, dette er selvstendige familiemedlemmer som også kan inngå i andre sammenhenger, f.eks. objekter av klassen "Skoleklasse" eller "Speidertropp". Familie-objektet vil derfor ikke ha selve dataene om familiemedlemmene, men kun en henvisning til objektet som representerer denne personen.

Så hvordan overfører vi et "Familie"-objekt til en fil på en slik måte at det kan leses inn igjen med strukturen intakt?

Alle dem som tenker "vi følger alle henvisningene til alle objektene og skriver dataene som ligger inne i dem" rekker en hånd i været. Godt tenkt, men vi må huske på to ting: Vi må sikre at hvert objekt bare skrives en gang, og vi må passe på å skrive dem med en slik syntaks at det blir mulig å skille dataelementene og gjenkjenne datastrukturen under innlesing igjen. Dere kan ta hendene ned igjen nå.

De som gjerne vil vite hvordan vi kan programmere med såkalte serialiserte objekter uten å interessere seg for de programtekniske detaljene kan hoppe over neste avsnitt, dere andre kan lese videre:

Et objekt er representert som en graf, og gjennom en algoritme for å gjennomløpe den grafen slik at hvert element (node) blir besøkt kun én gang kan vi skrive både dataene og henvisningene til andre objekter i serialisert form. Henvisningene kan ikke skrives med sin pekerverdi direkte, men må erstattes med en verdi som henviser til objektet der det ligger i serialisert form (i datastrømmen), ikke til objektet i strukturert form (i internminnet). Hvert objekt må også skrives ut med et "hode" som inneholder klassenavn og et "versjonsnummer". Versjonsnummeret skal sikre at klassekoden som behandlet objektet før det ble serialisert, er den samme som behandler objektet etter at det blir "strukturert" igjen.

Serialiserte objekter i Java

Jobben med å serialisere objekter er ikke noe du selv får til. Du trenger et programmeringsspråk og -miljø som gjør det for deg. I Microsoft COM-arkitekturen heter prosessen "Object Marshalling", og utføres i forbindelse med at objekter skal overføres som parametre mellom klient og tjener når de ikke har felles minne. I Java brukes serialisering av objekter på liknende måte, til parameteroverføring mellom "fjernobjekter".

Men serialisering kan også brukes til å oppnå "permanente objekter" (persistent objects), ved at et serialisert objekt kan legges i en diskfil eller en database, og dermed gjøres permanent tilgjengelig også etter at programmet er avsluttet og maskinen slått av.

Det vi skal se litt nærmere på, er et eksempel på bruk av serialiserte objekter som protokollenheter mellom en databasetjener og -klient. Vi ønsket å la en enkel databasetjener motta SQL-spørresetninger og returnere det resulterende radsettet som et serialiserte objekt, og på den måten unngå mye programmeringsarbeid med koding og analysering av byte-strenger.

Enkel databasetjener

Bortsett fra å demonstrere serialisering, hvorfor er det vi ønsker å lage en databasetjener når det finnes dusinvis av dem fra før? To årsaker: Den ene er at små, enkle databasemotorer (Access f.eks.) ikke lar seg utnytte via nettverk, kun lokalt. Det finnes dessuten en gratis databasemotor skrevet i Java som heter InstantDB (instantdb.lutris.com OBS! Denne er ikke lenger gratisvare), og som egner seg for "lettvekts" databaseanvendelser. Den andre årsaken er at når vi lager en klient-tjener protokoll som ikke benytter seg av installert programvare (f.eks. ODBC-drivere), så kan vi lage "tynne" klientprogrammer som lettere lar seg distribuere og installere. Vi kan f.eks. distribuere en slik databaseklient som en applet (dvs. et program som kjører inne i en web-side, men brannvegger kan skape problemer her).

Del av I/O-biblioteket

Serialisering av Java-objekter skjer gjennom I/O-pakken "java.io". Java baserer all I/O på objekter av "stream"-typen (mange ulike klasser), som alle representerer en form for datastrøm. Utdata mot en TCP-socket, en fil eller et minnebuffer kan skje på samme måte, så lenge stream-objektet er av en klasse som arver fra OutputStream.

Om vi f.eks. ønsker å sende et serialisert Java-objekt over en TCP-forbindelse (port 4567), bruker vi denne koden:

(import java.io.*;)
(import java.net.*;)
Socket s0 = new Socket("myserver.fongen.no",4567);
ObjectOutputStream oos = 
   new ObjectOutputStream(s0.getOutputStream());
oos.writeObject(thisIsMyObjectOfAnyClass);
s0.close();
Dette programmet forutsetter selvfølgelig at det er noen i andre enden av denne TCP-forbindelsen som har gjort en tilsvarende leseoperasjon. Dessuten kan det oppstå en del feilsituasjoner underveis, så det er nødvendig å sette opp en try/catch-blokk rundt koden for å takle feilsituasjoner.

JDBC

For å lage en databasetjener slik vi ønsker det, må vi benytte Javas metoder for databasebruk, som kalles JDBC. Vi hadde ikke tenkt å gi dette emnet særlig med plass i denne artikkelen, men bruk av JDBC er altså ganske likt bruk av ODBC: Man setter opp en forbindelse til databasen gjennom et å lage et Connection-objekt, lager et Statement-objekt som skal romme og utføre en SQL-setning, og det resulterende radsettet returneres i et ResultSet-objekt.

Det er dette ResultSet-objektet som inneholder både selve dataene i radsettet, og opplysninger om kolonnebredde, datatyper m.m. (metadata). Og det er dette objektet som vi ønsker å serialisere for å sende tilbake til klienten.

Uheldigvis kan ikke alle Java-objekter serialiseres, og det gjelder bl.a. objekter av klassen ResultSet. Objekter kan bare serialiseres dersom visse vilkår er oppfylt, og grunnen til at ResultSet ikke kan serialiseres er at det inneholder referanser til ressurser utenfor Java-systemet, nemlig databaseforbindelsen.

Så for å få returnert et resultatsett måtte vi lage en egen klasse som inneholder de opplysningene i resultatsettet som vi trenger, dvs. selve dataene og noe av metadatene. Vi kaller denne klassen for SerialResultSet. Den er i hovedsak en lagringsklasse, men som i tillegg til radsettet også kan inneholde opplysninger om oppståtte feilsituasjoner.

Nettverk

Hvor vanskelig er det å lage et Java-program som kommuniserer over nettverkets TCP-protokoll? Det er veldig enkelt, vi har allerede vist i programeksempelet over hvordan man setter opp en forbindelse til en tjener.

Nesten like enkelt er det å sette opp tjenersiden av en slik forbindelse. Java har en innebygget klasse som heter ServerSocket, og som har det som trengs. Se bare på denne programkoden:

ServerSocket s = new ServerSocket(4567);
Socket incoming = s.accept();
ObjectInputStream ois = 
   new ObjectInputStream(incoming.getInputStream());
String sqlComm = (String)ois.readObject();
incoming.close();
Accept-metoden returnerer ikke før en oppkopling er mottatt, og returnerer et Socket-objekt som representerer den aktive forbindelsen. Vi kan lese og skrive data gjennom en InputStream/OutputStream som vi får ut av dette objektet.

En ting som bør nevnes når man vil bruke nettverket som bærer av en to-veis "objekt"-strøm: Konstruktøren til ObjectOutputStream vil sende noe data til ut-strømmen, tilsvarende vil konstruktøren til ObjectInputStream lese noe data (en header) fra inn-strømmen. Om en klient først lager en ObjectInputStream og deretter en ObjectOutputStream, må tjeneren gjøre det i motsatt rekkefølge. Ellers vil begge vente på hverandre i det uendelige (deadlock).

Threads

Vår lille databasetjener skal betjene flere klienter samtidig, slik at en enkel SQL-setning kan fullføres raskt uten å måtte vente "i kø" bak en stor og kompleks SQL-setning. Dette løses ved å la tjenerprogrammet utføres i flere tråder, én tråd for hver forespørsel.. Hver gang noen kopler seg til TCP-porten på tjeneren, skapes det en ny utføringstråd som "overtar" den videre betjeningen av denne klienten, mens hovedprogrammet straks går tilbake til å lytte etter flere klienter. Dette er ikke vanskelig programmering når man først har skjønt hvordan det skal gjøres, og eksempler på slike programmer finnes i mange Java-lærebøker.

Men når man lager et program i flere tråder, er det viktig å forvisse seg om at man ikke lager feilsituasjoner fordi den underliggende databasen (eller andre slags systemtjenester) ikke tåler forespørsler som "overlappes" i tid. Om det f.eks. er slik at databasen benytter seg av en "global" buffer til å lagre SQL-setninger, kan det skje at vi med forespørsel nr.2 overskriver bufferet med nytt innhold før forespørsel nr.1 er ferdig utført. Slike tilstander kan vi selvfølgelig ikke leve med, så det er viktig at databasen er såkalt "Threadsafe". Threadsafe innebærer at den er programmert fri for ubeskyttede felles ressurser, slik at flere utføringstråder kan kalle på operasjoner i databasen samtidig. Er f.eks. MS Access Threadsafe? Vi vet ikke, men finner det naturlig at et program som kan kjøres med flere klienter på samme maskin har den nødvendige beskyttelsen for å betjene klienter i et nettverk. Vi har dessuten gjort noen enkle tester på samtidige operasjoner uten å ha sett noen feilsituasjoner. InstantDB sier uttrykkelig at den er Threadsafe.

Konklusjon

Hele programkoden til vår databasetjener og en enkel test-klient er på ca.220 kodelinjer, og demonstrerer hvordan programmeringen av en klient-tjener protokoll blir sterkt forenklet med bruk av serialiserte objekter. Vi har valgt å lage en SQL-basert databasetjener, men mange slags klient-tjener anvendelser kan utnytte denne teknikken. En filtjener kan kanskje lages på en tilsvarende måte, ved å lage subklasser av FileInputStream og FileOutputStream som omsetter de enkle kallene for lesing og skriving til kall som overføres til tjeneren i form av serialiserte objekter?

Kildekoden til dette eksperimentet

Dersom du har interesse av å se på kildekoden til dette lille eksperimentet, kan du følge denne lenken og pakke ut den arkivfilen (ZIP-format) som du får lastet ned.