Analisando YAML com SnakeYAML

Analisando YAML com SnakeYAML

1. Visão geral

Neste tutorial, aprenderemos como usar a bibliotecaSnakeYAML paraserialize Java objects to YAML documents and vice versa.

2. Configuração do Projeto

Para usar SnakeYAML em nosso projeto, adicionaremos a seguinte dependência Maven (a versão mais recente pode ser encontradahere):


    org.yaml
    snakeyaml
    1.21

3. Ponto de entrada

A classeYaml é o ponto de entrada para a API:

Yaml yaml = new Yaml();

Como a implementação não é segura para threads, diferentes threads devem ter sua própria instânciaYaml.

4. Carregando um documento YAML

A biblioteca fornece suporte para carregar o documento de umString ou umInputStream. A maioria dos exemplos de código aqui seria baseada na análise deInputStream.

Vamos começar definindo um documento YAML simples e nomeando o arquivo comocustomer.yaml:

firstName: "John"
lastName: "Doe"
age: 20

4.1. Uso básico

Agora vamos analisar o documento YAML acima com a classeYaml:

Yaml yaml = new Yaml();
InputStream inputStream = this.getClass()
  .getClassLoader()
  .getResourceAsStream("customer.yaml");
Map obj = yaml.load(inputStream);
System.out.println(obj);

O código acima gera a seguinte saída:

{firstName=John, lastName=Doe, age=20}

Por padrão, o métodoload() retorna uma instânciaMap. Consultar o objetoMap toda vez exigiria que saibamos os nomes das chaves de propriedade com antecedência, e também não é fácil percorrer as propriedades aninhadas.

4.2. Tipo personalizado

A biblioteca tambémprovides a way to load the document as a custom class. Esta opção permitiria fácil passagem de dados na memória.

Vamos definir uma classeCustomer e tentar carregar o documento novamente:

public class Customer {

    private String firstName;
    private String lastName;
    private int age;

    // getters and setters
}

Supondo que o documento YAML seja desserializado como um tipo conhecido, podemos especificar umtag global explícito no documento.

Vamos atualizar o documento e armazená-lo em um novo arquivocustomer_with_type.yaml:

!!com.example.snakeyaml.Customer
firstName: "John"
lastName: "Doe"
age: 20

Observe a primeira linha do documento, que contém as informações sobre a classe a ser usada ao carregá-la.

Agora vamos atualizar o código usado acima e passar o novo nome do arquivo como entrada:

Yaml yaml = new Yaml();
InputStream inputStream = this.getClass()
 .getClassLoader()
 .getResourceAsStream("yaml/customer_with_type.yaml");
Customer customer = yaml.load(inputStream);

O métodoload() agora retorna uma instância do tipoCustomer * A desvantagem dessa abordagem é que o tipo deve ser exportado como uma biblioteca para ser usado onde necessário *.

Embora possamos usar a tag local explícita, para a qual não somos obrigados a exportar bibliotecas.

Another way of loading a custom type is by using the Constructor class. Dessa forma, podemos especificar o tipo de raiz para um documento YAML a ser analisado. Vamos criar uma instânciaConstructor com o tipoCustomer como tipo raiz e passá-lo para a instânciaYaml.

Agora, ao carregar ocustomer.yaml, we obterá o objetoCustomer:

Yaml yaml = new Yaml(new Constructor(Customer.class));

4.3. Tipos implícitos

In case there’s no type defined for a given property, the library automatically converts the value to an implicit type.

Por exemplo:

1.0 -> Float
42 -> Integer
2009-03-30 -> Date

Vamos testar essa conversão implícita de tipo usando um caso de teste:

@Test
public void whenLoadYAML_thenLoadCorrectImplicitTypes() {
   Yaml yaml = new Yaml();
   Map document = yaml.load("3.0: 2018-07-22");

   assertNotNull(document);
   assertEquals(1, document.size());
   assertTrue(document.containsKey(3.0d));
}

4.4. Objetos e coleções aninhados

Given a top-level type, the library automatically detects the types of nested objects, a menos que sejam uma interface ou classe abstrata, e desserializa o documento no tipo aninhado relevante.

Vamos adicionarContacteAddress detalhes aoscustomer.yaml,e salvar o novo arquivo comocustomer_with_contact_details_and_address.yaml. 

Agora vamos analisar o novo documento YAML:

firstName: "John"
lastName: "Doe"
age: 31
contactDetails:
   - type: "mobile"
     number: 123456789
   - type: "landline"
     number: 456786868
homeAddress:
   line: "Xyz, DEF Street"
   city: "City Y"
   state: "State Y"
   zip: 345657

A classeCustomer também deve refletir essas mudanças. Aqui está a aula atualizada:

public class Customer {
    private String firstName;
    private String lastName;
    private int age;
    private List contactDetails;
    private Address homeAddress;
    // getters and setters
}

Vamos ver como as classesContact eAddress se parecem:

public class Contact {
    private String type;
    private int number;
    // getters and setters
}
public class Address {
    private String line;
    private String city;
    private String state;
    private Integer zip;
    // getters and setters
}

Agora vamos testar oYaml #load() com o caso de teste fornecido:

@Test
public void
  whenLoadYAMLDocumentWithTopLevelClass_thenLoadCorrectJavaObjectWithNestedObjects() {

    Yaml yaml = new Yaml(new Constructor(Customer.class));
    InputStream inputStream = this.getClass()
      .getClassLoader()
      .getResourceAsStream("yaml/customer_with_contact_details_and_address.yaml");
    Customer customer = yaml.load(inputStream);

    assertNotNull(customer);
    assertEquals("John", customer.getFirstName());
    assertEquals("Doe", customer.getLastName());
    assertEquals(31, customer.getAge());
    assertNotNull(customer.getContactDetails());
    assertEquals(2, customer.getContactDetails().size());

    assertEquals("mobile", customer.getContactDetails()
      .get(0)
      .getType());
    assertEquals(123456789, customer.getContactDetails()
      .get(0)
      .getNumber());
    assertEquals("landline", customer.getContactDetails()
      .get(1)
      .getType());
    assertEquals(456786868, customer.getContactDetails()
      .get(1)
      .getNumber());
    assertNotNull(customer.getHomeAddress());
    assertEquals("Xyz, DEF Street", customer.getHomeAddress()
      .getLine());
}

4.5. Coleções de tipo seguro

Quando uma ou mais propriedades de uma determinada classe Java são coleções de tipo seguro (genérico), então é importante especificar oTypeDescription para que o tipo parametrizado correto seja identificado.

Vamos pegar umCustomer com mais de umContact e tentar carregá-lo:

firstName: "John"
lastName: "Doe"
age: 31
contactDetails:
   - { type: "mobile", number: 123456789}
   - { type: "landline", number: 123456789}

Para carregar este documento,we can specify the TypeDescription for the given property on the top level class:

Constructor constructor = new Constructor(Customer.class);
TypeDescription customTypeDescription = new TypeDescription(Customer.class);
customTypeDescription.addPropertyParameters("contactDetails", Contact.class);
constructor.addTypeDescription(customTypeDescription);
Yaml yaml = new Yaml(constructor);

4.6. Carregando vários documentos

Pode haver casos em que, em um únicoFile, existem vários documentos YAML e queremos analisar todos eles. A classeYaml fornece um métodoloadAll() para fazer esse tipo de análise.

Por padrão, o método retorna uma instância deIterable<Object> onde cada objeto é do tipoMap<String, Object>. Se um tipo personalizado for desejado, podemos usar a instânciaConstructor conforme discutido acima

Considere os seguintes documentos em um único arquivo:

---
firstName: "John"
lastName: "Doe"
age: 20
---
firstName: "Jack"
lastName: "Jones"
age: 25

Podemos analisar o acima usando o métodoloadAll(), conforme mostrado no exemplo de código abaixo:

@Test
public void whenLoadMultipleYAMLDocuments_thenLoadCorrectJavaObjects() {
    Yaml yaml = new Yaml(new Constructor(Customer.class));
    InputStream inputStream = this.getClass()
      .getClassLoader()
      .getResourceAsStream("yaml/customers.yaml");

    int count = 0;
    for (Object object : yaml.loadAll(inputStream)) {
        count++;
        assertTrue(object instanceof Customer);
    }
    assertEquals(2,count);
}

5. Dumping YAML Documents

A biblioteca também fornece um método paradump a given Java object into a YAML document. A saída pode serString ou um arquivo / fluxo especificado.

5.1. Uso básico

Começaremos com um exemplo simples de despejo de uma instância deMap<String, Object> em um documento YAML (String):

@Test
public void whenDumpMap_thenGenerateCorrectYAML() {
    Map data = new LinkedHashMap();
    data.put("name", "Silenthand Olleander");
    data.put("race", "Human");
    data.put("traits", new String[] { "ONE_HAND", "ONE_EYE" });
    Yaml yaml = new Yaml();
    StringWriter writer = new StringWriter();
    yaml.dump(data, writer);
    String expectedYaml = "name: Silenthand Olleander\nrace: Human\ntraits: [ONE_HAND, ONE_EYE]\n";

    assertEquals(expectedYaml, writer.toString());
}

O código acima produz a seguinte saída (observe que usar uma instância deLinkedHashMap preserva a ordem dos dados de saída):

name: Silenthand Olleander
race: Human
traits: [ONE_HAND, ONE_EYE]

5.2. Objetos Java personalizados

Também podemos escolherdump custom Java types into an output stream. Isso, no entanto, adicionarátag explícito global ao documento de saída:

@Test
public void whenDumpACustomType_thenGenerateCorrectYAML() {
    Customer customer = new Customer();
    customer.setAge(45);
    customer.setFirstName("Greg");
    customer.setLastName("McDowell");
    Yaml yaml = new Yaml();
    StringWriter writer = new StringWriter();
    yaml.dump(customer, writer);
    String expectedYaml = "!!com.example.snakeyaml.Customer {age: 45, contactDetails: null, firstName: Greg,\n  homeAddress: null, lastName: McDowell}\n";

    assertEquals(expectedYaml, writer.toString());
}

Com a abordagem acima, ainda estamos despejando as informações da tag no documento YAML.

Isso significa que precisamos exportar nossa classe como uma biblioteca para qualquer consumidor que a desserializar. Para evitar o nome da tag no arquivo de saída, podemos usar o métododumpAs() fornecido pela biblioteca.

Portanto, no código acima, podemos ajustar o seguinte para remover a tag:

yaml.dumpAs(customer, Tag.MAP, null);

6. Conclusão

Este artigo ilustrou os usos da biblioteca SnakeYAML para serializar objetos Java para YAML e vice-versa.

Todos os exemplos podem ser encontrados emthe GitHub project - este é um projeto baseado em Maven, portanto, deve ser fácil de importar e executar como está.