Externalizar dados de configuração via CSV em um aplicativo Spring
1. Visão geral
Neste artigo, vamosexternalize the setup data of an application using CSV files, em vez de codificá-lo.
Esse processo de configuração se preocupa principalmente com a configuração de novos dados em um sistema novo.
2. Uma biblioteca CSV
Vamos começar apresentando uma biblioteca simples para trabalhar com CSV - oJackson CSV extension:
com.fasterxml.jackson.dataformat
jackson-dataformat-csv
2.5.3
É claro que existem várias bibliotecas disponíveis para trabalhar com CSVs no ecossistema Java.
O motivo pelo qual estamos indo com Jackson aqui é que - é provável que Jackson já esteja em uso no aplicativo e o processamento de que precisamos para ler os dados é bastante simples.
3. Os dados de configuração
Projetos diferentes precisarão configurar dados diferentes.
Neste tutorial, vamos configurar os dados do usuário - basicamentepreparing the system with a few default users.
Este é o arquivo CSV simples contendo os usuários:
id,username,password,accessToken
1,john,123,token
2,tom,456,test
Observe como a primeira linha do arquivo éthe header row - listando os nomes dos campos em cada linha de dados.
3. CSV Data Loader
Vamos começar criando um carregador de dados simples pararead up data from the CSV files into working memory.
3.1. Carregar uma lista de objetos
Implementaremos a funcionalidadeloadObjectList() para carregar uma lista totalmente parametrizada deObject específicos do arquivo:
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();
}
}
Notas:
-
Criamos oCSVSchema com base na primeira linha do “cabeçalho”.
-
A implementação é genérica o suficiente para manipular qualquer tipo de objeto.
-
Se ocorrer algum erro, uma lista vazia será retornada.
3.2. Relacionamento Tratar muitos para muitos
Objetos aninhados não são bem suportados no Jackson CSV - precisaremos usar uma maneira indireta para carregar relacionamentos muitos para muitos.
Representaremos essessimilar to simple Join Tables - então, naturalmente, carregaremos do disco como uma lista de matrizes:
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();
}
}
Veja como uma dessas relações -Roles <→ Privileges - é representada em um arquivo CSV simples:
role,privilege
ROLE_ADMIN,ADMIN_READ_PRIVILEGE
ROLE_ADMIN,ADMIN_WRITE_PRIVILEGE
ROLE_SUPER_USER,POST_UNLIMITED_PRIVILEGE
ROLE_USER,POST_LIMITED_PRIVILEGE
Observe como estamos ignorando o cabeçalho nesta implementação, já que não precisamos dessa informação.
4. Dados de configuração
Agora, usaremos um beanSetup simples para fazer todo o trabalho de configuração de privilégios, funções e usuários de arquivos CSV:
@Component
public class Setup {
...
@PostConstruct
private void setupData() {
setupRolesAndPrivileges();
setupUsers();
}
...
}
4.1. Configurar funções e privilégios
Primeiro, vamos carregarroles and privileges do disco para a memória de trabalho e, em seguida, mantê-los como parte do processo de configuração:
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();
}
Então, faremos o trabalho persistente aqui:
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);
}
}
E aqui está nossoSetupService:
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); }
}
Observe como, depois que carregamos Funções e Privilégios na memória de trabalho, carregamos seus relacionamentos um por um.
4.2. Configurar usuários iniciais
A seguir - vamos carregarusers na memória e mantê-los:
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();
}
A seguir, vamos nos concentrar em manter os usuários:
private void setupUsers() {
List users = setupData.getUsers();
for (User user : users) {
setupService.setupUser(user);
}
}
E aqui está nossoSetupService:
@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);
}
}
E aqui está o métodocreateSimplePreference():
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);
}
Observe como, antes de salvar um usuário, criamos uma entidadePreference simples para ele e persistimos nela primeiro.
5. Testar carregador de dados CSV
A seguir, vamos realizar um teste de unidade simples em nossoCsvDataLoader:
Testaremos a lista de carregamento de usuários, funções e privilégios:
@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());
}
A seguir, vamos testar o carregamento de alguns relacionamentos muitos para muitos por meio do carregador de dados:
@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. Dados de configuração de teste
Finalmente, vamos realizar um teste de unidade simples em nosso 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. Conclusão
Neste rápido artigo, exploramos um método de configuração alternativo para os dados iniciais que geralmente precisam ser carregados em um sistema na inicialização. Esta é, obviamente, apenas uma Prova de Conceito simples e uma boa base para desenvolver -not a production ready solution.
Também vamos usar essa solução no aplicativo da web Reddit monitorado porthis ongoing case study.