Будучи пользователем и энтузиастом Spring Framework в течение многих лет, я столкнулся с несколькими недоразумениями и проблемами с этим стеком. Также есть места, где абстракции ужасно просачиваются, чтобы эффективно и безопасно использовать преимущества всех функций, которые необходимо знать разработчикам. Вот почему я начинаю серию весенних ловушек . В первой части мы подробнее рассмотрим, как работает прокси.
Прокси-компонент Bean является важной и одной из наиболее важных функций инфраструктуры, предоставляемых Spring. Это настолько важно и низкоуровнево, что большую часть времени мы даже не осознаем, что оно существует. Однако транзакции, аспектно-ориентированное программирование, расширенные возможности,
поддержка @Async и различные другие варианты использования внутри страны были бы невозможны без него. Так что же такое
проксирование ?
Вот пример: когда вы вводите DAO в сервис, Spring принимает экземпляры DAO и внедряет их напрямую. Вот и все. Однако иногда Spring необходимо знать о каждом вызове, совершаемом службой (и любым другим компонентом) в DAO. Например, если DAO помечен как транзакционный, он должен начать транзакцию перед вызовом и зафиксировать или откатить впоследствии. Конечно, вы можете сделать это вручную, но это утомительно, подвержено ошибкам и смешивает проблемы. Вот почему мы используем декларативные транзакции в первую очередь.
Так как же Spring реализует этот механизм перехвата? Есть три метода от самых простых до самых продвинутых. Я пока не буду обсуждать их преимущества и недостатки, мы скоро увидим их на конкретных примерах.
Динамические прокси Java
Самое простое решение. Если DAO реализует какой-либо интерфейс, Spring создаст динамический прокси Java,
реализующий этот интерфейс (ы), и внедрит его вместо реального класса.
Реальный один до сих пор существует и прокси — сервер имеет отношение к нему, но с внешним миром — прокси — сервер является боб. Теперь каждый раз, когда вы вызываете методы в DAO, Spring может их перехватывать, добавлять магию AOP и вызывать оригинальный метод.
CGLIB генерируемые классы
Недостатком динамических прокси Java является требование к компоненту для реализации хотя бы одного интерфейса. CGLIB обходит это ограничение, динамически создавая подклассы исходного компонента и добавляя логику перехвата напрямую, переопределяя все возможные методы. Думайте об этом как о создании подкласса исходного класса и вызове супер версии среди прочего:
class DAO { def findBy(id: Int) = //... } class DAO$EnhancerByCGLIB extends DAO { override def findBy(id: Int) = { startTransaction try { val result = super.findBy(id) commitTransaction() result } catch { case e => rollbackTransaction() throw e } } }
Тем не менее, этот псевдокод не иллюстрирует, как он работает в реальности — что создает еще одну проблему, следите за обновлениями. Кстати, все примеры будут в Scala, жить с этим и привыкнуть к нему.
AspectJ ткачество
Это наиболее инвазивное, но и самое надежное и интуитивно понятное решение с точки зрения разработчика. В этом режиме перехват применяется непосредственно к байт-коду вашего класса, что означает, что класс, который запускает ваша JVM, отличается от класса, который вы написали. AspectJ weaver добавляет логику перехвата, напрямую изменяя ваш байт-код вашего класса, либо во время ткачества во время
компиляции (
CTW ), либо при загрузке ткачества во время загрузки класса
(
LTW ).
Если вам интересно, как волшебство AspectJ реализовано под капотом, вот декомпилированный и упрощенный файл .class, скомпилированный с предварительно сплетением AspectJ:
public void inInterfaceTransactional() { try { AnnotationTransactionAspect.aspectOf().ajc$before$1$2a73e96c(this, ajc$tjp_2); throwIfNotInTransaction(); } catch(Throwable throwable) { AnnotationTransactionAspect.aspectOf().ajc$afterThrowing$2$2a73e96c(this, throwable); throw throwable; } AnnotationTransactionAspect.aspectOf().ajc$afterReturning$3$2a73e96c(this); }
При переплетении времени загрузки такое же преобразование происходит во время выполнения, когда класс загружается. Как вы можете видеть, здесь нет ничего беспокоящего, фактически именно так вы и запрограммируете транзакции вручную. Примечание: помните ли вы времена, когда вирусы добавляли свой код в исполняемые файлы или динамически внедрялись, когда исполняемый файл загружался операционной системой?
Знание методов прокси важно, чтобы понять, как работает прокси и как он влияет на ваш код. Давайте придерживаться примера демаркации декларативных транзакций, вот наше поле битвы:
trait FooService { def inInterfaceTransactional() def inInterfaceNotTransactional(); } @Service class DefaultFooService extends FooService { private def throwIfNotInTransaction() { assume(TransactionSynchronizationManager.isActualTransactionActive) } def publicNotInInterfaceAndNotTransactional() { inInterfaceTransactional() publicNotInInterfaceButTransactional() privateMethod(); } @Transactional def publicNotInInterfaceButTransactional() { throwIfNotInTransaction() } @Transactional private def privateMethod() { throwIfNotInTransaction() } @Transactional override def inInterfaceTransactional() { throwIfNotInTransaction() } override def inInterfaceNotTransactional() { inInterfaceTransactional() publicNotInInterfaceButTransactional() privateMethod(); } }
Удобный
метод throwIfNotInTransaction () … генерирует исключение, когда не вызывается внутри транзакции. Кто бы мог подумать? Этот метод вызывается из разных мест и разных конфигураций. Если вы внимательно изучите, как вызываются методы — все должно работать. Однако жизнь наших разработчиков имеет тенденцию быть жестокой. Первое препятствие было неожиданным:
ScalaTest не поддерживает интеграционное тестирование Spring
через выделенного раннера. К счастью, это может быть легко перенесено с простой чертой (обрабатывает внедрение зависимостей в тестовые случаи и кеширование контекста приложения):
trait SpringRule extends AbstractSuite { this: Suite => abstract override def run(testName: Option[String], reporter: Reporter, stopper: Stopper, filter: Filter, configMap: Map[String, Any], distributor: Option[Distributor], tracker: Tracker) { new TestContextManager(this.getClass).prepareTestInstance(this) super.run(testName, reporter, stopper, filter, configMap, distributor, tracker) } }
Обратите внимание, что мы не запускаем и не откатываем транзакции, как в
исходной среде тестирования Не только потому, что это помешает нашей демонстрации, но и потому, что я считаю, что транзакционные тесты вредны — но об этом в будущем. Возвращаясь к нашему примеру, вот тест дыма. Полный исходный код можно скачать
здесь из ветки проблем с
прокси . Не жалуйтесь на отсутствие утверждений — здесь мы только проверяем, что исключения не выбрасываются:
@RunWith(classOf[JUnitRunner]) @ContextConfiguration class DefaultFooServiceTest extends FunSuite with ShouldMatchers with SpringRule{ @Resource private val fooService: FooService = null test("calling method from interface should apply transactional aspect") { fooService.inInterfaceTransactional() } test("calling non-transactional method from interface should start transaction for all called methods") { fooService.inInterfaceNotTransactional() } }
Удивительно, но тест не пройден. Ну, если вы читали мои статьи на некоторое время , вы не должны быть удивлены:
Spring AOP загадка и
Spring AOP загадка демистифицированы . На самом деле, справочная документация Spring объясняет это
очень подробно , также посмотрите
этот вопрос SO. Вкратце — нетранзакционный метод вызывает транзакционный, но в
обход транзакционного прокси. Даже если кажется очевидным, что когда inInterfaceNotTransactional () вызывает inInterfaceTransactional (), транзакция должна начинаться — это не так. Утечка абстракции. Кстати, также ознакомьтесь с увлекательными
стратегиями транзакций: статья « Понимание ловушек транзакций» .
Помните наш пример, показывающий, как работает CGLIB? Также зная, как работает полиморфизм, похоже, что использование прокси на основе классов должно помочь.
inInterfaceNotTransactional () теперь вызывает
inInterfaceTransactional (), переопределенный CGLIB / Spring, который, в свою очередь, вызывает исходные классы.
Нет шансов! Это реальная реализация в псевдокоде:
class DAO$EnhancerByCGLIB extends DAO { val target: DAO = ... override def findBy(id: Int) = { startTransaction try { val result = target.findBy(id) commitTransaction() result } catch { case e => rollbackTransaction() throw e } } }
Вместо создания подклассов и создания экземпляров подклассированного компонента Spring сначала создает исходный компонент, а затем создает подкласс, который оборачивает исходный компонент (в некоторой степени
образец Decorator ) в один из постпроцессоров. Это означает, что, опять же, вызов self внутри bean-компонента обходит AOP-прокси вокруг нашего класса. Конечно, использование CGLIB меняет поведение бина несколькими другими способами. Например, теперь мы можем внедрить конкретный класс, а не интерфейс, фактически интерфейс даже не нужен, и в этих обстоятельствах требуется прокси CGLIB. Есть и недостатки — инжекция в конструктор больше невозможна, см.
SPR-3150 , это
позор . Так что насчет более тщательных тестов?
@RunWith(classOf[JUnitRunner]) @ContextConfiguration class DefaultFooServiceTest extends FunSuite with ShouldMatchers with SpringRule { @Resource private val fooService: DefaultFooService = null test("calling method from interface should apply transactional aspect") { fooService.inInterfaceTransactional() } test("calling non-transactional method from interface should start transaction for all called methods") { fooService.inInterfaceNotTransactional() } test("calling transactional method not belonging to interface should start transaction for all called methods") { fooService.publicNotInInterfaceButTransactional() } test("calling non-transactional method not belonging to interface should start transaction for all called methods") { fooService.publicNotInInterfaceAndNotTransactional() } }
Пожалуйста, выберите тесты, которые не пройдут (выберите ровно два). Вы можете объяснить, почему? Опять же здравый смысл подсказывает, что все должно пройти, но это не так. Вы можете поиграть с самим собой, см.
Прокси- ветку на основе классов .
Мы здесь не для того, чтобы разоблачать проблемы, но чтобы их преодолеть. К сожалению, наш запутанный класс обслуживания может быть исправлен только с использованием тяжелой артиллерии — истинного ткачества AspectJ. Плетение как во время компиляции, так и во время загрузки проходит тест. Смотрите соответственно
ветки aspectj-ctw и
aspectj-ltw .
Теперь вы должны задать себе несколько вопросов.
Какой подход я должен выбрать (или:
мне действительно нужно использовать AspectJ? ) И
почему я должен даже беспокоиться? — среди других. Я бы сказал — в большинстве случаев достаточно простого проксирования Spring. Но вы обязательно должны знать, как работает распространение, а когда — нет. Иначе плохие вещи случаются. Фиксирует и выполняет откат в неожиданных местах, охватывая неожиданный объем данных, не работает грязная проверка ORM
, невидимые записи — поверьте, это происходит на пустом месте. И помните, что темы, которые мы рассмотрели здесь, относятся ко всем аспектам АОП, а не только к транзакциям.
С http://nurkiewicz.blogspot.com/2011/10/spring-pitfalls-proxying.html