JavaRush /Blogue Java /Random-PT /Usando JNDI em Java
Анзор Кармов
Nível 31
Санкт-Петербург

Usando JNDI em Java

Publicado no grupo Random-PT
Olá! Hoje apresentaremos a você o JNDI. Vamos descobrir o que é, porque é necessário, como funciona, como podemos trabalhar com ele. E então escreveremos um teste de unidade Spring Boot, dentro do qual brincaremos com este JNDI. Usando JNDI em Java - 1

Introdução. Serviços de nomenclatura e diretório

Antes de mergulharmos no JNDI, vamos entender o que são serviços de nomenclatura e diretório. O exemplo mais óbvio de tal serviço é o sistema de arquivos de qualquer PC, laptop ou smartphone. O sistema de arquivos gerencia arquivos (curiosamente). Os arquivos nesses sistemas são agrupados em uma estrutura de árvore. Cada arquivo possui um nome completo exclusivo, por exemplo: C:\windows\notepad.exe. Observação: o nome completo do arquivo é o caminho de algum ponto raiz (unidade C) até o próprio arquivo (notepad.exe). Os nós intermediários dessa cadeia são diretórios (diretório do Windows). Arquivos dentro de diretórios possuem atributos. Por exemplo, “Oculto”, “Somente leitura”, etc. Uma descrição detalhada de algo tão simples como um sistema de arquivos ajudará a entender melhor a definição de nomenclatura e serviços de diretório. Portanto, um serviço de nomes e diretórios é um sistema que gerencia o mapeamento de muitos nomes para muitos objetos. Em nosso sistema de arquivos, interagimos com nomes de arquivos que ocultam objetos – os próprios arquivos em vários formatos. No serviço de nomenclatura e diretório, os objetos nomeados são organizados em uma estrutura de árvore. E os objetos de diretório possuem atributos. Outro exemplo de serviço de nomes e diretórios é o DNS (Domain Name System). Este sistema gerencia o mapeamento entre nomes de domínio legíveis por humanos (por exemplo, https://javarush.com/) e endereços IP legíveis por máquina (por exemplo, 18.196.51.113). Além de DNS e sistemas de arquivos, existem muitos outros serviços, como:

JNDI

JNDI, ou Java Naming and Directory Interface, é uma API Java para acessar serviços de nomenclatura e diretório. JNDI é uma API que fornece um mecanismo uniforme para um programa Java interagir com vários serviços de nomenclatura e diretório. Nos bastidores, a integração entre JNDI e qualquer serviço é realizada usando uma Service Provider Interface (SPI). O SPI permite que vários serviços de nomenclatura e diretório sejam conectados de forma transparente, permitindo que um aplicativo Java use a API JNDI para acessar os serviços conectados. A figura abaixo ilustra a arquitetura JNDI: Usando JNDI em Java - 2

Fonte: Tutoriais Oracle Java

JNDI. Significado em palavras simples

A questão principal é: por que precisamos do JNDI? JNDI é necessário para que possamos obter um objeto Java de algum “registro” de objetos do código Java pelo nome do objeto vinculado a este objeto. Vamos decompor a afirmação acima em teses para que a abundância de palavras repetidas não nos confunda:
  1. Em última análise, precisamos obter um objeto Java.
  2. Obteremos este objeto de algum registro.
  3. Existem vários objetos neste registro.
  4. Cada objeto neste registro possui um nome exclusivo.
  5. Para obter um objeto do registro, devemos passar um nome em nossa solicitação. Como se dissesse: “Por favor, dê-me o que você tem sob tal e tal nome”.
  6. Podemos não apenas ler objetos pelo nome no registro, mas também salvar objetos neste registro com determinados nomes (de alguma forma, eles vão parar lá).
Portanto, temos algum tipo de registro, ou armazenamento de objetos, ou árvore JNDI. A seguir, usando um exemplo, vamos tentar entender o significado de JNDI. É importante notar que na maior parte o JNDI é usado no desenvolvimento empresarial. E esses aplicativos funcionam dentro de algum servidor de aplicativos. Este servidor pode ser algum servidor de aplicativos Java EE, ou um contêiner de servlet como o Tomcat, ou qualquer outro contêiner. O próprio registro do objeto, ou seja, a Árvore JNDI, geralmente está localizado dentro deste servidor de aplicação. Este último nem sempre é necessário (você pode ter essa árvore localmente), mas é o mais típico. A árvore JNDI pode ser gerenciada por uma pessoa especial (administrador de sistema ou especialista em DevOps) que irá “salvar no registro” os objetos com seus nomes. Quando nosso aplicativo e a árvore JNDI estão localizados dentro do mesmo contêiner, podemos acessar facilmente qualquer objeto Java armazenado nesse registro. Além disso, o registro e nossa aplicação podem estar localizados em contêineres diferentes e até mesmo em máquinas físicas diferentes. Mesmo assim, o JNDI permite acessar objetos Java remotamente. Caso típico. O administrador do servidor Java EE coloca no registro um objeto que armazena as informações necessárias para conexão com o banco de dados. Assim, para trabalhar com o banco de dados, simplesmente solicitaremos o objeto desejado da árvore JNDI e trabalharemos com ele. É muito confortável. A comodidade também reside no fato de que no desenvolvimento empresarial existem diferentes ambientes. Existem servidores de produção e servidores de teste (e geralmente há mais de um servidor de teste). Então, colocando um objeto para conexão ao banco de dados em cada servidor dentro do JNDI e utilizando esse objeto dentro de nossa aplicação, não teremos que alterar nada ao implantar nossa aplicação de um servidor (teste, lançamento) para outro. Haverá acesso ao banco de dados em todos os lugares. O exemplo, é claro, é um tanto simplificado, mas espero que ajude você a entender melhor por que o JNDI é necessário. A seguir conheceremos mais de perto o JNDI em Java, com alguns elementos de assalto.

API JNDI

JNDI é fornecido na plataforma Java SE. Para usar JNDI, você deve importar classes JNDI, bem como um ou mais provedores de serviços para acessar serviços de nomenclatura e diretório. O JDK inclui provedores de serviços para os seguintes serviços:
  • Protocolo Leve de Acesso a Diretórios (LDAP);
  • Arquitetura Common Object Request Broker (CORBA);
  • Serviço de nomes Common Object Services (COS);
  • Registro de Invocação de Método Remoto Java (RMI);
  • Serviço de Nome de Domínio (DNS).
O código da API JNDI é dividido em vários pacotes:
  • javax.nomeação;
  • javax.naming.diretório;
  • javax.naming.ldap;
  • javax.naming.event;
  • javax.naming.spi.
Começaremos nossa introdução ao JNDI com duas interfaces - Nome e Contexto, que contêm as principais funcionalidades do JNDI

Nome da interface

A interface Name permite controlar nomes de componentes, bem como a sintaxe de nomenclatura JNDI. No JNDI, todas as operações de nome e diretório são executadas em relação ao contexto. Não existem raízes absolutas. Portanto, JNDI define um InitialContext, que fornece um ponto de partida para operações de nomenclatura e diretório. Uma vez acessado o contexto inicial, ele pode ser usado para pesquisar objetos e outros contextos.
Name objectName = new CompositeName("java:comp/env/jdbc");
No código acima definimos algum nome sob o qual algum objeto está localizado (pode não estar localizado, mas contamos com isso). Nosso objetivo final é obter uma referência para este objeto e utilizá-lo em nosso programa. Assim, o nome consiste em várias partes (ou tokens), separadas por uma barra. Esses tokens são chamados de contextos. O primeiro é simplesmente contexto, todos os subsequentes são subcontexto (doravante denominado subcontexto). Os contextos são mais fáceis de entender se você pensar neles como análogos a diretórios ou diretórios, ou apenas como pastas normais. O contexto raiz é a pasta raiz. Subcontexto é uma subpasta. Podemos ver todos os componentes (contexto e subcontextos) de um determinado nome executando o seguinte código:
Enumeration<String> elements = objectName.getAll();
while(elements.hasMoreElements()) {
  System.out.println(elements.nextElement());
}
A saída será a seguinte:

java:comp
env
jdbc
A saída mostra que os tokens no nome estão separados uns dos outros por uma barra (no entanto, mencionamos isso). Cada token de nome possui seu próprio índice. A indexação do token começa em 0. O contexto raiz tem índice zero, o próximo contexto tem índice 1, o próximo 2, etc. Podemos obter o nome do subcontexto pelo seu índice:
System.out.println(objectName.get(1)); // -> env
Também podemos adicionar tokens adicionais (no final ou em um local específico do índice):
objectName.add("sub-context"); // Добавит sub-context в конец
objectName.add(0, "context"); // Добавит context в налачо
Uma lista completa de métodos pode ser encontrada na documentação oficial .

Contexto da interface

Esta interface contém um conjunto de constantes para inicializar um contexto, bem como um conjunto de métodos para criar e excluir contextos, vincular objetos a um nome e procurar e recuperar objetos. Vejamos algumas das operações executadas usando esta interface. A ação mais comum é procurar um objeto pelo nome. Isso é feito usando métodos:
  • Object lookup(String name)
  • Object lookup(Name name)
A vinculação de um objeto a um nome é feita usando métodos bind:
  • void bind(Name name, Object obj)
  • void bind(String name, Object obj)
Ambos os métodos vincularão o nome ao objeto. Object A operação inversa de vinculação - desvincular um objeto de um nome, é realizada usando os métodos unbind:
  • void unbind(Name name)
  • void unbind(String name)
Uma lista completa de métodos está disponível no site de documentação oficial .

Contexto Inicial

InitialContexté uma classe que representa o elemento raiz da árvore JNDI e implementa o Context. Você precisa procurar objetos por nome dentro da árvore JNDI relativa a um determinado nó. O nó raiz da árvore pode servir como tal nó InitialContext. Um caso de uso típico para JNDI é:
  • Pegar InitialContext.
  • Use InitialContextpara recuperar objetos por nome da árvore JNDI.
Existem várias maneiras de obtê-lo InitialContext. Tudo depende do ambiente em que o programa Java está localizado. Por exemplo, se um programa Java e uma árvore JNDI estiverem rodando dentro do mesmo servidor de aplicação, é InitialContextbastante simples obter:
InitialContext context = new InitialContext();
Se não for esse o caso, obter contexto torna-se um pouco mais difícil. Às vezes é necessário passar uma lista de propriedades do ambiente para inicializar o contexto:
Hashtable env = new Hashtable();
env.put(Context.INITIAL_CONTEXT_FACTORY,
    "com.sun.jndi.fscontext.RefFSContextFactory");

Context ctx = new InitialContext(env);
O exemplo acima demonstra uma das formas possíveis de inicializar um contexto e não carrega nenhuma outra carga semântica. Não há necessidade de mergulhar detalhadamente no código.

Um exemplo de uso de JNDI dentro de um teste de unidade SpringBoot

Acima, dissemos que para que o JNDI interaja com o serviço de nomenclatura e diretório é necessário ter em mãos o SPI (Service Provider Interface), com o qual será realizada a integração entre o Java e o serviço de nomenclatura. O JDK padrão vem com vários SPIs diferentes (listamos acima), cada um dos quais é de pouco interesse para fins de demonstração. Criar uma aplicação JNDI e Java dentro de um contêiner é um tanto interessante. Porém, o autor deste artigo é uma pessoa preguiçosa, então para demonstrar como o JNDI funciona, ele escolheu o caminho de menor resistência: executar o JNDI dentro de um teste unitário da aplicação SpringBoot e acessar o contexto JNDI usando um pequeno hack do Spring Framework. Então, nosso plano:
  • Vamos escrever um projeto Spring Boot vazio.
  • Vamos criar um teste unitário dentro deste projeto.
  • Dentro do teste demonstraremos como trabalhar com JNDI:
    • ter acesso ao contexto;
    • vincular (vincular) algum objeto sob algum nome em JNDI;
    • obtenha o objeto pelo seu nome (pesquisa);
    • Vamos verificar se o objeto não é nulo.
Vamos começar em ordem. Arquivo->Novo->Projeto... Usando JNDI em Java - 3 Em seguida, selecione o item Spring Initializr : Usando JNDI em Java - 4Preencha os metadados sobre o projeto: Usando JNDI em Java - 5Em seguida, selecione os componentes necessários do Spring Framework. Iremos vincular alguns objetos DataSource, portanto precisamos de componentes para funcionar com o banco de dados:
  • APIJDBC;
  • H2 DBanco de dados.
Usando JNDI em Java - 6Vamos determinar a localização no sistema de arquivos: Usando JNDI em Java - 7E o projeto é criado. Na verdade, um teste unitário foi gerado automaticamente para nós, que usaremos para fins de demonstração. Abaixo está a estrutura do projeto e o teste que precisamos: Usando JNDI em Java - 8Vamos começar a escrever o código dentro do teste contextLoads. Um pequeno hack do Spring, discutido acima, é a classe SimpleNamingContextBuilder. Esta classe foi projetada para aumentar facilmente o JNDI dentro de testes de unidade ou aplicativos independentes. Vamos escrever o código para obter o contexto:
final SimpleNamingContextBuilder simpleNamingContextBuilder
       = new SimpleNamingContextBuilder();
simpleNamingContextBuilder.activate();

final InitialContext context = new InitialContext();
As duas primeiras linhas de código nos permitirão inicializar facilmente o contexto JNDI posteriormente. Sem eles, InitialContextuma exceção será lançada ao criar uma instância: javax.naming.NoInitialContextException. Isenção de responsabilidade. A classe SimpleNamingContextBuilderé uma classe obsoleta. E este exemplo pretende mostrar como você pode trabalhar com JNDI. Estas não são práticas recomendadas para usar JNDI dentro de testes de unidade. Pode-se dizer que isso é uma muleta para construir um contexto e demonstrar a ligação e recuperação de objetos do JNDI. Tendo recebido um contexto, podemos extrair objetos dele ou procurar objetos no contexto. Ainda não há objetos no JNDI, então seria lógico colocar algo lá. Por exemplo, DriverManagerDataSource:
context.bind("java:comp/env/jdbc/datasource", new DriverManagerDataSource("jdbc:h2:mem:mydb"));
Nesta linha, vinculamos o objeto de classe DriverManagerDataSourceao nome java:comp/env/jdbc/datasource. A seguir, podemos obter o objeto do contexto por nome. Não temos escolha a não ser pegar o objeto que acabamos de colocar, pois não há outros objetos no contexto =(
final DataSource ds = (DataSource) context.lookup("java:comp/env/jdbc/datasource");
Agora vamos verificar se nosso DataSource possui uma conexão (conexão, conexão ou conexão é uma classe Java projetada para funcionar com um banco de dados):
assert ds.getConnection() != null;
System.out.println(ds.getConnection());
Se fizermos tudo corretamente, a saída será mais ou menos assim:

conn1: url=jdbc:h2:mem:mydb user=
Vale dizer que algumas linhas de código podem gerar exceções. As seguintes linhas são lançadas javax.naming.NamingException:
  • simpleNamingContextBuilder.activate()
  • new InitialContext()
  • context.bind(...)
  • context.lookup(...)
E ao trabalhar com uma classe DataSourceela pode ser lançada java.sql.SQLException. Nesse sentido, é necessário executar o código dentro de um bloco try-catch, ou indicar na assinatura da unidade de teste que ela pode lançar exceções. Aqui está o código completo da classe de teste:
@SpringBootTest
class JndiExampleApplicationTests {

    @Test
    void contextLoads() {
        try {
            final SimpleNamingContextBuilder simpleNamingContextBuilder
                    = new SimpleNamingContextBuilder();
            simpleNamingContextBuilder.activate();

            final InitialContext context = new InitialContext();

            context.bind("java:comp/env/jdbc/datasource", new DriverManagerDataSource("jdbc:h2:mem:mydb"));

            final DataSource ds = (DataSource) context.lookup("java:comp/env/jdbc/datasource");

            assert ds.getConnection() != null;
            System.out.println(ds.getConnection());

        } catch (SQLException | NamingException e) {
            e.printStackTrace();
        }
    }
}
Depois de executar o teste, você poderá ver os seguintes logs:

o.s.m.jndi.SimpleNamingContextBuilder    : Activating simple JNDI environment
o.s.mock.jndi.SimpleNamingContext        : Static JNDI binding: [java:comp/env/jdbc/datasource] = [org.springframework.jdbc.datasource.DriverManagerDataSource@4925f4f5]
conn1: url=jdbc:h2:mem:mydb user=

Conclusão

Hoje olhamos para JNDI. Aprendemos o que são serviços de nomenclatura e diretório e que JNDI é uma API Java que permite interagir uniformemente com diferentes serviços de um programa Java. Ou seja, com a ajuda do JNDI, podemos gravar objetos na árvore JNDI com um determinado nome e receber esses mesmos objetos pelo nome. Como tarefa bônus, você pode executar um exemplo de como o JNDI funciona. Vincule algum outro objeto ao contexto e, em seguida, leia esse objeto pelo nome.
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION