4. Ações: o modelo
Voltemos à arquitetura de uma aplicação Spring MVC:
![]() |
No capítulo anterior, analisámos o processo que leva a solicitação [1] ao controlador e à ação [2a] que a irão processar, um mecanismo a que chamamos de encaminhamento. Apresentámos também as diferentes respostas que uma ação pode enviar ao navegador. Até ao momento, apresentámos ações que não exploravam a solicitação que lhes era apresentada. Uma solicitação [1] transporta consigo diversas informações que o Spring MVC apresenta à ação sob a forma de um modelo. Não se deve confundir este termo 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], as informações contidas na solicitação serão transformadas no modelo de ação [3] — frequentemente, mas não necessariamente, uma classe — que servirá de entrada para a ação [4];
- em [4], a ação, a partir deste modelo, irá gerar uma resposta. Esta terá duas componentes: uma vista V [6] e o modelo M dessa 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 da vista [5] é o M e a vista [6] é o V.
Este capítulo analisa os mecanismos de ligação entre as informações transportadas pela solicitação, que são, por natureza, cadeias de caracteres, e o modelo da ação, que pode ser uma classe com propriedades de vários tipos.
Nota: o termo [Modèle d'action] não é um termo reconhecido.
Criamos um novo controlador para estas novas ações:
![]() |
O controlador [ActionModelController] será, por enquanto, o seguinte:
package istia.st.springmvc.controllers;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class ActionModelController {
}
- linha 5: recorde-se que a anotação [@RestController] faz com que a resposta enviada ao cliente seja a serialização, sob a forma de cadeia de caracteres, do resultado das ações do controlador;
4.1. [/m01]: parâmetros de um GET
Adicionamos a seguinte ação [/m01]:
// ----------------------- recuperar parâmetros com 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 [nom] e [age]. Estes serão inicializados com parâmetros com os mesmos nomes na solicitação HTTP GET;
Os resultados no Chrome para [1-3] são os seguintes:
![]() |
- em [1], a consulta GET com os parâmetros [nom] e [age];
- em [3], verifica-se que a ação [/m01] recuperou efetivamente esses parâmetros;
4.2. [/m02]: parâmetros de um POST
Adicionamos a seguinte ação [/m02]:
// ----------------------- recuperar parâmetros com 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 [nom] e [age]. Estes serão inicializados com parâmetros com os mesmos nomes na consulta HTTP POST;
Os resultados com [Advanced rest Client] são os seguintes:
![]() |
- em [1-3], a consulta POST com os parâmetros [nom] e [age];
- em [4-5], define-se o cabeçalho HTTP [Content-Type] da consulta POST. Deve ser [Content-Type: application/x-www-form-urlencoded];
- em [6], [Form Data] apresenta a lista de parâmetros de uma operação POST. Aqui vemos os parâmetros [nom] e [age];
- em [7], a resposta do servidor que mostra que a ação [/m02] recuperou corretamente os parâmetros [nom] e [age]; ;
4.3. [/m03]: parâmetros com os mesmos nomes
Vimos no parágrafo 2.5.2.8 que a lista de seleção múltipla podia enviar ao servidor parâmetros com os mesmos nomes. Vejamos como uma ação pode recuperá-los. Adicionamos a seguinte ação [/m03]:
// ----------------------- recuperar parâmetros com os mesmos nomes-----------------
@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 denominado [nome[]]. Este será inicializado aqui com todos os parâmetros com esse nome, quer se trate de um GET ou de um POST, uma vez que, neste caso, o tipo do pedido não foi especificado;
Os resultados são os seguintes:
![]() |
- através de um POST [1], enviam-se os parâmetros [2];
- também se inserem parâmetros no URL [3];
- no [4], os quatro parâmetros com o mesmo nome [nom]: [Query String parameters] são os parâmetros do URL, [Form Data] são os parâmetros enviados;
- em [5], verifica-se que a ação [/m03] recuperou os quatro parâmetros denominados [nom];
4.4. [/m04]: mapear os parâmetros da ação para um objeto Java
Considere a seguinte nova ação [/m04]:
// ------ mapear os parâmetros num objeto (Command Object) ---------------
@RequestMapping(value = "/m04", method = RequestMethod.POST)
public Personne m04(Personne personne) {
return person;
}
- linha 3: a ação tem como parâmetro uma pessoa do seguinte tipo:
public class Personne {
// identificador
private Integer id;
// nome
private String nom;
// idade
private int age;
....
// getters e setters
...
}
- para criar o parâmetro [Personne personne], o Spring MVC cria um [new Personne()];
- depois, se existirem parâmetros com os nomes dos campos [id, nom, age] do objeto criado, instancia-o com os campos através dos respetivos setters;
- linha 4: a ação devolve um tipo [Personne], que será, portanto, serializado como uma cadeia de caracteres antes de ser enviado ao cliente. Vimos que, por predefinição, a serialização efetuada era uma serialização jSON. O cliente deverá, portanto, receber a cadeia jSON de uma pessoa;
Eis um exemplo:
![]() |
- em [1], os parâmetros [id, nom, age] para construir um objeto [Personne];
- em [2], a cadeia jSON dessa 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 os elementos de um URL
Ou seja, a nova ação [/m05] seguinte:
// ----------------------- recuperar os elementos do 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: o URL processado tem o formato [/m05/{a}/x/{b}], em que {param} é um elemento de parâmetro do URL;
- linha 3: os elementos de parâmetro do URL são recuperados com a anotação [@PathVariable];
- linhas 4-6: os elementos [a] e [b] recuperados são colocados num dicionário;
- linha 7: a resposta será a cadeia jSON desse dicionário;
Os resultados são os seguintes:
![]() |
4.6. [/m06]: recuperar elementos de URL e parâmetros
Ou seja, a nova ação [/m06] a seguir:
// -------- recuperar elementos do URL e parâmetros---------------
@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: recuperam-se simultaneamente elementos de URL e [Integer a, Double b], bem como um parâmetro (GET ou POST) de [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-se no / no final do caminho [http://localhost:8080/m06/100/x/200.43/]. Sem ele, obtém-se o seguinte resultado incorreto:
![]() |
4.7. [/m07]: aceder à totalidade da consulta
Considere a seguinte nova ação [/m07]:
// ------ aceder à consulta HttpServletRequest ------------------------
@RequestMapping(value = "/m07", method = RequestMethod.GET, produces = "text/plain;charset=UTF-8")
public String m07(HttpServletRequest request) {
// os cabeçalhos do HTTP
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: solicita-se ao Spring MVC que injete o objeto [HttpServletRequest request], que encapsula todas as informações que se podem obter sobre a solicitação;
- linhas 5-10: recuperam-se todos os cabeçalhos HTTP da solicitação para os reunir numa cadeia de caracteres que é enviada ao cliente (linha 11);
Os resultados são os seguintes:
![]() |
- em [1], os cabeçalhos HTTP da consulta;
![]() |
- em [2], a resposta. Aqui encontram-se, de facto, todos os cabeçalhos HTTP da solicitação.
4.8. [/m08]: acesso ao objeto [Writer]
Consideremos a seguinte ação:
// ----------------------- injeção do 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 da resposta ao cliente;
- linha 3: a ação devolve um tipo [void], o que indica que deve construir ela própria a resposta para o cliente;
- linha 4: adição de um texto no fluxo da resposta ao cliente;
Os resultados são os seguintes:
![]() |
- em [2], verifica-se que o cabeçalho HTTP [Content-Type] não foi enviado;
- em [3], a resposta;
4.9. [/m09]: aceder a um cabeçalho HTTP
Consideremos a seguinte ação:
// ----------------------- injeção de 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")] permite recuperar o cabeçalho HTTP [User-Agent];
- linha 4: apresenta-se o texto deste cabeçalho;
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:
- servidor envia pela primeira vez ao cliente;
- o cliente reenvia, em seguida, sistematicamente ao servidor;
Vamos, em primeiro lugar, criar uma ação que crie o cookie:
// ----------------------- criação de cookie ------------------------
@RequestMapping(value = "/m10", method = RequestMethod.GET)
public void m10(HttpServletResponse response) {
response.addCookie(new Cookie("cookie1", "remember me"));
}
- linha 3: inserimos o objeto [HttpServletResponse response] para termos controlo total sobre a resposta;
- linha 4: criamos um cookie com a chave [cookie1] e o valor [remember me] (Nota: os caracteres acentuados no valor de um cookie provocam erros);
- linha 3: a ação não devolve qualquer resultado. Além disso, não escreve nada no corpo da resposta. Por conseguinte, o cliente irá receber um documento vazio. A resposta é utilizada apenas para adicionar o cabeçalho HTTP de 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 passará a enviar em cada pedido:
// ----------------------- injeção de cookie ------------------------
@RequestMapping(value = "/m11", method = RequestMethod.GET)
public String m10(@CookieValue("cookie1") String cookie1) {
return cookie1;
}
- linha 3: a anotação [@CookieValue("cookie1")] permite recuperar o cookie com a chave [cookie1];
- linha 4: este valor será a resposta enviada ao cliente;
Vejamos os resultados:
![]() |
- em [2], vemos que o navegador devolve o cookie;
- em [3], a ação recuperou-o corretamente;
4.11. [/m12]: aceder ao corpo de um POST
Os parâmetros enviados são normalmente acompanhados pelo cabeçalho HTTP [Content-Type: application/x-www-form-urlencoded]. É possível aceder à totalidade da cadeia enviada. Criamos a seguinte ação:
// ----------- recuperar o corpo de um POST do tipo 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 do POST. Aqui, partimos do princípio de que este é do tipo [String];
- linha 4: este corpo é devolvido ao cliente;
Eis um primeiro exemplo:
![]() |
- em [2], os valores lançados;
- em [3], o cabeçalho HTTP [Content-Type] da solicitação;
- em [4], a resposta do servidor;
Os parâmetros enviados nem sempre têm o formato simples [p1=v1&p2=v2] que temos vindo a utilizar frequentemente até agora. Vejamos um caso mais complexo:
![]() |
- em [2-3]: introduzimos os valores enviados no formato [clé:value];
- em [5], a cadeia que foi enviada;
Com o tipo [Content-Type: application/x-www-form-urlencoded], a cadeia a enviar deve ter o formato [p1=v1&p2=v2]. Se se quiser enviar qualquer coisa, deve-se utilizar o tipo [Content-Type: text/plain]. Eis um exemplo:
![]() |
- em [2-3], cria-se o cabeçalho HTTP [Content-Type]. Por predefinição, [5] é o que será utilizado em vez do definido em [6]. O atributo [charset=utf-8] é importante. Sem ele, perdem-se os caracteres acentuados da cadeia de caracteres enviada;
- em [4], a cadeia enviada é recuperada 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:
// ----------------------- recuperar o corpo jSON de um 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 esse corpo. Esta anotação foi associada a um objeto do tipo [Personne]. O corpo jSON será automaticamente deserializado neste objeto;
- linha 4: utiliza-se o método [Personne].toString() para devolver algo que não seja a cadeia 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;
É possível fazer o mesmo de outra forma:
// ----------------------- recuperar o corpo jSON de um 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: indicou-se que o método esperava 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 [Personne] (ver parágrafo 9.7, página 542);
Os resultados são os seguintes:
![]() |
- em [3], deve-se colocar corretamente [text/plain];
4.13. [/m15]: recuperar a sessão
Voltemos à arquitetura de execução de uma ação:
![]() |
A classe do controlador é instanciada no início do pedido do cliente e destruída no final do mesmo. Por isso, não pode ser utilizada para armazenar dados entre dois pedidos, mesmo que seja chamada repetidamente. Podemos querer armazenar dois tipos de dados:
- dados partilhados por todos os utilizadores da aplicação web. Trata-se, geralmente, de dados de leitura única;
- dados partilhados pelas solicitações de um mesmo cliente. Estes dados são armazenados num objeto denominado «Sessão». Fala-se então de «sessão do cliente» para designar a memória do cliente. Todas as solicitações de um cliente têm acesso a esta sessão. Podem armazenar e ler informações nessa sessão.
![]() |
Acima, mostramos os tipos de memória aos quais uma ação tem acesso:
- a memória da aplicação, que na maioria das vezes contém dados de leitura única e que é 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 que está acessível às solicitações sucessivas do mesmo utilizador;
- não representada acima, existe uma memória de pedido, ou contexto de pedido. O pedido de um utilizador pode ser processado por várias ações sucessivas. O contexto do pedido permite que uma ação 1 transmita informação a uma ação 2.
Vejamos um primeiro exemplo que ilustra estas diferentes memórias:
// ----------------------- recuperar a sessão ------------------------
@RequestMapping(value = "/m15", method = RequestMethod.GET, produces = "text/plain;charset=UTF-8")
public String m15(HttpSession session) {
// recuperamos o objeto-chave [compteur] na sessão
Object objCompteur = session.getAttribute("compteur");
// converte-se num inteiro para o incrementar
int iCompteur = objCompteur == null ? 0 : (Integer) objCompteur;
iCompteur++;
// coloca-se de volta na sessão
session.setAttribute("compteur", iCompteur);
// retorna-se como resultado da ação
return String.valueOf(iCompteur);
}
O Spring MVC mantém a sessão do utilizador num objeto do tipo [HttpSession].
- linha 3: solicita-se ao Spring MVC que injete o objeto [HttpSession] nos parâmetros da ação;
- linha 5: recupera-se, a partir deste, um atributo denominado [compteur]. Uma sessão comporta-se como um dicionário, um conjunto de pares [clé, valeur]. Se a chave [compteur] não existir na sessão, recupera-se um ponteiro null;
- linha 7: o valor associado à chave [compteur] será do tipo [Integer];
- linha 8: incremento do contador;
- linha 10: atualização do contador na sessão;
- linha 12: o valor do contador é enviado ao cliente;
Quando [/m15] for executada pela:
- primeira vez, na linha 12, o contador terá o valor 1;
- pela segunda vez, na linha 5, esse valor 1 será recuperado e alterado para 2;
- ...
Eis um exemplo de execução:
![]() |
- em [1], obtém-se efetivamente o primeiro valor do contador;
- em [2], o servidor enviou um cookie de sessão. Este tem a chave [JSESSIONID] e, como valor, uma cadeia de caracteres única para cada utilizador. Recorde-se que o navegador reenvia sistematicamente os cookies que recebe. Assim, quando solicitarmos a ação [/m15] uma segunda vez, o cliente reenviará esse cookie, o que permitirá ao servidor reconhecê-lo e associá-lo à sua sessão. É desta forma que a memória do utilizador é mantida;
Vejamos o segundo pedido:
![]() |
- em [3], vemos que o cliente reenvia o cookie de sessão. Podemos observar que, na resposta do servidor, já não existe esse cookie de sessão. Agora é o cliente que o envia para ser reconhecido;
- em [4], o segundo valor do contador. Este foi, de facto, incrementado;
4.14. [/m16]: recuperar um objeto do escopo [session]
Podemos querer colocar todos os dados da sessão de um utilizador num único objeto e colocar apenas esse objeto na sessão. Seguimos esta abordagem. Colocamos 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 transforma a classe [SessionModel] num 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 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 e não uma única instância para todos os utilizadores (singleton);
- linha 11: o contador;
Para que este novo componente Spring seja reconhecido, é necessário verificar a configuração da aplicação 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. Alteramos esta linha da seguinte forma:
@ComponentScan({ "istia.st.springmvc.controllers", "istia.st.springmvc.models" })
Adicionámos o pacote onde se encontra a classe [SessionModel].
Agora, adicionamos a seguinte ação:
@Autowired
private SessionModel session;
// ------ gerir um objeto de âmbito (scope) da sessão [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 como [@Autowired] no controlador. Recorde-se aqui que um controlador Spring é um singleton. É, portanto, paradoxal injetar nele um componente de âmbito inferior, neste caso com âmbito [Session]. É aqui que entra a anotação [@Scope(value = "session", proxyMode = ScopedProxyMode.TARGET_CLASS)] do componente [SessionModel]. Sempre que o código do controlador acede ao campo [session] da linha 2, é executado um método proxy para disponibilizar a sessão do pedido atualmente a ser processado pelo controlador;
- linha 6: já não é necessário o objeto [HttpSession] nos parâmetros da ação;
- linha 7: recupera-se/incrementa-se o contador;
- linha 8: devolve-se o seu valor;
Eis um exemplo de execução:
Na primeira vez
![]() |
Na segunda vez
![]() |
Agora, vamos utilizar outro navegador que representará um segundo utilizador. Neste caso, vamos utilizar o navegador Opera:
![]() |
Acima, em [1], este segundo utilizador obtém um valor de contador igual a 1. O que demonstra que a sua sessão e a do primeiro utilizador são diferentes. Se observarmos as trocas cliente/servidor (Ctrl-Shift-I também no Opera), vemos em [2] que este segundo utilizador tem 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]
Voltemos à arquitetura de execução de uma ação:
![]() |
Sabemos como construir a sessão do utilizador. Vamos agora construir um objeto de âmbito [application] cujo conteúdo será de leitura única e acessível a todos os utilizadores. Introduzimos a classe [ApplicationModel], que será 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 {
// contador
private AtomicLong compteur = new AtomicLong(0);
// getters e setters
public AtomicLong getCompteur() {
return compteur;
}
public void setCompteur(AtomicLong compteur) {
this.compteur = compteur;
}
}
- linha 5: a anotação [@Component] faz com que a classe [ApplicationModel] seja um componente gerido pelo Spring. A natureza por predefinição dos componentes Spring é do tipo [singleton]: o componente é criado numa única instância quando o contentor Spring é instanciado, ou seja, geralmente no arranque da aplicação. Podemos utilizar este ciclo de vida para armazenar no singleton informações de configuração que estarão acessíveis a todos os utilizadores;
- linha 11: um contador do tipo [AtomicLong]. Este tipo possui um método [incrementAndGet] denominado «atómico». Isto significa que um thread que execute este método tem a garantia de que outro thread não irá ler o valor do contador (Get) entre a sua leitura (Get) e o seu incremento (increment) pelo primeiro thread, o que provocaria erros, uma vez que dois threads iriam ler o mesmo valor do contador e este, em vez de ser incrementado em dois, seria incrementado em um;
Criamos a seguinte nova ação [/m17]:
@Autowired
private ApplicationModel application;
// ----- gerir um objeto com âmbito de aplicação [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: inserimos o componente [ApplicationModel] no controlador. Trata-se de um singleton. Assim, cada utilizador terá uma referência ao mesmo objeto;
- linha 7: devolvemos o contador de âmbito [application] após o termos incrementado;
Eis dois exemplos, um com o Chrome e outro com o Opera:
![]() | ![]() |
Acima, vemos que os dois navegadores trabalharam com o mesmo contador, o que não era o caso com a sessão. Estes dois navegadores representam dois utilizadores diferentes, ambos com acesso aos dados do escopo [application]. De um modo geral, deve-se evitar colocar nos objetos de âmbito [application] informações de leitura/gravação, tal como foi feito acima com o contador. Com efeito, os threads de execução dos diferentes utilizadores acedem simultaneamente aos dados do âmbito [application]. Se houver informações de escrita, é necessário sincronizar os acessos de escrita, tal como foi feito acima com o tipo [AtomicLong]. Os acessos concorrentes são fonte de erros de programação. Por isso, é preferível colocar apenas informações de leitura nos objetos de âmbito [application].
4.16. [/m18]: recuperar um objeto de âmbito [session] com [@SessionAttributes]
Existe outra forma de recuperar informações do escopo [session]. Vamos colocar o seguinte objeto na sessão:
package istia.st.springmvc.models;
public class Container {
// o contador
public int compteur=10;
// os getters e setters
public int getCompteur() {
return compteur;
}
public void setCompteur(int compteur) {
this.compteur = compteur;
}
}
Vamos utilizar este objeto com as duas ações seguintes:
// utilização de [@SessionAttribute] ----------------------
@RequestMapping(value = "/m18", method = RequestMethod.GET)
public void m18(HttpSession session) {
// aqui insere-se a chave [container] na sessão
session.setAttribute("container", new Container());
}
// utilização de [@ModelAttribute] ----------------------
// a chave [container] da sessão será inserida aqui
@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 produz qualquer resultado. Serve apenas para criar um objeto na sessão com a chave [container];
- linha 11: na ação [/m19], utiliza-se a anotação [@ModelAttribute]. O comportamento desta anotação é bastante complexo. O parâmetro [container] desta anotação pode designar várias coisas e, em particular, um objeto da sessão. Para tal, é necessário que este tenha sido declarado com uma anotação [@SessionAttributes] na própria classe:
@RestController
@SessionAttributes({"container"})
public class ActionModelController {
- a linha 2 acima indica que a chave [container] faz parte dos atributos da sessão;
Resumindo:
- em [/m18], a chave [container] é colocada na sessão;
- a anotação [@SessionAttributes({"container"})] faz com que esta chave possa ser inserida num parâmetro anotado com [@ModelAttribute("container")];
- embora não seja visível no exemplo de execução que se segue, uma informação anotada com [@ModelAttribute] passa automaticamente a fazer parte do modelo M transmitido à vista V;
Eis um exemplo de execução. Em primeiro lugar, insere-se a chave [container] na sessão com a ação [/m18] [1]. Em seguida, chamamos duas vezes a ação [/m19] para ver o contador a aumentar.
![]() |
4.17. [/m20-/m23]: inserção de informações com [@ModelAttribute]
Consideremos a seguinte nova ação:
// o atributo p fará parte de todos os modelos de visualização [Model] ----------------
@ModelAttribute("p")
public Personne getPersonne() {
return new Personne(7,"abcd", 14);
}
// ---------------instanciação de @ModelAttribute --------------------------
// será injetado se estiver na sessão
// será injetado se o controlador tiver definido um método para este atributo
// pode provir dos campos do URL, caso exista um conversor de String para o tipo do atributo
// caso contrário, é construído com o construtor por predefinição
// em seguida, os atributos do modelo são inicializados com os parâmetros do GET ou do POST
// o resultado final fará parte do modelo produzido pela ação
// o atributo p é inserido nos argumentos------------------------
@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]. Trata-se do modelo M de uma vista V, modelo representado por um tipo [Model] no Spring MVC. Um modelo comporta-se como um dicionário de pares [clé, valeur]. Aqui, a chave [p] está associada ao objeto [Personne] construído pelo método [getPersonne]. O nome do método pode ser qualquer um;
- linha 17: o atributo de modelo da chave [p] é injetado nos parâmetros da ação. Esta injeção é feita de acordo com as regras das linhas 8 a 12. Neste caso, estamos perante a situação definida na linha 9. Assim, na linha 17, o parâmetro [Personne personne] será o objeto [Personne(7,'abcd',14)];
- linha 18: devolvemos o objeto [personne] para verificação. Este será serializado como jSON antes de ser enviado ao cliente.
Eis um exemplo:
![]() |
Agora, analisemos a seguinte ação:
// --------- o atributo p passa automaticamente a fazer parte do modelo M da vista V
@RequestMapping(value = "/m21", method = RequestMethod.GET)
public String m21(Model model) {
return model.toString();
}
Uma ação que pretende apresentar uma vista V tem de construir o modelo M dessa vista. O Spring MVC gere este modelo com um tipo [Model], que pode ser injetado nos parâmetros da ação. Inicialmente, este modelo está vazio ou contém as informações marcadas com a anotação [@ModelAttribute]. A ação pode ou não enriquecer este modelo antes de o transmitir a uma vista.
- linha 3: injeção do modelo M;
- linha 4: queremos ver o que está lá dentro. Serializamo-lo numa cadeia de caracteres para o enviar ao cliente. Aqui, será utilizado o método [Personne.toString]. Por isso, é necessário que ele exista;
Eis uma execução:
![]() |
Acima, vemos que as instruções:
@ModelAttribute("p")
public Personne getPersonne() {
return new Personne(7,"abcd", 14);
}
criaram uma entrada [p, Personne(7,'abcd',14)] no modelo. É sempre assim.
Consideremos agora o seguinte caso:
// caso contrário, é construído com o construtor por predefinição
// em seguida, os atributos do modelo são inicializados com os parâmetros do GET ou do POST
com a seguinte ação:
// --------- o atributo do modelo [param1] faz parte do modelo, mas não está inicializado
@RequestMapping(value = "/m22", method = RequestMethod.GET)
public String m22(@ModelAttribute("param1") String p1, Model model) {
return model.toString();
}
- linha 3: o atributo de modelo de chave [param1] não existe. Neste caso, o tipo associado deve ter um construtor por predefinição. É o que acontece aqui com o tipo [String], mas não é possível escrever [@ModelAttribute("param1") Integer p1], pois a classe [Integer] não possui um construtor por predefinição;
- linha 4: devolve-se o modelo para verificar se o atributo de modelo de chave [param1] faz parte do mesmo;
Eis um exemplo de execução:
![]() |
O atributo de modelo [param1] está efetivamente presente no modelo, mas o método [toString] do valor associado não fornece qualquer indicação sobre esse valor.
Consideremos agora a seguinte ação, em que inserimos explicitamente uma informação no modelo:
// --------- o atributo do modelo [param2] é explicitamente incluído no modelo
@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 é inserido no modelo, associado à chave [param2]:
Eis um exemplo de execução:
![]() |
As regras alteram-se se o parâmetro da ação for um objeto. Eis um primeiro exemplo:
// ------ o atributo do modelo [unePersonne] é inserido automaticamente no modelo
@RequestMapping(value = "/m23b", method = RequestMethod.GET)
public String m23b(@ModelAttribute("unePersonne") Personne p1, Model model) {
return model.toString();
}
A ação não altera o modelo que lhe foi fornecido. O resultado é o seguinte:
![]() |
Verifica-se que a anotação [@ModelAttribute("unePersonne") Personne p1] inseriu a pessoa [p1] no modelo, associada à chave [unePersonne].
Consideremos agora a seguinte ação:
// --------- a pessoa p1 é automaticamente inserida no modelo
// -------- com a chave correspondente ao nome da sua classe, com o primeiro caractere em minúscula
@RequestMapping(value = "/m23c", method = RequestMethod.GET)
public String m23c(Personne p1, Model model) {
return model.toString();
}
- linha 4: não foi inserida a anotação [@ModelAttribute];
O resultado é o seguinte:
![]() |
Verifica-se que a presença do parâmetro [Personne p1] inseriu a pessoa [p1] no modelo, associada à chave [personne], que é o nome da classe [Personne] com o primeiro carácter em minúscula.
4.18. [/m24]: validação do modelo da ação
Consideremos o seguinte modelo de ação [ActionModel01]:
![]() |
package istia.st.springmvc.models;
import javax.validation.constraints.NotNull;
public class ActionModel01 {
// dados
@NotNull
private Integer a;
@NotNull
private Double b;
// getters e setters
...
}
- linhas 8 e 9: a anotação [@NotNull] é uma restrição de validação que indica que o dado anotado não pode ter o valor null;
Analisemos agora a seguinte ação:
// ----------------------- validação de um modelo ------------------------
@RequestMapping(value = "/m24", method = RequestMethod.GET)
public Map<String, Object> m24(@Valid ActionModel01 data, BindingResult result) {
Map<String, Object> map = new HashMap<String, Object>();
// erros?
if (result.hasErrors()) {
StringBuffer buffer = new StringBuffer();
// percurso pela lista de erros
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 {
// sem erros
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] serão inicializados com parâmetros com os mesmos nomes. A anotação [@Valid] indica que as restrições de validade devem ser verificadas. Os resultados desta verificação serão colocados no parâmetro de tipo [BindingResult] (segundo parâmetro). Serão realizadas as seguintes verificações:
- devido às anotações [@NotNull], os parâmetros [a] e [b] têm de estar presentes;
- devido ao tipo [Integer a], o parâmetro [a], que por natureza é do tipo [String], deve ser convertível para o tipo [Integer];
- devido ao tipo [Double b], o parâmetro [b], que por natureza é 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 provocam uma falha na ação 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]. Será a cadeia jSON deste resultado que será enviada ao cliente. Construem-se dois tipos de dicionário:
- em caso de falha, um dicionário com uma entrada ['errors', value], em que [value] é uma cadeia de caracteres que descreve todos os erros (linha 13);
- em caso de sucesso, um dicionário com uma entrada ['data',value], em que [value] é, por sua vez, um dicionário com duas entradas: ['a', value], ['b', value] (linha 19);
- linhas 9-12: para cada erro [error] detetado, constrói-se a cadeia [error.getField(), error.getRejectedValue(), error.Codes, error.getDefaultMessage()]:
- o primeiro elemento é o campo com erro, [a] ou [b],
- o segundo elemento é o valor rejeitado, por exemplo, [x],
- o terceiro elemento é uma lista de códigos de erro. Veremos as suas funções em breve;
- o quarto elemento é o código do erro. Faz parte da lista anterior;
- o último elemento é a mensagem de erro por predefinição. É possível, de facto, ter várias mensagens de erro;
Eis alguns exemplos de execução:
![]() |
No exemplo acima, verifica-se que:
- a atribuição de «x» ao campo [ActionModel01.a] falhou e a mensagem de erro indica o motivo;
- a atribuição de «y» ao campo [ActionModel01.b] falhou e a mensagem de erro explica o motivo;
Repare-se nos códigos de erro no campo [a]: [typeMismatch.actionModel01.a - typeMismatch.a - typeMismatch.java.lang.Integer - typeMismatch]. Voltaremos a abordar estes códigos de erro quando for necessário personalizar a mensagem de erro. Note-se que o código do erro é [typeMismatch].
Outro exemplo:
![]() |
Aqui, não foram passados os parâmetros [a] e [b]. Os validadores [@NotNull] do modelo de ação [ActionModel01] desempenharam então o seu papel;
Finalmente, valores corretos:
![]() |
4.19. [m/24]: personalização das mensagens de erro
Voltemos a uma captura de ecrã do exemplo anterior:
![]() |
Vemos acima as mensagens de erro predefinidas. É evidente que não podemos mantê-las numa aplicação real. É possível definir estas mensagens de erro. Para tal, vamos recorrer aos códigos de erro. Acima, vemos que o erro no campo [a] tem os seguintes códigos: [typeMismatch.actionModel01.a - typeMismatch.a - typeMismatch.java.lang.Integer - typeMismatch]. Estes códigos de erro vão 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;
Observa-se também que o código de erro no campo [a], obtido a partir de [error.getCode()], é [typeMismatch] (ver captura de ecrã acima).
Vamos colocar as mensagens de erro num ficheiro de propriedades:
![]() |
O ficheiro [messages.properties] acima ficará da seguinte forma:
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.
Recorde-se que os códigos de erro para os dois campos são:
- [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á ausente;
- [NotNull.actionModel01.b - NotNull.b - NotNull.java.lang.Double - NotNull] quando o parâmetro [b] estiver ausente;
O ficheiro [messages.properties] deve incluir uma mensagem de erro para todos os casos de erro possíveis. No caso de:
- os parâmetros [a] e [b] estiverem ausentes, será utilizado o código [NotNull];
- no caso de o parâmetro [a] estar incorreto, definimos 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 que o ficheiro [messages.properties] seja utilizado, é necessário 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 passa a configurar 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: encontram-se aqui as anotações de configuração que anteriormente estavam na classe [Application];
- linha 14: para configurar uma aplicação Spring MVC, é necessário estender a classe [WebMvcConfigurerAdapter];
- linha 15: a anotação [@Bean] introduz um componente Spring, um singleton;
- linha 16: define-se um bean denominado [messageSource] (o nome do método). Este bean serve para definir os ficheiros de mensagens da aplicação e deve obrigatoriamente ter este nome;
- linhas 17-19: indicam ao Spring que o ficheiro de mensagens:
- se encontra na pasta [i18n] no Classpath do projeto (linha 18),
- chama-se [messages.properties] (linha 18). Na verdade, o termo [messages] é a raiz dos nomes dos ficheiros de mensagens, e não o próprio nome. Veremos que, no âmbito da internacionalização, podem existir vários ficheiros de mensagens, um por cada cultura suportada. Assim, podemos ter [messages_fr.properties] para a língua francesa e [messages_en.properties] para a língua inglesa. Os sufixos adicionados à raiz [messages] são padronizados. Não se pode colocar qualquer coisa;
No projeto STS, é necessário colocar a pasta [i18n] na pasta de recursos, uma vez que esta é incluída no Classpath do projeto:
![]() |
Para utilizar este ficheiro, criamos a seguinte nova ação:
// validação de um modelo, gestão de mensagens de erro ------------------------
@RequestMapping(value = "/m25", method = RequestMethod.GET)
public Map<String, Object> m25(@Valid ActionModel01 data, BindingResult result, HttpServletRequest request)
throws Exception {
// o dicionário de resultados
Map<String, Object> map = new HashMap<String, Object>();
// o contexto da aplicação Spring
WebApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(request.getServletContext());
// local
Locale locale = RequestContextUtils.getLocale(request);
// erros?
if (result.hasErrors()) {
StringBuffer buffer = new StringBuffer();
for (FieldError error : result.getFieldErrors()) {
// pesquisa da mensagem de erro com base nos códigos de erro
// a mensagem é procurada nos ficheiros de mensagens
// os códigos de erro apresentados numa tabela
String[] codes = error.getCodes();
// na forma de cadeia de caracteres
String listCodes = String.join(" - ", codes);
// Pesquisa
String msg = null;
int i = 0;
while (msg == null && i < codes.length) {
try {
msg = ctx.getMessage(codes[i], null, locale);
} catch (Exception e) {
}
i++;
}
// foi encontrada?
if (msg == null) {
throw new Exception(String.format("Indiquez un message pour l'un des codes [%s]", listCodes));
}
// foi encontrado - adiciona-se a mensagem de erro à lista de mensagens de erro
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]. Explicamos 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 do Spring. Este contexto contém todos os beans Spring da aplicação. Permite também aceder aos ficheiros de mensagens;
- linha 10: recuperamos a localização da aplicação. Este termo é explicado mais adiante;
- linhas 15-31: para cada erro, procuramos uma mensagem correspondente a um destes códigos de erro. Estes são pesquisados pela ordem dos códigos encontrados em [error.getCodes()]. Assim que uma mensagem for encontrada, paramos;
- linha 26: como recuperar uma mensagem no [messages.properties]:
- o primeiro parâmetro é o código procurado no [messages.properties],
- o segundo é um conjunto de parâmetros, pois, por vezes, as mensagens são configuradas. Não é esse o caso aqui,
- o terceiro é a localização utilizada (obtida na linha 10). A localização indica o idioma utilizado: [fr_FR] para o francês da França, [en_US] para o inglês dos USA. A mensagem é procurada em messages_[locale].properties, por exemplo, em [messages_fr_FR.properties]. Se este ficheiro não existir, a mensagem é procurada em [messages_fr.properties]. Se esse ficheiro não existir, a mensagem é procurada em [messages.properties]. É este último caso que nos servirá;
- linhas 25-29: de forma um pouco inesperada, quando se procura um código inexistente num ficheiro de mensagens, obtém-se uma exceção em vez de um ponteiro nulo;
- linhas 33-35: tratamos o caso em que não existe mensagem de erro;
- linhas 37-38: construímos a cadeia de erro. Nesta, incluímos a localização e a mensagem de erro encontrada;
Eis alguns exemplos de execução:
![]() |
Vemos que:
- a localização da aplicação é [fr_FR]. Trata-se de um valor por predefinição, uma vez que não fizemos nada para a inicializar;
- que a mensagem utilizada para os dois 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 explorados pela ordem da tabela [error.getCodes()]. Verifica-se que esta ordem vai do código mais específico para o código 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 personalizar as mensagens de erro em francês, gostaríamos de as ter também em inglês, o que nos leva à internacionalização de uma aplicação Spring MVC. Para gerir esta internacionalização, vamos expandir a classe de configuração [Config], que passa a ter o seguinte aspeto:
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]. Esta classe inspeciona o pedido recebido antes de este ser processado por uma ação. Aqui, o interceptor [localeChangeInterceptor] irá procurar um parâmetro denominado [lang] na solicitação recebida, GET ou POST e alterará a localização da aplicação de acordo com esse parâmetro. Assim, se o parâmetro for [lang=en_US], a localização da aplicação passará a ser o inglês de USA;
- linhas 34-37: redefine-se o método [WebMvcConfigurerAdapter.addInterceptors] para adicionar o interceptor anterior;
- linhas 39-45: servem para definir a forma como a localização será encapsulada num cookie. Sabe-se que um cookie pode servir de 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 o nome [lang] a este cookie. O cookie também é utilizado para alterar a localização;
- linha 43: indica que, na ausência do cookie [lang], a localização será [fr];
Em resumo, a localização de uma solicitação pode ser definida de duas formas:
- passando um parâmetro denominado [lang];
- enviando um cookie denominado [lang]. Este cookie é criado automaticamente após a execução do método anterior;
Para utilizar esta localização, vamos 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]. Recorde-se que o ficheiro [messages.properties] é utilizado quando não é encontrado nenhum ficheiro correspondente à configuração regional do pedido. No nosso caso, se o utilizador enviar um parâmetro [lang=en], como o ficheiro [messages_en.properties] não existe, será utilizado o ficheiro [messages.properties]. O utilizador receberá, portanto, mensagens em inglês.
Vamos experimentar. Em primeiro lugar, no ambiente de desenvolvimento do Chrome (Ctrl-Shift-I), verifique os seus cookies:
![]() |
Se tiver um cookie chamado [lang], elimine-o. Em seguida, no Chrome, solicite o URL e o [http://localhost:8080/m25]:
![]() |
O navegador enviou os seguintes cabeçalhos HTTP:
Vemos que, nestes cabeçalhos, não existe nenhum cookie [lang]. Neste caso, o nosso código utiliza a localização [fr]. É isso que mostra a captura de ecrã. Vamos tentar outro caso:
![]() |
- em [1], passámos o parâmetro [lang=en] para alterar a localização para [en];
- em [2], vemos a nova configuração regional;
- em [3], a mensagem foi alterada para inglês;
Vejamos agora as trocas em HTTP:
![]() |
Vemos acima que o servidor devolveu um cookie [lang]. Isto tem uma consequência importante: a localização da próxima solicitação será novamente [en] devido ao cookie [lang] que será devolvido pelo navegador. Por isso, devemos manter as mensagens em inglês. Vamos verificar isso:
![]() |
Acima, vemos que a localização permaneceu como [en]. Devido ao cookie que o navegador envia sistematicamente, permanecerá assim enquanto o utilizador não a alterar, enviando o parâmetro [lang] da seguinte forma:
![]() |
4.21. [/m26]: injeção da localização no modelo da ação
No exemplo anterior, vimos uma forma de recuperar a localização da solicitação:
@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);
// erros?
A localização pode ser inserida diretamente nos parâmetros da ação. Eis um exemplo:
@RequestMapping(value = "/m26", method = RequestMethod.GET)
public String m26(Locale locale) {
return String.format("locale=%s", locale.toString());
}
![]() | ![]() |
![]() |
Como se pode ver acima, não há verificação da validade da localização solicitada. No entanto, o pedido seguinte do navegador provoca uma exceção no lado do servidor, uma vez que o cookie de localização que recebe está incorreto.
4.22. [/m27]: verificar a validade de um modelo com o Hibernate Validator
Consideremos a seguinte nova ação:
//validação de um modelo com o 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>();
// erros?
if (result.hasErrors()) {
// navegação pela lista de erros
for (FieldError error : result.getFieldErrors()) {
map.put(error.getField(),
String.format("[message=%s, codes=%s]", error.getDefaultMessage(), String.join("|", error.getCodes())));
}
} else {
// sem erros
map.put("data", data);
}
return map;
}
Temos aqui 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 [champ, message] com:
- campo: o campo com erro,
- mensagem: a mensagem de erro associada, bem como a lista de códigos de erro;
- linha 14: se não houver erros, devolve-se a cadeia jSON com os valores lançados;
Na linha 3, utiliza-se o modelo de ação [ActionModel02] seguinte:
![]() |
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 e setters
...
}
A classe utiliza restrições de validação provenientes de dois pacotes:
- [javax.validation.constraints] nas linhas 5-13;
- [org.hibernate.validator.constraints] nas linhas 15-19;
As dependências Maven destes dois pacotes estão presentes no projeto:
![]() |
Aqui, não vamos utilizar mensagens internacionalizadas, mas sim mensagens definidas no interior da restrição com o atributo [message]. Para testar esta ação, vamos utilizar o [Advanced Rest Client]:
![]() |
- em [1-2], a consulta POST;
- em [3], o cabeçalho HTTP [Content-Type] a utilizar;
- em [4], a ligação [Add new value] permite adicionar um par [paramètre, value];
- em [5], inserir 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 errado 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] relativa aos campos vazios foi acionada e a mensagem de erro associada foi apresentada;
- em [8], a mensagem do campo [assertFalse] para o qual a restrição [@AssertFalse] não foi verificada, bem como os códigos desse erro. Recorde-se que estes códigos podem estar associados a mensagens internacionalizadas;
Eis outro exemplo:
![]() |

Convidamos o leitor a testar os diferentes casos de erro até ao POST, cujos dados são todos válidos:
![]() | ![]() |
Nota: o formato das datas é o formato anglo-saxónico: mm/dd/aaaa.
4.23. [/m28]: externalização das mensagens de erro
Na classe [ActionModel02], colocámos as mensagens «fixas». É 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 e setters
...
}
As mensagens de erro são externalizadas para os ficheiros [messages.properties]:
![]() |
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
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
As mensagens de erro foram adicionadas às linhas 4-16. Apresentam-se da seguinte forma:
Os códigos não podem ser quaisquer. São os que foram apresentados na ação anterior [/m27]. Por exemplo:
![]()
Nos ficheiros de mensagens, é necessário utilizar um dos quatro códigos acima referidos no 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:
// ----------------------- externalização das mensagens de erro ------------------------
@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>();
// o contexto da aplicação Spring
WebApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(request.getServletContext());
// local
Locale locale = RequestContextUtils.getLocale(request);
// erros?
if (result.hasErrors()) {
for (FieldError error : result.getFieldErrors()) {
// Pesquisa da mensagem de erro com base nos códigos de erro
// a mensagem é procurada nos ficheiros de mensagens
// os códigos de erro apresentados numa tabela
String[] codes = error.getCodes();
// na forma de cadeia de caracteres
String listCodes = String.join(" - ", codes);
// Pesquisa
String msg = null;
int i = 0;
while (msg == null && i < codes.length) {
try {
msg = ctx.getMessage(codes[i], null, locale);
} catch (Exception e) {
}
i++;
}
// foi encontrado?
if (msg == null) {
msg = String.format("Indiquez un message pour l'un des codes [%s]", listCodes);
}
// foi encontrado - adiciona-se o erro ao dicionário
map.put(error.getField(), msg);
}
} else {
// sem erros
map.put("data", data);
}
return map;
}
Já comentámos este tipo de código. A única coisa realmente importante é a linha 23: a mensagem de erro obtida depende da configuração regional do pedido.
Eis um exemplo em francês:
![]() | ![]() |
e agora em inglês:
![]() | ![]() |









































































