Utilisation avancée de CXF : les intercepteurs

le 02/12/2011 par Mikael Robert
Tags: Software Engineering

Le framework CXF est aujourd'hui probablement le meilleur framework pour implémenter des web services selon la spécification JAX-WS en Java. Ayant réalisé un projet d'envergure autour de CXF, cet article n'a pas pour but d'être une initiation à ce framework car les tutoriaux de base de la documentation sont très bien faits ( http://cxf.apache.org/docs/index.html). Nous allons plutôt, dans une série d'articles, tenter de vous présenter quelques "tips avancés" sur CXF.

Une grande qualité de CXF est d'être un framework très modulaire de par sa conception autour d'un bus et d'une chaîne d'intercepteurs.

Lorsqu'on met en place un ensemble de WebServices, on peut être amené à effectuer un traitement commun à tous ces web services. Par exemple, rattraper les exceptions et maîtriser la soap:foault qui sera renvoyée au client. Ce type de traitement peut être réalisé de diverses façons, mais une bonne pratique est d'utiliser des intercepteurs.

La documentation officielle explique très bien les bases de la mise en place d'intercepteurs. CXF découpe en phases le cycle de vie d'un message depuis son arrivée en passant par son traitement jusqu'à la réponse à celui-ci. C'est sur ces phases que l'on "branche" les intercepteurs afin qu'ils puissent traiter un message en fonction de son étape de cycle de vie. On peut aussi spécifier explicitement que l'on branche un intercepteur avant ou après un autre intercepteur sur la même phase.

Le schéma ci-dessous donne un exemple d'architecture projet simplifié pour CXF avec une étape de sécurité, une étape de logging, une étape de validation des données et une étape de traitement des erreurs - cliquer pour zoommer :

Nous allons maintenant vous présenter deux intercepteurs en exemple. L'ensemble du code présenté ci dessous est disponible sur github dans un projet maven. Ce code a été testé et déployé sous JBoss AS 7.

Mise en place d'un WebService simple

Pour nos exemples, nous avons besoin d'un WebService à appeler.

Celui-ci aura deux WebMethod :

  • une simple qui répond "hello"
  • une autre générant des exceptions, pour illustrer l'interception de celles-ci.

Le code de notre WebService d'exemple :

@WebService
public class HelloWorldService {

    @WebMethod
    public String sayHello() {
        return "Hello world";
    }

    @WebMethod
    public void makeBusinessException() throws BusinessException {
    	throw new BusinessException();
    }

}

Nous utiliserons dans ce cas la configuration en mode Spring 3, par annotation dans le code Java :

@Configuration
public class ApplicationConfig {

	@Bean
	public HelloWorldService helloWorldService() {
		return new HelloWorldService();
	}

	@Bean
	public XmlInInterceptor xmlInInterceptor() {
		return new XmlInInterceptor();
	}

	@Bean
	public ExceptionInterceptor exceptionInterceptor() {
		return new ExceptionInterceptor();
	}

}

Récupération du message SOAP avant marshalling

Pour diverses raisons, il peut être intéressant de récupérer le messages XML (SOAP donc) que les WebServices vont être amenés à recevoir. Le problème est que lorsqu'on arrive dans la WebMethod d'un WebService, celui-ci est déjà démarshallé et n'est plus accessible facilement. D'autre part, ce type de traitement uniquement technique n'a pas à apparaître dans la WebMethod qui ne devrait porter que le code répondant au métier du Web Service. Il faut donc récupérer ce message avant l'arrivée dans le WebService. Excellent cas d'usage des intercepteurs. Cet exemple a été choisi car il ne figure pas dans la documentation. Il a été codé en fouillant le code source de CXF.

A noter qu'on ne fait que logger le message, cela pourrait être remplacé par la feature de logging de CXF activable dans la configuration Spring :

<cxf:bus >
	<cxf:features>
		<cxf:logging />
	</cxf:features>
</cxf:bus>

Néanmoins il peut être utile de logger de façon plus formelle, pour des raisons d'audit par exemple.

Interception du message reçu

Le code suivant est celui de l'intercepteur qui récupère le XML du message reçu par le WebService.

@NoJSR250Annotations
public class XmlInInterceptor extends AbstractSoapInterceptor { 

	private Logger logger = LoggerFactory.getLogger(XmlInInterceptor.class);

	public XmlInInterceptor() {
		super(Phase.PRE_PROTOCOL);
		getAfter().add(SAAJInInterceptor.class.getName());
	}

	public void handleMessage(SoapMessage msg) throws Fault {
		Exchange exchange = msg.getExchange();
		Endpoint ep = exchange.get(Endpoint.class);

		ServiceInfo si = ep.getEndpointInfo().getService();
		String serviceName = si.getName().getLocalPart();
		logger.info("Service name : {}", serviceName);

		XMLStreamReader xr = msg.getContent(XMLStreamReader.class);
		if (xr != null) { // If we are not even able to parse the message in the SAAJInInterceptor (CXF internal interceptor) this can be null
			QName name = xr.getName();
			String operationName = name.getLocalPart();
			logger.info("Operation name : {}", operationName);
		}

		// Set the calling ip
		ServletRequest request = (ServletRequest) msg.get(ServletDestination.HTTP_REQUEST);
		if (request != null) { // and if the ServletDestination isn't even processed, the request could be null
			String remoteHost = request.getRemoteHost();
			logger.info("Calling IP : {}", remoteHost);
		}

		try {
			SOAPMessage msgSOAP = msg.getContent(SOAPMessage.class);
			if (msgSOAP != null) {
				ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
				msgSOAP.writeTo(byteArrayOutputStream);
				String encoding = (String) msg.get(Message.ENCODING);
				String xmlRequest = new String(byteArrayOutputStream.toByteArray(), encoding);
				logger.info("Xml Request was : {}", xmlRequest);
			}
		} catch (IOException e) {
			// process io exception
		} catch (SOAPException e) {
			// process soap exception
		}

	}
}

Cet intercepteur est donc branché en Phase Pre-Protocol. Cela nous permet de récupérer le XML avant qu'il ne soit unmarshallé par les intercepteurs JAXB de CXF.

Comme vous pouvez le voir, on extrait d'autres informations utiles du SoapMessages, tel que le nom du service appelée, l'opération appelée et l'hôte appelant. Ce code est un peu opaque. De façon générale, la programmation d’intercepteur est mal documentée. Le conseil que l’on pourrait donner est de s’inspirer d’un intercepteur similaire et déjà existant. Il faut fouiller car il y en a beaucoup… eux aussi non documenté.

Dans le cas présent, le fonctionnement est le suivant. Un SoapMessage CXF est une grosse HashMap contenant toute sorte de choses. Il faut donc faire un getContent vers d’un SOAPMessage version java pour récupérer l’objet en question. Ensuite, on l’extrait via un ByteArrayOutputStream que l’on insère dans une String en prenant bien soin d’utiliser l’encoding du message.

L’annotation @NoJSR250Annotations est utilisée pour indiquer à CXF que cette classe ne contient pas d’annotation provenant de la JSR 250 et qu’il n’est donc pas nécessaire d’en charger. Cela accélère le démarrage de l’application.

Interception de la réponse émise

Voici maintenant l'intercepteur utilisé en sortie qui récupère dans ce cas le XML émis par CXF pour répondre à l'appel de WebService. Celui-ci est branché en phase PRE_STREAM, juste avant l'envoi du message au client donc.

@NoJSR250Annotations
public class XmlOutInterceptor extends AbstractSoapInterceptor {

	private Logger logger = LoggerFactory.getLogger(XmlOutInterceptor.class);

	public XmlOutInterceptor() {
		super(Phase.PRE_STREAM);
	}

	public void handleMessage(SoapMessage message) throws Fault {

		final OutputStream os = message.getContent(OutputStream.class);
		if (os == null) {
			return;
		}
		final CacheAndWriteOutputStream newOut = new CacheAndWriteOutputStream(os);
		message.setContent(OutputStream.class, newOut);

		newOut.registerCallback(new LoggingCallback(message, os));
	}

	private class LoggingCallback implements CachedOutputStreamCallback {

		private final Message message;
		private final OutputStream origStream;

		public LoggingCallback(final Message msg, final OutputStream os) {
			this.message = msg;
			this.origStream = os;
		}

		public void onFlush(CachedOutputStream cos) {

		}

		public void onClose(CachedOutputStream cos) {
			String encoding = (String) message.get(Message.ENCODING);
			String ct = (String) message.get(Message.CONTENT_TYPE);
			StringBuilder builder = new StringBuilder();
			try {
				writePayload(builder, cos, encoding, ct);
			} catch (IOException ex) {
				throw new RuntimeException("Cannot generate audit log for soap response", ex);
			}

			String msg = builder.toString();
			logger.info("OUT MESSAGE {}", msg);
			message.setContent(OutputStream.class, origStream);

		}

		protected void writePayload(StringBuilder builder, CachedOutputStream cos, String encoding, String contentType)
				throws IOException {
			if (StringUtils.isEmpty(encoding)) {
				cos.writeCacheTo(builder);
			} else {
				cos.writeCacheTo(builder, encoding);
			}
		}
	}

}

Ce code semble encore plus complexe que le précédent pour simplement aller chercher un fragment de XML dans le framework. En fait nous l'avons en partie repris de la LoggingFeature de CXF ainsi que de quelques astuces glanées dans le code du framework. L'idée globale est d'intercepter le stream de sortie à l'aide d'un callback (LoggingCallback). Le callback en question récupère l'encoding du flux, lis le flux, le transforme en chaine de caractères, puis le replace dans le flux afin que la réponse soit tout de même émise au client... c'est mieux.

Configuration Spring

Et pour finir voici la configuration Spring de notre WebService et de ses intercepteurs :

Configuration Spring :

<import resource="classpath:META-INF/cxf/cxf.xml" />
<context:annotation-config />

<bean class="com.octo.cxf.config.ApplicationConfig"/>

<bean id="saajInInterceptor" class="org.apache.cxf.binding.soap.saaj.SAAJInInterceptor" />

<cxf:bus >
	<cxf:features>
		<!-- <cxf:logging /> -->
	</cxf:features>
	<cxf:inInterceptors>
		<ref bean="saajInInterceptor" />
	</cxf:inInterceptors>
</cxf:bus>


<jaxws:endpoint address="/helloworld" implementor="#helloWorldService" serviceName="helloWorldService">
	<jaxws:inInterceptors>
		<ref bean="xmlInInterceptor" />
	</jaxws:inInterceptors>
	<jaxws:outInterceptors>
		<ref bean="xmlOutInterceptor" />		
	</jaxws:outInterceptors>
	<jaxws:outFaultInterceptors>
		<ref bean="exceptionInterceptor" />
	</jaxws:outFaultInterceptors>
</jaxws:endpoint>

Bien entendu, si on a plusieurs WebServices, il devient tout de suite intéressant de remonter les intercepteurs au niveau du bus et non d'un web service :

<cxf:bus >
		<cxf:features>
			<!-- <cxf:logging /> -->
		</cxf:features>
		<cxf:inInterceptors>
			<ref bean="saajInInterceptor" />
			<ref bean="xmlInInterceptor" />
		</cxf:inInterceptors>
		<cxf:outInterceptors>
			<ref bean="xmlOutInterceptor" />	
		</cxf:outInterceptors>
		<cxf:outFaultInterceptors>
			<ref bean="exceptionInterceptor" />
		</cxf:outFaultInterceptors>
	</cxf:bus>

Vous qui êtes perspicaces remarquerez sûrement l’apparition de l’intercepteur SAAJ. C’est celui qui va ajouter le SOAPMessage dans le SoapMessage. Il est donc nécessaire de l’ajouter avant notre intercepteur en entrée. Par défaut, CXF l’exécute en mode lazy au sein d’un autre intercepteur plus loin dans la chaine.

Interception des exceptions et personnalisation des soap:fault

Nous allons maintenant voir comment rattraper les erreurs générées par les WebServices et leur code sous jacent pour en faire des soap:fault personnalisées.

Le code suivant récupère l'exception incluse dans le SoapMessage, teste son type et va générer une soap:fault en conséquence. Ici, pour simplifier le code, on ne fait qu'ajouter un code d'erreur 8 (définit arbitrairement), mais dans une application plus évoluée cela peut être utile d'y placer ses propres codes d'erreur.

L'intercepteur :

@NoJSR250Annotations
public class ExceptionInterceptor extends AbstractSoapInterceptor {

	public ExceptionInterceptor() {
		super(Phase.PRE_LOGICAL);
	}

	public void handleMessage(SoapMessage message) throws Fault {
		Fault fault = (Fault) message.getContent(Exception.class);
		Throwable ex = fault.getCause();
		if (ex instanceof BusinessException) {
			BusinessException e = (BusinessException) ex;
			generateSoapFault(fault, e);
		} else {
			generateSoapFault(fault, null);
		}
	}

	private void generateSoapFault(Fault fault, Exception e) {
			fault.setFaultCode(createQName(8));
	}

	private static QName createQName(int errorCode) {
		return new QName("octo.com", String.valueOf(errorCode));
	}
}

Cet intercepteur doit logiquement être branché en sortie, car la soap:fault sera la réponse au message envoyé.

Configuration Spring :

<jaxws:endpoint address="/helloworld" implementor="#helloWorldService" serviceName="helloWorldService">
	<jaxws:inInterceptors>
		<ref bean="xmlInInterceptor" />
	</jaxws:inInterceptors>
	<jaxws:outFaultInterceptors>
		<ref bean="exceptionInterceptor" />
	</jaxws:outFaultInterceptors>
</jaxws:endpoint>

Ainsi si on appelle la WebMethod makeBusinessException, on obtient la réponse suivante :

<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
   <soap:Body>
      <soap:Fault>
         <faultcode xmlns:ns1="octo.com">ns1:8</faultcode>
         <faultstring>Fault occurred while processing.</faultstring>
         <detail>
            <ns1:BusinessException xmlns:ns1="http://services.cxf.octo.com/"/>
         </detail>
      </soap:Fault>
   </soap:Body>
</soap:Envelope>

Pour plus de détails je vous invite à vous référer au code sur github : https://github.com/mikrob/cxf-poc

J’espère que cette petite immersion dans le monde des intercepteurs CXF vous sera utile. Dans le prochain article, nous verrons des notions de sécurité avancées liées à CXF (nous sortirons un peu du use case authentification avec WS Security déjà souvent présenté).