Skip to content

4. Ações: o modelo

Voltemos à arquitetura de uma aplicação Spring MVC:

No capítulo anterior, analisámos o processo que encaminha o pedido [1] para o controlador e a ação [2a] que o irá tratar, um mecanismo conhecido como encaminhamento. Apresentámos também as várias respostas que uma ação pode enviar de volta ao navegador. Até agora, apresentámos ações que não processaram a solicitação que lhes foi apresentada. Uma solicitação [1] contém várias informações que o Spring MVC apresenta [2a] à ação na forma de um modelo. Este termo não deve ser confundido com o modelo M de uma vista V [2c] que é produzida pela ação:

  • a solicitação HTTP do cliente chega a [1];
  • em [2], a informação contida na solicitação é transformada num modelo de ação [3], frequentemente, mas não necessariamente, uma classe, que serve como entrada para a ação [4];
  • em [4], a ação, com base neste modelo, gera uma resposta. Esta resposta tem dois componentes: uma vista V [6] e o modelo M desta vista [5];
  • a vista V [6] utilizará o seu modelo M [5] para gerar a resposta HTTP destinada ao cliente.

No modelo MVC, a ação [4] faz parte do C (controlador), o modelo de vista [5] é o M e a vista [6] é o V.

Este capítulo examina os mecanismos para ligar a informação transportada pela solicitação — que é inerentemente composta por cadeias de caracteres — ao modelo de ação, que pode ser uma classe com propriedades de vários tipos.

Nota: o termo [Modelo de Ação] não é um termo reconhecido.

Criamos um novo controlador para estas novas ações:

  

O [ActionModelController] será, por enquanto, o seguinte:


package istia.st.springmvc.controllers;
 
import org.springframework.web.bind.annotation.RestController;
 
@RestController
public class ActionModelController {
 
}
  • Linha 5: Note que a anotação [@RestController] faz com que a resposta enviada ao cliente seja a serialização em string dos resultados da ação do controlador;

4.1. [/m01]: Parâmetros GET

Adicionamos a seguinte ação [/m01]:


 
    // ----------------------- retrieve parameters with GET------------------------
    @RequestMapping(value = "/m01", method = RequestMethod.GET, produces = "text/plain;charset=UTF-8")
    public String m01(String nom, String age) {
        return String.format("Hello [%s-%s]!, Greetings from Spring Boot!", nom, age);
}
  • Linha 4: A ação aceita dois parâmetros denominados [name] e [age]. Estes serão inicializados com os parâmetros com os mesmos nomes na solicitação HTTP GET;

Os resultados são os seguintes no Chrome [1-3]:

  • em [1], o pedido GET com os parâmetros [name] e [age];
  • em [3], vemos que a ação [/m01] recuperou com sucesso esses parâmetros;

4.2. [/m02]: Parâmetros POST

Adicionamos a seguinte ação [/m02]:


 
    // ----------------------- retrieve parameters with POST------------------------
    @RequestMapping(value = "/m02", method = RequestMethod.POST, produces = "text/plain;charset=UTF-8")
    public String m02(String nom, String age) {
        return String.format("Hello [%s-%s]!, Greetings from Spring Boot!", nom, age);
}
  • Linha 4: A ação aceita dois parâmetros denominados [name] e [age]. Estes serão inicializados com os parâmetros com os mesmos nomes na solicitação HTTP POST;

Os resultados com o [Advanced REST Client] são os seguintes:

  • Em [1-3], o pedido POST com os parâmetros [name] e [age];
  • Em [4-5], definimos o cabeçalho HTTP [Content-Type] para o pedido POST. Deve ser [Content-Type: application/x-www-form-urlencoded];
  • em [6], [Form Data] fornece a lista de parâmetros para uma operação POST. Aqui vemos os parâmetros [name] e [age];
  • em [7], a resposta do servidor mostra que a ação [/m02] recuperou com sucesso os parâmetros [name] e [age];

4.3. [/m03]: Parâmetros com os mesmos nomes

Vimos na secção 2.5.2.8 que a lista de seleção múltipla podia enviar parâmetros com os mesmos nomes para o servidor. Vamos ver como uma ação pode recuperá-los. Adicionamos a seguinte ação [/m03]:


    // ----------------------- retrieve parameters with the same names-----------------
    @RequestMapping(value = "/m03", method = RequestMethod.POST, produces = "text/plain;charset=UTF-8")
    public String m03(String nom[]) {
        return String.format("Hello [%s]!, Greetings from Spring Boot!", String.join("-", nom));
}
  • Linha 2: A ação aceita um parâmetro chamado [name[]]. Este será inicializado aqui com todos os parâmetros com este nome, quer se trate de uma solicitação GET ou POST, uma vez que o tipo de solicitação não foi especificado aqui;

Os resultados são os seguintes:

  • Com um POST [1], enviamos os parâmetros [2];
  • os parâmetros também estão incluídos na URL [3];
  • em [4], os quatro parâmetros com o mesmo nome [name]: [parâmetros da string de consulta] são os parâmetros da URL, [dados do formulário] são os parâmetros enviados;
  • em [5], vemos que a ação [/m03] recuperou os quatro parâmetros denominados [name];

4.4. [/m04]: mapeamento dos parâmetros da ação para um objeto Java

Considere a seguinte nova ação [/m04]:


    // ------ map parameters to a Command Object ---------------
    @RequestMapping(value = "/m04", method = RequestMethod.POST)
    public Personne m04(Personne personne) {
        return person;
}
  • Linha 3: A ação recebe uma Person do seguinte tipo como parâmetro:

public class Personne {
 
    // identifier
    private Integer id;
    // name
    private String nom;
    // age
    private int age;
....
    // getters and setters
...
}
  • Para criar o parâmetro [Person], o Spring MVC chama [new Person()];
  • depois, se existirem parâmetros com os nomes dos campos [id, name, age] do objeto criado, instancí-los-á utilizando os seus setters;
  • linha 4: a ação retorna um tipo [Person], que será, portanto, serializado numa string antes de ser enviado ao cliente. Vimos que, por predefinição, a serialização realizada é a serialização JSON. O cliente deverá, portanto, receber a string JSON de uma pessoa;

Eis um exemplo:

  • em [1], os parâmetros [id, nome, idade] para construir um objeto [Pessoa];
  • em [2], a cadeia JSON para esta pessoa;

O que acontece se não enviarmos todos os campos de uma pessoa? Vamos experimentar:

  • em [2], apenas o parâmetro [id] foi inicializado;

4.5. [/m05]: recuperar elementos de um URL

Considere a seguinte nova ação [/m05]:


    // ----------------------- retrieve elements from URL ------------------------
    @RequestMapping(value = "/m05/{a}/x/{b}", method = RequestMethod.GET)
    public Map<String, String> m05(@PathVariable("a") String a, @PathVariable("b") String b) {
        Map<String, String> map = new HashMap<String, String>();
        map.put("a", a);
        map.put("b", b);
        return map;
}
  • Linha 2: A URL que está a ser processada tem o formato [/m05/{a}/x/{b}], em que {param} é um parâmetro da URL;
  • Linha 3: Os elementos do parâmetro da URL são recuperados utilizando a anotação [@PathVariable];
  • linhas 4–6: os elementos recuperados [a] e [b] são colocados num dicionário;
  • linha 7: a resposta será a cadeia JSON deste dicionário;

Os resultados são os seguintes:

 

4.6. [/m06]: Recuperação de elementos e parâmetros de URL

Considere a seguinte nova ação [/m06]:


    // -------- retrieve elements from URL and parameters---------------
    @RequestMapping(value = "/m06/{a}/x/{b}", method = RequestMethod.GET)
    public Map<String, Object> m06(@PathVariable("a") Integer a, @PathVariable("b") Double b, Double c) {
        Map<String, Object> map = new HashMap<String, Object>();
        map.put("a", a);
        map.put("b", b);
        map.put("c", c);
        return map;
}
  • linha 3: recuperamos os dois elementos da URL [Integer a, Double b] e um parâmetro (GET ou POST) [Double c];
  • linhas 4–7: estes elementos são colocados num dicionário;
  • linha 8: que forma a resposta do cliente, que receberá, portanto, a cadeia JSON deste dicionário;

Eis os resultados:

 

Repare no / no final do caminho [http://localhost:8080/m06/100/x/200.43/]. Sem ele, obtemos o seguinte resultado incorreto:

 

4.7. [/m07]: aceder a todo o pedido

Aqui está a nova ação [/m07]:


    // ------ access the HttpServletRequest query ------------------------
    @RequestMapping(value = "/m07", method = RequestMethod.GET, produces = "text/plain;charset=UTF-8")
    public String m07(HttpServletRequest request) {
        // HTTP headers
        Enumeration<String> headerNames = request.getHeaderNames();
        StringBuffer buffer = new StringBuffer();
        while (headerNames.hasMoreElements()) {
            String name = headerNames.nextElement();
            buffer.append(String.format("%s : %s\n", name, request.getHeader(name)));
        }
        return buffer.toString();
}
  • linha 3: solicitamos ao Spring MVC que injete o objeto [HttpServletRequest request], que encapsula todas as informações disponíveis sobre a solicitação;
  • linhas 5–10: recuperamos todos os cabeçalhos HTTP da solicitação para os reunir numa string que enviamos ao cliente (linha 11);

Os resultados são os seguintes:

  • em [1], os cabeçalhos HTTP do pedido;
  • em [2], a resposta. Todos os cabeçalhos HTTP da solicitação estão, de facto, presentes aí.

4.8. [/m08]: acesso ao objeto [Writer]

Considere a seguinte ação:


    // ----------------------- injection de writer ------------------------
    @RequestMapping(value = "/m08", method = RequestMethod.GET)
    public void m08(Writer writer) throws IOException {
        writer.write("Bonjour le monde !");
}
  • Linha 3: O Spring MVC injeta o objeto [Writer writer], que permite escrever no fluxo de resposta para o cliente;
  • linha 3: a ação retorna um tipo [void], indicando que deve construir a resposta para o cliente por si própria;
  • linha 4: Adicionar texto ao fluxo de resposta para o cliente;

Os resultados são os seguintes:

  • em [2], vemos que o cabeçalho HTTP [Content-Type] não foi enviado;
  • em [3], a resposta;

4.9. [/m09]: aceder a um cabeçalho HTTP

Considere a seguinte ação:


    // ----------------------- injection of RequestHeader ------------------------
    @RequestMapping(value = "/m09", method = RequestMethod.GET)
    public String m09(@RequestHeader("User-Agent") String userAgent) {
        return userAgent;
}
  • linha 3: a anotação [@RequestHeader("User-Agent")] recupera o cabeçalho HTTP [User-Agent];
  • linha 4: o texto deste cabeçalho é devolvido;

Os resultados são os seguintes:

  • em [2], o cabeçalho HTTP [User-Agent];
  • em [3], a ação [/m08] recuperou corretamente este cabeçalho;

Um cookie é geralmente um cabeçalho HTTP que o:

  • o servidor envia ao cliente pela primeira vez;
  • o cliente, por sua vez, reenvia sistematicamente ao servidor;

Primeiro, vamos criar uma ação que crie o cookie:


    // ----------------------- Cookie creation ------------------------
    @RequestMapping(value = "/m10", method = RequestMethod.GET)
    public void m10(HttpServletResponse response) {
        response.addCookie(new Cookie("cookie1", "remember me"));
}
  • Linha 3: Injetamos o objeto [HttpServletResponse response] para ter controlo total sobre a resposta;
  • linha 4: criamos um cookie com a chave [cookie1] e o valor [remember me] (Nota: caracteres acentuados no valor de um cookie causam erros);
  • linha 3: a ação não retorna nada. Além disso, não escreve nada no corpo da resposta. O cliente receberá, portanto, um documento vazio. A resposta é usada apenas para adicionar o cabeçalho HTTP para um cookie;

Vejamos os resultados:

  • em [1]: o pedido;
  • em [2]: a resposta está vazia;
  • em [3]: o cookie criado pela ação;

Agora vamos criar uma ação para recuperar este cookie, que o navegador irá enviar em todas as solicitações:


    // ----------------------- Cookie injection ------------------------
    @RequestMapping(value = "/m11", method = RequestMethod.GET)
    public String m10(@CookieValue("cookie1") String cookie1) {
        return cookie1;
}
  • Linha 3: A anotação [@CookieValue("cookie1")] recupera o cookie com a chave [cookie1];
  • linha 4: este valor será a resposta enviada ao cliente;

Vamos ver os resultados:

  • em [2], vemos que o navegador devolve o cookie;
  • em [3], a ação conseguiu recuperá-lo com sucesso;

4.11. [/m12]: aceder ao corpo de um POST

Os parâmetros POST são normalmente acompanhados pelo cabeçalho HTTP [Content-Type: application/x-www-form-urlencoded]. É possível aceder à string completa enviada. Criamos a seguinte ação:


    // ----------- retrieve the body of a POST of type String------------------------
    @RequestMapping(value = "/m12", method = RequestMethod.POST)
    public String m12(@RequestBody String requestBody) {
        return requestBody;
}
  • Linha 3: A anotação [@RequestBody] permite recuperar o corpo da solicitação POST. Aqui, assumimos que é do tipo [String];
  • linha 4: devolvemos este corpo ao cliente;

Eis um primeiro exemplo:

  • em [2], os valores enviados;
  • em [3], o cabeçalho HTTP [Content-Type] do pedido;
  • em [4], a resposta do servidor;

Os parâmetros POST nem sempre têm o formato simples [p1=v1&p2=v2] que temos usado frequentemente até agora. Vamos considerar um caso mais complexo:

  • em [2-3]: inserimos os valores enviados no formato [chave:valor];
  • em [5], a string que foi enviada;

Com o tipo [Content-Type: application/x-www-form-urlencoded], a string enviada deve estar no formato [p1=v1&p2=v2]. Se quisermos enviar algo, usaremos o tipo [Content-Type: text/plain]. Aqui está um exemplo:

  • em [2-3], criamos o cabeçalho HTTP [Content-Type]. Por predefinição [5], é este que será utilizado em vez do definido em [6]. O atributo [charset=utf-8] é importante. Sem ele, perdemos os caracteres acentuados na cadeia de caracteres enviada;
  • em [4], a string enviada que recuperamos corretamente em [7];

4.12. [/m13, /m14]: recuperar valores enviados em JSON

É possível enviar parâmetros com o cabeçalho HTTP [Content-Type: application/json]. Criamos a seguinte ação:


    // ----------------------- retrieve the jSON body from a POST
    @RequestMapping(value = "/m13", method = RequestMethod.POST, consumes = "application/json")
    public String m13(@RequestBody Personne personne) {
        return personne.toString();
}
  • Linha 2: [consumes = "application/json"] especifica que a ação espera um corpo JSON;
  • linha 3: [@RequestBody] representa este corpo. Esta anotação foi associada a um objeto do tipo [Person]. O corpo JSON será automaticamente deserializado para este objeto;
  • linha 4: usamos o método [Person].toString() para devolver algo diferente da string JSON enviada;

Eis um exemplo:

  • em [2], a cadeia JSON enviada;
  • em [3], o [Content-Type] da solicitação;
  • em [4], a resposta do servidor;

Pode fazer o mesmo de outra forma:


    // ----------------------- retrieve the jSON body from a POST 2 -------------------
    @RequestMapping(value = "/m14", method = RequestMethod.POST, consumes = "text/plain")
    public String m14(@RequestBody String requestBody) throws JsonParseException, JsonMappingException, IOException {
        Personne personne = new ObjectMapper().readValue(requestBody, Personne.class);
        return personne.toString();
}
  • linha 2: especificámos que o método espera um fluxo do tipo [text/plain]. O Spring MVC tratará então o corpo da solicitação como um tipo [String] (linha 3);
  • linha 4: a cadeia JSON é deserializada num objeto [Person] (ver secção 9.7, página 542);

Os resultados são os seguintes:

  • em [3], certifique-se de que utiliza [text/plain];

4.13. [/m15]: recuperar a sessão

Vamos rever a arquitetura de execução de uma ação:

A classe controladora é instanciada no início da solicitação do cliente e destruída no final da mesma. Portanto, não pode ser utilizada para armazenar dados entre solicitações, mesmo que seja chamada repetidamente. Poderá querer armazenar dois tipos de dados:

  • dados partilhados por todos os utilizadores da aplicação web. Geralmente, trata-se de dados de leitura única;
  • dados partilhados entre pedidos do mesmo cliente. Estes dados são armazenados num objeto chamado Sessão. Referimo-nos a isto como a sessão do cliente para designar a memória do cliente. Todos os pedidos de um cliente têm acesso a esta sessão. Podem armazenar e ler informações a partir dela.

Acima, mostramos os tipos de memória aos quais uma ação tem acesso:

  • a memória da aplicação, que contém principalmente dados de leitura apenas e é acessível a todos os utilizadores;
  • a memória de um utilizador específico, ou sessão, que contém dados de leitura/gravação e é acessível a pedidos sucessivos do mesmo utilizador;
  • não mostrado acima, existe uma memória de solicitação, ou contexto de solicitação. A solicitação de um utilizador pode ser processada por várias ações sucessivas. O contexto de solicitação permite que a Ação 1 passe informações para a Ação 2.

Vejamos um primeiro exemplo que ilustra estes diferentes tipos de memória:


    // ----------------------- retrieve session ------------------------
    @RequestMapping(value = "/m15", method = RequestMethod.GET, produces = "text/plain;charset=UTF-8")
    public String m15(HttpSession session) {
        // retrieve the [counter] key object from the session
        Object objCompteur = session.getAttribute("compteur");
        // convert it to an integer to increment it
        int iCompteur = objCompteur == null ? 0 : (Integer) objCompteur;
        iCompteur++;
        // put it back in the session
        session.setAttribute("compteur", iCompteur);
        // we return it as the result of action
        return String.valueOf(iCompteur);
}

O Spring MVC mantém a sessão do utilizador num objeto do tipo [HttpSession].

  • linha 3: pedimos ao Spring MVC para injetar o objeto [HttpSession] nos parâmetros da ação;
  • linha 5: recuperamos um atributo chamado [counter] a partir dele. Uma sessão comporta-se como um dicionário, um conjunto de pares [chave, valor]. Se a chave [counter] não existir na sessão, obtemos um ponteiro nulo;
  • Linha 7: O valor associado à chave [counter] será do tipo [Integer];
  • linha 8: incrementamos o contador;
  • linha 10: Atualizamos o contador na sessão;
  • linha 12: o valor do contador é enviado para o cliente;

Quando [/m15] é executado pela:

  • primeira vez, linha 12, o contador terá o valor 1;
  • pela segunda vez, a linha 5 irá recuperar este valor 1 e defini-lo como 2;
  • ...

Aqui está um exemplo de execução:

  • em [1], obtemos de facto o primeiro valor do contador;
  • em [2], o servidor enviou um cookie de sessão. Este tem a chave [JSESSIONID] e um valor que é uma sequência de caracteres única para cada utilizador. Lembre-se de que o navegador reenvia sempre os cookies que recebe. Assim, quando solicitamos a ação [/m15] pela segunda vez, o cliente reenviará este cookie, o que permitirá ao servidor reconhecê-lo e associá-lo à sua sessão. É assim que a sessão do utilizador é mantida;

Vejamos a segunda solicitação:

  • em [3], vemos que o cliente envia o cookie de sessão. Note-se que, na resposta do servidor, este cookie de sessão já não está presente. Agora é o cliente que o envia para ser reconhecido;
  • Em [4], o segundo valor do contador. Este foi, de facto, incrementado;

4.14. [/m16]: recuperação de um objeto de âmbito [session]

Podemos querer colocar todos os dados da sessão de um utilizador num único objeto e colocar apenas esse objeto na sessão. Vamos adotar esta abordagem. Vamos colocar o contador no seguinte objeto [SessionModel]:

  

package istia.st.sprinmvc.models;
 
import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.ScopedProxyMode;
import org.springframework.stereotype.Component;
 
@Component
@Scope(value = "session", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class SessionModel {
 
    private int compteur;
 
    public int getCompteur() {
        return compteur;
    }
 
    public void setCompteur(int compteur) {
        this.compteur = compteur;
    }
 
}
  • linha 7: a anotação [@Component] é uma anotação Spring (linha 5) que torna a classe [SessionModel] um componente cujo ciclo de vida é gerido pelo Spring;
  • linha 8: a anotação [@Scope(value = "session", proxyMode = ScopedProxyMode.TARGET_CLASS)] é também uma anotação do Spring (linhas 3–4). Quando o Spring MVC a encontra, a classe correspondente é criada e colocada na sessão do utilizador. O atributo [proxyMode = ScopedProxyMode.TARGET_CLASS] é importante. É graças a ele que o Spring MVC cria uma instância por utilizador, em vez de uma única instância para todos os utilizadores (singleton);
  • linha 11: o contador;

Para que este novo componente Spring seja reconhecido, a configuração da aplicação deve ser verificada na classe [Application]:


package istia.st.springmvc.main;
 
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
 
@Configuration
@ComponentScan({"istia.st.springmvc.controllers"})
@EnableAutoConfiguration
public class Application {
 
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
  • Linha 9: Os componentes Spring são procurados no pacote [istia.st.springmvc.controllers]. Isto já não é suficiente. Atualizamos esta linha da seguinte forma:

@ComponentScan({ "istia.st.springmvc.controllers", "istia.st.springmvc.models" })

Adicionámos o pacote que contém a classe [SessionModel].

Agora, adicionamos a seguinte ação:


    @Autowired
    private SessionModel session;
 
    // ------ manage a scope object session [Autowired] -----------
    @RequestMapping(value = "/m16", method = RequestMethod.GET, produces = "text/plain;charset=UTF-8")
    public String m16() {
        session.setCompteur(session.getCompteur() + 1);
        return String.valueOf(session.getCompteur());
}
  • Linhas 1-2: O componente Spring [SessionModel] é injetado [@Autowired] no controlador. Recorde-se que um controlador Spring é um singleton. Por isso, é paradoxal injetar nele um componente com um âmbito mais restrito — neste caso, o âmbito [Session]. É aqui que a anotação [@Scope(value = "session", proxyMode = ScopedProxyMode.TARGET_CLASS)] no componente [SessionModel] entra em ação. Sempre que o código do controlador acede ao campo [session] na linha 2, é executado um método proxy para devolver a sessão do pedido atualmente a ser processado pelo controlador;
  • linha 6: o objeto [HttpSession] já não é necessário nos parâmetros da ação;
  • linha 7: o contador é recuperado e incrementado;
  • linha 8: o seu valor é devolvido;

Aqui está um exemplo de execução:

Na primeira vez

A segunda vez

Agora, vamos usar outro navegador para representar um segundo utilizador. Aqui, vamos usar o navegador Opera:

Acima, em [1], este segundo utilizador obtém um valor de contador igual a 1. Isto demonstra que a sua sessão e a do primeiro utilizador são diferentes. Se analisarmos as trocas cliente/servidor (Ctrl-Shift-I também no Opera), vemos em [2] que este segundo utilizador possui um cookie de sessão diferente do do primeiro utilizador. É isto que garante a independência das sessões.

4.15. [/m17]: Recuperar um objeto de âmbito [application]

Vamos rever a arquitetura de execução de uma ação:

Sabemos como criar a sessão do utilizador. Vamos agora criar um objeto de âmbito [application] cujo conteúdo será de leitura única e acessível a todos os utilizadores. Apresentamos a classe [ApplicationModel], que servirá como o objeto de âmbito [application]:

 

package istia.st.springmvc.models;
 
import java.util.concurrent.atomic.AtomicLong;
 
import org.springframework.stereotype.Component;
 
@Component
public class ApplicationModel {
 
    // meter
    private AtomicLong compteur = new AtomicLong(0);
 
    // getters and setters
    public AtomicLong getCompteur() {
        return compteur;
    }
 
    public void setCompteur(AtomicLong compteur) {
        this.compteur = compteur;
    }
 
}
  • Linha 5: A anotação [@Component] garante que a classe [ApplicationModel] será um componente gerido pelo Spring. A natureza padrão dos componentes Spring é do tipo [singleton]: o componente é criado como uma única instância quando o contentor Spring é instanciado, ou seja, geralmente quando a aplicação é iniciada. Podemos usar este ciclo de vida para armazenar informações de configuração no singleton que estarão acessíveis a todos os utilizadores;
  • linha 11: um contador do tipo [AtomicLong]. Este tipo possui um método atómico chamado [incrementAndGet]. Isto significa que uma thread que execute este método tem a garantia de que outra thread não irá ler o valor do contador (Get) entre a sua própria leitura (Get) e o seu incremento (increment) pela primeira thread, o que causaria erros, uma vez que duas threads iriam ler o mesmo valor do contador e o contador, em vez de ser incrementado em dois, seria incrementado em um;

Criamos a seguinte nova ação [/m17]:


@Autowired
    private ApplicationModel application;
 
    // ----- manage an application scope object [Autowired] ------------------------
    @RequestMapping(value = "/m17", method = RequestMethod.GET, produces = "text/plain;charset=UTF-8")
    public String m17() {
        return String.valueOf(application.getCompteur().incrementAndGet());
    }
  • linhas 1-2: injetamos o componente [ApplicationModel] no controlador. Trata-se de um singleton. Por conseguinte, cada utilizador terá uma referência ao mesmo objeto;
  • Linha 7: Devolvemos o contador do âmbito [application] após o incrementarmos;

Aqui estão dois exemplos, um com o Chrome e outro com o Opera:

Acima, vemos que ambos os navegadores funcionaram com o mesmo contador, o que não aconteceu com a sessão. Estes dois navegadores representam dois utilizadores diferentes que têm acesso aos dados do âmbito [application]. De um modo geral, devemos evitar colocar informações de leitura/gravação em objetos do âmbito [application], como foi feito acima com o contador. De facto, os threads de execução de diferentes utilizadores acedem simultaneamente aos dados do âmbito [application]. Se houver informação gravável, o acesso de escrita deve ser sincronizado, tal como foi feito acima com o tipo [AtomicLong]. O acesso simultâneo é uma fonte de erros de programação. Por conseguinte, é preferível colocar apenas informação de leitura nos objetos do âmbito [application].

4.16. [/m18]: Recuperar um objeto de âmbito [sessão] com [@SessionAttributes]

Existe outra forma de recuperar informações do âmbito [session]. Iremos colocar o seguinte objeto na sessão:


package istia.st.springmvc.models;
 
public class Container {
    // the meter
    public int compteur=10;
 
    // getters and setters
    public int getCompteur() {
        return compteur;
    }
 
    public void setCompteur(int compteur) {
        this.compteur = compteur;
    }
}

Vamos utilizar este objeto nas duas ações seguintes:


    // use of [@SessionAttribute] ----------------------
    @RequestMapping(value = "/m18", method = RequestMethod.GET)
    public void m18(HttpSession session) {
        // here we put the key [container] in the session
        session.setAttribute("container", new Container());
    }
 
    // use of [@ModelAttribute] ----------------------
    // the session's [container] key will be injected here
    @RequestMapping(value = "/m19", method = RequestMethod.GET)
    public String m19(@ModelAttribute("container") Container container) {
        container.setCompteur(1 + container.getCompteur());
        return String.valueOf(container.getCompteur());
    }
  • linhas 3–6: a ação [/m18] não retorna nenhum resultado. É utilizada exclusivamente para criar um objeto na sessão com a chave [container];
  • linha 11: Na ação [/m19], é utilizada a anotação [@ModelAttribute]. O comportamento desta anotação é bastante complexo. O parâmetro [container] desta anotação pode referir-se a várias coisas e, em particular, a um objeto de sessão. Para que isto funcione, o objeto deve ter sido declarado com uma anotação [@SessionAttributes] na própria classe:

@RestController
@SessionAttributes({"container"})
public class ActionModelController {
  • A linha 2 acima designa a chave [container] como parte dos atributos da sessão;

Resumindo:

  • em [/m18], a chave [container] é colocada na sessão;
  • a anotação [@SessionAttributes({"container"})] garante que esta chave possa ser injetada num parâmetro anotado com [@ModelAttribute("container")];
  • não é visível no exemplo de execução a seguir, mas as informações anotadas com [@ModelAttribute] fazem automaticamente parte do modelo M passado para a vista V;

Aqui está um exemplo de execução. Primeiro, colocamos a chave [container] na sessão com a ação [/m18] [1]. Em seguida, chamamos a ação [/m19] duas vezes para ver o contador a incrementar-se.

4.17. [/m20-/m23]: injetar dados com [@ModelAttribute]

Considere a seguinte nova ação:


    // the p attribute will be included in all [Model] view models ----------------
    @ModelAttribute("p")
    public Personne getPersonne() {
        return new Personne(7,"abcd", 14);
    }
 
    // ---------------instanciation of @ModelAttribute --------------------------
    // will be injected if it is in the
    // will be injected if the controller has defined a method for this attribute
    // can come from the URL fields if a String --> type converter exists for the attribute
    // otherwise is built with the default constructor
    // then the model attributes are initialized with the parameters of GET or POST
    // the final result will be part of the model produced by the action
 
    // the p attribute is injected into the arguments------------------------
    @RequestMapping(value = "/m20", method = RequestMethod.GET)
    public Personne m20(@ModelAttribute("p") Personne personne) {
        return personne;
}
  • Linhas 2–5: definem um atributo de modelo denominado [p]. Este é o modelo M de uma vista V, representado por um tipo [Model] no Spring MVC. Um modelo comporta-se como um dicionário de pares [chave, valor]. Aqui, a chave [p] está associada ao objeto [Person] construído pelo método [getPerson]. O nome do método pode ser qualquer coisa;
  • linha 17: o atributo do modelo com a chave [p] é injetado nos parâmetros da ação. Esta injeção segue as regras das linhas 8–12. Aqui, estamos no caso definido na linha 9. Portanto, na linha 17, o parâmetro [Person person] será o objeto [Person(7, 'abcd', 14)];
  • Linha 18: O objeto [person] é devolvido para validação. Será serializado em JSON antes de ser enviado para o cliente.

Eis um exemplo:

 

Agora, vamos analisar a seguinte ação:


    // --------- attribute p is automatically included in the M model of view V
    @RequestMapping(value = "/m21", method = RequestMethod.GET)
    public String m21(Model model) {
        return model.toString();
}

Uma ação que pretenda apresentar uma vista V deve construir o seu modelo M. O Spring MVC gere isto utilizando um tipo [Model] que pode ser injetado nos parâmetros da ação. Inicialmente, este modelo está vazio ou contém informações marcadas com a anotação [@ModelAttribute]. A ação pode ou não enriquecer este modelo antes de o passar para uma vista.

  • Linha 3: injeção do modelo M;
  • linha 4: queremos ver o que está lá dentro. Serializamo-lo numa string para o enviar ao cliente. Aqui, será utilizado o método [Person.toString]. Por isso, este deve existir;

Eis uma execução:

 

Acima, vemos que as instruções:


    @ModelAttribute("p")
    public Personne getPersonne() {
        return new Personne(7,"abcd", 14);
}

criámos uma entrada [p, Pessoa(7, "abcd", 14)] no modelo. É sempre assim.

Agora, considere o seguinte caso:


    // sinon est construit avec le constructeur par défaut
    // ensuite les attributs du modèle sont initialisés avec les paramètres du GET ou du POST

com a seguinte ação:


    // --------- model attribute [param1] is part of the model but is not initialized
    @RequestMapping(value = "/m22", method = RequestMethod.GET)
    public String m22(@ModelAttribute("param1") String p1, Model model) {
        return model.toString();
}
  • linha 3: o atributo-chave do modelo [param1] não existe. Neste caso, o tipo associado deve ter um construtor padrão. É o que acontece aqui com o tipo [String], mas não podemos escrever [@ModelAttribute("param1") Integer p1] porque a classe [Integer] não tem um construtor padrão;
  • Linha 4: Devolvemos o modelo para verificar se o atributo-chave do modelo [param1] faz parte dele;

Eis um exemplo de execução:

 

O atributo do modelo [param1] está, de facto, presente no modelo, mas o método [toString] do valor associado não fornece qualquer informação sobre este valor.

Considere agora a seguinte ação, na qual colocamos explicitamente informações no modelo:


    // --------- the model attribute [param2] is explicitly set in the model
    @RequestMapping(value = "/m23", method = RequestMethod.GET)
    public String m23(String p2, Model model) {
        model.addAttribute("param2",p2);
        return model.toString();
}
  • Linha 4: O valor [p2] recuperado na linha 3 é adicionado ao modelo sob a chave [param2]:

Aqui está um exemplo de execução:

 

As regras mudam se o parâmetro de ação for um objeto. Aqui está um primeiro exemplo:


    // ------ the template attribute [unePersonne] is automatically set in the template
    @RequestMapping(value = "/m23b", method = RequestMethod.GET)
    public String m23b(@ModelAttribute("unePersonne") Personne p1, Model model) {
        return model.toString();
}

A ação não modifica o modelo que lhe é fornecido. O resultado é o seguinte:

Podemos ver que a anotação [@ModelAttribute("unePersonne") Personne p1] adicionou a pessoa [p1] ao modelo, associada à chave [unePersonne].

Agora, vamos considerar a seguinte ação:


    // --------- person p1 is automatically included in the model
    // -------- with class name as key, 1st character lowercase
    @RequestMapping(value = "/m23c", method = RequestMethod.GET)
    public String m23c(Personne p1, Model model) {
        return model.toString();
}
  • linha 4: não incluímos a anotação [@ModelAttribute];

O resultado é o seguinte:

Podemos ver que a presença do parâmetro [Person p1] colocou a pessoa [p1] no modelo, associada à chave [person], que é o nome da classe [Person] com a primeira letra minúscula.

4.18. [/m24]: validação do modelo de ação

Considere o seguinte modelo de ação [ActionModel01]:

 

package istia.st.springmvc.models;
 
import javax.validation.constraints.NotNull;
 
public class ActionModel01 {
 
    // data
    @NotNull
    private Integer a;
    @NotNull
    private Double b;
 
    // getters and setters
...
    }
  • Linhas 8 e 9: A anotação [@NotNull] é uma restrição de validação que especifica que os dados anotados não podem ser nulos;

Vamos agora examinar a seguinte ação:


    // ----------------------- model validation ------------------------
    @RequestMapping(value = "/m24", method = RequestMethod.GET)
    public Map<String, Object> m24(@Valid ActionModel01 data, BindingResult result) {
        Map<String, Object> map = new HashMap<String, Object>();
        // mistakes?
        if (result.hasErrors()) {
            StringBuffer buffer = new StringBuffer();
            // browsing the error list
            for (FieldError error : result.getFieldErrors()) {
                buffer.append(String.format("[%s:%s:%s:%s:%s]", error.getField(), error.getRejectedValue(),
                        String.join(" - ", error.getCodes()), error.getCode(),error.getDefaultMessage()));
            }
            map.put("errors", buffer.toString());
        } else {
            // no errors
            Map<String, Object> mapData = new HashMap<String, Object>();
            mapData.put("a", data.getA());
            mapData.put("b", data.getB());
            map.put("data", mapData);
        }
        return map;
}
  • Linha 3: Um objeto [ActionModel01] será instanciado e os seus campos [a, b] inicializados com parâmetros com os mesmos nomes. A anotação [@Valid] indica que as restrições de validação devem ser verificadas. Os resultados desta validação serão colocados no parâmetro [BindingResult] (segundo parâmetro). Serão realizadas as seguintes validações:
    • devido às anotações [@NotNull], os parâmetros [a] e [b] devem estar presentes;
    • devido ao tipo [Integer a], o parâmetro [a], que é inerentemente do tipo [String], deve ser convertível para o tipo [Integer];
    • devido ao tipo [Double b], o parâmetro [b], que é inerentemente do tipo [String], deve ser convertível para o tipo [Double];

Com a anotação [@Valid], os erros de validação serão reportados no parâmetro [BindingResult result]. Sem a anotação [@Valid], os erros de validação fazem com que a ação falhe, e o servidor envia ao cliente uma resposta HTTP com o estado 500 (Erro Interno do Servidor).

  • Linha 3: O resultado da ação é do tipo [Map]. A cadeia JSON deste resultado será enviada ao cliente. Construímos dois tipos de dicionários:
    • em caso de falha, um dicionário com uma entrada ['errors', value], em que [value] é uma string que descreve todos os erros (linha 13);
    • em caso de sucesso, um dicionário com uma entrada ['data', valor], em que [valor] é, por sua vez, um dicionário com duas entradas: ['a', valor], ['b', valor] (linha 19);
  • linhas 9–12: para cada erro detetado [error], é construída a string [error.getField(), error.getRejectedValue(), error.Codes, error.getDefaultMessage()]:
    • o primeiro elemento é o campo errado, [a] ou [b],
    • o segundo elemento é o valor rejeitado, [x] por exemplo,
    • o terceiro elemento é uma lista de códigos de erro. Veremos as suas funções em breve;
    • o quarto elemento é o código de erro. Faz parte da lista anterior;
    • o último elemento é a mensagem de erro padrão. Na verdade, pode haver várias mensagens de erro;

Aqui estão alguns exemplos de execução:

Acima, vemos que:

  • a atribuição de «x» ao campo [ActionModel01.a] falhou, e a mensagem de erro explica o motivo;
  • a atribuição de 'y' ao campo [ActionModel01.b] falhou, e a mensagem de erro explica o motivo;

Tome nota dos códigos de erro para o campo [a]: [typeMismatch.actionModel01.a - typeMismatch.a - typeMismatch.java.lang.Integer - typeMismatch]. Voltaremos a abordar estes códigos de erro quando chegar a altura de personalizar a mensagem de erro. Note que o código de erro é [typeMismatch].

Outro exemplo:

Aqui, os parâmetros [a] e [b] não foram passados. Os validadores [@NotNull] no modelo de ação [ActionModel01] cumpriram então a sua função;

Por fim, os valores corretos:

4.19. [m/24]: personalização de mensagens de erro

Voltemos a uma captura de ecrã do exemplo anterior:

Acima, vemos as mensagens de erro padrão. É evidente que não podemos mantê-las numa aplicação real. É possível personalizar estas mensagens de erro. Para tal, utilizaremos os códigos de erro. Acima, vemos que o erro para o campo [a] tem os seguintes códigos: [typeMismatch.actionModel01.a - typeMismatch.a - typeMismatch.java.lang.Integer - typeMismatch]. Estes códigos de erro variam do mais específico ao menos específico:

  • [typeMismatch.actionModel01.a]: erro de tipo no campo [a] do tipo [ActionModel01];
  • [typeMismatch.a]: erro de tipo num campo denominado [a];
  • [typeMismatch.java.lang.Integer]: erro de tipo num tipo Integer;
  • [typeMismatch]: erro de tipo;

Observamos também que o código de erro para o campo [a] obtido através de [error.getCode()] é [typeMismatch] (ver captura de ecrã acima).

Colocaremos as mensagens de erro num ficheiro de propriedades:

  

O ficheiro [messages.properties] acima terá o seguinte conteúdo:


NotNull=Le champ ne peut être vide
typeMismatch=Format invalide
typeMismatch.model01.a=Le paramètre [a] doit être entier

Cada linha tem o seguinte formato:

    clé=message

Aqui, a chave será um código de erro e a mensagem será a mensagem de erro associada a esse código.

Vamos rever os códigos de erro para os dois campos:

  • [typeMismatch.actionModel01.a - typeMismatch.a - typeMismatch.java.lang.Integer - typeMismatch], quando o parâmetro [a] é inválido;
  • [typeMismatch.actionModel01.b - typeMismatch.b - typeMismatch.java.lang.Double - typeMismatch:typeMismatch ] quando o parâmetro [b] é inválido;
  • [NotNull.actionModel01.a - NotNull.a - NotNull.java.lang.Integer - NotNull] quando o parâmetro [a] está em falta;
  • [NotNull.actionModel01.b - NotNull.b - NotNull.java.lang.Double - NotNull] quando o parâmetro [b] está em falta;

O ficheiro [messages.properties] deve conter uma mensagem de erro para todos os casos de erro possíveis. No caso em que

  • os parâmetros [a] e [b] estiverem em falta, será utilizado o código [NotNull];
  • se o parâmetro [a] estiver incorreto, incluímos mensagens para dois códigos [typeMismatch.actionModel01.a, typeMismatch]. Veremos qual deles é utilizado;
  • se o parâmetro [b] estiver incorreto, será utilizado o código [typeMismatch];

Para utilizar o ficheiro [messages.properties], deve configurar o Spring:

  

Removemos as anotações de configuração da classe [Application]:


package istia.st.springmvc.main;
 
import org.springframework.boot.SpringApplication;
 
public class Application {
 
    public static void main(String[] args) {
        SpringApplication.run(Config.class, args);
    }
}
  • Linha 8: A aplicação Spring Boot é iniciada. O primeiro parâmetro do método estático [SpringApplication.run] é a classe que agora configura a aplicação;

A classe [Config] é a seguinte:


package istia.st.springmvc.main;
 
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ResourceBundleMessageSource;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
 
@Configuration
@ComponentScan({ "istia.st.springmvc.controllers", "istia.st.springmvc.models" })
@EnableAutoConfiguration
public class Config extends WebMvcConfigurerAdapter {
    @Bean
    public MessageSource messageSource() {
        ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
        messageSource.setBasename("i18n/messages");
        return messageSource;
    }
}
  • linhas 11–13: as anotações de configuração que anteriormente se encontravam na classe [Application] estão agora aqui;
  • linha 14: para configurar uma aplicação Spring MVC, deve-se estender a classe [WebMvcConfigurerAdapter];
  • linha 15: a anotação [@Bean] introduz um componente Spring, um singleton;
  • linha 16: definimos um bean chamado [messageSource] (o nome do método). Este bean é utilizado para definir os ficheiros de mensagens da aplicação e deve ter este nome;
  • Linhas 17–19: Indique ao Spring que o ficheiro de mensagens:
    • está localizado na pasta [i18n] dentro do classpath do projeto (linha 18),
    • se chama [messages.properties] (linha 18). Na verdade, o termo [messages] é a raiz dos nomes dos ficheiros de mensagens, e não o nome em si. Veremos que, no contexto da internacionalização, pode haver vários ficheiros de mensagens, um por localidade suportada. Assim, poderemos ter [messages_fr.properties] para o francês e [messages_en.properties] para o inglês. Os sufixos adicionados à raiz [messages] são padronizados. Não se pode usar qualquer coisa;

No projeto STS, a pasta [i18n] deve ser colocada na pasta resources porque é adicionada ao classpath do projeto:

  

Para utilizar este ficheiro, criamos a seguinte nova ação:


// model validation, error message handling ------------------------
    @RequestMapping(value = "/m25", method = RequestMethod.GET)
    public Map<String, Object> m25(@Valid ActionModel01 data, BindingResult result, HttpServletRequest request)
            throws Exception {
        // results dictionary
        Map<String, Object> map = new HashMap<String, Object>();
        // spring application context
        WebApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(request.getServletContext());
        // local
        Locale locale = RequestContextUtils.getLocale(request);
        // mistakes?
        if (result.hasErrors()) {
            StringBuffer buffer = new StringBuffer();
            for (FieldError error : result.getFieldErrors()) {
                // search for error msg using error codes
                // the msg is searched in the message files
                // error codes in table format
                String[] codes = error.getCodes();
                // in chain form
                String listCodes = String.join(" - ", codes);
                // research
                String msg = null;
                int i = 0;
                while (msg == null && i < codes.length) {
                    try {
                        msg = ctx.getMessage(codes[i], null, locale);
                    } catch (Exception e) {
 
                    }
                    i++;
                }
                // have we found?
                if (msg == null) {
                    throw new Exception(String.format("Indiquez un message pour l'un des codes [%s]", listCodes));
                }
                // found - add error msg to error msg chain
                buffer.append(String.format("[%s:%s:%s:%s]", locale.toString(), error.getField(), error.getRejectedValue(),
                        String.join(" - ", msg)));
            }
            map.put("errors", buffer.toString());
        } else {
            // ok
            Map<String, Object> mapData = new HashMap<String, Object>();
            mapData.put("a", data.getA());
            mapData.put("b", data.getB());
            map.put("data", mapData);
        }
        return map;
    }

Este código é semelhante ao da ação [/m24]. Aqui estão as diferenças:

  • linha 3: injetamos a solicitação [HttpServletRequest request] nos parâmetros da ação. Vamos precisar dela;
  • linhas 7–8: recuperamos o contexto Spring. Este contexto contém todos os beans Spring na aplicação. Também fornece acesso aos ficheiros de mensagens;
  • linha 10: recuperamos a localização da aplicação. Este termo é explicado com mais detalhe abaixo;
  • linhas 15–31: para cada erro, procuramos uma mensagem correspondente a um destes códigos de erro. São pesquisados pela ordem dos códigos encontrados em [error.getCodes()]. Assim que uma mensagem é encontrada, paramos;
  • linha 26: como recuperar uma mensagem de [messages.properties]:
    • o primeiro parâmetro é o código procurado em [messages.properties],
    • o segundo é uma matriz de parâmetros, uma vez que as mensagens são, por vezes, parametrizadas. Não é esse o caso aqui,
    • o terceiro é a localização utilizada (obtida na linha 10). A localização especifica o idioma utilizado, [fr_FR] para francês (França), [en_US] para inglês (EUA). A mensagem é procurada em messages_[locale].properties, por exemplo, [messages_fr_FR.properties]. Se este ficheiro não existir, a mensagem é procurada em [messages_fr.properties]. Se este ficheiro não existir, a mensagem é procurada em [messages.properties]. É este último caso que nos interessa;
  • linhas 25–29: de forma um pouco inesperada, ao procurar um código inexistente num ficheiro de mensagens, é lançada uma exceção em vez de um ponteiro nulo;
  • linhas 33–35: Tratamos o caso em que não há mensagem de erro;
  • linhas 37–38: construímos a cadeia de erro. Nela, incluímos a localização e a mensagem de erro encontrada;

Aqui estão alguns exemplos de execução:

 

Vemos que:

  • a localização da aplicação é [fr_FR]. Este é um valor predefinido, uma vez que não fizemos nada para a inicializar;
  • a mensagem utilizada para ambos os campos é a seguinte:

NotNull=Le champ ne peut être vide

Outro exemplo:

 

Vemos que:

  • a mensagem de erro utilizada para o parâmetro [a] é a seguinte:

typeMismatch.actionModel01.a=Le paramètre [a] doit être entier
  • A mensagem de erro utilizada para o parâmetro [b] é a seguinte:

typeMismatch=Format invalide

Por que razão existem duas mensagens diferentes? Para o parâmetro [a], havia duas mensagens possíveis:


typeMismatch=Format invalide
typeMismatch.actionModel01.a=Le paramètre [a] doit être entier

Os códigos de erro foram analisados na ordem especificada pela matriz [error.getCodes()]. Verifica-se que esta ordem vai do código mais específico para o mais geral. É por isso que o código [typeMismatch.model01.a] foi encontrado em primeiro lugar.

4.20. [/m25]: Internacionalização de uma aplicação Spring MVC

Agora que sabemos como personalizar mensagens de erro em francês, gostaríamos também de as ter em inglês, o que nos leva à internacionalização de uma aplicação Spring MVC. Para tratar disso, vamos expandir a classe de configuração [Config] para ficar assim:


package istia.st.springmvc.main;
 
import java.util.Locale;
 
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ResourceBundleMessageSource;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.springframework.web.servlet.i18n.CookieLocaleResolver;
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;
 
@Configuration
@ComponentScan({ "istia.st.springmvc.controllers", "istia.st.springmvc.models" })
@EnableAutoConfiguration
public class Config extends WebMvcConfigurerAdapter {
    @Bean
    public MessageSource messageSource() {
        ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
        messageSource.setBasename("i18n/messages");
        return messageSource;
    }
 
    @Bean
    public LocaleChangeInterceptor localeChangeInterceptor() {
        LocaleChangeInterceptor localeChangeInterceptor = new LocaleChangeInterceptor();
        localeChangeInterceptor.setParamName("lang");
        return localeChangeInterceptor;
    }
 
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(localeChangeInterceptor());
    }
 
    @Bean
    public CookieLocaleResolver localeResolver() {
        CookieLocaleResolver localeResolver = new CookieLocaleResolver();
        localeResolver.setCookieName("lang");
        localeResolver.setDefaultLocale(new Locale("fr"));
        return localeResolver;
    }
}
  • Linhas 28–32: Criamos um interceptor de pedidos. Um interceptor de pedidos estende a interface [HandlerInterceptor]. Essa classe inspeciona o pedido recebido antes de este ser processado por uma ação. Aqui, o [localeChangeInterceptor] irá procurar um parâmetro chamado [lang] na solicitação GET ou POST recebida e alterará a localidade da aplicação com base nesse parâmetro. Assim, se o parâmetro for [lang=en_US], a localidade da aplicação passará a ser Inglês dos EUA;
  • linhas 34–37: sobrescrevemos o método [WebMvcConfigurerAdapter.addInterceptors] para adicionar o interceptor anterior;
  • linhas 39–45: são utilizadas para configurar a forma como a localização será encapsulada num cookie. Sabemos que um cookie pode servir como memória do utilizador, uma vez que o navegador do cliente o reenvia sistematicamente para o servidor. O interceptor [localeChangeInterceptor] anterior cria um cookie que encapsula a localização. A linha 42 atribui a este cookie o nome [lang]. O cookie também é utilizado para alterar a localização;
  • linha 43: especifica que, se o cookie [lang] estiver ausente, a localização será [fr];

Em resumo, a localidade para um pedido pode ser definida de duas formas:

  • passando um parâmetro chamado [lang];
  • enviando um cookie chamado [lang]. Este cookie é criado automaticamente após a execução do método anterior;

Para utilizar esta localização, iremos criar ficheiros de mensagens para as localizações [fr] e [en]:

 

O ficheiro [messages_fr.properties] é o seguinte:


NotNull=Le champ ne peut être vide
typeMismatch=Format invalide
typeMismatch.actionModel01.a=Le paramètre [a] doit être entier

O ficheiro [messages_en.properties] é o seguinte:


NotNull=The field can't be empty
typeMismatch=Invalid format
typeMismatch.actionModel01.a=Parameter [a] must be an integer

O ficheiro [messages.properties] é uma cópia do ficheiro [messages_en.properties]. Note-se que o ficheiro [messages.properties] é utilizado quando não é encontrado nenhum ficheiro que corresponda à localização da solicitação. No nosso caso, se o utilizador enviar um parâmetro [lang=en], uma vez que o ficheiro [messages_en.properties] não existe, será utilizado o ficheiro [messages.properties]. O utilizador verá, portanto, mensagens em inglês.

Vamos experimentar. Primeiro, nas ferramentas de programador do Chrome (Ctrl-Shift-I), verifique os seus cookies:

 

Se tiver um cookie chamado [lang], elimine-o. Em seguida, no Chrome, aceda ao URL [http://localhost:8080/m25]:

 

O navegador enviou os seguintes cabeçalhos HTTP:

GET /m25 HTTP/1.1
Host: localhost:8080
Connection: keep-alive
Pragma: no-cache
Cache-Control: no-cache
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.71 Safari/537.36
Referer: http://localhost:8080/m25
Accept-Encoding: gzip, deflate, sdch
Accept-Language: fr-FR,fr;q=0.8,en-US;q=0.6,en;q=0.4

Podemos ver que, nestes cabeçalhos, não existe nenhum cookie [lang]. Neste caso, o nosso código utiliza a localização [fr]. Isto é mostrado na captura de ecrã. Vamos tentar outro caso:

  • em [1], passámos o parâmetro [lang=en] para definir a localização como [en];
  • em [2], vemos a nova localização;
  • em [3], a mensagem está agora em inglês;

Agora, vamos analisar as trocas HTTP:

 

Podemos ver acima que o servidor devolveu um cookie [lang]. Isto tem uma consequência importante: a localização para o próximo pedido será novamente [en], devido ao cookie [lang] que será devolvido pelo navegador. Devemos, portanto, manter as mensagens em inglês. Vamos verificar isto:

 

Acima, vemos que a localização permaneceu [en]. Devido ao cookie que o navegador envia sistematicamente, permanecerá assim até que o utilizador a altere, enviando o parâmetro [lang] da seguinte forma:

 

4.21. [/m26]: inserir a localização no modelo de ação

No exemplo anterior, vimos uma forma de recuperar a localização a partir do pedido:


    @RequestMapping(value = "/m25", method = RequestMethod.GET)
    public Map<String, Object> m25(@Valid ActionModel01 data, BindingResult result, HttpServletRequest request)
            throws Exception {
...
        // local
        Locale locale = RequestContextUtils.getLocale(request);
// mistakes?

A localização pode ser injetada diretamente nos parâmetros da ação. Aqui está um exemplo:


    @RequestMapping(value = "/m26", method = RequestMethod.GET)
    public String m26(Locale locale) {
        return String.format("locale=%s", locale.toString());
}
 

Como mostrado acima, a validade da localização solicitada não é verificada. No entanto, o pedido subsequente do navegador desencadeia uma exceção do lado do servidor, porque o cookie de localização que recebe está incorreto.

4.22. [/m27]: Validação de um modelo com o Hibernate Validator

Considere a seguinte nova ação:


    //model validation with Hibernate Validator ------------------------
    @RequestMapping(value = "/m27", method = RequestMethod.POST)
    public Map<String, Object> m27(@Valid ActionModel02 data, BindingResult result) {
        Map<String, Object> map = new HashMap<String, Object>();
        // mistakes?
        if (result.hasErrors()) {
            // browsing the error list
            for (FieldError error : result.getFieldErrors()) {
                map.put(error.getField(),
                        String.format("[message=%s, codes=%s]", error.getDefaultMessage(), String.join("|", error.getCodes())));
            }
        } else {
            // no errors
            map.put("data", data);
        }
        return map;
}

Aqui temos um código que já vimos várias vezes:

  • linha 3: a ação [/m27] é solicitada através de um POST;
  • linhas 8–11: cada erro será identificado por [campo, mensagem] com:
    • campo: o campo com o erro,
    • mensagem: a mensagem de erro associada e a lista de códigos de erro;
  • linha 14: se não houver erros, é devolvida a cadeia JSON dos valores enviados;

Linha 3: é utilizado o seguinte modelo de ação [ActionModel02]:

  

package istia.st.springmvc.models;
 
import java.util.Date;
 
import javax.validation.constraints.AssertFalse;
import javax.validation.constraints.AssertTrue;
import javax.validation.constraints.Future;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Past;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;
 
import org.hibernate.validator.constraints.Email;
import org.hibernate.validator.constraints.Length;
import org.hibernate.validator.constraints.NotBlank;
import org.hibernate.validator.constraints.Range;
import org.hibernate.validator.constraints.URL;
 
public class ActionModel02 {
 
    @NotNull(message = "La donnée est obligatoire")
    @AssertFalse(message = "Seule la valeur [false] est acceptée")
    private Boolean assertFalse;
 
    @NotNull(message = "La donnée est obligatoire")
    @AssertTrue(message = "Seule la valeur [true] est acceptée")
    private Boolean assertTrue;
 
    @NotNull(message = "La donnée est obligatoire")
    @Future(message = "Il faut une date postérieure à aujourd'hui")
    private Date dateInFuture;
 
    @NotNull(message = "La donnée est obligatoire")
    @Past(message = "Il faut une date antérieure à aujourd'hui")
    private Date dateInPast;
 
    @NotNull(message = "La donnée est obligatoire")
    @Max(value = 100, message = "Maximum 100")
    private Integer intMax100;
 
    @NotNull(message = "La donnée est obligatoire")
    @Min(value = 10, message = "Minimum 10")
    private Integer intMin10;
 
    @NotNull(message = "La donnée est obligatoire")
    @NotBlank(message = "La chaîne doit être non blanche")
    private String strNotBlank;
 
    @NotNull(message = "La donnée est obligatoire")
    @Size(min = 4, max = 6, message = "La chaîne doit avoir entre 4 et 6 caractères")
    private String strBetween4and6;
 
    @NotNull(message = "La donnée est obligatoire")
    @Pattern(regexp = "^\\d{2}:\\d{2}:\\d{2}$", message = "Le format doit être hh:mm:ss")
    private String hhmmss;
 
    @NotNull(message = "La donnée est obligatoire")
    @Email(message = "Adresse invalide")
    private String email;
 
    @NotNull(message = "La donnée est obligatoire")
    @Length(max = 4, min = 4, message = "La chaîne doit avoir 4 caractères exactement")
    private String str4;
 
    @Range(min = 10, max = 14, message = "La valeur doit être dans l'intervalle [10,14]")
    @NotNull(message = "La donnée est obligatoire")
    private Integer int1014;
 
    @URL(message = "URL invalide")
    private String url;
 
    // getters and setters
 
...
}

A classe utiliza restrições de validação de dois pacotes:

  • [javax.validation.constraints] nas linhas 5–13;
  • [org.hibernate.validator.constraints] nas linhas 15–19;

As dependências Maven para estes dois pacotes estão incluídas no projeto:

  

Aqui, não vamos utilizar mensagens internacionalizadas, mas sim mensagens definidas na restrição através do atributo [message]. Para testar esta ação, vamos utilizar o [Advanced Rest Client]:

  • em [1-2], o pedido POST;
  • em [3], o cabeçalho HTTP [Content-Type] a utilizar;
  • em [4], o link [Adicionar novo valor] permite adicionar um par [parâmetro, valor];
  • em [5], introduza um campo de [ActionModel02], neste caso o campo [assertFalse]:

    @NotNull(message = "La donnée est obligatoire")
    @AssertFalse(message = "Seule la valeur [false] est acceptée")
private Boolean assertFalse;
  • Em [6], introduza um valor incorreto para ver uma mensagem de erro. Acima, a restrição [@AssertFalse] exige que o campo [assertFalse] tenha o valor [false];
  • em [7], a resposta do servidor: a restrição [@NotNull] para campos vazios foi acionada e a mensagem de erro associada foi devolvida;
  • em [8], a mensagem relativa ao campo [assertFalse] para o qual a restrição [@AssertFalse] não foi satisfeita, juntamente com os códigos de erro. Note-se que estes códigos podem estar associados a mensagens internacionalizadas;

Aqui está outro exemplo:

 

Image

Convidamos o leitor a testar os vários casos de erro até conseguir enviar todos os dados válidos:

Nota: o formato da data é o formato anglo-saxónico: mm/dd/aaaa.

4.23. [/m28]: Externalização de mensagens de erro

Na classe [ActionModel02], codificámos as mensagens de forma rígida. É preferível externalizá-las para ficheiros de mensagens. Seguimos o exemplo da ação [/m25]. Criamos o seguinte novo modelo de ação [ActionModel03]:

  

package istia.st.springmvc.models;
 
import java.util.Date;
 
import javax.validation.constraints.AssertFalse;
import javax.validation.constraints.AssertTrue;
import javax.validation.constraints.Future;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Past;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;
 
import org.hibernate.validator.constraints.Email;
import org.hibernate.validator.constraints.Length;
import org.hibernate.validator.constraints.NotBlank;
import org.hibernate.validator.constraints.Range;
import org.hibernate.validator.constraints.URL;
 
public class ActionModel03 {
 
    @NotNull
    @AssertFalse
    private Boolean assertFalse;
 
    @NotNull
    @AssertTrue
    private Boolean assertTrue;
 
    @NotNull
    @Future
    private Date dateInFuture;
 
    @NotNull
    @Past
    private Date dateInPast;
 
    @NotNull
    @Max(value = 100)
    private Integer intMax100;
 
    @NotNull
    @Min(value = 10)
    private Integer intMin10;
 
    @NotNull
    @NotBlank
    private String strNotBlank;
 
    @NotNull
    @Size(min = 4, max = 6)
    private String strBetween4and6;
 
    @NotNull
    @Pattern(regexp = "^\\d{2}:\\d{2}:\\d{2}$")
    private String hhmmss;
 
    @NotNull
    @Email
    private String email;
 
    @NotNull
    @Length(max = 4, min = 4)
    private String str4;
 
    @Range(min = 10, max = 14)
    @NotNull
    private Integer int1014;
 
    @URL
    private String url;
 
    // getters and setters
        ...
}

As mensagens de erro são externalizadas nos ficheiros [messages.properties]:

  

O ficheiro [messages_fr.properties] tem o seguinte conteúdo:


NotNull=Le champ ne peut être vide
typeMismatch=Format invalide
typeMismatch.actionModel01.a=Le paramètre [a] doit être entier
Range.actionModel03.int1014=La valeur doit être dans l'intervalle [10,14]
NotBlank.actionModel03.strNotBlank=La chaîne doit être non blanche
AssertFalse.actionModel03.assertFalse=Seule la valeur [false] est acceptée
Pattern.actionModel03.hhmmss=Le format doit être hh:mm:ss
Past.actionModel03.dateInPast=Il faut une date antérieure ou égale à celle d'aujourd'hui
Future.actionModel03.dateInFuture=Il faut une date postérieure à celle d'aujourd'hui
Length.actionModel03.str4=La chaîne doit avoir 4 caractères exactement
Min.actionModel03.intMin10=Minimum 10
Max.actionModel03.intMax100=Maximum 100
AssertTrue.actionModel03.assertTrue=Seule la valeur [true] est acceptée
Email.actionModel03.email=Adresse invalide
Size.actionModel03.strBetween4and6=La chaîne doit avoir entre 4 et 6 caractères
URL.actionModel03.url=URL invalide

Foram adicionadas mensagens de erro às linhas 4–16. Estas apresentam o seguinte formato:

code=message

Os códigos não podem ser arbitrários. São os mesmos exibidos na ação anterior [/m27]. Por exemplo:

Image

Nos ficheiros de mensagem, deve utilizar um dos quatro códigos acima para o campo [int1014].

O ficheiro [messages_en.properties] é o seguinte:


NotNull=The field can't be empty
typeMismatch=Invalid format
typeMismatch.actionModel01.a=Parameter [a] must be an integer
Range.actionModel03.int1014=Value must be in [10,14] interval
NotBlank.actionModel03.strNotBlank=String can't be empty
AssertFalse.actionModel03.assertFalse=Only boolean [false] is allowed
Pattern.actionModel03.hhmmss=String format is hh:mm:ss
Past.actionModel03.dateInPast=Date must be before or equal to today's date
Future.actionModel03.dateInFuture=Date must be after today's date
Length.actionModel03.str4=String must be four characters long
Min.actionModel03.intMin10=Minimum 10
Max.actionModel03.intMax100=Maximum 100
AssertTrue.actionModel03.assertTrue=Only boolean [true] is allowed
Email.actionModel03.email=Invalid email
Size.actionModel03.strBetween4and6=String must be between four and six characters long
URL.actionModel03.url=Invalid URL

O modelo de ação [ActionModel03] é utilizado pela seguinte ação:


// ----------------------- externalization of error messages ------------------------
    @RequestMapping(value = "/m28", method = RequestMethod.POST)
    public Map<String, Object> m28(@Valid ActionModel03 data, BindingResult result, HttpServletRequest request) {
        Map<String, Object> map = new HashMap<String, Object>();
        // spring application context
        WebApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(request.getServletContext());
        // local
        Locale locale = RequestContextUtils.getLocale(request);
        // mistakes?
        if (result.hasErrors()) {
            for (FieldError error : result.getFieldErrors()) {
                // search for error msg using error codes
                // the msg is searched in the message files
                // error codes in table format
                String[] codes = error.getCodes();
                // in chain form
                String listCodes = String.join(" - ", codes);
                // research
                String msg = null;
                int i = 0;
                while (msg == null && i < codes.length) {
                    try {
                        msg = ctx.getMessage(codes[i], null, locale);
                    } catch (Exception e) {
 
                    }
                    i++;
                }
                // have we found?
                if (msg == null) {
                    msg = String.format("Indiquez un message pour l'un des codes [%s]", listCodes);
                }
                // we have found - we add the error to the dictionary
                map.put(error.getField(), msg);
            }
        } else {
            // no errors
            map.put("data", data);
        }
        return map;
    }

Já discutimos este tipo de código. A única coisa que realmente importa é a linha 23: a mensagem de erro devolvida depende da localização da solicitação.

Aqui está um exemplo em francês:

e agora em inglês: