Microservices are a hot topic in software design, and for good reason. They have plenty of advantages when it comes to handling infrastructure complexity, many of which were addressed in part one of our related Java post. Now, it’s time to talk about the code and design. In this post, we’ll take a deep dive into each module.
When creating an application, clean code means thinking about design and architecture. Architecture is the software process that handles flexibility, scalability, usability, security, and other points, so you have more time to focus on business rather than on technology. Some architecture examples include:
- Serverless architecture. Application designs that incorporate third-party, Backend-as-a-Service (BaaS) services, and include custom code run in managed, ephemeral containers on a Functions-as-a-Service (FaaS) platform.
- Event-driven architecture. A software architecture pattern promoting the production, detection, consumption of, and reaction to events.
- Microservices architecture. A variant of the service-oriented architecture (SOA) style that structures an application as a collection of loosely coupled services. In a microservices architecture, services are fine grained and the protocols lightweight.
The design has a low-level duty that supervises the code, such as what each module is going to do, the class's scope, the functions proposal, and so on.
- SOLID. The five design principles that make software designs more understandable, flexible, and maintainable.
- Design patterns. The ideal solutions to common problems in software design. Each pattern is like a blueprint you can customize to solve a particular design problem in your code.
Speaker service
The first service that we'll cover is the Speaker service, which uses Thorntail and PostgreSQL. The Eclipse MicroProfile doesn’t have support for a relational database. However, we can use the specification from its older and more mature brother, Jakarta EE. As a first step, let's define the Speaker entity.
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;
import java.util.Objects;
@Entity
@Table(name = "speaker")
public class Speaker {
@Id
@GeneratedValue
private Integer id;
@Column
private String name;
@Column
private String bio;
@Column
private String twitter;
@Column
private String github;
//more code
}
Revisit our previous article that covers the JPA integration, using Transactional annotation and a CDI Interceptor to manage the transaction.
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import javax.persistence.EntityManager;
import javax.transaction.Transactional;
import java.util.List;
import java.util.Optional;
@ApplicationScoped
public class SpeakerService {
@Inject
private EntityManager entityManager;
@Transactional
public Speaker insert(Speaker speaker) {
entityManager.persist(speaker);
return speaker;
}
@Transactional
public void update(Speaker speaker) {
entityManager.persist(speaker);
}
@Transactional
public void delete(Integer id) {
find(id).ifPresent(c -> entityManager.remove(c));
}
public Optional<Speaker> find(Integer id) {
return Optional.ofNullable(entityManager.find(Speaker.class, id));
}
public List<Speaker> findAll() {
String query = "select e from Speaker e";
return entityManager.createQuery(query).getResultList();
}
}
We have a resource to expose the services through an HTTP request. We've created a layer on DTO to avoid losing the encapsulation.
import javax.enterprise.context.RequestScoped;
import javax.inject.Inject;
import javax.validation.Valid;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import static javax.ws.rs.core.Response.Status.NO_CONTENT;
import static javax.ws.rs.core.Response.status;
@Path("speakers")
@RequestScoped
@Produces(MediaType.APPLICATION_JSON + "; charset=UTF-8")
@Consumes(MediaType.APPLICATION_JSON+ "; charset=UTF-8")
public class SpeakerResource {
@Inject
private SpeakerService speakerService;
@GET
public List<SpeakerDTO> findAll() {
return speakerService.findAll()
.stream().map(SpeakerDTO::of)
.collect(Collectors.toList());
}
@GET
@Path("{id}")
public SpeakerDTO findById(@PathParam("id") Integer id) {
final Optional<Speaker> conference = speakerService.find(id);
return conference.map(SpeakerDTO::of).orElseThrow(this::notFound);
}
@PUT
@Path("{id}")
public SpeakerDTO update(@PathParam("id") Integer id, @Valid SpeakerDTO speakerUpdated) {
final Optional<Speaker> optional = speakerService.find(id);
final Speaker speaker = optional.orElseThrow(() -> notFound());
speaker.update(Speaker.of(speakerUpdated));
speakerService.update(speaker);
return SpeakerDTO.of(speaker);
}
@DELETE
@Path("{id}")
public Response remove(@PathParam("id") Integer id) {
speakerService.delete(id);
return status(NO_CONTENT).build();
}
@POST
public SpeakerDTO insert(@Valid SpeakerDTO speaker) {
return SpeakerDTO.of(speakerService.insert(Speaker.of(speaker)));
}
private WebApplicationException notFound() {
return new WebApplicationException(Response.Status.NOT_FOUND);
}
}
In the .platform.app.yaml
file, we need to check that the speaker application has access to the PostgreSQL instance and anything else about the database that lets Platform.sh handle it.
relationships:
postgresql: "postgresql:postgresql"
Session service
The Session service will handle the contents of the conference, so that a user can find sessions related to topics like cloud and Java. This service uses Elasticsearch with Jakarta NoSQL and KumuluzEE.
import jakarta.nosql.mapping.Column;
import jakarta.nosql.mapping.Entity;
import jakarta.nosql.mapping.Id;
import java.util.Objects;
@Entity
public class Session {
@Id
private String id;
@Column
private String name;
@Column
private String title;
@Column
private String description;
@Column
private String conference;
@Column
private Integer speaker;
}
We're applying a distinct aspect of Elasticsearch to use it as a search engine. Jakarta NoSQL looks to the specialization that allows particular features for each NoSQL database. Therefore, we'll have a mix of repository and ElasticsearchTemplate, a specialty of DocumentTemplate.
import jakarta.nosql.mapping.Repository;
import java.util.List;
public interface SessionRepository extends Repository<Session, String> {
List<Session> findAll();
}
import org.elasticsearch.index.query.QueryBuilder;
import org.jnosql.artemis.elasticsearch.document.ElasticsearchTemplate;
import org.jnosql.artemis.util.StringUtils;
import javax.enterprise.context.RequestScoped;
import javax.inject.Inject;
import javax.validation.Valid;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import static javax.ws.rs.core.Response.Status.NO_CONTENT;
import static javax.ws.rs.core.Response.status;
import static org.elasticsearch.index.query.QueryBuilders.boolQuery;
import static org.elasticsearch.index.query.QueryBuilders.termQuery;
@Path("sessions")
@RequestScoped
@Produces(MediaType.APPLICATION_JSON + "; charset=UTF-8")
@Consumes(MediaType.APPLICATION_JSON + "; charset=UTF-8")
public class SessionResource {
private static Logger LOGGER = Logger.getLogger(SessionResource.class.getName());
@Inject
private SessionRepository speakerRepository;
@Inject
private ElasticsearchTemplate template;
@GET
public List<SessionDTO> findAll(@QueryParam("search") String search) {
LOGGER.info("searching with the field: " + search);
if (StringUtils.isNotBlank(search)) {
QueryBuilder queryBuilder = boolQuery()
.should(termQuery("name", search))
.should(termQuery("title", search))
.should(termQuery("description", search));
LOGGER.info("the query: " + queryBuilder);
List<Session> sessions = template.search(queryBuilder, "Session");
LOGGER.info("the result: " + sessions);
return sessions.stream()
.map(SessionDTO::of)
.collect(Collectors.toList());
}
return speakerRepository.findAll().stream()
.map(SessionDTO::of).collect(Collectors.toList());
}
@GET
@Path("{id}")
public Session findById(@PathParam("id") String id) {
final Optional<Session> conference = speakerRepository.findById(id);
return conference.orElseThrow(this::notFound);
}
@PUT
@Path("{id}")
public SessionDTO update(@PathParam("id") String id, @Valid SessionDTO sessionUpdated) {
final Optional<Session> optional = speakerRepository.findById(id);
final Session session = optional.orElseThrow(() -> notFound());
session.update(Session.of(sessionUpdated));
speakerRepository.save(session);
return SessionDTO.of(session);
}
@DELETE
@Path("{id}")
public Response remove(@PathParam("id") String id) {
speakerRepository.deleteById(id);
return status(NO_CONTENT).build();
}
@POST
public SessionDTO insert(@Valid SessionDTO session) {
session.setId(UUID.randomUUID().toString());
return SessionDTO.of(speakerRepository.save(Session.of(session)));
}
private WebApplicationException notFound() {
return new WebApplicationException(Response.Status.NOT_FOUND);
}
}
Conference service
The Conference service is the most connected of the services because it needs to keep information from both speaker and session services. It uses Payara Micro and MongoDB with Jakarta NoSQL.
import jakarta.nosql.mapping.Column;
import jakarta.nosql.mapping.Convert;
import jakarta.nosql.mapping.Entity;
import jakarta.nosql.mapping.Id;
import java.time.Year;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
@Entity
public class Conference {
@Id
@Convert(ObjectIdConverter.class)
private String id;
@Column
private String name;
@Column
private String city;
@Column
private String link;
@Column
@Convert(YearConverter.class)
private Year year;
@Column
private List<Speaker> speakers;
@Column
private List<Session> sessions;
}
@Entity
public class Session {
@Column
private String id;
@Column
private String name;
}
@Entity
public class Speaker {
@Column
private Integer id;
@Column
private String name;
}
One of the benefits of MongoDB is that we can use the subdocument instead of creating a relationship. Therefore, we can embed Speaker and Session instead of doing joins. Note, the Speaker and Session entities have brief information such as name and ID.
import jakarta.nosql.mapping.Repository;
import java.util.List;
public interface ConferenceRepository extends Repository<Conference, String> {
List<Conference> findAll();
}
import javax.enterprise.context.RequestScoped;
import javax.inject.Inject;
import javax.validation.Valid;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import static javax.ws.rs.core.Response.Status.NO_CONTENT;
import static javax.ws.rs.core.Response.status;
@Path("conferences")
@RequestScoped
@Produces(MediaType.APPLICATION_JSON + "; charset=UTF-8")
@Consumes(MediaType.APPLICATION_JSON+ "; charset=UTF-8")
public class ConferenceResource {
@Inject
private ConferenceRepository conferenceRepository;
@GET
public List<ConferenceDTO> findAll() {
return conferenceRepository.findAll().stream()
.map(ConferenceDTO::of)
.collect(Collectors.toList());
}
@GET
@Path("{id}")
public ConferenceDTO findById(@PathParam("id") String id) {
final Optional<Conference> conference = conferenceRepository.findById(id);
return conference.map(ConferenceDTO::of).orElseThrow(this::notFound);
}
@PUT
@Path("{id}")
public ConferenceDTO update(@PathParam("id") String id, @Valid ConferenceDTO conferenceUpdated) {
final Optional<Conference> optional = conferenceRepository.findById(id);
final Conference conference = optional.orElseThrow(() -> notFound());
conference.update(Conference.of(conferenceUpdated));
conferenceRepository.save(conference);
return ConferenceDTO.of(conference);
}
@DELETE
@Path("{id}")
public Response remove(@PathParam("id") String id) {
conferenceRepository.deleteById(id);
return status(NO_CONTENT).build();
}
@POST
public ConferenceDTO insert(@Valid ConferenceDTO conference) {
return ConferenceDTO.of(conferenceRepository.save(Conference.of(conference)));
}
private WebApplicationException notFound() {
return new WebApplicationException(Response.Status.NOT_FOUND);
}
}
Client service
The client will show the RESTful application using HTML 5 with Eclipse Krazo. Yes, Eclipse Krazo has several engine extensions to use more than JSP, such as HTML 5. The extension we'll use is Thymeleaf with Apache TomEE.
<dependencies>
<dependency>
<groupId>org.eclipse.microprofile</groupId>
<artifactId>microprofile</artifactId>
<type>pom</type>
</dependency>
<dependency>
<groupId>sh.platform</groupId>
<artifactId>config</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.krazo</groupId>
<artifactId>krazo-core</artifactId>
<version>${version.krazo}</version>
</dependency>
<dependency>
<groupId>org.eclipse.krazo</groupId>
<artifactId>krazo-cxf</artifactId>
<version>${version.krazo}</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>${jstl.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.krazo.ext</groupId>
<artifactId>krazo-thymeleaf</artifactId>
<version>${version.krazo}</version>
</dependency>
</dependencies>
The first step in the client is to create the bridge to request information from the services. Thankfully, we have an Eclipse MicroProfile Rest Client to deal just with interfaces and nothing more.
package org.jespanol.client.speaker;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.util.List;
@Path("speakers")
@RegisterRestClient
@Produces(MediaType.APPLICATION_JSON + "; charset=UTF-8")
@Consumes(MediaType.APPLICATION_JSON+ "; charset=UTF-8")
public interface SpeakerService {
@GET
List<Speaker> findAll();
@GET
@Path("{id}")
Speaker findById(@PathParam("id") Integer id);
@PUT
@Path("{id}")
Speaker update(@PathParam("id") Integer id, Speaker speaker);
@DELETE
@Path("{id}")
Response remove(@PathParam("id") Integer id);
@POST
Speaker insert(Speaker speaker);
}
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.util.List;
@Path("sessions")
@RegisterRestClient
@Produces(MediaType.APPLICATION_JSON + "; charset=UTF-8")
@Consumes(MediaType.APPLICATION_JSON+ "; charset=UTF-8")
public interface SessionService {
@GET
List<Session> findAll(@QueryParam("search") String search);
@GET
List<Session> findAll();
@GET
@Path("{id}")
Session findById(@PathParam("id") String id);
@PUT
@Path("{id}")
Session update(@PathParam("id") String id, Session session);
@DELETE
@Path("{id}")
Response remove(@PathParam("id") String id);
@POST
Session insert(Session session);
}
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.util.List;
@Path("conferences")
@RegisterRestClient
@Produces(MediaType.APPLICATION_JSON + "; charset=UTF-8")
@Consumes(MediaType.APPLICATION_JSON+ "; charset=UTF-8")
public interface ConferenceService {
@GET
List<Conference> findAll();
@GET
@Path("{id}")
Conference findById(@PathParam("id") String id);
@PUT
@Path("{id}")
Conference update(@PathParam("id") String id, Conference conference);
@DELETE
@Path("{id}")
Response remove(@PathParam("id") String id);
@POST
Conference insert(Conference conference);
}
To make sure that the services are up, we conduct an Eclipse Microprofile Health check, so we can evaluate the HTTP status and the response time in milliseconds.
import org.eclipse.microprofile.health.HealthCheck;
import org.eclipse.microprofile.health.HealthCheckResponse;
import javax.ws.rs.client.Client;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
abstract class AbstractHealthCheck implements HealthCheck {
abstract Client getClient();
abstract String getUrl();
abstract String getServiceName();
@Override
public HealthCheckResponse call() {
try {
long start = System.currentTimeMillis();
Response response = getClient().target(getUrl()).request(MediaType.TEXT_PLAIN_TYPE)
.get();
long end = System.currentTimeMillis() - start;
return HealthCheckResponse.named(getServiceName())
.withData("service", "available")
.withData("time millis", end)
.withData("status", response.getStatus())
.withData("status", response.getStatusInfo().toEnum().toString())
.up()
.build();
} catch (Exception exp) {
return HealthCheckResponse.named(getServiceName())
.withData("services", "not available")
.down()
.build();
}
}
}
@Health
@ApplicationScoped
public class ConferenceHealthCheck extends AbstractHealthCheck {
@Inject
@ConfigProperty(name = "org.jespanol.client.conference.ConferenceService/mp-rest/url")
private String url;
private Client client;
@PostConstruct
public void init() {
this.client = ClientBuilder.newClient();
}
@Override
Client getClient() {
return client;
}
@Override
String getUrl() {
return url;
}
@Override
String getServiceName() {
return "Conference Service";
}
}
@Health
@ApplicationScoped
public class SessionHealthCheck extends AbstractHealthCheck {
@Inject
@ConfigProperty(name = "org.jespanol.client.session.SessionService/mp-rest/url")
private String url;
private Client client;
@PostConstruct
public void init() {
this.client = ClientBuilder.newClient();
}
@Override
Client getClient() {
return client;
}
@Override
String getUrl() {
return url;
}
@Override
String getServiceName() {
return "Session Service";
}
}
@Health
@ApplicationScoped
public class SpeakerHealthCheck extends AbstractHealthCheck {
@Inject
@ConfigProperty(name = "org.jespanol.client.speaker.SpeakerService/mp-rest/url")
private String url;
private Client client;
@PostConstruct
public void init() {
this.client = ClientBuilder.newClient();
}
@Override
Client getClient() {
return client;
}
@Override
String getUrl() {
return url;
}
@Override
String getServiceName() {
return "Speaker Service";
}
}
We can access the status at https://server_ip/health.
{
"checks": [
{
"data": {
"time millis": 11,
"service": "available",
"status": "OK"
},
"name": "Speaker Service",
"state": "UP"
},
{
"data": {
"time millis": 11,
"service": "available",
"status": "OK"
},
"name": "Conference Service",
"state": "UP"
},
{
"data": {
"time millis": 10,
"service": "available",
"status": "OK"
},
"name": "Session Service",
"state": "UP"
}
],
"outcome": "UP",
"status": "UP"
}
Once Services and health check are ready, let's go to the controllers. Eclipse Krazo is an API built on JAX-RS. Therefore, any Jakarta EE developer will feel at home when creating a Controller class.
import org.eclipse.microprofile.rest.client.inject.RestClient;
import org.jespanol.client.session.SessionService;
import org.jespanol.client.speaker.SpeakerService;
import javax.inject.Inject;
import javax.mvc.Controller;
import javax.mvc.Models;
import javax.mvc.View;
import javax.ws.rs.BeanParam;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import java.util.Optional;
@Controller
@Path("conference")
public class ConferenceController {
@Inject
private Models models;
@Inject
@RestClient
private SessionService sessionService;
@Inject
@RestClient
private ConferenceService conferenceService;
@Inject
@RestClient
private SpeakerService speakerService;
@GET
@View("conference.html")
public void home() {
this.models.put("conferences", conferenceService.findAll());
}
@Path("add")
@GET
@View("conference-add.html")
public void add() {
this.models.put("conference", new Conference());
this.models.put("speakers", speakerService.findAll());
this.models.put("presentations", sessionService.findAll());
}
@Path("delete/{id}")
@GET
@View("conference.html")
public void delete(@PathParam("id") String id) {
conferenceService.remove(id);
this.models.put("conferences", conferenceService.findAll());
}
@Path("edit/{id}")
@GET
@View("conference-add.html")
public void edit(@PathParam("id") String id) {
final Conference conference = Optional.ofNullable(conferenceService.findById(id))
.orElse(new Conference());
this.models.put("conference", conference);
this.models.put("speakers", speakerService.findAll());
this.models.put("presentations", sessionService.findAll());
}
@Path("add")
@POST
@View("conference.html")
public void add(@BeanParam Conference conference) {
conference.update(speakerService, sessionService);
if (conference.isIdEmpty()) {
conferenceService.insert(conference);
} else {
conferenceService.update(conference.getId(), conference);
}
this.models.put("conferences", conferenceService.findAll());
}
}
The HTML5 resource and replacement are on Thymeleaf. We wrote about it in a previous post, Thymeleaf puts the Java instance information on the HTML5 template dynamically.
<!DOCTYPE html>
<html xmlns:th="https://www.thymeleaf.org">
<html>
<head>
<title>Latin America Conf (Session)</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="/css/bootstrap.min.css" />
<meta charset="UTF-8" />
</head>
<body>
<div class="container">
<h1>Conference</h1>
<form
th:action="@{/conference/add}"
method="post"
accept-charset="UTF-8"
>
<input id="id" name="id" type="hidden" th:value="${conference.id}" />
<div class="form-group">
<label for="conferenceName">Name</label>
<input
type="text"
class="form-control"
th:value="${conference.name}"
name="name"
id="conferenceName"
placeholder="Enter Session Name"
required
/>
</div>
<div class="form-group">
<label for="conferenceCity">City</label>
<input
type="text"
class="form-control"
th:value="${conference.city}"
name="city"
id="conferenceCity"
placeholder="Enter Conference City"
required
/>
</div>
<div class="form-group">
<label for="conferenceLink">Link</label>
<input
type="url"
class="form-control"
th:value="${conference.link}"
name="link"
id="conferenceLink"
placeholder="Enter Conference Link"
required
/>
</div>
<div class="form-group">
<label for="conferenceYear">Year</label>
<input
type="number"
class="form-control"
th:value="${conference.year}"
name="year"
id="conferenceYear"
placeholder="Enter Conference Year"
required
/>
</div>
<div class="form-group">
<label for="conferenceSpeakers">Speakers</label>
<select
class="form-control"
id="conferenceSpeakers"
th:value="${conference.speakersIds}"
name="speakers"
multiple
>
<tr th:each="speaker : ${speakers}">
<option
th:value="${speaker.id}"
th:text="${speaker.name}"
th:selected="${conference.speakersIds.contains(speaker.id)}"
></option>
</tr>
</select>
</div>
<div class="form-group">
<label for="conferenceSpeakers">Sessions</label>
<select
class="form-control"
id="conferenceSessions"
th:value="${conference.sessionsIds}"
name="presentations"
multiple
>
<tr th:each="presentation : ${presentations}">
<option
th:value="${presentation.id}"
th:text="${presentation.name}"
th:selected="${conference.sessionsIds.contains(presentation.id)}"
></option>
</tr>
</select>
</div>
<button type="submit" class="btn">Save</button>
</form>
</div>
<script src="https://code.jquery.com/jquery.js"></script>
<script src="/js/bootstrap.min.js"></script>
</body>
</html>
</html>
Finally, the code
In this post, we finally saw some code and design, which is always exciting! Eclipse MicroProfile has a bright future integrated with Jakarta EE to allow Java developers to create several applications styles—like microservices and monoliths—that use either JPA or NoSQL. Besides that, these projects offer scalability and straightforward Java applications for your business. Platform.sh delivers on the promise of a NoOps pipeline for your Java microservices.