27. prosince 2012

Java a fault handling policies v Oracle SOA Suite

Jednou ze součástí Oracle SOA Suite je tzv. Fault Management Framework, který se mmj. stará o zpracování výjimek v BPELu. Pokud během invoke aktivity nastane výjimka, framework ji odchytí a předá ji ke zpracování akci, která je definovaná ve fault policy.

Tyto politiky jsou zajímavou alternativou k odchytávání výjimek v samotném BPELu  pomocí fault handleru <catch>. Dá se na ně pohlížet jako na aspekt (ve smyslu AOP), který je deklarativně navěšený na procesu (nebo i celé kompozitní aplikaci).

No, pokud jsem řekl aspekt, pravděpodobně jsem vzbudil velká očekávání. Tak to bohužel není - sám jsem musel pohřbít některé své designové představy - má to spoustu omezení, resp. možnosti nejsou rozsáhlé. Takže co to vlastně umí?

V rámci politiky se dají definovat následující zotavné akce:
  • Retry. Nepovedený invoke se dá zopakovat. Dá se nastavit počet opakování, prodleva, exponenciální prodleva a následující zřetězená akce, pokud retry nezafunguje.
  • Human Intervention. Proces se zastaví a je možné ho manuálně obnovit z management konzole.
  • Terminate Process. Proces je ukončen. Totéž jako aktivita.exit.
  • Rethrow Fault. Chyba je vyhozena zpátky do BPEL procesu.
  • Replay Scope. Vyhodí reply fault, což způsobí znovu vykonání aktivit ve scope.
  • Java Code. Zavolá externí Java třídu. To je to, na co se budeme dále soustředit.

Problém

Mám pro klienty vystavenou službu SOAPFault, která má ve svém kontraktu definovaný SOAP Fault:
<xsd:element name="fault">
  <xsd:complexType>
    <xsd:sequence>
      <xsd:element name="resultCode"
                   type="xsd:string"/>
      <xsd:element name="error">
        <xsd:complexType>
          <xsd:sequence>
            <xsd:element name="errorCode"
                         type="xsd:string"/>
            <xsd:element name="errorDescription"
                         type="xsd:string"/>
          </xsd:sequence>
        </xsd:complexType>
      </xsd:element>
    </xsd:sequence>
  </xsd:complexType>
</xsd:element>

Pro graficky orientované to vypadá takhle:


Tento fault je pak použit ve WSDL:
<wsdl:message name="faultMessage">
    <wsdl:part name="payload" element="inp1:fault"/>
</wsdl:message>

<wsdl:portType name="SOAPFaultPort">
    <wsdl:operation name="test">
        <wsdl:input message="tns:requestMessage"/>
        <wsdl:output message="tns:replyMessage"/>
        <wsdl:fault name="testfault"
                    message="tns:faultMessage"/>
    </wsdl:operation>
</wsdl:portType>
Služba je implementovaná pomocí BPEL procesu, který v rámci orchestrace volá další službu, nazvanou BPELFault:


Tak, to byla expozice. Teď přijde kolize. V BPELu mám definovanou invoke aktivitu, která volá externí službu BPELFault. Chtěl bych použít politiku tak, aby když externí služba vrátí chybu, aby mi politika nastavila můj definovaný SOAP fault a proces ho vrátil klientovi.

Než se pustíme do implementace politiky, musíme v BPEL procesu ještě splnit dvě podmínky. Jednak v procesu definovat proměnnou, která bude mít nastavený typ zprávy jako daný fault:
<variable name="soapFault"
          messageType="ns1:faultMessage"/>
A druhak, proces musí náš fault vyhodit ven pomocí aktivity throw:
<catchAll>
  <throw name="ThrowFault"
         faultName="ns1:testfault"
         faultVariable="soapFault"/>
</catchAll>

Jak to celé funguje?

Obrázek je za tisíc slov, takže tady je BPMN diagram. Swimliny jsou trochu nečitelný :-/ takže odshora: BPEL, External WS, Fault Management Framework a Java Class.


Java

Java třída, která bude z politiky volaná musí implementovat rohraní IFaultRecoveryJavaClass. To má dvě metody, nás bude zajímat pouze handleFault, která má jako parametr IFaultRecoveryContext. Pomocí tohoto kontextu lze přistupovat k objektům v BPEL procesu, odkud vyletěla výjimka.

Z kontextu si tak vytáhneme výše uvedenou BPEL proměnnou soapFault a pomocí XPath, nebo DOMu ji naplníme. Na závěr vrátíme z metody string RETHROW, aby politika vrátila řízení zpátky do BPEL procesu, odkud je pak už vrácena SOAP fault klientovi.

(Omlouvám se, že ten kód uvádím celý. Ale kdyby náhodou to někdo (v Česku) řešil, tak ať to má trochu jednodušší :-)
package cz.swsamuraj.soa.fault;

import com.collaxa.cube.engine.fp.BPELFaultRecoveryContextImpl;

import
   oracle.integration.platform.faultpolicy.IFaultRecoveryContext;
import
   oracle.integration.platform.faultpolicy.IFaultRecoveryJavaClass;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;

/**
 * This class is called by fault policy. Its purpose is to set up
 * a variable which is then throwed as a SOAP fault.
 * 
 * @author Guido
 */
public class SOAPFaultHandler implements IFaultRecoveryJavaClass {

   private static final String VARIABLE = "soapFault";
   private static final String PART = "payload";
   private static final String RESULT_CODE_NAME = "resultCode";
   private static final String RESULT_CODE_VALUE = "255";
   private static final String ERROR_NAME = "error";
   private static final String ERROR_CODE_NAME = "errorCode";
   private static final String ERROR_CODE_VALUE =
                               "EXTERNAL_SERVICE_ERROR";
   private static final String ERROR_DESCRIPTION_NAME =
                               "errorDescription";
   private static final String RESULT = "RETHROW";

   @Override
   public void handleRetrySuccess(
                    IFaultRecoveryContext iFaultRecoveryContext) {
   }

   /**
    * Method for set up a BPEL variable for SOAP fault.
    * 
    * @param recoveryContext
    * @return
    */
   @Override
   public String handleFault(IFaultRecoveryContext recoveryContext) {
      if (recoveryContext instanceof BPELFaultRecoveryContextImpl) {
         BPELFaultRecoveryContextImpl bpelContext =
               (BPELFaultRecoveryContextImpl) recoveryContext;

         Element root =(Element)
                           bpelContext.getVariableData(VARIABLE,
                                                       PART, "/");
         Node soapFault = root.getFirstChild();
         String namespaceURI = soapFault.getNamespaceURI();

         bpelContext.addAuditTrailEntry(String.format(
               "Obtained a root element: {%s}%s.", namespaceURI,
               soapFault.getLocalName()));

         // removes all child nodes
         if (soapFault.hasChildNodes()) {
            int count = soapFault.getChildNodes().getLength();

            for (int i = 0; i < count; i++) {
               Node child = soapFault.getFirstChild();
               soapFault.removeChild(child);
            }
         }

         Document document = soapFault.getOwnerDocument();
         Node resultCode = createNode(document, namespaceURI,
               RESULT_CODE_NAME, RESULT_CODE_VALUE);
         soapFault.appendChild(resultCode);

         Node error = createNode(document, namespaceURI,
                                 ERROR_NAME, null);
         soapFault.appendChild(error);

         Node errorCode = createNode(document, namespaceURI,
                                     ERROR_CODE_NAME,
                                     ERROR_CODE_VALUE);
         error.appendChild(errorCode);

         Node errorDescription = createNode(document, namespaceURI,
                                            ERROR_DESCRIPTION_NAME,
                                            bpelContext.getFault()
                                                       .toString());
         error.appendChild(errorDescription);

         bpelContext.addAuditTrailEntry(String.format(
               "SOAP fault response has been set"
               + " by fault policy '%s'.",
               bpelContext.getPolicyId()));
      }

      return RESULT;
   }

   /**
    * Method creates a new {@link Node} with given parameters.
    * 
    * @param document {@link Document}
    * @param qualifiedName name of the element
    * @param namespaceURI namespace of the element
    * @return {@link Node}
    */
   private Node createNode(Document document, String namespaceURI,
         String qualifiedName, String textContent) {
      Node node = document.createElementNS(namespaceURI,
                                           qualifiedName);

      if (textContent != null) {
         node.setTextContent(textContent);
      }

      return node;
   }
}

Zajímavá je zde asi pouze metoda kontextu addAuditTrailEntry, která propisuje informace do auditního záznamu procesu, což je pak vidět v konzoli Enterprise Managera:


Definice politiky

Už máme ruce, teď mozek. Aby politika byla funkční, je potřeba vytvořit dva soubory. Jednak samotnou definici, soubor fault-policies.xml a potom navázání politiky na konkrétní kompozitiní aplikaci (nebo komponent), soubor fault-bindings.xml. Toto jsou defaultní názvy souborů. Pokud chceme jiné názvy, např. protože chceme politiky verzovat, je potřeba tuto odlišnost uvést v definici kompozitní aplikace.
<property name="oracle.composite.faultPolicyFile">
        fault-policies-v1.0.xml</property>
<property name="oracle.composite.faultBindingFile">
        fault-bindings-v1.0.xml</property>
Nebo graficky:


V definičním souboru fault-policies-v1.0.xml říkáme, že chceme odchytávat výjimku remoteFault (není to standardní chyba definovaná BPELem, ale Oracle BPEL Extension) a chceme, aby byla zpracovaná naší Java třídou:
<?xml version='1.0' encoding='UTF-8'?>
<faultPolicies xmlns="http://schemas.oracle.com/bpel/faultpolicy">
  <faultPolicy version="2.0.1" id="BpelInvokeActivityFaults-v1.0">
    <Conditions>
      <faultName
            xmlns:bpelx="http://schemas.oracle.com/bpel/extension"
            name="bpelx:remoteFault">
        <condition>
          <action ref="ora-java"/>
        </condition>
      </faultName>
    </Conditions>
    <Actions>
      <Action id="ora-java">
        <javaAction
             className="cz.swsamuraj.soa.fault.SOAPFaultHandler"
             defaultAction="ora-rethrow">
          <returnValue value="RETRHOW" ref="ora-rethrow"/>
        </javaAction>
      </Action>
      <Action id="ora-rethrow">
        <rethrowFault/>
      </Action>
    </Actions>
  </faultPolicy>
</faultPolicies>
Ve vazebním souboru fault-bindings-v1.0.xml jenom říkáme, že naše politika BpelInvokeActivityFaults-v1.0 je svázaná s celou kompozitní aplikací:
<?xml version='1.0' encoding='UTF-8'?>
<faultPolicyBindings
       version="2.0.1"
       xmlns="http://schemas.oracle.com/bpel/faultpolicy">
  <composite faultPolicy="BpelInvokeActivityFaults-v1.0"/>
</faultPolicyBindings>

Deployment do runtime

Pokud máme třídu i politické :-) soubory přímo v kompozitní aplikaci, nemusíme deployment nijak řešit - prostě kompozitní aplikaci standardně nasadíme. Pokud však chceme politiky externalizovat (protože je chceme přepoužít pro více kompozitek), je vhodné umístit XML soubory do MDS (Metadata Services repository) a Java třídu nasadit do runtimu SOA Suity:
  1. Zkompilovanou třídu zabalíme do JAR souboru.
  2. JAR soubor nakopírujeme do adresáře <ORACLE_HOME>/Oracle_SOA1/soa/modules/oracle.soa.ext_11.1.1
  3. V tomto adresáři spustíme příkaz ant.
  4. Restartujeme WebLogic.

Související články


Odkazy

Žádné komentáře:

Okomentovat