Externaliser les données de configuration via CSV dans une application Spring

Externaliser les données de configuration via CSV dans une application Spring

1. Vue d'ensemble

Dans cet article, nous allonsexternalize the setup data of an application using CSV files, au lieu de le coder en dur.

Ce processus de configuration concerne principalement la configuration de nouvelles données sur un nouveau système.

2. Une bibliothèque CSV

Commençons par présenter une bibliothèque simple pour travailler avec CSV - lesJackson CSV extension:


    com.fasterxml.jackson.dataformat
    jackson-dataformat-csv
    2.5.3

Il existe bien sûr une multitude de bibliothèques disponibles pour travailler avec les CSV dans l'écosystème Java.

La raison pour laquelle nous choisissons Jackson ici est que - il est probable que Jackson soit déjà utilisé dans l'application, et le traitement dont nous avons besoin pour lire les données est assez simple.

3. Les données de configuration

Différents projets devront configurer différentes données.

Dans ce didacticiel, nous allons configurer les données utilisateur - essentiellementpreparing the system with a few default users.

Voici le simple fichier CSV contenant les utilisateurs:

id,username,password,accessToken
1,john,123,token
2,tom,456,test

Notez comment la première ligne du fichier estthe header row - listant les noms des champs dans chaque ligne de données.

3. Chargeur de données CSV

Commençons par créer un simple chargeur de données versread up data from the CSV files into working memory.

3.1. Charger une liste d'objets

Nous allons implémenter la fonctionnalitéloadObjectList() pour charger une liste entièrement paramétrée deObject spécifiques à partir du fichier:

public  List loadObjectList(Class type, String fileName) {
    try {
        CsvSchema bootstrapSchema = CsvSchema.emptySchema().withHeader();
        CsvMapper mapper = new CsvMapper();
        File file = new ClassPathResource(fileName).getFile();
        MappingIterator readValues =
          mapper.reader(type).with(bootstrapSchema).readValues(file);
        return readValues.readAll();
    } catch (Exception e) {
        logger.error("Error occurred while loading object list from file " + fileName, e);
        return Collections.emptyList();
    }
}

Remarques:

  • Nous avons créé lesCSVSchema basés sur la première ligne «d'en-tête».

  • L'implémentation est suffisamment générique pour gérer n'importe quel type d'objet.

  • Si une erreur survient, une liste vide sera retournée.

3.2. Gérer les relations plusieurs à plusieurs

Les objets imbriqués ne sont pas bien pris en charge dans Jackson CSV - nous devrons utiliser un moyen indirect de charger des relations plusieurs à plusieurs.

Nous allons représenter cessimilar to simple Join Tables - donc naturellement nous allons charger à partir du disque comme une liste de tableaux:

public List loadManyToManyRelationship(String fileName) {
    try {
        CsvMapper mapper = new CsvMapper();
        CsvSchema bootstrapSchema = CsvSchema.emptySchema().withSkipFirstDataRow(true);
        mapper.enable(CsvParser.Feature.WRAP_AS_ARRAY);
        File file = new ClassPathResource(fileName).getFile();
        MappingIterator readValues =
          mapper.reader(String[].class).with(bootstrapSchema).readValues(file);
        return readValues.readAll();
    } catch (Exception e) {
        logger.error(
          "Error occurred while loading many to many relationship from file = " + fileName, e);
        return Collections.emptyList();
    }
}

Voici comment l'une de ces relations -Roles <→ Privileges - est représentée dans un simple fichier CSV:

role,privilege
ROLE_ADMIN,ADMIN_READ_PRIVILEGE
ROLE_ADMIN,ADMIN_WRITE_PRIVILEGE
ROLE_SUPER_USER,POST_UNLIMITED_PRIVILEGE
ROLE_USER,POST_LIMITED_PRIVILEGE

Notez que nous ignorons l'en-tête dans cette mise en œuvre, car nous n'avons pas vraiment besoin de ces informations.

4. Données de configuration

Nous allons maintenant utiliser un simple beanSetup pour effectuer tout le travail de configuration des privilèges, des rôles et des utilisateurs à partir de fichiers CSV:

@Component
public class Setup {
    ...

    @PostConstruct
    private void setupData() {
        setupRolesAndPrivileges();
        setupUsers();
    }

    ...
}

4.1. Configurer les rôles et privilèges

Commençons par charger lesroles and privileges du disque dans la mémoire de travail, puis les conserver dans le cadre du processus d’installation:

public List getPrivileges() {
    return csvDataLoader.loadObjectList(Privilege.class, PRIVILEGES_FILE);
}

public List getRoles() {
    List allPrivileges = getPrivileges();
    List roles = csvDataLoader.loadObjectList(Role.class, ROLES_FILE);
    List rolesPrivileges = csvDataLoader.
      loadManyToManyRelationship(SetupData.ROLES_PRIVILEGES_FILE);

    for (String[] rolePrivilege : rolesPrivileges) {
        Role role = findRoleByName(roles, rolePrivilege[0]);
        Set privileges = role.getPrivileges();
        if (privileges == null) {
            privileges = new HashSet();
        }
        privileges.add(findPrivilegeByName(allPrivileges, rolePrivilege[1]));
        role.setPrivileges(privileges);
    }
    return roles;
}

private Role findRoleByName(List roles, String roleName) {
    return roles.stream().
      filter(item -> item.getName().equals(roleName)).findFirst().get();
}

private Privilege findPrivilegeByName(List allPrivileges, String privilegeName) {
    return allPrivileges.stream().
      filter(item -> item.getName().equals(privilegeName)).findFirst().get();
}

Ensuite, nous effectuerons le travail de persistance ici:

private void setupRolesAndPrivileges() {
    List privileges = setupData.getPrivileges();
    for (Privilege privilege : privileges) {
        setupService.setupPrivilege(privilege);
    }

    List roles = setupData.getRoles();
    for (Role role : roles) {
        setupService.setupRole(role);
    }
}

Et voici nosSetupService:

public void setupPrivilege(Privilege privilege) {
    if (privilegeRepository.findByName(privilege.getName()) == null) {
        privilegeRepository.save(privilege);
    }
}

public void setupRole(Role role) {
    if (roleRepository.findByName(role.getName()) == null) {
        Set privileges = role.getPrivileges();
        Set persistedPrivileges = new HashSet();
        for (Privilege privilege : privileges) {
            persistedPrivileges.add(privilegeRepository.findByName(privilege.getName()));
        }
        role.setPrivileges(persistedPrivileges);
        roleRepository.save(role); }
}

Notez que, après avoir chargé les rôles et les privilèges dans la mémoire de travail, nous chargeons leurs relations une par une.

4.2. Configurer les utilisateurs initiaux

Ensuite, chargeons lesusers en mémoire et conservons-les:

public List getUsers() {
    List allRoles = getRoles();
    List users = csvDataLoader.loadObjectList(User.class, SetupData.USERS_FILE);
    List usersRoles = csvDataLoader.
      loadManyToManyRelationship(SetupData.USERS_ROLES_FILE);

    for (String[] userRole : usersRoles) {
        User user = findByUserByUsername(users, userRole[0]);
        Set roles = user.getRoles();
        if (roles == null) {
            roles = new HashSet();
        }
        roles.add(findRoleByName(allRoles, userRole[1]));
        user.setRoles(roles);
    }
    return users;
}

private User findByUserByUsername(List users, String username) {
    return users.stream().
      filter(item -> item.getUsername().equals(username)).findFirst().get();
}

Ensuite, concentrons-nous sur la persistance des utilisateurs:

private void setupUsers() {
    List users = setupData.getUsers();
    for (User user : users) {
        setupService.setupUser(user);
    }
}

Et voici nosSetupService:

@Transactional
public void setupUser(User user) {
    try {
        setupUserInternal(user);
    } catch (Exception e) {
        logger.error("Error occurred while saving user " + user.toString(), e);
    }
}

private void setupUserInternal(User user) {
    if (userRepository.findByUsername(user.getUsername()) == null) {
        user.setPassword(passwordEncoder.encode(user.getPassword()));
        user.setPreference(createSimplePreference(user));
        Set roles = user.getRoles();
        Set persistedRoles = new HashSet();
        for (Role role : roles) {
            persistedRoles.add(roleRepository.findByName(role.getName()));
        }
        user.setRoles(persistedRoles);
        userRepository.save(user);
    }
}

Et voici la méthodecreateSimplePreference():

private Preference createSimplePreference(User user) {
    Preference pref = new Preference();
    pref.setId(user.getId());
    pref.setTimezone(TimeZone.getDefault().getID());
    pref.setEmail(user.getUsername() + "@test.com");
    return preferenceRepository.save(pref);
}

Notez comment, avant de sauvegarder un utilisateur, nous créons une simple entitéPreference pour lui et la persévérons en premier.

5. Test du chargeur de données CSV

Ensuite, effectuons un test unitaire simple sur nosCsvDataLoader:

Nous allons tester la liste de chargement des utilisateurs, des rôles et des privilèges:

@Test
public void whenLoadingUsersFromCsvFile_thenLoaded() {
    List users = csvDataLoader.
      loadObjectList(User.class, CsvDataLoader.USERS_FILE);
    assertFalse(users.isEmpty());
}

@Test
public void whenLoadingRolesFromCsvFile_thenLoaded() {
    List roles = csvDataLoader.
      loadObjectList(Role.class, CsvDataLoader.ROLES_FILE);
    assertFalse(roles.isEmpty());
}

@Test
public void whenLoadingPrivilegesFromCsvFile_thenLoaded() {
    List privileges = csvDataLoader.
      loadObjectList(Privilege.class, CsvDataLoader.PRIVILEGES_FILE);
    assertFalse(privileges.isEmpty());
}

Ensuite, testons le chargement de certaines relations plusieurs à plusieurs via le chargeur de données:

@Test
public void whenLoadingUsersRolesRelationFromCsvFile_thenLoaded() {
    List usersRoles = csvDataLoader.
      loadManyToManyRelationship(CsvDataLoader.USERS_ROLES_FILE);
    assertFalse(usersRoles.isEmpty());
}

@Test
public void whenLoadingRolesPrivilegesRelationFromCsvFile_thenLoaded() {
    List rolesPrivileges = csvDataLoader.
      loadManyToManyRelationship(CsvDataLoader.ROLES_PRIVILEGES_FILE);
    assertFalse(rolesPrivileges.isEmpty());
}

6. Données de configuration du test

Enfin, effectuons un simple test unitaire sur notre beanSetupData:

@Test
public void whenGettingUsersFromCsvFile_thenCorrect() {
    List users = setupData.getUsers();

    assertFalse(users.isEmpty());
    for (User user : users) {
        assertFalse(user.getRoles().isEmpty());
    }
}

@Test
public void whenGettingRolesFromCsvFile_thenCorrect() {
    List roles = setupData.getRoles();

    assertFalse(roles.isEmpty());
    for (Role role : roles) {
        assertFalse(role.getPrivileges().isEmpty());
    }
}

@Test
public void whenGettingPrivilegesFromCsvFile_thenCorrect() {
    List privileges = setupData.getPrivileges();
    assertFalse(privileges.isEmpty());
}

7. Conclusion

Dans cet article rapide, nous avons exploré une méthode de configuration alternative pour les données initiales qui doivent généralement être chargées dans un système au démarrage. Ceci est bien sûr juste une simple preuve de concept et une bonne base sur laquelle s'appuyer -not a production ready solution.

Nous allons également utiliser cette solution dans l'application Web Reddit suivie parthis ongoing case study.