Во время последней конференции 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.
Аспект перехватывает весь доступ к открытым полям в пакете модели, а затем использует самоанализ для вызова метода получения или установки, если они существуют.
Недостатки
К сожалению, у этого подхода есть как минимум два важных недостатка:
-
Он не работает при доступе к свойствам с помощью самоанализа.
-
В 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/