O termo microservice é um tópico importante e amplamente discutido. Após falar um pouco sobre as camadas de software de um microservice na primeira parte do artigo, neste artigo vamos abordar o código e o design para cada serviço.
Ao criar uma aplicação com foco em código limpo, sempre retornamos aos conceitos de design e arquitetura. A arquitetura é o processo de software que lida com flexibilidade, escalabilidade, usabilidade, segurança e outros pontos, permitindo que tenhamos mais tempo para nos concentrar no negócio e não na tecnologia. Alguns exemplos de arquitetura incluem:
- Arquitetura serverless: Projetos de aplicações que incorporam serviços BaaS (Backend como Serviço) de terceiros e incluem um código personalizado executado em contêineres temporários gerenciados em uma plataforma FaaS (Funções como Serviço);
- Arquitetura orientada a eventos: Um padrão de arquitetura de software que promove a produção, a detecção, o consumo e a reação à eventos;
- Arquitetura de microservices: Uma variante do estilo de arquitetura orientada a serviços (SOA) que estrutura uma aplicação como uma coleção de serviços minimamente acoplados. Em uma arquitetura de microservices, os serviços são granularizados e os protocolos são leves.
O design tem uma tarefa de baixo nível, que supervisiona o código, como o que cada módulo fará, o escopo da classe, a proposta de funções e assim por diante.
- SOLID: Os cinco princípios de design que tornam os projetos de software mais compreensíveis, flexíveis e sustentáveis;
- Padrões de design (Design Patterns): As soluções ideais para problemas comuns no design de software. Cada padrão é como um modelo que se pode personalizar para resolver um problema de design de código específico.
Uma vez discutidas as diferenças entre design e arquitetura vamos um pouco mais fundo para detalhar um pouco sobre cada serviço que cada microservice executa. Para ter uma visão de todo o contexto, vale a leitura da primeira parte deste artigo.
Serviço do palestrante
O primeiro serviço que abordaremos é o serviço do palestrante, que usa Thorntail e PostgreSQL. O MicroProfile do Eclipse não tem suporte para um banco de dados relacional, porém, podemos utilizar a especificação de seu irmão mais velho e maduro, o Jakarta EE. Como primeiro passo, vamos definir a entidade Speaker
.
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;
// ...
}
O próximo código que será mostrado é o serviço dos palestrantes. A integração com o JPA acontece de maneira bastante simples. Um ponto importante para salientar nesse código é a anotação Transactional ele garante que ao fim do método a operação será efetivada no banco e caso exista uma exceção será realizado o rollback. Esse recurso muito poderoso é realizado graças ao CDI interceptor.
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();
}
}
Temos um recurso para expor os serviços através de uma solicitação HTTP. Criamos uma camada no DTO para evitar a perda do encapsulamento.
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);
}
}
Pensando no aspecto do container utilizando Platform.sh como PaaS, é necessário ir no arquivo .platform.app.yaml
, para verificar se a aplicação do palestrante tem acesso à instância do PostgreSQL e qualquer outra coisa relacionada ao banco de dados que o Platform.sh será responsável.
relationships:
postgresql: 'postgresql:postgresql'
Serviço da palestra
O serviço da palestra (Session) manipulará as atividades da conferência, permitindo que um usuário encontre as palestras relacionadas aos tópicos, por exemplo, uma palestra que contenha nuvem e/ou Java.
Será utilizado o serviço Elasticsearch, além do Jakarta NoSQL e KumuluzEE como implementação do MicroProfile.
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;
}
Estamos aplicando o Elasticsearch como uma engine de pesquisa. O Jakarta NoSQL possui uma especialização que permite usar os recursos específicos para cada banco de dados NoSQL. Portanto, teremos uma mistura de repositório e ElasticsearchTemplate, uma especialidade da 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);
}
}
Serviço da conferência
O serviço da conferência é o mais conectado dos serviços, porque precisa manter as informações de ambos os serviços de palestra e palestrante. Neste caso será utilizado o Payara Micro e MongoDB com 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;
}
Um dos benefícios do MongoDB é que podemos usar o subdocumento ao invés de criar um relacionamento. Portanto, podemos incorporar o palestrante e a conferência em vez de fazer junções. Observe que as entidades palestrante e conferência possuem informações resumidas, ou seja, apenas os campos nome e 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);
}
}
Serviço Cliente
O cliente mostrará a aplicação RESTful usando HTML5 com o Eclipse Krazo. Sim, o Eclipse Krazo possui várias extensões de mecanismo para usar mais do que o JSP, como o HTML5. A extensão que usaremos é o Thymeleaf com 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>
A primeira etapa do cliente é criar a ponte para solicitar informações dos serviços. Felizmente, temos um Eclipse MicroProfile Rest Client para lidar apenas com interfaces e nada mais.
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);
}
Para garantir a disponibilidade dos serviços, realizamos uma verificação de integridade do microprofile do Eclipse, para que possamos avaliar o status do HTTP e o tempo de resposta em milissegundos.
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 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";
}
}
Podemos acessar o status em 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"
}
Quando os serviços e a verificação de integridade estiverem prontos, vamos para os controladores. O Eclipse Krazo é uma API construída em JAX-RS. Portanto, qualquer desenvolvedor de Jakarta EE se sentirá em casa ao criar uma classe controller.
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());
}
}
Um ponto importante é que estamos utilizando o HTML5 com Thymeleaf, muito similar para quem já trabalha com o Spring MVC, a única diferença é que nesse caso estamos utilizando o poder da especificação.
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<html>
<head>
<title>Latin America Conf (Session)</title>
<meta content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="/css/bootstrap.min.css">
<meta charset="UTF-8">
</head>
<body>
<div>
<h1>Conference</h1>
<form th:action="@{/conference/add}" method="post" accept-charset="UTF-8">
<input 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}" 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}" 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}" 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}" placeholder="Enter Conference Year" required>
</div>
<div class="form-group">
<label for="conferenceSpeakers">Speakers</label>
<select class="form-control" th:value="${conference.speakersIds}" 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" th:value="${conference.sessionsIds}" 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">Save</button>
</form>
</div>
<script src="https://code.jquery.com/jquery.js"></script>
<script src="/js/bootstrap.min.js"></script>
</body>
</html>
Finalmente, o código
Neste post, finalmente vimos o código e o design, que é sempre emocionante! O Eclipse MicroProfile tem um futuro brilhante integrado ao Jakarta EE para permitir que os desenvolvedores Java criem vários estilos de aplicações, como microservices e monólitos, que usam JPA ou NoSQL. Além disso, esses projetos oferecem escalabilidade e aplicações Java simples para o seu negócio.
Sobre o autor
Otávio é engenheiro de software na Platform.sh, com grande experiência em desenvolvimento open source, com diversas contribuições ao JBoss Weld, Hibernate, Apache Commons e outros projetos. Focado em desenvolvimento poliglota e aplicações de alto desempenho, Otávio trabalhou em grandes projetos nas áreas de finanças, governo, mídias sociais e e-commerce. Membro do comitê executivo do JCP e de vários Expert Groups de JSRs, é também Java Champion e recebeu os prêmios JCP Outstanding Award e Duke's Choice Award.