Статьи

Как создать динамический HTTP-прокси с мулом

В этой статье я покажу концептуальное подтверждение динамического HTTP-прокси, реализованного с помощью Mule.

Принцип

Прокси-сервер перенаправляет HTTP-запрос, используя контекстную и относительную части пути URL-адреса запроса, чтобы определить сервер и порт, на который должен быть перенаправлен запрос.
В примере из этой статьи будет развернута веб-служба SOAP для прослушивания следующего URL:

http://localhost:8182/services/GreetingService

В приведенном выше URL-адресе сервер и порт — localhost: 8182, части контекста и относительного пути URL-адреса — «services / GreetingService».
Программа-пример будет развернута для прослушивания запросов по следующему URL:

http://localhost:8981/dynamicHttpProxy/

Чтобы вызвать GreetingService через HTTP-прокси, URL-адрес конечной точки будет выглядеть следующим образом:

http://localhost:8981/dynamicHttpProxy/services/GreetingService

мотивация

Основной мотивацией для динамического прокси-сервера HTTP является возможность добавления новых прокси-серверов HTTP с минимальными усилиями и без перезапуска прокси-сервера.

Ограничения примера программы

Недостатком примера программы для ее использования в производственной среде являются:

  • Обработка ошибок.
  • Извлечение конфигурации из базы данных.
    В этом примере простая карта используется для хранения сопоставления между относительным путем HTTP и сервером назначения. Это, конечно, не позволяет динамически изменять конфигурацию прокси.
  • Поддержка дополнительных HTTP-глаголов.
    В примере программы была реализована поддержка только HTTP-глаголов GET и POST. Тривиально добавлять поддержку дополнительных HTTP-глаголов по мере необходимости.
  • Обработка параметров HTTP.
    Программа-пример не учитывает параметры HTTP, но считается, что они являются частью относительного пути HTTP.
  • Поддержка HTTPS.

Вероятно, есть дополнительные вещи, которых можно было бы считать недостающими — не стесняйтесь добавлять предложения в комментарии!

Сервис для прокси

Пример программы будет реализован в проекте Mule в SpringSource Tool Suite с установленным плагином MuleStudio. Любая основанная на Eclipse IDE с установленным плагином MuleStudio.

Чтобы иметь сервис для прокси, простой сервис приветствия SOAP реализован с использованием одного файла конфигурации Mule и одного класса Java.
Конфигурация Mule содержит следующую конфигурацию:

<?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>

Класс Java, реализующий сервис, выглядит следующим образом:

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();
    }
}

Класс информационного компонента сервера

Экземпляры класса информационных компонентов сервера содержат информацию о сервере, на который следует пересылать запросы.

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;
    }
}

Причины хранения этой информации в выделенном классе bean-компонентов заключаются в том, чтобы упростить расширение класса дополнительной информацией, облегчить миграцию хранилища в базу данных и свести к минимуму различные виды данных, хранящихся в контексте Mule.

Конфигурация динамического прокси HTTP-мула

Динамическая конфигурация Mule прокси HTTP реализована следующим образом:

<?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:

soapUIModifyEndpointURL

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.