Эта статья является второй в серии статей об Apache Camel и о том, как я использовал ее вместо IBM Message Broker для клиента. В первой статье « Разработка сервисов с использованием Apache Camel. Часть I: Вдохновение» описывается, почему я выбрал Camel для этого проекта.
Чтобы убедиться, что эти новые сервисы правильно заменили существующие, был использован трехэтапный подход:
- Напишите интеграционный тест, указывающий на старый сервис.
- Напишите реализацию и модульный тест, чтобы доказать, что это работает.
- Напишите интеграционный тест, указывающий на новый сервис.
Я решил начать с замены самого простого сервиса. Это была служба SOAP, которая обращалась к базе данных для получения значения на основе входного параметра. Чтобы узнать больше о Camel и о том, как он работает, я начал с примера CXF Tomcat . Я узнал, что Camel используется для обеспечения маршрутизации запросов. Используя его компонент CXF , он может легко создавать конечные точки веб-службы SOAP. Конечная точка — это просто интерфейс, и Camel позаботится о создании реализации.
Legacy Integration Test
Я начал с написания LegacyDrugServiceTests
интеграционного теста для старой наркологической службы. Я попробовал два разных способа тестирования, используя сгенерированные WSDL классы Java, а также с помощью SOAP API JAX-WS. Найти WSDL для устаревшей службы было сложно, поскольку IBM Message Broker не раскрывает его при добавлении «? Wsdl» в URL-адрес службы. Вместо этого мне пришлось копаться в файлах проекта, пока я не нашел его. Затем я использовал cxf-codegen-plugin для генерации клиента веб-сервиса. Ниже показано, как выглядит один из тестов, использующий API-интерфейс JAX-WS.
@Test public void sendGPIRequestUsingSoapApi() throws Exception { SOAPElement bodyChildOne = getBody(message).addChildElement("gpiRequest", "m"); SOAPElement bodyChildTwo = bodyChildOne.addChildElement("args0", "m"); bodyChildTwo.addChildElement("NDC", "ax22").addTextNode("54561237201"); SOAPMessage reply = connection.call(message, getUrlWithTimeout(SERVICE_NAME)); if (reply != null) { Iterator itr = reply.getSOAPBody().getChildElements(); Map resultMap = TestUtils.getResults(itr); assertEquals("66100525123130", resultMap.get("GPI")); } }
Внедрение наркологической службы
В прошлой статье я упоминал, что не хотел XML в проекте. Для этого я использовал Java DSL Camel для определения маршрутов и JavaConfig от Spring для настройки зависимостей.
Первым маршрутом, который я написал, был поиск GPI (общего идентификатора продукта) по NDC (Национальный кодекс по лекарственным средствам).
@WebService public interface DrugService { @WebMethod(operationName = "gpiRequest") GpiResponse findGpiByNdc(GpiRequest request); }
Чтобы представить это как конечную точку веб-службы с CXF, мне нужно было сделать две вещи:
- Скажите Spring, как настроить CXF, импортировав «classpath: META-INF / cxf / cxf.xml» в класс @Configuration.
- Настройте сервлет CXF, чтобы конечные точки могли обслуживаться по определенному URL-адресу.
Чтобы удовлетворить пункт № 1, я создал CamelConfig
класс, который расширяет CamelConfiguration . Этот класс позволяет настраивать Camel с помощью SpringConfig. В нем я импортировал конфигурацию CXF, позволил динамически настраивать трассировку и выставил свой application.properties
верблюд. Я также настроил (с @ComponentScan
), чтобы искать маршруты Camel, отмеченные @Component
.
@Configuration @ImportResource("classpath:META-INF/cxf/cxf.xml") @ComponentScan("com.raibledesigns.camel") public class CamelConfig extends CamelConfiguration { @Value("${logging.trace.enabled}") private Boolean tracingEnabled; @Override protected void setupCamelContext(CamelContext camelContext) throws Exception { PropertiesComponent pc = new PropertiesComponent(); pc.setLocation("classpath:application.properties"); camelContext.addComponent("properties", pc); // see if trace logging is turned on if (tracingEnabled) { camelContext.setTracing(true); } super.setupCamelContext(camelContext); } @Bean public Tracer camelTracer() { Tracer tracer = new Tracer(); tracer.setTraceExceptions(false); tracer.setTraceInterceptors(true); tracer.setLogName("com.raibledesigns.camel.trace"); return tracer; } }
CXF имеет сервлет, который отвечает за обслуживание своих служб по общему пути. Чтобы отобразить сервлет CXF, я использовал SpringApplicationInitializer в AppInitializer
классе. Я решил обслужить все с /api/*
базового URL.
package com.raibledesigns.camel.config; import org.apache.cxf.transport.servlet.CXFServlet; import org.springframework.web.WebApplicationInitializer; import org.springframework.web.context.ContextLoaderListener; import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; import javax.servlet.ServletContext; import javax.servlet.ServletException; import javax.servlet.ServletRegistration; public class AppInitializer implements WebApplicationInitializer { @Override public void onStartup(ServletContext servletContext) throws ServletException { servletContext.addListener(new ContextLoaderListener(getContext())); ServletRegistration.Dynamic servlet = servletContext.addServlet("CXFServlet", new CXFServlet()); servlet.setLoadOnStartup(1); servlet.setAsyncSupported(true); servlet.addMapping("/api/*"); } private AnnotationConfigWebApplicationContext getContext() { AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext(); context.setConfigLocation("com.raibledesigns.camel.config"); return context; } }
Чтобы реализовать этот веб-сервис с Camel, я создал DrugRoute
класс, который расширяет Camel RouteBuilder
.
@Component
public class DrugRoute extends RouteBuilder {
private String uri = "cxf:/drugs?serviceClass=" + DrugService.class.getName();
@Override
public void configure() throws Exception {
from(uri)
.recipientList(simple("direct:${header.operationName}"));
from("direct:gpiRequest").routeId("gpiRequest")
.process(new Processor() {
public void process(Exchange exchange) throws Exception {
// get the ndc from the input
String ndc = exchange.getIn().getBody(GpiRequest.class).getNDC();
exchange.getOut().setBody(ndc);
}
})
.to("sql:{{sql.selectGpi}}")
.to("log:output")
.process(new Processor() {
public void process(Exchange exchange) throws Exception {
// get the gpi from the input
List<HashMap> data = (ArrayList<HashMap>) exchange.getIn().getBody();
DrugInfo drug = new DrugInfo();
if (data.size() > 0) {
drug = new DrugInfo(String.valueOf(data.get(0).get("GPI")));
}
GpiResponse response = new GpiResponse(drug);
exchange.getOut().setBody(response);
}
});
}
}
sql.selectGpi
Свойство читается src/main/resources/application.properties
и выглядит следующим образом :
sql.selectGpi=select GPI from drugs where ndc = #?dataSource=ds.drugs
Ссылка «ds.drugs» относится к источнику данных, который создан Spring. Из моего AppConfig
класса:
@Configuration @PropertySource("classpath:application.properties") public class AppConfig { @Value("${ds.driver.db2}") private String jdbcDriverDb2; @Value("${ds.password}") private String jdbcPassword; @Value("${ds.url}") private String jdbcUrl; @Value("${ds.username}") private String jdbcUsername; @Bean(name = "ds.drugs") public DataSource drugsDataSource() { return createDataSource(jdbcDriverDb2, jdbcUsername, jdbcPassword, jdbcUrl); } private BasicDataSource createDataSource(String driver, String username, String password, String url) { BasicDataSource ds = new BasicDataSource(); ds.setDriverClassName(driver); ds.setUsername(username); ds.setPassword(password); ds.setUrl(url); ds.setMaxActive(100); ds.setMaxWait(1000); ds.setPoolPreparedStatements(true); return ds; } }
Модульное тестирование
Самым сложным в модульном тестировании этого маршрута было выяснить, как использовать поддержку тестирования Camel . Я отправил вопрос в список рассылки пользователей Camel в начале июня. Основываясь на полученных советах, я купил Camel в действии , прочитал главу 6 о тестировании и пошел на работу. Я хотел устранить зависимость от источника данных, поэтому я использовал функцию AdviceWith компании Camel, чтобы изменить мой маршрут и перехватить вызов SQL. Это позволило мне вернуть заранее определенные результаты и убедиться, что все работает.
@RunWith(CamelSpringJUnit4ClassRunner.class)
@ContextConfiguration(loader = CamelSpringDelegatingTestContextLoader.class, classes = CamelConfig.class)
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD)
@UseAdviceWith
public class DrugRouteTests {
@Autowired
CamelContext camelContext;
@Produce
ProducerTemplate template;
@EndpointInject(uri = "mock:result")
MockEndpoint result;
static List<Map> results = new ArrayList<Map>() {{
add(new HashMap<String, String>() {{
put("GPI", "123456789");
}});
}};
@Before
public void before() throws Exception {
camelContext.setTracing(true);
ModelCamelContext context = (ModelCamelContext) camelContext;
RouteDefinition route = context.getRouteDefinition("gpiRequest");
route.adviceWith(context, new RouteBuilder() {
@Override
public void configure() throws Exception {
interceptSendToEndpoint("sql:*").skipSendToOriginalEndpoint().process(new Processor() {
@Override
public void process(Exchange exchange) throws Exception {
exchange.getOut().setBody(results);
}
});
}
});
route.to(result);
camelContext.start();
}
@Test
public void testMockSQLEndpoint() throws Exception {
result.expectedMessageCount(1);
GpiResponse expectedResult = new GpiResponse(new DrugInfo("123456789"));
result.allMessages().body().contains(expectedResult);
GpiRequest request = new GpiRequest();
request.setNDC("123");
template.sendBody("direct:gpiRequest", request);
MockEndpoint.assertIsSatisfied(camelContext);
}
}
Я обнаружил, что AdviceWith чрезвычайно полезен, так как я разработал больше маршрутов и тестов в этом проекте. Я использовал его функцию weaveById для перехвата вызовов хранимых процедур, замены шагов в моих маршрутах и удаления шагов, которые я не хотел тестировать. Например, на одном маршруте был сложный рабочий процесс для взаимодействия с данными клиента.
- Вызовите хранимую процедуру в удаленной базе данных, которая затем вставит запись во временную таблицу.
- Найдите эти данные, используя значение, возвращенное хранимой процедурой.
- Удалить запись из временной таблицы.
- Разбор данных (как CSV), поскольку возвращаемое значение разделено ~.
- Преобразуйте проанализированные данные в объекты, затем выполните вставку базы данных в локальную базу данных (если данные не существуют).
Что еще хуже, удаленный доступ к базе данных был ограничен IP-адресом. Это означало, что во время разработки я не мог даже вручную тестировать с моей локальной машины. Чтобы решить эту проблему, я использовал следующее:
interceptSendToEndpoint("bean:*")
перехватить вызов моего бина хранимой процедуры.weaveById("myJdbcProcessor").before()
заменить поиск временной таблицы на файл CSV.- Mockito, чтобы издеваться над JdbcTemplate, который делает вставки.
Чтобы выяснить, как настроить и выполнить хранимые процедуры в маршруте, я использовал проект camel-store-процедур на GitHub . Mockito в ArgumentCaptor также стал очень полезным при разработке маршрута , который называется веб — службы третьей стороной в рамках маршрута. Джеймс Карр имеет больше информации о том, как вы можете использовать это для проверки значений аргумента.
Чтобы проверить, затрагивают ли мои тесты все аспекты кода, я интегрировал модуль cobertura-maven-plugin для отчетов о покрытии кода (генерируемых при запуске mvn site
).
<build> <plugins> <plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>cobertura-maven-plugin</artifactId> <configuration> <instrumentation> <excludes> <exclude>**/model/*.class</exclude> <exclude>**/AppInitializer.class</exclude> <exclude>**/StoredProcedureBean.class</exclude> <exclude>**/SoapActionInterceptor.class</exclude> </excludes> </instrumentation> <check/> </configuration> <version>2.6</version> </plugin> ... <reporting> <plugins> <plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>cobertura-maven-plugin</artifactId> <version>2.6</version> </plugin>
Интеграционное тестирование
Написание интеграционного теста было довольно простым. Я создал DrugRouteITest
класс, клиент, использующий JaxWsProxyFactoryBean в CXF, и вызвал метод службы.
public class DrugRouteITest { private static final String URL = "http://localhost:8080/api/drugs"; protected static DrugService createCXFClient() { JaxWsProxyFactoryBean factory = new JaxWsProxyFactoryBean(); factory.setBindingId("http://schemas.xmlsoap.org/wsdl/soap12/"); factory.setServiceClass(DrugService.class); factory.setAddress(getTestUrl(URL)); return (DrugService) factory.create(); } @Test public void findGpiByNdc() throws Exception { // create input parameter GpiRequest input = new GpiRequest(); input.setNDC("54561237201"); // create the webservice client and send the request DrugService client = createCXFClient(); GpiResponse response = client.findGpiByNdc(input); assertEquals("66100525123130", response.getDrugInfo().getGPI()); } }
Этот интеграционный тест запускается только после запуска Tomcat и развертывания приложения. Модульные тесты запускаются с помощью плагина Maven surefire , а интеграционные тесты — с помощью плагина failsafe . Доступный порт Tomcat определяется модулем build-helper-maven-plugin . Этот порт устанавливается как системное свойство и читается с помощью getTestUrl()
вызова метода выше.
public static String getTestUrl(String url) { if (System.getProperty("tomcat.http.port") != null) { url = url.replace("8080", System.getProperty("tomcat.http.port")); } return url; }
Ниже приведены соответствующие биты, pom.xml
которые определяют, когда запускать / останавливать Tomcat, а также какие тесты запускать.
<plugin> <groupId>org.apache.tomcat.maven</groupId> <artifactId>tomcat7-maven-plugin</artifactId> <version>2.2</version> <configuration> <path>/</path> </configuration> <executions> <execution> <id>start-tomcat</id> <phase>pre-integration-test</phase> <goals> <goal>run</goal> </goals> <configuration> <fork>true</fork> <port>${tomcat.http.port}</port> </configuration> </execution> <execution> <id>stop-tomcat</id> <phase>post-integration-test</phase> <goals> <goal>shutdown</goal> </goals> </execution> </executions> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <version>2.17</version> <configuration> <excludes> <exclude>**/*IT*.java</exclude> <exclude>**/Legacy**.java</exclude> </excludes> <includes> <include>**/*Tests.java</include> <include>**/*Test.java</include> </includes> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-failsafe-plugin</artifactId> <version>2.17</version> <configuration> <includes> <include>**/*IT*.java</include> </includes> <systemProperties> <tomcat.http.port>${tomcat.http.port}</tomcat.http.port> </systemProperties> </configuration> <executions> <execution> <goals> <goal>integration-test</goal> <goal>verify</goal> </goals> </execution> </executions> </plugin>
Самая полезная часть интеграционного тестирования пришла, когда я скопировал в него один из своих старых тестов и начал проверять обратную совместимость. Поскольку мы хотели заменить существующие сервисы и не требовать изменений клиента, мне пришлось сопоставить запрос XML и ответ. Чарльз был очень полезен в этом упражнении, позволив мне проверить запрос / ответ и настроить его на соответствие. Следующие аннотации JAX-WS позволили мне изменить имена элементов XML и добиться обратной совместимости.
@BindingType(SOAPBinding.SOAP12HTTP_BINDING)
@WebResult(name = "return", targetNamespace = "...")
@ResponseWrapper(localName = "gpiResponse")
@WebParam(name = "args0", targetNamespace = "...")
@XmlElement(name = "...")
Непрерывная интеграция и развертывание
Следующим моим бизнесом была настройка работы в Дженкинсе для постоянного тестирования и развертывания. Пройти все тесты было легко, а развертывание в Tomcat было достаточно простым благодаря плагину Deploy и этой статье . Однако после нескольких развертываний Tomcat выдаст исключения OutOfMemory. Поэтому я закончил тем, что создал второе задание «развернуть», которое останавливает Tomcat, копирует успешно построенную WAR в $ CATALINA_HOME / webapps, удаляет $ CATALINA_HOME / webapps / ROOT и перезапускает Tomcat. Я использовал Jenkins «Execute shell» для настройки этих трех шагов. Я был рад обнаружить, что мой /etc/init.d/tomcat
скрипт все еще работает для запуска Tomcat во время загрузки и предоставляет удобные команды запуска / остановки.
Резюме
В этой статье показано, как я реализовал и протестировал простой маршрут Apache Camel. Описанный маршрут выполняет только простой поиск в базе данных, но вы можете увидеть, как поддержка тестирования Camel позволяет имитировать результаты и сосредоточиться на разработке логики маршрута. Я нашел его среду тестирования очень полезной и плохо документированной, так что, надеюсь, эта статья поможет это исправить. В следующей статье я расскажу об обновлении до Spring 4, интеграции Spring Boot и обсуждении развертывания микросервисов в нашей команде.