
Началось все с желания посчитать, сколько запросов в БД улетает на каждый rest запрос при использовании spring data jpa + hibernate.
Гугл выдал интересное видео про xrebel, но так же сообщил, что xrebel платный.
Дальнейший поиск привел к статье Counting Queries per Request with Hibernate and Spring
Её и взял за основу для своего счетчика. Какого-то ещё примера не нашел, поэтому решил оставить эту заметку
Первое, что нужно создать — класс счетчик:
package ru.counter.utils.querycounter import org.hibernate.EmptyInterceptor import org.springframework.stereotype.Component @Component class HibernateStatisticsInterceptor : EmptyInterceptor() { private val queryCount = ThreadLocal<Long>() fun startCounter() { queryCount.set(0L) } fun getQueryCount(): Long { return queryCount.get() } fun clearCounter() { queryCount.remove() } override fun onPrepareStatement(sql: String?): String? { val count = queryCount.get() if (count != null) { queryCount.set(count + 1) } return super.onPrepareStatement(sql) } }
Т.к. используется обычный mvc а не асинхронный, ThreadLocal будет достаточно
Далее понадобится фильтр http запросов, который будет обнулять счетчик в начале запроса и выводить данные по завершению:
package ru.counter.utils.querycounter import org.slf4j.Logger import org.slf4j.LoggerFactory import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty import org.springframework.core.Ordered import org.springframework.core.annotation.Order import org.springframework.stereotype.Component import javax.servlet.FilterChain import javax.servlet.http.HttpFilter import javax.servlet.http.HttpServletRequest import javax.servlet.http.HttpServletResponse @Component @ConditionalOnProperty(name = ["sql.query.count.per.request.log.enable"], havingValue = true.toString()) @Order(Ordered.HIGHEST_PRECEDENCE) // максимальный приоритет, чтобы фильтр был первым // на случай, если другие фильтры тоже кидают запросы в БД class RequestStatisticsSqlQueryCountFilter( private val statisticsInterceptor: HibernateStatisticsInterceptor ) : HttpFilter() { private val log: Logger = LoggerFactory.getLogger(javaClass) init { log.warn( "http фильтр для подсчета количества sql запросов на 1 http запрос включен," + " негативно для производительности" ) } private val time = ThreadLocal<Long>() override fun doFilter(request: HttpServletRequest?, response: HttpServletResponse?, chain: FilterChain?) { if (request?.requestURI?.startsWith("/api") != true) { // мне были важны только запросы к api chain?.doFilter(request, response) } else { time.set(System.currentTimeMillis()) statisticsInterceptor.startCounter() chain?.doFilter(request, response) val duration = System.currentTimeMillis() - time.get() val queryCount: Long = statisticsInterceptor.getQueryCount() log.info("[Time: {} ms] [Queries: {}] {} {}", duration, queryCount, request.method, request.requestURI) statisticsInterceptor.clearCounter() time.remove() } } }
Важное тут:
-
ConditionalOnProperty, считать количество запросов в проме мне не надо, это информация нужна только во время разработки, поэтому добавлена возможность управлять наличием этого фильтра через spring property
-
На всякий случай вывод в консоль при создании бина о, том что он может негативно влиять на производительность
Третий элемент это регистрация перехватчика запросов для hibernate, который и будет инкрементировать счетчик:
package ru.counter.configuration import org.slf4j.LoggerFactory import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Value import org.springframework.boot.autoconfigure.orm.jpa.HibernatePropertiesCustomizer import org.springframework.context.annotation.Configuration import ru.counter.utils.querycounter.HibernateStatisticsInterceptor @Configuration class HibernateInterceptorRegistration : HibernatePropertiesCustomizer { private val log = LoggerFactory.getLogger(javaClass) @Value("\${sql.query.count.per.request.log.enable:false}") private var statisticsInterceptorEnabled: Boolean = false @Autowired private lateinit var statisticsInterceptor: HibernateStatisticsInterceptor override fun customize(hibernateProperties: MutableMap<String, Any>) { if (statisticsInterceptorEnabled) { log.warn("Счетчик sql запросов на 1 http запрос включен, негативно для производительности") hibernateProperties["hibernate.session_factory.interceptor"] = statisticsInterceptor } } }
-
Здесь так же он регистрируется, только если при запуске sql.query.count.per.request.log.enable указана и имеет значение true, наверно можно переделать так же под ConditionalOnProperty
-
И точно так же вывод предупреждения в лог.
В итоге запустив приложение со счетчиком можно увидеть данные в логе
INFO RequestStatisticsSqlQueryCountFilter : [Time: 154 ms] [Queries: 18] GET /api/private/v1/auth/check INFO RequestStatisticsSqlQueryCountFilter : [Time: 149 ms] [Queries: 4] GET /api/private/v1/organisation/employee INFO RequestStatisticsSqlQueryCountFilter : [Time: 0 ms] [Queries: 0] GET /api/private/v1/auth/check INFO RequestStatisticsSqlQueryCountFilter : [Time: 62 ms] [Queries: 16] GET /api/private/v1/organisation INFO RequestStatisticsSqlQueryCountFilter : [Time: 30 ms] [Queries: 1] GET /api/public/v1/image/1001 INFO RequestStatisticsSqlQueryCountFilter : [Time: 30 ms] [Queries: 1] GET /api/public/v1/image/1000
Spring data jpa дает очень удобные интерфейс для работы с БД, но одна ошибка, может стоит 100 или 1000 запросов. Главное, что jpa дает, это скорость.
Возможность быстро сделать прототип и потом его немного допилить, когда интеграции уже зафиксированы и каждая сторона отлаживает и улучшает свою часть.
Про импорты
В коде специально оставил все импорты, некоторые моменты могут быть очевидны, но не все.
ссылка на оригинал статьи https://habr.com/ru/post/665266/
Добавить комментарий