Русский
Русский
English
Статистика
Реклама

Spring framework

Spring Ускоряем запись в базу данных с помощью XML

10.11.2020 22:10:33 | Автор: admin
Всем привет!

В этой статье пойдет речь о том как ускорить запись большого объема информации в реляционную базу данных для приложений, написанных с использованием Spring Boot. При записи большого количества строк за раз Hibernate вставляет их по одному, что приводит к существенному ожиданию, если строк много. Рассмотрим кейс как это обойти.

Используем Spring Boot приложение. В качестве СУБД -> MS SQL Server, в качестве языка программирования- Kotlin. Разумеется для Java разницы не будет.

Entity для данных, которые нам необходимо записывать:

@Entity@Table(schema = BaseEntity.schemaName, name = GoodsPrice.tableName)data class GoodsPrice(        @Id        @Column(name = "GoodsPriceId")        @GeneratedValue(strategy =  GenerationType.IDENTITY)        override val id: Long,        @Column(name = "GoodsId")        val goodsId: Long,        @Column(name = "Price")        val price: BigDecimal,        @Column(name = "PriceDate")        val priceDate: LocalDate): BaseEntity(id) {        companion object {                const val tableName: String = "GoodsPrice"        }}

SQL:

CREATE TABLE [dbo].[GoodsPrice]([GoodsPriceId] [int] IDENTITY(1,1) NOT NULL,[GoodsId] [int] NOT NULL,[Price] [numeric](18, 2) NOT NULL,[PriceDate] nvarchar(10) NOT NULL, CONSTRAINT [PK_GoodsPrice] PRIMARY KEY(GoodsPriceId))

В качестве демонстрационного примера будем предполагать, что нам необходимо записывать по 20 000 и по 50 000 записей.

Создадим контроллер, который будет генерировать данные и передавать их на запись и логгировать время:

@RestController@RequestMapping("/api")class SaveDataController(private val goodsPriceService: GoodsPriceService) {    @PostMapping("/saveViaJPA")    fun saveDataViaJPA(@RequestParam count: Int) {        val timeStart = System.currentTimeMillis()        goodsPriceService.saveAll(prepareData(count))        val secSpent = (System.currentTimeMillis() - timeStart) / 60        logger.info("Seconds spent : $secSpent")    }    private fun prepareData(count: Int) : List<GoodsPrice> {        val prices = mutableListOf<GoodsPrice>()        for (i in 1..count) {            prices.add(GoodsPrice(                    id = 0L,                    priceDate = LocalDate.now().minusDays(i.toLong()),                    goodsId = 1L,                    price = BigDecimal.TEN            ))        }        return prices    }    companion object {        private val logger = LoggerFactory.getLogger(SaveDataController::class.java)    }}

Так же создадим сервис для записи данных и репозиторий GoodsPriceRepository

@Serviceclass GoodsPriceService(        private val goodsPriceRepository: GoodsPriceRepository) {    private val xmlMapper: XmlMapper = XmlMapper()    fun saveAll(prices: List<GoodsPrice>) {        goodsPriceRepository.saveAll(prices)    }}

После этого последовательно вызовем наш метод saveDataViaJPA для 20 000 записей и 50 000 записей.

Консоль:

Hibernate: insert into dbo.GoodsPrice (GoodsId, Price, PriceDate) values (?, ?, ?)Hibernate: insert into dbo.GoodsPrice (GoodsId, Price, PriceDate) values (?, ?, ?)Hibernate: insert into dbo.GoodsPrice (GoodsId, Price, PriceDate) values (?, ?, ?)Hibernate: insert into dbo.GoodsPrice (GoodsId, Price, PriceDate) values (?, ?, ?)Hibernate: insert into dbo.GoodsPrice (GoodsId, Price, PriceDate) values (?, ?, ?)Hibernate: insert into dbo.GoodsPrice (GoodsId, Price, PriceDate) values (?, ?, ?)Hibernate: insert into dbo.GoodsPrice (GoodsId, Price, PriceDate) values (?, ?, ?)Hibernate: insert into dbo.GoodsPrice (GoodsId, Price, PriceDate) values (?, ?, ?)Hibernate: insert into dbo.GoodsPrice (GoodsId, Price, PriceDate) values (?, ?, ?)Hibernate: insert into dbo.GoodsPrice (GoodsId, Price, PriceDate) values (?, ?, ?)Hibernate: insert into dbo.GoodsPrice (GoodsId, Price, PriceDate) values (?, ?, ?)Hibernate: insert into dbo.GoodsPrice (GoodsId, Price, PriceDate) values (?, ?, ?)Hibernate: insert into dbo.GoodsPrice (GoodsId, Price, PriceDate) values (?, ?, ?)Hibernate: insert into dbo.GoodsPrice (GoodsId, Price, PriceDate) values (?, ?, ?)2020-11-10 19:11:58.886  INFO 10364 --- [  restartedMain] xmlsave.controller.SaveDataController    : Seconds spent : 63

Проблема заключается в том что Hibernate пытался встававить каждую строку отдельным запросом, то есть 20 000 раз. И на моей машине это заняло 63 сек.

Для 50 000 записей 166 сек.

Решение

Что можно сделать? Главная идея заключается в том, что будем записывать через буфферную таблицу:

@Entity@Table(schema = BaseEntity.schemaName, name = SaveBuffer.tableName)data class SaveBuffer(        @Id        @Column(name = "BufferId")        @GeneratedValue(strategy =  GenerationType.IDENTITY)        override val id: Long,        @Column(name = "UUID")        val uuid: String,        @Column(name = "xmlData")        val xmlData: String): BaseEntity(id) {        companion object {                const val tableName: String = "SaveBuffer"        }}

SQL script для таблицы в базе данных

CREATE TABLE [dbo].[SaveBuffer]([BufferId] [int] IDENTITY NOT NULL,[UUID] [varchar](64) NOT NULL,[xmlData] [xml] NULL, CONSTRAINT [PK_SaveBuffer] PRIMARY KEY (BufferId))

В SaveDataController добавим метод:

@PostMapping("/saveViaBuffer")    fun saveViaBuffer(@RequestParam count: Int) {        val timeStart = System.currentTimeMillis()        goodsPriceService.saveViaBuffer(prepareData(count))        val secSpent = (System.currentTimeMillis() - timeStart) / 60        logger.info("Seconds spent : $secSpent")    }

Так же добавим в GoodsPriceService метод:

@Transactional    fun saveViaBuffer(prices: List<GoodsPrice>) {        val uuid = UUID.randomUUID().toString()        val values = prices.map {            BufferDTO(                    goodsId = it.goodsId,                    priceDate = it.priceDate.format(DateTimeFormatter.ISO_DATE),                    price = it.price.stripTrailingZeros().toPlainString()            )        }        bufferRepository.save(                    SaveBuffer(                            id = 0L,                            uuid = uuid,                            xmlData = xmlMapper.writeValueAsString(values)                    )            )        goodsPriceRepository.saveViaBuffer(uuid)        bufferRepository.deleteAllByUuid(uuid)    }

Для записи для начала генерим уникальный uuid, чтобы отличить текущие данные, которые записываем. Далее записываем наши данные в созданный буффер текстом в виде xml. То есть будет не 20 000 инсертов, а всего 1.

И после этого перебрасываем одним запросом типа Insert into select данные из буффера в таблицу GoodsPrice.

GoodsPriceRepository с методом saveViaBuffer:

@Repositoryinterface GoodsPriceRepository: JpaRepository<GoodsPrice, Long> {    @Modifying    @Query("""    insert into dbo.GoodsPrice(GoodsId,Price,PriceDate)select res.*from dbo.SaveBuffer buffercross apply(select temp.n.value('goodsId[1]', 'int') as GoodsId, temp.n.value('price[1]', 'numeric(18, 2)') as Price, temp.n.value('priceDate[1]', 'nvarchar(10)') as PriceDatefrom buffer.xmlData.nodes('/ArrayList/item') temp(n)) reswhere buffer.UUID = :uuid    """, nativeQuery = true)    fun saveViaBuffer(uuid: String)}

И в конце для того, чтобы не хранить в базе дублированную информацию удаляем по uuid данные из буффера.

Вызовем наш метод saveViaBuffer для 20 000 строк и 50 000 строк:

Hibernate: insert into dbo.SaveBuffer (UUID, xmlData) values (?, ?)Hibernate: insert into dbo.SaveBuffer (UUID, xmlData) values (?, ?)Hibernate: insert into dbo.SaveBuffer (UUID, xmlData) values (?, ?)Hibernate: insert into dbo.SaveBuffer (UUID, xmlData) values (?, ?)Hibernate:     insert into dbo.GoodsPrice(GoodsId,Price,PriceDate)select res.*from dbo.SaveBuffer buffercross apply(select temp.n.value('goodsId[1]', 'int') as GoodsId, temp.n.value('price[1]', 'numeric(18, 2)') as Price, temp.n.value('priceDate[1]', 'nvarchar(10)') as PriceDatefrom buffer.xmlData.nodes('/ArrayList/item') temp(n)) reswhere buffer.UUID = ?    Hibernate: select savebuffer0_.BufferId as bufferid1_1_, savebuffer0_.UUID as uuid2_1_, savebuffer0_.xmlData as xmldata3_1_ from dbo.SaveBuffer savebuffer0_ where savebuffer0_.UUID=?Hibernate: delete from dbo.SaveBuffer where BufferId=?Hibernate: delete from dbo.SaveBuffer where BufferId=?Hibernate: delete from dbo.SaveBuffer where BufferId=?Hibernate: delete from dbo.SaveBuffer where BufferId=?2020-11-10 20:01:58.788  INFO 7224 --- [  restartedMain] xmlsave.controller.SaveDataController    : Seconds spent : 13

Как видим по результатам получили существенное ускорение записи данных.
Для 20 000 записей 13 секунд было 63.
Для 50 000 записей 27 секунд было 166.

Ссылка на тестовый проект
Подробнее..

Перевод Что нового в Spring Data (Klara Dan von) Neumann

20.08.2020 16:15:21 | Автор: admin
Перевод статьи подготовлен в преддверии старта курса Разработчик на Spring Framework.
Подробнее о курсе можно узнать посмотрев запись дня открытых дверей.



Spring Data Neumann это первый релиз, после перехода на новый шестимесячный релизный цикл. Сокращение сроков между релизами позволит нам чаще выпускать новые фичи, а это, в свою очередь, ускорит и вас. В этом релизе, помимо нового функционала, есть также важные изменения, потенциально ломающие совместимость с предыдущими версиями.

Изменение мажорных версий


Для проектов, указанных ниже, увеличен номер мажорной версии из-за изменений, ломающих совместимость в публичных API или драйверах:

  • Spring Data JDBC 2.0 (предыдущая версия 1.1)
  • Миграция с 1.1 на 2.0 описана в этом посте.
  • Spring Data MongoDB 3.0 (предыдущая версия 2.2)
  • Spring Data для Apache Cassandra 3.0 (предыдущая версия 2.2)
  • Spring Data Couchbase 4.0 (предыдущая версия 3.2)
  • Spring Data Elasticsearch 4.0 (предыдущая версия 3.2)
  • Подробнее об изменениях см. этом посте.


Перед тем как перейти к описанию новой функциональности, давайте посмотрим на изменения в API. Подробнее об этом смотрите в разделах по обновлению (Upgrading) в документации соответствующих модулей.

Если вы не готовы обновляться сейчас, то имейте в виду, что предыдущий релиз Moore будет поддерживаться еще в течение двенадцати месяцев.

JDBC


У каждого SQL-хранилища есть свои особенности, требующие особого подхода. Для улучшения их поддержки были внесены изменения, которые повлияли на увеличение мажорной версии. Теперь AbstractJdbcConfiguration по умолчанию пытается определить Dialect базы данных из заданного DataSource или зарегистрированного DialectResolver. По умолчанию модуль JDBC поставляется с диалектами для H2, HSQLDB, MySQL, Postgres, MariaDB, Microsoft SqlServer и DB2. Spring Data JDBC теперь по умолчанию экранирует все имена таблиц и колонок. Несмотря на то что из-за этого вам, возможно, придется изменить ваши CREATE TABLE или аннотации @Column, это даст большую гибкость при именовании объектов.

MongoDB


Единый jar с драйверами для MongoDB (mongo-java-driver) разбит на несколько: -sync и -reactivestreams, что позволяет вам выбрать только необходимый драйвер. То есть, и синхронный, и реактивный драйверы MongoDB теперь являются необязательными зависимостями, которые необходимо добавлять вручную. При переходе на новые драйвера некоторые из уже устаревших API были окончательно удалены, что повлияло на классы конфигурации, такие как AbstractMongoConfiguration и пространства имен XML, предоставляемые Spring Data. Подробнее смотрите раздел по обновлению в документации.

Apache Cassandra


Давно назревшее обновление драйверов Apache Cassandra до 4.0 не только обновляет пакет и структуру данных, но также изменяет поведение в кластере и в обработке сеансов. Это привело к серьезным изменениям в конфигурации, которые влияют на XML-конфигурацию и могут повлиять на конфигурацию в коде для каких-то сложных сценариев (сложнее, чем простая настройка по умолчанию AbstractCassandraConfiguration).

Couchbase


Вслед за Couchbase SDK мы обновились с версии 3.x до 4.x, что добавило автоматическое управление индексами и поддержку транзакций. Подробнее читайте в блоге Couchbase.

Elasticsearch


Добавлена поддержка HTTP Client API, SSL и Proxy. Также сделан ряд изменений, включающих в себя оптимизацию и удаление устаревшего API, что повлияло на изменение мажорного номера версии. В модуль Elasticsearch теперь входит Document, включающий в себя Get-, Index- и Search-Requests, что позволяет при маппинге использовать такие типы как SearchHit, SearchHits и SearchPage.

Теперь давайте перейдем к новшествам.

Репозитории с поддержкой корутин Kotlin


Релиз Neumann продолжает развитие поддержки корутин Kotlin, начавшуюся в предыдущем релизе Moore, добавив их поддержку в репозиториях.

Корутины поддерживаются через реактивные Spring Data-репозитории. Теперь можно использовать реактивные методы запросов или писать свои suspended-функции.

interface StudentRepository : CoroutineCrudRepository<Student, String> {    suspend fun findOne(id: String): User    fun findByLastname(firstname: String): Flow<Student>}


@Primary-репозитории и ключевое слово search


Эти два небольших изменения улучшают поиск бинов репозиториев и именование методов запросов. Теперь аннотация @Primary на репозиториях-интерфейсах учитывается в конфигурации бинов, что помогает контейнеру резолвить зависимости. Для методов запросов теперь можно использовать префикс "search", аналогично "find". То есть теперь можно писать методы "search...By...", например, searchByFirstname.

Несмотря на то что это было сделано для таких баз данных, как Elasticsearch, давайте двигаться дальше и посмотрим как search...By... можно использовать в Spring Data R2DBC.

Генерация запросов R2DBC


До настоящего времени в Spring Data R2DBC для методов запросов использовалась аннотация @Query за исключением методов по умолчанию, предоставляемых через интерфейсы *.Repository. Теперь генерация запросов по имени метода работает аналогично другим модулям:

interface StudentRepository extends ReactiveCrudRepository<Student, Long> {Flux<Student> searchByLastname(String lastname); (1)}


Это эквивалентно:

@Query("select id, firstname, lastname from customer c where c.lastname = :lastname")

Разбивка на страницы и генерация запросов для JDBC


Spring Data JDBC 2.0 поддерживает еще больше реляционных баз данных. Теперь мы запускаем наши интеграционные тесты на H2, HSQLDB, MySQL, MariaDB, PostgreSQL и DB2.

Для этих баз данных мы поддерживаем генерацию запросов и разбивку на страницы. Например:

interface StudentRepository extends PagingAndSortingRepository<Student, Long> {Page<Student> findByLastname(String lastname);}


В этом релизе мы также продолжаем двигаться в сторону NoSQL, начав с MongoDB и нового способа модификации документов.

MongoDB Update Aggregations


Это важное изменение (которое не было полностью готово в релизе Moore) позволяет использовать Aggregation Pipeline для обновления данных. Таким образом, изменения могут содержать сложные выражения, такие как условия по значениям полей, например:

AggregationUpdate update = Aggregation.newUpdate()    .set("average").toValue(ArithmeticOperators.valueOf("tests").avg())    .set("grade").toValue(ConditionalOperators.switchCases(        when(valueOf("average").greaterThanEqualToValue(90)).then("A"),        when(valueOf("average").greaterThanEqualToValue(80)).then("B"),        when(valueOf("average").greaterThanEqualToValue(70)).then("C"),        when(valueOf("average").greaterThanEqualToValue(60)).then("D"))        .defaultTo("F")    );template.update(Student.class)    .apply(update)    .all();


Также Spring Data MongoDB, несомненно, выиграет от недавно добавленной в другие модули поддержки встроенных объектов (embedded object).

Поддержка embedded-типов в Apache Cassandra


Apache Cassandra теперь поддерживает маппинг встроенных типов (embedded type), которые уже давно были доступны в Spring Data JDBC. В модели предметной области встроенные объекты используются для объектов-значений (Value Object), свойства которых хранятся в одной таблице. В следующем примере над полем Student.name стоит аннотация @Embedded, что приводит к тому, что все поля класса Name будут храниться в таблице Student, состоящей из трех столбцов (student_id, firstname и lastname):

public class Student {    @PrimaryKey("student_id")    private String studentId;    @Embedded(onEmpty = USE_NULL)    Name name;}public class Name {    private String firstname;    private String lastname;}


Аудит в Elasticsearch


Поскольку в ElasticSearch наличие id не является достаточным критерием для определения того, является ли объект новым, необходимо при реализации Persistable предоставить дополнительную информацию с помощью метода isNew():

@Document(indexName = "person")public class Person implements Persistable<Long> {    @Id private Long id;    private String lastName;    private String firstName;    @Field(type = Date)    private Instant createdDate;    private String createdBy    @Field(type = Date)    private Instant lastModifiedDate;    private String lastModifiedBy;    @Override    public boolean isNew() {        return id == null || (createdDate == null && createdBy == null);    }}


После этого добавление @EnableElasticsearchAuditing в конфигурацию регистрирует все компоненты, необходимые для аудита.

Neo4j


Spring Data Neo4j теперь поддерживает синтаксис запросов Neo4j 4.0 с параметрами. Синтаксис с заполнителями (placeholder) был ранее объявлен устаревшим, а сейчас полностью удален. Теперь модуль зависит от последних версий драйверов Neo4j-OGM и Neo4j Java для улучшения взаимодействия с последней версией Neo4j.
Также идет активная работа по поддержке реактивности для графовых баз данных и ее интеграция в Spring Data с Neo4j RX (хотя это и не входит в текущий релиз, но уже готово для включения в следующий).

Apache Geode / VMware Tanzu GemFire


Модули Spring Data для Apache Geode и VMware Tanzu GemFire (spring-data-geode и spring-data-gemfire) объединены в один проект под общим названием SDG. Apache Geode был обновлен до 1.12.0, а GemFire до 9.10.0, который, в свою очередь, основан на Apache Geode 1.12. Кроме того, SDG компилируется и запускается на версиях JDK с 8 по 14.

SDG теперь поддерживает публикацию событий автотранзакций, которая преобразует событие Cache TransactionEvent от GemFire / Geode Cache в соответствующий ApplicationEvent контекста.

Также теперь можно приостановить отправку событий на AEQ, настроенном с помощью SDG. Кроме того, при создании приложений на основе GemFire / Geode Locator с использованием аннотации SDG @LocatorApplication можно настроить Locator для подключения к другим Locator, создавая таким образом высокодоступный и устойчивый кластер.



Узнать подробно о курсе.




Читать ещё:


Подробнее..

Тривиальная и неправильная облачная компиляция

28.01.2021 00:21:03 | Автор: admin


Введение


Данная статья не история успеха, а скорее руководство как не надо делать. Весной 2020 для поддержания спортивного тонуса участвовал в студенческом хакатоне (спойлер: заняли 2-е место). Удивительно, но задача из полуфинала оказалась более интересной и сложной чем финальная. Как вы поняли, о ней и своём решении расскажу под катом.


Задача


Данный кейс был предложен Deutsche Bank в направлении WEB-разработка.
Необходимо было разработать онлайн-редактор для проекта Алгосимулятор тестового стенда для проверки работы алгоритмов электронной торговли на языке Java. Каждый алгоритм реализуется в виде наследника класса AbstractTradingAlgorythm.


AbstractTradingAlgorythm.java
public abstract class AbstractTradingAlgorithm {    abstract void handleTicker(Ticker ticker) throws Exception;    public void receiveTick(String tick) throws Exception {        handleTicker(Ticker.parse(tick));    }    static class Ticker {        String pair;        double price;       static Ticker parse(String tick) {           Ticker ticker = new Ticker();           String[] tickerSplit = tick.split(",");           ticker.pair = tickerSplit[0];           ticker.price = Double.valueOf(tickerSplit[1]);           return ticker;       }    }}

Сам же редактор во время работы говорит тебе три вещи:


  1. Наследуешь ли ты правильный класс
  2. Будут ли ошибки на этапе компиляции
  3. Успешен ли тестовый прогон алгоритма. В данном случае подразумевается, что "В результате вызова new <ClassName>().receiveTick(RUBHGD,100.1) отсутствуют runtime exceptions".


Ну окей, скелет веб-сервиса через spring накидать дело на 5-10 минут. Пункт 1 работа для регулярных выражений, поэтому даже думать об этом сейчас не буду. Для пункта 2 можно конечно написать синтаксический анализатор, но зачем, когда это уже сделали за меня. Может и пункт 3 получится сделать, использовав наработки по пункту 2. В общем, дело за малым, уместить в один метод, ну например, компиляцию исходного кода программы на Java, переданного в контроллер строкой.


Решение


Здесь и начинается самое интересное. Забегая вперёд, как сделали другие ребята: установили на машину джаву, отдавали команды на ось и грепали stdout. Конечно, это более универсальный метод, но во-первых, нам сказали слово Java, а во-вторых...



у каждого свой путь.


Естественно, Java окружение устанавливать и настраивать всё же придётся. Правда компилировать и исполнять код мы будем не в терминале, а, как бы это ни звучало, в коде. Начиная с 6 версии, в Java SE присутствует пакет javax.tools, добавленный в стандартный API для компиляции исходного кода Java.
Теперь привычные понятия такие, как файлы с исходным кодом, параметры компилятора, каталоги с выходными файлами, сообщения компилятора, превратились в абстракции, используемые при работе с интерфейсом JavaCompiler, через реализации которого ведётся основная работа с задачами компиляции. Подробней о нём можно прочитать в официальной документации. Главное, что оттуда сейчас перейдёт моментально в текст статьи, это класс JavaSourceFromString. Дело в том, что, по умолчанию, исходный код загружается из файловой системы. В нашем же случае исходный код будет приходить строкой извне.


JavaSourceFromString.java
import javax.tools.SimpleJavaFileObject;import java.net.URI;public class JavaSourceFromString extends SimpleJavaFileObject {    final String code;    public JavaSourceFromString(String name, String code) {        super(URI.create("string:///" + name.replace('.', '/') + Kind.SOURCE.extension), Kind.SOURCE);        this.code = code;    }    @Override    public CharSequence getCharContent(boolean ignoreEncodingErrors) {        return code;    }}

Далее, в принципе уже ничего сложного нет. Получаем строку, имя класса и преобразуем их в объект JavaFileObject. Этот объект передаём в компилятор, компилируем и собираем вывод, который и возвращаем на клиент.
Сделаем класс Validator, в котором инкапсулируем процесс компиляции и тестового прогона некоторого исходника.


public class Validator {    private JavaSourceFromString sourceObject;    public Validator(String className, String source) {        sourceObject = new JavaSourceFromString(className, source);    }}

Далее добавим компиляцию.


public class Validator {    ...    public List<Diagnostic<? extends JavaFileObject>> compile() {        // получаем компилятор, установленный в системе        var compiler = ToolProvider.getSystemJavaCompiler();        // компилируем        var compilationUnits = Collections.singletonList(sourceObject);        var diagnostics = new DiagnosticCollector<JavaFileObject>();        compiler.getTask(null, null, diagnostics, null, null, compilationUnits).call();        // возворащаем диагностику        return diagnostics.getDiagnostics();    }}

Пользоваться этим можно как-то так.


public void MyMethod() {        var className = "TradeAlgo";        var sourceString = "public class TradeAlgo extends AbstractTradingAlgorithm{\n" +                "@Override\n" +                "    void handleTicker(Ticker ticker) throws Exception {\n" +                "       System.out.println(\"TradeAlgo::handleTicker\");\n" +                "    }\n" +                "}\n";        var validator = new Validator(className, sourceString);        for (var message : validator.compile()) {            System.out.println(message);        }    }

При этом, если компиляция прошла успешно, то возвращённый методом compile список будет пуст. Что интересно? А вот что.

На приведённом изображении вы можете видеть директорию проекта после завершения выполнения программы, во время выполнения которой была осуществлена компиляция. Красным прямоугольником обведены .class файлы, сгенерированные компилятором. Куда их девать, и как это чистить, не знаю жду в комментариях. Но что это значит? Что скомпилированные классы присоединяются в runtime, и там их можно использовать. А значит, следующий пункт задачи решается тривиально с помощью средств рефлексии.
Создадим вспомогательный POJO для хранения результата прогона.


TestResult.java
public class TestResult {    private boolean success;    private String comment;    public TestResult(boolean success, String comment) {        this.success = success;        this.comment = comment;    }    public boolean success() {        return success;    }    public String getComment() {        return comment;    }}

Теперь модифицируем класс Validator с учётом новых обстоятельств.


public class Validator {    ...    private String className;    private boolean compiled = false;    public Validator(String className, String source) {        this.className = className;        ...    }    ...    public TestResult testRun(String arg) {        var result = new TestResult(false, "Failed to compile");        if (compiled) {            try {                // загружаем класс                var classLoader = URLClassLoader.newInstance(new URL[]{new File("").toURI().toURL()});                var c = Class.forName(className, true, classLoader);                // создаём объект класса                var constructor = c.getConstructor();                var instance = constructor.newInstance();                // выполняем целевой метод                c.getDeclaredMethod("receiveTick", String.class).invoke(instance, arg);                result = new TestResult(true, "Success");            } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException | ClassNotFoundException | RuntimeException | MalformedURLException | InstantiationException e) {                var sw = new StringWriter();                e.printStackTrace(new PrintWriter(sw));                result = new TestResult(false, sw.toString());            }        }        return result;    }}

Возвращаясь к предыдущему примеру использования, можно дописать туда такие строчки.


public void MyMethod() {        ...        var result = validator.testRun("RUBHGD,100.1");        System.out.println(result.success() + " " + result.getComment());    }

Вставить этот код в реализацию API контроллера задача нетрудная, поэтому подробности её решения можно опустить.


Какие проблемы?


  1. Ещё раз напомню про кучу .class файлов.


  2. Поскольку опять же идёт работа с компиляцией некоторых классов, есть риск отказа в записи любого непредвиденного .class файла.


  3. Самое главное, наличие уязвимости для инъекции вредосного кода на языке программирования Java. Ведь, на самом деле, пользователь может написать что угодно в теле вызываемого метода. Это может быть вызов полного перегруза системы, затирание всей файловой системы машины и тому подобное. В общем, исполняемую среду нужно изолировать, как минимум, а в рамках хакатона этим в команде никто естественно не занимался ввиду нехватки времени.



Поэтому делать в точности как я не надо)


P.S. Ссылка на гитхаб с исходным кодом из статьи.

Подробнее..

JPA Buddy Умный помощник половина работы

17.03.2021 14:12:14 | Автор: admin

От переводчика: это статья моего коллеги @aleksey-stukalov, которую мы опубликовали в блоге JPA Buddy пару месяцев назад. С тех пор мы выпустили JPA Buddy 2.0, но все сказанное в этой статье актуальности не потеряло.

Ну что ж, Hello World... После почти года разработки наконец-то вышла первая версия JPA Buddy! Это инструмент, который должен стать вашим верным помощником по написанию кода для проектов с JPA и всем, что с этим связано: Hibernate, Spring Data, Liquibase и другим ПО из типичного стека разработки.

Чем он вам поможет? Если кратко, JPA Buddy упростит работу с JPA и тем самым сэкономит ваше время. В этой статье мы взглянем на основные фичи JPA Buddy, немного обсудим его историю и поговорим о его преимуществах. Надеюсь, он займет достойное место среди любимых инструментов Java-разработчиков, которые пользуютсяJPA, Spring, Liquibase и, конечно же, самой продвинутой Java IDE IntelliJ IDEA.

Откуда растут ноги

Мы создатели CUBA Platform (кстати, не так давно мы переименовали ее в Jmix :)) среды быстрой разработки приложений на Java. Платформа CUBA уникальный продукт. Он состоит из двух частей: фреймворка и инструмента разработки CUBA Studio. Одной из самых полюбившихся частей CUBA Studio стал Entity Designer. В сообществе CUBA более 20 000 разработчиков, и почти все они отмечают, насколько легко и быстро он дает создавать модель данных. Даже для тех, кто никогда не слышал о JPA, такие вещи как создание JPA-сущностей, ограничений модели данных и DDL-скриптов становятся легкой задачей.

В 2016 году CUBA стала open-source проектом. С того момента мы приняли участие в десятках конференций по всему миру, собрали много отзывов и лучше поняли, что именно нужно разработчикам. Они нас часто спрашивали: Можно ли использовать ваш конструктор сущностей без CUBA Platform?. Рады сообщить, что с появлением JPA Buddy мы теперь можем смело ответить да!.

JPA Buddy не требует дополнительных зависимостей это просто инструмент. Вы можете пользоваться им не только на новых, но и на уже существующих проектах, которые используют JPA. Это плагин для IntelliJ IDEA, который помогает вам писать связанный с JPA код, находить и исправлять потенциальные ошибки, проводить рефакторинг и выполнять другие трудоемкие, но достаточно шаблонные операции.

Область применения

Перед тем, как написать первую строчку исходного кода JPA Buddy, мы провели опрос и собрали различные сценарии использования JPA и сопутствующих технологий. Результат оказался достаточно предсказуемым: среднестатистическое приложение в настоящее время это приложение на Spring Boot с Hibernate в качестве реализации ORM, Spring Data JPA в качестве механизма управления данными и Flyway или Liquibase для системы миграции базы данных. Ах да, чуть не забыли про Lombok... В общем, на этом стеке мы и сконцентрировались в первом релизе.

Говоря о предназначении JPA Buddy, мы поставили перед собой следующие цели:

  • Сократить написание шаблонного кода вручную: инструмент должен генерировать код быстрее ручного ввода

  • Не заставлять тратить время на чтение документации: инструмент должен предоставлять интуитивно понятные визуальные конструкторы

  • Оставить свободу выбора: инструмент должен не диктовать какойлибо конкретный стиль написания кода, а поддерживать его различные варианты

  • Выявлять потенциальные проблемы и предлагать решения для наиболее распространенных: инструмент должен предупреждать разработчика о проблемах на этапе написания кода, а не в рантайме

  • Обеспечить обзор проекта с точки зрения данных и удобную навигацию между связанными сущностями

В первом релизе мы смогли реализовать достаточно значительный объем функциональности, охватывающей большинство аспектов разработки модели данных. Хорошая новость для тех, кто использует Liquibase: инструменты для работы с ним уже реализованы и доступны. Не очень хорошая новость для пользователей Flyway: он входит в список наиболее приоритетных фич :)

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

  • Аннотации Hibernate: @Where, @NaturalId, @Formula, поисковые аннотации и т. п.

  • Визуальный конструктор запросов

  • Аудит с использованием Envers и Spring Data JPA

  • Генерация модели по существующей схеме базы данных

  • Поддержка Kotlin

В дальнейшем мы учтем и другие функции: поддержка Quarkus и Micronaut, REST API и генерация пользовательского интерфейса для CRUD-операций.

Как видите, впереди еще долгий путь, и мы были бы очень признательны за вашу помощь в расстановке приоритетов разработки не стесняйтесь высказывать свою точку зрения через нашу форму, Twitter или Discord.

Общий обзор

Давайте посмотрим, как выглядит JPA Buddy. После его установки у вас появятся 3 новые панели инструментов: JPA Structure, JPA Palette и JPA Inspector.

JPA Structure

Панель JPA Structure расположена в левом нижнем углу. Она позволяет посмотреть на проект с точки зрения модели данных. С ее помощью можно:

  1. Перемещаться по модели данных. Структура сущностей представлена в иерархическом виде. По ней легко перейти к объектам, ссылающимся на текущий объект, и к тем, на которые ссылается текущий объект. Это особенно полезно для тех, кто только начинает погружаться в существующий проект с большим графом сущностей, или для код-ревьюеров, которые видят часть модели впервые и им нужно в ней быстро разобраться.

  2. Создавать объекты, связанные с данными: сущности, JPA-конвертеры/Hibernate-типы, Spring Data репозитории и Liquibase-скрипты.

  3. Увидеть, для каких сущностей созданы какие репозитории.

  4. Просматривать скрипты Liquibase и их внутреннюю структуру.

  5. Настраивать параметры плагина, такие как соединение с БД, Persistence Units и некоторые другие, которые плагин не обнаружил автоматически.

JPA Palette и JPA Inspector

Панель JPA Palette находится справа вверху, и ее содержимое зависит от контекста. Она доступна только тогда, когда Buddy готов предложить чтото для быстрой генерации кода. Сейчас панель появляется при редактировании следующих объектов: JPA Entity, Spring Data репозиториев и Liquibase-скриптов. Для генерации какого-либо элемента просто выберите нужный вариант в списке и кликните по нему дважды.

JPA Inspector размещается справа внизу, под JPA Palette, и открывается вместе с ним. С помощью JPAPalette можно генерировать новый код, а с помощью JPAInspector редактировать уже существующий. В нем видно, как можно сконфигурировать выбранную часть кода, будь то поле сущности или выражение изLiquibase-скрипта.

Все это будет полезно как для новичков, так и для опытных разработчиков. Первые могут посмотреть на полный список вариантов, выбрать подходящий и сгенерировать корректный код с помощью понятных визуальных конструкторов. Сеньоры, в свою очередь, часто забывают мелочи, которые не используются в повседневной жизни, и Buddy сэкономит несколько секунд драгоценного времени на поиск в документации правильной аннотации или синтаксиса.

Интеграция с Liquibase

Хорошие новости для тех, кто использует Liquibase для управления версиями схемы базы данных: JPA Buddy тесно с ним интегрирован, он позволяет удобно редактировать и создавать Liquibase-скрипты, а также гибко управлять типами атрибутов. Давайте посмотрим, как именно JPA Buddy поможет вам работать с Liquibase.

Во-первых, в JPA Buddy есть визуальный конструктор для редактирования Liquibase-скриптов. Он показывает различные доступные команды плагина в JPAPalette, а панель инструментов JPAInspector дает увидеть настройки, которые можно применить к выбранному выражению.

Во-вторых, JPA Buddy дает определить свои собственные маппинги для сопоставления Java-типов (Конвертеров/Hibernateтипов) и типов, которые должны использоваться для каждой конкретной СУБД. Приведем несколько примеров, когда эта функция будет полезна:

  • Допустим, у вас в сущности есть поле типа byte[]. Что генератор схемы Hibernate, что Liquibase сопоставят ваш массив с довольно экзотическим типом OID. Думаю, многие разработчики предпочли бы использовать bytea вместо предлагаемого по умолчанию типа. Это можно легко сделать, создав маппинг с Java-типа byte[] на БД-тип bytea в настройках проекта в JPA Buddy.

  • Использование JPA-конвертера или кастомного Hibernate-типа вызывает непредсказуемое поведение или даже ошибки в стандартных генераторе схемы Hibernate и генераторе скриптов Liquibase. С помощью JPA Buddy можно явно указать нужный тип, чтобы решить эту проблему.

  • Поля типа String по-умолчанию хранятся как varchar, то есть не поддерживают Юникод. С помощью JPA Buddy легко изменить этот тип на nvarchar, с которым этой проблемы нет.

Все эти случаи обычно решаются с помощью атрибута columnDefinition, однако это решение не будет работать для проектов, где одновременно используется несколько СУБД. JPA Buddy позволяет указать, какой именно маппинг использовать для конкретной СУБД.

Наконец, функция, экономящая наибольшее количество времени наш генератор Liquibase-скриптов. Исходя из нашего опыта (который совпадает с опытом разработчиков принявших участие в ранее упомянутом опросе), есть два основных способа создания Liquibase-скриптов:

  • Написание вручную

  • Создание скриптов путем сравнения двух баз данных: исходной (представляющей фактическое состояние модели) и целевой (с предыдущим состоянием модели)

Для тех, кто предпочитает первый вариант, JPA Buddy включает уже упомянутый ранее конструктор Liquibase-скриптов.

Однако, второй вариант наиболее распространен. Итак, сравниваем две базы данных. Целевую базу данных относительно легко получить, например, можно просто создать дамп базы данных продакшена. Сложности возникают с получением исходной базы данных.

Начнем с небольшой проблемы: может случиться так, что база данных, используемая для разработки на ноутбуке разработчика, не того же типа, что и база данных на продакшене. Это можно исправить, например, запустив MS SQL в Docker на Mac.

Более серьезная проблема состоит втом, что трудно найти чистую исходную базу данных, поскольку она обычно формируется впроцессе разработки генератором схемы Hibernate. Часто это приводит кбольшому количеству мусорных таблиц иатрибутов, которые вконечном итоге появляются вбазе данных продакшена. Чтобы избежать подобного рода мусора, приходится тратить еще час или два напросмотр схемы базы данных перед запуском генерации Liquibase-скриптов.

ВJPA Buddy есть замечательная функция генерации Liquibase-скриптов напрямую, путем сравнения ваших объектов JPA сцелевой базой данных (или snapshotом целевой базы данных). Выможете использовать H2в целях разработки ипо-прежнему генерировать правильные журналы изменений для Oracle, MSSQL или того, что выиспользуете впродакшене. Подобный подход гарантирует, что если увас есть мусор вжурналах изменений, это именно тот мусор, который есть увас висходном коде. Все, что вам нужно, это содержать модель данных вчистоте, итогда миграция неприведет кнежелательным артефактам вбазе данных продакшена.

Еще одна особенность генератора журнала изменений это возможность фильтровать полученные выражения по 3 категориям:

  • содержащие только безопасные операторы, которые не могут дать сбой при обновлении или полностью уронить ваше приложение, например, добавление нового столбца

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

  • содержащие операторы, которые вызовут потерю данных, например, удаление таблицы или столбца

Вы сами решаете, как разделить такие утверждения: поместить их в отдельные скрипты Liquibase или просто выбрать им нужную метку или контекст в редакторе.

Заключение

В этой статье мы посмотрели на первый релиз JPA Buddy. Очевидно, что как и любой новый продукт, JPA Buddy не может полностью избежать проблем и багов, особенно в своем первом релизе. Сейчас мы концентрируемся на устранении замечаний от пользователей, улучшении существующей функциональности, определении наиболее приоритетных функций, которые мы могли упустить из виду, и их реализации.

Говоря прямо, мы хотели бы вдохновить вас стать первыми пользователями JPA Buddy и сформировать сообщество энтузиастов. Установите JPA Buddy, попользуйтесь им и поделитесь своими отзывами с нашей командой разработчиков: это очень поможетнам выбрать правильное направление развития продукта. Также подпишитесь на наш Twitter: там мы показываем, какие еще фичи есть у JPA Buddy и как ими пользоваться. Думаю, вы найдете что-то полезное именно для вас.

Подробнее..

Hibernate и Spring Boot кто отвечает за имена таблиц?

28.04.2021 16:12:49 | Автор: admin

Когда мы добавляем зависимость в проект, мы подписываем контракт. Зачастую, многие условия в нем написаны мелким шрифтом. В этой статье мы рассмотрим кое-что, что легко пропустить при подписании трехстороннего контракта между вами, Hibernate и Spring Boot. Речь пойдет о стратегиях именования.

Значения по умолчанию в JPA

Главное правило для значений по умолчанию: они должны быть интуитивно понятными. Давайте проверим, следует ли этому правилу обычное приложение на Spring Boot с конфигурацией по умолчанию, Hibernate в качестве реализации JPA и PostgreSQL в качестве БД. Допустим, у нас есть сущность PetType. Давайте угадаем, как будет называться ее таблица в базе данных.

Первый пример:

@Entitypublic class PetType {    // fields omitted}

Я бы предположил, что именем таблицы станет имя класса, то есть PetType. Однако, после запуска приложения оказалось, что имя таблицы на самом деле pet_type.

Явно зададим имя с помощью @Table:

@Entity@Table(name = "PetType")public class PetType {    // fields omitted}

В этот раз имя точно должно быть PetType. Запустим приложение Таблица снова называется pet_type!

Тогда попробуем заключить название таблицы в кавычки. Таким образом, должно сохраниться не только имя, но и регистр:

@Entity@Table(name = "\"PetType\"")public class PetType {  // fields omitted}

И опять наши ожидания не оправдались, имя снова "pet_type", но теперь в кавычках!

Стратегии именования Hibernate

На запрос имя таблицы поумолчанию для JPA-сущностей Google выдает следующий результат (англ.):

По умолчанию, имя таблицы в JPA это имя класса (без пакета) с заглавной буквы. Каждый атрибут класса хранится в столбце таблицы.

Именно это мы ожидали увидеть в первом примере, не так ли? Очевидно, что-то нарушает стандарт.

Углубимся в Hibernate. Согласно документации (англ.), в Hibernate есть два интерфейса, отвечающих за именование таблиц, столбцов и пр.: ImplicitNamingStrategy и PhysicalNamingStrategy.

ImplicitNamingStrategy отвечает за генерацию имен для всех объектов, которые не были явно названы разработчиком: имя сущности, имя таблицы, имя столбца, индекса, внешнего ключа и т.д.. Получившееся имя называется логическим, оно используется внутри Hibernate для идентификации объекта. Это не то имя, которое будет использовано в базе данных.

PhysicalNamingStrategy создает подлинное физическое имя на основе логического имени объекта JPA. Именно физическое имя используется в базе данных. Фактически, это означает, что с помощью Hibernate нельзя напрямую указать физическое имя объекта в БД, можно указать только логическое. Чтобы лучше понять, как это все работает, посмотрите на схему ниже.

По умолчанию, в Hibernate используются следующие реализации этих интерфейсов: ImplicitNamingStrategyJpaCompliantImpl и PhysicalNamingStrategyStandardImpl. Первый генерирует логические имена в соответствии со спецификацией JPA, а второй использует их как физические имена без каких-либо изменений. Лучше всего это описано в документации:

JPA определяет четкие правила автоматического именования. Если вам важна независимость от конкретной реализации JPA, или если вы хотите придерживаться определенных в JPA правил именования, используйте ImplicitNamingStrategyJpaCompliantImpl (стратегия по умолчанию). Кроме того, в JPA нет разделения между логическим и физическим именем. Согласно спецификации JPA, логическое имя это и есть физическое имя. Если вам важна независимость от реализации JPA, не переопределяйте PhysicalNamingStrategy.

Однако наше приложение ведет себя по-другому. И вот почему. Spring Boot переопределяет реализации Hibernate по умолчанию для обоих интерфейсов и вместо них использует SpringImplicitNamingStrategy и SpringPhysicalNamingStrategy.

SpringImplicitNamingStrategy фактически копирует поведение ImplicitNamingStrategyJpaCompliantImpl, есть только незначительное различие в именовании join таблиц. Значит, дело в SpringPhysicalNamingStrategy. В документации (англ.) указано следующее:

По умолчанию, в Spring Boot используется SpringPhysicalNamingStrategy в качестве физической стратегии именования. Эта реализация использует те же правила именования, что и Hibernate 4:
1. Все точки заменяются символами подчеркивания.
2. Заглавные буквы CamelCase приводятся к нижнему регистру, и между ними добавляются символы подчеркивания. Кроме того, все имена таблиц генерируются в нижнем регистре. Например, сущности TelephoneNumber соответствует таблица с именем telephone_number.

По сути, Spring Boot всегда преобразовывает camelCase и PascalCase в snake_case. Более того, использование чего-либо помимо snake_case вообще невозможно. Я бы не стал использовать camelCase или PascalCase для именования объектов базы данных, но иногда у нас нет выбора. Если ваше приложение на Spring Boot работает со сторонней базой данных, где используется PascalCase или camelCase, конфигурация Spring Boot по умолчанию для вас не подойдет. Обязательно убедитесь, что используемая PhysicalNamingStrategy совместима с именами в базе данных.

Получается, Hibernate соответствует спецификации JPA, а Spring Boot нет. Можно подумать, что это ошибка. Однако Spring Boot фреймворк, в котором множество решений уже принято за разработчика, в этом и есть его ценность. Другими словами, он имеет полное право по-своему реализовывать стандарты и спецификации тех технологий, которые он использует. Для разработчика это означает следующее:

  • Конечное поведение всегда определяется конкретной реализацией и может отличаться от спецификации.

  • Если что-то и работает изкоробки, всегда нужно разбираться, что именно оно делает под капотом.

  • Поведение по умолчанию может измениться с обновлением версии библиотеки, что может привести к непредсказуемым побочным эффектам;

Заключение

Магия конфигураций изкоробки это отлично, но может привести к неожиданному поведению. Один из способов избежать подобных проблем явно задавать все значения и не полагаться на автогенерацию. Для именования объектов JPA рекомендации следующие:

  1. Всегда называйте свои объекты JPA явно, чтобы никакая автоматическая стратегия именования не повлияла на ваш код.

  2. Используйте snake_case для имен столбцов, таблиц, индексов и других объектов JPA, чтобы все реализации PhysicalNamingStrategy никак их не преобразовывали.

  3. Если нельзя использовать snake_case (например, при использовании сторонней базы данных), используйте PhysicalNamingStrategyStandardImpl в качестве PhysicalNamingStrategy.

Еще один плюс явного именования объектов JPA вы никогда случайно не переименуете таблицу или атрибут в самой базе при рефакторинге Java-модели. Для этого придется менять имя в @Table или @Column.

Однако, введя эти правила мы просто переложили ответственность на разработчиков. Теперь нам надо следить, чтобы вся команда им следовала. К счастью, этот процесс можно автоматизировать с помощью инструментов разработки.

Если вы пользуетесь IntelliJ IDEA, попробуйте JPA Buddy плагин, который упрощает работу с JPA, Hibernate, Spring Data JPA, Liquibase и подобными технологиями. В настройках JPA Buddy есть специальная секция, в которой можно установить шаблоны для имен сущностей, атрибутов и пр.. Эти шаблоны применяются каждый раз, когда разработчики создают сущность или атрибут:

По задумке, настройки JPA Buddy хранятся в Git, таким образом все разработчики используют одни и те же шаблоны и следуют одним и тем же правилам.

Подробнее..

Как расширить Spring своим типом Repository на примере Infinispan

27.12.2020 16:14:22 | Автор: admin

Зачем об этом писать?

Это моя первая статья, в ней я попытаюсь описать полученный мною практический опыт работы со Spring Repository под капотом фреймворка. Готовых статей про эту тему я в интернете не нашёл ни на русском, ни на английском, были только несколько репозиториев исходников на github, ну и исходники самого Spring. Поэтому и решил, почему бы не написать, вдруг тема написания своих типов репозиториев для Spring для кого-то ещё актуальна.

Программирование для Infinispan я не буду рассматривать подробно, детали реализации всегда можно посмотреть в исходниках, указанных в конце статьи. Основной упор сделан именно на сопряжение механизма Spring Boot Repository и нового типа репозитория.

С чего всё начиналось

В ходе работы на одном из проектов у одного из архитектора возникла идея, что можно написать свои типы репозиториев по аналогии, как это сделано в разных модулях Spring (например, JPARepository, KeyValueRepository, CassandraRepository и т.п.). В качестве пробной реализации решили выбрать работу с данными через Infinispan.

Естественно, что архитекторы - люди занятые, поэтому реализовывать идею поручили Java разработчику, т.е. мне.

Когда я начал прорабатывать тему в интернете, то Google упорно выдавал почти одни статьи про то, как замечательно использовать JPARepository во всех видах на тривиальных примерах. По KeyValueRepository информации было ещё меньше. На StackOverFlow печальные никем не отвеченные вопросы по подобной теме. Делать нечего, пришлось лезть в исходники Spring.

Infinispan

Если говорить кратко про Infinispan, то это всего лишь распределённое хранилище данных в виде ключ-значение, и всё это постоянно висит закэшированное в памяти. Перегружаем Infinispan, данные все обнуляются.

Было решено, что наиболее подходящий кандидат для исследования - KeyValueRepository, как самый близкий к данной области, уже реализованный в Spring. Вся разница только в том, что вместо Infinispan (на его месте мог быть и Hazelcast, например), как хранилища данных, в KeyValueRepository обычный ConcurrentHashMap.

Реализация

Чтобы в Spring проекте подключить возможность пользоваться репозиторием для хранилища ключ-значение пользуются аннотацией EnableMapRepositories.

@SpringBootApplication@EnableMapRepositories("my.person.package.for.entities")public class Application {    public static void main(String[] args) {        SpringApplication.run(Application.class, args);    }}

Можем практически полностью скопировать содержимое кода данной аннотации и создать свою EnableInfinispanRepositories.

Чтобы каждый раз это не писать, скажу, что слово map мы всегда заменяем на infinispan, в аналогичных реализациях, скрытых спойлерами.

Код аннотации EnableInfinispanRepositories
@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)@Documented@Inherited@Import(InfinispanRepositoriesRegistrar.class)public @interface EnableInfinispanRepositories {    String[] value() default {};    String[] basePackages() default {};    Class<?>[] basePackageClasses() default {};    ComponentScan.Filter[] excludeFilters() default {};    ComponentScan.Filter[] includeFilters() default {};    String repositoryImplementationPostfix() default "Impl";    String namedQueriesLocation() default "";    QueryLookupStrategy.Key queryLookupStrategy() default QueryLookupStrategy.Key.CREATE_IF_NOT_FOUND;    Class<?> repositoryFactoryBeanClass() default KeyValueRepositoryFactoryBean.class;    Class<?> repositoryBaseClass() default DefaultRepositoryBaseClass.class;    String keyValueTemplateRef() default "infinispanKeyValueTemplate";    boolean considerNestedRepositories() default false;}

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

@Import(MapRepositoriesRegistrar.class)public @interface EnableMapRepositories {}

Ниже код MapRepositoriesRegistar.

public class MapRepositoriesRegistrar extends RepositoryBeanDefinitionRegistrarSupport {@Overrideprotected Class<? extends Annotation> getAnnotation() {return EnableMapRepositories.class;}@Overrideprotected RepositoryConfigurationExtension getExtension() {return new MapRepositoryConfigurationExtension();}}

В коде перегружаются два метода. В одном связывается Registar со своей активирующей аннотацией, чтобы потом из неё получить заполненные атрибуты конфигурации. В другом находится реализация хранилища данных, специфичных для данного типа репозитория.

Сделаем по аналогии свой InfinispaRepositoriesRegistar.
@NoArgsConstructorpublic class InfinispanRepositoriesRegistrar extends RepositoryBeanDefinitionRegistrarSupport {    @Override    protected Class<? extends Annotation> getAnnotation() {        return EnableInfinispanRepositories.class;    }    @Override    protected RepositoryConfigurationExtension getExtension() {        return new InfinispanRepositoryConfigurationExtension();    }}

Теперь посмотрим, как же выглядит сама реализация хранилища.

public class MapRepositoryConfigurationExtension extends KeyValueRepositoryConfigurationExtension {@Overridepublic String getModuleName() {return "Map";}@Overrideprotected String getModulePrefix() {return "map";}@Overrideprotected String getDefaultKeyValueTemplateRef() {return "mapKeyValueTemplate";}@Overrideprotected AbstractBeanDefinition getDefaultKeyValueTemplateBeanDefinition(RepositoryConfigurationSource configurationSource) {BeanDefinitionBuilder adapterBuilder = BeanDefinitionBuilder.rootBeanDefinition(MapKeyValueAdapter.class);adapterBuilder.addConstructorArgValue(getMapTypeToUse(configurationSource));BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(KeyValueTemplate.class);    ...  }  ...}

В MapKeyValueAdapter будет реализована самая специфическая часть, характерная именно для локального хранения кэша в HashMap. А вот KeyValueTemplate оборачивает методы адаптера довольно общим кодом.

Поэтому чтобы выполнить задачу и заменить хранение данных с локального кэша на распределённое хранилище Infinispan, нужно сделать аналогичный ConfigurationExtension, но заменить на специфичный адаптер, где и будет реализована вся логика чтения/записи/поиска данных, характерная для Infinispan.

Реализация InfinispanRepositoriesConfigurationExtension
@NoArgsConstructorpublic class InfinispanRepositoryConfigurationExtension extends KeyValueRepositoryConfigurationExtension {    @Override    public String getModuleName() {        return "Infinispan";    }    @Override    protected String getModulePrefix() {        return "infinispan";    }    @Override    protected String getDefaultKeyValueTemplateRef() {        return "infinispanKeyValueTemplate";    }    @Override    protected Collection<Class<?>> getIdentifyingTypes() {        return Collections.singleton(InfinispanRepository.class);    }    @Override    protected AbstractBeanDefinition getDefaultKeyValueTemplateBeanDefinition(RepositoryConfigurationSource configurationSource) {        RootBeanDefinition infinispanKeyValueAdapterDefinition = new RootBeanDefinition(InfinispanKeyValueAdapter.class);        RootBeanDefinition keyValueTemplateDefinition = new RootBeanDefinition(KeyValueTemplate.class);        ConstructorArgumentValues constructorArgumentValuesForKeyValueTemplate = new ConstructorArgumentValues();        constructorArgumentValuesForKeyValueTemplate.addGenericArgumentValue(infinispanKeyValueAdapterDefinition);        keyValueTemplateDefinition.setConstructorArgumentValues(constructorArgumentValuesForKeyValueTemplate);        return keyValueTemplateDefinition;    }}

Нужно обязательно в нашем ConfigurationExtension ещё перегрузить метод getIdentifyingTypes(), чтобы в нём сослаться на наш новый тип репозитория (см. реализацию выше).

@NoRepositoryBeanpublic interface InfinispanRepository <T, ID> extends PagingAndSortingRepository<T, ID> {}

Чтобы окончательно всё заработало, нужно сконфигурировать KeyValueTemplate, подсунув ему наш адаптер.

@Configurationpublic class InfinispanConfiguration extends CachingConfigurerSupport {    @Autowired    private ApplicationContext applicationContext;    @Bean    public InfinispanKeyValueAdapter getInfinispanAdapter() {        return new InfinispanKeyValueAdapter(                applicationContext.getBean(CacheManager.class)        );    }    @Bean("infinispanKeyValueTemplate")    public KeyValueTemplate getInfinispanKeyValueTemplate() {        return new KeyValueTemplate(getInfinispanAdapter());    }}

На этом всё.

Можно, конечно, копать глубже и не пользоваться готовыми Spring-овыми реализациями для репозиториев, а наследоваться исключительно от их абстрактных классов и интерфейсов, но объём работ будет намного больше, чем в этой статье.

Резюме

Написав всего 6 своих классов, мы получили новый тип репозитория, который может работать с Infinispan в качестве хранилища данных. И работает этот новый тип репозитория очень похоже на стандартные Spring репозитории.

Полный комплект исходников можно найти на моём github.

Исходники Spring Data KeyValue можно увидеть также на github.

Если у вас есть конструктивные замечания к данной реализации, то пишите в комментариях, либо можете сделать pull request в исходном проекте.

Подробнее..

Валидация данных в Spring Boot

10.01.2021 12:14:24 | Автор: admin

Не редко пользователи пытаются передать в приложение некорректные данные. Это происходит либо из злого умысла, либо по ошибке. Поэтому стоит проверять данные на соответствие бизнес-требованиям.


Эту задачу решает Bean Validation. Он интегрирован со Spring и Spring Boot. Hibernate Validator считается эталонной реализацией Bean Validation.


Основы валидации Bean


Для проверки данных используются аннотации над полями класса. Это декларативный подход, который не загрязняет код.


При передаче размеченного таким образом объекта класса в валидатор, происходит проверка на ограничения.


Настройка


Добавьте следующие зависимости в проект:


<dependency>    <groupId>javax.validation</groupId>    <artifactId>validation-api</artifactId>    <version>2.0.1.Final</version></dependency><dependency>    <groupId>org.hibernate</groupId>    <artifactId>hibernate-validator</artifactId>    <version>7.0.0.Final</version></dependency>

dependencies {    compile group: 'javax.validation', name: 'validation-api', version: '2.0.1.Final'    compile group: 'org.hibernate', name: 'hibernate-validator', version: '7.0.0.Final'}

Валидация в Spring MVC Controller


Сначала данные попадают в контроллер. У входящего HTTP-запроса возможно проверить следующие параметры:


  • тело запроса
  • переменные пути (например, id в /foos/{id})
  • параметры запроса

Рассмотрим каждый из них подробнее.


Валидация тела запроса


Тело запроса POST и PUT обычно содержит данные в формате JSON. Spring автоматически сопоставляет входящий JSON с объектом Java.


Проверяем соответствует ли входящий Java объект нашим требованиям.


class Input {     @Min(1)     @Max(10)     private int numberBetweenOneAndTen;     @Pattern(regexp = "^[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}$")     private String ipAddress;     // ...}

  • Поле numberBetweenOneAndTen должно быть от 1 до 10, включительно.
  • Поле ipAddress должно содержать строку в формате IP-адреса.

Контроллер REST принимает объект Input и выполняет проверку:


@RestControllerclass ValidateRequestBodyController {  @PostMapping("/validateBody")  public ResponseEntity<String> validateBody(@Valid @RequestBody Input input) {    return ResponseEntity.ok("valid");  }}

Достаточно добавить в параметр input аннотацию @Valid, чтобы сообщить спрингу передать объект Валидатору, прежде чем делать с ним что-либо еще.


Если класс содержит поле с другим классом, который тоже необходимо проверить это поле необходимо пометить аннотацией Valid.


Исключение MethodArgumentNotValidException выбрасывается, когда объект не проходит проверку. По умолчанию, Spring переведет это исключение в HTTP статус 400.


Проверка переменных пути и параметров запроса


Проверка переменных пути и параметров запроса работает по-другому.


Не проверяются сложные Java-объекты, так как path-переменные и параметры запроса являются примитивными типами, такими как int, или их аналогами: Integer или String.


Вместо аннотации поля класса, как описано выше, добавляют аннотацию ограничения (в данном случае @Min) непосредственно к параметру метода в контроллере Spring:


@Validated@RestControllerclass ValidateParametersController {  @GetMapping("/validatePathVariable/{id}")  ResponseEntity<String> validatePathVariable(      @PathVariable("id") @Min(5) int id  ) {    return ResponseEntity.ok("valid");  }  @GetMapping("/validateRequestParameter")  ResponseEntity<String> validateRequestParameter(      @RequestParam("param") @Min(5) int param  ) {     return ResponseEntity.ok("valid");  }}

Обратите внимание, что необходимо добавить @Validated Spring в контроллер на уровне класса, чтобы сказать Spring проверять ограничения на параметрах метода.


В этом случае аннотация @Validated устанавливается на уровне класса, даже если она присутствует на методах.


В отличии валидации тела запроса, при неудачной проверки параметра вместо метода MethodArgumentNotValidException будет выброшен ConstraintViolationException. По умолчанию последует ответ со статусом HTTP 500 (Internal Server Error), так как Spring не регистрирует обработчик для этого исключения.


Вернем HTTP статус 400, так как клиент предоставил недействительный параметр. Для этого добавляем пользовательский обработчик исключений в контоллер:


@RestController@Validatedclass ValidateParametersController {  // request mapping method omitted  @ExceptionHandler(ConstraintViolationException.class)  @ResponseStatus(HttpStatus.BAD_REQUEST)  public ResponseEntity<String> handleConstraintViolationException(ConstraintViolationException e) {    return new ResponseEntity<>("not valid due to validation error: " + e.getMessage(), HttpStatus.BAD_REQUEST);  }}

Позже рассмотрим, как вернуть структурированный ответ об ошибке, содержащий подробности обо всех неудачных подтверждениях для проверки клиентом.


Валидация в сервисном слое


Можно проверять данные на любых компонентах Spring. Для этого используется комбинация аннотаций @Validated и @Valid.


@Service@Validatedclass ValidatingService{    void validateInput(@Valid Input input){      // do something    }}

Аннотация @Validated устанавливается только на уровне класса, так что не ставьте ее на метод в данном случае.


Валидация сущностей JPA


Persistence Layer это последняя линия проверки данных. По умолчанию Spring Data использует Hibernate, который поддерживает Bean Validation из коробки.


Обычно мы не хотим делать проверку так поздно, поскольку это означает, что бизнес-код работал с потенциально невалидными объектами, что может привести к непредвиденным ошибкам.


Допустим, необходимо хранить объекты нашего класса Input в базе данных. Сначала добавляем нужную JPA аннотацию @Entity, а так же поле id:


@Entitypublic class Input {  @Id  @GeneratedValue  private Long id;  @Min(1)  @Max(10)  private int numberBetweenOneAndTen;  @Pattern(regexp = "^[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}$")  private String ipAddress;  // ...}

Когда репозиторий пытается сохранить невалидный Input, чьи аннотации ограничений нарушаются, выбрасывается ConstraintViolationException.


Bean Validation запускается Hibernate только после того как EntityManager вызовет flush.


Чтобы отключить Bean Validation в репозиториях Spring, достаточно установить свойство Spring Boot spring.jpa.properties.javax.persistence.validation.mode равным null.


Валидация конфигурации приложения


Spring Boot аннотация @ConfigurationProperties используется для связывания свойств из application.properties с Java объектом.


Данные из application необходимы для стабильной работы приложения. Bean Validation поможет обнаружить ошибку в этих данных при старте приложения.


Допустим имеется следующий конфигурационный класс:


@Validated@ConfigurationProperties(prefix="app.properties")class AppProperties {  @NotEmpty  private String name;  @Min(value = 7)  @Max(value = 30)  private Integer reportIntervalInDays;  @Email  private String reportEmailAddress;  // getters and setters}

При попытке запуска с недействительным адресом электронной почты получаем ошибку:


***************************APPLICATION FAILED TO START***************************Description:Binding to target org.springframework.boot.context.properties.bind.BindException:  Failed to bind properties under 'app.properties' to  io.reflectoring.validation.AppProperties failed:    Property: app.properties.reportEmailAddress    Value: manager.analysisapp.com    Reason: must be a well-formed email addressAction:Update your application's configuration

Стандартные ограничения


Библиотека javax.validation имеет множество аннотаций для валидации.


Каждая аннотация имеет следующие поля:


  • message указывает на ключ свойства в ValidationMessages.properties, который используется для отправки сообщения в случае нарушения ограничения.
  • groups позволяет определить, при каких обстоятельствах будет срабатывать эта проверка (о группах проверки поговорим позже).
  • payload позволяет определить полезную нагрузку, которая будет передаваться сс проверкой.
  • @Constraint указывает на реализацию интерфейса ConstraintValidator.

Рассмотрим популярные ограничения.


@NotNull и @Null


@NotNull аннотированный элемент не должен быть null. Принимает любой тип.
@Null аннотированный элемент должен быть null. Принимает любой тип.


@NotBlank и @NotEmpty


@NotBlank аннотированный элемент не должен быть null и должен содержать хотя бы один непробельный символ. Принимает CharSequence.
@NotEmpty аннотированный элемент **не** должен быть null или пустым. Поддерживаемые типы:


  • CharSequence
  • Collection. Оценивается размер коллекции
  • Map. Оценивается размер мапы
  • Array. Оценивается длина массива

@NotBlank применяется только к строкам и проверяет, что строка не пуста и не состоит только из пробелов.


@NotNull применяется к CharSequence, Collection, Map или Array и проверяет, что объект не равен null. Но при этом он может быть пуст.


@NotEmpty применяется к CharSequence, Collection, Map или Array и проверяет, что он не null имеет размер больше 0.


Аннотация @Size(min=6) пропустит строку состоящую из 6 пробелов и/или символов переноса строки, а @NotBlank не пропустит.


@Size


Размер аннотированного элемента должен быть между указанными границами, включая сами границы. null элементы считаются валидными.


Поддерживаемые типы:


  • CharSequence. Оценивается длина последовательности символов
  • Collection. Оценивается размер коллекции
  • Map. Оценивается размер мапы
  • Array. Оценивается длина массива

Добавление пользовательского валидатора


Если имеющихся аннотаций ограничений недостаточно, то создайте новые.


В классе Input использовалось регулярное выражение для проверки того, что строка является IP адресом. Регулярное выражение не является полным: оно позволяет сокеты со значениями больше 255, таким образом "111.111.111.333" будет считаться действительным.


Давайте напишем валидатор, который реализует эту проверку на Java. Потому что как говорится, до решения проблемы регулярным выражением у вас была одна проблема, а теперь стало двe :smile:


Сначала создаем пользовательскую аннотацию @IpAddress:


@Target({ FIELD })@Retention(RUNTIME)@Constraint(validatedBy = IpAddressValidator.class)@Documentedpublic @interface IpAddress {  String message() default "{IpAddress.invalid}";  Class<?>[] groups() default { };  Class<? extends Payload>[] payload() default { };}

Реализация валидатора выглядит следующим образом:


class IpAddressValidator implements ConstraintValidator<IpAddress, String> {  @Override  public boolean isValid(String value, ConstraintValidatorContext context) {    Pattern pattern =       Pattern.compile("^([0-9]{1,3})\\.([0-9]{1,3})\\.([0-9]{1,3})\\.([0-9]{1,3})$");    Matcher matcher = pattern.matcher(value);    try {      if (!matcher.matches()) {        return false;      } else {        for (int i = 1; i <= 4; i++) {          int octet = Integer.valueOf(matcher.group(i));          if (octet > 255) {            return false;          }        }        return true;      }    } catch (Exception e) {      return false;    }  }}

Теперь можно использовать аннотацию @IpAddress, как и любую другую аннотацию ограничения.


class InputWithCustomValidator {  @IpAddress  private String ipAddress;  // ...}

Принудительный вызов валидации


Для принудительного вызова проверки, без использования Spring Boot, создайте валидатор вручную.


class ProgrammaticallyValidatingService {  void validateInput(Input input) {    ValidatorFactory factory = Validation.buildDefaultValidatorFactory();    Validator validator = factory.getValidator();    Set<ConstraintViolation<Input>> violations = validator.validate(input);    if (!violations.isEmpty()) {      throw new ConstraintViolationException(violations);    }  }}

Тем не менее, Spring Boot предоставляет предварительно сконфигурированный экземпляр валидатора. Внедрив этот экземпляр в сервис не придется создавать его вручную.


@Serviceclass ProgrammaticallyValidatingService {  private Validator validator;  public ProgrammaticallyValidatingService(Validator validator) {    this.validator = validator;  }  public void validateInputWithInjectedValidator(Input input) {    Set<ConstraintViolation<Input>> violations = validator.validate(input);    if (!violations.isEmpty()) {      throw new ConstraintViolationException(violations);    }  }}

Когда этот сервис внедряется Spring, в конструктор автоматически вставляется экземпляр валидатора.


Группы валидаций {#validation-groups}


Некоторые объекты участвуют в разных вариантах использования.


Возьмем типичные операции CRUD: при обновлении и создании, скорее всего, будет использоваться один и тот же класс. Тем не менее, некоторые валидации должны срабатывать при различных обстоятельствах:


  • только перед созданием
  • только перед обновлением
  • или в обоих случаях

Функция Bean Validation, которая позволяет нам внедрять такие правила проверки, называется "Validation Groups".


Все аннотации ограничений должны иметь поле groups. Это поле быть использовано для передачи любых классов, каждый из которых определяет группу проверки.


Для нашего примера CRUD определим два маркерных интерфейса OnCreate и OnUpdate:


interface OnCreate {}interface OnUpdate {}

Затем используем эти интерфейсы с любой аннотацией ограничения:


class InputWithGroups {  @Null(groups = OnCreate.class)  @NotNull(groups = OnUpdate.class)  private Long id;  // ...}

Это позволит убедиться, что id пуст при создании и заполнен при обновлении.


{{< admonition type=warning title="" open=true >}}
Spring поддерживает группы проверки только с аннотацией @Validated
{{< /admonition >}}


@Service@Validatedclass ValidatingServiceWithGroups {    @Validated(OnCreate.class)    void validateForCreate(@Valid InputWithGroups input){      // do something    }    @Validated(OnUpdate.class)    void validateForUpdate(@Valid InputWithGroups input){      // do something    }}

Обратите внимание, что аннотация @Validated применяется ко всему классу. Чтобы определить, какая группа проверки активна, она также применяется на уровне метода.


Использование групп проверки может легко стать анти-паттерном. При использовании групп валидации сущность должна знать правила валидации для всех случаев использования (групп), в которых она используется.


Возвращение структурных ответов на ошибки


Когда проверка не удается, лучше вернуть клиенту понятное сообщение об ошибке. Для этого необходимо вернуть структуру данных с сообщением об ошибке для каждой проверки, которая не прошла валидацию.


Сначала нужно определить эту структуру данных. Назовем ее ValidationErrorResponse и она содержит список объектов Violation:


public class ValidationErrorResponse {  private List<Violation> violations = new ArrayList<>();  // ...}public class Violation {  private final String fieldName;  private final String message;  // ...}

Затем создадим глобальный ControllerAdvice, который обрабатывает все ConstraintViolationExventions, которые пробрасываются до уровня контроллера. Чтобы отлавливать ошибки валидации и для тел запросов, мы также будем работать с MethodArgumentNotValidExceptions:


@ControllerAdviceclass ErrorHandlingControllerAdvice {  @ExceptionHandler(ConstraintViolationException.class)  @ResponseStatus(HttpStatus.BAD_REQUEST)  @ResponseBody  ValidationErrorResponse onConstraintValidationException(      ConstraintViolationException e) {    ValidationErrorResponse error = new ValidationErrorResponse();    for (ConstraintViolation violation : e.getConstraintViolations()) {      error.getViolations().add(        new Violation(violation.getPropertyPath().toString(), violation.getMessage()));    }    return error;  }  @ExceptionHandler(MethodArgumentNotValidException.class)  @ResponseStatus(HttpStatus.BAD_REQUEST)  @ResponseBody  ValidationErrorResponse onMethodArgumentNotValidException(      MethodArgumentNotValidException e) {    ValidationErrorResponse error = new ValidationErrorResponse();    for (FieldError fieldError : e.getBindingResult().getFieldErrors()) {      error.getViolations().add(        new Violation(fieldError.getField(), fieldError.getDefaultMessage()));    }    return error;  }}

Здесь информацию о нарушениях из исключений переводится в нашу структуру данных ValidationErrorResponse.


Обратите внимание на аннотацию @ControllerAdvice, которая делает методы обработки исключений глобально доступными для всех контроллеров в контексте приложения.

Подробнее..
Категории: Java , Spring , Validation , Spring boot , Spring framework

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru