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 = 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( ); 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 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.
Ресурсы
- Исходный код проекта: https://github.com/kolorobot/spring-boot-jersey-demo
- Продолжение : создание API HATEOAS с JAX-RS и Spring
Ссылка: | Начало работы с Jersey и Spring Boot от нашего партнера JCG Рафаля Боровца в блоге Codeleak.pl . |