Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
Из этой статьи вы узнаете о некоторых не очень известных, но полезных библиотеках Java. Это вторая статья из серии «Полезное и неизвестное». Предыдущая описывала несколько привлекательных, но малоизвестных возможностей Java. Подробнее об этом можно прочитать здесь.
Сегодня мы сосредоточимся на библиотеках Java. Обычно мы используем в своих проектах несколько внешних библиотек — даже если не включаем их напрямую. Например, Spring Boot поставляется с определенным набором зависимостей, включенным стартерами. Если мы включаем, например, spring-boot-starter-test
, то одновременно мы включаем такие библиотеки, как mockito
, junit-jupiter
или hamcrest
. Конечно, это известные библиотеки для сообщества.
На самом деле существует множество различных Java-библиотек. Обычно мне не нужно использовать многие из них (или даже они мне не нужны) при работе с такими фреймворками, как Spring Boot или Quarkus.
Однако есть несколько очень интересных библиотек, которые могут пригодиться везде. Возможно, вы не слышали ни об одной из них. Я собираюсь представить 5 моих любимых «полезных и неизвестных» Java-библиотек. Давай начнем!
Исходный код
Если вы хотите попробовать сделать это самостоятельно, вы всегда можете посмотреть на мой исходный код. Для этого вам нужно клонировать мой репозиторий на GitHub.
Вы также можете найти пример. Затем вам нужно просто следовать моим инструкциям.
Instancio
На первый огонь отправится Instancio. Как вы генерируете тестовые данные в ваших модульных тестах? Instancio поможет нам в этом. Ее цель - сократить время и количество строк кода, затрачиваемых на ручную настройку данных в модульных тестах. Она создает объекты и заполняет их случайными данными, делая наши тесты более динамичными. С помощью Instancio мы можем генерировать случайные данные, но в то же время мы можем установить пользовательские данные в определенном поле.
Прежде чем начать работу с Instancio, давайте обсудим нашу модель данных. Вот первый класс — Person
:
public class Person {
private Long id;
private String name;
private int age;
private Gender gender;
private Address address;
// getters and setters ...
}
Наш класс содержит три простых поля ( id
, name
, age
), одно перечисление Gender
и экземпляр класса Address
. Пол — это просто перечисление, содержащее значения MALE
и FEMALE
. Вот реализация класса Address
:
public class Address {
private String country;
private String city;
private String street;
private int houseNumber;
private int flatNumber;
// getters and setters ...
}
Теперь давайте создадим тест для проверки успешного добавления и получения объектов из хранилища сервисом Person
. Мы хотим генерировать случайные данные для всех полей, кроме поля id
, которое задается сервисом. Вот наш тест:
@Test
void addAndGet() {
Person person = Instancio.of(Person.class)
.ignore(Select.field(Person::getId))
.create();
person = personService.addPerson(person);
Assertions.assertNotNull(person.getId());
person = personService.findById(person.getId());
Assertions.assertNotNull(person);
Assertions.assertNotNull(person.getAddress());
}
Значения, сгенерированные для моего тестового запуска, видны ниже. Как видите, поле id
равно null
. Другие поля содержат случайные значения, сгенерированные в соответствии с типом поля ( String
или int
).
Person(id=null, name=ATDLCA, age=2619, gender=MALE,
address=Address(country=FWOFRNT, city=AIRICCHGGG, street=ZZCIJDZ, houseNumber=5530, flatNumber=1671))
Давайте посмотрим, как мы можем сгенерировать несколько объектов с помощью Instancio. Предположив, для нашего теста что нам нужно 5 объектов в списке, мы можем сделать это следующим образом. Мы также установим постоянное значение для полей city
внутри объекта Address
. Затем мы хотим протестировать метод для поиска объектов по названию города.
@Test
void addListAndGet() {
final int numberOfObjects = 5;
final String city = "Warsaw";
List<Person> persons = Instancio.ofList(Person.class)
.size(numberOfObjects)
.set(Select.field(Address::getCity), city)
.create();
personService.addPersons(persons);
persons = personService.findByCity(city);
Assertions.assertEquals(numberOfObjects, persons.size());
}
Давайте рассмотрим последний пример. Как и раньше, мы генерируем список объектов — на этот раз 100. Мы можем легко задать дополнительные критерии для генерируемых значений. Например, я хочу задать значение для поля age
между 18 и 65 годами.
@Test
void addGeneratorAndGet() {
List<Person> persons = Instancio.ofList(Person.class)
.size(100)
.ignore(Select.field(Person::getId))
.generate(Select.field(Person::getAge),
gen -> gen.ints().range(18, 65))
.create();
personService.addPersons(persons);
persons = personService.findAllGreaterThanAge(40);
Assertions.assertTrue(persons.size() > 0);
}
Это лишь небольшой набор настроек, которые Instancio предлагает для генерации тестовых данных. Более подробно о других возможностях вы можете прочитать в ее документации.
Datafaker
Следующая библиотека, которую мы сегодня рассмотрим, это Datafaker. Назначение этой библиотеки очень похоже на предыдущую. Нам нужно сгенерировать случайные данные. Однако на этот раз нам нужны данные, похожие на реальные данные. С моей точки зрения, это полезно для демонстрационных презентаций или запускаемых где-либо примеров.
Datafaker создает фиктивные данные для ваших программ JVM за считанные минуты, используя наш широкий спектр из более чем 100 поставщиков данных. Это может быть очень полезно при генерации тестовых данных для заполнения базы данных, генерации данных для стресс-теста или анонимизации данных из производственных сервисов. Давайте включим его в наши зависимости.
<dependency>
<groupId>net.datafaker</groupId>
<artifactId>datafaker</artifactId>
<version>1.7.0</version>
</dependency>
Мы немного расширим наш пример модели. Вот новое определение класса. Класс Contact содержит два поля email
и phoneNumber
. Мы проверим оба этих поля с помощью модуля jakarta.validation
.
public class Contact {
@Email
private String email;
@Pattern(regexp="\\d{2}-\\d{3}-\\d{2}-\\d{2}")
private String phoneNumber;
// getters and setters ...
}
Вот новая версия нашего класса Person
, которая содержит экземпляр объекта Contant
:
public class Person {
private Long id;
private String name;
private int age;
private Gender gender;
private Address address;
@Valid
private Contact contact;
// getters and setters ...
}
Теперь давайте сгенерируем фиктивные данные для объекта Person
. Мы можем создать локализованные данные, просто установив объект Locale в конструкторе Faker (1). Для меня это Польша.
Существует множество провайдеров для стандартных значений. Для настройки email
нужно использовать провайдера Internet
(2). Существуют специальные провайдеры для генерации телефонных номеров (3), адресов (4) и имен людей (5). Полный список доступных провайдеров можно посмотреть здесь. После создания тестовых данных мы можем запустить тест, добавляющий новый проверенный объект Person
на стороне сервера.
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class PersonsControllerTests {
@Autowired
private TestRestTemplate restTemplate;
@Test
void add() {
Faker faker = new Faker(Locale.of("pl")); // (1)
Contact contact = new Contact();
contact.setEmail(faker.internet().emailAddress()); // (2)
contact.setPhoneNumber(faker.phoneNumber().cellPhone()); // (3)
Address address = new Address();
address.setCity(faker.address().city()); // (4)
address.setCountry(faker.address().country());
address.setStreet(faker.address().streetName());
int number = Integer
.parseInt(faker.address().streetAddressNumber());
address.setHouseNumber(number);
number = Integer.parseInt(faker.address().buildingNumber());
address.setFlatNumber(number);
Person person = new Person();
person.setName(faker.name().fullName()); // (5)
person.setContact(contact);
person.setAddress(address);
person.setGender(Gender.valueOf(
faker.gender().binaryTypes().toUpperCase()));
person.setAge(faker.number().numberBetween(18, 65));
person = restTemplate
.postForObject("/persons", person, Person.class);
Assertions.assertNotNull(person);
Assertions.assertNotNull(person.getId());
}
}
Вот данные, сгенерированные во время моего тестирования. Думаю, вы можете найти здесь одно несоответствие (поле country
)
Person(id=null, name=Stefania Borkowski, age=51, gender=FEMALE, address=Address(country=Ekwador, city=Sępopol, street=al. Chudzik, houseNumber=882, flatNumber=318), contact=Contact{email='gilbert.augustyniak@gmail.com', phoneNumber='69-733-43-77'})
Иногда требуется сгенерировать более предсказуемый случайный результат. В конструкторе Faker можно указать начальное значение. При предоставлении начального значения создание объектов Faker всегда будет происходить предсказуемым образом, что может быть удобно для многократной генерации результатов. Вот новая версия объявления моего объекта Faker
:
Faker faker = new Faker(Locale.of("pl"), new Random(0));
JPA Streamer
Наша следующая библиотека связана с запросами JPA. Если вам нравится использовать потоки Java и вы создаете приложения, взаимодействующие с базами данных через JPA или Hibernate, библиотека JPA Streamer может быть интересным выбором. Это библиотека для выражения запросов JPA/Hibernate/Spring с использованием стандартных потоков Java. JPA Streamer мгновенно предоставляет разработчикам Java типо-безопасные, выразительные и интуитивно понятные средства получения данных в приложениях баз данных. Более того, вы можете легко интегрировать ее с Spring Boot и Quarkus. Прежде всего, давайте включим JPA Streamer в наши зависимости:
<dependency>
<groupId>com.speedment.jpastreamer</groupId>
<artifactId>jpastreamer-core</artifactId>
<version>1.1.2</version>
</dependency>
Если вы хотите интегрировать ее со Spring Boot, вам нужно добавить одну дополнительную зависимость:
<dependency>
<groupId>com.speedment.jpastreamer.integration.spring</groupId>
<artifactId>spring-boot-jpastreamer-autoconfigure</artifactId>
<version>1.1.2</version>
</dependency>
Чтобы протестировать JPA Streamer, нам нужно создать пример модели сущностей.
@Entity
public class Employee {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String name;
private String position;
private int salary;
@ManyToOne(fetch = FetchType.LAZY)
private Department department;
@ManyToOne(fetch = FetchType.LAZY)
private Organization organization;
// getters and setters ...
}
Есть также две другие сущности: Organization
и Department
. Вот их определения:
@Entity
public class Department {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String name;
@OneToMany(mappedBy = "department")
private Set<Employee> employees;
@ManyToOne(fetch = FetchType.LAZY)
private Organization organization;
// getters and setters ...
}
@Entity
public class Organization {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String name;
@OneToMany(mappedBy = "organization")
private Set<Department> departments;
@OneToMany(mappedBy = "organization")
private Set<Employee> employees;
// getters and setters ...
}
Теперь мы можем подготовить несколько запросов с использованием шаблона потоков Java. В следующем фрагменте кода мы ищем объект по идентификатору, а затем соединяем два отношения. По умолчанию это LEFT JOIN, но мы можем настроить его при вызове метода joining()
. В следующем фрагменте кода мы соединяем Department
и Organization
, которые находятся в отношении @ManyToOne
с сущностью Employee. Затем мы фильтруем результат, преобразуем объект в DTO и выбираем первый результат.
@GetMapping("/{id}")
public EmployeeWithDetailsDTO findById(@PathVariable("id") Integer id) {
return streamer.stream(of(Employee.class)
.joining(Employee$.department)
.joining(Employee$.organization))
.filter(Employee$.id.equal(id))
.map(EmployeeWithDetailsDTO::new)
.findFirst()
.orElseThrow();
}
Конечно, мы можем вызывать множество других потоковых методов Java. В следующем фрагменте кода мы подсчитываем количество сотрудников, приписанных к определенному отделу.
@GetMapping("/{id}/count-employees")
public long getNumberOfEmployees(@PathVariable("id") Integer id) {
return streamer.stream(Department.class)
.filter(Department$.id.equal(id))
.map(Department::getEmployees)
.mapToLong(Set::size)
.sum();
}
Если вы ищете подробное объяснение и дополнительные примеры с JPA Streamer, вы можете прочитать мою статью, посвященную этой теме.
Blaze Persistence
Blaze Persistence — еще одна библиотека из области JPA и Hibernate. Она позволяет писать сложные запросы с помощью последовательного API конструктора с богатым API Criteria для JPA провайдеров. Но это еще не все. Вы также можете использовать модуль Entity-View, предназначенный для маппинга в DTO. Конечно, вы можете легко интегрироваться со Spring Boot или Quarkus. Если вы хотите использовать все модули Blaze Persistence в своем приложении, стоит добавить секцию dependencyManagement
в Maven pom.xml
:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.blazebit</groupId>
<artifactId>blaze-persistence-bom</artifactId>
<version>1.6.8</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
Лично я использую Blaze Persistence для маппинга DTO. Благодаря интеграции со Spring Boot мы можем заменить Spring Data Projections на Blaze Persistence Entity-Views. Это будет особенно полезно для более сложных карт, поскольку Blaze Persistence предлагает больше возможностей и лучшую производительность для этого. Подробное сравнение вы можете найти в следующей статье. Если мы хотим интегрировать Entity-Views Blaze Persistence со Spring Data, мы должны добавить следующие зависимости:
<dependency>
<groupId>com.blazebit</groupId>
<artifactId>blaze-persistence-integration-spring-data-2.7</artifactId>
</dependency>
<dependency>
<groupId>com.blazebit</groupId>
<artifactId>blaze-persistence-integration-hibernate-5.6</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.blazebit</groupId>
<artifactId>blaze-persistence-entity-view-processor</artifactId>
</dependency>
Затем необходимо создать интерфейс с геттерами для маппированых полей. Он должен быть снабжен аннотацией @EntityView
, которая относится к целевому классу сущностей. В следующем примере мы сопоставляем два поля сущности firstName
и lastName
отдельные поля внутри объекта PersonDTO
. Чтобы маппить первичный ключ объекта, необходимо использовать аннотацию @IdMapping
.
@EntityView(Person.class)
public interface PersonView {
@IdMapping
Integer getId();
void setId(Integer id);
@Mapping("CONCAT(firstName,' ',lastName)")
String getName();
void setName(String name);
}
Мы все еще можем воспользоваться преимуществами шаблона репозитория Spring Data. Интерфейс нашего репозитория должен расширять интерфейс EntityViewRepository
.
@Transactional(readOnly = true)
public interface PersonViewRepository
extends EntityViewRepository<PersonView, Integer> {
PersonView findByAgeGreaterThan(int age);
}
Нам также нужно предоставить некоторую дополнительную конфигурацию и включить Blaze Persistence в основном классе или классе конфигурации:
@SpringBootApplication
@EnableBlazeRepositories
@EnableEntityViews
public class PersonApplication {
public static void main(String[] args) {
SpringApplication.run(PersonApplication.class, args);
}
}
Hoverfly
Наконец, последняя из Java-библиотек в моем списке — Hoverfly. Точнее, мы будем использовать задокументированную здесь Java-версию библиотеки Hoverfly. Это легковесный инструмент виртуализации сервисов, позволяющий создавать заглушки или имитировать HTTP(S) сервисы. Hoverfly Java — это привязка к нативному языку, которая дает вам выразительный API для управления Hoverfly в Java. Она предоставляет вам класс Hoverfly, который абстрагируется от бинарных и API вызовов, DSL для создания симуляций и интеграцию JUnit для использования в модульных тестах.
Хорошо, есть и другие похожие библиотеки… но мне почему-то очень нравится Hoverfly.
Это простая, легковесная библиотека, которая может выполнять тесты в различных режимах, таких как симуляция, слежка, захват или сравнение. Вы можете использовать Java DSL для создания сопоставлений запросов и ответов. Давайте включим последнюю версию Hoverfly в зависимости Maven:
<dependency>
<groupId>io.specto</groupId>
<artifactId>hoverfly-java-junit5</artifactId>
<version>0.14.3</version>
</dependency>
Предположим, у нас есть следующий метод в нашем Spring @RestController
. Прежде чем вернуть себе ответ на ping, он вызывает другую службу по адресу http://callme-service:8080/callme/ping
.
@GetMapping("/ping")
public String ping() {
String response = restTemplate
.getForObject("http://callme-service:8080/callme/ping",
String.class);
LOGGER.info("Calling: response={}", response);
return "I'm caller-service " + version + ". Calling... " + response;
}
Теперь мы создадим тест для нашего контроллера. Чтобы использовать Hoverfly для перехвата исходящего трафика, зарегистрируем HoverflyExtension (1). Затем мы можем использовать объект Hoverfly для создания запроса и имитации HTTP-ответа (2). Имитируемое тело ответа I'm callme-service v1
.
@SpringBootTest(properties = {"VERSION = v2"},
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ExtendWith(HoverflyExtension.class) // (1)
public class CallerCallmeTest {
@Autowired
TestRestTemplate restTemplate;
@Test
void callmeIntegration(Hoverfly hoverfly) {
hoverfly.simulate(
dsl(service("http://callme-service:8080")
.get("/callme/ping")
.willReturn(success().body("I'm callme-service v1.")))
); // (2)
String response = restTemplate
.getForObject("/caller/ping", String.class);
assertEquals("I'm caller-service v2. Calling... I'm callme-service v1.", response);
}
}
Мы можем легко настроить поведение Hoverfly с помощью аннотации @HoverflyConfig
. По умолчанию Hoverfly работает в режиме прокси. Если мы хотим, чтобы он работал как веб-сервер, нам нужно установить для свойства webserver
значение true
(1). После этого он будет прослушивать запросы на localhost
и порту, указанном свойством proxyPort
. На следующем шаге мы также включим Spring Cloud @LoadBalancedClient
для настройки статического списка целевых URL-адресов вместо динамического обнаружения (2). Наконец, мы можем создать тест Hoverfly. На этот раз мы перехватываем трафик с веб-сервера, прослушивающего localhost:8080
(3).
@SpringBootTest(webEnvironment =
SpringBootTest.WebEnvironment.RANDOM_PORT)
@HoverflyCore(config =
@HoverflyConfig(logLevel = LogLevel.DEBUG,
webServer = true,
proxyPort = 8080)) // (1)
@ExtendWith(HoverflyExtension.class)
@LoadBalancerClient(name = "account-service",
configuration = AccountServiceConf.class) // (2)
public class GatewayTests {
@Autowired
TestRestTemplate restTemplate;
@Test
public void findAccounts(Hoverfly hoverfly) {
hoverfly.simulate(dsl(
service("http://localhost:8080")
.andDelay(200, TimeUnit.MILLISECONDS).forAll()
.get(any())
.willReturn(success("[{\"id\":\"1\",\"number\":\"1234567890\",\"balance\":5000}]", "application/json")))); // (3)
ResponseEntity<String> response = restTemplate
.getForEntity("/account/1", String.class);
Assertions.assertEquals(200, response.getStatusCodeValue());
Assertions.assertNotNull(response.getBody());
}
}
Вот конфигурация клиента балансировщика нагрузки, созданная только для целей тестирования.
class AccountServiceInstanceListSuppler implements
ServiceInstanceListSupplier {
private final String serviceId;
AccountServiceInstanceListSuppler(String serviceId) {
this.serviceId = serviceId;
}
@Override
public String getServiceId() {
return serviceId;
}
@Override
public Flux<List<ServiceInstance>> get() {
return Flux.just(Arrays
.asList(new DefaultServiceInstance(serviceId + "1",
serviceId,
"localhost", 8080, false)));
}
}
Заключительные мысли
Как вы, наверное, догадались, я использовал все эти Java-библиотеки в приложениях Spring Boot. Хотя Spring Boot поставляется с определенным набором внешних библиотек, иногда нам могут понадобиться некоторые дополнения. Представленные мной Java-библиотеки обычно создаются для решения одной конкретной задачи, такой как, например, генерация тестовых данных. С моей точки зрения, это совершенно нормально. Надеюсь, что вы найдете хотя бы одну позицию из моего списка полезной в своих проектах.