Como criar um Servidor Web Java Sem Framework
Tags: java, javac, webserver, braziliandevs
Published: 2023-04-20 01:26:15

Olá, espero que você esteja bem 😊 Uma das coisas que gosto de fazer ao iniciar meus estudos em uma tecnologia, é entender como as coisas funcionam nos níveis mais básico da ferramenta, ou seja, construir algo sem o auxílio de frameworks. Assim, passo a ter uma boa noção do que é framework e o que é a nativo da ferramente.

A proposta aqui é criar e configurar o projeto de um servidor backend simples na linguagem Java, que execute no ambiente local e possua uma rota `get` que imprima uma mensagem de “Hello, World!” padrão. Ademais, a ideia é codificar o servidor tentando utilizar o nível mais básico e nativo e entender o que cada ferramenta ou configuração altera no projeto.

# Quais são os pré-requisitos?

Para iniciar, como quero o menor auxílio possível de outras ferramentas, não vou escolher uma IDE JAVA, uma vez que esse software me ajudaria nessa parte de configuração inicial, não me deixando livre para percorrer o caminho das pedras que é meu propósito. Por isso, o editor de texto que estarei usando é o Visual Studio Code.

A escolha do VS-code não é influenciada por ser o editor que mais gosto 🤐 mas sim por eu precisar de uma ferramenta que me auxilie o mínimo, mas que ainda torne esse processo operacional. Diferente de utilizar um bloco de notas, que não mostra em destaque as palavras chave ou no Vi ou Nano que são editores que rodam no terminal, que eu particularmente tenho dificuldade de navegar entre o código.

Além do editor de texto, preciso é claro, do JDK. A sigla para Java Development Kit, compreende o conjunto de ferramentas que os desenvolvedores usam para criar aplicativos em Java. Dentro dele estão inclusos o compilador, depuradores, documentadores e outras ferramentas.

Para saber se você possui o JDK instalado em sua máquina, no terminal execute o comando

```bash
java -version
javac -version
```

Como saída, você deve ver algo parecido com isso, variando a versão do Java que você instalou. Para este projeto estou usando a versão **`11.0.7`** do Java.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3g2n8903xunkjpaq68vx.png)

Se você não tiver o JDK instalado, você pode baixá-lo e instalá-lo a partir do site oficial da Oracle ou do [OpenJDK](https://openjdk.org/install/).

# Familiarizando-se com o Java

Com tudo configurado, vou fazer um pequeno teste criando um arquivo que imprime ”Hello, World!”, compilar e executar através do terminal.

No diretório do projeto, vou criar a pasta **`src`** onde ficará os arquivos fontes do projeto, e dentro dela, o arquivo **`Main.java`**. Perceba que a extensão do arquivo é **`.java`**, isso indica que se trata de um arquivo fonte, ou seja, o arquivo onde o programador escreverá o código propriamente dito. Dentro desse arquivo estarei criando a classe que leva o mesmo nome, e o método **`main`**.

```java
public class Main {
  public static void main(String[] args) {

    String greeting = "Hello, World!";
    System.out.println(greeting);
  }
}
```

O método **`public static void main(String[] args)`**
em Java é o ponto de entrada para qualquer programa em Java. É o método que é executado primeiro quando você executa um programa Java a partir da linha de comando ou do IDE. Em programação, funções main semelhantes à essa são chamadas de **Entry Point**.

Com o código criado, o próximo passo é a compilação. Para isso, através de um terminal, navegue até a pasta do projeto e execute o seguinte comando:

```bash
javac src/Main.java
```

Se não apareceu nenhuma saída no terminal, no diretório src se encontra o bytecode do nosso programa em Java. Bytecode é uma forma de representar um programa Java compilado em uma linguagem intermediária que é interpretada pela Java Virtual Machine (JVM).

Agora, para executar nosso programa utilizamos o comando:

```bash
java -cp src Main
```

O parâmetro **`-cp`** especifica o classpath, ou seja, o caminho onde a JVM deve procurar pelas classes referenciadas pelo programa. Neste caso, estamos especificando que a pasta "src" contém as classes do programa.

# Hora do show

Com tudo pronto, testado e funcionando, podemos avançar para a parte que de criação do nosso servidor propriamente dito. Para isso teremos que fazer a importação de 3 módulos do Java.

```java
import com.sun.net.httpserver.HttpServer;
import java.io.IOException;
import java.net.InetSocketAddress;

public class Main {
  public static void main(String[] args) {

    String greeting = "Hello, World!";
    System.out.println(greeting);
  }
}
```

A razão pela qual você precisa fazer o import desses módulos é que eles não estão incluídos na biblioteca padrão do Java. A classe **`com.sun.net.httpserver.HttpServer`** pertence à API do servidor HTTP incorporado, que é uma parte do JDK, mas não é incluída por padrão em todos os ambientes de execução do Java. Da mesma forma, as classes **`java.io.IOException`** e **`java.net.InetSocketAddress`** são parte da biblioteca padrão do Java, mas precisam ser importadas porque são usadas no código e não estão no pacote padrão.

Além disso, mesmo que as classes estejam no pacote padrão, é uma boa prática importá-las explicitamente para deixar o código mais claro e fácil de entender.

1. **`com.sun.net.httpserver.HttpServer`**

Esse é o módulo da API do servidor HTTP incorporado que é fornecido pelo JDK. Essa API permite que você crie um servidor web em Java sem precisar usar uma biblioteca de terceiros ou um servidor web externo. A classe **`HttpServer`** representa o servidor web em si, e é responsável por gerenciar as conexões de entrada e saída, bem como pelo roteamento das solicitações para as classes de manipuladores correspondentes.

1. **`java.io.IOException`**

Este import é necessário para que o servidor HTTP incorporado possa lançar uma exceção de **`IOException`**. Isso acontece porque o servidor precisa criar um (socket) para escutar as conexões de entrada. No entanto, pode haver problemas ao criar o soquete, como falha na conexão, porta em uso ou outro erro. A exceção **`IOException`** é lançada quando ocorre um erro na criação do soquete. Essa exceção é uma exceção verificada, o que significa que o código precisa lidar com ela de alguma forma (como mostrado na resposta anterior).

1. **`java.net.InetSocketAddress`**

Este import é necessário porque a classe **`HttpServer`** precisa de um objeto **`InetSocketAddress`** para determinar qual porta e endereço IP usar para escutar as conexões de entrada. É por isso que a criação de um objeto **`InetSocketAddress`** é um passo importante na criação de um servidor HTTP incorporado, porque ele especifica onde o servidor web estará "ouvindo" as solicitações HTTP recebidas.

Repare que até aqui, apenas fizemos os imports necessários para os módulos básicos, disponíveis no JDK, para a criação do nosso servidor web.

Como já comentado, o módulo de **`IOException`** é importante para lançar uma exceção. No Java, uma Exception é uma classe que representa uma condição excepcional que ocorre durante a execução de um programa. Ao ocorrer um erro, uma instância de uma classe de Exception é criada e lançada, interrompendo a execução normal do programa. As Exceptions permitem que os programas lidem com erros e situações excepcionais de forma mais controlada.

Por isso, faremos uma pequena alteração no código fonte do servidor utilizando a palavra **`throws`** após a assinatura do nosso entry point. O **`throws`** é uma declaração que informa que um método pode lançar uma exceção durante a sua execução.

```java
import com.sun.net.httpserver.HttpServer;
import java.io.IOException;
import java.net.InetSocketAddress;

public class Main {
  public static void main(String[] args) throws IOException {

    String greeting = "Hello, World!";
    System.out.println(greeting);
  }
}
```

Como próximo passo, devemos criar uma instância que representa o endereço e a porta em que o servidor deve ser executado. Para isso utilizaremos a classe **`InetSocketAddress`**. A classe **`InetSocketAddress`** possui dois construtores que podem ser utilizados para instanciar um objeto. O primeiro deles recebe um objeto do tipo **`InetAddress`** e um número de porta, e o segundo recebe uma string com o endereço IP ou nome do host e um número de porta.

Aqui está uma explicação mais detalhada de cada um dos parâmetros:

- **`InetAddress`**: é um objeto que representa um endereço IP. Ele pode ser obtido através da classe **`InetAddress`** do Java. Ao utilizar esse parâmetro, estamos especificando o endereço IP do servidor ou cliente que será utilizado na conexão.
- **`String`**: é uma string que representa o endereço IP ou nome do host. Ele é útil quando queremos criar uma instância de **`InetSocketAddress`** a partir de uma string. Por exemplo, se quisermos criar um endereço a partir de uma string "localhost", podemos passar essa string como parâmetro.
- **`int`**: é um número inteiro que representa a porta que será utilizada na conexão. A porta é utilizada para especificar o canal de comunicação que será utilizado pelo servidor ou cliente.

Ao instanciar um objeto de **`InetSocketAddress`**, precisamos passar esses parâmetros no construtor de acordo com o que desejamos.

Para deixar mais didático, declarei uma variável do tipo **`String`** com o identificador **`host`** e uma variável do tipo **`int`** com o identificador **`port`**, além de alterar nossa antiga variável chamada **`greetings`** para **`response`**.

```java
import com.sun.net.httpserver.HttpServer;
import java.io.IOException;
import java.net.InetSocketAddress;

public class Main {
  public static void main(String[] args) throws IOException {

    int port = 8080;
    String host = "localhost";

    String response = "Hello, World!";

    InetSocketAddress address = new InetSocketAddress(host, port)

    System.out.println(response);
  }
}
```

Por fim a cereja do nosso bolo, a classe responsável por criar o servidor HTTP. A classe **`HttpServer`**, que pode ser usada para criar um servidor HTTP que ouve em uma porta específica e manipula solicitações HTTP de entrada,

A classe **`HttpServer`** fornece um método estático **`create()`** que cria uma instância do servidor HTTP. O método **`create()`** aceita dois parâmetros: um **`InetSocketAddress`** e um **`int`** que indica o número máximo de solicitações que o servidor pode manipular simultaneamente. Quando o servidor recebe uma solicitação, ele cria uma nova thread para manipular a solicitação e liberar o thread principal para lidar com novas solicitações.

```java
import com.sun.net.httpserver.HttpServer;
import java.io.IOException;
import java.net.InetSocketAddress;

public class Main {
  public static void main(String[] args) throws IOException {

    int port = 8080;
    String host = "localhost";

    String response = "Hello, World!";

    InetSocketAddress address = new InetSocketAddress(host, port)

    HttpServer server = HttpServer.create(address, 0);

    System.out.println(response);
  }
}
```

Agora precisamos criar um contexto para o servidor HTTP, indicando o que deve acontecer quando o servidor receber uma requisição para a rota "/" (root). No contexto de um servidor HTTP, o termo "contexto" se refere a um mapeamento entre uma determinada URL e uma função ou método que deve ser executado para lidar com as requisições feitas para essa URL.

```java
import com.sun.net.httpserver.HttpServer;
import java.io.IOException;
import java.net.InetSocketAddress;

public class Main {
  public static void main(String[] args) throws IOException {

    int port = 8080;
    String host = "localhost";

    String response = "Hello, World!";

    InetSocketAddress address = new InetSocketAddress(host, port)

    HttpServer server = HttpServer.create(address, 0);

    server.createContext("/", (exchange -> {}));

    System.out.println(response);
  }
}
```

server.createContext("/", (exchange -> {...} cria um contexto para a URL raiz ("/") e define uma função lambda para lidar com as requisições feitas para essa URL. O conceito de lambda no Java é uma expressão que representa uma função anônima, que pode ser passada como argumento para um método ou atribuída a uma variável.

A palavra-chave **`exchange`** representa a troca de informações entre o cliente e o servidor. A função lambda recebe esse objeto **`exchange`** como parâmetro e, a partir dele, é possível acessar e manipular os dados da requisição e da resposta.

Com o contexto criado precisamos aplicar algumas configurações para definir o formato do conteúdo da resposta.

```java
import com.sun.net.httpserver.HttpServer;
import java.io.IOException;
import java.net.InetSocketAddress;

public class Main {
  public static void main(String[] args) throws IOException {

    int port = 8080;
    String host = "localhost";

    String response = "Hello, World!";

    InetSocketAddress address = new InetSocketAddress(host, port)

    HttpServer server = HttpServer.create(address, 0);

    server.createContext("/", (exchange -> {
			 
      exchange.getResponseHeaders().set("Content-Type", "text/plain");
      exchange.sendResponseHeaders(200, response.getBytes().length);
      exchange.getResponseBody().write(response.getBytes());
      exchange.close();
    }));

    System.out.println(response);
  }
}
```

Dentro da função lambda, primeiro é definido o tipo do conteúdo da resposta, usando o método **`set()`** do objeto **`getResponseHeaders()`**.

Em seguida, o código envia a resposta ao cliente, indicando o código de status 200 (OK), o tamanho da resposta e o conteúdo em si, usando os métodos **`sendResponseHeaders()`**, **`getResponseBody()`** e **`write()`** do objeto **`exchange`**.

Por fim, o método **`close()`** é chamado para encerrar a troca de informações entre o cliente e o servidor.

Ao final, precisamos inicializar nosso servidor web. O método **`server.start()`** é responsável por iniciar o servidor HTTP. Após a chamada desse método, o servidor começa a ouvir as solicitações HTTP recebidas na porta e host especificados.

A última linha, **`System.out.println("Servidor iniciado em http://localhost:8080/");`**, exibe uma mensagem no console informando ao usuário que o servidor foi iniciado com sucesso e em qual URL ele está sendo executado. Essa mensagem é apenas informativa e não interfere no funcionamento do servidor. É uma forma de indicar ao usuário que o servidor está pronto para receber as solicitações.

```java
import com.sun.net.httpserver.HttpServer;
import java.io.IOException;
import java.net.InetSocketAddress;

public class Main {
  public static void main(String[] args) throws IOException {

    int port = 8080;
    String host = "localhost";

    String response = "Hello, World!";

    InetSocketAddress address = new InetSocketAddress(host, port)

    HttpServer server = HttpServer.create(address, 0);

    server.createContext("/", (exchange -> {
			 
      exchange.getResponseHeaders().set("Content-Type", "text/plain");
      exchange.sendResponseHeaders(200, response.getBytes().length);
      exchange.getResponseBody().write(response.getBytes());
      exchange.close();
    }));

    server.start();
    System.out.println("Servidor iniciado em http://" + host + ":" + port + "/");
  }
}
```

# Considerações finais

Em resumo, esse projeto cria um servidor HTTP simples na porta 8080 e no host "localhost". Quando uma solicitação HTTP get é feita para a URL raiz "/", o servidor responde com o texto "Hello

World!".

O método main() é responsável por inicializar o servidor e definir o contexto para a URL raiz "/". Dentro do contexto, é definida uma função lambda que trata a solicitação HTTP recebida. Nessa função, é definido o tipo de conteúdo da resposta como "text/plain", o código de status da resposta como 200 (OK), o corpo da resposta como o texto "Hello World!" e, por fim, a resposta é enviada para o cliente e a conexão é fechada.

Por fim, o servidor é iniciado e uma mensagem é exibida no console informando a URL em que o servidor está sendo executado.

Neste artigo, vimos como criar um servidor web Java simples sem o uso de nenhum framework. Com apenas algumas linhas de código, é possível criar um servidor que recebe solicitações HTTP e responde com o conteúdo desejado.

Embora seja possível criar um servidor web Java sem framework, é importante lembrar que frameworks como Spring e Spark oferecem muitas funcionalidades úteis que podem acelerar o desenvolvimento de um aplicativo web.

Se você quiser ver o código completo do servidor web Java que criamos neste artigo, confira o meu projeto no GitHub: [pure-java-server](https://github.com/diogoizele/pure-java-server).

Espero que este artigo tenha sido útil e que você tenha aprendido algo novo sobre como criar um servidor web Java simples. Fique à vontade para compartilhar suas ideias e comentários abaixo. Até a próxima! 👋