web-dev-qa-db-ja.com

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

namefinalであることに注意してください。コンストラクタを介してのみ指定でき、不変性を保証します。その他の依存関係は、Thingクラスの実装固有の依存関係であり、要求ハンドラーの実装に認識(密結合)しないでください。

このコードは、Spring XML configで完璧に機能します。たとえば:

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

Java configで同じことを実現するにはどうすればよいですか?以下は、Spring 3.xを使用すると機能しません。

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

今、私はcould Factoryを作成します、例えば:

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

しかし、それはSpringを使用してServiceLocatorとFactoryの設計パターンを置き換えるという点全体を無効にしますです。これはこのユースケースに理想的です。

Spring Java Configがこれを実行できれば、次のことを回避できます。

  • factoryインターフェースの定義
  • ファクトリー実装の定義
  • factory実装のテストを作成する

これは、SpringがすでにXML configを介してサポートしている非常に些細な作業のための(比較的言えば)膨大な作業です。

113
Les Hazlewood

@Configurationクラスでは、そのような@Beanメソッド

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

bean定義を登録し、beanを作成するためのファクトリを提供するために使用されます。定義されているBeanは、直接またはApplicationContextをスキャンして決定された引数を使用して、要求時にのみインスタンス化されます。

prototype Beanの場合、毎回新しいオブジェクトが作成されるため、対応する@Beanメソッドも実行されます。

ApplicationContextから BeanFactory#getBean(String name, Object... args) メソッドを通じてBeanを取得できます。

明示的なコンストラクター引数/ファクトリーメソッド引数を指定して、Bean定義の指定されたデフォルト引数(存在する場合)をオーバーライドできます。

パラメーター:

args静的ファクトリメソッドへの明示的な引数を使用してプロトタイプを作成する場合に使用する引数。その他の場合にnull以外のargs値を使用することは無効です。

つまり、このprototypeスコープBeanには、Beanクラスのコンストラクターではなく、@Beanメソッドの呼び出しで使用される引数を提供します。

これは、少なくともSpringバージョン4+には当てはまります。

83

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のような特定のクラスを記述する時間を節約できます(ただし、3つ以上のパラメーターを渡すためにカスタム@FunctionalInterfaceを記述する必要があります)。

37
Roman Golyshev

コメントごとに更新

まず、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

したがって、「シングルトン」Beanは2回要求されます。ただし、予想どおり、Springは一度だけ作成します。 2回目は、そのBeanがあることを確認し、既存のオブジェクトを返します。コンストラクター(@Beanメソッド)は2回呼び出されません。これとは異なり、「Prototype」Beanが同じコンテキストオブジェクトから2回要求されると、参照が出力で変更され、コンストラクター(@Beanメソッド)ISが2回呼び出されることがわかります。

したがって、問題は、シングルトンをプロトタイプにどのように注入するかです。上記の構成クラスは、その方法も示しています!そのような参照はすべてコンストラクターに渡す必要があります。これにより、作成されたクラスが純粋な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;
14
JoeG

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文字列が出力されます。

6
David Barda

内部クラス を使用するだけで、同様の効果を実現できます。

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