DEV Community

Salad Lam
Salad Lam

Posted on

About injecting beans managed by Spring Framework into Apache Wicket Component

Notice

I wrote this article and was originally published on Qiita on 24 July 2022.


Assumption

  • Using wicket-spring-boot-starter, which include Spring Boot 2.6.1 and Apache Wicket 9.7.0
  • ApplicationContext is successfully created in some place of the program

How to use

If you want beans managed by Spring Framework inject into Component, you may annotate the field with @org.apache.wicket.spring.injection.annot.SpringBean. (OR @javax.inject.Inject also works) Below is a example.

public class Product extends WebPage {
    private static final long serialVersionUID = 1L;  // Serializable interface
    @SpringBean
    private Dao dao;

    // other method...
}
Enter fullscreen mode Exit fullscreen mode

Something you need to know

  • all Component is managed by Apache Wicket, not by Spring Framework. So injection is performed by Apache Wicket also.
  • the instance injected is a proxy instance.
  • Component implementes Serializable interface

What happen inside

First, Apache Wicket creates Component by calling its constructor.

public abstract class Component implements
IClusterable, IConverterLocator, IRequestableComponent, IHeaderContributor, IHierarchical<Component>, IEventSink, IEventSource, IMetadataContext<Serializable, Component>, IFeedbackContributor
{
    //...
    public Component(final String id, final IModel<?> model)
    {
        checkId(id);
        this.id = id;

        init();

        Application application = getApplication();
        // <carries out injection>
        application.getComponentInstantiationListeners().onInstantiation(this);
        //

        final DebugSettings debugSettings = application.getDebugSettings();
        if (debugSettings.isLinePreciseReportingOnNewComponentEnabled() && debugSettings.getComponentUseCheck())
        {
            setMetaData(CONSTRUCTED_AT_KEY,
                ComponentStrings.toString(this, new MarkupException("constructed")));
        }

        if (model != null)
        {
            setModelImpl(wrap(model));
        }
    }
    //...
Enter fullscreen mode Exit fullscreen mode

When calling application.getComponentInstantiationListeners().onInstantiation(this), finally org.apache.wicket.injection.Injector.inject(Object, IFieldValueFactory) will be called. Object of IFieldValueFactory is org.apache.wicket.spring.injection.annot.AnnotProxyFieldValueFactory.

public abstract class Injector {
    //...
    protected void inject(final Object object, final IFieldValueFactory factory)
    {
        final Class<?> clazz = object.getClass();

        Field[] fields = null;

        // try cache
        fields = cache.get(clazz);

        if (fields == null)
        {
            // cache miss, discover fields
            fields = findFields(clazz, factory);

            // write to cache
            cache.put(clazz, fields);
        }

        for (final Field field : fields)
        {
            if (!field.canAccess(object))
            {
                field.setAccessible(true);
            }
            try
            {

                if (field.get(object) == null)
                {
                    // <get bean from Spring Framework>
                    Object value = factory.getFieldValue(field, object);
                    //

                    if (value != null)
                    {
                        field.set(object, value);
                    }
                }
            }
            catch (IllegalArgumentException e)
            {
                throw new RuntimeException("error while injecting object [" + object.toString() +
                    "] of type [" + object.getClass().getName() + "]", e);
            }
            catch (IllegalAccessException e)
            {
                throw new RuntimeException("error while injecting object [" + object.toString() +
                    "] of type [" + object.getClass().getName() + "]", e);
            }
        }
    }
    //...
Enter fullscreen mode Exit fullscreen mode

factory.getFieldValue(field, object) is org.apache.wicket.spring.injection.annot.AnnotProxyFieldValueFactory.getFieldValue(Field, Object).

public class AnnotProxyFieldValueFactory implements IFieldValueFactory {
    //...
    @Override
    public Object getFieldValue(final Field field, final Object fieldOwner)
    {
        if (supportsField(field))
        {
            SpringBean annot = field.getAnnotation(SpringBean.class);

            String name;
            boolean required;
            if (annot != null)
            {
                name = annot.name();
                required = annot.required();
            }
            else
            {
                Named named = field.getAnnotation(Named.class);
                name = named != null ? named.value() : "";
                required = true;
            }

            Class<?> generic = ResolvableType.forField(field).resolveGeneric(0);
            String beanName = getBeanName(field, name, required, generic);

            SpringBeanLocator locator = new SpringBeanLocator(beanName, field.getType(), field, contextLocator);

            // only check the cache if the bean is a singleton
            Object cachedValue = cache.get(locator);
            if (cachedValue != null)
            {
                return cachedValue;
            }

            Object target;
            try
            {
                // check whether there is a bean with the provided properties
                target = locator.locateProxyTarget();
            }
            catch (IllegalStateException isx)
            {
                if (required)
                {
                    throw isx;
                }
                else
                {
                    return null;
                }
            }

            if (wrapInProxies)
            {
                // <actual point of get bean and then wrap it by proxy>
                target = LazyInitProxyFactory.createProxy(field.getType(), locator);
                //
            }

            // only put the proxy into the cache if the bean is a singleton
            if (locator.isSingletonBean())
            {
                Object tmpTarget = cache.putIfAbsent(locator, target);
                if (tmpTarget != null)
                {
                    target = tmpTarget;
                }
            }
            return target;
        }
        return null;
    }
    //...
Enter fullscreen mode Exit fullscreen mode

Bean is obtained in org.apache.wicket.spring.SpringBeanLocator.lookupSpringBean(ApplicationContext, String, Class<<??>).

    private Object lookupSpringBean(ApplicationContext ctx, String name, Class<?> clazz)
    {
        try
        {
            // If the name is set the lookup is clear
            if (name != null)
            {
                return ctx.getBean(name, clazz);
            }

            // If the beanField information is null the clazz is going to be used
            if (fieldResolvableType == null)
            {
                return ctx.getBean(clazz);
            }

            // If the given class is a list try to get the generic of the list
            Class<?> lookupClass = fieldElementsResolvableType != null ? 
                fieldElementsResolvableType.resolve() : clazz;

            // Else the lookup is done via Generic
            List<String> names = loadBeanNames(ctx, lookupClass);

            Object foundBeans = getBeansByName(ctx, names);

            if(foundBeans != null)
            {
                return foundBeans;
            }

            throw new IllegalStateException(
                "Concrete bean could not be received from the application context for class: " +
                    clazz.getName() + ".");
        }
        catch (NoSuchBeanDefinitionException e)
        {
            throw new IllegalStateException("bean with name [" + name + "] and class [" +
                clazz.getName() + "] not found", e);
        }
    }
Enter fullscreen mode Exit fullscreen mode

At here you can find out familiar org.springframework.beans.factory.BeanFactory.getBean() call.

Why wrapped by proxy

All components will be serialize at the end of request, therefore Component implementes Serializable interface. But the injected field is not serializable (at the worst case entire Spring Framework will be serialized), so the injected field is wrapped by a proxy. In the proxy, writeReplace() is defined for saving org.apache.wicket.proxy.LazyInitProxyFactory.ProxyReplacement instance instead of saving injected instance. (If you don't know what writeReplace() and readResolve() function is, please refer here)

In ProxyReplacement instance, necessary information for relocate the bean is saved. And the method readResolve() is defined for reinject the field again when deserialize the Component.

        private Object readResolve() throws ObjectStreamException
        {
            Class<?> clazz = WicketObjects.resolveClass(type);
            if (clazz == null)
            {
                try
                {
                    clazz = Class.forName(type, false, Thread.currentThread().getContextClassLoader());
                }
                catch (ClassNotFoundException ignored1)
                {
                    try
                    {
                        clazz = Class.forName(type, false, LazyInitProxyFactory.class.getClassLoader());
                    }
                    catch (ClassNotFoundException ignored2)
                    {
                        ClassNotFoundException cause = new ClassNotFoundException(
                                "Could not resolve type [" + type +
                                        "] with the currently configured org.apache.wicket.application.IClassResolver");
                        throw new WicketRuntimeException(cause);
                    }
                }
            }
            return LazyInitProxyFactory.createProxy(clazz, locator);
        }
Enter fullscreen mode Exit fullscreen mode

Top comments (0)