Перейти к основному содержимому

Экстернализация данных настройки через CSV в приложении Spring

· 6 мин. чтения

1. Обзор

В этой статье мы будем экстернализовать данные настройки приложения с помощью CSV-файлов , а не запрограммировать их жестко.

Этот процесс настройки в основном связан с настройкой новых данных в новой системе.

2. Библиотека CSV

Начнем с знакомства с простой библиотекой для работы с CSV — расширением Jackson CSV :

<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-csv</artifactId>
<version>2.5.3</version>
</dependency>

Конечно, в экосистеме Java существует множество доступных библиотек для работы с CSV.

Причина, по которой мы здесь используем Джексона, заключается в следующем: вполне вероятно, что Джексон уже используется в приложении, и обработка, необходимая для чтения данных, довольно проста.

3. Данные установки

В разных проектах необходимо настроить разные данные.

В этом уроке мы собираемся настроить данные пользователя — по сути , подготовить систему с несколькими пользователями по умолчанию .

Вот простой CSV-файл, содержащий пользователей:

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

Обратите внимание, что первая строка файла является строкой заголовка — в ней перечислены имена полей в каждой строке данных.

3. Загрузчик данных CSV

Начнем с создания простого загрузчика данных для чтения данных из CSV-файлов в рабочую память .

3.1. Загрузить список объектов

Мы реализуем функцию loadObjectList() для загрузки полностью параметризованного списка определенного объекта из файла:

public <T> List<T> loadObjectList(Class<T> type, String fileName) {
try {
CsvSchema bootstrapSchema = CsvSchema.emptySchema().withHeader();
CsvMapper mapper = new CsvMapper();
File file = new ClassPathResource(fileName).getFile();
MappingIterator<T> 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();
}
}

Заметки:

  • Мы создали CSVSchema на основе первой строки заголовка.
  • Реализация достаточно универсальна, чтобы обрабатывать объекты любого типа.
  • Если произойдет какая-либо ошибка, будет возвращен пустой список.

3.2. Обработка отношения «многие ко многим»

Вложенные объекты плохо поддерживаются в Jackson CSV — нам нужно будет использовать непрямой способ загрузки отношений «многие ко многим».

Мы будем представлять их аналогично простым таблицам соединения, поэтому, естественно, мы будем загружать с диска в виде списка массивов:

public List<String[]> 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<String[]> 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();
}
}

Вот как одно из этих отношений — Роли <-> Привилегии — представлено в простом CSV-файле:

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

Обратите внимание, как мы игнорируем заголовок в этой реализации, так как нам на самом деле не нужна эта информация.

4. Данные настройки

Теперь мы будем использовать простой компонент установки , чтобы выполнить всю работу по настройке привилегий, ролей и пользователей из CSV-файлов:

@Component
public class Setup {
...

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

...
}

4.1. Настройка ролей и привилегий

Во-первых, давайте загрузим роли и привилегии с диска в рабочую память, а затем сохраним их как часть процесса установки:

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

public List<Role> getRoles() {
List<Privilege> allPrivileges = getPrivileges();
List<Role> roles = csvDataLoader.loadObjectList(Role.class, ROLES_FILE);
List<String[]> rolesPrivileges = csvDataLoader.
loadManyToManyRelationship(SetupData.ROLES_PRIVILEGES_FILE);

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

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

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

Затем мы выполним работу с сохранением здесь:

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

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

А вот и наш SetupService :

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<Privilege> privileges = role.getPrivileges();
Set<Privilege> persistedPrivileges = new HashSet<Privilege>();
for (Privilege privilege : privileges) {
persistedPrivileges.add(privilegeRepository.findByName(privilege.getName()));
}
role.setPrivileges(persistedPrivileges);
roleRepository.save(role); }
}

Обратите внимание, как после того, как мы загрузили в рабочую память и Роли, и Привилегии, мы загружаем их отношения одну за другой.

4.2. Настройка начальных пользователей

Далее — давайте загрузим пользователей в память и сохраним их:

public List<User> getUsers() {
List<Role> allRoles = getRoles();
List<User> users = csvDataLoader.loadObjectList(User.class, SetupData.USERS_FILE);
List<String[]> usersRoles = csvDataLoader.
loadManyToManyRelationship(SetupData.USERS_ROLES_FILE);

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

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

Далее, давайте сосредоточимся на сохранении пользователей:

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

А вот и наш SetupService :

@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<Role> roles = user.getRoles();
Set<Role> persistedRoles = new HashSet<Role>();
for (Role role : roles) {
persistedRoles.add(roleRepository.findByName(role.getName()));
}
user.setRoles(persistedRoles);
userRepository.save(user);
}
}

А вот и метод createSimplePreference() :

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);
}

Обратите внимание, как перед сохранением пользователя мы создаем для него простую сущность Preference и сначала сохраняем ее.

5. Протестируйте загрузчик данных CSV

Далее давайте выполним простой модульный тест на нашем CsvDataLoader :

Мы протестируем загрузку списка пользователей, ролей и привилегий:

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

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

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

Затем давайте проверим загрузку некоторых отношений «многие ко многим» через загрузчик данных:

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

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

6. Данные настройки теста

Наконец, давайте выполним простой модульный тест для нашего bean -компонента SetupData :

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

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

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

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

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

7. Заключение

В этой быстрой статье мы рассмотрели альтернативный метод настройки исходных данных, которые обычно необходимо загружать в систему при запуске. Это, конечно, простое доказательство концепции и хорошая база для дальнейшего развития, а не готовое решение .

Мы также собираемся использовать это решение в веб-приложении Reddit, отслеживаемом в этом продолжающемся тематическом исследовании .