Я думаю, будет справедливо сказать, что Java EE приобрела довольно плохую репутацию среди разработчиков Java. Несмотря на то, что с годами он, безусловно, улучшился по всем направлениям, даже поменялся домом на Eclipse Foundation, чтобы стать Jakarta EE , его горький вкус все еще довольно силен. С другой стороны, у нас есть Spring Framework (или, чтобы лучше отражать реальность, полноценная Spring Platform ): блестящая, легкая, быстрая, инновационная и сверхпродуктивная замена Java EE . Так зачем беспокоиться о Java EE ?
Мы собираемся ответить на этот вопрос, показав, как легко создавать современные приложения Java, используя большинство спецификаций Java EE . И ключевым элементом успеха здесь является Eclipse Microprofile : корпоративная Java в эпоху микросервисов .
Приложение, которое мы собираемся создать, представляет собой веб-API RESTful для простого управления людьми. Стандартный способ создания веб-сервисов RESTful в Java — использование JAX-RS 2.1 ( JSR-370 ). Следовательно, CDI 2.0 ( JSR-365 ) будет заботиться о внедрении зависимостей, тогда как JPA 2.0 ( JSR-317 ) будет охватывать уровень доступа к данным. И, конечно, Bean Validation 2.0 ( JSR-380 ) помогает нам справиться с проверкой входных данных.
Единственная спецификация, не относящаяся к Java EE, на которую мы будем полагаться, — это OpenAPI v3.0, которая помогает предоставить полезное описание наших веб-API RESTful . После этого давайте начнем с модели предметной области PersonEntity (опуская геттеры и сеттеры как не очень важные детали):
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
@Entity@Table(name = "people")public class PersonEntity { @Id @Column(length = 256) private String email; @Column(nullable = false, length = 256, name = "first_name") private String firstName; @Column(nullable = false, length = 256, name = "last_name") private String lastName; @Version private Long version;} |
Он просто имеет абсолютный минимальный набор свойств. Репозиторий JPA довольно прост и реализует типичный набор методов CRUD .
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
|
@ApplicationScoped@EntityManagerConfig(qualifier = PeopleDb.class)public class PeopleJpaRepository implements PeopleRepository { @Inject @PeopleDb private EntityManager em; @Override @Transactional(readOnly = true) public Optional<PersonEntity> findByEmail(String email) { final CriteriaBuilder cb = em.getCriteriaBuilder(); final CriteriaQuery<PersonEntity> query = cb.createQuery(PersonEntity.class); final Root<PersonEntity> root = query.from(PersonEntity.class); query.where(cb.equal(root.get(PersonEntity_.email), email)); try { final PersonEntity entity = em.createQuery(query).getSingleResult(); return Optional.of(entity); } catch (final NoResultException ex) { return Optional.empty(); } } @Override @Transactional public PersonEntity saveOrUpdate(String email, String firstName, String lastName) { final PersonEntity entity = new PersonEntity(email, firstName, lastName); em.persist(entity); return entity; } @Override @Transactional(readOnly = true) public Collection<PersonEntity> findAll() { final CriteriaBuilder cb = em.getCriteriaBuilder(); final CriteriaQuery<PersonEntity> query = cb.createQuery(PersonEntity.class); query.from(PersonEntity.class); return em.createQuery(query).getResultList(); } @Override @Transactional public Optional<PersonEntity> deleteByEmail(String email) { return findByEmail(email) .map(entity -> { em.remove(entity); return entity; }); }} |
Управление транзакциями (а именно аннотация @Transactional ) нуждается в пояснениях. В типичном приложении Java EE за управление транзакциями отвечает среда выполнения контейнера. Поскольку мы не хотим встроить контейнер приложения, но остаемся простыми, мы могли бы использовать EntityManager для запуска / фиксации / отката транзакций. Это, конечно, сработает, но загрязнит код с помощью шаблона. Можно утверждать, что лучшим вариантом является использование расширений Apache DeltaSpike CDI для декларативного управления транзакциями ( отсюда исходят аннотации @Transactional и @EntityManagerConfig ). Фрагмент ниже иллюстрирует, как он интегрируется.
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
|
@ApplicationScopedpublic class PersistenceConfig { @PersistenceUnit(unitName = "peopledb") private EntityManagerFactory entityManagerFactory; @Produces @PeopleDb @TransactionScoped public EntityManager create() { return this.entityManagerFactory.createEntityManager(); } public void dispose(@Disposes @PeopleDb EntityManager entityManager) { if (entityManager.isOpen()) { entityManager.close(); } }} |
Круто, самое сложное уже позади! Далее идут объект передачи данных Person и сервисный уровень.
|
1
2
3
4
5
|
public class Person { @NotNull private String email; @NotNull private String firstName; @NotNull private String lastName;} |
Честно говоря, чтобы сохранить пример приложения настолько маленьким, насколько это возможно, мы могли бы вообще пропустить сервисный уровень и напрямую перейти в хранилище. Но в целом это не очень хорошая практика, поэтому давайте все равно представим PeopleServiceImpl .
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
@ApplicationScopedpublic class PeopleServiceImpl implements PeopleService { @Inject private PeopleRepository repository; @Override public Optional<Person> findByEmail(String email) { return repository .findByEmail(email) .map(this::toPerson); } @Override public Person add(Person person) { return toPerson(repository.saveOrUpdate(person.getEmail(), person.getFirstName(), person.getLastName())); } @Override public Collection<Person> getAll() { return repository .findAll() .stream() .map(this::toPerson) .collect(Collectors.toList()); } @Override public Optional<Person> remove(String email) { return repository .deleteByEmail(email) .map(this::toPerson); } private Person toPerson(PersonEntity entity) { return new Person(entity.getEmail(), entity.getFirstName(), entity.getLastName()); }} |
Единственная оставшаяся часть — это определение приложения и ресурсов JAX-RS .
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
@Dependent@ApplicationPath("api")@OpenAPIDefinition( info = @Info( title = "People Management Web APIs", version = "1.0.0", license = @License( name = "Apache License", ) ))public class PeopleApplication extends Application {} |
Не так много, чтобы сказать, так просто, как это могло бы быть. Реализация ресурса JAX-RS немного более интересна (аннотации OpenAPI занимают большую часть места).
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
|
@ApplicationScoped@Path( "/people" ) @Tag(name = "people")public class PeopleResource { @Inject private PeopleService service; @Produces(MediaType.APPLICATION_JSON) @GET @Operation( description = "List all people", responses = { @ApiResponse( content = @Content(array = @ArraySchema(schema = @Schema(implementation = Person.class))), responseCode = "200" ) } ) public Collection<Person> getPeople() { return service.getAll(); } @Produces(MediaType.APPLICATION_JSON) @Path("/{email}") @GET @Operation( description = "Find person by e-mail", responses = { @ApiResponse( content = @Content(schema = @Schema(implementation = Person.class)), responseCode = "200" ), @ApiResponse( responseCode = "404", description = "Person with such e-mail doesn't exists" ) } ) public Person findPerson(@Parameter(description = "E-Mail address to lookup for", required = true) @PathParam("email") final String email) { return service .findByEmail(email) .orElseThrow(() -> new NotFoundException("Person with such e-mail doesn't exists")); } @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) @POST @Operation( description = "Create new person", requestBody = @RequestBody( content = @Content(schema = @Schema(implementation = Person.class)), ), responses = { @ApiResponse( content = @Content(schema = @Schema(implementation = Person.class)), headers = @Header(name = "Location"), responseCode = "201" ), @ApiResponse( responseCode = "409", description = "Person with such e-mail already exists" ) } ) public Response addPerson(@Context final UriInfo uriInfo, @Parameter(description = "Person", required = true) @Valid Person payload) { final Person person = service.add(payload); return Response .created(uriInfo.getRequestUriBuilder().path(person.getEmail()).build()) .entity(person) .build(); } @Path("/{email}") @DELETE @Operation( description = "Delete existing person", responses = { @ApiResponse( responseCode = "204", description = "Person has been deleted" ), @ApiResponse( responseCode = "404", description = "Person with such e-mail doesn't exists" ) } ) public Response deletePerson(@Parameter(description = "E-Mail address to lookup for", required = true ) @PathParam("email") final String email) { return service .remove(email) .map(r -> Response.noContent().build()) .orElseThrow(() -> new NotFoundException("Person with such e-mail doesn't exists")); }} |
И с этим мы закончили! Но как мы можем собрать и соединить все эти части вместе? Вот время, когда Microprofile может выйти на сцену. Есть много реализаций на выбор, одна из которых мы будем использовать в этом посте, это Project Hammock . Единственное, что нам нужно сделать, — это указать реализации CDI 2.0 , JAX-RS 2.1 и JPA 2.0, которые мы хотели бы использовать, что переводит соответственно в Weld , Apache CXF и OpenJPA (выражается через зависимости Project Hammock ). Давайте посмотрим на файл Apache Maven pom.xml .
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
|
<properties> <deltaspike.version>1.8.1</deltaspike.version> <hammock.version>2.1</hammock.version></properties><dependencies> <dependency> <groupId>org.apache.deltaspike.modules</groupId> <artifactId>deltaspike-jpa-module-api</artifactId> <version>${deltaspike.version}</version> <scope>compile</scope> </dependency> <dependency> <groupId>org.apache.deltaspike.modules</groupId> <artifactId>deltaspike-jpa-module-impl</artifactId> <version>${deltaspike.version}</version> <scope>runtime</scope> </dependency> <dependency> <groupId>ws.ament.hammock</groupId> <artifactId>dist-microprofile</artifactId> <version>${hammock.version}</version> </dependency> <dependency> <groupId>ws.ament.hammock</groupId> <artifactId>jpa-openjpa</artifactId> <version>${hammock.version}</version> </dependency> <dependency> <groupId>ws.ament.hammock</groupId> <artifactId>util-beanvalidation</artifactId> <version>${hammock.version}</version> </dependency> <dependency> <groupId>ws.ament.hammock</groupId> <artifactId>util-flyway</artifactId> <version>${hammock.version}</version> </dependency> <dependency> <groupId>ws.ament.hammock</groupId> <artifactId>swagger</artifactId> <version>${hammock.version}</version> </dependency></dependencies> |
Без лишних слов давайте сразу же создадим и запустим приложение (если вам интересно, какое реляционное хранилище данных использует приложение, это H2 с базой данных, сконфигурированной в памяти).
|
1
2
|
> mvn clean package> java -jar target/eclipse-microprofile-hammock-0.0.1-SNAPSHOT-capsule.jar |
Лучший способ обеспечить полную функциональность наших веб-API RESTful для управления персоналом — это отправить ему пару запросов:
|
01
02
03
04
05
06
07
08
09
10
11
12
|
> curl -X POST http://localhost:10900/api/people -H "Content-Type: application\json" \ -d '{"email": "a@b.com", "firstName": "John", "lastName": "Smith"}'HTTP/1.1 201 CreatedLocation: http://localhost:10900/api/people/a@b.comContent-Type: application/json{ "firstName":"John"," "lastName":"Smith", "email":"a@b.com"} |
Как насчет того, чтобы убедиться, что Bean Validation работает нормально? Чтобы вызвать это, давайте отправим частично подготовленный запрос.
|
1
2
3
4
5
|
> curl --X POST http://localhost:10900/api/people -H "Content-Type: application\json" \ -d '{"firstName": "John", "lastName": "Smith"}'HTTP/1.1 400 Bad RequestContent-Length: 0 |
Спецификация OpenAPI и предустановленный дистрибутив Swagger UI также доступны по адресу http: // localhost: 10900 / index.html? Url = http: // localhost: 10900 / api / openapi.json .
Пока все хорошо, но, честно говоря, мы вообще не говорили о тестировании нашего приложения. Насколько сложно было бы придумать интеграционный тест, скажем, для сценария добавления человека? Оказывается, что рамки тестирования приложений Java EE значительно улучшились. В частности, это исключительно легко сделать с помощью инфраструктуры тестирования Arquillian (вместе с любимым JUnit и REST Assured ). Один реальный пример стоит тысячи слов.
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
@RunWith(Arquillian.class)@EnableRandomWebServerPortpublic class PeopleApiTest { @ArquillianResource private URI uri; @Deployment public static JavaArchive createArchive() { return ShrinkWrap .create(JavaArchive.class) .addClasses(PeopleResource.class, PeopleApplication.class) .addClasses(PeopleServiceImpl.class, PeopleJpaRepository.class, PersistenceConfig.class) .addPackages(true, "org.apache.deltaspike"); } @Test public void shouldAddNewPerson() throws Exception { final Person person = new Person("a@b.com", "John", "Smith"); given() .contentType(ContentType.JSON) .body(person) .post(uri + "/api/people") .then() .assertThat() .statusCode(201) .body("email", equalTo("a@b.com")) .body("firstName", equalTo("John")) .body("lastName", equalTo("Smith")); }} |
Удивительно, не правда ли? На самом деле очень интересно разрабатывать современные приложения Java EE , кто-то может сказать, Spring ! И на самом деле, параллели с Spring не случайны, поскольку он вдохновлял, вдохновлял и, несомненно, будет продолжать вдохновлять множество инноваций в экосистеме Java EE .
Как выглядит будущее? Я думаю, что, безусловно, ярко, как для Jakarta EE, так и для Eclipse Microprofile . Последний только приблизился к версии 2.0 с тоннами новых спецификаций, ориентированных на удовлетворение потребностей микросервисных архитектур . Удивительно наблюдать, как происходят эти преобразования.
Полный исходный код проекта доступен на Github .
| Опубликовано на Java Code Geeks с разрешения Андрея Редько, партнера нашей программы JCG . См. Оригинальную статью здесь: Создание корпоративных Java-приложений, Spring.
Мнения, высказанные участниками Java Code Geeks, являются их собственными. |
