Skip to content

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:

  1. 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.
  2. 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];
  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:

{"id":2,"content":"Hello, World!"}

ou

{"id":2,"content":"Hello, John!"}

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

Vamos executar o projeto:

 

Recebemos os seguintes registos da consola:

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v1.1.9.RELEASE)

2014-11-28 15:22:55.005  INFO 3152 --- [           main] hello.Application                        : Starting Application on Gportpers3 with PID 3152 (started by ST in D:\data\istia-1415\spring mvc\dvp-final\gs-rest-service)
2014-11-28 15:22:55.046  INFO 3152 --- [           main] ationConfigEmbeddedWebApplicationContext : Refreshing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@62e136d3: startup date [Fri Nov 28 15:22:55 CET 2014]; root of context hierarchy
2014-11-28 15:22:55.762  INFO 3152 --- [           main] o.s.b.f.s.DefaultListableBeanFactory     : Overriding bean definition for bean 'beanNameViewResolver': replacing [Root bean: class [null]; scope=; abstract=false; lazyInit=false; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=org.springframework.boot.autoconfigure.web.ErrorMvcAutoConfiguration$WhitelabelErrorViewConfiguration; factoryMethodName=beanNameViewResolver; initMethodName=null; destroyMethodName=(inferred); defined in class path resource [org/springframework/boot/autoconfigure/web/ErrorMvcAutoConfiguration$WhitelabelErrorViewConfiguration.class]] with [Root bean: class [null]; scope=; abstract=false; lazyInit=false; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration$WebMvcAutoConfigurationAdapter; factoryMethodName=beanNameViewResolver; initMethodName=null; destroyMethodName=(inferred); defined in class path resource [org/springframework/boot/autoconfigure/web/WebMvcAutoConfiguration$WebMvcAutoConfigurationAdapter.class]]
2014-11-28 15:22:56.567  INFO 3152 --- [           main] .t.TomcatEmbeddedServletContainerFactory : Server initialized with port: 8080
2014-11-28 15:22:56.738  INFO 3152 --- [           main] o.apache.catalina.core.StandardService   : Starting service Tomcat
2014-11-28 15:22:56.740  INFO 3152 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet Engine: Apache Tomcat/7.0.56
2014-11-28 15:22:56.869  INFO 3152 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2014-11-28 15:22:56.870  INFO 3152 --- [ost-startStop-1] o.s.web.context.ContextLoader            : Root WebApplicationContext: initialization completed in 1827 ms
2014-11-28 15:22:57.478  INFO 3152 --- [ost-startStop-1] o.s.b.c.e.ServletRegistrationBean        : Mapping servlet: 'dispatcherServlet' to [/]
2014-11-28 15:22:57.481  INFO 3152 --- [ost-startStop-1] o.s.b.c.embedded.FilterRegistrationBean  : Mapping filter: 'hiddenHttpMethodFilter' to: [/*]
2014-11-28 15:22:57.685  INFO 3152 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**/favicon.ico] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2014-11-28 15:22:57.879  INFO 3152 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/greeting],methods=[],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public hello.Greeting hello.GreetingController.greeting(java.lang.String)
2014-11-28 15:22:57.884  INFO 3152 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],methods=[],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.BasicErrorController.error(javax.servlet.http.HttpServletRequest)
2014-11-28 15:22:57.885  INFO 3152 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],methods=[],params=[],headers=[],consumes=[],produces=[text/html],custom=[]}" onto public org.springframework.web.servlet.ModelAndView org.springframework.boot.autoconfigure.web.BasicErrorController.errorHtml(javax.servlet.http.HttpServletRequest)
2014-11-28 15:22:57.906  INFO 3152 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/webjars/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2014-11-28 15:22:57.907  INFO 3152 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2014-11-28 15:22:58.231  INFO 3152 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Registering beans for JMX exposure on startup
2014-11-28 15:22:58.318  INFO 3152 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8080/http
2014-11-28 15:22:58.319  INFO 3152 --- [           main] hello.Application                        : Started Application in 3.788 seconds (JVM running for 4.424)
  • 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.

[INFO] --- spring-boot-maven-plugin:1.1.0.RELEASE:repackage (default) @ gs-rest-service ---

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:


    @RequestMapping(value = "/addProducts",
method = RequestMethod.POST,
content-type = "application/json; charset=UTF-8")
    public String addProducts(HttpServletRequest request) {
...
    }
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.

    @RequestMapping(value = "/deleteAllProducts",
method = RequestMethod.GET)
    public String deleteAllProducts() {
..
    }
Elimina todos os produtos da base de dados.

    @RequestMapping(value = "/updateProducts",
method = RequestMethod.POST,
content-type = "application/json; charset=UTF-8")
    public String updateProducts(HttpServletRequest request) {
..
    }

Atualiza os produtos na base de dados. Estas são enviadas. A resposta é uma cadeia JSON que contém a lista de produtos atualizados.

    @RequestMapping(value = "/getAllProducts",
method = RequestMethod.GET)
    public String getAllProducts() {
..
    }

Recupera a cadeia JSON para todos os produtos.

    @RequestMapping(value = "/addCategories",
method = RequestMethod.POST,
content-type = "application/json; charset=UTF-8")
    public String addCategories(HttpServletRequest request) {
..
    }

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.

    @RequestMapping(value = "/deleteAllCategories",
method = RequestMethod.GET)
    public String deleteAllCategories() {
...
    }

Elimina todas as categorias da base de dados, juntamente com todos os produtos nelas contidos. Depois disso, a base de dados fica vazia.

    @RequestMapping(value = "/updateCategories",
method = RequestMethod.POST,
content-type = "application/json; charset=UTF-8")
    public String updateCategories
(HttpServletRequest request) {
...
    }

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;

    @RequestMapping(value = "/getAllCategories",
method = RequestMethod.GET)
    public String getAllCategories() {
...
    }

Recupera a cadeia JSON para todas as categorias.

    @RequestMapping(value = "/getProductByIdWithCategory/{productId}",
method = RequestMethod.GET)
    public String getProductByIdWithCategory
(@PathVariable("productId") Long productId) {
...
    }

Recupera a cadeia JSON de um produto identificado pelo seu ID, juntamente com a sua categoria.

    @RequestMapping(value = "/getProductByIdWithoutCategory/{productId}",
method = RequestMethod.GET)
    public String getProductByIdWithoutCategory
@PathVariable("productId") Long productId) {
...
    }

Recupera a cadeia JSON de um produto identificado pelo seu ID, sem a sua categoria.

    @RequestMapping(value = "/getProductByNameWithCategory/{name}",
method = RequestMethod.GET)
    public String getProductByNameWithCategory(
@PathVariable("name") String name) {
...
    }

Recupera a string JSON de um produto identificado pelo seu nome, juntamente com a sua categoria.

    @RequestMapping(value = "/getProductByNameWithoutCategory/{name}",
method = RequestMethod.GET)
    public String getProductByNameWithoutCategory
(@PathVariable("name") String name) {
...
    }

Recupera a cadeia JSON de um produto identificado pelo seu nome, sem a sua categoria.

    @RequestMapping(value = "/getCategoryByIdWithProducts/{categoryId}",
 method = RequestMethod.GET)
    public String getCategoryByIdWithProducts
@PathVariable("idCategorie") Long idCategorie) {
...
    }

Recupera a cadeia JSON de uma categoria especificada pelo seu ID, juntamente com os seus produtos.

    @RequestMapping(value = "/getCategoryByNameWithProducts/{name}",
method = RequestMethod.GET)
    public String getCategoryByNameWithProducts
(@PathVariable("name") String name) {
...
    }

Recupera a cadeia JSON de uma categoria especificada pelo seu nome, juntamente com os seus produtos.

    @RequestMapping(value = "/getCategoryByNameWithoutProducts/{name}",
method = RequestMethod.GET)
    public String getCategoryByNameWithoutProducts(
@PathVariable("name") String name) {
...
    }

Recupera a cadeia JSON de uma categoria especificada pelo seu nome, sem os seus produtos.

    @RequestMapping(value = "/getCategoryByIdWithoutProducts/{categoryId}",
método = RequestMethod.GET)
    public String getCategoryByIdWithoutProducts(
@PathVariable("idCategorie") Long idCategorie) {
...
    }

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:

.   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v1.2.2.RELEASE)

2015-03-24 16:22:46.608  INFO 9492 --- [           main] spring.webjson.server.boot.Boot          : Starting Boot on Gportpers3 with PID 9492 (D:\data\istia-1415\eclipse\intro-web-json\intro-webjson-server-02\target\classes started by ST in D:\data\istia-1415\eclipse\intro-web-json\intro-webjson-server-02)
2015-03-24 16:22:46.654  INFO 9492 --- [           main] ationConfigEmbeddedWebApplicationContext : Refreshing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@1d7acb34: startup date [Tue Mar 24 16:22:46 CET 2015]; root of context hierarchy
2015-03-24 16:22:47.521  INFO 9492 --- [           main] o.s.b.f.s.DefaultListableBeanFactory     : Overriding bean definition for bean 'beanNameViewResolver': replacing [Root bean: class [null]; scope=; abstract=false; lazyInit=false; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=org.springframework.boot.autoconfigure.web.ErrorMvcAutoConfiguration$WhitelabelErrorViewConfiguration; factoryMethodName=beanNameViewResolver; initMethodName=null; destroyMethodName=(inferred); defined in class path resource [org/springframework/boot/autoconfigure/web/ErrorMvcAutoConfiguration$WhitelabelErrorViewConfiguration.class]] with [Root bean: class [null]; scope=; abstract=false; lazyInit=false; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration$WebMvcAutoConfigurationAdapter; factoryMethodName=beanNameViewResolver; initMethodName=null; destroyMethodName=(inferred); defined in class path resource [org/springframework/boot/autoconfigure/web/WebMvcAutoConfiguration$WebMvcAutoConfigurationAdapter.class]]
2015-03-24 16:22:47.569  INFO 9492 --- [           main] o.s.b.f.s.DefaultListableBeanFactory     : Overriding bean definition for bean 'entityManagerFactory': replacing [Root bean: class [null]; scope=; abstract=false; lazyInit=false; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=spring.data.config.DaoConfig; factoryMethodName=entityManagerFactory; initMethodName=null; destroyMethodName=(inferred); defined in class spring.data.config.DaoConfig] with [Root bean: class [null]; scope=; abstract=false; lazyInit=false; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=true; factoryBeanName=org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration; factoryMethodName=entityManagerFactory; initMethodName=null; destroyMethodName=(inferred); defined in class path resource [org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaAutoConfiguration.class]]
2015-03-24 16:22:48.137  INFO 9492 --- [           main] trationDelegate$BeanPostProcessorChecker : Bean 'org.springframework.transaction.annotation.ProxyTransactionManagementConfiguration' of type [class org.springframework.transaction.annotation.ProxyTransactionManagementConfiguration$$EnhancerBySpringCGLIB$$405db6ba] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2015-03-24 16:22:48.162  INFO 9492 --- [           main] trationDelegate$BeanPostProcessorChecker : Bean 'transactionAttributeSource' of type [class org.springframework.transaction.annotation.AnnotationTransactionAttributeSource] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2015-03-24 16:22:48.172  INFO 9492 --- [           main] trationDelegate$BeanPostProcessorChecker : Bean 'transactionInterceptor' of type [class org.springframework.transaction.interceptor.TransactionInterceptor] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2015-03-24 16:22:48.178  INFO 9492 --- [           main] trationDelegate$BeanPostProcessorChecker : Bean 'org.springframework.transaction.config.internalTransactionAdvisor' of type [class org.springframework.transaction.interceptor.BeanFactoryTransactionAttributeSourceAdvisor] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2015-03-24 16:22:48.586  INFO 9492 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat initialized with port(s): 8080 (http)
2015-03-24 16:22:48.850  INFO 9492 --- [           main] o.apache.catalina.core.StandardService   : Starting service Tomcat
2015-03-24 16:22:48.852  INFO 9492 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet Engine: Apache Tomcat/8.0.20
2015-03-24 16:22:48.992  INFO 9492 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2015-03-24 16:22:48.992  INFO 9492 --- [ost-startStop-1] o.s.web.context.ContextLoader            : Root WebApplicationContext: initialization completed in 2342 ms
2015-03-24 16:22:49.645  INFO 9492 --- [ost-startStop-1] o.s.b.c.e.ServletRegistrationBean        : Mapping servlet: 'dispatcherServlet' to [/]
2015-03-24 16:22:49.650  INFO 9492 --- [ost-startStop-1] o.s.b.c.embedded.FilterRegistrationBean  : Mapping filter: 'characterEncodingFilter' to: [/*]
2015-03-24 16:22:49.651  INFO 9492 --- [ost-startStop-1] o.s.b.c.embedded.FilterRegistrationBean  : Mapping filter: 'hiddenHttpMethodFilter' to: [/*]
2015-03-24 16:22:50.380  INFO 9492 --- [           main] j.LocalContainerEntityManagerFactoryBean : Building JPA container EntityManagerFactory for persistence unit 'default'
2015-03-24 16:22:50.392  INFO 9492 --- [           main] o.hibernate.jpa.internal.util.LogHelper  : HHH000204: Processing PersistenceUnitInfo [
    name: default
    ...]
2015-03-24 16:22:50.478  INFO 9492 --- [           main] org.hibernate.Version                    : HHH000412: Hibernate Core {4.3.8.Final}
2015-03-24 16:22:50.480  INFO 9492 --- [           main] org.hibernate.cfg.Environment            : HHH000206: hibernate.properties not found
2015-03-24 16:22:50.483  INFO 9492 --- [           main] org.hibernate.cfg.Environment            : HHH000021: Bytecode provider name : javassist
2015-03-24 16:22:50.697  INFO 9492 --- [           main] o.hibernate.annotations.common.Version   : HCANN000001: Hibernate Commons Annotations {4.0.5.Final}
2015-03-24 16:22:50.806  INFO 9492 --- [           main] org.hibernate.dialect.Dialect            : HHH000400: Using dialect: org.hibernate.dialect.MySQLDialect
2015-03-24 16:22:51.058  INFO 9492 --- [           main] o.h.h.i.ast.ASTQueryTranslatorFactory    : HHH000397: Using ASTQueryTranslatorFactory
2015-03-24 16:22:52.581  INFO 9492 --- [           main] s.w.s.m.m.a.RequestMappingHandlerAdapter : Looking for @ControllerAdvice: org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@1d7acb34: startup date [Tue Mar 24 16:22:46 CET 2015]; root of context hierarchy
2015-03-24 16:22:52.654  INFO 9492 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/addProduits],methods=[POST],params=[],headers=[],consumes=[application/json;charset=UTF-8],produces=[],custom=[]}" onto public spring.webjson.webjson.models.Response<java.util.List<spring.data.entities.Produit>> spring.webjson.server.service.Controller.addProduits(javax.servlet.http.HttpServletRequest)
2015-03-24 16:22:52.655  INFO 9492 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/updateProduits],methods=[POST],params=[],headers=[],consumes=[application/json;charset=UTF-8],produces=[],custom=[]}" onto public spring.webjson.webjson.models.Response<java.util.List<spring.data.entities.Produit>> spring.webjson.server.service.Controller.updateProduits(javax.servlet.http.HttpServletRequest)
2015-03-24 16:22:52.655  INFO 9492 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/getAllProduits],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public spring.webjson.webjson.models.Response<java.util.List<spring.data.entities.Produit>> spring.webjson.server.service.Controller.getAllProduits()
2015-03-24 16:22:52.655  INFO 9492 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/getAllCategories],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public spring.webjson.webjson.models.Response<java.util.List<spring.data.entities.Categorie>> spring.webjson.server.service.Controller.getAllCategories()
2015-03-24 16:22:52.655  INFO 9492 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/addCategories],methods=[POST],params=[],headers=[],consumes=[application/json;charset=UTF-8],produces=[],custom=[]}" onto public spring.webjson.webjson.models.Response<java.util.List<spring.data.entities.Categorie>> spring.webjson.server.service.Controller.addCategories(javax.servlet.http.HttpServletRequest)
2015-03-24 16:22:52.655  INFO 9492 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/updateCategories],methods=[POST],params=[],headers=[],consumes=[application/json;charset=UTF-8],produces=[],custom=[]}" onto public spring.webjson.webjson.models.Response<java.util.List<spring.data.entities.Categorie>> spring.webjson.server.service.Controller.updateCategories(javax.servlet.http.HttpServletRequest)
2015-03-24 16:22:52.656  INFO 9492 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/getCategorieByNameWithoutProduits/{nom}],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public spring.webjson.webjson.models.Response<spring.data.entities.Categorie> spring.webjson.server.service.Controller.getCategorieByNameWithoutProduits(java.lang.String)
2015-03-24 16:22:52.656  INFO 9492 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/getProduitByNameWithoutCategorie/{nom}],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public spring.webjson.webjson.models.Response<spring.data.entities.Produit> spring.webjson.server.service.Controller.getProduitByNameWithoutCategorie(java.lang.String)
2015-03-24 16:22:52.656  INFO 9492 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/getProduitByNameWithCategorie/{nom}],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public spring.webjson.webjson.models.Response<spring.data.entities.Produit> spring.webjson.server.service.Controller.getProduitByNameWithCategorie(java.lang.String)
2015-03-24 16:22:52.656  INFO 9492 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/getProduitByIdWithCategorie/{idProduit}],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public spring.webjson.webjson.models.Response<spring.data.entities.Produit> spring.webjson.server.service.Controller.getProduitByIdWithCategorie(java.lang.Long)
2015-03-24 16:22:52.656  INFO 9492 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/getCategorieByNameWithProduits/{nom}],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public spring.webjson.webjson.models.Response<spring.data.entities.Categorie> spring.webjson.server.service.Controller.getCategorieByNameWithProduits(java.lang.String)
2015-03-24 16:22:52.657  INFO 9492 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/getCategorieByIdWithProduits/{idCategorie}],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public spring.webjson.webjson.models.Response<spring.data.entities.Categorie> spring.webjson.server.service.Controller.getCategorieByIdWithProduits(java.lang.Long)
2015-03-24 16:22:52.657  INFO 9492 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/deleteAllCategories],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public spring.webjson.webjson.models.Response<java.lang.Void> spring.webjson.server.service.Controller.deleteAllCategories()
2015-03-24 16:22:52.657  INFO 9492 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/getCategorieByIdWithoutProduits/{idCategorie}],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public spring.webjson.webjson.models.Response<spring.data.entities.Categorie> spring.webjson.server.service.Controller.getCategorieByIdWithoutProduits(java.lang.Long)
2015-03-24 16:22:52.657  INFO 9492 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/deleteAllProduits],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public spring.webjson.webjson.models.Response<java.lang.Void> spring.webjson.server.service.Controller.deleteAllProduits()
2015-03-24 16:22:52.658  INFO 9492 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/getProduitByIdWithoutCategorie/{idProduit}],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public spring.webjson.webjson.models.Response<spring.data.entities.Produit> spring.webjson.server.service.Controller.getProduitByIdWithoutCategorie(java.lang.Long)
2015-03-24 16:22:52.659  INFO 9492 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],methods=[],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.BasicErrorController.error(javax.servlet.http.HttpServletRequest)
2015-03-24 16:22:52.659  INFO 9492 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],methods=[],params=[],headers=[],consumes=[],produces=[text/html],custom=[]}" onto public org.springframework.web.servlet.ModelAndView org.springframework.boot.autoconfigure.web.BasicErrorController.errorHtml(javax.servlet.http.HttpServletRequest)
2015-03-24 16:22:52.691  INFO 9492 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/webjars/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2015-03-24 16:22:52.692  INFO 9492 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2015-03-24 16:22:52.742  INFO 9492 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**/favicon.ico] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2015-03-24 16:22:53.001  INFO 9492 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Registering beans for JMX exposure on startup
2015-03-24 16:22:53.106  INFO 9492 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8080 (http)
2015-03-24 16:22:53.108  INFO 9492 --- [           main] spring.webjson.server.boot.Boot          : Started Boot in 6.752 seconds (JVM running for 7.433)
  • 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