1_uHzooF1EtgcKn9_XiSST4w
Compartilhar no facebook
Compartilhar no twitter
Compartilhar no linkedin
Diurno

APIs REST em Spring Boot: performance e produtividade na criação de microsserviços stand-alone

As APIs REST estão cada vez mais relevantes devido à importância que elas têm no contexto de microsserviços. Inúmeras frameworks propõem soluções no que diz respeito ao desenvolvimento desse tipo de aplicação, introduzindo novas linguagens e paradigmas. Mesmo assim, Spring Boot – uma framework escrita em Java – vem se destacando nesse mercado. Por quê?

O Spring Boot

Spring Boot é uma framework para criação de aplicações web em Java, e se destaca por possibilitar o desenvolvimento de aplicações “stand-alone”, ou seja, que não necessitam de um servidor para serem executadas: a própria aplicação contém um servidor embutido. Além disso, Spring Boot provê uma extensa lista de funcionalidades que facilitam e agilizam o desenvolvimento de APIs REST de alta performance.

JPA: it just works!

JPA é uma framework que define uma interface para ORMs (object-relational mapping), que são sistemas que abstraem o acesso à base de dados, permitindo operações sem a necessidade de escrever queries. Com isso, ganha-se agilidade no desenvolvimento dos microsserviços, uma vez que não haverá necessidade de escrever queries para consultas simples em banco de dados. Os sistemas ORM são especialmente interessantes para APIs REST, devido ao fato de facilitarem as operações CRUD (Create, Read, Update and Delete), que são extremamente comuns nesse tipo de aplicação.

Configuração

Além de fazer configuração automática, o Spring Boot também permite a realização de configuração manual de acesso a banco. Para utilizar JPA, devemos realizar uma configuração manual:

@Profile("!test")
@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(
basePackages = "br.com.dtidigital.repository.oracle",
entityManagerFactoryRef = "oracle-em",
transactionManagerRef = "oracle-tm"
)
public class OracleConfig {

@Value("${datasource.oracle.url}")
private String url;
@Value("${datasource.oracle.username}")
private String username;
@Value("${datasource.oracle.password}")
private String password;
@Value("${datasource.oracle.class-name}")
private String driverClassName;

Nessa classe, carregamos os dados de configuração externalizados, e criamos o objeto DataSource (fonte de dados), além do EntityManager e TransactionManager, que gerenciam toda a camada de repositório:

.driverClassName(driverClassName)
.username(username)
.password(password)
.url(url)
.build();
}

@PersistenceContext(unitName = "oracle")
@Bean(name = "oracle-em")
public LocalContainerEntityManagerFactoryBean oracleEntityManagerFactory(
EntityManagerFactoryBuilder builder) {
return builder
.dataSource(oracleDataSourceFactory())
.persistenceUnit("oracle")
.properties(jpaProperties())
.packages("br.com.dtidigital.entity.oracle")
.build();
}

@Bean(name = "oracle-tm")
public PlatformTransactionManager oracleTransactionManagerFactory(
@Qualifier("oracle-em") EntityManagerFactory em) {
return new JpaTransactionManager(em);
}

private Map<String, Object> jpaProperties() {
Map<String, Object> props = new HashMap<>();
props.put("hibernate.dialect", "org.hibernate.dialect.Oracle10gDialect");
return props;
}

Mapeando entidades

As entidades (classes que representam as tabelas) podem ser facilmente mapeadas com o uso de anotações.

@Data
@Entity
@Table(name = "TBL_STATUS", schema = "dbo")
public class StatusEntity {

@Id
@Column(name = "CD_STATUS")
private Long cdStatus;

@Column(name = "DS_STATUS")
private String dsStatus;

}

A anotação @Entity informa ao JPA que se trata de uma classe de entidade, e a anotação @Table informa o nome e o schema da tabela no banco.

Similarmente, a anotação @Column informa o nome da coluna, e também pode ser especificado se a coluna é nullable, tamanho máximo, dentre outros. A anotação @Id identifica uma chave primária.

Criando um repositório CRUD

A classe CrudRepository<Entity, Id> provê abstrações para diversas operações de acesso à banco. A própria framework abstrai as queries, e para utilizá-la, basta criar uma interface @Repository que herde de CrudRepository:

@Repository
public interface StatusRepository extends CrudRepository<StatusEntity, Long> {

Com isso, o repositório StatusRepository terá as seguintes funcionalidades prontas (!) para uso:

<S extends T> S save(S entity);
<S extends T> Iterable<S> saveAll(Iterable<S> entities);
Optional<T> findById(ID id);
boolean existsById(ID id);
Iterable<T> findAll();
Iterable<T> findAllById(Iterable<ID> ids);
long count();
void deleteById(ID id);
void delete(T entity);
void deleteAll(Iterable<? extends T> entities);
void deleteAll();

Criando a camada de negócios

Pelo padrão MVC do Spring, a lógica de negócios deve estar contida nas classes de serviço. As classes de serviço, além de realizar as validações de negócio, devem delegar as responsabilidades de persistência para o repositório:

@RequiredArgsConstructor
@Service
public class StatusService {

private StatusRepository statusRepository;

public Optional<StatusEntity> find(Long id) {
// lógica de negócio
return statusRepository.findById(id);
}

public StatusEntity saveOrUpdate(StatusEntity status) {
// lógica de negócio
return statusRepository.save(status);
}

public void deleteStatusById(Long id) {
// lógica de negócio
statusRepository.deleteById(id);
}

Criando os controladores

Para criarmos os controladores, que são responsáveis por fazer a interface da API com o “mundo externo”, basta criarmos uma classe anotada com @RestController, e mapear as rotas da API com as anotações @RequestMapping, @GetMapping, @PostMapping, @PutMapping e @DeleteMapping.

@RestController
@RequiredArgsConstructor
@RequestMapping("/status")
@Api(tags = { "API de Status" })
public class StatusRestController {

private final StatusService service;

@GetMapping
@ApiOperation(value = "Busca todos os Status")
public ResponseEntity<List<StatusEntity>> findAllStatus() {
return new ResponseEntity<>(service.findAllStatus(), HttpStatus.OK);
}

}

Nesse exemplo, foi exposta apenas uma rota “/status”, na qual o método GET retorna todos os status existentes no banco de dados. Com isso, já temos uma API REST funcionando!

Aspects: onde a mágica acontece

Spring Boot dá suporte à AOP (Aspect Oriented Programming), que é um paradigma de programação que permite separar os chamados cross-cutting concerns (preocupações transversais, em tradução literal) da lógica principal da aplicação. Isto é, questões como autorização, gerenciamento de transações e logging podem ser separadas do código principal da aplicação, resultando em menor duplicação e uma base de código mais limpa.

A AOP possui três conceitos principais, que são a fundação do paradigma:

 

  • Join point: ponto de execução do código

 

    • Advice: ação a ser executada nos join points especificados pelo pointcut. Exemplos: before (executado antes da execução do join point), after (executado depois da execução do join point), around (executa antes e depois da execução do join point)

 

  • Pointcut: predicado que informa quais join points devem ser “interceptados” pelo advice

 

Um exemplo de aspect é para fazer logs de todas as requests recebidas pela API e todos as responses enviadas pela API. Para configurá-lo, basta criar uma classe de configuração de aspect, observando os join points adequados:

@Aspect
@Configuration
@ConditionalOnExpression("${api.custom.log.rest.enabled:true}")
public class ApiCustomLogRestAspect {
@Pointcut("within(@org.springframework.web.bind.annotation.RestController *)")
public void restController() {
throw new UnsupportedOperationException();
}
@Pointcut("execution(* *.*(..))")
protected void allMethod() {
throw new UnsupportedOperationException();
}
@Pointcut("@annotation(org.springframework.web.bind.annotation.GetMapping)")
public void getMapping() {
throw new UnsupportedOperationException();
}
@Pointcut("@annotation(org.springframework.web.bind.annotation.PostMapping)")
public void postmapping() {
throw new UnsupportedOperationException();
}

@Before("restController() && allMethod() && getMapping() && bean(*Controller)")
public void logBeforeGetRequest(JoinPoint joinPoint) {
ApiCustomLogUtils.logRequest(null);
}

@Before("restController() && allMethod() && "
+ "postmapping() && bean(*Controller) && args(.., @RequestBody body)")
public void logBeforePostRequest(JoinPoint joinPoint, Object body) {
ApiCustomLogUtils.logRequest(body);
}

@AfterReturning(pointcut = "restController() && allMethod() && bean(*Controller)", returning = "result")
public void logAfterRequest(JoinPoint joinPoint, Object result) {
ApiCustomLogUtils.logResponse(result);
}

Dessa forma, os logs acontecerão de forma “automática”: não será necessário realizar chamadas explícitas de logging nos controladores!

Lombok

Lombok é uma biblioteca que ajuda a manter o código limpo e conciso, diminuindo sua verbosidade por meio de anotações. O Lombok possui uma série de anotações que visam diminuir a quantidade de código “boilerplate”. Alguns exemplos são:

  • @Getter e @Setter: gerar getters e setters para campos ou classes
  • @NoArgsConstructor: gerar um construtor vazio
  • @AllArgsConstructor: gerar um construtor com todos os parâmetros
  • @Builder: gerar um builder (classe interna que auxilia na construção de objetos) para a classe
  • @Data: gerar getters, setters, construtor para os campos necessários, e métodos equals e hashCode

Cache

Implementar mecanismos de cache também é uma tarefa bastante simples em aplicações Spring Boot. Para isso, é necessário anotar os métodos que deverão possuir cache com a anotação @Cacheable(nomeDoCache), onde nomeDoCache é o nome do container de cache, que deve ser criado em uma classe de configuração:

@Log4j2
@Configuration
@EnableCaching
public class CacheConfig implements CachingConfigurer {

@Autowired
private Environment env;

@Bean
@Override
public CacheManager cacheManager() {
log.info("Inicializando gerenciador de Caches da aplicação...");
SimpleCacheManager cacheManager = new SimpleCacheManager();
List<GuavaCache> caches = new ArrayList<>();
Map<String, String> cacheProperties = loadCacheProperties();
List<String> cacheNames = obterListaNomesCache(cacheProperties);
validarConfiguracao(Caches.getAll(), cacheNames);
for (Entry<String, String> cacheProperty: cacheProperties.entrySet()) {
String nomeCache = obterNomeCacheDaPropriedade(cacheProperty.getKey());
long tempoInvalidacao = Long.parseLong(cacheProperty.getValue());
GuavaCache cache = new GuavaCache(nomeCache, CacheBuilder.newBuilder()
.expireAfterWrite(tempoInvalidacao, TimeUnit.MINUTES)
.build());
caches.add(cache);
}
cacheManager.setCaches(caches);
return cacheManager;
}

A configuração de cache pode ser externalizada nas propriedades da aplicação:

application.cache.invalidation-time.[nome-do-cache]=[tempo-em-minutos]

onde [tempo-em-minutos] é o tempo em minutos para que o cache expire. Existem vários outros pontos configuráveis, dependendo do caso de uso. Utilizando caching, conseguimos obter ganhos de performance expressivos em operações de leitura em banco, por exemplo.