Будучи пользователем и энтузиастом 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