Webservices
Introdução
As features do tipo webservice fazem parte da camada de negócio da aplicação.
As regras de negócio devem ser implementadas em classes Java conforme o disposto na seção Negócio, que explana sobre o padrão Feature Service.
Toda feature possui uma classe principal @Service
, que contém o ponto de entrada da
transação de negócio. Essa classe, conforme o padrão Feature Service, encapsula as regras
de negócio relativas à funcionalidade.
Após desenvolvimento da classe de serviço @Service
, é feito seu mapeamento na forma
de webservice. Esse mapeamento é responsabilidade da classe adicional de estereótipo @Ws
,
localizada no mesmo pacote da feature.
A classe @Ws
contém o mapeamento de endpoint HTTP/REST bem como sua documentação OpenAPI / Swagger.
Ela injeta e consome a classe de serviço. Não é função da classe @Ws
implementar
regras de negócio, tão somente adequar o serviço para o protocolo HTTP.
Um “endpoint”, nos termos deste manual, é unicamente identificado pelo path da URL,
i.e., a URL sem a query string. Diferentes query strings não determinam diferentes endpoints.
Diferentes métodos HTTP também não determinam diferentes endpoints. Exemplo de endpoint HTTP:
/nomeMicrosservico/nomeFuncionalidade.v1
.
Estrutura de Pacotes
As features do tipo webservice têm a seguinte estrutura de pacotes:
Pacote | Descrição |
---|---|
.services.web.assunto.nomeFuncionalidade.versao | Funcionalidades na forma de webservices. |
Especificamente no caso de webservice, as features são versionadas, onde cada versão é um subpacote.
Exemplo de estrutura de pacotes de uma determinada feature que possui três versões:
services
web
contas
pesquisarContas
v1
v2
v3
Mais informações sobre versionamento de API são apresentadas à frente.
Convenções HTTP
As convenções para webservice são baseadas num híbrido de REST (Representational State Transfer) e RPC (Remote Procedure Call) sobre HTTP. São elas:
-
Sintaxe do path:
/{nomeMicrosservico}/{nomeFuncionalidade}.{versao}
, onde:-
{nomeFuncionalidade}
é o nome da feature, iniciando com caractere minúsculo. -
{versao}
é o sequencial de versão do endpoint (ex: “v1”, “v2”, “v3” etc).
-
-
Métodos HTTP:
-
GET
se a funcionalidade faz apenas leitura de dados. -
POST
se a funcionalidade altera o banco de dados ou executa regras transacionais do negócio (exemplo: validações). -
Demais métodos HTTP não são utilizados.
-
-
Entrada e saída:
-
Os parâmetros de entrada de endpoint GET são recebidos via query string.
-
Os parâmetros de entrada de endpoint POST são recebidos via corpo HTTP no formato
JSON
(JavaScript Object Notation), com Content-Typeapplication/json
no header do request. -
Dados de saída dos endpoints são retornados no formato
JSON
, com Content-Typeapplication/json
no header do response.
-
-
Códigos de retorno HTTP:
-
200
: Requisição processada com sucesso e dados foram retornados. -
202
: A requisição foi solicitada com sucesso. Retorna-se um UID da solicitação. Utilizado em fluxos assíncronos. -
400
: Erros no formato da requisição (ex: JSON corrompido, JSON necessário ausente, tipo inválido de parâmetro, etc). -
401
: Usuário não autenticado (ex: token JWT ausente). -
403
: Usuário não autorizado (ex: token JWT sem a permissão necessária, por exemplo, sem authority na claimroles
). -
404
: Endpoint inexistente. -
422
: Erro de violação de regra de negócio. Ocorre quando o serviço lançaBusinessException
ou quando há erro na validação declarativa de parâmetros (@Valid
). -
500
: Erro inesperado no backend. Ocorre quando o serviço lança erro diferente deBusinessException
.
-
Convenções de Tratamento de Erros
A arquitetura provê recursos automáticos relativos a tratamento de erros, gerando registros de log e retornos para o frontend conforme:
-
BusinessExceptions: Será emitido o campo
message
no Error Whitelabel do Spring Boot, com código HTTP 422. A mensagem é projetada para ser mostrada para o usuário. -
Jakarta/Hibernate Validations (
@Valid
): Será emitido o campofieldMessages
no Whitelabel contendo uma lista e{field, message}
, com código HTTP 422. O frontend poderá, opcionalmente, posicionar o erro no formulário com base no campo técnicofield
. As mensagens são projetadas para serem mostradas para o usuário. -
Outros erros de cliente código 400: Será emitido o campo
message
no Error Whitelabel contendo o nome simples da classe de erro mais a mensagem. Também será emitido o campotracking
com o código de rastreamento. O erro será logado como WARN, incluindo o rastreamento, mas sem stack trace. Preferencialmente o frontend deve mostrar o rastreamento e não a mensagem. -
Outros erros de cliente código não 400: Será emitido o campo
message
no Error Whitelabel contendo o nome simples da classe de erro mais a mensagem. Não há código de rastreamento nem log. O frontend pode mostrar uma mensagem genérica conforme o código HTTP. -
Erros de servidor: Será suprimido o campo
message
por motivo de segurança. Será emitido o campotracking
com o código de rastreamento. O erro será logado como ERROR, incluindo o rastreamento, e com stack trace. O frontend deve mostrar o rastreamento.
Resumo:
Se | Código HTTP | Retorno | Log | Interface de Usuário |
---|---|---|---|---|
BusinessException | 422 | message | não | mostrar a mensagem |
Exceção @Valid | 422 | fieldMessages | não | mostrar as mensagens de fieldMessages |
Outros erros de cliente 400 | (mesmo) | message, tracking | WARN, sem stack trace | preferencialmente mostrar o rastreamento e não a mensagem |
Outros erros de cliente não 400 | (mesmo) | message | não | mostrar uma mensagem genérica de acordo com o código |
Erros de servidor | 500 | tracking | ERROR, com stack trace | mostrar o rastreamento |
Vide anexo ao final, que contém uma lista de exemplos de erros.
As convenções da arquitetura e os tratamentos padrões acima descritos podem ser desativados via propriedade
cloudsupport.error.conventions.enabled=false
.
Estereótipos de Classes
O mapeamento do endpoint é realizado via classes de estereótipo @Ws
conforme as diretrizes
estabelecidas a seguir.
@Ws
Para feature do tipo webservice, o Cloudsupport
estabelece o estereótipo @Ws
,
que indica que o objetivo da classe é realizar e documentar o mapeamento webservice de uma
determinada funcionalidade.
A anotação @Ws
faz o Cloudsupport
realizar o mapeamento automático de endpoint,
em aderência às convenções HTTP acima.
É possível mapear webservice via anotações do Spring MVC, sem limites. Nesse caso, o código-fonte ficará mais verboso e poderá divergir dos padrões. Utilize-as somente quando for requisito técnico do projeto.
São diretrizes para classes de mapeamento webservice:
-
Herdam de
BaseWs
e são anotadas com@Ws
. -
Têm sufixo
Ws
. -
São singletons e stateless, não devendo manter estados em seus atributos.
-
Não controlam ou delimitam transação.
-
Não implementam regras de negócio.
-
Contêm 1 (um) método público que mapeia a operação da classe
@Service
correspondente. -
Não devem receber como parâmetro HTTP o
id
sequencial do banco de dados. Nesses casos, o parâmetro de identificação de determinada entidade ou resource deverá ser ouid
. -
Não precisam tratar exceções nem códigos de retorno. Há tratamento padrão no
Cloudsupport
que gera os códigos de retorno HTTP elencados nas convenções, bem como retorna umJSON
padrozinado nos casos de erro (4xx ou 5xx). -
Não precisam mapear o path do endpoint. Há tratamento padrão no
Cloudsupport
que realiza o mapeamento conforme as convenções elencadas acima. -
No método que invoca a classe serviço, possuem a anotação:
-
@GetMapping
se o serviço fizer apenas consulta de dados; ou -
@PostMapping
no caso de serviço que altera dados ou executa regras transacionais do negócio (exemplo: validações). -
Em ambos os casos não se aplica o atributo
path
, pois há tratamento automático peloCloudsupport
que realiza o mapeamento conforme as convenções definidas.
-
-
No caso de POST, possuem a anotação
@RequestBody
no DTO que representa os parâmetros de entrada. -
Têm a responsabilidade de manter a documentação da funcionalidade, incluindo ao menos um resumo das regras de negócio e descrição de uso dos parâmetros de entrada. A documentação deverá ser via OpenAPI, utilizando a anotação
@Operation
. -
Não precisam utilizar a anotação
@Tag
. Há tratamento padrão noCloudsupport
que organiza a documentação Swagger de acordo com os pacotes que agrupam as funcionalidades. -
Tipicamente não fazem LOG.
Exemplo das classes envolvidas em uma funcionalidade webservice:
services.web.contas.pesquisarContas.v1
PesquisarContasServiceV1.java
PesquisarContasWsV1.java
PesquisarContasParamsV1.java
PesquisarContasRetornoV1.java
Classe
ServiceV1
implementa as regras de negócio.Classe
WsV1
faz o mapeamento HTTP.Classes
ParamsV1
eRetornoV1
são os DTOs de entrada e saída, respectivamente, do webservice.
Exemplo de uma implementação de @Ws
:
@Ws
public class PesquisarContasWsV1 extends BaseWs {
@Inject
private PesquisarContasServiceV1 service;
@GetMapping
@Operation(summary = "Realiza pesquisa paginada de Contas")
public PesquisarContasRetornoV1 pesquisar(@Valid @ParameterObject PesquisarContasParamsV1 params) {
return service.pesquisar(params);
}
}
Versionamento
As features de webservice são, por boa prática, versionadas.
Diretrizes:
-
O pacote da cada feature deve ser subdividido em versões, começando de v1. Exemplo:
services.web.contas.pesquisarContas.v1
. -
O sufixo da versão deve ser incluído em todas as classes que são beans do Spring (para evitar conflitos de nomes), como por exemplo nas classes
@Servive
,@Fragment
e@Ws
, e também nos DTOs de entrada/saída (por uma limitação do Swagger). Sintaxe: “V1”, “V2”, “V3” etc. -
Não há necessidade de versionamento quando a mudança na funcionalidade for retrocompatível. O versionamento da feature deve ser realizado quando a manutenção evolutiva não garantir a retrocompabilidade da API.
-
Quando ocorrer o versionamento, todas as classes da feature são clonadas para a nova versão e então adaptadas. Classes de uma determinada versão não podem refazer referência a classes de outra versão.
São exemplos de manutenções, a princípio, retrocompatíveis:
-
Inclusão de novos atributos no retorno.
-
Inclusão de novos parâmetros de entrada.
-
Aumento do domínio de valores em parâmetros de entrada existentes.
São exemplos de manutenções, a princípio, não retrocompatíveis:
-
Remoção de atributos no retorno.
-
Remoção de parâmetros de entrada.
-
Redução do domínio de valores em parâmetros de entrada existentes.
-
Mudanças em regras de processamento tal que o retorno é diferente para uma mesma combinação dos parâmetros de entrada.
-
Mudanças no formato ou semântica de atributos de retorno ou de parâmetros de entrada.
Validações de Parâmetros
As validações estáticas e básicas de parâmetros de endpoint (ex: obrigatoriedade, tipo, máscara)
podem implementadas declarativamente via Jakarta Validation, com uso do @Valid
. Neste caso, os erros
de validação são retornados no atributo fieldMessages
do JSON de retorno, com código HTTP 400, na forma
de uma lista de {field, message}
. A interface gráfica pode posicionar os erros nos respectivos campos de
formulário ou apenas exibir a lista de mensagens.
É importante que o backend configure uma mensagem amigável, legível e completa em cada anotação do
Jakarta Validation (atributo message
). Se a mensagem for uma instrução completa para o usuário,
por exemplo “Informe o nome” no lugar da frase genérica “obrigatório”, o frontend poderá optar por apenas
mostrar a lista de mensagens, sem obrigatoriedade de precisar posicionar cada mensagem no respectivo
campo do formulário (que seria com base no atributo field
), pois este pode ser um requisito limitador
para o frontend.
Outras validações são implementadas programaticamente na classe de serviço, que podem incluir validações
parâmetros de endpoint (ex: o valor do parâmetro não faz parte do domínio possível) ou outras regras
de negócio mais avançadas. As classes de serviço emitem erro de violação de regra de negócio via
BusinessException
. Nesses casos, os erros são retornados no atributo message
do JSON de retorno,
com código HTTP 422.
Observação: O Swagger não documenta automaticamente as anotações do Jakarta Validation, tão somente a obrigatoriedade de campos (ex: @NotNull, @NotEmpty, @NotBlank).
Anexos
I - Exemplos de Erros
A seguir constam exemplos de retorno HTTP relativos a erros mais comuns.
Erro de requisição não autenticada (ex: sem token JWT):
401 (corpo response HTTP vazio)
Erro de requisição não autorizada (ex: JWT sem permissão necessária):
403 (corpo response HTTP vazio)
Erro de regra de negócio (BusinessException):
{
"timestamp": "2024-06-10T14:00:04.398+00:00",
"status": 422,
"error": "Unprocessable Entity",
"path": "/arquetipo/cadastrarExecutor.v1",
"message": "O tipo de executor é inválido"
}
Erro de bind por violação de @Valid (MethodArgumentNotValidException):
{
"timestamp": "2024-06-10T14:00:40.182+00:00",
"status": 422,
"error": "Unprocessable Entity",
"path": "/arquetipo/cadastrarExecutor.v1",
"fieldMessages": [
{
"field": "nome",
"message": "Informe o nome do executor"
},
{
"field": "tipoExecutorId",
"message": "Informe o tipo do executor"
}
]
}
Erro de bind por formato inválido de parâmetro (HttpMessageNotReadableException):
{
"timestamp": "2024-06-10T14:01:55.422+00:00",
"status": 400,
"error": "Bad Request",
"path": "/arquetipo/cadastrarExecutor.v1",
"message": "HttpMessageNotReadableException: JSON parse error: Cannot deserialize value of type `java.lang.Integer` from String \"x\": not a valid `java.lang.Integer` value",
"tracking": "240610-k1f9-24aw872660w00"
}
Log:
11:01:55.427 WARN dsupport.services.web.WebErrorAttributes : Client error: Unexpected exception, tracking: 240610-k1f9-24aw872660w00, error: [HttpMessageNotReadableException: JSON parse error: Cannot deserialize value of type `java.lang.Integer` from String "x": not a valid `java.lang.Integer` value], root cause: [InvalidFormatException: Cannot deserialize value of type `java.lang.Integer` from String "x": not a valid `java.lang.Integer` value
at [Source: REDACTED (`StreamReadFeature.INCLUDE_SOURCE_IN_LOCATION` disabled); line: 3, column: 21] (through reference chain: dominio.arquetipo.services.web.executor.cadastrarExecutor.v1.CadastrarExecutorParamsV1["tipoExecutorId"])]
Erro de bind por JSON malformado no request (HttpMessageNotReadableException):
{
"timestamp": "2024-06-10T14:02:38.632+00:00",
"status": 400,
"error": "Bad Request",
"path": "/arquetipo/cadastrarExecutor.v1",
"message": "HttpMessageNotReadableException: JSON parse error: Unexpected character ('\"' (code 34)): was expecting comma to separate Object entries",
"tracking": "240610-k1f9-24aw87kf28w00"
}
Log:
11:02:38.632 WARN dsupport.services.web.WebErrorAttributes : Client error: Unexpected exception, tracking: 240610-k1f9-24aw87kf28w00, error: [HttpMessageNotReadableException: JSON parse error: Unexpected character ('"' (code 34)): was expecting comma to separate Object entries], root cause: [JsonParseException: Unexpected character ('"' (code 34)): was expecting comma to separate Object entries
at [Source: REDACTED (`StreamReadFeature.INCLUDE_SOURCE_IN_LOCATION` disabled); line: 3, column: 3]]
Erro de bind por JSON ausente no request (HttpMessageNotReadableException):
{
"timestamp": "2024-06-10T14:06:24.882+00:00",
"status": 400,
"error": "Bad Request",
"path": "/arquetipo/cadastrarExecutor.v1",
"message": "HttpMessageNotReadableException: Required request body is missing: public cloudsupport.services.Uid dominio.arquetipo.services.web.executor.cadastrarExecutor.v1.CadastrarExecutorWsV1.cadastrar(dominio.arquetipo.services.web.executor.cadastrarExecutor.v1.CadastrarExecutorParamsV1)",
"tracking": "240610-k1f9-24aw8h42f3w00"
}
Log:
11:06:24.883 WARN dsupport.services.web.WebErrorAttributes : Client error: Unexpected exception, tracking: 240610-k1f9-24aw8h42f3w00, error: [HttpMessageNotReadableException: Required request body is missing: public cloudsupport.services.Uid dominio.arquetipo.services.web.executor.cadastrarExecutor.v1.CadastrarExecutorWsV1.cadastrar(dominio.arquetipo.services.web.executor.cadastrarExecutor.v1.CadastrarExecutorParamsV1)], root cause: [self]
Erro endpoint não encontrado:
{
"timestamp": "2024-06-10T14:08:11.902+00:00",
"status": 404,
"error": "Not Found",
"message": "NoResourceFoundException: No static resource arquetipo/cadastrarExecutor.v2.",
"path": "/arquetipo/cadastrarExecutor.v2"
}
Erro de exceção não tratada (RuntimeException):
{
"timestamp": "2024-06-10T14:24:11.788+00:00",
"status": 500,
"error": "Internal Server Error",
"path": "/arquetipo/cadastrarExecutor.v1",
"tracking": "240610-k1f9-24aw9h8a8x700"
}
Log:
11:24:11.790 ERROR dsupport.services.web.WebErrorAttributes : Unexpected exception, tracking: 240610-k1f9-24aw9h8a8x700, error: [RuntimeException: Could not connect to the gateway], root cause: [self]
java.lang.RuntimeException: Could not connect to the gateway
at dominio.arquetipo.services.web.executor.cadastrarExecutor.v1.CadastrarExecutorServiceV1.cadastrar(CadastrarExecutorServiceV1.java:25)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:568)
at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:354)
at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:196)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163)
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:768)
at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:123)
at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:392)
at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:119)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:768)
at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:720)
at dominio.arquetipo.services.web.executor.cadastrarExecutor.v1.CadastrarExecutorServiceV1$$SpringCGLIB$$0.cadastrar(<generated>)
at dominio.arquetipo.services.web.executor.cadastrarExecutor.v1.CadastrarExecutorWsV1.cadastrar(CadastrarExecutorWsV1.java:45)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:568)
(...)
Próximos Passos
A próxima leitura sugerida é a seção Rotinas, que detalha os Jobs da camada de negócio.