Spring АйО
8.42K subscribers
301 photos
209 videos
400 links
Русскоязычное сообщество Spring-разработчиков.

Habr: bit.ly/433IK46
YouTube: bit.ly/4h3Ci0x
VK: bit.ly/4hF0OG8
Rutube: bit.ly/4b4UeX6
Яндекс Музыка: bit.ly/3EIizWy

Канал для общения: @spring_aio_chat
Download Telegram
👨‍💻 Spring Tips: Кастомные реализации репозиториев

Spring Data упрощает создание запросов, но стандартные методы не всегда подходят под конкретные задачи. В таких случаях мы можем создать собственные реализации методов репозиториев. Например, если мы хотим, чтобы запрос формировался динамически на основе фильтра и возвращал DTO с меньшим количеством полей, нам понадобится создать fragment interface и кастомный метод с собственной реализацией.


//Кастомный интерфейс
public interface CustomizedUserRepository {
List<UserDto> findAllUsers(UserFilter filter);
}

//Реализация нашего интерфейса, которая будет использоваться Spring'ом
public class CustomizedUserRepositoryImpl implements CustomizedUserRepository {
private final EntityManager em;

public CustomizedUserRepositoryImpl(JpaContext jpaContext) {
em = jpaContext.getEntityManagerByManagedType(User.class);
}

//Реализация нашего метода
@Override
public List<UserDto> findAllUsers(UserFilter filter) {
var cb = em.getCriteriaBuilder();
var query = cb.createQuery(UserDto.class);

var root = query.from(User.class);
var emailPath = root.<String>get(User_.EMAIL);
var usernamePath = root.<String>get(User_.USERNAME);
query.multiselect(root.get(User_.ID), emailPath, usernamePath);

var predicates = new ArrayList<Predicate>();

var email = filter.email();
if (StringUtils.hasLength(email)) {
predicates.add(cb.like(cb.lower(emailPath), "%" + email.toLowerCase() + "%"));
}

var username = filter.username();
if (StringUtils.hasLength(username)) {
predicates.add(cb.like(cb.lower(usernamePath), "%" + username.toLowerCase() + "%"));
}

query.where(predicates.toArray(new Predicate[]{}));
return em.createQuery(query).getResultList();
}
}

//Использование кастомного интерфейса
interface UserRepository extends CrudRepository<User, Long>, CustomizedUserRepository {}


Теперь при инжекции UserRepository, кастомный метод будет доступен для вызова:


List<UserDto> users = userRepository.findAllUsers(new UserFilter("Maksim", null));


Важные замечания

1. CustomizedUserRepositoryImpl — полноценный Spring-бин, поддерживающий инжекцию других бинов и специфическую функциональность (AOT, Lifecycle Callbacks и т.д.).

2. Инжекция UserRepository может привести к циклической зависимости. Чтобы избежать этого, его можно получить через ApplicationContext.getBean().

3. Spring пытается автоматически обнаружить пользовательские fragment интерфейсы, если классы следуют соглашению об именовании с постфиксом Impl. Модифицировать значение по умолчанию можно через атрибут аннотации @EnableJpaRepositories:


@EnableJpaRepositories(repositoryImplementationPostfix = "MyPostfix")


4. Репозитории могут включать несколько пользовательских реализаций, которые имеют более высокий приоритет, чем базовая реализация, что позволяет переопределять базовые методы. Например, создадим кастомный интерфейс и переопределим метод save:


public interface CustomizedSave<T> {
<S extends T> S save(S entity);
}

class CustomizedSaveImpl<T> implements CustomizedSave<T> {
@Override
public <S extends T> S save(S entity) {
// наша кастомная реализация
}
}


Теперь, если мы объявим следующий репозиторий:


interface UserRepository extends CrudRepository<User, Long>, CustomizedSave<User> {}


то при вызове метода userRepository.save(user) будет использован метод из нашей реализации CustomizedSaveImpl.

5. В примерах выше мы рассматривали Spring Data JPA, но эта концепция поддерживается для всех модулей Spring Data (MongoDB, Redis, JDBC и т.д.).

Подробнее про реализацию кастомных репозиториев читайте в документации.

#SpringBoot #SpringTips #CustomRepository
Please open Telegram to view this post
VIEW IN TELEGRAM
👍24🔥8👌3
Spring Tips: Переопределение и дополнение свойств для Spring Boot тестов

При разработке на Spring Boot часто возникает необходимость переопределять конфигурационные свойства для тестов. Представим, что у нас есть application.properties в директории src/main/resources со следующими свойствами:


spring.application.name=properties
spring.jpa.open-in-view=false


Мы хотим изменить spring.application.name и добавить новое свойство server.port в тестах. Интуитивно разработчики создают application.properties в src/test/resources, ожидая, что он дополнит основной файл. Однако это не так (подробное объяснение можно найти здесь). Рассмотрим несколько способов переопределения свойств в тестах.

1. @ActiveProfiles

Создайте файл application-test.properties в src/test/resources и активируйте его в тестах.


@SpringBootTest
@ActiveProfiles("test")
class ActiveProfileTest {

@Autowired
private Environment env;

@Test
void test() {
Assertions.assertEquals("new-name", env.getProperty("spring.application.name"));
Assertions.assertEquals(false, env.getProperty("spring.jpa.open-in-view", Boolean.class));
Assertions.assertEquals(8099, env.getProperty("server.port", Integer.class));
}
}


2. Директория config

Spring загружает конфигурационные файлы в определенном порядке. Файлы в src/test/resources/config имеют приоритет и переопределяют свойства из src/main/resources. Более подробно ознакомиться с тем, по какому принципу Spring загружает свойства можно в документации.

3. @DynamicPropertySource

Используйте динамическую подмену свойств, если значения известны только во время выполнения.


@SpringBootTest
class DynamicPropertySourceTest {

@Autowired
private Environment env;

@DynamicPropertySource
static void properties(DynamicPropertyRegistry registry) {
registry.add("spring.application.name", () -> "new-name");
registry.add("server.port", () -> 8099);
}

@Test
void test() {
Assertions.assertEquals("new-name", env.getProperty("spring.application.name"));
Assertions.assertEquals(false, env.getProperty("spring.jpa.open-in-view", Boolean.class));
Assertions.assertEquals(8099, env.getProperty("server.port", Integer.class));
}
}


4. @TestPropertySource с параметром locations

Укажите путь к *.properties файлу с нужными значениями.


@SpringBootTest
@TestPropertySource(locations = "classpath:test.properties")
class PropertiesSourceLocationTest {

@Autowired
private Environment env;

@Test
void test() {
Assertions.assertEquals("new-name", env.getProperty("spring.application.name"));
Assertions.assertEquals(false, env.getProperty("spring.jpa.open-in-view", Boolean.class));
Assertions.assertEquals(8099, env.getProperty("server.port", Integer.class));
}
}


Продолжение в комментариях

#SpringBoot #SpringTips
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥19👍94
🦥 Spring Tips: Аннотация @Lazy

По умолчанию Spring инициализирует все sigleton-бины во время запуска приложения. Аннотация @Lazy позволяет изменить это поведение, инициализируя бины только по мере необходимости. Используя аннотацию @Lazy можно существенно снизить потребление памяти и уменьшить время запуска приложения, отметив бины, которые используются в приложении не так часто и занимают немало памяти.

Способы применения

Чтобы сделать бин лениво инициализируемым, нужно отметить его аннотацией @Lazy в месте его объявления:


@Lazy
@Component
public class MyComponent {
//...
}

@Configuration
@Lazy
public class LazyConfig {
// Все объявленные в этом конфиге бины будут ленивыми
}

@Configuration
public class AppConfig {
//Этот конкретный бин будет ленивым
@Bean
@Lazy
public MyBean myBean() {
return new MyBean();
}
}


А также в месте его инжекции:


@Service
public class MyService {
private final MyBean myBean;
private final MyComponent myComponent;

public MyService(@Lazy MyBean myBean, @Lazy MyComponent myComponent) {
this.myBean = myBean;
this.myComponent = myComponent;
}
}


Если не воспользоваться аннотацией @Lazy в месте объявления бина или в месте его инжекции, то он НЕ БУДЕТ ленивым.

Ленивая инициализация контекста по умолчанию

Чтобы сделать инициализацию ленивой для всего контекста, можно добавить следущее свойство в конфигурационный файл:


spring.main.lazy-initialization=true


Однако стоит быть осторожным с этим подходом. Ошибки инициализации могут возникнуть не во время запуска приложения, а позже – в рантайме, что усложнит их обнаружение и исправление (что в целом справедливо и при ленивой инициализации лишь некоторых бинов). Но если вы всё таки решите использовать глобальную ленивую инициализацию, можно исключить определённые бины, инициализируя их жадно (во время запуска приложения) с помощью @Lazy(false):


@Service
@Lazy(false)
public class MyService {
// код
}


А много ли бинов лениво инициализируется на вашем проекте? Может быть вообще все? Поделитесь своим опытом в комментариях, будет интересно почитать!

#SpringBoot #SpringTips
Please open Telegram to view this post
VIEW IN TELEGRAM
👍316🔥6
🛡 Кастомные валидаторы в Spring Boot: Как сделать валидацию под себя

Валидация входных данных — важная часть любой системы, обеспечивающая целостность данных и предотвращение ошибок. Иногда стандартные аннотации Jakarta Bean Validation (например, @NotNull, @Size, @Pattern) не покрывают все наши потребности, и нам приходится писать свои собственные валидаторы. В этом посте мы разберем два подхода к созданию кастомных валидаторов в Spring Boot, а также узнаем, как настроить отображение ошибки валидации.

1. Кастомные валидации с мета-аннотациями

Если стандартных аннотаций не хватает, можно создать свою собственную аннотацию для валидации. Это удобно, если вам нужно сочетать несколько стандартных аннотаций в одной или переиспользовать уже имеющиеся аннотации. Например, давайте создадим аннотацию @CardNumber, которая проверяет, соответствует ли строка формату номера кредитной карты.


@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Pattern(regexp = "([0-9]{4}-){3}[0-9]{4}$", message = "Invalid credit card number")
@Constraint(validatedBy = {})
public @interface CardNumber {
String message() default "Invalid credit card number";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}


В этом примере аннотация @CardNumber использует @Pattern для проверки формата номера кредитной карты. Такой подход упрощает код, потому что вся логика валидации сконцентрирована в одной аннотации.


@PostMapping("/card")
void checkCardNumber(@RequestParam @Valid @CardNumber String cardNumber) {
}



@Test
public void validCardNumber() throws Exception {
mockMvc.perform(post("/card")
.param("cardNumber", "1111-1111-1111-1111"))
.andExpect(status().isOk())
.andDo(print());
}

@Test
public void invalidCardNumber() throws Exception {
mockMvc.perform(post("/card")
.param("cardNumber", "1111-1111-1111"))
.andExpect(status().is(400))
.andDo(print());
}


2. Реализация собственных валидаторов

Когда стандартные аннотации не подходят, можно создать собственные валидаторы. Для этого нужно реализовать интерфейс ConstraintValidator.

Предположим, нам нужно валидировать пароли с определенными требованиями. Для этого создадим аннотацию @ValidPassword и валидатор PasswordValidator.


@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = PasswordValidator.class)
public @interface ValidPassword {
String message() default "Invalid password";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
int minLength() default 8;
String specialChars() default "!@#$%^&*()";
}

public class PasswordValidator implements ConstraintValidator<ValidPassword, String> {
private int minLength;
private String specialChars;

@Override
public void initialize(ValidPassword constraintAnnotation) {
this.minLength = constraintAnnotation.minLength();
this.specialChars = constraintAnnotation.specialChars();
}

@Override
public boolean isValid(String password, ConstraintValidatorContext context) {
if (password == null) {
return false;
}

boolean hasUpperCase = !password.equals(password.toLowerCase());
boolean hasLowerCase = !password.equals(password.toUpperCase());
boolean hasDigit = password.chars().anyMatch(Character::isDigit);
boolean hasSpecialChar = password.chars().anyMatch(ch -> specialChars.indexOf(ch) >= 0);
boolean isLongEnough = password.length() >= minLength;

return hasUpperCase && hasLowerCase && hasDigit && hasSpecialChar && isLongEnough;
}
}


В данном примере аннотация @ValidPassword проверяет, что пароль содержит и верхний, и нижний регистр, цифры, специальные символы и имеет достаточную длину. Логика валидации инкапсулирована в PasswordValidator.

Продолжение в комментариях 👇

#SpringBoot #SpringTips
Please open Telegram to view this post
VIEW IN TELEGRAM
👍317🔥4
Spring Tips: Переопределение свойств через переменные окружения

Часто в файле application.properties (или application.yml) объявляются свойства, значения которых содержат переменные вида ${ENV_PROPERTY_KEY:defaultValue}:


spring.datasource.url=jdbc:postgresql://${POSTGRES_HOST:localhost}/${POSTGRES_DB_NAME:local_dev_db}
spring.datasource.username=${POSTGRES_USERNAME:root}
spring.datasource.password=${POSTGRES_PASSWORD:root}
spring.datasource.driver-class-name=org.postgresql.Driver


Такой подход позволяет переопределять свойства при запуске приложения в разных средах (local, test, prod и т.д.).

Например, если мы решили запустить наше приложение через Docker Compose, предварительно собрав его в Docker образ, то передача значений для объявленных нами переменных окружения будет выглядеть следующим образом:


spring_test_app:
image: spring_test_app:latest
build:
context: .
dockerfile: docker/Dockerfile
args:
DOCKER_BUILDKIT: 1
restart: "no"

# задаём конкретные значения тем самым переменным окружения
environment:
POSTGRES_HOST: postgres:5432
POSTGRES_DB_NAME: test_stand_db
POSTGRES_USERNAME: admin
POSTGRES_PASSWORD: admin
ports:
- "8080:8080"


В этом примере переменные из application.properties переопределяются через переменные окружения при запуске Docker контейнера.

Переопределение свойств напрямую

Но на самом деле, можно обойтись без дополнительных переменных в application.properties, переопределив Spring-свойства напрямую.

Теперь application.properties выглядит следующим образом:


spring.datasource.url=jdbc:postgresql://localhost:5432/local_dev_db
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=org.postgresql.Driver


А код сервиса нашего приложения в Docker Compose следующим образом:


spring_test_app:
image: spring_test_app:latest
build:
context: .
dockerfile: docker/Dockerfile
args:
DOCKER_BUILDKIT: 1
restart: "no"
environment:
SPRING_DATASOURCE_URL: jdbc:postgresql://postgres/test_stand_db
SPRING_DATASOURCE_USERNAME: admin
SPRING_DATASOURCE_PASSWORD: admin
ports:
- "8080:8080"


Для spring.datasource.url, spring.datasource.username и spring.datasource.password будут использованы те значения, которые мы указали в Docker Compose файле.

За счёт чего это становится возможным?

Spring Boot использует определенный порядок загрузки свойств приложения, чтобы обеспечить разумное переопределение значений. Переменные окружения загружаются позже и перезаписывают свойства, заданные в application.properties. Полный порядок загрузки можно найти в документации.

Relaxed Binding в Spring Boot

Кстати, обратите внимание на стиль написания названий свойств. Spring Boot использует концепцию Relaxed Binding, которая позволяет указать название свойства без полного совпадения. Например:


@ConfigurationProperties(prefix = "my.main-project.person")
class MyPersonProperties {
var firstName: String? = null
}


Для этого класса можно использовать следующие стили именования свойств:

- my.main-project.person.first-name — Kebab стиль для .properties/.yaml файлов
- my.main-project.person.firstName — CamelCase стиль
- my.main-project.person.first_name — Underscore стиль
- MY_MAINPROJECT_PERSON_FIRSTNAME — Uppercase + Underscore стиль для переменных окружения. При использовании Uppercase + Underscore стиля следует учитывать, что многие операционные системы ограничивают имена переменных окружения. Например, в Linux переменные могут содержать только буквы (a-z, A-Z), цифры (0-9) и символ подчеркивания (_). Подробнее читайте в документации.

В приведённом выше примере с Docker Compose файлом мы как раз воспользовались Uppercase + Underscore стилем, чтобы указать значения для нужных нам свойств. Именно такой вариант именования переменных окружения является негласным для Linux.

Ставь 🔥 если знал про это и 🤔 если слышишь впервые)

#SpringBoot #SpringTips
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥37🤔24👍121
⭐️ CascadeType.ALL и @ManyToMany

Использовать CascadeType.ALL для @ManyToMany не рекомендуется, так как это может привести к непредсказуемым результатам во время удаления JPA сущностей. Вместо этого следует использовать CascadeType.DETACH, CascadeType.MERGE, CascadeType.PERSIST и CascadeType.REFRESH.

Подробнее об этом рассказано в отдельном видео!

😉 СМОТРЕТЬ НА YOUTUBE
😄 СМОТРЕТЬ В VK ВИДЕО
🥰 СМОТРЕТЬ НА RUTUBE
Please open Telegram to view this post
VIEW IN TELEGRAM
👍23🔥74