15 июля 2014 в 23:00

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

Как известно, в 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:
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;
	}


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

UPD: Добавил коммит с примером инъекции пары enum и одного класса.
Виктор Аленьков @Borz
карма
22,7
рейтинг 7,3
Java Senior Developer
Похожие публикации
Самое читаемое Разработка

Комментарии (21)

  • 0
    Не знаю, какая цель в итоге стояла. Но думаю можно было обойтить простым util:constant
    • 0
      не совсем. util:constant позволяет присвоить константы (это может и Value), но он не может
      1) присвоить микшированное значение только по интерфейсу, т.е. собрать все бины, которые реализуют некий интерфейс и их присвоить в поле

      2) динамически подстраиваться под текущее окружение.
      Сегодня у меня один ENUM, завтра три, послезавтра один ENUM «удалили» из сборки
      А если в процессе разработки в ENUM добавили новое значение, то вам придётся пробежаться по всем бинам, к которым вы применили util:constant и добавить туда новое значение (насколько мне известно, util:constant не позволяет работать с масками типа "*")
      • 0
        я немного про другое. зачем именно enum? чем не подходит обычный класс, если вы все равно используете интерфейс?
        • 0
          Я исхожу из простого — если есть константы, влияющие или используемые в логике приложения, то их надо делать как enum.
          • 0
            у вас используется интерфейс. псевдо константы, которые вы постоянно меняете/удаляете/добавляется можно заменить на обычные бины (по одному на каждое значение енума) и забыть про весь этот код как страшный сон. @Autowired для массива интерфейсов прекрасно работает.

            каждый изгаляется как хочет, конечно. странно что никто из коллег не предложил другого варианта.
            • 0
              да, вы правы — каждый изгаляется как хочет.
  • +1
    Казалось бы, enum инжектить в виде бина, а столько геммороя, да только нет ясности — как, зачем и при каких обстоятельствах это может понадобиться?
    • 0
      Поддержу вопрос. Зачем инжектить enum?
      • –1
        ну, например я в enum храню роли, которые собираются в группы. роли на проекте фиксированные всегда и почему бы их как константы не оформить? При этом, на каждый модуль удобнее сделать свой enum, дабы не смешивать роли.
        ЗЫ: думал это в одном из следующих постов написать более детально
        • 0
          Что-то я пока не вижу логики. Зачем он в Spring контексте, когда он уже есть в JVM?
        • 0
          Точно не поменяются? Я бы на вашем месте не изгалялся с энамом. Сдали бы какойнить RolesManager и каждый модуль через имплементацию RolesProvider регал бы свои роли в системе. Можно было бы еще и префиксы по имени модуля делать и дать, что бы избегать коллизий, централизованные настройки доступа. Но это так имхо. Никогда бы не додумался инжэктить энам для таких целей(хоть и в C#, но все же:))
          • 0
            если вы обратите внимание, то в конечном итоге enum-ов «нет» при инжекте — они нужны только на этапе поднятия контекста и на этапе написания кода. И да, где-то рядом есть некий RolesManager, который с этим всем работает.

            но это всё отходит от данной темы факультатива
            • +1
              гыгы, если енамов в итоге нет, а получается Set, то получается статья на тему «Как заинжектить Set, который изначально описан как enum?».

              «Ну да лиха беда начало», сказал Иван-дурак, да пошёл копать дальше. (с) из статьи
              • 0
                Каждый видит то, что хочет видеть — Инструмент всегда в твоих руках.
                • 0
                  Абсолютно согласен. Только вот стоит ли описывать геморройный способ выстрелить себе в ногу на java о том как Сет описанный как Енам может быть заинжектенным с кастомными обработками, СупрессВариннгами и прочими прелестями… не понятно до конца. Вроде как опыт, а вроде как… не понятно вообщем.
                  • 0
                    На самом деле, главный вопрос в этом посте — какую проблему пытается решить автор,
                    • 0
                      Выдуманную?
                      • 0
                        уважаемые dougrinch и dikkini, именно для таких как вы, в названии присутствует слово «факультатив». Если вам это слово не знакомо, то прочтите его как «Just For Fun».
                        • 0
                          Первое предложение:
                          Как известно, в Spring нельзя сделать бины для перечисляемых типов без «костылей» — у этого типа «нет» конструктора.
                          и второе:
                          В данном посте я попробую обойти это ограничение.


                          Из чего следует однозначный вывод, что в посте Вы пытаетесь решить какую-то проблему. Только вот не понятно какую. Сразу вспомнилось: «Иван-дурак, он ведь дурак, он ведь не знает что они бессмертные… взял и поубивал всех.». Так вот: нет, мне не известно что нельзя. И одиночный экземпляр енума отлично получается сделать бином и инжектить, и все экземпляры запихать в сет.

                          Так что повторяю вопрос: какую проблему Вы хотели решить?
                          • 0
                            Знаете ли, я оставлю этот вопрос без ответа.
                    • 0
                      выражайтесь корректнее — это не «главный вопрос в посте», а главный вопрос у вас лично. В посте вопросов вообще не поднималось.

Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.