Статьи

Создание REST API с использованием JAXB, Spring Boot и Spring Data

Если бы кто-то попросил вас разработать REST API на JVM, какие платформы вы бы использовали? Недавно мне был поручен такой проект. Мой клиент попросил меня реализовать REST API для получения запросов от сторонних разработчиков. Проект включал в себя использование XML-запросов, сохранение данных в базе данных, а затем представление данных внутреннему приложению с конечной точкой JSON. Наконец, это позволит принять запрос JSON и превратить его в запрос XML для третьей стороны.

С недавним  выпуском 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. Эти фреймворки отлично сработали для меня в этом конкретном проекте.