Skip to content

Commit e4a0dec

Browse files
committedMar 12, 2025
GH-1498: early steps to index bean registrations from bean registrars
1 parent 281cfa1 commit e4a0dec

File tree

5 files changed

+323
-28
lines changed

5 files changed

+323
-28
lines changed
 

Diff for: ‎headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/Annotations.java

+5-2
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,10 @@ public class Annotations {
9999
public static final String APPLICATION_LISTENER = "org.springframework.context.ApplicationListener";
100100
public static final String EVENT_PUBLISHER = "org.springframework.context.ApplicationEventPublisher";
101101

102+
public static final String BEAN_REGISTRAR_INTERFACE = "org.springframework.beans.factory.BeanRegistrar";
103+
public static final String BEAN_REGISTRY_INTERFACE = "org.springframework.beans.factory.BeanRegistry";
104+
105+
102106
public static final Map<String, String> AOP_ANNOTATIONS = Map.of(
103107
"org.aspectj.lang.annotation.Pointcut", "Pointcut",
104108
"org.aspectj.lang.annotation.Before", "Before",
@@ -108,7 +112,6 @@ public class Annotations {
108112
"org.aspectj.lang.annotation.AfterThrowing", "AfterThrowing",
109113
"org.aspectj.lang.annotation.DeclareParents", "DeclareParents"
110114
);
111-
112-
115+
113116

114117
}

Diff for: ‎headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/ComponentSymbolProvider.java

+206-12
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
*******************************************************************************/
1111
package org.springframework.ide.vscode.boot.java.beans;
1212

13+
import java.util.ArrayList;
1314
import java.util.Arrays;
1415
import java.util.Collection;
1516
import java.util.HashSet;
@@ -18,9 +19,11 @@
1819
import java.util.stream.Collectors;
1920
import java.util.stream.Stream;
2021

22+
import org.eclipse.jdt.core.dom.ASTNode;
2123
import org.eclipse.jdt.core.dom.ASTVisitor;
2224
import org.eclipse.jdt.core.dom.AbstractTypeDeclaration;
2325
import org.eclipse.jdt.core.dom.Annotation;
26+
import org.eclipse.jdt.core.dom.Block;
2427
import org.eclipse.jdt.core.dom.Expression;
2528
import org.eclipse.jdt.core.dom.IMethodBinding;
2629
import org.eclipse.jdt.core.dom.ITypeBinding;
@@ -140,6 +143,7 @@ private void createSymbol(TypeDeclaration type, Annotation node, ITypeBinding an
140143
indexEventListenerInterfaceImplementation(beanDefinition, type, context, doc);
141144
indexRequestMappings(beanDefinition, type, annotationType, metaAnnotations, context, doc);
142145
indexConfigurationProperties(beanDefinition, type, context, doc);
146+
indexBeanRegistrarImplementation(beanDefinition, type, context, doc);
143147

144148
context.getGeneratedSymbols().add(new CachedSymbol(context.getDocURI(), context.getLastModified(), symbol));
145149
context.getBeans().add(new CachedBean(context.getDocURI(), beanDefinition));
@@ -321,6 +325,7 @@ public void addSymbols(TypeDeclaration typeDeclaration, SpringIndexerJavaContext
321325
// check for event listener implementations on classes that are not annotated with component, but created via bean methods (for example)
322326
if (!isComponment) {
323327
indexEventListenerInterfaceImplementation(null, typeDeclaration, context, doc);
328+
indexBeanRegistrarImplementation(null, typeDeclaration, context, doc);
324329
}
325330

326331
}
@@ -377,6 +382,191 @@ private MethodDeclaration findHandleEventMethod(TypeDeclaration type) {
377382
return null;
378383
}
379384

385+
private MethodDeclaration findRegisterMethod(TypeDeclaration type, ITypeBinding beanRegistrarType) {
386+
IMethodBinding[] beanRegistrarMethods = beanRegistrarType.getDeclaredMethods();
387+
if (beanRegistrarMethods == null || beanRegistrarMethods.length != 1 || !"register".equals(beanRegistrarMethods[0].getName())) {
388+
return null;
389+
}
390+
391+
MethodDeclaration[] methods = type.getMethods();
392+
393+
for (MethodDeclaration method : methods) {
394+
IMethodBinding binding = method.resolveBinding();
395+
boolean overrides = binding.overrides(beanRegistrarMethods[0]);
396+
if (overrides) {
397+
return method;
398+
}
399+
}
400+
401+
return null;
402+
}
403+
404+
private void indexBeanRegistrarImplementation(Bean bean, TypeDeclaration typeDeclaration, SpringIndexerJavaContext context, TextDocument doc) {
405+
try {
406+
ITypeBinding typeBinding = typeDeclaration.resolveBinding();
407+
if (typeBinding == null) return;
408+
409+
ITypeBinding inTypeHierarchy = ASTUtils.findInTypeHierarchy(typeDeclaration, doc, typeBinding, Set.of(Annotations.BEAN_REGISTRAR_INTERFACE));
410+
if (inTypeHierarchy == null) return;
411+
412+
MethodDeclaration registerMethod = findRegisterMethod(typeDeclaration, inTypeHierarchy);
413+
if (registerMethod == null) return;
414+
415+
if (!context.isFullAst()) { // needs full method bodies to continue
416+
throw new RequiredCompleteAstException();
417+
}
418+
419+
if (bean == null) { // need to create and register bean element
420+
String beanType = typeBinding.getQualifiedName();
421+
String beanName = BeanUtils.getBeanNameFromType(typeBinding.getName());
422+
423+
Location location = new Location(doc.getUri(), doc.toRange(typeDeclaration.getStartPosition(), typeDeclaration.getLength()));
424+
425+
WorkspaceSymbol symbol = new WorkspaceSymbol(
426+
beanLabel("+", null, null, beanName, beanType),
427+
SymbolKind.Class,
428+
Either.forLeft(location));
429+
430+
InjectionPoint[] injectionPoints = ASTUtils.findInjectionPoints(typeDeclaration, doc);
431+
432+
Set<String> supertypes = new HashSet<>();
433+
ASTUtils.findSupertypes(typeBinding, supertypes);
434+
435+
Collection<Annotation> annotationsOnMethod = ASTUtils.getAnnotations(typeDeclaration);
436+
AnnotationMetadata[] annotations = ASTUtils.getAnnotationsMetadata(annotationsOnMethod, doc);
437+
438+
bean = new Bean(beanName, beanType, location, injectionPoints, supertypes, annotations, false, symbol.getName());
439+
440+
context.getGeneratedSymbols().add(new CachedSymbol(context.getDocURI(), context.getLastModified(), symbol));
441+
context.getBeans().add(new CachedBean(context.getDocURI(), bean));
442+
}
443+
444+
scanBeanRegistryInvocations(bean, registerMethod.getBody(), context, doc);
445+
446+
} catch (BadLocationException e) {
447+
log.error("", e);
448+
}
449+
}
450+
451+
private void scanBeanRegistryInvocations(Bean component, Block body, SpringIndexerJavaContext context, TextDocument doc) {
452+
if (body == null) {
453+
return;
454+
}
455+
456+
body.accept(new ASTVisitor() {
457+
458+
@Override
459+
public boolean visit(MethodInvocation methodInvocation) {
460+
try {
461+
String methodName = methodInvocation.getName().toString();
462+
if ("registerBean".equals(methodName)) {
463+
464+
IMethodBinding methodBinding = methodInvocation.resolveMethodBinding();
465+
ITypeBinding declaringClass = methodBinding.getDeclaringClass();
466+
467+
if (declaringClass != null && Annotations.BEAN_REGISTRY_INTERFACE.equals(declaringClass.getQualifiedName())) {
468+
469+
@SuppressWarnings("unchecked")
470+
List<Expression> arguments = methodInvocation.arguments();
471+
List<ITypeBinding> types = new ArrayList<>();
472+
473+
for (Expression argument : arguments) {
474+
ITypeBinding typeBinding = argument.resolveTypeBinding();
475+
if (typeBinding != null) {
476+
types.add(typeBinding);
477+
}
478+
else {
479+
return true;
480+
}
481+
}
482+
483+
if (arguments.size() == 1 && "java.lang.Class".equals(types.get(0).getBinaryName())) {
484+
// <T> String registerBean(Class<T> beanClass);
485+
486+
ITypeBinding typeBinding = types.get(0);
487+
ITypeBinding[] typeParameters = typeBinding.getTypeArguments();
488+
if (typeParameters != null && typeParameters.length == 1) {
489+
String typeParamName = typeParameters[0].getBinaryName();
490+
491+
String beanName = BeanUtils.getBeanNameFromType(typeParameters[0].getName());
492+
String beanType = typeParamName;
493+
494+
createBean(component, beanName, beanType, typeParameters[0], methodInvocation, context, doc);
495+
}
496+
}
497+
else if (arguments.size() == 2 && "java.lang.String".equals(types.get(0).getQualifiedName()) && "java.lang.Class".equals(types.get(1).getBinaryName())) {
498+
// <T> void registerBean(String name, Class<T> beanClass);
499+
500+
String beanName = ASTUtils.getExpressionValueAsString(arguments.get(0), (dep) -> {});
501+
502+
ITypeBinding typeBinding = types.get(1);
503+
ITypeBinding[] typeParameters = typeBinding.getTypeArguments();
504+
if (typeParameters != null && typeParameters.length == 1) {
505+
String typeParamName = typeParameters[0].getBinaryName();
506+
String beanType = typeParamName;
507+
508+
createBean(component, beanName, beanType, typeParameters[0], methodInvocation, context, doc);
509+
}
510+
}
511+
else if (arguments.size() == 2 && "java.lang.Class".equals(types.get(0).getBinaryName()) && "java.util.function.Consumer".equals(types.get(1).getBinaryName())) {
512+
// <T> String registerBean(Class<T> beanClass, Consumer<Spec<T>> customizer);
513+
514+
ITypeBinding typeBinding = types.get(0);
515+
ITypeBinding[] typeParameters = typeBinding.getTypeArguments();
516+
if (typeParameters != null && typeParameters.length == 1) {
517+
String typeParamName = typeParameters[0].getBinaryName();
518+
519+
String beanName = BeanUtils.getBeanNameFromType(typeParameters[0].getName());
520+
String beanType = typeParamName;
521+
522+
createBean(component, beanName, beanType, typeParameters[0], methodInvocation, context, doc);
523+
}
524+
}
525+
else if (arguments.size() == 3 && "java.lang.String".equals(types.get(0).getQualifiedName())
526+
&& "java.lang.Class".equals(types.get(1).getBinaryName()) && "java.util.function.Consumer".equals(types.get(2).getBinaryName())) {
527+
// <T> void registerBean(String name, Class<T> beanClass, Consumer<Spec<T>> customizer);
528+
529+
String beanName = ASTUtils.getExpressionValueAsString(arguments.get(0), (dep) -> {});
530+
531+
ITypeBinding typeBinding = types.get(1);
532+
ITypeBinding[] typeParameters = typeBinding.getTypeArguments();
533+
if (typeParameters != null && typeParameters.length == 1) {
534+
String typeParamName = typeParameters[0].getBinaryName();
535+
String beanType = typeParamName;
536+
537+
createBean(component, beanName, beanType, typeParameters[0], methodInvocation, context, doc);
538+
}
539+
}
540+
}
541+
}
542+
543+
} catch (BadLocationException e) {
544+
log.error("", e);
545+
}
546+
return super.visit(methodInvocation);
547+
}
548+
});
549+
}
550+
551+
public void createBean(Bean parentBean, String beanName, String beanType, ITypeBinding beanTypeBinding, ASTNode node, SpringIndexerJavaContext context, TextDocument doc) throws BadLocationException {
552+
Location location = new Location(doc.getUri(), doc.toRange(node.getStartPosition(), node.getLength()));
553+
554+
WorkspaceSymbol symbol = new WorkspaceSymbol(
555+
beanLabel("+", null, null, beanName, beanType),
556+
SymbolKind.Class,
557+
Either.forLeft(location));
558+
context.getGeneratedSymbols().add(new CachedSymbol(context.getDocURI(), context.getLastModified(), symbol));
559+
560+
InjectionPoint[] injectionPoints = DefaultValues.EMPTY_INJECTION_POINTS;
561+
Set<String> supertypes = new HashSet<>();
562+
ASTUtils.findSupertypes(beanTypeBinding, supertypes);
563+
564+
AnnotationMetadata[] annotations = DefaultValues.EMPTY_ANNOTATIONS;
565+
566+
Bean bean = new Bean(beanName, beanType, location, injectionPoints, supertypes, annotations, false, symbol.getName());
567+
parentBean.addChild(bean);
568+
}
569+
380570
public static String beanLabel(String searchPrefix, String annotationTypeName, Collection<String> metaAnnotationNames, String beanName, String beanType) {
381571
StringBuilder symbolLabel = new StringBuilder();
382572
symbolLabel.append("@");
@@ -385,21 +575,25 @@ public static String beanLabel(String searchPrefix, String annotationTypeName, C
385575
symbolLabel.append('\'');
386576
symbolLabel.append(beanName);
387577
symbolLabel.append('\'');
388-
symbolLabel.append(" (@");
389-
symbolLabel.append(annotationTypeName);
390-
if (!metaAnnotationNames.isEmpty()) {
391-
symbolLabel.append(" <: ");
392-
boolean first = true;
393-
for (String ma : metaAnnotationNames) {
394-
if (!first) {
395-
symbolLabel.append(", ");
578+
579+
if (annotationTypeName != null) {
580+
symbolLabel.append(" (@");
581+
symbolLabel.append(annotationTypeName);
582+
if (!metaAnnotationNames.isEmpty()) {
583+
symbolLabel.append(" <: ");
584+
boolean first = true;
585+
for (String ma : metaAnnotationNames) {
586+
if (!first) {
587+
symbolLabel.append(", ");
588+
}
589+
symbolLabel.append("@");
590+
symbolLabel.append(ma);
591+
first = false;
396592
}
397-
symbolLabel.append("@");
398-
symbolLabel.append(ma);
399-
first = false;
400593
}
594+
symbolLabel.append(")");
401595
}
402-
symbolLabel.append(") ");
596+
symbolLabel.append(" ");
403597
symbolLabel.append(beanType);
404598
return symbolLabel.toString();
405599
}

Diff for: ‎headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/ASTUtils.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -403,7 +403,7 @@ public static ITypeBinding findInTypeHierarchy(TypeDeclaration typeDeclaration,
403403
simplifiedType = resolvedInterface.getBinaryName();
404404
}
405405
else {
406-
simplifiedType = resolvedType.getQualifiedName();
406+
simplifiedType = resolvedInterface.getQualifiedName();
407407
}
408408

409409
if (typesToCheck.contains(simplifiedType)) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2025 Broadcom
3+
* All rights reserved. This program and the accompanying materials
4+
* are made available under the terms of the Eclipse Public License v1.0
5+
* which accompanies this distribution, and is available at
6+
* https://www.eclipse.org/legal/epl-v10.html
7+
*
8+
* Contributors:
9+
* Broadcom - initial API and implementation
10+
*******************************************************************************/
11+
package org.springframework.ide.vscode.boot.index.test;
12+
13+
import static org.junit.jupiter.api.Assertions.assertEquals;
14+
15+
import java.io.File;
16+
import java.util.Arrays;
17+
import java.util.List;
18+
import java.util.concurrent.CompletableFuture;
19+
import java.util.concurrent.TimeUnit;
20+
21+
import org.eclipse.lsp4j.TextDocumentIdentifier;
22+
import org.junit.jupiter.api.BeforeEach;
23+
import org.junit.jupiter.api.Test;
24+
import org.junit.jupiter.api.extension.ExtendWith;
25+
import org.springframework.beans.factory.annotation.Autowired;
26+
import org.springframework.context.annotation.Import;
27+
import org.springframework.ide.vscode.boot.app.SpringSymbolIndex;
28+
import org.springframework.ide.vscode.boot.bootiful.BootLanguageServerTest;
29+
import org.springframework.ide.vscode.boot.bootiful.SymbolProviderTestConf;
30+
import org.springframework.ide.vscode.boot.index.SpringMetamodelIndex;
31+
import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder;
32+
import org.springframework.ide.vscode.commons.protocol.spring.Bean;
33+
import org.springframework.ide.vscode.commons.protocol.spring.SpringIndexElement;
34+
import org.springframework.ide.vscode.project.harness.BootLanguageServerHarness;
35+
import org.springframework.ide.vscode.project.harness.ProjectsHarness;
36+
import org.springframework.test.context.junit.jupiter.SpringExtension;
37+
38+
/**
39+
* @author Martin Lippert
40+
*/
41+
@ExtendWith(SpringExtension.class)
42+
@BootLanguageServerTest
43+
@Import(SymbolProviderTestConf.class)
44+
public class SpringIndexerBeanRegistrarTest {
45+
46+
@Autowired private BootLanguageServerHarness harness;
47+
@Autowired private JavaProjectFinder projectFinder;
48+
@Autowired private SpringSymbolIndex indexer;
49+
@Autowired private SpringMetamodelIndex springIndex;
50+
51+
private File directory;
52+
53+
@BeforeEach
54+
public void setup() throws Exception {
55+
harness.intialize(null);
56+
57+
directory = new File(ProjectsHarness.class.getResource("/test-projects/test-framework-7-indexing/").toURI());
58+
59+
String projectDir = directory.toURI().toString();
60+
61+
// trigger project creation
62+
projectFinder.find(new TextDocumentIdentifier(projectDir)).get();
63+
64+
CompletableFuture<Void> initProject = indexer.waitOperation();
65+
initProject.get(5, TimeUnit.SECONDS);
66+
}
67+
68+
@Test
69+
void testSimpleBeanRegistration() throws Exception {
70+
String docUri = directory.toPath().resolve("src/main/java/com/example/MyBeanRegistrar.java").toUri().toString();
71+
72+
Bean[] beans = springIndex.getBeansOfDocument(docUri);
73+
assertEquals(5, beans.length);
74+
75+
Bean beanRegistrarBean = Arrays.stream(beans).filter(bean -> bean.getName().equals("myBeanRegistrar")).findFirst().get();
76+
assertEquals("com.example.MyBeanRegistrar", beanRegistrarBean.getType());
77+
78+
List<SpringIndexElement> children = beanRegistrarBean.getChildren();
79+
assertEquals(4, children.size());
80+
81+
Bean fooFoo = (Bean) children.get(0);
82+
assertEquals("fooFoo", fooFoo.getName());
83+
assertEquals("com.example.FooFoo", fooFoo.getType());
84+
85+
Bean foo = (Bean) children.get(1);
86+
assertEquals("foo", foo.getName());
87+
assertEquals("com.example.Foo", foo.getType());
88+
89+
Bean bar = (Bean) children.get(2);
90+
assertEquals("bar", bar.getName());
91+
assertEquals("com.example.Bar", bar.getType());
92+
93+
Bean baz = (Bean) children.get(3);
94+
assertEquals("baz", baz.getName());
95+
assertEquals("com.example.Baz", baz.getType());
96+
}
97+
98+
}

Diff for: ‎headless-services/spring-boot-language-server/src/test/resources/test-projects/test-framework-7-indexing/src/main/java/com/example/MyBeanRegistrar.java

+13-13
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,19 @@
66

77
public class MyBeanRegistrar implements BeanRegistrar {
88

9-
@Override
10-
public void register(BeanRegistry registry, Environment env) {
11-
registry.registerBean(FooFoo.class);
12-
registry.registerBean("foo", Foo.class);
13-
registry.registerBean("bar", Bar.class, spec -> spec
14-
.prototype()
15-
.lazyInit()
16-
.description("Custom description")
17-
.supplier(context -> new Bar(context.bean(Foo.class))));
18-
if (env.matchesProfiles("baz")) {
19-
registry.registerBean(Baz.class, spec -> spec
20-
.supplier(context -> new Baz("Hello World!")));
9+
@Override
10+
public void register(BeanRegistry registry, Environment env) {
11+
registry.registerBean(FooFoo.class);
12+
registry.registerBean("foo", Foo.class);
13+
registry.registerBean("bar", Bar.class, spec -> spec
14+
.prototype()
15+
.lazyInit()
16+
.description("Custom description")
17+
.supplier(context -> new Bar(context.bean(Foo.class))));
18+
if (env.matchesProfiles("baz")) {
19+
registry.registerBean(Baz.class, spec -> spec
20+
.supplier(context -> new Baz("Hello World!")));
21+
}
2122
}
22-
}
2323

2424
}

0 commit comments

Comments
 (0)
Please sign in to comment.