null

Неизменяемые коллекции и JSF

В заметке рассмотрим проблему, которая может возникнуть при использовании неизменяемых коллекций в JSF.

Рассмотрим следующий пример. Допустим, что имеется кусочек view содержащий следующий тег:

<p:outputPanel rendered="#{someBean.someList.size() ne 0}">

Такое постоянно встречается в приложениях на JSF. В теге задан атрибут rendered, который определяет будет ли рисоваться компонент или нет. Здесь идёт проверка на то, является ли список пустым или нет.

Именно в использовании коллекции и кроется опасность. Так, например, досталось чинить следующий баг.

При отрисовки view страница падала со следующим stacktrace'ом.

Caused by: javax.el.ELException: java.lang.IllegalAccessException: Class javax.el.ELUtil can not access a member of class java.util.Collections$EmptyList with modifiers "public"
	at javax.el.ELUtil.invokeMethod(ELUtil.java:328)
	at javax.el.BeanELResolver.invoke(BeanELResolver.java:536)
	at javax.el.CompositeELResolver.invoke(CompositeELResolver.java:256)
	at com.sun.el.parser.AstValue.getValue(AstValue.java:136)
	at com.sun.el.parser.AstValue.getValue(AstValue.java:204)
	at com.sun.el.parser.AstNotEqual.getValue(AstNotEqual.java:58)
	at com.sun.el.ValueExpressionImpl.getValue(ValueExpressionImpl.java:226)
	at com.sun.faces.facelets.el.TagValueExpression.getValue(TagValueExpression.java:109)
	... 106 common frames omitted

Из stacktrace становится ясно, что внутри EL expression происходит обращение к неизменяемой коллекции. Давайте взглянем в код бина и посмотрим, как и где могло произойти такое присваивание.

if (someConidition) {
     // Много кода
    someList = someRepository.getSomedata(); 
} else {
  someList = Collections.EMPTY_LIST;
}

Окей.  Человек, писавший бизнес-логику в одной из нечасто возникающих ситуация присвоил значению поля равное Collections.EMPTY_LIST. Если взглянуть внутрь коллекции, то окажется, что этот статическое, final поле. которые является экземпляром класса EmptyList.

Хорошо, допустим, что мы действительно используем неизменяемую коллекцию, но разве мы пытаемся вызвать какой-либо из методов меняющий состояние коллекции? Нет, происходит лишь обращение к методу isEmpty. В таком случае не должно быть никаких проблем с вызовом метода.

Однако, проблема всё же возникает, чтобы понять почему она происходит. Давайте взглянем на stackTrace чуть лучше. Видно что выбрасывается следующее исключение: IllegalAccessException. Если взглянуть javadoc этого класса, то у него имеется следующее описание:

An IllegalAccessException is thrown when an application tries
to reflectively create an instance (other than an array),
set or get a field, or invoke a method, but the currently
executing method does not have access to the definition of
the specified class, field, method or constructor.

Из описания следует, что возникает оно при попытке создания экземпляра или, например, вызова метода, не имея при этом прав на данную операцию. Например такое может возникнуть при попытке через Reflection API установить значения приватного поля или сделать invoke приватного метода

Из этого происходит мысль о том, что возможно у EmptyList метод isEmpty по каким-либо причинам является private. Оказалось что нет. Проблема в заключается в том, что класс EmptyList является приватным вложенным статическим классом внутри класса Collections.

public class Collections {
 private static class EmptyList<E>
        extends AbstractList<E>
        implements Serializable {
             public int size() {return 0;}
             public boolean isEmpty() {return true;}
    }
 public static final List EMPTY_LIST = new EmptyList<>();
}

Для того, чтобы воспроизвести эту ситуацию напишем следующий пример:

public static void main(String[] args) {
        List<Object> list = Collections.EMPTY_LIST;
        for (Method method : list.getClass().getMethods()) {
            if ("isEmpty".equals(method.getName())) {
                try{
                    method.invoke(list); // IllegalAccessException.
                }
                catch(IllegalAccessException e) {
                    System.out.println("Произошёл IllegalAccessException");
                }
                catch(InvocationTargetException e) {
                    System.out.println("Произошёл InvocationTargetException");
                }
            }
        }
}

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

Произошёл IllegalAccessException

Для того, чтобы понять почему это происходит взглянем внутрь метода invoke.

public Object invoke(Object obj, Object... args)
        throws IllegalAccessException, IllegalArgumentException,
           InvocationTargetException
    {
        if (!override) {
            if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
                Class<?> caller = Reflection.getCallerClass();
                checkAccess(caller, clazz, obj, modifiers);
            }
        }
        MethodAccessor ma = methodAccessor;             // read volatile
        if (ma == null) {
            ma = acquireMethodAccessor();
        }
        return ma.invoke(obj, args);
    }

Здесь нас интерсует метод checkAccess, в котором происходит проверка та самая проверка доступа. Посмотрим содержимое этого метода.

    void checkAccess(Class<?> caller, Class<?> clazz, Object obj, int modifiers)
        throws IllegalAccessException
    {
        if (caller == clazz) {  // quick check
            return;             // ACCESS IS OK
        }
        Object cache = securityCheckCache;  // read volatile
        Class<?> targetClass = clazz;
        if (obj != null
            && Modifier.isProtected(modifiers)
            && ((targetClass = obj.getClass()) != clazz)) {
            // Must match a 2-list of { caller, targetClass }.
            if (cache instanceof Class[]) {
                Class<?>[] cache2 = (Class<?>[]) cache;
                if (cache2[1] == targetClass &&
                    cache2[0] == caller) {
                    return;     // ACCESS IS OK
                }
                // (Test cache[1] first since range check for [1]
                // subsumes range check for [0].)
            }
        } else if (cache == caller) {
            // Non-protected case (or obj.class == this.clazz).
            return;             // ACCESS IS OK
        }

        // If no return, fall through to the slow path.
        slowCheckMemberAccess(caller, clazz, obj, modifiers, targetClass);
    }

Здесь много букв, но в любом случае данное исключение будет выброшено в методе slowCheckMemberAccess. Преждем чем заглянуем туда, следует запомнить что в метод передаются следующие аргументы: caller - класс, который вызывает метод, clazz - класс у которого вызывается метод, obj - объект, чей метод вызывается.

Внутри этого метода для нас интересно лишь только следующая строка:

Reflection.ensureMemberAccess(caller, clazz, obj, modifiers);

Содержимое этого метода следующего

public static void ensureMemberAccess(Class<?> var0, Class<?> var1, Object var2, int var3) throws IllegalAccessException {
        if (var0 != null && var1 != null) {
            if (!verifyMemberAccess(var0, var1, var2, var3)) {
                throw new IllegalAccessException("Class " + var0.getName() + " can not access a member of class " + var1.getName() + " with modifiers \"" + Modifier.toString(var3) + "\"");
            }
        } else {
            throw new InternalError();
        }
    }

    public static boolean verifyMemberAccess(Class<?> var0, Class<?> var1, Object var2, int var3) {
        boolean var4 = false;
        boolean var5 = false;
        if (var0 == var1) {
            return true;
        } else {
            if (!Modifier.isPublic(getClassAccessFlags(var1))) {
                var5 = isSameClassPackage(var0, var1);
                var4 = true;
                if (!var5) {
                    return false;
                }
            }

            if (Modifier.isPublic(var3)) {
                return true;
            } else {
                boolean var6 = false;
                if (Modifier.isProtected(var3) && isSubclassOf(var0, var1)) {
                    var6 = true;
                }

                if (!var6 && !Modifier.isPrivate(var3)) {
                    if (!var4) {
                        var5 = isSameClassPackage(var0, var1);
                        var4 = true;
                    }

                    if (var5) {
                        var6 = true;
                    }
                }

                if (!var6) {
                    return false;
                } else {
                    if (Modifier.isProtected(var3)) {
                        Class var7 = var2 == null ? var1 : var2.getClass();
                        if (var7 != var0) {
                            if (!var4) {
                                var5 = isSameClassPackage(var0, var1);
                                var4 = true;
                            }

                            if (!var5 && !isSubclassOf(var7, var0)) {
                                return false;
                            }
                        }
                    }

                    return true;
                }
            }
        }
    }


Здесь видно, что исключение бросается только в том случае если verifyMemberAccess возвращается false. В данном методе False будет возвращаться в трёх случаях. В нашем конкретном случае это будет происходить в первой ветке, которая возвращает false.

Это произойдёт, если класс у которого вызывается метод не является public и вызывающий класс находится в другом пакете. Именно это у нас и происходит, класс EmptyList является вложенным и помечен модификатором private, а метод isEmpty вызывается из нашего класса, который находится в другом пакете. Именно из-за этого и возникает исключение.

Если бы, обработчик ELexpresiion'а прежде чем вызывать метод, делал setAccessible(true), то исключение бы не возникало. Повлиять на логику работы библиотеки мы не можем, поэтому есть два варианта:

заменить Collections.EMPTY_LIST на new ArrayList<>() или же использовать в EL выражении не метод isEmpty() ne 0, а empty. Выглядеть второй вариант будет следующим образом.

<p:outputPanel rendered="#{empty someBean.someList}">

Вот и всё. На этом, пожалуй, откланяюсь.