Dymamic SOAP September 16, 2009 07:45 11 months ago
Jeg skal udstille en SOAP WebService fra et et lagacy system. Det er en forholdsvis triviel proces med enkelte retninglinjer men bliver ofte til noget overengineered markværk. Min tanke er at det må kunne gøres på en let og clean måde. Efter min mening er begrebet service i forbindelse med IT er desværre druknet i forretningprocesser og total mangle på vision.
Mine forbehold er enkle! Jeg gider ikke skrive en WSDL fil og jeg vil helst ikke compilere tusindvis af filer flere måneder før jeg kender kravene til applikationen. Under normale forhold ville jeg være djævlens advokat når det gælder SOA, SOAP, Webservices og andre dødstjerne teknologer. Jeg vil have en service orienteret arkitektur med løs kobling og blød binding mellem komponenterne.
Her er klasse med noget opdigtet funktionalitet.
/**
* Leagcy business rule
*/
public class Palindrome {
public static boolean isPalindrome(String word) {
int left = 0; // index of leftmost unchecked char
int right = word.length() - 1; // index of the rightmost
while (left < right) { // continue until they reach center
if (word.charAt(left) != word.charAt(right)) {
return false; // if chars are different, finished
}
left++; // move left index toward the center
right--; // move right index toward the center
}
return true; // if finished, all chars were same
}
}
Andrew S. Tanenbaum bog om distribuerede systemer dukker svagt op i min erindring når vi taler om server klient applikationer. Derfor føler jeg det er vigtigt at præcisere context. Det første perspektiv er fra leverandørens(supplier) synspunkt. Rigtig mange problemer opstår i forskellige opfattelse af distribuerede systemer og det kan være svært over tid at skele mellem hvad der er forbruger og leverandør i forskellige forretningsområder. Leverandør processer som har flere afhængigheder fra andre under-leverandører som agere forbruger skal snart omskrives.
Første step er at fremstille en service. Jeg kalder processen for BusinessService men i teorien kunne det være hvilken slags forretnings regel eller dataplukning. For at spare taste slag bruger jeg Groovy til service interface. Det statiske metode kald til legacy kassen er verdens værste smutter men sådan er det jo med legacy.
class BusinessService {
boolean isPalindrome(String word) {
Palindrome.isPalindrome(word)
}
}
Dette service objekt skal nu omformes til et endpoint. Denne gang bruger jeg groovyWS til at tale SOAP. Bemærk navne-sammenfald i konstruktionen der invokere groovy’s dynamiske newinstance metode. Objektet kalder jeg for BusinessServiceEndpoint. Grunden er at jeg forstår at en port på en maskine er et endpoint mens mange forstår alt muligt andet. Måske er jeg for logisk? Logisk er det også at man skal passe meget på sin service stak og ikke mindst rækkefølgen i classpath.
new WSServer("BusinessService", "http://localhost:6900/BusinessService").start()
Det var serverens endpoint. WSDL filen specificere alle nødvendige forhold omkring hvilken muligheder man som WS forbruger kan opnå. Man kan se WSDL filen live på url: http://localhost:6900/BusinessService?wsdl
Nu er det tid til at tage det store skridt til højre for at lege forbruger(consumer) af webservice. Som klient modtager jeg en url der peger på et service endpoint. Dette bør være den eneste kobling mellem leverandør og forbruger. Hvis man har brug for mere som fx, et kompilerer jar arkiv, eller andre artifakter er man bundet for hårdt sammen. leverandøren har det med at ændre i sin kontrakt og fjerne dermed grundlaget for alle forbrugere. Løsningen er enkelt, en leverandør push’er successivt alle ændringer til sine forbruger. Eneste forhindring er at man som udstiller af services skal vide hvor mange forbruger man har hvilket totalt ødelægger service tanken. Og Tanenbaum vender sig.
- SOA er som et autovaskeanlæg, du kan ikke bruge det uden at vide alt. I morgen er vaskehallen fx kun 1m bred men heldigvis ringer tankpasseren rundt til alle med ændringer.
GroovyWS har en fin klient til Webservices. Klienten skal blot have endpoint og en classloader. Instansen kalder jeg proxy fordi den illustrere det objekt som ligger hos udbyderen. Når man kalder initalize generes alle de nødvendige stubs for dig og man kan derefter direkte invokere metoder gennem proxy’en. I dette tilfælde kalder jeg isPalindrome med argumentet otto.
def proxy = new WSClient("http://localhost:6900/BusinessService?wsdl", this.class.classLoader)
proxy.initialize()
println proxy.isPalindrome("otto") # true
Det fede er naturligvis at der foregår en masse nedeunder som jeg ikke engang gider ta mig af. Men dette var ret nemt fordi jeg jo blot bruger samme teknologi på begge sider af ledningen.
Lad og prøve at skifte hele servicestakken på forbruger siden med noget helt andet. I teorien vil leverandøren gerne ha at så mange så muligt benytter de udstillede services fra så mange platforme som muligt. Dette er en af de gode grunde til at man ikke skal lade sine EA kommer med alt for mange gode ideer når de udvikler meget komplekset WSDL’er. Sikkerhed består ikke kun af kompleksitet.
Ruby har en ‘soap/wsdlDriver’ indbygget man kan bruge til at tilgå soap endpoints. Syntakstisk minder det noget om groovyWS og man operere også direkte på en proxy uden at tænke på at afkode WSDL filen manuelt. Evt kan man se metodernavnen i browseren for derefter at invokere dem. Bemærk at argumentet i ruby er en hash fremfor en typed parameter. Hvis servicestakken ændres kan man risikere at skulle bruge andre navne end arg0, arg1 osv.
require 'soap/wsdlDriver'
proxy = SOAP::WSDLDriverFactory.new("http://localhost:6900/BusinessService?wsdl")
proxy.create_rpc_driver
puts proxy.isPalindrome("arg0" => "jruby-yburj")
Indtil nu har vi kun kørt med SOAP version 1.1. Måske er det tid at udstille en soap service i version 1.2. En ESB kan nemt udstille services af begge versioner og det ville være ret cool fremfor at en central admin bestemmer at der kun findes SOAP version 1.2. Ved en sådan fremgangsmåde bliver man ramt af en alt for kraftig binding mellem endpoints og services og har voldsomme implikationer for alle forbrugerne.
GroovyWS er ikke klar til soap version 1.2 ud af boksen og det kræver for mange omgåelser så det er tid at skifte til Metro 2.0 for at udstille vores næste service som sjovt nok også er palindrome. Legacy klassen er stadig den samme men service wrapperen er udskifte til en POJO java klasse med JAXB annotationer.
Service endpoint interface(SEI). For ethvert defineret interface i WSDL filen kan man generer en SEI. Fra et Java perspektiv kan man sige at wsdl:porttype mapper lige over til en implementation med annotationen @webservice hvor hver operation defineret i wsdl mapper til en metode i SEI.
Bemærk at SOAP 1.2 support is added til JAX-WS 2.2
/** SOAP 1.2 WS service */
@WebService
@BindingType(value = "http://java.sun.com/xml/ns/jaxws/2003/05/soap/bindings/HTTP/")
public class PalindromeService {
@WebMethod
public boolean isPalindrome(String word) throws PalindromeServiceException {
if (word == null || word.length() < 2) {
throw new PalindromeServiceException("No word added!", "Must have a word with length over 2 chars.");
}
return Palindrome.isPalindrome(word);
}
}
Nu er den samme legacy kode altså dækket af flere service wrapper med forskellige egenskaber. Jeg kan bedst li Java klasse uden “det tredie sprog” annotationer men okey, det er bedre en rå xml konfiguration. Her er tilføjet en eksplicit exception og et par valideringsregler. Nu kan vi gå direkte til endpoint som også er en Java klasse som kalder et JAX-WS endpoint og publicer en ny service.
public class PalindromeServiceEndpoint {
public static void main(String[] args) throws Exception {
Endpoint.publish("http://localhost:8080/business/ispalindrome", new PalindromeService());
}
}
Grunden til at dette er i Java fremfor Groovy er at jeg ikke vil lade JAXB binde superclass metoder. En groovy klasse har et metaClass interface og det kan JAXB ikke håndter. Dette kan afhjælpes ved brug af af en ekstra annotation. Det virker dog ikke særligt robust på den nye metro distribution og jeg synes det forurener koden at man skal fortælle hvilken objekter der er root.
Nuvel, nu findes en service udbyder som har publiseret en service i SOAP version 1.2. Men behøver man ikke vide om den. Vel? Det er nu vigtig at pointere at jeg har kaldt denne post “dynamic” fordi jeg ikke vil tilgå services med en binær afhængighed. Det er ikke hensigten at modtage en *jar fil fra serviceudbyderen som jeg skal lægge i min classpath og kode imod fordi jeg ikke vil rammes af namespace ændringer overalt i min kodebase når næste version kommer.
Mit udgangspunkt er at alle i teorien kan benytte denne service ligegyldigt hvilken platform og hvilken teknologi de fortrækker. Jeg vil betragte denne service(endpoint) som et sted at starte og jeg håber at wsdl filen fortæller mig om de næste skridt jeg kan tage.
Jeg høre til den nysgerrige type og vil forsøge at forbruge denne service med med udgangspunkt i SOAP beskeden selv, altså “the Envelope”. I konvolutten ligger information om sikkerhed, transaktioner og andre, for mig, ligegyldige ting.
Klient koden til en soap 1.2 besked med standart Java 1.6 indbygende features.
QName serviceName = new QName(service_namespace, service_name);
QName servicePort = new QName(service_namespace, service_port);
// Create a service and add at least one port to it.
Service service = Service.create(serviceName);
service.addPort(servicePort, SOAPBinding.SOAP12HTTP_BINDING, service_endpoint);
// Create a Dispatch instance from a service.
Dispatch<SOAPMessage> dispatch = service.createDispatch(servicePort, SOAPMessage.class, Service.Mode.MESSAGE);
// compose a request message
MessageFactory mf = MessageFactory.newInstance(SOAPConstants.SOAP_1_2_PROTOCOL);
// Create a message with the SOAPPART.
SOAPMessage request = mf.createMessage();
SOAPPart part = request.getSOAPPart();
// Obtain the SOAPEnvelope and header and body elements.
SOAPEnvelope env = part.getEnvelope();
SOAPHeader header = env.getHeader();
SOAPBody body = env.getBody();
// Construct the message payload, ant binding type value
SOAPElement operation = body.addChildElement(business_method, "ns1", service_namespace);
SOAPElement value = operation.addChildElement("arg0");
value.addTextNode(business_args);
request.saveChanges();
// Invoke the service endpoint.
SOAPMessage o = dispatch.invoke(request);
// Get reply content
Source sc = o.getSOAPPart().getContent();
I dette eksempel var en IDE nødvendig både fordi der er alt for meget kode og fordi JAXB bibliotekerne i denne EA version er noget forskellige fra den tidligere. Et par ting der er værd at lægge mærke til: Jeg må bygge et soap xml request på Java’s kedelige set get’er metodik. Ydermere, tiltrods for at jeg selv har konstrueret denne service må jeg ind i wsdl filer for at finde navnet hvori input består, isPalindrome(String word) er altså blevet til en række af inputparameter med fortløbende arg0, arg1 osv. Væk er det fine proxy objekt med stub’ens metode navne og hele interaktionen er alt for lowlevel for min smag. Og man bliver nu nødt til at bruge en Transformer for at MOF’e til et model objekt.
Klient koden til en soap 1.2 besked med Java 1.6 + Apache CXF.
JaxWsDynamicClientFactory dcf = JaxWsDynamicClientFactory.newInstance(); URL serviceEnpoint = new URL(service_endpoint); QName serviceName = new QName(service_namespace, service_name); QName servicePort = new QName(service_namespace, service_port); // create proxy and genered classes from wsdl Client proxy = dcf.createClient(serviceEnpoint, serviceName, Thread.currentThread().getContextClassLoader(), servicePort); // call method on proxy Object[] res = proxy.invoke(business_method, business_args); // use the new genered class in this classloader Object palindrome = Thread.currentThread().getContextClassLoader().loadClass(business_class).newInstance(); p(palindrome.getClass()); p(palindrome.getClass().getMethods() );
Ahhh, noget mindre kode men stadig langt mere verbose en klienten med groovyWS. Men jeg fik min dynamiske proxy som svare til objektet på den anden side og det er muligt at kalde metoder direkte på proxy objektet. Hvor kom klient objektet fra? Tja de blev generet af wsgen udfra wsdl filen på samme måde som man ville gøre på normal vis blot runtime. Ved den traditionelle metoder vil model være dekoreret med referacer fra wsgen på compiletidspunkt hvor man med denne metoder først kender service objekterne runtime, dvs at ens model er uden reference til de generede service objekter.
Hvilket påpege et helt nyt spørgsmål. Vil jeg arbejde på med en masse generede filer fra WSDL import eller vil jeg mappe en masse værdier i wrapper klasser? Sjovt nok er der ikke en perfekt løsning på nogen af disse problemer.
Når et projekt i start fasen beslutter at bruge SOA/Webservices er det ikke fragmenteringen af service der kommer i første række. En regel siger at gamle udkørte lagacy systemer, og forretningens forældede tanke mønstre, specificere det nye service layout. Lad mig gætte, dine services er store WS beskeder og det er kun nødvendigt at kalde ca 5 service for at udføre alle forretnings processer.
Hvis du kan svare ja til forgående sætning har du en hård kobling i dit system. Din udviklingsgruppe generet en domain model ud fra WSDL’er og der findes tusindvis af referencer til dine proprietære domain model. Den hurtige siger: vi har en rig domain model med et mapping layer! Okey, fint men samme problematik.
Så hvordan opnår man en løs kobling med SOA og WebServices?
Kom til at tænke på at man også kunne omskrive legacy klasse til groovy..
def isPalindrome(String word) { word == word.reverse() }
By Frank Vilhelmsen - 5 tags: java ruby soa ws* groovy - Add comment