【问题标题】:Spring Java Config: how do you create a prototype-scoped @Bean with runtime arguments?Spring Java Config:如何使用运行时参数创建原型范围的@Bean?
【发布时间】:2014-04-05 00:52:31
【问题描述】:

使用 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 配置支持的微不足道的事情,这是大量的工作(相对而言)。

【问题讨论】:

  • 但是,您是否有理由不能自己实例化该类而必须从 Spring 中获取它?它是否依赖于其他bean?
  • @SotiriosDelimanolis 是的,Thing 实现实际上更复杂,并且确实依赖于其他 bean(为了简洁起见,我只是省略了它们)。因此,我不希望请求处理程序实现知道它们,因为这会将处理程序紧密耦合到它不需要的 API/bean。我会更新问题以反映您的(优秀)问题。
  • 我不确定 Spring 是否允许在构造函数上这样做,但我知道您可以将 @Qualifier 放在参数上,并将 @Autowired 放在设置器本身上。
  • 在 Spring 4 中,您使用 @Bean 的示例有效。使用您传递给 getBean(..) 的适当参数调用 @Bean 方法。
  • 注入标有@Autowired的字段或自己使用Spring类的setter并不难(几行代码),我已经使用了一段时间(如果我没记错的话,使用Spring 2.5 ) 不使用@Bean@Scope("prototype")@Configuration。我认为BeanFactory.getBean(String, Object[]) 不合理,因为它缺少编译时检查。当我想出我可以推荐的设计时,我可能会发布一个答案(我目前的设计有一些问题)。

标签: java spring scope prototype spring-java-config


【解决方案1】:

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

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

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

对于prototype bean,每次都会创建一个新对象,因此也会执行相应的@Bean 方法。

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

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

参数:

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

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

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

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

请注意,如果您不想从 ApplicationContextBeanFactory 开始检索 bean,则可以注入 ObjectProvider(自 Spring 4.3 起)。

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

并使用其getObject(Object... args) 方法

返回对象的一个​​实例(可能是共享的或独立的) 由这家工厂管理。

允许指定显式构造参数,沿线 BeanFactory.getBean(String, Object).

例如,

@Autowired
private ObjectProvider<Thing> things;

[...]
Thing newThing = things.getObject(name);
[...]

【讨论】:

    【解决方案2】:

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

    【讨论】:

    • @AbhijitSarkar 哦,我明白了。但是您不能将参数传递给ProviderObjectFactory,还是我错了?在我的示例中,您可以将字符串参数传递给它(或任何参数)
    • AFAIK,并非没有编写额外的代码。如果这是要求,那么两者的开箱即用实现都不会这样做。我在你的回答中错过了这一点,现在投票了
    • 如果您不想(或不需要)使用 Spring bean 生命周期方法(原型 bean 不同...),您可以跳过 @BeanScope 注释Thing thing 方法。此外,此方法可以私有化以隐藏自身并仅保留工厂。
    • @m52509791 你是对的:如果你不需要它,你可以完全省略方法并直接在thingFactory lambda body 中创建新实例
    • 赞成这个答案,因为使用 Spring 获取原型 bean 的默认方法是使用 ApplicationContext.getBean(...),这会污染代码并使其依赖于 Spring,这与 Spring 的存在哲学相矛盾非侵入性的。这个解决方案实现了这个意图并保持代码 Spring 免费,除了可以用 JSR 注释替换的注释。
    【解决方案3】:

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

    【讨论】:

    • 如果MyPrototype 是一个依赖于其他bean 的bean,这意味着它不能使用new 实例化怎么办?
    • @Searene,你可以随时注入其他bean来创建原型,spring会为你注入。
    【解决方案4】:

    更新每条评论

    首先,我不确定您为什么说“这不起作用”,因为在 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;
    

    【讨论】:

    • 感谢您的回复!但是,我认为您误解了这个问题。这个问题最重要的部分是,在获取(实例化)原型时,必须将运行时值作为构造函数参数提供。
    • 我更新了我的回复。实际上,处理运行时值似乎是正确完成的,所以我确实忽略了那部分。从程序的更新和输出中可以看出,它是明确支持的。
    【解决方案5】:

    如果你需要创建一个合格的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");
    
    

    【讨论】:

      【解决方案6】:

      您只需使用inner class 即可达到类似的效果:

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

      【讨论】:

        【解决方案7】:

        采用略有不同的方法延迟回答。 这是这个recent question 的后续,它指的是这个问题本身。

        是的,如前所述,您可以在 @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
            // ...    
        }
        

        它既类型安全又简洁。

        【讨论】:

        • 感谢您的回复,但这是一个 Spring 反模式。配置对象不应该“泄漏”到应用程序代码中——它们的存在是为了配置您的应用程序对象图和与 Spring 结构的接口。这类似于应用程序 bean 中的 XML 类(即另一种配置机制)。也就是说,如果 Spring 带有另一种配置机制,您将不得不重构您的应用程序代码 - 一个明确的指标,这违反了关注点分离。最好让您的配置创建工厂/函数接口的实例并注入工厂 - 与配置没有紧密耦合。
        • 1) 我完全同意,在一般情况下,配置对象不能作为字段泄漏。但是在这种特定情况下,注入一个定义一个且只有一个 bean 的配置对象来生成原型 bean,IHMO 完全有意义:这个配置类变成了一个工厂。如果只是这样,关注点分离问题在哪里? ...
        • ... 2) 关于“也就是说,如果 Spring 附带另一种配置机制”,这是一个错误的论点,因为当您决定在应用程序中使用框架时,您的应用程序就会耦合接着就,随即。因此,无论如何,如果该机制发生变化,您还必须重构依赖于@Configuration 的任何 Spring 应用程序。
        • ...3) 您接受的答案建议使用BeanFactory#getBean()。但这在耦合方面要糟糕得多,因为这是一个允许获取/实例化应用程序的任何 bean 的工厂,而不仅仅是当前 bean 需要的。使用这种用法,您可以非常轻松地混合类的职责,因为它可能拉取的依赖项是无限的,这确实不建议但例外情况。
        • @davidxxx - 在 JDK 8 和 Spring 4 成为事实之前,我几年前就接受了这个答案。上面 Roman 的回答对于现代 Spring 用法更正确。关于您的陈述“因为当您决定在应用程序中使用框架时,您将应用程序与它结合起来”与 Spring 团队的建议和 Java Config 最佳实践完全背道而驰 - 如果您得到一个有机会亲自与他们交谈(我有,而且我可以向您保证,他们明确建议不要尽可能将您的应用程序代码耦合到 Spring)。干杯。
        【解决方案8】:

        在您的 beans xml 文件中使用属性 scope="prototype"

        【讨论】:

          猜你喜欢
          • 2012-04-12
          • 2012-01-22
          • 1970-01-01
          • 2018-11-08
          • 2011-05-06
          • 1970-01-01
          • 2013-06-14
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多