13. [Curso]: Expor uma base de dados na Web com Spring MVC
Palavras-chave: arquitetura multicamadas, Spring, injeção de dependências, serviço web / JSON, cliente / servidor
13.1. Suporte
![]() | ![]() |
Os projetos para este capítulo podem ser encontrados na pasta [support / chap-13]. O script SQL [dbintrospringdata.sql] cria a base de dados MySQL necessária para os testes.
13.2. O papel do Spring MVC numa aplicação web
Vamos situar o Spring MVC no contexto do desenvolvimento de uma aplicação web. Na maioria das vezes, esta será construída com base numa arquitetura multicamadas, como a seguinte:
![]() |
- a camada [Web] é a camada em contacto com o utilizador da aplicação web. O utilizador interage com a aplicação web através de páginas web visualizadas num navegador. O Spring MVC está localizado nesta camada e apenas nesta camada;
- a camada [business] implementa a lógica de negócio da aplicação, como o cálculo de um salário ou de uma fatura. Esta camada utiliza dados do utilizador através da camada [Web] e do SGBD através da camada [DAO];
- a camada [DAO] (Data Access Objects), a camada [ORM] (Object Relational Mapper) e o controlador JDBC gerem o acesso aos dados no SGBD. A camada [ORM] atua como uma ponte entre os objetos tratados pela camada [DAO] e as linhas e colunas das tabelas numa base de dados relacional. A especificação JPA (Java Persistence API) permite a abstração do ORM utilizado, desde que este implemente essas especificações. Será esse o caso aqui, e doravante referir-nos-emos à camada ORM como a camada JPA;
- A integração das camadas é gerida pela estrutura Spring;
13.3. O Modelo de Desenvolvimento Spring MVC
O Spring MVC implementa o padrão arquitetónico MVC (Model–View–Controller) da seguinte forma:
![]() |
O processamento de um pedido do cliente decorre da seguinte forma:
- pedido - os URLs solicitados têm o formato http://machine:port/contexte/Action/param1/param2/....?p1=v1&p2=v2&... O [Front Controller] utiliza um ficheiro de configuração ou anotações Java para «encaminhar» o pedido para o controlador correto e para a ação correta dentro desse controlador. Para tal, utiliza o campo [Action] do URL. O resto da URL [/param1/param2/...] consiste em parâmetros opcionais que serão passados para a ação. O C em MVC aqui refere-se à cadeia [Front Controller, Controller, Action]. Se nenhum controlador puder lidar com a ação solicitada, o servidor web responderá que a URL solicitada não foi encontrada.
- processamento
- A ação selecionada pode utilizar os parâmetros que o [Controlador Frontal] lhe passou. Estes podem provir de várias fontes:
- o caminho [/param1/param2/...] da URL,
- os parâmetros [p1=v1&p2=v2] da URL,
- dos parâmetros enviados pelo navegador com o seu pedido;
- ao processar a solicitação do utilizador, a ação pode necessitar da camada [business] [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;
Agora, vamos esclarecer a relação entre a arquitetura web MVC e a arquitetura em camadas. Dependendo de como o modelo é definido, estes dois conceitos podem ou não estar relacionados. Considere uma aplicação web Spring MVC de camada única:
![]() |
Se implementarmos a camada [Web] com Spring MVC, teremos, de facto, uma arquitetura web MVC, mas não uma arquitetura em camadas. Aqui, a camada [Web] tratará de tudo: apresentação, lógica de negócio e acesso aos dados. São as ações que realizarão este trabalho.
Agora, vamos considerar uma arquitetura web multicamadas:
![]() |
A camada [Web] pode ser implementada sem um framework e sem seguir o padrão MVC. Neste caso, continuamos a ter uma arquitetura multicamadas, mas a camada Web não implementa o padrão MVC.
Por exemplo, no mundo .NET, a camada [Web] descrita acima pode ser implementada utilizando o ASP.NET MVC, resultando numa arquitetura em camadas com uma camada [Web] de estilo MVC. Dito isto, esta camada ASP.NET MVC pode ser substituída por uma camada ASP.NET clássica (WebForms), mantendo o resto (lógica de negócio, DAO, ORM) inalterado. Temos então uma arquitetura em camadas com uma camada [Web] que já não é baseada em MVC.
No MVC, dissemos que o modelo M era o da vista V, ou seja, o conjunto de dados exibidos pela vista V. É dada outra definição do modelo M no MVC:
![]() |
Muitos autores consideram que o que se encontra à direita da camada [Web] constitui o modelo M do MVC. Para evitar ambiguidades, podemos referir-nos a:
- o modelo de domínio quando nos referimos a tudo o que está à direita da camada [Web]
- o modelo de vista quando nos referimos aos dados apresentados por uma vista V
Daqui em diante, o termo «modelo M» referir-se-á exclusivamente ao modelo de uma vista V.
13.4. Um projeto Web/JSON com Spring MVC
O site [http://spring.io/guides] oferece tutoriais de introdução para explorar o ecossistema Spring. Seguiremos um deles para descobrir a configuração do Maven necessária para um projeto Spring MVC.
13.4.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 dados JSON são frequentemente designados por serviços REST (REpresentational State Transfer). Diz-se que um serviço é RESTful se seguir determinadas regras.
Vamos agora examinar o projeto importado, começando pela sua configuração do Maven.
13.4.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.2.2.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
<properties>
<start-class>hello.Application</start-class>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>spring-releases</id>
<url>https://repo.spring.io/libs-release</url>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-releases</id>
<url>https://repo.spring.io/libs-release</url>
</pluginRepository>
</pluginRepositories>
</project>
- linhas 6–8: as propriedades do projeto Maven. Falta uma tag [<packaging>] que especifique o tipo de ficheiro produzido pela compilação do Maven. Na sua ausência, é utilizado o tipo [jar]. A aplicação é, portanto, uma aplicação executável baseada em consola, e não uma aplicação web, caso em que a embalagem seria [war];
- linhas 10–14: O projeto Maven tem um projeto pai [spring-boot-starter-parent]. Este 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 17–20: O artefacto [spring-boot-starter-web] inclui as bibliotecas necessárias para um projeto de serviço web Spring MVC onde não são geradas vistas. Este artefacto inclui um número muito grande de bibliotecas, incluindo as destinadas a um servidor Tomcat incorporado. A aplicação será executada neste servidor;
As bibliotecas incluídas nesta configuração são muito numerosas:
![]() | ![]() |
Acima, vemos os três arquivos do servidor Tomcat.
13.4.3. A arquitetura de um serviço Spring [web / JSON]
Para um serviço web/JSON, o Spring MVC implementa o modelo MVC da seguinte forma:
![]() |
- 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;
13.4.4. O controlador C
![]() |
A aplicação importada tem o seguinte controlador:
package hello;
import java.util.concurrent.atomic.AtomicLong;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class GreetingController {
private static final String template = "Hello, %s!";
private final AtomicLong counter = new AtomicLong();
@RequestMapping("/greeting")
public Greeting greeting(@RequestParam(value = "name", defaultValue = "World") String name) {
return new Greeting(counter.incrementAndGet(), String.format(template, name));
}
}
- Linha 9: A anotação [@RestController] torna a classe [GreetingController] um controlador Spring, o que significa que os seus métodos estão registados para tratar de URLs. Já vimos a anotação semelhante [@Controller]. O tipo de retorno dos métodos desse controlador era [String], que era o nome da vista a apresentar. Aqui, é diferente. Os métodos de um [@RestController] devolvem objetos que são serializados para serem enviados para o navegador. O tipo de serialização realizada depende da configuração do Spring MVC. Aqui, serão serializados para JSON. É 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 14: a anotação [@RequestMapping] especifica a URL tratada pelo método, neste caso a URL [/greeting];
- linha 15: já explicámos a anotação [@RequestParam]. O resultado devolvido pelo método é um objeto do tipo [Greeting].
- linha 12: um inteiro longo de tipo atómico. Isto significa que suporta acesso simultâneo. Várias threads podem querer incrementar a variável [counter] ao mesmo tempo. Isto será tratado corretamente. Uma thread só pode ler o valor do contador depois de a thread que o está a modificar ter concluído a sua modificação.
13.4.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
13.4.6. Execução
![]() |
A classe [Application.java] é a classe executável do projeto. O seu código é o seguinte:
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);
}
}
Já nos deparámos com este código e explicámo-lo no exemplo anterior.
13.4.7. Executar o projeto
![]() |
Recebemos os seguintes registos da consola:
- linha 13: o servidor Tomcat inicia na porta 8080 (linha 12);
- linha 17: o servlet [DispatcherServlet] está presente;
- linha 20: o método [GreetingController.greeting] foi descoberto;
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 a extensão do Chrome chamada [Advanced Rest Client] (Chrome / Ctrl-T / menu [Aplicações] / [Advanced Rest Client] - ver Apêndices, parágrafo 22.5):
![]() |
- 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» e o seu valor;
- em [8], o navegador informa ao servidor que está a enviar-lhe informações [urlencoded];
- em [9], a resposta JSON do servidor;
13.4.8. Criação de um arquivo executável
Vamos agora criar um arquivo executável:
![]() |
![]() |
- em [1]: executamos um alvo do Maven;
- em [2]: existem dois objetivos: [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]: geramos o alvo;
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:
D:\Temp\wksSTS\gs-rest-service\target>dir
...
11/06/2014 15:30 <DIR> classes
11/06/2014 15:30 <DIR> generated-sources
11/06/2014 15:30 11 073 572 gs-rest-service-0.1.0.jar
11/06/2014 15:30 3 690 gs-rest-service-0.1.0.jar.original
11/06/2014 15:30 <DIR> maven-archiver
11/06/2014 15:30 <DIR> maven-status
...
- linha 5: o arquivo gerado;
Este arquivo é executado da seguinte forma:
D:\Temp\wksSTS\gs-rest-service-complete\target>java -jar gs-rest-service-0.1.0.jar
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v1.1.0.RELEASE)
2014-06-11 15:32:47.088 INFO 4972 --- [ main] hello.Application
: Starting Application on Gportpers3 with PID 4972 (D:\Temp\wk
sSTS\gs-rest-service-complete\target\gs-rest-service-0.1.0.jar started by ST in
D:\Temp\wksSTS\gs-rest-service-complete\target)
...
Agora que a aplicação web está em execução, pode aceder-lhe utilizando um navegador:
![]() |
13.4.9. Implantação da aplicação num servidor Tomcat
Tal como fizemos no projeto anterior, modificamos 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>
...
</project>
- Linha 9: Deve especificar que vai gerar um ficheiro WAR (Web Archive);
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-2], execute o projeto num dos servidores registados no IDE Eclipse;
Depois de fazer isso, pode aceder ao URL [http://localhost:8080/gs-rest-service/greeting/?name=Mitchell] num navegador:
![]() |
13.4.10. Conclusão
Apresentámos um tipo de projeto Spring MVC em que a aplicação web envia um fluxo JSON para o navegador. Vamos agora desenvolver uma aplicação web/JSON para expor na web a base de dados [dbintrospringdata] estudada no tutorial [Introdução ao Spring Data].
13.5. Exposição da base de dados [dbintrospringdata] na Web
13.5.1. Arquitetura do serviço Web/JSON
Iremos implementar a seguinte arquitetura:
![]() |
As camadas [DAO] e [JPA] são implementadas pela aplicação escrita no tutorial [Introdução ao Spring Data].
13.5.2. Instalação da base de dados
![]() |
O script SQL [dbintrospringdata.sql] cria a base de dados MySQL necessária para os testes.
13.5.3. O projeto Eclipse para o serviço web / JSON
O projeto Eclipse para o serviço web / JSON é o seguinte:
![]() |
Este é um projeto Maven cujo ficheiro [pom.xml] é o seguinte:
<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>istia.st.webjson</groupId>
<artifactId>intro-server-webjson01</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>intro-server-webjson01</name>
<description>démo spring mvc</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.7.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>istia.st.springdata</groupId>
<artifactId>intro-spring-data-01</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.18.1</version>
</plugin>
</plugins>
</build>
</project>
- linhas 11–15: o projeto Maven pai já utilizado para a camada [DAO];
- linhas 18–22: a dependência da camada [DAO];
- linhas 23–26: a dependência do artefacto [spring-boot-starter-web]. Este artefacto inclui todas as dependências necessárias para criar um serviço web/JSON. Também inclui bibliotecas desnecessárias. Seria, portanto, necessária uma configuração mais precisa. No entanto, esta configuração é útil para começar;
- linhas 28–30: a dependência do artefacto [spring-boot-starter] permite-lhe gerir as anotações do Spring Boot;
As dependências introduzidas por esta configuração são as seguintes:
![]() |
- Em [1], podemos ver que o Eclipse detectou a dependência do arquivo do projeto [intro-spring-data-01];
As dependências acima são tanto da camada [DAO] como da camada [web].
13.5.3.1. Configuração da camada [web]
A camada [web] é configurada por um ficheiro [AppConfig]:
![]() |
A classe [WebConfig] configura a camada [web]:
package spring.webjson.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.embedded.EmbeddedServletContainerFactory;
import org.springframework.boot.context.embedded.ServletRegistrationBean;
import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;
@EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter {
// -------------------------------- layer configuration [web]
@Autowired
private ApplicationContext context;
@Bean
public DispatcherServlet dispatcherServlet() {
DispatcherServlet servlet = new DispatcherServlet((WebApplicationContext) context);
return servlet;
}
@Bean
public ServletRegistrationBean servletRegistrationBean(DispatcherServlet dispatcherServlet) {
return new ServletRegistrationBean(dispatcherServlet, "/*");
}
@Bean
public EmbeddedServletContainerFactory embeddedServletContainerFactory() {
return new TomcatEmbeddedServletContainerFactory("", 8080);
}
// filters jSON
@Bean(name = "jsonMapper")
public ObjectMapper jsonMapper() {
return new ObjectMapper();
}
@Bean(name = "jsonMapperCategorieWithProduits")
public ObjectMapper jsonMapperCategorieWithProduits() {
// mapper jSON
ObjectMapper mapper = new ObjectMapper();
// filters
mapper.setFilters(
new SimpleFilterProvider().addFilter("jsonFilterCategorie", SimpleBeanPropertyFilter.serializeAllExcept())
.addFilter("jsonFilterProduit", SimpleBeanPropertyFilter.serializeAllExcept("categorie")));
// result
return mapper;
}
@Bean(name = "jsonMapperProduitWithCategorie")
public ObjectMapper jsonMapperProduitWithCategorie() {
// mapper jSON
ObjectMapper mapper = new ObjectMapper();
// filters
mapper.setFilters(
new SimpleFilterProvider().addFilter("jsonFilterProduit", SimpleBeanPropertyFilter.serializeAllExcept())
.addFilter("jsonFilterCategorie", SimpleBeanPropertyFilter.serializeAllExcept("produits")));
// result
return mapper;
}
@Bean(name = "jsonMapperCategorieWithoutProduits")
public ObjectMapper jsonMapperCategorieWithoutProduits() {
// mapper jSON
ObjectMapper mapper = new ObjectMapper();
// filters
mapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterCategorie",
SimpleBeanPropertyFilter.serializeAllExcept("produits")));
// result
return mapper;
}
@Bean(name = "jsonMapperProduitWithoutCategorie")
public ObjectMapper jsonMapperProduitWithoutCategorie() {
// mapper jSON
ObjectMapper mapper = new ObjectMapper();
// filters
mapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterProduit",
SimpleBeanPropertyFilter.serializeAllExcept("categorie")));
// result
return mapper;
}
}
- linha 18: a anotação [@EnableWebMvc] aciona configurações automáticas para a estrutura Spring MVC;
- linha 19: a classe [WebConfig] estende a classe [WebMvcConfigurerAdapter] do Spring para redefinir determinados beans (linhas 26–40);
- linhas 22–23: injeção do contexto Spring;
- linhas 25–29: definição do servlet da estrutura Spring MVC, que encaminha os pedidos HTTP para o controlador e método corretos. [DispatcherServlet] é uma classe Spring;
- linhas 31–34: especificamos que este servlet lida com todas as URLs;
- linhas 36–39: a presença deste bean ativará o servidor Tomcat incluído nos arquivos do projeto. Ele ficará à escuta de solicitações na porta 8080;
- linhas 42–91: beans que serão utilizados para gerir filtros JSON;
- linhas 42–45: um mapeador JSON sem filtros;
- linhas 47–57: o mapeador JSON que permite recuperar uma categoria juntamente com os seus produtos. Note que, ao solicitar uma categoria com os seus produtos, deve configurar tanto o filtro JSON para a classe [Category] como o da classe [Product]. Isto acontece sempre. Ao serializar/deserializar uma classe para JSON, deve configurar o filtro JSON para a classe e os de todas as dependências a incluir nela;
- linhas 59–69: o mapeador JSON que permite que um produto seja exibido com a sua categoria;
- linhas 71–80: o mapeador JSON que permite obter uma categoria sem os seus produtos;
- linhas 82–91: o mapeador JSON que permite recuperar um produto sem a sua categoria;
A classe [AppConfig] configura toda a aplicação, ou seja, as camadas [web] e [DAO]:
package spring.webjson.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Import;
import spring.data.config.DaoConfig;
@ComponentScan(basePackages = { "spring.webjson" })
@Import({ DaoConfig.class, WebConfig.class})
public class AppConfig {
}
- Linha 9: importa os beans da camada [DAO] e da camada [web];
- linha 8: especifica os pacotes onde outros beans Spring podem ser encontrados;
Note que não utilizámos a anotação [@EnableAutoConfiguration] em nenhum ponto. Preferimos controlar a configuração nós próprios.
13.5.4. O modelo da aplicação
![]() |
A classe [ApplicationModel] é a seguinte:
package spring.webjson.models;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import spring.data.dao.IDao;
import spring.data.entities.Categorie;
import spring.data.entities.Produit;
@Component
public class ApplicationModel implements IDao {
// the [DAO] layer
@Autowired
private IDao dao;
@Override
public void addProduits(List<Produit> produits) {
dao.addProduits(produits);
}
@Override
public void deleteAllProduits() {
dao.deleteAllProduits();
}
@Override
public void updateProduits(List<Produit> produits) {
dao.updateProduits(produits);
}
@Override
public List<Produit> getAllProduits() {
return dao.getAllProduits();
}
@Override
public void addCategories(List<Categorie> categories) {
dao.addCategories(categories);
}
@Override
public void deleteAllCategories() {
dao.deleteAllCategories();
}
@Override
public void updateCategories(List<Categorie> categories) {
dao.updateCategories(categories);
}
@Override
public List<Categorie> getAllCategories() {
return dao.getAllCategories();
}
@Override
public Produit getProduitByIdWithCategorie(Long idProduit) {
return dao.getProduitByIdWithCategorie(idProduit);
}
@Override
public Produit getProduitByNameWithCategorie(String nom) {
return dao.getProduitByNameWithCategorie(nom);
}
@Override
public Categorie getCategorieByIdWithProduits(Long idCategorie) {
return dao.getCategorieByIdWithProduits(idCategorie);
}
@Override
public Categorie getCategorieByNameWithProduits(String nom) {
return dao.getCategorieByNameWithProduits(nom);
}
@Override
public Produit getProduitByIdWithoutCategorie(Long idProduit) {
return dao.getProduitByIdWithoutCategorie(idProduit);
}
@Override
public Categorie getCategorieByIdWithoutProduits(Long idCategorie) {
return dao.getCategorieByIdWithoutProduits(idCategorie);
}
@Override
public Produit getProduitByNameWithoutCategorie(String nom) {
return dao.getProduitByNameWithoutCategorie(nom);
}
@Override
public Categorie getCategorieByNameWithoutProduits(String nom) {
return dao.getCategorieByNameWithoutProduits(nom);
}
}
- linha 12: a classe é um singleton Spring;
- linha 13: que implementa a interface [IDao] da camada [DAO];
- linhas 16–17: injeção de uma referência na camada [DAO];
- linhas 19–99: implementação da interface [IDao];
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 no que diz respeito à gestão de um potencial cache. A classe [ApplicationModel] pode ser utilizada para armazenar informações obtidas da camada [DAO] ou dados de configuração. Isto pode ser útil quando não se tem controlo sobre a camada [DAO]. Esta estratégia de cache pode evoluir ao longo do tempo. As alterações não terão impacto no código do(s) controlador(es).
13.5.5. O controlador
![]() |
![]() |
Aqui temos apenas um controlador, a classe [MyController].
13.5.5.1. URLs expostas
As URLs expostas por este controlador são as seguintes:
| Adiciona produtos à base de dados. Estes são enviados. A resposta é uma cadeia JSON contendo a lista de produtos adicionados com as suas chaves primárias. |
| Elimina todos os produtos da base de dados. |
| Atualiza os produtos na base de dados. Estas são enviadas. A resposta é uma cadeia JSON que contém a lista de produtos atualizados. |
| Recupera a cadeia JSON para todos os produtos. |
| Adiciona categorias à base de dados. Estas são enviadas. A resposta é uma string JSON que contém a lista de categorias adicionadas, juntamente com as suas chaves primárias. Se as categorias contiverem produtos, estes também são adicionados à base de dados. |
| Elimina todas as categorias da base de dados, juntamente com todos os produtos nelas contidos. Depois disso, a base de dados fica vazia. |
| Atualiza as categorias na base de dados. Estas são enviadas. A resposta é a lista das categorias atualizadas. Se as categorias contiverem produtos, estes também são atualizados na base de dados. Devolve a cadeia JSON das categorias modificadas; |
| Recupera a cadeia JSON para todas as categorias. |
| Recupera a cadeia JSON de um produto identificado pelo seu ID, juntamente com a sua categoria. |
| Recupera a cadeia JSON de um produto identificado pelo seu ID, sem a sua categoria. |
| Recupera a string JSON de um produto identificado pelo seu nome, juntamente com a sua categoria. |
| Recupera a cadeia JSON de um produto identificado pelo seu nome, sem a sua categoria. |
| Recupera a cadeia JSON de uma categoria especificada pelo seu ID, juntamente com os seus produtos. |
| Recupera a cadeia JSON de uma categoria especificada pelo seu nome, juntamente com os seus produtos. |
| Recupera a cadeia JSON de uma categoria especificada pelo seu nome, sem os seus produtos. |
| Recupera a cadeia JSON de uma categoria identificada pelo seu ID, excluindo os seus produtos. |
As URLs expostas correspondem aos métodos da interface [IDao] na camada [DAO]. Os métodos do serviço web / JSON são todos construídos com base no mesmo modelo. Vamos examinar alguns deles.
13.5.5.2. O esqueleto do controlador
O esqueleto do controlador é o seguinte:
package spring.webjson.service;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import javax.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.io.CharStreams;
import spring.data.dao.DaoException;
import spring.data.entities.Categorie;
import spring.data.entities.Produit;
import spring.webjson.models.ApplicationModel;
import spring.webjson.models.Response;
@Controller
public class MyController {
// spring dependencies
@Autowired
private ApplicationModel application;
// filters jSON
@Autowired
@Qualifier("jsonMapper")
private ObjectMapper jsonMapper;
@Autowired
@Qualifier("jsonMapperCategorieWithProduits")
private ObjectMapper jsonMapperCategorieWithProduits;
@Autowired
@Qualifier("jsonMapperProduitWithCategorie")
private ObjectMapper jsonMapperProduitWithCategorie;
@Autowired
@Qualifier("jsonMapperCategorieWithoutProduits")
private ObjectMapper jsonMapperCategorieWithoutProduits;
@Autowired
@Qualifier("jsonMapperProduitWithoutCategorie")
private ObjectMapper jsonMapperProduitWithoutCategorie;
// class [MyController] is a singleton and is instantiated only once the bean
public MyController() {
// System.out.println("MyController");
}
@RequestMapping(value = "/addProduits", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8", produces = "application/json; charset=UTF-8")
@ResponseBody
public String addProduits(HttpServletRequest request) throws JsonProcessingException {
...
}
- linha 28: a anotação [@Controller] torna a classe um componente Spring;
- linhas 32–33: injeção de uma referência à classe [ApplicationModel];
- linhas 36–50: injeção de referências aos mapeadores JSON;
- linha 58: a URL exposta é [/addProducts]. O cliente deve utilizar um método [POST] para efetuar o seu pedido (method = RequestMethod.POST). Deve enviar o valor enviado como uma cadeia JSON (content-type = "application/json; charset=UTF-8"). O próprio método devolve a resposta ao cliente (linha 59). Esta será uma string (linha 60). O cabeçalho HTTP [Content-type: application/json; charset=UTF-8] será enviado ao cliente para indicar que este irá receber uma string JSON (linha 58);
- linha 60: o método [addProduits] devolve a cadeia JSON contendo a lista de produtos adicionados à base de dados;
13.5.5.3. Respostas dos métodos do controlador
Todos os métodos do controlador devolvem o seguinte tipo [Response]:
![]() |
package spring.webjson.service;
import java.util.List;
public class Response<T> {
// ----------------- properties
// operation status
private int status;
// any error messages
private List<String> messages;
// the body of the reply
private T body;
// manufacturers
public Response() {
}
public Response(int status, List<String> messages, T body) {
this.status = status;
this.messages = messages;
this.body = body;
}
// getters and setters
...
}
- linha 5: a resposta encapsula um tipo T;
- linha 13: a resposta do tipo T;
- linhas 9–11: um método pode encontrar uma exceção. Neste caso, irá devolver uma resposta com:
- linha 9: status!=0;
- linha 11: a lista de erros encontrados;
13.5.5.4. A URL [/addProducts]
A URL [/addProducts] é tratada pelo seguinte método:
@RequestMapping(value = "/addProduits", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8", produces = "application/json; charset=UTF-8")
@ResponseBody
public String addProduits(HttpServletRequest request) throws JsonProcessingException {
// answer
Response<List<Produit>> response;
try {
// retrieve the posted value
String body = CharStreams.toString(request.getReader());
List<Produit> produits = jsonMapperProduitWithoutCategorie.readValue(body, new TypeReference<List<Produit>>() {
});
// we re-establish the link between products and categories
for (Produit produit : produits) {
produit.setCategorie(application.getCategorieByIdWithoutProduits(produit.getIdCategorie()));
}
// we persist products
application.addProduits(produits);
response = new Respon se<List<Produit>>(0, null, produits);
} catch (DaoException e1) {
response = new Response<List<Produit>>(1000, e1.getErreurs(), null);
} catch (Exception e2) {
response = new Response<List<Produit>>(1000, getErreursForException(e2), null);
}
// answer jSON
return jsonMapperProduitWithoutCategorie.writeValueAsString(response);
}
- linha 3: o método recebe [HttpServletRequest request] como parâmetro, que encapsula todas as informações sobre o pedido do cliente;
- linha 5: a resposta que será enviada ao cliente: uma lista de produtos;
- linha 8: recuperamos o valor enviado. A classe [CharStreams] pertence à biblioteca [Google Guava], cuja referência adicionámos ao ficheiro [pom.xml]. Obtemos a cadeia JSON enviada pelo cliente. Precisamos de a deserializar para poder utilizá-la;
- linhas 8–10: a deserialização é realizada. Obtemos uma lista de produtos em que cada produto tem um campo [category=null];
- linhas 12–14: redefinimos o campo [category] para todos os produtos da lista. Para tal, utilizamos o campo [categoryId] do produto, que é inicializado;
- linha 16: os produtos são inseridos na base de dados;
- linha 17: o objeto [response] é inicializado com a lista de produtos;
- linhas 18-19: caso em que o método encontra uma exceção da camada [DAO]. Inicializamos a resposta com [status=1000] (código de erro) [messages=e1.getMessages()], ou seja, enviamos ao cliente a lista de erros encontrados no lado do servidor;
- linhas 20–21: caso em que o método encontra outro tipo de exceção. Inicializamos a resposta com [status=1000] (código de erro) [messages=getErrorsForException(e)], onde [getErrorsForException] é um método privado da classe que retorna a lista de erros associados às exceções na pilha de exceções de e, e [body=null];
- linha 24: a cadeia JSON da resposta é devolvida;
13.5.5.5. A URL [/getAllProducts]
A URL [/getAllProducts] é tratada pelo seguinte método:
@RequestMapping(value = "/getAllProduits", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getAllProduits() throws JsonProcessingException {
// answer
Response<List<Produit>> response;
try {
response = new Response<List<Produit>>(0, null, application.getAllProduits());
} catch (DaoException e1) {
response = new Response<List<Produit>>(1003, e1.getErreurs(), null);
} catch (Exception e2) {
response = new Response<List<Produit>>(1003, getErreursForException(e2), null);
}
// answer jSON
return jsonMapperProduitWithoutCategorie.writeValueAsString(response);
}
- Linha 1: A URL [/getAllProduits] é solicitada utilizando uma operação [GET]. Ela retorna JSON;
- linha 2: o próprio método envia a resposta JSON ao cliente;
- linha 5: o método devolve uma cadeia JSON do tipo [Response<List<Product>>];
- linha 7: os produtos são solicitados sem a sua categoria;
- linhas 8–12: em caso de erro, a resposta é inicializada com um código de erro e mensagens de erro;
- linha 14: a resposta JSON é enviada ao cliente;
13.5.5.6. Conclusão
Não abordaremos os outros métodos do controlador. São semelhantes a um ou outro dos dois métodos que acabámos de apresentar.
13.5.6. O Serviço Web / Classe de Execução JSON
![]() |
A classe [Boot] é a classe executável do projeto:
package spring.webjson.boot;
import org.springframework.boot.SpringApplication;
import spring.webjson.server.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. A classe [SpringApplication] é uma classe do projeto [Spring Boot] (linha 3). São-lhe passados dois parâmetros:
- [AppConfig.class]: a classe que configura toda a aplicação;
- [args]: quaisquer argumentos passados para o método [main] na linha 9. Este parâmetro não é utilizado aqui;
Quando esta classe é executada, são gerados os seguintes registos:
- linhas 17-19: Inicialização do servidor Tomcat para executar o serviço web/JSON;
- linhas 25-33: construção da camada [DAO];
- linhas 32-51: as URLs expostas são descobertas;
13.5.7. Testes do serviço Web / JSON
Para realizar os testes, geramos a base de dados MySQL [dbintrospringdata] a partir do script SQL [dbintrospringdata.sql]:
![]() |
Depois de fazer isso, usamos o [Advanced Rest Client] (ver secção 22.5) para consultar os URLs expostos pelo serviço web / JSON (o serviço web / JSON deve estar em execução).
![]() |
- Em [1-3], solicitamos o URL [/getAllCategories] através de uma solicitação HTTP GET;
Recebemos a seguinte resposta:
![]() |
- Em [1], o pedido HTTP do cliente;
- em [2], a resposta HTTP do servidor;
- em [3], o estado [200 OK] indica que o servidor processou a solicitação com sucesso;
- em [4], a resposta JSON do servidor;
A resposta JSON completa é a seguinte:
{"status":0,"messages":null,"body":[{"id":415,"version":0,"nom":"categorie0","produits":[{"id":1849,"version":0,"nom":"produit00","idCategorie":415,"prix":100.0,"description":"desc00"},{"id":1850,"version":0,"nom":"produit01","idCategorie":415,"prix":101.0,"description":"desc01"},{"id":1851,"version":0,"nom":"produit02","idCategorie":415,"prix":102.0,"description":"desc02"},{"id":1852,"version":0,"nom":"produit03","idCategorie":415,"prix":103.0,"description":"desc03"},{"id":1853,"version":0,"nom":"produit04","idCategorie":415,"prix":104.0,"description":"desc04"}]},{"id":416,"version":0,"nom":"categorie1","produits":[{"id":1856,"version":0,"nom":"produit12","idCategorie":416,"prix":112.0,"description":"desc12"},{"id":1857,"version":0,"nom":"produit13","idCategorie":416,"prix":113.0,"description":"desc13"},{"id":1858,"version":0,"nom":"produit14","idCategorie":416,"prix":114.0,"description":"desc14"},{"id":1854,"version":0,"nom":"produit10","idCategorie":416,"prix":110.0,"description":"desc10"},{"id":1855,"version":0,"nom":"produit11","idCategorie":416,"prix":111.0,"description":"desc11"}]}]}
- status:0 significa que não houve erros do lado do servidor;
- mensagens: nulo significa que não há mensagens de erro;
- body: é o corpo da resposta, neste caso a lista de categorias com os seus produtos. Existem duas categorias, cada uma com 5 produtos;
Vamos adicionar o produto [product15] à categoria [category1]. Para isso, utilizaremos a URL [/addCategories], que contém o seguinte código:
@RequestMapping(value = "/addCategories", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8", produces = "application/json; charset=UTF-8")
@ResponseBody
public String addCategories(HttpServletRequest request) throws JsonProcessingException {
Response<List<Categorie>> response;
ObjectMapper mapper = context.getBean(ObjectMapper.class);
// we persist categories
try {
// retrieve the posted value
String body = CharStreams.toString(request.getReader());
mapper.setFilters(jsonFilterCategorieWithProduits);
List<Categorie> categories = mapper.readValue(body, new TypeReference<List<Categorie>>() {
});
// we re-establish the link between products and categories
for (Categorie categorie : categories) {
Set<Produit> produits = categorie.getProduits();
if (produits != null) {
for (Produit produit : categorie.getProduits()) {
produit.setCategorie(categorie);
}
}
}
// we persist categories
application.addCategories(categories);
response = new Response<List<Categorie>>(0, null, categories);
} catch (Exception e) {
response = new Response<List<Categorie>>(1004, getErreursForException(e), null);
}
// answer jSON
return mapper.writeValueAsString(response);
}
- linha 1: o cliente deve enviar uma solicitação POST, e o valor enviado deve ser uma string JSON;
- linhas 9–12: o valor enviado deve ser uma lista de categorias com os seus produtos associados;
Vamos criar uma categoria [category2] com um produto [product21]. A cadeia JSON a enviar é então a seguinte:
[{"id":null,"version":0,"nom":"categorie2","produits":[{"id":null,"version":0,"nom":"produit21","idCategorie":null,"prix":111.0,"description":"desc21"}]}]
O pedido ao serviço web / JSON é feito da seguinte forma:
![]() |
- em [1], o URL solicitado;
- em [2], é solicitada através de uma operação POST;
- em [3], a cadeia JSON enviada;
- em [4], o servidor é informado de que serão enviados dados JSON;
A resposta do servidor é a seguinte:
![]() |
- em [1], vemos que tanto a categoria como o seu produto têm agora uma chave primária, o que indica que provavelmente foram inseridos na base de dados. Iremos verificar isto utilizando o URL [/getCategorieByNameWithProduits/categorie2]:
![]() |
Obtemos o seguinte resultado:
![]() |
De facto, recuperámos a categoria [categorie2] com o seu único produto [produit21]. Também podemos solicitar apenas o produto. Para tal, vamos utilizar o URL [/getProduitByIdWithoutCategorie/1859]:
![]() |
Obtemos o seguinte resultado:
![]() |
Todas as operações [GET] podem ser realizadas num navegador web padrão:
![]() |
Convidamos os leitores a testarem os outros URLs do serviço web /json.
13.6. Um cliente programado para o serviço web /json
Agora que a base de dados [dbintrospringdata] está disponível na Web, vamos escrever uma aplicação que a utilize. Teremos então a seguinte arquitetura cliente/servidor:
![]() |
A aplicação cliente terá duas camadas:
- uma camada [DAO] [2] para comunicar com a aplicação web /json que expõe a base de dados;
- uma camada de testes JUnit [1] para verificar se o cliente e o servidor estão a funcionar corretamente;
13.6.1. O projeto Eclipse
O projeto Eclipse do cliente é o seguinte:
![]() |
- a pasta [src/main/java] implementa a camada [DAO];
- a pasta [src/test/java] implementa os testes JUnit;
13.6.2. Configuração do projeto Maven
O projeto é um projeto Maven configurado pelo seguinte ficheiro [pom.xml]:
<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>istia.st.webjson</groupId>
<artifactId>intro-client-webjson-01</artifactId>
<version>0.0.1-SNAPSHOT</version>
<description>Client console du serveur web / jSON</description>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
</properties>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.7.RELEASE</version>
</parent>
<dependencies>
<!-- Spring -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
</dependency>
<!-- jSON library used by Spring -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<!-- component used by Spring RestTemplate -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
<!-- Google Guava -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>16.0.1</version>
<scope>test</scope>
</dependency>
<!-- log library -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</dependency>
<!-- Spring Boot Test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<!-- plugins -->
<build>
<plugins>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.18.1</version>
</plugin>
</plugins>
</build>
<name>intro-client-webjson-01</name>
</project>
- linhas 14–18: o projeto Maven pai [spring-boot-starter-parent], que nos permite definir várias dependências sem especificar as suas versões, uma vez que estas estão definidas no projeto pai;
- linhas 22–25: embora não estejamos a escrever uma aplicação web, precisamos da dependência [spring-web], que inclui a classe [RestTemplate] que permite uma fácil interação com uma aplicação web/JSON;
- linhas 27–34: uma biblioteca JSON;
- linhas 36–39: uma dependência que nos permitirá definir um tempo limite para os pedidos HTTP do cliente. Um tempo limite é o tempo máximo de espera pela resposta do servidor. Após esse tempo, o cliente sinaliza um erro de tempo limite lançando uma exceção;
- linhas 41–46: a biblioteca Google Guava utilizada no teste JUnit. Por este motivo, definimos o seu âmbito como [test] (linha 45). Isto significa que esta dependência é incluída apenas ao executar código a partir do ramo [src/test/java];
- linhas 48–51: a biblioteca de registo;
- linhas 52–63: a dependência para os testes JUnit. Em particular, inclui a biblioteca JUnit 4 necessária para os testes. Estas dependências têm o atributo [<scope>test</scope>], indicando que só são necessárias para a fase de testes. Não são incluídas no arquivo final do projeto;
13.6.3. Implementação da camada [DAO]
![]() |
![]() |
- O pacote [spring.client.config] contém a configuração Spring para a camada [DAO];
- O pacote [spring.client.dao] contém a implementação da camada [DAO];
- O pacote [spring.client.entities] contém os objetos trocados com o serviço web / JSON;
13.6.3.1. Configuração
![]() |
A classe [DaoConfig] gere a configuração Spring da camada [DAO]. O seu código é o seguinte:
package spring.client.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;
@ComponentScan({ "spring.client.dao" })
public class DaoConfig {
// constants
static private final int TIMEOUT = 1000;
static private final String URL_WEBJSON = "http://localhost:8080";
@Bean
public RestTemplate restTemplate(int timeout) {
// creation of the RestTemplate component
HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
RestTemplate restTemplate = new RestTemplate(factory);
// exchange timeout
factory.setConnectTimeout(timeout);
factory.setReadTimeout(timeout);
// result
return restTemplate;
}
@Bean
public int timeout() {
return TIMEOUT;
}
@Bean
public String urlWebJson() {
return URL_WEBJSON;
}
// filters jSON
@Bean(name = "jsonMapper")
public ObjectMapper jsonMapper() {
return new ObjectMapper();
}
@Bean(name = "jsonMapperCategorieWithProduits")
public ObjectMapper jsonMapperCategorieWithProduits() {
// mapper jSON
ObjectMapper mapper = new ObjectMapper();
// filters
mapper.setFilters(
new SimpleFilterProvider().addFilter("jsonFilterCategorie", SimpleBeanPropertyFilter.serializeAllExcept())
.addFilter("jsonFilterProduit", SimpleBeanPropertyFilter.serializeAllExcept("categorie")));
// result
return mapper;
}
@Bean(name = "jsonMapperProduitWithCategorie")
public ObjectMapper jsonMapperProduitWithCategorie() {
// mapper jSON
ObjectMapper mapper = new ObjectMapper();
// filters
mapper.setFilters(
new SimpleFilterProvider().addFilter("jsonFilterProduit", SimpleBeanPropertyFilter.serializeAllExcept())
.addFilter("jsonFilterCategorie", SimpleBeanPropertyFilter.serializeAllExcept("produits")));
// result
return mapper;
}
@Bean(name = "jsonMapperCategorieWithoutProduits")
public ObjectMapper jsonMapperCategorieWithoutProduits() {
// mapper jSON
ObjectMapper mapper = new ObjectMapper();
// filters
mapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterCategorie",
SimpleBeanPropertyFilter.serializeAllExcept("produits")));
// result
return mapper;
}
@Bean(name = "jsonMapperProduitWithoutCategorie")
public ObjectMapper jsonMapperProduitWithoutCategorie() {
// mapper jSON
ObjectMapper mapper = new ObjectMapper();
// filters
mapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterProduit",
SimpleBeanPropertyFilter.serializeAllExcept("categorie")));
// result
return mapper;
}
}
- linha 13: a classe é uma classe de configuração Spring — os componentes Spring podem ser encontrados no pacote [spring.client.dao];
- linha 17: é definido um tempo limite de um segundo (1000 ms);
- linhas 32–35: o bean que devolve este valor;
- linha 18: a URL do serviço web / JSON;
- linhas 37–40: o bean que retorna este valor;
- linhas 20–30: a configuração da classe [RestTemplate] que lida com a comunicação com o serviço web / JSON. Quando não é necessária nenhuma configuração, pode ser instanciada no código com um simples [new RestTemplate()]. Aqui, queremos definir o tempo de espera para a comunicação com o serviço web / JSON. O bean [timeout] na linha 36 é passado como parâmetro para o método [RestTemplate] na linha 24;
- linha 23: o componente [HttpComponentsClientHttpRequestFactory] é aquele que nos permite definir o tempo limite para as comunicações (linhas 29–30);
- linha 24: a classe [RestTemplate] é construída utilizando este componente. Uma vez que depende deste componente para comunicar com o serviço web / JSON, as trocas estarão, de facto, sujeitas ao tempo limite;
- O cliente e o servidor trocarão linhas de texto. Um conversor lida com a serialização de um objeto em texto e, inversamente, com a desserialização de texto num objeto. Podem existir vários conversores associados à classe [RestTemplate], e o escolhido em qualquer momento depende dos cabeçalhos HTTP enviados pelo servidor. Aqui, não teremos nenhum conversor. Portanto, o componente [RestTemplate] não tentará converter os dois elementos seguintes de forma alguma:
- o texto enviado;
- o texto recebido em resposta;
Estes textos serão cadeias JSON, que serão, portanto, deixadas tal como estão pelo componente [RestTemplate]. Somos nós, os programadores, que iremos realizar a serialização e deserialização JSON necessárias. Isto porque os filtros a aplicar ao valor enviado e à resposta recebida podem diferir, e a experiência mostra que é mais fácil lidar com eles por conta própria do que tentar configurar o componente [RestTemplate] para utilizar o conversor JSON correto;
- linhas 42–92: definem filtros JSON. Estes são os mesmos que os filtros do lado do servidor apresentados e explicados na Secção 13.5.3.1;
- linhas 43–46: um mapeador JSON sem filtros;
- linhas 64–68: um mapeador JSON para recuperar uma categoria sem os seus produtos;
- linhas 48–58: um mapeador JSON para recuperar uma categoria com os seus produtos;
- linhas 83–92: um mapeador JSON para recuperar um produto sem a sua categoria;
- linhas 60-70: um mapeador JSON para recuperar um produto com a sua categoria;
Todos estes beans estarão disponíveis para o código da camada [DAO], bem como para o teste JUnit.
13.6.3.2. As Entidades
![]() |
As entidades tratadas pela camada [DAO] são aquelas que esta troca com o serviço web / JSON. Estas são os itens e os produtos. No lado do servidor, estas entidades tinham anotações de persistência JPA. Aqui, essas anotações foram removidas. Estamos a incluir novamente o código da entidade para referência:
[AbstractEntity]
package spring.client.entities;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
public abstract class AbstractEntity {
// properties
protected Long id;
protected Long version;
// manufacturers
public AbstractEntity() {
}
public AbstractEntity(Long id, Long version) {
this.id = id;
this.version = version;
}
// redefine [equals] and [hashcode]
@Override
public int hashCode() {
return (id != null ? id.hashCode() : 0);
}
@Override
public boolean equals(Object entity) {
if (!(entity instanceof AbstractEntity)) {
return false;
}
String class1 = this.getClass().getName();
String class2 = entity.getClass().getName();
if (!class2.equals(class1)) {
return false;
}
AbstractEntity other = (AbstractEntity) entity;
return id != null && this.id == other.id.longValue();
}
// signature jSON
public String toString() {
ObjectMapper mapper = new ObjectMapper();
try {
return mapper.writeValueAsString(this);
} catch (JsonProcessingException e) {
e.printStackTrace();
return null;
}
}
// getters and setters
...
}
[Categoria]
package spring.client.entities;
import java.util.HashSet;
import java.util.Set;
import com.fasterxml.jackson.annotation.JsonFilter;
@JsonFilter("jsonFilterCategorie")
public class Categorie extends AbstractEntity {
// properties
private String nom;
// related products
public Set<Produit> produits = new HashSet<Produit>();
// manufacturers
public Categorie() {
}
public Categorie(String nom) {
this.nom = nom;
}
// methods
public void addProduit(Produit produit) {
// we add the product
produits.add(produit);
// set your category
produit.setCategorie(this);
}
// getters and setters
...
}
[Produto]
package spring.webjson.client.entities;
import com.fasterxml.jackson.annotation.JsonFilter;
@JsonFilter("jsonFilterProduit")
public class Produit extends AbstractEntity {
// the name
private String nom;
// category number
private Long idCategorie;
// the price
private double prix;
// the description
private String description;
// the category
private Categorie categorie;
// manufacturers
public Produit() {
}
public Produit(String nom, double prix, String description) {
this.nom = nom;
this.prix = prix;
this.description = description;
}
// getters and setters
...
}
13.6.3.3. A classe [DaoException]
![]() |
Quando a camada [DAO] encontra um erro, lança uma [DaoException]. Esta classe é utilizada no lado do servidor e é descrita na secção 11.3.7.
13.6.3.4. A interface da camada [DAO]
![]() |
A camada [DAO] implementa a interface [IDao] descrita na secção 11.3.7.
package spring.client.dao;
import java.util.List;
import spring.client.entities.Categorie;
import spring.client.entities.Produit;
public interface IDao {
// insert product list
public List<Produit> addProduits(List<Produit> produits);
// removal of all products
public void deleteAllProduits();
// product list update
public List<Produit> updateProduits(List<Produit> produits);
// all products obtained
public List<Produit> getAllProduits();
// inserting a list of categories
public List<Categorie> addCategories(List<Categorie> categories);
// delete all categories
public void deleteAllCategories();
// updating a list of categories
public List<Categorie> updateCategories(List<Categorie> categories);
// obtaining all categories
public List<Categorie> getAllCategories();
// a special product
public Produit getProduitByIdWithCategorie(Long idProduit);
public Produit getProduitByIdWithoutCategorie(Long idProduit);
public Produit getProduitByNameWithCategorie(String nom);
public Produit getProduitByNameWithoutCategorie(String nom);
// a special category
public Categorie getCategorieByIdWithProduits(Long idCategorie);
public Categorie getCategorieByIdWithoutProduits(Long idCategorie);
public Categorie getCategorieByNameWithProduits(String nom);
public Categorie getCategorieByNameWithoutProduits(String nom);
}
13.6.3.5. O serviço web / resposta JSON
![]() |
Vimos que todos os URLs do serviço web / JSON devolvem um tipo [Response] definido na secção 13.5.5.3. Reproduzimos esta classe aqui:
package spring.client.dao;
import java.util.List;
public class Response<T> {
// ----------------- properties
// operation status
private int status;
// any error messages
private List<String> messages;
// the body of the reply
private T body;
// manufacturers
public Response() {
}
public Response(int status, List<String> messages, T body) {
this.status = status;
this.messages = messages;
this.body = body;
}
// getters and setters
...
}
13.6.3.6. Implementação da comunicação com o serviço web / JSON
![]() |
A classe [ AbstractDao] implementa a comunicação com o serviço web / JSON:
package spring.client.dao;
import java.net.URI;
import java.net.URISyntaxException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.MediaType;
import org.springframework.http.RequestEntity;
import org.springframework.web.client.RestTemplate;
public abstract class AbstractDao {
// data
@Autowired
protected RestTemplate restTemplate;
@Autowired
protected String urlServiceWebJson;
// generic request
protected String getResponse(String url, String jsonPost) {
// url : URL to contact
// jsonPost: the jSON value to be posted
try {
// request execution
RequestEntity<?> request;
if (jsonPost != null) {
// query POST
request = RequestEntity.post(new URI(String.format("%s%s", urlServiceWebJson, url)))
.header("Content-Type", "application/json").accept(MediaType.APPLICATION_JSON).body(jsonPost);
} else {
// query GET
request = RequestEntity.get(new URI(String.format("%s%s", urlServiceWebJson, url)))
.accept(MediaType.APPLICATION_JSON).build();
}
// execute the query
return restTemplate.exchange(request, new ParameterizedTypeReference<String>() {
}).getBody();
} catch (URISyntaxException e1) {
throw new DaoException(20, e1);
} catch (RuntimeException e2) {
throw new DaoException(21, e2);
}
}
}
- linhas 15-16: injeção do componente [RestTemplate], que lida com a comunicação com o servidor;
- linhas 17-18: injeção da URL do serviço web / JSON;
A implementação dos métodos para comunicação com o servidor está incorporada no método [getResponse]:
- linha 21: o método recebe 2 parâmetros:
- [url]: a URL solicitada;
- [jsonPost]: a string JSON a enviar, ou nulo caso contrário. Se [jsonPost == null], o pedido de URL é feito utilizando um GET; caso contrário, utilizando um POST;
- linha 38: a instrução que envia a solicitação ao servidor e recebe a sua resposta. O componente [RestTemplate] oferece uma ampla gama de métodos para interagir com o servidor. Escolhemos aqui o método [exchange], mas existem outros disponíveis;
- linhas 27–36: precisamos de construir a solicitação [RequestEntity]. Ela difere dependendo se usamos uma solicitação GET ou POST;
- linhas 30–31: a solicitação para um GET. A classe [RequestEntity] fornece métodos estáticos para criar solicitações GET, POST, HEAD e outras. O método [RequestEntity.get] permite criar uma solicitação GET encadeando os vários métodos que a constroem:
- o método [RequestEntity.get] recebe a URL de destino como parâmetro na forma de uma instância URI,
- o método [accept] permite definir os elementos do cabeçalho HTTP [Accept]. Aqui, especificamos que aceitamos o tipo [application/json] que o servidor irá enviar;
- o método [build] utiliza esta informação para construir o tipo [RequestEntity] da solicitação;
- linhas 34–35: a solicitação POST. O método [RequestEntity.post] cria uma solicitação POST encadeando os vários métodos que a constroem:
- o método [RequestEntity.post] recebe a URL de destino como parâmetro na forma de uma instância URI,
- O método [header] define um cabeçalho HTTP. Aqui, enviamos o cabeçalho [Content-Type: application/json] ao servidor para indicar que os dados enviados chegarão na forma de uma cadeia JSON;
- o método [accept] permite-nos indicar que aceitamos o tipo [application/json] que o servidor irá enviar;
- o método [body] define o valor enviado. Este é o quarto parâmetro do método genérico [getResponse] (linha 1);
- linha 38: o método [RestTemplate].exchange devolve um tipo [ResponseEntity<String>] que encapsula toda a resposta do servidor: cabeçalhos HTTP e corpo do documento. O método [ResponseEntity].getBody() recupera este corpo, que representa a resposta do servidor — neste caso, uma string;
13.6.3.7. Implementação da interface [IDao]
![]() |
A classe [Dao] implementa a interface [IDao]:
package spring.client.dao;
import java.io.IOException;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import spring.client.entities.Categorie;
import spring.client.entities.Produit;
@Component
public class Dao extends AbstractDao implements IDao {
@Autowired
private ApplicationContext context;
// filters jSON
@Autowired
@Qualifier("jsonMapper")
private ObjectMapper jsonMapper;
@Autowired
@Qualifier("jsonMapperCategorieWithProduits")
private ObjectMapper jsonMapperCategorieWithProduits;
@Autowired
@Qualifier("jsonMapperProduitWithCategorie")
private ObjectMapper jsonMapperProduitWithCategorie;
@Autowired
@Qualifier("jsonMapperCategorieWithoutProduits")
private ObjectMapper jsonMapperCategorieWithoutProduits;
@Autowired
@Qualifier("jsonMapperProduitWithoutCategorie")
private ObjectMapper jsonMapperProduitWithoutCategorie;
@Override
public List<Produit> addProduits(List<Produit> produits) {
// ----------- add products (without category)
...
}
- linha 17: a classe [Dao] é um componente Spring no qual outros componentes Spring podem ser injetados;
- linha 18: a classe [Dao] estende a classe [AbstractDao] que acabámos de ver e implementa a interface [IDao];
- linhas 20–21: injetamos o contexto Spring para aceder aos seus beans;
- linhas 24–38: injeção dos mapeadores JSON definidos na classe [AppConfig] apresentada na secção 13.6.2;
As implementações dos vários métodos da interface [IDao] seguem todas o mesmo padrão. Apresentaremos dois métodos, um baseado numa operação [POST] e outro numa operação [GET].
Um exemplo de [GET]: [getCategorieByNameWithProduits]
@Override
public Categorie getCategorieByNameWithProduits(String nom) {
// ----------- obtain a category designated by its name, with its products
try {
// request
Response<Categorie> response = jsonMapperCategorieWithProduits.readValue(
getResponse(String.format("/getCategorieByNameWithProduits/%s", nom), null),
new TypeReference<Response<Categorie>>() {
});
// mistake?
if (response.getStatus() != 0) {
// 1 exception is thrown
throw new DaoException(response.getStatus(), response.getMessages());
} else {
// render the core of the server response
return response.getBody();
}
} catch (DaoException e1) {
throw e1;
} catch (RuntimeException | IOException e2) {
throw new DaoException(113, e2);
}
}
- Linha 7: O método [getResponse] da classe pai é chamado. Este método trata da comunicação com o serviço web/JSON. Os seus parâmetros são os seguintes:
getResponse(String.format("/getCategorieByNameWithProduits/%s", nom), null)
- (continuação)
- a URL do serviço que está a ser consultado [/getCategoryByNameWithProducts/name];
- o valor enviado. Aqui, não há nenhum;
O método [getResponse] devolve uma String que representa a resposta JSON enviada pelo servidor. Deserializamos esta resposta JSON da seguinte forma:
jsonMapperCategorieWithProduits.readValue(
jsonResponse,
new TypeReference<Response<Categorie>>() {
});
porque a cadeia JSON é a serialização de um tipo [Response<Category>];
- linhas 11–17: verificamos o estado da resposta. Se o estado não for 0, então ocorreu um erro do lado do servidor. Em seguida, lançamos uma exceção (linha 13), utilizando as informações contidas na resposta (estado e lista de mensagens de erro);
- linha 16: se não houve erro do lado do servidor, devolvemos o corpo do tipo [Response<Category>], ou seja, a categoria solicitada;
- linhas 18–19: tratamos a exceção lançada na linha 16;
- linhas 20–22: tratam todas as outras exceções;
Um exemplo de [POST]: [addCategories]
@Override
public List<Categorie> addCategories(List<Categorie> categories) {
// ----------- add categories (with their products)
try {
// request
Response<List<Categorie>> response = jsonMapperCategorieWithProduits.readValue(
getResponse("/addCategories", jsonMapperCategorieWithProduits.writeValueAsString(categories)),
new TypeReference<Response<List<Categorie>>>() {
});
// mistake?
if (response.getStatus() != 0) {
// 1 exception is thrown
throw new DaoException(response.getStatus(), response.getMessages());
} else {
// render the core of the server response
return response.getBody();
}
} catch (DaoException e1) {
throw e1;
} catch (RuntimeException | IOException e2) {
throw new DaoException(104, e2);
}
}
- linha 2: o método [addCategories] é utilizado para persistir as categorias passadas como parâmetros na base de dados. Este método devolve essas mesmas categorias, enriquecidas com as suas chaves primárias. Se as categorias forem passadas juntamente com produtos, estes também são persistidos;
- linha 7: o método [getResponse] do pai é chamado para lidar com a comunicação com o serviço web / JSON;
- o primeiro parâmetro é a URL [/addCategories];
- o segundo parâmetro é o valor enviado, neste caso a lista de categorias a serem guardadas;
getResponse("/addCategories", jsonMapperCategorieWithProduits.writeValueAsString(categories))
A cadeia JSON resultante é então deserializada para obter o tipo [Response<List<Category>>] esperado:
Response<List<Categorie>> response = jsonMapperCategorieWithProduits.readValue(
jsonResponse,
new TypeReference<Response<List<Categorie>>>() {
});
- linhas 11–17: tratamento da resposta do servidor (erro ou não);
- linhas 20–22: tratamento de exceções;
Todos os outros métodos seguem o padrão dos dois métodos apresentados.
13.6.4. O teste JUnit
Voltemos à arquitetura cliente/servidor atualmente em desenvolvimento:
![]() |
Criámos uma camada [DAO] [2] com a mesma interface que a camada [DAO] [4]. Para testar a camada [DAO] [2], podemos, portanto, utilizar o teste JUnit que foi utilizado para testar a camada [DAO] [4]. A título de recordação, é o seguinte:
![]() |
package spring.client.junit;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.Lists;
import spring.client.config.DaoConfig;
import spring.client.dao.DaoException;
import spring.client.dao.IDao;
import spring.client.entities.Categorie;
import spring.client.entities.Produit;
@SpringApplicationConfiguration(classes = DaoConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class Test01 {
// layer [DAO]
@Autowired
private IDao dao;
// filters jSON
@Autowired
@Qualifier("jsonMapper")
private ObjectMapper jsonMapper;
@Autowired
@Qualifier("jsonMapperCategorieWithProduits")
private ObjectMapper jsonMapperCategorieWithProduits;
@Autowired
@Qualifier("jsonMapperProduitWithCategorie")
private ObjectMapper jsonMapperProduitWithCategorie;
@Autowired
@Qualifier("jsonMapperCategorieWithoutProduits")
private ObjectMapper jsonMapperCategorieWithoutProduits;
@Autowired
@Qualifier("jsonMapperProduitWithoutCategorie")
private ObjectMapper jsonMapperProduitWithoutCategorie;
@Before
public void cleanAndFill() {
// the base is cleaned before each test
log("Vidage de la base de données", 1);
// table [CATEGORIES] is emptied - by cascade, table [PRODUITS] will be emptied
dao.deleteAllCategories();
// --------------------------------------------------------------------------------------
log("Remplissage de la base", 1);
// fill the tables
List<Categorie> categories = new ArrayList<Categorie>();
for (int i = 0; i < 2; i++) {
Categorie categorie = new Categorie(String.format("categorie%d", i));
for (int j = 0; j < 5; j++) {
categorie.addProduit(new Produit(String.format("produit%d%d", i, j), 100 * (1 + (double) (i * 10 + j) / 100),
String.format("desc%d%d", i, j)));
}
categories.add(categorie);
}
// add the category - the products will be cascaded in as well
categories = dao.addCategories(categories);
}
@Test
public void showDataBase() throws BeansException, JsonProcessingException {
// list of categories
log("Liste des catégories", 2);
List<Categorie> categories = dao.getAllCategories();
affiche(categories, jsonMapperCategorieWithoutProduits);
// product list
log("Liste des produits", 2);
List<Produit> produits = dao.getAllProduits();
affiche(produits, jsonMapperProduitWithoutCategorie);
// a few checks
Assert.assertEquals(2, categories.size());
Assert.assertEquals(10, produits.size());
Categorie categorie = findCategorieByName("categorie0", categories);
Assert.assertNotNull(categorie);
Produit produit = findProduitByName("produit03", produits);
Assert.assertNotNull(produit);
Long idCategorie = produit.getIdCategorie();
Assert.assertEquals(categorie.getId(), idCategorie);
}
@Test
public void getCategorieByNameWithProduits() {
log("getCategorieByNameWithProduits", 1);
Categorie categorie1 = dao.getCategorieByNameWithProduits("categorie1");
Assert.assertNotNull(categorie1);
Assert.assertEquals(5, categorie1.getProduits().size());
}
@Test
public void getCategorieByNameWithoutProduits() {
log("getCategorieByNameWithoutProduits", 1);
Categorie categorie1 = dao.getCategorieByNameWithoutProduits("categorie1");
Assert.assertNotNull(categorie1);
Assert.assertEquals("categorie1", categorie1.getNom());
}
@Test
public void getCategorieByIdWithProduits() {
log("getCategorieByIdWithProduits", 1);
Categorie categorie1 = dao.getCategorieByNameWithProduits("categorie1");
Categorie categorie2 = dao.getCategorieByIdWithProduits(categorie1.getId());
Assert.assertNotNull(categorie2);
Assert.assertEquals(categorie1.getId(), categorie2.getId());
Assert.assertEquals(categorie1.getNom(), categorie2.getNom());
}
@Test
public void getCategorieByIdWithoutProduits() {
log("getCategorieByIdWithoutProduits", 1);
Categorie categorie1 = dao.getCategorieByNameWithProduits("categorie1");
Categorie categorie2 = dao.getCategorieByIdWithoutProduits(categorie1.getId());
Assert.assertNotNull(categorie2);
Assert.assertEquals(categorie1.getNom(), categorie2.getNom());
}
@Test
public void getProduitByNameWithCategorie() {
log("getProduitByNameWithCategorie", 1);
Produit produit = dao.getProduitByNameWithCategorie("produit03");
Assert.assertNotNull(produit);
Assert.assertNotNull(produit.getCategorie());
}
@Test
public void getProduitByNameWithoutCategorie() {
log("getProduitByNameWithoutCategorie", 1);
Produit produit = dao.getProduitByNameWithoutCategorie("produit03");
Assert.assertNotNull(produit);
Assert.assertEquals("produit03", produit.getNom());
}
@Test
public void getProduitByIdWithCategorie() {
log("getProduitByNameWithCategorie", 1);
Produit produit = dao.getProduitByNameWithCategorie("produit03");
Produit produit2 = dao.getProduitByIdWithCategorie(produit.getId());
Assert.assertNotNull(produit2);
Assert.assertEquals(produit2.getNom(), produit.getNom());
Assert.assertEquals(produit2.getId(), produit.getId());
Assert.assertEquals(produit.getCategorie().getId(), produit2.getCategorie().getId());
}
@Test
public void getProduitByIdWithoutCategorie() {
log("getProduitByIdWithoutCategorie", 1);
Produit produit = dao.getProduitByNameWithCategorie("produit03");
Produit produit2 = dao.getProduitByIdWithoutCategorie(produit.getId());
Assert.assertNotNull(produit2);
Assert.assertEquals(produit2.getNom(), produit.getNom());
Assert.assertEquals(produit2.getId(), produit.getId());
}
@Test
public void doInsertsInTransaction() {
log("Ajout d'une catégorie [cat1] avec deux produits de même nom", 1);
// we insert
Categorie categorie = new Categorie("cat1");
categorie.addProduit(new Produit("x", 1.0, ""));
categorie.addProduit(new Produit("x", 1.0, ""));
// add the category - the products will be cascaded in as well
try {
categorie = dao.addCategories(Lists.newArrayList(categorie)).get(0);
} catch (DaoException e) {
show("Les erreurs suivantes se sont produites :", e.getErreurs());
}
// checks
List<Categorie> categories = dao.getAllCategories();
Assert.assertEquals(2, categories.size());
List<Produit> produits = dao.getAllProduits();
Assert.assertEquals(10, produits.size());
}
@Test
public void updateDataBase() {
log("Mise à jour du prix des produits de [categorie1]", 1);
Categorie categorie1 = dao.getCategorieByNameWithProduits("categorie1");
Categorie categorie1Saved = dao.getCategorieByNameWithProduits("categorie1");
Set<Produit> produits = categorie1.getProduits();
for (Produit produit : produits) {
produit.setPrix(1.1 * produit.getPrix());
}
List<Produit> produits2 = Lists.newArrayList(produits);
produits2 = dao.updateProduits(produits2);
// checks
List<Produit> produitsSaved = Lists.newArrayList(categorie1Saved.getProduits());
for (Produit produit2 : produits2) {
Produit produit = findProduitByName(produit2.getNom(), produitsSaved);
Assert.assertEquals(produit2.getPrix(), produit.getPrix() * 1.1, 1e-6);
}
}
@Test
public void addProduits() throws BeansException, JsonProcessingException {
log("Ajout de deux produits de catégorie [categorie0]", 1);
Categorie categorie0 = dao.getCategorieByNameWithoutProduits("categorie0");
Long idCategorie = categorie0.getId();
Produit p1 = new Produit("x", 1, "");
p1.setIdCategorie(idCategorie);
p1.setCategorie(categorie0);
Produit p2 = new Produit("y", 1, "");
p2.setIdCategorie(idCategorie);
p2.setCategorie(categorie0);
List<Produit> produits = new ArrayList<Produit>();
produits.add(p1);
produits.add(p2);
produits = dao.addProduits(produits);
// check
affiche(produits, jsonMapperProduitWithoutCategorie);
}
// -------------- private methods
private Produit findProduitByName(String nom, List<Produit> produits) {
for (Produit produit : produits) {
if (produit.getNom().equals(nom)) {
return produit;
}
}
return null;
}
private Categorie findCategorieByName(String nom, List<Categorie> categories) {
for (Categorie categorie : categories) {
if (categorie.getNom().equals(nom)) {
return categorie;
}
}
return null;
}
// display of a T-type element
static private <T> void affiche(T element, ObjectMapper jsonMapper) throws JsonProcessingException {
System.out.println(jsonMapper.writeValueAsString(element));
}
// display a list of elements of type T
static private <T> void affiche(List<T> elements, ObjectMapper jsonMapper) throws JsonProcessingException {
for (T element : elements) {
affiche(element, jsonMapper);
}
}
private static void log(String message, int mode) {
// poster message
String toPrint = null;
switch (mode) {
case 1:
toPrint = String.format("%s --------------------------------", message);
break;
case 2:
toPrint = String.format("-- %s", message);
break;
}
System.out.println(toPrint);
}
private static void show(String title, List<String> messages) {
// title
System.out.println(String.format("%s : ", title));
// messages
for (String message : messages) {
System.out.println(String.format("- %s", message));
}
}
}
A execução é bem-sucedida e produz os seguintes resultados na consola:
Vidage de la base de données --------------------------------
Remplissage de la base --------------------------------
Ajout de deux produits de catégorie [categorie0] --------------------------------
{"id":6285,"version":0,"nom":"x","idCategorie":1319,"prix":1.0,"description":""}
{"id":6286,"version":0,"nom":"y","idCategorie":1319,"prix":1.0,"description":""}
Vidage de la base de données --------------------------------
Remplissage de la base --------------------------------
Mise à jour du prix des produits de [categorie1] --------------------------------
Vidage de la base de données --------------------------------
Remplissage de la base --------------------------------
getCategorieByIdWithoutProduits --------------------------------
Vidage de la base de données --------------------------------
Remplissage de la base --------------------------------
getProduitByNameWithoutCategorie --------------------------------
Vidage de la base de données --------------------------------
Remplissage de la base --------------------------------
getCategorieByNameWithProduits --------------------------------
Vidage de la base de données --------------------------------
Remplissage de la base --------------------------------
getCategorieByNameWithoutProduits --------------------------------
Vidage de la base de données --------------------------------
Remplissage de la base --------------------------------
getProduitByNameWithCategorie --------------------------------
Vidage de la base de données --------------------------------
Remplissage de la base --------------------------------
getProduitByNameWithCategorie --------------------------------
Vidage de la base de données --------------------------------
Remplissage de la base --------------------------------
getProduitByIdWithoutCategorie --------------------------------
Vidage de la base de données --------------------------------
Remplissage de la base --------------------------------
-- Liste des catégories
{"id":1337,"version":0,"nom":"categorie0"}
{"id":1338,"version":0,"nom":"categorie1"}
-- Liste des produits
{"id":6367,"version":0,"nom":"produit00","idCategorie":1337,"prix":100.0,"description":"desc00"}
{"id":6368,"version":0,"nom":"produit01","idCategorie":1337,"prix":101.0,"description":"desc01"}
{"id":6369,"version":0,"nom":"produit02","idCategorie":1337,"prix":102.0,"description":"desc02"}
{"id":6370,"version":0,"nom":"produit03","idCategorie":1337,"prix":103.0,"description":"desc03"}
{"id":6371,"version":0,"nom":"produit04","idCategorie":1337,"prix":104.0,"description":"desc04"}
{"id":6372,"version":0,"nom":"produit10","idCategorie":1338,"prix":110.0,"description":"desc10"}
{"id":6373,"version":0,"nom":"produit11","idCategorie":1338,"prix":111.0,"description":"desc11"}
{"id":6374,"version":0,"nom":"produit12","idCategorie":1338,"prix":112.0,"description":"desc12"}
{"id":6375,"version":0,"nom":"produit13","idCategorie":1338,"prix":113.0,"description":"desc13"}
{"id":6376,"version":0,"nom":"produit14","idCategorie":1338,"prix":114.0,"description":"desc14"}
Vidage de la base de données --------------------------------
Remplissage de la base --------------------------------
getCategorieByIdWithProduits --------------------------------
Vidage de la base de données --------------------------------
Remplissage de la base --------------------------------
Ajout d'une catégorie [cat1] avec deux produits de même nom --------------------------------
Les erreurs suivantes se sont produites :
- org.hibernate.exception.ConstraintViolationException: could not execute statement
- could not execute statement
- Duplicate entry 'x' for key 'NOM'
11:24:37.650 [Thread-1] INFO o.s.c.a.AnnotationConfigApplicationContext - Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@f8c1ddd: startup date [Fri Nov 20 11:24:34 CET 2015]; root of context hierarchy



























































