2. O servidor Spring 4
![]() |
Na arquitetura acima, abordaremos agora a construção do serviço web / JSON criado com o framework Spring 4. Iremos escrevê-lo em várias etapas:
- primeiro as camadas [business] e [DAO] (Data Access Object). Iremos utilizar o Spring Data aqui;
- depois, o serviço web JSON sem autenticação. Aqui, utilizaremos o Spring MVC;
- depois, adicionaremos o componente de autenticação utilizando o Spring Security.
Começaremos por explicar a estrutura da base de dados subjacente à aplicação.
2.1. A base de dados
![]() |
A base de dados, doravante designada por [ dbrdvmedecins], é uma base de dados MySQL5 com as seguintes tabelas:
![]() |
As consultas são geridas pelas seguintes tabelas:
- [médicos]: contém a lista de médicos do consultório;
- [clientes]: contém a lista de pacientes da clínica;
- [slots]: contém os horários disponíveis para cada médico;
- [rv]: contém a lista de consultas dos médicos.
As tabelas [roles], [users] e [users_roles] estão relacionadas com a autenticação. Por enquanto, não vamos abordá-las.
As relações entre as tabelas que gerem as consultas são as seguintes:
![]() |
- um horário pertence a um médico – um médico tem 0 ou mais horários;
- uma consulta liga um cliente a um médico através do intervalo de tempo do médico;
- um cliente tem 0 ou mais consultas;
- um intervalo de tempo está associado a 0 ou mais consultas (em dias diferentes).
2.1.1. A tabela [MEDECINS]
Contém informações sobre os médicos geridos pela aplicação [RdvMedecins].
![]() | ![]() |
- ID: Número de identificação do médico — chave primária da tabela
- VERSION: Número que identifica a versão da linha na tabela. Este número é incrementado em 1 cada vez que é feita uma alteração na linha.
- LAST_NAME: o apelido do médico
- FIRST_NAME: o nome próprio do médico
- TITLE: o seu título (Sra., Sra., Sr.)
2.1.2. A tabela [CLIENTS]
Os clientes dos vários médicos estão armazenados na tabela [CLIENTS]:
![]() | ![]() |
- ID: número de identificação do cliente - chave primária da tabela
- VERSION: número que identifica a versão da linha na tabela. Este número é incrementado em 1 cada vez que é feita uma alteração na linha.
- APELIDO: o apelido do cliente
- NOME: o nome do cliente
- TÍTULO: o seu título (Sra., Sra., Sr.)
2.1.3. A tabela [SLOTS]
Apresenta os horários disponíveis para marcação de consultas:
![]() |
![]() |
- ID: Número de identificação do intervalo de tempo - chave primária da tabela (linha 8)
- VERSION: número que identifica a versão da linha na tabela. Este número é incrementado em 1 cada vez que é feita uma alteração na linha.
- DOCTOR_ID: Número de identificação do médico a quem este intervalo de tempo pertence – chave estrangeira na coluna DOCTORS(ID).
- START_TIME: Hora de início do intervalo de tempo
- MSTART: Minutos de início do intervalo de tempo
- HFIN: hora de fim do intervalo
- MFIN: minutos de fim do intervalo
A segunda linha da tabela [SLOTS] (ver [1] acima) indica, por exemplo, que o intervalo n.º 2 começa às 8h20 e termina às 8h40 e pertence à médica n.º 1 (Sra. Marie PELISSIER).
2.1.4. A tabela [RV]
Apresenta a lista de consultas marcadas para cada médico:
![]() |
- ID: identificador único da consulta – chave primária
- DAY: dia da consulta
- SLOT_ID: intervalo horário da consulta – chave estrangeira no campo [ID] da tabela [SLOTS] – determina tanto o intervalo horário como o médico envolvido.
- CLIENT_ID: ID do cliente para quem a reserva é feita – chave estrangeira no campo [ID] da tabela [CLIENTS]
Esta tabela possui uma restrição de exclusividade nos valores das colunas associadas (DAY, SLOT_ID):
Se uma linha na tabela [RV] tiver o valor (DAY1, SLOT_ID1) para as colunas (DAY, SLOT_ID), este valor não pode aparecer em mais nenhum outro local. Caso contrário, isso significaria que foram marcadas duas consultas ao mesmo tempo para o mesmo médico. Do ponto de vista da programação Java, o controlador JDBC da base de dados lança uma SQLException quando isto ocorre.
A linha com ID igual a 3 (ver [1] acima) significa que foi marcada uma consulta para o horário n.º 20 e o cliente n.º 4 em 23/08/2006. A tabela [SLOTS] indica-nos que o horário n.º 20 corresponde ao intervalo horário das 16h20 às 16h40 e pertence à médica n.º 1 (Sra. Marie PELISSIER). A tabela [CLIENTS] indica-nos que o cliente n.º 4 é a Sra. Brigitte BISTROU.
2.2. Introdução ao Spring Data
Iremos implementar a camada [DAO] do projeto utilizando o Spring Data, um ramo do ecossistema Spring.
![]() |
O site do Spring oferece vários tutoriais para começar a utilizar o Spring [http://spring.io/guides]. Iremos utilizar um deles para apresentar o Spring Data. Para tal, iremos utilizar o Spring Tool Suite (STS).
![]() |
- Em [1], importamos um dos tutoriais de [spring.io/guides];
![]() |
- Em [2], selecionamos o tutorial [Acessar dados com JPA], que demonstra como aceder a uma base de dados utilizando o Spring Data;
- Em [3], selecionamos um projeto configurado pelo Maven;
- em [4], o tutorial está disponível em duas formas: [initial], que é uma versão vazia que preenche seguindo o tutorial, ou [complete], que é a versão final do tutorial. Escolhemos a última;
- Em [5], pode optar por visualizar o tutorial num navegador;
- Em [6], o projeto final.
2.2.1. A configuração Maven do projeto
As dependências Maven do projeto estão configuradas no ficheiro [pom.xml]:
<groupId>org.springframework</groupId>
<artifactId>gs-accessing-data-jpa</artifactId>
<version>0.1.0</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.0.2.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>
</dependencies>
<properties>
<!-- use UTF-8 for everything -->
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<start-class>hello.Application</start-class>
</properties>
- linhas 5–9: definem um projeto Maven pai. Este projeto define a maioria das dependências do projeto. Estas podem ser suficientes, caso em que não são adicionadas dependências adicionais, ou podem não ser, caso em que as dependências em falta são adicionadas;
- linhas 12–15: definem uma dependência do [spring-boot-starter-data-jpa]. Este artefacto contém as classes Spring Data;
- Linhas 16–19: definem uma dependência do SGBD H2, que permite criar e gerir bases de dados na memória.
Vejamos as classes fornecidas por estas dependências:
![]() | ![]() | ![]() |
Existem muitos:
- algumas pertencem ao ecossistema Spring (aquelas que começam por «spring»);
- outros pertencem ao ecossistema Hibernate (hibernate, jboss), cuja implementação JPA estamos a utilizar aqui;
- outras são bibliotecas de testes (JUnit, Hamcrest);
- outras são bibliotecas de registo (log4j, logback, slf4j);
Vamos ficar com todas elas. Para uma aplicação de produção, devemos ficar apenas com as que são necessárias.
Na linha 26 do ficheiro [pom.xml], encontramos a linha:
<start-class>hello.Application</start-class>
Esta linha está ligada às seguintes linhas:
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
Linhas 6–9: O [spring-boot-maven-plugin] permite gerar o JAR executável da aplicação. A linha 26 do ficheiro [pom.xml] especifica então a classe executável deste JAR.
2.2.2. A camada [JPA]
O acesso à base de dados é tratado através de uma camada [JPA], a Java Persistence API:
![]() |
![]() |
A aplicação é básica e gere entidades [Cliente]. A classe [Cliente] faz parte da camada [JPA] e é a seguinte:
package hello;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
@Entity
public class Customer {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private long id;
private String firstName;
private String lastName;
protected Customer() {
}
public Customer(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
@Override
public String toString() {
return String.format("Customer[id=%d, firstName='%s', lastName='%s']", id, firstName, lastName);
}
}
Um cliente tem um ID [id], um nome próprio [firstName] e um apelido [lastName]. Cada instância [Customer] representa uma linha numa tabela de base de dados.
- linha 8: Anotação JPA que garante que a persistência das instâncias [Customer] (Criar, Ler, Atualizar, Eliminar) será gerida por uma implementação JPA. Com base nas dependências do Maven, podemos ver que está a ser utilizada a implementação JPA/Hibernate;
- Linhas 11–12: Anotações JPA que associam o campo [id] à chave primária da tabela [Customer]. A linha 12 indica que a implementação JPA utilizará o método de geração de chave primária específico do SGBD em uso, neste caso o H2;
Não existem outras anotações JPA. Serão, portanto, utilizados valores por defeito:
- a tabela [Customer] receberá o nome da classe, ou seja, [Customer];
- as colunas desta tabela receberão nomes baseados nos campos da classe: [id, firstName, lastName], observando-se que as maiúsculas e minúsculas não são levadas em conta nos nomes das colunas da tabela;
Note-se que a implementação JPA utilizada nunca é nomeada.
2.2.3. A camada [DAO]
![]() |
![]() |
A classe [CustomerRepository] implementa a camada [DAO]. O seu código é o seguinte:
package hello;
import java.util.List;
import org.springframework.data.repository.CrudRepository;
public interface CustomerRepository extends CrudRepository<Customer, Long> {
List<Customer> findByLastName(String lastName);
}
Trata-se, portanto, de uma interface e não de uma classe (linha 7). Ela estende a interface [CrudRepository], uma interface do Spring Data (linha 5). Esta interface é parametrizada por dois tipos: o primeiro é o tipo dos elementos geridos, neste caso o tipo [Customer]; o segundo é o tipo da chave primária dos elementos geridos, neste caso um tipo [Long]. A interface [CrudRepository] é a seguinte:
package org.springframework.data.repository;
import java.io.Serializable;
@NoRepositoryBean
public interface CrudRepository<T, ID extends Serializable> extends Repository<T, ID> {
<S extends T> S save(S entity);
<S extends T> Iterable<S> save(Iterable<S> entities);
T findOne(ID id);
boolean exists(ID id);
Iterable<T> findAll();
Iterable<T> findAll(Iterable<ID> ids);
long count();
void delete(ID id);
void delete(T entity);
void delete(Iterable<? extends T> entities);
void deleteAll();
}
Esta interface define as operações CRUD (Criar – Ler – Atualizar – Eliminar) que podem ser realizadas num tipo JPA T:
- Linha 8: O método `save` é utilizado para persistir uma entidade `T` na base de dados. Ele persiste a entidade utilizando a chave primária que lhe foi atribuída pelo SGBD. Também permite atualizar uma entidade `T` identificada pela sua chave primária `id`. A escolha entre estas duas ações depende do valor da chave primária `id`: se for nulo, ocorre a operação de persistência; caso contrário, ocorre a operação de atualização;
- linha 10: igual ao acima, mas para uma lista de entidades;
- linha 12: o método findOne recupera uma entidade T identificada pela sua chave primária id;
- linha 22: o método delete permite eliminar uma entidade T identificada pela sua chave primária id;
- linhas 24–28: variações do método [delete];
- linha 16: o método [findAll] recupera todas as entidades T persistidas;
- linha 18: igual ao anterior, mas limitado às entidades para as quais foi fornecida uma lista de identificadores;
Voltemos à interface [CustomerRepository]:
package hello;
import java.util.List;
import org.springframework.data.repository.CrudRepository;
public interface CustomerRepository extends CrudRepository<Customer, Long> {
List<Customer> findByLastName(String lastName);
}
- A linha 9 permite-lhe recuperar um [Cliente] pelo seu [apelido];
E é tudo quanto à camada [DAO]. Não existe uma classe de implementação para a interface anterior. Esta é gerada em tempo de execução pelo [Spring Data]. Os métodos da interface [CrudRepository] são implementados automaticamente. Quanto aos métodos adicionados à interface [CustomerRepository], depende. Voltemos à definição de [Customer]:
private long id;
private String firstName;
private String lastName;
O método na linha 9 é implementado automaticamente pelo [Spring Data] porque faz referência ao campo [lastName] (linha 3) de [Customer]. Quando encontra um método [findBySomething] na interface a ser implementada, o Spring Data implementa-o utilizando a seguinte consulta JPQL (Java Persistence Query Language):
Portanto, o tipo T deve ter um campo chamado [something]. Assim, o método
será implementado com código semelhante ao seguinte:
return [em].createQuery("select c from Customer c where c.lastName=:value").setParameter("value",lastName).getResultList()
onde [em] se refere ao contexto de persistência JPA. Isto só é possível se a classe [Customer] tiver um campo chamado [lastName], o que é o caso.
Em conclusão, em casos simples, o Spring Data permite-nos implementar a camada [DAO] com uma interface simples.
2.2.4. A camada [console]
![]() |
![]() |
A classe [Application] é a seguinte:
package hello;
import java.util.List;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Configuration;
@Configuration
@EnableAutoConfiguration
public class Application {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(Application.class);
CustomerRepository repository = context.getBean(CustomerRepository.class);
// save a couple of customers
repository.save(new Customer("Jack", "Bauer"));
repository.save(new Customer("Chloe", "O'Brian"));
repository.save(new Customer("Kim", "Bauer"));
repository.save(new Customer("David", "Palmer"));
repository.save(new Customer("Michelle", "Dessler"));
// fetch all customers
Iterable<Customer> customers = repository.findAll();
System.out.println("Customers found with findAll():");
System.out.println("-------------------------------");
for (Customer customer : customers) {
System.out.println(customer);
}
System.out.println();
// fetch an individual customer by ID
Customer customer = repository.findOne(1L);
System.out.println("Customer found with findOne(1L):");
System.out.println("--------------------------------");
System.out.println(customer);
System.out.println();
// fetch customers by last name
List<Customer> bauers = repository.findByLastName("Bauer");
System.out.println("Customer found with findByLastName('Bauer'):");
System.out.println("--------------------------------------------");
for (Customer bauer : bauers) {
System.out.println(bauer);
}
context.close();
}
}
- Linha 10: indica que a classe é utilizada para configurar o Spring. As versões recentes do Spring podem, de facto, ser configuradas em Java em vez de em XML. Ambos os métodos podem ser utilizados simultaneamente. No código de uma classe anotada com [Configuration], encontramos normalmente beans do Spring, ou seja, definições de classes a instanciar. Aqui, não estão definidos quaisquer beans. É importante notar aqui que, ao trabalhar com um SGBD, devem ser definidos vários beans do Spring:
- um [EntityManagerFactory] que define a implementação JPA a utilizar,
- um [DataSource] que define a fonte de dados a utilizar,
- um [TransactionManager] que define o gestor de transações a utilizar;
Aqui, nenhum destes feijões está definido.
- Linha 11: A anotação [EnableAutoConfiguration] é uma anotação do projeto [Spring Boot] (linhas 5–6). Esta anotação instrui o Spring Boot, através da classe [SpringApplication] (linha 16), a configurar a aplicação com base nas bibliotecas encontradas no seu classpath. Como as bibliotecas do Hibernate estão no classpath, o bean [entityManagerFactory] será implementado utilizando o Hibernate. Como a biblioteca do SGBD H2 está no classpath, o bean [dataSource] será implementado utilizando o H2. No bean [dataSource], devemos também definir o nome de utilizador e a palavra-passe. Aqui, o Spring Boot utilizará o administrador H2 predefinido, que não tem palavra-passe. Como a biblioteca [spring-tx] está no Classpath, será utilizado o gestor de transações do Spring.
Além disso, o diretório que contém a classe [Application] será verificado em busca de beans reconhecidos implicitamente pelo Spring ou definidos explicitamente por anotações do Spring. Assim, as classes [Customer] e [CustomerRepository] serão inspecionadas. Como a primeira possui a anotação [@Entity], será catalogada como uma entidade a ser gerida pelo Hibernate. Como a segunda estende a interface [CrudRepository], será registada como um bean do Spring.
Vamos examinar as linhas 16–17 do código:
ConfigurableApplicationContext context = SpringApplication.run(Application.class);
CustomerRepository repository = context.getBean(CustomerRepository.class);
- Linha 1: O método estático [run] da classe [SpringApplication] no projeto Spring Boot é executado. O seu parâmetro é a classe que possui uma anotação [Configuration] ou [EnableAutoConfiguration]. Tudo o que foi explicado anteriormente irá então ocorrer. O resultado é um contexto de aplicação Spring, ou seja, um conjunto de beans geridos pelo Spring;
- linha 17: solicitamos um bean que implementa a interface [CustomerRepository] a partir deste contexto Spring. Aqui, recuperamos a classe gerada pelo Spring Data para implementar esta interface.
As operações seguintes utilizam simplesmente os métodos do bean que implementa a interface [CustomerRepository]. Repare que, na linha 50, o contexto é fechado. A saída da consola é a seguinte:
- linhas 1-8: o logótipo do projeto Spring Boot;
- linha 9: a classe [hello.Application] é executada;
- linha 10: [AnnotationConfigApplicationContext] é uma classe que implementa a interface [ApplicationContext] do Spring. É um contentor de beans;
- linha 11: o bean [entityManagerFactory] é implementado utilizando a classe [LocalContainerEntityManagerFactory], uma classe do Spring;
- linha 12: aparece [hibernate]. Esta é a implementação JPA que foi escolhida;
- linha 19: um dialeto Hibernate é a variante SQL a ser utilizada com o SGBD. Aqui, o dialeto [H2Dialect] indica que o Hibernate irá funcionar com o SGBD H2;
- linhas 22–24: a tabela [CUSTOMER] é criada. Isto significa que o Hibernate foi configurado para gerar tabelas a partir de definições JPA, neste caso a definição JPA da classe [Customer];
- linhas 27–32: registos do Hibernate a mostrar a inserção de linhas na tabela [CUSTOMER]. Isto significa que o Hibernate foi configurado para gerar registos;
- linhas 35–39: os cinco clientes inseridos;
- linhas 42–44: resultado do método [findOne] da interface;
- linhas 47–50: resultados do método [findByLastName];
- linhas 51 e seguintes: registos do encerramento do contexto Spring.
2.2.5. Configuração manual do projeto Spring Data
Duplicamos o projeto anterior para o projeto [gs-accessing-data-jpa-2]:
![]() |
Neste novo projeto, não vamos recorrer à configuração automática fornecida pelo Spring Boot. Vamos configurá-lo manualmente. Isto pode ser útil se as configurações predefinidas não corresponderem às nossas necessidades.
Primeiro, vamos especificar as dependências necessárias no ficheiro [pom.xml]:
<dependencies>
<!-- Spring Core -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>4.0.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>4.0.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>4.0.5.RELEASE</version>
</dependency>
<!-- Spring transactions -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>4.0.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>4.0.5.RELEASE</version>
</dependency>
<!-- Spring Data -->
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa</artifactId>
<version>1.5.2.RELEASE</version>
</dependency>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot</artifactId>
<version>1.0.2.RELEASE</version>
</dependency>
<!-- Hibernate -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-entitymanager</artifactId>
<version>4.3.4.Final</version>
</dependency>
<!-- H2 Database -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>1.4.178</version>
</dependency>
<!-- Commons DBCP -->
<dependency>
<groupId>commons-dbcp</groupId>
<artifactId>commons-dbcp</artifactId>
<version>1.4</version>
</dependency>
<dependency>
<groupId>commons-pool</groupId>
<artifactId>commons-pool</artifactId>
<version>1.6</version>
</dependency>
</dependencies>
- linhas 3–17: Bibliotecas principais do Spring;
- linhas 19–28: Bibliotecas Spring para gerir transações de base de dados;
- linhas 30–34: Spring Data utilizado para aceder à base de dados;
- linhas 36–40: Spring Boot para iniciar a aplicação;
- linhas 48–52: o SGBD H2;
- linhas 54–63: Os bancos de dados são frequentemente utilizados com pools de conexões abertas, o que evita a abertura e o fechamento repetidos de conexões. Aqui, a implementação utilizada é a do [commons-dbcp];
Ainda no [pom.xml], alteramos o nome da classe executável:
<properties>
...
<start-class>demo.console.Main</start-class>
</properties>
No novo projeto, a entidade [Customer] e a interface [CustomerRepository] permanecem inalteradas. Iremos modificar a classe [Application], que será dividida em duas classes:
- [Config], que será a classe de configuração:
- [Main], que será a classe executável;
![]() |
A classe executável [Main] é a mesma de antes, sem as anotações de configuração:
package demo.console;
import java.util.List;
import org.springframework.boot.SpringApplication;
import org.springframework.context.ConfigurableApplicationContext;
import demo.config.Config;
import demo.entities.Customer;
import demo.repositories.CustomerRepository;
public class Main {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(Config.class);
CustomerRepository repository = context.getBean(CustomerRepository.class);
...
context.close();
}
}
- linha 12: a classe [Main] já não tem quaisquer anotações de configuração;
- linha 16: a aplicação é iniciada com o Spring Boot. O parâmetro [Config.class] é a nova classe de configuração do projeto;
A classe [Config] que configura o projeto é a seguinte:
package demo.config;
import javax.persistence.EntityManagerFactory;
import javax.sql.DataSource;
import org.apache.commons.dbcp.BasicDataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.Database;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
//@ComponentScan(basePackages = { "demo" })
//@EntityScan(basePackages = { "demo.entities" })
@EnableTransactionManagement
@EnableJpaRepositories(basePackages = { "demo.repositories" })
@Configuration
public class Config {
// h2 data source
@Bean
public DataSource dataSource() {
BasicDataSource dataSource = new BasicDataSource();
dataSource.setDriverClassName("org.h2.Driver");
dataSource.setUrl("jdbc:h2:./demo");
dataSource.setUsername("sa");
dataSource.setPassword("");
return dataSource;
}
// the provider JPA
@Bean
public JpaVendorAdapter jpaVendorAdapter() {
HibernateJpaVendorAdapter hibernateJpaVendorAdapter = new HibernateJpaVendorAdapter();
hibernateJpaVendorAdapter.setShowSql(false);
hibernateJpaVendorAdapter.setGenerateDdl(true);
hibernateJpaVendorAdapter.setDatabase(Database.H2);
return hibernateJpaVendorAdapter;
}
// EntityManagerFactory
@Bean
public EntityManagerFactory entityManagerFactory(JpaVendorAdapter jpaVendorAdapter, DataSource dataSource) {
LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
factory.setJpaVendorAdapter(jpaVendorAdapter);
factory.setPackagesToScan("demo.entities");
factory.setDataSource(dataSource);
factory.afterPropertiesSet();
return factory.getObject();
}
// Transaction manager
@Bean
public PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
JpaTransactionManager txManager = new JpaTransactionManager();
txManager.setEntityManagerFactory(entityManagerFactory);
return txManager;
}
}
- linha 22: a anotação [@Configuration] torna a classe [Config] uma classe de configuração do Spring;
- linha 21: a anotação [@EnableJpaRepositories] especifica os diretórios onde se encontram as interfaces [CrudRepository] do Spring Data. Estas interfaces tornar-se-ão componentes do Spring e estarão disponíveis no seu contexto;
- linha 20: a anotação [@EnableTransactionManagement] indica que os métodos das interfaces [CrudRepository] devem ser executados dentro de uma transação;
- linha 19: a anotação [@EntityScan] especifica os diretórios onde as entidades JPA devem ser pesquisadas. Aqui, ela foi comentada porque esta informação foi explicitamente fornecida na linha 50. Esta anotação deve estar presente se estiver a utilizar o modo [@EnableAutoConfiguration] e as entidades JPA não estiverem no mesmo diretório que a classe de configuração;
- linha 18: a anotação [@ComponentScan] permite listar os diretórios onde os componentes Spring devem ser pesquisados. Os componentes Spring são classes marcadas com anotações Spring, tais como @Service, @Component, @Controller, etc. Aqui, não há outros além daqueles definidos na classe [Config], pelo que a anotação foi comentada;
- Linhas 25–33: definem a fonte de dados, a base de dados H2. É a anotação @Bean na linha 25 que torna o objeto criado por este método um componente gerido pelo Spring. O nome do método aqui pode ser qualquer um. No entanto, deve ser nomeado [dataSource] se a EntityManagerFactory na linha 47 estiver ausente e for definida através da configuração automática;
- linha 29: a base de dados será denominada [demo] e será gerada na pasta do projeto;
- Linhas 36–43: definem a implementação JPA utilizada, neste caso uma implementação Hibernate. O nome do método aqui pode ser qualquer um;
- linha 39: sem registos SQL;
- linha 30: a base de dados será criada se não existir;
- linhas 46–54: definem o EntityManagerFactory que irá gerir a persistência JPA. O método deve ser denominado [entityManagerFactory];
- linha 47: o método recebe dois parâmetros dos tipos dos dois beans definidos anteriormente. Estes serão então construídos e injetados pelo Spring como parâmetros do método;
- linha 49: define a implementação JPA a ser utilizada;
- linha 50: especifica os diretórios onde as entidades JPA podem ser encontradas;
- linha 51: define a fonte de dados a ser gerida;
- linhas 57–62: o gestor de transações. O método deve ser denominado [transactionManager]. Recebe o bean das linhas 46–54 como parâmetro;
- linha 60: o gestor de transações é associado ao EntityManagerFactory;
Os métodos anteriores podem ser definidos em qualquer ordem.
A execução do projeto produz os mesmos resultados. Um novo ficheiro aparece na pasta do projeto, o ficheiro da base de dados H2:
![]() |
Finalmente, podemos passar sem o Spring Boot. Criamos uma segunda classe executável [Main2]:
![]() |
A classe [Main2] tem o seguinte código:
package demo.console;
import java.util.List;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import demo.config.Config;
import demo.entities.Customer;
import demo.repositories.CustomerRepository;
public class Main2 {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Config.class);
CustomerRepository repository = context.getBean(CustomerRepository.class);
....
context.close();
}
}
- Linha 15: A classe de configuração [Config] é agora utilizada pela classe Spring [AnnotationConfigApplicationContext]. Como se pode ver na linha 5, já não existe qualquer dependência do Spring Boot.
A execução produz os mesmos resultados que antes.
2.2.6. Criação de um arquivo executável
Para criar um arquivo executável do projeto, proceda da seguinte forma:
![]() |
- em [1]: crie uma configuração de tempo de execução;
- em [2]: do tipo [Aplicação Java]
- em [3]: especifique o projeto a executar (utilize o botão Procurar);
- em [4]: especifique a classe a ser executada;
- em [5]: o nome da configuração de execução — pode ser qualquer nome;
![]() |
- em [6]: exporte o projeto;
- em [7]: como um arquivo JAR executável;
- em [8]: especifique o caminho e o nome do ficheiro executável a ser criado;
- em [9]: o nome da configuração de execução criada em [5];
Depois de fazer isto, abra um terminal na pasta que contém o arquivo executável:
O arquivo é executado da seguinte forma:
.....\dist>java -jar gs-accessing-data-jpa-2.jar
Os resultados apresentados na consola são os seguintes:
2.2.7. Criar um novo projeto Spring Data
Para criar um modelo de projeto Spring Data, siga estes passos:
![]() |
- Em [1], crie um novo projeto;
- em [2]: selecione [Spring Starter Project];
- O projeto gerado será um projeto Maven. Em [3], especifique o nome do grupo do projeto;
- Em [4], especifique o nome do artefacto (um ficheiro JAR, neste caso) que será criado quando o projeto for compilado;
- em [5]: especifique o pacote da classe executável que será criada no projeto;
- em [6]: o nome do projeto no Eclipse – pode ser qualquer coisa (não tem de ser igual ao de [4]);
- em [7]: especifique que está a criar um projeto com uma camada [JPA]. As dependências necessárias para tal projeto serão então incluídas no ficheiro [pom.xml];
![]() |
- em [8]: o projeto criado;
O ficheiro [pom.xml] inclui as dependências necessárias para um projeto JPA:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.1.0.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
- linhas 9–12: dependências necessárias para o JPA — incluirão [Spring Data];
- linhas 13–17: dependências necessárias para testes JUnit integrados com o Spring;
A classe executável [Application] não faz nada, mas está pré-configurada:
package istia.st;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@ComponentScan
@EnableAutoConfiguration
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
A classe de teste [ApplicationTests] não faz nada, mas está pré-configurada:
package istia.st;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = Application.class)
public class ApplicationTests {
@Test
public void contextLoads() {
}
}
- linha 9: a anotação [@SpringApplicationConfiguration] permite que o ficheiro de configuração [Application] seja utilizado. A classe de teste irá, assim, beneficiar de todos os beans definidos neste ficheiro;
- linha 8: a anotação [@RunWith] permite a integração do Spring com o JUnit: a classe pode ser executada como um teste JUnit. [@RunWith] é uma anotação do JUnit (linha 4), enquanto a classe [SpringJUnit4ClassRunner] é uma classe do Spring (linha 6);
Agora que temos um esqueleto de aplicação JPA, podemos completá-lo para escrever a camada de persistência do lado do servidor da nossa aplicação de gestão de compromissos.
2.3. O projeto de servidor do Eclipse
![]() |
![]() |
Os principais componentes do projeto são os seguintes:
- [pom.xml]: o ficheiro de configuração Maven do projeto;
- [rdvmedecins.entities]: as entidades JPA;
- [rdvmedecins.repositories]: interfaces Spring Data para aceder às entidades JPA;
- [rdvmedecins.metier]: a camada [de negócio];
- [rdvmedecins.domain]: as entidades geridas pela camada [de negócio];
- [rdvmdecins.config]: as classes de configuração da camada de persistência;
- [rdvmedecins.boot]: uma aplicação de consola básica;
2.4. A configuração do Maven
![]() | ![]() | ![]() |
O ficheiro [pom.xml] do projeto é o seguinte:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<modelVersion>4.0.0</modelVersion>
<groupId>istia.st.spring4.rdvmedecins</groupId>
<artifactId>rdvmedecins-metier-dao</artifactId>
<version>0.0.1-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.0.0.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>commons-dbcp</groupId>
<artifactId>commons-dbcp</artifactId>
</dependency>
<dependency>
<groupId>commons-pool</groupId>
<artifactId>commons-pool</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>16.0.1</version>
</dependency>
</dependencies>
<properties>
<!-- use UTF-8 for everything -->
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<start-class>istia.st.spring.data.main.Application</start-class>
</properties>
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>http://repo.spring.io/libs-milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
<repository>
<id>org.jboss.repository.releases</id>
<name>JBoss Maven Release Repository</name>
<url>https://repository.jboss.org/nexus/content/repositories/releases</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>http://repo.spring.io/libs-milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>
</pluginRepositories>
</project>
- linhas 8–12: O projeto depende do projeto pai [spring-boot-starter-parent]. Para as dependências já presentes no projeto pai, não é especificada nenhuma versão. Será utilizada a versão definida no projeto pai. As outras dependências são declaradas como habitualmente;
- linhas 14–17: para o Spring Data;
- linhas 18–22: para testes JUnit;
- linhas 23–26: controlador JDBC para o SGBD MySQL5;
- linhas 27–34: pool de conexões Commons DBCP;
- linhas 35–38: biblioteca Jackson para tratamento de JSON;
- linhas 39–43: biblioteca Google Collections;
A versão 1.1.0.RC1 do [spring-boot-starter-parent] utiliza as seguintes versões de bibliotecas:
2.5. Entidades JPA
![]() |
As entidades JPA são os objetos que encapsulam as linhas nas tabelas da base de dados.
![]() |
A classe [AbstractEntity] é a classe pai das entidades [Person, Slot, Appointment]. A sua definição é a seguinte:
package rdvmedecins.entities;
import java.io.Serializable;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.MappedSuperclass;
import javax.persistence.Version;
@MappedSuperclass
public class AbstractEntity implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
protected Long id;
@Version
protected Long version;
@Override
public int hashCode() {
int hash = 0;
hash += (id != null ? id.hashCode() : 0);
return hash;
}
// initialization
public AbstractEntity build(Long id, Long version) {
this.id = id;
this.version = version;
return this;
}
@Override
public boolean equals(Object entity) {
String class1 = this.getClass().getName();
String class2 = entity.getClass().getName();
if (!class2.equals(class1)) {
return false;
}
AbstractEntity other = (AbstractEntity) entity;
return this.id == other.id;
}
// getters and setters
..
}
- linha 11: a anotação [@MappedSuperclass] indica que a classe anotada é uma superclasse das entidades JPA [@Entity];
- linhas 15–17: definem a chave primária [id] para cada entidade. É a anotação [@Id] que torna o campo [id] uma chave primária. A anotação [@GeneratedValue(strategy = GenerationType.AUTO)] indica que o valor desta chave primária é gerado pelo SGBD e que não é imposto nenhum modo de geração;
- Linhas 18–19: definem a versão de cada entidade. A implementação JPA incrementará este número de versão sempre que a entidade for modificada. Este número é utilizado para impedir atualizações simultâneas da entidade por dois utilizadores diferentes: dois utilizadores, U1 e U2, leem a entidade E com um número de versão igual a V1. U1 modifica E e persiste esta alteração na base de dados: o número de versão passa então a V1+1. U2, por sua vez, modifica E e persiste essa alteração na base de dados: receberá uma exceção porque a sua versão (V1) difere da que se encontra na base de dados (V1+1);
- linhas 29–33: o método [build] inicializa os dois campos de [AbstractEntity]. Este método devolve uma referência à instância de [AbstractEntity] assim inicializada;
- linhas 36–44: o método [equals] da classe é redefinido: duas entidades são consideradas iguais se tiverem o mesmo nome de classe e o mesmo identificador id;
A entidade [Person] é a classe pai das entidades [Doctor] e [Client]:
package rdvmedecins.entities;
import javax.persistence.Column;
import javax.persistence.MappedSuperclass;
@MappedSuperclass
public class Personne extends AbstractEntity {
private static final long serialVersionUID = 1L;
// attributes of a person
@Column(length = 5)
private String titre;
@Column(length = 20)
private String nom;
@Column(length = 20)
private String prenom;
// default builder
public Personne() {
}
// builder with parameters
public Personne(String titre, String nom, String prenom) {
this.titre = titre;
this.nom = nom;
this.prenom = prenom;
}
// toString
public String toString() {
return String.format("Personne[%s, %s, %s, %s, %s]", id, version, titre, nom, prenom);
}
// getters and setters
...
}
- linha 6: a anotação [@MappedSuperclass] indica que a classe anotada é uma superclasse das entidades JPA [@Entity];
- linhas 10–15: uma pessoa tem um título (Sra.), um nome próprio (Jacqueline) e um apelido (Tatou). Não é fornecida qualquer informação sobre as colunas da tabela. Por predefinição, estas terão, portanto, os mesmos nomes que os campos;
A entidade [Medecin] é a seguinte:
package rdvmedecins.entities;
import javax.persistence.Entity;
import javax.persistence.Table;
@Entity
@Table(name = "medecins")
public class Medecin extends Personne {
private static final long serialVersionUID = 1L;
// default builder
public Medecin() {
}
// builder with parameters
public Medecin(String titre, String nom, String prenom) {
super(titre, nom, prenom);
}
public String toString() {
return String.format("Medecin[%s]", super.toString());
}
}
- linha 6: a classe é uma entidade JPA;
- linha 7: associada à tabela [DOCTORS] na base de dados;
- linha 8: a entidade [Doctor] deriva da entidade [Person];
Um médico pode ser inicializado da seguinte forma:
Se também quisermos atribuir-lhe um ID e uma versão, podemos escrever:
onde o método [build] é aquele definido em [AbstractEntity].
A entidade [Client] é a seguinte:
package rdvmedecins.entities;
import javax.persistence.Entity;
import javax.persistence.Table;
@Entity
@Table(name = "clients")
public class Client extends Personne {
private static final long serialVersionUID = 1L;
// default builder
public Client() {
}
// builder with parameters
public Client(String titre, String nom, String prenom) {
super(titre, nom, prenom);
}
// identity
public String toString() {
return String.format("Client[%s]", super.toString());
}
}
- linha 6: a classe é uma entidade JPA;
- linha 7: associada à tabela [CLIENTS] na base de dados;
- linha 8: a entidade [Client] deriva da entidade [Person];
A entidade [TimeSlot] é a seguinte:
package rdvmedecins.entities;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
@Entity
@Table(name = "creneaux")
public class Creneau extends AbstractEntity {
private static final long serialVersionUID = 1L;
// characteristics of a RV slot
private int hdebut;
private int mdebut;
private int hfin;
private int mfin;
// a slot is linked to a doctor
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "id_medecin")
private Medecin medecin;
// foreign key
@Column(name = "id_medecin", insertable = false, updatable = false)
private long idMedecin;
// default builder
public Creneau() {
}
// builder with parameters
public Creneau(Medecin medecin, int hdebut, int mdebut, int hfin, int mfin) {
this.medecin = medecin;
this.hdebut = hdebut;
this.mdebut = mdebut;
this.hfin = hfin;
this.mfin = mfin;
}
// toString
public String toString() {
return String.format("Créneau[%d, %d, %d, %d:%d, %d:%d]", id, version, idMedecin, hdebut, mdebut, hfin, mfin);
}
// foreign key
public long getIdMedecin() {
return idMedecin;
}
// setters - getters
...
}
- linha 10: a classe é uma entidade JPA;
- linha 11: associada à tabela [CRENEAUX] na base de dados;
- linha 12: a entidade [Creneau] deriva da entidade [AbstractEntity] e, portanto, herda os campos [id] e [version];
- linha 16: hora de início do intervalo (14);
- linha 17: minutos de início do intervalo (20);
- linha 18: hora de fim do intervalo (14);
- linha 19: minutos de fim do intervalo (40);
- linhas 22–24: o médico responsável pelo horário. A tabela [CRENEAUX] possui uma chave estrangeira na tabela [MEDECINS]. Esta relação é representada pelas linhas 22–24;
- Linha 22: A anotação [@ManyToOne] indica uma relação muitos-para-um (slots) para um (médico). O atributo [fetch=FetchType.LAZY] especifica que, quando uma entidade [Creneau] é solicitada a partir do contexto de persistência e tem de ser recuperada da base de dados, a entidade [Medecin] não é recuperada juntamente com ela. A vantagem deste modo é que a entidade [Doctor] só é recuperada se o programador a solicitar. Isto poupa memória e melhora o desempenho;
- linha 23: especifica o nome da coluna da chave estrangeira na tabela [CRENEAUX];
- Linhas 27–28: a chave estrangeira na tabela [MEDECINS];
- linha 27: a coluna [ID_MEDECIN] já foi utilizada na linha 23. Isto significa que pode ser modificada de duas formas diferentes, o que não é permitido pela norma JPA. Por isso, adicionamos os atributos [insertable = false, updatable = false], o que significa que a coluna só pode ser lida;
A entidade [Rv] é a seguinte:
package rdvmedecins.entities;
import java.util.Date;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
@Entity
@Table(name = "rv")
public class Rv extends AbstractEntity {
private static final long serialVersionUID = 1L;
// characteristics of an Rv
@Temporal(TemporalType.DATE)
private Date jour;
// an appointment is linked to a customer
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "id_client")
private Client client;
// an appointment is linked to a time slot
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "id_creneau")
private Creneau creneau;
// foreign keys
@Column(name = "id_client", insertable = false, updatable = false)
private long idClient;
@Column(name = "id_creneau", insertable = false, updatable = false)
private long idCreneau;
// default builder
public Rv() {
}
// with parameters
public Rv(Date jour, Client client, Creneau creneau) {
this.jour = jour;
this.client = client;
this.creneau = creneau;
}
// toString
public String toString() {
return String.format("Rv[%d, %s, %d, %d]", id, jour, client.id, creneau.id);
}
// foreign keys
public long getIdCreneau() {
return idCreneau;
}
public long getIdClient() {
return idClient;
}
// getters and setters
...
}
- linha 14: a classe é uma entidade JPA;
- linha 15: associada à tabela [RV] na base de dados;
- linha 16: a entidade [Rv] deriva da entidade [AbstractEntity] e, portanto, herda os campos [id] e [version];
- linha 21: a data do compromisso;
- linha 20: o tipo Java [Date] contém tanto uma data como uma hora. Aqui especificamos que apenas a data é utilizada;
- linhas 24–26: o cliente para quem esta marcação foi feita. A tabela [RV] tem uma chave estrangeira na tabela [CLIENTS]. Esta relação é representada pelas linhas 24–26;
- linhas 29–31: o intervalo de tempo da marcação. A tabela [RV] possui uma chave estrangeira na tabela [CRENEAUX]. Esta relação é representada pelas linhas 29–31;
- linhas 34–35: a chave estrangeira [idClient];
- linhas 36–37: a chave estrangeira [idCreneau];
2.6. A camada [DAO]
![]() |
Iremos implementar a camada [DAO] utilizando o Spring Data:
![]() |
A camada [DAO] é implementada utilizando quatro interfaces Spring Data:
- [ClientRepository]: fornece acesso às entidades JPA [Client];
- [CreneauRepository]: fornece acesso às entidades JPA [Creneau];
- [MedecinRepository]: fornece acesso às entidades JPA [Medecin];
- [RvRepository]: fornece acesso às entidades JPA [Rv];
A interface [MedecinRepository] é a seguinte:
package rdvmedecins.repositories;
import org.springframework.data.repository.CrudRepository;
import rdvmedecins.entities.Medecin;
public interface MedecinRepository extends CrudRepository<Medecin, Long> {
}
- Linha 7: A interface [MedecinRepository] simplesmente herda os métodos da interface [CrudRepository] sem adicionar nenhum outro;
A interface [ClientRepository] é a seguinte:
package rdvmedecins.repositories;
import org.springframework.data.repository.CrudRepository;
import rdvmedecins.entities.Client;
public interface ClientRepository extends CrudRepository<Client, Long> {
}
- Linha 7: A interface [ClientRepository] simplesmente herda os métodos da interface [CrudRepository] sem adicionar nenhum outro;
A interface [CreneauRepository] é a seguinte:
package rdvmedecins.repositories;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
import rdvmedecins.entities.Creneau;
public interface CreneauRepository extends CrudRepository<Creneau, Long> {
// list of physician slots
@Query("select c from Creneau c where c.medecin.id=?1")
Iterable<Creneau> getAllCreneaux(long idMedecin);
}
- linha 8: a interface [CreneauRepository] herda os métodos da interface [CrudRepository];
- linhas 10-11: o método [getAllCreneaux] recupera os horários disponíveis de um médico;
- linha 11: o parâmetro é o ID do médico. O resultado é uma lista de horários na forma de um objeto [Iterable<Creneau>];
- linha 10: a anotação [@Query] é utilizada para especificar a consulta JPQL (Java Persistence Query Language) que implementa o método. O parâmetro [?1] será substituído pelo parâmetro [idMedecin] do método;
A interface [RvRepository] é a seguinte:
package rdvmedecins.repositories;
import java.util.Date;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
import rdvmedecins.entities.Rv;
public interface RvRepository extends CrudRepository<Rv, Long> {
@Query("select rv from Rv rv left join fetch rv.client c left join fetch rv.creneau cr where cr.medecin.id=?1 and rv.jour=?2")
Iterable<Rv> getRvMedecinJour(long idMedecin, Date jour);
}
- Linha 10: A interface [RvRepository] herda os métodos da interface [CrudRepository];
- Linhas 12–13: O método [getRvMedecinJour] recupera as consultas de um médico para um determinado dia;
- linha 13: Os parâmetros são o ID do médico e o dia. O resultado é uma lista de consultas na forma de um objeto [Iterable<Rv>];
- linha 12: a anotação [@Query] permite especificar a consulta JPQL que implementa o método. O parâmetro [?1] será substituído pelo parâmetro [idMedecin] do método, e o parâmetro [?2] será substituído pelo parâmetro [jour] do método. A seguinte consulta JPQL não é suficiente:
porque os campos da classe Rv, dos tipos [Client] e [Creneau], são recuperados no modo [FetchType.LAZY], o que significa que têm de ser explicitamente solicitados para serem obtidos. Isto é feito na consulta JPQL utilizando a sintaxe [left join fetch entity], que requer que seja realizada uma junção com a tabela referenciada pela chave estrangeira, a fim de recuperar a entidade referenciada;
2.7. A camada [business]
![]() |
![]() |
- [IMetier] é a interface da camada [business] e [Metier] é a sua implementação;
- [DoctorDailySchedule] e [DoctorDailySlot] são duas entidades de negócio;
2.7.1. As entidades
A entidade [DoctorTimeSlot] associa um intervalo de tempo a qualquer consulta marcada dentro desse intervalo:
package rdvmedecins.domain;
import java.io.Serializable;
import rdvmedecins.entities.Creneau;
import rdvmedecins.entities.Rv;
public class CreneauMedecinJour implements Serializable {
private static final long serialVersionUID = 1L;
// fields
private Creneau creneau;
private Rv rv;
// manufacturers
public CreneauMedecinJour() {
}
public CreneauMedecinJour(Creneau creneau, Rv rv) {
this.creneau=creneau;
this.rv=rv;
}
// toString
@Override
public String toString() {
return String.format("[%s %s]", creneau, rv);
}
// getters and setters
...
}
- linha 12: o intervalo de tempo;
- linha 13: a consulta, se houver – nulo caso contrário;
A entidade [AgendaMedecinJour] é a agenda de um médico para um determinado dia, ou seja, a lista das suas consultas:
package rdvmedecins.domain;
import java.io.Serializable;
import java.text.SimpleDateFormat;
import java.util.Date;
import rdvmedecins.entities.Medecin;
public class AgendaMedecinJour implements Serializable {
private static final long serialVersionUID = 1L;
// fields
private Medecin medecin;
private Date jour;
private CreneauMedecinJour[] creneauxMedecinJour;
// manufacturers
public AgendaMedecinJour() {
}
public AgendaMedecinJour(Medecin medecin, Date jour, CreneauMedecinJour[] creneauxMedecinJour) {
this.medecin = medecin;
this.jour = jour;
this.creneauxMedecinJour = creneauxMedecinJour;
}
public String toString() {
StringBuffer str = new StringBuffer("");
for (CreneauMedecinJour cr : creneauxMedecinJour) {
str.append(" ");
str.append(cr.toString());
}
return String.format("Agenda[%s,%s,%s]", medecin, new SimpleDateFormat("dd/MM/yyyy").format(jour), str.toString());
}
// getters and setters
...
}
- linha 13: o médico;
- linha 14: o dia no calendário;
- linha 15: os horários disponíveis, com ou sem marcação;
2.7.2. O serviço
A interface da camada [de negócios] é a seguinte:
package rdvmedecins.metier;
import java.util.Date;
import java.util.List;
import rdvmedecins.domain.AgendaMedecinJour;
import rdvmedecins.entities.Client;
import rdvmedecins.entities.Creneau;
import rdvmedecins.entities.Medecin;
import rdvmedecins.entities.Rv;
public interface IMetier {
// customer list
public List<Client> getAllClients();
// list of doctors
public List<Medecin> getAllMedecins();
// list of physician slots
public List<Creneau> getAllCreneaux(long idMedecin);
// list of doctor's appointments on a given day
public List<Rv> getRvMedecinJour(long idMedecin, Date jour);
// find a customer identified by its id
public Client getClientById(long id);
// find a customer identified by its id
public Medecin getMedecinById(long id);
// find an Rv identified by its id
public Rv getRvById(long id);
// find a time slot identified by its id
public Creneau getCreneauById(long id);
// add a RV to the list
public Rv ajouterRv(Date jour, Creneau créneau, Client client);
// delete a RV
public void supprimerRv(Rv rv);
// job
public AgendaMedecinJour getAgendaMedecinJour(long idMedecin, Date jour);
}
Os comentários explicam a função de cada método.
A implementação da interface [IMetier] é a seguinte classe [Metier]:
package rdvmedecins.metier;
import java.util.Date;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import rdvmedecins.domain.AgendaMedecinJour;
import rdvmedecins.domain.CreneauMedecinJour;
import rdvmedecins.entities.Client;
import rdvmedecins.entities.Creneau;
import rdvmedecins.entities.Medecin;
import rdvmedecins.entities.Rv;
import rdvmedecins.repositories.ClientRepository;
import rdvmedecins.repositories.CreneauRepository;
import rdvmedecins.repositories.MedecinRepository;
import rdvmedecins.repositories.RvRepository;
import com.google.common.collect.Lists;
@Service("métier")
public class Metier implements IMetier {
// repositories
@Autowired
private MedecinRepository medecinRepository;
@Autowired
private ClientRepository clientRepository;
@Autowired
private CreneauRepository creneauRepository;
@Autowired
private RvRepository rvRepository;
// interface implementation
@Override
public List<Client> getAllClients() {
return Lists.newArrayList(clientRepository.findAll());
}
@Override
public List<Medecin> getAllMedecins() {
return Lists.newArrayList(medecinRepository.findAll());
}
@Override
public List<Creneau> getAllCreneaux(long idMedecin) {
return Lists.newArrayList(creneauRepository.getAllCreneaux(idMedecin));
}
@Override
public List<Rv> getRvMedecinJour(long idMedecin, Date jour) {
return Lists.newArrayList(rvRepository.getRvMedecinJour(idMedecin, jour));
}
@Override
public Client getClientById(long id) {
return clientRepository.findOne(id);
}
@Override
public Medecin getMedecinById(long id) {
return medecinRepository.findOne(id);
}
@Override
public Rv getRvById(long id) {
return rvRepository.findOne(id);
}
@Override
public Creneau getCreneauById(long id) {
return creneauRepository.findOne(id);
}
@Override
public Rv ajouterRv(Date jour, Creneau créneau, Client client) {
return rvRepository.save(new Rv(jour, client, créneau));
}
@Override
public void supprimerRv(Rv rv) {
rvRepository.delete(rv.getId());
}
public AgendaMedecinJour getAgendaMedecinJour(long idMedecin, Date jour) {
...
}
}
- linha 24: a anotação [@Service] é uma anotação Spring que torna a classe anotada um componente gerido pelo Spring. Pode ou não atribuir um nome a um componente. Este tem o nome [business];
- linha 25: a classe [Metier] implementa a interface [IMetier];
- linha 28: a anotação [@Autowired] é uma anotação do Spring. O valor do campo anotado desta forma será inicializado (injetado) pelo Spring com a referência a um componente do Spring do tipo ou nome especificado. Aqui, a anotação [@Autowired] não especifica um nome. Portanto, será realizada uma injeção baseada no tipo;
- linha 29: o campo [medecinRepository] será inicializado com a referência a um componente Spring do tipo [MedecinRepository]. Esta será a referência à classe gerada pelo Spring Data para implementar a interface [MedecinRepository] que já apresentámos;
- linhas 30–35: este processo é repetido para as outras três interfaces discutidas;
- linhas 39–41: implementação do método [getAllClients];
- linha 40: utilizamos o método [findAll] da interface [ClientRepository]. Este método retorna um tipo [Iterable<Client>], que convertemos para um [List<Client>] utilizando o método estático [Lists.newArrayList]. A classe [Lists] está definida na biblioteca Google Guava. No [pom.xml], esta dependência foi importada:
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>16.0.1</version>
</dependency>
- linhas 38–86: os métodos da interface [IMetier] são implementados utilizando classes da camada [DAO];
Apenas o método na linha 88 é específico da camada [business]. Foi colocado aqui porque executa lógica de negócio que vai além do simples acesso a dados. Sem este método, não haveria razão para criar uma camada [business]. O método [getAgendaMedecinJour] é o seguinte:
public AgendaMedecinJour getAgendaMedecinJour(long idMedecin, Date jour) {
// list of doctor's time slots
List<Creneau> creneauxHoraires = getAllCreneaux(idMedecin);
// list of bookings for the same doctor on the same day
List<Rv> reservations = getRvMedecinJour(idMedecin, jour);
// a dictionary is created from the Rvs taken
Map<Long, Rv> hReservations = new Hashtable<Long, Rv>();
for (Rv resa : reservations) {
hReservations.put(resa.getCreneau().getId(), resa);
}
// create the agenda for the requested day
AgendaMedecinJour agenda = new AgendaMedecinJour();
// the doctor
agenda.setMedecin(getMedecinById(idMedecin));
// the day
agenda.setJour(jour);
// reservation slots
CreneauMedecinJour[] creneauxMedecinJour = new CreneauMedecinJour[creneauxHoraires.size()];
agenda.setCreneauxMedecinJour(creneauxMedecinJour);
// filling reservation slots
for (int i = 0; i < creneauxHoraires.size(); i++) {
// line i agenda
creneauxMedecinJour[i] = new CreneauMedecinJour();
// time slot
Creneau créneau = creneauxHoraires.get(i);
long idCreneau = créneau.getId();
creneauxMedecinJour[i].setCreneau(créneau);
// is the slot free or reserved?
if (hReservations.containsKey(idCreneau)) {
// the slot is occupied - we note the resa
Rv resa = hReservations.get(idCreneau);
creneauxMedecinJour[i].setRv(resa);
}
}
// we return the result
return agenda;
}
Recomenda-se aos leitores que leiam os comentários. O algoritmo é o seguinte:
- recuperar todos os horários disponíveis para o médico especificado;
- recuperar todas as suas consultas para o dia especificado;
- com esta informação, podemos determinar se um intervalo de tempo está disponível ou reservado;
2.8. Configuração do projeto
![]() |
A classe [DomainAndPersistenceConfig] configura todo o projeto:
package rdvmedecins.config;
import javax.sql.DataSource;
import org.apache.commons.dbcp.BasicDataSource;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.orm.jpa.EntityScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.vendor.Database;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@EnableJpaRepositories(basePackages = { "rdvmedecins.repositories" })
@EnableAutoConfiguration
@ComponentScan(basePackages = { "rdvmedecins" })
@EntityScan(basePackages = { "rdvmedecins.entities" })
@EnableTransactionManagement
public class DomainAndPersistenceConfig {
// the MySQL data source
@Bean
public DataSource dataSource() {
BasicDataSource dataSource = new BasicDataSource();
dataSource.setDriverClassName("com.mysql.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://localhost:3306/dbrdvmedecins");
dataSource.setUsername("root");
dataSource.setPassword("");
return dataSource;
}
// provider JPA - not required if you're happy with the default values used by Spring boot
// here we define it to enable / disable logs SQL
@Bean
public JpaVendorAdapter jpaVendorAdapter() {
HibernateJpaVendorAdapter hibernateJpaVendorAdapter = new HibernateJpaVendorAdapter();
hibernateJpaVendorAdapter.setShowSql(false);
hibernateJpaVendorAdapter.setGenerateDdl(false);
hibernateJpaVendorAdapter.setDatabase(Database.MYSQL);
return hibernateJpaVendorAdapter;
}
// the EntityManagerFactory and TransactionManager are defined with default values by Spring boot
}
- Linha 45: Não definiremos os beans [EntityManagerFactory] e [TransactionManager]. Em vez disso, recorreremos à anotação [@EnableAutoConfiguration] do Spring Boot (linha 17);
- Linhas 24–32: Defina a fonte de dados MySQL 5. Este é um bean que o Spring Boot geralmente não consegue configurar automaticamente;
- Linhas 36–43: Também configuramos a implementação JPA para definir o atributo [showSql] do Hibernate como false (linha 39). Por predefinição, está definido como true;
- Por enquanto, os únicos componentes geridos pelo Spring são os beans nas linhas 25 e 37, além dos beans [EntityManagerFactory] e [TransactionManager] através da configuração automática. Precisamos de adicionar os beans das camadas [business] e [DAO];
- A linha 16 adiciona as interfaces do pacote [rdvmdecins.repositories] que herdam da interface [CrudRepository] ao contexto do Spring;
- A linha 18 adiciona ao contexto Spring todas as classes do pacote [rdvmedecins] e suas subclasses que possuem uma anotação Spring. No pacote [rdvmdecins.metier], a classe [Metier] com a sua anotação [@Service] será encontrada e adicionada ao contexto Spring;
- Linha 45: Um bean [entityManagerFactory] será definido por padrão pelo Spring Boot. Temos de indicar a este bean onde se encontram as entidades JPA que ele precisa de gerir. A linha 19 faz isso;
- linha 20: especifica que os métodos das interfaces que herdam da interface [CrudRepository] devem ser executados dentro de uma transação;
2.9. Testes para a camada [business]
A classe [rdvmedecins.tests.Metier] é uma classe de teste Spring/JUnit 4:
package rdvmedecins.tests;
import java.text.ParseException;
import java.util.Date;
import java.util.List;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import rdvmedecins.config.DomainAndPersistenceConfig;
import rdvmedecins.domain.AgendaMedecinJour;
import rdvmedecins.entities.Client;
import rdvmedecins.entities.Creneau;
import rdvmedecins.entities.Medecin;
import rdvmedecins.entities.Rv;
import rdvmedecins.metier.IMetier;
@SpringApplicationConfiguration(classes = DomainAndPersistenceConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class Metier {
@Autowired
private IMetier métier;
@Test
public void test1(){
// customer display
List<Client> clients = métier.getAllClients();
display("Liste des clients :", clients);
// physician display
List<Medecin> medecins = métier.getAllMedecins();
display("Liste des médecins :", medecins);
// display doctor's slots
Medecin médecin = medecins.get(0);
List<Creneau> creneaux = métier.getAllCreneaux(médecin.getId());
display(String.format("Liste des créneaux du médecin %s", médecin), creneaux);
// list of doctor's appointments on a given day
Date jour = new Date();
display(String.format("Liste des rv du médecin %s, le [%s]", médecin, jour), métier.getRvMedecinJour(médecin.getId(), jour));
// add a RV
Rv rv = null;
Creneau créneau = creneaux.get(2);
Client client = clients.get(0);
System.out.println(String.format("Ajout d'un Rv le [%s] dans le créneau %s pour le client %s", jour, créneau,
client));
rv = métier.ajouterRv(jour, créneau, client);
// check
Rv rv2 = métier.getRvById(rv.getId());
Assert.assertEquals(rv, rv2);
display(String.format("Liste des Rv du médecin %s, le [%s]", médecin, jour), métier.getRvMedecinJour(médecin.getId(), jour));
// add a RV in the same slot on the same day
// must trigger an exception
System.out.println(String.format("Ajout d'un Rv le [%s] dans le créneau %s pour le client %s", jour, créneau,
client));
Boolean erreur = false;
try {
rv = métier.ajouterRv(jour, créneau, client);
System.out.println("Rv ajouté");
} catch (Exception ex) {
Throwable th = ex;
while (th != null) {
System.out.println(ex.getMessage());
th = th.getCause();
}
// we note the error
erreur = true;
}
// check for errors
Assert.assertTrue(erreur);
// RV list
display(String.format("Liste des Rv du médecin %s, le [%s]", médecin, jour), métier.getRvMedecinJour(médecin.getId(), jour));
// calendar display
AgendaMedecinJour agenda = métier.getAgendaMedecinJour(médecin.getId(), jour);
System.out.println(agenda);
Assert.assertEquals(rv, agenda.getCreneauxMedecinJour()[2].getRv());
// delete a RV
System.out.println("Suppression du Rv ajouté");
métier.supprimerRv(rv);
// check
rv2 = métier.getRvById(rv.getId());
Assert.assertNull(rv2);
display(String.format("Liste des Rv du médecin %s, le [%s]", médecin, jour), métier.getRvMedecinJour(médecin.getId(), jour));
}
// utility method - displays items in a collection
private void display(String message, Iterable<?> elements) {
System.out.println(message);
for (Object element : elements) {
System.out.println(element);
}
}
}
- linha 22: a anotação [@SpringApplicationConfiguration] permite que o ficheiro de configuração [DomainAndPersistenceConfig], discutido anteriormente, seja utilizado. A classe de teste beneficia, assim, de todos os beans definidos por este ficheiro;
- linha 23: a anotação [@RunWith] permite a integração do Spring com o JUnit: a classe pode ser executada como um teste JUnit. [@RunWith] é uma anotação do JUnit (linha 9), enquanto a classe [SpringJUnit4ClassRunner] é uma classe do Spring (linha 12);
- Linhas 26–27: Injeção de uma referência à camada [business] na classe de teste;
- muitos testes são simplesmente testes visuais:
- linhas 32–33: lista de clientes;
- linhas 35–36: lista de médicos;
- linhas 39–40: lista dos horários disponíveis de um médico;
- linha 43: lista de consultas de um médico;
- linha 50: adicionar uma nova consulta. O método [addAppt] devolve a consulta com informações adicionais, a sua chave primária id;
- linha 53: esta chave primária é utilizada para pesquisar a consulta na base de dados;
- linha 54: verificamos se a consulta que está a ser pesquisada e a consulta encontrada são a mesma. Recorde-se que o método [equals] da entidade [Rv] foi redefinido: duas consultas são iguais se tiverem o mesmo id. Aqui, isto mostra-nos que a consulta adicionada foi de facto inserida na base de dados;
- Linhas 61–73: Tentamos adicionar o mesmo compromisso uma segunda vez. Isto deve ser rejeitado pelo SGBD porque existe uma restrição de unicidade:
CREATE TABLE IF NOT EXISTS `rv` (
`ID` bigint(20) NOT NULL AUTO_INCREMENT,
`JOUR` date NOT NULL,
`ID_CLIENT` bigint(20) NOT NULL,
`ID_CRENEAU` bigint(20) NOT NULL,
`VERSION` int(11) NOT NULL DEFAULT '0',
PRIMARY KEY (`ID`),
UNIQUE KEY `UNQ1_RV` (`JOUR`,`ID_CRENEAU`),
KEY `FK_RV_ID_CRENEAU` (`ID_CRENEAU`),
KEY `FK_RV_ID_CLIENT` (`ID_CLIENT`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_swedish_ci AUTO_INCREMENT=60 ;
A linha 8 acima especifica que a combinação [DAY, SLOT_ID] deve ser única, o que impede que duas marcações sejam agendadas no mesmo intervalo de tempo no mesmo dia.
- linha 73: verificamos se ocorreu realmente uma exceção;
- linha 77: recuperamos o calendário do médico para quem acabámos de adicionar um compromisso;
- linha 79: verificamos se a consulta adicionada está efetivamente presente na agenda;
- linha 82: eliminamos a consulta adicionada;
- linha 84: recuperamos a consulta eliminada da base de dados;
- linha 85: verificamos se recuperámos um ponteiro nulo, indicando que a consulta que procurámos não existe;
O teste é executado com sucesso:
![]() |
2.10. O programa de consola
![]() |
O programa de consola é básico. Ilustra como recuperar uma chave estrangeira:
package rdvmedecins.boot;
import java.text.SimpleDateFormat;
import java.util.Date;
import org.springframework.boot.SpringApplication;
import org.springframework.context.ConfigurableApplicationContext;
import rdvmedecins.config.DomainAndPersistenceConfig;
import rdvmedecins.entities.Client;
import rdvmedecins.entities.Creneau;
import rdvmedecins.entities.Rv;
import rdvmedecins.metier.IMetier;
public class Boot {
// the boot
public static void main(String[] args) {
// prepare the configuration
SpringApplication app = new SpringApplication(DomainAndPersistenceConfig.class);
app.setLogStartupInfo(false);
// launch it
ConfigurableApplicationContext context = app.run(args);
// business
IMetier métier = context.getBean(IMetier.class);
try {
// add a RV to the list
Date jour = new Date();
System.out.println(String.format("Ajout d'un Rv le [%s] dans le créneau 1 pour le client 1", new SimpleDateFormat("dd/MM/yyyy").format(jour)));
Client client = (Client) new Client().build(1L, 1L);
Creneau créneau = (Creneau) new Creneau().build(1L, 1L);
Rv rv = métier.ajouterRv(jour, créneau, client);
System.out.println(String.format("Rv ajouté = %s", rv));
// check
créneau = métier.getCreneauById(1L);
long idMedecin = créneau.getIdMedecin();
display("Liste des rendez-vous", métier.getRvMedecinJour(idMedecin, jour));
} catch (Exception ex) {
System.out.println("Exception : " + ex.getCause());
}
// closing the Spring context
context.close();
}
// utility method - displays items in a collection
private static <T> void display(String message, Iterable<T> elements) {
System.out.println(message);
for (T element : elements) {
System.out.println(element);
}
}
}
O programa adiciona um compromisso e, em seguida, verifica se este foi adicionado.
- linha 19: a classe [SpringApplication] utilizará a classe de configuração [DomainAndPersistenceConfig];
- linha 20: supressão dos registos de arranque da aplicação;
- linha 22: a classe [SpringApplication] é executada. Ela retorna um contexto Spring, ou seja, a lista de beans registados;
- linha 24: é recuperada uma referência ao bean que implementa a interface [IMetier]. Trata-se, portanto, de uma referência à camada [business];
- linhas 27–31: Adicionar uma nova consulta para hoje, para o cliente n.º 1 no horário n.º 1. O cliente e o horário foram criados do zero para demonstrar que apenas são utilizados identificadores. Inicializámos a versão aqui, mas poderíamos ter utilizado qualquer valor. Ela não é utilizada aqui;
- linha 34: queremos saber qual o médico que ocupa o horário n.º 1. Para tal, precisamos de consultar a base de dados para o horário n.º 1. Como estamos no modo [FetchType.LAZY], o médico não é devolvido juntamente com o horário. No entanto, certificámo-nos de incluir um campo [idMedecin] na entidade [Creneau] para recuperar a chave primária do médico;
- linha 35: recuperamos a chave primária do médico;
- linha 36: exibimos a lista de consultas do médico;
A saída da consola é a seguinte:
2.11. Introdução ao Spring MVC
![]() |
Vamos agora abordar a construção da camada web. Esta camada consiste principalmente em métodos que tratam de URLs específicas e respondem com uma linha de texto no formato JSON (JavaScript Object Notation). Esta camada web é uma interface web, por vezes designada por API web. Iremos implementar esta interface utilizando o Spring MVC, outro componente do ecossistema Spring. Começaremos por rever um dos guias disponíveis em [http://spring.io].
2.11.1. O projeto de demonstração
![]() |
- em [1], importamos um dos guias do Spring;
![]() |
- em [2], selecionamos o exemplo [Rest Service];
- em [3], selecionamos o projeto Maven;
- em [4], selecionamos a versão final do guia;
- em [5], confirmamos;
- em [6], o projeto importado;
Os serviços web acessíveis através de URLs padrão que devolvem texto JSON são frequentemente designados por serviços REST (REpresentational State Transfer). Neste documento, referir-me-ei simplesmente ao serviço que vamos construir como um serviço web/JSON. Diz-se que um serviço é RESTful se seguir determinadas regras. Não tentei cumprir essas regras.
Vamos agora examinar o projeto importado, começando pela sua configuração do Maven.
2.11.2. Configuração do Maven
O ficheiro [pom.xml] é o seguinte:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.springframework</groupId>
<artifactId>gs-rest-service</artifactId>
<version>0.1.0</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.1.0.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
</dependencies>
<properties>
<start-class>hello.Application</start-class>
</properties>
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>spring-releases</id>
<url>http://repo.spring.io/release</url>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-releases</id>
<url>http://repo.spring.io/release</url>
</pluginRepository>
</pluginRepositories>
</project>
- linhas 10–14: tal como no projeto [Spring Data], o projeto pai [Spring Boot] está presente;
- linhas 17–20: O artefacto [spring-boot-starter-web] inclui as bibliotecas necessárias para um projeto Spring MVC. Em particular, inclui um servidor Tomcat incorporado. A aplicação será executada neste servidor;
- linhas 21–24: A biblioteca Jackson lida com JSON: convertendo um objeto Java numa cadeia JSON e vice-versa;
Esta configuração inclui um grande número de bibliotecas:
![]() | ![]() |
Acima, vemos os três arquivos do servidor Tomcat.
2.11.3. A arquitetura de um serviço REST Spring
O Spring MVC implementa o padrão arquitetónico MVC (Modelo–Visão–Controlador) da seguinte forma:
![]() |
O processamento de um pedido do cliente decorre da seguinte forma:
- solicitação - os URLs solicitados têm o formato http://machine:port/contexte/Action/param1/param2/....?p1=v1&p2=v2&... O [Dispatcher Servlet] é a classe Spring que lida com os URLs recebidos. Ele «encaminha» o URL para a ação que deve tratá-lo. Estas ações são métodos de classes específicas chamadas [Controllers]. O C em MVC aqui é a cadeia [Dispatcher Servlet, Controller, Action]. Se nenhuma ação tiver sido configurada para tratar a URL recebida, o [Dispatcher Servlet] responderá que a URL solicitada não foi encontrada (erro 404 NOT FOUND);
- o processamento
- a ação selecionada pode utilizar os parâmetros que o [Servlet Dispatcher] lhe passou. Estes podem provir de várias fontes:
- o caminho [/param1/param2/...] da URL,
- os parâmetros da URL [p1=v1&p2=v2],
- dos parâmetros enviados pelo navegador com o seu pedido;
- ao processar a solicitação do utilizador, a ação pode necessitar da camada [de negócios] [2b]. Uma vez processada a solicitação do cliente, ela pode desencadear várias respostas. Um exemplo clássico é:
- uma página de erro, se a solicitação não puder ser processada corretamente
- uma página de confirmação, caso contrário
- a ação instrui que uma vista específica seja exibida [3]. Esta vista exibirá dados conhecidos como o modelo de vista. Este é o M em MVC. A ação criará este modelo M [2c] e instruirá que uma vista V seja exibida [3];
- resposta - a vista V selecionada utiliza o modelo M construído pela ação para inicializar as partes dinâmicas da resposta HTML que deve enviar ao cliente e, em seguida, envia essa resposta.
Para um serviço web / JSON, a arquitetura anterior é ligeiramente modificada:
![]() |
- em [4a], o modelo, que é uma classe Java, é convertido numa cadeia JSON por uma biblioteca JSON;
- em [4b], esta cadeia JSON é enviada para o navegador;
2.11.4. O controlador C
![]() |
A aplicação importada tem o seguinte controlador:
package hello;
import java.util.concurrent.atomic.AtomicLong;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class GreetingController {
private static final String template = "Hello, %s!";
private final AtomicLong counter = new AtomicLong();
@RequestMapping("/greeting")
public @ResponseBody
Greeting greeting(@RequestParam(value = "name", required = false, defaultValue = "World") String name) {
return new Greeting(counter.incrementAndGet(), String.format(template, name));
}
}
- linha 9: a anotação [@Controller] torna a classe [GreetingController] um controlador Spring, o que significa que os seus métodos são registados para tratar URLs;
- linha 15: a anotação [@RequestMapping] especifica a URL tratada pelo método, neste caso a URL [/greeting]. Veremos mais adiante que esta URL pode ser parametrizada e que é possível recuperar esses parâmetros;
- linha 16: a anotação [@ResponseBody] indica que o método não gera um modelo para uma vista (JSP, JSF, Thymeleaf, etc.) a ser enviada para o navegador do cliente, mas sim gera a resposta diretamente para o próprio navegador. Aqui, produz um objeto do tipo [Greeting] (linha 18). Embora não seja imediatamente evidente aqui, este objeto será primeiro convertido para JSON antes de ser enviado para o navegador. É a presença de uma biblioteca JSON nas dependências do projeto que faz com que o Spring Boot configure automaticamente o projeto desta forma;
- Linha 17: O método [greeting] tem um parâmetro [String name]. A anotação [@RequestParam(value = "name", required = false, defaultValue = "World"] indica que este parâmetro deve ser inicializado com um parâmetro chamado [name] (@RequestParam(value = "name"). Este pode ser um parâmetro GET ou POST. Este parâmetro não é obrigatório (required = false). Neste caso, o parâmetro [name] do método será inicializado com o valor [World] (defaultValue = "World").
2.11.5. O modelo M
O modelo M produzido pelo método anterior é o seguinte objeto [Greeting]:
![]() |
package hello;
public class Greeting {
private final long id;
private final String content;
public Greeting(long id, String content) {
this.id = id;
this.content = content;
}
public long getId() {
return id;
}
public String getContent() {
return content;
}
}
A transformação JSON deste objeto irá criar a string {"id":n,"content":"text"}. Por fim, a string JSON produzida pelo método do controlador terá o seguinte formato:
ou
2.11.6. Configuração do projeto
![]() |
O projeto é configurado pela seguinte classe [Application]:
package hello;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.SpringApplication;
import org.springframework.context.annotation.ComponentScan;
@ComponentScan
@EnableAutoConfiguration
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
- Linha 11: Curiosamente, esta classe é executável com um método [main] específico para aplicações de consola. É efetivamente esse o caso. A classe [SpringApplication] na linha 12 irá iniciar o servidor Tomcat presente nas dependências e implementar o serviço REST nele;
- linha 4: podemos ver que a classe [SpringApplication] pertence ao projeto [Spring Boot];
- linha 12: o primeiro parâmetro é a classe que configura o projeto, o segundo contém quaisquer parâmetros adicionais;
- linha 8: a anotação [@EnableAutoConfiguration] instrui o Spring Boot a configurar o projeto;
- linha 7: a anotação [@ComponentScan] faz com que o diretório que contém a classe [Application] seja verificado em busca de componentes Spring. Será encontrado um: a classe [GreetingController], que possui a anotação [@Controller], tornando-a um componente Spring;
2.11.7. Executar o projeto
Vamos executar o projeto:
![]() |
Recebemos os seguintes registos da consola:
____ _ __ _ _
- linha 12: o servidor Tomcat inicia na porta 8080 (linha 11);
- linha 16: o servlet [DispatcherServlet] está presente;
- linha 19: o método [GreetingController.greeting] foi detetado;
Para testar a aplicação web, solicitamos a URL [http://localhost:8080/greeting]:
![]() | ![]() |
Recebemos a cadeia JSON esperada. Pode ser interessante ver os cabeçalhos HTTP enviados pelo servidor. Para tal, utilizaremos o plugin do Chrome chamado [Advanced Rest Client] (ver Apêndices):
![]() |
- em [1], o URL solicitado;
- em [2], é utilizado o método GET;
- em [3], a resposta JSON;
- em [4], o servidor indicou que estava a enviar uma resposta no formato JSON;
- em [5], solicitamos a mesma URL, mas desta vez utilizando uma solicitação POST;
- em [7], a informação é enviada para o servidor no formato [urlencoded];
- em [6], o parâmetro name com o seu valor;
- em [8], o navegador informa ao servidor que está a enviar informações [urlencoded];
- em [9], a resposta JSON do servidor;
2.11.8. Criação de um arquivo executável
É possível criar um arquivo executável fora do Eclipse. A configuração necessária encontra-se no ficheiro [pom.xml]:
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<start-class>istia.st.Application</start-class>
<java.version>1.7</java.version>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
- As linhas 9–12 definem o plugin que irá criar o arquivo executável;
- A linha 3 define a classe executável do projeto;
Eis como proceder:
![]() |
- em [1]: execute uma meta do Maven;
- em [2]: existem duas metas: [clean] para eliminar a pasta [target] do projeto Maven, [package] para a regenerar;
- em [3]: a pasta [target] gerada ficará localizada nesta pasta;
- em [4]: o alvo é gerado;
Nos registos que aparecem na consola, é importante verificar se o plugin [spring-boot-maven-plugin] está presente. Este é o plugin que gera o arquivo executável.
Utilizando um console, navegue até à pasta gerada:
- linha 5: o arquivo gerado;
Este arquivo é executado da seguinte forma:
Agora que a aplicação web está em execução, pode aceder-lhe utilizando um navegador:
![]() |
2.11.9. Implantação da aplicação num servidor Tomcat
Embora o Spring Boot seja muito prático no modo de desenvolvimento, é provável que uma aplicação de produção seja implementada num servidor Tomcat real. Veja como fazê-lo:
Modifique o ficheiro [pom.xml] da seguinte forma:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.springframework</groupId>
<artifactId>gs-rest-service</artifactId>
<version>0.1.0</version>
<packaging>war</packaging>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.1.0.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
<properties>
<start-class>hello.Application</start-class>
</properties>
....
</project>
É necessário efetuar alterações em dois locais:
- linha 9: deve especificar que vai gerar um ficheiro WAR (Web Archive);
- Linhas 26–30: é necessário adicionar uma dependência do artefacto [spring-boot-starter-tomcat]. Este artefacto adiciona todas as classes do Tomcat às dependências do projeto;
- Linha 29: este artefacto é [fornecido], o que significa que os arquivos correspondentes não serão incluídos no ficheiro WAR gerado. Em vez disso, esses arquivos estarão localizados no servidor Tomcat onde a aplicação será executada;
Deve também configurar a aplicação web. Na ausência de um ficheiro [web.xml], isto é feito utilizando uma classe que estende [SpringBootServletInitializer]:
![]() |
A classe [ApplicationInitializer] é a seguinte:
package hello;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.context.web.SpringBootServletInitializer;
public class ApplicationInitializer extends SpringBootServletInitializer {
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(Application.class);
}
}
- linha 6: a classe [ApplicationInitializer] estende a classe [SpringBootServletInitializer];
- linha 9: o método [configure] é reescrito (linha 8);
- linha 10: é fornecida a classe que configura o projeto;
Para executar o projeto, proceda da seguinte forma:
![]() |
- em [1], execute o projeto num dos servidores registados no IDE Eclipse;
- em [2], selecione [tc Server Developer], que é a opção predefinida. Trata-se de uma variante do Tomcat;
Depois de fazer isto, pode introduzir o URL [http://localhost:8080/gs-rest-service/greeting/?name=Mitchell] num navegador:
![]() |
Agora já sabemos como gerar um arquivo WAR. A seguir, continuaremos a trabalhar com o Spring Boot e o seu arquivo JAR executável.
2.11.10. Criar um novo projeto web
Para criar um novo projeto web, siga estes passos:
![]() |
- em [1]: Ficheiro / Novo / Projeto Spring Starter
- em [2]: selecione [Web]. Não selecione nenhuma biblioteca de visualizações, pois num serviço web / JSON não existem visualizações;
- O projeto criado será um projeto Maven. Em [3], introduza o nome do grupo para o artefacto Maven a ser criado; em [4], introduza o nome do artefacto;
- em [5], introduza o nome de um pacote onde o Spring irá colocar a classe de configuração do projeto;
- em [6], atribua um nome ao projeto Eclipse — pode ser diferente do nome em [4];
![]() |
2.12. A camada [web]
![]() |
![]() |
Vamos construir a camada web em várias etapas:
- Etapa 1: uma camada web funcional sem autenticação;
- Etapa 2: Implementação da autenticação com o Spring Security;
- Etapa 3: Implementação de CORS [A partilha de recursos entre origens (CORS) é um mecanismo que permite que muitos recursos (por exemplo, tipos de letra, JavaScript, etc.) numa página web sejam solicitados a partir de outro domínio fora do domínio de origem do recurso. (Wikipedia)]. O cliente do nosso serviço web será um cliente web Angular que não pertence necessariamente ao mesmo domínio que o nosso serviço web. Por predefinição, não pode aceder ao serviço web, a menos que este o autorize a fazê-lo. Veremos como;
2.12.1. Configuração do Maven
O ficheiro [pom.xml] do projeto é o seguinte:
<modelVersion>4.0.0</modelVersion>
<groupId>istia.st.spring4.mvc</groupId>
<artifactId>rdvmedecins-webapi-v1</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>rdvmedecins-webapi-v1</name>
<description>Gestion de RV Médecins</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.0.0.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>istia.st.spring4.rdvmedecins</groupId>
<artifactId>rdvmedecins-metier-dao</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>
- linhas 7–11: o projeto Maven pai;
- linhas 13–16: dependências para um projeto Spring MVC;
- linhas 17–21: dependências das camadas [lógica de negócio, DAO, JPA];
2.12.2. A interface do serviço web
![]() |
- Em [1] acima, o navegador só pode solicitar um número limitado de URLs com uma sintaxe específica;
- em [4], recebe uma resposta JSON;
As respostas do nosso serviço web terão todas o mesmo formato, correspondendo à representação JSON de um objeto do tipo [Response], como se segue:
package rdvmedecins.web.models;
public class Reponse {
// ----------------- properties
// operation status
private int status;
// the answer JSON
private Object data;
// ---------------constructeurs
public Reponse() {
}
public Reponse(int status, Object data) {
this.status = status;
this.data = data;
}
// methods
public void incrStatusBy(int increment) {
status += increment;
}
// ----------------------getters and setters
...
}
- linha 7: código de erro da resposta 0: OK, qualquer outra coisa: KO;
- linha 9: o corpo da resposta;
Apresentamos agora as capturas de ecrã que ilustram a interface do serviço web / JSON:
Lista de todos os pacientes do consultório médico [/getAllClients]
![]() |
Lista de todos os médicos do consultório [/getAllMedecins]
![]() |
Lista dos horários disponíveis de um médico [/getAllCreneaux/{idMedecin}]
![]() |
Lista de consultas de um médico [/getRvMedecinJour/{idMedecin}/{aaaa-mm-dd}
![]() |
Agenda diária do médico [/getAgendaMedecinJour/{idMedecin}/{aaaa-mm-dd}]
![]() |
Para adicionar ou eliminar uma consulta, utilizamos a extensão do Chrome [Advanced Rest Client], uma vez que estas operações são realizadas através de um pedido POST.
Adicionar um compromisso [/ addRv]
![]() |
- em [0], a URL do serviço web;
- em [1], é utilizado o método POST;
- em [2], o texto JSON das informações enviadas ao serviço web no formato {day, clientId, slotId};
- em [3], o cliente especifica ao serviço web que está a enviar informações no formato JSON;
A resposta é então a seguinte:
![]() |
- em [4]: o cliente envia o cabeçalho indicando que os dados que está a enviar estão no formato JSON;
- em [5]: o serviço web responde que também está a enviar JSON;
- em [6]: a resposta JSON do serviço web. O campo [data] contém a representação JSON do compromisso adicionado;
A presença do novo compromisso pode ser verificada:
![]() |
Eliminar um compromisso [/deleteApp]
![]() |
- em [1], o URL do serviço web;
- em [2], é utilizado o método POST;
- em [3], o texto JSON da informação enviada ao serviço web na forma {idRv};
- em [4], o cliente especifica ao serviço web que está a enviar dados JSON;
A resposta é então a seguinte:
![]() |
- em [5]: o campo [status] é definido como 0, indicando que a operação foi bem-sucedida;
A eliminação do compromisso pode ser verificada:
![]() |
Como se pode ver acima, a consulta da paciente [Sra. GERMAN] já não consta da lista.
O serviço web também permite recuperar entidades pelo seu ID:
![]() |
![]() |
![]() |
![]() |
Todas estas URLs são geridas pelo controlador [RdvMedecinsController], que iremos agora apresentar.
2.12.3. A estrutura do controlador [ RdvMedecinsController]
![]() |
O controlador [RdvMedecinsController] é o seguinte:
package rdvmedecins.web.controllers;
import java.text.ParseException;
...
@RestController
public class RdvMedecinsController {
@Autowired
private ApplicationModel application;
private List<String> messages;
@PostConstruct
public void init() {
// application error messages
messages = application.getMessages();
}
// list of doctors
@RequestMapping(value = "/getAllMedecins", method = RequestMethod.GET)
public Reponse getAllMedecins() {
...
}
// customer list
@RequestMapping(value = "/getAllClients", method = RequestMethod.GET)
public Reponse getAllClients() {
...
}
// list of physician slots
@RequestMapping(value = "/getAllCreneaux/{idMedecin}", method = RequestMethod.GET)
public Reponse getAllCreneaux(@PathVariable("idMedecin") long idMedecin) {
...
}
// list of doctor's appointments
@RequestMapping(value = "/getRvMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET)
public Reponse getRvMedecinJour(@PathVariable("idMedecin") long idMedecin,
@PathVariable("jour") String jour) {
...
}
@RequestMapping(value = "/getClientById/{id}", method = RequestMethod.GET)
public Reponse getClientById(@PathVariable("id") long id) {
...
}
@RequestMapping(value = "/getMedecinById/{id}", method = RequestMethod.GET)
public Reponse getMedecinById(@PathVariable("id") long id) {
...
}
@RequestMapping(value = "/getRvById/{id}", method = RequestMethod.GET)
public Reponse getRvById(@PathVariable("id") long id) {
...
}
@RequestMapping(value = "/getCreneauById/{id}", method = RequestMethod.GET)
public Reponse getCreneauById(@PathVariable("id") long id) {
...
}
@RequestMapping(value = "/ajouterRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
public Reponse ajouterRv(@RequestBody PostAjouterRv post) {
...
}
@RequestMapping(value = "/supprimerRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
public Reponse supprimerRv(@RequestBody PostSupprimerRv post) {
...
}
@RequestMapping(value = "/getAgendaMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET)
public Reponse getAgendaMedecinJour(
@PathVariable("idMedecin") long idMedecin,
@PathVariable("jour") String jour) {
...
}
}
- linha 6: a anotação [@RestController] torna a classe [RdvMedecinsController] um controlador Spring. Além disso, garante que os métodos que tratam de URLs gerem uma resposta que é automaticamente convertida para JSON;
- linhas 9–10: Um objeto do tipo [ApplicationModel] será injetado aqui pelo Spring;
- linha 13: a anotação [@PostConstruct] marca um método para ser executado imediatamente após a instância da classe. Quando este método é executado, os objetos injetados pelo Spring estão disponíveis;
- Todos os métodos devolvem um objeto do tipo [Response] da seguinte forma:
package rdvmedecins.web.models;
public class Reponse {
// ----------------- properties
// operation status
private int status;
// the answer
private Object data;
...
}
Este objeto é serializado em JSON antes de ser enviado para o navegador do cliente;
- linha 20: a anotação [@RequestMapping] define as condições para chamar o método. Aqui, o método trata de uma solicitação GET da URL [/getAllMedecins]. Se esta URL fosse solicitada via POST, seria rejeitada e o Spring MVC enviaria um código de erro HTTP ao cliente web;
- linha 32: a URL é configurada com {idMedecin}. Este parâmetro é recuperado utilizando a anotação [@PathVariable] na linha 33;
- linha 33: o único parâmetro [long idMedecin] recebe o seu valor do parâmetro {idMedecin} na URL [@PathVariable("idMedecin")]. O parâmetro na URL e o do método podem ter nomes diferentes. Note que [@PathVariable("idMedecin")] é do tipo String (toda a URL é uma String), enquanto o parâmetro [long idMedecin] é do tipo [long]. A conversão de tipos é realizada automaticamente. É devolvido um código de erro HTTP se esta conversão de tipos falhar;
- linha 65: a anotação [@RequestBody] refere-se ao corpo da solicitação. Numa solicitação GET, quase nunca há um corpo (mas é possível incluir um). Numa solicitação POST, geralmente há um (mas é possível omiti-lo). Para a URL [ajouterRv], o cliente web envia a seguinte string JSON na sua solicitação POST:
A sintaxe [@RequestBody PostAjouterRv post] (linha 65), combinada com o facto de o método esperar JSON [consumes = "application/json; charset=UTF-8"] (linha 64), fará com que a cadeia JSON enviada pelo cliente web seja deserializada num objeto do tipo [PostAjouter]. Este objeto é definido da seguinte forma:
package rdvmedecins.web.models;
public class PostAjouterRv {
// pOST DATA
private String jour;
private long idClient;
private long idCreneau;
// getters and setters
...
}
Aqui também, as conversões de tipo necessárias ocorrerão automaticamente;
- As linhas 69–70 contêm um mecanismo semelhante para o URL [/deleteRv]. A string JSON enviada é a seguinte:
e o tipo [PostSupprimerRv] é o seguinte:
package rdvmedecins.web.models;
public class PostSupprimerRv {
// pOST DATA
private long idRv;
// getters and setters
...
}
2.12.4. Modelos de serviços web
![]() |
Já apresentámos os modelos [Response, PostAddAppointment, PostDeleteAppointment]. O modelo [ApplicationModel] é o seguinte:
package rdvmedecins.web.models;
import java.util.Date;
...
@Component
public class ApplicationModel implements IMetier {
// the [business] layer
@Autowired
private IMetier métier;
// data from the [business] layer
private List<Medecin> médecins;
private List<Client> clients;
// error messages
private List<String> messages;
@PostConstruct
public void init() {
// we get the doctors and the customers
try {
médecins = métier.getAllMedecins();
clients = métier.getAllClients();
} catch (Exception ex) {
messages = Static.getErreursForException(ex);
}
}
// getter
public List<String> getMessages() {
return messages;
}
// ------------------------- [business] layer interface
@Override
public List<Client> getAllClients() {
return clients;
}
@Override
public List<Medecin> getAllMedecins() {
return médecins;
}
@Override
public List<Creneau> getAllCreneaux(long idMedecin) {
return métier.getAllCreneaux(idMedecin);
}
@Override
public List<Rv> getRvMedecinJour(long idMedecin, Date jour) {
return métier.getRvMedecinJour(idMedecin, jour);
}
@Override
public Client getClientById(long id) {
return métier.getClientById(id);
}
@Override
public Medecin getMedecinById(long id) {
return métier.getMedecinById(id);
}
@Override
public Rv getRvById(long id) {
return métier.getRvById(id);
}
@Override
public Creneau getCreneauById(long id) {
return métier.getCreneauById(id);
}
@Override
public Rv ajouterRv(Date jour, Creneau creneau, Client client) {
return métier.ajouterRv(jour, creneau, client);
}
@Override
public void supprimerRv(Rv rv) {
métier.supprimerRv(rv);
}
@Override
public AgendaMedecinJour getAgendaMedecinJour(long idMedecin, Date jour) {
return métier.getAgendaMedecinJour(idMedecin, jour);
}
}
- linha 6: a anotação [@Component] torna a classe [ApplicationModel] um componente Spring. Tal como todos os componentes Spring vistos até agora (com exceção de @Controller), apenas um único objeto deste tipo será instanciado (singleton);
- linha 7: a classe [ApplicationModel] implementa a interface [IMetier];
- linhas 10–11: Uma referência à camada [business] é injetada pelo Spring;
- linha 19: a anotação [@PostConstruct] garante que o método [init] será executado imediatamente após a instância da classe [ApplicationModel] ser criada;
- linhas 23–24: as listas de médicos e clientes são recuperadas da camada [business];
- linha 26: se ocorrer uma exceção, armazenamos as mensagens da pilha de exceções no campo da linha 17;
A classe [ApplicationModel] terá duas finalidades:
- como um cache para armazenar as listas de médicos e pacientes (clientes);
- como uma interface única para os controladores;
A arquitetura da camada web evolui da seguinte forma:
![]() |
- em [2b], os métodos do(s) controlador(es) comunicam com o singleton [ApplicationModel];
Esta estratégia proporciona flexibilidade na gestão da cache. Atualmente, os horários das consultas médicas não são armazenados na cache. Para os armazenar, basta modificar a classe [ApplicationModel]. Isto não tem qualquer impacto no controlador, que continuará a utilizar o método [List<Creneau> getAllCreneaux(long idMedecin)] tal como fazia anteriormente. É a implementação deste método em [ApplicationModel] que será alterada.
2.12.5. A Classe Static
A classe [Static] contém um conjunto de métodos utilitários estáticos que não têm aspetos «de negócio» ou «web»:
![]() |
O código é o seguinte:
package rdvmedecins.web.helpers;
import java.text.SimpleDateFormat;
...
public class Static {
public Static() {
}
// list of exception error messages
public static List<String> getErreursForException(Exception exception) {
// retrieve the list of exception error messages
Throwable cause = exception;
List<String> erreurs = new ArrayList<String>();
while (cause != null) {
erreurs.add(cause.getMessage());
cause = cause.getCause();
}
return erreurs;
}
// mappers Object --> Map
// --------------------------------------------------------
....
}
- linha 12: o método [Static.getErrorsForException] que foi utilizado (linha 8 abaixo) no método [init] da classe [ApplicationModel]:
@PostConstruct
public void init() {
// we get the doctors and the customers
try {
médecins = métier.getAllMedecins();
clients = métier.getAllClients();
} catch (Exception ex) {
messages = Static.getErreursForException(ex);
}
}
O método constrói um objeto [List<String>] contendo as mensagens de erro [exception.getMessage()] de uma exceção [exception] e as das suas exceções internas [exception.getCause()].
A classe [Static] contém outros métodos utilitários que revisitaremos quando os encontrarmos.
Vamos agora detalhar o tratamento das URLs do serviço web. Três classes principais estão envolvidas neste processo:
- o controlador [RdvMedecinsController];
- a classe de métodos utilitários [Static];
- a classe de cache [ApplicationModel];
![]() |
2.12.6. O método [init] do controlador
O controlador [RdvMedecinsController] (ver secção 2.12.3) possui um método [init] que é executado imediatamente após a sua instanciação:
@Autowired
private ApplicationModel application;
private List<String> messages;
@PostConstruct
public void init() {
// application error messages
messages = application.getMessages();
}
- Linha 8: As mensagens de erro armazenadas no cache da aplicação [ApplicationModel] são guardadas localmente no campo da linha 3. Isto permite que os métodos determinem se a aplicação foi inicializada corretamente.
2.12.7. A URL [/getAllMedecins]
A URL [/getAllDoctors] é tratada pelo seguinte método no controlador [RdvMedecinsController]:
// list of doctors
@RequestMapping(value = "/getAllMedecins", method = RequestMethod.GET)
public Reponse getAllMedecins() {
// application status
if (messages != null) {
return new Reponse(-1, messages);
}
// list of doctors
try {
return new Reponse(0, application.getAllMedecins());
} catch (Exception e) {
return new Reponse(1, Static.getErreursForException(e));
}
}
- linha 5: verificamos se a aplicação foi inicializada corretamente (messages == null). Caso contrário, devolvemos uma resposta com status = -1 e data = messages;
- linha 10: caso contrário, devolvemos a lista de médicos com um estado igual a 0. O método [application.getAllMedecins()] não lança uma exceção porque se limita a devolver uma lista armazenada em cache. No entanto, manteremos este tratamento de exceções para o caso de os médicos já não se encontrarem em cache;
Ainda não ilustrámos o caso em que a aplicação não conseguiu inicializar corretamente. Vamos parar o SGBD MySQL5, iniciar o serviço web e, em seguida, solicitar a URL [/getAllMedecins]:

De facto, obtemos um erro. Em circunstâncias normais, obtemos a seguinte visualização:
![]() |
2.12.8. A URL [/getAllClients]
A URL [/getAllClients] é tratada pelo seguinte método no [RdvMedecinsController]:
// customer list
@RequestMapping(value = "/getAllClients")
public Reponse getAllClients() {
// application status
if (messages != null) {
return new Reponse(-1, messages);
}
// customer list
try {
return new Reponse(0, application.getAllClients());
} catch (Exception e) {
return new Reponse(1, Static.getErreursForException(e));
}
}
É semelhante ao método [getAllMedecins] que já estudámos. Os resultados obtidos são os seguintes:
![]() |
2.12.9. A URL [/getAllSlots/{doctorId}]
A URL [/getAllSlots/{doctorId}] é tratada pelo seguinte método no controlador [RdvMedecinsController]:
// list of physician slots
@RequestMapping(value = "/getAllCreneaux/{idMedecin}", method = RequestMethod.GET)
public Reponse getAllCreneaux(@PathVariable("idMedecin") long idMedecin) {
// application status
if (messages != null) {
return new Reponse(-1, messages);
}
// we get the doctor back
Reponse réponse = getMedecin(idMedecin);
if (réponse.getStatus() != 0) {
return réponse;
}
Medecin médecin = (Medecin) réponse.getData();
// doctor's slots
List<Creneau> créneaux = null;
try {
créneaux = application.getAllCreneaux(médecin.getId());
} catch (Exception e1) {
return new Reponse(3, Static.getErreursForException(e1));
}
// we return the answer
return new Reponse(0, Static.getListMapForCreneaux(créneaux));
}
- linha 9: o médico identificado pelo parâmetro [id] é solicitado a partir de um método local:
private Reponse getMedecin(long id) {
// we get the doctor back
Medecin médecin = null;
try {
médecin = application.getMedecinById(id);
} catch (Exception e1) {
return new Reponse(1, Static.getErreursForException(e1));
}
// existing doctor?
if (médecin == null) {
return new Reponse(2, null);
}
// ok
return new Reponse(0, médecin);
}
Este método devolve um valor de estado no intervalo [0,1,2]. Voltemos ao código do método [getAllSlots]:
- linhas 10–12: se status ≠ 0, devolve a resposta imediatamente;
- linha 13: recuperamos o médico;
- linha 17: recuperamos os horários disponíveis deste médico;
- linha 22: devolvemos um objeto [Static.getListMapForCreneaux(slots)] como resposta;
Vamos rever a definição da classe [Creneau]:
@Entity
@Table(name = "creneaux")
public class Creneau extends AbstractEntity {
private static final long serialVersionUID = 1L;
// characteristics of a RV slot
private int hdebut;
private int mdebut;
private int hfin;
private int mfin;
// a slot is linked to a doctor
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "id_medecin")
private Medecin medecin;
// foreign key
@Column(name = "id_medecin", insertable = false, updatable = false)
private long idMedecin;
...
}
- linha 13: o médico é recuperado no modo [FetchType.LAZY];
Lembre-se da consulta JPQL que implementa o método [getAllCreneaux] na camada [DAO]:
@Query("select c from Creneau c where c.medecin.id=?1")
A notação [c.medecin.id] força uma junção entre as tabelas [CRENEAUX] e [MEDECINS]. Como resultado, a consulta devolve todos os horários de consulta do médico, com o médico incluído em cada um deles. Quando serializamos esses horários para JSON, a cadeia JSON do médico aparece em cada um deles. Isto é desnecessário. Assim, em vez de serializar um objeto [Creneau], serializaremos um objeto [Map] contendo apenas os campos desejados.
Voltemos ao código que vimos anteriormente:
// we return the answer
return new Reponse(0, Static.getListMapForCreneaux(créneaux));
O método [Static.getListMapForCreneaux] é o seguinte:
// List<Creneau> --> List<Map>
public static List<Map<String, Object>> getListMapForCreneaux(List<Creneau> créneaux) {
// liste de dictionnaires <String,Object>
List<Map<String, Object>> liste = new ArrayList<Map<String, Object>>();
for (Creneau créneau : créneaux) {
liste.add(Static.getMapForCreneau(créneau));
}
// on rend la liste
return liste;
}
e o método [Static.getMapForCreneau] é o seguinte:
// Creneau --> Map
public static Map<String, Object> getMapForCreneau(Creneau créneau) {
// qq chose à faire ?
if (créneau == null) {
return null;
}
// dictionnaire <String,Object>
Map<String, Object> hash = new HashMap<String, Object>();
hash.put("id", créneau.getId());
hash.put("hDebut", créneau.getHdebut());
hash.put("mDebut", créneau.getMdebut());
hash.put("hFin", créneau.getHfin());
hash.put("mFin", créneau.getMfin());
// on rend le dictionnaire
return hash;
}
- linha 8: criamos um dicionário;
- linhas 9–13: adicionamos os campos que queremos manter na string JSON. O campo [doctor] não está incluído;
- linha 15: devolvemos este dicionário;
Os resultados obtidos são os seguintes:
![]() |
ou estes, caso o intervalo de tempo não exista:
![]() |
ou estes, em caso de erro ao aceder à base de dados:
![]() |
2.12.10. A URL [/getRvMedecinJour/{idMedecin}/{jour}]
A URL [/getRvMedecinJour/{idMedecin}/{jour}] é tratada pelo seguinte método no controlador [RdvMedecinsController]:
// list of doctor's appointments
@RequestMapping(value = "/getRvMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET)
public Reponse getRvMedecinJour(@PathVariable("idMedecin") long idMedecin, @PathVariable("jour") String jour) {
// application status
if (messages != null) {
return new Reponse(-1, messages);
}
// check the date
Date jourAgenda = null;
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
sdf.setLenient(false);
try {
jourAgenda = sdf.parse(jour);
} catch (ParseException e) {
return new Reponse(3, null);
}
// we get the doctor back
Reponse réponse = getMedecin(idMedecin);
if (réponse.getStatus() != 0) {
return réponse;
}
Medecin médecin = (Medecin) réponse.getData();
// list of appointments
List<Rv> rvs = null;
try {
rvs = application.getRvMedecinJour(médecin.getId(), jourAgenda);
} catch (Exception e1) {
return new Reponse(4, Static.getErreursForException(e1));
}
// we return the answer
return new Reponse(0, Static.getListMapForRvs(rvs));
}
- Linha 31: Devolvemos um objeto List<Map<String, Object>> em vez de um objeto List<Rv>. Recorde a definição da classe [Rv]:
@Entity
@Table(name = "rv")
public class Rv extends AbstractEntity {
private static final long serialVersionUID = 1L;
// characteristics of an Rv
@Temporal(TemporalType.DATE)
private Date jour;
// an appointment is linked to a customer
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "id_client")
private Client client;
// an appointment is linked to a time slot
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "id_creneau")
private Creneau creneau;
// foreign keys
@Column(name = "id_client", insertable = false, updatable = false)
private long idClient;
@Column(name = "id_creneau", insertable = false, updatable = false)
private long idCreneau;
...
}
- linha 11: o cliente é recuperado utilizando o modo [FetchType.LAZY];
- linha 18: o slot é recuperado utilizando o modo [FetchType.LAZY];
Recordemos a consulta JPQL que recupera os compromissos:
@Query("select rv from Rv rv left join fetch rv.client c left join fetch rv.creneau cr where cr.medecin.id=?1 and rv.jour=?2")
As junções são realizadas explicitamente para recuperar os campos [client] e [creneau]. Além disso, devido à junção [cr.medecin.id=?1], também teremos o médico. O médico aparecerá, portanto, na cadeia JSON de cada consulta. No entanto, esta informação duplicada é desnecessária. Voltemos ao código do método:
- linha 31: construímos nós próprios o dicionário a ser serializado em JSON;
O dicionário construído para uma consulta é o seguinte:
// Rv --> Map
public static Map<String, Object> getMapForRv(Rv rv) {
// anything to do?
if (rv == null) {
return null;
}
// dictionary <String,Object>
Map<String, Object> hash = new HashMap<String, Object>();
hash.put("id", rv.getId());
hash.put("client", rv.getClient());
hash.put("creneau", getMapForCreneau(rv.getCreneau()));
// we return the dictionary
return hash;
}
- Linha 11: Recuperamos o dicionário do objeto [Creneau] que apresentámos anteriormente;
Os resultados obtidos são os seguintes:
![]() |
ou estes com um dia incorreto:
![]() |
ou estes com um médico incorreto:
![]() |
2.12.11. A URL [/getAgendaMedecinJour/{idMedecin}/{jour}]
A URL [/getAgendaMedecinJour/{idMedecin}/{jour}] é tratada pelo seguinte método no controlador [RdvMedecinsController]:
@RequestMapping(value = "/getAgendaMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET)
public Reponse getAgendaMedecinJour(@PathVariable("idMedecin") long idMedecin, @PathVariable("jour") String jour) {
// application status
if (messages != null) {
return new Reponse(-1, messages);
}
// check the date
Date jourAgenda = null;
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
sdf.setLenient(false);
try {
jourAgenda = sdf.parse(jour);
} catch (ParseException e) {
return new Reponse(3, new String[] { String.format("jour [%s] invalide", jour) });
}
// we get the doctor back
Reponse réponse = getMedecin(idMedecin);
if (réponse.getStatus() != 0) {
return réponse;
}
Medecin médecin = (Medecin) réponse.getData();
// get your diary back
AgendaMedecinJour agenda = null;
try {
agenda = application.getAgendaMedecinJour(médecin.getId(), jourAgenda);
} catch (Exception e1) {
return new Reponse(4, Static.getErreursForException(e1));
}
// ok
return new Reponse(0, Static.getMapForAgendaMedecinJour(agenda));
}
}
- A linha 30 devolve um objeto do tipo List<Map<String, Object>>.
O método [Static.getMapForAgendaMedecinJour] é o seguinte:
// AgendaMedecinJour --> Map
public static Map<String, Object> getMapForAgendaMedecinJour(AgendaMedecinJour agenda) {
// anything to do?
if (agenda == null) {
return null;
}
// dictionary <String,Object>
Map<String, Object> hash = new HashMap<String, Object>();
hash.put("medecin", agenda.getMedecin());
hash.put("jour", new SimpleDateFormat("yyyy-MM-dd").format(agenda.getJour()));
List<Map<String, Object>> créneaux = new ArrayList<Map<String, Object>>();
for (CreneauMedecinJour créneau : agenda.getCreneauxMedecinJour()) {
créneaux.add(getMapForCreneauMedecinJour(créneau));
}
hash.put("creneauxMedecin", créneaux);
// we return the dictionary
return hash;
}
O dicionário construído tem três campos:
- [doctor]: o médico proprietário da agenda. Mantivemos esta informação porque aparece apenas uma vez, enquanto nos casos anteriores era repetida em todas as cadeias JSON;
- [day]: o dia do calendário;
- [doctorSlots]: a lista dos horários disponíveis do médico, incluindo quaisquer consultas agendadas para esse horário;
O método [getMapForCreneauMedecinJour] utilizado na linha 13 é o seguinte:
// CreneauMedecinJour --> map
public static Map<String, Object> getMapForCreneauMedecinJour(CreneauMedecinJour créneau) {
// anything to do?
if (créneau == null) {
return null;
}
// dictionary <String,Object>
Map<String, Object> hash = new HashMap<String, Object>();
hash.put("creneau", getMapForCreneau(créneau.getCreneau()));
hash.put("rv", getMapForRv(créneau.getRv()));
// we return the dictionary
return hash;
}
- linhas 9-10: usamos os dicionários já discutidos para os tipos [Creneau] e [Rv], que, portanto, não contêm quaisquer objetos [Medecin];
Os resultados obtidos são os seguintes:
![]() |
ou estes, se o dia estiver incorreto:
![]() |
ou estes, se o número de identificação do médico for inválido:
![]() |
2.12.12. A URL [/getMedecinById/{id}]
A URL [/getMedecinById/{id}] é tratada pelo seguinte método no controlador [RdvMedecinsController]:
@RequestMapping(value = "/getMedecinById/{id}", method = RequestMethod.GET)
public Reponse getMedecinById(@PathVariable("id") long id) {
// application status
if (messages != null) {
return new Reponse(-1, messages);
}
// we get the doctor back
return getMedecin(id);
}
Na linha 8, o método [getMedecin] é o seguinte:
private Reponse getMedecin(long id) {
// we get the doctor back
Medecin médecin = null;
try {
médecin = application.getMedecinById(id);
} catch (Exception e1) {
return new Reponse(1, Static.getErreursForException(e1));
}
// existing doctor?
if (médecin == null) {
return new Reponse(2, null);
}
// ok
return new Reponse(0, médecin);
}
Os resultados são os seguintes:
![]() |
ou estes, se o ID do médico estiver incorreto:
![]() |
2.12.13. A URL [/getClientById/{id}]
A URL [/getClientById/{id}] é tratada pelo seguinte método no controlador [RdvMedecinsController]:
@RequestMapping(value = "/getClientById/{id}", method = RequestMethod.GET)
public Reponse getClientById(@PathVariable("id") long id) {
// application status
if (messages != null) {
return new Reponse(-1, messages);
}
// we get the customer back
return getClient(id);
}
Na linha 8, o método [getClient] é o seguinte:
private Reponse getClient(long id) {
// we get the customer back
Client client = null;
try {
client = application.getClientById(id);
} catch (Exception e1) {
return new Reponse(1, Static.getErreursForException(e1));
}
// existing customer?
if (client == null) {
return new Reponse(2, null);
}
// ok
return new Reponse(0, client);
}
Os resultados são os seguintes:
![]() |
ou estes, se o ID do cliente estiver incorreto:
![]() |
2.12.14. A URL [/getCreneauById/{id}]
A URL [/getCreneauById/{id}] é tratada pelo seguinte método no controlador [RdvMedecinsController]:
@RequestMapping(value = "/getCreneauById/{id}", method = RequestMethod.GET)
public Reponse getCreneauById(@PathVariable("id") long id) {
// application status
if (messages != null) {
return new Reponse(-1, messages);
}
// we get the slot back
Reponse réponse = getCreneau(id);
if (réponse.getStatus() == 0) {
réponse.setData(Static.getMapForCreneau((Creneau) réponse.getData()));
}
// result
return réponse;
}
Na linha 8, o método [getCreneau] é o seguinte:
private Reponse getCreneau(long id) {
// we get the slot back
Creneau créneau = null;
try {
créneau = application.getCreneauById(id);
} catch (Exception e1) {
return new Reponse(1, Static.getErreursForException(e1));
}
// existing niche?
if (créneau == null) {
return new Reponse(2, null);
}
// ok
return new Reponse(0, créneau);
}
Os resultados obtidos são os seguintes:
![]() |
ou estes, se o número da ranhura estiver incorreto:
![]() |
2.12.15. A URL [/getRvById/{id}]
A URL [/getRvById/{id}] é tratada pelo seguinte método no controlador [RdvMedecinsController]:
@RequestMapping(value = "/getRvById/{id}", method = RequestMethod.GET)
public Reponse getRvById(@PathVariable("id") long id) {
// application status
if (messages != null) {
return new Reponse(-1, messages);
}
// recovering the rv
Reponse réponse = getRv(id);
if (réponse.getStatus() == 0) {
réponse.setData(Static.getMapForRv2((Rv) réponse.getData()));
}
// result
return réponse;
}
Na linha 8, o método [getRv] é o seguinte:
private Reponse getRv(long id) {
// we recover the Rv
Rv rv = null;
try {
rv = application.getRvById(id);
} catch (Exception e1) {
return new Reponse(1, Static.getErreursForException(e1));
}
// Existing Rv?
if (rv == null) {
return new Reponse(2, null);
}
// ok
return new Reponse(0, rv);
}
Na linha 10, o método [Static.getMapForRv2] é o seguinte:
// Rv --> Map
public static Map<String, Object> getMapForRv2(Rv rv) {
// qq chose à faire ?
if (rv == null) {
return null;
}
// dictionnaire <String,Object>
Map<String, Object> hash = new HashMap<String, Object>();
hash.put("id", rv.getId());
hash.put("idClient", rv.getIdClient());
hash.put("idCreneau", rv.getIdCreneau());
// on rend le dictionnaire
return hash;
}
Os resultados são os seguintes:
![]() |
ou estes, se o ID da marcação estiver incorreto:
![]() |
2.12.16. A URL [/ajouterRv]
A URL [/ajouterRv] é tratada pelo seguinte método no controlador [RdvMedecinsController]:
@RequestMapping(value = "/ajouterRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
public Reponse ajouterRv(@RequestBody PostAjouterRv post) {
// application status
if (messages != null) {
return new Reponse(-1, messages);
}
// retrieve posted values
String jour = post.getJour();
long idCreneau = post.getIdCreneau();
long idClient = post.getIdClient();
// check the date
Date jourAgenda = null;
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
sdf.setLenient(false);
try {
jourAgenda = sdf.parse(jour);
} catch (ParseException e) {
return new Reponse(6, null);
}
// we get the slot back
Reponse réponse = getCreneau(idCreneau);
if (réponse.getStatus() != 0) {
return réponse;
}
Creneau créneau = (Creneau) réponse.getData();
// we get the customer back
réponse = getClient(idClient);
if (réponse.getStatus() != 0) {
réponse.incrStatusBy(2);
return réponse;
}
Client client = (Client) réponse.getData();
// we add the Rv
Rv rv = null;
try {
rv = application.ajouterRv(jourAgenda, créneau, client);
} catch (Exception e1) {
return new Reponse(5, Static.getErreursForException(e1));
}
// we return the answer
return new Reponse(0, Static.getMapForRv(rv));
}
Não há aqui nada que não tenhamos visto antes. Na linha 41, devolvemos o compromisso que foi adicionado na linha 36.
Os resultados obtidos têm este aspeto com o [Advanced Rest Client]:
![]() |
ou assim se, por exemplo, indicarmos um número de slot inexistente:
![]() |
![]() |
2.12.17. A URL [/deleteAppointment]
A URL [/deleteAppointment] é tratada pelo seguinte método no controlador [RdvMedecinsController]:
@RequestMapping(value = "/supprimerRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
public Reponse supprimerRv(@RequestBody PostSupprimerRv post) {
// application status
if (messages != null) {
return new Reponse(-1, messages);
}
// retrieve posted values
long idRv = post.getIdRv();
// recovering the rv
Reponse réponse = getRv(idRv);
if (réponse.getStatus() != 0) {
return réponse;
}
// rv deletion
try {
application.supprimerRv(idRv);
} catch (Exception e1) {
return new Reponse(3, Static.getErreursForException(e1));
}
// ok
return new Reponse(0, null);
}
Os arquivos <a id="supprimerrv"></a> resultantes são os seguintes:
![]() |
ou estes, caso o ID da marcação não exista:
![]() |
Já terminámos com o controlador. Agora vamos ver como configurar o projeto.
2.12.18. Configuração do serviço web
![]() |
A classe de configuração [AppConfig] é a seguinte:
package rdvmedecins.web.config;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Import;
import rdvmedecins.config.DomainAndPersistenceConfig;
@EnableAutoConfiguration
@ComponentScan(basePackages = { "rdvmedecins.web" })
@Import({ DomainAndPersistenceConfig.class })
public class AppConfig {
}
- Linha 9: Definimos o modo como [AutoConfiguration] para que o Spring Boot possa configurar o projeto com base nos ficheiros que encontrar no classpath do projeto;
- linha 10: especificamos que os componentes Spring devem ser procurados no pacote [rdvmedecins.web] e nos seus subpacotes. É assim que os seguintes componentes serão descobertos:
- [@RestController RdvMedecinsController] no pacote [rdvmedecins.web.controllers];
- [@Component ApplicationModel] no pacote [rdvmedecins.web.models];
- Linha 11: Importamos a classe [DomainAndPersistenceConfig], que configura o projeto [rdvmedecins-metier-dao] para fornecer acesso aos beans desse projeto;
2.12.19. A classe executável do serviço web
![]() |
A classe [Boot] é a seguinte:
package rdvmedecins.web.boot;
import org.springframework.boot.SpringApplication;
import rdvmedecins.web.config.AppConfig;
public class Boot {
public static void main(String[] args) {
SpringApplication.run(AppConfig.class, args);
}
}
Linha 10: O método estático [SpringApplication.run] é executado com a classe de configuração do projeto [AppConfig] como seu primeiro parâmetro. Este método irá configurar automaticamente o projeto, iniciar o servidor Tomcat incorporado nas dependências e implementar o controlador [RdvMedecinsController] nele.
Os registos durante a execução são os seguintes:
- linha 17: o servidor Tomcat inicia;
- linhas 23-31: as camadas [lógica de negócio, DAO, JPA] são inicializadas;
- linha 34: o método que trata da URL [/getRvMedecinJour/{idMedecin}/{jour}] foi descoberto. Este processo de descoberta de métodos do controlador repete-se até à linha 44;
- linha 52: o servlet Spring MVC [DispatcherServlet] está pronto para responder a pedidos de clientes web;
Temos agora um serviço web funcional que pode ser consultado por um cliente web. Vamos agora abordar a segurança deste serviço: queremos que apenas determinadas pessoas possam gerir as consultas médicas. Para tal, utilizaremos o framework Spring Security, um componente do ecossistema Spring.
2.13. Introdução ao Spring Security
Vamos importar mais uma vez um guia do Spring, seguindo os passos 1 a 3 abaixo:
![]() |
![]() |
O projeto é composto pelos seguintes elementos:
- na pasta [templates], encontrará as páginas HTML do projeto;
- [Application]: é a classe executável do projeto;
- [MvcConfig]: é a classe de configuração do Spring MVC;
- [WebSecurityConfig]: é a classe de configuração do Spring Security;
2.13.1. Configuração do Maven
O projeto [3] é um projeto Maven. Vamos examinar o seu ficheiro [pom.xml] para ver as suas dependências:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.1.1.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
</dependencies>
- linhas 1–5: o projeto é um projeto Spring Boot;
- linhas 8–11: dependência da estrutura [Thymeleaf], que permite a criação de páginas HTML dinâmicas. Esta estrutura pode substituir o JSP (Java Server Pages), que até recentemente era a estrutura de visualização padrão para o Spring MVC;
- linhas 12–15: dependência da estrutura Spring Security;
2.13.2. Visualizações Thymeleaf
![]() |
A vista [home.html] é a seguinte:
![]() |
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security Example</title>
</head>
<body>
<h1>Welcome!</h1>
<p>
Click <a th:href="@{/hello}">here</a> to see a greeting.
</p>
</body>
</html>
- Os atributos [th:xx] são atributos do Thymeleaf. São interpretados pelo Thymeleaf antes de a página HTML ser enviada para o cliente. O cliente não os vê;
- linha 12: o atributo [th:href="@{/hello}"] irá gerar o atributo [href] da tag <a>. O valor [@{/hello}] irá gerar o caminho [<context>/hello], em que [context] é o contexto da aplicação web;
O código HTML gerado é o seguinte:
- linha 10: o contexto da aplicação é a raiz /;
A visualização [hello.html] é a seguinte:
![]() |
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Hello World!</title>
</head>
<body>
<h1 th:inline="text">Hello [[${#httpServletRequest.remoteUser}]]!</h1>
<form th:action="@{/logout}" method="post">
<input type="submit" value="Sign Out" />
</form>
</body>
</html>
- linha 9: O atributo [th:inline="text"] irá gerar o texto da tag <h1>. Este texto contém uma expressão $ que deve ser avaliada. O elemento [[${#httpServletRequest.remoteUser}]] é o valor do atributo [RemoteUser] do pedido HTTP atual. Este é o nome do utilizador que está conectado;
- linha 10: um formulário HTML. O atributo [th:action="@{/logout}"] irá gerar o atributo [action] da tag [form]. O valor [@{/logout}] irá gerar o caminho [<context>/logout], onde [context] é o contexto da aplicação web;
O código HTML gerado é o seguinte:
- linha 8: a tradução de Olá [[${#httpServletRequest.remoteUser}]]!;
- linha 9: a tradução de @{/logout};
- linha 11: um campo oculto denominado (atributo name) _csrf;
A visualização final [login.html] é a seguinte:
![]() |
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security Example</title>
</head>
<body>
<div th:if="${param.error}">Invalid username and password.</div>
<div th:if="${param.logout}">You have been logged out.</div>
<form th:action="@{/login}" method="post">
<div>
<label> User Name : <input type="text" name="username" />
</label>
</div>
<div>
<label> Password: <input type="password" name="password" />
</label>
</div>
<div>
<input type="submit" value="Sign In" />
</div>
</form>
</body>
</html>
- Linha 9: O atributo [th:if="${param.error}"] garante que a tag <div> só será gerada se o URL que exibe a página de login contiver o parâmetro [error] (http://context/login?error);
- linha 10: o atributo [th:if="${param.logout}"] garante que a tag <div> só será gerada se a URL que exibe a página de login contiver o parâmetro [logout] (http://context/login?logout);
- linhas 11–23: um formulário HTML;
- linha 11: o formulário será enviado para a URL [<context>/login], onde <context> é o contexto da aplicação web;
- linha 13: um campo de entrada denominado [username];
- linha 17: um campo de entrada denominado [password];
O código HTML gerado é o seguinte:
Repare que, na linha 21, o Thymeleaf adicionou um campo oculto chamado [_csrf].
2.13.3. Configuração do Spring MVC
![]() |
A classe [MvcConfig] configura a estrutura Spring MVC:
package hello;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
@Configuration
public class MvcConfig extends WebMvcConfigurerAdapter {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/home").setViewName("home");
registry.addViewController("/").setViewName("home");
registry.addViewController("/hello").setViewName("hello");
registry.addViewController("/login").setViewName("login");
}
}
- linha 7: a anotação [@Configuration] torna a classe [MvcConfig] uma classe de configuração;
- linha 8: a classe [MvcConfig] estende a classe [WebMvcConfigurerAdapter] para substituir determinados métodos;
- linha 10: redefinição de um método da classe pai;
- linhas 11–16: o método [addViewControllers] permite associar URLs a vistas HTML. São feitas as seguintes associações:
view | |
/templates/home.html | |
/templates/hello.html | |
/modelos/login.html |
O sufixo [html] e a pasta [templates] são os valores predefinidos utilizados pelo Thymeleaf. Podem ser alterados através da configuração. A pasta [templates] deve estar na raiz do classpath do projeto:
![]() |
Em [1] acima, as pastas [main] e [resources] são ambas pastas de origem. Isto significa que o seu conteúdo estará na raiz do classpath do projeto. Portanto, em [2], as pastas [hello] e [templates] estarão na raiz do classpath.
2.13.4. Configuração do Spring Security
![]() |
A classe [WebSecurityConfig] configura a estrutura Spring Security:
package hello;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity;
@Configuration
@EnableWebMvcSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/", "/home").permitAll().anyRequest().authenticated();
http.formLogin().loginPage("/login").permitAll().and().logout().permitAll();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication().withUser("user").password("password").roles("USER");
}
}
- linha 9: a anotação [@Configuration] torna a classe [WebSecurityConfig] uma classe de configuração;
- linha 10: a anotação [@EnableWebSecurity] torna a classe [WebSecurityConfig] uma classe de configuração do Spring Security;
- linha 11: a classe [WebSecurity] estende a classe [WebSecurityConfigurerAdapter] para substituir determinados métodos;
- linha 12: redefinição de um método da classe pai;
- linhas 13–16: o método [configure(HttpSecurity http)] é substituído para definir direitos de acesso para os vários URLs da aplicação;
- linha 14: o método [http.authorizeRequests()] permite associar URLs a direitos de acesso. São feitas as seguintes associações:
regra | código | |
acesso sem autenticação | | |
apenas acesso autenticado |
- Linha 15: define o método de autenticação. A autenticação é realizada através de um formulário URL [/login] acessível a todos [http.formLogin().loginPage("/login").permitAll()]. O logout também é acessível a todos.
- linhas 19-21: redefinem o método [configure(AuthenticationManagerBuilder auth)] que gere os utilizadores;
- linha 20: a autenticação é realizada utilizando utilizadores codificados [auth.inMemoryAuthentication()]. Um utilizador é definido aqui com o nome de utilizador [user], palavra-passe [password] e função [USER]. Aos utilizadores com a mesma função podem ser atribuídas as mesmas permissões;
2.13.5. Classe executável
![]() |
A classe [Application] é a seguinte:
package hello;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.SpringApplication;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@EnableAutoConfiguration
@Configuration
@ComponentScan
public class Application {
public static void main(String[] args) throws Throwable {
SpringApplication.run(Application.class, args);
}
}
- Linha 8: A anotação [@EnableAutoConfiguration] instrui o Spring Boot (linha 3) a realizar a configuração que o programador não definiu explicitamente;
- linha 9: torna a classe [Application] uma classe de configuração do Spring;
- linha 10: instrui o sistema a analisar o diretório que contém a classe [Application] para procurar componentes Spring. As duas classes [MvcConfig] e [WebSecurityConfig] serão, assim, encontradas porque possuem a anotação [@Configuration];
- linha 13: o método [main] da classe executável;
- linha 14: o método estático [SpringApplication.run] é executado com a classe de configuração [Application] como parâmetro. Já nos deparámos com este processo e sabemos que o servidor Tomcat incorporado nas dependências Maven do projeto será iniciado e o projeto implementado nele. Vimos que quatro URLs eram geridas [/, /home, /login, /hello] e que algumas estavam protegidas por direitos de acesso.
2.13.6. Testar a aplicação
Vamos começar por solicitar a URL [/], que é uma das quatro URLs aceites. Está associada à vista [/templates/home.html]:
![]() |
A URL solicitada [/] é acessível a todos. É por isso que conseguimos recuperá-la. O link [aqui] é o seguinte:
A URL [/hello] será solicitada quando clicarmos no link. Esta está protegida:
regra | código | |
acesso sem autenticação | | |
apenas acesso autenticado |
É necessário estar autenticado para aceder. O Spring Security redirecionará então o navegador do cliente para a página de autenticação. Com base na configuração apresentada, esta é a página no URL [/login]. Esta página é acessível a todos:
http.formLogin().loginPage("/login").permitAll().and().logout().permitAll();
Assim, obtemos isto [1]:
![]() |
O código-fonte da página obtida é o seguinte:
- Na linha 7, aparece um campo oculto que não consta na página original [login.html]. Foi adicionado pelo Thymeleaf. Este código, conhecido como CSRF (Cross-Site Request Forgery), foi concebido para eliminar uma vulnerabilidade de segurança. Este token deve ser reenviado ao Spring Security juntamente com a autenticação para que seja aceite;
Recordamos que apenas o par utilizador/palavra-passe é reconhecido pelo Spring Security. Se introduzirmos outra coisa em [2], obtemos a mesma página com uma mensagem de erro em [3]. O Spring Security redirecionou o navegador para o URL [http://localhost:8080/login?error]. A presença do parâmetro [error] desencadeou a exibição da tag:
<div th:if="${param.error}">Invalid username and password.</div>
Agora, vamos introduzir os valores esperados de utilizador/palavra-passe [4]:
![]() |
- em [4], fazemos o login;
- em [5], o Spring Security redireciona-nos para a URL [/hello] porque essa é a URL que solicitámos quando fomos redirecionados para a página de login. A identidade do utilizador foi exibida pela seguinte linha de [hello.html]:
<h1 th:inline="text">Hello [[${#httpServletRequest.remoteUser}]]!</h1>
A página [5] apresenta o seguinte formulário:
<form th:action="@{/logout}" method="post">
<input type="submit" value="Sign Out" />
</form>
Quando clica no botão [Sair], é enviada uma solicitação POST para a URL [/logout]. Tal como a URL [/login], esta URL é acessível a todos:
http.formLogin().loginPage("/login").permitAll().and().logout().permitAll();
No nosso mapeamento de URL/visualização, não definimos nada para a URL [/logout]. O que irá acontecer? Vamos experimentar:
![]() |
- Em [6], clicamos no botão [Sair];
- em [7], vemos que fomos redirecionados para a URL [http://localhost:8080/login?logout]. O Spring Security solicitou este redirecionamento. A presença do parâmetro [logout] na URL fez com que a seguinte linha fosse exibida na vista:
<div th:if="${param.logout}">You have been logged out.</div>
2.13.7. Conclusão
No exemplo anterior, poderíamos ter escrito primeiro a aplicação web e, posteriormente, protegido-a. O Spring Security é não intrusivo. É possível implementar segurança numa aplicação web que já tenha sido escrita. Além disso, descobrimos os seguintes pontos:
- é possível definir uma página de autenticação;
- a autenticação deve ser acompanhada pelo token CSRF emitido pelo Spring Security;
- se a autenticação falhar, é redirecionado para a página de autenticação com um parâmetro de erro adicional no URL;
- se a autenticação for bem-sucedida, é redirecionado para a página solicitada no momento da autenticação. Se solicitar a página de autenticação diretamente, sem passar por uma página intermédia, o Spring Security redireciona-o para a URL [/] (este caso não foi demonstrado);
- O utilizador sai da sessão solicitando a URL [/logout] com um pedido POST. O Spring Security redireciona-o então para a página de autenticação com o parâmetro «logout» na URL;
Todas estas conclusões baseiam-se no comportamento padrão do Spring Security. Este comportamento pode ser alterado através da configuração, substituindo determinados métodos da classe [WebSecurityConfigurerAdapter].
O tutorial anterior será de pouca utilidade para nós daqui em diante. Na verdade, iremos utilizar:
- uma base de dados para armazenar utilizadores, as suas palavras-passe e as suas funções;
- autenticação baseada em cabeçalhos HTTP;
Existem muito poucos tutoriais disponíveis para o que pretendemos fazer aqui. A solução que iremos propor é uma combinação de trechos de código encontrados aqui e ali.
2.14. Implementação de segurança para o serviço de marcação de consultas online
2.14.1. A base de dados
A base de dados [rdvmedecins] está a ser atualizada para incluir os utilizadores, as suas palavras-passe e as suas funções. Foram adicionadas três novas tabelas:

Tabela [USERS]: utilizadores
- ID: chave primária;
- VERSION: coluna de versionamento de linhas;
- IDENTITY: um identificador descritivo para o utilizador;
- LOGIN: o nome de utilizador do utilizador;
- PASSWORD: a sua palavra-passe;
Na tabela USERS, as palavras-passe não são armazenadas em texto simples:
![]() |
O algoritmo utilizado para encriptar as palavras-passe é o algoritmo BCRYPT.
Tabela [ROLES]: funções
- ID: chave primária;
- VERSION: coluna de versão para a linha;
- NAME: nome da função. Por predefinição, o Spring Security espera nomes no formato ROLE_XX, por exemplo, ROLE_ADMIN ou ROLE_GUEST;
![]() |
Tabela [USERS_ROLES]: tabela de junção USERS/ROLES
Um utilizador pode ter várias funções, e uma função pode incluir vários utilizadores. Esta é uma relação muitos-para-muitos representada pela tabela [USERS_ROLES].
- ID: chave primária;
- VERSION: coluna de versionamento de linhas;
- USER_ID: identificador do utilizador;
- ROLE_ID: identificador de uma função;
![]() |
Como estamos a modificar a base de dados, todas as camadas do projeto [lógica de negócio, DAO, JPA] têm de ser modificadas:
![]() |
2.14.2. O novo projeto Eclipse para [lógica de negócio, DAO, JPA]
Duplicamos o projeto inicial [rdvmedecins-business-dao] para [rdvmedecins-business-dao-v2]:
![]() |
- em [1]: o novo projeto;
- em [2]: as alterações introduzidas pela implementação da segurança foram agrupadas num único pacote [rdvmedecins.security]. Estes novos elementos pertencem às camadas [JPA] e [DAO], mas, por uma questão de simplicidade, agrupei-os num único pacote.
2.14.3. As novas entidades [JPA]
![]() |
A camada JPA define três novas entidades:
![]() |
A classe [User] representa a tabela [USERS]:
package rdvmedecins.entities;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Table;
@Entity
@Table(name = "USERS")
public class User extends AbstractEntity {
private static final long serialVersionUID = 1L;
// properties
private String identity;
private String login;
private String password;
// manufacturer
public User() {
}
public User(String identity, String login, String password) {
this.identity = identity;
this.login = login;
this.password = password;
}
// identity
@Override
public String toString() {
return String.format("User[%s,%s,%s]", identity, login, password);
}
// getters and setters
....
}
- linha 9: a classe estende a classe [AbstractEntity] já utilizada para as outras entidades;
- linhas 13–15: não são especificados nomes de colunas porque têm os mesmos nomes que os seus campos associados;
A classe [Role] reflete a tabela [ROLES]:
package rdvmedecins.entities;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Table;
@Entity
@Table(name = "ROLES")
public class Role extends AbstractEntity {
private static final long serialVersionUID = 1L;
// properties
private String name;
// manufacturers
public Role() {
}
public Role(String name) {
this.name = name;
}
// identity
@Override
public String toString() {
return String.format("Role[%s]", name);
}
// getters and setters
...
}
A classe [UserRole] representa a tabela [USERS_ROLES]:
package rdvmedecins.entities;
import javax.persistence.Entity;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
@Entity
@Table(name = "USERS_ROLES")
public class UserRole extends AbstractEntity {
private static final long serialVersionUID = 1L;
// a UserRole refers to a User
@ManyToOne
@JoinColumn(name = "USER_ID")
private User user;
// a UserRole refers to a Role
@ManyToOne
@JoinColumn(name = "ROLE_ID")
private Role role;
// getters and setters
...
}
- linhas 15–17: definir a chave estrangeira da tabela [USERS_ROLES] para a tabela [USERS];
- linhas 19-21: implementam a chave estrangeira da tabela [USERS_ROLES] para a tabela [ROLES];
2.14.4. Alterações na camada [DAO]
![]() |
A camada [DAO] foi melhorada com três novos [Repository]s:
![]() |
A interface [UserRepository] gere o acesso às entidades [User]:
package rdvmedecins.repositories;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
import rdvmedecins.entities.Role;
import rdvmedecins.entities.User;
public interface UserRepository extends CrudRepository<User, Long> {
// list of user roles identified by id
@Query("select ur.role from UserRole ur where ur.user.id=?1")
Iterable<Role> getRoles(long id);
// list of user roles identified by login and password
@Query("select ur.role from UserRole ur where ur.user.login=?1 and ur.user.password=?2")
Iterable<Role> getRoles(String login, String password);
// search for a user via login
User findUserByLogin(String login);
}
- linha 9: a interface [UserRepository] estende a interface [CrudRepository] do Spring Data (linha 4);
- linhas 12-13: o método [getRoles(User user)] recupera todas as funções de um utilizador identificado pelo seu [id]
- linhas 16-17: igual ao anterior, mas para um utilizador identificado pelo seu nome de utilizador e palavra-passe;
A interface [RoleRepository] gere o acesso às entidades [Role]:
package rdvmedecins.security;
import org.springframework.data.repository.CrudRepository;
public interface RoleRepository extends CrudRepository<Role, Long> {
// search for a role by name
Role findRoleByName(String name);
}
- linha 5: a interface [RoleRepository] estende a interface [CrudRepository];
- linha 8: pode pesquisar uma função pelo seu nome;
A interface [userRoleRepository] gere o acesso às entidades [UserRole]:
package rdvmedecins.security;
import org.springframework.data.repository.CrudRepository;
public interface UserRoleRepository extends CrudRepository<UserRole, Long> {
}
- linha 5: a interface [UserRoleRepository] simplesmente estende a interface [CrudRepository] sem adicionar nenhum método novo;
2.14.5. Classes de gestão de utilizadores e funções
![]() |
O Spring Security requer a criação de uma classe que implemente a seguinte interface [UsersDetail]:
![]() |
Esta interface é implementada aqui pela classe [AppUserDetails]:
package rdvmedecins.security;
import java.util.ArrayList;
import java.util.Collection;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
public class AppUserDetails implements UserDetails {
private static final long serialVersionUID = 1L;
// properties
private User user;
private UserRepository userRepository;
// manufacturers
public AppUserDetails() {
}
public AppUserDetails(User user, UserRepository userRepository) {
this.user = user;
this.userRepository = userRepository;
}
// -------------------------interface
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> authorities = new ArrayList<>();
for (Role role : userRepository.getRoles(user.getId())) {
authorities.add(new SimpleGrantedAuthority(role.getName()));
}
return authorities;
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getLogin();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
// getters and setters
...
}
- linha 10: a classe [AppUserDetails] implementa a interface [UserDetails];
- linhas 15-16: a classe encapsula um utilizador (linha 15) e o repositório que fornece detalhes sobre esse utilizador (linha 16);
- linhas 22–25: o construtor que instancia a classe com um utilizador e o seu repositório;
- linhas 28–35: implementação do método [getAuthorities] da interface [UserDetails]. Deve construir uma coleção de elementos do tipo [GrantedAuthority] ou de um tipo derivado. Aqui, usamos o tipo derivado [SimpleGrantedAuthority] (linha 32), que encapsula o nome de uma das funções do utilizador da linha 15;
- linhas 31–33: percorremos a lista de funções do utilizador da linha 15 para construir uma lista de elementos do tipo [SimpleGrantedAuthority];
- linhas 38–40: implementamos o método [getPassword] da interface [UserDetails]. Devolvemos a palavra-passe do utilizador da linha 15;
- linhas 38–40: implementamos o método [getUserName] da interface [UserDetails]. Devolvemos o nome de utilizador do utilizador da linha 15;
- linhas 47–50: a conta do utilizador nunca expira;
- linhas 52–55: a conta do utilizador nunca é bloqueada;
- linhas 57–60: as credenciais do utilizador nunca expiram;
- linhas 62–65: a conta do utilizador está sempre ativa;
O Spring Security também requer a existência de uma classe que implemente a interface [AppUserDetailsService]:
![]() |
Esta interface é implementada pela seguinte classe [AppUserDetails]:
package rdvmedecins.security;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
@Service
public class AppUserDetailsService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String login) throws UsernameNotFoundException {
// search for user via login
User user = userRepository.findUserByLogin(login);
// found?
if (user == null) {
throw new UsernameNotFoundException(String.format("login [%s] inexistant", login));
}
// render user details
return new AppUserDetails(user, userRepository);
}
}
- linha 9: a classe será um componente Spring, pelo que estará disponível no seu contexto;
- linhas 12–13: o componente [UserRepository] será injetado aqui;
- linhas 16–25: implementação do método [loadUserByUsername] da interface [UserDetailsService] (linha 10). O parâmetro é o nome de utilizador do utilizador;
- linha 18: o utilizador é procurado através do seu nome de utilizador;
- linhas 20–22: se o utilizador não for encontrado, é lançada uma exceção;
- linha 24: um objeto [AppUserDetails] é construído e devolvido. É, de facto, do tipo [UserDetails] (linha 16);
2.14.6. Testes da camada [DAO]
![]() |
Primeiro, criamos uma classe executável [CreateUser] capaz de criar um utilizador com uma função:
package rdvmedecins.security;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.security.crypto.bcrypt.BCrypt;
import rdvmedecins.config.DomainAndPersistenceConfig;
import rdvmedecins.security.Role;
import rdvmedecins.security.RoleRepository;
import rdvmedecins.security.User;
import rdvmedecins.security.UserRepository;
import rdvmedecins.security.UserRole;
import rdvmedecins.security.UserRoleRepository;
public class CreateUser {
public static void main(String[] args) {
// syntax: login password roleName
// three parameters are required
if (args.length != 3) {
System.out.println("Syntaxe : [pg] user password role");
System.exit(0);
}
// parameters are retrieved
String login = args[0];
String password = args[1];
String roleName = String.format("ROLE_%s", args[2].toUpperCase());
// spring context
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(DomainAndPersistenceConfig.class);
UserRepository userRepository = context.getBean(UserRepository.class);
RoleRepository roleRepository = context.getBean(RoleRepository.class);
UserRoleRepository userRoleRepository = context.getBean(UserRoleRepository.class);
// does the role already exist?
Role role = roleRepository.findRoleByName(roleName);
// if it doesn't exist, we create it
if (role == null) {
role = roleRepository.save(new Role(roleName));
}
// does the user already exist?
User user = userRepository.findUserByLogin(login);
// if it doesn't exist, we create it
if (user == null) {
// hash the password with bcrypt
String crypt = BCrypt.hashpw(password, BCrypt.gensalt());
// save user
user = userRepository.save(new User(login, login, crypt));
// we create the relationship with the role
userRoleRepository.save(new UserRole(user, role));
} else {
// the user already exists - does he/she have the required role?
boolean trouvé = false;
for (Role r : userRepository.getRoles(user.getId())) {
if (r.getName().equals(roleName)) {
trouvé = true;
break;
}
}
// if not found, we create the relationship with the role
if (!trouvé) {
userRoleRepository.save(new UserRole(user, role));
}
}
// closing Spring context
context.close();
}
}
- linha 17: a classe espera três argumentos que definem um utilizador: o seu nome de utilizador, palavra-passe e função;
- linhas 25–27: os três parâmetros são recuperados;
- linha 29: o contexto Spring é construído a partir da classe de configuração [DomainAndPersistenceConfig]. Esta classe já existia no projeto anterior. Deve ser atualizada da seguinte forma:
@EnableJpaRepositories(basePackages = { "rdvmedecins.repositories", "rdvmedecins.security" })
@EnableAutoConfiguration
@ComponentScan(basePackages = { "rdvmedecins" })
@EntityScan(basePackages = { "rdvmedecins.entities", "rdvmedecins.security" })
@EnableTransactionManagement
public class DomainAndPersistenceConfig {
....
}
- Linha 1: Deve especificar que agora existem componentes [Repository] no pacote [rdvmedecins.security];
- linha 4: deve especificar que existem agora entidades JPA no pacote [rdvmedecins.security];
Voltemos ao código para criar um utilizador:
- linhas 30–32: recuperamos as referências dos três objetos [Repository] que podem ser úteis para criar o utilizador;
- linha 34: verificamos se a função já existe;
- linhas 36–38: se não, criamo-la na base de dados. Terá um nome do tipo [ROLE_XX];
- linha 40: verificamos se o login já existe;
- linhas 42-49: se o nome de utilizador não existir, criamo-lo na base de dados;
- linha 44: encriptamos a palavra-passe. Aqui, utilizamos a classe [BCrypt] do Spring Security (linha 4). Por isso, precisamos dos arquivos para este framework. O ficheiro [pom.xml] inclui uma nova dependência:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
- Linha 46: O utilizador é guardado na base de dados;
- linha 48: assim como a relação que o liga à sua função;
- linhas 51–57: se o login já existir, verificamos se a função que pretendemos atribuir-lhe já se encontra entre as suas funções;
- Linhas 59–61: Se a função procurada não for encontrada, é criada uma linha na tabela [USERS_ROLES] para ligar o utilizador à sua função;
- Não implementámos proteções contra possíveis exceções. Esta é uma classe auxiliar para criar rapidamente um utilizador com uma função.
Quando a classe é executada com os argumentos [x x guest], obtêm-se os seguintes resultados na base de dados:
Tabela [USERS]
![]() |
Tabela [FUNÇÕES]
![]() |
Tabela [USERS_ROLES]
![]() |
Agora, vamos considerar a segunda classe [UsersTest], que é um teste JUnit:
![]() |
package rdvmedecins.security;
import java.util.List;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.crypto.bcrypt.BCrypt;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import rdvmedecins.config.DomainAndPersistenceConfig;
import com.google.common.collect.Lists;
@SpringApplicationConfiguration(classes = DomainAndPersistenceConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class UsersTest {
@Autowired
private UserRepository userRepository;
@Autowired
private AppUserDetailsService appUserDetailsService;
@Test
public void findAllUsersWithTheirRoles() {
Iterable<User> users = userRepository.findAll();
for (User user : users) {
System.out.println(user);
display("Roles :", userRepository.getRoles(user.getId()));
}
}
@Test
public void findUserByLogin() {
// user [admin] is retrieved
User user = userRepository.findUserByLogin("admin");
// we check that his password is [admin]
Assert.assertTrue(BCrypt.checkpw("admin", user.getPassword()));
// check admin / admin role
List<Role> roles = Lists.newArrayList(userRepository.getRoles("admin", user.getPassword()));
Assert.assertEquals(1L, roles.size());
Assert.assertEquals("ROLE_ADMIN", roles.get(0).getName());
}
@Test
public void loadUserByUsername() {
// user [admin] is retrieved
AppUserDetails userDetails = (AppUserDetails) appUserDetailsService.loadUserByUsername("admin");
// we check that his password is [admin]
Assert.assertTrue(BCrypt.checkpw("admin", userDetails.getPassword()));
// check admin / admin role
@SuppressWarnings("unchecked")
List<SimpleGrantedAuthority> authorities = (List<SimpleGrantedAuthority>) userDetails.getAuthorities();
Assert.assertEquals(1L, authorities.size());
Assert.assertEquals("ROLE_ADMIN", authorities.get(0).getAuthority());
}
// utility method - displays items in a collection
private void display(String message, Iterable<?> elements) {
System.out.println(message);
for (Object element : elements) {
System.out.println(element);
}
}
}
- linhas 27–34: teste visual. Exibimos todos os utilizadores juntamente com as suas funções;
- linhas 36–46: verificamos se o utilizador [admin] tem a palavra-passe [admin] e a função [ROLE_ADMIN] utilizando o [UserRepository];
- linha 41: [admin] é a palavra-passe em texto simples. Na base de dados, esta é encriptada utilizando o algoritmo BCrypt. O método [BCrypt.checkpw] verifica se a palavra-passe encriptada corresponde à que se encontra na base de dados;
- linhas 48–59: verificamos se o utilizador [admin] tem a palavra-passe [admin] e a função [ROLE_ADMIN] utilizando o [appUserDetailsService];
Os testes são executados com sucesso com os seguintes registos:
2.14.7. Conclusão provisória
As classes necessárias para o Spring Security foram adicionadas com alterações mínimas ao projeto original. Recapitulando:
- adição de uma dependência do Spring Security no ficheiro [pom.xml];
- criação de três tabelas adicionais na base de dados;
- criação de entidades JPA e componentes Spring no pacote [rdvmedecins.security];
Este cenário muito favorável decorre do facto de as três tabelas adicionadas à base de dados serem independentes das tabelas existentes. Poderíamos até tê-las colocado numa base de dados separada. Isto foi possível porque decidimos que um utilizador existe independentemente dos médicos e dos clientes. Se estes últimos fossem utilizadores potenciais, teríamos de criar ligações entre a tabela [USERS] e as tabelas [MEDECINS] e [CLIENTS]. Isto teria tido um impacto significativo no projeto existente.
2.14.8. O projeto Eclipse para a camada [web]
![]() |
O projeto anterior [rdvmedecins-webapi] está duplicado no projeto [rdvmedecins-webapi-v2] [1]:
![]() |
As únicas alterações a efetuar devem ser feitas no pacote [rdvmedecins.web.config], onde o Spring Security deve ser configurado. Já nos deparámos com uma classe de configuração do Spring Security:
package hello;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity;
@Configuration
@EnableWebMvcSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/", "/home").permitAll().anyRequest().authenticated();
http.formLogin().loginPage("/login").permitAll().and().logout().permitAll();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication().withUser("user").password("password").roles("USER");
}
}
Seguiremos o mesmo procedimento:
- linha 11: definir uma classe que estenda a classe [WebSecurityConfigurerAdapter];
- linha 13: definir um método [configure(HttpSecurity http)] que define os direitos de acesso às várias URLs do serviço web;
- linha 19: definir um método [configure(AuthenticationManagerBuilder auth)] que define os utilizadores e as suas funções;
A configuração do Spring Security é gerida pela classe [SecurityConfig]:
package rdvmedecins.web.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import rdvmedecins.security.AppUserDetailsService;
@EnableAutoConfiguration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private AppUserDetailsService appUserDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder registry) throws Exception {
// authentication is performed by bean [appUserDetailsService]
// the password is encrypted using the Bcrypt hash algorithm
registry.userDetailsService(appUserDetailsService).passwordEncoder(new BCryptPasswordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// CSRF
http.csrf().disable();
// the password is transmitted by the header Authorization: Basic xxxx
http.httpBasic();
// only the ADMIN role can use the application
http.authorizeRequests() //
.antMatchers("/", "/**") // all URL
.hasRole("ADMIN");
}
}
- linhas 14-15: reutilizámos as anotações do exemplo;
- linhas 17-18: a classe [AppUserDetails], que fornece acesso aos utilizadores da aplicação, é injetada;
- linhas 20-21: o método [configure(HttpSecurity http)] define os utilizadores e as suas funções. Recebe um tipo [AuthenticationManagerBuilder] como parâmetro. Este parâmetro é enriquecido com duas informações:
- uma referência ao [appUserDetailsService] da linha 18, que fornece acesso aos utilizadores registados. Note-se aqui que o facto de estarem armazenados numa base de dados não é explicitamente mencionado. Podem, portanto, estar num cache, fornecidos por um serviço web, etc.
- o tipo de encriptação utilizado para a palavra-passe. Recorde-se que utilizámos o algoritmo BCrypt;
- linhas 27–40: o método [configure(HttpSecurity http)] define os direitos de acesso às URLs do serviço web;
- linha 30: vimos no projeto introdutório que, por predefinição, o Spring Security gere um token CSRF (Cross-Site Request Forgery) que o utilizador que deseja autenticar-se deve enviar de volta ao servidor. Aqui, este mecanismo está desativado;
- linha 32: ativamos a autenticação via cabeçalho HTTP. O cliente deve enviar o seguinte cabeçalho HTTP:
onde code é a codificação Base64 da cadeia de caracteres login:password. Por exemplo, a codificação Base64 da cadeia de caracteres admin:admin é YWRtaW46YWRtaW4=. Portanto, um utilizador com o nome de utilizador [admin] e a palavra-passe [admin] enviará o seguinte cabeçalho HTTP para se autenticar:
- Linhas 34–36: indicam que todos os URLs do serviço web estão acessíveis a utilizadores com a função [ROLE_ADMIN]. Isto significa que um utilizador sem esta função não pode aceder ao serviço web;
A classe [AppConfig], que configura toda a aplicação, é atualizada da seguinte forma:
![]() |
package rdvmedecins.web.config;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Import;
import rdvmedecins.config.DomainAndPersistenceConfig;
@EnableAutoConfiguration
@ComponentScan(basePackages = { "rdvmedecins.web" })
@Import({ DomainAndPersistenceConfig.class, SecurityConfig.class })
public class AppConfig {
}
- A alteração é feita na linha 11: especifica que agora existem dois ficheiros de configuração a utilizar: [DomainAndPersistenceConfig] e [SecurityConfig].
2.14.9. Teste do serviço web
Iremos testar o serviço web utilizando o cliente Chrome [Advanced Rest Client]. Teremos de especificar o cabeçalho de autenticação HTTP:
onde [código] é a cadeia codificada em Base64 [login:password]. Para gerar este código, pode utilizar o seguinte programa:
![]() |
package rdvmedecins.helpers;
import org.springframework.security.crypto.codec.Base64;
public class Base64Encoder {
public static void main(String[] args) {
// we expect two arguments: login password
if (args.length != 2) {
System.out.println("Syntaxe : login password");
System.exit(0);
}
// we retrieve the two arguments
String chaîne = String.format("%s:%s", args[0], args[1]);
// encode the string
byte[] data = Base64.encode(chaîne.getBytes());
// displays its Base64 encoding
System.out.println(new String(data));
}
}
Se executarmos este programa com os dois argumentos [admin admin]:
![]() |
obtemos o seguinte resultado:
Agora que sabemos como gerar o cabeçalho de autenticação HTTP, lançamos o serviço web agora seguro. Em seguida, utilizando o cliente Chrome [Advanced Rest Client], solicitamos a lista de todos os médicos:
![]() |
- em [1], solicitamos a URL dos médicos;
- em [2], utilizando o método GET;
- em [3], fornecemos o cabeçalho de autenticação HTTP. O código [YWRtaW46YWRtaW4=] é a codificação Base64 da cadeia [admin:admin];
- em [4], enviamos o pedido HTTP;
A resposta do servidor é a seguinte:
![]() |
- em [1], o cabeçalho de autenticação HTTP;
- em [2], o servidor devolve uma resposta JSON;
- em [3], a lista de médicos.
Agora vamos tentar uma solicitação HTTP com um cabeçalho de autenticação incorreto. A resposta é a seguinte:
![]() |
- em [1] e [3]: o cabeçalho de autenticação HTTP;
- em [2]: a resposta do serviço web;
Agora, vamos experimentar o utilizador / user. Ele existe, mas não tem acesso ao serviço web. Se executarmos o programa de codificação Base64 com os dois argumentos [user user]:
![]() |
obtemos o seguinte resultado:
![]() |
- em [1] e [3]: o cabeçalho de autenticação HTTP;
- em [2]: a resposta do serviço web. Diferencia-se da anterior, que era [401 Não autorizado]. Desta vez, o utilizador autenticou-se com sucesso, mas não possui permissões suficientes para aceder ao URL;
2.15. Conclusão
Vamos rever a arquitetura geral da nossa aplicação cliente/servidor:
![]() |
Um serviço web seguro está agora operacional. Veremos que ele precisará de ser modificado devido a problemas que surgirão durante o desenvolvimento do cliente Angular JS. Mas vamos esperar até encontrarmos o problema para o resolver. Vamos agora construir o cliente Angular que fornecerá uma interface web para a gestão das consultas médicas.

















































































































































