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.andjakarta.alike); -
it is assigned by a constructor or an initializer block, or by a Lombok-generated constructor (
@AllArgsConstructor/@Valuetake every non-static field;@RequiredArgsConstructor/@Datatake thefinaland@NonNullones); -
it is assigned by a lifecycle callback — a
@PostConstructor@EventListenermethod, or theafterPropertiesSet()of anInitializingBean; -
it is assigned by a setter carrying an injection annotation, or by the callback of an
*Awareinterface the class implements (ApplicationContextAwareis filled throughsetApplicationContext, 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);
}
}