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