четверг, 15 октября 2009 г.

Другие острова

Это статья является этаким вольным переводом-пересказом.



Оказывается, Ява - это не единственный остров, который как-то связан с разработками на Java? Вот вам ещё один, который называется "Ломбок". Именем этого острова назван замечательный проект, распространяемый по лицензии MIT.

Что такое Project Lombok?

Project Lombok это две вещи в одном: генератор кода на этапе компиляции и генератор кода на этапе разработки. Что и делает находку бриллиантовой. В сущности, Lombok интегрируется непосредственно в цикл компиляции Eclipse (манипулируя абстрактным деревом синтаксиса кода в процессе его набора) и моментально генерирует код на основе аннотаций. Сгенерированный код тут же становится доступен всем другим классам.

Какой код может генерировать Lombok из аннотаций? Самое важное, порождаются основные шаблонные вещи, которые обычно заставляют классы Java выглядеть дико многословно и монструозно, а именно:

  • получатели (getters) и устанавливалки (setters) для полей
  • Строковое представление класса POJO в toString()
  • hashCode() и equals()
В то же время, Lombok обеспечивает автоматическое управление ресурсами. Например, ваши потоки будут всегда закрываться безопасно, без необходимости использовать блоки try/catch/finally.


Установка Lombok и поддержка Maven

Lombok предоставляется в виде исполняемого файла JAR (lombok.jar). Правда, в настоящее время режим среды разработки работает только в Eclipse. При запуске файла JAR, появится простой мастер и попросит указать, где находятся исполняемые файлы Eclipse (см. Рисунок 1).






Рисунок 1. Мастер установки Lombok.



Просто укажите на исполняемые файлы Eclipse и нажмите "Install/Update". Вы должны будете делать это каждый раз для всех новых версий Lombok.

Чтобы включить поддержку Maven, просто добавьте репозиторий Lombok и его зависимости в файл pom.xml, следуя инструкциям на веб-сайте проекта. После чего, Lombok будет работать с жизненным циклом компиляции Maven "прямо из коробки".



Project Lombok в действии

Для полного понимания, каким образом Lombok истребляет массу строк кода в типичном классе POJO, рассмотрим следующую типичную сущность Person:


public class Person  {
   private Long personId;
   
   private String salutation;
   private String firstName;
   private String middleName;
   private String lastName;
   
   private String phoneNumber;
   private String email;
   private String addressLine1;
   private String addressLine2;
   private String city;
   private String state;
   private String country;
   
   private Calendar birthDate;
}


Для того чтобы этот класс стал настоящим правильным POJO, необходимо дописать для него код getters, setters, toString(), equals() и hashCode(). Если бы вы решили использовать в Eclipse функции автоматической генерации кода, то Person POJO со скоростью грибов разрастётся до более чем 240 строк кода, большинство из которых, хоть и необходимый, но фактически просто мусор, затрудняющий чтение и понимание. (см. Листинг 1).


public class Person  {

   private Long personId;
   
   private String salutation;
   private String firstName;
   private String middleName;
   private String lastName;
   
   private String phoneNumber;
   private String email;
   private String addressLine1;
   private String addressLine2;
   private String city;
   private String state;
   private String country;
   
   private Calendar birthDate;
   
   public Long getPersonId() {
      return personId;
   }

   public void setPersonId(Long personId) {
      this.personId = personId;
   }

   public String getSalutation() {
      return salutation;
   }

   public void setSalutation(String salutation) {
      this.salutation = salutation;
   }

   public String getFirstName() {
      return firstName;
   }

   public void setFirstName(String firstName) {
      this.firstName = firstName;
   }

   public String getMiddleName() {
      return middleName;
   }

   public void setMiddleName(String middleName) {
      this.middleName = middleName;
   }

   public String getLastName() {
      return lastName;
   }

   public void setLastName(String lastName) {
      this.lastName = lastName;
   }

   public String getPhoneNumber() {
      return phoneNumber;
   }

   public void setPhoneNumber(String phoneNumber) {
      this.phoneNumber = phoneNumber;
   }

   public String getEmail() {
      return email;
   }

   public void setEmail(String email) {
      this.email = email;
   }

   public String getAddressLine1() {
      return addressLine1;
   }

   public void setAddressLine1(String addressLine1) {
      this.addressLine1 = addressLine1;
   }

   public String getAddressLine2() {
      return addressLine2;
   }

   public void setAddressLine2(String addressLine2) {
      this.addressLine2 = addressLine2;
   }

   public String getCity() {
      return city;
   }

   public void setCity(String city) {
      this.city = city;
   }

   public String getState() {
      return state;
   }

   public void setState(String state) {
      this.state = state;
   }

   public String getCountry() {
      return country;
   }

   public void setCountry(String country) {
      this.country = country;
   }

   public Calendar getBirthDate() {
      return birthDate;
   }

   public void setBirthDate(Calendar birthDate) {
      this.birthDate = birthDate;
   }

   @Override
   public String toString() {
      return "Person [addressLine1=" + addressLine1 + ", addressLine2="
            + addressLine2 + ", birthDate=" + birthDate + ", city=" + city
            + ", country=" + country + ", email=" + email + ", firstName="
            + firstName + ", lastName=" + lastName + ", middleName="
            + middleName + ", personId=" + personId + ", phoneNumber="
            + phoneNumber + ", salutation=" + salutation + ", state="
            + state + "]";
   }

   @Override
   public int hashCode() {
      final int prime = 31;
      int result = 1;
      result = prime * result
            + ((addressLine1 == null) ? 0 : addressLine1.hashCode());
      result = prime * result
            + ((addressLine2 == null) ? 0 : addressLine2.hashCode());
      result = prime * result
            + ((birthDate == null) ? 0 : birthDate.hashCode());
      result = prime * result + ((city == null) ? 0 : city.hashCode());
      result = prime * result + ((country == null) ? 0 : country.hashCode());
      result = prime * result + ((email == null) ? 0 : email.hashCode());
      result = prime * result
            + ((firstName == null) ? 0 : firstName.hashCode());
      result = prime * result
            + ((lastName == null) ? 0 : lastName.hashCode());
      result = prime * result
            + ((middleName == null) ? 0 : middleName.hashCode());
      result = prime * result
            + ((personId == null) ? 0 : personId.hashCode());
      result = prime * result
            + ((phoneNumber == null) ? 0 : phoneNumber.hashCode());
      result = prime * result
            + ((salutation == null) ? 0 : salutation.hashCode());
      result = prime * result + ((state == null) ? 0 : state.hashCode());
      return result;
   }

   @Override
   public boolean equals(Object obj) {
      if (this == obj)
         return true;
      if (obj == null)
         return false;
      if (getClass() != obj.getClass())
         return false;
      Person other = (Person) obj;
      if (addressLine1 == null) {
         if (other.addressLine1 != null)
            return false;
      } else if (!addressLine1.equals(other.addressLine1))
         return false;
      if (addressLine2 == null) {
         if (other.addressLine2 != null)
            return false;
      } else if (!addressLine2.equals(other.addressLine2))
         return false;
      if (birthDate == null) {
         if (other.birthDate != null)
            return false;
      } else if (!birthDate.equals(other.birthDate))
         return false;
      if (city == null) {
         if (other.city != null)
            return false;
      } else if (!city.equals(other.city))
         return false;
      if (country == null) {
         if (other.country != null)
            return false;
      } else if (!country.equals(other.country))
         return false;
      if (email == null) {
         if (other.email != null)
            return false;
      } else if (!email.equals(other.email))
         return false;
      if (firstName == null) {
         if (other.firstName != null)
            return false;
      } else if (!firstName.equals(other.firstName))
         return false;
      if (lastName == null) {
         if (other.lastName != null)
            return false;
      } else if (!lastName.equals(other.lastName))
         return false;
      if (middleName == null) {
         if (other.middleName != null)
            return false;
      } else if (!middleName.equals(other.middleName))
         return false;
      if (personId == null) {
         if (other.personId != null)
            return false;
      } else if (!personId.equals(other.personId))
         return false;
      if (phoneNumber == null) {
         if (other.phoneNumber != null)
            return false;
      } else if (!phoneNumber.equals(other.phoneNumber))
         return false;
      if (salutation == null) {
         if (other.salutation != null)
            return false;
      } else if (!salutation.equals(other.salutation))
         return false;
      if (state == null) {
         if (other.state != null)
            return false;
      } else if (!state.equals(other.state))
         return false;
      return true;
   }
}


А теперь, как выглядит тот же POJO после "Ломбокизации":


@Data
public class Person  {

   private Long personId;
   
   private String salutation;
   private String firstName;
   private String middleName;
   private String lastName;
   
   private String phoneNumber;
   private String email;
   private String addressLine1;
   private String addressLine2;
   private String city;
   private String state;
   private String country;
   
   private Calendar birthDate;
   
}


И всё! Когда Lombok установлен в вашей Eclipse Runtime, то простая аннотация @Data рисует волшебным образом весь тот код необходимого стандартного мусора. Что делает определение сущностей JPA/Hibernate невероятно легким и быстрым.

Но и это ещё не всё! Все методы getters и setters появятся в схеме, как если бы код существовал на самом деле (см. Рисунок 2).




Рисунок 2. Все методы getters и setters видны в схеме

А также их видно и при дополнении кода (см. Рисунок 3).





Рисунок 3. Все методы видны при дополнении кода


Если необходим более филигранный контроль, то Project Lombok предлагает следующие аннотации @Getter, @Setter, @ToString и @EqualsAndHashCode. А вышесказанное означает, что аннотация @Data комбинируя, заключает их все в себе. В нашем процессе разработки, аннотация @Data используется в 99% случаях.

Дополнительные особенности Lombok

Помимо основных характеристик, которые вы только что видели, Lombok поставляется с несколькими дополнениями:

  • Аннотация @Cleanup обеспечивает автоматическое управление ресурсами. Например, в следующем сниппете, @Cleanup гарантирует, что метод Close () потока будет выполнен до выхода из метода:


    @Cleanup InputStream in = new FileInputStream(args[0]);
    @Cleanup OutputStream out = new FileOutputStream(args[1]);
    

    И нет необходимости ждать Java 7, чтобы использовать автоматическое управление ресурсами уже сегодня в Java 6.


  • Аннотация @Synchronized это альтернатива ключевому слову synchronized, но лучше реализованная:


    @Synchronized
    public static void hello() {
       System.out.println("world");
    }


  • Аннотация @SneakyThrows отключает в Java проверяемые исключения:


    @SneakyThrows
    public void run() {
       throw new Throwable();
    }
    
    Решите вы или нет использовать эту функцию (у меня смешанные чувства по поводу нее), но такую возможность иметь полезно.


Планы на будущее: Добавление новых особенностей Java

На момент написания, актуальной версией Проекта Ломбок была 0.8.5. Краткосрочной перспективой команды Ломбок была стабилизация и безотказность работы в Eclipse. Хотя, имеются некоторые незначительные проблемы ранних версий (например, некоторые неожиданные предупреждения при компиляции), но простая перекомпиляция проекта позволяет быстро от них избавиться. В целом, выгода от использования Ломбок намного перевешивает имеющиеся небольшие сбои Eclipse (и которые довольно редки, по моему опыту). Долгосрочные же планы у Ломбок гораздо более грандиозные. Оба автора проекта Ломбок (Reinier Zwitserloot и Roel Spilker) хотят перехватить процесс компиляции в Eclipse до точки, где они действительно могут добавить новые функции в Java, в частности, реальное завершение. Узнайте больше об этой амбициозной цели в  этом обсуждении Google Groups.



Недостатки

Основной недостаток Ломбок очевиден: работа в режиме среды разработки поддерживается только в Eclipse. Если Eclipse не является вашей IDE, то на данный момент проект Ломбок это не ваш вариант. Возможно такая поддержка в будущем может появиться в средах NetBeans и IntelliJ, но она предусматривает несколько довольно тяжелых IDE-хаков для конкретной среды. Зато, все мастерские с Eclipse должны рассмотреть добавление Ломбок в своей ежедневный инструментарий прямо сегодня.



Прощай многословность POJO

С внедрением Проекта Ломбок, Reiner и Roel сделали многословие Java POJO достоянием истории. Когда вы добавите Ломбок к вашей ежедневной практике разработки, то просто не захотите возвращаться назад. Это очень просто и на наш взгляд, это самое революционное дополнение к экосистеме Java после рождения самого Eclipse.



Ссылки



Комментариев нет: