wicket GuiceComponentInjectorはCustomInjectionsを無視する


GuiceComponentInjectorを使用している状況で、
CustomInjectionsを使用したところ、ひたすらNullPointerExceptionが出力され、
Injectできなくて困ってしまったことについて、備忘として、メモします。
※正確には、GuiceFieldValueFactoryがCustomInjectionsを無視していそうに思います。

参考サイト


Wicket 1.5 と Google guice 3.0
Custom Injector SLF4JAdd Star
WicketとCayenneとGuice

問題


wicket guice dropwizardの連携はうまくいっているのに、 @InjectLoggerがNullになっている。

原因


処理シーケンスで書きます。
※嘘を書いているかもしれません。卓上デバックのため..

1. GuiceComponentInjectorのコンストラクタでGuiceFieldValueFactoryがnewされる。

    /**
     * Creates a new Wicket GuiceComponentInjector instance, using the provided Guice
     * {@link Injector} instance.
     * 
     * @param app
     * @param injector
     * @param wrapInProxies
     *            whether or not wicket should wrap dependencies with specialized proxies that can
     *            be safely serialized. in most cases this should be set to true.
     */
    public GuiceComponentInjector(final Application app, final Injector injector,
        final boolean wrapInProxies)
    {
        app.setMetaData(GuiceInjectorHolder.INJECTOR_KEY, new GuiceInjectorHolder(injector));
        fieldValueFactory = new GuiceFieldValueFactory(wrapInProxies);
        app.getBehaviorInstantiationListeners().add(this);
        bind(app);
    }

2. GuiceComponentInjector#onInstantiation()がWicketの処理シーケンスのどっかで呼び出される。


3. GuiceComponentInjector#onInstantiation()から規定クラスorg.apache.wicket.injection.Injectorのinject()メソッドが呼び出される。


4. org.apache.wicket.injection.Injector#inject()から、factory#getFieldValue()で、GuiceFieldValueFactoryのgetFieldValue()が呼び出さる。


5. GuiceFieldValueFactory#getFieldValue()内で、GuiceFieldValueFactory#supportsField()が呼び出さる。

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean supportsField(final Field field)
    {
        return field.isAnnotationPresent(Inject.class) || field.isAnnotationPresent(javax.inject.Inject.class);
    }

6.[5].でCustomInjectionsはあからさまにサポートされていないので、Null、

なのでNullPointerExceptionとなる。

※予想です…


対処方法(暫定1) IPageFactoryの実装クラスを作成する。

以下のIPageFactoryの実装クラスを作成したところ、上手くinjectされました。 ※chacheからの復旧時は不明です。(そこが大事かもです..)

/*
 * Copyright 2016 Kem.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package xyz.monotalk.xxx;

import com.google.inject.Injector;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Modifier;
import java.util.concurrent.ConcurrentMap;
import javax.inject.Inject;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.wicket.IPageFactory;
import org.apache.wicket.Page;
import org.apache.wicket.WicketRuntimeException;
import org.apache.wicket.authorization.AuthorizationException;
import org.apache.wicket.markup.MarkupException;
import org.apache.wicket.request.RequestHandlerStack.ReplaceHandlerException;
import org.apache.wicket.request.component.IRequestablePage;
import org.apache.wicket.request.mapper.parameter.PageParameters;
import org.apache.wicket.session.DefaultPageFactory;
import org.apache.wicket.util.lang.Generics;

/**
 * AppGuicePageFactory
 *
 * @author Kem
 */
public class AppGuicePageFactory implements IPageFactory {

    @Inject
    private Injector injector;

    /**
     * Log for reporting.
     */
    private static final Logger log = LogManager.getLogger(DefaultPageFactory.class);

    /**
     * Map of Constructors for Page subclasses
     */
    private final ConcurrentMap<Class<?>, Constructor<?>> constructorForClass = Generics.newConcurrentHashMap();

    /**
     * {@link #isBookmarkable(Class)} is expensive, we cache the result here
     */
    private final ConcurrentMap<String, Boolean> pageToBookmarkableCache = Generics.newConcurrentHashMap();

    @Override
    public final <C extends IRequestablePage> C newPage(final Class<C> pageClass) {
        try {
            // throw an exception in case default constructor is missing
            // => improved error message
            Constructor<C> constructor = pageClass.getDeclaredConstructor((Class<?>[]) null);
            return processPage(newPage(constructor, null), null);

        } catch (NoSuchMethodException e) {
            // a bit of a hack here..
            Constructor<C> constructor = constructor(pageClass, PageParameters.class);
            if (constructor != null) {
                PageParameters pp = new PageParameters();
                return processPage(newPage(constructor, pp), pp);

            } else {
                throw new WicketRuntimeException("Unable to create page from " + pageClass
                        + ". Class does not have a visible default constructor.", e);
            }
        }
    }

    @Override
    public final <C extends IRequestablePage> C newPage(final Class<C> pageClass,
            final PageParameters parameters) {
        // Try to get constructor that takes PageParameters
        Constructor<C> constructor = constructor(pageClass, PageParameters.class);

        // If we got a PageParameters constructor
        if (constructor != null) {
            final PageParameters nullSafeParams = parameters == null ? new PageParameters() : parameters;

            // return new Page(parameters)
            return processPage(newPage(constructor, nullSafeParams), nullSafeParams);
        }

        // Always try default constructor if one exists
        return processPage(newPage(pageClass), parameters);
    }

    /**
     * Looks up a one-arg Page constructor by class and argument type.
     *
     * @param pageClass The class of page
     * @param argumentType The argument type
     * @return The page constructor, or null if no one-arg constructor can be
     * found taking the given argument type.
     */
    private <C extends IRequestablePage> Constructor<C> constructor(final Class<C> pageClass,
            final Class<PageParameters> argumentType) {
        // Get constructor for page class from cache
        Constructor<C> constructor = (Constructor<C>) constructorForClass.get(pageClass);

        // Need to look up?
        if (constructor == null) {
            try {
                // Try to find the constructor
                constructor = pageClass.getDeclaredConstructor(new Class[]{argumentType});

                // Store it in the cache
                Constructor<C> tmpConstructor = (Constructor<C>) constructorForClass.putIfAbsent(pageClass, constructor);
                if (tmpConstructor != null) {
                    constructor = tmpConstructor;
                }

                log.debug("Found constructor for Page of type '{}' and argument of type '{}'.",
                        pageClass, argumentType);
            } catch (NoSuchMethodException e) {
                log.debug(
                        "Page of type '{}' has not visible constructor with an argument of type '{}'.",
                        pageClass, argumentType);

                return null;
            }
        }

        return constructor;
    }

    /**
     * Creates a new Page using the given constructor and argument.
     *
     * @param constructor The constructor to invoke
     * @param argument The argument to pass to the constructor or null to pass
     * no arguments
     * @return The new page
     * @throws WicketRuntimeException Thrown if the Page cannot be instantiated
     * using the given constructor and argument.
     */
    private <C extends IRequestablePage> C newPage(final Constructor<C> constructor, final PageParameters argument) {
        try {
            if (argument != null) {
                return constructor.newInstance(argument);
            } else {
                return constructor.newInstance();
            }
        } catch (InstantiationException | IllegalAccessException e) {
            throw new WicketRuntimeException(createDescription(constructor, argument), e);
        } catch (InvocationTargetException e) {
            if (e.getTargetException() instanceof ReplaceHandlerException
                    || e.getTargetException() instanceof AuthorizationException
                    || e.getTargetException() instanceof MarkupException) {
                throw (RuntimeException) e.getTargetException();
            }
            throw new WicketRuntimeException(createDescription(constructor, argument), e);
        }
    }

    private <C extends IRequestablePage> C processPage(final C page, final PageParameters pageParameters) {
        // the page might have not propagate page parameters from constructor. if that's the case
        // we force the parameters
        if ((pageParameters != null) && (page.getPageParameters() != pageParameters)) {
            page.getPageParameters().overwriteWith(pageParameters);
        }
        ((Page) page).setWasCreatedBookmarkable(true);
        injector.injectMembers(page);
        return page;
    }

    private String createDescription(final Constructor<?> constructor, final Object argument) {
        StringBuilder msg = new StringBuilder();
        msg.append("Can't instantiate page using constructor '").append(constructor).append('\'');
        if (argument != null) {
            msg.append(" and argument '").append(argument).append('\'');
        }
        msg.append('.');

        if (constructor != null) {
            if (Modifier.isPrivate(constructor.getModifiers())) {
                msg.append(" This constructor is private!");
            } else {
                msg.append(" An exception has been thrown during construction!");
            }
        } else {
            msg.append(" There is no such constructor!");
        }

        return msg.toString();
    }

    @Override
    public <C extends IRequestablePage> boolean isBookmarkable(Class<C> pageClass) {
        Boolean bookmarkable = pageToBookmarkableCache.get(pageClass.getName());
        if (bookmarkable == null) {
            try {
                if (pageClass.getDeclaredConstructor(new Class[]{}) != null) {
                    bookmarkable = Boolean.TRUE;
                }
            } catch (NoSuchMethodException | SecurityException ignore) {
                try {
                    if (pageClass.getDeclaredConstructor(new Class[]{PageParameters.class}) != null) {
                        bookmarkable = Boolean.TRUE;
                    }
                } catch (NoSuchMethodException | SecurityException ignored) {
                    log.warn("Error Occured..", ignored);
                }
            }

            if (bookmarkable == null) {
                bookmarkable = Boolean.FALSE;
            }
            Boolean tmpBookmarkable = pageToBookmarkableCache.putIfAbsent(pageClass.getName(), bookmarkable);
            if (tmpBookmarkable != null) {
                bookmarkable = tmpBookmarkable;
            }
        }

        return bookmarkable;
    }
}

上記は、DefaultPageFacotryのprocesspage内に以下の記述を追加しただけです。

        ((Page) page).setWasCreatedBookmarkable(true);
        injector.injectMembers(page);
        return page;

対処方法(暫定2) コンストラクター内でInjectしてしまう。

onInitialize()等で、Injector#injectMembers を呼んでしまう。

        WebApplication webApp = (WebApplication) getApplication();
        Injector i = (Injector) webApp.getServletContext().getAttribute("com.google.inject.Injector");
        i.injectMembers(this);

恒久な対処方法

GuiceComponentInjectorあたりからGuiceFieldValueFactoryからの呼び出し下位クラスを自前作成する…

個人で実装してる感じでの結論

Pagaクラスで発生する問題だから、問題が発生しないレベルで対処すれば、いいのでは?
と思いました。(なので、CustomInjectionsはPageクラスで使わない。)
@InjectでInjectorは使えるから、Pageクラスからの呼び出しクラスで、CustomInjectionsは使うという
自分ルールで対処しようかと思います。

コメント