innoQ

CAPSLOCKMarcel’s (CAPS)Weblog


Ergänzung zum Artikel "Lose Kopplung mit Web-Services einfach gemacht"

24.09.05

Eine kleine Ergänzung zum Artikel möchte ich noch anführen. Den Original-Code zum Artikel findet man auf der Seite bei Java-Spektrum. Die Ergänzungen, die ich hier anführe, können direkt mit dem Artikel-Beispiel verwendet werden.

Die Frage, die sich stellt ist: Bietet Axis nicht auch die Möglichkeit, mit diesen Non-Breaking-Changes umzugehen? Von Hause aus ist Axis da sehr rigoros, wie wir im Artikel gezeigt haben, was meiner Meinung nach auch ein Problem ist. Sichelrlich, kann man mit Axis sehr schnell einfache Web Services basteln, aber Eingriffe in den Gesamtprozes sind doch eher schmerzhaft und nervig. Warum also nicht einfach XML verschicken. Aber warum sollte man dann noch Axis benutzen, da doch das gesamte Know-How in der Serialisierung und Deserialisierung steckt. Das Verschicken und Empfangen von SOAP-Nachrichten ist selbst keine Kunst. Nun gut, ich schweife ab :-)
Ok, wenn man schon Axis verwendet, soll man gefälligst auch die wenigen Vorteile genießen. Es gibt nämlich durchaus eine Möglichkeit Axis dazu zubewegen, ebenfalls bei Non-Breaking-Changes keinen Fehler zu verursachen.
Bei dem Artikel gab es einen Service, bei dem Tickets für die WM bestellt werden konnten. In der ersten Version hat dieser Service für das bestellte Ticket nur Nummer des Tickets und den Block zurückgibt. Eine Bestellbestätigung sah also wie folgt aus:


public class Confirmation  implements java.io.Serializable {
    private java.lang.String block;
    private long number;
    ...
}

In der zweiten Version gab es zusätzlich auch Informationen über den Sitzplatz (Seat). Hier sah die Bestätigung folgendermaßen aus:


public class Confirmation  implements java.io.Serializable {
    private java.lang.String block;
    private long number;
    private java.lang.String seat;
    ...
}

Wurden nach der Änderung des Services keine neuen Proxy-Klassen erzeugt, meldete der Client (Consumer) den folgenden Fehler:

org.xml.sax.SAXException: Invalid element in com.innoq.ws.client1.Confirmation - seat

Obwohl ich es wesentliche "Service-orientierter" finde mit der Nachricht - also dem puren XML - zu arbeiten, gibt es bei der Verwendung von WSDL2Java und der daraus resultierenden Proxy-Klassen auch Vorteile. Auf jeden Fall ist die Entwicklung machmal einfacher - aber auch fehleranfälliger. Axis bietet nun die Möglichkeit eigene Serialisierer und Deserialisierer zu verwenden. Diese können einem nun aus diesem Problem (SAXException siehe oben) zu befreien. Damit ein CustomDeserializer aber auch verwendet wird, muss er in der getDeserializer()-Methode der JavaBean zurückgegeben werden:


public class Confirmation  implements java.io.Serializable {
    private java.lang.String block;
    private long number;

    public Confirmation() {
    }

    ....

    /**
     * Get Custom Deserializer
     */
    public static org.apache.axis.encoding.Deserializer getDeserializer(
           java.lang.String mechType, 
           java.lang.Class _javaType,  
           javax.xml.namespace.QName _xmlType) {
        return 
         // urspünglich
         //new  org.apache.axis.encoding.ser.BeanDeserializer(
                _javaType, _xmlType, typeDesc);
          // mein eigener Deserialisierer
         new  CustomDeserializer(_javaType, _xmlType, typeDesc);
    }

}


Ein CustomerDeserialiser, der überflüssige Elemente in einem SOAP-Request ignoriert, sieht folgendermaßen aus - hierzu musste ich die onStartChild()-Methode des BeanDeserializer modifizieren:


package com.innoq.ws.client1;

import java.util.Map;

import javax.xml.namespace.QName;
import org.apache.axis.encoding.DeserializationContext;

import org.apache.axis.Constants;
import org.apache.axis.description.ElementDesc;
import org.apache.axis.description.FieldDesc;
import org.apache.axis.description.TypeDesc;
import org.apache.axis.encoding.ConstructorTarget;
import org.apache.axis.encoding.Deserializer;
import org.apache.axis.encoding.ser.ArrayDeserializer;
import org.apache.axis.encoding.ser.BeanDeserializer;
import org.apache.axis.encoding.ser.BeanDeserializerFactory;
import org.apache.axis.encoding.ser.BeanPropertyTarget;
import org.apache.axis.message.MessageElement;
import org.apache.axis.message.SOAPHandler;
import org.apache.axis.soap.SOAPConstants;
import org.apache.axis.utils.BeanPropertyDescriptor;
import org.apache.axis.utils.Messages;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;

public class CustomDeserializer extends BeanDeserializer {

  private Class javaType;

  // Construct BeanSerializer for the indicated class/qname
  public CustomDeserializer(Class javaType, QName xmlType) {
    this(javaType, xmlType, TypeDesc.getTypeDescForClass(javaType));
  }

  // Construct BeanDeserializer for the indicated class/qname and meta Data
  public CustomDeserializer(Class javaType, QName xmlType, TypeDesc typeDesc) {
    this(javaType, xmlType, typeDesc, BeanDeserializerFactory
        .getProperties(javaType, typeDesc));
  }

  // Construct BeanDeserializer for the indicated class/qname and meta Data
  public CustomDeserializer(Class javaType, QName xmlType, TypeDesc typeDesc,
      Map propertyMap) {
    super(javaType, xmlType, typeDesc, propertyMap);
    this.javaType = javaType;
  }

  /**
   * Deserializer interface called on each child element encountered in the
   * XML stream.
   */
  public SOAPHandler onStartChild(String namespace, String localName,
      String prefix, Attributes attributes, DeserializationContext context)
      throws SAXException {
    handleMixedContent();

    BeanPropertyDescriptor propDesc = null;
    FieldDesc fieldDesc = null;

    SOAPConstants soapConstants = context.getSOAPConstants();
    String encodingStyle = context.getEncodingStyle();
    boolean isEncoded = Constants.isSOAP_ENC(encodingStyle);

    QName elemQName = new QName(namespace, localName);
    // The collectionIndex needs to be reset for Beans with multiple arrays
    if ((prevQName == null) || (!prevQName.equals(elemQName))) {
      collectionIndex = -1;
    }
    prevQName = elemQName;

    boolean isArray = false;
    QName itemQName = null;
    if (typeDesc != null) {
      // Lookup the name appropriately (assuming an unqualified
      // name for SOAP encoding, using the namespace otherwise)
      String fieldName = typeDesc.getFieldNameForElement(elemQName,
          isEncoded);
      propDesc = (BeanPropertyDescriptor) propertyMap.get(fieldName);
      fieldDesc = typeDesc.getFieldByName(fieldName);

      if (fieldDesc != null) {
        ElementDesc element = (ElementDesc) fieldDesc;
        isArray = element.isMaxOccursUnbounded();
        itemQName = element.getItemQName();
      }
    }

    if (propDesc == null) {
      // look for a field by this name.
      propDesc = (BeanPropertyDescriptor) propertyMap.get(localName);
    }

    // try and see if this is an xsd:any namespace="##any" element before
    // reporting a problem
    if (propDesc == null) {
      // try to put unknown elements into a SOAPElement property, if
      // appropriate
      propDesc = getAnyPropertyDesc();
      if (propDesc != null) {
        try {
          MessageElement[] curElements = (MessageElement[]) propDesc
              .get(value);
          int length = 0;
          if (curElements != null) {
            length = curElements.length;
          }
          MessageElement[] newElements = new MessageElement[length + 1];
          if (curElements != null) {
            System
                .arraycopy(curElements, 0, newElements, 0,
                    length);
          }
          MessageElement thisEl = context.getCurElement();

          newElements[length] = thisEl;
          propDesc.set(value, newElements);
          // if this is the first pass through the MessageContexts
          // make sure that the correct any element is set,
          // that is the child of the current MessageElement, however
          // on the first pass this child has not been set yet, so
          // defer it to the child SOAPHandler
          if (!localName.equals(thisEl.getName())) {
            return new SOAPHandler(newElements, length);
          }
          return new SOAPHandler();
        } catch (Exception e) {
          throw new SAXException(e);
        }
      }
    }

    if (propDesc == null) {
      // todo: Hier wird auf ein fehlendes Attribute-Mapping reagiert!!!!!

      System.out.println("Attribute " + localName + "[" + namespace + "] is unknown!");

      // todo: vorher wurde hier eine Exception geworfen!
      // No such field
      // throw new SAXException(Messages.getMessage("badElem00",
      // javaType.getName(),localName));
      return null;
    } else {

      // Get the child's xsi:type if available
      QName childXMLType = context.getTypeFromAttributes(namespace,
          localName, attributes);

      String href = attributes.getValue(soapConstants.getAttrHref());
      Class fieldType = propDesc.getType();

      // If no xsi:type or href, check the meta-data for the field
      if (childXMLType == null && fieldDesc != null && href == null) {
        childXMLType = fieldDesc.getXmlType();
        if (itemQName != null) {
          // This is actually a wrapped literal array and should be
          // deserialized with the ArrayDeserializer
          childXMLType = Constants.SOAP_ARRAY;
          fieldType = propDesc.getActualType();
        } else {
          childXMLType = fieldDesc.getXmlType();
        }
      }

      // Get Deserializer for child, default to using DeserializerImpl
      Deserializer dSer = getDeserializer(childXMLType, fieldType, href,
          context);

      // It is an error if the dSer is not found - the only case where we
      // wouldn't have a deserializer at this point is when we're trying
      // to deserialize something we have no clue about (no good xsi:type,
      // no good metadata).
      if (dSer == null) {
        dSer = context.getDeserializerForClass(propDesc.getType());
      }

      // Fastpath nil checks...
      if (context.isNil(attributes)) {
        if (propDesc != null && (propDesc.isIndexed() || isArray)) {
          if (!((dSer != null) && (dSer instanceof ArrayDeserializer))) {
            collectionIndex++;
            dSer.registerValueTarget(new BeanPropertyTarget(value,
                propDesc, collectionIndex));
            addChildDeserializer(dSer);
            return (SOAPHandler) dSer;
          }
        }
        return null;
      }

      if (dSer == null) {
        throw new SAXException(Messages.getMessage("noDeser00",
            childXMLType.toString()));
      }

      if (constructorToUse != null) {
        if (constructorTarget == null) {
          constructorTarget = new ConstructorTarget(constructorToUse,
              this);
        }
        dSer.registerValueTarget(constructorTarget);
      } else if (propDesc.isWriteable()) {
        // If this is an indexed property, and the deserializer we found
        // was NOT the ArrayDeserializer, this is a non-SOAP array:
        // 
        // value1
        // value2
        // ...
        // In this case, we want to use the collectionIndex and make
        // sure
        // the deserialized value for the child element goes into the
        // right place in the collection.

        // Register value target
        if ((itemQName != null || propDesc.isIndexed() || isArray)
            && !(dSer instanceof ArrayDeserializer)) {
          collectionIndex++;
          dSer.registerValueTarget(new BeanPropertyTarget(value,
              propDesc, collectionIndex));
        } else {
          // If we're here, the element maps to a single field value,
          // whether that be a "basic" type or an array, so use the
          // normal (non-indexed) BeanPropertyTarget form.
          collectionIndex = -1;
          dSer.registerValueTarget(new BeanPropertyTarget(value,
              propDesc));
        }
      }

      // Let the framework know that we need this deserializer to complete
      // for the bean to complete.
      addChildDeserializer(dSer);

      return (SOAPHandler) dSer;
    }
  }

}

Post a comment




Remember Me?