如何在Spring中以编程方式生成新的托管bean

时间:2019-10-30 16:03:11

标签: java spring spring-boot dependency-injection

该示例只是一个虚拟示例,它显示了我遇到的问题,因此不要以其他方式陷入困境以解决此处的具体问题。我的问题更多是关于了解在Spring中解决某种类型问题的适当技术

说我有一个托管bean Info

@Component
public class Info {
  private final String activeProfile;
  private final Instant timestamp;

  public Info(@Value("${spring.profiles.active}") String activeProfile) {
    this.activeProfile = activeProfile;
    this.timestamp = Instant.now();
  }
}

这里的关键是bean需要Spring注入的东西(在我的示例中为活动配置文件)以及每次创建bean时都会更改的东西(在我的示例中为时间戳)。由于后者,我不能使用Singleton范围。获得此类Bean的新实例的正确方法是什么?

我目前所拥有的是,该bean是 not 托管的(没有@Component,没有@Value),并且我有一个托管服务(控制器),该服务调用构造函数常规Info POJO的显式。像

@RestController
public class InfoRestController { 
  @GetMapping
  public Info getInfo(@Value("${spring.profiles.active}") String activeProfile) {
    return new Info(activeProfile);
  } 
}

此解决方案的问题在于,它会将活动概要文件的知识泄漏给控制器,只是将其传递给Info的构造函数,而从概念上讲,控制器不应该知道构造Info bean的知识。这就是依赖注入的要点之一

我已经想到了一些可能的解决方案:

  • 在控制器中先引用InfoFactory FactoryBean,然后再引用return factory.getObject();。但是我真的需要为这种简单的情况创建一个新的类吗?
  • 具有@Bean工厂方法来构造托管bean。仍然存在一个问题,该方法正在显式实例化Info POJO,因此它本身需要对它进行Spring注入。而且,这是完整的样板。

Info bean的构造非常简单,以至于我想在Spring中有一种更简单的方法可以完成此操作。有吗?

3 个答案:

答案 0 :(得分:1)

似乎在请求到来时需要一个托管的新对象。为此,您可以使用@Scope("prototype")标记Bean,以解决您的问题。

  

具有原型范围的bean每次从容器请求时都会返回不同的实例。

要了解更多信息,请访问以下页面来了解原型的工作原理:

  1. Spring - Prototype scope example using @Scope annotation
  2. 3. Prototype Scope

答案 1 :(得分:1)

这一切都取决于变化的事物。您可以使用protoype范围,如ruhul的答案所示。但是,这里的问题是原型实例在创建InfoRestController bean时仅被注入一次。

您可以尝试使用request范围-当您通过这种范围自动连接bean时,将为每个HTTP请求创建一个新实例:

@Component
@Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
public class Info {
    private final String activeProfile;
    private final Instant timestamp;

    public Info(@Value("${spring.profiles.active}") String activeProfile) {
        this.activeProfile = activeProfile;
        this.timestamp = Instant.now();
    }
}

这将确保为每个请求创建Info的新实例:

@RestController
public class InfoRestController {

    private Info info;

    @Autowired
    public InfoRestController(Info info) {
        this.info = info;
    }

    @GetMapping("/test")
    public Info get() {
        return info;
    }
}

还要记住,将注入到Info中的InfoRestController实例是代理,因此在此示例中,您将返回带有一些其他字段的代理实例。为了克服这个问题,我们可以从注入的bean复制值:

@RestController
public class InfoRestController {

    private Info info;

    @Autowired
    public InfoRestController(Info info) {
        this.info = info;
    }

    @GetMapping("/test")
    public Info get() {
        return Info.of(info.getActiveProfile(), info.getTimestamp()); // create a copy
    }
}

@Component
@Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
public class Info {

    private final String activeProfile;
    private final Instant timestamp;

    @Autowired // this constructor should be used by spring
    public Info(@Value("${spring.profiles.active}") String activeProfile) {
        this.activeProfile = activeProfile;
        this.timestamp = Instant.now();
    }

    private Info(String activeProfile, Instant timestamp) { // private constructor
        this.activeProfile = activeProfile;
        this.timestamp = timestamp;
    }

    public static Info of(String activeProfile, Instant instant) { //static factory method
        return new Info(activeProfile, instant);
    }

    // getters
}

答案 2 :(得分:1)

这个难题的缺失部分是javax.inject.Provider。我对此一无所知,但它恰好具有我要寻找的接口。最终的解决方案确实是让Spring管理bean(Info)并在rest控制器中使用Provider。这是bean,几乎与OP中的一样

@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class Info {
  private final String activeProfile;
  private final Instant timestamp;

  public Info(@Value("${spring.profiles.active}") String activeProfile) {
    this.activeProfile = activeProfile;
    this.timestamp = Instant.now();
  }
}

bean具有ruhul建议的Prototype范围。我之前曾尝试过,但是没有Provider的情况下我仍然很困惑。这是在控制器中返回它的方法

@RestController
public class InfoRestController { 
  @Autowire
  private Provider<Info> infoProvider;

  @GetMapping
  public Info getInfo() {
    return infoProvider.get();
  } 
}

出于完整性考虑,我发现了另一种更丑陋的方法,方法是注入弹簧ApplicationContext,然后使用context.getBean("info"),但是对spring上下文和字符串名称的依赖是一种气味。 Provider的解决方案更具针对性