引子:赤手空拳的SOA


我正在研究这个想法,我不知道它是否吸引你们。我想听听你对这是否是需要进一步探索的问题的看法。

事情是这样的:我遇到过这样的团队,他们在使用SOA技术时,被工具的复杂性拖进了泥沼。我只在Java中见过这种情况,但我从一些C#开发人员那里听说他们也认识到了这种现象。我想探索另一种方法。

这种方法比添加WSDL(网络服务定义语言)需要更多的努力。Hocus pocus)文件到您的项目并自动生成素材。但是它带来了更多的理解和更强的可测试性。最终,我体会到这让我能够更快地完成任务,尽管需要额外的体力劳动。

这篇博文的目的(如果你喜欢的话,它是扩展)是探索一种更基本的方法来处理一般的SOA和具体的web服务。我用一个具体的例子来说明这些原则:当用户的货币相对于美元跌破一个阈值时,让用户得到通知。为了使这项服务在技术上更有趣,我将使用用户的IP地址来决定他们的货币。

第一步:通过模仿外部互动来创建你的主动服务

嘲笑您自己服务的活动可以帮助您构建定义您与外部服务交互的接口。

引子:

public class CurrencyPublisherTest {

    private SubscriptionRepository subscriptionRepository = mock(SubscriptionRepository.class);
    private EmailService emailService = mock(EmailService.class);
    private CurrencyPublisher publisher = new CurrencyPublisher();
    private CurrencyService currencyService = mock(CurrencyService.class);
    private GeolocationService geolocationService = mock(GeolocationService.class);

    @Test
    public void shouldPublishCurrency() throws Exception {
        Subscription subscription = TestDataFactory.randomSubscription();
        String location = TestDataFactory.randomCountry();
        String currency = TestDataFactory.randomCurrency();
        double exchangeRate = subscription.getLowLimit() * 0.9;

        when(subscriptionRepository.findPendingSubscriptions()).thenReturn(Arrays.asList(subscription));

        when(geolocationService.getCountryByIp(subscription.getIpAddress())).thenReturn(location);

        when(currencyService.getCurrency(location)).thenReturn(currency);
        when(currencyService.getExchangeRateFromUSD(currency)).thenReturn(exchangeRate);

        publisher.runPeriodically();

        verify(emailService).publishCurrencyAlert(subscription, currency, exchangeRate);
    }

    @Before
    public void setupPublisher() {
        publisher.setSubscriptionRepository(subscriptionRepository);
        publisher.setGeolocationService(geolocationService);
        publisher.setCurrencyService(currencyService);
        publisher.setEmailService(emailService);
    }
}

剧透:我最近开始在我的测试中使用随机测试数据生成,效果很好。

发行者有许多它使用的服务。现在让我们关注一个服务:地理定位服务。

步骤2:为每个服务创建一个测试和一个存根——从地理位置服务开始

顶层测试显示了我们需要从每个外部服务中得到什么。得知此事后阅读(耶!)对于服务的WSDL,我们可以测试驱动服务的存根。在这个例子中,我们实际上是通过启动嵌入在测试中的Jetty来运行测试的。

引子:
public class GeolocationServiceStubHttpTest {

    @Test
    public void shouldAnswerCountry() throws Exception {
        GeolocationServiceStub stub = new GeolocationServiceStub();
        stub.addLocation("80.203.105.247", "Norway");

        Server server = new Server(0);
        ServletContextHandler context = new ServletContextHandler();
        context.addServlet(new ServletHolder(stub), "/GeoService");
        server.setHandler(context);
        server.start();

        String url = "http://localhost:" + server.getConnectors()[0].getLocalPort();

        GeolocationService wsClient = new GeolocationServiceWsClient(url + "/GeoService");
        String location = wsClient.getCountryByIp("80.203.105.247");

        assertThat(location).isEqualTo("Norway");
    }
}

验证并创建XML负载

这是第一个“裸关节”钻头。在这里,我不使用框架来创建XML负载(groovy“$”语法是由JOOX库,内置JAXP类之上的薄包装):

我在项目和代码中添加了实际服务的XSD(更多骗人的把戏)来验证消息。然后,我开始通过跟踪验证错误来构建XML负载。

引子:
public class GeolocationServiceWsClient implements GeolocationService {

    private Validator validator;
    private UrlSoapEndpoint endpoint;

    public GeolocationServiceWsClient(String url) throws Exception {
        this.endpoint = new UrlSoapEndpoint(url);
        validator = createValidator();
    }

    @Override
    public String getCountryByIp(String ipAddress) throws Exception {
        Element request = createGeoIpRequest(ipAddress);
        Document soapRequest = createSoapEnvelope(request);
        validateXml(soapRequest);
        Document soapResponse = endpoint.postRequest(getSOAPAction(), soapRequest);
        validateXml(soapResponse);
        return parseGeoIpResponse(soapResponse);
    }

    private void validateXml(Document soapMessage) throws Exception {
        validator.validate(toXmlSource(soapMessage));
    }

    protected Validator createValidator() throws SAXException {
        SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
        Schema schema = schemaFactory.newSchema(new Source[] {
              new StreamSource(getClass().getResource("/geoipservice.xsd").toExternalForm()),
              new StreamSource(getClass().getResource("/soap.xsd").toExternalForm()),
        });
        return schema.newValidator();
    }

    private Document createSoapEnvelope(Element request) throws Exception {
        return $("S:Envelope",
                $("S:Body", request)).document();
    }

    private Element createGeoIpRequest(String ipAddress) throws Exception {
        return $("wsx:GetGeoIP", $("wsx:IPAddress", ipAddress)).get(0);
    }

    private String parseGeoIpResponse(Element response) {
        // TODO
        return null;
    }

    private Source toXmlSource(Document document) throws Exception {
        return new StreamSource(new StringReader($(document).toString()));
    }
}

在这个例子中,我从JOOXJava中的XML操作库。由于Java的XML库是疯狂的,我也放弃了检查异常。

剧透:到目前为止,我发现所有的XML库中对名称空间、验证、XPath和检查异常的处理都非常不满意。所以我在考虑创造我自己的。

当然,您可以对从XSD自动生成的类使用相同的方法,但是我不认为这真的有什么帮助。

通过超文本传输协议传输XML

Java内置的HttpURLConnection是一种笨拙但可维护的方式,可以将XML发送到服务器(只要您没有进行高级的超文本传输协议身份验证)。

引子:

public class UrlSoapEndpoint {

    private final String url;

    public UrlSoapEndpoint(String url) {
        this.url = url;
    }

    public Document postRequest(String soapAction, Document soapRequest) throws Exception {
        URL httpUrl = new URL(url);
        HttpURLConnection connection = (HttpURLConnection) httpUrl.openConnection();
        connection.setDoInput(true);
        connection.setDoOutput(true);
        connection.addRequestProperty("SOAPAction", soapAction);
        connection.addRequestProperty("Content-Type", "text/xml");
        $(soapRequest).write(connection.getOutputStream());

        int responseCode = connection.getResponseCode();
        if (responseCode != 200) {
            throw new RuntimeException("Something went terribly wrong: " + connection.getResponseMessage());
        }
        return $(connection.getInputStream()).document();
    }
}

剧透:这段代码应该用日志和错误处理来扩展,并且验证应该转移到修饰器中。通过控制超文本传输协议的处理,我们可以解决人们购买ESB要解决的大部分问题。

创建存根并解析XML

存根使用xpath来查找请求中的位置。它生成响应的方式与ws客户端生成请求的方式非常相似(未显示)。

public class GeolocationServiceStub extends HttpServlet {

    private Map<String,String> locations = new HashMap<String, String>();

    public void addLocation(String ipAddress, String country) {
        locations.put(ipAddress, country);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        try {
            String ipAddress = $(req.getReader()).xpath("/Envelope/Body/GetGeoIP/IPAddress").text();
            String location = locations.get(ipAddress);
            createResponse(location).write(resp.getOutputStream());
        } catch (Exception e) {
            throw new RuntimeException("Exception at server " + e);
        }
    }
}

剧透:存根可以扩展成一个网页,让我测试我的系统,而不需要真正集成到任何外部服务。

验证并解析响应

ws客户端现在可以验证来自存根的响应符合XSD并解析该响应。同样,这是使用XPath完成的。我不展示代码,因为它只是更多的相同。

真实的东西!

代码现在验证XML有效负载符合XSD。这意味着ws客户端现在应该可以使用真实的东西了。让我们写一个单独的测试来检查它:

 

public class GeolocationServiceLiveTest {

    @Test
    public void shouldFindLocation() throws Exception {
        GeolocationService wsClient = new GeolocationServiceWsClient("http://www.webservicex.net/geoipservice.asmx");
        assertThat(wsClient.getCountryByIp("80.203.105.247")).isEqualTo("Norway");
    }

}

耶。奏效了。实际上,我第一次尝试它就失败了,因为我没有测试的IP地址的正确国家名称。

这种点对点集成测试比我的其他单元测试更慢、更不健壮。然而,我并不认为这一事实有什么大不了。我从我的Infinitest除此之外,我并不在乎。

充实所有服务

SubscriptionRepository、CurrencyService和EmailService需要以与地理定位服务相同的方式进行充实。然而,因为我们知道我们只需要与这些服务中的每一个进行非常具体的交互,所以我们不需要担心作为SOAP服务的一部分可能被发送或接收的所有事情。只要我们能完成业务逻辑(CurrencyPublisher)需要的工作,我们就可以开始了!

演示和价值链测试

如果我们为存根创建网络用户界面,我们现在可以向我们的客户展示这项服务的整个价值链。在我的SOA项目中,我们依赖的一些服务只会在项目后期上线。在这种情况下,我们可以使用存根来显示我们的服务是有效的。

剧透:当我厌倦了验证手工价值链测试的工作时,我可能最终会创建一个使用WebDriver来建立存根并验证测试运行正常,就像我在手动测试中所做的那样。

在SOA竞技场战斗时脱下手套

在本文中,我已经展示并暗示了六种以上的技术来处理不涉及框架、ESB或代码生成的测试、http、xml和验证。这种方法让程序员100%控制他们在SOA生态系统中的位置。每一个领域都有更深入的探索。如果你想探索它,请告诉我。

哦,我也希望有更好的网络服务来使用,因为地理定位货币电子邮件是相当做作的。