Вышел Spring Boot 3.3.0. В обновлении представлены следующие нововведения:
🚀 Поддержка CDS для ускорения времени запуска.
🔍 Улучшение Observability.
🔐 Новые возможности Spring Security.
🧵 Поддержка виртуальных потоков для вебсокетов.
⚡ Обновление зависимостей и многое другое.
#Spring #SpringBoot
🔗 https://spring.io/blog/2024/05/23/spring-boot-3-3-0-available-now
Подписывайтесь:
😌 @spring_aio
🚀 Поддержка CDS для ускорения времени запуска.
🔍 Улучшение Observability.
🔐 Новые возможности Spring Security.
🧵 Поддержка виртуальных потоков для вебсокетов.
⚡ Обновление зависимостей и многое другое.
#Spring #SpringBoot
🔗 https://spring.io/blog/2024/05/23/spring-boot-3-3-0-available-now
Подписывайтесь:
Please open Telegram to view this post
VIEW IN TELEGRAM
Иногда полезно освежить в памяти базовые принципы фреймворка. В Spring Framework центральное место занимает Bean. В своем видео Джош Лонг рассказывает об истории Bean и его жизненном цикле.
#SpringTips #SpringBoot
📱 https://www.youtube.com/watch?v=Z5hxolai4Tk
Подписывайтесь:
😌 @spring_aio
#SpringTips #SpringBoot
Подписывайтесь:
Please open Telegram to view this post
VIEW IN TELEGRAM
Spring Стартер Недели
Как сделать CRUD за 5 минут? Создаем модель данных, репозиторий, контроллер с 5-8 методами и, возможно, несколько dto. Если печатать со скоростью 600 символов в минуту, то можно успеть 🙈
Или берем Spring Data Rest, и получаем круды для своих репозиториев из коробки. И не только круды. Можно выставить наружу любой метод доступа к данным, прикрутить пагинацию и сортировку.
Однако, быстрый старт в начале может обернуться болью в процессе кастомизации или добавлении сложной (и не очень) логики. Тем не менее, выглядит как вполне себе хорошее решение для небольших REST сервисов, почти что Low Code 🙃.
Больше информации о данном стартере можно получить из доклада Рустама Курамшина на JPoint 2023.
📱 https://www.youtube.com/watch?v=roaGUHaWPxw
🔗 https://spring.io/projects/spring-data-rest
#SpringStarter #SpringBoot
Подписывайтесь:
😌 @spring_aio
Как сделать CRUD за 5 минут? Создаем модель данных, репозиторий, контроллер с 5-8 методами и, возможно, несколько dto. Если печатать со скоростью 600 символов в минуту, то можно успеть 🙈
Или берем Spring Data Rest, и получаем круды для своих репозиториев из коробки. И не только круды. Можно выставить наружу любой метод доступа к данным, прикрутить пагинацию и сортировку.
Однако, быстрый старт в начале может обернуться болью в процессе кастомизации или добавлении сложной (и не очень) логики. Тем не менее, выглядит как вполне себе хорошее решение для небольших REST сервисов, почти что Low Code 🙃.
Больше информации о данном стартере можно получить из доклада Рустама Курамшина на JPoint 2023.
🔗 https://spring.io/projects/spring-data-rest
#SpringStarter #SpringBoot
Подписывайтесь:
Please open Telegram to view this post
VIEW IN TELEGRAM
🕐 Тонкое управление Scheduled задачами в Spring
Какими способами можно настроить время и условие запуска Scheduled задач в Spring?
Первый способ - через
Но в таком варианте, мы управляем только условием запуска, но не можем настроить переодичность.
Другой вариант, это объявление cron expression в пропертях:
Но как тогда в таком случае отключить джобу? В качестве cron expression можно использовать дефис(-), что означает, что джобу запускать не нужно вовсе. И тогда мы обходимся без явного перечисления профилей в
Такой подход особо полезен при использовании spring-cloud-config, тогда нет необходимости делать передеплой приложения, чтобы выключить, или донастроить джобу. Нужно только не забыть повесить
#SpringBoot #SpringTips
Какими способами можно настроить время и условие запуска Scheduled задач в Spring?
Первый способ - через
@Profile
над сервисом, в котором объявлена задача, чтобы указать профили, в которых она должна (или не должна) запускаться.
java
@Profile("test,!prod")
class Teapot {
@Scheduled(cron = "0 15 10 15 * ?")
void makeTea() {
log.info("I'am a teapot.");
}
}
Но в таком варианте, мы управляем только условием запуска, но не можем настроить переодичность.
Другой вариант, это объявление cron expression в пропертях:
#application-test.properties
teapot.make_tea.cron=0 15 10 15 * ?
@Scheduled(cron = "${teapot.make_tea.cron}")
void makeTea() {
log.info("I'am a teapot.");
}
Но как тогда в таком случае отключить джобу? В качестве cron expression можно использовать дефис(-), что означает, что джобу запускать не нужно вовсе. И тогда мы обходимся без явного перечисления профилей в
@Profile
.
#application-prod.properties
teapot.make_tea.cron=-
Такой подход особо полезен при использовании spring-cloud-config, тогда нет необходимости делать передеплой приложения, чтобы выключить, или донастроить джобу. Нужно только не забыть повесить
@RefreshScope
на класс.#SpringBoot #SpringTips
🎉 Встречайте нашу дебютную статью на Habr!
👩💻 Блеск и нищета нового Scrolling API в Spring Data
В статье мы разобрали новое Scrolling API, которое предназначено для эффективной пагинации. В частности, Scrolling API приносит KeySet пагинацию в Spring Data, которая в теории может быть сильно быстрее стандратной и всем известной offset пагинации.
Однако, в процессе исследования выяснилось, что с PostgreSQL новое API работает не совсем так, как ожидается. А ведь это одна из самых популярных СУБД в мире!
Заинтриговали? Переходите по ссылке, оставляйте комментарии, и, конечно, ждем ваших лайков)
🔗 https://habr.com/ru/companies/spring_aio/articles/819193/
#SpringBoot #SpringData
Подписывайтесь:
😌 @spring_aio
В статье мы разобрали новое Scrolling API, которое предназначено для эффективной пагинации. В частности, Scrolling API приносит KeySet пагинацию в Spring Data, которая в теории может быть сильно быстрее стандратной и всем известной offset пагинации.
Однако, в процессе исследования выяснилось, что с PostgreSQL новое API работает не совсем так, как ожидается. А ведь это одна из самых популярных СУБД в мире!
Заинтриговали? Переходите по ссылке, оставляйте комментарии, и, конечно, ждем ваших лайков)
🔗 https://habr.com/ru/companies/spring_aio/articles/819193/
#SpringBoot #SpringData
Подписывайтесь:
Please open Telegram to view this post
VIEW IN TELEGRAM
🌟 Статическая и динамическая конфигурация в Spring Boot: что выбрать?
В мире облачных сервисов правильная настройка конфигурации играет ключевую роль. В этой статье рассматриваются два подхода: статическая и динамическая конфигурация.
🔧 Статическая конфигурация удобна предсказуемостью и простотой развертывания. Пример на Spring Boot:
🔄 Динамическая конфигурация обеспечивает гибкость и адаптивность. Например, для изменения конфигураций во время выполнения приложения можно использовать Spring Cloud:
Spring Cloud предоставляет возможность создания config сервера, который может подгружать новые конфигурации из Git. Ознакомьтесь с полной статьей для более детального погружения и примеров кода!
📚 Читать далее
#SpringBoot #SpringCloud
В мире облачных сервисов правильная настройка конфигурации играет ключевую роль. В этой статье рассматриваются два подхода: статическая и динамическая конфигурация.
🔧 Статическая конфигурация удобна предсказуемостью и простотой развертывания. Пример на Spring Boot:
@Value("${myapp.staticValue}")
private String staticValue;
public void printStaticValue() {
System.out.println(staticValue);
}
🔄 Динамическая конфигурация обеспечивает гибкость и адаптивность. Например, для изменения конфигураций во время выполнения приложения можно использовать Spring Cloud:
@RefreshScope
@RestController
public class MessageRestController {
@Value("${message:Hello default}")
private String message;
@GetMapping("/message")
public String getMessage() {
return this.message;
}
}
Spring Cloud предоставляет возможность создания config сервера, который может подгружать новые конфигурации из Git. Ознакомьтесь с полной статьей для более детального погружения и примеров кода!
📚 Читать далее
#SpringBoot #SpringCloud
На первый взгляд, выбор правильного Java-рантайма для вашего проекта на Spring Boot может показаться тривиальным. В конце концов, все популярные рантаймы основываются на коде OpenJDK и предлагают одинаковые программные интерфейсы.
Но не все рантаймы реализованы одинаково. В этой статье мы обсудим различные показатели, которые могут повлиять на ваше решение выбрать определенный дистрибутив Java для Spring Boot приложения.
🔗 https://habr.com/ru/companies/spring_aio/articles/819899/
#JVM #Java #SpringBoot
Please open Telegram to view this post
VIEW IN TELEGRAM
🐢 Неожиданное падение производительности при переходе на Spring Boot 3.3.0 👩💻
Hibernate подложил небольшую свинью, конвертируя JPQL в SQL. Внезапно, запрос поиска записей по списку id с передачей пустого списка деградирует в table full scan.
Однако, Hibernate пофиксил баг в течении 10 часов после поста в запрещенную в РФ соцсеть. Осталось дождаться новой версии Spring Boot, которая включит себе этот фикс.
🔗Подробнее читайте на Хабре
#SpringBoot #Hibernate #BreakingNews
Hibernate подложил небольшую свинью, конвертируя JPQL в SQL. Внезапно, запрос поиска записей по списку id с передачей пустого списка деградирует в table full scan.
Однако, Hibernate пофиксил баг в течении 10 часов после поста в запрещенную в РФ соцсеть. Осталось дождаться новой версии Spring Boot, которая включит себе этот фикс.
🔗Подробнее читайте на Хабре
#SpringBoot #Hibernate #BreakingNews
Please open Telegram to view this post
VIEW IN TELEGRAM
Одной из главных причин популярности Spring Boot является его способность автоматически конфигурировать (auto-configuration) множество компонентов, существенно упрощая жизнь разработчикам. Однако иногда возникает необходимость слегка подправить настройки этих компонентов без отказа от всех преимуществ автоматической конфигурации.
Предположим, вам нужно кастомизировать настройки кэша в вашем приложении. Вместо того чтобы полностью переопределять CacheManager, вы можете использовать Customizer интерфейс, чтобы внести необходимые изменения:
@Bean
CacheManagerCustomizer<ConcurrentMapCacheManager> cacheManagerCustomizer() {
return cacheManager -> cacheManager.setAllowNullValues(false);
}
Аналогичным образом можно настроить и любые другие компоненты:
//Кастомизация свойств Hibernate
@Bean
HibernatePropertiesCustomizer hibernatePropertiesCustomizer() {
return properties -> properties.put("hibernate.integrator_provider",
(IntegratorProvider) () -> List.of(new BeanValidationIntegrator()));
}
//Кастомизация Jackson2ObjectMapperBuilderCustomizer
@Bean
Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer() {
return builder -> {
builder.serializationInclusion(JsonInclude.Include.NON_EMPTY);
builder.featuresToEnable(
SerializationFeature.WRITE_ENUMS_USING_TO_STRING,
DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY);
};
}
Но бывает и такое, что подходящего Customizer интерфейса просто нет. В таком случае, можно использовать BeanPostProcessor для кастомизации уже инициализированных бинов:
//Кастомизация springLiquibase бина
@Bean
BeanPostProcessor liquibaseBeanPostProcessor() {
return new BeanPostProcessor() {
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof SpringLiquibase springLiquibase) {
springLiquibase.setContexts("my-context");
}
return bean;
}
};
}
#SpringBoot #SpringTips
Please open Telegram to view this post
VIEW IN TELEGRAM
Более миллиона раз разработчики обращались к вопросу на StackOverflow о том, как получить значение из
application.properties
. Возможно, вы удивитесь @Value
– это не единственный способ. И далеко не всегда самый удобный.Способ №1: @Value
Аннотация
@Value
используется для инъекции отдельных значений из файла конфигурации. Например:
@Value("${property.name}")
private String propertyName;
Пожалуй, самый простой, но не самый поддерживаемый вариант, из-за отсутствия группировки и большого количества дублирующего кода, когда значений, с которыми надо работать, становится довольного много.
Способ №2: @ConfigurationProperties
Аннотация
@ConfigurationProperties
используется для группировки связанных свойств в объект класса. Свойства могут настраиваться через .properties
и .yaml
файлы, переменные окружения и т.д.Типичное использование
@ConfigurationProperties
может выглядеть следующим образом:
@ConfigurationProperties(prefix = "app")
@Component
@Getter
@Setter
public class ApplicationProperties {
private String name;
}
@SpringBootTest(properties = "app.name=my-app")
class ApplicationPropertiesTest {
@Autowired
private ApplicationProperties applicationProperties;
@Test
void appName() {
var appName = applicationProperties.getName();
assertEquals("my-app", appName);
}
}
Да, нужно будет создать отдельный класс, зато преимуществ у этого подхода относительно
@Value
довольно много. Перечислим некоторые из них:– Группировка по префиксу
– Возможность иерархического представления свойств
– Это бин и этим всё сказано :)
–
@ConfigurationPropertiesBinding
Отдельно отметим
@ConfigurationPropertiesBinding
. Допустим, нам нужно добавить новое свойство version
, которое будет представлять класс Version
для обработки семантического версионирования (major.minor.patch).
@ConfigurationProperties(prefix = "app")
@Component
@Getter
@Setter
public class ApplicationProperties {
private String name;
private Version version;
}
Для преобразования строкового значения из конфигурации в объект класса
Version
нам потребуется конвертор, который как раз будет отмечен аннотацией @ConfigurationPropertiesBinding
:
@ConfigurationPropertiesBinding
@Component
class SemVerPropertyConverter implements Converter<String, Version> {
@Override
public Version convert(String source) {
return StringUtils.hasLength(source) ? Version.parse(source) : null;
}
}
Пример использования:
@SpringBootTest(properties = {
"app.name=my-app",
"app.version=1.1.0"
})
class ApplicationPropertiesTest {
@Autowired
private ApplicationProperties applicationProperties;
@Test
void appVersion() {
Version version = applicationProperties.getVersion();
assertEquals(1, version.getMajorVersion());
assertEquals(1, version.getMinorVersion());
assertEquals(0, version.getPatchVersion());
}
}
Подробнее про
@ConfigurationProperties
и @ConfigurationPropertiesBinding
вы можете прочитать в документации.Способ №3: Environment
А если вы хотите обойтись без специальных аннотаций, можно использовать
Environment
:
@Autowired
private Environment env;
public void someMethod() {
String property = env.getProperty("property.name");
}
#SpringBoot #SpringTips
Please open Telegram to view this post
VIEW IN TELEGRAM
Валидация данных – ключевой аспект любого приложения. В Spring она часто используется в параметрах методов
@RestController
, например:
@RestController
@RequestMapping("/api/products")
public class ProductController {
@GetMapping("/search")
public ResponseEntity<List<Product>> searchProducts(@RequestParam @NotNull @Size(min = 3, max = 50) String name,
@RequestParam @NotNull @Min(0) @Max(10000) Double price) {
// Логика поиска продуктов
List<Product> products = productService.search(name, price);
return new ResponseEntity<>(products, HttpStatus.OK);
}
}
Валидация в сервисном слое
Валидация помогает гарантировать, что данные, поступающие в приложение, соответствуют требованиям. Для валидации в сервисном слое нужно добавить аннотацию
@Validated
над сервисом:
@Service
@Validated
public class EmailService {
public void send(@Email String email,
@Length(max = 10) String subject,
@NotBlank String body) {
// бизнес-логика
}
}
Теперь метод send вызовет ошибку, если данные не проходят валидацию:
emailService.send(
"i am not email",
"I am too loooooooooong",
""
);
jakarta.validation.ConstraintViolationException: send.body: must not be blank, send.email: must be a well-formed email address, send.subject: length must be between 0 and 10
...
Валидация DTO в методах сервиса
Чтобы не дублировать поля в разных методах, разработчики часто используют DTO. Аннотации валидации применимы и здесь:
public record EmailRequest(
@Email String email,
@Length(max = 10) String subject,
@NotBlank String body
) {
}
Но в этом случае помимо
@Validated
над классом, нужно также не забыть добавить @Valid
перед типом параметра в методе:
@Service
@Validated
public class EmailService {
public void sendBatch(
@Valid List<EmailRequest> requests
) {
// do work
}
}
Следующий код вызовет ошибку:
emailService.sendBatch(
List.of(
new EmailRequest("not email", "test", "Hello"),
new EmailRequest("alex@spring.aio", "I am too loooooooooong", "")
)
);
jakarta.validation.ConstraintViolationException: sendBatch.requests[1].subject: length must be between 0 and 10, sendBatch.requests[1].body: must not be blank, sendBatch.requests[0].email: must be a well-formed email address
Валидация элементов коллекций
Кстати, точно также можно валидировать и элементы коллекций, а также ключи и значения в Map:
@Service
@Validated
public class EmailService {
public void search(
Map<@NotBlank String, @NotBlank String> searchParams
) {
// do work
}
}
Следующий код вызовет ошибку:
searchService.search(Map.of("", ""));
jakarta.validation.ConstraintViolationException: search.searchParams<K>[].<map key>: must not be blank, search.searchParams[].<map value>: must not be blank
#SpringBoot #SpringTips #Validation
Please open Telegram to view this post
VIEW IN TELEGRAM
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
При разработке на 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
По умолчанию 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
Валидация входных данных — важная часть любой системы, обеспечивающая целостность данных и предотвращение ошибок. Иногда стандартные аннотации 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
Часто в файле
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
@ManyToMany
Использовать
CascadeType.ALL
для @ManyToMany
не рекомендуется, так как это может привести к непредсказуемым результатам во время удаления JPA сущностей. Вместо этого следует использовать CascadeType.DETACH
, CascadeType.MERGE
, CascadeType.PERSIST
и CascadeType.REFRESH
. Подробнее об этом рассказано в отдельном видео!
Please open Telegram to view this post
VIEW IN TELEGRAM
YouTube
Никогда не используй CascadeType.ALL вместе с @ManyToMany | Amplicode
#Amplicode #Spring #SpringBoot #SpringData #JPA #Hibernate #IntelliJ #Java #Kotlin
Использовать CascadeType.ALL для @ManyToMany не рекомендуется, так как это может привести к непредсказуемым результатам во время удаления JPA сущностей. Вместо этого следует…
Использовать CascadeType.ALL для @ManyToMany не рекомендуется, так как это может привести к непредсказуемым результатам во время удаления JPA сущностей. Вместо этого следует…