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...
}
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));
}
}
//...
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);
}
}
}
//...
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;
}
//...
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);
}
}
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);
}