menu icon

Lire et parser les ressources en Java facilement

Il peut paraitre que c'est un vieux sujet et que tout est déjà dit. Néanmoins, même en 2020 manipuler les ressources en Java reste plus compliqué que ça ne devrait l'être. Ensemble, nous allons faire le tour des solutions existantes et nous allons en découvrir une nouvelle, plus simple et plus conviviale.

Lire et parser les ressources en Java facilement

Ici, chez Adelean, on travaille avec Elasticsearch et nous sommes habitués à voir beaucoup de JSONs: les requêtes, les réponses, les mappings, les settings, etc. 90% des fonctionnalités qu’on développe impliquent la manipulation du JSON. En conséquence, il est habituel pour nous d’avoir du JSON en tant que fichiers ressources dans notre code Java.

Un jour, après avoir chargé et parsé le JSON pour trente-six-millième fois je m’en suis rendu compte: lire le contenu des ressources en Java est plus compliqué que ça ne devrait être!

Regardons ensemble. Assumons que nous avons un fichier ressource com/adelean/junit/jupiter/resource.txt qui contient ce texte:

The quick brown fox jumps over the lazy dog.

Voici une des solutions pour lire son contenu avec Java pur:

String resourcePath = "com/adelean/junit/jupiter/resource.txt";
ClassLoader classLoader = getClass().getClassLoader();
              
StringBuilder textBuilder = new StringBuilder();
              
try (InputStream resourceAsStream = classLoader.getResourceAsStream(resourcePath);
     InputStreamReader streamReader = new InputStreamReader(resourceAsStream);
     BufferedReader bufferedReader = new BufferedReader(streamReader)) {
    int c = 0;
    while ((c = bufferedReader.read()) != -1) {
        textBuilder.append((char) c);
    }
} catch (IOException ioException) {
    // handle exception
}
              
String resourceContent = textBuilder.toString();

Ça a l’air compliqué, n’est-ce pas ? Il faut ouvrir et fermer plusieurs streams/readers, gérer les exceptions I/O, tout ça juste pour avoir le contenu textuel.

Cette discussion sur Stackoverflow (https://stackoverflow.com/questions/15749192/how-do-i-load-a-file-from-resource-folder) et beaucoup d’autres expliquent comment lire les ressources. Vous pouvez y jeter un coup d’oeil, ou mieux, continuer à lire cet article.

Heureusement pour nous, il existe d’excellentes librairies comme google Guava, qui simplifient significativement la tâche. Voici la même opération avec Guava:

URL url = Resources.getResource("com/adelean/junit/jupiter/resource.txt");
try {
    String resourceContent = Resources.toString(url, StandardCharsets.UTF_8);
} catch (IOException ioException) {
    // handle exception
}

Même plus simple, cette solution reste assez compliquée. Cette tâche, probablement, devrait être faite en une seule ligne de code. Et l’on a toujours besoin de gérer les exceptions.

Je trouve ça surprenant que l’on doive gérer les exceptions quand on manipule les ressources en Java. Étant justifiée pour les fichiers, car un fichier peut manquer ou ne pas avoir de bonnes permissions, la situation est différente pour les ressources.

Les ressources se trouvent sur le classpath de l’application Java, pas dans un système de fichiers. L’exception IOException ne va pratiquement jamais survenir, sauf dans le cas d’erreur dans votre code. La lecture d’une ressource rassemble plus à un import d’une classe qu'à la lecture à partir du système des fichiers. Vous n’entourez pas vos déclarations des imports avec les try-catch, n’est-ce pas ?


C’est ici que la librairie @InjectResources (https://github.com/hosuaby/inject-resources) vient au secours. Elle offre une syntaxe simple et fluide pour lire et parser les ressources sans le code “boilerplate”. Elle fournit également les extensions pour Spring et les frameworks de tests comme JUnit4/5. Avec @InjectResources lire une ressource devient aussi simple qu’ajouter une annotation sur un champ d’une classe.

@TextResource("com/adelean/junit/jupiter/resource.txt")
private String text;

Regardons plus en détail comment ça marche!


Au coeur de @InjectResources (https://hosuaby.github.io/inject-resources/0.1.0/asciidoc/#inject-resources-core) se trouve un Java DSL convivial et fluide, qui nous libère du code “boilerplate” comme l’ouverture et fermeture des InputStreams et la gestion des exceptions I/O. Tout d’abord, il faut ajouter inject-resources-core à votre projet:

Avec Gradle:

repositories {
    jcenter()
}

dependencies {
    compile group: 'com.adelean', name: 'inject-resources-core', version: '0.1.0'
}

Ou avec Maven:

<repositories>
    <repository>
        <snapshots>
            <enabled>false</enabled>
        </snapshots>
        <id>central</id>
        <name>bintray</name>
        <url>https://jcenter.bintray.com</url>
    </repository>
</repositories>

<dependencies>
    <dependency>
        <groupId>com.adelean</groupId>
        <artifactId>inject-resources-core</artifactId>
        <version>0.1.0</version>
    </dependency>
</dependencies>

Le point d’entrée du DSL est la méthode InjectResources.resource. Il est recommandé de l’embarquer avec un import statique:

import static com.adelean.inject.resources.core.InjectResources.resource;

Après, on peut utiliser la syntaxe fluide de la librairie pour obtenir le contenu de n’importe quelle ressource:

var text = resource()
        .withPath("/com/adelean/junit/jupiter", "resource.txt")
        .text();

Cette syntaxe offre également différentes méthodes pour ouvrir la ressource en tant que l’InputStream binaire ou le Reader du texte.

var schema = JavaPropsSchema
        .emptySchema()
        .withoutPathSeparator();

var reader = new JavaPropsMapper()
        .readerFor(DbConnection.class)
        .with(schema);

DbConnection dbConnection = resource()
        .withPath("/com/adelean/junit/jupiter", "db.properties")
        .asInputStream()
        .parseChecked(reader::readValue);

On peut également lire la ressource ligne par ligne de manière fonctionnelle:

var header = new AtomicReference<String>();
var lines = new ArrayList<String>();

resource()
        .onClassLoaderOf(getClass())
        .withPath("/com/adelean/junit/jupiter", "cities.csv")
        .asLines()
        .onFirstLine(header::set)
        .forEachLine(lines::add);

Certaines méthodes ont un nom qui se termine avec le mot “Checked”. Ces méthodes peuvent accepter en paramètre des lambdas qui peuvent jeter des exceptions. Elles nous évitent à gérer les exceptions dans notre code applicatif:

À la place de ça:

var dbConnection = resource()
        .withPath("db.properties")
        .asInputStream()
        .parse(inputStream -> {
            try {
                return reader.readValue(inputStream);
            } catch (IOException parsingException) {
                throw new RuntimeException(parsingException);
            }
        });

On peut écrire ça:

var dbConnection = resource()
        .withPath("db.properties")
        .asInputStream()
        .parseChecked(reader::readValue);

inject-resources-core est une excellente alternative aux librairies de bas niveau comme Apache Commons ou Google Guava. Si vous voulez en savoir plus, n’hésitez pas à faire un tour sur la documentation officielle pour plus d’informations et d’exemples: https://hosuaby.github.io/inject-resources/0.1.0/asciidoc/#inject-resources-core

Mais si vous utilisez Spring, vous pouvez même avoir vos ressources parsées et injectées directement dans vos composants. La prochaine section parlera de l’intégration de @InjectResources avec Spring.


Spring possède son propre mécanisme de chargement des ressources. D’abord, on doit obtenir la référence sur la ressource avec une des trois méthodes proposées. Avec ResourceLoader:

@Autowired
private ResourceLoader resourceLoader;

// Later...

var resource = resourceLoader
    .getResource("classpath:com/adelean/junit/jupiter/resource.txt");

Avec ResourceUtils:

var file = ResourceUtils
      .getFile("classpath:com/adelean/junit/jupiter/resource.txt");

Ou avec annotation @Value:

@Value("classpath:com/adelean/junit/jupiter/resource.txt")
private Resource resource;

Après avoir obtenu la référence de la ressource, on peut lire son contenu:

var textContent = Files.readString(resource.getFile().toPath());

Le module inject-resources-spring (https://hosuaby.github.io/inject-resources/0.1.0/asciidoc/#inject-resources-spring) permet d’obtenir le contenu de la ressource avec une annotation seulement. Il sait aussi comment lire les ressources dans différents formats, comme les fichiers binaires, le texte, les properties Java ou également JSON et YAML.

D’abord, ajoutez la dépendance dans votre projet

Avec Gradle:

compile group: 'com.adelean', name: 'inject-resources-spring', version: '0.1.0'

Ou avec Maven:

<dependency>
    <groupId>com.adelean</groupId>
    <artifactId>inject-resources-spring</artifactId>
    <version>0.1.0</version>
</dependency>

ensuite, activez l’injection des ressources dans votre projet Spring:

@Configuration
@EnableResourceInjection
public class MyConfig {
}

Après, vous pouvez avoir le contenu des ressources injecté directement dans vos composants:

@Component
public class BeanWithTextResource {

    @TextResource("/com/adelean/junit/jupiter/resource.txt")
    private String text;
}

Jusqu'à présent, nous avons vu comment traiter les ressources texte, mais @InjectResources est capable faire plus que ça. On peut également obtenir le contenu des properties Java

@Component
public class BeanWithPropertiesResource {

    @PropertiesResource("/com/adelean/junit/jupiter/db.properties")
    private Properties dbProperties;
}

et des documents JSON

@Component
public class BeanWithJsonResource {

    @JsonResource("/com/adelean/junit/jupiter/sponge-bob.json")
    private Map<String, Object> jsonAsMap;
}

ou des documents YAML

@Component
public class BeanWithYamlResource {

    @YamlResource("/com/adelean/junit/jupiter/sponge-bob.yaml")
    private Person spongeBob;
}

Les annotations JsonResource et YamlResource vont automatiquement convertir le contenu en objet du bon type. Mais pour le faire, elles ont besoin de parseur. JsonResource utilise l’objet ObjectMapper fourni par la librairie Jackson, ou l’objet Gson (fourni par Gson) comme parseur. Assurez-vous d’en avoir un dans votre contexte d’application:

@Configuration
public class JacksonConfig {

    @Bean
    public ObjectMapper defaultObjectMapper() {
        return new ObjectMapper();
    }
}

YamlResource , de son côté, utilise la librairie Snakeyaml, donc le “Bean” du type Yaml doit être présent dans le contexte:

@Configuration
public class SnakeyamlConfig {

    @Bean
    public Yaml defaultYaml() {
        return new Yaml();
    }
}

Nous avons terminé l’introduction de @InjectResource. Cette librairie est très simple, mais puissante. N’hésitez pas à aller voir sa documentation pour plus de détails et des exemples: https://hosuaby.github.io/inject-resources/0.1.0/asciidoc/#inject-resources-spring

Dans le prochain article, nous allons voir comment avoir le contenu des ressources facilement dans nos tests JUnit.