Вот в чем дело: я столкнулся с командами, которые при работе с технологиями SOA были втянуты в грязь из-за сложности их инструментов. Я видел это только на Java, но слышал от некоторых разработчиков на C #, что они также распознают это явление. Я хотел бы изучить альтернативный подход.
Этот подход требует более тяжелой работы, чем добавление файла WSDL (язык определения веб-сервиса. Hocus pocus) в ваш проект и автоматическое создание материала. Но это приходит с дополнительным пониманием и повышенной тестируемостью. В конце концов я понял, что это позволило мне быстрее выполнять свои задачи, несмотря на дополнительный ручной труд.
Цель этого поста (и, если вам нравится, это расширение) — изучить более простой подход к SOA в целом и к веб-сервисам в частности. Я иллюстрирую эти принципы на конкретном примере: пусть пользователи будут уведомлены, когда их валюта упадет ниже порога по отношению к доллару США. Чтобы сделать сервис технологически интересным, я буду использовать IP-адрес абонента для определения его валюты.
Шаг 1: создайте свои активные сервисы, высмеивая внешние взаимодействия
Подчеркивание активности ваших собственных сервисов может помочь вам создать интерфейсы, которые определяют ваше взаимодействие с внешними сервисами.
Тизер:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
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); } } |
Спойлер: Я недавно начал генерировать случайные тестовые данные для своих тестов с большим эффектом.
Издатель имеет ряд сервисов, которые он использует. Давайте сосредоточимся сейчас на одном сервисе: GeoLocationService.
Шаг 2: создайте тест и заглушку для каждого сервиса — начиная с geolocationservice
Тест верхнего уровня показывает, что нам нужно от каждого внешнего сервиса. Получив информацию и прочитав (да!) WSDL для службы, мы можем протестировать заглушку для службы. В этом примере мы фактически запускаем тест с использованием HTTP, запуская Jetty, встроенный в тест.
Тизер:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
|
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(); GeolocationService wsClient = new GeolocationServiceWsClient(url + "/GeoService" ); String location = wsClient.getCountryByIp( "80.203.105.247" ); assertThat(location).isEqualTo( "Norway" ); } } |
Проверьте и создайте полезную нагрузку xml
Это первый «битый» бит. Здесь я создаю полезную нагрузку XML без использования фреймворка (шпунтовый «$» — синтаксис предоставлен библиотекой JOOX, тонкой оболочкой поверх встроенных классов JAXP):
Я добавляю XSD (более сфокусированный фокус) для фактического сервиса в проект и код для проверки сообщения. Затем я начинаю создавать полезную нагрузку XML, следуя ошибкам проверки.
Тизер:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
|
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())); } } |
В этом примере я получаю небольшую помощь (и небольшую боль) от библиотеки JOOX для манипулирования XML в Java. Поскольку XML-библиотеки для Java безумны, я также отказываюсь от проверенных исключений.
Спойлер: Я вообще очень недоволен обработкой пространств имен, проверкой, XPath и проверенными исключениями во всех библиотеках XML, которые я нашел до сих пор. Поэтому я думаю о создании своего.
Конечно, вы можете использовать тот же подход с классами, которые автоматически генерируются из XSD, но я не уверен, что это действительно сильно поможет.
Поток XML через http
Встроенный в Java HttpURLConnection — это неуклюжий, но полезный способ передачи XML на сервер (если вы не используете расширенную HTTP-аутентификацию).
Тизер:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
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(); } } |
Спойлер: этот код должен быть расширен регистрацией и обработкой ошибок, а проверка должна быть перенесена в декоратор. Взяв под контроль обработку HTTP, мы можем решить большую часть того, что решают люди, покупающие ESB.
Создать заглушку и разобрать xml
Заглушка использует xpath для поиска местоположения в запросе. Он генерирует ответ почти так же, как клиент ws генерировал запрос (не показан).
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
|
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 теперь должен использоваться с реальными вещами. Давайте напишем отдельный тест, чтобы проверить это:
1
2
3
4
5
6
7
8
9
|
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 должны быть реализованы так же, как GeolocationService. Однако, поскольку мы знаем, что нам нужно только очень специфическое взаимодействие с каждой из этих служб, нам не нужно беспокоиться обо всем, что может быть отправлено или получено как часть служб SOAP. Пока мы можем выполнять работу, которая нужна бизнес-логике (CurrencyPublisher), мы готовы к работе!
Демонстрация и тестирование цепочки создания стоимости
Если мы создадим веб-интерфейс для заглушек, мы теперь сможем продемонстрировать всю цепочку создания стоимости этой услуги для наших клиентов. В моих проектах SOA некоторые сервисы, от которых мы зависим, будут доступны только в конце проекта. В этом случае мы можем использовать наши заглушки, чтобы показать, что наш сервис работает.
Спойлер: Поскольку я устал от проверки работы ручного теста цепочки создания ценности, я могу закончить тем, что создаю тест, который использует WebDriver для настройки заглушек и проверки того, что тест прошел нормально, так же, как и в ручном тесте.
Снятие перчаток в бою на соа-арене
В этой статье я показал и намекнул на более чем полдюжины методов работы с тестами, http, xml и валидацией, которые не включают в себя фреймворки, ESB или генерацию кода. Подход дает программисту 100% контроль над своим местом в экосистеме SOA. У каждой из областей есть намного больше глубины, чтобы исследовать. Дайте мне знать, если вы хотите, чтобы это было изучено.
О, и я также хотел бы, чтобы идеи по улучшению веб-сервисов использовались, так как электронная почта с валютой геолокации довольно обманчива.
Ссылка: Тизер: SOA с открытым исходным кодом от нашего партнера по JCG Йоханнеса Бродуолла в блоге Thinking Inside a Bigger Box .