В этой статье рассматривается как работает аннотация @Transactional
в Spring под капотом. При этом в основном рассматривается НЕ реактивный стек. Статья рассматривает код Spring 5.3 и Spring Boot 2.4
Оглавление
Где лежит
@Transactional
и как его добавить в проект?Кто создает инфраструктуру для обработки
@Transactional
?Кто обрабатывает
@Transactional
?Кратко о том как работает proxy или самый популярный вопрос на собеседовании
Как обрабатывается
@Transactional
?TransactionalManager
- что это такое?Императивная работа с транзакциями (через TransactionTemplate)
Где лежит @Transactional и как его добавить в проект?
Аннотация @Transactional
находится в пакете org.springframework.transaction.annotation
, т.е. ее полное имя org.springframework.transaction.annotation.Transactional
и является частью Spring Framework с версии 1.2. Не путайте с javax.transaction.Transactional
. Собственная аннотация Spring предоставляет более расширенные возможности настройки, которые рассматриваются дальше. Аннотацию javax.transaction.Transactional
Spring также поддерживает, но лучше их не смешивать и, если создаете приложение на Spring, использовать родные аннотации.
Для того, чтобы добавить в проект пакет, требуется прописать зависимость:
для maven
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
</dependency>
для gradle
compile group: 'org.springframework', name: 'spring-tx', version: '5.3.2'
Но с приходом Spring Boot так делать уже не требуется - этот пакет добавлен во все требуемые starter-ы как транзитивная зависимость и, когда вы добавляете, например, spring-boot-starter-data-jpa, то spring-tx подтягивается автоматически через spring-data-jpa
Кто создает инфраструктуру для обработки @Transactional?
В те темные времена, когда еще не было Spring Boot, чтобы включить поддержку @Transactional
мы добавляли над конфигурацией аннотацию @EnableTransactionManager
, которая является также частью пакета org.springframework.transaction.annotation
.
Эта аннотация имеет следующие настройки:
proxyTargetClass
(по умолчанию, false) - будет ли прокси создаваться через CGLIB (true) или через interface-based proxies (false). Обратите внимание, что, если поставить true, ВСЕ объекты Spring (beans) будут создаваться через CGLIB, не только те, что помечены@Transactional
mode
(по умолчаниюAdviceMode.PROXY
) - как будут применены Advise. Возможные варианты -AdviceMode.PROXY
илиAdviceMode.ASPECTJ
. Если выбрать AspectJ и корректно его настроить, то при компиляции будет сгенерирован код так, что тело метода будет уже обернуто кодом, управляющим транзакцией. Если выбранAdviceMode.PROXY
, то будет использован стандартный механизм создания proxy объектов. (пример с AspectJ)order
- указывает, когда будет применен advice. По умолчанию, LOWEST_PRECEDENCE - т.е. он будет вызван последним в цепочке advice. Это может быть важно, когда вы добавляете собственные advice, в которых есть работа с базой данных
Что же эта аннотация делает? Посмотрим на нее:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(TransactionManagementConfigurationSelector.class)
public @interface EnableTransactionManagement {...}
Она содержит селектор TransactionManagementConfigurationSelector
, работа которого будет рассмотрена дальше.
С приходом Spring Boot необходимость в аннотации@EnableTransactionManager
отпала. Теперь это перешло в ответственность Spring Boot. Как же он это делает?
Когда вы добавляете в проект зависимость spring-boot-starter-что-то, то подтягивается транзитивная зависимость - spring-boot-autoconfigure, который содержит в файле spring.factories список авто-конфигураций. Как это работает - можно посмотреть SpingBoot-потрошитель от Борисова. В этом файле есть упоминание конфигурации
org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration
которая будет загружена при подъеме контекста. Полный текст для удобства приведен ниже.
TransactionAutoConfiguration
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(PlatformTransactionManager.class)
@AutoConfigureAfter({JtaAutoConfiguration.class, HibernateJpaAutoConfiguration.class,
DataSourceTransactionManagerAutoConfiguration.class, Neo4jDataAutoConfiguration.class})
@EnableConfigurationProperties(TransactionProperties.class)
public class TransactionAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public TransactionManagerCustomizers platformTransactionManagerCustomizers(
ObjectProvider<PlatformTransactionManagerCustomizer<?>> customizers) {
return new TransactionManagerCustomizers(customizers.orderedStream().collect(Collectors.toList()));
}
@Bean
@ConditionalOnMissingBean
@ConditionalOnSingleCandidate(ReactiveTransactionManager.class)
public TransactionalOperator transactionalOperator(ReactiveTransactionManager transactionManager) {
return TransactionalOperator.create(transactionManager);
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnSingleCandidate(PlatformTransactionManager.class)
public static class TransactionTemplateConfiguration {
@Bean
@ConditionalOnMissingBean(TransactionOperations.class)
public TransactionTemplate transactionTemplate(PlatformTransactionManager transactionManager) {
return new TransactionTemplate(transactionManager);
}
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnBean(TransactionManager.class)
@ConditionalOnMissingBean(AbstractTransactionManagementConfiguration.class)
public static class EnableTransactionManagementConfiguration {
@Configuration(proxyBeanMethods = false)
@EnableTransactionManagement(proxyTargetClass = false)
@ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "false",
matchIfMissing = false)
public static class JdkDynamicAutoProxyConfiguration {
}
@Configuration(proxyBeanMethods = false)
@EnableTransactionManagement(proxyTargetClass = true)
@ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "true",
matchIfMissing = true)
public static class CglibAutoProxyConfiguration {
}
}
}
Как работают авто-конфигурации хорошо объяснено в "SpringBoot-потрошитель", здесь упомяну только наиболее интересные моменты.
Данная авто-конфигурация будет работать только, если в classpath есть класс PlatformTransactionManager
, о работе которого будет написано дальше. Здесь же создается TransactionalOperator, используемый в реактивном стеке.
Наиболее важной частью является статический класс EnableTransactionManagementConfiguration
Он содержит два подкласса, отличаются они только настройкой
spring.aop.proxy-target-class
Если spring.aop.proxy-target-class = true
, то применяется аннотация @EnableTransactionManagement(proxyTargetClass = true)
Если spring.aop.proxy-target-class = false
, то применяется аннотация EnableTransactionManagement(proxyTargetClass = false)
Обратите внимание, что по умолчанию для всех современных spring-boot проектов
spring.aop.proxy-target-class=true
Поэтому будет использован механизм CGLIB для создания proxy. (Советую посмотреть интервью, где Борисов рассказывает про это изменение поведения Spring Boot).
Кто обрабатывает @Transactional?
В этой части рассмотрим, какая инфраструктура создается для обработки .
В прошлой части мы закончили на
@Import(TransactionManagementConfigurationSelector.class)
Посмотрим, что происходит дальше.
Здесь используется аннотация @Import.
Она обычно используется для обработки следующих трех типов компонентов (component): @Configuration
, ImportSelector
, ImportBeanDefinitionRegistrar
.
TransactionManagementConfigurationSelector
, который здесь загружается, принадлежит к ImportSelector
. При загрузке контекста он, основываясь на настройках EnableTransactionManagement
, определяет какие классы будут подгружаться дальше. Ниже приведен часть кода TransactionManagementConfigurationSelector
TransactionManagementConfigurationSelector.
public class TransactionManagementConfigurationSelector
extends AdviceModeImportSelector<EnableTransactionManagement> {
@Override
protected String[] selectImports(AdviceMode adviceMode) {
switch (adviceMode) {
case PROXY:
return new String[]{AutoProxyRegistrar.class.getName(),
ProxyTransactionManagementConfiguration.class.getName()};
case ASPECTJ:
return new String[]{determineTransactionAspectClass()};
default:
return null;
}
}
.....
Здесь используются следующие классы для конфигураций:
AutoProxyRegistrar
- класс для регистрации средств создания бинов, ProxyTransactionManagementConfiguration
- класс для настройки ProxyTransactionManagement
.
Посмотрим, что внутри ProxyTransactionManagementConfiguration
. (Для удобство код приведен ниже. Хотя форматирование его сильно побило)
ProxyTransactionManagementConfiguration
@Configuration(proxyBeanMethods = false)
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public class ProxyTransactionManagementConfiguration
extends AbstractTransactionManagementConfiguration {
@Bean(name = TransactionManagementConfigUtils.TRANSACTION_ADVISOR_BEAN_NAME)
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public BeanFactoryTransactionAttributeSourceAdvisor transactionAdvisor(
TransactionAttributeSource transactionAttributeSource, TransactionInterceptor transactionInterceptor) {
BeanFactoryTransactionAttributeSourceAdvisor advisor = new BeanFactoryTransactionAttributeSourceAdvisor();
advisor.setTransactionAttributeSource(transactionAttributeSource);
advisor.setAdvice(transactionInterceptor);
if (this.enableTx != null) {
advisor.setOrder(this.enableTx.<Integer>getNumber("order"));
}
return advisor;
}
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public TransactionAttributeSource transactionAttributeSource() {
return new AnnotationTransactionAttributeSource();
}
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public TransactionInterceptor transactionInterceptor(TransactionAttributeSource transactionAttributeSource) {
TransactionInterceptor interceptor = new TransactionInterceptor();
interceptor.setTransactionAttributeSource(transactionAttributeSource);
if (this.txManager != null) {
interceptor.setTransactionManager(this.txManager);
}
return interceptor;
}
}
Первым создается bean transactionAttributeSource из класса AnnotationTransactionAttributeSource
. Он реализует интерфейс TransactionAttributeSource.
public interface TransactionAttributeSource {
default boolean isCandidateClass(Class<?> targetClass) {
return true;
}
@Nullable
TransactionAttribute getTransactionAttribute(Method method,
@Nullable Class<?> targetClass);
}
Этот bean применяется для проверки: используется ли где-то @Transactional
и для получения метаданных (например, propagation) для аннотированных методов или классов.
Дальше создается TransactionInterceptor
, внутри которого и будет происходить магия транзакции, которая рассматривается дальше.
И наконец BeanFactoryTransactionAttributeSourceAdvisor
, внутри которого помещается TransactionInterceptor
.
BeanFactoryTransactionAttributeSourceAdvisor
- это обычный PointcutAdvisor (который объединил в одну сущность advisor and pointcut. Пример использования можно посмотреть здесь или здесь, или в видео "Spring patterns для взрослых".
Если вкратце описать принцип их действия, то pointcut определяет, какие классы и методы в них будут проксированы, а advice - какой код будет выполняться.
В нашем случае в конце концов в качестве pointcut будет использован TransactionAttributeSource
, а в качестве advice - TransactionInterceptor
.
Возникает вопрос - а где всеми любимый BeanPostProcessor
, про которые нам так часто все говорили?
Здесь на себя всю работу берет InfrastructureAdvisorAutoProxyCreator
, который как раз и является BeanPostProcessor
.
Обработка @Transactional
выполняется по обычным правилам Spring и никакой особой магии здесь нет. Примерная схема работы (взята из официальной документации)
Кратко о том как работает proxy или самый популярный вопрос на собеседовании
Во многих фирмах, где проходил собеседование задавали следующий вопрос:
Дан сервис
@Service
public class ServiceTest{
@Transactional
public void test1{
test2();
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void test2(){
}
}
Будет ли при вызове test2 из метода test1 создана новая транзакция?
Самый грамотный и полный ответ, который я встречал, был уже приведен в этой статье на habr. В этой же статье рассмотрим только классический и самый простой случай для лучшего понимания, что происходит дальше.
Под капотом наш bean будет иметь примерно следующий вид
public class Proxy{
private ServiceTest targetService;
public void test1{
//код начала транзакции
// ...
targetService.test1();
//код конца транзакции
// ...
}
public void test2(){
//код начала транзакции
// ...
targetService.test2();
//код конца транзакции
// ...
}
}
Это прекрасно иллюстрирует этот рисунок
И отсюда следует, что при вызове метода test1 код, управляющий транзакциями, вызван не будет и новая транзакция не откроется. Аналогичное поведение - когда вызывается метод родительского класса (реальный случай в одном из проектов).
Как обрабатываются @Transactional
Прежде чем перейти к рассмотрению порядка обработки, давайте посмотрим, какие настройки предоставляет нам эта аннотация. Здесь рассмотрена только часть из них. Остальные можно посмотреть в этой статье на habr или в документации
Наиболее интересные настройка @Transactional
propagation - способ "распространения" транзакций
Выделяется следующие способы:
MANDATORY
- если есть текущая активная транзакция - выполняется в ней, иначе выбрасывается исключение
NESTED
- выполняется внутри вложенной транзакции, если есть активная, если нет активной - то аналогично REQUIRED
NEVER
- выполняется вне транзакции, если есть активная - выбрасывается исключение
NOT_SUPPORTED
- выполняется вне транзакции - если есть активная, она приостанавливается
REQUIRED
- (значение по умолчанию) - если есть активная, то выполняется в ней, если нет, то создается новая транзакция
REQUIRES_NEW
- всегда создается новая транзакция, если есть активная - то она приостанавливается
SUPPORTS
- если есть активная - то выполняется в ней, если нет - то выполняется не транзакционно
Правила управления откатом
noRollbackFor
и noRollbackForClassName
- определяет исключения, при которых транзакция НЕ будет откатана
rollbackFor
и rollbackForClassName
- определяет исключения, при которых транзакция БУДЕТ откатана
Перед тем как перейти к TransactionInterceptor
, давайте вспомним, как мы работали с транзакциями до Spring.
Код взят из статьи
Connection connection = DriverManager.getConnection(...);
try {
connection.setAutoCommit(false);
PreparedStatement firstStatement = connection.prepareStatement(...);
firstStatement.executeUpdate();
PreparedStatement secondStatement = connection.prepareStatement(...);
secondStatement.executeUpdate();
connection.commit();
} catch (Exception e) {
connection.rollback();
}
Порядок работы такой:
создаем соединение - DriverManager.getConnection(...)
выполняем необходимые запросы
если не было ошибок - выполняем commit (connection.commit())
Если все-таки была ошибка - откатываем изменения
Вернемся к TransactionInterceptor
. Основным методом является - invoke
, который делегирует работу родительскому методу invokeWithinTransaction
класса TransactionalAspectSupport
Сокращенный код приведен ниже
protected Object invokeWithinTransaction(Method method,
Class<?> targetClass,
final InvocationCallback invocation)
// получаем TransactionManager tm и TransactionAttribute txAttr
// ...
if (this.reactiveAdapterRegistry != null &&
tm instanceof ReactiveTransactionManager) {
//код для работы с реактивным стэком
// ...
}
PlatformTransactionManager ptm = asPlatformTransactionManager(tm);
final String joinpointIdentification =
methodIdentification(method, targetClass, txAttr);
if (txAttr == null ||
!(ptm instanceof CallbackPreferringPlatformTransactionManager)) {
// начинаем транзакцию, если нужно
TransactionInfo txInfo = createTransactionIfNecessary(ptm, txAttr,
joinpointIdentification);
Object retVal;
try {
// выполняем работу внутри транзакции
retVal = invocation.proceedWithInvocation();
} catch (Throwable ex) {
// откатываемся, если нужно
completeTransactionAfterThrowing(txInfo, ex);
throw ex;
} finally {
// чистим ThreadLocal переменные
cleanupTransactionInfo(txInfo);
}
if (retVal != null && vavrPresent && VavrDelegate.isVavrTry(retVal)) {
//код для библиотеки vavr
// ...
}
// выполняем commit, если не было ошибок
commitTransactionAfterReturning(txInfo);
return retVal;
} else {
// код для WebSphere
// ...
}
}
Если посмотреть внимательно, то этот код повторяет предыдущую логику. Ничего нового:
получаем соединение/транзакцию - createTransactionIfNecessary
выполняем необходимые запросы - invocation.proceedWithInvocation
если не было ошибок - выполняем commitTransactionAfterReturning
Если все-таки была ошибка - откатываем изменения - completeTransactionAfterThrowing
Рассмотрим внимательно каждую часть.
Получение транзакции
protected TransactionInfo createTransactionIfNecessary
(PlatformTransactionManager tm, TransactionAttribute txAttr,
final String joinpointIdentification) {
if (txAttr != null && txAttr.getName() == null) {
txAttr = new DelegatingTransactionAttribute(txAttr) {
@Override
public String getName() {
return joinpointIdentification;
}
};
}
TransactionStatus status = null;
if (txAttr != null) {
if (tm != null) {
status = tm.getTransaction(txAttr);
}
}
return prepareTransactionInfo(tm, txAttr, joinpointIdentification, status);
}
Из важных частей здесь можно отметить только вызов transactionalManager.getTransaction(...)
(работа с TransactionalManager
будет описана позже) и вызов prepareTransactionInfo
, в котором выполняется txInfo.bindToThread()
; этот метод устанавливает TransactionInfo
в ThreadLocal
переменную transactionInfoHolder
, из которой теперь всегда можно получить статус транзакции через статический метод currentTransactionStatus
Управление откатом изменений
protected void completeTransactionAfterThrowing(@Nullable TransactionInfo txInfo, Throwable ex) {
if (txInfo != null && txInfo.getTransactionStatus() != null) {
if (txInfo.transactionAttribute != null &&
txInfo.transactionAttribute.rollbackOn(ex)) {
try {
txInfo.getTransactionManager()
.rollback(txInfo.getTransactionStatus());
} catch (Exception ex) {
// ...
}
} else {
try {
txInfo.getTransactionManager()
.commit(txInfo.getTransactionStatus());
} catch (Exception ex) {
// ...
}
}
}
}
Если транзакция активна, то проверяем, надо ли ее откатывать при этой ошибке, если не надо, то выполняем фиксацию транзакции commit(txInfo.getTransactionStatus())
Проверка исключений выполняется в txInfo.transactionAttribute.rollbackOn(ex)
. Выигрывает наиболее близкое по иерархии исключений требование.
Фиксация транзакции
protected void commitTransactionAfterReturning(TransactionInfo txInfo) {
if (txInfo != null &&
txInfo.getTransactionStatus() != null) {
txInfo.getTransactionManager().commit(txInfo.getTransactionStatus());
}
}
Просто выполняется фиксация, если транзакция активна.
TransactionManager - что это такое?
На данный момент TransactionManager
, маркировочный интерфейс, не содержащий никаких методов. Его наследуют ReactiveTransactionManager
и PlatformTransactionManager
Рассмотрим внимательней последний.
public interface PlatformTransactionManager extends TransactionManager {
TransactionStatus getTransaction(TransactionDefinition definition)
throws TransactionException;
void commit(TransactionStatus status) throws TransactionException;
void rollback(TransactionStatus status) throws TransactionException;
}
Интерфейс содержит всего 3 метода - создание транзакции, commit и rollback.
Spring предоставляет абстрактный класс AbstractPlatformTransactionManager
, который реализовывает требования по propagation
. Наследники этого класса по разному реализуют его абстрактные методы, так как это довольно сильно зависит от используемой технологии.
Стоит отметить, что в своей работе AbstractPlatformTransactionManager
и его подклассы активно используют org.springframework.transaction.support.TransactionSynchronizationManage
для синхронизации и хранения метаинформации, включая connection. Хранение информации осуществляется в наборе статических ThreadLocal переменных.
private static final ThreadLocal<Map<Object, Object>> resources =
new NamedThreadLocal<>("Transactional resources");
private static final ThreadLocal<Set<TransactionSynchronization>> synchronizations =
new NamedThreadLocal<>("Transaction synchronizations");
private static final ThreadLocal<String> currentTransactionName =
new NamedThreadLocal<>("Current transaction name");
private static final ThreadLocal<Boolean> currentTransactionReadOnly =
new NamedThreadLocal<>("Current transaction read-only status");
private static final ThreadLocal<Integer> currentTransactionIsolationLevel =
new NamedThreadLocal<>("Current transaction isolation level");
private static final ThreadLocal<Boolean> actualTransactionActive =
new NamedThreadLocal<>("Actual transaction active");
Как выводить логи транзакций?
Spring выполняет логирование практически всех действий с транзакциями: для этого надо включить уровень логирования DEBUG для пакета org.springframework.transaction
, и если используете hibernate, - org.hibernate.transaction
logging:
level:
org.springframework.orm.jpa: DEBUG
org.springframework.transaction: DEBUG
Также можно узнать, что происходит с транзакциями программно, обращаясь к ThreadLocal переменным, которые мы рассмотрели, например,
TransactionSynchronizationManager.isActualTransactionActive();
TransactionAspectSupport.currentTransactionStatus();
Императивная работа с транзакциями (через TransactionTemplate)
Кроме управления транзакциями через аннотации, Spring предоставляет возможность императивного управления транзакциями. Для этого Spring, как и во многих других случаях, использует шаблон "Template" (JdbcTemplate, RestTemplate). Используется класс TransactionTemplate
. Bean продекларирован в TransactionAutoConfiguration
. Основным методом является метод execute
public <T> T execute(TransactionCallback<T> action) throws TransactionException {
if (this.transactionManager instanceof CallbackPreferringPlatformTransactionManager) {
return ((CallbackPreferringPlatformTransactionManager) this.transactionManager).execute(this, action);
} else {
TransactionStatus status = this.transactionManager.getTransaction(this);
T result;
try {
result = action.doInTransaction(status);
} catch (RuntimeException | Error ex) {
rollbackOnException(status, ex);
throw ex;
} catch (Throwable ex) {
rollbackOnException(status, ex);
throw new UndeclaredThrowableException(ex, "TransactionCallback threw undeclared checked exception");
}
this.transactionManager.commit(status);
return result;
}
}
который повторяет стандартный механизм rollback/commit:
получаем транзакцию (getTransaction)
выполняем действие (doInTransaction)
если была ошибка, откатываемся (rollbackOnException)
если все хорошо, то фиксируем транзакцию (commit)
Интересна строчка начала транзакции
this.transactionManager.getTransaction(this);
Так как transactionManager
является одновременно и TransactionDefinition
(который мы настраиваем перед вызовом execute), то он передает самого себя в transactionManager
, поэтому для транзакции используются те параметры, которые были переданы в transactionManager
.
Так же можно заметить, что в метод выполнения передается TrsansactionStatus
, поэтому во время выполнения вы можете вручную указать, когда нужно откатывать, не пробрасывая исключение
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
protected void doInTransactionWithoutResult(TransactionStatus status) {
try {
updateOperation1();
updateOperation2();
} catch (SomeBusinessExeption ex) {
status.setRollbackOnly();
}
}
});
Обработка ошибок в HibernateTransactionManager
При работе с hibernate, даже если вы поймали ошибку и обработали ее, вы не сможете уже зафиксировать транзакцию, так как она будет помечаться как rollbackOnly (в отличие от работы с JdbcTemplate, например). Как это работает и почему?
Начнем с почему? - При работе с JPA вы не управляете последовательностью выполнения запросов и поэтому, если у вас произошла ошибка, hibernate не может восстановить правильный контекст и единственное, что ему остается, - пометить транзакцию как rollbackOnly
Как это работает? Если hibernate ловит ошибку, внутри себя он вызывает
TransactionDriverControl().markRollbackOnly()
который ставит флаг - rollbackOnly = true
, даже если вы поймали эту ошибку и обработали. По логике Spring, если нет исключения, то вызывается PlatformTransactionManager.commit
При вызове PlatformTransactionManager.commit
получаем статус транзакции, который внутри хранит флаг:
unexpectedRollback = status.isGlobalRollbackOnly(); //true
doCommit(status); //вызывается все равно
if (unexpectedRollback) { //кидаем исключение
throw new UnexpectedRollbackException(
"Transaction silently rolled back because it has been marked as rollback-only");
}
doCommit все равно вызывается и мы могли бы ожидать, что хоть что-то зафиксируется, но hibernate не имеет теперь консистентных данных, поэтому внутри hibernate есть такой код
@Override
public void commit() {
try {
//хоть метод и называется commit, но включает в себя логику отката
if ( rollbackOnly ) {
try {
rollback();
//...
return;
}
catch (RollbackException e) {
throw e;
}
catch (RuntimeException e) {
throw e;
}
}
JdbcResourceLocalTransactionCoordinatorImpl.this.beforeCompletionCallback();
jdbcResourceTransaction.commit();
// ...
То есть ответственность за откат здесь переходит на сторону Hibernate, а не Spring, хотя Spring и не вызывает PlatformTransactionManager.rollback
Использованные материалы
Раскрытие секретов о Spring Boot от Борисова
Еще один пример использования Pointcut
Spring AOP. Маленький вопросик с собеседования
Kwisatz
Я вот давненько не писал на ява, все порываюсь вернуться, поскольку в текущих проектах мне очень нужен нормальный бекенд для приложений реального времени. Лепить его на питоне или php довольно сомнительная идея. Но Spring меня пугает до жути, во первых своей магией на изучение которой нужно потратить ну совсем неразумное количество времени, да и не так то это просто, во вторых каким то совершенно нереальных оверхедом.
Отсюда вопрос: он стоит того? Не могу придумать почему бы мне не накидать пачку сервисов без какого либо фреймворка.
PrinceKorwin
Победивший дракона сам становится драконом :)
А так посмотрите в сторону Vert.x или Quarkus. Легковестные, асинхронные фреймворки.
Последний умеет в native (GraalVM).
Не скажу, что они очень простые — асинхронная парадигма накладывает свой отпечаток. Но после них использовать спринг у меня нет никакого желания.
Kwisatz
А зачем вообще в яве асинхронность? Для тяжелых IO?
За наводку спасибо я гляну, но последний раз когда смотрел легковесный Spark мне мой отче, который всю жизнь пишет на яве задал очень простой вопрос: «а нафига он вообще нужен» и я знаете ли тоже не увидел, потому как простая маршрутизация на основе структуре проекты + jsp + само приложение и все работает.
PrinceKorwin
Если у вас есть возможность справиться с нагрузкой в синхронном исполнении логики, то так и делайте.
Если же количество запросов велико и вы уже получаете отлупы по таймаутам, то асинхронка может вам помочь.
Также она позволяет экономить на пямяти и на физических ядрах.
Это всё, конечно, если в ваших обработчиках логики есть IO (доступ к БД, сетевые вызовы, работа с файлами и т.д.).
Простой пример. На DigitalOcean базовом тарифе мое простое приложение на SpringBoot 2 даже не могло стартовать пока не включил своп — ядро убивало процесс из-за активного потребления памяти.
Это же приложение на Vert.x стартовало в 20 раз быстрее и потребляло всего 80Мб (против 300Мб спринговых).
Конечно, можно до бесконечности тюнить спринг. Но мне проще просто его не успользовать.
Kwisatz
Дак в том и вопрос, чего такого он делать то позволяет?
Ну серьезно, что у нас обычно есть? Десериализация с гидрацией, затем это пихаем в сервис где тыкаем ACL и прогоняем через проверки бизнес логики, дальеш суем в базу, отрабатываем от нее ответ и наружу, все.
По сути самое сложное тут как раз сериализация/де и обогащение объектов.
snuk182
В спринге куча архитектурного легаси еще с времен, когда альтернатив не существовало, и в спринг пихали поддержку вообще всего.
Kwisatz
Не существовало для чего? Я на ява писал всевозможных краулеров и демонов, немного бекенда на JSP (причем задолго до появления спринга), я правда не знаю для чего он жизненно необходим.
snuk182
Буквально для всего — первый релиз был в начале 2000х, когда Java сама по себе (все еще) рождалась в муках, и как раз в это время зарождался веб в его современном виде. Были наколенные поделки разной степени пахучести, J2EE в лице танков вроде WebSphere, на фоне которых спринг просто пушинка. Запрос был в основном со стороны энтерпрайз-заказчиков, которым уже сказали про "стабильную работу под невероятной нагрузкой", но подкрепление сказанного на практике либо сильно отставало от теории, либо жрало ресурсы серверов и деньги на поддержку.
iiwabor
Стоит. Во всех серьезных вакансиях по Java — Spring must have и его знание сильно повышает востребованность на рынке труда и ЗП
Kwisatz
Аргументы?
Не аргумент. Меня интересует практическая сторона.
Тоже не интересно.
snuk182
Спринг сейчас ИМХО спрашивают в двух случаях — если надо что-то спринговое саппортить, и если заказчик больше ничего не знает. Если же таких ограничений нет — те же Vert.x + Dagger будут сильно проще в старте, поддержке и нагрузке на рантайм.
Kwisatz
А вот это уже аргумент. *записывает еще одно умное слово в блокнотик*
Будем почитать. Пробежался пока глазами по доке, пока не понял что сие за зверь)
У мну ситуевина примерно следующая: приложение реального времени, jsonRPC / REST, PostgreSQL, http / websockets, клиент с сервером обменивается объектами разной степени наполненности, блокировки на уровне приложения.
snuk182
Все, что касается веба, скармливается в Vertx. БД реализуется отдельно, вместе с необходимыми блокировками — тут можно хоть поверх хибера все реализовать, хоть напрямую в JDBC (это сделано из-за большего, чем раньше, выбора хранилищ — SQL, NoSQL, Graph, FS...). Dagger — легковесный DI, чтоб все это хозяйство не болталось вразброд по проекту. Сериалайзеры соответственно берутся по вкусу.
Kwisatz
Hibernate сразу лесом за ее невероятный оверхед и жуткие танцы со сложнгыми типами пг. А на чистом JDBC мэпить запрос на иерархию объектов это боль, уж лучше сразу тогда GraphQL и приходим как раз к вкусу сериалайзеров, искать действительно хороший обычно жуткая боль, есть что посоветовать?
ЗЫ Блокировки все таки на уровне приложения волнуют а не на уровне бд, тут немного другое.
snuk182
Я к тому, что механизм транзакций хранилища не входит в комплект к вертексу (поскольку зачем он там нужен), и либо пишется отдельно, либо берется из ORM, или что там в комплекте к базе рекомендуется.
Для JSON можно взять https://github.com/FasterXML/jackson, по скорости и нетребовательности у меня на проекте претензий нет.
cyber_ua
нет, жизнь без спринга прекрасна, есть такие клевые штуки как jooq, spark + что то для инжекта(например guice) из которых можно сделать замену связки spring + hibernate. Кроме огромного буста к перформансу, на подобной связки куда приятнее писать/дебажить/поддерживать
В любую среднестатистическую галеру
Lure_of_Chaos
На самом деле, не так всё страшно. Во-первых, для разработки проекта средней сложности понадобится очень мало «магических» классов Spring Boot, он сейчас как конструктор — на initializr выбираем, что нам понадобится, и все работает сразу на очень разумных умолчаниях, а что не нравится — то можно конфигить.
Изучать его тоже не нужно «неразумное количество времени», особенно если учесть экономию по сравнению с ручным написанием простейшего бойлерплейта.
Насчет оверхеда: он есть, да, но не «нереальный», поскольку большинство спринговой магии превращается в код на этапе компиляции, и не очень лапшистый, так что и этим можно пренебречь.
Kwisatz
Как раз написание кода обычно занимает не так много времени.
Только hibernate Легко даст вам 100-200% к запросам.
Да но знать что оно внутри делает очень хочу я, а документация прям не понравилась, а гугление сильно осложняют тонны фуфловых howto
littlegreenman
Не уверен, что хоть кто-то бы использовал данный инструмент, если бы были такие показатели. Однозначно есть оверхэд, но не столь критичный, зато в связки со спринг дата, очень даже полезная вещь, и на раннем этапе разработки позволяют практически избавиться от запросов, которые есть в 99% процентов проектов(Crud, пагинация) + есть генерация запросов через сигнатуру метода(и это не магия, просто парсинг название метода согласно чётким правилам). А если просели в производительности, пишите native запросы и будет вам счастье.
Что касается "фуфловых howto", то есть целый блок гайдов с примерами кода(+ссылки на исходники, чтобы самим запустить и поиграться) на многие темы(от "как запустить с бд и рестом" до построения микросервисов с каким-либо брокером сообщений) на официальной страницы фреймворка и мне совсем не понятно, что вам надо ещё, чтобы вам понравилось. Если внутренности интересны, то есть стрим Евгения Борисова(кого-же ещё), где с нуля пишут DI механизм спринга. У Спринг большая история и конечно скопилось кучу гайдов разной степени поршивости. Но в основном, для начального этапа опытному программисту не составим труда написать простое приложение, а уже потом находить интересные механизмы и изучать их более предметно, а бывает, что люди так и остаются на поверхностом знании и чувствуют себя отлично, многие энтерпрайзы так живут и ничего.
Не вижу явного недостатка у спринга в отличии от X и я уверен, что при текущем подходе, вы столкнетесь с проблемами на любом другом фреймворке или технологии, потому что всегда придется копаться, читать кучу полезных и не очень how-to, ломать голову как это реализовано внутри и потратить кучу времени на изучение, чтобы понять, что никакой магии не существует.
Kwisatz
Я очень невзлюбил аннотации явы, вот прям сильно. Особенно после того как наткнулся на аннотацию которая по описанию делала одно, а на самом деле была переписана ручками на совсем другое.
Хорошо подходит для копипаста новичками, плохо для осознания что там вообще происходит.
А вот это интересно, спасибо
Не всегда понятно зачем брать таких монстров, да они хороши для прототипов, но если мы пишем руками SQL запрос или скажем пользуем какойнить hasura.io то пол фреймворка уже отвалилось. А что нам по сути нужно? простейший роутер, сериализатор, пачка сервисов, все. Ну di контейнер в лучшем случае. Упрощенно конечно, но борьба с большим фреймворками — вот это часто проблема.
А технологии… скажем так я убежден что часто их переоценивают. Скажем тот же RabbitMQ сейчас любят сувать везде и по любому поводу, но если задуматься: мы выполняем сложную транзакцию в бд, пишем кучу данных, контроль констистентности и все дела, возможно даже serializable изоляций, а потом мы решаем взять и часть процесса вывести из под транзакции кинув в сервер очередей. Но, простите, почему не поставить маркер в той же бд? Причем на моих глазах кролика убивали не раз с полной потерей данных очереди. Моя не понимать этого
Я вас умоляю, да половина веба работает на кешах. Битрикс вон без кеша вообще шевелится неспособен
littlegreenman
А есть пример? Просто очень интересно, что это за аннотация, которая делает не то, для чего она существует или её документация не соотвествует действительности.
Ну скажем так, по такому корпоративному монстру как Спринг десятки книг и как по мне — хорошая или как минимум неплохая документация. А в ней как раз то, что нужно для осознания того, как это работает. Само собой там не разъясняются все тонкости, ибо зачем нам как клиентам знать детали реализации, нам важен интерфейс, тем не менее исходники никто не закрывал. Те вещи что в гайдах — зачастую давольно простые и спокойно гуглятся, как мне кажется, при желании.
Дальше, я думаю, немного оффтоп. Вы спросили стоит ли изучать такого монстра как Спринг и на аргументы против(монстр, у которого оверхэд больше чем пользы), я постарался привести обратные аргументы. Экосистема Спринга огромная, солидное коммьюнити, он протестирован в бою сотнями больших компаний и не сильно, и изучать его не так страшно, как вам кажется.
Если честно, то меня смутил один факт, что программист, которому что-то интересно, пугается сложности изучить это, и, не изучив, пытается сказать, что на самом деле и изучать не стоит, оперируя домыслами. А были бы у вас факты, значит вы осилили изучить достаточно много, чтобы этого уже и не боятся.
Kwisatz
Ну чужой код я точно сюда не кину)
Не пугается что вы, но стоит ли оно того не может решить это да. У меня еще не на все вопросы есть идеальные ответы, есть чем занятся) А это на ближайшее будущее развлечение, буквально в ближайшие полгода-год надо проект снимать с php и отправлять на нормальный язык уже.
Lure_of_Chaos
Это уже не проблема аннотаций, а документации. Да и всегда можно пройтись дебагом даже в магии аннотаций (которая на самом деле, как правило, всего лишь проксирование), и в исходниках покопаться. А уж в Спринге с этим хорошо дело обстоит — и с документацией, и с качеством кода.
Как раз Спринг не полновесный монстр и его вполне можно есть по очень небольшим частям — беря нужное и не затаскивая ненужное. Плюс, несмотря на обширную кодовую базу, количество кода, выполняемое в рантайме («путь» запроса) не столь велико. Стоит напомнить, что много и «легкого» и «тяжелого» энтерпрайза написано на Спринге и отлично себя чувствует. А если тормозит ORM, есть отличная легковесная обертка над JDBC в лице JdbcTemplate
А точнее пихают их туда, где они являются пушкой по воробьям. Надо четко понимать, где
пушкаузкое место у технологииНе все способно адекватно закешироваться. Универсальный рецепт — оптимизируем не все, а только узкие места.
cyber_ua
попробуйте jooq + hikari разница оч серьезная
semyong
Советую helidon или micronaut