Создание бинов с помощью фабричных методов

В этой статье мы расскажем, как в Spring сделать бинами те объекты, которые создаются с помощью фабричных методов — статических и нестатических.
Предположим, у нас уже есть класс с фабричным методом, и нам надо, чтобы он инициализировался контейнером Spring, иначе говоря, стал бином. В этой статье мы опишем, как это сделать, не разрушая дизайн класса.

Статический фабричный метод

Допустим, у нас есть простая фабрика животных, класс Animal с фабричным методом getAnimal(), возвращающим определенный подкласс животного в зависимости от аргумента (Dog или Duck):

public class Animal {

    public static Animal getAnimal(String animalType) {
        if ("Dog".equalsIgnoreCase(animalType)) {
            return new Dog();
        } else if ("Duck".equalsIgnoreCase(animalType)) {
            return new Duck();
        }
        return null;
    }
}

Класс Animal уже есть как данность. Предположим, что он из чужой библиотеки, в которой мы не можем задать ни аннотации, ни конструктор.  Классы Dog и Duck тоже оттуда:

public class Dog extends Animal {

}
public class Duck extends Animal {

}

Конфигурация XML

Сконфигурируем бин dogstatic так, чтобы он создавался с помощью фабричного метода getAnimal():

<bean id="dogstatic" scope = "singleton" class="ru.javalang.factorymethod.Animal"
    factory-method="getAnimal">
    <constructor-arg index="0" value="dog" />
</bean>

Здесь class – класс фабрики, содержащей метод (а не класс не возвращаемого фабрикой объекта!), просто в нашем случае они совпадают.
factory-method – имя фабричного метода в этом классе.
Если у фабричного метода есть аргументы, то они задаются в constructor-arg.
Атрибут scope можно было не задавать, так как он по умолчанию singleton. Он задан исключительно для наглядности, поскольку ниже будет прототипный бин.
Сконфигурировав XML таким образом, мы создали бин dogstatic, который инициализируется контейнером Spring. И теперь можем как внедрить этот бин в другие бины, так и просто получить его из контекста приложения:

    @Before
    public void init() {
        xmlConfigContext = new ClassPathXmlApplicationContext("factorymethod.xml");
    }

    @Test
    public void whenStaticMethodCalled_ThenObjectExist() {
        assertTrue(xmlConfigContext.getBean("dogstatic") instanceof Dog);
        assertTrue(xmlConfigContext.getBean("duckstatic") instanceof Duck);
    }

Давайте проверим, что бин действительно синглтон, то есть сколько бы раз мы его ни брали, экземпляр будет один и тот же:

@Test
 public void givenSingletonConfig_WhenStaticMethodCalled_ThenObjectsEqual() {
     Object dog1 = xmlConfigContext.getBean("dogstatic");
     Object dog2 = xmlConfigContext.getBean("dogstatic");
     assertEquals(dog1, dog2);
 }

Обратите внимение, что аргумент фабричного метода  animalType заранее прописан в XML: в теге constructor-arg, но если бы мы хотели его перезаписать, то передали бы вторым параметром метода getBean().

А теперь создадим прототипный бин типа Duck:

<bean id="duckstatic"  scope = "prototype" class="ru.javalang.factorymethod.Animal"
    factory-method="getAnimal">
    <constructor-arg index="0" value="duck" />
</bean>

Здесь обязательно указать атрибут scope = «prototype», так как по умолчанию бин не будет прототипным.

Убедимся, что контейнер создает разные экземпляры уток при получении бина:

@Test
public void givenPrototypeConfig_WhenStaticMethodCalled_ThenObjectsDifferent() {

    Object duck1 = xmlConfigContext.getBean("duckstatic");
    Object duck2 = xmlConfigContext.getBean("duckstatic");
    assertNotEquals(duck1, duck2);
}

Конфигурация с помощью аннотаций

А теперь то же самое сконфигурируем с помощью аннотаций.

Для этого создадим класс Config и аннотируем его с помощью @Configuration:

@Configuration
public class Config {

}

Напишем методы, возвращающие бины dogstatic и duckstatic:

@Configuration
public class Config {

    @Bean(name = "dogstatic")
    public Animal createStatDog() {
        return Animal.getAnimal("dog");
    }

    @Bean(name = "duckstatic")
    @Scope("prototype")
    public Animal createStatDuck() {
        return Animal.getAnimal("duck");
    }
}

Методы аннотированы с помощью @Bean, в скобках указывается имя возвращаемого бина: dogstatic и duckstatic.

Для duckstatic указан прототипный жизненный цикл.

Чтобы протестировать, что объекты создаются, и создаются в нужном количестве экземпляров, инициализируем контейнер. Поскольку у нас конфигурация, заданная аннотациями, контейнер создаем с помощью класса AnnotationConfigApplicationContext:

@Before
public void init() {
    javaConfigApplicationContext = 
        new AnnotationConfigApplicationContext(Config.class);
}

И тестируем наличие бинов аналогично тому, как это делали раньше:

@Test
public void whenStaticMethodCalled_ThenObjectExist() {
    assertTrue(javaConfigApplicationContext.getBean("dogstatic") instanceof Dog);
    assertTrue(javaConfigApplicationContext.getBean("duckstatic") instanceof Duck);
}

@Test
public void givenSingletonConfig_WhenStaticMethodCalled_ThenObjectsEqual() {
    Object dog1 = javaConfigApplicationContext.getBean("dogstatic");
    Object dog2 = javaConfigApplicationContext.getBean("dogstatic");

    assertEquals(dog1, dog2);
}

@Test
public void givenPrototypeConfig_WhenStaticMethodCalled_ThenObjectsDifferent() {

    Object duck1 = javaConfigApplicationContext.getBean("duckstatic");
    Object duck2 = javaConfigApplicationContext.getBean("duckstatic");
    assertNotEquals(duck1, duck2);
}

Нестатический фабричный метод

Теперь рассмотрим, как сконфигурировать бин Animal, если он создается нестатическим фабричным методом, например так:

public class AnimalFactory {
 
    public Animal getAnimal(String animalType) {
        if ("Dog".equalsIgnoreCase(animalType)) {
            return new Dog();
        } else if ("Duck".equalsIgnoreCase(animalType)) {
            return new Duck();
        }
        return null;
    }
}

Понятно, что раз метод нестатический, то без экземпляра класса, его вызывающего (то есть AnimalFactory), контейнеру  не обойтись.

Конфигурация XML

Так что для начала зададим бин animalFactory:

<bean id="animalFactory" class="ru.javalang.factorymethod.AnimalFactory" />

Теперь можно конфигурировать бины типа Animal. Они конфигурируются почти так же, как в случае статического метода, но только вместо class надо задать  factory-bean – имя бина, содержащего этот нестатический метод. То есть animalFactory:

<bean id="doginstance" scope="singleton" factory-bean="animalFactory" factory-method="getAnimal">
    <constructor-arg index="0" value="duck" />
</bean>

Создается бин-синглтон. И создадим бин duckinstance, он будет прототипный:

<bean id="duckinstance" scope="prototype" factory-bean="animalFactory"
    factory-method="getAnimal">
    <constructor-arg index="0" value="duck" />
</bean>

Давайте проверим, что бины создаются:

@Test
public void whenInstanceMethodCalled_ThenObjectExist() {
    assertTrue(xmlConfigContext.getBean("doginstance") instanceof Dog);
    assertTrue(xmlConfigContext.getBean("duckinstance") instanceof Duck);

}

И убедимся, что синглтон создается в одном экземпляре:

@Test
public void givenSingletonConfig_WhenInstanceMethodCalled_ThenObjectsEqual() {
    Object dog1 = xmlConfigContext.getBean("doginstance");
    Object dog2 = xmlConfigContext.getBean("doginstance");
    assertEquals(dog1, dog2);
}

Также убедимся, что прототипный бин создается каждый раз в новом экземпляре:

@Test
public void givenPrototypeConfig_WhenInstanceMethodCalled_ThenObjectsDifferent() {

    Object duck1 = xmlConfigContext.getBean("duckinstance");
    Object duck2 = xmlConfigContext.getBean("duckinstance");
    assertNotEquals(duck1, duck2);
}

Конфигурация с помощью аннотаций

Теперь сконфигурируем бины с помощью аннотаций.

Для этого добавим в класс Config два метода:

@Bean(name = "doginstance")
public Animal createInstDog() {
    return new AnimalFactory().getAnimal("dog");
}

@Bean(name = "duckinstance")
@Scope("prototype")
public Animal createInstDuck() {
    return new AnimalFactory().getAnimal("duck");
}

Протестируем, что бины создаются:

@Test
public void whenInstanceMethodCalled_ThenObjectExist() {
    assertTrue(javaConfigApplicationContext.getBean("doginstance") instanceof Dog);
    assertTrue(javaConfigApplicationContext.getBean("duckinstance") instanceof Duck);

}

@Test
public void givenSingletonConfig_WhenInstanceMethodCalled_ThenObjectsEqual() {
    Object dog1 = javaConfigApplicationContext.getBean("doginstance");
    Object dog2 = javaConfigApplicationContext.getBean("doginstance");
    assertEquals(dog1, dog2);
}

@Test
public void givenPrototypeConfig_WhenInstanceMethodCalled_ThenObjectsDifferent() {

    Object duck1 = javaConfigApplicationContext.getBean("duckinstance");
    Object duck2 = javaConfigApplicationContext.getBean("duckinstance");
    assertNotEquals(duck1, duck2);
}

Заключение

В этой статье мы рассмотрели, как с помощью XML либо аннотаций сконфигурировать объекты, создаваемые статическими и нестатическими фабричными методами и сделать их бинами в контейнере Spring.

Мы сконфигурировали собаку как бин-синглтон, а утку как прототипный бин, но, разумеется, могло быть и наоборот. Мы это сделали исключительно в демонстрационных целях, чтобы задействовать оба scope в примере.

Пример доступен на GitHub.

Создание бинов с помощью фабричных методов: 3 комментария

  1. Только мне кажется, что Автор разъясняет гораздо лучше, чем J.Cosmina, R. Harrop, C.Schaefer, C.Hо в книге «Spring 5» ?

  2. В случае с нестатичным фабричным методом подход как мне кажется не очень хороший. В методах создания бина каждый раз создается экземпляр фабрики, которая может быть достаточно тяжелой и ее создание может занимать большое количество времени. Не лучше ли создать бин (singltone) этой фабрики и ее инжектить в метод создания бина, внутри которого вызывать уже метод создания животного?

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *