[java] Spring Java Config : 런타임 인수로 프로토 타입 범위의 @Bean을 어떻게 작성합니까?

Spring의 Java Config를 사용하면 런타임에서만 얻을 수있는 생성자 인수가있는 프로토 타입 범위의 Bean을 획득 / 인스턴스화해야합니다. 다음 코드 예제 (간단하게 단순화)를 고려하십시오.

@Autowired
private ApplicationContext appCtx;

public void onRequest(Request request) {
    //request is already validated
    String name = request.getParameter("name");
    Thing thing = appCtx.getBean(Thing.class, name);

    //System.out.println(thing.getName()); //prints name
}

여기서 Thing 클래스는 다음과 같이 정의됩니다.

public class Thing {

    private final String name;

    @Autowired
    private SomeComponent someComponent;

    @Autowired
    private AnotherComponent anotherComponent;

    public Thing(String name) {
        this.name = name;
    }

    public String getName() {
        return this.name;
    }
}

공지 사항 name입니다 final그것이 단지 생성자를 통해 제공 할 수 있으며, 불변성을 보장한다. 다른 종속성은 Thing클래스 의 구현 별 종속성이며 요청 처리기 구현에 알려지지 않아야합니다.

이 코드는 Spring XML 설정과 완벽하게 호환됩니다.

<bean id="thing", class="com.whatever.Thing" scope="prototype">
    <!-- other post-instantiation properties omitted -->
</bean>

Java 구성으로 동일한 것을 어떻게 달성합니까? 다음은 Spring 3.x를 사용하여 작동하지 않습니다.

@Bean
@Scope("prototype")
public Thing thing(String name) {
    return new Thing(name);
}

이제 팩토리를 만들 있습니다. 예 :

public interface ThingFactory {
    public Thing createThing(String name);
}

그러나 이는 ServiceLocator 및 Factory 디자인 패턴을 대체하기 위해 Spring을 사용하는 전체 요점을 무효화 합니다.이 사용 사례에 이상적입니다.

Spring Java Config가 이것을 할 수 있다면, 나는 피할 수 있습니다 :

  • 팩토리 인터페이스 정의
  • 팩토리 구현의 정의
  • 팩토리 구현을위한 테스트 작성

Spring이 이미 XML 구성을 통해 지원하는 매우 사소한 작업에 대해서는 많은 작업 (상대적으로 말하면)입니다.



답변

@Configuration클래스에서 이와 @Bean같은 방법

@Bean
@Scope("prototype")
public Thing thing(String name) {
    return new Thing(name);
}

Bean 정의 를 등록하고 Bean 작성을위한 팩토리를 제공 하는 데 사용됩니다 . 정의 된 Bean은 요청시 직접 스캔하거나 스캔을 통해 결정된 인수를 사용하여 인스턴스화됩니다 ApplicationContext.

prototypeBean 의 경우 매번 새 오브젝트가 작성되므로 해당 @Bean메소드도 실행됩니다.

메소드를 ApplicationContext통해 Bean을 검색 할 수 있습니다.BeanFactory#getBean(String name, Object... args)

Bean 정의에서 지정된 기본 인수 (있는 경우)를 대체하여 명시적인 생성자 인수 / 팩토리 메소드 인수를 지정할 수 있습니다.

매개 변수 :

인수의 인수는 정적 팩토리 메소드에 명시 적으로 인수를 사용하여 프로토 타입을 만드는 경우에 사용합니다. 다른 경우 널이 아닌 인수 값을 사용하는 것은 유효하지 않습니다.

즉,이 prototype범위가 지정된 Bean의 경우 Bean 클래스의 생성자가 아니라 @Bean메소드 호출 에서 사용될 인수를 제공합니다 .

이것은 스프링 버전 4 이상에서는 사실입니다.


답변

Spring> 4.0 및 Java 8을 사용하면 더 안전하게 유형을 지정할 수 있습니다.

@Configuration
public class ServiceConfig {

    @Bean
    public Function<String, Thing> thingFactory() {
        return name -> thing(name); // or this::thing
    }

    @Bean
    @Scope(value = "prototype")
    public Thing thing(String name) {
       return new Thing(name);
    }

}

용법:

@Autowired
private Function<String, Thing> thingFactory;

public void onRequest(Request request) {
    //request is already validated
    String name = request.getParameter("name");
    Thing thing = thingFactory.apply(name);

    // ...
}

이제 런타임에 Bean을 얻을 수 있습니다. 이것은 팩토리 패턴이지만 특정 클래스를 작성하는 데 시간을 절약 할 수 있습니다 ThingFactory(그러나 @FunctionalInterface두 개 이상의 매개 변수를 전달하려면 사용자 정의 를 작성해야 함 ).


답변

Spring 4.3 이후로, 그것을 할 수있는 새로운 방법이 있습니다.

ObjectProvider- “인수 된”프로토 타입 범위의 Bean에 대한 종속성으로 추가하고 인수를 사용하여 인스턴스화 할 수 있습니다.

사용 방법에 대한 간단한 예는 다음과 같습니다.

@Configuration
public class MyConf {
    @Bean
    @Scope(BeanDefinition.SCOPE_PROTOTYPE)
    public MyPrototype createPrototype(String arg) {
        return new MyPrototype(arg);
    }
}

public class MyPrototype {
    private String arg;

    public MyPrototype(String arg) {
        this.arg = arg;
    }

    public void action() {
        System.out.println(arg);
    }
}


@Component
public class UsingMyPrototype {
    private ObjectProvider<MyPrototype> myPrototypeProvider;

    @Autowired
    public UsingMyPrototype(ObjectProvider<MyPrototype> myPrototypeProvider) {
        this.myPrototypeProvider = myPrototypeProvider;
    }

    public void usePrototype() {
        final MyPrototype myPrototype = myPrototypeProvider.getObject("hello");
        myPrototype.action();
    }
}

이것은 물론 usePrototype을 호출 할 때 hello 문자열을 인쇄합니다.


답변

댓글 당 업데이트

첫째, 왜 Spring 3.x에서 잘 작동하는 것에 대해 “이것이 작동하지 않는다”고 말했는지 잘 모르겠습니다. 어딘가에 구성에 문제가 있다고 생각합니다.

이것은 작동합니다 :

-구성 파일 :

@Configuration
public class ServiceConfig {
    // only here to demo execution order
    private int count = 1;

    @Bean
    @Scope(value = "prototype")
    public TransferService myFirstService(String param) {
       System.out.println("value of count:" + count++);
       return new TransferServiceImpl(aSingletonBean(), param);
    }

    @Bean
    public AccountRepository aSingletonBean() {
        System.out.println("value of count:" + count++);
        return new InMemoryAccountRepository();
    }
}

-테스트 파일 :

@Test
public void prototypeTest() {
    // create the spring container using the ServiceConfig @Configuration class
    ApplicationContext ctx = new AnnotationConfigApplicationContext(ServiceConfig.class);
    Object singleton = ctx.getBean("aSingletonBean");
    System.out.println(singleton.toString());
    singleton = ctx.getBean("aSingletonBean");
    System.out.println(singleton.toString());
    TransferService transferService = ctx.getBean("myFirstService", "simulated Dynamic Parameter One");
    System.out.println(transferService.toString());
    transferService = ctx.getBean("myFirstService", "simulated Dynamic Parameter Two");
    System.out.println(transferService.toString());
}

Spring 3.2.8 및 Java 7을 사용하면 다음과 같은 출력이 제공됩니다.

value of count:1
com.spring3demo.account.repository.InMemoryAccountRepository@4da8692d
com.spring3demo.account.repository.InMemoryAccountRepository@4da8692d
value of count:2
Using name value of: simulated Dynamic Parameter One
com.spring3demo.account.service.TransferServiceImpl@634d6f2c
value of count:3
Using name value of: simulated Dynamic Parameter Two
com.spring3demo.account.service.TransferServiceImpl@70bde4a2

따라서 ‘Singleton’Bean은 두 번 요청됩니다. 그러나 예상대로 Spring은 한 번만 만듭니다. 두 번째로 해당 Bean이 있고 기존 오브젝트 만 리턴 함을 알게됩니다. 생성자 (@Bean 메소드)는 두 번째로 호출되지 않습니다. 이와 관련하여 ‘Prototype’Bean이 동일한 컨텍스트 오브젝트에서 두 번 요청되면 출력에서 ​​참조가 변경되고 생성자 (@Bean 메소드)가 두 번 호출 된 것을 볼 수 있습니다.

그렇다면 질문은 단일 톤을 프로토 타입에 주입하는 방법입니다. 위의 구성 클래스는 그 방법도 보여줍니다! 이러한 모든 참조를 생성자에 전달해야합니다. 이렇게하면 생성 된 클래스가 순수 POJO가되며 포함 된 참조 객체를 변경할 수 없게됩니다. 전송 서비스는 다음과 같습니다.

public class TransferServiceImpl implements TransferService {

    private final String name;

    private final AccountRepository accountRepository;

    public TransferServiceImpl(AccountRepository accountRepository, String name) {
        this.name = name;
        // system out here is only because this is a dumb test usage
        System.out.println("Using name value of: " + this.name);

        this.accountRepository = accountRepository;
    }
    ....
}

단위 테스트를 작성하면 @Autowired없이 클래스를 만들게되어 매우 기쁠 것입니다. 자동 유선 구성 요소가 필요한 경우 해당 구성 요소를 Java 구성 파일에 로컬로 유지하십시오.

BeanFactory에서 아래 메소드를 호출합니다. 설명에서 이것이 정확한 사용 사례를위한 방법에 유의하십시오.

/**
 * Return an instance, which may be shared or independent, of the specified bean.
 * <p>Allows for specifying explicit constructor arguments / factory method arguments,
 * overriding the specified default arguments (if any) in the bean definition.
 * @param name the name of the bean to retrieve
 * @param args arguments to use if creating a prototype using explicit arguments to a
 * static factory method. It is invalid to use a non-null args value in any other case.
 * @return an instance of the bean
 * @throws NoSuchBeanDefinitionException if there is no such bean definition
 * @throws BeanDefinitionStoreException if arguments have been given but
 * the affected bean isn't a prototype
 * @throws BeansException if the bean could not be created
 * @since 2.5
 */
Object getBean(String name, Object... args) throws BeansException;


답변

내부 클래스 를 사용하여 비슷한 효과를 얻을 수 있습니다 .

@Component
class ThingFactory {
    private final SomeBean someBean;

    ThingFactory(SomeBean someBean) {
        this.someBean = someBean;
    }

    Thing getInstance(String name) {
        return new Thing(name);
    }

    class Thing {
        private final String name;

        Thing(String name) {
            this.name = name;
        }

        void foo() {
            System.out.format("My name is %s and I can " +
                    "access bean from outer class %s", name, someBean);
        }
    }
}


답변

bean xml 파일에서 scope = “prototype” 속성을 사용하십시오.


답변

약간 다른 접근법으로 늦은 답변. 이것이이 질문 자체를 나타내는 최근 질문 의 후속 조치입니다 .

그렇습니다 @Configuration. 각 주입에서 새 Bean을 작성할 수 있는 클래스 의 매개 변수를 허용하는 프로토 타입 Bean을 선언 할 수 있습니다 .
이것은이 @Configuration 클래스를 팩토리로 만들고이 팩토리에 너무 많은 책임을주지 않기 위해 다른 빈은 포함하지 않아야합니다.

@Configuration
public class ServiceFactory {

    @Bean
    @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
    public Thing thing(String name) {
       return new Thing(name);
   }

}

그러나 해당 구성 Bean을 삽입하여 Things 를 작성할 수도 있습니다 .

@Autowired
private ServiceFactory serviceFactory;

public void onRequest(Request request) {
    //request is already validated
    String name = request.getParameter("name");
    Thing thing = serviceFactory.thing(name); // create a new bean at each invocation
    // ...    
}

형식이 안전하고 간결합니다.