
С недавним выпуском Apache Camel 2.14 и моим успехом с его использованием я начал с копирования своего проекта Apache Camel / CXF / Spring Boot и обрезки его до самого необходимого. Я собрал простой сервис Hello World, используя Camel и Spring MVC. Я также интегрировал Swagger в оба. Обе реализации было довольно легко создать ( пример кода ), но я решил использовать Spring MVC. Мои причины были просты: поддержка REST была более зрелой, я хорошо это знал, а Spring MVC Test упрощает тестирование API.
Поддержка Camel Swagger без web.xml
В рамках вышеупомянутого всплеска я узнал, как настроить поддержку Camel REST и Swagger с использованием SpringConfig Java и отсутствия web.xml. Я сделал это в качестве примера проекта и поместил его на GitHub как camel-rest-swagger .
В этой статье показано, как я создал REST API с использованием Java 8, Spring Boot / MVC, JAXB и Spring Data (компоненты JPA и REST). Я несколько раз споткнулся при разработке этого проекта, но понял, как преодолеть все препятствия. Я надеюсь, что это поможет команде, которая сейчас поддерживает этот проект (мой последний день был в пятницу), и тем, кто пытается сделать что-то подобное.
XML в Java с помощью JAXB
Данные, которые нам нужно было получить от третьих лиц, основывались на стандартах NCPDP Как участник, мы смогли загрузить ряд файлов XSD, поместить их в наш проект и сгенерировать классы Java для обработки входящих / исходящих запросов. Я использовал maven-jaxb2-plugin для генерации классов Java.
<plugin>
<groupId>org.jvnet.jaxb2.maven2</groupId>
<artifactId>maven-jaxb2-plugin</artifactId>
<version>0.8.3</version>
<executions>
<execution>
<goals>
<goal>generate</goal>
</goals>
<configuration>
<args>
<arg>-XtoString</arg>
<arg>-Xequals</arg>
<arg>-XhashCode</arg>
<arg>-Xcopyable</arg>
</args>
<plugins>
<plugin>
<groupId>org.jvnet.jaxb2_commons</groupId>
<artifactId>jaxb2-basics</artifactId>
<version>0.6.4</version>
</plugin>
</plugins>
<schemaDirectory>src/main/resources/schemas/ncpdp</schemaDirectory>
</configuration>
</execution>
</executions>
</plugin>
Первая ошибка, с которой я столкнулся, касалась уже определенного свойства.
[INFO] --- maven-jaxb2-plugin:0.8.3:generate (default) @ spring-app ---
[ERROR] Error while parsing schema(s).Location [ file:/Users/mraible/dev/spring-app/src/main/resources/schemas/ncpdp/structures.xsd{1811,48}].
com.sun.istack.SAXParseException2; systemId: file:/Users/mraible/dev/spring-app/src/main/resources/schemas/ncpdp/structures.xsd;
lineNumber: 1811; columnNumber: 48; Property "MultipleTimingModifierAndTimingAndDuration" is already defined.
Use <jaxb:property> to resolve this conflict.
at com.sun.tools.xjc.ErrorReceiver.error(ErrorReceiver.java:86)
Я смог обойти это путем обновления до версии 0.9.1 maven-jaxb2-plugin. Я создал контроллер и заглушил ответ жестко закодированными данными. Я подтвердил, что входящая сортировка XML-Java сработала, протестировав образец запроса, предоставленного нашим сторонним клиентом. Я начал с curl команды, потому что она была простой в использовании и могла запускаться любым пользователем с установленным файлом и curl.
curl -X POST -H 'Accept: application/xml' -H 'Content-type: application/xml' \
--data-binary @sample-request.xml http://localhost:8080/api/message -v
Это когда я столкнулся с другим камнем преткновения: ответ не был правильно направлен обратно в XML. После некоторого исследования я обнаружил, что это было вызвано отсутствием @XmlRootElement аннотаций на моих сгенерированных классах. Я отправил вопрос в Stack Overflow под названием « Возвращение сгенерированных JAXB элементов из Spring Boot Controller» . Пару дней ударившись головой о стену, я нашел решение .
Я создал файл bindings.xjb в том же каталоге, что и мои схемы. Это заставляет JAXB генерировать @XmlRootElement на классах.
<?xml version="1.0"?>
<jxb:bindings version="1.0"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:jxb="http://java.sun.com/xml/ns/jaxb"
xmlns:xjc="http://java.sun.com/xml/ns/jaxb/xjc"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/jaxb http://java.sun.com/xml/ns/jaxb/bindingschema_2_0.xsd">
<jxb:bindings schemaLocation="transport.xsd" node="/xsd:schema">
<jxb:globalBindings>
<xjc:simple/>
</jxb:globalBindings>
</jxb:bindings>
</jxb:bindings>
Чтобы добавить префиксы пространств имен в возвращаемый XML, мне пришлось изменить плагин maven-jaxb2-, чтобы добавить пару аргументов.
<arg>-extension</arg>
<arg>-Xnamespace-prefix</arg>
И добавьте зависимость:
<dependencies>
<dependency>
<groupId>org.jvnet.jaxb2_commons</groupId>
<artifactId>jaxb2-namespace-prefix</artifactId>
<version>1.1</version>
</dependency>
</dependencies>
Затем я изменил, bindings.xjbчтобы включить параметры пакета и префикса. Я также перешел <xjc:simple/>в глобальную обстановку. В конце концов мне пришлось добавить префиксы для всех схем и их пакетов.
<?xml version="1.0"?>
<bindings version="2.0" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="http://java.sun.com/xml/ns/jaxb"
xmlns:xjc="http://java.sun.com/xml/ns/jaxb/xjc" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:namespace="http://jaxb2-commons.dev.java.net/namespace-prefix"
xsi:schemaLocation="http://java.sun.com/xml/ns/jaxb http://java.sun.com/xml/ns/jaxb/bindingschema_2_0.xsd
http://jaxb2-commons.dev.java.net/namespace-prefix http://java.net/projects/jaxb2-commons/sources/svn/content/namespace-prefix/trunk/src/main/resources/prefix-namespace-schema.xsd">
<globalBindings>
<xjc:simple/>
</globalBindings>
<bindings schemaLocation="transport.xsd" node="/xsd:schema">
<schemaBindings>
<package name="org.ncpdp.schema.transport"/>
</schemaBindings>
<bindings>
<namespace:prefix name="transport"/>
</bindings>
</bindings>
</bindings>
Я научился добавлять префиксы на странице плагинов namespace-prefix .
Наконец, я настроил процесс генерации кода для генерации Joda-Time DateTime вместо значения по умолчанию XMLGregorianCalendar. Это включало пару пользовательских XmlAdapters и пару дополнительных строк bindings.xjb. Вы можете увидеть адаптеры и bindings.xjb все необходимые префиксы в этой сути . Настроить привязки JAXB Николаса Френкеля было отличным ресурсом для выполнения всей этой работы.
Я написал тест, чтобы доказать, что интерфейс API работает так, как нужно.
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = Application.class)
@WebAppConfiguration
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS)
public class InitiateRequestControllerTest {
@Inject
private InitiateRequestController controller;
private MockMvc mockMvc;
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
this.mockMvc = MockMvcBuilders.standaloneSetup(controller).build();
}
@Test
public void testGetNotAllowedOnMessagesAPI() throws Exception {
mockMvc.perform(get("/api/initiate")
.accept(MediaType.APPLICATION_XML))
.andExpect(status().isMethodNotAllowed());
}
@Test
public void testPostPaInitiationRequest() throws Exception {
String request = new Scanner(new ClassPathResource("sample-request.xml").getFile()).useDelimiter("\\Z").next();
mockMvc.perform(post("/api/initiate")
.accept(MediaType.APPLICATION_XML)
.contentType(MediaType.APPLICATION_XML)
.content(request))
.andExpect(status().isOk())
.andExpect(content().contentType(MediaType.APPLICATION_XML))
.andExpect(xpath("/Message/Header/To").string("3rdParty"))
.andExpect(xpath("/Message/Header/SenderSoftware/SenderSoftwareDeveloper").string("HID"))
.andExpect(xpath("/Message/Body/Status/Code").string("010"));
}
}
Весенние данные для JPA и REST
С JAXB я перешел к созданию внутреннего API, который мог бы использоваться другим приложением. Весенние Данные были свежи в моей памяти после чтения об этом прошлым летом. Я создал классы для сущностей, которые я хотел сохранить, используя @Data Lombok, чтобы уменьшить шаблон.
Я прочитал руководство « Доступ к данным с помощью JPA» , создал пару репозиториев и написал несколько тестов, чтобы доказать, что они работают. Я столкнулся с проблемой, пытаясь сохранить DateTime Джоды и обнаружил, что Jadira предоставила решение.
Я добавил его usertype.core в качестве зависимости к моему pom.xml:
<dependency>
<groupId>org.jadira.usertype</groupId>
<artifactId>usertype.core</artifactId>
<version>3.2.0.GA</version>
</dependency>
… и аннотированные переменные DateTime соответственно.
@Column(name = "last_modified", nullable = false)
@Type(type="org.jadira.usertype.dateandtime.joda.PersistentDateTime")
private DateTime lastModified;
Работая с JPA, я переключился на выставление конечных точек REST. В качестве руководства я использовал Доступ к данным JPA с помощью REST и за считанные минуты посмотрел на JSON в своем браузере. Я был удивлен, увидев рядом с собой службу «Профиль», и отправил вопрос команде Spring Boot. Оливер Гирке дал отличный ответ .
развязность
Интеграция Spring MVC для Swagger значительно улучшилась с тех пор, как я в последний раз писал об этом . Теперь вы можете включить его с @EnableSwagger аннотацией. Ниже приведен SwaggerConfig класс, который я использовал для настройки Swagger и чтения свойств application.yml.
@Configuration
@EnableSwagger
public class SwaggerConfig implements EnvironmentAware {
public static final String DEFAULT_INCLUDE_PATTERN = "/api/.*";
private RelaxedPropertyResolver propertyResolver;
@Override
public void setEnvironment(Environment environment) {
this.propertyResolver = new RelaxedPropertyResolver(environment, "swagger.");
}
/**
* Swagger Spring MVC configuration
*/
@Bean
public SwaggerSpringMvcPlugin swaggerSpringMvcPlugin(SpringSwaggerConfig springSwaggerConfig) {
return new SwaggerSpringMvcPlugin(springSwaggerConfig)
.apiInfo(apiInfo())
.genericModelSubstitutes(ResponseEntity.class)
.includePatterns(DEFAULT_INCLUDE_PATTERN);
}
/**
* API Info as it appears on the swagger-ui page
*/
private ApiInfo apiInfo() {
return new ApiInfo(
propertyResolver.getProperty("title"),
propertyResolver.getProperty("description"),
propertyResolver.getProperty("termsOfServiceUrl"),
propertyResolver.getProperty("contact"),
propertyResolver.getProperty("license"),
propertyResolver.getProperty("licenseUrl"));
}
}
После того как Swagger заработал, я обнаружил, что опубликованные конечные точки @RepositoryRestResource не собраны Swagger. Существует открытая проблема поддержки Spring Data в проекте swagger-springmvc.
Liquibase Integration
Я настроил этот проект для использования H2 в разработке и PostgreSQL в производстве. Для этого я использовал профили Spring и скопировал XML / YAML (для файлов Maven и приложений * .yml) из ранее созданного проекта JHipster .
Далее мне нужно было создать базу данных. Я решил использовать Liquibase для создания таблиц, а не экспорт схемы Hibernate. Я выбрал LiquiBase над Flyway на основе дискуссий в проекте JHipster . Использовать Liquibase с Spring Boot очень просто: добавьте следующую зависимость в pom.xml, а затем поместите файлы изменений в src/main/resources/db/changelog.
<dependency>
<groupId>org.liquibase</groupId>
<artifactId>liquibase-core</artifactId>
</dependency>
Я начал с использования схемы-экспорта Hibernate и переключился hibernate.ddl-auto на «create-drop» application-dev.yml. Я также прокомментировал зависимость между жидкостью и ядром. Затем я установил базу данных PostgreSQL и запустил приложение с помощью «mvn spring-boot: run -Pprod».
Я создал список изменений liquibase из существующей схемы, используя следующую команду (после загрузки и установки Liquibase).
liquibase --driver=org.postgresql.Driver --classpath="/Users/mraible/.m2/repository/org/postgresql/postgresql/9.3-1102-jdbc41/postgresql-9.3-1102-jdbc41.jar:/Users/mraible/snakeyaml-1.11.jar" --changeLogFile=/Users/mraible/dev/spring-app/src/main/resources/db/changelog/db.changelog-02.yaml --url="jdbc:postgresql://localhost:5432/mydb" --username=user --password=pass generateChangeLog
Я нашел одну ошибку — команда generateChangeLog генерирует слишком много ограничений в версии 3.2.2 . Мне удалось это исправить, вручную отредактировав сгенерированный файл YAML.
Совет: Если вы хотите удалить все таблицы в вашей базе данных, чтобы убедиться, что создание Liquibase работает в PostgeSQL, выполните следующие команды:
psql -d mydb
drop schema public cascade;
create schema public;
После написания минимального кода для Spring Data и настройки Liquibase для создания таблиц / отношений я немного расслабился, задокументировал, как все работает, и добавил LoggingFilter . LoggingFilter был удобен для просмотра запросов и ответов API.
@Bean
public FilterRegistrationBean loggingFilter() {
LoggingFilter filter = new LoggingFilter();
FilterRegistrationBean registrationBean = new FilterRegistrationBean();
registrationBean.setFilter(filter);
registrationBean.setUrlPatterns(Arrays.asList("/api/*"));
return registrationBean;
}
Доступ к API с помощью RestTemplate
Последний шаг, который мне нужно было сделать, это выяснить, как получить доступ к моему новому и интересному API с помощью RestTemplate . Сначала я думал, что это будет легко. Затем я понял, что Spring Data создает HAL- совместимый API, поэтому его содержимое встроено в «_embedded» ключ JSON.
После долгих проб и ошибок я обнаружил, что мне нужно создать шаблон RestTemplate с поддержкой HAL и Joda-Time.
@Bean
public RestTemplate restTemplate() {
ObjectMapper mapper = new ObjectMapper();
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
mapper.registerModule(new Jackson2HalModule());
mapper.registerModule(new JodaModule());
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
converter.setSupportedMediaTypes(MediaType.parseMediaTypes("application/hal+json"));
converter.setObjectMapper(mapper);
StringHttpMessageConverter stringConverter = new StringHttpMessageConverter();
stringConverter.setSupportedMediaTypes(MediaType.parseMediaTypes("application/xml"));
List<HttpMessageConverter<?>> converters = new ArrayList<>();
converters.add(converter);
converters.add(stringConverter);
return new RestTemplate(converters);
}
Это JodaModuleбыло обеспечено следующей зависимостью:
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-joda</artifactId>
</dependency>
После завершения настройки я смог написать MessagesApiITest интеграционный тест, который отправляет запрос и получает его с помощью API. API был защищен с помощью базовой аутентификации, поэтому мне понадобилось немного времени, чтобы понять, как заставить это работать с RestTemplate. Базовая аутентификация Вилли Уилера с Spring RestTemplate была большой помощью.
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = IntegrationTestConfig.class)
public class MessagesApiITest {
private final static Log log = LogFactory.getLog(MessagesApiITest.class);
@Value("http://${app.host}/api/initiate")
private String initiateAPI;
@Value("http://${app.host}/api/messages")
private String messagesAPI;
@Value("${app.host}")
private String host;
@Inject
private RestTemplate restTemplate;
@Before
public void setup() throws Exception {
String request = new Scanner(new ClassPathResource("sample-request.xml").getFile()).useDelimiter("\\Z").next();
ResponseEntity<org.ncpdp.schema.transport.Message> response = restTemplate.exchange(getTestUrl(initiateAPI),
HttpMethod.POST, getBasicAuthHeaders(request), org.ncpdp.schema.transport.Message.class,
Collections.emptyMap());
assertEquals(HttpStatus.OK, response.getStatusCode());
}
@Test
public void testGetMessages() {
HttpEntity<String> request = getBasicAuthHeaders(null);
ResponseEntity<PagedResources<Message>> result = restTemplate.exchange(getTestUrl(messagesAPI), HttpMethod.GET,
request, new ParameterizedTypeReference<PagedResources<Message>>() {});
HttpStatus status = result.getStatusCode();
Collection<Message> messages = result.getBody().getContent();
log.debug("messages found: " + messages.size());
assertEquals(HttpStatus.OK, status);
for (Message message : messages) {
log.debug("message.id: " + message.getId());
log.debug("message.dateCreated: " + message.getDateCreated());
}
}
private HttpEntity<String> getBasicAuthHeaders(String body) {
String plainCreds = "user:pass";
byte[] plainCredsBytes = plainCreds.getBytes();
byte[] base64CredsBytes = Base64.encodeBase64(plainCredsBytes);
String base64Creds = new String(base64CredsBytes);
HttpHeaders headers = new HttpHeaders();
headers.add("Authorization", "Basic " + base64Creds);
headers.add("Content-type", "application/xml");
if (body == null) {
return new HttpEntity<>(headers);
} else {
return new HttpEntity<>(body, headers);
}
}
}
Чтобы Spring Data заполнил идентификатор сообщения, я создал собственный RestConfigкласс для его представления. Я научился делать это у Томми Циглера .
/**
* Used to expose ids for resources.
*/
@Configuration
public class RestConfig extends RepositoryRestMvcConfiguration {
@Override
protected void configureRepositoryRestConfiguration(RepositoryRestConfiguration config) {
config.exposeIdsFor(Message.class);
config.setBaseUri("/api");
}
}
Резюме
В этой статье объясняется, как я создал REST API с использованием JAXB, Spring Boot, Spring Data и Liquibase. Его было относительно легко построить, но потребовалось несколько хитростей, чтобы получить к нему доступ с помощью RestTemplate Spring. Выяснение того, как настроить генерацию кода JAXB, также было важно, чтобы все заработало.
Я начал разработку проекта с Spring Boot 1.1.7, но обновил его до 1.2.0.M2 после того, как обнаружил, что он поддерживает Log4j2 и настройку базового URI Spring Data REST в application.yml. Когда я передал проект моему клиенту на прошлой неделе, он использовал 1.2.0.BUILD-SNAPSHOT из-за ошибки при запуске в Tomcat .
Это был приятный проект для работы. Мне особенно понравилось, как Spring Data позволяет отображать сущности JPA в API. Spring Boot упростил настройку, и Liquibase кажется хорошим инструментом для миграции баз данных.
Если бы кто-то попросил меня разработать REST API на JVM, какие фреймворки я бы использовал? Spring Boot, Spring Data, Джексон, Joda-Time, Lombok и Liquibase. Эти фреймворки отлично сработали для меня в этом конкретном проекте.