Ir para o conteúdo
AZ Tools

Analisador de Java .properties

Um arquivo .properties parece o formato de configuração mais simples que existe e é o que os parsers feitos à mão mais erram. Esta página executa o algoritmo que java.util.Properties.load realmente usa — o LineReader do JDK e depois load0 e loadConvert — sobre o texto que você colar ou soltar, e mostra o mapa resultante chave por chave, como JSON e como o arquivo que Properties.store escreveria de volta. Ela aponta exatamente as partes em que as pessoas tropeçam. O separador é =, : ou uma sequência de espaços, então "c three" é a chave c com o valor three, e "key with spaces=x" é a chave "key" com o valor "with spaces=x". O espaço em volta do separador some; o do fim do valor não some, e nenhum editor mostra. Uma barra invertida no fim da linha a continua e remove a indentação da linha seguinte, mas um número par de barras não continua nada, e uma linha de comentário terminada em barra também não. # e ! abrem comentário apenas como primeiro caractere não branco da linha, então o # de um fragmento de URL fica dentro do valor. Os únicos escapes são \t \n \r \f e \uXXXX; qualquer outra barra invertida é descartada em silêncio. O maior problema real deste formato é a codificação. O load(InputStream) — a chamada que quase todo framework faz — decodifica os bytes como ISO-8859-1, então um arquivo salvo em UTF-8 transforma cada caractere acentuado em dois ou três. Quando a entrada tem caracteres não ASCII, esta página mostra as duas leituras e nomeia as chaves que mudam, para você escolher entre escapes \uXXXX e load(Reader) com um charset explícito. Tudo roda no seu navegador e nada é enviado. O que a ferramenta não pode dizer é qual das duas chaves duplicadas você queria, nem se um valor que parece errado está errado: ela relata o que o JDK fará com esses bytes, não o que você pretendia escrever.

Conteúdo do arquivo .properties

Chaves únicas

11

Atribuições

12

Linhas de comentário

1

Linhas continuadas

1

Achados

6

Ler os bytes como:

O load(InputStream) é o padrão em quase todo framework e sempre lê ISO-8859-1. Alterne entre as duas leituras para ver quais chaves mudam.

O que o analisador notou
  • Texto não ASCII vai corromper · linha 10

    O load(InputStream) decodifica os bytes como ISO-8859-1, então um arquivo salvo em UTF-8 transforma cada caractere acentuado em dois ou três latinos. Escreva-os como \uXXXX ou carregue com um Reader e um charset explícito.

    • greeting: "Grüße" → "Grüße"
  • Separador de espaço · linha 4

    Nesta linha não há = nem :: a primeira sequência de espaços ou tabulações separa a chave do valor, então "c three" é c = three.

    app.mode

  • Espaços finais preservados · linha 9

    O espaço em volta do separador é descartado, mas o do fim do valor faz parte do valor. Nenhum editor mostra isso e nenhum erro menciona.

    app.token (+3)

  • Separador dentro da chave · linha 6

    Esta chave contém um =, : ou espaço escapado, então a linha é uma chave só. Código que corta no primeiro "=" partiria no lugar errado.

    menu.label 1

  • Não é um comentário · linha 14

    # e ! abrem comentário apenas como primeiro caractere não branco da linha. Aqui um deles vem depois de um valor, então fica dentro do valor.

    note

  • Chave duplicada · linha 2, 12

    A mesma chave é atribuída mais de uma vez. O load mantém o último valor e não avisa nada: as linhas anteriores são configuração morta que ainda parece viva num diff.

    app.name → "orders-api-v2"

LinhaChaveValorSeparadorNotas
1# Spring Boot style configuration — and every trap in the formatcomentário
2app.nameorders-api=sobrescrita
3server.port8080:
4app.modeproductionespaçoSeparador de espaço
5jdbc.urljdbc:postgresql://db:5432/orders?ssl=true=
6menu.label 1Save as…=Separador dentro da chave
7–8welcome.messageHello and welcome=
9app.tokenabc123···=Espaços finais preservados
10greetingGrüße=
11greeting.escapedGrüße=
12app.nameorders-api-v2=
13pathC:\\Users\\dev\\app=
14notevalue # this is not a comment=Não é um comentário

A tabela escapa caracteres de controle (\n, \t) e marca espaços finais com · para que fiquem visíveis; a visão JSON é exata.

Como usar

  1. Cole o conteúdo na caixa ou solte um arquivo .properties na área indicada: ele é lido no navegador e nunca enviado.
  2. Leia a tabela por linha: cada entrada com a linha de onde veio, o separador realmente usado e uma etiqueta para o que houver de incomum.
  3. Percorra os achados acima. Chaves duplicadas, espaço no fim do valor e uma continuação que engoliu a entrada seguinte são os três que mudam o comportamento em produção sem avisar.
  4. Se houver caracteres não ASCII, alterne a leitura entre UTF-8 e ISO-8859-1 para ver exatamente o que o load(InputStream) vai entregar à sua aplicação.
  5. Copie a visão JSON para comparar configurações, ou a visão Properties.store para obter o mesmo mapa escrito com todos os separadores e caracteres não ASCII já escapados.

Perguntas frequentes

Por que "c three" vira chave e valor se não há um sinal de =?
Porque neste formato o espaço em branco também é separador. O Properties.load procura o primeiro =, : ou caractere de espaço não escapado, o que vier primeiro, e toma como chave tudo o que vem antes. Por isso "c three" é a chave c com o valor three, e em "key with spaces=x" a chave é "key" e o valor é "with spaces=x": o = nunca chega a agir como separador. Se você precisa de um espaço dentro da chave, tem de escrevê-lo como "\ ", que é exatamente o que o Properties.store faz ao gravar essa chave de volta.
Meus caracteres acentuados saem como é. O que aconteceu?
A especificação do java.util.Properties.load(InputStream) diz que o fluxo é decodificado como ISO-8859-1, um byte por caractere. Um arquivo salvo em UTF-8 guarda é em dois bytes, e esses dois bytes viram dois caracteres latinos — o mojibake clássico. Nada é lançado, então a corrupção viaja até onde o texto for exibido. Há três correções: escrever o não ASCII como \uXXXX (o que native2ascii e Properties.store produzem), chamar load(Reader) com um InputStreamReader em UTF-8, ou usar loadFromXML, que é UTF-8 por padrão.
O espaço no fim de um valor sobrevive mesmo?
Sobrevive, e é a origem de um gênero inteiro de relatos de bug. O espaço no início da linha é descartado, o que cerca o separador é descartado e a indentação de uma linha de continuação também, mas o espaço no fim do valor é mantido literalmente, porque a linha acaba ali e ninguém apara. Uma senha ou URL com um espaço a mais é analisada sem erro, não conecta em nada e parece idêntica à versão boa em qualquer editor e em qualquer revisão de código. A tabela desta página marca esses caracteres para que fiquem visíveis.
Quando uma linha continua na seguinte?
Quando termina em um número ímpar de barras invertidas. A última barra é removida, a linha seguinte é anexada e a indentação dela é retirada, então uma continuação indentada lê-se naturalmente. Um número par não é continuação: são barras literais, pela metade, e a entrada acaba ali. Daí saem duas surpresas. Uma continuação pode engolir o que parece a próxima entrada: se um valor termina em uma barra perdida, o "chave=valor" de baixo passa a fazer parte daquele valor e a chave some. E uma linha de comentário terminada em barra não continua de jeito nenhum, porque comentários são descartados inteiros.
Duas linhas definem a mesma chave. Qual vence?
A última, em silêncio. Properties é uma Hashtable e o load apenas chama put para cada entrada conforme avança, então uma linha posterior sobrescreve a anterior sem aviso, sem erro e sem deixar registro. É fácil criar isso sem querer quando um arquivo é montado a partir de vários fragmentos, ou quando uma chave é definida uma vez com = e outra com : e as duas não se parecem à primeira vista. Esta página lista cada chave duplicada com todos os números de linha que a definem e esmaece as que perderam.
É o mesmo formato de um arquivo .env?
Não, e tratar um como o outro é fonte comum de quebras silenciosas. Um .env não tem separador por espaço, usa aspas para proteger valores e guardar textos de várias linhas, trata um # depois do valor como comentário na mesma linha e muitas vezes suporta expansão ${VAR}. Um .properties não tem nada disso: aspas são caracteres comuns que acabam dentro do valor, o # depois de um valor faz parte do valor, o formato em si não expande variáveis (o Spring acrescenta a dele por cima) e a continuação é feita com barras invertidas, não com aspas.

Ferramentas relacionadas