Advanced Kotlin - Part 2 - Use-Site Targets
Annotation Use-Site targets
This is our second entry in our on-going “Advanced Kotlin” blog series. Be sure to check out our first post; Advanced Kotlin - Delegates if you haven’t yet.
In this post we’re going to take a deep-dive into Kotlin Annotation Use-Site targets.
You’ve probably used @get
and @set
in Kotlin before, but have you come across @receiver
or @delegate
?
Kotlin provides us with nine different Annotation use-site targets. In this post we’ll cover all of them.
By writing Kotlin code that uses each of these annotation use-site targets and then decompiling from Kotlin into Java using the Show Kotlin Bytecode -> Decompile
tool in IntelliJ we’ll see exactly where each annotation ends up in the resulting code.
The Basics
But first, what exactly are use-site targets and why do we need them?
Many of the libraries, frameworks, and tools we use in Kotlin are actually designed for Java. Either at compile time or at runtime a library may require an annotation to be in a very specific place in your code and/or bytecode for it to function correctly. However, Kotlin is not Java and therefore doesn’t have some of the constructs expected in a Java program, and Kotlin also has some new constructs which are not available to Java. We can use annotation use-site targets to bridge this gap so that various libraries will work as expected.
Simply put, annotation use-site targets allow any @Annotations
in your source code to end up at a very specific place in your compiled bytecode or in the Java code generated by kapt
.
A Simple Example
Let’s start with a simple example that many of you may have already written yourselves:
@get:SomeAnnotation
var someProperty: String? = null
By prefixing the annotation with @get:
the resulting bytecode will have the annotation on the generated getter function:
// Decompiled from the resulting Kotlin Bytecode
@SomeAnnotation
@Nullable
public final String getSomeProperty() {
return this.someProperty;
}
However, the member variable, the setter function, and any function parameters will not have the SomeAnnotation
annotation.
In many cases this may be what a framework requires, be it @get:Rule
for a JUnit test, @get:ColorRes
for an Android resource, or @get:GET
for a Retrofit interface.
Overview
In this article we’ll be looking at all nine of the annotation use-site targets available in Kotlin:
@get
@set
@file
@param
@field
@setparam
@receiver
@property
@delegate
Some of these will be either familiar or obvious, such as @get
and @set
, but some are less obvious and less used, such as @delegate
and @receiver
.
Knowing all of the above is good knowledge to have even if you’ll only use it a handful of times.
We will be using the same method as used above with the @get
example in order to test where an annotation resides in decompiled bytecode when using each use-site targets.
Each example will begin with an annotation that can be used in our sample code that matches the use-site target name so that it’s easy to spot:
annotation class GetAnnotation
annotation class SetAnnotation
annotation class PropertyAnnotation
annotation class ReceiverAnnotation
...etc...
We’ll then add the matching use-site target to the annotation in some sample Kotlin code, decompile the bytecode and see where the resulting annotation lives.
You may notice that many items on this list closely match Java naming conventions (getters, setters, parameters, etc) as they are intended to help facilitate Kotlin-Java interop. Kotlin’s annotation use-site targets currently only affect JVM bytecode not Kotlin/native, Kotlin/js, or Kotlin Multiplatform projects. This may change in the future, especially for multiplatform language targets such as Swift.
Get and Set use-site targets
Let’s start with the few annotation use-site targets that you’ve probably already seen or used.
As an added bonus I’ll also include how you can provide multiple annotations to a single use-site target using []
brackets.
annotation class GetAnnotation
annotation class SetAnnotation
annotation class SetAnnotation2
class Person(
@get:GetAnnotation val first: String,
@set:[SetAnnotation SetAnnotation2] var last: String
) {
// ...
}
The resulting decompiled bytecode is as follows:
public final class Person {
@NotNull private final String first;
@NotNull private String last;
@GetAnnotation
@NotNull
public final String getFirst() {
return this.first;
}
@NotNull
public final String getLast() {
return this.last;
}
@SetAnnotation
@SetAnnotation2
public final void setLast(@NotNull String var1) {
this.last = var1;
}
public Person(@NotNull String first, @NotNull String last) {
super();
this.first = first;
this.last = last;
}
}
As you can see, the @GetAnnotation
is on the getter and both @SetAnnotation
and @SetAnnotation2
are on the setter. They don’t appear anywhere else in the code.
File use-site targets
@file
is most commonly used with @file:JvmName
in order to provide a custom name for a file.
This is useful when a file only contains top-level functions or constants.
In this case the Kotlin compiler creates a class called YourFileNameKt
, which ends with Kt
.
This can look odd when used from Java:
@file:JvmName("HttpConstants")
const val HTTP_OK = 200
const val HTTP_NOT_FOUND = 404
Without the above @file:JvmName
annotation the usage of these constants from java would appear as HttpConstantsKt.HTTP_OK
instead of HttpConstants.HTTP_OK
.
The @file
use-site target can also be used with other annotations, but since this annotation does not end up in the resulting bytecode it would normally be used by Kotlin-specific tools and libraries, not Java ones.
An example of this would be using @file:Suppress
to suppress lint or detekt rules for an entire file.
Field and Parameter use-site targets
Let’s look at how @param
and @field
affect resulting bytecode by using both in a single piece of code on similar class properties:
annotation class ParamAnnotation
annotation class FieldAnnotation
class Person(
@param:ParamAnnotation val first: String,
@field:FieldAnnotation val last: String
) {
// ...
}
The resulting decompiled bytecode is:
public final class Person {
@NotNull private final String first;
@FieldAnnotation
@NotNull
private final String last;
@NotNull
public final String getFirst() {
return this.first;
}
@NotNull
public final String getLast() {
return this.last;
}
public Person(@ParamAnnotation @NotNull String first, @NotNull String last) {
super();
this.first = first;
this.last = last;
}
}
As you can see, where @param
was used on first
, only the parameter to the constructor is annotated.
If this was a var
instead of a val
the parameter to the setter function would not be annotated.
We’ll see @setparam
shortly which can be used to target setter parameters.
last
, being annotated with @field
, only has an annotation on the private
field used by the getter function.
The getter/setter function and the constructor params do not contain the FieldAnnotation
annotation.
These can be useful for targeting specific pieces of code when using a dependency injection framework:
class MyClass @Inject constructor(
@param:SpecificString private val str: String
)
...
@Inject
@field:MainThread
lateinit var scheduler: Scheduler
Setter Parameter use-site targets
As mentioned above, you can use @setparam
to add annotations to setter parameters.
Let’s compare @set
and @setparam
in the same example so you can clearly see the difference.
annotation class SetParamAnnotation
annotation class SetAnnotation
class SetParamAnnTest() {
@setparam:SetParamAnnotation
@set:SetAnnotation
var myStr: String? = null
}
Here we’ve added two use-site targets to the same property; @setparam
and @set
. The resulting decompiled bytecode shows the difference between these 2:
public final class SetParamAnnTest {
@Nullable
private String myStr;
@Nullable
public final String getMyStr() {
return this.myStr;
}
@SetAnnotation
public final void setMyStr(@SetParamAnnotation @Nullable String var1) {
this.myStr = var1;
}
}
As expected, both the field and getter method are not annotated.
As we’ve seen before using @set
resulted in the annotation being applied to the setter function.
The @setparam
use-site target added an annotation to the parameter expected by the setter function.
A combination of these can be useful with some java-based dependency injection libraries:
@set:Inject
@setparam:[Nullable SomeString]
lateinit var str: String
Receiver use-site targets
In Kotlin a “receiver” is the instance on which an extension function is defined, or the type for a lambda with receiver.
It is essentially the type on which a block of code is intended to run.
In the case of a lambda with receiver in Kotlin, the lambda runs as if it is part of the receiving class.
In the case of extension functions, the defined function also runs as if it is part of the receiving class type.
The bytecode for extension functions actually contain the receiver as the first parameter to a static function.
This allows extension functions to be used by Java code as well as Kotlin code by simply passing the receiving type when used from Java.
Knowing this helps us better understand the resulting bytecode and therefore also understand where an @receiver
annotation will reside.
annotation class ReceiverAnnotation
fun @receiver:ReceiverAnnotation String.capitalizeVowels() =
this.map {
if (it in listOf('a', 'e', 'i', 'o', 'u')) it.toUpperCase() else it
}.joinToString("")
The result is the function’s receiver parameter being annotated:
public static final String capitalizeVowels(
@ReceiverAnnotation @Nullable String $receiver
) {
...
}
Property use-site targets
@property
is the only use-site target that has no effect on the resulting bytecode when viewed from Java.
This is due to the fact that Java does not have the same notion of properties as Kotlin.
Because of this, adding property-specific annotations will only be useful to kotlin-specific libraries and tools.
To demonstrate this we will add two @property
-targeted annotations to a class and then use the Kotlin reflection library to see which items are annotated.
annotation class PropertyAnnotation
class SomeClass1(
@property:PropertyAnnotation val constructorProp: String
) {
@property:PropertyAnnotation
val regProp = "Test"
}
As you can see, we’ve added PropertyAnnotation
to both a constructor argument and also to a regular Kotlin property.
If we decompile the kotlin bytecode you will see that these annotations are not present in what would be the Java equivalent of this Kotlin code:
public final class SomeClass1 {
@NotNull
private final String regProp;
@NotNull
private final String constructorProp;
@NotNull
public final String getRegProp() {
return this.regProp;
}
@NotNull
public final String getConstructorProp() {
return this.constructorProp;
}
public SomeClass1(@NotNull String constructorProp) {
this.constructorProp = constructorProp;
this.regProp = "Test";
}
}
In order to read the property annotations we will need to use Kotlin’s reflection library to read the annotations at runtime. This would be similar to how other libraries or tools would need to make use of property-targeted annotations.
fun main(args: Array<String>) {
for (prop in SomeClass1::class.memberProperties) {
println("${prop.name} has annotations ${prop.annotations}")
}
}
The output from this code shows that each member property has the PropertyAnnotation
annotation specified:
constructorProp has annotations [@PropertyAnnotation()]
regProp has annotations [@PropertyAnnotation()]
Delegate use-site targets
The previous blog post in our “Advanced Kotlin” series covered delegates in Kotlin. If you are not familiar with delegates I recommend you check out that post.
The @delegate
use-site target might be one the hardest to guess where the actual annotation will end up.
Is it on the delegate itself? On the delegate class? On the function used to receive the value from the delegate?
Let’s take a look at some code and see what happens.
annotation class DelegateAnnotation
class MyDel {
@delegate:DelegateAnnotation
val name by lazy { "something" }
}
As you can see from the decompiled code, the annotation will reside on the generated private
backing property whose type is the delegate type.
The function used to call the delegate (in this case getName
) does not receive the annotation.
public final class MyDel {
@DelegateAnnotation
@NotNull
private final Lazy name$delegate;
@NotNull
public final String getName() {
Lazy var1 = this.name$delegate;
KProperty var3 = $$delegatedProperties[0];
return (String)var1.getValue();
}
public MyDel() {
this.name$delegate = LazyKt.lazy((Function0)null.INSTANCE);
}
}
You may find @delegate
useful for targeting a property that you wish to wrap in something like a lazy
delegate:
@delegate:Transient
val myLock by lazy { ... }
Default Targets
Now that we’ve covered all nine of the available use-site targets, let’s cover what happens when a target is not specified on an annotation.
@SomeAnnotation
val str: String? = null
When an annotation is created, it can itself be annotated with an @Target
annotation which lists the targets that are available for the annotation.
The available target values for @Target
are defined in the AnnotationTarget
enum:
- ‘CLASS’
- ‘ANNOTATION_CLASS’
- ‘TYPE_PARAMETER’
- ‘PROPERTY’
- ‘FIELD’
- ‘LOCAL_VARIABLE’
- ‘VALUE_PARAMETER’
- ‘CONSTRUCTOR’
- ‘FUNCTION’
- ‘PROPERTY_GETTER’
- ‘PROPERTY_SETTER’
- ‘TYPE’
- ‘EXPRESSION’
- ‘FILE’
- ‘TYPEALIAS’
The main purpose of these values is to let the compiler know where in source code an annotation is allowed to be used.
For example, if an annotation is defined with the CLASS
target:
@Target(AnnotationTarget.CLASS)
annotation class ClassAnnotation
Then the annotation can only be applied to a class:
@ClassAnnotation // OK
class Temp {
@ClassAnnotation // Compilation error!
val str: String? = null
}
These target values along with the placement of the annotation are used to pick a default use-site target for an annotation if one is not specified.
If there’s more than one applicable target either @param
(constructor parameters), @property
, or @field
are used, in that order.
Let’s see an example of this. Using the same example code as above:
@SomeAnnotation
val str: String? = null
If the @SomeAnnotation
annotation was defined with @Target(FIELD)
then your annotation usage would target using @field
as though your code was written as @field:SomeAnnotation
.
As we saw earlier, using @field
means that the private
field value would have the annotation in the bytecode.
Now, if the @SomeAnnotation
annotation in this example had no @Target
defined, then it would apply to any element meaning that @property
would be chosen first from the list (@param
, @property
, @field
).
Remember that @property
is only visible from Kotlin so the resulting annotation would not be useful from Java.
If you’ve ever struggled with a framework not finding an annotation that you thought you had added to the code, this is the likely culprit.
Stay tuned! More to come!
This article hopefully covered some annotation use-site targets that you have not encountered before. Knowing each of these can often get you out of a bind when trying to use a framework that requires very specific placement of annotations in your code.
If you would like to read more about use-site targets in Kotlin take a look at:
- The official Kotlin Annotation docs
- Chapter 10 in Kotlin In Action
Stay tuned for more!
Important Notice: Opinions expressed here are the author’s alone. While we're proud of our engineers and employee bloggers, they are not your engineers, and you should independently verify and rely on your own judgment, not ours. All article content is made available AS IS without any warranties. Third parties and any of their content linked or mentioned in this article are not affiliated with, sponsored by or endorsed by American Express, unless otherwise explicitly noted. All trademarks and other intellectual property used or displayed remain their respective owners'. This article is © 2019 American Express Company. All Rights Reserved.