Статьи

Реальная Scala: управление сквозными задачами с использованием Mixin Composition и AOP

В предыдущем посте я показал вам, как можно использовать миксиновую композицию и аннотации собственного типа для включения Dependency Injection (DI). Композиция Mixin — чрезвычайно мощный инструмент, который вы можете использовать различными способами для включения модульного и многократно используемого кода. В этом посте я попытаюсь показать вам, как вы можете использовать его для решения проблемы сквозных задач, используя композицию в стиле AOP / interceptor.

Перекрестные проблемы и АОП

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

Так что же такое сквозная проблема? Озабоченность — это конкретная концепция или область интересов. Например, в системе заказов основными задачами могут быть обработка и изготовление заказов, а проблемами системы могут быть обработка транзакций и управление безопасностью. Межсекторальная проблема — это проблема, которая затрагивает несколько классов или модулей, проблема, которая недостаточно хорошо локализована и модульна.

Симптомами сквозного беспокойства являются:

  • Запутывание кода — когда модуль или секция кода управляют несколькими проблемами одновременно
  • Рассеяние кода — когда проблема распространяется на многие модули и не очень хорошо локализована и модульна

Эти симптомы по-разному влияют на программное обеспечение; например, они усложняют обслуживание и повторное использование программного обеспечения, а также затрудняют написание и понимание.

Аспектно-ориентированное программирование (АОП) пытается решить эти проблемы путем введения концепции разделения интересов, в которой проблемы могут быть реализованы модульным и хорошо локализованным способом. AOP решает эту проблему, добавляя дополнительное измерение в область проектирования, и вводит конструкции, которые позволяют нам определить сквозные задачи, вывести их в новое измерение и упаковать их модульным способом.

В настоящее время мы используем два разных типа перехватчиков (аспекты, если хотите):

  • Стеки смешанных композиций — ограниченный, но иногда очень полезный подход
  • Общие перехватчики / аспекты, использующие язык шаблонов pointcut

Смешанные композиции стеков

Стеки смешанных композиций — это базовая языковая особенность Scala, сходная с идеей Рикарда Оберга об использовании так называемого шаблона абстрактной схемы для безопасного типа АОП в простой Java. (Это очень надуманный пример, который, вероятно, показывает, что вы не знаете, что такое собаки, но, пожалуйста, держитесь за меня.)

Сначала давайте определим пару интерфейсов; Dogи DogMoodмоделируется как миксин (в этом случае без реализации, очень похожей на интерфейс Java):

trait Dog {
def greet(me: Human)
}

trait DogMood extends Dog {
def greet(me: Human) {
println(me.sayHello)
}
}

Теперь давайте определим два разных «перехватчика», которые реализуют эти интерфейсы. Первый определяет злую собаку, а другой — голодную собаку:

trait AngryDog extends DogMood {
abstract override def greet(me: Human) {
println("Dog: Barks @ " + me)
super.doStuff(me)
}
}

trait HungryDog extends DogMood {
abstract override def greet(me: Human) {
super.doStuff(me)
println("Dog: Bites " + me)
}
}

Как мы видим в этом примере, они оба переопределяют Mood.greetметод. Если мы посмотрим более внимательно, мы увидим, что они следуют той же схеме:

  • Введите метод ( приветствовать )
  • Сделай что-нибудь
  • Вызовите тот же метод на супер ( super.greet )
  • Сделать что-то еще

Хитрость здесь в семантике призыва к супер. Здесь Scala будет вызывать следующий миксин в стеке миксинов, например, тот же метод в «следующем» миксине, в который был добавлен микширование. Именно то, что делает AspectJ в своем методе continue (..) и что Spring делает в своих перехватчиках.

Теперь давайте запустим Scala REPL и создадим компонент на основе Dogинтерфейса. Смешанный состав Scala может иметь место, когда мы создаем экземпляр, например, он позволяет нам смешивать функциональные возможности с конкретными экземплярами, которые время создания объекта для конкретных экземпляров объекта.

scala> val dog = new Dog with AngryDog with HungryDog
stuff2: Dog with AngryDog with HungryDog = $anon$1@1082d45

scala> dog.greet(new Human("Me"))
Dog: Barks @ Me
Me: Hello doggiedoggie
Dog: Bites Me

Как вы можете видеть, призыв к Dog.greetперехватывается различными настроениями, которые добавляются собаке во время создания экземпляра.

Подобные перехватчики, как вы можете видеть, не могут использоваться повторно, поскольку они привязаны к определенному интерфейсу, однако, если они хорошо спроектированы, это может быть довольно мощной техникой. Преимущество состоит в том, что все статически компилируется и проверяется типом компилятором Scala.

Общие аспекты на основе точек

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

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

case class Invocation(val method: Method, val args: Array[AnyRef], val target: AnyRef) {
def invoke: AnyRef = method.invoke(target, args:_*)
override def toString: String = "Invocation [method: " + method.getName + ", args: " + args + ", target: " + target + "]"
override def hashCode(): Int = { ... }
override def equals(that: Any): Boolean = { ... }
}

Второе, что нам нужно сделать, это создать базовую черту Interceptor. Этот интерфейс определяет два разных метода сопоставления точек. Первый соответствует предварительно скомпилированному выражению pointcut AspectJ с использованием PointcutParser в AspectJ. Это позволяет определять совпадения перехватчиков выражений, совместимых с AspectJ (методом) pointcut. Второе сопоставление сопоставляет методы или классы, аннотированные конкретной аннотацией.

trait Interceptor {
protected val parser = PointcutParser.getPointcutParserSupportingAllPrimitivesAndUsingContextClassloaderForResolution

protected def matches(pointcut: PointcutExpression, invocation: Invocation): Boolean = {
pointcut.matchesMethodExecution(invocation.method).alwaysMatches ||
invocation.target.getClass.getDeclaredMethods.exists(pointcut.matchesMethodExecution(_).alwaysMatches) ||
false
}

protected def matches(annotationClass: Class[T] forSome {type T < : Annotation}, invocation: Invocation): Boolean = {
invocation.method.isAnnotationPresent(annotationClass) ||
invocation.target.getClass.isAnnotationPresent(annotationClass) ||
false
}

def invoke(invocation: Invocation): AnyRef
}

The last thing we need to do is to create a factory method allows us to wire in our interceptors, declarative, in a seamless fashion. This factory is using the plain old Java Dynamic Proxy API to create a proxy for our base components.

object ManagedComponentFactory {
def createComponent[T](intf: Class[T] forSome {type T}, proxy: ManagedComponentProxy): T =
Proxy.newProxyInstance(
proxy.target.getClass.getClassLoader,
Array(intf),
proxy).asInstanceOf[T]
}

class ManagedComponentProxy(val target: AnyRef) extends InvocationHandler {
def invoke(proxy: AnyRef, m: Method, args: Array[AnyRef]): AnyRef = invoke(Invocation(m, args, target))
def invoke(invocation: Invocation): AnyRef = invocation.invoke
}

Just using this factory pass is won’t do any wiring for us, which is actually good since if we would use the dynamic proxy the old-fashioned way and we would have it to invoke each interceptor explicitly using reflection. But we can do better than that. Instead we will let the Scala compiler statically compiled in an interceptor stack with all our interceptors. This is best explained with an example.

In this example we will define a couple of simple services called Foo and Bar along with their implementations. We will then implement two different infrastructure in interceptors; logging and transaction demarcation.

Let’s first define the service.

  import javax.ejb.{TransactionAttribute, TransactionAttributeType}

trait Foo {
@TransactionAttribute(TransactionAttributeType.REQUIRED)
def foo(msg: String)
def bar(msg: String)
}

class FooImpl extends Foo {
val bar: Bar = new BarImpl
def foo(msg: String) = println("msg: " + msg)
def bar(msg: String) = bar.bar(msg)
}

trait Bar {
def bar(msg: String)
}

class BarImpl extends Bar {
def bar(msg: String) = println("msg: " + msg)
}

Now let’s define a logging interceptor. Both of these interceptors are just mockups, since the actual implementation is not really of interest. The logging interceptor is defined using a standard AspectJ pointcut while the transaction interceptor is wired to a specific annotation.

  trait LoggingInterceptor extends Interceptor {
val loggingPointcut = parser.parsePointcutExpression("execution(* *.foo(..))")

abstract override def invoke(invocation: Invocation): AnyRef =
if (matches(loggingPointcut , invocation)) {
println("=====> Enter: " + invocation.method.getName + " @ " + invocation.target.getClass.getName)
val result = super.invoke(invocation)
println("=====> Exit: " + invocation.method.getName + " @ " + invocation.target.getClass.getName)
result
} else super.invoke(invocation)
}

trait TransactionInterceptor extends Interceptor {
val matchingJtaAnnotation = classOf[javax.ejb.TransactionAttribute]

abstract override def invoke(invocation: Invocation): AnyRef =
if (matches(matchingJtaAnnotation, invocation)) {
println("=====> TX begin")
try {
val result = super.doStuff
println("=====> TX commit")
result
} catch {
case e: Exception =>
println("=====> TX rollback ")
}
} else super.invoke(invocation)
}

Now let’s do the wiring. Here we are using dynamic proxy-based factory that we implemented because you can see, the actual wiring on the interceptor stack is done using Scala mixing composition and therefore has all its benefits, like compiler type checking and enforcement, the speed of statically compiled code, refactoring safety etc.

  var foo = ManagedComponentFactory.createComponent[Foo](
classOf[Foo],
new ManagedComponentProxy(new FooImpl)
with LoggingInterceptor
with TransactionInterceptor)

foo.foo("foo")
foo.bar("bar")
}

This will produce the following output:

=====> TX begin
=====> Enter: foo @ FooImpl
msg: foo
=====> Exit: foo @ FooImpl
=====> TX commit
=====> TX begin
=====> Enter: bar @ FooImpl
msg: bar
=====> Exit: bar @ FooImpl
=====> TX commit

So this wraps it up. I hope that you have learned a little bit about how powerful mixin composition in Scala is and how it can be used to write modular and reusable components with little effort.

This is working fine for us, but there is definitely room for improvement. For example, runtime matcher in the interceptor is fast enough for the annotation matching (only a boolean check) but the AspectJ pointcut matcher is a bit slower since it has to do some more work. This might turn out be a problem or not, most infrastructure services (like persistence and security) performs quite a lot to work and in these cases the overhead over the interceptor of matching will not affect the overall performance much, but in other cases (such as logging or auditing) it might. We are so far only use the annotation matching, so it has not turned out to be a problem so far. However, if it turns out to be a performance bottleneck then we will most likely switch to using my old AspectWerkz AWProxy to get rid of all the Java reflection code and runtime matching.

For those that are interested, here is the actual JTA transaction demarcation interceptor that we are using in production (implementing all the EJB transaction semantics).

trait EjbTransactionInterceptor extends Interceptor with TransactionProtocol {
val matchingJtaAnnotation = classOf[javax.ejb.TransactionAttribute]

abstract override def invoke(invocation: Invocation): AnyRef = if (matches(matchingJtaAnnotation, invocation)) {
val txType = getTransactionAttributeTypeFor(invocation.target.getClass, invocation.method)
if (txType == TransactionAttributeType.REQUIRED) withTxRequired { super.invoke(invocation) }
else if (txType == TransactionAttributeType.REQUIRES_NEW) withTxRequiresNew { super.invoke(invocation) }
else if (txType == TransactionAttributeType.MANDATORY) withTxMandatory { super.invoke(invocation) }
else if (txType == TransactionAttributeType.NEVER) withTxNever { super.invoke(invocation) }
else if (txType == TransactionAttributeType.SUPPORTS) withTxSupports { super.invoke(invocation) }
else if (txType == TransactionAttributeType.NOT_SUPPORTED) withTxNotSupported { super.invoke(invocation) }
else super.invoke(invocation)
} else super.invoke(invocation)
}

About this entry