DEV Community

Cover image for Abstraindo o gerenciamento de conexões do banco de dados em uma API Rest com Javalin e seus Handlers
josevjunior
josevjunior

Posted on

1

Abstraindo o gerenciamento de conexões do banco de dados em uma API Rest com Javalin e seus Handlers

Como os Frameworks abstraem isso?

Quando utilizei o Spring framework pela primeira vez fiquei curioso quando descobri que as classes criadas através dele (Beans) são, por padrão, singleton. Ou seja, a mesma instância de um @Controller por exemplo, é utilizada em toda requisição feita para a url em que ela está mapeada. Minha curiosidade era em saber como ele lidava com o estado dos atributos desses objetos. Em um bean podemos injetar outros objetos como um EntityManager e nesse caso os valores injetados em uma requisição não devem se misturar com os valores de outra. Devem ser Thread-Safe.

Graças a internet consegui encontrar alguém que teve a mesma curiosidade, foi estudar o código-fonte do Spring e escreveu um post sobre que você pode acessar aqui. O artigo tem o foco na utilização do @Repository, mas o princípio é o mesmo.

Em resumo, o Spring cria um proxy para cada Bean criado (Que pode ser um @Service, @Repository ou qualquer outro tipo) e internamente ele salva as propriedades que devem ser Thread Safe em um objeto ThreadLocal. Normalmente objetos com a anotação @PersistenceContext não devem existir em 2 threads(Requisições) diferentes. Então, quando existir uma propriedade com essa anotação em um fonte, o que acessamos é apenas um proxy que dará acesso ao valor pertencente àquela thread. Dessa forma não ocorrerá a situação de um EntityManager existir em duas requisições simultâneas.

Um exemplo bem simples da utilização de um ThreadLocal

import java.util.*;
public class ThreadLocalExample {
public void setThreadLocalValue(){
ThreadLocal<String> localString = new ThreadLocal<>();
localString.set("Texto que pode ser acessado apenas na thread em que ele foi preenchido");
System.out.println(localString.get()); // Retornará o valor informado
new Thread(()-> {
System.out.println(localString.get()); // Retornará null porque está em outra thread
}).start();
}
}

O Microframework Javalin

Quando se trata de microframeworks, existem algumas opções que sempre vem a tona como o Vert.x e Spark (Não o Apache Spark). Porém, o microframework que tive contato foi o Javalin, mesmo não sendo o mais completo, possui uma curva de aprendizado muito baixa, é compatível com Kotlin, tem suporte a Websocket e Server Sent Events (SSE), e é extremamente leve.

Dependências necessárias para utilizá-lo

<dependency>
<groupId>io.javalin</groupId>
<artifactId>javalin</artifactId>
<version>3.9.1</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>1.7.30</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.10.3</version>
</dependency>
view raw pom.xml hosted with ❤ by GitHub

Um simples exemplo esperando uma requisição na porta 7000

import io.javalin.Javalin;
public class HelloJavalin {
public static void main(String[] args) {
Javalin app = Javalin.create().start(7000);
app.get("/", ctx -> ctx.result("Hello World"));
}
}

Como o Javalin funciona

Quando se cria um novo servidor com o Javalin, é possível usar os chamados Handlers (Semelhante a um callback, listener, observable, etc) que serão responsáveis por tratar as requisições http. Os Handlers geralmente são interfaces de apenas um método com um paramêtro do tipo Context (que é um facilitador de acesso aos métodos da classe HttpServletRequest e HttpServletResponse. Sim, o javalin utiliza a api do javax.servlet e sua implementação é o Jetty). Abaixo um exemplo de como utilizá-los.

package br.com.jvjr.person;
import io.javalin.Javalin;
public class JavalinCallbacks {
public static void main(String[] args) {
Javalin app = Javalin.create().start(8080);
// Utilizando expressão lambda
app.get("/", ctx -> ctx.result("Lambda is life"));
// Implementação concreta
app.post("/", new PostCallbackImpl());
// Referência de método
PutCallbackImpl putCallbackImpl = new PutCallbackImpl();
app.put("/", putCallbackImpl::putCallback);
}
}
package br.com.jvjr.person;
import io.javalin.http.Context;
import io.javalin.http.Handler;
public class PostCallBackImpl implements Handler {
public PostCallBackImpl() {
}
@Override
public void handle(Context cntxt) throws Exception {
}
}

O problema com as conexões

Da primeira vez que utilizei o Javalin eu precisava lidar com o operações com banco de dados. Na época eu não optei por nenhuma biblioteca de ORM para facilitar o acesso aos dados ou de injeção de dependência para outros aspectos. O resultado disso foi muito código chato e repetitivo para tratar acesso ao banco e transações como os abaixo:

import java.sql.*;
public class ExampleService extends BaseService{
public void doServiceJob() {
Connection con = null;
try {
// Regra de negócio ou consulta no banco de dados
/* PreparedStatement stam = con...
ResultSet rs = stam.executeQuery...
stam.executeUpdate();
*/
con.commit(); // Confirma as alterações
} catch(Exception e) {
try {
if (con != null) con.rollback();
} catch(SQLException se) {
}
} finnally {
try {
if (con != null) con.close();
} catch(SQLException e) {
}
}
}
}

A situação era ainda pior quando precisava implementar algo muito extenso e com várias etapas. Como um método depende do resultado do outro, para manter tudo na mesma transação era preciso passar a referência da conexão que está sendo usada. Como no exemplo seguinte:

import java.sql.*;
public class ExampleService2 extends BaseService{
public AService1 service1;
public AService2 service2;
public void doComplexServiceJob() {
Connection con = null;
try {
con = getConnection();
// Vários métodos que dependem um do outro
doFirstJob(con);
Result result = doSecondJobWhichDependsOnFirst(con);
service1.processResult(result, con);
service2.createResultLogInDatabase(result, con);
con.commit(); // Confirma as alterações
} catch(Exception e) {
try {
if (con != null) con.rollback();
} catch(SQLException se) {
}
} finnally {
try {
if (con != null) con.close();
} catch(SQLException e) {
}
}
}
}

O que poderia ser feito para evitar esse tipo de código?

Depois de entender a premissa de como o Spring trata seus beans em um ambiente multithread, podemos usar as funcionalidades que o próprio Javalin disponibiliza para chegar em um resultado semelhante.

Com o objetivo de usar uma conexão por thread, podemos abstrair a criação, fechamento e rollbacks das conexões no ciclo de vida das requisições do Javalin. Dessa forma, podemos nos concentrar somente na lógica dos métodos de negócio sem a necessidade de passar a referência para todos os métodos que dependem de outro.

Mas, por onde começar?

Vinculando uma conexão do banco de dados a uma thread (que é criada para cada requisição feito por um client). E para isso vamos utilizar a classe ThreadLocal para nos ajudar.

  • Abaixo temos a classe que será responsável por vincular uma conexão a uma thread. Dessa forma não precisamos nos preocupar com thread-safety na camada de acesso a dados.
package br.com.jvjr.connection;
import br.com.jvjr.exception.TransactionalException;
import java.sql.Connection;
public class ConnectionHolder {
private static final ThreadLocal<Connection> currentConnection = new InheritableThreadLocal<>();
public static Connection get() {
return currentConnection.get();
}
public static void set(Connection connection) {
final Connection current = get();
if(current != null && connection != current) { // para evitar 2 conexões sendo criadas na mesma thread
throw new TransactionalException("Cannot bind more than one transaction per thread.");
}
currentConnection.set(connection);
}
public static void clear() {
currentConnection.set(null);
}
}
  • Utilizei o hikari CP para o pool de conexões com a base de dados. O arquivo .properties está disponível no repositório do github.
package br.com.jvjr.connection;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import java.sql.Connection;
import java.sql.SQLException;
public class ConnectionProvider {
private HikariDataSource dataSource;
private static final ConnectionProvider instance = new ConnectionProvider();
private ConnectionProvider() {
init();
}
private void init() {
HikariConfig config = new HikariConfig("/hikari.properties");
dataSource = new HikariDataSource(config);
// para fechar o datasource quando a aplicação for encerrada
Runtime.getRuntime().addShutdownHook(new Thread(dataSource::close));
}
public static ConnectionProvider getInstance() {
return instance;
}
public Connection createConnection() throws SQLException{
return dataSource.getConnection();
}
}

Agora com os handlers que conversam com o ciclo de vida da requisição, é possíveis criar, fechar e desfazer as alterações feitas durante aquela requisição.

  1. Um handler para abrir a conexão a cada requisição

    package br.com.jvjr.interceptor;
    import br.com.jvjr.connection.ConnectionHolder;
    import br.com.jvjr.connection.ConnectionProvider;
    import io.javalin.http.Context;
    import io.javalin.http.Handler;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    public class OpenTransactionHandler implements Handler{
    private static final Logger LOGGER = LoggerFactory.getLogger(OpenTransactionHandler.class);
    @Override
    public void handle(Context cntxt) throws Exception {
    LOGGER.info("Opening transaction...");
    ConnectionProvider connectionProvider = ConnectionProvider.getInstance();
    ConnectionHolder.set(connectionProvider.createConnection());
    }
    }
  2. Um Handler para commitar e fechar a conexão após cada requisição

    package br.com.jvjr.interceptor;
    import br.com.jvjr.connection.ConnectionHolder;
    import br.com.jvjr.exception.TransactionalException;
    import io.javalin.http.Context;
    import io.javalin.http.Handler;
    import java.sql.Connection;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    public class CloseTransactionHandler implements Handler {
    private static final Logger LOGGER = LoggerFactory.getLogger(CloseTransactionHandler.class);
    @Override
    public void handle(Context cntxt) throws Exception {
    LOGGER.info("Closing transaction...");
    Connection connection = null;
    try {
    connection = ConnectionHolder.get();
    connection.commit();
    } catch (Exception e) {
    throw new TransactionalException(e);
    } finally {
    if(connection != null) connection.close();
    ConnectionHolder.clear();
    }
    }
    }
  3. Um handler que irá fazer rollback na transação caso uma exceção especifica seja lançada. (Poderia ser qualquer tipo de exceção)

    package br.com.jvjr.exception;
    import br.com.jvjr.connection.ConnectionHolder;
    import io.javalin.http.Context;
    import io.javalin.http.ExceptionHandler;
    import java.sql.Connection;
    import java.sql.SQLException;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    public class TransactionalExceptionHandler implements ExceptionHandler<TransactionalException>{
    private static final Logger LOGGER = LoggerFactory.getLogger(TransactionalExceptionHandler.class);
    @Override
    public void handle(TransactionalException t, Context ctx) {
    LOGGER.info("Rolling back transaction...");
    LOGGER.error(t.getLocalizedMessage(), t);
    Connection connection = ConnectionHolder.get();
    if(connection != null) {
    try {
    connection.rollback();
    }catch (SQLException e){}
    }
    ctx.status(500);
    ctx.json(t);
    }
    }

Para testar tudo isso, vamos fazer um cadastro simples

  1. Uma entidade simples

    package br.com.jvjr.person;
    public class Person {
    private Long id;
    private String name;
    public Person(Long id, String name) {
    this.id = id;
    this.name = name;
    }
    // for serializers use
    protected Person() {
    //
    }
    public Long getId() {
    return id;
    }
    public void setId(Long id) {
    this.id = id;
    }
    public String getName() {
    return name;
    }
    public void setName(String name) {
    this.name = name;
    }
    }
    view raw Person.java hosted with ❤ by GitHub
  2. Um service para acessar os dados

    package br.com.jvjr.person;
    import br.com.jvjr.connection.ConnectionHolder;
    import br.com.jvjr.exception.TransactionalException;
    import java.sql.Connection;
    import java.sql.PreparedStatement;
    import java.sql.ResultSet;
    import java.sql.SQLException;
    import java.sql.Statement;
    import java.util.ArrayList;
    import java.util.List;
    public class PersonService {
    public Person create(Person person) {
    try {
    Connection connection = getConnection();
    String sql = "INSERT INTO person (name) VALUES (?)";
    PreparedStatement stam = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
    stam.setObject(1, person.getName());
    stam.executeUpdate();
    ResultSet rs = stam.getGeneratedKeys();
    Person createdPerson = null;
    if(rs.next()) {
    createdPerson = new Person(
    rs.getLong(1),
    person.getName()
    );
    }
    return createdPerson;
    } catch (SQLException e) {
    throw new TransactionalException(e);
    }
    }
    public List<Person> getAll() {
    List<Person> persons = new ArrayList<>();
    try {
    Connection connection = getConnection();
    Statement stam = connection.createStatement();
    ResultSet rs = stam.executeQuery("SELECT * FROM person");
    while(rs.next()) {
    Person person = new Person(
    rs.getLong("id"),
    rs.getString("name")
    );
    persons.add(person);
    }
    } catch (SQLException e) {
    throw new TransactionalException(e);
    }
    return persons;
    }
    private Connection getConnection() {
    return ConnectionHolder.get();
    }
    }
  3. Um controller que chamará o service

    package br.com.jvjr.person;
    import io.javalin.http.Context;
    import java.util.List;
    public class PersonController {
    private final PersonService personService;
    public PersonController(PersonService personService) {
    this.personService = personService;
    }
    public void create(Context ctx) {
    Person person = ctx.bodyAsClass(Person.class);
    Person newPerson = personService.create(person);
    ctx.status(200).json(newPerson);
    }
    public void getAll(Context ctx) {
    List<Person> persons = personService.getAll();
    ctx.status(200);
    ctx.json(persons);
    }
    }
  4. Por fim a classe principal que inicializa tudo. Como não precisamos nos preocupar com threads em relação ao acesso a base de dados, os handlers podem usar sempre a mesma instancia do controlador

    package br.com.jvjr;
    import br.com.jvjr.exception.TransactionalException;
    import br.com.jvjr.person.PersonController;
    import br.com.jvjr.person.PersonService;
    import br.com.jvjr.exception.TransactionalExceptionHandler;
    import br.com.jvjr.interceptor.OpenTransactionHandler;
    import br.com.jvjr.interceptor.CloseTransactionHandler;
    import io.javalin.Javalin;
    public class Application {
    public static void main(String[] args) {
    Javalin app = Javalin.create(config -> {
    config.enableCorsForAllOrigins();
    }).start(7000);
    // vinculando os callbacks (Handlers)
    app.before("/api/*", new OpenTransactionHandler());
    app.after("/api/*", new CloseTransactionHandler());
    app.exception(TransactionalException.class, new TransactionalExceptionHandler());
    PersonController controller = new PersonController(new PersonService());
    app.get("/api/person", controller::getAll);
    app.post("/api/person", controller::create);
    }
    }

Conclusão

Esse foi apenas uma demonstração de como é possível utilizar apenas o que o Javalin nos dá para resolver o problema relacionado a gerência de transações/conexões. Claro que poderia ser mais sofisticado utilizando um framework de injeção de dependência ou ORM, mas não era o objetivo do artigo. O código pode ser acessado neste repositório.

Referências

Speedy emails, satisfied customers

Postmark Image

Are delayed transactional emails costing you user satisfaction? Postmark delivers your emails almost instantly, keeping your customers happy and connected.

Sign up

Top comments (0)

Postmark Image

Speedy emails, satisfied customers

Are delayed transactional emails costing you user satisfaction? Postmark delivers your emails almost instantly, keeping your customers happy and connected.

Sign up