Статьи

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

Будучи пользователем и энтузиастом Spring Framework в течение многих лет, я столкнулся с несколькими недоразумениями и проблемами с этим стеком. Также есть места, где абстракции ужасно просачиваются, чтобы эффективно и безопасно использовать преимущества всех функций, которые необходимо знать разработчикам. Вот почему я начинаю серию весенних ловушек. В первой части мы подробнее рассмотрим, как работает прокси.

Прокси-компонент Bean является важной и одной из наиболее важных функций инфраструктуры, предоставляемых Spring. Это настолько важно и низкоуровнево, что большую часть времени мы даже не осознаем, что оно существует. Однако транзакции, аспектно-ориентированное программирование, расширенные возможности, поддержка @Async и различные другие варианты использования внутри страны были бы невозможны без него. Так что же такое проксирование ?

Вот пример: когда вы вводите DAO в сервис, Spring принимает экземпляры DAO и внедряет их напрямую. Вот и все. Однако иногда Spring необходимо знать о каждом вызове, совершаемом службой (и любым другим компонентом) в DAO. Например, если DAO помечен как транзакционный, он должен начать транзакцию перед вызовом и зафиксировать или откатить впоследствии. Конечно, вы можете сделать это вручную, но это утомительно, подвержено ошибкам и смешивает проблемы. Вот почему мы используем декларативные транзакции в первую очередь.

Так как же Spring реализует этот механизм перехвата? Есть три метода от самых простых до самых продвинутых. Я пока не буду обсуждать их преимущества и недостатки, мы скоро увидим их на конкретных примерах.

Динамические прокси Java

Самое простое решение. Если DAO реализует какой-либо интерфейс, Spring создаст динамический прокси Java, реализующий этот интерфейс (ы), и внедрит его вместо реального класса. Реальный еще существует, и прокси имеет ссылку на него, но на внешний мир — прокси — это боб. Теперь каждый раз, когда вы вызываете методы в DAO, Spring может их перехватывать, добавлять магию AOP и вызывать оригинальный метод.

CGLIB генерируемые классы

Недостатком динамических прокси Java является требование к компоненту для реализации хотя бы одного интерфейса. CGLIB обходит это ограничение, динамически создавая подклассы исходного компонента и добавляя логику перехвата напрямую, переопределяя все возможные методы. Думайте об этом как о создании подкласса исходного класса и вызове супер версии среди прочего:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
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
    }
  }
}

Тем не менее, этот псевдокод не иллюстрирует, как он работает в реальности — что создает еще одну проблему, следите за обновлениями.

AspectJ ткачество

Это наиболее инвазивное, но и самое надежное и интуитивно понятное решение с точки зрения разработчика. В этом режиме перехват применяется непосредственно к байт-коду вашего класса, что означает, что класс, который запускает ваша JVM, отличается от класса, который вы написали. AspectJ weaver добавляет логику перехвата, напрямую изменяя ваш байт-код вашего класса, либо во время ткачества во время компиляции (CTW), либо при загрузке ткачества во время загрузки класса (LTW).

Если вам интересно, как волшебство AspectJ реализовано под капотом, вот декомпилированный и упрощенный файл .class, скомпилированный с предварительно сплетением AspectJ:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
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);
}

При переплетении времени загрузки такое же преобразование происходит во время выполнения, когда класс загружается. Как вы можете видеть, здесь нет ничего беспокоящего, фактически именно так вы и запрограммируете транзакции вручную. Примечание: помните ли вы времена, когда вирусы добавляли свой код в исполняемые файлы или динамически внедрялись, когда исполняемый файл загружался операционной системой?

Знание методов прокси важно, чтобы понять, как работает прокси и как он влияет на ваш код. Давайте придерживаться примера демаркации декларативных транзакций, вот наше поле битвы:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
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 через выделенного раннера. К счастью, это может быть легко перенесено с простой чертой (обрабатывает внедрение зависимостей в тестовые случаи и кеширование контекста приложения):

1
2
3
4
5
6
7
8
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)
  }
  
}

Обратите внимание, что мы не запускаем и не откатываем транзакции, как в исходной среде тестирования. Не только потому, что это помешает нашей демонстрации, но и потому, что я считаю, что транзакционные тесты вредны — но об этом в будущем. Возвращаясь к нашему примеру, вот тест дыма. Полный исходный код можно скачать здесь из ветки проблем с прокси . Не жалуйтесь на отсутствие утверждений — здесь мы только проверяем, что исключения не выбрасываются:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
@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, который, в свою очередь, вызывает исходные классы. Нет шансов! Это реальная реализация в псевдокоде:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
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 , это позор . Так что насчет более тщательных тестов?

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@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, невидимые записи — поверьте, это происходит на пустом месте. И помните, что темы, которые мы рассмотрели здесь, относятся ко всем аспектам АОП, а не только к транзакциям.

Ссылка: весенние ловушки: прокси от нашего партнера JCG Томаша Нуркевича в блоге NoBlogDefFound .

Статьи по Теме :