I think it is fair to say that Java EE has gained pretty bad reputation among Java developers. Despite the fact that it has certainly improved on all fronts over the years, even changed home to Eclipse Foundation to become Jakarta EE, its bitter taste is still quite strong. On the other side we have Spring Framework (or to reflect the reality better, a full-fledged Spring Platform): brilliant, lightweight, fast, innovative and hyper-productive Java EE replacement. So why to bother with Java EE?
We are going to answer this question by showing how easy it is to build modern Java applications using most of Java EE specs. And the key ingredient to succeed here is Eclipse Microprofile: enterprise Java in the age of microservices.
The application we are going to build is RESTful web API to manage people, as simple as that. The standard way to build RESTful web services in Java is by using JAX-RS 2.1 (JSR-370). Consequently, CDI 2.0 (JSR-365) is going to take care of dependency injection whereas JPA 2.0 (JSR-317) is going to cover the data access layer. And certainly, Bean Validation 2.0 (JSR-380) is helping us to deal with input verification.
The only non-Java EE specification we would be relying on is OpenAPI v3.0 which helps to provide the usable description of our RESTful web APIs. With that, let us get started with the PersonEntity domain model (omitting getters and setters as not very relevant details):
@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; }
It just has the absolute minimum set of properties. The JPA repository is pretty straightforward and implements typical set of CRUD methods.
@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; }); } }
The transaction management (namely, the @Transactional annotation) needs some explanation. In the typical Java EE application, the container runtime is responsible for managing the transactions. Since we don't want to onboard the application container but stay lean, we could have used EntityManager to start / commit / rollback transactions. It would certainly work out but pollute the code with the boilerplate. Arguably, the better option is to use Apache DeltaSpike CDI extensions for declarative transaction management (this is where @Transactional and @EntityManagerConfig annotations are coming from). The snippet below illustrates how it is being integrated.
@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(); } } }
Awesome, the hardest part is already behind! The Person data transfer object and the service layer are coming next.
public class Person { @NotNull private String email; @NotNull private String firstName; @NotNull private String lastName; }
Honestly, for the sake of keeping the example application as small as possible we could skip the service layer altogether and go to the repository directly. But this is, in general, not a very good practice so let us introduce PeopleServiceImpl anyway.
@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()); } }
The only part left is the definition of the JAX-RS application and resources.
@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 { }
Not much to say, as simple as it could possibly be. The JAX-RS resource implementation is a bit more interesting though (the OpenAPI annotations are taking most of the place).
@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")); } }
And with that, we are done! But how could we assemble and wire all these pieces together? Here is the time for Microprofile to enter the stage. There are many implementations to chose from, the one we are going to use in this post is Project Hammock. The only thing we have to do is to specify the CDI 2.0, JAX-RS 2.1 and JPA 2.0 implementations we would like to use, which translates to Weld, Apache CXF, and OpenJPA respectively (expressed through the Project Hammock dependencies). Let us take a look on the Apache Maven pom.xml file.
<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>
Without further ado, let us build and run the application right away (if you are curious what relational datastore the application is using, it is H2 with the database configured in-memory).
> mvn clean package > java -jar target/eclipse-microprofile-hammock-0.0.1-SNAPSHOT-capsule.jar
The best way to ensure that our people management RESTful web APIs are fully functional is to send a couple of requests to it:
> 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" }
What about making sure the Bean Validation is working fine? To trigger that, let us send the partially prepared request.
> 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
The OpenAPI specification and pre-bundled Swagger UI distribution are also available at http://localhost:10900/index.html?url=http://localhost:10900/api/openapi.json.
So far so good but fairly speaking we have not talked about testing our application at all. How hard it would be to come up with the integration test for, let say, the scenario of adding a person? It turns out that the frameworks around testing Java EE applications have improved a lot. In particular, it is exceptionally easy to accomplish with Arquillian test framework (along with beloved JUnit and REST Assured). One real example is worth thousand words.
@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")); } }
Amazing, isn't it? It is actually a lot of fun to develop modern Java EE applications, someone may say, the Spring way! And in fact, the parallels with Spring are not coincidental since it was inspiring, is inspiring and undoubtedly is going to continue inspire a lot of innovations in the Java EE ecosystem.
How the future is looking like? I think, by all means bright, both for Jakarta EE and Eclipse Microprofile. The latter just approached the version 2.0 with tons of new specifications included, oriented to address the needs of the microservice architectures. It is awesome to witness these transformations happening.
The complete source of the project is available on Github.
No comments:
Post a Comment