BT

Início Artigos Microservices na nuvem - Parte 2

Microservices na nuvem - Parte 2

Favoritos

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.

Avalie esse artigo

Relevância
Estilo/Redação

Olá visitante

Você precisa cadastrar-se no InfoQ Brasil ou para enviar comentários. Há muitas vantagens em se cadastrar.

Obtenha o máximo da experiência do InfoQ Brasil.

HTML é permitido: a,b,br,blockquote,i,li,pre,u,ul,p

Comentários da comunidade

HTML é permitido: a,b,br,blockquote,i,li,pre,u,ul,p

HTML é permitido: a,b,br,blockquote,i,li,pre,u,ul,p

BT

Seu cadastro no InfoQ está atualizado? Poderia rever suas informações?

Nota: se você alterar seu email, receberá uma mensagem de confirmação

Nome da empresa:
Cargo/papel na empresa:
Tamanho da empresa:
País:
Estado:
Você vai receber um email para validação do novo endereço. Esta janela pop-up fechará em instantes.