Почему самопрозвание не работает для прокси Spring (например, с AOP)?

#java #spring #aop #aspectj #aspect

Вопрос:

Пожалуйста, объясните, почему самостоятельный вызов прокси выполняется на целевом сервере, но не на прокси-сервере? Если это сделано нарочно, то почему? Если прокси создаются путем создания подклассов, то перед каждым вызовом метода может выполняться некоторый код, даже при самостоятельном вызове. Я пытался, и у меня есть прокси для самостоятельного вызова

 public class DummyPrinter {
    public void print1() {
        System.out.println("print1");
    }

    public void print2() {
        System.out.println("print2");
    }

    public void printBoth() {
        print1();
        print2();
    }
}
 
 public class PrinterProxy extends DummyPrinter {
    @Override
    public void print1() {
        System.out.println("Before print1");
        super.print1();
    }

    @Override
    public void print2() {
        System.out.println("Before print2");
        super.print2();
    }

    @Override
    public void printBoth() {
        System.out.println("Before print both");
        super.printBoth();
    }
}
 
 public class Main {
    public static void main(String[] args) {
        DummyPrinter p = new PrinterProxy();
        p.printBoth();
    }
}
 

Выход:

 Before print both
Before print1
print1
Before print2
print2
 

Здесь каждый метод вызывается по прокси-серверу. Почему в документации упоминается, что AspectJ следует использовать в случае самостоятельного вызова?

Ответ №1:

Пожалуйста, прочтите эту главу в руководстве Spring, тогда вы поймете. Там используется даже термин «самовзвращение». Если вы все еще не понимаете, не стесняйтесь задавать дополнительные вопросы, если они соответствуют контексту.


Обновление: Хорошо, теперь, после того как мы установили, что вы действительно прочитали эту главу, и после повторного прочтения вашего вопроса и анализа вашего кода, я вижу, что вопрос на самом деле довольно глубокий (я даже поддержал его) и стоит ответить более подробно.

Ваше (ложное) предположение о том, как это работает

Ваше недопонимание связано с тем, как работают динамические прокси, потому что они работают не так, как в вашем примере кода. Позвольте мне добавить идентификатор объекта (хэш-код) в вывод журнала для иллюстрации к вашему собственному коду:

 package de.scrum_master.app;

public class DummyPrinter {
  public void print1() {
    System.out.println(this   " print1");
  }

  public void print2() {
    System.out.println(this   " print2");
  }

  public void printBoth() {
    print1();
    print2();
  }
}
 
 package de.scrum_master.app;

public class PseudoPrinterProxy extends DummyPrinter {
  @Override
  public void print1() {
    System.out.println(this   " Before print1");
    super.print1();
  }

  @Override
  public void print2() {
    System.out.println(this   " Before print2");
    super.print2();
  }

  @Override
  public void printBoth() {
    System.out.println(this   " Before print both");
    super.printBoth();
  }

  public static void main(String[] args) {
    new PseudoPrinterProxy().printBoth();
  }
}
 

Журнал консоли:

 de.scrum_master.app.PseudoPrinterProxy@59f95c5d Before print both
de.scrum_master.app.PseudoPrinterProxy@59f95c5d Before print1
de.scrum_master.app.PseudoPrinterProxy@59f95c5d print1
de.scrum_master.app.PseudoPrinterProxy@59f95c5d Before print2
de.scrum_master.app.PseudoPrinterProxy@59f95c5d print2
 

Видишь? Всегда существует один и тот же идентификатор объекта, что неудивительно. Самовзвращение для вашего «прокси» (который на самом деле не является прокси, а статически скомпилированным подклассом) работает из-за полиморфизма. Об этом заботится компилятор Java.

Как это на самом деле работает

Теперь, пожалуйста, помните, что мы говорим здесь о динамических прокси-серверах, т. е. подклассах и объектах, созданных во время выполнения:

  • Прокси-серверы JDK работают для классов, реализующих интерфейсы, что означает, что классы, реализующие эти интерфейсы, создаются во время выполнения. В этом случае все равно нет суперкласса, что также объясняет, почему он работает только для общедоступных методов: интерфейсы имеют только общедоступные методы.
  • Прокси CGLIB также работают для классов, не реализующих никаких интерфейсов, и, следовательно, также работают для защищенных методов и методов с областью действия пакета (не частных, хотя, поскольку вы не можете их переопределить, поэтому термин «частный»).
  • Решающим моментом, однако, является то, что в обоих вышеперечисленных случаях исходный объект уже (и все еще) существует при создании прокси-серверов, поэтому такой вещи, как полиморфизм, не существует. Ситуация такова, что у нас есть динамически созданный прокси-объект, делегирующий исходный объект, т. е. у нас есть два объекта: прокси и делегат.

Я хочу проиллюстрировать это так:

 package de.scrum_master.app;

public class DelegatingPrinterProxy extends DummyPrinter {
  DummyPrinter delegate;

  public DelegatingPrinterProxy(DummyPrinter delegate) {
    this.delegate = delegate;
  }

  @Override
  public void print1() {
    System.out.println(this   " Before print1");
    delegate.print1();
  }

  @Override
  public void print2() {
    System.out.println(this   " Before print2");
    delegate.print2();
  }

  @Override
  public void printBoth() {
    System.out.println(this   " Before print both");
    delegate.printBoth();
  }

  public static void main(String[] args) {
    new DelegatingPrinterProxy(new DummyPrinter()).printBoth();
  }
}
 

Видите разницу? Следовательно, журнал консоли изменяется на:

 de.scrum_master.app.DelegatingPrinterProxy@59f95c5d Before print both
de.scrum_master.app.DummyPrinter@5c8da962 print1
de.scrum_master.app.DummyPrinter@5c8da962 print2
 

Это поведение, которое вы видите с Spring AOP или другими частями Spring, использующими динамические прокси-серверы или даже приложения, не являющиеся Spring, использующие прокси JDK или CGLIB в целом.

Это особенность или ограничение? Я, как пользователь AspectJ (а не Spring AOP), думаю, что это ограничение. Возможно, кто-то еще может подумать, что это функция, потому что из-за того, как использование прокси реализовано весной, вы в принципе можете (отменить)динамически регистрировать советы по аспектам или перехватчики во время выполнения, т. Е. У вас есть один прокси на исходный объект (делегат), но для каждого прокси есть динамический список перехватчиков, вызываемых до и/или после вызова исходного метода делегата. Это может быть хорошей вещью в очень динамичных условиях. Я понятия не имею, как часто вы могли бы использовать это. Но в AspectJ у вас также есть обозначение if() pointcut, с помощью которого вы можете определить во время выполнения, следует ли применять определенные советы (язык AOP для перехватчиков) или нет.

Решения

Что вы можете сделать для решения этой проблемы, так это:

  • Переключитесь на собственный AspectJ, используя плетение во время загрузки, как описано в руководстве Spring. В качестве альтернативы вы также можете использовать плетение во время компиляции, например, с помощью плагина AspectJ Maven.
  • Если вы хотите придерживаться Spring AOP, вам нужно сделать ваш бобовый прокси-сервер осведомленным, т. Е. косвенно также осведомленным о AOP, что далеко не идеально с точки зрения дизайна. Я не рекомендую этого делать, но это достаточно просто реализовать: просто самостоятельно введите ссылку на компонент, например @Autowire MyComponent INSTANCE , а затем всегда вызывайте методы, использующие этот экземпляр компонента: INSTANCE.internalMethod() . Таким образом, все вызовы будут проходить через прокси-серверы, и будут срабатывать аспекты Spring AOP.

Комментарии:

1. Привет. Конечно, я его читал. Я не понимаю, почему введено это ограничение на самовзвращение? Есть ли от этого какая-то польза? Если я доверяю методы, интуитивно я ожидаю, что этот код будет выполнен. Eaven если это функция, почему там сказано, что AspectJ следует использовать для самостоятельного вызова, если можно получить тот же результат путем подкласса?

2. Пример с делегатом выглядит как декоратор, но цель другая. И прокси, и декоратор являются структурными шаблонами. И оба они оборачивают другой объект.

3. Как у прокси, так и у декоратора есть делегат. Исходный шаблон прокси-сервера-это именно то, что мы видим с динамическими прокси JDK, т. Е. Как делегат, так и прокси реализуют один и тот же интерфейс. Шаблон декоратора также сделал бы это, но имел бы базового декоратора и любое количество конкретных декораторов, расширяющих базового декоратора, т. Е. Структура отличается от прокси-сервера. Мой пример кода-это скорее то, что делает прокси CGLIB, т. Е. Расширяет конкретный класс. Как прокси, так и декоратор могут добавлять функциональность. Это не делает декораторов весенних прокси, но это довольно академично и не сильно помогает.

4. В качестве позднего ответа на @krund: Мой ответ уже объясняет это: динамические прокси расширяют исходный объект во время выполнения (!), а не во время компиляции, как подкласс, который вы пишете в своей среде разработки, а затем компилируете его вместе с базовым классом. Вы всегда будете создавать экземпляр подкласса только во время выполнения, но самый простой способ-создать исходный объект, а затем дополнительный прокси-сервер. Следовательно, самовзвращение (вызовы методов через this ) не может работать так, как вы ожидаете, это невозможно с помощью прокси-шаблона. Но, как я уже сказал, AspectJ не использует прокси-серверы, поэтому вы можете просто использовать его, если вам нужен самовзвращение.