春季不在原型Bean中调用@Autowired的setter方法。

huangapple go评论74阅读模式
英文:

Spring not calling @Autowired setters in Prototype Beans

问题

问题描述:

该问题出现在一个运行spring-boot、java 8的k8s容器中(更多细节见下文)。

当在Spring配置中使用ObjectProvider<>并调用*provider.getObject(.....)*时,针对一个原型bean进行调用,在有时(无法定期发生)不会调用setter注入方法。

更新十月2日:Spring问题#25840

大部分情况下都能正常工作,但有时会构造一个新对象,但却未调用@Autowired的setter方法(通过日志信息进行检查)。

我们还发现,90%的情况发生在应用程序启动后,但并不总是如此。

更新九月21日: 删除(重新启动)pod可以解决问题。

更新九月22日: 一旦发生,就会一直发生,我的意思是不是它仅仅一次失败,下一次就正常工作,它会始终无法调用setter方法。

更新九月23日: 关于真实应用程序中的并发问题的新证据,直到现在才出现,一个单独的线程似乎会产生问题(请查看下面的类以更好地理解这个描述)。

ToipParentClass(这是策略实现)为以下setter方法标有@Autowired

  • VlocityService
  • OrderManagemenService

InternetParentClass(这是策略实现)为以下setter方法标有@Autowired

  • VlocityService
  • OrderManagemenService

日志(已注释)

[-nio-80-exec-10] GuidTaskController   : 使用XOM_TOIP_OFFER构建策略。                    
[p-nio-80-exec-2] GuidTaskController   : 使用XOM_INTERNET_OFFER构建策略。                    
[-nio-80-exec-10] ToipParentClass      : 构造ToipParentClass                                             
[p-nio-80-exec-2] InternetParentClass  : 构造InternetParentClass                                         
[-nio-80-exec-10] ToipParentClass      : 对VlocityServices进行自动装配@7951cd46                                      
[p-nio-80-exec-2] InternetParentClass  : 对VlocityServices进行自动装配@7951cd46                                      
[-nio-80-exec-10] ToipParentClass      : 对OrderManagementService进行自动装配@3385326a     
-------------------------------------------------------------
错误!缺少@Autowiring日志  
[p-nio-80-exec-2] InternetParentClass  : 对OrderManagementService进行自动装配@7951cd46                      
-------------------------------------------------------------
[p-nio-80-exec-2] Controller           : 发生意外错误
2020-09-22T18:56:45.544525916Z
-------------------------------------------------------------
错误:使用未设置的OrderManagementService时出现NullPointer
-------------------------------------------------------------
2020-09-22T18:56:45.544530395Z java.lang.NullPointerException: null
2020-09-22T18:56:45.544534074Z  at InternetParentClass.generateIds(InternetParentClass.java:50) ~[classes!/:BUG001_PrototypeBeanAutowired-8]                                       
2020-09-22T18:56:45.544538568Z  at GuidTaskController.setGUID(GuidTaskController.java:180) ~[classes!/:BUG001_PrototypeBeanAutowired-8]

我在https://github.com/toniocus/strategy-calculator中进行了一个简单的测试,独立运行它,使用不同的jdk8版本,还在pod中使用的相同的Docker镜像(项目中的所有内容),但无法让它失败。

关于在哪里查找问题、尝试什么以及甚至解决方案的任何想法将不胜感激,提前感谢。

以下是产品版本、类的详细信息。

版本详细信息:

k8s:

v1.14.9-eks-658790

spring-boot:

2.1.4.RELEASE

JDK:

  openjdk version "1.8.0_212"
  OpenJDK Runtime Environment (IcedTea 3.12.0) (Alpine 8.212.04-r0)
  OpenJDK 64-Bit Server VM (build 25.212-b04, mixed mode)

类的详细信息

涉及的类的片段,无需担心语法错误。
(真实代码可以在https://github.com/toniocus/strategy-calculator中找到)

// ============================================================
public interface Strategy {
   void work(...);
}

// ============================================================
@Service
public class Service {
    public void doSomething() {
       ......
       ......
    }
}

// ============================================================
public class MobileStrategy implements Strategy {
   private Service service;

   @Autowired
   public void setService(Service s) {
       this.service = s;   // 有时不会调用setter
   }

   public void work(...) {
       this.service.doSomething();  // 有时会抛出NullPointerException
   }
}

// ============================================================
public enum StrategyEnum {

    MOBILE("mobileKey", MobileStrategy.class),
    TV("tvKey", TvStrategy.class),
    .......

    public Class<Strategy> getImplementationClass() {
       ......
    }

    public StrategyEnum findByKey(String key) {
       ......
    }
}

// ============================================================
@Configuration
public class Configuration {

    @Bean
    @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
    public Strategy getStrategy(final StrategyEnum selectorEnum) {

        try {

            Constructor<? extends Strategy> constructor =
                    selectorEnum.getImplementationClass().getConstructor();

            return constructor.newInstance();
        }
        catch (Exception ex) {

            throw new Exception("Not able to instantiate interface implementation"
                    + " for enum: " + selectorEnum
                    , ex);
        }

    }
}

// ============================================================
@RestController
public class MathOperRestController {

    @Autowired
    ObjectProvider<Strategy> provider;

    @GetMapping("/{operation}/{x}/{y}")
    public BigDecimal add(
            @PathVariable("operation") final String operation
            , @PathVariable("x") final BigDecimal x
            , @PathVariable("y") final BigDecimal y
            ) {

        Strategy strategy =

<details>
<summary>英文:</summary>

## The problem: ##

The problem appears in a k8s pod running spring-boot, java 8 (more details below)

When using `ObjectProvider&lt;&gt;` and calling `*provider.getObject(.....)*`,  on
a prototype bean defined in Spring Configuration, every now and then (never
find a way to make it happen regularly) setter injection methods are not called.

**Update oct, 2:** See [Spring Issue #25840][1]

Most of the time this works perfectly well, but sometimes, it constructs a new object
but misses to call the @Autowired setter method (checked with log information).

We also discover that 90% of the time it happens after application startup, but not always.

**UPDATE sept, 21**: Deleting (Restarting) de pod solves the problem.

**UPDATE sept, 22**: Once it happens it will happen all the time, I mean is not that it fails once, and the next time it works ok, it will always fail to call setters.

**UPDATE sept, 23**: New evidence related to concurrency problem in the real application, didn&#39;t appear until now, a single thread alone seemed
to generate the problem. (take a look at classes below to understand better 
this description)

**ToipParentClass** (this is a Strategy Implementation) has setter @Autowired for

 - VlocityService 
 - OrderManagemenService

**InternetParentClass** (this is a Strategy Implementation) has setter @Autowired for

 - VlocityService 
 - OrderManagemenService

**Log (commented)**

    [-nio-80-exec-10] GuidTaskController   : Build Strategy using XOM_TOIP_OFFER .                    
    [p-nio-80-exec-2] GuidTaskController   : Build Strategy using XOM_INTERNET_OFFER .                    
    [-nio-80-exec-10] ToipParentClass      : @Constructing ToipParentClass                                             
    [p-nio-80-exec-2] InternetParentClass  : @Constructing InternetParentClass                                         
    [-nio-80-exec-10] ToipParentClass      : @Autowiring VlocityServices@7951cd46                                      
    [p-nio-80-exec-2] InternetParentClass  : @Autowiring VlocityServices@7951cd46                                      
    [-nio-80-exec-10] ToipParentClass      : @Autowiring OrderManagementService@3385326a     
    -------------------------------------------------------------
    ERROR !!! Missing @Autowiring log  
    [p-nio-80-exec-2] InternetParentClass  : @Autowiring OrderManagementService@7951cd46                      
    -------------------------------------------------------------
    [p-nio-80-exec-2] Controller           : Unexpected Error
    2020-09-22T18:56:45.544525916Z
    -------------------------------------------------------------
    ERROR: NullPointer when using not set OrderManagementService
    -------------------------------------------------------------
    2020-09-22T18:56:45.544530395Z java.lang.NullPointerException: null
    2020-09-22T18:56:45.544534074Z  at InternetParentClass.generateIds(InternetParentClass.java:50) ~[classes!/:BUG001_PrototypeBeanAutowired-8]                                       
    2020-09-22T18:56:45.544538568Z  at GuidTaskController.setGUID(GuidTaskController.java:180) ~[classes!/:BUG001_PrototypeBeanAutowired-8]

I made a simple test in https://github.com/toniocus/strategy-calculator run it standalone, different jdk8 versions, also in the same docker image used in the pod (all things in the project), and failed to make it FAIL.

Any ideas on where to look for a problem, suggestions on what to try, or even
a solution :-),  will be greatly welcome, thanks in advance

Below the product versions, classes.

## Detail about versions: ##

**k8s:**
  

    v1.14.9-eks-658790

  
**spring-boot:**
  

    2.1.4.RELEASE

**JDK:**
  

      openjdk version &quot;1.8.0_212&quot;
      OpenJDK Runtime Environment (IcedTea 3.12.0) (Alpine 8.212.04-r0)
      OpenJDK 64-Bit Server VM (build 25.212-b04, mixed mode)

## The Classes ##
A snippet on the classes involved, no real code, do not worry about syntax errors.
(real code can be found in https://github.com/toniocus/strategy-calculator)

// ============================================================
public interface Strategy {
void work(...);
}

// ============================================================
@Service
public class Service {
public void doSomething() {
......
......
}
}

// ============================================================
public class MobileStrategy implements Strategy {
private Service service;

@Autowired
public void setService(Service s) {
this.service = s; // setter not called every now and then
}

public void work(...) {
this.service.doSomething(); // throws NullPointerException every now an then
}
}

// ============================================================
public enum StrategyEnum {

MOBILE(&quot;mobileKey&quot;, MobileStrategy.class),
TV(&quot;tvKey&quot;, TvStrategy.class),
.......
public Class&lt;Strategy&gt; getImplementationClass() {
......
}
public StrategyEnum findByKey(String key) {
.....
}

}

// ============================================================
@Configuration
public class Configuration {

@Bean
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public Strategy getStrategy(final StrategyEnum selectorEnum) {
try {
Constructor&lt;? extends Strategy&gt; constructor =
selectorEnum.getImplementationClass().getConstructor();
return constructor.newInstance();
}
catch (Exception ex) {
throw new Exception(&quot;Not able to instantiate interface implementation&quot;
+ &quot; for enum: &quot; + selectorEnum
, ex);
}
}

}

// ============================================================
@RestController
public class MathOperRestController {

@Autowired
ObjectProvider&lt;Strategy&gt; provider;
@GetMapping(&quot;/{operation}/{x}/{y}&quot;)
public BigDecimal add(
@PathVariable(&quot;operation&quot;) final String operation
, @PathVariable(&quot;x&quot;) final BigDecimal x
, @PathVariable(&quot;y&quot;) final BigDecimal y
) {
Strategy strategy = this.provider.getObject(StrategyEnum.findByKey(operation));
strategy.doWork(x, y);
return BigDecimal.ZERO;
}

**UPDATE sept,21** added Service class to samples
[1]: https://github.com/spring-projects/spring-framework/issues/25840
</details>
# 答案1
**得分**: 1
## 我发现的问题 ##
**更新于十月1日:** 请阅读问题中的 @Denium 评论!!!(感谢该评论)。
在今天(9月23日)的更新后,问题显然是一个并发问题,我能够轻松地重现它(见 [SimpleSpringAppTest.java][1])在一个简单的 Spring 应用程序中,没有使用 spring-boot。
我使所有策略实现都具有相同的 @Autowired 设置器,错误仍然存在。
似乎进行了一些缓存,@Autowired 设置器是从缓存中获取的,而不是从新构造的对象中获取的,尽管我试图在如此短的时间内深入了解 spring 源码是困难的。
问题已经解决,避免了并发(以下是更改),所以现在我的问题是:
**这是预期的行为还是一个 bug?**
我无法找到任何关于此问题的文档,也没有在任何地方描述这种行为,所以对我来说仍然是一个问题。
我在 spring-boot 版本 1.5.22、2.1.4(我们目前在使用的版本)和 2.3.4 中进行了测试,在所有情况下都出现了相同的问题,只是在一个简单的 spring 应用程序中,不需要 RestController 或其他内容。
## 解决方法 1 ##
添加一个中间的工厂以确保 Strategy beans 被“同步”地创建。
**更新于十月1日:** 根据 @Deinum 的评论,从我理解的角度来看,Spring 将会每次(或几乎每次)扫描类以寻找注解,所以我想 Workaround 2 可能是一个更好的解决方案。
这个解决方案更适合我的当前环境。
### 新的类 StrategyFactory ###
注意 `getStrategy(...)` 方法是同步的,我想这个解决方案将会有一些性能影响,但仍然无法测量它。
```java
@Component
public class StrategyFactory {
@Autowired
ObjectProvider<Strategy> provider;
public synchronized Strategy getStrategy(final MathOperEnum operation) {
return this.provider.getObject(operation);
}
}

RestController 中的更改

现在使用 StrategyFactory 替代 ObjectProvider

@RestController
public class MathOperRestController {

    @Autowired
    StrategyFactory factory;

    @GetMapping("/{operation}/{x}/{y}")
    public BigDecimal add(
            @PathVariable("operation") final String operation
            , @PathVariable("x") final BigDecimal x
            , @PathVariable("y") final BigDecimal y
            ) {

        Strategy strategy = this.factory
             .getStrategy(StrategyEnum.findByKey(operation));

        strategy.doWork(x, y);

        return BigDecimal.ZERO;
    }
}

解决方法 2

  • 使 StrategyFactory 实现 ApplicationContextAware 接口
  • 为每个策略实现添加 @Component / @Scope 注解
  • 移除 @Configuration 类
@Component
public class StrategyFactory implements ApplicationContextAware {

    private ApplicationContext ctx;

    public Strategy getStrategy(final StrategyEnum operation) {
        return this.ctx.getBean(
               operation.getImplementationClass()
               );
    }

    @Override
    public void setApplicationContext(
         final ApplicationContext applicationContext) 
    throws BeansException {

        this.ctx = applicationContext;

    }

}
英文:

The problem I found

Update oct, 1: Please read @Denium comment in the question !!!! (thanks for it).

After today's (Sept, 23) update, seems clearly the problem is a concurrency problem, I was able to reproduce it easily (See SimpleSpringAppTest.java)
in a Simple Spring app, no spring-boot.

Made all Strategy implementations have the same set of @Autowired setters
and the error is still there.

Seems there is some caching done, and @Autowired setters are taken from the cache, and not from the newly constructed object, although I try to dig into spring sources difficult to understand in so short time.

The problem was solved, avoiding concurrency (changes below), so now my question:

Is this the expected behaviour or is it a bug ?

I was not able to find any documentation regarding this problem or describing this behaviour anywhere, so still a question for me.

I tested in spring-boot versions 1.5.22, 2.1.4 (what we are currently using), and 2.3.4 and in all cases the same problem appears, just in a simple spring application, no need of RestController or so.

Workaround 1

Add an intermediate Factory to ensure Strategy beans are created 'synchronously'.

Update oct, 1: After @Deinum comments, from what I understood, Spring will be scanning classes every time (or almost every time) for annotations, so I guess Workaround 2 is probably a better solution.

This solution is more suitable for my current environment.

New Class StrategyFactory

Note the getStrategy(...) method is synchronized, I guess this solution
will have some performance impact, but still not able to measure it.

@Component
public class StrategyFactory {
@Autowired
ObjectProvider&lt;Strategy&gt; provider;
public synchronized Strategy getStrategy(final MathOperEnum operation) {
return this.provider.getObject(operation);
}
}

Changes in RestController

Now using the StrategyFactory instead of the ObjectProvider

@RestController
public class MathOperRestController {
@Autowired
StrategyFactory factory;
@GetMapping(&quot;/{operation}/{x}/{y}&quot;)
public BigDecimal add(
@PathVariable(&quot;operation&quot;) final String operation
, @PathVariable(&quot;x&quot;) final BigDecimal x
, @PathVariable(&quot;y&quot;) final BigDecimal y
) {
Strategy strategy = this.factory
.getStrategy(StrategyEnum.findByKey(operation));
strategy.doWork(x, y);
return BigDecimal.ZERO;
}
}

Workaround 2

  • Make the StrategyFactory ApplicationContextAware
  • Add @Componente/@Scope annotation to each strategy implementation
  • Remove @Configuration class
@Component
public class StrategyFactory implements ApplicationContextAware {
private ApplicationContext ctx;
public Strategy getStrategy(final StrategyEnum operation) {
return this.ctx.getBean(
operation.getImplementationClass()
);
}
@Override
public void setApplicationContext(
final ApplicationContext applicationContext) 
throws BeansException {
this.ctx = applicationContext;
}
}

答案2

得分: -2

这不是在Configuration#getStrategy方法中创建Strategy bean 实例的正确方式,因为它不会使用自动装配来调用 setter 方法。

Constructor<? extends Strategy> constructor =
                selectorEnum.getImplementationClass().getConstructor();

return constructor.newInstance();

@Autowired的使用方式表明您希望创建一个由Spring处理实例创建的bean。

您可以参考这个回答 https://stackoverflow.com/a/52355649/2614885,使用AutowireCapableBeanFactory来创建Spring容器中的bean,指定您在这种情况下可能是 'mobileKey' 或 'tvKey'。

请尝试在您的 @Configuration 类中使用以下代码:

@Configuration
public class Configuration {

    @Autowired private AutowireCapableBeanFactory autowireCapableBeanFactory;
    
    @Bean
    @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
    public Strategy getStrategy(final StrategyEnum selectorEnum) {

        try {

            Constructor<? extends Strategy> constructor =
                    selectorEnum.getImplementationClass().getConstructor();

            Strategy strategyBean = constructor.newInstance();
            autowireCapableBeanFactory.autowireBean(strategyBean);
            return strategyBean;
        }
        catch (Exception ex) {

            throw new Exception("Not able to instantiate interface implementation"
                    + " for enum: " + selectorEnum
                    , ex);
        }

    }
}
英文:

This is not the correct way to create an instance of Strategy bean in Configuration#getStrategy method as it will not call setter using autowiring.

Constructor&lt;? extends Strategy&gt; constructor =
selectorEnum.getImplementationClass().getConstructor();
return constructor.newInstance();

The way @Autowired is used, it seems you want to create a bean of which instance creation is handled by Spring.

You can refer this answer https://stackoverflow.com/a/52355649/2614885 to use AutowireCapableBeanFactory which creates bean in spring container for the bean id you specify which seems to be either 'mobileKey' or 'tvKey' in your case.

try following code in your @Configuration class

@Configuration
public class Configuration {
@Autowired private AutowireCapableBeanFactory autowireCapableBeanFactory;
@Bean
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public Strategy getStrategy(final StrategyEnum selectorEnum) {
try {
Constructor&lt;? extends Strategy&gt; constructor =
selectorEnum.getImplementationClass().getConstructor();
Strategy strategyBean = constructor.newInstance();
autowireCapableBeanFactory.autowireBean(strategyBean);
return strategyBean;
}
catch (Exception ex) {
throw new Exception(&quot;Not able to instantiate interface implementation&quot;
+ &quot; for enum: &quot; + selectorEnum
, ex);
}
}
}

huangapple
  • 本文由 发表于 2020年9月22日 02:49:01
  • 转载请务必保留本文链接:https://go.coder-hub.com/63998293.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定