Skip to content

An annotation processor for generating getter and setter suppliers

License

Notifications You must be signed in to change notification settings

alexengrig/metter

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Metter Logo

metter

Maven Central Javadocs GitHub

Build Status Codecov Coverage Status Coveralls Coverage Status Codacy Coverage Codacy Grade BCH Compliance FOSSA Status

Metter (meta getter / setter) is an annotation processor for generating getter and setter suppliers.

  • Supports Lombok annotations: @Data, @Getter and @Setter.
  • Supports inheritance (getters/setters of superclasses).
  • Ignores private methods.

See examples.

Table of Contents

Get Started

Install

Gradle

Add this code to dependencies section in your build.gradle:

compileOnly 'dev.alexengrig:metter:0.1.1'
annotationProcessor 'dev.alexengrig:metter:0.1.1'

Maven

Add this code to dependencies section in your pom.xml:

<dependency>
    <groupId>dev.alexengrig</groupId>
    <artifactId>metter</artifactId>
    <version>0.1.1</version>
    <scope>provided</scope>
    <optional>true</optional>
</dependency>

Specify the annotation processor to maven-compiler-plugin plugin:

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.8.1</version>
            <configuration>
                <annotationProcessorPaths>
                    <annotationProcessorPath>
                        <groupId>dev.alexengrig</groupId>
                        <artifactId>metter</artifactId>
                        <version>0.1.1</version>
                    </annotationProcessorPath>
                </annotationProcessorPaths>
            </configuration>
        </plugin>
    </plugins>
</build>

Using

Add to your class @GetterSupplier for to generate getters and/or @SetterSupplier for to generate setters:

import dev.alexengrig.metter.annotation.GetterSupplier;
import dev.alexengrig.metter.annotation.SetterSupplier;

@GetterSupplier
@SetterSupplier
public class Domain {
    private int integer;
    private boolean bool;
    private String string;

    public int getInteger() {
        return integer;
    }

    public void setInteger(int integer) {
        this.integer = integer;
    }

    public boolean isBool() {
        return bool;
    }

    public void setBool(boolean bool) {
        this.bool = bool;
    }

    public String getString() {
        return string;
    }

    public void setString(String string) {
        this.string = string;
    }
}

The generated suppliers have a default name consisting of a prefix as a class name, and a suffix as the supplier name: ${CLASS_NAME}GetterSupplier and ${CLASS_NAME}SetterSupplier. You can set a custom name using the annotation parameter value.

All fields that have getters/setter will be added to the map that DomainGetterSupplier/DomainSetterSupplier stores. You can set included/excluded field names using the annotation parameters includeFields/excludeFields.

Instance

The generated suppliers implement the Supplier functional interface and to get the map of getters/setters, you need to call Supplier#get:

import java.util.Map;
import java.util.function.BiConsumer;
import java.util.function.Function;

public class DomainService {
    private final Map<String, Function<Domain, Object>> getterByField = new DomainGetterSupplier().get();
    private final Map<String, BiConsumer<Domain, Object>> setterByField = new DomainSetterSupplier().get();

    public void printFieldValues(Domain domain) {
        getterByField.forEach((field, getter) -> System.out.println(field + " = " + getter.apply(domain)));
    }

    public void transfer(Domain from, Domain to) {
        setterByField.forEach((field, setter) -> setter.accept(to, getterByField.get(field).apply(from)));
    }
}

Inheritance

You can extend:

import java.util.Collections;
import java.util.Map;
import java.util.function.Function;

public class CustomDomainGetterSupplier extends DomainGetterSupplier {
    @Override
    protected Map<String, Function<Domain, Object>> createMap() {
        Map<String, Function<Domain, Object>> generatedMap = super.createMap();
        generatedMap.put("name", domain -> "Name: domain.toString()");
        return Collections.unmodifiableMap(generatedMap);
    }

    public Function<Domain, Object> getGetter(String field) {
        return this.getterByField.get(field);
    }
}

Bean

You can create a bean (e.g. Spring):

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ApplicationConfiguration {
    @Bean
    public Map<String, Function<Domain, Object>> getterByField() {
        return new DomainGetterSupplier().get();
    }

    @Bean
    public Map<String, BiConsumer<Domain, Object>> setterByField() {
        return new DomainSetterSupplier().get();
    }
}

API

GetterSupplier

An annotation for to generate a getters supplier.

Field Type Default Description
value String ${CLASS_NAME}GetterSupplier Supplier class name
includedFields String[] empty Array of fields to include in the supplier
excludedFields String[] empty Array of fields to exclude in the supplier

SetterSupplier

An annotation for to generate a setters supplier.

Field Type Default Description
value String ${CLASS_NAME}SetterSupplier Supplier class name
includedFields String[] empty Array of fields to include in the supplier
excludedFields String[] empty Array of fields to exclude in the supplier

Motivation

Problem

Has a domain:

class Man {
    private String name;
    private int age;

    /*Getters and Setters*/
}

Need a change journal:

// field: Old value -> New value
name: Tomas -> Tom
age: 18  -> 19

Solution

See full code.

Manual

Each field:

import java.util.StringJoiner;

class ManualManChangeLogGenerator extends BaseChangeLogGenerator<Man> {
    @Override
    public String generate(Man man, Man newMan) {
        StringJoiner joiner = new StringJoiner("\n");
        if (!man.getName().equals(newMan.getName())) {
            joiner.add(changeLog("name", man.getName(), newMan.getName()));
        }
        if (man.getAge() != newMan.getAge()) {
            joiner.add(changeLog("age", man.getAge(), newMan.getAge()));
        }
        return joiner.toString();
    }
}

Using Map:

import java.util.*;
import java.util.function.Function;

class MapManChangeLogGenerator extends BaseChangeLogGenerator<Man> {
    protected Map<String, Function<Man, Object>> getterByField = createMap();

    protected Map<String, Function<Man, Object>> createMap() {
        return new HashMap<String, Function<Man, Object>>() {{
            put("name", Man::getName);
            put("age", Man::getAge);
        }};
    }

    @Override
    public String generate(Man man, Man newMan) {
        StringJoiner joiner = new StringJoiner("\n");
        getterByField.forEach((field, getter) -> {
            Object value = getter.apply(man);
            Object newValue = getter.apply(newMan);
            if (!Objects.equals(value, newValue)) {
                joiner.add(changeLog(field, value, newValue));
            }
        });
        return joiner.toString();
    }
}

Reflection

Each field via Java Reflection:

import java.lang.reflect.*;
import java.util.*;
import java.util.function.Function;

class ReflectionManChangeLogGenerator extends MapManChangeLogGenerator {
    @Override
    protected Map<String, Function<Man, Object>> createMap() {
        Field[] fields = Man.class.getDeclaredFields();
        HashMap<String, Function<Man, Object>> map = new HashMap<>(fields.length);
        for (Field field : fields) {
            String fieldName = field.getName();
            String capitalizedFieldName = getCapitalized(fieldName);
            for (Method method : Man.class.getDeclaredMethods()) {
                String methodName = method.getName();
                if (isGetter(methodName, capitalizedFieldName)) {
                    map.put(fieldName, createGetter(method));
                    break;
                }
            }
        }
        return map;
    }

    private String getCapitalized(String name) {
        return name.substring(0, 1).toUpperCase() + name.substring(1);
    }

    private boolean isGetter(String methodName, String lastMethodNamePart) {
        return (methodName.startsWith("get") && methodName.equals("get" + lastMethodNamePart))
                || (methodName.startsWith("is") && methodName.equals("is" + lastMethodNamePart));
    }

    private Function<Man, Object> createGetter(Method method) {
        return instance -> {
            try {
                return method.invoke(instance);
            } catch (IllegalAccessException | InvocationTargetException e) {
                throw new IllegalArgumentException(instance.toString(), e);
            }
        };
    }
}

Generation

Add @GetterSupplier annotation:

import dev.alexengrig.metter.annotation.GetterSupplier;

@GetterSupplier
class Man {/*...*/}

Using ManGetterSupplier:

import java.util.Map;
import java.util.function.Function;

class GenerationManChangeLogGenerator extends MapManChangeLogGenerator {
    @Override
    protected Map<String, Function<Man, Object>> createMap() {
        return new ManGetterSupplier().get();
    }
}

Conclusion

If you add a new field to Man, then the reflection solution and the generation solution will continue to work, unlike the manual solution. The generation solution is faster than the reflection solution (reflection is slow, see benchmarks).

Benchmarks

See source. See build.

domainN (16-128), where N is number of fields.

generation - using metter; handling - using MethodHandle.

Benchmark                                                          Mode  Cnt     Score    Error  Units
GetterSupplierBenchmarks.get_allValuesOf_domain128_via_generation  avgt   10  1849.651 ± 16.663  ns/op
GetterSupplierBenchmarks.get_allValuesOf_domain128_via_handling    avgt   10  1984.062 ± 24.774  ns/op
GetterSupplierBenchmarks.get_allValuesOf_domain128_via_manually    avgt   10   785.069 ±  5.008  ns/op
GetterSupplierBenchmarks.get_allValuesOf_domain128_via_map         avgt   10  1988.466 ± 27.310  ns/op
GetterSupplierBenchmarks.get_allValuesOf_domain128_via_reflection  avgt   10  2339.250 ± 11.748  ns/op
GetterSupplierBenchmarks.get_allValuesOf_domain16_via_generation   avgt   10   199.299 ±  2.471  ns/op
GetterSupplierBenchmarks.get_allValuesOf_domain16_via_handling     avgt   10   195.565 ±  1.666  ns/op
GetterSupplierBenchmarks.get_allValuesOf_domain16_via_manually     avgt   10    40.004 ±  1.263  ns/op
GetterSupplierBenchmarks.get_allValuesOf_domain16_via_map          avgt   10   195.722 ±  1.465  ns/op
GetterSupplierBenchmarks.get_allValuesOf_domain16_via_reflection   avgt   10   225.331 ±  2.115  ns/op
GetterSupplierBenchmarks.get_allValuesOf_domain32_via_generation   avgt   10   386.447 ±  3.224  ns/op
GetterSupplierBenchmarks.get_allValuesOf_domain32_via_handling     avgt   10   354.387 ±  4.330  ns/op
GetterSupplierBenchmarks.get_allValuesOf_domain32_via_manually     avgt   10    87.422 ±  4.101  ns/op
GetterSupplierBenchmarks.get_allValuesOf_domain32_via_map          avgt   10   423.696 ±  3.109  ns/op
GetterSupplierBenchmarks.get_allValuesOf_domain32_via_reflection   avgt   10   482.624 ± 10.645  ns/op
GetterSupplierBenchmarks.get_allValuesOf_domain64_via_generation   avgt   10   868.321 ±  9.389  ns/op
GetterSupplierBenchmarks.get_allValuesOf_domain64_via_handling     avgt   10   775.914 ± 11.301  ns/op
GetterSupplierBenchmarks.get_allValuesOf_domain64_via_manually     avgt   10   247.175 ±  1.137  ns/op
GetterSupplierBenchmarks.get_allValuesOf_domain64_via_map          avgt   10   965.654 ± 10.060  ns/op
GetterSupplierBenchmarks.get_allValuesOf_domain64_via_reflection   avgt   10  1043.159 ± 10.393  ns/op

License

This project is licensed under Apache License, version 2.0.

JetBrains Mono typeface is licensed under Apache License, version 2.0 and it used in logo and preview.

FOSSA Status