Для тех, кто был слишком занят на неделе или просто пропустил некоторые посты, публикуем дайджест!
Spring Boot Tips: Service Layer Validation – разобрались с тем, как валидировать данные на уровне сервисов с помощью аннотаций Valid, Validated и т.д.
Liquibase + Spring Boot: настройка и написание миграций баз данных – узнали, как можно автоматизировать процесс написания скриптом миграций для Liquibase.
Как избежать утечек соединений в Spring Boot приложении? – рассмотрели интересный кейс использования аннотации Transactional, который позволил решить проболему с утечками соединений.
Spring Boot Tips: Кастомные реализации репозиториев – выяснили, как можно "подложить" Spring'у свои реализации репозиториев.
Переписывая историю: от инструментов версионирования БД к практике – погрузились в сравнение Liquibase и Flyway вместе с Александром Шустановым и транскриптом его доклада на Joker 2023.
Первый обзор на GigaIDE - Российская IntelliJ IDEA от СБЕРа – посмотрели в действии, что же из себя представляет GigaIDE в сравнении с IntelliJ IDEA CE и Ultimate.
Новые возможности конструкторов в Java (часть 1) – узнали, что за собой скрывает JEP №447 и какие возможности он привнесёт в нашу любимую Java.
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥15👍7❤3
При разработке на 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👍9❤4
В условиях растущих требований к производительности современных приложений, кеширование становится одним из ключевых инструментов для их удовлетворения.
В новом переводе от команды Spring АйО вы узнаете про 7 основных техник оптимизации кеширования в Spring Boot, которые могут помочь значительно улучшить производительность. От выбора идеальных кандидатов для кеширования до реализации асинхронного кеша и мониторинга метрик кеша.
📚 Подробнее читайте на Хабре
Please open Telegram to view this post
VIEW IN TELEGRAM
👍20🔥10❤4
Java 22 принесла множество новых фич, но наиболее обсуждаемой стала String Templates. Хотя эта preview-фича не попадет в будущий релиз Java 23, огорчаться не стоит. В Java 22 достаточно других интересных обновлений. Помните их?
Если забыли, самое время освежить память: https://habr.com/ru/articles/801467/.
Ну и, конечно же, выбрать любимую фичу
Поделитесь в комментариях, какая фича Java 22 вам нравится больше всего?
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥9❤3👍3
Полгода назад команда Gradle представила новый продукт — Declarative Gradle. Команда Spring АйО, со своей стороны, провела исследование этого продукта и готова поделиться результатами.
TL;DR: технология всё ещё активно развивается, но пока что не поддерживается привычными инструментами, такими как IntelliJ IDEA. Несмотря на это, нам удалось запустить Spring Boot приложение!
📚 Подробнее читайте на Хабре
Please open Telegram to view this post
VIEW IN TELEGRAM
👍12🤔8❤3😁3🔥2
Не так давно мы рассказывали про гибкие конструкторы в Java. В 23 версии Java появится возможность инициализировать поля в том же классе перед явным вызовом конструктора (JEP-482). А пока эта фича не реализована, мы вынуждены использовать вспомогательные методы, если нам необходимо проиницизировать поля.
Рассмотрим класс, который принимает аргумент типа
Certificate
и должен преобразовать его в массив байтов для конструктора суперкласса.Java ≤ 22:
public class Sub extends Super {
private static byte[] prepareByteArray(Certificate certificate) {
var publicKey = certificate.getPublicKey();
if (publicKey == null) throw new IllegalArgumentException(..);
return switch (publicKey) {
case RSAKey rsaKey -> ...
case DSAPublicKey dsaKey -> ...
default -> ...
};
}
public Sub(Certificate certificate) {
super(prepareByteArray(certificate));
}
}
В коде выше метод
prepareByteArray
выполняет необходимую подготовку аргументов. Затем этот метод вызывается внутри конструктора Sub
как часть вызова super
.Java ≥ 23:
Можно будет сделать тоже самое, но без дополнительных “приседаний” в виде вынесения логики которую мы хотим выполнить в конструкторе в отдельный метод:
public class Sub extends Super {
public Sub(Certificate certificate) {
var publicKey = certificate.getPublicKey();
if (publicKey == null) throw new IllegalArgumentException(..);
byte[] certBytes = switch (publicKey) {
case RSAKey rsaKey -> ...
case DSAPublicKey dsaKey -> ...
default -> ...
};
super(certBytes);
}
}
Также иногда возникает необходимость передать одно и то же значение в несколько аргументов конструктора суперкласса. Ранее это можно было сделать только с использованием вспомогательного конструктора.
Рассмотрим пример, связанный с системой регистрации студентов в университете. У нас есть базовый класс
Person
, который представляет человека, и класс Student
, который наследует Person
. Каждый студент имеет идентификатор, который также используется в качестве номера студенческого билета и учетной записи в университетеJava ≤ 22:
class Person {
private String id;
private String accountId;
public Person(String id, String accountId) {
this.id = id;
this.accountId = accountId;
}
}
class Student extends Person {
private Student(String id) {
super(id, id); // Передача одного и того же значения дважды
}
public Student(String firstName, String lastName) {
this(generateStudentId(firstName, lastName));
}
private static String generateStudentId(String firstName, String lastName) {
// Генерация уникального идентификатора студента
return firstName + ", " + lastName + "-" + System.currentTimeMillis();
}
}
В этом примере вспомогательный конструктор
Student(String id)
используется для передачи одного и того же id
в два аргумента конструктора суперкласса Person
.Java ≥ 23:
class Person {
private String id;
private String accountId;
public Person(String id, String accountId) {
this.id = id;
this.accountId = accountId;
}
}
class Student extends Person {
public Student(String firstName, String lastName) {
String id = generateStudentId(firstName, lastName);
super(id, id); // Передача одного и того же значения дважды
}
private static String generateStudentId(String firstName, String lastName) {
// Генерация уникального идентификатора студента
return firstName + ", " + lastName + "-" + System.currentTimeMillis();
}
}
Ставьте 🔥 если понравился пост и вы были бы рады видеть больше разборов JEP
#Java #JEP
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥39👍3❤2
This media is not supported in your browser
VIEW IN TELEGRAM
This media is not supported in your browser
VIEW IN TELEGRAM
This media is not supported in your browser
VIEW IN TELEGRAM
This media is not supported in your browser
VIEW IN TELEGRAM
This media is not supported in your browser
VIEW IN TELEGRAM
This media is not supported in your browser
VIEW IN TELEGRAM
😁37🔥9👍8
Для тех, кто был слишком занят на неделе или просто пропустил некоторые посты, публикуем дайджест!
Переопределение и дополнение свойств для Spring Boot тестов – собрали в одном месте 6 различных вариантов переопределения свойств
7 основных способов оптимизировать кеширование в Spring Boot – узнали, что, где и как кэшировать, а что лучше не стоит
Самая любимая фича в Java 22 – освежили в памяти все фичи, которые вошли в 22-й релиз Java
Declarative Gradle: рывок или прорыв? – выяснили, что из себя представляет новый проект от Gradle, и попробовали его в действии
Новые возможности конструкторов в Java (часть 2) – продолжили изучать нововведения, связанные с конструкторами объектов
Please open Telegram to view this post
VIEW IN TELEGRAM
❤11👍9🔥7
По умолчанию 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
👍31❤6🔥6
Судя по реакциям на предыдущих постах про JEP, они вам нравятся, так что продолжаем разбирать свежые JEP'ы.
Представим себе небольшой веб-фреймворк, который обрабатывает HTTP-запросы. Фреймворк создает контекст для каждого запроса и передает его через методы:
@Override
public void handle(Request request, Response response) {
var userInfo = readUserInfo();
}
private UserInfo readUserInfo() {
return (UserInfo) framework.readKey("userInfo", context);
}
Ранее для передачи контекста использовались переменные типа
ThreadLocal
:
private final static ThreadLocal<FrameworkContext> CONTEXT = new ThreadLocal<>();
void serve(Request request, Response response) {
var context = createContext(request);
CONTEXT.set(context);
Application.handle(request, response);
}
public PersistedObject readKey(String key) {
var context = CONTEXT.get();
var db = getDBConnection(context);
db.readKey(key);
}
Scoped Values API впервые был представлен в JDK 20 и прошел несколько итераций и улучшений в последующих версиях JDK (JEP 429, JEP 446, JEP 464, JEP 481). Scoped Values представляют собой новый механизм для передачи неизменяемых данных между методами в одном потоке и между дочерними потоками. Этот механизм проще в использовании по сравнению с
ThreadLocal
и обладает меньшими затратами по времени и памяти.У подхода, использующего
ThreadLocal
, есть несколько недостатков:1. Неограниченная изменяемость — переменные могут изменяться в любое время любым кодом в потоке
2. Неограниченное время жизни — переменные могут существовать дольше, чем необходимо, что может приводить к утечкам памяти
3. Высокая стоимость наследования — при создании дочерних потоков переменные должны копироваться, что увеличивает затраты по памяти
Scoped Values позволяют избежать этих проблем, обеспечивая одноразовую запись и ограниченное время жизни значений:
final static ScopedValue<FrameworkContext> CONTEXT = ScopedValue.newInstance();
void serve(Request request, Response response) {
var context = createContext(request);
ScopedValue.runWhere(CONTEXT, context, () -> Application.handle(request, response));
}
public PersistedObject readKey(String key) {
var context = CONTEXT.get();
var db = getDBConnection(context);
db.readKey(key);
}
В данном случае метод
ScopedValue.runWhere
связывает значение с текущим потоком на время выполнения лямбда-выражения, после чего связь уничтожается, что улучшает производительность и безопасность кода.В 23 версии Java фича будет пересмотрена повторно с одним изменением: тип параметра операции метода
ScopedValue.callWhere
будет являться новым функциональным интерфейсом, который позволит компилятору Java делать вывод о том, может ли быть выброшено проверяемое исключение. Подробнее про ScopedValue.callWhere
и улучшения, связанные с ним, поговорим во второй части.Ставьте 🔥 если хотите вторую часть!
#Java #JEP
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥59👍9❤2
Благодаря этой статье вы узнаете, как подключить и настроить Flyway в Spring Boot приложении, а также как сгенерировать скрипты инициализации и миграции схемы базы данных.
📚 Подробнее читайте на Хабре
Please open Telegram to view this post
VIEW IN TELEGRAM
👍13🔥6❤2
Мы рады представить вам новую рубрику, в которой эксперты сообщества Spring АйО будут отвечать на актуальные и интересные вопросы. Для дебютного выпуска мы пригласили Михаила Поливаху, контрибьютора в Spring Data JDBC, который любезно согласился принять участие. Большое спасибо ему за это! Если вам понравится этот формат, поддержите нас лайками и репостами.
Итак, ниже ответ Михаила на заявленный в заголовке вопрос.
–––
Друзья, по поводу аналога Criteria API в Spring Data JDBC и механизма динамического построения запросов в целом.
TL;DR: Аналог, какой-никакой, имеется, он даже работает😅. Но он менее функциональный, чем тот же Spring Data Specification API.
Long answer:
Если говорить про динамическое построение запроса в целом, то мы можем разбить это на несколько частей:
1. Динамическое построение условий.
Например,
LIKE
, BETWEEN
, IS NOT NULL
и любые другие keyword-ы, которые предназначены для фильтрации набора данных, но которые никак не влияют на структуру запроса в целом. Вот это в Spring Data JDBC имеется. Основные классы, которыми вы будете оперировать в таком случае, - Criteria
и Query
. Использовать это API достаточно просто, единственное - вам придется использовать напрямую JdbcAggregateTemplate
. На данный момент распознавание методов с Query
как параметром метода в Repository/CrudRepository
не поддерживается (речь не про default методы, а именно про абстрактные методы в интерфейсе, которые воспринимаются как PartTreeJdbcQuery
запросы), и скорее всего не будет поддерживаться, так как мало кому это пока было нужно (то есть никакого аналога интерфейсу JpaSpecificationExecutor
в Spring Data JDBC нет). Вот пример использования:
Сущность:
@Data
@Table("users")
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
public class User {
@Id
@EqualsAndHashCode.Include
private Long id;
private String status;
private String name;
private OffsetDateTime createdAt;
}
И пример запроса:
@Autowired
private JdbcAggregateTemplate repository;
@Test
void testCriteriaApi() {
Criteria criteria = Criteria
.where("status").is("READY").ignoreCase(true)
.and("created_at").between(OffsetDateTime.now().minusDays(2), OffsetDateTime.now().plusDays(2))
.and("name").like("%J%");
Query query = Query.query(criteria);
Page<User> allByQuery = repository.findAll(query, User.class, Pageable.unpaged());
Assertions.assertThat(allByQuery.getTotalElements()).isEqualTo(3);
}
Надеюсь, здесь все понятно. Код довольно легко читаемый, вот какой SQL мы сгенерируем:
SELECT
"users"."id" AS "id",
"users"."name" AS "name",
"users"."status" AS "status",
"users"."created_at" AS "created_at"
FROM
"users"
WHERE
UPPER("users"."status") = UPPER(?)
AND "users".created_at BETWEEN ?
AND ?
AND "users"."name" LIKE ?
2. Динамическое построение структуры запроса
Теперь, есть другая часть той же Criteria API - это уже динамическое построение самой структуры запроса. Частично с этим помогает
CriteriaQuery
из Criteria API. Это все возможные группировки, формирование селект листа, формирование подзапросов и т.п. Вот этого в Spring Data JDBC нет и в ближайшее время не планируется. Это огромный пласт работы, который предстоит сделать, если, конечно, это кому-то нужно. Сейчас там конь не валялся по этому вопросу.Please open Telegram to view this post
VIEW IN TELEGRAM
🔥20👍10❤5
Audio
Оказывается, нейросеть Suno не только умеет сочинять классные песни, но и отлично разбирается в программировании! На запрос "The best programming language" она написала песню про Java 🥰
Как перестать подпевать? 😂
java-java is super cool
java-java not just a tool
java here and java there
java language is superb
Please open Telegram to view this post
VIEW IN TELEGRAM
❤17🔥13😁9
Для тех, кто был слишком занят на неделе или просто пропустил некоторые посты, публикуем дайджест!
Spring Tips: Аннотация @Lazy – разобрались, как лениво инициализировать бины, и обсудили, стоит ли вообще это делать
Scoped Values в Java (Часть 1) – продолжили изучать нововведения Java, которые появятся в нашем любимом языке программирования в ближайшее время
Flyway + Spring Boot: настройка и написание миграций баз данных – узнали, как можно быстро и просто писать скрипты миграции баз данных
#ВопросЭксперту: Есть ли аналог Criteria API в Spring Data JDBC? – запустили новую рубрику, в первом выпуске которой дебютировал Михаил Поливаха
Java - лучший язык программирования!? – нашли еще один аргумент в пользу Java для спора о лучшем языке программирования
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥13❤3👍1
Валидация входных данных — важная часть любой системы, обеспечивающая целостность данных и предотвращение ошибок. Иногда стандартные аннотации 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
👍31❤7🔥4