3.15 Bean Introspection

Since Micronaut 1.1, a compile-time replacement for the JDK’s Introspector class has been included.

The BeanIntrospector and BeanIntrospection interfaces allow looking up bean introspections to instantiate and read/write bean properties without using reflection or caching reflective metadata, which consume excessive memory for large beans.

Making a Bean Available for Introspection

Unlike the JDK’s Introspector, every class is not automatically available for introspection. To make a class available for introspection you must at a minimum enable Micronaut’s annotation processor (micronaut-inject-java for Java and Kotlin and micronaut-inject-groovy for Groovy) in your build and ensure you have a runtime time dependency on micronaut-core.

  1. annotationProcessor("io.micronaut:micronaut-inject-java:3.0.0")
  1. <annotationProcessorPaths>
  2. <path>
  3. <groupId>io.micronaut</groupId>
  4. <artifactId>micronaut-inject-java</artifactId>
  5. <version>3.0.0</version>
  6. </path>
  7. </annotationProcessorPaths>
For Kotlin, add the micronaut-inject-java dependency in kapt scope, and for Groovy add micronaut-inject-groovy in compileOnly scope.
  1. runtime("io.micronaut:micronaut-core:3.0.0")
  1. <dependency>
  2. <groupId>io.micronaut</groupId>
  3. <artifactId>micronaut-core</artifactId>
  4. <version>3.0.0</version>
  5. <scope>runtime</scope>
  6. </dependency>

Once your build is configured you have a few ways to generate introspection data.

Use the @Introspected Annotation

The @Introspected annotation can be used on any class to make it available for introspection. Simply annotate the class with @Introspected:

  1. import io.micronaut.core.annotation.Introspected;
  2. @Introspected
  3. public class Person {
  4. private String name;
  5. private int age = 18;
  6. public Person(String name) {
  7. this.name = name;
  8. }
  9. public String getName() {
  10. return name;
  11. }
  12. public void setName(String name) {
  13. this.name = name;
  14. }
  15. public int getAge() {
  16. return age;
  17. }
  18. public void setAge(int age) {
  19. this.age = age;
  20. }
  21. }
  1. import groovy.transform.Canonical
  2. import io.micronaut.core.annotation.Introspected
  3. @Introspected
  4. @Canonical
  5. class Person {
  6. String name
  7. int age = 18
  8. Person(String name) {
  9. this.name = name
  10. }
  11. }
  1. import io.micronaut.core.annotation.Introspected
  2. @Introspected
  3. data class Person(var name : String) {
  4. var age : Int = 18
  5. }

Once introspection data has been produced at compile time, retrieve it via the BeanIntrospection API:

  1. final BeanIntrospection<Person> introspection = BeanIntrospection.getIntrospection(Person.class); (1)
  2. Person person = introspection.instantiate("John"); (2)
  3. System.out.println("Hello " + person.getName());
  4. final BeanProperty<Person, String> property = introspection.getRequiredProperty("name", String.class); (3)
  5. property.set(person, "Fred"); (4)
  6. String name = property.get(person); (5)
  7. System.out.println("Hello " + person.getName());
  1. def introspection = BeanIntrospection.getIntrospection(Person) (1)
  2. Person person = introspection.instantiate("John") (2)
  3. println("Hello $person.name")
  4. BeanProperty<Person, String> property = introspection.getRequiredProperty("name", String) (3)
  5. property.set(person, "Fred") (4)
  6. String name = property.get(person) (5)
  7. println("Hello $person.name")
  1. val introspection = BeanIntrospection.getIntrospection(Person::class.java) (1)
  2. val person : Person = introspection.instantiate("John") (2)
  3. print("Hello ${person.name}")
  4. val property : BeanProperty<Person, String> = introspection.getRequiredProperty("name", String::class.java) (3)
  5. property.set(person, "Fred") (4)
  6. val name = property.get(person) (5)
  7. print("Hello ${person.name}")
1You can retrieve a BeanIntrospection with the static getIntrospection method
2Once you have a BeanIntrospection you can instantiate a bean with the instantiate method.
3A BeanProperty can be retrieved from the introspection
4Use the set method to set the property value
5Use the get method to retrieve the property value

Bean Fields

By default Java introspections treat only JavaBean getters/setters or Java 16 record components as bean properties. You can however define classes with public or package protected fields in Java using the accessKind member of the @Introspected annotation:

  1. import io.micronaut.core.annotation.Introspected;
  2. @Introspected(accessKind = Introspected.AccessKind.FIELD)
  3. public class User {
  4. public final String name; (1)
  5. public int age = 18; (2)
  6. public User(String name) {
  7. this.name = name;
  8. }
  9. }
  1. import io.micronaut.core.annotation.Introspected
  2. @Introspected(accessKind = Introspected.AccessKind.FIELD)
  3. class User {
  4. public final String name (1)
  5. public int age = 18 (2)
  6. User(String name) {
  7. this.name = name
  8. }
  9. }
1Final fields are treated like read-only properties
2Mutable fields are treated like read-write properties
The accessKind accepts an array so it is possible to allow for both types of accessors but prefer one or the other depending on the order they appear in the annotation. The first one in the list has priority.
Introspections on fields are not possible in Kotlin because it is not possible to declare fields directly.

Constructor Methods

For classes with multiple constructors, apply the @Creator annotation to the constructor to use.

  1. import io.micronaut.core.annotation.Creator;
  2. import io.micronaut.core.annotation.Introspected;
  3. import javax.annotation.concurrent.Immutable;
  4. @Introspected
  5. @Immutable
  6. public class Vehicle {
  7. private final String make;
  8. private final String model;
  9. private final int axles;
  10. public Vehicle(String make, String model) {
  11. this(make, model, 2);
  12. }
  13. @Creator (1)
  14. public Vehicle(String make, String model, int axles) {
  15. this.make = make;
  16. this.model = model;
  17. this.axles = axles;
  18. }
  19. public String getMake() {
  20. return make;
  21. }
  22. public String getModel() {
  23. return model;
  24. }
  25. public int getAxles() {
  26. return axles;
  27. }
  28. }
  1. import io.micronaut.core.annotation.Creator
  2. import io.micronaut.core.annotation.Introspected
  3. import javax.annotation.concurrent.Immutable
  4. @Introspected
  5. @Immutable
  6. class Vehicle {
  7. final String make
  8. final String model
  9. final int axles
  10. Vehicle(String make, String model) {
  11. this(make, model, 2)
  12. }
  13. @Creator (1)
  14. Vehicle(String make, String model, int axles) {
  15. this.make = make
  16. this.model = model
  17. this.axles = axles
  18. }
  19. }
  1. import io.micronaut.core.annotation.Creator
  2. import io.micronaut.core.annotation.Introspected
  3. import javax.annotation.concurrent.Immutable
  4. @Introspected
  5. @Immutable
  6. class Vehicle @Creator constructor(val make: String, val model: String, val axles: Int) { (1)
  7. constructor(make: String, model: String) : this(make, model, 2) {}
  8. }
1The @Creator annotation denotes which constructor to use
This class has no default constructor, so calls to instantiate without arguments throw an InstantiationException.

Static Creator Methods

The @Creator annotation can be applied to static methods that create class instances.

  1. import io.micronaut.core.annotation.Creator;
  2. import io.micronaut.core.annotation.Introspected;
  3. import javax.annotation.concurrent.Immutable;
  4. @Introspected
  5. @Immutable
  6. public class Business {
  7. private final String name;
  8. private Business(String name) {
  9. this.name = name;
  10. }
  11. @Creator (1)
  12. public static Business forName(String name) {
  13. return new Business(name);
  14. }
  15. public String getName() {
  16. return name;
  17. }
  18. }
  1. import io.micronaut.core.annotation.Creator
  2. import io.micronaut.core.annotation.Introspected
  3. import javax.annotation.concurrent.Immutable
  4. @Introspected
  5. @Immutable
  6. class Business {
  7. final String name
  8. private Business(String name) {
  9. this.name = name
  10. }
  11. @Creator (1)
  12. static Business forName(String name) {
  13. new Business(name)
  14. }
  15. }
  1. import io.micronaut.core.annotation.Creator
  2. import io.micronaut.core.annotation.Introspected
  3. import javax.annotation.concurrent.Immutable
  4. @Introspected
  5. @Immutable
  6. class Business private constructor(val name: String) {
  7. companion object {
  8. @Creator (1)
  9. fun forName(name: String): Business {
  10. return Business(name)
  11. }
  12. }
  13. }
1The @Creator annotation is applied to the static method which instantiates the class
There can be multiple “creator” methods annotated. If there is one without arguments, it will be the default construction method. The first method with arguments will be used as the primary construction method.

Enums

It is possible to introspect enums as well. Add the annotation to the enum and it can be constructed through the standard valueOf method.

Use the @Introspected Annotation on a Configuration Class

If the class to introspect is already compiled and not under your control, an alternative option is to define a configuration class with the classes member of the @Introspected annotation set.

  1. import io.micronaut.core.annotation.Introspected;
  2. @Introspected(classes = Person.class)
  3. public class PersonConfiguration {
  4. }
  1. import io.micronaut.core.annotation.Introspected
  2. @Introspected(classes = Person)
  3. class PersonConfiguration {
  4. }
  1. import io.micronaut.core.annotation.Introspected
  2. @Introspected(classes = [Person::class])
  3. class PersonConfiguration

In the above example the PersonConfiguration class generates introspections for the Person class.

You can also use the packages member of the @Introspected which package scans at compile time and generates introspections for all classes within a package. Note however this feature is currently regarded as experimental.

Write an AnnotationMapper to Introspect Existing Annotations

If there is an existing annotation that you wish to introspect by default you can write an AnnotationMapper.

An example of this is EntityIntrospectedAnnotationMapper which ensures all beans annotated with javax.persistence.Entity are introspectable by default.

The AnnotationMapper must be on the annotation processor classpath.

The BeanWrapper API

A BeanProperty provides raw access to read and write a property value for a given class and does not provide any automatic type conversion.

It is expected that the values you pass to the set and get methods match the underlying property type, otherwise an exception will occur.

To provide additional type conversion smarts the BeanWrapper interface allows wrapping an existing bean instance and setting and getting properties from the bean, plus performing type conversion as necessary.

  1. final BeanWrapper<Person> wrapper = BeanWrapper.getWrapper(new Person("Fred")); (1)
  2. wrapper.setProperty("age", "20"); (2)
  3. int newAge = wrapper.getRequiredProperty("age", int.class); (3)
  4. System.out.println("Person's age now " + newAge);
  1. final BeanWrapper<Person> wrapper = BeanWrapper.getWrapper(new Person("Fred")) (1)
  2. wrapper.setProperty("age", "20") (2)
  3. int newAge = wrapper.getRequiredProperty("age", Integer) (3)
  4. println("Person's age now $newAge")
  1. val wrapper = BeanWrapper.getWrapper(Person("Fred")) (1)
  2. wrapper.setProperty("age", "20") (2)
  3. val newAge = wrapper.getRequiredProperty("age", Int::class.java) (3)
  4. println("Person's age now $newAge")
1Use the static getWrapper method to obtain a BeanWrapper for a bean instance.
2You can set properties, and the BeanWrapper will perform type conversion, or throw ConversionErrorException if conversion is not possible.
3You can retrieve a property using getRequiredProperty and request the appropriate type. If the property doesn’t exist a IntrospectionException is thrown, and if it cannot be converted a ConversionErrorException is thrown.

Jackson and Bean Introspection

Jackson is configured to use the BeanIntrospection API to read and write property values and construct objects, resulting in reflection-free serialization/deserialization. This is beneficial from a performance perspective and requires less configuration to operate correctly with runtimes such as GraalVM native.

This feature is enabled by default; disable it by setting the jackson.bean-introspection-module configuration to false.

Currently only bean properties (private field with public getter/setter) are supported and usage of public fields is not supported.
This feature is currently experimental and may be subject to change in the future.