Статьи

Все из бассейна! Гобелен Goes Singleton!

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

У этого также есть свой недостаток: Tapestry должен поддерживать пул экземпляров страниц. А в Tapestry экземпляры страниц большие : дерево из сотен или, возможно, тысяч взаимосвязанных объектов: дерево структурных объектов Tapestry, которое формирует базовую структуру страницы, компоненты и смешанные объекты, свисающие с этого дерева, объекты привязки, которые соединяют параметры компоненты к свойствам содержащего их компонента, объекты-шаблоны, представляющие элементы и контент из шаблонов компонентов, и многие, многие другие, о которых большинство разработчиков Tapestry не знают.

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

Так что это кажется непримиримой проблемой, а? Удаление изменяемого состояния из страниц и компонентов превратит Tapestry в нечто совершенно другое. С другой стороны, допустимое изменяемое состояние означает, что приложения, особенно большие сложные приложения с большим количеством страниц, превращаются в проблемы с памятью.

Я полагаю, что одним из подходов было бы просто создать экземпляр страницы на время запроса и отбросить его в конце. Тем не менее, создание страницы в Tapestry очень сложно, и хотя в Tapestry 5.1 были приложены некоторые усилия для снижения стоимости создания страницы, она все еще присутствует. Кроме того, Tapestry полон небольших оптимизаций, которые улучшают производительность … при условии, что страница повторно используется со временем. Выбрасывать страницы — это не стартер.

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

Гобелен уже шел по этому маршруту: способ обработки постоянных полей создает иллюзию того, что страница сохраняется между запросами. Вы можете подумать, что Tapestry сериализует страницу и сохраняет все это в сеансе. В действительности, Tapestry тасует только отдельные постоянные значения полей в HttpSessio и из него. Чтобы как конечного пользователя и разработчик Гобелена, он чувствует , как вся страница находится в прямой эфире между запросами, но это немного игры оболочкой, обеспечивая эквивалентный экземпляр страницы , которая имеет то же значение в своих областях.

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

Конечным результатом является то, что один экземпляр страницы может использоваться в нескольких потоках без каких-либо проблем с синхронизацией и без каких-либо конфликтов значений полей.

Эта идея была предложена в прошлые годы, но API-интерфейсы для ее реализации (а также необходимые знания метапрограммирования) просто не были доступны. Однако, как побочный эффект переписывания и упрощения API преобразования классов в 5.2, это стало очень разумным.

Давайте рассмотрим важный пример: обработка типичных, изменяемых полей. За это отвечает класс UnclaimedFieldWorker, являющийся частью конвейера преобразования класса компонентов Tapestry. UnclaimedFieldWorker находит поля, которые не были «востребованы» какой-либо другой частью конвейера, и преобразует их для чтения и записи своих значений в карту для каждого потока. Заявленное поле может хранить внедренную услугу, актив или компонент или быть параметром компонента.

public class UnclaimedFieldWorker implements ComponentClassTransformWorker
{
    private final PerthreadManager perThreadManager;

    private final ComponentClassCache classCache;

    static class UnclaimedFieldConduit implements FieldValueConduit
    {
        private final InternalComponentResources resources;

        private final PerThreadValue<Object> fieldValue;

        // Set prior to the containingPageDidLoad lifecycle event
        private Object fieldDefaultValue;

        private UnclaimedFieldConduit(InternalComponentResources resources, PerThreadValue<Object> fieldValue,
                Object fieldDefaultValue)
        {
            this.resources = resources;

            this.fieldValue = fieldValue;
            this.fieldDefaultValue = fieldDefaultValue;
        }

        public Object get()
        {
            return fieldValue.exists() ? fieldValue.get() : fieldDefaultValue;
        }

        public void set(Object newValue)
        {
            fieldValue.set(newValue);

            // This catches the case where the instance initializer method sets a value for the field.
            // That value is captured and used when no specific value has been stored.

            if (!resources.isLoaded())
                fieldDefaultValue = newValue;
        }

    }

    public UnclaimedFieldWorker(ComponentClassCache classCache, PerthreadManager perThreadManager)
    {
        this.classCache = classCache;
        this.perThreadManager = perThreadManager;
    }

    public void transform(ClassTransformation transformation, MutableComponentModel model)
    {
        for (TransformField field : transformation.matchUnclaimedFields())
        {
            transformField(field);
        }
    }

    private void transformField(TransformField field)
    {
        int modifiers = field.getModifiers();

        if (Modifier.isFinal(modifiers) || Modifier.isStatic(modifiers))
            return;

        ComponentValueProvider<FieldValueConduit> provider = createFieldValueConduitProvider(field);

        field.replaceAccess(provider);
    }

    private ComponentValueProvider<FieldValueConduit> createFieldValueConduitProvider(TransformField field)
    {
        final String fieldName = field.getName();
        final String fieldType = field.getType();

        return new ComponentValueProvider<FieldValueConduit>()
        {
            public FieldValueConduit get(ComponentResources resources)
            {
                Object fieldDefaultValue = classCache.defaultValueForType(fieldType);

                String key = String.format("UnclaimedFieldWorker:%s/%s", resources.getCompleteId(), fieldName);

                return new UnclaimedFieldConduit((InternalComponentResources) resources,
                        perThreadManager.createValue(key), fieldDefaultValue);
            }
        };
    }
}

Это кажется много, но давайте разберем это постепенно.

    public void transform(ClassTransformation transformation, MutableComponentModel model)
    {
        for (TransformField field : transformation.matchUnclaimedFields())
        {
            transformField(field);
        }
    }

    private void transformField(TransformField field)
    {
        int modifiers = field.getModifiers();

        if (Modifier.isFinal(modifiers) || Modifier.isStatic(modifiers))
            return;

        ComponentValueProvider<FieldValueConduit> provider = createFieldValueConduitProvider(field);

        field.replaceAccess(provider);
    }

Метод transform () является единственным методом для этого класса, как определено
ComponentClassTransformWorker . Он использует метод
ClassTransformation, чтобы найти все невостребованные поля.
TransformField — это представление поля класса компонента в процессе преобразования. Как мы увидим, очень легко перехватить доступ к полю.

Some of those fields are final or static and are just ignored. A ComponentValueProvider is a callback object: when the component (whatever it is) is first instantiated, the provider will be invoked and the return value stored into a new field. A FieldValueConduit is an object that takes over responsibility for access to a TransformField: internally, all read and write access to the field is passed through the conduit object.

So, what we’re saying is: when the component is first created, use the callback to create a conduit, and change any read or write access to the field to pass through the created conduit. If a component is instantiated multiple times (either in different pages, or within the same page) each instance of the component will end up with a specific FieldValueConduit.

Fine so far; it comes down to what’s inside the createFieldValueConduitProvider() method:

    private ComponentValueProvider<FieldValueConduit> createFieldValueConduitProvider(TransformField field)
    {
        final String fieldName = field.getName();
        final String fieldType = field.getType();

        return new ComponentValueProvider<FieldValueConduit>()
        {
            public FieldValueConduit get(ComponentResources resources)
            {
                Object fieldDefaultValue = classCache.defaultValueForType(fieldType);

                String key = String.format("UnclaimedFieldWorker:%s/%s", resources.getCompleteId(), fieldName);

                return new UnclaimedFieldConduit((InternalComponentResources) resources,
                        perThreadManager.createValue(key), fieldDefaultValue);
            }
        };
    }

Here we capture the name of the field and its type (expressed as String). Inside the get() method we determine the initial default value for the field: typically just null, but may be 0 (for a primitive numeric field) or false (for a primitive boolean field).

Next we build a unique key used to store and retrieve the field’s value inside the per-thread Map. The key includes the complete id of the component and the name of the field: thus two different component instances, in the same page or across different pages, will have their own unique key.

We use the PerthreadManager service to create a PerThreadValue for the field.

Lastly, we create the conduit object. Let’s look at the conduit in more detail:

    static class UnclaimedFieldConduit implements FieldValueConduit
    {
        private final InternalComponentResources resources;

        private final PerThreadValue<Object> fieldValue;

        // Set prior to the containingPageDidLoad lifecycle event
        private Object fieldDefaultValue;

        private UnclaimedFieldConduit(InternalComponentResources resources, PerThreadValue<Object> fieldValue,
                Object fieldDefaultValue)
        {
            this.resources = resources;

            this.fieldValue = fieldValue;
            this.fieldDefaultValue = fieldDefaultValue;
        }

We use the special InternalComponentResources interface because we’ll need to know if the page is loading, or in normal operation (that’s coming up). We capture our initial guess at a default value for the field (remember: null, false or 0) but that may change.

        public Object get()
        {
            return fieldValue.exists() ? fieldValue.get() : fieldDefaultValue;
        }

Whenever code inside the component reads the field, this method will be invoked. It checks to see if a value has been stored into the PerThreadValue object this request; if so the stored value is returned, otherwise the field default value is returned.

Notice the distinction here between null and no value at all. Just because the field is set to null doesn’t mean we should switch over the the default value (assuming the default is not null).

The last hurdle is updates to the field:

      public void set(Object newValue)
        {
            fieldValue.set(newValue);

            // This catches the case where the instance initializer method sets a value for the field.
            // That value is captured and used when no specific value has been stored.

            if (!resources.isLoaded())
                fieldDefaultValue = newValue;
        }

The basic logic is just to stuff the value assigned to the component field into the PerThreadValue object. However, there’s one special case: a field initialization (whether it’s in the component’s constructor, or at the point in the code where the field is first defined) turns into a call to set(). We can differentiate the two cases because that update occurs before the page is marked as fully loaded, rather than in normal use of the page.

And that’s it! Now, to be honest, this is more detail than a typical Tapestry developer ever needs to know. However, it’s a good demonstration of how Tapestry’s class transformation APIs make Java code fluid; capable of being changed dynamically (under carefully controlled circumstances).

Back to pooling: how is this going to affect performance? That’s an open question, and putting together a performance testing environment is another task at the top of my list. My suspicion is that the new overhead will not make a visible difference for small applications (dozens of pages, reasonable number of concurrent users) … but for high end sites (hundreds of pages, large numbers of concurrent users) the avoidance of pooling and page construction will make a big difference!

From http://tapestryjava.blogspot.com/2010/07/everyone-out-of-pool-tapestry-goes.html