Статьи

Защита от случайной рекурсии с помощью Java ThreadLocals

Теперь вот небольшая хитрость для тех из вас, кто взламывает сторонние инструменты, пытается расширить их, не понимая их полностью (пока!). Предположим следующую ситуацию:

  • Вы хотите расширить библиотеку, которая предоставляет иерархическую модель данных (предположим, вы хотите расширить Apache Jackrabbit )
  • Эта библиотека внутренне проверяет права доступа перед доступом к любым узлам хранилища содержимого
  • Вы хотите реализовать свой собственный алгоритм контроля доступа
  • Ваш алгоритм контроля доступа будет обращаться к другим узлам хранилища контента
  • … Что, в свою очередь, снова вызовет контроль доступа
  • … Который, в свою очередь, снова получит доступ к другим узлам хранилища контента

… Бесконечная рекурсия, возможно, приводящая к ошибке StackOverflowError , если вы не используете рекурсивную ширину.

Теперь у вас есть два варианта:

  1. Потратьте время, сядьте, разберитесь с внутренностями и сделайте это правильно. Вы, вероятно, не должны возвращаться в свой контроль доступа, как только вы достигли своего собственного добавочного номера. В случае расширения Jackrabbit это будет сделано с помощью системного сеанса для дальнейшего доступа к узлам в вашем алгоритме управления доступом. Системный сеанс обычно обходит контроль доступа.
  2. Будьте нетерпеливы, желая быстро получить результаты, и с помощью хитрости предотвратите рекурсию

Конечно, вы действительно должны выбрать вариант 1. Но у кого есть время, чтобы все понять?

Вот как реализовать этот трюк.

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
40
41
/**
 * This thread local indicates whether you've
 * already started recursing with level 1
 */
static final ThreadLocal<Boolean> RECURSION_CONTROL
    = new ThreadLocal<Boolean>();
  
/**
 * This method executes a delegate in a "protected"
 * mode, preventing recursion. If a inadvertent
 * recursion occurred, return a default instead
 */
public static <T> T protect(
        T resultOnRecursion,
        Protectable<T> delegate)
throws Exception {
  
    // Not recursing yet, allow a single level of
    // recursion and execute the delegate once
    if (RECURSION_CONTROL.get() == null) {
        try {
            RECURSION_CONTROL.set(true);
            return delegate.call();
        }
        finally {
            RECURSION_CONTROL.remove();
        }
    }
  
    // Abort recursion and return early
    else {
        return resultOnRecursion;
    }
}
  
/**
 * An API to wrap your code with
 */
public interface Protectable<T> {
    T call() throws Exception;
}

Это работает легко, как видно из этого примера использования:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
public static void main(String[] args)
throws Exception {
    protect(null, new Protectable<Void>() {
        @Override
        public Void call() throws Exception {
  
            // Recurse infinitely
            System.out.println("Recursing?");
            main(null);
  
            System.out.println("No!");
            return null;
        }
    });
}

Рекурсивный вызов метода main() будет прерван методом protect и возвратится раньше, чем при выполнении call() . Эту идею также можно развить, используя вместо этого Map of ThreadLocals , позволяющую указать различные ключи или контексты, для которых необходимо предотвратить рекурсию. Затем вы также можете поместить Integer в ThreadLocal , увеличивая его при рекурсии, допуская максимум N уровней рекурсии.

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
static final ThreadLocal<Integer> RECURSION_CONTROL
    = new ThreadLocal<Integer>();
  
public static <T> T protect(
        T resultOnRecursion,
        Protectable<T> delegate)
throws Exception {
    Integer level = RECURSION_CONTROL.get();
    level = (level == null) ? 0 : level;
  
    if (level < 5) {
        try {
            RECURSION_CONTROL.set(level + 1);
            return delegate.call();
        }
        finally {
            if (level > 0)
                RECURSION_CONTROL.set(level - 1);
            else
                RECURSION_CONTROL.remove();
        }
    }
    else {
        return resultOnRecursion;
    }
}

Но опять. Может быть, вам стоит потратить пару минут и узнать, как на самом деле работают внутренние компоненты вашей хост-библиотеки, и понять все правильно с самого начала … Как всегда, применяя трюки и хаки!