Эта статья является второй в серии статей об 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 и обсуждении развертывания микросервисов в нашей команде.