Статьи

Мета-программирование Java с гобеленом

Значительная часть того, что делает Tapestry, — это метапрограммирование : код, который модифицирует другой код. Как правило, мы говорим о добавлении поведения в классы компонентов, которые преобразуются по мере их загрузки в память. Мета-программирование — это код, который видит все эти аннотации для методов и полей и перестраивает классы, чтобы все работало во время выполнения.

В отличие от AspectJ, Tapestry выполняет все свои метапрограммирования во время выполнения. Это лучше подходит для перезагрузки живых классов, а также позволяет загруженным библиотекам расширять встроенное в среду метапрограммирование.

Все возможности Tapestry, разработанные для обработки метапрограммирования, позволяют легко добавлять новые функции. Например, я работал с объектом окружающей среды Heartbeat . Сердцебиение позволяет запланировать часть вашего поведения на «потом». Прежде всего, зачем вам это нужно?

Простым примером является связь между компонентом Label и компонентом управления формой, например TextField. В вашем шаблоне вы можете использовать два вместе:

  <t:label for="email"/>
  <t:textfield t:id="email"/>

Параметр for не является простой строкой, это идентификатор компонента. Вы можете видеть это в источнике для компонента Label:

    @Parameter(name = "for", required = true, allowNull = false, defaultPrefix = BindingConstants.COMPONENT)
    private Field field;

Почему for = «email» совпадает с компонентом электронной почты, а не с некоторым свойством страницы с именем email? Это то, что делает атрибут аннотации defaultPrefix: он говорит: «притворитесь, что в привязке есть префикс компонента: если только программист не предоставит явный префикс».

Таким образом, вы можете подумать, что это обернулось, нам просто нужно сделать следующее в коде Label:

  writer.element("label", "for", field.getClientId());

Правильно? Просто спросите поле для его идентификатора на стороне клиента, и теперь все счастливы.

Увы, это не сработает. Компонент Label визуализируется до TextField, а свойство clientId не устанавливается до тех пор, пока не будет выполнен TextField. Нам нужно подождать, пока они оба отрендерится, а затем заполнить атрибут for после факта.

Вот где приходит Heartbeat. Heartbeat представляет собой контейнер, такой как Loop или Form. Сердцебиение начинается и накапливает отложенные команды . Когда Heartbeat заканчивается, отложенные команды выполняются. Кроме того, Heartbeats может гнездиться.

Используя Heartbeat, мы можем подождать до конца текущего пульса после того, как Label и TextField отрендерится, а затем получить точное представление идентификатора стороны клиента поля. Так как Tapestry визуализирует DOM (не простой текстовый поток), мы можем изменить элемент DOM Label по факту.

Без метапрограммирования это выглядит так:

    @Environmental
    private Heartbeat heartbeat;

    private Element labelElement;

    boolean beginRender(MarkupWriter writer)
    {
        final Field field = this.field;

        decorator.beforeLabel(field);

        labelElement = writer.element("label");

        resources.renderInformalParameters(writer);

        Runnable command = new Runnable()
        {
            public void run()
            {
                String fieldId = field.getClientId();

                labelElement.forceAttributes("for", fieldId, "id", fieldId + "-label");

                decorator.insideLabel(field, labelElement);          
            }
        };
        
        heartbeat.defer(command);

        return !ignoreBody;
    }

See, we’ve gotten the active Heartbeat instance for this request and we provide a command, as a Runnable. We capture the label’s Element in an instance variable, and force the values of the for (and id) attributes. Notice all the steps: inject the Heartbeat environmental, create the Runnable, and pass it to defer().

So where does the meta-programming come in? Well, since Java doesn’t have closures, it has a pattern of using component methods for the same function. Following that line of reasoning, we can replace the Runnable instance with a method call that has special semantics, triggered by an annotation:

    private Element labelElement;

    boolean beginRender(MarkupWriter writer)
    {
        final Field field = this.field;

        decorator.beforeLabel(field);

        labelElement = writer.element("label");

        resources.renderInformalParameters(writer);

        updateAttributes();

        return !ignoreBody;
    }

    @HeartbeatDeferred
    private void updateAttributes()
    {
        String fieldId = field.getClientId();

        labelElement.forceAttributes("for", fieldId, "id", fieldId + "-label");

        decorator.insideLabel(field, labelElement);
    }

See what’s gone on here? We invoke updateAttributes, but because of this new annotation, @HeartbeatDeferred, the code doesn’t execute immediately, it waits for the end of the current heartbeat.

What’s more surprising is how little code is necessary to accomplish this. First, the new annotation:

@Target(ElementType.METHOD)
@Retention(RUNTIME)
@Documented
@UseWith(
{ COMPONENT, MIXIN, PAGE })
public @interface HeartbeatDeferred
{

}

The @UseWith annotation is for documentation purposes only, to make it clear that this annotation is for use with components, pages and mixins … but can’t be expected to work elsewhere, such as in services layer objects.

Next we need the actual meta-programming code. Component meta-programming is accomplished by classes that implement the ComponentClassTransformationWorker interface.

public class HeartbeatDeferredWorker implements ComponentClassTransformWorker
{
  private final Heartbeat heartbeat;

  private final ComponentMethodAdvice deferredAdvice = new ComponentMethodAdvice()
  {
    public void advise(final ComponentMethodInvocation invocation)
    {
      heartbeat.defer(new Runnable()
      {

        public void run()
        {
          invocation.proceed();
        }
      });
    }
  };

  public HeartbeatDeferredWorker(Heartbeat heartbeat)
  {
    this.heartbeat = heartbeat;
  }

  public void transform(ClassTransformation transformation, MutableComponentModel model)
  {
    for (TransformMethod method : transformation.matchMethodsWithAnnotation(HeartbeatDeferred.class))
    {
      deferMethodInvocations(method);
    }
  }

  void deferMethodInvocations(TransformMethod method)
  {
    validateVoid(method);

    validateNoCheckedExceptions(method);

    method.addAdvice(deferredAdvice);

  }

  private void validateNoCheckedExceptions(TransformMethod method)
  {
    if (method.getSignature().getExceptionTypes().length > 0)
      throw new RuntimeException(
          String
              .format(
                  "Method %s is not compatible with the @HeartbeatDeferred annotation, as it throws checked exceptions.",
                  method.getMethodIdentifier()));
  }

  private void validateVoid(TransformMethod method)
  {
    if (!method.getSignature().getReturnType().equals("void"))
      throw new RuntimeException(String.format(
          "Method %s is not compatible with the @HeartbeatDeferred annotation, as it is not a void method.",
          method.getMethodIdentifier()));
  }
}

It all comes down to method advice. We can provide method advice that executes around the call to the annotated method.

When advice is triggered, it does not call invocation.proceed() immediately, to continue on to the original method. Instead, it builds a Runnable command that it defers into the Heartbeat. When the command is executed, the invocation finally does proceed and the annotated method finally gets invoked.

That just leaves a bit of configuration code to wire this up. Tapestry uses a chain-of-command to identify all the different workers (theres more than a dozen built in) that get their chance to transform component classes. Since HeartbeatDeferredWorker is part of Tapestry, we need to extend contributeComponentClassTransformWorker() in TapestryModule:

  public static void contributeComponentClassTransformWorker(
      OrderedConfiguration<ComponentClassTransformWorker> configuration
  {
  
    ...
    
    configuration.addInstance("HeartbeatDeferred", HeartbeatDeferredWorker.class, "after:RenderPhase");
  }      

Meta-programming gives you the ability to change the semantics of Java programs and eliminate boiler-plate code while you’re at it. Because Tapestry is a managed environment (it loads, transforms and instantiates the component classes) it is a great platform for meta-programming. Whether your concerns are security, caching, monitoring, parallelization or something else entirely, Tapestry gives you the facilities to you need to move Java from what it is to what you would like it to be.

From http://tapestryjava.blogspot.com/2010/04/meta-programming-java-with-tapestry.html