From 311c27621731703eaf2cb23f544fe5dd9f19fecc Mon Sep 17 00:00:00 2001 From: aboyko Date: Thu, 29 Sep 2022 20:31:48 -0400 Subject: [PATCH] [boot-3] BeanPostProcessor ignored in AOT setup problem and quick fix --- .../java/BeanPostProcessingIgnoreInAot.java | 150 ++++++++++++++++++ .../boot/java/Boot3JavaProblemType.java | 4 +- .../rewrite/BootCodeActionRepository.java | 4 +- .../BeanPostProcessingIgnoreInAotProblem.java | 95 +++++++++++ .../src/main/resources/problem-types.json | 6 + .../vscode-spring-boot/package.json | 14 +- 6 files changed, 270 insertions(+), 3 deletions(-) create mode 100644 headless-services/commons/commons-rewrite/src/main/java/org/springframework/ide/vscode/commons/rewrite/java/BeanPostProcessingIgnoreInAot.java create mode 100644 headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/rewrite/reconcile/BeanPostProcessingIgnoreInAotProblem.java diff --git a/headless-services/commons/commons-rewrite/src/main/java/org/springframework/ide/vscode/commons/rewrite/java/BeanPostProcessingIgnoreInAot.java b/headless-services/commons/commons-rewrite/src/main/java/org/springframework/ide/vscode/commons/rewrite/java/BeanPostProcessingIgnoreInAot.java new file mode 100644 index 000000000..5766fa60c --- /dev/null +++ b/headless-services/commons/commons-rewrite/src/main/java/org/springframework/ide/vscode/commons/rewrite/java/BeanPostProcessingIgnoreInAot.java @@ -0,0 +1,150 @@ +/******************************************************************************* + * Copyright (c) 2022 VMware, 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.commons.rewrite.java; + +import java.util.ArrayList; +import java.util.List; + +import org.openrewrite.ExecutionContext; +import org.openrewrite.Recipe; +import org.openrewrite.TreeVisitor; +import org.openrewrite.java.JavaIsoVisitor; +import org.openrewrite.java.JavaParser; +import org.openrewrite.java.JavaTemplate; +import org.openrewrite.java.tree.J; +import org.openrewrite.java.tree.JavaType; +import org.openrewrite.java.tree.TypeUtils; + +public class BeanPostProcessingIgnoreInAot extends Recipe { + + private static final String RUNTIME_BEAN_POST_PROCESSOR = "org.springframework.beans.factory.config.BeanPostProcessor"; + private static final String COMPILE_BEAN_POST_PROCESSOR = "org.springframework.beans.factory.aot.BeanRegistrationAotProcessor"; + public static final String METHOD_NAME = "isBeanExcludedFromAotProcessing"; + + @Override + public String getDisplayName() { + return "Prevent BeanPostProcessor from being ignored in AOT"; + } + + @Override + public String getDescription() { + return "Bean implementing both 'BeanPostProcessor' and 'BeanRegistrationAotProcessor' and does not return 'false' from 'isBeanExcludedFromAotProcessing()' method then Spring's behavior is to IGNORE the `BeanPostProcessor`." + + " Recipe adds 'isBeanExcludedFromAotProcessing()' implemetation returning 'false' if necessary"; + } + + public static boolean isApplicableClass(J.ClassDeclaration classDecl) { + JavaType.FullyQualified type = classDecl.getType(); + if (classDecl.getImplements() != null) { + return classDecl.getImplements().stream() + .anyMatch(f -> TypeUtils.isAssignableTo(RUNTIME_BEAN_POST_PROCESSOR, type) + && TypeUtils.isAssignableTo(COMPILE_BEAN_POST_PROCESSOR, type)); + } + return false; + } + + public static boolean isApplicableMethod(J.MethodDeclaration m) { + return METHOD_NAME.equals(m.getSimpleName()) && m.getParameters().size() == 1 && m.getParameters().get(0) instanceof J.Empty; + } + + public static boolean isReturnTrue(J.MethodDeclaration m) { + List returnStatements = findReturnStatementsInMethod(m); + if (returnStatements.size() == 1) { + J.Return returnStatement = returnStatements.get(0); + if (returnStatement.getExpression() instanceof J.Literal) { + if (((J.Literal) returnStatement.getExpression()).getValue() == Boolean.TRUE) { + // Method returns "true" boolean value constant + return true; + } + } else if (returnStatement.getExpression() instanceof J.FieldAccess) { + J.FieldAccess fa = (J.FieldAccess) returnStatement.getExpression(); + if (fa.getName() != null && fa.getName().getSimpleName().equals("TRUE")) { + if (fa.getTarget() != null) { + JavaType.FullyQualified type = TypeUtils.asFullyQualified(fa.getTarget().getType()); + // Method returns Boolean.TRUE + if ("java.lang.Boolean".equals(type.getFullyQualifiedName())) { + return true; + } + } + } + } + } + return false; + } + + @Override + protected TreeVisitor getApplicableTest() { + return new JavaIsoVisitor() { + @Override + public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, ExecutionContext executionContext) { + if (isApplicableClass(classDecl)) { + return classDecl.withMarkers(classDecl.getMarkers().searchResult()); + } + return super.visitClassDeclaration(classDecl, executionContext); + } + }; + } + + @Override + protected TreeVisitor getVisitor() { + return new JavaIsoVisitor() { + + private JavaTemplate createTemplate() { + return JavaTemplate.builder(this::getCursor, "@Override\n" + + "public boolean isBeanExcludedFromAotProcessing() {\n" + + " return false;\n" + + "}\n" + ) + .javaParser(() -> JavaParser.fromJavaVersion().build()) + .build(); + } + + @Override + public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, ExecutionContext executionContext) { + J.ClassDeclaration c = super.visitClassDeclaration(classDecl, executionContext); + if (isApplicableClass(classDecl)) { + J.MethodDeclaration method = c.getBody().getStatements().stream().filter(J.MethodDeclaration.class::isInstance).map(J.MethodDeclaration.class::cast).filter(BeanPostProcessingIgnoreInAot::isApplicableMethod).findFirst().orElse(null); + if (method == null) { + J.Block body = c.getBody().withTemplate(createTemplate(), classDecl.getBody().getCoordinates().addMethodDeclaration((m1, m2) -> 1)); + c = c.withBody(body); + } + } + return c; + } + + @Override + public J.MethodDeclaration visitMethodDeclaration(J.MethodDeclaration method, ExecutionContext executionContext) { + J.MethodDeclaration m = super.visitMethodDeclaration(method, executionContext); + if (isApplicableMethod(m)) { + J.ClassDeclaration c = getCursor().firstEnclosing(J.ClassDeclaration.class); + if (c != null && isApplicableClass(c) && isReturnTrue(m)) { + m = m.withTemplate(createTemplate(), m.getCoordinates().replace()); + } + } + return m; + } + }; + } + + private static List findReturnStatementsInMethod(J.MethodDeclaration m) { + List returnStatements = new ArrayList<>(); + new JavaIsoVisitor>() { + + @Override + public J.Return visitReturn(J.Return _return, List returns) { + if (getCursor().firstEnclosing(J.MethodDeclaration.class) == m) { + returns.add(_return); + } + return _return; + } + }.visit(m, returnStatements); + return returnStatements; + } +} \ No newline at end of file diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/Boot3JavaProblemType.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/Boot3JavaProblemType.java index dd2c91afc..9546c36d6 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/Boot3JavaProblemType.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/Boot3JavaProblemType.java @@ -23,7 +23,9 @@ import org.springframework.ide.vscode.commons.languageserver.reconcile.ProblemTy */ public enum Boot3JavaProblemType implements ProblemType { - JAVA_CONCRETE_BEAN_TYPE(WARNING, "Bean definition should have precise type for Spring 6 AOT", "Not precise bean defintion type"); + JAVA_CONCRETE_BEAN_TYPE(WARNING, "Bean definition should have precise type for Spring 6 AOT", "Not precise bean defintion type"), + + JAVA_BEAN_NOT_REGISTERED_IN_AOT(WARNING, "'BeanPostProcessor' behaviour is ignored in Spring 6 AOT", "'BeanPostProcessor' behaviour is ignored in AOT"); private final ProblemSeverity defaultSeverity; private String description; diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/rewrite/BootCodeActionRepository.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/rewrite/BootCodeActionRepository.java index 87c982141..367d5fa95 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/rewrite/BootCodeActionRepository.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/rewrite/BootCodeActionRepository.java @@ -16,6 +16,7 @@ import org.springframework.ide.vscode.boot.java.rewrite.codeaction.AutowiredFiel import org.springframework.ide.vscode.boot.java.rewrite.codeaction.BeanMethodsNotPublicCodeAction; import org.springframework.ide.vscode.boot.java.rewrite.codeaction.NoRequestMappingAnnotationCodeAction; import org.springframework.ide.vscode.boot.java.rewrite.codeaction.UnnecessarySpringExtensionCodeAction; +import org.springframework.ide.vscode.boot.java.rewrite.reconcile.BeanPostProcessingIgnoreInAotProblem; import org.springframework.ide.vscode.boot.java.rewrite.reconcile.BeanMethodNotPublicProblem; import org.springframework.ide.vscode.boot.java.rewrite.reconcile.NoAutowiredOnConstructorProblem; import org.springframework.ide.vscode.boot.java.rewrite.reconcile.PreciseBeanTypeProblem; @@ -42,7 +43,8 @@ public class BootCodeActionRepository extends CodeActionRepository { new BeanMethodNotPublicProblem(), new NoAutowiredOnConstructorProblem(), new UnnecessarySpringExtensionProblem(), - new PreciseBeanTypeProblem() + new PreciseBeanTypeProblem(), + new BeanPostProcessingIgnoreInAotProblem() ); } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/rewrite/reconcile/BeanPostProcessingIgnoreInAotProblem.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/rewrite/reconcile/BeanPostProcessingIgnoreInAotProblem.java new file mode 100644 index 000000000..5ebdd5d30 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/rewrite/reconcile/BeanPostProcessingIgnoreInAotProblem.java @@ -0,0 +1,95 @@ +/******************************************************************************* + * Copyright (c) 2022 VMware, 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.rewrite.reconcile; + +import static org.springframework.ide.vscode.commons.java.SpringProjectUtil.springBootVersionGreaterOrEqual; + +import java.util.List; +import java.util.stream.Collectors; + +import org.openrewrite.ExecutionContext; +import org.openrewrite.Tree; +import org.openrewrite.java.JavaIsoVisitor; +import org.openrewrite.java.JavaVisitor; +import org.openrewrite.java.tree.J.ClassDeclaration; +import org.openrewrite.java.tree.J.MethodDeclaration; +import org.openrewrite.marker.Range; +import org.springframework.ide.vscode.boot.java.Boot3JavaProblemType; +import org.springframework.ide.vscode.commons.java.IJavaProject; +import org.springframework.ide.vscode.commons.languageserver.reconcile.ProblemType; +import org.springframework.ide.vscode.commons.rewrite.config.RecipeCodeActionDescriptor; +import org.springframework.ide.vscode.commons.rewrite.config.RecipeScope; +import org.springframework.ide.vscode.commons.rewrite.config.RecipeSpringJavaProblemDescriptor; +import org.springframework.ide.vscode.commons.rewrite.java.BeanPostProcessingIgnoreInAot; +import org.springframework.ide.vscode.commons.rewrite.java.FixAssistMarker; + +public class BeanPostProcessingIgnoreInAotProblem implements RecipeSpringJavaProblemDescriptor { + + private static final String LABEL = "Add method 'isBeanExcludedFromAotProcessing' that returns 'false'"; + + @Override + public String getRecipeId() { + return "org.springframework.ide.vscode.commons.rewrite.java.BeanPostProcessingIgnoreInAot"; + } + + @Override + public String getLabel(RecipeScope s) { + return RecipeCodeActionDescriptor.buildLabel(LABEL, s); + } + + @Override + public RecipeScope[] getScopes() { + return RecipeScope.values(); + } + + @Override + public JavaVisitor getMarkerVisitor() { + return new JavaIsoVisitor() { + + @Override + public ClassDeclaration visitClassDeclaration(ClassDeclaration classDecl, ExecutionContext p) { + ClassDeclaration c = super.visitClassDeclaration(classDecl, p); + if (BeanPostProcessingIgnoreInAot.isApplicableClass(classDecl)) { + List methods = classDecl.getBody().getStatements().stream() + .filter(MethodDeclaration.class::isInstance).map(MethodDeclaration.class::cast) + .filter(BeanPostProcessingIgnoreInAot::isApplicableMethod) + .collect(Collectors.toList()); + FixAssistMarker marker = new FixAssistMarker(Tree.randomId()) + .withRecipeId(getRecipeId()) + .withScope(classDecl.getMarkers().findFirst(Range.class).orElse(null)); + if (methods.isEmpty()) { + // Didn't find a method. Default implementation return true therefore mark it. + c = c.withName(c.getName().withMarkers(c.getName().getMarkers().add(marker))); + } else { + MethodDeclaration m = methods.stream().filter(BeanPostProcessingIgnoreInAot::isReturnTrue).findFirst().orElse(null); + // Found method that return true explicitly + if (m != null) { + c = c.withName(c.getName().withMarkers(c.getName().getMarkers().add(marker))); + } + } + } + return c; + } + + }; + } + + @Override + public boolean isApplicable(IJavaProject project) { + return springBootVersionGreaterOrEqual(3, 0, 0).test(project); + } + + @Override + public ProblemType getProblemType() { + return Boot3JavaProblemType.JAVA_BEAN_NOT_REGISTERED_IN_AOT; + } + +} diff --git a/headless-services/spring-boot-language-server/src/main/resources/problem-types.json b/headless-services/spring-boot-language-server/src/main/resources/problem-types.json index dbac17f6e..ce90e0a34 100644 --- a/headless-services/spring-boot-language-server/src/main/resources/problem-types.json +++ b/headless-services/spring-boot-language-server/src/main/resources/problem-types.json @@ -54,6 +54,12 @@ "label": "Not precise bean defintion type", "description": "Bean definition should have precise type for Spring 6 AOT", "defaultSeverity": "WARNING" + }, + { + "code": "JAVA_BEAN_NOT_REGISTERED_IN_AOT", + "label": "'BeanPostProcessor' behaviour is ignored in AOT", + "description": "'BeanPostProcessor' behaviour is ignored in Spring 6 AOT", + "defaultSeverity": "WARNING" } ] }, diff --git a/vscode-extensions/vscode-spring-boot/package.json b/vscode-extensions/vscode-spring-boot/package.json index 2e1eedb82..162014aad 100644 --- a/vscode-extensions/vscode-spring-boot/package.json +++ b/vscode-extensions/vscode-spring-boot/package.json @@ -335,6 +335,18 @@ "HINT", "ERROR" ] + }, + "spring-boot.ls.problem.boot3.JAVA_BEAN_NOT_REGISTERED_IN_AOT": { + "type": "string", + "default": "WARNING", + "description": "'BeanPostProcessor' behaviour is ignored in Spring 6 AOT", + "enum": [ + "IGNORE", + "INFO", + "WARNING", + "HINT", + "ERROR" + ] } } }, @@ -704,4 +716,4 @@ "extensionDependencies": [ "redhat.java" ] -} +} \ No newline at end of file