Статьи

Свойства Java без методов получения и установки

Во время последней конференции Devoxx Марк Рейнхольд, главный инженер Sun по Java SE, выступил с докладом о последних направлениях Java 7. (Краткое изложение презентации Марка Гамлетом Д’Арси доступно здесь .)

Одной из функций, которая обсуждалась для возможного включения в Java 7, но не будет включена в окончательный выпуск, являются свойства первого класса для Java. Первоклассная поддержка свойств вышла бы за рамки простых методов set и getter спецификации JavaBeans и обеспечила бы краткий и элегантный способ определения свойств для объектов Java.

Свойства уже являются первоклассными элементами многих современных языков, поэтому этот недостаток в Java 7 будет ощущаться многими разработчиками, привыкшими к поддержке свойств других языков. В то же время, разработчики Java могут прибегнуть к нескольким другим методам при работе с подобными свойствам атрибутами Java, и некоторые из возможных методов работают даже в Java 1.4. В оставшейся части этой статьи я продемонстрирую, как один такой метод использует простой аспект с AspectJ, а некоторые следуют некоторым соглашениям.

мотивация

Идея этого решения возникла при работе с платформой OpenXava 3. OpenXava определяет механизм разработки приложений Java Enterprise с использованием бизнес-компонентов. Это альтернативный способ MVC: вместо организации кода в Model, View и Controller код с OpenXava организован вокруг Invoice, Customer, Order и подобных бизнес-ориентированных объектов. OpenXava первоначально использовал XML для определения компонентов, но начиная с версии 3 использование POJO с аннотациями Java 5 также стало доступным в качестве предпочтительного способа определения бизнес-компонентов.

При использовании определения объекта на основе XML вы можете указать компонент следующим образом:

<component name="Teacher">

<entity>
<property name="id" type="String" key="true"
size="5" required="true"/>
<property name="name" type="String"
size="40" required="true"/>
<collection name="pupils">
<reference model="Pupil"/>
</collection>
</entity>

</component>

Используя аннотацию на основе OpenXava 3, тот же компонент будет определен следующим образом:

@Entity
public class Teacher {

@Id @Column(length=5) @Required
private String id;

@Column(length=40) @Required
private String name;

@OneToMany(mappedBy="teacher")
private Collection pupils;

public String getId() {
return id;
}

public void setId(String id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public Collection getPupils() {
return pupils;
}

public void setPupils(Collection pupils) {
this.pupils = pupils;
}

}

Это иллюстрирует некоторые подробности с определением Java: 37 строк против 13 версии XML. Проблема возникает из-за геттеров и сеттеров. Стандартный код добавляет шум и увеличивает размер файла без увеличения информации для программиста. Использование Java и аннотаций в качестве языка определения в OpenXava будет приветствовать более сжатый и элегантный синтаксис.

Более сжатое решение

Лучшее решение можно получить, определив бизнес-компонент следующим образом:

@Entity
public class Teacher {

@Id @Column(length=5) @Required
public String id;

@Column(length=40) @Required
public String name;

@OneToMany(mappedBy="teacher")
public Collection pupils;

}

Это делает Java более красивой, лаконичной и элегантной … как XML.

При этом вы должны использовать следующие свойства без геттеров и сеттеров. То есть вместо:

teacher.setName("M. Carmen");
String result = teacher.getName();

ты пишешь:

teacher.name = "M. Carmen";
String result = teacher.name;

В этом случае имя — это не поле, а свойство . Вы можете уточнить доступ к этому свойству и даже создать чисто вычисляемые свойства.

Например, если вы хотите вычисляемое свойство, вы можете определить его следующим образом:

@Entity
public class Person {

...

public String name;
public String surname;


transient public String fullName; // (1)
protected String getFullName() { // (2)
return name + " " + surname;
}
}

Чтобы определить вычисляемое свойство, вы определяете открытое поле (1), а затем защищенный (или частный, но не публичный) метод получения (2). Этот метод getFullName () в данном случае предназначен для реализации логики для свойства fullName.

Использовать это свойство просто:

Person p = new Person();
p.name = "M. Carmen";
p.surname = "Gimeno Alabau";
assertEquals("M. Carmen Gimeno Alabau", p.fullName);

Когда используется p.fullName, метод getFullName () выполняется для возврата значения. Вы можете видеть, что fullName — это свойство, а не простое поле.

Вы также можете уточнить доступ к написанию и чтению свойств. Например, вы можете определить свойство следующим образом:

public String address;
protected String getAddress() {
return address.toUpperCase();
}

Когда адрес используется извне класса, метод getAddress () используется для получения результата, но этот метод возвращает адрес только с некоторым уточнением. Поскольку мы используем адресное поле изнутри, getAddress () используется адрес поля изнутри его класса, а метод get не вызывается.

Теперь вы можете использовать это свойство следующим образом:

Person p = new Person();
p.address = "Av. Baron de Carcer";
assertEquals("AV. BARON DE CARCER", p.address);

Кроме того, вы можете определить сеттеры даже для наложения вето на данные, которые должны быть установлены, как в следующем примере:

public int age;
protected void setAge(int age) {
if (age > 200) {
throw new IllegalArgumentException("Too old");
}
this.age = age;
}

Как и в случае с геттером, если присутствует метод установки, он будет выполняться для установки данных, вы можете использовать эту логику для установки данных в поле или для любой другой логики, которую вы хотите. Теперь вы можете использовать это свойство следующим образом:

Person p = new Person();
p.age = 33;
assertEquals(p.age, 33);
p.age = 250; // Here an IllegalArgumentException is thrown

В итоге:

  • Свойства ведут себя снаружи класса как открытые поля.

  • Если нет получателя или установщика для поля, прямой доступ к полю выполняется.

  • Если для поля существует метод получения, он будет выполнен при попытке прочитать поле извне.

  • Если для поля существует установщик, он будет выполнен при попытке записи поля извне.

  • Внутри класса все ссылки на поле имеют прямой доступ без вызова getter или setter.

Это обеспечивает простой и естественный способ иметь свойства в Java без ненужных методов получения и установки.

Реализация

Этот простой механизм легко реализовать с помощью AspectJ и простого аспекта, и он будет работать даже в Java 1.4.

Чтобы это работало, вам нужно иметь только аспект в вашем проекте:

aspect PropertyAspect {


pointcut getter() :
get(public !final !static * *..model*.*.*) &&
!get(public !final !static * *..model*.*Key.*) &&
!within(PropertyAspect);
pointcut setter() :
set(public !final !static * *..model*.*.*) &&
!set(public !final !static * *..model*.*Key.*) &&
!within(PropertyAspect);

Object around(Object target, Object source) :
target(target) && this(source) && getter() {

if (target == source) {
return proceed(target, source);
}
try {
String methodName = "get" +
Strings.firstUpper(
thisJoinPointStaticPart.getSignature().getName());
Method method =
target.getClass().
getDeclaredMethod(methodName, null);
method.setAccessible(true);
return method.invoke(target, null);
}
catch (NoSuchMethodException ex) {
return proceed(target, source);
}
catch (InvocationTargetException ex) {
Throwable tex = ex.getTargetException();
if (tex instanceof RuntimeException) {
throw (RuntimeException) tex;
}
else throw new RuntimeException(
XavaResources.getString("getter_failed",
thisJoinPointStaticPart.
getSignature().getName(),
target.getClass()), ex);
}
catch (Exception ex) {
throw new RuntimeException(
XavaResources.getString("getter_failed",
thisJoinPointStaticPart.getSignature().getName(),
target.getClass()), ex);
}
}

void around(Object target, Object source, Object newValue) : target(target) && this(source) && args(newValue) && setter() {
if (target == source) {
proceed(target, source, newValue);
return;
}
try {
String methodName = "set" +
Strings.firstUpper(thisJoinPointStaticPart.getSignature().getName());
Class fieldType = ((FieldSignature)
thisJoinPointStaticPart.getSignature()).getFieldType(
Method method =
target.getClass().
getDeclaredMethod(methodName, new Class[] {fieldType});
method.setAccessible(true);
method.invoke(target, new Object[] { newValue } );
}
catch (NoSuchMethodException ex) {
proceed(target, source, newValue);
}
catch (InvocationTargetException ex) {
Throwable tex = ex.getTargetException();
if (tex instanceof RuntimeException) {
throw (RuntimeException) tex;
}
else throw new RuntimeException(
XavaResources.getString("setter_failed",
thisJoinPointStaticPart.getSignature().getName(),
target.getClass()), ex);
}
catch (Exception ex) {
throw new RuntimeException(
XavaResources.getString("setter_failed",
thisJoinPointStaticPart.getSignature().getName(),
target.getClass()), ex);
}
}
}

Определив этот аспект, вам нужно будет скомпилировать ваш проект, используя AspectJ.

Аспект перехватывает весь доступ к открытым полям в пакете модели, а затем использует самоанализ для вызова метода получения или установки, если они существуют.

Недостатки

К сожалению, у этого подхода есть как минимум два важных недостатка:

  1. Он не работает при доступе к свойствам с помощью самоанализа.

  2. В JPA, по крайней мере с использованием реализации Hibernate, ленивая инициализация не работает.

Вы можете подумать, что проблему интроспекции можно решить с помощью собственного кода интроспекции. Но сторонние библиотеки, фреймворки и инструментарий не будут подчиняться этим правилам. Например, если вы используете генератор отчетов, который может генерировать отчет из коллекции объектов Java, генератор отчетов может ожидать реальных общедоступных методов получения и установки в объектах.

В спецификации JPA для API Java Persistence указано, что: 2.0:

«Клиенты объекта не должны получать доступ к переменным экземпляра. Состояние объекта доступно клиентам только через методы объекта, т. Е. Методы доступа (методы получения / установки) или другие бизнес-методы».

JSR 317: API персистентности JavaTM, версия 2.0 — проект для публичного просмотра — раздел 2.1

То есть переносимый код JPA не должен полагаться на прямой доступ к свойствам.

Вывод

В этой статье представлен очень простой способ работы со свойствами в Java, хотя язык не поддерживает свойства. Если реализация AspectJ нецелесообразна, вы можете найти другие реализации этой идеи, используя asm или cglib .

Рекомендации

OpenXava — http://www.openxava.org/

AspectJ — http://www.eclipse.org/aspectj/