Статьи

Создание корпоративных Java-приложений, Spring

Я думаю, будет справедливо сказать, что 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
@ApplicationScoped
public 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
@ApplicationScoped
public 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",
            url = "https://www.apache.org/licenses/LICENSE-2.0"
        )
    )
)
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 Created
Location: http://localhost:10900/api/people/a@b.com
Content-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 Request
Content-Length: 0

Спецификация OpenAPI и предустановленный дистрибутив Swagger UI также доступны по адресу http: // localhost: 10900 / index.html? Url = http: // localhost: 10900 / api / openapi.json .

Java-приложения

Пока все хорошо, но, честно говоря, мы вообще не говорили о тестировании нашего приложения. Насколько сложно было бы придумать интеграционный тест, скажем, для сценария добавления человека? Оказывается, что рамки тестирования приложений 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)
@EnableRandomWebServerPort
public 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, являются их собственными.