Spring API Tools
Simplifica o desenvolvimento de APIs, através de abstrações e padronizações que normalmente realizamos de forma repetitiva, durante o desenvolvimento.
Features
- Abstrações para Repository, Service e Resource.
- Desenvolvimento de API para CRUD sem necessidade de implementar código. Suporte aos métodos GET, POST, DELETE, PUT e PATH
- Suporte a NoDelete annotation, para gerenciar delete lógico.
- Suporte a Find e FindAttribute annotation para configurar filtros.
- Configuração básica para exception handler, através de @RestControllerAdvice.
- Suporte a query nos resources sem a implementação de código. Resource {path}/query, através de método GET e POST.
- Consultas com configuração de join e fecth de forma dinâmica e sem codificação.
- Suporte a query com definição de visualização, onde é possível definir quais atributos devem ser retornados.
Como utilizar
Configure o Spring para carregar as configurações do Spring-API-Tools.
Dependência
<dependency>
<groupId>com.github.eoscode</groupId>
<artifactId>spring-api-tools</artifactId>
<version>1.3.0-RELEASE</version>
</dependency>
Carregando o spring-api-tools com @Configuration do Spring
package com.eoscode.springapitools.sample.config;
import com.eoscode.springapitools.config.SpringApiToolsScan;
import org.springframework.context.annotation.Configuration;
@Configuration
public class SpringApiToolsScanConfig extends SpringApiToolsScan {}
Entity
As entidades devem implementar a interface Identifier para indicar o atributo que representa a chave da entidade.
package com.eoscode.springapitools.sample.entity;
import com.eoscode.springapitools.data.domain.Identifier;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import org.hibernate.annotations.GenericGenerator;
import javax.persistence.*;
@Entity
@Table(name = "city")
@NamedEntityGraphs(
@NamedEntityGraph(name = "City.findDetailById", attributeNodes = {
@NamedAttributeNode("state")
})
)
public class City implements Identifier<String> {
@Id
@Column(name = "id")
@GenericGenerator(name = "uuid", strategy = "org.hibernate.id.UUIDGenerator")
@GeneratedValue(generator = "uuid")
private String id;
@Column(name = "name", length = 60)
private String name;
@JsonIgnoreProperties({"cities"})
@JoinColumn(name = "state_id", updatable = false, insertable = false)
@ManyToOne(fetch = FetchType.LAZY)
private State state;
@Column(name = "state_id")
private String stateId;
@Column(name = "population")
private long population;
}
@NamedEntityGraphs
Também é possível utilizar @NamedEntityGraphs para ajustar alguns comportamentos default. Para isso, é necessário informar o @NamedEntityGraph com os seguintes nomes:
- {Entity}.findById - Altera o comportamento default para busca por id.
- {Entity}.findDetailById - Altera o comportamento default da busca detalhada por id.
Observações:
- Os
@NamedEntityGraphdevem ser declarados da seguite forma: nome da entidade + nome da query. Ex.:{Entity}.findById. - O
findDetailByiddeve ser definido quando for necessário um carregamento diferente do mapeamento da entidade, realizado através defetch = FetchType.LAZYdo JPA. - Por padrão, será utilizado o
findByIdda implementação doSpring Data, caso identificado um@NamedEntityGraph, ele será selecionado de forma prioritária.
Repository
A classe Repository, deve especializar a implementação do framework com.eoscode.springapitools.data.repository.Repository.
package com.eoscode.springapitools.sample.repository;
import com.eoscode.springapitools.data.repository.Repository;
import com.eoscode.springapitools.sample.entity.City;
@org.springframework.stereotype.Repository
public interface CityRepository extends Repository<City, String> {}
Service
A classe Service, deve especializar AbstractService, que implementa as rotinas para save, update, delete, find, findById, query e etc.
package com.eoscode.springapitools.sample.service;
import com.eoscode.springapitools.sample.entity.City;
import com.eoscode.springapitools.sample.repository.CityRepository;
import com.eoscode.springapitools.service.AbstractService;
import org.springframework.stereotype.Service;
@Service
public class CityService extends AbstractService<CityRepository, City, String> {}
Resource
A classe Resource, deve especializar AbstractResource ou AbstractRepositoryResource.
| Path | Método | Resposta HTTP | Descrição |
|---|---|---|---|
| {path}/{id} | GET | 200 | Realiza consulta pelo id da entidade. |
| {path}/detail/{id} | GET | 200 | Realiza consulta detalhada pelo id da entidade. Utiliza findDetailByI definido através de @NamedEntityGraph. |
| {path}/ | POST | 201 | Salva a entidade. Devolve o header Location, indicando o caminho para o recurso. |
| {path}/ | PUT | 204 | Atualiza a entidade. |
| {path}/ | PATH | 204 | Atualiza a entidade de forma parcial, aplicando o update apenas aos atributos enviados. |
| {path}/{id} | DELETE | 204 | Remove a entidade, utilizando o id. |
| {path}/ | GET | 200 | Realiza filtro nos atributos da entidade. Também pode ser acessado através de {path}/{find}. Por padrão, utiliza valores exatos, ou seja, o operador = (igual). Retorna uma lista da entidade consulta. Contudo, podemos usar o parâmetro pageable com valor true para retornar o tipo Page do Spring. |
| {path}/page | GET | 200 | Realiza filtro nos atributos da entidade. Também pode ser acessado através de {path}/find/page. Por padrão, utiliza valores exatos, ou seja, o operador = (igual). Retorna o tipo Page do Spring. |
| {path}/query | GET | 200 | Realiza query nos atributos da entidade, com suporte a múltiplos operadores. Retorna uma lista da entidade consulta. Contudo, podemos usar o parâmetro pageable com valor true para retornar o tipo Page do Spring |
| {path}/query/page | GET | 200 | Realiza query nos atributos da entidade, com suporte a múltiplos operadores. Retorna o tipo Page do Spring. |
| {path}/query | POST | 200 | Realiza query nos atributos da entidade, com suporte a múltiplos operadores. Retorna uma lista da entidade consulta. Contudo, podemos usar o parâmetro pageable com valor true para retornar o tipo Page do Spring. Obs.: Utiliza requisição JSON. |
| {path}/query/page | POST | 200 | Realiza query nos atributos da entidade, com suporte a múltiplos operadores. Retorna o tipo Page do Spring. Obs.: Utiliza requisição JSON. |
| {path}/all | GET | 200 | Lista todas as entidades associadas ao recurso. |
AbstractResource
Disponibiliza as funcionalidades comuns de uma API CRUD. Contudo, permite o desenvolvimento de novas funcionalidades, através do AbstractService.
package com.eoscode.springapitools.sample.resource;
import com.eoscode.springapitools.resource.AbstractResource;
import com.eoscode.springapitools.sample.entity.City;
import com.eoscode.springapitools.sample.service.CityService;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/cities")
public class CityResource extends AbstractResource<CityService, City, String> {}
AbstractRepositoryResource
Disponibiliza as funcionalidades comuns de um CRUD padrão, que não requer o desenvolvimento de funcionalidades adicionais.
package com.eoscode.springapitools.sample.resource;
import com.eoscode.springapitools.resource.AbstractRepositoryResource;
import com.eoscode.springapitools.sample.entity.City;
import com.eoscode.springapitools.sample.repository.CityRepository;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/cities")
public class CityResource extends AbstractRepositoryResource<CityRepository, City, String> {}
Consultas
Por padrão, todas as consultas são realizadas com operador lógico and, quando possuem mais de um filtro. Para alterar esse comportamento, utilize o parâmetro operator, que suporta o valor and e or.
As consultas passaram a ser executadas com paginação desabilitada, a partir da versão 1.1.0. Sendo assim, devemos utilizar os novos recursos {path}/page e {path}/query/page para realizar consultas paginadas ou utilizar {path}/ e {path}/query com o parâmetro pageable informando valor true, para indicar que a consulta deve retornar o tipo Page do Spring.
Outra forma de configurar o comportamento da paginação, é através do arquivo de configuração do Spring application.proporties ou application.yml:
spring-api-tools:
enable-default-pageable: false //valor default
O valor true, irá indicar que toda consulta deve retornar o tipo Page do Spring. O parâmetro pageable, enviado nas consultas, pode ser utilizado para mudar o comportamento padrão.
Obs.: A paginação e ordenação, utilizam as configurações padrões e sintaxe do Spring. Sendo assim, devemos consultar a documentação do framework, se necessário alterar algum comportamento padrão.
spring:
data:
rest:
default-page-size: 10 //valor default do spring
As consultas realizadas sem paginação, suportam o parâmetro size para impor um limite na quantidade de registros retornados. Essa configuração pode ser realizada por consulta ou de forma global, no application.yml do Spring.
Também é possível desabilitar o override do size nas consultas, e forçar o uso da configuração global.
spring-api-tools:
list-default-size-override: true //permite alterar o valor do size nas consultas
list-default-size: 0 //valor limite desabilitado. Se diferente de zero, ira impor o limite
Filtros aplicados a atributos do tipo string, realizam comparação com case sensitive habilitado. Para alterar o comportamento, configure o parâmetro string-case-sensitive, que indica se o valor informado na expressão, será no formato lowercase ou uppercase.
spring-api-tools:
string-case-sensitive: lowercase
Tipos suportados
| Tipo | Operadores |
|---|---|
| String | =, !=, $like, $notLike, $startsWith, $endsWth, $btw, $in, $isNull, $isNotNull |
| Integer / int | =, !=, >, >=, <, <=, $in, $btw, $isNull, $isNotNull |
| Long / long | =, !=, >, >=, <, <=, $in, $btw, $isNull, $isNotNull |
| Double / double | =, !=, >, >=, <, <=, $in, $btw, $isNull, $isNotNull |
| Boolean / boolean | =, !=, $isNull, $isNotNull |
| BigDecimal | =, !=, >, >=, <, <=, $in, $btw, $isNull, $isNotNull |
| Date | =, !=, >, >=, <, <=, $in, $btw, $isNull, $isNotNull Obs.: Não suportado por {path}/ e {path}/find. Suporta representação da data no formato timestamp (long) e ISO 8601 no formato UTC time zone (String) |
| List | $size, $isEmpty, $isNotEmpty |
| Set | $size, $isEmpty, $isNotEmpty |
Join e Fetch
As consultas suportam configuração automática de join, baseada na sintaxe atributo.atributo, onde a primeira ocorrência, indica o atributo da Entidade consultada e mapeada no JPA com OneToMany, ManyToMany ou ManyToOne. A segunda ocorrência, o atributo da classe que foi associada ao mapeamento e que será aplicado o filtro. Sendo assim, quando realizamos filtros utilizando essa sintaxe, é realizado um left outer join de forma automática, possibilitando consultas com um nível de profundidade.
Para indicar que a consulta deve retornar os dados da associação join, é possível utilizar o parâmetro fetches nas consultas com método GET, onde se espera uma lista com o nome dos atributos que representam as associações. Ou o parâmetro fetch, introduzido no filtro das consultas com método POST.
As consultas realizadas com mensagens do tipo json e método POST são mais flexíveis, em relação à configuração do join, uma vez que permitem indicar se o join será do tipo inner outer join ou left outer join (default), além de uma configuração centralizada para o fetch.
Consultar as cidades, onde o nome do Estado inicia com pe.
GET
/api/cities/query?opt=state.name=%startsWithpe&fetches=state
POST
{
"filters": [
{
"field": "state.name",
"operator": "$startsWith",
"value": "pe",
"fetch":true
}
]
}
ou
{
"filters": [
{
"field": "state.name",
"operator": "$startsWith",
"value": "pe"
}
],
"joins": [
{
"field": "state",
"fetch": "true"
}
]
}
Esse comportamento pode ser desabilitado no arquivo de configuração do Spring.
spring-api-tools:
query-with-join-configuration: false // desabilitar o suporte a consultas com configuracao de join
{path}/ e {path}/find
As funcionalidades disponíveis em AbstractResouce e AbstractRepositoryResource, possibilitam aplicar filtros nos atributos da entidade.
Por padrão, todos os atributos são suportados (tipos primitivos e objetos). Contudo, os tipos primitivos são configurados para ignorar o valor default. Ex.: Para os atributos do tipo int e long, o valor zero é ignorado, para boolean, o valor false.
Para mudar esse comportamento, temos a configuração ignoreDefaultValue na annotation Find, que por padrão, informa que os valores default, devem ser ignorados. Se necessário, podemos configurar uma lista de atributos que devem ser exceção ao tratamento default supportedDefaultValueForAttributes.
A annotation FindAttribute, possui um comportamento similiar ao Find, porém, aplicado aos atributos.
{path}/query
Diferente do /find, o suporte a /query, permite realizar consultas com um conjunto maior de operadores.
Operadores
| Operador | Descrição | GET | POS |
|---|---|---|---|
| > | Maior que | [x] | [x] |
| >= | Maior ou igual que | [x] | [x] |
| < | Menor que | [x] | [x] |
| <= | Menor ou igual que | [x] | [x] |
| = | Igual a | [x] | [x] |
| != | Diferente de | [x] | [x] |
| $like | Contém o valor | [x] | [x] |
| $notLike | Não contém o valor | [x] | [x] |
| $isNull | Valor é NULL | [x] | [x] |
| $isNotNull | Valor não é NULL | [x] | [x] |
| $btw | Entre valores Sintaxe: "10;50" (deve ser informado como String) | [x] | [x] |
| $in | Algum dos valores Sintaxe: "2;4;5;6" (deve ser informado como String) | [x] | [x] |
| $size | Verifica o número de ocorrências na coleção, com suporte aos operadores: >, >=, <, <=, =, !=. Sintaxe: operador;valor. Ex.: ">=;2" (deve ser informado como String). Obs.: Aplicado a atributos do tipo coleção. |
[ ] | [x] |
| $isEmpty | Verifica se a coleção está vazia Obs.: Aplicado a atributos do tipo coleção. |
[x] | [x] |
| $isNotEmpty | Verifica se a coleção não está vazia Obs.: Aplicado a atributos do tipo coleção. |
[x] | [x] |
Exemplos:
Método GET
Para consultas com método GET, devemos utilizar a sintaxe atributo + operador + valor. Quando mais de um filtro for aplicado a consulta, devemos utilizar o separador , para indicar o limite de cada filtro. Os filtros são informados através do parâmetro opt.
Os filtros que utilizam operadores que não suportam valor, tais como: $isNull, $isNotNull, $isEmpty, $isNotEmpty, devem omitir o valor. Ex.: name$isNotNull.
A partir da versão 1.1.0, podemos utilizar o parâmetro filters com sintaxe multiple values para informar múltiplos filtros.
/query?filters=population>=40000&filters=rate=5.5&operator=or&sort=population,desc
Exemplos:
- Listar as cidades com população maior ou igual a
40000habitantes ou rate igual a 5.5, ordenado pelo número de habitantes de forma decrescente
/query?opt=population>=40000,rate=5.5&operator=or&sort=population,desc
- Listar as cidades com stateId igual a
52e0a6a7-d72d-4b0f-bab9-aebfcf888e21e população maior que20000habitantes
/api/cities/query?opt=stateId=52e0a6a7-d72d-4b0f-bab9-aebfcf888e21,population>20000
- Listar as cidades com população entre
40000e550000habitantes
/api/cities/query?opt=population$btw40000;55000
- Listar os estados que possuem cidades com população maior ou igual a
50000habitantes
/api/state/query?opt=cities.population>=50000&distinct=true
- Listar os estados que foram fundados no dia 14/04/20
/api/state/query?opt=dateOfFoundation=2020-04-14T22:42:53Z
/api/state/query?opt=dateOfFoundation=1586833200000
Obs.:
- As consultas suportam
org.springframework.data.domain.Pageable(parâmetro page e size), com os valores default do Spring. - As consultas suportam
org.springframework.data.domain.Sort, com os valores default do Spring. - O valor default do parâmetro
distincté true. Sendo assim, pode ser omitido.
Método POST
O Layout da consulta, segue a seguinte definição:
{
"operator": "and",
"filters": [
{
"field": "population",
"operator": ">=",
"value": 50000
}
],
"sorts": [
{
"field": "population",
"direction": "ASC"
}
],
"distinct": true
}
Exemplos:
- Listar as cidades com população maior ou igual a
40000habitantes
{
"filters": [
{
"field": "population",
"operator": ">=",
"value": 40000
}
],
"distinct": true
}
- Listar as cidades com stateId igual a
52e0a6a7-d72d-4b0f-bab9-aebfcf888e21e população maior que20000habitantes
{
"filters": [
{
"field": "stateId",
"operator": "=",
"value": "52e0a6a7-d72d-4b0f-bab9-aebfcf888e21"
},
{
"field": "population",
"operator": ">",
"value": 20000
}
],
"distinct": true
}
- Listar as cidades com população entre
40000e550000habitantes
{
"filters": [
{
"field": "population",
"operator": "$btw",
"value": "40000;55000"
}
]
}
- Listar os estados que possuem cidades com população maior ou igual a 50000 habitantes.
{
"filters": [
{
"field": "cities.population",
"operator": ">=",
"value": 50000
}
],
"distinct": true
}
- Listar os estados que foram fundados antes de 14/04/20
{
"filters": [
{
"field": "dateOfFoundation",
"operator": "<",
"value": "2020-04-14T22:42:53Z"
}
]
}
Obs.:
- O valor default do parâmetro
distincté true. Sendo assim, pode ser omitido. - O tipo
Sort, suportadirectioncom valores ASC e DESC. - Todas as configurações de consulta, são realizadas com base no nome do atributo. Também é suportado consultas no atributo filho (ainda não suportado para o tipo Sort).
- O parâmetro
operator, possui valor defaultande pode ser omitido. - O parâmetro
valuedo tipoFilter, suporta múltiplos tipos. Sendo assim, deve ser informado com sintaxe equivalente ao tipo definido na entidade.
Visualização (views)
Os resources de consulta suportam configuração dinâmica de visualização. Ou seja, permite minimizar o over-fetching comum em API-REST, uma vez que possibilita indicar quais informações devem ser retornadas no json.
Essa configuração dinâmica é realiza através do atributo views, do QueryDefinition. Exemplo:
{
"views": ["id", "name", "population"],
"filters": [
{
"field": "population",
"operator": ">=",
"value": 50000
}
],
"sorts": [
{
"field": "population",
"direction": "ASC"
}
]
}
Da mesma forma que temos o suporte ao fetch dinâmico, que possibilita indicar quais relacionamentos devem ser retornados na consulta, também temos suporte a visualização dinâmica desses atributos. Exemplo:
POST - {path}/query, {path}/query/page
{
"views": ["id", "name", "population", "state.id", "state.name"],
"joins": [
{
"field": "state",
"fetch": true
}
]
}
GET - {path}/{id}, {path}/detail/{id}, {path}/all, {path}/query, {path}/query/page
/api/cities/dcacfe35-2aa7-4627-972e-1d42f6c8cc1f?views=id,name
/api/cities/detail/dcacfe35-2aa7-4627-972e-1d42f6c8cc1f?views=id,name,state.id,state.name
/api/cities/query?opt=population>=50000views=id,name
/api/cities/query?opt=population>=50000&fetches=state&views=id,name,state.id,state.name
/api/cities/all?views=id,name
Obs.:
- A visualização dinâmica em relacionamentos é aplicada apenas em relacionamentos que foram definidos com
fetch true. Sendo assim, o relacionamento deve estar configurado em algum filtro comfetch trueou indicado explicitamente na lista dejoins. - O parser do
jsoné realizado nativamente com ojackson. Sendo assim, as visualizações respeitam as annotations@JsonIgnoree@JsonIgnoreProperties. - Quando configurado com
query-with-views: entity, os relacionamentos só serão carregados se estiverem com annotation@DynamicView. - O processamento da visualização ocorre a nível do
resource, no momento da serialização dojson. Sendo assim, não existe uma otimização a nível das consultas de banco, onde essas são realizadas com base nos mapeamentosJPAe definições doSpring Data. - O suporte a {path}/{id} e {path}/detail/{id} com método
GET, não permite a configuração defetch. Sendo assim, será aplicado diretamente ao resultado defindByIdefindDetailByIddo Service correspondente ou a definição deNamedEntityGraphsdoEntity.
Configuração
spring-api-tools:
query-with-views: all
- all - habilita para todas as classes de domínio (valor default).
- entity - Indica que estará habilitado apenas para os domínios que estiverem configurados com a annotation
@DynamicView. - none - desabilitado.