BT

Accueil InfoQ Articles Microservices Dans Le Cloud, Seconde Partie

Microservices Dans Le Cloud, Seconde Partie

Favoris

Points Clés

  • Comprendre ce que sont les microservices
  • Comprendre la différence entre architecture et conception
  • Utilisation de JPA avec Eclipse MicroProfile
  • Découverte de KumuluZEE, Thorntail, Payara et Apache TomEE
  • Intégration d’Eclipse Microprofile et JakartaEE avec les bases de données relationnelles et NoSQL

Les microservices représentent un grand sujet important dans la conception de logiciels, et à juste titre. Ils ont beaucoup d'avantages lorsqu'il s'agit de gérer la complexité de l'infrastructure, que nous avons abordés dans le premier article. Maintenant, parlons du code et de la conception. Dans cet article, nous allons plonger en profondeur dans chaque module.

Lors de la création d'une application, un code propre implique que la conception et l'architecture aient été prises en compte. L'architecture est le processus logiciel qui gère la flexibilité, l'évolutivité, l'utilisabilité, la sécurité et d'autres points, de sorte que vous ayez plus de temps pour vous concentrer sur votre business plutôt que sur la technologie. Voici quelques exemples d'architecture :

  • Architecture sans serveur (serverless). Une conception d'applications qui intègre des services BaaS (Backend-as-a-Service) de tierces parties et incluent du code personnalisé exécuté dans des conteneurs éphémères gérés sur une plate-forme Functions-as-a-Service (FaaS).

  • Architecture événementielle. Un modèle d'architecture logicielle favorisant la production, la détection, la consommation et la réaction aux événements.

  • Architecture de microservices. Variante de l'architecture orientée services (SOA) qui structure une application sous la forme d'un ensemble de services associés. Dans une architecture de microservices, les services sont sur mesure et les protocoles sont légers.

La conception est une tâche de bas niveau qui organise le code, telle que ce que chaque module va faire, la portée de la classe, les fonctionnalités proposées, etc. Des principes et des modèles existent :

  • SOLID. Les cinq principes de design qui rendent les conceptions logicielles plus compréhensibles, plus flexibles et plus faciles à maintenir.

  • Modèles de conception (design patterns). Des solutions éprouvées aux problèmes courants de conception de logiciels. Chaque modèle est comme un plan que vous pouvez personnaliser pour résoudre un problème de design particulier dans votre code.

Le service pour les orateurs

Le premier service dont nous allons parler est le service pour les orateurs, qui utilise Thorntail et PostgreSQL. Eclipse MicroProfile ne prend pas en charge les bases de données relationnelles. Cependant, nous pouvons utiliser la spécification JPA de son grand frère, Jakarta EE. Dans un premier temps, définissons l'entité Speaker qui encapsule un orateur.

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;

    // reste du code   
}

Revoyez l’article précédent qui traite de l'intégration de JPA, en utilisant l'annotation @Transactional et un intercepteur CDI pour gérer la 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();
	}
}

Nous avons une ressource pour exposer les services à travers de requêtes HTTP. Nous allons utiliser des objets de transfert de données (DTO) : ils ont des champs avec accesseurs (getter et setter), qui sont requis par certains frameworks. Lorsque vous utilisez cette couche, vous pouvez avoir un modèle riche à partir de l'entité et ne limiter l'absence d'encapsulation qu'au DTO. Nous avons donc des DTO pour faciliter la création d’un modèle riche à partir des entités.

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);
	}
}

Dans le fichier .platform.app.yaml, nous devons vérifier que l'application Conférencier a accès à l'instance PostgreSQL. Platform.sh va aussi gérer les autres éléments liés à la base de données. Tout ce qui concerne l'infrastructure est important, mais vous n'avez pas à vous en préoccuper : par exemple, la sécurité, la sauvegarde, le mot de passe, le pare-feu, etc.

relationships:
  postgresql: 'postgresql:postgresql'

Le service pour les sessions

Le service Session gère le contenu de la conférence, de sorte qu'un utilisateur puisse trouver des sessions liées à des sujets tels que le cloud et Java. Ce service utilise Elasticsearch avec Jakarta NoSQL et 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;

}

Nous allons utiliser Elasticsearch  comme moteur de recherche. Jakarta NoSQL utilise la spécialisation pour mettre en oeuvre des fonctionnalités particulières pour chaque base de données NoSQL. Par conséquent, nous aurons un mélange de référentiel et d’ElasticsearchTemplate, une spécialisation de 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);
	}
}

Le service pour les conférences

Le service des Conférences est le plus connecté des services, car il doit conserver les informations des services des Orateurs et des Sessions. Il utilise Payara Micro et MongoDB avec 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;

}

Un des avantages de MongoDB est que nous pouvons utiliser un sous-document au lieu de créer une relation. Par conséquent, nous pouvons intégrer orateur et session au lieu de faire des jointures. Notez que les entités session et orateur disposent d'informations courtes telles que le nom et l'identité de l'orateur.

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);
	}
}

Le service pour le client

Le client affichera l'application RESTful en utilisant HTML 5 grâce à Eclipse Krazo. Oui, Eclipse Krazo a plusieurs extensions de moteur à utiliser, bien sûr JSP, mais aussi HTML 5. L'extension que nous utiliserons ici est Thymeleaf avec 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>

La première étape pour le client est de créer les fonctionnalités pour demander des informations aux services. Heureusement, nous avons un client REST dans Eclipse MicroProfile pour interagir uniquement avec les interfaces.

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);
}

Pour s'assurer que les services sont opérationnels, nous utilisons Eclipse Microprofile Health, afin de pouvoir évaluer le statut HTTP et son temps de réponse en millisecondes.

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";
	}
}

Il est possible d’accéder au statut du projet à l'adresse 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"
}

Une fois que les services et le health check sont prêts, l’étape suivante est de développer les contrôleurs. Eclipse Krazo est une API basée sur JAX-RS. Par conséquent, tout développeur Jakarta EE se sentira à l'aise lors de la création d'une 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());
	}
}

Il est possible d’utiliser Thymeleaf avec des modèles HTML5. Nous en avons parlé dans l’article précédent, Thymeleaf met dynamiquement les données d’une instance Java dans le modèle HTML5.

<!DOCTYPE html>
<html xmlns:th="http://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>

Conclusion

Dans cet article, nous avons enfin vu du code et de la conception, ce qui est toujours excitant ! Eclipse MicroProfile a un bel avenir intégré avec Jakarta EE pour permettre aux développeurs Java de créer plusieurs styles d'applications (microservices et monolithiques) qui utilisent soit JPA soit NoSQL. En outre, ces projets offrent une évolutivité et des applications Java simples pour votre entreprise. Vous pouvez compter sur notre équipe à Platform.sh pour les migrer vers le cloud !

 

A propos de l'auteur

Otavio SantanaEncourageant les développeurs dans le monde à construire de meilleurs logiciels plus rapidement, capables d’évoluer dans le Cloud, Otavio est un ingénieur développeur passionné spécialisé en technologies Cloud et Java. Il a de l'expérience principalement dans la persistance des applications multi-technos et de haute performance en finances, médias sociaux et e-commerce. Otavio fait partie de Groupes Experts et Leaders dans plusieurs comités exécutifs JSRs et JCP. Il travaille sur plusieurs projets de la Fondation Apache et Eclipse, comme par exemple Apache Tamaya, MicroProfile, ou Jakarta EE, où il dirige la première spécification de Jakarta EE avec Jakarta NoSQL. JUG Leader et speaker international aux conférences JavaOne et Devoxx, Otavio a reçu de nombreux prix pour ses contributions OSS, tels que le JCP Outstanding Award, Membre de l’année et innovateur JSR, Duke’s Choice Award, ainsi que Java Champion Awards, pour n’en citer que quelques uns.

 

Evaluer cet article

Pertinence
Style

Bonjour étranger!

Vous devez créer un compte InfoQ ou cliquez sur pour déposer des commentaires. Mais il y a bien d'autres avantages à s'enregistrer.

Tirez le meilleur d'InfoQ

Html autorisé: a,b,br,blockquote,i,li,pre,u,ul,p

Commentaires de la Communauté

Html autorisé: a,b,br,blockquote,i,li,pre,u,ul,p

Html autorisé: a,b,br,blockquote,i,li,pre,u,ul,p

BT

Votre profil est-il à jour? Merci de prendre un instant pour vérifier.

Note: en cas de modification de votre adresse email, une validation sera envoyée.

Nom de votre entreprise:
Rôle dans votre entreprise:
Taille de votre entreprise:
Pays/Zone:
État/Province/Région:
Vous allez recevoir un email pour confirmer la nouvelle adresse email. Ce pop-up va se fermer de lui-même dans quelques instants.