Mostrando entradas con la etiqueta Software Development. Mostrar todas las entradas
Mostrando entradas con la etiqueta Software Development. Mostrar todas las entradas

martes, febrero 21, 2012

Scala Implicits: simple examples

Durante la entrada anterior analizábamos los conceptos básicos de los implicits en Scala desde un punto de vista "teórico", dejando a un lado los ejemplos de código con el objetivo de no convertir el post en algo completamente inmanejable e incomprensible. A lo largo de la entrada que nos ocupa analizaremos diversos ejemplos de aplicación, describiendo con la mayor exactitud posible el mecanismo de funcionamiento de los implicits aquí descritos.

Como punto de partida realizaremos un sencillo ejemplo en el que veremos como realizar conversiones automáticas a un tipo determinado (el compilador las realizará por nosotros). Supongamos que disponemos de la siguiente definición de clase

class User (private val username:String)

y que deseamos obtener instancias de la clase anterior sin la necesidad de realizar llamadas del tipo 

val user:User = new User("migue")

El primer paso que debemos dar será llevar a cabo la definición del implicit correspondiente. En este caso la conversión deberá realizarse desde String hacia User. Para ello definimos algo como lo que sigue:

object UserImplicits {
    implicit def stringToUsername(user: String) = new User(user)
}

Nuestra siguiente tarea será hacer que el implicit anterior esté disponible en el ámbito donde lo queramos utilizar (una buena práctica suele ser realizar la definición de todos nuestros implicits en un singleton object llamado Preamble e importar dicho objeto al comienzo de nuestras clases):


import UserImplicits.stringToUsername
val user:User = "migue"

¿De verdad el código anterior compila? ¿Que está pasando? El proceso es "muy sencillo", el compilador detecta que hay un error de tipos puesto que no puede asignar una cadena a una variable de tipo User. Antes de declarar que existe un error ocasionado por una asignación de tipos incorrecta, el compilador busca si existe una conversión implicita disponible que le permita resolver el error encontrado. En este caso, el compilador buscará si existe un implicit que nos permita convertir una cadena de texto en un objeto de la clase User. Si revisamos la definición de nuestro primer implicit veremos que disponemos de una función que cumple los requisitos que el compilador está intentando satisfacer, por lo que este último, de manera interna, realizará algo similar a


val user:User = stringToUsername("migue")

El segundo ejemplo de uso de los implicits que vamos a tratar son las conversiones de objetos sobre los que se realiza la llamada de un método. Nuestro objetivo será añadir nuevos métodos a una clase externa que podría estar definida en una librería third-party sobre la que no tenemos ningún tipo de control.

Imaginemos la siguiente definición de clase en una librería externa (por sencillez esta clase estará definida en el propio archivo de código fuente junto al resto de ejemplos)


class ExternalClass {
def f1() = {
"f1"
}


def f2() = {
"f2"
}
}

en la que nos gustaría incluir el método def f3() = {"f3"}. Para llevar a cabo nuestra tarea construiremos una clase auxiliar en la que definiremos el método que deseamos incluir:



class MethodHelper {
def f3(): String = {
"f3"
}
}

e incorporamos un nuevo implicit


implicit def newMethodsToExternalClass(externalClass:ExternalClass) : MethodHelper = {new MethodHelper }



A continuación podremos escribir un código similar al que sigue:


import UserImplictis.newMethodsToExternalClass


println("Method f3() is added via an implicit conversion: " + externalClass.f3())


¿Qué está ocurriendo en esta ocasión? Durante el proceso de compilación, Scala detectará que existe un error puesto que el método f3() no forma parte de la definición de la clase ExternalClass. Antes de emitir un error, el compilador buscará una conversión implícita en la que se convierta el objecto receptor de la llamada del método f3() en un objeto que realmente posea dicho método. En nuestro ejemplo newMethodsToExternalClass es el elemento que nos permite que el código anterior compile correctamente. ¿Que está ocurriendo internamente? El compilador estará emitiendo un código similar al siguiente:

newMethodsToExternallClass(externalClass).f3()

Como último ejemplo para esta entrada, revisaremos cómo podemos hacer uso de parámetros implícitos en las llamadas a funciones. Incorporemos el siguiente método a la definición de la clase User

def encodePassword(password:String)(implicit encoder: Encoder) : String = {

encoder.encode(password)
}

Fijémonos que hemos marcado el segundo argumento de la función como implicit por lo que el compilador intentará completar las llamadas a la función añadiendo los parámetros necesarios. Definamos un encoder de ejemplo (que no realiza ningún tipo de operación útil) y utilizemos


implicit object IdentityEncoder extends Encoder {
  override def encode(password:String): String = password
}



import UserImplictis.IdentityEncoder


user2.encodePassword("foo")

Como podemos observar en el fragmento del código anterior en ningún momento estamos indicando el segundo parámetro de la función encodePassword sino que es el propio compilador el que está intentado incluir en la lista de parámetros alguno de los implictis disponibles de manera que la llamada de la función sea correcta.

Ha sido una entrada un poco larga pero he considerado que todos los ejemplos encajarían mucho mejor en una misma entrada de manera que pudieramos realizar un comparativa de los diferentes puntos de utilización de este poderoso mecanismo.

Importante: los snippets de código que aparecen a lo largo de esta entrada no están listos para ejecutarse en el REPL por lo que os aconsejo que, si quereis jugar con el código fuente, utilicéis aquel que está disponible aquí (realizaré el merge de esta rama sobre master tan pronto como me sea posible).

Hasta pronto!

Migue


lunes, febrero 20, 2012

Scala implicits (I)

Durante numerosas entradas hemos estado hablando del lenguaje de programación Scala, analizando algunas de sus principales características y/o la interacción con otros lenguajes: conceptos básicos, traits, parser combinators, combinando AspectJ y Scala, . . . Podéis encontrar todas las entradas relacionadas en el siguiente enlace: http://miguelinlas3.blogspot.com/search/label/Scala y un pequeño documento en el que se realiza una somera introducción al lenguaje: https://github.com/migue/blog-examples/tree/master/scala-intro

Durante las siguientes entradas me gustaría ahondar en algunas características más "avanzadas" del lenguaje como "type parametrization", "abstract members" o "implicitis" (estás últimas las abordaremos durante la entrada que nos ocupa).

El principal problema que se nos presenta cuando trabaja mos con una librería de terceros es la dificultad de extender y/o modificar el comportamiento de la misma. A lo largo de los diferentes lenguajes existentes podemos encontrar numerosas alternativas, cada una con sus ventajas e inconvenientes, como las open classes de Ruby o el ExpandoMetaclass de Groovy.

La respuesta de Scala al problema que nos ocupa son los implicits. El compilador intentará incluir en nuestro código fuente aquellos implicits que se encuentren disponibles con el objetivo de solventar cualquier "problema de tipos". De manera general, se rigen por las siguientes reglas:

  • Sólo estarán disponibles para el compilador aquellas definiciones que nosotros marquemos de manera explícita como implicit.
  • Los implicits deben estar disponibles en el ámbito de uso.
  • El compilador solamente intentará incluir un implicit de cada vez
  • El compilador no reemplazará código correcto.
  • ¿Dónde se utilizan los implicits?
    • Conversiones a un tipo determinado
    • Conversiones del objeto que recibe la llamada a un método
    • Parámetros de funciones
Hemos visto una pequeña introducción sobre la teoría de los implicits en Scala, las reglas que rigen el funcionamiento de los mismo y las aproximaciones que otros lenguajes utilizan. Puede que con este contenido la entrada se quede un poquito escueta pero he preferido dividirlo en dos entradas independientes por lo que en el próximo post realizaremos un ejemplo práctico y veremos las aplicaciones reales de esta potente (y a veces compleja) funcionalidad que el lenguaje nos brinda.

Hasta pronto!

Migue

lunes, noviembre 28, 2011

Algunos libros sobre Java y JVM

Durante estas últimas semanas he tenido la ocasión de terminar varios de los libros que tenía pendientes así como de realizar alguna nueva adquisición que, de momento, está encolada en mi lista de lectura.

Me gustaría aprovechar esta entrada para compartir con vosotros algunos de los libros anteriores que, bajo mi punto de vista, podrían resultar interesantes para tod@s aquell@s que os dediquéis, de algún modo, a trabajar con la plataforma Java (lenguaje y/o máquina virtual).

El primero de ellos es Java Performance from The Java Series. Escrito por Charlie Hunt, JVM performance lead engineer en Oracle, y  Binu John, senior performance engineer en Ning. Ha sido mi última adquisición y únicamente he podido leer los dos primeros capítulos pero creo que estamos ante uno de las lecturas obligatorias para tod@s aquell@s que se dedican y/o les gusta el mundo del rendimiento de aplicaciones sobre la JVM.

JVM Tuning, profiling, mediciones, herramientas, escritura de benchmarks, internals de la JVM, algoritmos de recolección de basura y tuneo de los mismos, gestión de la memoria,  . . .  son sólo algunas de las temáticas tratadas en este magnífico manuscrito.


Otro magnífica referencia  relativa al mundo Java: The well-grounded Java developer. Se trata de un early access por lo que sólo he podido leer los capítulos disponibles hasta el momento. Pensado para desarrolladores con unos sólidos conocimientos de Java el libro abarca un amplio espectro de temáticas como pueden ser:
  • Algunas de las novedades de Java 7
  • Nuevo mecanismo de IO y concurrencia
  • Classfiles y bytecode
  • Inyección de dependencias
  • Lenguajes alternativos en la JVM: Clojure, Groovy y Scala
  • Integración continua
  • . . .
En lineas generales creo que se trata de una lectura recomendada (a la espera de que se publique la segunda mitad del libro). Los capítulos pueden ser leidos de manera independiente, saltándose aquellos en los que ya se conoce de manera profunda la temática tratada en el mismo.

Un libro que había leido hace tiempo y que he vuelto a leer hace  unas semanas ha sido Java Concurrency in Practice. Poco puedo decir de este libro que no se haya dicho ya. Otra de las lecturas obligatorias para todos aquellos desarrolladores que trabajen con el lenguaje Java.

Ofrece una visión práctica de cómo se deben escribir programas escalables y seguros aprovechando la capacidad de las nuevas arquitecturas hardware.









Functional Programming for Java Developers. Escrito por Dean Wampler. Es un libro en el que se introducen los conceptos básicos de la programación funcional (que está "renaciendo" gracias a lenguajes como Scala o Clojure, además de sus beneficios a la hora de escribir programas concurrentes entre otras muchas cosas).

Es un libro muy cortito aunque personalmente no me ha entusiasmado demasiado. Si no tenéis ninguna experiencia en programación funcional puede ser un buen punto de partida. No se cubren mónadas, laziness ni combinators.

jueves, octubre 06, 2011

Escalabilidad, disponibilidad y estabilidad

En algunas ocasiones hemos hablado de temas de escalabilidad y disponibilidad de sistemas aunque siempre desde un punto de vista muy práctico, haciendo referencia a los problemas que ocupan nuestro día a día. Me gustaría recopilar, a lo largo de una serie de posts, algunos de los patrones y/o tecnologías más populares relacionados con la escalabilidad, la concurrencia o la alta disponibilidad.

Me gustaría dejar claro desde un principio que lo aquí expuesto son simplemente mis ideas, adquiridas a lo largo del tiempo gracias a la experiencia acumulada en la construcción de diversos sistemas software, mis estudios de doctorado, leer y practicar mucho, aprender de los maestros y pasarme muchas, pero muchas muchas,  horas aporreando el teclado. Por tanto, las cosas que aquí aparecen podrían ser erróneas y/o incompletas.

Me gustaría comenzar con una tecnología relacionada con la concurrencia: la memoria transaccional (STM en adelante).

Escribir programas concurrentes es una tarea sumamente complicada y, habitualmente, no determinista; ¿cuantas veces habremos probado un programa multihilo en nuestros sistemas de integración continua y , tras poner el sistema en producción, nos hemos encontrado con nuevos errores? Tan real como la vida misma.

Habitualmente las inmutabilidad (hablaremos de ella en el futuro) es un gran aliado a la hora de escribir programas concurrentes dado que resulta mucho más sencillo razonar sobre información cuyo estado permanece estable. Sin embargo, como en la mayoría de las situaciones, la inmutabilidad no es una bala de plata que soluciona todos los problemas por lo que tendremos que lidiar con información cuyo estado puede variar y con toda la problemática asociada a la misma. Llegados a esta situación, y siguiendo con el enfoque con el que estamos más familiarizados, estaríamos peleándonos con bloqueos, granularidad de los mismos, etc.

Un vistazo a la STM

La STM puede ayudarnos a resolver el problema anterior de manera mucho más sencilla visualizando la memoria (heap + pila) como si de un dataset transaccional se tratara. Algunas de las características más relevantes:

  • Concepto muy similar al de una base de datos tradicional. Se llevan a cabo operaciones como begin, commit o abort/rollback
  • En caso de que se produzca una colisión la transacción será relanzada de manera automática
  • Se realiza un proceso de rollback sobre la memoria cuando se produce una operación de abort.
  • Las transacciones se pueden componer y anidar

Un aspecto sumamente importante que debemos tener en cuenta a la hora de utilizar la memoria transaccional es que todas las operaciones englobadas en el ámbito de una transacción deberán ser idempotentes (anteriormente hablábamos del relanzamiento automático en caso de que se produjera una colisión).

Existen numerosas implementaciones disponibles para la máquina virtual de Java (JVM) entre las que destacan:

En la siguiente entrada realizaremos varios ejemplos prácticos de uso de la memoria transaccional y analizaremos cuales son los escenarios más propicios para la utilización de esta solución.

Hasta pronto!

Migue

miércoles, junio 29, 2011

AspectJ y Scala (y III)

Última entrada (en esta ocasión mucho menos ladrillo) de esta mini-comparativa que estamos llevando a cabo entre AspectJ y Scala (podéis consultar las anteriores entradas en los siguientes enlaces: AspectJ y Scala (I), AspectJ y Scala (II).

Hemos estado revisando cómo llevar a cabo la aplicación de varias funcionalidades como el uso de cachés o la gestión de transacciones mediante el uso de AspectJ y Scala, haciendo hincapié en las ventajas y desventajas de las dos aproximaciones. Nótese que hemos estado hablando de funcionalidad transversal como las transacciones y la caché pero no hemos hablado de otro tipo de funcionalidades las cuales abarcan un espectro mucho más amplio como por ejemplo FFDC (First Faiulure Data Capture), Profiling, gestión de la concurrencia, etc. En este tipo de situaciones el uso de Scala y HFOs resultaría mucho más tedioso que los ejemplos vistos en las entradas anteriores; derivando en los síntomas clásicos de funcionalidad "difuminada" por toda nuestra apliación.

¿Conclusión? El uso de cualquiera de las dos aproximaciones que hemos estado analizando dependerá, como en el 90 por ciento de todas las situaciones, del problema que estemos resolviendo, por lo que tendremos que escoger la "herramienta" más adecuada para el problema que se nos plantea.

¿Y por qué no un mixin de las anteriores? Esta es una de las alternativas que más me convence:
  • Compilamos nuestros programas Scala de la manera habitual.
  • Definimos nuestros aspectos para llevar a cabo la funcionalidad transversal deseada.
  • Realizamos un proceso de binary weaving (ya sea build time o load time)
Mediante el mecanismo anterior estaremos aplicando nuestros aspectos a nuestro código Scala compilado.

En mi cuenta de GitHub podréis encontrar dos proyectos Eclipse (scala-simple-service y aspectj-advice-scala) que deben ser utilizados de manera conjunta. Mediante estos dos pequeños proyectos de ejemplo se ilustra cómo podemos utilizar conjuntamente aspectos escritos en AspectJ que hagan advice sobre código Scala compilado.

Hasta pronto!


martes, junio 28, 2011

AspectJ y Scala (II)

En la entrada anterior analizamos algunas de las similitudes y diferencias existentes entre Scala y AspectJ, llevando a cabo una implementación de una caché extremadamente simple en ambos lenguajes, describiendo las ventajas y desventajas presentes en cada una de las aproximaciones. Durante esta entrada analizaremos un ejemplo de aplicación de una funcionalidad transversal como la gestión de transacciones. 

El ejemplo que aquí nos ocupa no persigue realizar una implementación detallada de control de transacciones sino simplemente ilustrar como podríamos aplicar una funcionalidad transversal mediante el uso de AspectJ o Scala.

Del mismo modo que en el post anterior, comenzamos por la aproximación basada en AspectJ. Para ello en primer lugar definimos una simple anotación que nos sirva como elemento de marcado de aquellas clases y/o métodos que se deben ejecutar bajo un contexto transaccional.

Una vez definida la anotación anterior definiremos un aspecto que sea capaz de detectar aquellos joint point en los que deseamos establecer un contexto transaccional. 

pointcut transactionalOperation() : execution(@Transactional * * (..) );

Con el pointcut anterior estaremos capturando las llamadas realizadas sobre cualquier método anotado con nuestra anotación @Transactional. Ahora necesitaremos definir un advice en el que llevamos a cabo la gestión de la transacción (nótese que este gestor de transacciones es completamente absurdo y simplemente imprime por pantalla la funcionalidad real que debería ser llevada a cabo por un gestor de transacciones operativo)


En muchas ocasiones todos los métodos de una clase necesitarán ser transaccionales, y no querremos anotar todos los métodos de dicha clase. En este supuesto podríamos utilizar anotaciones a nivel de clase que nos permitan seleccionar aquellos métodos que deseemos (por ejemplo, todos los métodos públicos).

Adicionalmente, podríamos hacer uso de los ITD y establecer la transaccionalidad a todas aquellas clases  que nosotros deseemos. Por ejemplo, supongamos que todos los nombres de nuestros servicios, los cuales deseamos que sean transaccionales, terminan con el sufijo Service. Podríamos, mediante el uso de un aspecto, añadir la anotación @Transactional a todos nuestros servicios.

declare @type : *Service : @Transactional ;

Dicho aspecto, en conjunción con el anterior, provocaría que todos nuestros servicios fueran transaccionales

En Scala, de nuevo, haremos uso de funciones de alto nivel (high order functions) de manera muy similar a la solución de la caché descrita en la entrada anterior:

Si deseamos que un método de uno de nuestros servicios sea transaccional no tendremos más que extender de la clase abstracta y utilizar la HOF anterior:

Si comparamos las soluciones aportadas por cada una de las dos alternativas tendremos que
  • En el caso de AspectJ no necesitamos anotar cada método de manera independiente puesto que podríamos hacer uso de anotaciones a nivel de clase. El nivel de granularidad que podemos alcanzar está definido a nivel de método (aunque tampoco es un problema excesivamente grande puesto que podríamos utilizar el patrón Extract Method para extraer la funcionalidad transaccional a un método sobre el cual podríamos aplicar nuestro aspecto).
  • En el caso de Scala necesitaremos recubrir nuestra lógica con la HOF definida en nuestro ejemplo anterior. En esta situación, el nivel de granularidad es notablemente superior puesto que podemos recubrir una simple parte de nuestro método
Durante esta segunda entrada hemos vuelto a confrontar Scala y AspectJ a la hora de aplicar una funcionalidad transversal, intentando plasmar las ventajas y desventajas de cada uno de ellos.

En la siguiente y última entrada de esta serie analizaremos cómo aunar lo mejor de ambos mundos en un ejemplo práctico que nos sirva como base para futuros ejemplos.

Como siempre podéis encontrar el código fuente de todos los ejemplos del blog (o casi todos :) ) en mi cuenta de GitHub, bajo el proyecto blog-examples.

Hasta pronto!

PD: perdón por el ladrillo de entrada :(.

martes, junio 21, 2011

AspectJ y Scala (I)

¿Podemos establecer alguna conexión entre estos dos mundos? Si nos detenemos por un instante veremos que comparten muchas más cosas de las que a simple vista podría parecer:
  • Son lenguajes de tipado estático.
  • Ambos producen código compatible con la máquina virtual de Java (JVM)
  • Las funciones de alto nivel de Scala comparten algunas características con los advices de AspectJ
  • Los traits de Scala comparten algunas características con el static crosscuting de AspectJ.

Durante el resto de la entrada que nos ocupa (y la siguiente) realizaremos una pequeña comparativa en la que pondremos de manifiesto el modo en que cada uno de estos lenguajes resuelve determinado tipo de problemas. Pongámonos manos a la obra.

Cachés

A través de este ejemplo se pretende presentar el modo en que ambos lenguajes solucionan el problema de aplicar una funcionalidad transversal en un punto determinado del código. Implementaremos un mecanismos de caché extremadamente sencillo de manera que podamos centrar nuestra atención en los aspectos relevantes de las alternativas que estamos planteando

En el caso de AspectJ se está tendiendo a utilizar las anotaciones como un elemento de marcado (y parece que está siendo recibido con una aceptación más que notable). Nuestro primer paso será definir la anotación con la que realizaremos el "marcado" de aquellos métodos que deseamos establecer como cacheados:

@Retention(RetentionPolicy.RUNTIME)
public @interface Cachable {
   String scriptKey () default "";
}


El atributo keyScript de la anotación anterior actuaría (en una caché real), como un pequeño lenguaje de scripting de manera que pudiera ser evaluado por la caché para generar la clave bajo la que se almacenará una determinada llamada (obviaremos esta parte para intentar no distraernos de nuestro principal objetivo).

Una vez definido el elemento de marcado, definiremos nuestro aspecto, el cual será capaz de capturar las llamadas de aquellos métodos anotados con @Cacheable y realizar la lógica necesaria de nuestra sencilla caché



En el caso de Scala llevaremos a cabo nuestra funcionalidad transversal mediante el uso de funciones de primer nivel (high order functions)

Gracias a la funcionalidad anterior, podemos pasar al método cache una función que será ejecutada en caso de ser necesario, devolviendo los valores de la caché en el supuesto de que haya sido calculado en un paso anterior.

Si comparamos las dos alternativas que hemos planteado hasta este momento:
  • En el caso de AspectJ, cada uno de los elementos que deseamos cachear tendremos que anotarlos con, valga la redundancia, la anotación que hemos definido anteriormente. Mientras tanto, en el caso de Scala, tendremos que recubrir cada uno de los métodos que deseamos cachear con la función de alto nivel (high order function).
  • En el caso de AspectJ estamos utilizando un "lenguaje" externo que actua como elementos de las claves de nuestra caché mientras que en el caso de Scala estamos utilizando el propio lenguaje de manera nativa, con el consiguiente beneficio que ello conlleva.
Esta ha sido nuestra primera aproximación a Scala y AspectJ. Durante las siguientes entradas analizaremos algunos ejemplos adicionales y veremos como podemos integrar lo mejor de ambos mundos.

Podéis encontrar el código fuente de los ejemplos anteriores en el siguiente repositorio de GitHub, concretamente en los proyectos AspectJCacheExample y ScalaCacheExample.

Hasta pronto!

    viernes, junio 03, 2011

    Mecanismo de Load Time Weaving (LTW)

    Durante la última entrada analizábamos de manera introductoria los diferentes mecanismos de tejido ofrecidos por AspectJ así como las principales características de los mismos. Durante esta entrada nos acercaremos un poquito más al mecanismo de tejido en tiempo de carga, por normal general más desconocido, desde mi punto de vista, que el mecanismo de tejido en tiempo de construcción (durante todos los ejemplos que hemos visto en anteriores entradas siempre hemos utilizado el tejido en tiempo de construcción).

    Los pasos que tenemos que seguir cuando utilizamos el tejido en tiempo de carga son los siguientes:

    • Iniciar nuestra aplicación con el archivo aspectjweaver.jar el actuando como un agente (hablaremos de agentes en otra entrada). Para ello podríamos utilizar una línea como la siguiente
    java -javaagent:/aspectjweaver.jar
    • Durante el proceso de inicialización del agente (llevado a cabo por la máquina virtual) el propio agente recupera aquellos archivos existentes en el classpath que coincidan con META-INF/aop.xml (en el caso de encontrar múltiples llevará a cabo la combinación de los mismos).
    • Carga de los aspectos indicados.
    • El agente se registra como un listener del evento de carga de clases de la máquina virtual. Mediante este mecanismo se tiene acceso a la definición de la clase, permitiendo incluso la modificación de la misma.
    • Continua el proceso normal de carga de nuestro aplicación.
    • Cada vez que una nueva clase es cargada la máquina virtual notifica al agente dicha situación. En ese momento es posible examinar la clase en cuestión y determinar si algunos de los aspectos cargados con anterioridad necesita ser tejido. En caso afirmativo, la clase será tejida con el aspecto en cuestión, retornando el bytecode resultante a la máquina virtual.
    • La máquina virtual utiliza el bytecode resultante como elemento de definición de la clase.
    Mediante el conjunto de pasos anteriores, aquellas clases que hayan hecho matching con las definiciones de nuestros aspectos tendrán incorporada la funcionalidad definida en éstos últimos.

    El agente anterior utiliza un interfaz de la máquina virtual conocido como JVMTI (Java Virtual Machine Tools Interface) que ha sido introducido a partir de Java 5. En el caso de que utilicéis una versión anterior podréis hacer uso de este mecanismo mediante una versión basada en classloaders específicos.

    viernes, mayo 27, 2011

    Weaving en AspectJ (I)

    Hace un tiempo escribí una serie de entradas relacionadas con la orientación a aspectos en general y AspectJ en particular (podéis encontrar dichas entradas aquí) que posteriormente agrupé en un documento.

    Hasta este momento nos hemos centrado en la teoría de la orientación a aspectos, cómo escribir los mismos y algunas de sus posibles aplicaciones. Sin embargo, no hemos prestado especial atención al modo en el que los aspectos se combinan con las clases para componer nuestro sistema final. Durante esta entrada y las siguientes profundizaremos en diferentes aspectos del proceso de tejido (weaving).

    El mecanismo de tejido es aquel que nos permite combinar la definición de nuestras clases y de nuestros aspectos en un ente ejecutable que conforma nuestro sistema final.

    Podemos clasificar los diferentes modelos en base al momento en el que llevan a cabo el tejido. En este caso tendríamos:

    • Build time. Las clases y los aspectos son combinados durante el proceso de construcción de nuestra aplicación.
    • Load time (LTW). En este caso el tejido se realiza en el momento en el que una clase se carga en la máquina virtual, por lo que en este caso no será necesario llevar a cabo ningún proceso adicional en nuestro proceso de construcción. 
    Otra posible clasificación podría establecerse en función del los tipos de entrada del proceso de weaving
    • Source code weaving. La entrada al proceso son archivos de código fuente (clases y aspectos)
    • Weaving binario (bytecode). La entrada al proceso es el bytecode obtenido como resultado del proceso de compilación de nuestras clases
    De manera esquemática, a continuación se reflejan las diferente combinaciones ofrecidas por el weaver de AspectJ:

    • Tiempo de construcción: las entradas admitidas son tanto código fuente como clases compiladas (bytecode)
    • Tiempo de carga: las entradas pueden ser bytecode o un archivo xml (lo veremos más adelante)

    Con esta pequeña entrada nos hemos aproximado de manera sencilla a los procesos de tejido presentes en AspectJ de una manera teórica. En futuras entradas analizaremos con un poco más de detalle los contenidos aquí presentados, haciendo especial hincapié en los mecanismos de tejido en tiempo de carga.

    Hasta pronto!

    sábado, mayo 07, 2011

    Parser Combinators: Parsing simple bytecode

    Anteriormente, http://miguelinlas3.blogspot.com/2011/03/parser-combinators-un-poco-de-teoria.html, introducíamos de manera somera el concepto de parser combinators y parte de la teoría relacionada con los mismos. Durante esta entrada vamos a construir un sencillo parser que nos permita analizar un conjunto mínimo de instrucciones de la máquina virtual java (simplemente permitiremos la definición de funciones y la capacidad de ejecución de las mismas).

    No analizaremos en detalle el funcionamiento de la máquina virtual java ni el conjunto de instrucciones  de la misma (podríamos llevarnos algo más que unos cuantos posts :) ) dado que el objetivo principal de esta entrada es profundizar en el concepto de parser combinators y presentar un ejemplo de uso real de los mismos. Pongámonos manos a la obra.

    El código fuente que nos servirá como conductor de la entrada será el de la función factorial mostrado a continuación:

    static int fact(int);
    Code:
    Stack=2, Locals=3, Args_size=1
    0: iconst_1
    1: istore_1
    2: iconst_1
    3: istore_2
    4: iload_2
    5: iload_0
    6: if_icmpgt 19
    9: iload_1
    10: iload_2
    11: imul
    12: istore_1
    13: iinc 2, 1
    16: goto 4
    19: iload_1
    20: ireturn

    Podréis objener el código desensamblado como el mostrado anteriormente mediante el comando javap -v Class donde Class represente un archivo bytecode.

    El proceso de parsing se lleva a cabo mediante el uso de parser combinators (no perdamos de vista el objetivo de nuestra entrada ;) ) y presenta las siguientes características

    1. Para cada una de las diferentes tipos de instrucciones soportadas definimos un parser combinator que es el encargado de parsear este tipo de instrucciones.
    2. Cada uno de los parsers anteriores sabe cómo generar un objeto del tipo correspondiente. Se utiliza una jerarquía de tipos en la que cada una de las instrucciones bytecode soportadas está representada mediante una clase Scala.
    3. El parser global construye un AST (en este caso es un simple lista de objetos) y un conjunto de infraestructura adicional con el objetivo de implementar un intérprete que ejecute la secuencia de instrucciones obtenida (fuera del ámbito de esta entrada podéis echarle un vistazo al código fuente)
    ¿Cómo actúan nuestros diferentes parsers? Analicemos por ejemplo el componente encargado de parsear las instrucciones ISTORE

    lazy val beginInstructionFragment: Parser[Int] = integer <~ ":" ^^ { _.toInt}

    lazy val iStoreBytecode: Parser[List[BytecodeInstruction]] = beginInstructionFragment ~> "istore_" ~ integer ^^ {
        case name ~ value => new IStoreInstruction(name,value)  :: List()
      }

    El parser beginInstructionFragment es el encargado de parsear la primera parte de la instrucción, la que corresponde al lugar que ocupa la instrucción en el array de instrucciones. 

    El segundo parser hace uso del primero y además sabe cómo parsear el texto restante de manera adecuada. En este caso nuestro este analizador es capaz de parsear las instrucciones istore_N donde N representa un número entero positivo cualquiera y devolver un objeto (dentro de una lista) de tipo IStoreInstruction en el que se incluyen el nombre de la instrucción y el entero correspondiente.

    Como seguramente a muchos de los lectores de este post el fragmento de código anterior les suene un poco raro intentaremos analizar las diferentes opciones de combinaciones de parsers disponibles:
    • Si escribimos un parser en el que utilizamos una cadena, como por ejemplo "istore", el resultado es la propia cadena.
    • Podemos utilizar expresiones regulares, como por ejemplo, "[A-Z]".r . En este caso el resultado también sería la propia cadena
    • Si utilizamos el símbolo ~ (composición secuencial) el resultado del proceso de parseo será la combinación de ambos resultados. Por tanto, si A devuelve "X" y B devuelve "true", el resultado de A~B sería  ~("X","true") (el tipo de este resultado sería una case class de tipo ~ . Adicionalmente podemos hacer uso de los operadores <~ y ~> los cuales mantienen los valores a la izquierda y a la derecha respectivamente.
    • Otro elemento de combinación de parsers sería | . En este caso el valor de retorno sería el de aquel parser que pudiera parsear la entrada con éxito.
    • rep(P) o rep(P,separator) retornan una lista de las múltiples ejecuciones de P.
    • opt(P) retorna una instancia de tipo Option.
    Adicionalmente a la combinación de los parsers, necesitaremos un mecanismo que nos permita reescribir la salida de los mismos, en nuestro caso para instanciar los objetos de tipo necesario y configurar nuestro AST. Para ello tendremos que hacer uso del operador de transformación ^^.

    Una definición formar del operador anterior sería: Dado un parser P y una funcion f cualesquiera, una expresión del tipo P^^f implica que para cada resultado R de P el resultado de la expresión global es f(R) 

    En el ejemplo que estamos tratando nuestra función es un simple pattern matching que transforma la case class retornada por nuestra combinación de parsers en un objeto de tipo IStoreInstruction.

    No me ha resultado sencillo ponerlo por escrito y tampoco estoy seguro de que me haya explicado con la mayor claridad posible. Si estáis interesados os recomiendo que clonéis el repositorio alojado en GitHub

    git://github.com/migue/parser-combinators.git

    Hasta pronto!

    miércoles, marzo 30, 2011

    Parser Combinators: un poco de teoría

    Puede que en algunas ocasiones os hayais tenido que enfrentar con la necesidad de escribir un pequeño lenguaje que realice una tarea muy concreta (profundizaremos en el tema de los DSLs en un post futuro): como, por ejemplo, procesar determinados archivos de configuración de vuestra aplicación o la definición de interfaces de usuario de manera sencilla.

    Independientemente de las razones por las que estamos desarrollando este componente, necesitaremos un parser que nos ayude a transformar el lenguaje de entrada en una estructura de datos que nuestro programa pueda comprender y procesar. Algunas de las alternativas que se nos plantean son:
    • Escribir nuestro propio parser (conllevaría escribir también el analizador léxico). Si no somos expertos en la materia estaremos enfrentándonos a una tarea relativamente complicada.
    • Utilizar herramientas para la generación de parsers como Antlr,Bison o JavaCC entre otras muchas. En este caso la dificultad estriba en la necesidad de aprender a manejar una nueva herramienta/lenguaje e integrarlo en nuestro desarrollo y ecosistema.
    Durante esta entrada, y posiblemente la siguiente, vamos a presentar un enfoque alternativo a las dos opciones anteriores. En lugar de utilizar un DSL externo como podría ser el ofrecido por Antlr, vamos a utilizar un DSL interno. Dicho DSL estará formado por parser combinators (funciones y operadores definidos en Scala que servirán como base para la construcción de nuestros futuros parsers).

    Siendo originales :), imaginemos que deseamos cosntruir un parser de expresiones aritméticas de números enteros. En primer lugar, definamos la gramática de nuestro lenguaje:

    expr  ::= term {"+" term | "-" term}. 
    term  ::= factor {"*" factor | "/" factor}.
    factor  ::=  integer | "(" expr ")".

    El fragmento de código anterior representa una gramática libre de contexto (no vamos a profundizar en este tema porque tendríamos que escribir miles de posts) que modela nuestro lenguaje de expresiones aritméticas de números enteros. ¿Y ahora?

    Una vez definida la gramática anterior hemos llevado a cabo la tarea más complicada de todas. Si utilizais los parser combinators ofrecidos por Scala tendremos casi todo el trabajo sucio realizado. A modo de ejemplo:

    import scala.util.parsing.combinator._

    class IntegerArithmetics extends JavaTokenParsers {
       def expr: Parser[Any] = term~rep("+"~term | "-"~term)
       def term: Parser[Any] = factor~rep("*"~factor | "/"~factor)
       def factor: Parser[Any] = integer | "("~expr~")"
    }

    Si comparamos el código Scala con la definición en notación EBNF de nuestra gramática observaremos que podríamos inferir nuestro código fuente Scala sin más que realizar una serie de reemplazos en nuestra notación EBNF:
    1. Cada regla se convierte en un método por lo que tendremos que prefijarlas con def.
    2. El tipo de retorno de cada uno de los métodos anteriores es Parser[Any] (veremos en la siguiente entrada que significa esto) por lo que tendremos que cambiar el símbolo "::=" por ":Parser[Any] ="
    3. Insertar el símbolo ~ entre todos los elementos de cada una de las reglas (en la notación EBNF esto es implícito)
    4. La repetición se refleja mediante el uso de rep(...) en lugar de {...}
    5. El punto al final de cada regla no es necesario (podríamos poner ; si lo deseamos)

    Esto no ha sido más que una toma de contacto con el mundo de los parser combinators en Scala. En la siguiente entrada descubriremos cómo realizar construcciones más complejas, formatear la salida de manera que podamos construir las estructuras de datos requeridas para nuestro procesamiento o ejecutar nuestros parsers. 

    Para todo ello diseñaremos y construiremos un pequeño parser que nos permita analizar el bytecode de la máquina virtual una vez desemsamblado (para los más inquietos javap -v ClassFile (sin .class))

    miércoles, febrero 23, 2011

    Introducción a Scala

    El siguiente documento es un pequeño resumen (introductorio) sobre el lenguaje de programación Scala y alguno de sus beneficios. No es nada nuevo que no podáis encontrar en los libros de referencia :) pero a lo mejor a alguien le sirve para dar sus primeros pasos en este genial lenguaje de programación. 

    Espero que os guste:

    martes, junio 22, 2010

    Aspect Oriented Programming Intro Guide

    Hace tiempo que habíamos comenzado una serie de post relativos a la programación orientada a aspectos y los tenemos dispersos a lo largo de diferentes entradas. He decido recolectar todos los post publicados junto a los que estaban por venir y hacer una sola entrega.

    Espero que os guste:


    Está escrito en docbook por lo que si alguien está interesado en los fuentes no tiene más que pedírmelos.

    Hasta pronto!

    sábado, junio 19, 2010

    AOP: crosscutting (II)

    Durante la última entrada de la serie relacionada con la programación orientada a aspectos analizamos el modo en el que se puede alterar el comportamiento de un sistema, analizando las categorías de advices y realizando una comparativa con los métodos de una clase. A lo largo de esta entrada realizaremos un análisis en profundidad de los advices.

    Before advice
    Este tipo de advices se ejecutan antes de la ejecución del joint point sobre el que actúan. En el siguiente ejemplo:

                  before():execution(@Secured * * (..)){
                // asegurarse de que el usuario puede realizar la operación
                  }                   

    el advice realiza una comprobación de seguridad antes de que se produzca la ejecución de cualquier método anotado con Secured.

    En caso de que el advice dispare un excepción, el joint point no se ejecutará. Este tipo de advices son comúnmente utilizados en aspectos tales como seguridad o trazabilidad.
    After advice

    Se ejecutan después de la ejecución del joint point sobre el que actúan.Dentro de esta categoría, AspectJ ofrece tres tipos de advices:
    • Ejecución del advice independientemente del resultado de la ejecución del joint point.
    • Ejecución del advice única y exclusivamente si el joint point ha finalizado correctamente.
    • Ejecución del advice después que el joint point haya disparado una excepción.
    Veamos en detalle cada uno de los tres tipos anteriores:

    1.- Advice After
    Este tipo de advices se ejecutan independientemente del resultado de la ejecución del joint point sobre el que actúan.Habitualmente se conoce a este tipo de advices como after finally puesto que su semántica es similar a la de un bloque finally.

    El siguiente advice:
                    after(): call(@Logging * ServiceManager.*(..)){
                       // registrar el resultado de la operación
                   }

    registra el resultado de todas las operaciones de la clase ServiceManager que estén marcadas con la anotación Logging, independientemente si retornan correctamente o terminan su ejecución de forma inesperada mediante el disparo de una excepción.

    2.-Advice After Returning
    En muchas ocasiones,será necesario ejecutar el código de nuestro advice única y exclusivamente cuando la ejecución del joint point haya terminado de forma correcta. Continuando con el ejemplo anterior:
                   
                after () returning: call(@Logging * ServiceManager.*(..)){
                  // registrar el resultado de la operación
                }

    se seguirá registrando el resultado de las operaciones, siempre y cuando, la ejecución haya terminado correctamente, sin el disparo de ninguna excepción.

    AspectJ ofrece una pequeña variante para este tipo de advices:

    after() returning (ReturnType returnObject)

    gracias a la cual se permite recuperar el objeto retornado por la ejecución del joint point dentro del advice. Veamos un pequeño ejemplo ilustrativo:
                               
            after() returning (java.sql.Connection connection):
                call(java.sql.Connection DriverManager.getConnection(..)){
                        System.out.println("Se ha recuperado la conexión "
                                       + connection);
           }

    Es importante tener claro que no se puede retornar un objeto nuevo (sí puede ser modificado pero no retornar uno nuevo).

    3.- Advice After Exception
    Este tipo de advices son similares a los descritos en el apartado anterior. En este caso, el advice se ejecutará única y exclusivamente cuando el joint point dispare una excepción. Presentan la siguiente estructura:
                                
    after() throwing:execution (* ServiceManager+.*(..))
                           
    El advice del ejemplo anterior se ejecutará siempre y cuando algún método de la clase ServiceManager (o alguna de sus subclases) dispare una excepción. En el supuesto de que la ejecución del joint point termine correctamente, este tipo de advices no serán ejecutados.

    Al igual que los advices del apartado anterior, AspectJ ofrece un modo de recuperar la excepción que ha sido disparada por el joint point de manera que esté disponible en el cuerpo del advice. Siguiendo una sintaxis similar a la anterior, tendríamos:
                              
    after() throwing (ExceptionType exceptionObject):
                           
    Un after throwing advice nunca podrá tragarse la excepción, por lo que seguirá subiendo por la pila de llamadas hasta llegar al objeto que realizó la invocación del joint point.

    Around advice
    Este clase de advices engloban al joint point, pudiendo ejecutar la lógica del mismo un número indefinido de veces. Incluso pueden omitir la ejecución del propio joint point. Algunos de los usos principales de este tipo de advices son los siguientes:
    • Ejecución de lógica adicional antes y después de la ejecución de un joint point, como por ejemplo, acciones de profiling.
    • Omitir la ejecución original, y realizar otra en su lugar, como por ejemplo, operaciones con cachés.
    • Envolver la operación con el objetivo de aplicar una política de gestión de excepciones. Un ejemplo de este uso sería la gestión de transacciones.
    Este advice ofrece una potencia superior a todos los advices vistos hasta el momento, puesto que podrían sustituir a los anteriores. De todos modos, se considera una buena práctica utilizar el advice más sencillo que cumpla las necesidades de la tarea que necesita ser llevada a cabo.

    Si desde el around advice se desea llevar a cabo la ejecución del joint point, será necesario hacer uso de la palabra reservada proceed() dentro del cuerpo del advice. Recuérdese que, puesto que la invocación de proceed() ejecuta el joint point, deberán pasarse el mismo número de argumentos que han sido recolectados por el advice.
    Asimismo, puesto que la invocación de proceed() supone la ejecución del joint point, el valor de retorno será el retornado por éste último.

    A continuación se adjunta un pequeño ejemplo de utilización de advices de este tipo:
                              
            void around(User user,int credits)
                       throws InsufficientCreditsException:
                    call(* User.pay*(int)) && target(user) & &  args(credits){
                        try
                        {
                            proceed(user,credits);
                        }catch(InsufficientCreditsException ex){
                            if(!processException()){
                                throw ex;
                            }
                        }

    Analicemos en detalle la construcción anterior:
    1. El pointcut selecciona cualquier llamada a los métodos de la clase User cuyo nombre comience por pay y disparen una excepción de tipo InsufficientCreditsException.
    2. La segunda parte del pointcut recolecta el contexto del joint point: el usuario sobre el que se está realizando la llamada y el número de créditosque se están pasando como argumento del método que se está ejecutando.
    3. En el cuerpo del advice, se engloba la ejecución del método con un bloque de gestión de excepciones, para realizar una protección adicional en caso de que se produzca una excepción. En el caso de que la protección adicional no sea correcta, la excepción será disparada de nuevo.
    Todos los around advices deben declarar un valor de retorno (pudiendo ser void). Habitualmente el tipo de retorno de éstos se corresponde con el tipo de retorno de los joint points sobre los que está actuando.
    En algunas ocasiones, todos los joint points sobre los que actúa el advice no presentan el mismo tipo de retorno, como puede ocurrir cuando estamos añadiendo soporte transaccional a diferentes  operaciones. En estas situaciones el tipo de retorno que debe declarar el advice será Object. AspectJ acomodará el valor de retorno de acuerdo a las  siguientes reglas:
    • Si se está retornando un tipo primitivo, AspectJ realizará el boxing/unboxing correspondiente. Esta característica es similar a la incluida a partir de Java 5, pero AspectJ no precisa de dicha versión de Java para realizar la operación.
    • En el caso en el que el tipo de retorno no sea primitivo, AspectJ realizará los casts oportunos antes de retornar el valor.
    Muchas ocasiones es necesario acceder a los objetos que conforman la ejecución del joint point para que el advice pueda llevar a cabo la lógica correspondiente. Por tanto, los pointcuts, necesitan exponer el contexto disponible en la ejecución del joint point de modo que pueda estar disponible en el cuerpo del advice. Dicho contexto puede definirse de dos modos diferentes:
    • Objetos (incluyendo los tipos primitivos) que conforman el joint point.
    • Anotaciones asociadas al joint point.
    La siguiente tabla describe el conjunto de pointcuts que AspectJ ofrece para recuperar el contexto en los joint points.

    Table 1.14. Pointcuts para recuperar el contexto en un joint point
    Pointcut Contexto recuperado
    this(obj) Objeto this en el joint point que se está
    ejecutando
    target(obj) Objetivo de la llamada en el joint
    point
    que se está ejecutando. En el caso de un
    joint point de una
    llamada a un método, el target será el objeto que realiza la llamada.
    Para la ejecución de un método, el target será el objeto this. En los accesos a campos, el target será el objeto que se está
    accediendo. En el resto de joint
    points
    no existe un target disponible
    args(obj1,obj2,...) Objetos que representa los argumentos en el joint point. Para las
    llamadas/ejecuciones de métodos/constructores, recupera los
    argumentos de los mismos. En el caso de los manejadores de
    excepciones, recupera la excepción producida. Para los accesos
    en modo escritura a un campo, recupera el nuevo valor del
    campo.
    @this(annot) Anotación asociada con el tipo del objeto this del joint point
    @target(annot) Anotación asociada con el tipo del objeto target del joint point
    @args(annot1, annot2,
    ...)
    Anotación asociada con el tipo de los argumentos del joint point
    @within(annot) Anotación asociada con el tipo "enclosing" del joint point
    @withincode(annot) Anotación asociada con el método "enclosing" del joint point
    annotation(annot) Anotación asociada con el asunto actual del joint point.

    Durante la próxima entrada analizaremos el concepto de Aspecto.

    Hasta pronto!

    domingo, mayo 30, 2010

    AOP: crosscutting (I)

    A lo largo de las entradas anteriores hemos analizado el modelo de joint point de AspectJ y la manera de definir las reglas que permitan seleccionar aquellos joint points de nuestro interés. 
    Durante esta entrada analizaremos el modo en el que se puede alterar el comportamiento de un sistema en los joint points seleccionados mediante la definición de los pointcuts.

    Descripción general


    Las reglas de tejido están compuestas de dos partes:

    • advice: qué deseamos hacer.
    • pointcuts: dónde aplicamos el advice anterior.

    AspectJ soporta el crosscutting dinámico mediante el concepto de advices, construcciones similares a los métodos gracias a los cuales se permiten definir las acciones a ejecutar en los joint points seleccionados por un pointcut.

    Categorías de advices


    Dependiendo de las funcionalidades que se estén implementando será necesario llevar a cabo la lógica en un determinado lugar del flujo de ejecución original; así por ejemplo, si se está construyendo la seguridad de un sistema, el código tendrá que verificar dicha seguridad antes de la ejecución del joint point. En un sistema de cachés, la nueva funcionalidad tendría que ejecutarse alrededor del joint point original, intentando recuperar el valor de la caché, y en caso de que no exista, ejecutar el código real y añadirlo a la misma para futuras invocaciones. AspectJ ofrece tres categorías de advices que satisfacen los escenarios anteriores (y alguno más):

    • Before Advice: se ejecutan anteriormente a la ejecución del joint point
    • After Advice: se ejecutan posteriormente a la ejecución del joint point. Existen tres variantes diferentes
      • After finally: se ejecuta tras la ejecución del join point independientemente del resultado de la misma.
      • After returning: se ejecuta tras la ejecución del joint point siempre y cuando ésta última haya finalizado correctamente, es decir, sin lanzar ninguna excepción.
      • After throwing: se ejecuta tras la ejecución fallida de un joint point, es decir, después de que dicho joint point dispare una excepción.
    • Around Advice: rodean la ejecución del joint point.


    Sintaxis de los advices


    Aunque la sintaxis varía ligeramente dependiendo del tipo de advice que se esté escribiendo, se podría dividir su estructura general en tres partes claramente diferenciadas:

    • Declaración del advice. En esta parte de la declaración se especifica el momento de ejecución del advice, es decir, si se ejecutará antes, después o alrededor de los joint points.
    • Definición de los pointcuts. Se especifican los pointcuts sobre los que se desea actuar.
    • Cuerpo del advice. Definición del código a ejecutar una vez se haya alcanzado el joint point indicado.

    Veamos, por partes, un ejemplo sencillo de definición de un advice:


    •  En primer lugar se define un sencillo pointcut :

    pointcut secureOperation(User user): call( * User.*(..)) && target(user)


    • En el pointcut anterior estamos capturando todas las llamadas a cualquier método de la clase User, y, adicionalmente estamos recogiendo el objeto que actúa como target de la llamada. A continuación veremos un around advice para ilustrar la sintaxis:

                Object around(User user):secureOperation(user){
    System.out.println("Securing operation on user " 
    + user.toString());
                    Object retValue = proceed(user);
    System.out.println("Finished secured operation on user " 
    + user.toString());
    return retValue;
               }

    • En la definición anterior se puede ver la estructura de la declaración de un advice descrita anteriormente:
      • La parte que precede a los dos puntos indica el momento de ejecución del advice (after,before,around). En este caso, se ejecutará alrededor del joint point seleccionado.
      • La parte que sigue a los dos puntos representa el pointcut, es decir, la definición de los criterios que determinan cuándo se ejecutará el advice.
      • La última parte representa el cuerpo del advice, es decir, el código que se ejecutará cuando alguno de los joint point definidos por el pointcut sea alcanzado.

    Advices y métodos

    Al igual que los métodos de una clase, los advices se utilizan para definir comportamiento. La sintaxis de éstos últimos es similar a la de los métodos aunque existen algunas diferencias dado que los advices son aplicados de manera automática, sin la necesidad de realizar explícitamente la invocación del mismo.

    A continuación se analizan las similitudes entre ambos en tres categorías diferentes: declaración, cuerpo y comportamiento. La declaración de un advice es similar a la signatura de un método tradicional en que:
    • Opcionalmente puede asignarse un nombre al advice mediante el uso de la anotación @AdviceName.
    • Recibe argumentos a través del contexto del joint point, que posteriormente podrán ser utilizados en el cuerpo para implementar la lógica necesaria.
    • Puede declarar el lanzamiento de una excepción.
    • El cuerpo de los advices también es muy parecido al de los métodos puesto que:
    • El código del cuerpo del advice sigue las mismas reglas de acceso a miembros de otros tipos y/o aspectos.
    • Se puede referenciar a la propia instancia del aspecto mediante el uso de this.
    • Los advices de tipo around pueden retornar un valor.
    • Los advices deben declarar las excepciones que sean checked que la implementación podría disparar.
    • En la categoría relativa al comportamiento, los advices :
    • No pueden declarar el disparo de una excepción que no está declarada en TODOS los joint points sobre los que actúa.
    • Pueden omitir algunas de las excepciones de tipo checked que han sido declaradas por alguno de los joint point sobre los que actúa.
    • Pueden declarar el disparo de excepciones más específicas (de tipo checked) que las definidas por los joint point sobre los que está actuando.
    • Pueden lanzar cualquier tipo de excepción de tipo runtime. 

    En comparación con los métodos, los advices presentan las siguientes diferencias:

    • La declaración de un nombre es opcional.
    • No pueden ser invocados directamente.
    • No presentan especificadores de acceso (relacionado con la característica de que no pueden ser invocados directamente).
    • No presentan un tipo de retorno en los advices de tipo before y after.
    • Tienen acceso a unas cuantas variables dentro del propio aspecto: thisJointPoint, thisJointPointStaticPart, thisEnclosingJointPointStaticPart.
    • Se puede utilizar la palabra reservada proceed en los advices de tipo around para ejecutar el joint point sobre el cual se está realizando el advice.

    La siguiente entrada realizará un análisis más detallado de los advices y cómo se puede acceder a los contextos del joint point.


    Hasta pronto!!

    lunes, mayo 03, 2010

    AOP: non kinded pointcuts

    En la entrada anterior analizábamos aquellos tipos de pointcuts en los que los joint point seleccionados encajaban en una determinada categoría (de ahí su nombre).

    El mecanismo implementado mediante non-kinded pointcuts permite la selección de joint points basados en criterios adicionales a las signaturas vistas anteriormente. Por ejemplo, podremos seleccionar todos los joint point donde el objeto this es de un determinado tipo. Dicho joint point incluiría las llamadas a métodos, ejecuciones, manejadores de excepciones, etc.

    A continuación, a través de ejemplos, veremos los diferentes tipos de non-kinded pointcuts ofrecidos por AspectJ.

    Non-kinded pointcuts basados en control de flujo

    PointcutDescripción
    cflow( execution(* TransactionManager. commit() ))Selecciona todos los joint points en el flujo de la ejecución de cualquier operación commit de la clase TransactionManager, incluyendo la ejecución del propio método.
    cflowbellow( execution(* TransactionManager. commit() ))Selecciona todos los joint points en el flujo de la ejecución de cualquier operación commit de la clase TransactionManager, excluyendo la ejecución del método.
    cflow(execution(@Secured * * (..)))Todos los joint points en el flujo de la ejecución de cualquier método anotado con la anotación Secured
    cflow(transacted())Cualquier joint point en el flujo de ejecución de los joint points seleccionados por el pointcut transacted()
    Non-kinded pointcuts basados en la estructura léxica
    Dentro de este tipo de pointcuts tenemos dos categorías:
    • within(TypeSingnature): selecciona cualquier joint point que aparece en el cuerpo de las clases y aspectos que concuerden con el tipo especificado.
    • withincode(ConstructorSignature),withincode(MethodSignature): selecciona cualquier joint point que aparezca dentro de un método o un constructor, incluyendo la definición de cualquier clase local que pudiera aparecer en los mismos.

    PointcutDescripción
    within(User)Selecciona todos los joint points que aparecen dentro de la clase User
    within(User+)Selecciona todos los joint points que aparecen dentro de la clase user y cualquiera de sus clases derivadas
    within(@Transactional *)Selecciona todos los joint points que aparecen dentro de cualquier clase que se encuentre marcada con la notación Transactional
    withincode(* TransactionManager. retrieve*(..))Selecciona todos los joint points que parecen dentro de cualquier método de la clase TransactionManager cuyo nombre comience por retrieve
    Non-kinded pointcuts de ejecución
    Este tipo de pointcuts permite seleccionar joint points en base al tipo de los objetos en tiempo de ejecución. De este modo se dispone de:
    • this(). Acepta dos formas diferentes: this(ObjectIdentifier) o this(Type). Seleccionará aquellos joint points cuyo objeto this sea del tipo (o el objeto) indicado.
    • target(). Similar al concepto anterior, aunque en este caso, se utilizar el target del joint point en lugar del this.

    PointcutDescripción
    this(User)Selecciona cualquier joint point en que la expresión this instanceof User sea cierta. Así por ejemplo, seleccionará las llamadas a métodos o accesos a campos donde el objeto actual sea de tipo User o cualquier de sus subclases.
    target(User)Selecciona cualquier joint point en el que el objeto sobre el que se realiza la llamada al método es instanceof User
    Non-kinded pointcuts sobre argumentos
    Este tipo de pointcuts permite seleccionar joint points en base al tipo de los argumentos en tiempo de ejecución. Veamos los distintos tipos de argumentos, en función del tipo del joint point :
    • En el caso de los joint points manejadores de excepciones el argumento será la excepción manejada.
    • En los métodos y constructores, los argumentos serán los argumentos del método y constructor.
    • En los accesos de modificación de un campo, el argumento será el nuevo valor que va a tomar dicho campo.

    PointcutDescripción
    args(User, . . , String)Selecciona cualquier joint point de tipo método o constructor en el que el primer argumento es de tipo User (o cualquiera de sus subclases), y el último argumento es de tipo String.
    args (SqlException)Selecciona cualquier joint point con un único argumento de tipo SqlException. Seleccionaría cualquier método o constructor que esperase un único argumento de tipo SqlException, un acceso de escritura a un campo estableciendo un nuevo valor de tipo SqlException, y también seleccionaría un manejador de excepciones de tipo SqlException
    Non-kinded pointcuts condicionales
    Este tipo de pointcuts permiten seleccionar joint points basados en una expresión condicional.

    PointcutDescripción
    if(debug)Selecciona cualquier joint point donde el campo estático debug (en la definición del aspecto) toma el valor cierto

    Poco a poco vamos completando, de manera sencilla, una idea general de los mecanismos que nos ofrece la orientación a aspectos en general, y AspectJ en particular. En la siguiente entrada analizaremos los conceptos de crosscutting dinámico y los mecanismos disponibles para la modificación del comportamiento de un programa en un determinado joint point.