GH-1498: early steps to index bean registrations from bean registrars

This commit is contained in:
Martin Lippert
2025-03-11 10:32:43 +01:00
parent 281cfa1306
commit e4a0dece0f
5 changed files with 323 additions and 28 deletions

View File

@@ -99,6 +99,10 @@ public class Annotations {
public static final String APPLICATION_LISTENER = "org.springframework.context.ApplicationListener";
public static final String EVENT_PUBLISHER = "org.springframework.context.ApplicationEventPublisher";
public static final String BEAN_REGISTRAR_INTERFACE = "org.springframework.beans.factory.BeanRegistrar";
public static final String BEAN_REGISTRY_INTERFACE = "org.springframework.beans.factory.BeanRegistry";
public static final Map<String, String> AOP_ANNOTATIONS = Map.of(
"org.aspectj.lang.annotation.Pointcut", "Pointcut",
"org.aspectj.lang.annotation.Before", "Before",
@@ -108,7 +112,6 @@ public class Annotations {
"org.aspectj.lang.annotation.AfterThrowing", "AfterThrowing",
"org.aspectj.lang.annotation.DeclareParents", "DeclareParents"
);
}

View File

@@ -10,6 +10,7 @@
*******************************************************************************/
package org.springframework.ide.vscode.boot.java.beans;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
@@ -18,9 +19,11 @@ import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.AbstractTypeDeclaration;
import org.eclipse.jdt.core.dom.Annotation;
import org.eclipse.jdt.core.dom.Block;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.IMethodBinding;
import org.eclipse.jdt.core.dom.ITypeBinding;
@@ -140,6 +143,7 @@ public class ComponentSymbolProvider implements SymbolProvider {
indexEventListenerInterfaceImplementation(beanDefinition, type, context, doc);
indexRequestMappings(beanDefinition, type, annotationType, metaAnnotations, context, doc);
indexConfigurationProperties(beanDefinition, type, context, doc);
indexBeanRegistrarImplementation(beanDefinition, type, context, doc);
context.getGeneratedSymbols().add(new CachedSymbol(context.getDocURI(), context.getLastModified(), symbol));
context.getBeans().add(new CachedBean(context.getDocURI(), beanDefinition));
@@ -321,6 +325,7 @@ public class ComponentSymbolProvider implements SymbolProvider {
// check for event listener implementations on classes that are not annotated with component, but created via bean methods (for example)
if (!isComponment) {
indexEventListenerInterfaceImplementation(null, typeDeclaration, context, doc);
indexBeanRegistrarImplementation(null, typeDeclaration, context, doc);
}
}
@@ -377,6 +382,191 @@ public class ComponentSymbolProvider implements SymbolProvider {
return null;
}
private MethodDeclaration findRegisterMethod(TypeDeclaration type, ITypeBinding beanRegistrarType) {
IMethodBinding[] beanRegistrarMethods = beanRegistrarType.getDeclaredMethods();
if (beanRegistrarMethods == null || beanRegistrarMethods.length != 1 || !"register".equals(beanRegistrarMethods[0].getName())) {
return null;
}
MethodDeclaration[] methods = type.getMethods();
for (MethodDeclaration method : methods) {
IMethodBinding binding = method.resolveBinding();
boolean overrides = binding.overrides(beanRegistrarMethods[0]);
if (overrides) {
return method;
}
}
return null;
}
private void indexBeanRegistrarImplementation(Bean bean, TypeDeclaration typeDeclaration, SpringIndexerJavaContext context, TextDocument doc) {
try {
ITypeBinding typeBinding = typeDeclaration.resolveBinding();
if (typeBinding == null) return;
ITypeBinding inTypeHierarchy = ASTUtils.findInTypeHierarchy(typeDeclaration, doc, typeBinding, Set.of(Annotations.BEAN_REGISTRAR_INTERFACE));
if (inTypeHierarchy == null) return;
MethodDeclaration registerMethod = findRegisterMethod(typeDeclaration, inTypeHierarchy);
if (registerMethod == null) return;
if (!context.isFullAst()) { // needs full method bodies to continue
throw new RequiredCompleteAstException();
}
if (bean == null) { // need to create and register bean element
String beanType = typeBinding.getQualifiedName();
String beanName = BeanUtils.getBeanNameFromType(typeBinding.getName());
Location location = new Location(doc.getUri(), doc.toRange(typeDeclaration.getStartPosition(), typeDeclaration.getLength()));
WorkspaceSymbol symbol = new WorkspaceSymbol(
beanLabel("+", null, null, beanName, beanType),
SymbolKind.Class,
Either.forLeft(location));
InjectionPoint[] injectionPoints = ASTUtils.findInjectionPoints(typeDeclaration, doc);
Set<String> supertypes = new HashSet<>();
ASTUtils.findSupertypes(typeBinding, supertypes);
Collection<Annotation> annotationsOnMethod = ASTUtils.getAnnotations(typeDeclaration);
AnnotationMetadata[] annotations = ASTUtils.getAnnotationsMetadata(annotationsOnMethod, doc);
bean = new Bean(beanName, beanType, location, injectionPoints, supertypes, annotations, false, symbol.getName());
context.getGeneratedSymbols().add(new CachedSymbol(context.getDocURI(), context.getLastModified(), symbol));
context.getBeans().add(new CachedBean(context.getDocURI(), bean));
}
scanBeanRegistryInvocations(bean, registerMethod.getBody(), context, doc);
} catch (BadLocationException e) {
log.error("", e);
}
}
private void scanBeanRegistryInvocations(Bean component, Block body, SpringIndexerJavaContext context, TextDocument doc) {
if (body == null) {
return;
}
body.accept(new ASTVisitor() {
@Override
public boolean visit(MethodInvocation methodInvocation) {
try {
String methodName = methodInvocation.getName().toString();
if ("registerBean".equals(methodName)) {
IMethodBinding methodBinding = methodInvocation.resolveMethodBinding();
ITypeBinding declaringClass = methodBinding.getDeclaringClass();
if (declaringClass != null && Annotations.BEAN_REGISTRY_INTERFACE.equals(declaringClass.getQualifiedName())) {
@SuppressWarnings("unchecked")
List<Expression> arguments = methodInvocation.arguments();
List<ITypeBinding> types = new ArrayList<>();
for (Expression argument : arguments) {
ITypeBinding typeBinding = argument.resolveTypeBinding();
if (typeBinding != null) {
types.add(typeBinding);
}
else {
return true;
}
}
if (arguments.size() == 1 && "java.lang.Class".equals(types.get(0).getBinaryName())) {
// <T> String registerBean(Class<T> beanClass);
ITypeBinding typeBinding = types.get(0);
ITypeBinding[] typeParameters = typeBinding.getTypeArguments();
if (typeParameters != null && typeParameters.length == 1) {
String typeParamName = typeParameters[0].getBinaryName();
String beanName = BeanUtils.getBeanNameFromType(typeParameters[0].getName());
String beanType = typeParamName;
createBean(component, beanName, beanType, typeParameters[0], methodInvocation, context, doc);
}
}
else if (arguments.size() == 2 && "java.lang.String".equals(types.get(0).getQualifiedName()) && "java.lang.Class".equals(types.get(1).getBinaryName())) {
// <T> void registerBean(String name, Class<T> beanClass);
String beanName = ASTUtils.getExpressionValueAsString(arguments.get(0), (dep) -> {});
ITypeBinding typeBinding = types.get(1);
ITypeBinding[] typeParameters = typeBinding.getTypeArguments();
if (typeParameters != null && typeParameters.length == 1) {
String typeParamName = typeParameters[0].getBinaryName();
String beanType = typeParamName;
createBean(component, beanName, beanType, typeParameters[0], methodInvocation, context, doc);
}
}
else if (arguments.size() == 2 && "java.lang.Class".equals(types.get(0).getBinaryName()) && "java.util.function.Consumer".equals(types.get(1).getBinaryName())) {
// <T> String registerBean(Class<T> beanClass, Consumer<Spec<T>> customizer);
ITypeBinding typeBinding = types.get(0);
ITypeBinding[] typeParameters = typeBinding.getTypeArguments();
if (typeParameters != null && typeParameters.length == 1) {
String typeParamName = typeParameters[0].getBinaryName();
String beanName = BeanUtils.getBeanNameFromType(typeParameters[0].getName());
String beanType = typeParamName;
createBean(component, beanName, beanType, typeParameters[0], methodInvocation, context, doc);
}
}
else if (arguments.size() == 3 && "java.lang.String".equals(types.get(0).getQualifiedName())
&& "java.lang.Class".equals(types.get(1).getBinaryName()) && "java.util.function.Consumer".equals(types.get(2).getBinaryName())) {
// <T> void registerBean(String name, Class<T> beanClass, Consumer<Spec<T>> customizer);
String beanName = ASTUtils.getExpressionValueAsString(arguments.get(0), (dep) -> {});
ITypeBinding typeBinding = types.get(1);
ITypeBinding[] typeParameters = typeBinding.getTypeArguments();
if (typeParameters != null && typeParameters.length == 1) {
String typeParamName = typeParameters[0].getBinaryName();
String beanType = typeParamName;
createBean(component, beanName, beanType, typeParameters[0], methodInvocation, context, doc);
}
}
}
}
} catch (BadLocationException e) {
log.error("", e);
}
return super.visit(methodInvocation);
}
});
}
public void createBean(Bean parentBean, String beanName, String beanType, ITypeBinding beanTypeBinding, ASTNode node, SpringIndexerJavaContext context, TextDocument doc) throws BadLocationException {
Location location = new Location(doc.getUri(), doc.toRange(node.getStartPosition(), node.getLength()));
WorkspaceSymbol symbol = new WorkspaceSymbol(
beanLabel("+", null, null, beanName, beanType),
SymbolKind.Class,
Either.forLeft(location));
context.getGeneratedSymbols().add(new CachedSymbol(context.getDocURI(), context.getLastModified(), symbol));
InjectionPoint[] injectionPoints = DefaultValues.EMPTY_INJECTION_POINTS;
Set<String> supertypes = new HashSet<>();
ASTUtils.findSupertypes(beanTypeBinding, supertypes);
AnnotationMetadata[] annotations = DefaultValues.EMPTY_ANNOTATIONS;
Bean bean = new Bean(beanName, beanType, location, injectionPoints, supertypes, annotations, false, symbol.getName());
parentBean.addChild(bean);
}
public static String beanLabel(String searchPrefix, String annotationTypeName, Collection<String> metaAnnotationNames, String beanName, String beanType) {
StringBuilder symbolLabel = new StringBuilder();
symbolLabel.append("@");
@@ -385,21 +575,25 @@ public class ComponentSymbolProvider implements SymbolProvider {
symbolLabel.append('\'');
symbolLabel.append(beanName);
symbolLabel.append('\'');
symbolLabel.append(" (@");
symbolLabel.append(annotationTypeName);
if (!metaAnnotationNames.isEmpty()) {
symbolLabel.append(" <: ");
boolean first = true;
for (String ma : metaAnnotationNames) {
if (!first) {
symbolLabel.append(", ");
if (annotationTypeName != null) {
symbolLabel.append(" (@");
symbolLabel.append(annotationTypeName);
if (!metaAnnotationNames.isEmpty()) {
symbolLabel.append(" <: ");
boolean first = true;
for (String ma : metaAnnotationNames) {
if (!first) {
symbolLabel.append(", ");
}
symbolLabel.append("@");
symbolLabel.append(ma);
first = false;
}
symbolLabel.append("@");
symbolLabel.append(ma);
first = false;
}
symbolLabel.append(")");
}
symbolLabel.append(") ");
symbolLabel.append(" ");
symbolLabel.append(beanType);
return symbolLabel.toString();
}

View File

@@ -403,7 +403,7 @@ public class ASTUtils {
simplifiedType = resolvedInterface.getBinaryName();
}
else {
simplifiedType = resolvedType.getQualifiedName();
simplifiedType = resolvedInterface.getQualifiedName();
}
if (typesToCheck.contains(simplifiedType)) {

View File

@@ -0,0 +1,98 @@
/*******************************************************************************
* Copyright (c) 2025 Broadcom
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Broadcom - initial API and implementation
*******************************************************************************/
package org.springframework.ide.vscode.boot.index.test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.io.File;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import org.eclipse.lsp4j.TextDocumentIdentifier;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Import;
import org.springframework.ide.vscode.boot.app.SpringSymbolIndex;
import org.springframework.ide.vscode.boot.bootiful.BootLanguageServerTest;
import org.springframework.ide.vscode.boot.bootiful.SymbolProviderTestConf;
import org.springframework.ide.vscode.boot.index.SpringMetamodelIndex;
import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder;
import org.springframework.ide.vscode.commons.protocol.spring.Bean;
import org.springframework.ide.vscode.commons.protocol.spring.SpringIndexElement;
import org.springframework.ide.vscode.project.harness.BootLanguageServerHarness;
import org.springframework.ide.vscode.project.harness.ProjectsHarness;
import org.springframework.test.context.junit.jupiter.SpringExtension;
/**
* @author Martin Lippert
*/
@ExtendWith(SpringExtension.class)
@BootLanguageServerTest
@Import(SymbolProviderTestConf.class)
public class SpringIndexerBeanRegistrarTest {
@Autowired private BootLanguageServerHarness harness;
@Autowired private JavaProjectFinder projectFinder;
@Autowired private SpringSymbolIndex indexer;
@Autowired private SpringMetamodelIndex springIndex;
private File directory;
@BeforeEach
public void setup() throws Exception {
harness.intialize(null);
directory = new File(ProjectsHarness.class.getResource("/test-projects/test-framework-7-indexing/").toURI());
String projectDir = directory.toURI().toString();
// trigger project creation
projectFinder.find(new TextDocumentIdentifier(projectDir)).get();
CompletableFuture<Void> initProject = indexer.waitOperation();
initProject.get(5, TimeUnit.SECONDS);
}
@Test
void testSimpleBeanRegistration() throws Exception {
String docUri = directory.toPath().resolve("src/main/java/com/example/MyBeanRegistrar.java").toUri().toString();
Bean[] beans = springIndex.getBeansOfDocument(docUri);
assertEquals(5, beans.length);
Bean beanRegistrarBean = Arrays.stream(beans).filter(bean -> bean.getName().equals("myBeanRegistrar")).findFirst().get();
assertEquals("com.example.MyBeanRegistrar", beanRegistrarBean.getType());
List<SpringIndexElement> children = beanRegistrarBean.getChildren();
assertEquals(4, children.size());
Bean fooFoo = (Bean) children.get(0);
assertEquals("fooFoo", fooFoo.getName());
assertEquals("com.example.FooFoo", fooFoo.getType());
Bean foo = (Bean) children.get(1);
assertEquals("foo", foo.getName());
assertEquals("com.example.Foo", foo.getType());
Bean bar = (Bean) children.get(2);
assertEquals("bar", bar.getName());
assertEquals("com.example.Bar", bar.getType());
Bean baz = (Bean) children.get(3);
assertEquals("baz", baz.getName());
assertEquals("com.example.Baz", baz.getType());
}
}

View File

@@ -6,19 +6,19 @@ import org.springframework.core.env.Environment;
public class MyBeanRegistrar implements BeanRegistrar {
@Override
public void register(BeanRegistry registry, Environment env) {
registry.registerBean(FooFoo.class);
registry.registerBean("foo", Foo.class);
registry.registerBean("bar", Bar.class, spec -> spec
.prototype()
.lazyInit()
.description("Custom description")
.supplier(context -> new Bar(context.bean(Foo.class))));
if (env.matchesProfiles("baz")) {
registry.registerBean(Baz.class, spec -> spec
.supplier(context -> new Baz("Hello World!")));
@Override
public void register(BeanRegistry registry, Environment env) {
registry.registerBean(FooFoo.class);
registry.registerBean("foo", Foo.class);
registry.registerBean("bar", Bar.class, spec -> spec
.prototype()
.lazyInit()
.description("Custom description")
.supplier(context -> new Bar(context.bean(Foo.class))));
if (env.matchesProfiles("baz")) {
registry.registerBean(Baz.class, spec -> spec
.supplier(context -> new Baz("Hello World!")));
}
}
}
}