iberck
9/29/2014 - 4:25 PM

Tapestry Ioc

Tapestry Ioc

Características de los servicios:

Scope de los servicios.

  • Por default los servicios son singleton, pero hay otros como por ejemplo per-thread (útil en entornos web).
  • Si un servicio es singleton, hay que tener cuidado de que sea thread-safe.
  • El scope se define cuando se crea el servicio.

Service Lifecycle

Todos los servicios son lazy, es decir no son completamente inicializados hasta que se utilizan por completo, a menudo lo que parece un servicio es un objeto proxy, la primer vez que se invoca un método del proxy, el servicio actual es inicializado e instanciado (realized). Estados de un servicio:

  • defined: Algún modulo lo definió, pero aún no ha sido referenciado.
  • virtual: El servicio ha sido referenciado, por lo tanto se ha creado un proxy para la clase.
  • realized: Ha sido invocado un método sobre el proxy, por lo tanto ha sido instanciada la implementación del servicio.

Construyendo servicios (bind/build)

  • Todo servicio tiene un id
  • Cuando se define un servicio, si no se indica un nombre entonces toma por default el nombre de la interfaz.
  • Todo servicio tiene una interfaz y una implementación.
  • Para construir el servicio se realiza dentro del método estático bind, si se desea hacer una construcción personalizada se hace desde el método build.

Construcción simple (autobuild services - bind):

package org.example.myapp.services;
 
import org.apache.tapestry5.ioc.ServiceBinder;
 
public class MyAppModule
{
  public static void bind(ServiceBinder binder)
  {
     binder.bind(Indexer.class, IndexerImpl.class);
  }
}

Construcción personalizada (build):
El método empieza con la frase build``serviceName, el siguiente código crea el servicio "Indexer":

package org.example.myapp.services;
 
public class MyAppModule
{
  public static Indexer buildIndexer()
  {
    return new IndexerImpl();
  }
}

Configurar los servicios (Contribute)

Un servicio puede ser configurado a través de recibir un objeto (mapa, lista, colección) que contenga elementos agregados por otros servicios (servicios que contribuyen a la configuración de un servicio).

Identificando servicios

  1. Todos los servicios tienen un id.
  2. Se puede utilizar un marcador (para cuando el servicio tiene la misma interfaz y el mismo id tal vez cuando cambia de versión).
  3. Dependencias locales: @Local inyecta solo los servicios definidos en el modulo.

Para obtener un servicio por id:

public static Indexer build(@InjectService("JobScheduler") JobScheduler scheduler,
                            @InjectService("FileSystem") FileSystem fileSystem)
{
  IndexerImpl indexer = new IndexerImpl(fileSystem);
  scheduler.scheduleDailyJob(indexer);
 
  return indexer;
}

Inyectando dependencias

Dentro de tapestry IoC, se pueden inyectar servicios dentro de la implementación de otros servicios. Las dependencias son proporcionadas a los servicios de las siguientes maneras:

  1. Como parámetros del constructor de la clase de implementación del servicio (for autobuilt services - tapestry asume que los argumentos del constructor son dependencias).
  2. Como parámetros del método service builder, (tapestry asume que estos parámetros son dependencias).
  3. Como parámetros pasados al constructor del módulo del servicio (para ser cachados dentro de variables de instancia).
  4. Directamente como propiedades dentro de la implementación del servicio.

Como parámetros en el método service builder:

public static Indexer build(JobScheduler scheduler, FileSystem fileSystem)
{
  IndexerImpl indexer = new IndexerImpl(fileSystem);
 
  scheduler.scheduleDailyJob(indexer);
 
  return indexer;
}

Inyecciones a través del constructor:

package org.example.myapp.services;
 
import org.apache.tapestry5.ioc.annotations.InjectService;
 
public class IndexerImpl implements Indexer
{
  private final FileSystem fileSystem;
 
  public IndexerImpl(@InjectService("FileSystem") FileSystem fileSystem)
  {
    this.fileSystem = fileSystem;
  }
}

Inyección a través de campos (nota: internamente utiliza reflection por lo tanto no es thread-safe por lo que se recomienda realizar la inyección a través del constructor y campos finales):

package org.example.myapp.services;
 
import org.apache.tapestry5.ioc.annotations.InjectService;
 
public class IndexerImpl implements Indexer
{
  @InjectService("FileSystem")
  private FileSystem fileSystem;
 
  . . .
}

Inyectando recursos

Además de inyectar servicios, tapestry permite inyectar otras cosas:

  • java.lang.String: id del servicio
  • org.slf4j.Logger: logger para el servicio
  • java.lang.Class: interfaz que implementa el servicio que se va a construir.
  • ServiceResources: permite acceder a otros servicios.
  • ObjectProviders: se puede pasar cualquier tipo de objeto, especificando con anotaciones de qué tipo de objeto se trata.
  • Configuraciones (List, Map, Collection)

Ejemplo inyectando object providers, alertEmail es un símbolo inyectado:

public static Indexer build(String serviceId, Log serviceLog,
   JobScheduler scheduler, FileSystem fileSystem,
   @Inject @Value("${index-alerts-email}")
   String alertEmail)
{
  IndexerImpl indexer = new IndexerImpl(serviceLog, fileSystem, alertEmail);
 
  scheduler.scheduleDailyJob(serviceId, indexer);
 
  return indexer;
}

Nota: los recursos no pueden ser inyectados a través de campos, debe ser a través de constructores.

Contribuciones de servicios (contributing service configurations)

  • Las contribuciones aceptan servicios y recursos como parámetros.
  • Se pueden hacer contribuciones desde otros servicios.
  • El método debe empezar con la palabra contribute y tener la anotación @Contribute.

Ejemplo: se construye el servicio FileServicerDispatcher, en la construcción se envía un mapa que tendrá las configuraciones:

public static FileServiceDispatcher buildFileServicerDispatcher(Map<String,FileServicer> contributions)
{
  return new FileServiceDispatcherImpl(contributions);
}

Luego, para especificar un parámetro en la configuración se debe contrubuir el servicio, MappedConfiguration es el mapa definido como primer parámetro de buildFileServiceDispatcher:

public static void contributeFileServicerDispatcher(MappedConfiguration<String,FileServicer> configuration)
{
  configuration.add("txt", new TextFileServicer());
  configuration.add("pdf", new PDFFileServicer());
}

Ejemplo de cómo contribuir un servicio con anotación:

@Contribute(FileServiceDispatcher.class)
public static void arbitraryMethodName(MappedConfiguration<String,FileServicer> configuration)
{
    configuration.add("doc", new WordFileServicer());
    configuration.add("ppt", new PowerPointFileServicer());
}  

Tipos de configuración

Existen 3 tipos de configuración.

  • Unordered Collection: Las contribuciones son simplemente agregadas sin importar el orden.
  • Ordered list: Las contribuciones se agregan a una lista, no están ordenadas ya que no se puede predecir el orden de contribución, pero se le pueden agregar constrains ("before:", "after:") para indicar la posición de los elementos en la lista.
  • Map: Las contribuciones son a través de llave->valor

Todas las configuraciones contienen los métodos:

  1. El método add toma un nombre, un objeto contribuido para ese nombre, y luego cero o más constraints. Los constraints controlan el orden. El constraint after asegura que la contribución es ordenada después de esa contribución, el constraint before es lo opuesto.
  2. El método addInstance toma un nombre y un objeto .class a partir del cuál se creará la instancia, al crear la instancia del servicio, la construye y carga por completo.
contribute parambuild param
ConfigurationCollection
OrderedConfigurationList
MappedConfigurationMap

Ordered List

Cada configuración tiene un nombre único:

public void contributeMasterDispatcher(OrderedConfiguration<Dispatcher> configuration, . . .)
{
  configuration.add("RootPath", new RootPathDispatcher(. . .), "before:Asset");
  configuration.add(
          "Asset",
          new AssetDispatcher(. . .),
          "before:PageRender");

  configuration.add("PageRender", new PageRenderDispatcher(. . .));
 
  configuration.add("ComponentAction", new ComponentActionDispatcher(. . .), "after:PageRender");
}

Nota: La configuración, una vez ensamblada y ordenada, es proporcionada como una lista.

Agregar funcionalidad a los servicios (Advice/Decorate)

Se puede agragar funcionalidad a los servicios decorándolos (a través de service decorator methods). Esos métodos crean interceptores que agregan comportamiento.

Service advice permite interceptar invocaciones de métodos en tus servicios, es una parte de la programación orientada a aspectos (AOP).
Service decorators son otra forma de hacer lo mismo (antes de tap5.1), services advisors es una característica más reciente (agregada en tap5.1). No se recomienda mezclar advice y decorator, si se hace los decoradores tienen preferencia.

Ejemplos de intercepciones: logging, validaciones de seguridad, manejo de transacciones,

Ejemplo que intercepta los métodos del servicio Executable, y remplaza el retorno, en vez de retornar null retorna la constante Executable.NULL. En todos los métodos advise, es obligatorio el parámetro MethodAdviceReceiver:

public static void adviseExecutable(MethodAdviceReceiver receiver) {

        MethodAdvice advice = new MethodAdvice() {
            public void advise(MethodInvocation invocation) {
                invocation.proceed(); // ejecuta el método

                if (invocation.getReturnValue() == null) {
                    invocation.setReturnValue(Executable.NULL);
                }
            }
        };

        receiver.adviseAllMethods(advice);
    }

El mismo ejemplo pero con anotaciones:

@Advise(serviceInterface = Executable.class)
    public static void adviseExecutable(MethodAdviceReceiver receiver) {
       ...
    }
}

Se puede utilizar la anotación @Order para indicar en qué orden se ejecuta el advise. Por ejemplo, "before:*" indica que el advise se debe ejecutar antes que cualquier otro decorador.

Artículo sobre advice/decorators con anotaciones

Autoload modules

Permite agregar nuevas características a una aplicación con tan solo soltar un jar que contenga un módulo. Los servicios dentro del módulo son automáticamente integrados al registro global de la aplicación.

Para que un archivo jar sea identificado como un módulo autoloaded necesita contener en el manifest algo como:

Manifest-Version: 1.0
Tapestry-Module-Classes: org.example.mylib.LibModule, org.example.mylib.internal.InternalModule

La anotación @SubModule sirve para enlistar los módulos adicionales que serán autoloaded.
En general, deberá identificar un módulo en el manifest del jar, y hacer uso de @SubModule para agregar módulos adicionales (sin agregarlos al archivo manifest).

Símbolos

Los símbolos son llamados los parámetros de configuración de los servicios.

Los símbolos se pueden inyectar con la anotación @Simbol o @Value:

public class MyService implements MyServiceInterface
{
  public MyService(@Inject @Value("${tapestry.production-mode}") boolean productionMode, ...)
  {
    if (productionMode) {
      . . .

Note: When injecting a symbol as a string into a service, you must use the @Inject annotation as well as @Value or @Symbol; otherwise Tapestry will inject the service's service id.

De dónde se obtienen los símbolos?:

  1. System Properties (tapestry tiene un provider que convierte los system properties -D en símbolos).
  2. ApplicationDefaults

Para agregar símbolos a la aplicación:

public void contributeApplicationDefaults(MappedConfiguration<String, String> configuration)
{
  configuration.add("some-service-id", "WackyCollaborator");
}