enable feign client and feign config indexing and reconciling to keep track of dependencies and trigger re-reconciling

This commit is contained in:
Martin Lippert
2025-03-27 12:03:23 +01:00
parent f08bfb3f47
commit ca4fc3c0f4
10 changed files with 310 additions and 17 deletions

View File

@@ -38,6 +38,7 @@ import org.springframework.ide.vscode.boot.java.reconcilers.BeanPostProcessingIg
import org.springframework.ide.vscode.boot.java.reconcilers.BeanRegistrarDeclarationReconciler;
import org.springframework.ide.vscode.boot.java.reconcilers.Boot3NotSupportedTypeReconciler;
import org.springframework.ide.vscode.boot.java.reconcilers.EntityIdForRepoReconciler;
import org.springframework.ide.vscode.boot.java.reconcilers.FeignClientReconciler;
import org.springframework.ide.vscode.boot.java.reconcilers.HttpSecurityLambdaDslReconciler;
import org.springframework.ide.vscode.boot.java.reconcilers.ImplicitWebAnnotationNamesReconciler;
import org.springframework.ide.vscode.boot.java.reconcilers.ModulithTypeReferenceViolationReconciler;
@@ -74,6 +75,10 @@ public class JdtConfig {
return new BeanRegistrarDeclarationReconciler(server.getQuickfixRegistry(), springIndex);
}
@Bean FeignClientReconciler feignClientReconciler(SpringMetamodelIndex springIndex) {
return new FeignClientReconciler(springIndex);
}
@Bean AutowiredFieldIntoConstructorParameterReconciler autowiredFieldIntoConstructorParameterReconciler(SimpleLanguageServer server) {
return new AutowiredFieldIntoConstructorParameterReconciler(server.getQuickfixRegistry());
}

View File

@@ -13,6 +13,7 @@ package org.springframework.ide.vscode.boot.java.beans;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -31,10 +32,13 @@ import org.eclipse.lsp4j.jsonrpc.messages.Tuple;
import org.eclipse.lsp4j.jsonrpc.messages.Tuple.Two;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ide.vscode.boot.index.SpringMetamodelIndex;
import org.springframework.ide.vscode.boot.java.Annotations;
import org.springframework.ide.vscode.boot.java.handlers.SymbolProvider;
import org.springframework.ide.vscode.boot.java.utils.ASTUtils;
import org.springframework.ide.vscode.boot.java.utils.CachedSymbol;
import org.springframework.ide.vscode.boot.java.utils.SpringIndexerJavaContext;
import org.springframework.ide.vscode.commons.protocol.spring.AnnotationAttributeValue;
import org.springframework.ide.vscode.commons.protocol.spring.AnnotationMetadata;
import org.springframework.ide.vscode.commons.protocol.spring.Bean;
import org.springframework.ide.vscode.commons.protocol.spring.InjectionPoint;
@@ -53,8 +57,11 @@ public class FeignClientSymbolProvider implements SymbolProvider {
WorkspaceSymbol symbol = result.getFirst();
Bean beanDefinition = result.getSecond();
context.getGeneratedSymbols().add(new CachedSymbol(context.getDocURI(), context.getLastModified(), symbol));
context.getBeans().add(new CachedBean(context.getDocURI(), beanDefinition));
markNewFeignConfigTypeForReconciling(beanDefinition, context);
}
}
catch (BadLocationException e) {
@@ -62,6 +69,19 @@ public class FeignClientSymbolProvider implements SymbolProvider {
}
}
private void markNewFeignConfigTypeForReconciling(Bean beanDefinition, SpringIndexerJavaContext context) {
List<String> configurationTypes = Arrays.stream(beanDefinition.getAnnotations())
.filter(annotation -> annotation.getAnnotationType().equals(Annotations.FEIGN_CLIENT))
.map(annotation -> annotation.getAttributes())
.filter(attributes -> attributes.containsKey("configuration"))
.map(attributes -> attributes.get("configuration"))
.flatMap(attributeValues -> Arrays.stream(attributeValues))
.map(attributeValue -> attributeValue.getName())
.toList();
}
private Two<WorkspaceSymbol, Bean> createSymbol(Annotation node, ITypeBinding annotationType, Collection<ITypeBinding> metaAnnotations, TextDocument doc) throws BadLocationException {
String annotationTypeName = annotationType.getName();
Collection<String> metaAnnotationNames = metaAnnotations.stream()
@@ -94,7 +114,7 @@ public class FeignClientSymbolProvider implements SymbolProvider {
.toArray(AnnotationMetadata[]::new);
Bean beanDefinition = new Bean(beanName, beanType == null ? "" : beanType.getQualifiedName(), location, injectionPoints, supertypes, annotations, false, symbol.getName());
return Tuple.two(symbol, beanDefinition);
}

View File

@@ -69,7 +69,7 @@ public class AddConfigurationIfBeansPresentReconciler implements JdtAstReconcile
@Override
public boolean visit(TypeDeclaration classDecl) {
if (isApplicableClass(project, cu, classDecl)) {
if (isApplicableClass(project, cu, classDecl, context)) {
SimpleName nameAst = classDecl.getName();
ReconcileProblemImpl problem = new ReconcileProblemImpl(getProblemType(), PROBLEM_LABEL,
nameAst.getStartPosition(), nameAst.getLength());
@@ -92,7 +92,7 @@ public class AddConfigurationIfBeansPresentReconciler implements JdtAstReconcile
};
}
private boolean isApplicableClass(IJavaProject project, CompilationUnit cu, TypeDeclaration classDecl) {
private boolean isApplicableClass(IJavaProject project, CompilationUnit cu, TypeDeclaration classDecl, ReconcilingContext context) {
if (classDecl.isInterface()) {
return false;
}
@@ -128,15 +128,22 @@ public class AddConfigurationIfBeansPresentReconciler implements JdtAstReconcile
// No '@Configuration' present. Check if any methods have '@Bean' annotation
for (MethodDeclaration m : classDecl.getMethods()) {
if (isBeanMethod(m) && !isException(project, classDecl)) {
return true;
if (isBeanMethod(m)) {
if (context.isIndexComplete()) {
if (!isException(project, classDecl, context)) {
return true;
}
}
else {
throw new RequiredCompleteIndexException();
}
}
}
return false;
}
private boolean isException(IJavaProject project, TypeDeclaration classDecl) {
private boolean isException(IJavaProject project, TypeDeclaration classDecl, ReconcilingContext context) {
if (springIndex == null) {
return false;
}
@@ -154,19 +161,30 @@ public class AddConfigurationIfBeansPresentReconciler implements JdtAstReconcile
return true;
}
boolean isConfiguredAsfeignConfigClass = Arrays.stream(beans)
.flatMap(bean -> Arrays.stream(bean.getAnnotations()))
.filter(annotation -> annotation.getAnnotationType().equals(Annotations.FEIGN_CLIENT))
.map(annotation -> annotation.getAttributes().get("configuration"))
.flatMap(attributeValues -> attributeValues != null ? Arrays.stream(attributeValues) : Stream.empty())
.anyMatch(attributeValue -> attributeValue.getName().equals(beanClassName));
List<Bean> feignClients = Arrays.stream(beans)
.filter(bean -> isConfiguredAsFeignConfigClass(bean, beanClassName))
.toList();
if (isConfiguredAsfeignConfigClass) {
if (!feignClients.isEmpty()) {
// record dependency for feign clients that have this type configured as feign configuration
for (Bean feignClient : feignClients) {
context.addDependency(feignClient.getType());
}
return true;
}
return false;
}
private boolean isConfiguredAsFeignConfigClass(Bean bean, String beanClassName) {
return Arrays.stream(bean.getAnnotations())
.filter(annotation -> annotation.getAnnotationType().equals(Annotations.FEIGN_CLIENT))
.map(annotation -> annotation.getAttributes().get("configuration"))
.flatMap(attributeValues -> attributeValues != null ? Arrays.stream(attributeValues) : Stream.empty())
.anyMatch(attributeValue -> attributeValue.getName().equals(beanClassName));
}
private static boolean isBeanMethod(MethodDeclaration m) {
AnnotationHierarchies annotationHierarchies = AnnotationHierarchies.get(m);

View File

@@ -0,0 +1,86 @@
/*******************************************************************************
* Copyright (c) 2025 Broadcom, Inc.
* 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:
* VMware, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.vscode.boot.java.reconcilers;
import java.net.URI;
import java.util.Arrays;
import java.util.List;
import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.TypeDeclaration;
import org.springframework.ide.vscode.boot.index.SpringMetamodelIndex;
import org.springframework.ide.vscode.boot.java.Annotations;
import org.springframework.ide.vscode.boot.java.annotations.AnnotationHierarchies;
import org.springframework.ide.vscode.commons.java.IJavaProject;
import org.springframework.ide.vscode.commons.java.SpringProjectUtil;
import org.springframework.ide.vscode.commons.languageserver.reconcile.ProblemType;
import org.springframework.ide.vscode.commons.protocol.spring.Bean;
import org.springframework.ide.vscode.commons.protocol.spring.BeanMethodContainerElement;
import org.springframework.ide.vscode.commons.protocol.spring.SpringIndexElement;
import org.springframework.ide.vscode.commons.util.UriUtil;
public class FeignClientReconciler implements JdtAstReconciler {
private final SpringMetamodelIndex springIndex;
public FeignClientReconciler(SpringMetamodelIndex springIndex) {
this.springIndex = springIndex;
}
@Override
public boolean isApplicable(IJavaProject project) {
return SpringProjectUtil.hasDependencyStartingWith(project, "spring-cloud-openfeign-core", null);
}
@Override
public ProblemType getProblemType() {
return null;
}
@Override
public ASTVisitor createVisitor(IJavaProject project, URI docURI, CompilationUnit cu, ReconcilingContext context) {
return new ASTVisitor() {
@Override
public boolean visit(TypeDeclaration node) {
AnnotationHierarchies annotationHierarchy = AnnotationHierarchies.get(node);
if (!annotationHierarchy.isAnnotatedWith(node.resolveBinding(), Annotations.FEIGN_CLIENT)) {
return true;
}
List<SpringIndexElement> elements = context.getCreatedIndexElements();
List<String> configurationTypes = elements.stream()
.filter(element -> element instanceof Bean)
.map(element -> (Bean) element)
.flatMap(bean -> Arrays.stream(bean.getAnnotations()))
.filter(annotation -> annotation.getAnnotationType().equals(Annotations.FEIGN_CLIENT))
.map(annotation -> annotation.getAttributes())
.filter(attributes -> attributes.containsKey("configuration"))
.flatMap(attributes -> Arrays.stream(attributes.get("configuration")))
.map(attribute -> attribute.getName())
.toList();
List<BeanMethodContainerElement> beanMethodContainers = springIndex.getNodesOfType(BeanMethodContainerElement.class);
beanMethodContainers.stream().filter(beanContainer -> configurationTypes.contains(beanContainer.getType()))
.map(beanContainer -> beanContainer.getLocation().getUri()).map(docURI -> UriUtil.toFileString(docURI))
.forEach(file -> context.markForAffetcedFilesIndexing(file));
return true;
}
};
}
}

View File

@@ -61,8 +61,8 @@ public class ReconcilingContext {
return isIndexComplete;
}
public void addDependency(String typeOfConfigClassWithImport) {
this.dependencies.add(typeOfConfigClassWithImport);
public void addDependency(String type) {
this.dependencies.add(type);
}
public Set<String> getDependencies() {

View File

@@ -41,6 +41,10 @@ public class SpringIndexerJavaDependencyTracker {
public Multimap<String, String> getAllDependencies() {
return dependencies;
}
public Collection<String> get(String file) {
return dependencies.get(file);
}
public void update(String file, Set<String> dependenciesForFile) {
dependencies.replaceValues(file, dependenciesForFile);

View File

@@ -0,0 +1,148 @@
/*******************************************************************************
* 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.java.reconcilers.test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.io.File;
import java.nio.charset.Charset;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import org.apache.commons.io.FileUtils;
import org.eclipse.lsp4j.Diagnostic;
import org.eclipse.lsp4j.PublishDiagnosticsParams;
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.java.Boot2JavaProblemType;
import org.springframework.ide.vscode.boot.java.utils.test.TestFileScanListener;
import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder;
import org.springframework.ide.vscode.commons.util.UriUtil;
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 AddConfigurationIfBeansPresentAdvancedReconcilingTest {
@Autowired private BootLanguageServerHarness harness;
@Autowired private JavaProjectFinder projectFinder;
@Autowired private SpringSymbolIndex indexer;
private File directory;
@BeforeEach
public void setup() throws Exception {
harness.intialize(null);
directory = new File(ProjectsHarness.class.getResource("/test-projects/test-feign-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 testNoErrorOnReferencedFeignConfigClass() throws Exception {
String docUri = directory.toPath().resolve("src/main/java/com/example/feign/demo/FeignConfigExample.java").toUri().toString();
PublishDiagnosticsParams diagnosticsResult = harness.getDiagnostics(docUri);
List<Diagnostic> diagnostics = diagnosticsResult.getDiagnostics();
assertEquals(0, diagnostics.size());
}
@Test
void testErrorOnNotReferencedFeignConfigClass() throws Exception {
String docUri = directory.toPath().resolve("src/main/java/com/example/feign/demo/FeignConfigNotRegistered.java").toUri().toString();
PublishDiagnosticsParams diagnosticsResult = harness.getDiagnostics(docUri);
List<Diagnostic> diagnostics = diagnosticsResult.getDiagnostics();
assertEquals(1, diagnostics.size());
assertEquals(Boot2JavaProblemType.MISSING_CONFIGURATION_ANNOTATION.getCode(), diagnostics.get(0).getCode().getLeft());
}
@Test
void testErrorGoesAwayWhenFeignClientMentionsFeignConfig() throws Exception {
String feignClientDocUri = directory.toPath().resolve("src/main/java/com/example/feign/demo/FeignClientExample.java").toUri().toString();
String feignConfigRegisterd = directory.toPath().resolve("src/main/java/com/example/feign/demo/FeignConfigExample.java").toUri().toString();
String feignConfigNotRegisterd = directory.toPath().resolve("src/main/java/com/example/feign/demo/FeignConfigNotRegistered.java").toUri().toString();
// now change the config class source code and update doc
TestFileScanListener fileScanListener = new TestFileScanListener();
indexer.getJavaIndexer().setFileScanListener(fileScanListener);
String feignClientSource = FileUtils.readFileToString(UriUtil.toFile(feignClientDocUri), Charset.defaultCharset());
String updatedFeignClientSource = feignClientSource.replace("@FeignClient(name = \"stores\", configuration = FeignConfigExample.class)",
"@FeignClient(name = \"stores\", configuration = {FeignConfigNotRegistered.class, FeignConfigExample.class})");
CompletableFuture<Void> updateFuture = indexer.updateDocument(feignClientDocUri, updatedFeignClientSource, "test triggered");
updateFuture.get(5, TimeUnit.SECONDS);
// check if the bean registrar files have been re-scanned
fileScanListener.assertScannedUri(feignClientDocUri, 1);
fileScanListener.assertScannedUri(feignConfigNotRegisterd, 1);
fileScanListener.assertScannedUri(feignConfigRegisterd, 1);
fileScanListener.assertFileScanCount(3);
// check diagnostics result
PublishDiagnosticsParams diagnosticsResult = harness.getDiagnostics(feignConfigNotRegisterd);
List<Diagnostic> diagnostics = diagnosticsResult.getDiagnostics();
assertEquals(0, diagnostics.size());
}
@Test
void testErrorAppearsWhenFeignClientNotMentionsFeignConfigAnymore() throws Exception {
String feignClientDocUri = directory.toPath().resolve("src/main/java/com/example/feign/demo/FeignClientExample.java").toUri().toString();
String feignConfigRegisterd = directory.toPath().resolve("src/main/java/com/example/feign/demo/FeignConfigExample.java").toUri().toString();
// now change the config class source code and update doc
TestFileScanListener fileScanListener = new TestFileScanListener();
indexer.getJavaIndexer().setFileScanListener(fileScanListener);
String feignClientSource = FileUtils.readFileToString(UriUtil.toFile(feignClientDocUri), Charset.defaultCharset());
String updatedFeignClientSource = feignClientSource.replace("@FeignClient(name = \"stores\", configuration = FeignConfigExample.class)",
"@FeignClient(name = \"stores\")");
CompletableFuture<Void> updateFuture = indexer.updateDocument(feignClientDocUri, updatedFeignClientSource, "test triggered");
updateFuture.get(5, TimeUnit.SECONDS);
// check if the bean registrar files have been re-scanned
fileScanListener.assertScannedUri(feignClientDocUri, 1);
fileScanListener.assertScannedUri(feignConfigRegisterd, 1);
fileScanListener.assertFileScanCount(2);
// check diagnostics result
PublishDiagnosticsParams diagnosticsResult = harness.getDiagnostics(feignConfigRegisterd);
List<Diagnostic> diagnostics = diagnosticsResult.getDiagnostics();
assertEquals(1, diagnostics.size());
assertEquals(Boot2JavaProblemType.MISSING_CONFIGURATION_ANNOTATION.getCode(), diagnostics.get(0).getCode().getLeft());
}
}

View File

@@ -9,7 +9,7 @@
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example.qualifier</groupId>
<artifactId>feign-demo</artifactId>
<artifactId>test-feign-indexing</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>feign-demo</name>
<description>Demo project for Spring Boot</description>

View File

@@ -5,7 +5,7 @@ import org.springframework.context.annotation.Bean;
public class FeignConfigExample {
@Bean
public BeanType specialBean() {
BeanType specialBean() {
return new BeanType();
}

View File

@@ -0,0 +1,12 @@
package com.example.feign.demo;
import org.springframework.context.annotation.Bean;
public class FeignConfigNotRegistered {
@Bean
BeanType specialBean2() {
return new BeanType();
}
}