Project Lombok 소개

1. 반복적 인 코드 피하기

Java는 훌륭한 언어이지만 일반적인 작업을 위해 코드에서 수행해야하는 작업이나 일부 프레임 워크 관행을 준수하기에는 너무 장황합니다. 이는 프로그램의 비즈니스 측면에 실질적인 가치를 가져다주지 않는 경우가 많습니다. 여기에서 Lombok이 여러분의 삶을 더 행복하게 만들고 생산성을 높이기 위해 여기에 있습니다.

작동 방식은 빌드 프로세스에 연결하고 코드에 도입 한 여러 프로젝트 주석에 따라 Java 바이트 코드를 .class 파일에 자동 생성 하는 것입니다.

어떤 시스템을 사용하든 빌드에 포함시키는 것은 매우 간단합니다. 그들의 프로젝트 페이지에는 세부 사항에 대한 자세한 지침이 있습니다. 내 프로젝트의 대부분은 maven 기반이므로 일반적으로 제공된 범위 에서 종속성을 삭제하고 갈 수 있습니다.

 ...  org.projectlombok lombok 1.18.10 provided  ... 

여기에서 사용 가능한 최신 버전을 확인하십시오.

Lombok에 따라 .jar 사용자가 런타임이 아닌 순수한 빌드 종속성이기 때문에 .jar 사용자 도 이에 의존하지 않습니다.

2. 게터 / 세터, 생성자 – 너무 반복적

공용 getter 및 setter 메소드를 통해 객체 속성을 캡슐화하는 것은 Java 세계에서 매우 일반적인 관행이며, 많은 프레임 워크는이 "Java Bean"패턴에 광범위하게 의존합니다. 빈 생성자가있는 클래스와 "속성"에 대한 get / set 메소드입니다.

이것은 매우 일반적이어서 대부분의 IDE는 이러한 패턴 (및 그 이상)에 대한 코드 자동 생성을 지원합니다. 그러나이 코드는 소스에 있어야하며 새 속성이 추가되거나 필드 이름이 변경 될 때도 유지되어야합니다.

JPA 엔티티로 사용하려는이 클래스를 예로 들어 보겠습니다.

@Entity public class User implements Serializable { private @Id Long id; // will be set when persisting private String firstName; private String lastName; private int age; public User() { } public User(String firstName, String lastName, int age) { this.firstName = firstName; this.lastName = lastName; this.age = age; } // getters and setters: ~30 extra lines of code }

이것은 다소 간단한 클래스이지만 getter 및 setter에 대한 추가 코드를 추가하면 관련 비즈니스 정보보다 더 많은 상용구 0 값 코드가있는 정의로 끝날 것입니다. 성, 나이.”

이제이 클래스를 Lombok-ize 해 보겠습니다 .

@Entity @Getter @Setter @NoArgsConstructor // <--- THIS is it public class User implements Serializable { private @Id Long id; // will be set when persisting private String firstName; private String lastName; private int age; public User(String firstName, String lastName, int age) { this.firstName = firstName; this.lastName = lastName; this.age = age; } }

@Getter@Setter 주석 을 추가하여 Lombok에게 클래스의 모든 필드에 대해이를 생성하도록 지시했습니다. @NoArgsConstructor 는 빈 생성자를 생성합니다.

이것은 전체 클래스 코드입니다. // getters 및 setters 주석 이있는 위 버전과는 반대로 아무것도 생략하지 않았습니다 . 세 가지 관련 속성 클래스의 경우 이는 코드를 크게 절약합니다!

User 클래스 에 속성 (속성)을 추가 하면 동일한 일이 발생합니다. 유형 자체에 주석을 적용하여 기본적으로 모든 필드를 고려합니다.

일부 속성의 가시성을 개선하려면 어떻게해야합니까? 예를 들어, 필자는 엔티티의 id 필드 수정 자 패키지 또는 보호 된 항목 을 읽을 것으로 예상되지만 애플리케이션 코드에 의해 명시 적으로 설정되지 않기 때문에 표시 되도록 유지하고 싶습니다 . 이 특정 필드에 대해 더 미세한 @Setter 를 사용하십시오.

private @Id @Setter(AccessLevel.PROTECTED) Long id;

3. 게으른 게터

종종 애플리케이션은 비용이 많이 드는 작업을 수행하고 후속 사용을 위해 결과를 저장해야합니다.

예를 들어 파일이나 데이터베이스에서 정적 데이터를 읽어야한다고 가정 해 보겠습니다. 일반적으로이 데이터를 한 번 검색 한 다음 캐시하여 애플리케이션 내에서 메모리 내 읽기를 허용하는 것이 좋습니다. 이렇게하면 응용 프로그램이 값 비싼 작업을 반복하지 않아도됩니다.

또 다른 일반적인 패턴은 이 데이터가 처음 필요할 때만 검색하는 것 입니다. 즉, 해당 getter가 처음 호출 될 때만 데이터를 가져옵니다. 이를 지연 로딩 이라고 합니다.

이 데이터가 클래스 내부의 필드로 캐시된다고 가정합니다. 이제 클래스는이 필드에 대한 모든 액세스가 캐시 된 데이터를 반환하는지 확인해야합니다. 이러한 클래스를 구현하는 한 가지 가능한 방법은 필드가 null 인 경우에만 getter 메서드가 데이터를 검색하도록하는 것 입니다. 이런 이유로 우리는 이것을 lazy getter 라고 부릅니다 .

Lombok은 위에서 본 @ Getter 어노테이션lazy 매개 변수를 사용하여이를 가능하게합니다 .

예를 들어 다음과 같은 간단한 클래스를 고려하십시오.

public class GetterLazy { @Getter(lazy = true) private final Map transactions = getTransactions(); private Map getTransactions() { final Map cache = new HashMap(); List txnRows = readTxnListFromFile(); txnRows.forEach(s -> { String[] txnIdValueTuple = s.split(DELIMETER); cache.put(txnIdValueTuple[0], Long.parseLong(txnIdValueTuple[1])); }); return cache; } }

이것은 파일에서 일부 트랜잭션을 Map 으로 읽습니다 . 파일의 데이터는 변경되지 않으므로 한 번 캐시하고 getter를 통한 액세스를 허용합니다.

이제이 클래스의 컴파일 된 코드를 살펴보면 캐시가 null 인 경우 캐시를 업데이트 한 다음 캐시 된 데이터를 반환 하는 getter 메서드를 볼 수 있습니다 .

public class GetterLazy { private final AtomicReference transactions = new AtomicReference(); public GetterLazy() { } //other methods public Map getTransactions() { Object value = this.transactions.get(); if (value == null) { synchronized(this.transactions) { value = this.transactions.get(); if (value == null) { Map actualValue = this.readTxnsFromFile(); value = actualValue == null ? this.transactions : actualValue; this.transactions.set(value); } } } return (Map)((Map)(value == this.transactions ? null : value)); } }

Lombok이 AtomicReference 에서 데이터 필드를 래핑 했다는 점은 흥미 롭습니다 . 이렇게하면 트랜잭션 필드 에 대한 원자 적 업데이트가 보장됩니다 . getTransactions () 메서드는 경우 파일을 읽을 확인합니다 거래 입니다 널 (null)이.

클래스 내에서 직접 AtomicReference 트랜잭션 필드를 사용하는 것은 권장되지 않습니다. 필드에 액세스하려면 getTransactions () 메서드 를 사용하는 것이 좋습니다 .

우리가 같은 다른 롬복 주석을 사용하는 경우 이러한 이유로, ToString을 같은 클래스에서 , 그것은 사용 ) getTransactions를 ( 직접 필드에 액세스하는 대신.

4. 가치 등급 / DTO

복잡한 "값"또는 "데이터 전송 객체"를 나타내는 유일한 목적으로 데이터 유형을 정의하려는 상황이 많이 있습니다. 대부분의 경우 한 번만 빌드하고 절대 변경하지 않으려는 불변 데이터 구조의 형태로 .

성공적인 로그인 작업을 나타내는 클래스를 디자인합니다. 모든 필드가 null이 아니고 객체가 변경 불가능하므로 해당 속성에 스레드로부터 안전하게 액세스 할 수 있습니다.

public class LoginResult { private final Instant loginTs; private final String authToken; private final Duration tokenValidity; private final URL tokenRefreshUrl; // constructor taking every field and checking nulls // read-only accessor, not necessarily as get*() form }

다시 말하지만, 주석 처리 된 섹션에 대해 작성해야하는 코드의 양은 캡슐화하려는 정보보다 훨씬 더 많은 양이 될 것이며 이는 우리에게 실질적인 가치가 있습니다. 이를 개선하기 위해 Lombok을 다시 사용할 수 있습니다.

@RequiredArgsConstructor @Accessors(fluent = true) @Getter public class LoginResult { private final @NonNull Instant loginTs; private final @NonNull String authToken; private final @NonNull Duration tokenValidity; private final @NonNull URL tokenRefreshUrl; }

@RequiredArgsConstructor 주석을 추가하기 만하면 선언 한대로 클래스의 모든 최종 필드에 대한 생성자를 얻을 수 있습니다. @NonNull 을 속성에 추가하면 생성자가 null 허용 여부를 확인하고 그에 따라 NullPointerException을 발생 시킵니다. 필드가 최종이 아니고 @Setter 를 추가 한 경우 에도 마찬가지 입니다.

속성에 대해 지루한 이전 get * () 양식을 원하지 않습니까? 이 예제에서 @Accessors (fluent = true) 를 추가했기 때문에 "getters"는 속성과 동일한 메서드 이름을 갖게됩니다. getAuthToken ()은 단순히 authToken ()이 됩니다.

이 "유창한"양식은 속성 설정자에 대한 최종 필드가 아닌 필드에 적용되며 연결 호출도 허용합니다.

// Imagine fields were no longer final now return new LoginResult() .loginTs(Instant.now()) .authToken("asdasd") . // and so on

5. 핵심 자바 보일러 플레이트

유지해야 할 코드를 작성하는 또 다른 상황은 toString () , equals ()hashCode () 메서드를 생성 할 때 입니다. IDE는 클래스 속성 측면에서 이러한 템플릿을 자동 생성하는 데 도움이됩니다.

다른 Lombok 클래스 수준 주석을 사용하여이를 자동화 할 수 있습니다.

  • @ToString: will generate a toString() method including all class attributes. No need to write one ourselves and maintain it as we enrich our data model.
  • @EqualsAndHashCode: will generate both equals() and hashCode() methods by default considering all relevant fields, and according to very well though semantics.

These generators ship very handy configuration options. For example, if your annotated classes take part of a hierarchy you can just use the callSuper=true parameter and parent results will be considered when generating the method's code.

More on this: say we had our User JPA entity example include a reference to events associated to this user:

@OneToMany(mappedBy = "user") private List events;

We wouldn't like to have the whole list of events dumped whenever we call the toString() method of our User, just because we used the @ToString annotation. No problem: just parameterize it like this: @ToString(exclude = {“events”}), and that won't happen. This is also helpful to avoid circular references if, for example, UserEvents had a reference to a User.

For the LoginResult example, we may want to define equality and hash code calculation just in terms of the token itself and not the other final attributes in our class. Then, simply write something like @EqualsAndHashCode(of = {“authToken”}).

Bonus: if you liked the features from the annotations we've reviewed so far you may want to examine @Data and @Value annotations as they behave as if a set of them had been applied to our classes. After all, these discussed usages are very commonly put together in many cases.

5.1. (Not) Using the @EqualsAndHashCode With JPA Entities

Whether to use the default equals() and hashCode() methods or create custom ones for the JPA entities, is an often discussed topic among developers. There are multiple approaches we can follow; each having its pros and cons.

By default, @EqualsAndHashCode includes all non-final properties of the entity class. We can try to “fix” this by using the onlyExplicitlyIncluded attribute of the @EqualsAndHashCode to make Lombok use only the entity's primary key. Still, however, the generated equals() method can cause some issues. Thorben Janssen explains this scenario in greater detail in one of his blog posts.

In general, we should avoid using Lombok to generate the equals() and hashCode() methods for our JPA entities!

6. The Builder Pattern

The following could make for a sample configuration class for a REST API client:

public class ApiClientConfiguration { private String host; private int port; private boolean useHttps; private long connectTimeout; private long readTimeout; private String username; private String password; // Whatever other options you may thing. // Empty constructor? All combinations? // getters... and setters? }

We could have an initial approach based on using the class default empty constructor and providing setter methods for every field. However, we'd ideally want configurations not to be re-set once they've been built (instantiated), effectively making them immutable. We therefore want to avoid setters, but writing such a potentially long args constructor is an anti-pattern.

Instead, we can tell the tool to generate a builder pattern, preventing us to write an extra Builder class and associated fluent setter-like methods by simply adding the @Builder annotation to our ApiClientConfiguration.

@Builder public class ApiClientConfiguration { // ... everything else remains the same }

Leaving the class definition above as such (no declare constructors nor setters + @Builder) we can end up using it as:

ApiClientConfiguration config = ApiClientConfiguration.builder() .host("api.server.com") .port(443) .useHttps(true) .connectTimeout(15_000L) .readTimeout(5_000L) .username("myusername") .password("secret") .build();

7. Checked Exceptions Burden

Lots of Java APIs are designed so that they can throw a number of checked exceptions client code is forced to either catch or declare to throws. How many times have you turned these exceptions you know won't happen into something like this?

public String resourceAsString() { try (InputStream is = this.getClass().getResourceAsStream("sure_in_my_jar.txt")) { BufferedReader br = new BufferedReader(new InputStreamReader(is, "UTF-8")); return br.lines().collect(Collectors.joining("\n")); } catch (IOException | UnsupportedCharsetException ex) { // If this ever happens, then its a bug. throw new RuntimeException(ex); <--- encapsulate into a Runtime ex. } }

If you want to avoid this code patterns because the compiler won't be otherwise happy (and, after all, you know the checked errors cannot happen), use the aptly named @SneakyThrows:

@SneakyThrows public String resourceAsString() { try (InputStream is = this.getClass().getResourceAsStream("sure_in_my_jar.txt")) { BufferedReader br = new BufferedReader(new InputStreamReader(is, "UTF-8")); return br.lines().collect(Collectors.joining("\n")); } }

8. Ensure Your Resources Are Released

Java 7 introduced the try-with-resources block to ensure your resources held by instances of anything implementing java.lang.AutoCloseable are released when exiting.

Lombok provides an alternative way of achieving this, and more flexibly via @Cleanup. Use it for any local variable whose resources you want to make sure are released. No need for them to implement any particular interface, you'll just get its close() method called.

@Cleanup InputStream is = this.getClass().getResourceAsStream("res.txt");

Your releasing method has a different name? No problem, just customize the annotation:

@Cleanup("dispose") JFrame mainFrame = new JFrame("Main Window");

9. Annotate Your Class to Get a Logger

Many of us add logging statements to our code sparingly by creating an instance of a Logger from our framework of choice. Say, SLF4J:

public class ApiClientConfiguration { private static Logger LOG = LoggerFactory.getLogger(ApiClientConfiguration.class); // LOG.debug(), LOG.info(), ... }

This is such a common pattern that Lombok developers have cared to simplify it for us:

@Slf4j // or: @Log @CommonsLog @Log4j @Log4j2 @XSlf4j public class ApiClientConfiguration { // log.debug(), log.info(), ... }

Many logging frameworks are supported and of course you can customize the instance name, topic, etc.

10. Write Thread-Safer Methods

In Java you can use the synchronized keyword to implement critical sections. However, this is not a 100% safe approach: other client code can eventually also synchronize on your instance, potentially leading to unexpected deadlocks.

This is where @Synchronized comes in: annotate your methods (both instance and static) with it and you'll get an autogenerated private, unexposed field your implementation will use for locking:

@Synchronized public /* better than: synchronized */ void putValueInCache(String key, Object value) { // whatever here will be thread-safe code }

11. Automate Objects Composition

Java does not have language level constructs to smooth out a “favor composition inheritance” approach. Other languages have built-in concepts such as Traits or Mixins to achieve this.

Lombok's @Delegate comes in very handy when you want to use this programming pattern. Let's consider an example:

  • We want Users and Customers to share some common attributes for naming and phone number
  • We define both an interface and an adapter class for these fields
  • We'll have our models implement the interface and @Delegate to their adapter, effectively composing them with our contact information

First, let's define an interface:

public interface HasContactInformation { String getFirstName(); void setFirstName(String firstName); String getFullName(); String getLastName(); void setLastName(String lastName); String getPhoneNr(); void setPhoneNr(String phoneNr); }

And now an adapter as a support class:

@Data public class ContactInformationSupport implements HasContactInformation { private String firstName; private String lastName; private String phoneNr; @Override public String getFullName() { return getFirstName() + " " + getLastName(); } }

The interesting part comes now, see how easy it is to now compose contact information into both model classes:

public class User implements HasContactInformation { // Whichever other User-specific attributes @Delegate(types = {HasContactInformation.class}) private final ContactInformationSupport contactInformation = new ContactInformationSupport(); // User itself will implement all contact information by delegation }

The case for Customer would be so similar we'd omit the sample for brevity.

12. Rolling Lombok Back?

Short answer: Not at all really.

You may be worried there is a chance that you use Lombok in one of your projects, but later want to rollback that decision. You'd then have a maybe large number of classes annotated for it… what could you do?

I have never really regretted this, but who knows for you, your team or your organization. For these cases you're covered thanks to the delombok tool from the same project.

By delombok-ing your code you'd get autogenerated Java source code with exactly the same features from the bytecode Lombok built. So then you may simply replace your original annotated code with these new delomboked files and no longer depend on it.

This is something you can integrate in your build and I have done this in the past to just study the generated code or to integrate Lombok with some other Java source code based tool.

13. Conclusion

There are some other features we have not presented in this article, I'd encourage you to take a deeper dive into the feature overview for more details and use cases.

또한 우리가 보여준 대부분의 기능에는 이름 지정 등에 대한 팀 관행에 가장 적합한 도구를 생성하는 데 유용한 사용자 정의 옵션이 많이 있습니다. 사용 가능한 기본 제공 구성 시스템도 도움이 될 수 있습니다.

Lombok이 Java 개발 도구 세트에 들어갈 수있는 기회를 제공 할 동기를 찾았기를 바랍니다. 시도해보고 생산성을 높이십시오!

예제 코드는 GitHub 프로젝트에서 찾을 수 있습니다.