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-Type application/json no header do request.

    • Dados de saída dos endpoints são retornados no formato JSON, com Content-Type application/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 claim roles).

    • 404: Endpoint inexistente.

    • 422: Erro de violação de regra de negócio. Ocorre quando o serviço lança BusinessException 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 de BusinessException.

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 campo fieldMessages 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écnico field. 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 campo tracking 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 campo tracking 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 o uid.

  • 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 um JSON 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 pelo Cloudsupport 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 no Cloudsupport 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 e RetornoV1 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:

  1. Inclusão de novos atributos no retorno.

  2. Inclusão de novos parâmetros de entrada.

  3. 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:

  1. Remoção de atributos no retorno.

  2. Remoção de parâmetros de entrada.

  3. Redução do domínio de valores em parâmetros de entrada existentes.

  4. Mudanças em regras de processamento tal que o retorno é diferente para uma mesma combinação dos parâmetros de entrada.

  5. 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.