Avoid borrowing @Qualifier from unrelated injection parameters
Require parameter-name or setter-name agreement when inferring a field qualifier from constructor/autowired parameter annotations, preventing false-positive routing for multi-bean StateMachine<String,String> injections. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -62,9 +62,9 @@ public class InjectionPointAnalyzer {
|
|||||||
try {
|
try {
|
||||||
IAnnotationBinding[] paramAnns = method.getParameterAnnotations(i);
|
IAnnotationBinding[] paramAnns = method.getParameterAnnotations(i);
|
||||||
String paramQual = getQualifierValue(paramAnns);
|
String paramQual = getQualifierValue(paramAnns);
|
||||||
if (paramQual != null && paramType.getErasure().isEqualTo(binding.getType().getErasure())) {
|
if (paramQual != null
|
||||||
// For setters, verify it roughly matches the field name or just rely on type.
|
&& paramType.getErasure().isEqualTo(binding.getType().getErasure())
|
||||||
// We will rely on type equality for now as a heuristic.
|
&& parameterMatchesField(method, i, binding.getName())) {
|
||||||
return paramQual;
|
return paramQual;
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@@ -90,4 +90,29 @@ public class InjectionPointAnalyzer {
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static boolean parameterMatchesField(
|
||||||
|
org.eclipse.jdt.core.dom.IMethodBinding method,
|
||||||
|
int parameterIndex,
|
||||||
|
String fieldName) {
|
||||||
|
if (method == null || fieldName == null || fieldName.isBlank() || parameterIndex < 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String[] names;
|
||||||
|
try {
|
||||||
|
names = method.getParameterNames();
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
names = null;
|
||||||
|
}
|
||||||
|
if (names != null && parameterIndex < names.length) {
|
||||||
|
return fieldName.equals(names[parameterIndex]);
|
||||||
|
}
|
||||||
|
// If parameter names aren't available, only accept clear setter-like methods.
|
||||||
|
String methodName = method.getName();
|
||||||
|
if (methodName != null && methodName.startsWith("set") && method.getParameterTypes().length == 1) {
|
||||||
|
String expected = "set" + Character.toUpperCase(fieldName.charAt(0)) + fieldName.substring(1);
|
||||||
|
return expected.equals(methodName);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,6 +66,58 @@ class SpringInjectionRoutingTest {
|
|||||||
chain, "com.example.OrderStateMachineConfig", context)).isFalse();
|
chain, "com.example.OrderStateMachineConfig", context)).isFalse();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldNotBorrowConstructorQualifierFromUnrelatedParameter(@TempDir Path tempDir) throws Exception {
|
||||||
|
Files.writeString(tempDir.resolve("PaymentService.java"), """
|
||||||
|
package com.example;
|
||||||
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
|
import org.springframework.statemachine.StateMachine;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
@Service
|
||||||
|
public class PaymentService {
|
||||||
|
private final StateMachine<String, String> orderMachine;
|
||||||
|
private final StateMachine<String, String> paymentMachine;
|
||||||
|
public PaymentService(StateMachine<String, String> orderMachine,
|
||||||
|
@Qualifier("paymentStateMachine") StateMachine<String, String> paymentMachine) {
|
||||||
|
this.orderMachine = orderMachine;
|
||||||
|
this.paymentMachine = paymentMachine;
|
||||||
|
}
|
||||||
|
public void captureOnOrder() {
|
||||||
|
orderMachine.sendEvent("CAPTURE");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@org.springframework.context.annotation.Configuration
|
||||||
|
@org.springframework.statemachine.config.EnableStateMachine(name = "paymentStateMachine")
|
||||||
|
class PaymentStateMachineConfig
|
||||||
|
extends org.springframework.statemachine.config.StateMachineConfigurerAdapter<String, String> {}
|
||||||
|
@org.springframework.context.annotation.Configuration
|
||||||
|
@org.springframework.statemachine.config.EnableStateMachine(name = "orderStateMachine")
|
||||||
|
class OrderStateMachineConfig
|
||||||
|
extends org.springframework.statemachine.config.StateMachineConfigurerAdapter<String, String> {}
|
||||||
|
""");
|
||||||
|
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.setResolveBindings(true);
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
HeuristicBeanResolutionEngine engine = new HeuristicBeanResolutionEngine();
|
||||||
|
CallChain chain = CallChain.builder()
|
||||||
|
.triggerPoint(TriggerPoint.builder()
|
||||||
|
.className("com.example.PaymentService")
|
||||||
|
.methodName("captureOnOrder")
|
||||||
|
.lineNumber(18)
|
||||||
|
.event("CAPTURE")
|
||||||
|
.eventTypeFqn("java.lang.String")
|
||||||
|
.stateTypeFqn("java.lang.String")
|
||||||
|
.build())
|
||||||
|
.methodChain(List.of("com.example.PaymentService.captureOnOrder()"))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// Both are String,String machines and receiver has no explicit qualifier -> should fail closed.
|
||||||
|
assertThat(engine.isRoutedToCorrectMachine(chain, "com.example.PaymentStateMachineConfig", context)).isFalse();
|
||||||
|
assertThat(engine.isRoutedToCorrectMachine(chain, "com.example.OrderStateMachineConfig", context)).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldFailClosedForAmbiguousStringStateMachineWithoutQualifier(@TempDir Path tempDir) throws Exception {
|
void shouldFailClosedForAmbiguousStringStateMachineWithoutQualifier(@TempDir Path tempDir) throws Exception {
|
||||||
Files.writeString(tempDir.resolve("OrderService.java"), """
|
Files.writeString(tempDir.resolve("OrderService.java"), """
|
||||||
|
|||||||
Reference in New Issue
Block a user