Содержание
I. Описание проблемы
II. Обзор существующих решений
III. Вариант решения без применения аспектов.
IV. Решение на AspectJ
V. Динамические аспекты
VI. Послесловие.
VII. Ссылки и литература
I.Описание проблемы
При программировании на Java (и некоторых других языках) часто приходиться работать со строковыми переменными и другими типами, представляющими собой объекты-обёртки примитивных типов например: Boolean, Integer, Double. Например есть бин (здесь и далее под бином имеется ввиду класс, поддерживающий рекомендации Java Beans):
public class SomeBean{
private String strField;
public void setStrField(String strField){
this.strField=strField;
}
public String getStrField(){
return strField;
}
// остальное описание пропущено
}
В дальнейшем, при использовании этого бина мы должны проверять поле на null:
String s=someBean.getStrField();
if (s!=null){
if (!s.isEmpty()){ // s.length()>0 Java 5-
doSomeThing();
}
}
И так нужно делать с любым полем-объектом. Если не сделать, то рано или поздно приложение завершится по непроверяемому исключению NullPointerException. Можно, конечно, принудительно перехватывать данное исключение, перекладывая свои ошибки на систему, однако это считается признаком безграмотного стиля программирования и влечет за собой потерю производительности, поскольку генерация исключения эквивалентна десяткам обычных команд. Такую проверку на null приходиться делать неоднократно, в самом бине и во многих местах за его пределами. Чем чаще используется бин, и чем сложнее приложение, тем чаще приходиться проверять на null, и соответственно приходиться выполнять рутинную работу, что влечет вероятность что-то пропустить, а это — увеличивает вероятность неработоспособности приложения. Подобный подход запутывает код, делает его менее наглядным, приводит к увеличению излишнего кода и уменьшает быстродействие. Я как программист часто сталкиваюсь с подобными вещами у коллег по цеху и даже в своей практике. Далее будет представлен краткий обзор существующих средств борьбы с null-зависимостью, и эффективное, на мой взгляд, общее решение на основе применения аспектов.
II. Обзор существующих решений
Я предположил, что кто-то это делал уже до меня, и сделал обзор уже существующих решений.
1) аннотация @ NotNull из JSR-305 подробно описана в [1] и [2].
Ставится перед полями и методами, возвращающими объекты. Позволяет определить на уровне исходных текстов предварительно отмеченные IDE проблемные места.
@NotNull
public Color notNullMethod(){
return null;
}
Недостаток: сама аннотация не решает проблему, и нужно не забыть поставить ее во всех проблемных местах, JSR-305 не в состоянии проанализировать сложный код.
2) Checker Framework JSR-308 описана в [3] и [4].
Представляет более продвинутую технику по сравнению с JSR-305 за счет введения дополнительных проверок кода для анализа более сложного кода при проверке на null, а также предоставляет множество других полезных аннотаций для проверок многозадачности, типов, регулярных выражений и других.
Достоинства: развитый фреймворк
Недостатки применительно к обсуждаемой проблеме: аннотация @ NotNull не представляет решения, нужно не забыть поставить аннотацию во всех проблемных местах.
3) The Java Annotation Checker (JACK) [5] имеет те же достоинства и недостатки что и JSR-305
4) Project Coin, содержащий множество рекомендаций по улучшению Java (под влиянием языка Groovy), подробнее в [6] и [7]. Рассмотрим пример функции:
public String getPostcode(Person person){
if (person != null){
Address address = person.getAddress();
if (address != null){
return address.getPostcode();
}
}
return null;
}
По предложению Project Coin эту функцию можно было бы переписать через NullSafe-navigation:
public String getPostcode(Person person){
return person?.getAddress()?.getPostcode();
}
К сожалению, Oracle не включила эту возможность в Java 7, поэтому для Java придеться довольствоваться другими решениями.
5) Применение языка Groovy. Собственно, описанный выше NullSafe-navigation пошёл именно оттуда [8].
Достоинство: значительное упрощение кода
Недостаток: нужно всё равно помнить обо все проблемных местах
6) Apache Commons, StringUtils [9] и PropertyUtils [10]:
StringUtils.isBlank(null)= true
StringUtils.isBlank("")= true
StringUtils.isBlank(" ")= true
String firstName = (String) PropertyUtils.getSimpleProperty(employee, "firstName");
String city = (String)PropertyUtils.getNestedProperty(employee, "address(home).city");
Достоинства: упрощение проверок
Недостатки: проверять нужно везде, где требуется. Нужен дополнительный код для обработки исключений, генерируемых PropertyUtils.
7) Анализаторы статического кода FindBugs [11] и PMD [12], имеющие плагины под большинство IDE.
Достоинства: Очень мощные и полезные инструменты, анализирующие статический код без предварительной инъекции аннотаций.
Недостатки: Показывают проблемные места, но не предоставляют готового общего решения.
III. Вариант решения без применения аспектов
Проверка на null, как и всякая задача чтения данных, на практике происходит чаще, чем присвоение (запись) значений. Поэтому операция чтения должна быть предельно оптимизирована по производительности. Можно переписать взаимодействие с полями бина в этом случае:
public class SomeBean{
private String strField="";
public void setStrField(String strField){
if (strField!=null){ this.strField=strField; }
}
public String getStrField(){
return strField;
}
}
Null-значения в таком случае игнорируются. Также поля принудительно инициализируются. Всё это вместе позволяет создать null-безопасный код, в котором уже не потребуются производить многочисленные проверки на null, что значительно упрощает написание кода и кардинально снижает вероятность что-либо пропустить.
Достоинства: не нужно больше заботиться о проверке на null для работы с такими полями, что значительно упрощает код и увеличивает быстродействие.
Недостатки: для каждого поля приходится писать код инициализации и код отсечения null.
IV. Решение на AspectJ
Чтобы не писать для каждого кода отсечение null просто напрашивается на решение с помощью аспектов которое позволит обеспечить сквозную функциональность. Очевидно, что желательно действовать на уровне полей, так как операции с полями могут быть внутри бина, а сквозная функциональность должна быть универсальной, иначе в ней бы не было смысла.
Для выборочной пометки полей и целиком классов введем аннотацию NullSafe:
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.TYPE})
public @interface NullSafe {
boolean getter() default true;
}
@NullSafe(getter=false)
– нужно для упрощения логики и увеличения быстродействия в случае, если программист не поленился инициализировать поля в бине, в этом случае действию аспекта подвергается только запись в поле.
Для будущей лёгкой модификации аспект написан с использованием аннотированного AspectJ 5 и требует для работы Java 5+. Подробно про аспекты можно прочитать в [13],[14],[15].
@Aspect
public class NullSafeAspect {
@Pointcut(value="within(@NullSafe *)") // support annotation by class
public void byClass() {}
@Pointcut(value="!within(@NullSafe(getter=false) *)")
public void byClassNoGetter() {}
@Pointcut(value="get(@NullSafe Long||Integer||Double||Float||Short||Byte||java.math.BigDecimal *)")
public void fieldGetNumerics() {}
@Pointcut(value="!get(@NullSafe(getter=false) * *)")
public void fieldNoGetter() {}
@Pointcut(value="get(Long||Integer||Double||Float||Short||Byte||java.math.BigDecimal *)")
public void classGetNumerics() {}
@Pointcut(value="set(Long||Integer||Double||Float||Short||Byte||java.math.BigDecimal *)")
public void classSetNumerics() {}
@Pointcut(value="set(@NullSafe * *)") // all field-annotated
public void fieldSetAllSupported() {}
@Pointcut(value="classSetNumerics() || set(String||Boolean||Character *)")
public void classSetAllSupported() {}
@Pointcut(value="fieldGetNumerics() || get(@NullSafe String||Boolean||Character *)")
public void fieldGetAllSupported() {}
@Pointcut(value="classGetNumerics() || get(String||Boolean||Character *)")
public void classGetAllSupported() {}
@Around(value="(fieldSetAllSupported() || byClass() && classSetAllSupported()) && args(v)")
public void aroundSet(ProceedingJoinPoint jp, Object v) throws Throwable{
toLogSetter(jp, v);
if (v!=null){ jp.proceed();}
}
@Around(value="get(@NullSafe String *) && fieldNoGetter() || byClass() && get(String *) && byClassNoGetter()")
public String aroundGetString(ProceedingJoinPoint jp) throws Throwable{
String v=(String)jp.proceed();
if (v==null){return "";}
return v;
}
private Field getField(JoinPoint jp){
try{
Signature sig=jp.getStaticPart().getSignature();
Field field=sig.getDeclaringType().getDeclaredField(sig.getName());
field.setAccessible(true);
return field;
}catch(Exception e){
}
return null;
}
private Object getBean(JoinPoint jp){
try {
Field field=getField(jp);
if (field==null){return null;}
Object obj=field.getType().newInstance();
field.set(jp.getTarget(),obj);
return obj;
}catch(Exception e){
stackTraceToLog(e);
}
return null;
}
@Around(value="!fieldGetAllSupported() && get(@NullSafe * *) && fieldNoGetter() && byClassNoGetter()")
public Object aroundGetBean(ProceedingJoinPoint jp) throws Throwable{
Object v=jp.proceed();
if (v==null){
return getBean(jp);
}
return v;
}
private Object getNumeric(JoinPoint jp){
try {
Field field=getField(jp);
if (field==null){return null;}
Object obj=field.getType().getDeclaredConstructor(String.class).newInstance("0");
field.set(jp.getTarget(),obj);
return obj;
}catch(Exception e){
stackTraceToLog(e);
}
return null;
}
@Around(value="fieldGetNumerics() && fieldNoGetter() || byClass() && classGetNumerics() && byClassNoGetter()")
public Object aroundGetNumerics(ProceedingJoinPoint jp) throws Throwable{
Object v=jp.proceed();
if (v==null){
return getNumeric(jp);
}
return v;
}
}
Пример применения аннотации:
@NullSafe
public class SomeClassTestByClass extends SomeClass {
...
}
@NullSafe
private String strField;
@NullSafe
private HashMap<Integer,HashMap<String,String>> map;
@NullSafe(getter=false)
private ArrayList<String> listString=new ArrayList<String>();
Далее приведены разъяснения по поводу данного аспекта. Аннотация @Pointcut содержит описание стандартных точек пересечения. Within – означает действие внутри класса, в данном случае любого класса помеченного аннотацией @ NullSafe. Поддерживаются численные типы Long,Integer,Double,Float,Short,Byte,BigDecimal, которые инициализируются одинаковым шаблоном. Также поддерживаются String,Boolean,Character. Все упомянутые выше классы-обертки входят в понятие поддерживаемых аспектом типов. Также можно аннотировать на уровне поля любой другой бин или класс поддерживающий конструктор без параметров. Аннотирование класса подразумевает полную поддержку сразу для всех поддерживаемых типов полей. Все эти поля инициализируются с помощью reflection. Если инициализировать поля в коде, то это позволит увеличить быстродействие, отказавшись от геттеров используя @ NullSafe(getter=false). Set() и get() — в точках пересечения ответственны за запись или чтение поля бина, в том числе для операций с полями внутри бина. Совет @Advice отвечает за действия в точках пересечения. Использование совета @Before для пересечения set() мне не показалось хорошей идеей, потому что для предотвращения выполнения кода требуется вызывать исключение, что неприемлемо с точки зрения быстродействия. По умолчанию аспект создается в виде Singleton. Вся обработка обеспечивается всего лишь одним методом в проекте для каждого типа. Полный пример готового аспекта с тестами приведен в проекте NullSafe в [18].
Достоинство: сквозная функциональность только там, где это необходимо.
Недостатки: всё еще нужно ставить аннотации на уровне классов и отдельных полей, а сквозная функциональность для всех классов без ограничений аннотацией @ NullSafe кажется мне некорректным решением.
V. Динамические аспекты
Аспекты обычно являются статической модификацией кода и выполняется на этапе компиляции. Поэтому сложное описание точек пересечения никак не влияет на быстродействие готовой программы. Также AspectJ может производить динамическую модификацию байт-кода с помощью изменения загрузчика. Это нужно в случае применения аспектов для байт-кода, для которого нет исходных текстов. В этом случае нужно убрать аннотации и добавить упоминание определенных классов в аспекте NullSafeAspect (или создать новый аспект, который его наследует). Так как такая техника специфична для разных серверов приложений и сред, я не буду на ней подробно останавливаться. Подробнее об использовании динамических аспектов можно узнать из [16] и [17].
VI. Послесловие
Данная статья является попыткой взглянуть на общеизвестную проблему под другим взглядом. Возможно использование null-значений необходимо для какого-либо конкретного алгоритма или базы данных, но этом случае, как показывает моя практика, часто возникает сомнения, оптимален ли алгоритм или база данных, не содержат ли они лишних данных. Подход, описанный в статье вполне может стать стандартной техникой написания более компактного и багоустойчивого кода, то есть стать одним из шаблонов проектирования на Java.
VII. Ссылки и литература*
1. jcp.org/en/jsr/detail?id=305
2. www.jetbrains.com/idea/documentation/howto.html
3. jcp.org/en/jsr/detail?id=308
4. types.cs.washington.edu/checker-framework/current/checkers-manual.html
5. homepages.ecs.vuw.ac.nz/~djp/JACK/
6. blogs.oracle.com/darcy/entry/project_coin_final_five
7. metoojava.wordpress.com/2010/11/15/java-7-awesome-features/
8. groovy.codehaus.org/Operators
9. commons.apache.org/lang/api-2.5/org/apache/commons/lang/StringUtils.html
10. commons.apache.org/beanutils/apidocs/org/apache/commons/beanutils/package-summary.html#package_description
11. findbugs.sourceforge.net/
12. pmd.sourceforge.net/
13. eclipse.org/aspectj/doc/released/adk15notebook/index.html
14. eclipse.org/aspectj/doc/released/progguide/index.html
15. AspectJ in Action. Second Edition. ISBN 978-1-933988-05-4
16. www.eclipse.org/aspectj/doc/next/devguide/ltw.html
17. static.springsource.org/spring/docs/3.2.x/spring-framework-reference/html/aop.html#aop-aj-ltw
18. sourceforge.net/projects/nullsafe/files/
*все интернет-ссылки на момент написания статьи были действительны
02/2013 Oleg Nikolaenko
Автор: olegnik