About injecting beans managed by Spring Framework into Apache Wicket Component
Tags: spring, apachewicket
Published: 2024-01-31 20:25:42

## Notice

I wrote this article and was originally published on [Qiita](https://qiita.com/saladlam/items/4dcc2028b072488cb0f0) on 24 July 2022.

---

# Assumption
- Using [wicket-spring-boot-starter](https://github.com/MarcGiffing/wicket-spring-boot), 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. 

```java
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.

```java
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*.

```java
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)*.

```java
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<<??>)**.

```java
	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](https://qiita.com/saladlam/items/abff0b09f9f0b9e1cb1d))

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.

```java
		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);
		}
```