Building Enterprise Java Applications the Spring Way
Learn more about building enterprise Java applications with Java EE and the Spring framework by building a simple RESTful web API in this tutorial.
Join the DZone community and get the full member experience.
Join For FreeI think it is fair to say that Java EE has gained a pretty bad reputation among Java developers. Despite the fact that it has certainly improved on all fronts over the years and even changed its home from the Eclipse Foundation to become Jakarta EE, its bitter taste is still quite strong. On the other side, we have the Spring Framework (or to reflect the reality better, a full-fledged Spring Platform) this is a brilliant, lightweight, fast, innovative, and hyper-productive Java EE replacement. So why 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 the Java EE specs. And, the key ingredient to succeeding 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; it's 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 thePersonEntity
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 a 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 the EntityManager
to start/commit/rollback transactions. It would certainly work out, but it would also pollute the code with the boilerplate. Arguably, the better option is to use Apache DeltaSpikeCDI 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 us! ThePerson
data transfer object and the service layer are coming up 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; it is 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 choose 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 at 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's 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 the 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, some might 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 to inspire innovations in the Java EE ecosystem.
How is the future looking? 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 that are oriented to address the needs of the microservices architectures. It is awesome to witness these transformations.
The complete source of the project is available on GitHub.
Published at DZone with permission of Andriy Redko, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments