Spring и @Autowired для ENUM-типов. Факультатив

в 19:00, , рубрики: java, spring, spring framework, примеры кода, Программирование

Как известно, в Spring нельзя сделать бины для перечисляемых типов без «костылей» — у этого типа «нет» конструктора.

Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'demoEnum0' defined in file [...DemoEnum0.class]: Instantiation of bean failed; nested exception is org.springframework.beans.BeanInstantiationException: Could not instantiate bean class [ru.itbasis.demo.spring.enums.DemoEnum0]: No default constructor found; nested exception is java.lang.NoSuchMethodException: ru.itbasis.demo.spring.enums.DemoEnum0.<init>()

(коммит)

В данном посте я попробую обойти это ограничение.

Шаг 1. Подменяем factory-метод

Создадим класс EnumHandlerBeanFactoryPostProcessor, реализующий интерфейс BeanFactoryPostProcessor.

@Component
public class EnumHandlerBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
	private static final transient Logger LOG = LoggerFactory.getLogger(EnumHandlerBeanFactoryPostProcessor.class.getName());

	@Override
	public void postProcessBeanFactory(final ConfigurableListableBeanFactory beanFactory) throws BeansException {
		final String[] beanDefinitionNames = beanFactory.getBeanDefinitionNames();

		for (String beanDefinitionName : beanDefinitionNames) {
			final BeanDefinition beanDefinition = beanFactory.getBeanDefinition(beanDefinitionName);
			LOG.debug("beanDefinitionName: {}", beanDefinitionName);

			final String beanClassName = beanDefinition.getBeanClassName();
			LOG.debug("beanClassName: {}", beanClassName);

			try {
				final Class<?> beanClass = Class.forName(beanClassName);
				if (beanClass.isEnum()) {

					LOG.trace("found ENUM class: {}", beanClass);
					LOG.trace("interfaces: {}", beanClass.getInterfaces());

					beanDefinition.setFactoryMethodName("values");
				}

			} catch (ClassNotFoundException e) {
				LOG.error(e.getMessage(), e);
			}
		}
	}
}

Здесь мы ищем в будущих бинах все классы типа ENUM и заменяем для них factory-конструктор на вызов метода “values” от бина. Теперь результатом при создании данного бина будет не ошибка, а массив из его констант.

(коммит)

Но этого недостаточно – Spring не даст просто взять, да и подсунуть созданный бин, ибо нельзя получить такой бин по его типу.
«Ну да лиха беда начало», сказал Иван-дурак, да пошёл копать дальше.

Шаг 2. «Черновые» работы

«Обернём» наш enum-тип в интерфейс и в тестируемом классе изменим тип с enum-типа на Set<IEnum>:

public interface IEnum {
}

@Component
public class SpringEnum {

	@Autowired(required = false)
	private Set<IEnum> fieldEnumSet;

	public Set<IEnum> getFieldEnum() {
		return fieldEnumSet;
	}
}

И добавим новую аннотацию @EnumAnnotation на базе аннотации @Component

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Component
public @interface EnumAnnotation {
}

@EnumAnnotation
public enum DemoEnum0 implements IEnum {
	VALUE_0, VALUE_1
}

(коммит)

Шаг 3 Заставляем Spring сделать Autowire для нашего бина

Создадим BeanPostProcessor, который «осведомим» о контексте:

@Component
public class EnumBeanPostProcessor implements BeanPostProcessor, ApplicationContextAware {
	private ApplicationContext context;

	@Override
	public Object postProcessBeforeInitialization(final Object bean, final String beanName) throws BeansException {
		return bean;
	}

	@Override
	public Object postProcessAfterInitialization(final Object bean, final String beanName) throws BeansException {
		return bean;
	}

	@Override
	public void setApplicationContext(final ApplicationContext applicationContext) throws BeansException {
		this.context = applicationContext;
	}
}

Нам понадобится метод "getEnums", возвращающий список объектов значений реализующих интерфейс IEnum:

	private Set<IEnum> getEnums() {
		final Map<String, Object> enumMap = context.getBeansWithAnnotation(EnumAnnotation.class);
		LOG.debug("enumMap.size: {}", enumMap.size());

		Set<IEnum> result = new HashSet<IEnum>();
		for (Object o : enumMap.values()) {

			if (o.getClass().isArray()) {
				final IEnum[] o1 = (IEnum[]) o;
				Collections.addAll(result, o1);

			} else {
				result.add((IEnum) o);

			}
		}

		LOG.debug("result: {}", result);
		return result;
	}
тест на внимательность

Внимательный читатель обратит внимание, что я добавляю не только список значений, но и простые объекты в список — это сделано по причине того, что таким образом можно заанотировать не только enum-типы, но и любой класс для данного интерфейса.

Disclamer: проверку на то, что возвращённые бины точно реализуют интерфейс IEnum я описывать в данном примере не стал.

Добавляем метод "isAutowiredEnumSetField", проверяющий, что поле бина ожидает инъекции:

	@SuppressWarnings("unchecked")
	private boolean isAutowiredEnumSetField(Field field) {
		if (!AnnotatedElementUtils.isAnnotated(field, Autowired.class.getName())) {
			return false;
		}

		final Class<?> fieldType = field.getType();

		if (!Set.class.isAssignableFrom(fieldType)) {
			return false;
		}

		final ParameterizedType type = (ParameterizedType) field.getGenericType();
		final Type[] typeArguments = type.getActualTypeArguments();

		final Class<? extends Type> aClass = (Class<? extends Type>) typeArguments[0];

		return aClass.isAssignableFrom(IEnum.class);
	}

Ну и, наконец, пробегаемся по полям бина и делаем инъекцию:

	@Override
	public Object postProcessBeforeInitialization(final Object bean, final String beanName) throws BeansException {
		LOG.debug("bean: {}", bean);
		LOG.debug("beanName: {}", beanName);

		final Set<IEnum> enums = getEnums();
		if (enums.size() < 1) {
			return bean;
		}

		final Class<?> beanClass = bean.getClass();
		final Field[] fields = beanClass.getDeclaredFields();

		for (Field field : fields) {

			if (isAutowiredEnumSetField(field)) {
				LOG.trace("field inject values.");
				field.setAccessible(true);
				ReflectionUtils.setField(field, bean, enums);
			}
		}

		return bean;
	}

Запускаем тест и проверяем результат нашей работы.
(коммит)

Автор: Borz

Источник


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js