163

使用 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 配置完美配合,例如:

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

但这违背了使用 Spring 替换 ServiceLocator 和 Factory 设计模式的全部意义,这对于这个用例来说是理想的。

如果 Spring Java Config 可以做到这一点,我将能够避免:

  • 定义工厂接口
  • 定义工厂实现
  • 为工厂实现编写测试

对于 Spring 已经通过 XML 配置支持的微不足道的事情,这是大量的工作(相对而言)。

4

8 回答 8

113

在一个@Configuration类中,@Bean像这样的方法

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

用于注册bean 定义并提供创建 bean 的工厂。它定义的 bean 仅根据请求使用直接或通过扫描确定的参数进行实例化ApplicationContext

prototypebean 的情况下,每次都会创建一个新对象,因此@Bean也会执行相应的方法。

ApplicationContext您可以通过其BeanFactory#getBean(String name, Object... args)声明的方法从 bean 中检索

允许指定显式构造函数参数/工厂方法参数,覆盖 bean 定义中指定的默认参数(如果有)。

参数:

如果使用静态工厂方法的显式参数创建原型,则使用args参数。在任何其他情况下使用非空 args 值是无效的。

换句话说,对于这个prototype作用域 bean,您提供的参数不是在 bean 类的构造函数中,而是在@Bean方法调用中。(此方法具有非常弱的类型保证,因为它使用 bean 的名称查找。)

BeanFactory#getBean(Class requiredType, Object... args)或者,您可以使用按类型查找 bean的 typed方法。

至少对于 Spring 4+ 版本是这样。

请注意,如果您不想从ApplicationContext或开始BeanFactory检索 bean,则可以注入一个ObjectProvider(从 Spring 4.3 开始)。

ObjectFactory专为注入点设计的变体,允许程序化的可选性和宽松的非唯一处理。

并使用它的getObject(Object... args)方法

返回此工厂管理的对象的实例(可能是共享的或独立的)。

允许按照BeanFactory.getBean(String, Object).

例如,

@Autowired
private ObjectProvider<Thing> things;

[...]
Thing newThing = things.getObject(name);
[...]
于 2014-03-04T21:59:33.377 回答
71

使用 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来传递两个以上的参数)。

于 2017-04-24T18:08:23.510 回答
30

自 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 字符串。

于 2018-08-25T23:13:39.807 回答
16

每条评论更新

首先,我不确定你为什么说“这不起作用”,因为在 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 方法)不会被第二次调用。考虑到这一点,当两次从同一个上下文对象请求“原型”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;
于 2014-03-04T23:02:21.800 回答
1

如果你需要创建一个合格的 bean,你可以这样做:

@Configuration
public class ThingConfiguration {

   @Bean
   @Scope(SCOPE_PROTOTYPE)
   public Thing simpleThing(String name) {
       return new Thing(name);
   }

   @Bean
   @Scope(SCOPE_PROTOTYPE)
   public Thing specialThing(String name) {
       Thing thing = new Thing(name);
       // some special configuration
       return thing;
   }

}

// Usage 

@Autowired
private ApplicationContext context;

AutowireCapableBeanFactory beanFactory = context.getAutowireCapableBeanFactory();
((DefaultListableBeanFactory) beanFactory).getBean("specialThing", Thing.class, "name");

于 2020-12-19T14:17:08.077 回答
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);
        }
    }
}
于 2018-11-02T21:02:34.100 回答
0

较晚的答案,方法略有不同。这是最近这个问题的后续问题,它指的是这个问题本身。

是的,正如上面所说,您可以在@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
    // ...    
}

它既类型安全又简洁。

于 2019-08-11T11:03:35.683 回答
-3

在你的 bean xml 文件中使用属性scope="prototype"

于 2019-10-06T17:44:36.713 回答