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;
4.10. [/m10, /m11]: aceder a um cookie
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:
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:
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:
![]() |

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:
Os códigos não podem ser arbitrários. São os mesmos exibidos na ação anterior [/m27]. Por exemplo:
![]()
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:
![]() | ![]() |









































































