Пример работы с динамическими прокси-классами в Scala

28 января 2015

Рассмотрим такую практическую задачу. Есть некое множество классов. У этих классов есть методы, по которым нам хотелось бы, например, собирать метрики: количество вызовов, время выполнения, число ошибок. Наиболее простой и красивый способ решения этой задачи, не требующий написания тысяч строк кода в разных местах проекта, заключается в использовании динамических прокси-классов.

Ссылку на соответствующий пример исходного кода мне подсказал Alexander Semenov в ScalaChat. В упрощенном виде решение выглядит как-то так:

import scala.concurrent._
import java.lang.reflect.{Method, InvocationHandler, Proxy}
import scala.concurrent.ExecutionContext.Implicits.global

package object util {
  def createProxy[I](proxee: I,
                     interfaceClass: Class[I],
                     handler: InvocationHandler): I = {
    Proxy.newProxyInstance(
      interfaceClass.getClassLoader,
      Array(interfaceClass),
      handler).asInstanceOf[I]
  }
}

import util._

abstract class AspectBase(proxee: AnyRef) extends InvocationHandler {
  protected def invokeProxee(method: Method,
                             args: Array[Object]): AnyRef =
    method.invoke(proxee, args: _*)
}

class ProxyClass
    (proxee: AnyRef)
    (implicit protected val executionContext: ExecutionContext)
    extends AspectBase(proxee) {

  override def invoke(proxy: Object,
                      method: Method,
                      args: Array[Object]): AnyRef = {

    println(s"${method.getName} invoked")

    val result = invokeProxee(method, args)
    result match {
      case f: Future[_] =>
        println("Future returned!")
      case _ => ;
    }
    result
  }
}

trait TestTrait {
  def someMethod(x: Long): Long
  def someOtherMethod(x: Long): Future[Long]
}

class TestTraitImpl extends TestTrait {
  def someMethod(x: Long) = x * 2
  def someOtherMethod(x: Long) = Future { x * 3 }
}

object Debug extends App {
  val t = {
    val proxee = new TestTraitImpl()
    createProxy(proxee, classOf[TestTrait], new ProxyClass(proxee))
  }

  t.someMethod(1)
  t.someOtherMethod(2)
}

При запуске программы мы получим следующий вывод:

someMethod invoked
someOtherMethod invoked
Future returned!

Как видите, мы перехватываем имя метода, его аргументы, возвращаемое значение. Таким образом, мы можем получить время перед вызовом оригинального метода и при возврате из него, повесить onComplete на возвращаемую футуру и так далее. Можно писать логи, собирать метрики, сериализовать аргументы и отправлять их в REST-сервис, и так далее.

Работает это примерно таким образом. При вызове Proxy.newProxyInstance генерируется обычный (то есть, как бы статический) прокси-класс, как если бы мы писали его вручную. Только делается это во время исполнения, потому прокси и динамический. Хорошо все-таки работать под виртуальной машиной, правда? Однако важно помнить, что при использовании динамических классов-прокси мы можем проксировать только интерфейс (в приведенном ранее примере — TestTrait), а не какой попало класс.

Ссылки по теме:

А используете ли вы в своих проектах динамические прокси-классы?

Метки: , .


Вы можете прислать свой комментарий мне на почту, или воспользоваться комментариями в Telegram-группе.