Lombok - Java annotation library

개요

Java 로 개발시 반복해서 해야 되는 작업들(getter/setter, cleanup, ToString(), equals(), hashCode()  method 구현등.,)을 자동화 해주는 library.

 

설치

  1. http://projectlombok.org/download.html 에서 lombok.jar 를 다운로드
  2. lombok.jar 실행하여 IDE 에 등록

    java -jar lombok.jar
  3. 사용하는 IDE 를 못 찾을 경우 Specify location 을 클릭해서 수동으로 지정
     
  4. Install/Update 를 눌러서 IDE 에 설치
  5. 설치후 다음과 같은 안내 메시지가 표시됨. custom vm 옵션을 줘서 실행하는 경우  IDE 가 변경되거나 upgrade되면 lombok.jar 를 재설치해야 함

  6. IDE 를 재구동

 

maven 연계

다음 코드를 pom.xml 에 추가하면 maven 프로젝트에서 lombok 을 사용할 수 있다.

lombok maven dependency
<dependencies>
	<dependency>
		<groupId>org.projectlombok</groupId>
		<artifactId>lombok</artifactId>
		<version>1.16.14</version>
		<scope>provided</scope>
	</dependency>
</dependencies>

사용

@NonNull

기존 Java 코드
import lombok.NonNull;
 
 public class NonNullExample extends Something {
   private String name;
   
   public NonNullExample(@NonNull Person person) {
     super("Hello");
     if (person == null) {
       throw new NullPointerException("person");
     }
     this.name = person.getName();
   }
 }
기존 Java 코드
 import lombok.NonNull;
 
 public class NonNullExample extends Something {
   private String name;
   
   public NonNullExample(@NonNull Person person) {
     super("Hello");
     this.name = person.getName();
   }
 }

@Cleanup

local variable 에 annotation 을 붙이면 cleanup code 가 현재 code 가 종료될 때 자동으로 호출됨. 

  1. close() method 가 있으면 자동으로 호출 (exception 처리를 위해 try/finially 블록 자동으로 추가)
  2. close() 가 없고 다른 method가 cleanup 을 수행할 경우 @Cleanup("dispose") 같이 method 명 기술

Cleanup 에서 호출되는 close method 에서 exception 이 발생할 경우 문제가 발생할 수 있으니 주의. jakarta common-ioIOUtils.closeQuitely(..); 사용도 검토

기존 Java 코드
import java.io.*;
 
 public class CleanupExample {
   public static void main(String[] args) throws IOException {
     InputStream in = new FileInputStream(args[0]);
     try {
       OutputStream out = new FileOutputStream(args[1]);
       try {
         byte[] b = new byte[10000];
         while (true) {
           int r = in.read(b);
           if (r == -1) break;
           out.write(b, 0, r);
         }
       } finally {
         if (out != null) {
           out.close();
         }
       }
     } finally {
       if (in != null) {
         in.close();
       }
     }
   }
 }
Lombok 적용
public class CleanupExample {
   public static void main(String[] args) throws IOException {
     @Cleanup InputStream in = new FileInputStream(args[0]);
     @Cleanup OutputStream out = new FileOutputStream(args[1]);
     byte[] b = new byte[10000];
     while (true) {
       int r = in.read(b);
       if (r == -1) break;
       out.write(b, 0, r);
     }
   }
 }

@ToString()

debugging 용으로 class field를 출력할 경우 annotation 으로 간단하게 처리

  1. 제외할 필드가 있을 경우 exclude 에 field 명시

기존 Java 코드
import java.util.Arrays;
public class ToStringExample {
    private static final int STATIC_VAR = 10;
    private String name;
    private Shape shape = new Square(5, 10);
    private String[] tags;
    private int id;
    public String getName() {
        return this.getName();
    }
    public static class Square extends Shape {
        private final int width, height;
        public Square(int width, int height) {
            this.width = width;
            this.height = height;
        }
        @Override public String toString() {
            return "Square(super=" + super.toString() + ", width=" + this.width + ", height=" + this.height + ")";
        }
    }
    @Override public String toString() {
        return "ToStringExample(" + this.getName() + ", " + this.shape + ", " + Arrays.deepToString(this.tags) + ")";
    }
}
Lombok 적용
import lombok.ToString;
@ToString(exclude="id")
public class ToStringExample {
    private static final int STATIC_VAR = 10;
    private String name;
    private Shape shape = new Square(5, 10);
    private String[] tags;
    private int id;
    public String getName() {
        return this.getName();
    }
    @ToString(callSuper=true, includeFieldNames=true)
    public static class Square extends Shape {
        private final int width, height;
        public Square(int width, int height) {
            this.width = width;
            this.height = height;
        }
    }
}

 

@Getter and @Setter

@Getter 와 @Setter annotation으로 lombok 이 자동으로 getter/settter 를 생성하게 할 수 있음.

기본 getter 는 field 를 리턴하며 field 명이 foo 일때 getter method name은 getFoo (foo가 boolean 일때는 isFoo)

기본 setter는  filed 명이 foo 일때 setFoo 가 되며 return type은 void 며 field 와 동일한 type의 라파미터를 한개만 입력받음

생성된 getter/setter method 의 기본 접근레벨은 AccessLevel 키워드를 명시적으로 지정하지 않았다면 public 이며 Accesslevels 은 PUBLICPROTECTEDPACKAGE, and PRIVATE 중에 설정할 수 있다.

class 에 대해서도 @Getter and/or @Setter annotation 을 지정할수 있는데 이럴 경우 class 내의 non-static field 에 대해 Getter/Setter 가 생성된다.

You can always manually disable getter/setter generation for any field by using the special AccessLevel.NONE access level. This lets you override the behaviour of a @Getter@Setter or @Data annotation on a class.

To put annotations on the generated method, you can use onMethod=@_({@AnnotationsHere}); to put annotations on the only parameter of a generated setter method, you can useonParam=@_({@AnnotationsHere}). Be careful though! This is an experimental feature. For more details see the documentation on the onX feature.

NEW in lombok v1.12.0: javadoc on the field will now be copied to generated getters and setters. Normally, all text is copied, and @return is moved to the getter, whilst @param lines are moved to the setter. Moved means: Deleted from the field's javadoc. It is also possible to define unique text for each getter/setter. To do that, you create a 'section' named GETTER and/or SETTER. A section is a line in your javadoc containing 2 or more dashes, then the text 'GETTER' or 'SETTER', followed by 2 or more dashes, and nothing else on the line. If you use sections, @return and @param stripping for that section is no longer done (move the @return or @param line into the section).

  1.  

기존 Java 코드
public class GetterSetterExample {

    /**
     * Age of the person. Water is wet.
     */
    private int age = 10;

    /**
     * Name of the person.
     */
    private String name;
    @Override public String toString() {
        return String.format("%s (age: %d)", name, age);
    }

    /**
     * Age of the person. Water is wet.
     *
     * @return The current value of this person's age. Circles are round.
     */
    public int getAge() {
        return age;
    }

    /**
     * Age of the person. Water is wet.
     *
     * @param age New value for this person's age. Sky is blue.
     */
    public void setAge(int age) {
        this.age = age;
    }

    /**
     * Changes the name of this person.
     *
     * @param name The new value.
     */
    protected void setName(String name) {
        this.name = name;
    }
}
Lombok 적용
import lombok.AccessLevel;
import lombok.Getter;
import lombok.Setter;

public class GetterSetterExample {
    /**
     * Age of the person. Water is wet.
     *
     * @param age New value for this person's age. Sky is blue.
     * @return The current value of this person's age. Circles are round.
     */
    @Getter @Setter private int age = 10;

    /**
     * Name of the person.
     * -- SETTER --
     * Changes the name of this person.
     *
     * @param name The new value.
     */
    @Setter(AccessLevel.PROTECTED) private String name;

    @Override public String toString() {
        return String.format("%s (age: %d)", name, age);
    }
}

@EqualsAndHashCode

Any class definition may be annotated with @EqualsAndHashCode to let lombok generate implementations of the equals(Object other) and hashCode() methods. By default, it'll use all non-static, non-transient fields, but you can exclude more fields by naming them in the optional exclude parameter to the annotation. Alternatively, you can specify exactly which fields you wish to be used by naming them in the of parameter.

By setting callSuper to true, you can include the equals and hashCode methods of your superclass in the generated methods. For hashCode, the result of super.hashCode() is included in the hash algorithm, and for equals, the generated method will return false if the super implementation thinks it is not equal to the passed in object. Be aware that not all equals implementations handle this situation properly. However, lombok-generated equals implementations do handle this situation properly, so you can safely call your superclass equals if it, too, has a lombok-generated equals method.

Setting callSuper to true when you don't extend anything (you extend java.lang.Object) is a compile-time error, because it would turn the generated equals() and hashCode() implementations into having the same behaviour as simply inheriting these methods from java.lang.Object: only the same object will be equal to each other and will have the same hashCode. Not setting callSuper to truewhen you extend another class generates a warning, because unless the superclass has no (equality-important) fields, lombok cannot generate an implementation for you that takes into account the fields declared by your superclasses. You'll need to write your own implementations, or rely on the callSuper chaining facility.

NEW in Lombok 0.10: Unless your class is final and extends java.lang.Object, lombok generates a canEqual method which means JPA proxies can still be equal to their base class, but subclasses that add new state don't break the equals contract. The complicated reasons for why such a method is necessary are explained in this paper: How to Write an Equality Method in Java. If all classes in a hierarchy are a mix of scala case classes and classes with lombok-generated equals methods, all equality will 'just work'. If you need to write your own equals methods, you should always override canEqual if you change equals and hashCode.

  1.  

기존 Java 코드
import java.util.Arrays;
public class EqualsAndHashCodeExample {
    private transient int transientVar = 10;
    private String name;
    private double score;
    private Shape shape = new Square(5, 10);
    private String[] tags;
    private int id;
    public String getName() {
        return this.name;
    }
    @Override public boolean equals(Object o) {
        if (o == this) return true;
        if (!(o instanceof EqualsAndHashCodeExample)) return false;
        EqualsAndHashCodeExample other = (EqualsAndHashCodeExample) o;
        if (!other.canEqual((Object)this)) return false;
        if (this.getName() == null ? other.getName() != null : !this.getName().equals(other.getName())) return false;
        if (Double.compare(this.score, other.score) != 0) return false;
        if (!Arrays.deepEquals(this.tags, other.tags)) return false;
        return true;
    }
    @Override public int hashCode() {
        final int PRIME = 59;
        int result = 1;
        final long temp1 = Double.doubleToLongBits(this.score);
        result = (result*PRIME) + (this.name == null ? 0 : this.name.hashCode());
        result = (result*PRIME) + (int)(temp1 ^ (temp1 >>> 32));
        result = (result*PRIME) + Arrays.deepHashCode(this.tags);
        return result;
    }
    public boolean canEqual(Object other) {
        return other instanceof EqualsAndHashCodeExample;
    }
    public static class Square extends Shape {
        private final int width, height;
        public Square(int width, int height) {
            this.width = width;
            this.height = height;
        }
        @Override public boolean equals(Object o) {
            if (o == this) return true;
            if (!(o instanceof Square)) return false;
            Square other = (Square) o;
            if (!other.canEqual((Object)this)) return false;
            if (!super.equals(o)) return false;
            if (this.width != other.width) return false;
            if (this.height != other.height) return false;
            return true;
        }
        @Override public int hashCode() {
            final int PRIME = 59;
            int result = 1;
            result = (result*PRIME) + super.hashCode();
            result = (result*PRIME) + this.width;
            result = (result*PRIME) + this.height;
            return result;
        }
        public boolean canEqual(Object other) {
            return other instanceof Square;
        }
    }
}
Lombok 적용
import lombok.EqualsAndHashCode;
@EqualsAndHashCode(exclude= {"id", "shape"})
public class EqualsAndHashCodeExample {
    private transient int transientVar = 10;
    private String name;
    private double score;
    private Shape shape = new Square(5, 10);
    private String[] tags;
    private int id;

    public String getName() {
        return this.name;
    }

    @EqualsAndHashCode(callSuper=true)
    public static class Square extends Shape {
        private final int width, height;
        public Square(int width, int height) {
            this.width = width;
            this.height = height;
        }
    }
}

@Data

@Data is a convenient shortcut annotation that bundles the features of @ToString@EqualsAndHashCode@Getter / @Setter and @RequiredArgsConstructor together: In other words, @Data generatesall the boilerplate that is normally associated with simple POJOs (Plain Old Java Objects) and beans: getters for all fields, setters for all non-final fields, and appropriate toStringequals and hashCodeimplementations that involve the fields of the class, and a constructor that initializes all final fields, as well as all non-final fields with no initializer that have been marked with @NonNull, in order to ensure the field is never null.

@Data is like having implicit @Getter@Setter@ToString@EqualsAndHashCode and @RequiredArgsConstructor annotations on the class. However, the parameters of these annotations (such ascallSuperincludeFieldNames and exclude) cannot be set with @Data. If you need to set non-default values for any of these parameters, just add those annotations explicitly; @Data is smart enough to defer to those annotations.

All generated getters and setters will be public. To override the access level, annotate the field or class with an explicit @Setter and/or @Getter annotation. You can also use this annotation (by combining it with AccessLevel.NONE) to suppress generating a getter and/or setter altogether.

All fields marked as transient will not be considered for hashCode and equals. All static fields will be skipped entirely (not considered for any of the generated methods, and no setter/getter will be made for them).

If the class already contains a method with the same name as any method that would normally be generated, that method is not generated, and no warning or error is emitted. For example, if you already have a method with signature void hashCode(int a, int b, int c), no int hashCode() method will be generated, even though technically int hashCode() is an entirely different method. The same rule applies to the constructor, toStringequals, and all getters and setters.

@Data can handle generics parameters for fields just fine. In order to reduce the boilerplate when constructing objects for classes with generics, you can use the staticConstructor parameter to generate a private constructor, as well as a static method that returns a new instance. This way, javac will infer the variable name. Thus, by declaring like so:@Data(staticConstructor="of") class Foo<T> { private T x;} you can create new instances of Foo by writing: Foo.of(5); instead of having to write: new Foo<Integer>(5);.

기존 코드
import java.util.Arrays;
public class DataExample {
    private final String name;
    private int age;
    private double score;
    private String[] tags;
    public DataExample(String name) {
        this.name = name;
    }
    public String getName() {
        return this.name;
    }
    void setAge(int age) {
        this.age = age;
    }
    public int getAge() {
        return this.age;
    }
    public void setScore(double score) {
        this.score = score;
    }
    public double getScore() {
        return this.score;
    }
    public String[] getTags() {
        return this.tags;
    }
    public void setTags(String[] tags) {
        this.tags = tags;
    }
    @Override public String toString() {
        return "DataExample(" + this.getName() + ", " + this.getAge() + ", " + this.getScore() + ", " + Arrays.deepToString(this.getTags()) + ")";
    }
    @Override public boolean equals(Object o) {
        if (o == this) return true;
        if (!(o instanceof DataExample)) return false;
        DataExample other = (DataExample) o;
        if (!other.canEqual((Object)this)) return false;
        if (this.getName() == null ? other.getName() != null : !this.getName().equals(other.getName())) return false;
        if (this.getAge() != other.getAge()) return false;
        if (Double.compare(this.getScore(), other.getScore()) != 0) return false;
        if (!Arrays.deepEquals(this.getTags(), other.getTags())) return false;
        return true;
    }
    @Override public int hashCode() {
        final int PRIME = 59;
        int result = 1;
        final long temp1 = Double.doubleToLongBits(this.getScore());
        result = (result*PRIME) + (this.getName() == null ? 0 : this.getName().hashCode());
        result = (result*PRIME) + this.getAge();
        result = (result*PRIME) + (int)(temp1 ^ (temp1 >>> 32));
        result = (result*PRIME) + Arrays.deepHashCode(this.getTags());
        return result;
    }
    public static class Exercise<T> {
        private final String name;
        private final T value;
        private Exercise(String name, T value) {
            this.name = name;
            this.value = value;
        }
        public static <T> Exercise<T> of(String name, T value) {
            return new Exercise<T>(name, value);
        }
        public String getName() {
            return this.name;
        }
        public T getValue() {
            return this.value;
        }
        @Override public String toString() {
            return "Exercise(name=" + this.getName() + ", value=" + this.getValue() + ")";
        }
        @Override public boolean equals(Object o) {
            if (o == this) return true;
            if (!(o instanceof Exercise)) return false;
            Exercise<?> other = (Exercise<?>) o;
            if (!other.canEqual((Object)this)) return false;
            if (this.getName() == null ? other.getValue() != null : !this.getName().equals(other.getName())) return false;
            if (this.getValue() == null ? other.getValue() != null : !this.getValue().equals(other.getValue())) return false;
            return true;
        }
        @Override public int hashCode() {
            final int PRIME = 59;
            int result = 1;
            result = (result*PRIME) + (this.getName() == null ? 0 : this.getName().hashCode());
            result = (result*PRIME) + (this.getValue() == null ? 0 : this.getValue().hashCode());
            return result;
        }
    }
}
Lombok 적용
import lombok.AccessLevel;
import lombok.Setter;
import lombok.Data;
import lombok.ToString;

@Data public class DataExample {
    private final String name;
    @Setter(AccessLevel.PACKAGE) private int age;
    private double score;
    private String[] tags;

    @ToString(includeFieldNames=true)
    @Data(staticConstructor="of")
    public static class Exercise<T> {
        private final String name;
        private final T value;
    }
}

@Value

@Value is the immutable variant of @Data; all fields are made private and final by default, and setters are not generated. The class itself is also made final by default, because immutability is not something that can be forced onto a subclass. Like @Data, useful toString()equals() and hashCode() methods are also generated, each field gets a getter method, and a constructor that covers every argument (except final fields that are initialized in the field declaration) is also generated.

In practice, @Value is shorthand for: final @ToString @EqualsAndHashCode @AllArgsConstructor @FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE) @Getter.

It is possible to override the final-by-default and private-by-default behaviour using either an explicit access level on a field, or by using the @NonFinal or @PackagePrivate annotations.
It is possible to override any default behaviour for any of the 'parts' that make up @Value by explicitly using that annotation.

  1.  

기존 Java 코드
import java.util.Arrays;
 public final class ValueExample {
   private final String name;
   private int age;
   private final double score;
   protected final String[] tags;

   @java.beans.ConstructorProperties({"name", "age", "score", "tags"})
   public ValueExample(String name, int age, double score, String[] tags) {
     this.name = name;
     this.age = age;
     this.score = score;
     this.tags = tags;
   }

   public String getName() {
     return this.name;
   }

   public int getAge() {
     return this.age;
   }

   public double getScore() {
     return this.score;
   }

   public String[] getTags() {
     return this.tags;
   }

   @java.lang.Override
   public boolean equals(Object o) {
     if (o == this) return true;
     if (!(o instanceof ValueExample)) return false;
     final ValueExample other = (ValueExample)o;
     final Object this$name = this.getName();
     final Object other$name = other.getName();
     if (this$name == null ? other$name != null : !this$name.equals(other$name)) return false;
     if (this.getAge() != other.getAge()) return false;
     if (Double.compare(this.getScore(), other.getScore()) != 0) return false;
     if (!Arrays.deepEquals(this.getTags(), other.getTags())) return false;
       return true;
     }
   @java.lang.Override
   public int hashCode() {
     final int PRIME = 59;
     int result = 1;
     final Object $name = this.getName();
     result = result * PRIME + ($name == null ? 0 : $name.hashCode());
     result = result * PRIME + this.getAge();
     final long $score = Double.doubleToLongBits(this.getScore());
     result = result * PRIME + (int)($score >>> 32 ^ $score);
     result = result * PRIME + Arrays.deepHashCode(this.getTags());
     return result;
   }
   @java.lang.Override
   public String toString() {
     return "ValueExample(name=" + getName() + ", age=" + getAge() + ", score=" + getScore() + ", tags=" + Arrays.dee
pToString(getTags()) + ")";
   }
   ValueExample withAge(int age) {
     return this.age == age ? this : new ValueExample(name, age, score, tags);
   }
   public static final class Exercise<T> {
     private final String name;
     private final T value;
     private Exercise(String name, T value) {
       this.name = name;
       this.value = value;
     }
     public static <T> Exercise<T> of(String name, T value) {
       return new Exercise<T>(name, value);
     }
 public String getName() {
       return this.name;
     }
     public T getValue() {
       return this.value;
     }
     @java.lang.Override
     public boolean equals(Object o) {
       if (o == this) return true;
       if (!(o instanceof ValueExample.Exercise)) return false;
       final Exercise<?> other = (Exercise<?>)o;
       final Object this$name = this.getName();
       final Object other$name = other.getName();
       if (this$name == null ? other$name != null : !this$name.equals(other$name)) return false;
       final Object this$value = this.getValue();
       final Object other$value = other.getValue();
       if (this$value == null ? other$value != null : !this$value.equals(other$value)) return false;
       return true;
     }
     @java.lang.Override
     public int hashCode() {
       final int PRIME = 59;
       int result = 1;
       final Object $name = this.getName();
       result = result * PRIME + ($name == null ? 0 : $name.hashCode());
       final Object $value = this.getValue();
       result = result * PRIME + ($value == null ? 0 : $value.hashCode());
       return result;
     }
     @java.lang.Override
     public String toString() {
       return "ValueExample.Exercise(name=" + getName() + ", value=" + getValue() + ")";
     }
   }
 }
Lombok 적용
import lombok.AccessLevel;
import lombok.experimental.NonFinal;
import lombok.experimental.Value;
import lombok.experimental.Wither;
import lombok.ToString;

@Value public class ValueExample {
    String name;
    @Wither(AccessLevel.PACKAGE) @NonFinal int age;
    double score;
    protected String[] tags;

    @ToString(includeFieldNames=true)
    @Value(staticConstructor="of")
    public static class Exercise<T> {
        String name;
        T value;
    }
}

@Accessors

기본 생성된 setter 들은 return type 이 void 이므로 method chaining 을 사용할 수 없다.

@Accessors 애노테이션에 chain=true 파라미터를 사용하면 생성되는 setter 의 리턴 타입이 this 로 변경된다.

@Data
@Accessors(chain=true)
public class IssueFields {
	private Project  project;
	private String summary;
}
 
// 메소드 체이닝
IssueFields  f = new IssueFields ();
f.setProject(new Project()
 .setSummary(new Summary());

 

 

문제점

  1. Getter, Setter 들에 대해 JavaDoc 이 생성되지 않는다.
    1. javadoc 이 꼭 필요하면 delombok 으로 처리.
    2. delombok maven plugin

 

 

참고 자료