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

Top comments (0)

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs