Как кэшировать инструментированный класс с помощью экспедитора экземпляров?

Вариант использования - реализовать грязный полевой трекер. Для этого у меня есть интерфейс:

public interface Dirtyable {

    String ID = "dirty";

    Set<String> getDirty();

    static <T> T wrap(final T delegate) {
        return DirtyableInterceptor.wrap(delegate, ReflectionUtils::getPropertyName);
    }

    static <T> T wrap(final T delegate, final Function<Method, String> resolver) {
        return DirtyableInterceptor.wrap(delegate, resolver);
    }

}

В классе перехватчика метод обертки:

static <T> T wrap(final T delegate, final Function<Method, String> resolver) {
    requireNonNull(delegate, "Delegate must be non-null");
    requireNonNull(resolver, "Resolver must be non-null");

    final Try<Class<T>> delegateClassTry = Try.of(() -> getClassForType(delegate.getClass()));

    return delegateClassTry.flatMapTry(delegateClass ->
            dirtyableFor(delegate, delegateClass, resolver))
            .mapTry(Class::newInstance)
            .getOrElseThrow(t -> new IllegalStateException(
                    "Could not wrap dirtyable for " + delegate.getClass(), t));
}

Метод dirtyableFor определяет ByteBuddy, который перенаправляет на конкретный экземпляр при каждом вызове. Однако инструментарий при каждом вызове стоит немного дороже, поэтому он кэширует инструментированный подкласс из класса данного экземпляра. Для этого я использую resilience4j библиотека javaslang-circuitbreaker).

private static <T> Try<Class<? extends T>> dirtyableFor(final T delegate,
                                                        final Class<T> clazz,
                                                        final Function<Method, String> resolver) {

    long start = System.currentTimeMillis();

    Try<Class<? extends T>> r = Try.of(() -> ofCheckedSupplier(() ->
            new ByteBuddy().subclass(clazz)
                    .defineField(Dirtyable.ID, Set.class, Visibility.PRIVATE)
                    .method(nameMatches("getDirty"))
                    .intercept(reference(new HashSet<>()))
                    .implement(Dirtyable.class)
                    .method(not(isDeclaredBy(Object.class))
                            .and(not(isAbstract()))
                            .and(isPublic()))
                    .intercept(withDefaultConfiguration()
                            .withBinders(Pipe.Binder.install(Function.class))
                            .to(new DirtyableInterceptor(delegate, resolver)))
                    .make().load(clazz.getClassLoader())
                    .getLoaded())
            .withCache(getCache())
            .decorate()
            .apply(clazz));

    System.out.println("Instrumentation time: " + (System.currentTimeMillis() - start));

    return r;
}

private static <T> Cache<Class<? super T>, Class<T>> getCache() {

    final CachingProvider provider = Caching.getCachingProvider();
    final CacheManager manager = provider.getCacheManager();

    final javax.cache.Cache<Class<? super T>, Class<T>> cache =
            manager.getCache(Dirtyable.ID);

    final Cache<Class<? super T>, Class<T>> dirtyCache = Cache.of(cache);
    dirtyCache.getEventStream().map(Object::toString).subscribe(logger::debug);

    return dirtyCache;
}

Из журналов время интрументации уменьшается с 70-100 мс при потере кеша до 0-2 мс при попадании в кеш.

Для полноты здесь используется метод перехватчика:

@RuntimeType
@SuppressWarnings("unused")
public Object intercept(final @Origin Method method, final @This Dirtyable dirtyable,
                        final @Pipe Function<Object, Object> pipe) throws Throwable {

    if (ReflectionUtils.isSetter(method)) {
        final String property = resolver.apply(method);
        dirtyable.getDirty().add(property);

        logger.debug("Intercepted setter [{}], resolved property " +
                "[{}] flagged as dirty.", method, property);
    }

    return pipe.apply(this.delegate);
}

Это решение работает хорошо, за исключением того, что DirtyableInterceptor всегда одинаково для попаданий в кэш, поэтому экземпляр делегата также одинаков.

Можно ли привязать экспедитор к поставщику экземпляра, чтобы перехваченные методы передавали его? Как это можно сделать?

1 ответ

Решение

Вы можете создать перехватчик без состояния, сделав intercept метод static, Чтобы получить доступ к состоянию объекта, определите два поля вашего подкласса, к которым вы обращаетесь, используя @FieldValue аннотации в вашем теперь статическом перехватчике. Вместо использования FixedValue::reference инструментов, вам также необходимо использовать FieldAccessor реализация, чтобы прочитать значение. Вы также должны определить поля, используя defineField метод строителя.

Вы можете установить эти поля либо:

  1. Добавление методов установки в ваш Dirtyable интерфейс и перехватывать их с помощью FieldAccessor реализация.
  2. Определение явного конструктора, которому вы предоставляете значения. Это также позволяет вам определять поля, которые будут final, Чтобы реализовать конструктор, сначала нужно вызвать супер-конструктор, а затем вызвать FieldAccessor несколько раз, чтобы установить поля.

Таким образом, вы создали класс без сохранения состояния, который вы можете использовать повторно, но тот, который вам нужно инициализировать. Byte Buddy уже предлагает встроенный TypeCache для легкого повторного использования.

Другие вопросы по тегам