Utilisation avancée de CXF : les intercepteurs

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é).

22 commentaires sur “Utilisation avancée de CXF : les intercepteurs”

  • Article bien fait et facile a suivre. Merci. Il serait tres interessant d'avoir plus d'information sur la facon dont les exceptions et/ou faults sont retournees. Peut on ecrire un test unitaire que va inspecter une exception contenant certaines informations ajoutees par l'intercepteur ? Ou doit on avoir un test unitaire qui va inspecter la reponse xml elle meme ?
  • Bonjour, Qu'entendez vous par plus d'informations sur les faults et exceptions? L'exemple donné illustre ceci, non? Pour les deux autres questions : - On peut écrire un test d'intégration qui va utiliser un client CXF, qui, pour vous va demarshaller l'exception ce qui vous permettra de la catcher et de regarder ce qu'elle contient - la 2e méthode est possible aussi c'est à vous de voir ce que vous préférez ou votre besoin. Néanmoins ça reviens à tester le framework qui est déjà fort bien testé par ses developpeurs :) Toutefois j'ai déjà écrit ce genre de tests pour analyser des comportements "anormaux" de web services, c'est à dire en l’appelant avec des données incohérentes (tests de sécurité entre autres) .. bref à vous de voir!
  • Bonjour, Merci pour votre attention et votre reponse rapide. Je me permet de poursuivre la discussion.. Etant un nouveau dans la monde du SOAP et faisant mes premiers pas avec Apache CXF je navigue a vue. C'est un article qui teste la reponse SOAP qui m'a fait me poser la question si il etait interessant de faire un tel test. J'etait assez dubitatif. Votre reponse sur l'inutilite d'un tel test me conforte dans ma premiere vague impression. Voila l'article en question: http://stackoverflow.com/questions/8066474/how-to-transform-soapfault-to-soapmessage-via-interceptor-in-cxf J'ai d'autre part fais un test d'integration visant a evaluer une exception mais mon intercepteur ne semble pas entrer en action. L'exception est bien lancee et est bien recu par le test client, mais sans etre modifiee comme il se doit par l'intercepteur. J'ai poste un fil dans le forum a la page: http://cxf.547215.n5.nabble.com/Why-is-my-exception-interceptor-not-being-called-td5464238.html Encore merci pour votre article qui m'a permis de decouvrir les intercepteurs et de les deployer dans un projet.
  • Vous devez enregistrer l'intercepteur soit au niveau du bus CXF, comme dans un des exemples ci dessus, soit au niveau d'un web services, ça devrait mieux marcher :) Sur le post de forum que j'ai lu, je n'ai pas l'ensemble de la configuration mais on dirait qu'il n'est ni sur l'un ni sur l'autre.
  • Bonjour, J'ai deja ce parametrage dans le projet: N'est ce pas ainsi que l'intercepteur doit etre enregistre au sein du bus ? Merci !
  • Je pense que le copy/coll ou le xml n'est pas passé
  • Oui, il a ete filtre :-) Du coup je l'ai ajoute au fil de discussion que j'avais ouvert sur le forum de CXF http://cxf.547215.n5.nabble.com/Why-is-my-exception-interceptor-not-being-called-td5464238.html
  • J'ai ajoute le project sur GitHub https://github.com/stephaneeybert/springws
  • Je n'ai hélas pas le temps de regarder en détail mais vérifier que votre erreur se produit pendant l'appel ou pendant la réponse ça vous permettra de savoir si votre intercepteur doit être branché en in ou en out.
  • Je ne sais pas trop quoi repondre. La BusinessException est jetee par le service, donc apres la bonne reception du message en IN. Et l'intercepteur exceptionInterceptor est injecte dans le bean outFaultInterceptors ce qui me fait dire qu'il devrait intervenir est OUT. Mais l'erreur est difficile a "voir" car l'erreur est que justement il ne se passe rien et que l'intercepteur exceptionInterceptor n'est pas appelle.
  • Vu ce que vous decrivez on dirait que l'exception est catchée quelque part et que du coup l'intercepteur ne la voit pas passer. Essayer de mettre un breakpoint d'exception dans Eclipse pour voir quand elle part et suivre ou elle est rattrapée.
  • Je viens cette fois ci, d'ajouter a la classe Client ligne de commande, une exception et l'interceptor exceptionInterceptor se declenche et se comporte bien. Je pense donc a un probleme de configuration ou d'injection du service dans ma classe de test. Je me demande si mon test utilise bien le web service, ou s'il n'utilise pas plutot directement le service. En effet, le test est dans le meme module Maven que le service.
  • Effectivement si votre classe est directement appelée et ne passe pas par CXF l'intercepteur ne travailleras pas.
  • J'ai donc maintenant injecte le web service et non pas le service. Je vous remercie encore une fois pour votre tres aimable assistance.
  • Je rencontre un soucis sur la définition des beans ... "Error creating bean with name 'cxf.config0': Cannot resolve reference to bean 'xmlInInterceptor' while setting bean property 'inInterceptors' with key [0] (...) No bean named 'xmlInInterceptor' is defined" une idée ?
  • Il faut que le bean xmlInInterceptor soit déclaré dans la configuration Spring comme bean.
  • Sans vouloir flooder ton article, (...), je te présente la façon dont j'ai adapaté la gestion des intercepteurs à mon projet (spring2.5.6) J'ai défini dans mon fichier applicationContext.xml Ce que je comprends de l'architecture est que le bean xmlInInterceptor est chargé via la classe ApplicationConfig, est ce bien cela ?
  • Oui par annotations. Je m'excuses mais vos questions sont vraiment hors sujet et j'ai déjà donné la réponse à votre question. Après à vous de voir si vous voulez déclarez vos beans par annotations ou par configuration XML mais la configuration de Beans dans Spring n'est pas le sujet de l'article.
  • C'est Ok pour la mise en place des interceptors, je retrouve bien le contenu des enveloppes SOAP ([XmlInInterceptor] et [XmlOutInterceptor]. Par contre sur la gestion des exceptions, je n’obtiens pas la réponse attendue: je ne visualise pas la réponse SOAP. Appel: [XmlInInterceptor] - Xml Request was : {} Je passe bien par la classe ExceptionInterceptor (trace [ExceptionInterceptor] - CAUGHT A FAULT présente). Je reçois bien une exception "Fault occurred while processing" Comment puis tracer ma réponse SOAP ? Par avance, merci pour votre aide
  • En ce qui concerne la trace de l'enveloppe SOAP pour l'interceptor ExceptionInterceptor, je me suis "inspiré" de l'intercepteur XmlOutInterceptor et en modifiant la phase PRE_LOGICAL par PRE_STREAM (Cf. http://www.mastertheboss.com/web-interfaces/337-apache-cxf-interceptors.html?showall=1).
  • Utilisez SOAPUi pour faire vos appels et vois les messages SOAP échangés.
  • Merci pour cet article : ce que j'en retiens : Les intercepteurs CXF sont puissants MAIS sont complétements "pourri" en terme de limpidité de code et de documentation. On veut juste récupérer cette fichu requête xml soap (le message) et on doit fouiller dans 10 objets pour récupérer la requête ....
    1. Laisser un commentaire

      Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *


      Ce formulaire est protégé par Google Recaptcha