Агент ByteBuddy для замены одного параметра метода другим - PullRequest
0 голосов
/ 11 апреля 2020

У меня большая сторонняя кодовая база, которую я не могу изменить, но мне нужно внести небольшое, но важное изменение во многих разных местах. Я надеялся использовать агент на основе ByteBuddy, но я не могу понять, как. Вызов, который мне нужно заменить, имеет вид:

SomeSystemClass.someMethod("foo")

, и мне нужно заменить его на

SomeSystemClass.someMethod("bar")

, оставив все остальные вызовы того же метода без изменений

SomeSystemClass.someMethod("ignore me")

Поскольку SomeSystemClass является классом JDK, я не хочу его советовать, а только те классы, которые содержат обращения к нему. Как это можно сделать?

Обратите внимание, что:

  1. someMethod is stati c и
  2. вызовы (по крайней мере, некоторые из них) находятся внутри блок инициализации c

1 Ответ

1 голос
/ 11 апреля 2020

Существует два подхода к этому с Byte Buddy:

  1. Вы преобразуете все классы с помощью рассматриваемого сайта вызова:

    new AgentBuilder.Default()
      .type(nameStartsWith("my.lib.pkg."))
      .transform((builder, type, loader, module) -> builder.visit(MemberSubstitution.relaxed()
         .method(SomeSystemClass.class.getMethod("someMethod", String.class))
         .replaceWith(MyAlternativeDispatcher.class.getMethod("substitution", String.class)
         .on(any()))
       .installOn(...);
    

    В этом случае я предлагаем вам реализовать класс MyAlternativeDispatcher в вашем пути к классам (он также может быть доставлен как часть агента, если у вас нет более сложной установки загрузчика классов, такой как OSGi, где вы реализуете условный лог c:

    public class MyAlternativeDispatcher {
      public static void substitution(String argument) {
        if ("foo".equals(argument) {
          argument = "bar";
        }
        SomeSystemClass.someMethod(argument)
      }
    } 
    

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

  2. Инструментируйте сам системный класс и сделайте его чувствительным к вызывающему:

    new AgentBuilder.Default()
      .with(RetransformationStrategy.RETRANSFORMATION)
       .disableClassFormatChanges()
      .type(is(SomeSystemClass.class))
      .transform((builder, type, loader, module) -> builder.visit(Advice.to(MyAdvice.class).on(named("someMethod").and(takesArguments(String.class))
       .installOn(...);
    

    В этом случае вам нужно подумать о классе вызывающего, чтобы убедиться, что вы измените поведение только для классов, для которых вы хотите применить это изменение. Это не редкость в JDK, и так как Advice вставляет ("копирует вставки") код вашего класса рекомендации в системный класс, вы можете использовать внутренние API JDK без ограничений (Java 8 и более ранние версии), если вы не можете использовать API обхода стека (Java 9 и более поздние версии):

    class MyAdvice {
      @Advice.OnMethodEnter
      static void enter(@Advice.Argument(0) String argument) {
        Class<?> caller = sun.reflect.Reflection.getCallerClass(1); // or stack walker
        if (caller.getName().startsWith("my.lib.pkg.") && "foo".equals(argument) {
          argument = "bar";
        }
      }
    }
    

Какой подход выбрать?

Первый подход, вероятно, более надежен, но он довольно дорог, так как вам нужно обрабатывать все классы в пакете или подпакетах. Если в этом пакете много классов, вы заплатите довольно большую цену за обработку всех этих классов, чтобы проверить наличие соответствующих сайтов вызовов и, следовательно, задержать запуск приложения. Как только все классы загружены, вы, однако, заплатили цену, и все в порядке, не изменив системный класс. Однако вам нужно позаботиться о загрузчиках классов, чтобы убедиться, что ваш метод подстановки виден всем. В простейшем случае вы можете использовать API Instrumentation для добавления jar с этим классом в загрузчик, что делает его видимым глобально.

При втором подходе вам нужно только (повторно) преобразовать единственный метод. Это очень дешево сделать, но вы будете добавлять (минимальные) накладные расходы к каждому вызову метода. Поэтому, если этот метод часто вызывается на критическом пути выполнения, вы платите цену за каждый вызов, если JIT не обнаруживает шаблон оптимизации, чтобы избежать его. Я бы предпочел такой подход для большинства случаев, я думаю, что одиночное преобразование часто более надежно и производительно.

В качестве третьего варианта вы также можете использовать MemberSubstitution и добавить свой собственный байт-код в качестве замены. (Byte Buddy предоставляет ASM на шаге replaceWith, где вы можете определить собственный байтовый код вместо делегирования). Таким образом, вы можете избежать требования добавления метода замены и просто добавить код замены на месте. Однако это требует серьезного требования:

  • не добавлять условные операторы
  • пересчитывать фреймы стековой карты класса

Последнее требуется, если вы добавляете условные операторы и Byte Buddy (или кто-либо еще) не может оптимизировать его в методе. Пересчет фрейма карты стека очень дорогой, часто не сравнимый и может потребовать блокировки загрузки класса для полной блокировки. Byte Buddy оптимизирует пересчет по умолчанию в ASM, пытаясь избежать мертвых блокировок, избегая загрузки классов, но также не дает никаких гарантий, поэтому вы должны помнить об этом.

...