How to Create a Dynamic HTTP Proxy with Mule
Join the DZone community and get the full member experience.
Join For FreeIn this post I will show a proof-of-concept for a dynamic HTTP proxy implemeted using Mule.
Principle
The proxy forwards HTTP request using the context and relative path parts of the request URL to determine the server and port to which the request is to be forwarded.
In the example in this article a SOAP web service will be deployed to listen to the following URL:
http://localhost:8182/services/GreetingService
In the above URL, the server and port is localhost:8182, the context and relative path parts of the URL is “services/GreetingService”.
The example program will be deployed to listen to requests at the following URL:
http://localhost:8981/dynamicHttpProxy/
In order to invoke the GreetingService via the HTTP proxy, the endpoint URL will look like this:
http://localhost:8981/dynamicHttpProxy/services/GreetingService
Motivation
The main motivation for the dynamic HTTP proxy is the ability to be able to add new HTTP proxies with a minimum of effort and without having to restart the proxy.
Limitations of the Example Program
Lacking from the example program to make it useable in a production environment are:
- Error handling.
- Retrieval of configuration from database.
In the example, a simple map is used to store mapping between the HTTP relative path and the destination server. This does of course not allow for dynamically modifying the proxy configuration. - Support for additional HTTP verbs.
In the example program only support for the HTTP verbs GET and POST have been implemented. It is trivial to add support for additional HTTP verbs as needed. - Handling of HTTP parameters.
The example program does not consider HTTP parameters but these are considered to be part of the HTTP relative path. - Support for HTTPS.
There are probably additional things that one would consider lacking – feel free to add suggestions in the comments!
A Service to Proxy
The example program will be implemented in a Mule Project in SpringSource Tool Suite with the MuleStudio plug-in installed. Any Eclipse-based IDE with the MuleStudio plug-in installed.
In order to be have a service to proxy, a simple SOAP greeting-service is implemented using one Mule configuration file and one Java class.
The Mule configuration contains the following configuration:
<?xml version="1.0" encoding="UTF-8"?> <mule xmlns:cxf="http://www.mulesoft.org/schema/mule/cxf" xmlns="http://www.mulesoft.org/schema/mule/core" xmlns:doc="http://www.mulesoft.org/schema/mule/documentation" xmlns:spring="http://www.springframework.org/schema/beans" xmlns:test="http://www.mulesoft.org/schema/mule/test" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=" http://www.mulesoft.org/schema/mule/cxf http://www.mulesoft.org/schema/mule/cxf/current/mule-cxf.xsd http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-current.xsd http://www.mulesoft.org/schema/mule/core http://www.mulesoft.org/schema/mule/core/current/mule.xsd http://www.mulesoft.org/schema/mule/test http://www.mulesoft.org/schema/mule/test/current/mule-test.xsd"> <spring:beans> <spring:bean id="helloService" class="com.ivan.mule.dynamichttpproxy.HelloService"/> </spring:beans> <flow name="GreetingFlow"> <inbound-endpoint address="http://localhost:8182/services/GreetingService" exchange-pattern="request-response"/> <cxf:jaxws-service serviceClass="com.ivan.mule.dynamichttpproxy.HelloService"/> <component> <spring-object bean="helloService"/> </component> </flow> </mule>
The Java class implementing the service looks like this:
package com.ivan.mule.dynamichttpproxy; import java.util.Date; import javax.jws.WebParam; import javax.jws.WebResult; import javax.jws.WebService; /** * SOAP web service endpoint implementation class that implements * a service that extends greetings. * * @author Ivan Krizsan */ @WebService public class HelloService { /** * Greets the person with the supplied name. * * @param inName Name of person to greet. * @return Greeting. */ @WebResult(name = "greeting") public String greet(@WebParam(name = "inName") final String inName) { return "Hello " + inName + ", the time is now " + new Date(); } }
Server Information Bean Class
Instances of the server information bean class holds information about a server which to forward requests to.
package com.ivan.mule.dynamichttpproxy; /** * Holds information about a server which to forward requests to. * * @author Ivan Krizsan */ public class ServerInformationBean { private String serverAddress; private String serverPort; private String serverName; /** * Creates an instance holding information about a server with supplied * address, port and name. * * @param inServerAddress * @param inServerPort * @param inServerName */ public ServerInformationBean(final String inServerAddress, final String inServerPort, final String inServerName) { serverAddress = inServerAddress; serverPort = inServerPort; serverName = inServerName; } public String getServerAddress() { return serverAddress; } public String getServerPort() { return serverPort; } public String getServerName() { return serverName; } }
The reasons for storing this information in a dedicated bean class is to make it easy to extend the class with additional information, to facilitate migration of storage to a database and to keep the different kinds of data stored in the Mule context to a minimum.
Dynamic HTTP Proxy Mule Configuration
The dynamic HTTP proxy Mule configuration is implemeted as follows:
<?xml version="1.0" encoding="UTF-8"?> <!-- The dynamic HTTP proxy Mule configuration file. Author: Ivan Krizsan --> <mule xmlns:scripting="http://www.mulesoft.org/schema/mule/scripting" xmlns:http="http://www.mulesoft.org/schema/mule/http" xmlns="http://www.mulesoft.org/schema/mule/core" xmlns:doc="http://www.mulesoft.org/schema/mule/documentation" xmlns:spring="http://www.springframework.org/schema/beans" xmlns:util="http://www.springframework.org/schema/util" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:test="http://www.mulesoft.org/schema/mule/test" version="CE-3.4.0" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-current.xsd http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-current.xsd http://www.mulesoft.org/schema/mule/core http://www.mulesoft.org/schema/mule/core/current/mule.xsd http://www.mulesoft.org/schema/mule/http http://www.mulesoft.org/schema/mule/http/current/mule-http.xsd http://www.mulesoft.org/schema/mule/test http://www.mulesoft.org/schema/mule/test/current/mule-test.xsd http://www.mulesoft.org/schema/mule/scripting http://www.mulesoft.org/schema/mule/scripting/current/mule-scripting.xsd"> <spring:beans> <!-- Mappings from path to server represented by a hash map. A map has been choosen to limit the scope of this example. Storing data about mappings between path to server in a database will enable runtime modifications to the mapping data without having to stop and restart the general proxy Mule application. --> <util:map id="pathToServerAndPortMapping" map-class="java.util.HashMap"> <!-- Entry for MyServer. --> <spring:entry key="services/GreetingService"> <spring:bean class="com.ivan.mule.dynamichttpproxy.ServerInformationBean"> <spring:constructor-arg value="localhost"/> <spring:constructor-arg value="8182"/> <spring:constructor-arg value="MyServer"/> </spring:bean> </spring:entry> <!-- Entry for SomeOtherServer. --> <spring:entry key="services/GreetingService?wsdl"> <spring:bean class="com.ivan.mule.dynamichttpproxy.ServerInformationBean"> <spring:constructor-arg value="127.0.0.1"/> <spring:constructor-arg value="8182"/> <spring:constructor-arg value="SomeOtherServer"/> </spring:bean> </spring:entry> </util:map> </spring:beans> <flow name="HTTPGeneralProxyFlow"> <!-- Note that if you increase the length of the path to, for instance generalProxy/additionalPath, then the expression determining the outgoing path need to be modified accordingly. Changing the path, without changing its length, require no modification to outgoing path. --> <http:inbound-endpoint exchange-pattern="request-response" host="localhost" port="8981" path="dynamicHttpProxy" doc:name="HTTP Receiver"/> <!-- Extract outgoing path from received HTTP request. --> <set-property value="#[org.mule.util.StringUtils.substringAfter(org.mule.util.StringUtils.substringAfter(message.inboundProperties['http.request'], '/'), '/')]" propertyName="outboundPath" doc:name="Extract Outbound Path From Request" /> <logger message="#[string:Outbound path = #[message.outboundProperties['outboundPath']]]" level="DEBUG"/> <!-- Using the HTTP request path, select which server to forward the request to. Note that there should be some kind of error handling in case there is no server for the current path. Error handling has been omitted in this example. --> <enricher target="#[variable:outboundServer]"> <scripting:component doc:name="Groovy"> <!-- If storing mapping data in a database, this Groovy script should be replaced with a database query. --> <scripting:script engine="Groovy"> <![CDATA[ def theMap = muleContext.getRegistry().lookupObject("pathToServerAndPortMapping") def String theOutboundPath = message.getOutboundProperty("outboundPath") def theServerBean = theMap[theOutboundPath] theServerBean ]]> </scripting:script> </scripting:component> </enricher> <logger message="#[string:Server address = #[groovy:message.getInvocationProperty('outboundServer').serverAddress]]" level="DEBUG"/> <logger message="#[string:Server port = #[groovy:message.getInvocationProperty('outboundServer').serverPort]]" level="DEBUG"/> <logger message="#[string:Server name = #[groovy:message.getInvocationProperty('outboundServer').serverName]]" level="DEBUG"/> <!-- Log the request and its metadata for development purposes, --> <test:component logMessageDetails="true"/> <!-- Cannot use a MEL expression in the value of the method attribute on the HTTP outbound endpoints so have to revert to this way of selecting HTTP method in the outgoing request. In this example, only support for GET and POST has been implemented. This can of course easily be extended to support additional HTTP verbs as desired. --> <choice doc:name="Choice"> <!-- Forward HTTP GET requests. --> <when expression="#[message.inboundProperties['http.method']=='GET']"> <http:outbound-endpoint exchange-pattern="request-response" host="#[groovy:message.getInvocationProperty('outboundServer').serverAddress]" port="#[groovy:message.getInvocationProperty('outboundServer').serverPort]" method="GET" path="#[message.outboundProperties['outboundPath']]" doc:name="Send HTTP GET"/> </when> <!-- Forward HTTP POST requests. --> <when expression="#[message.inboundProperties['http.method']=='POST']"> <http:outbound-endpoint exchange-pattern="request-response" host="#[groovy:message.getInvocationProperty('outboundServer').serverAddress]" port="#[groovy:message.getInvocationProperty('outboundServer').serverPort]" method="POST" path="#[message.outboundProperties['outboundPath']]" doc:name="Send HTTP POST"/> </when> <!-- If HTTP method not recognized, use GET. --> <otherwise> <http:outbound-endpoint exchange-pattern="request-response" host="#[groovy:message.getInvocationProperty('outboundServer').serverAddress]" port="#[groovy:message.getInvocationProperty('outboundServer').serverPort]" method="GET" path="#[message.outboundProperties['outboundPath']]" doc:name="Default: Send HTTP GET"/> </otherwise> </choice> </flow> </mule>
Note that:
- A map named “pathToServerAndPortMapping” is configured using Spring XML.
This map contains the mapping between context and relative path of an URL to the server to which requests are to be forwarded, as discussed above. - The map contains an entry for “services/GreetingService?wsdl”.
As discussed in the section on limitations of the example program, it currently does not handle HTTP parameters. I also wanted more than one single mapping in order to make the example more interesting. - There is a <set-property> element setting the property “outboundPath” immediately after the HTTP inbound endpoint.
The slightly complicated expression in the value attribute is used to remove the context part of incoming HTTP requests. The context part of the dynamic HTTP proxy can be changed without requiring modifications of the expression. However, if you want to add another part to the URL which should not be regarded when determining which server to forward a request to, this expression need to be modified. - An <enricher> is used to retrieve the correct instance of the ServerInformationBean class.
Instead of using a Groovy script, the enricher should perform a database query.
In addition, there is no error handling for the case where there is no server information available for a particular key. - There is a <choice> element containing multiple outbound HTTP endpoints.
The outbound HTTP endpoints only differ as far as the method attribute is concerned. The reason for having to use the <choice> element and multiple HTTP outbound endpoints is that Mule does not allow for expressions to be entered in the method attribute.
Test the Example Program
The example program is now complete and can be started by right-clicking the project in the IDE and selecting Run As -> Mule Application.
When the Mule instance has started up, try issuing a reques to the following URL in a browser of your choice:
http://localhost:8182/services/GreetingService?wsdl
You should see the WSDL of the greeting service.
Using soapUI, try sending a request to the greeting service. You should receive a greeting containing the current date and time.
Next, add a new endpoint to the request in soapUI and enter the following URL:
http://localhost:8981/dynamicHttpProxy/services/GreetingService
Then send the request again from soapUI. You should receive the same kind of response as when communicating directly with the greeting service:
If examining the console log in the IDE, output similar to the following four lines should be present (if not, try changing the log level to ERROR and repeat send a request again):
... Outbound path = services/GreetingService ... Server address = localhost ... Server port = 8182 ... Server name = MyServer
In a browser, issue a request for the greeting service’s WSDL using the following URL:
http://localhost:8981/dynamicHttpProxy/services/GreetingService
The four lines of console output sawn earlier now changes to:
... Outbound path = services/GreetingService?wsdl ... Server address = localhost ... Server port = 8182 ... Server name = SomeOtherServer
From this we can see that different mappings come into effect depending on the outbound part of the URL.
Published at DZone with permission of Ivan K. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments