В условиях растущих требований к производительности современных приложений, кеширование становится одним из ключевых инструментов для их удовлетворения.
В новом переводе от команды 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
🔥40👍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
Бывало ли у вас такое, что тест падает на первом же assertion'e из десяти? Вы исправляете ошибку, запускаете тест снова, и он падает на втором assertion'e. И так десять раз. Выматывает, не так ли?
На самом деле, есть способ ускорить этот процесс — использовать soft assertions. С их помощью тест выполнится полностью, даже если один или несколько assertion'ов упадут, и вы сразу увидите все ошибки.
В новой статье от Михаила Поливахи, эксперта сообщества Spring АйО, вы узнаете, что такое soft assertions и как ими пользоваться.
📚 Подробнее читайте на Хабре
Please open Telegram to view this post
VIEW IN TELEGRAM
👍18❤9🔥7
Часто в файле
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👍12❤1
В новом переводе от команды Spring АйО вы узнаете, как можно сгенерировать код HTTP клиентов для Spring Boot приложения по OpenAPI спецификации, используя плагин openapi-generator для Gradle.
В статье вы найдете:
💡 Пошаговую инструкцию по использованию openapi-generator для Gradle
⚙️ Настройки для генератора, которые помогут оставить только нужный код
🍃 Пример конфигурации сгенерированных Spring-бинов
📚 Подробнее читайте на Хабре
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥17👍10❤4🤔1