Unmanaged Member In Singleton Bean

ID

java.spring_component_members_injected

Severity

high

Remediation Complexity

medium

Remediation Risk

low

Remediation Effort

medium

Resource

Reliability

Language

Java

Tags

reliability, spring

Description

Reports a non-static, non-final member of a singleton Spring stereotype (@Component, @Controller, @RestController, @Service, @Repository) that no part of the container ever writes.

Spring has four ways of filling a field and only one of them puts an annotation on the field, so a member is treated as managed — and is not reported — when any of these holds:

  • it carries a recognized injection annotation (@Autowired, @Value, @Inject, @Resource, @PersistenceContext, javax. and jakarta. alike);

  • it is assigned by a constructor or an initializer block, or by a Lombok-generated constructor (@AllArgsConstructor / @Value take every non-static field; @RequiredArgsConstructor / @Data take the final and @NonNull ones);

  • it is assigned by a lifecycle callback — a @PostConstruct or @EventListener method, or the afterPropertiesSet() of an InitializingBean;

  • it is assigned by a setter carrying an injection annotation, or by the callback of an *Aware interface the class implements (ApplicationContextAware is filled through setApplicationContext, and so on).

Not reported either on a volatile member — the modifier is an explicit statement that the sharing is deliberate, as in a double-checked-locking lazy initializer — on a class annotated @ConfigurationProperties, or on a bean that is not a singleton: a literal @Scope resolving to anything else, or one of the composed scope annotations @RequestScope, @SessionScope, @ApplicationScope, @RefreshScope, @JobScope, @StepScope, which are meta-annotated @Scope and so carry no literal of their own.

The assignment search is syntactic and class-local: it recognises this.f = …​ and f = …​ inside the writers listed above and nothing else, so a field written only through a helper the constructor calls, through reflection, or by a superclass is a known false positive. A field written by an ordinary business method is still reported — that is the cross-request leak the rule is about.

Rationale

Spring’s stereotypes are singletons by default: one instance is built and shared across every caller for the life of the application context. A field on that shared instance which is not either constant (static) or fixed at construction (final, injected through the constructor) is state every caller reads and writes through the same object — exactly the shape of a cross-request data race or leak.

@Controller
public class GreetingController {

  private String lastGreeted;   // FLAW -- shared, mutable, unmanaged

  public String greet(String name) {
    this.lastGreeted = name;
    return "Hello " + this.lastGreeted;  // could be the PREVIOUS caller's name under load
  }
}

Remediation

Inject the member through the constructor — Spring auto-wires a bean’s sole constructor without needing any annotation at all — or, if the member genuinely does not vary per instance, make it static. A member that is set once at construction and never reassigned should also be final.

@Controller
public class GreetingController {

  private final GreetingService greetingService;

  public GreetingController(GreetingService greetingService) {
    this.greetingService = greetingService;
  }

  public String greet(String name) {
    return greetingService.greet(name);
  }
}

© 2026 Xygeni. All rights reserved