Scala - Annotations
• Scala annotations provide metadata for classes, methods, and fields that can be processed at compile-time, runtime, or by external tools, enabling cross-cutting concerns like serialization,...
Key Insights
• Scala annotations provide metadata for classes, methods, and fields that can be processed at compile-time, runtime, or by external tools, enabling cross-cutting concerns like serialization, deprecation warnings, and framework integration without polluting business logic.
• Custom annotations require extending scala.annotation.Annotation and can leverage StaticAnnotation for compile-time processing or ClassfileAnnotation for runtime reflection, with Java interoperability maintained through standard annotation syntax.
• Annotation arguments support literals, arrays, and nested annotations, while specialized annotations like @tailrec, @switch, and @specialized enable compiler optimizations that directly impact application performance.
Understanding Scala Annotations
Annotations in Scala serve as metadata attachments to program elements. Unlike comments, annotations are processed by the compiler, runtime environment, or external tools. They enable declarative programming patterns where you specify what should happen rather than implementing how it happens.
Scala supports both built-in annotations and custom annotations. The compiler uses some annotations for optimization and verification, while others remain available at runtime for reflection-based frameworks.
// Basic annotation usage
@deprecated("Use newMethod instead", "2.0")
def oldMethod(): Unit = {
println("Legacy implementation")
}
@throws(classOf[IOException])
def readFile(path: String): String = {
Source.fromFile(path).mkString
}
// Multiple annotations
@SerialVersionUID(123L)
@deprecated("Migrating to new data model", "3.0")
class LegacyUser(val name: String) extends Serializable
Built-in Annotations for Compiler Optimization
Scala provides annotations that guide the compiler to perform specific optimizations or verifications. These annotations can significantly impact code performance and correctness.
import scala.annotation.{tailrec, switch}
object OptimizationExamples {
// @tailrec ensures tail call optimization
@tailrec
def factorial(n: BigInt, acc: BigInt = 1): BigInt = {
if (n <= 1) acc
else factorial(n - 1, n * acc)
}
// @switch optimizes pattern matching to tableswitch/lookupswitch
def parseCommand(cmd: Int): String = (cmd: @switch) match {
case 1 => "START"
case 2 => "STOP"
case 3 => "PAUSE"
case 4 => "RESUME"
case _ => "UNKNOWN"
}
// @specialized generates specialized versions for primitive types
class Container[@specialized(Int, Double) T](val value: T) {
def get: T = value
}
}
// Without @tailrec, this would fail to compile if not tail-recursive
@tailrec
def sum(list: List[Int], acc: Int = 0): Int = list match {
case Nil => acc
case head :: tail => sum(tail, acc + head)
}
The @tailrec annotation is particularly valuable because it converts what would be stack-consuming recursive calls into efficient loops. If the method isn’t actually tail-recursive, the compiler produces an error, preventing potential stack overflow issues.
Creating Custom Annotations
Custom annotations extend scala.annotation.Annotation. For compile-time processing, use StaticAnnotation. For runtime availability, extend ClassfileAnnotation.
import scala.annotation.{StaticAnnotation, ClassfileAnnotation}
// Compile-time annotation
class inline extends StaticAnnotation
// Runtime-accessible annotation
class ApiVersion(version: String) extends ClassfileAnnotation
// Annotation with multiple parameters
class Route(
path: String,
method: String = "GET",
authenticated: Boolean = false
) extends ClassfileAnnotation
// Usage examples
class UserService {
@inline
private def validateEmail(email: String): Boolean = {
email.contains("@")
}
@ApiVersion("v1")
@Route(path = "/users", method = "POST", authenticated = true)
def createUser(name: String, email: String): User = {
require(validateEmail(email), "Invalid email")
User(name, email)
}
@ApiVersion("v2")
@Route(path = "/users/:id", method = "GET")
def getUser(id: Long): Option[User] = {
// Implementation
None
}
}
case class User(name: String, email: String)
Runtime Annotation Processing with Reflection
Runtime annotations enable framework-level functionality through reflection. This pattern is common in dependency injection, ORM frameworks, and REST API libraries.
import scala.reflect.runtime.universe._
object AnnotationProcessor {
def extractRoutes[T: TypeTag]: List[RouteInfo] = {
val tpe = typeOf[T]
val methods = tpe.decls.collect {
case m: MethodSymbol if m.isPublic => m
}
methods.flatMap { method =>
method.annotations.collect {
case annotation if annotation.tree.tpe =:= typeOf[Route] =>
val args = annotation.tree.children.tail
// Extract annotation arguments
val path = args.collectFirst {
case Literal(Constant(p: String)) => p
}.getOrElse("")
RouteInfo(
methodName = method.name.toString,
path = path,
httpMethod = extractStringArg(args, "method").getOrElse("GET")
)
}
}.toList
}
private def extractStringArg(args: List[Tree], name: String): Option[String] = {
args.collectFirst {
case AssignOrNamedArg(Ident(TermName(n)), Literal(Constant(v: String)))
if n == name => v
}
}
}
case class RouteInfo(methodName: String, path: String, httpMethod: String)
// Extract routes from UserService
val routes = AnnotationProcessor.extractRoutes[UserService]
routes.foreach(r =>
println(s"${r.httpMethod} ${r.path} -> ${r.methodName}")
)
Java Interoperability
Scala annotations are compatible with Java, but require specific patterns for full interoperability. Use constructor parameters to match Java annotation syntax.
import scala.annotation.meta._
// Target specific elements (like Java's @Target)
@getter @setter
class JsonProperty(value: String) extends scala.annotation.StaticAnnotation
// Retention policy (like Java's @Retention)
@scala.annotation.meta.field
class Column(name: String) extends ClassfileAnnotation
class DatabaseEntity {
@Column(name = "user_name")
var username: String = _
@JsonProperty("email_address")
var email: String = _
}
// Using Java annotations in Scala
import javax.persistence._
@Entity
@Table(name = "users")
class JpaUser {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
var id: Long = _
@Column(nullable = false, unique = true)
var username: String = _
}
Practical Annotation Patterns
Annotations excel at separating cross-cutting concerns from business logic. Here’s a validation framework example:
import scala.annotation.StaticAnnotation
class NotNull extends StaticAnnotation
class Min(value: Int) extends StaticAnnotation
class Max(value: Int) extends StaticAnnotation
class Email extends StaticAnnotation
case class UserRegistration(
@NotNull @Email email: String,
@NotNull username: String,
@Min(18) @Max(120) age: Int
)
object Validator {
def validate[T: TypeTag](instance: T): Either[List[String], T] = {
val tpe = typeOf[T]
val mirror = runtimeMirror(instance.getClass.getClassLoader)
val instanceMirror = mirror.reflect(instance)
val errors = tpe.members.collect {
case m: MethodSymbol if m.isCaseAccessor =>
val fieldMirror = instanceMirror.reflectField(m.asTerm)
val value = fieldMirror.get
m.annotations.flatMap { ann =>
ann.tree.tpe match {
case t if t =:= typeOf[NotNull] =>
Option.when(value == null)(s"${m.name} cannot be null")
case t if t =:= typeOf[Email] =>
val str = value.toString
Option.when(!str.contains("@"))(s"${m.name} must be valid email")
case t if t =:= typeOf[Min] =>
val minValue = extractIntArg(ann.tree.children.tail)
val intValue = value.asInstanceOf[Int]
Option.when(intValue < minValue)(
s"${m.name} must be >= $minValue"
)
case _ => None
}
}
}.flatten.toList
if (errors.isEmpty) Right(instance) else Left(errors)
}
private def extractIntArg(args: List[Tree]): Int = {
args.collectFirst {
case Literal(Constant(v: Int)) => v
}.getOrElse(0)
}
}
// Usage
val user = UserRegistration("invalid-email", "john", 15)
Validator.validate(user) match {
case Left(errors) => errors.foreach(println)
case Right(valid) => println(s"Valid: $valid")
}
Annotations provide a powerful mechanism for metadata-driven programming in Scala. They enable clean separation of concerns, compiler optimizations, and framework integration while maintaining code readability. Whether using built-in annotations for performance or creating custom annotations for domain-specific needs, understanding annotation mechanics is essential for building robust Scala applications.