原型Bean没有按预期进行自动装配

27

测试控制器.java

@RestController
public class TestController {

    @Autowired
    private TestClass testClass;

    @RequestMapping(value = "/test", method = RequestMethod.GET)
    public void testThread(HttpServletResponse response) throws Exception {
        testClass.doSomething();
    }
}

TestClass.java

@Component
@Scope("prototype")
public class TestClass {

    public TestClass() {
        System.out.println("new test class constructed.");
    }

    public void doSomething() {

    }

}

正如您所看到的,我正在尝试弄清楚当访问“xxx/test”时是否已注入新的TestClass"new test class constructed."仅在第一次触发“xxx/test”时打印,而我希望它被平等地打印。这是否意味着@Autowired对象只能是@Singleton?那么@Scope是如何工作的呢?

编辑:

TestController.java

@RestController
public class TestController {

    @Autowired
    private TestClass testClass;

    @RequestMapping(value = "/test", method = RequestMethod.GET)
    public void testThread(HttpServletResponse response) throws Exception {
        testClass.setProperty("hello");
        System.out.println(testClass.getProperty());
    }
}

我尝试过@Valerio Vaudi的解决方案,注册为Scope(scopeName = "request")。以下是当我访问“xxx/test”时的三次结果:

(第一次)

  • 新测试类被构造。
  • null

(第二次)

  • null

(第三次)

  • null

我不明白为什么结果是null,因为它每次使用时都没有重建新的实例。

然后我尝试了@Nikolay Rusev的解决方案@Scope("prototype")

(第一次)

  • 新实例被构造。
  • 新实例被构造。
  • null

(第二次)

  • 新实例被构造。
  • 新实例被构造。
  • null

(第三次)

  • 新实例被构造。
  • 新实例被构造。
  • null

这很容易理解,因为每次使用它(TestClass)时,Spring会自动重新生成一个新实例。但第一个场景我仍然无法理解,因为它似乎对于每个请求仅保留一个新实例。

真正的目的是:在每个请求生命周期中,需要一个新的testClass(如果需要),并且只需要一个。此时似乎只有ApplicationContext解决方案是可行的(我已经知道了),但我只想知道是否可以通过使用@Component+ @Scope+@Autowired来自动完成这项工作。


1
我不知道第三个 null 是从哪里来的?请贴出完整的 TestClass 代码。 - Nikolay Rusev
5个回答

31

以上所有答案都是正确的。默认情况下,控制器是singleton,而注入的testClass只会实例化一次,因为默认范围代理模式是DEFAULT,来自spring文档

public abstract ScopedProxyMode proxyMode 指定组件是否应配置为作用域代理,如果是,则代理应基于接口还是基于子类。默认值为ScopedProxyMode.DEFAULT,通常表示除非在组件扫描指令级别上配置了不同的默认值,否则不应创建作用域代理。

与Spring XML中的<aop:scoped-proxy/>支持类似。

另请参阅:ScopedProxyMode 默认值为:org.springframework.context.annotation.ScopedProxyMode.DEFAULT

如果您希望每次需要时注入新实例,您应将TestClass更改为:

@Component
@Scope(value="prototype", proxyMode=ScopedProxyMode.TARGET_CLASS)
public class TestClass {

    public TestClass() {
        System.out.println("new test class constructed.");
    }

    public void doSomething() {

    }

}

通过这个额外的配置,注入的 testClass 实际上不会是一个真正的 TestClass bean,而是代理到 TestClass bean,并且这个代理将了解 prototype 作用域,并且每次需要时都会返回新的实例。


最佳和简单的解决方案。 - Arundev
正是我所需要的。为了看到每次调用控制器时是否获得了 TestClass 的新实例,请确保调用上述 doSomething() 方法。 - flyingAssistant
如果我调用两个方法,实例会被创建两次吗?在每次代理调用时都会创建新实例吗? - Rustam

9
如前所述,控制器默认为单例,这就是为什么在创建时只执行TestClass的实例化和注入一次的原因。
解决方法可以是注入应用程序上下文并手动获取bean:
@RestController
public class TestController {

    @Autowired
    ApplicationContext ctx;

    @RequestMapping(value = "/test", method = RequestMethod.GET)
    public void testThread(HttpServletResponse response) throws Exception {
        ((TestClass) ctx.getBean(TestClass.class)).doSomething();
    }
}

现在,当请求一个TestClass bean时,Spring知道它是@Prototype,将创建一个新的实例并返回它。
另一种解决方案是将控制器设为@Scope("prototype")

4

Spring控制器默认情况下是单例的(由于它们是无状态的,所以这是可以接受的),其他Spring bean也是如此。

这就是为什么只需要实例化一个TestClass实例即可获得唯一的TestController实例。

如果需要,在另一个控制器中注入或从上下文中以编程方式获取TestClass实例也很容易。


2

您无法自动装配原型bean(当然,您可以这样做,但是bean始终都是相同的)...请自动装配ApplicationContext并手动获取所需的原型bean实例(例如在构造函数中):

    TestClass test = (TestClass) context.getBean("nameOfTestClassBeanInConfiguration");

以这种方式,您可以确保获得TestClass的新实例。

2
关键点在于restController bean是一个单例,Spring在创建bean时仅会创建一个实例。
当您强制使用原型bean范围时,Spring将为每个DI点实例化一个新的bean。换句话说,如果您通过xml或java-config配置bean两次或n次,则该bean将具有原型作用域bean的新实例。
在您的情况下,您使用注释样式,这实际上是Web层开始使用Spring 3.x的默认方式。
注入新的bean的一种可能方法可以通过会话范围的bean实现,但在我看来,如果您的用例是一个无状态的rest WS,那么会话的使用在我看来是一个不好的选择。
解决方案可能是使用请求范围。
更新: 我也写了一个简单的示例。
     @SpringBootApplication
     public class DemoApplication {

        public static void main(String[] args) {
            SpringApplication.run(DemoApplication.class, args);
        }

        @Bean
        @Scope(scopeName = "request",proxyMode = ScopedProxyMode.TARGET_CLASS)
        public RequestBeanTest requestBeanTest(){
            return new RequestBeanTest();
        }

    }

    class RequestBeanTest {
        public RequestBeanTest(){
            Random random = new Random();
            System.out.println(random.nextGaussian());
            System.out.println("new object was created");
        }

        private String prop;

        public String execute(){

            return "hello!!!";
        }

        public String getProp() {
            return prop;
        }

        public void setProp(String prop) {
            this.prop = prop;
        }
    }


    @RestController
    class RestTemplateTest {

        @Autowired
        private RequestBeanTest requestBeanTest;

        @RequestMapping("/testUrl")
        public ResponseEntity responseEntity(){
            requestBeanTest.setProp("test prop");

            System.out.println(requestBeanTest.getProp());
            return ResponseEntity.ok(requestBeanTest.execute());
        }
    }

我的pom.xml文件

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>demo</name>
    <description>Demo project for Spring Boot</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.3.3.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>


        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

浏览器界面: enter image description here 我的日志界面: enter image description here 我不知道为什么它对您不起作用,可能是您忘记了一些配置。
我希望这个更详细的解决方案可以帮助您了解如何解决您的问题。

我在TestClass中有一个String属性,并且有getter和setter。我在处理程序方法中将属性设置为某个值,然后获取该属性,但它返回null,为什么会这样?虽然我可以看到它为每个请求恰好构造了一个新的TestClass。 - Kim
抱歉,我不理解使用情况。假设您有一个像我建议的配置,但现在您还有一个字符串属性,在处理方法中设置了该属性,但是当您尝试检索数据时,您会得到null。请注意,如果您在rest api方法中的某个方法中设置属性,但然后在另一个方法中获取您设置的值,则很明显会检索到null,因为请求不同,Spring将注入一个新的bean,该bean将具有先前设置为null的属性。 - Valerio Vaudi
如果您想扩展注入bean的生命周期,您应该考虑使用会话范围。但是,如果您构建了一个REST API,则API应该是无状态的而不是有状态的。但是,如果您的用例强制要求在REST API交互之间进行对话,则应考虑使用@Scope(scopeName =“session”,proxyMode = ScopedProxyMode.TARGET_CLASS)。 - Valerio Vaudi
我希望能够充分理解您的问题。 - Valerio Vaudi
我已经阅读了你的更新,感觉非常不错。我会尽快更新我的答案,并附上我基本配置的完整代码,以便向你展示。 - Valerio Vaudi
这个例子绝对不能证明bean的创建方法被调用过。通过@Autowired注解引入的实例很可能是由Spring通过调用该类的默认构造函数来创建的。 - mlfan

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接