Статьи

Начало работы с Джерси и Spring Boot

Spring Boot 1.2 наряду со многими новыми функциями поддерживает Джерси. Это отличный шаг для привлечения разработчиков, которым нравится стандартный подход, поскольку теперь они могут создавать API-интерфейсы RESTful с использованием спецификации JAX-RS и легко развертывать его в Tomcat или любом другом контейнере, поддерживаемом загрузкой Spring. Трикотаж с платформой Spring может сыграть важную роль в развитии микроуслуг. В этой статье я покажу, как можно быстро создать приложение, используя Spring Boot (включая: Spring Data, Spring Test, Spring Security) и Jersey.

Bootstrap новый проект

Приложение является обычным приложением Spring Boot и использует Gradle и его последнюю версию 2.2. Gradle менее многословен, чем Maven, и особенно хорош для приложений Spring Boot. Gradle можно загрузить с веб-сайта Gradle: http://www.gradle.org/downloads .

Начальные зависимости для запуска проекта:

01
02
03
04
05
06
07
08
09
10
11
12
dependencies {
    compile("org.springframework.boot:spring-boot-starter-web")
    compile("org.springframework.boot:spring-boot-starter-jersey")
    compile("org.springframework.boot:spring-boot-starter-data-jpa")
    // HSQLDB for embedded database support
    compile("org.hsqldb:hsqldb")
    // Utilities
    compile("com.google.guava:guava:18.0")
    // AssertJ
    testCompile("org.assertj:assertj-core:1.7.0")
    testCompile("org.springframework.boot:spring-boot-starter-test")
}

Точка входа приложения — это класс, содержащий метод main и он аннотируется аннотацией @SpringBootApplication :

1
2
3
4
5
6
@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

Аннотация @SpringBootApplication — это удобная аннотация, которая эквивалентна объявлению @Configuration , @EnableAutoConfiguration и @ComponentScan и является новой для Spring Boot 1.2.

Конфигурация Джерси

@Path можно так же просто, как создать корневой ресурс, аннотированный @Path и Spring @Component :

1
2
3
4
5
6
7
8
9
@Component
@Path("/health")
public class HealthController {
    @GET
    @Produces("application/json")
    public Health health() {
        return new Health("Jersey: Up and Running!");
    }
}

и зарегистрировать его в классе @Configuration , выходящем из Jersey ResourceConfig :

1
2
3
4
5
6
@Configuration
public class JerseyConfig extends ResourceConfig {
    public JerseyConfig() {
        register(HealthController.class);
    }
}

Мы можем запустить приложение с помощью gradlew bootRun : http: // localhost: 8080 / health, и мы должны увидеть следующий результат:

1
2
3
{
    "status": "Jersey: Up and Running!"
}

Но также можно написать интеграционный тест Spring Boot с полностью загруженным контекстом приложения:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = Application.class)
@WebAppConfiguration
@IntegrationTest("server.port=9000")
public class HealthControllerIntegrationTest {
 
    private RestTemplate restTemplate = new TestRestTemplate();
 
    @Test
    public void health() {
        ResponseEntity<Health> entity =
                restTemplate.getForEntity("http://localhost:9000/health", Health.class);
 
        assertThat(entity.getStatusCode().is2xxSuccessful()).isTrue();
        assertThat(entity.getBody().getStatus()).isEqualTo("Jersey: Up and Running!");
    }
}

Jersey 2.x имеет встроенную поддержку Spring ( jersey-spring3 ), а Spring Boot обеспечивает поддержку автоконфигурации для него с помощью spring-boot-starter-jersey starter. Для получения более подробной информации взгляните на класс JerseyAutoConfiguration .

В зависимости от spring.jersey.type свойства spring.jersey.type Jersey или фильтр регистрируются как Spring Bean:

1
Mapping servlet: 'jerseyServlet' to [/*]

Путь отображения по умолчанию можно изменить с помощью аннотации javax.ws.rs.ApplicationPath добавленной в класс конфигурации ResourceConfig :

1
2
3
@Configuration
@ApplicationPath("/jersey")
public class JerseyConfig extends ResourceConfig {}

Поддержка медиа-типов JSON поставляется с зависимостью jersey-media-json-jackson которая регистрирует поставщиков JSON Jackson, которые будут использоваться Джерси.

Spring Data JPA Integration

Spring Data JPA, являющаяся частью более широкого семейства Spring Data, позволяет легко внедрять репозитории на основе JPA. Для тех, кто не знаком с проектом, посетите: http://projects.spring.io/spring-data-jpa/

Клиент и Клиент Репозиторий

Модель домена для этого примера проекта — это просто Customer с некоторыми основными полями:

1
2
3
4
5
6
7
@Entity
public class Customer extends AbstractEntity {
 
    private String firstname, lastname;
 
    @Column
    private EmailAddress emailAddress;

Customer нужен @Repository , поэтому мы создали базовый с использованием репозитория данных Spring. Репозитории Spring Data сокращают большую часть стандартного кода благодаря простому определению интерфейса:

1
2
3
public interface CustomerRepository extends PagingAndSortingRepository<Customer, Long> {
 
}

При наличии модели предметной области некоторые тестовые данные могут оказаться полезными. Самый простой способ — предоставить файл data.sql с SQL-скриптом, который будет выполняться при запуске приложения. Файл помещается в src/main/resources и Spring будет автоматически подхвачен. Скрипт содержит несколько вставок SQL для заполнения таблицы customer . Например:

1
insert into customer (id, email, firstname, lastname) values (1, '[email protected]', 'Joe', 'Doe');

Контроллер клиента

Имея репозиторий Spring Data JPA, я создал контроллер (в терминах JAX-RS — ресурс), который позволяет выполнять операции CRUD над объектом Customer .

Примечание: я придерживаюсь соглашений по именованию Spring MVC для конечных точек HTTP, но не стесняюсь называть их JAX-RS.

Получить клиентов

Давайте начнем с метода, возвращающего всех клиентов:

01
02
03
04
05
06
07
08
09
10
11
12
13
@Component
@Path("/customer")
@Produces(MediaType.APPLICATION_JSON)
public class CustomerController {
 
    @Autowired
    private CustomerRepository customerRepository;
 
    @GET
    public Iterable<Customer> findAll() {
        return customerRepository.findAll();
    }
}

Использование @Component гарантирует, что CustomerController является управляемым объектом Spring. @Autowired можно легко заменить стандартным javax.inject.@Inject annotation.

Так как мы используем Spring Data в проекте, я мог бы легко использовать нумерацию страниц, предлагаемую PagingAndSortingRepository. Я изменил метод ресурса для поддержки некоторых параметров запроса страницы:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
@GET
public Page<Customer> findAll(
        @QueryParam("page") @DefaultValue("0") int page,
        @QueryParam("size") @DefaultValue("20") int size,
        @QueryParam("sort") @DefaultValue("lastname") List<String> sort,
        @QueryParam("direction") @DefaultValue("asc") String direction) {
 
    return customerRepository.findAll(
            new PageRequest(
                    page,
                    size,
                    Sort.Direction.fromString(direction),
                    sort.toArray(new String[0])
            )
    );
}

Чтобы проверить приведенный выше код, я создал интеграционный тест Spring. В первом тесте я вызову все записи, и на основе ранее подготовленных тестовых данных я рассчитываю получить 3 клиентов на 1 странице формата 20:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
@Test
public void returnsAllPages() {
    // act
    ResponseEntity<Page<Customer>> responseEntity = getCustomers(
            "http://localhost:9000/customer"
    );
    Page<Customer> customerPage = responseEntity.getBody();
    // assert
    PageAssertion.assertThat(customerPage)
            .hasTotalElements(3)
            .hasTotalPages(1)
            .hasPageSize(20)
            .hasPageNumber(0)
            .hasContentSize(3);
}

Во втором тесте я вызову страницу 0 размером 1 и сортировку по имени и направлению сортировки по descending . Я ожидаю, что общее количество элементов не изменилось (3), общее количество возвращенных страниц равно 3, а размер содержимого возвращаемой страницы равен 1:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
@Test
public void returnsCustomPage() {
 
    // act
    ResponseEntity<Page<Customer>> responseEntity = getCustomers(
    );
    // assert
    Page<Customer> customerPage = responseEntity.getBody();
 
    PageAssertion.assertThat(customerPage)
            .hasTotalElements(3)
            .hasTotalPages(3)
            .hasPageSize(1)
            .hasPageNumber(0)
            .hasContentSize(1);
}

Код также можно проверить с помощью curl :

1
2
3
4
5
6
7
8
9
$ curl -i http://localhost:8080/customer
 
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Content-Type: application/json;charset=UTF-8
Content-Length: 702
Date: Sat, 03 Jan 2015 14:27:01 GMT
 
{...}

Обратите внимание, что для простоты тестирования разбиения на страницы с помощью RestTemplate я создал несколько вспомогательных классов: Page , Sort и PageAssertion . Вы найдете их в исходном коде приложения на Github.

Добавить нового клиента

В этом коротком фрагменте я использовал некоторые из функций Джерси, такие как внедрение @Context . В случае создания новой сущности мы обычно хотим вернуть ссылку на ресурс в заголовке. В приведенном ниже примере я UriBuilder в класс конечных точек и использую его для создания URI местоположения вновь созданного клиента:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
@Context
private UriInfo uriInfo;
 
@POST
public Response save(Customer customer) {
 
    customer = customerRepository.save(customer);
 
    URI location = uriInfo.getAbsolutePathBuilder()
            .path("{id}")
            .resolveTemplate("id", customer.getId())
            .build();
 
    return Response.created(location).build();
}

При вызове метода POST (с несуществующей электронной почтой):

1
$ curl -i -X POST -H 'Content-Type:application/json' -d '{"firstname":"Rafal","lastname":"Borowiec","emailAddress":{"value": "[email protected]"}}' http://localhost:8080/customer

Мы получим:

1
2
3
4
5
HTTP/1.1 201 Created
Server: Apache-Coyote/1.1
Location: http://localhost:8080/customer/4
Content-Length: 0
Date: Sun, 21 Dec 2014 22:49:30 GMT

Естественно, интеграционный тест тоже можно создать. Он использует RestTemplate для сохранения клиента с postForLocation метода postForLocation а затем извлекает его с помощью getForEntity :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
@Test
public void savesCustomer() {
    // act
    URI uri = restTemplate.postForLocation("http://localhost:9000/customer",
            new Customer("John", "Doe"));
    // assert
    ResponseEntity<Customer> responseEntity =
            restTemplate.getForEntity(uri, Customer.class);
 
    Customer customer = responseEntity.getBody();
 
    assertThat(customer.getFirstname())
            .isEqualTo("John");
    assertThat(customer.getLastname())
            .isEqualTo("Doe");
}

Другие методы

Остальные методы конечной точки очень легко реализовать:

01
02
03
04
05
06
07
08
09
10
11
12
@GET
@Path("{id}")
public Customer findOne(@PathParam("id") Long id) {
    return customerRepository.findOne(id);
}
 
@DELETE
@Path("{id}")
public Response delete(@PathParam("id") Long id) {
    customerRepository.delete(id);
    return Response.accepted().build();
}

Безопасность

Добавить Spring Security в приложение можно быстро, добавив новую зависимость в проект:

1
compile("org.springframework.boot:spring-boot-starter-security")

При использовании Spring Security в classpath приложение будет защищено базовой аутентификацией на всех конечных точках HTTP. Имя пользователя и пароль по умолчанию можно изменить с помощью двух следующих настроек приложения ( src/main/resources/application.properties ):

1
2
security.user.name=demo
security.user.password=123

После запуска приложения с приложением Spring Security нам необходимо предоставить действительные параметры аутентификации для каждого запроса. С curl мы можем использовать ключ --user :

1
$ curl -i --user demo:123 -X GET http://localhost:8080/customer/1

С добавлением Spring Security наши ранее созданные тесты не пройдут, поэтому нам нужно предоставить параметры имени пользователя и пароля для RestTemplate :

1
private RestTemplate restTemplate = new TestRestTemplate("demo", "123");

Диспетчер Сервлет

Сервлет-диспетчер Spring зарегистрирован вместе с сервлетом Джерси, и они оба сопоставлены с корневым ресурсом . Я расширил HealthController и добавил в него сопоставление запросов Spring MVC:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
@Component
@RestController // Spring MVC
@Path("/health")
public class HealthController {
 
    @GET
    @Produces({"application/json"})
    public Health jersey() {
        return new Health("Jersey: Up and Running!");
    }
 
    @RequestMapping(value = "/spring-health", produces = "application/json")
    public Health springMvc() {
        return new Health("Spring MVC: Up and Running!");
    }
}

С помощью приведенного выше кода я ожидал, что конечные точки работоспособности и работоспособности пружины будут доступны в корневом контексте, но, очевидно, это не сработало. Я пробовал несколько вариантов конфигурации, включая настройку spring.jersey.filter.order но безуспешно.

Единственное решение, которое я нашел, состояло в том, чтобы либо изменить Jersey @ApplicationPath либо изменить @ApplicationPath Spring MVC server.servlet-path :

1
server.servlet-path=/s

В последнем примере вызов:

1
$ curl -i --user demo:123 -X GET http://localhost:8080/s/spring-health

вернул ожидаемый результат:

1
2
3
{
    "status":"Spring MVC: Up and Running!"
}

Используйте Undertow вместо Tomcat

Начиная с Spring Boot 1.2 поддерживается легкий и производительный контейнер Servlet 3.1. Чтобы использовать Undertow вместо Tomcat, необходимо обменять зависимости Tomcat с зависимостями Undertow:

1
2
3
4
5
6
7
8
9
buildscript {
    configurations {
        compile.exclude module: "spring-boot-starter-tomcat"
    }
}   
 
dependencies {
    compile("org.springframework.boot:spring-boot-starter-undertow:1.2.0.RELEASE")
}

При запуске приложения журнал будет содержать:

1
2
3
org.xnio: XNIO version 3.3.0.Final
org.xnio.nio: XNIO NIO Implementation Version 3.3.0.Final
Started Application in 4.857 seconds (JVM running for 5.245)

Резюме

В этом посте я продемонстрировал простой пример того, как начать работу с Spring Boot и Jersey. Благодаря автоматической настройке в Джерси добавить поддержку JAX-RS в приложение Spring очень просто.

В целом Spring Boot 1.2 упрощает создание приложений с помощью Java EE: транзакции JTA с использованием встроенного менеджера транзакций Atomikos или Bitronix, поиск JNDI для DataSource и JMS ConnectionFactory в сервере приложений JEE и упрощение конфигурации JMS.

Ресурсы