Статьи

Весенние ловушки: проксирование

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