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


134

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 구성을 통해 지원하는 매우 사소한 작업에 대해서는 많은 작업 (상대적으로 말하면)입니다.


15
훌륭한 질문입니다.
Sotirios Delimanolis 2014 년

그러나 클래스를 직접 인스턴스화 할 수 없으며 Spring에서 가져와야하는 이유가 있습니까? 다른 콩에 의존성이 있습니까?
Sotirios Delimanolis 2014 년

@SotiriosDelimanolis 그렇습니다. Thing구현은 실제로 더 복잡하고 다른 bean에 의존성을 가지고 있습니다 (간단히 생략했습니다). 따라서 요청 처리기 구현에서 필요하지 않은 API / Bean에 처리기를 밀접하게 연결하기 때문에 Request 처리기 구현에 대해 알고 싶지 않습니다. 귀하의 (우수한) 질문을 반영하여 질문을 업데이트하겠습니다.
Les Hazlewood

Spring이 생성자에서 이것을 허용하는지 확실하지 않지만 setter 자체로 @Qualifier매개 변수를 setter에 넣을 수 있다는 것을 알고 있습니다 @Autowired.
CodeChimp

2
Spring 4에서는 예제가 @Bean작동합니다. 이 @Bean메소드는 전달한 적절한 인수와 함께 호출됩니다 getBean(..).
Sotirios Delimanolis 2014 년

답변:


94

@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 이상에서는 사실입니다.


12
이 접근법의 내 문제는 @Bean메소드를 수동 호출로 제한 할 수 없다는 것 입니다. 당신이 경우 지금까지 방법은 가능성이 매개 변수를 삽입 할 수없는 존재에 죽어 호출됩니다. 당신도 마찬가지 입니다. 나는 이것이 약간 깨지기 쉽다는 것을 알았다. @Autowire Thing@Bean@Autowire List<Thing>
Jan Zyka

@ JanZyka,이 답변에 설명 된 것 이외의 것을 자동으로 연결할 수있는 방법이 있습니까? 더 구체적으로 말하면, (컴파일 / 구성 시간에) 인수를 미리 알고 있다면, 이러한 인수를 한정 할 수있는 주석으로 표현할 수있는 방법 @Autowire이 있습니까?
M. Prokhorov 2016 년

52

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두 개 이상의 매개 변수를 전달하려면 사용자 정의 를 작성해야 함 ).


1
이 접근법은 매우 유용하고 깨끗합니다. 감사!
Alex Objelean

1
직물이란? 나는 당신의 사용을 이해 ...하지만 용어는 .. 내가 "직물 패턴"들어 본 적이 생각하지 않는다
AnthonyJClink

1
@AnthonyJClink 방금 fabric대신에 factory내 나쁜 대신 사용했다고 생각합니다 :)
Roman Golyshev

1
@AbhijitSarkar 아, 알겠습니다. 그러나 매개 변수를 a Provider또는 a에 전달할 수 없거나 ObjectFactory잘못 되었습니까? 그리고 나의 예에서는 문자열 매개 변수를 전달할 수 있습니다.
Roman Golyshev

2
당신이 (프로토 타입 콩 다릅니다 ...) 사용 스프링 빈의 라이프 사이클 방법에 원하지 않는 (또는 필요하지 않는) 경우에 당신은 건너 뛸 수 있습니다 @BeanScope오버 주석 Thing thing방법. 또한이 방법은 자신을 숨기고 공장 만 남겨두기 위해 비공개로 만들 수 있습니다.
m52509791

17

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 문자열을 인쇄합니다.


15

댓글 당 업데이트

첫째, 왜 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;

3
답장을 보내 주셔서 감사합니다! 그러나 나는 당신이 그 질문을 오해했다고 생각합니다. 문제의 가장 중요한 부분은 프로토 타입을 획득 (인스턴스화) 할 때 런타임 값을 생성자 인수로 제공해야한다는 것입니다.
Les Hazlewood

답장을 업데이트했습니다. 실제로 런타임 값 처리가 올바르게 수행 된 것처럼 보이므로 해당 부분을 제외했습니다. 업데이트 및 프로그램 출력에서 ​​볼 수 있듯이 명시 적으로 지원됩니다.
JoeG

0

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

@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);
        }
    }
}


-1

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

그렇습니다 @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
    // ...    
}

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


1
답장을 보내 주셔서 감사합니다. 그러나 이것은 스프링의 안티 패턴입니다. 구성 객체는 애플리케이션 코드에 '누설'되어서는 안됩니다. 애플리케이션 객체 그래프를 구성하고 Spring 구문과의 인터페이스를 구성하기 위해 존재합니다. 이것은 애플리케이션 빈의 XML 클래스와 유사합니다 (예 : 다른 구성 메커니즘). 즉, Spring이 다른 구성 메커니즘과 함께 제공되는 경우 응용 프로그램 코드를 리팩터링해야합니다. 이는 우려의 분리를 위반하는 명확한 지표입니다. Config가 Factory / Function 인터페이스의 인스턴스를 생성하고 Factory를 주입하는 것이 낫습니다.
Les Hazlewood

1) 나는 일반적으로 구성 객체가 필드로 누출되지 않아야한다는 것에 전적으로 동의합니다. 그러나이 특정한 경우 프로토 타입 Bean을 생성하기 위해 하나의 Bean 만 정의하는 구성 오브젝트를 주입하면 IHMO는 완전히 의미가 있습니다.이 구성 클래스는 팩토리가됩니다. 그것만으로 문제의 분리 문제는 어디에 있습니까? ...
davidxxx

... 2) "즉, Spring이 다른 구성 메커니즘과 함께 제공되는 경우"는 응용 프로그램에서 프레임 워크를 사용하기로 결정할 때 응용 프로그램과 그 구성 요소를 연결하기 때문에 잘못된 주장입니다. 따라서 어떤 경우에도 @Configuration해당 메커니즘이 변경되었는지에 의존하는 Spring 애플리케이션을 리팩터링해야합니다 .
davidxxx

1
... 3) 귀하가 수락 한 답변은 사용을 제안합니다 BeanFactory#getBean(). 그러나 현재 빈이 필요한 것뿐만 아니라 응용 프로그램의 Bean을 가져 오거나 인스턴스화 할 수있는 팩토리이기 때문에 커플 링 측면에서 훨씬 나쁩니다. 이러한 사용법을 사용하면 클래스의 책임을 매우 쉽게 혼합 할 수 있습니다. 종속성은 무제한이므로 실제로 권장하지 않지만 예외적입니다.
davidxxx

@ davidxxx-JDK 8과 Spring 4가 사실상 사라지기 전에 몇 년 전에 답변을 수락했습니다. 위의 로마의 대답은 현대의 봄 사용법에 더 정확합니다. "응용 프로그램에서 프레임 워크를 사용하기로 결정할 때 응용 프로그램과 프레임 워크를 연결하기 때문에"라는 진술과 관련하여 Spring 팀의 권장 사항과 Java Config 모범 사례와 상당히 유사합니다. Josh Long 또는 Jeurgen Hoeller에게 문의하십시오. 그들과 직접 대화 할 수있는 기회 (필자가 있을 때마다 앱 코드를 Spring 에 연결 하지 말 것을 명시 적으로 보증 할 수 있습니다). 건배.
Les Hazlewood 21
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.