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 configで同じことをするにはどうすればよいですか?以下は、Spring 3.xを使用すると機能しません。

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

ここで、ファクトリを作成できます。例:

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

ただし、これは、Springを使用してServiceLocatorおよびFactory設計パターンを置き換えるという全体のポイント打ち負かします。これは、このユースケースに理想的です。

Spring Java Configがこれを行うことができれば、私は避けることができます:

  • ファクトリインターフェースの定義
  • ファクトリ実装の定義
  • ファクトリ実装のテストを書く

これは、Springが既にXML構成を介してサポートしているほど簡単なものの(比較的言えば)大量の作業です。


15
すばらしい質問です。
ソティリオスデリマノリス2014

ただし、クラスを自分でインスタンス化してSpringから取得する必要がある理由はありますか?他のBeanに依存していますか?
Sotirios Delimanolis 14

@SotiriosDelimanolisはい、Thing実装は実際にはより複雑であり、他のBeanに依存しています(簡潔にするために省略しました)。そのため、リクエストハンドラーの実装がそれらについて知る必要はありません。これにより、ハンドラーが必要のないAPI / Beanに密接に結合されるからです。(優れた)質問を反映するように質問を更新します。
Les Hazlewood 2014

Springがコンストラクターでこれを許可するかどうかはわかりませんが、セッター自体@Qualifierでパラメーターをセッターに設定できることはわかっています@Autowired
CodeChimp、2014

2
Spring 4では、例が@Bean機能します。@Beanこの方法は、あなたがに渡された適切な引数で呼び出されますgetBean(..)
Sotirios Delimanolis 14

回答:


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定義でオーバーライドできます。

パラメーター:

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

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

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


12
このアプローチの私の問題は、@Beanメソッドを手動での呼び出しに制限できないことです。あなた@Autowire Thing@Beanメソッドを呼び出すと、パラメータを注入できなくなる可能性があります。同じ場合@Autowire List<Thing>。これは少し壊れやすいことがわかりました。
Jan Zyka

@JanZyka、これらの回答で概説されている以外に、私がThingを自動配線できる方法はありますか(目を細めると、すべて同じです)。より具体的には、(コンパイル/構成時に)前もって引数を知っている場合、これらの引数を修飾できるアノテーションでこれらの引数を表現する方法はあります@Autowireか?
M.プロホロフ

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を作成して3つ以上のパラメーターを渡す必要があります)。


1
このアプローチは非常に便利でクリーンだと思います。ありがとう!
Alex Objelean

1
生地は?あなたの用法はわかりますが、用語はわかりません。「ファブリックパターン」について聞いたことがないと思います
AnthonyJClink

1
@AnthonyJClink私のfabric代わりに使用したと思いますfactory、私の悪い:)
Roman Golyshev

1
@AbhijitSarkarああ、なるほど。しかし、Providerまたはにパラメータを渡すことはできませんかObjectFactory、それとも間違っていますか?そして、私の例では、文字列パラメーター(または任意のパラメーター)を渡すことができます
Roman Golyshev

2
Spring Beanのライフサイクルメソッド(プロトタイプBeanとは異なります)を使用したくない(または使用する必要がない)場合は、メソッドをスキップ@BeanしてScope注釈を付けることができます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

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

3
返信いただきありがとうございます!しかし、あなたはその質問を誤解したと思います。質問の最も重要な部分は、プロトタイプを取得(インスタンス化)するときに、ランタイム値をコンストラクター引数として指定する必要があることです。
Les Hazlewood 2014年

返信を更新しました。実際、ランタイム値の処理は正しく行われたように見えたので、その部分は省略しました。プログラムからの更新と出力からわかるように、明示的にサポートされています。
JoeG、2014年

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 クラスをファクトリーにし、このファクトリーにあまり多くの責任を与えないようにするために、これには他のBeanを含めるべきではありません。

@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のアンチパターンです。構成オブジェクトは、アプリケーションコードに「リーク」しないでください。これらは、アプリケーションオブジェクトグラフを構成し、Springコンストラクトとインターフェースするために存在します。これは、アプリケーションBeanのXMLクラス(つまり、別の構成メカニズム)に似ています。つまり、Springに別の構成メカニズムが付属している場合は、アプリケーションコードをリファクタリングする必要があります。これは、懸念の分離に違反する明確な指標です。ConfigでFactory / Functionインターフェイスのインスタンスを作成し、Factoryをインジェクトすることをお勧めします-構成への密結合はありません。
Les Hazlewood

1)私は、一般的なケースでは、構成オブジェクトがフィールドとしてリークしないことに完全に同意します。しかし、この特定のケースでは、1つだけのBeanを定義する構成オブジェクトをインジェクトしてプロトタイプBeanを生成すると、IHMOは完全に理にかなっています。この構成クラスがファクトリーになります。それだけを行う場合、懸念の分離の問題はどこにありますか?...
davidxxx

... 2)「つまり、Springに別の構成メカニズムが付属している場合」については、アプリケーションでフレームワークを使用することを決定すると、アプリケーションとそれを結合するため、これは間違った引数です。したがって、いずれの場合でも、@Configurationそのメカニズムが変更された場合に依存するSpringアプリケーションをリファクタリングする必要があります。
davidxxx

1
... 3)あなたが受け入れた答えはを使用することを提案していますBeanFactory#getBean()。しかし、これは、現在のBeanが必要とするものだけでなく、アプリケーションのBeanを取得/インスタンス化できるファクトリであるため、カップリングに関してははるかに悪いです。そのような使用法では、プルする可能性のある依存関係が無制限であるため、クラスの責任を非常に簡単に混在させることができます。これは、実際には推奨されませんが、例外的なケースです。
davidxxx

@ davidxxx-JDK 8とSpring 4が事実上のものになる前、私は数年前に回答を受け入れました。上記のRomanの答えは、最新のSpringの使用法に対してより正確です。「アプリケーションでフレームワークを使用することを決定すると、アプリケーションとそれを結びつけるため」というステートメントに関しては、Springチームの推奨事項とJava Configのベストプラクティスとは正反対です。直接彼らと話す機会があります(私は持っていますし、可能な限りアプリのコードをSpringに結合しないように彼らが明示的にアドバイスすることを保証できます)。乾杯。
Les Hazlewood
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.