Skip to content

Instantly share code, notes, and snippets.

@stringbean
Last active November 1, 2019 11:40
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save stringbean/0f264c150e93e84c4d3b0e58cd1b2c91 to your computer and use it in GitHub Desktop.
Save stringbean/0f264c150e93e84c4d3b0e58cd1b2c91 to your computer and use it in GitHub Desktop.
Scala annotation target issue

Scala annotation behaviour change

The target of Java annotations used in Scala code has changed in some scenarios. It looks like the behaviour in Scala 2.11 was slightly incorrect if the containing case class was defined within a trait. From 2.12 onwards the behaviour has changed significantly.

Scala Library Docs

Taken from the scala.annotation.meta API reference:

When defining a field, the Scala compiler creates up to four accessors for it: a getter, a setter, and if the field is annotated with @BeanProperty, a bean getter and a bean setter.

...

By default, annotations on (val-, var- or plain) constructor parameters end up on the parameter, not on any other entity. Annotations on fields by default only end up on the field.

...

The target meta-annotations can be put on the annotation type when instantiating the annotation. In the following example, the annotation @Id will be added only to the bean getter getX.

import javax.persistence.Id
class A {
  @(Id @beanGetter) @BeanProperty val x = 0
}

Test Harness

The test harness applies the Java annotation @Info to the fields of two case classes - one defined in a separate compilation unit (TestBean) and one within a trait (WrappedBean). It then uses the Java reflection API to test where the annotation has been applied.

The test harness can be run from sbt using:

sbt +run

Results

Test Case Expected 2.11 2.12 2.13
TestBean.defaultTarget C C C C
TestBean.fieldTarget F F F F
TestBean.getterTarget M M M M
TestBean.beanPropertyDefault C C C C
TestBean.beanPropertyField F F F F
TestBean.beanPropertyGetter M M M M
TestBean.beanPropertyBeanGetter B B B B
WrappedBean.defaultTarget C - M M
WrappedBean.fieldTarget F F FMC FMC
WrappedBean.getterTarget M M M M
WrappedBean.beanPropertyDefault C - MB MB
WrappedBean.beanPropertyField F FC FMCB FMCB
WrappedBean.beanPropertyGetter M MC MC MC
WrappedBean.beanPropertyBeanGetter B B B B

Key:

  • C: constructor
  • F: field
  • M: getter method
  • B: bean getter method

Raw Output

Scala 2.11.12

=== TestBean ===
defaultTarget:
  field:       false
  method:      false
  constructor: true
fieldTarget:
  field:       true
  method:      false
  constructor: false
getterTarget:
  field:       false
  method:      true
  constructor: false
beanPropertyDefault:
  field:       false
  method:      false
  getter:      false
  constructor: true
beanPropertyField:
  field:       true
  method:      false
  getter:      false
  constructor: false
beanPropertyGetter:
  field:       false
  method:      true
  getter:      false
  constructor: false
beanPropertyBeanGetter:
  field:       false
  method:      false
  getter:      true
  constructor: false
=== WrappedBean ===
defaultTarget:
  field:       false
  method:      false
  constructor: false
fieldTarget:
  field:       true
  method:      false
  constructor: true
getterTarget:
  field:       false
  method:      true
  constructor: false
beanPropertyDefault:
  field:       false
  method:      false
  getter:      false
  constructor: false
beanPropertyField:
  field:       true
  method:      false
  getter:      false
  constructor: true
beanPropertyGetter:
  field:       false
  method:      true
  getter:      false
  constructor: true
beanPropertyBeanGetter:
  field:       false
  method:      false
  getter:      true
  constructor: false

Scala 2.12.10

=== TestBean ===
defaultTarget:
  field:       false
  method:      false
  constructor: true
fieldTarget:
  field:       true
  method:      false
  constructor: false
getterTarget:
  field:       false
  method:      true
  constructor: false
beanPropertyDefault:
  field:       false
  method:      false
  getter:      false
  constructor: true
beanPropertyField:
  field:       true
  method:      false
  getter:      false
  constructor: false
beanPropertyGetter:
  field:       false
  method:      true
  getter:      false
  constructor: false
beanPropertyBeanGetter:
  field:       false
  method:      false
  getter:      true
  constructor: false
=== WrappedBean ===
defaultTarget:
  field:       false
  method:      true
  constructor: false
fieldTarget:
  field:       true
  method:      true
  constructor: true
getterTarget:
  field:       false
  method:      true
  constructor: false
beanPropertyDefault:
  field:       false
  method:      true
  getter:      true
  constructor: false
beanPropertyField:
  field:       true
  method:      true
  getter:      true
  constructor: true
beanPropertyGetter:
  field:       false
  method:      true
  getter:      false
  constructor: true
beanPropertyBeanGetter:
  field:       false
  method:      false
  getter:      true
  constructor: false

Scala 2.13.1

=== TestBean ===
defaultTarget:
  field:       false
  method:      false
  constructor: true
fieldTarget:
  field:       true
  method:      false
  constructor: false
getterTarget:
  field:       false
  method:      true
  constructor: false
beanPropertyDefault:
  field:       false
  method:      false
  getter:      false
  constructor: true
beanPropertyField:
  field:       true
  method:      false
  getter:      false
  constructor: false
beanPropertyGetter:
  field:       false
  method:      true
  getter:      false
  constructor: false
beanPropertyBeanGetter:
  field:       false
  method:      false
  getter:      true
  constructor: false
=== WrappedBean ===
defaultTarget:
  field:       false
  method:      true
  constructor: false
fieldTarget:
  field:       true
  method:      true
  constructor: true
getterTarget:
  field:       false
  method:      true
  constructor: false
beanPropertyDefault:
  field:       false
  method:      true
  getter:      true
  constructor: false
beanPropertyField:
  field:       true
  method:      true
  getter:      true
  constructor: true
beanPropertyGetter:
  field:       false
  method:      true
  getter:      false
  constructor: true
beanPropertyBeanGetter:
  field:       false
  method:      false
  getter:      true
  constructor: false
name := "annotation-behaviour"
scalaVersion := "2.12.10"
crossScalaVersions := Seq(scalaVersion.value, "2.11.12", "2.13.1")
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.ANNOTATION_TYPE, ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface Info { }
import scala.util.Properties
object Main extends App with Wrapper {
val annotation = classOf[Info]
def checkAnnotations(clazz: Class[_]): Unit = {
println(s"=== ${clazz.getSimpleName} ===")
val constructor = clazz.getConstructors.head
val params = constructor.getParameters
println("defaultTarget:")
println(s" field: ${clazz.getDeclaredField("defaultTarget").isAnnotationPresent(annotation)}")
println(s" method: ${clazz.getDeclaredMethod("defaultTarget").isAnnotationPresent(annotation)}")
println(s" constructor: ${params(0).isAnnotationPresent(annotation)}")
println("fieldTarget:")
println(s" field: ${clazz.getDeclaredField("fieldTarget").isAnnotationPresent(annotation)}")
println(s" method: ${clazz.getDeclaredMethod("fieldTarget").isAnnotationPresent(annotation)}")
println(s" constructor: ${params(1).isAnnotationPresent(annotation)}")
println("getterTarget:")
println(s" field: ${clazz.getDeclaredField("getterTarget").isAnnotationPresent(annotation)}")
println(s" method: ${clazz.getDeclaredMethod("getterTarget").isAnnotationPresent(annotation)}")
println(s" constructor: ${params(2).isAnnotationPresent(annotation)}")
println("beanPropertyDefault:")
println(s" field: ${clazz.getDeclaredField("beanPropertyDefault").isAnnotationPresent(annotation)}")
println(s" method: ${clazz.getDeclaredMethod("beanPropertyDefault").isAnnotationPresent(annotation)}")
println(s" getter: ${clazz.getDeclaredMethod("getBeanPropertyDefault").isAnnotationPresent(annotation)}")
println(s" constructor: ${params(3).isAnnotationPresent(annotation)}")
println("beanPropertyField:")
println(s" field: ${clazz.getDeclaredField("beanPropertyField").isAnnotationPresent(annotation)}")
println(s" method: ${clazz.getDeclaredMethod("beanPropertyField").isAnnotationPresent(annotation)}")
println(s" getter: ${clazz.getDeclaredMethod("getBeanPropertyField").isAnnotationPresent(annotation)}")
println(s" constructor: ${params(4).isAnnotationPresent(annotation)}")
println("beanPropertyGetter:")
println(s" field: ${clazz.getDeclaredField("beanPropertyGetter").isAnnotationPresent(annotation)}")
println(s" method: ${clazz.getDeclaredMethod("beanPropertyGetter").isAnnotationPresent(annotation)}")
println(s" getter: ${clazz.getDeclaredMethod("getBeanPropertyGetter").isAnnotationPresent(annotation)}")
println(s" constructor: ${params(4).isAnnotationPresent(annotation)}")
println("beanPropertyBeanGetter:")
println(s" field: ${clazz.getDeclaredField("beanPropertyBeanGetter").isAnnotationPresent(annotation)}")
println(s" method: ${clazz.getDeclaredMethod("beanPropertyBeanGetter").isAnnotationPresent(annotation)}")
println(s" getter: ${clazz.getDeclaredMethod("getBeanPropertyBeanGetter").isAnnotationPresent(annotation)}")
println(s" constructor: ${params(5).isAnnotationPresent(annotation)}")
}
println(s"Scala ${Properties.versionNumberString}")
checkAnnotations(classOf[TestBean])
checkAnnotations(classOf[WrappedBean])
}
import scala.annotation.meta.{beanGetter, field, getter}
import scala.beans.BeanProperty
case class TestBean(
@Info defaultTarget: String,
@(Info @field) fieldTarget: String,
@(Info @getter) getterTarget: String,
@BeanProperty @Info beanPropertyDefault: String,
@BeanProperty @(Info @field) beanPropertyField: String,
@BeanProperty @(Info @getter) beanPropertyGetter: String,
@BeanProperty @(Info @beanGetter) beanPropertyBeanGetter: String)
import scala.annotation.meta.{beanGetter, field, getter}
import scala.beans.BeanProperty
trait Wrapper {
case class WrappedBean(
@Info defaultTarget: String,
@(Info @field) fieldTarget: String,
@(Info @getter) getterTarget: String,
@BeanProperty @Info beanPropertyDefault: String,
@BeanProperty @(Info @field) beanPropertyField: String,
@BeanProperty @(Info @getter) beanPropertyGetter: String,
@BeanProperty @(Info @beanGetter) beanPropertyBeanGetter: String)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment