Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -70,15 +70,19 @@
*/
public abstract class ArchitectureCheck extends DefaultTask {

private static final String CONDITIONAL_ON_CLASS_ANNOTATION = "org.springframework.boot.autoconfigure.condition.ConditionalOnClass";

private FileCollection classes;

public ArchitectureCheck() {
getOutputDirectory().convention(getProject().getLayout().getBuildDirectory().dir(getName()));
getConditionalOnClassAnnotation().convention(CONDITIONAL_ON_CLASS_ANNOTATION);
getRules().addAll(getProhibitObjectsRequireNonNull().convention(true)
.map(whenTrue(ArchitectureRules::noClassesShouldCallObjectsRequireNonNull)));
getRules().addAll(ArchitectureRules.standard());
getRules().addAll(whenMainSources(
() -> Collections.singletonList(ArchitectureRules.allBeanMethodsShouldReturnNonPrivateType())));
getRules().addAll(whenMainSources(() -> List
.of(ArchitectureRules.allBeanMethodsShouldReturnNonPrivateType(), ArchitectureRules
.allBeanMethodsShouldNotHaveConditionalOnClassAnnotation(getConditionalOnClassAnnotation().get()))));
getRuleDescriptions().set(getRules().map(this::asDescriptions));
}

Expand Down Expand Up @@ -186,4 +190,7 @@ final FileTree getInputClasses() {
@Input // Use descriptions as input since rules aren't serializable
abstract ListProperty<String> getRuleDescriptions();

@Internal
abstract Property<String> getConditionalOnClassAnnotation();

}
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,15 @@ static ArchRule allBeanMethodsShouldReturnNonPrivateType() {
.allowEmptyShould(true);
}

static ArchRule allBeanMethodsShouldNotHaveConditionalOnClassAnnotation(String annotationName) {
return methodsThatAreAnnotatedWith("org.springframework.context.annotation.Bean").should()
.notBeAnnotatedWith(annotationName)
.because("@ConditionalOnClass on @Bean methods is ineffective - it doesn't prevent "
+ "the method signature from being loaded. Such condition need to be placed"
+ " on a @Configuration class, allowing the condition to back off before the type is loaded.")
.allowEmptyShould(true);
}

private static ArchRule allPackagesShouldBeFreeOfTangles() {
return SlicesRuleDefinition.slices().matching("(**)").should().beFreeOfCycles();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import java.util.function.UnaryOperator;

import org.gradle.api.tasks.SourceSet;
import org.gradle.testkit.runner.BuildResult;
Expand All @@ -39,6 +40,7 @@
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource;

import org.springframework.boot.build.architecture.annotations.TestConditionalOnClass;
import org.springframework.util.ClassUtils;
import org.springframework.util.FileSystemUtils;
import org.springframework.util.StringUtils;
Expand Down Expand Up @@ -180,7 +182,7 @@ void whenClassCallsObjectsRequireNonNullWithMessageShouldFailAndWriteReport(Task
void whenClassCallsObjectsRequireNonNullWithMessageAndProhibitObjectsRequireNonNullIsFalseShouldSucceedAndWriteEmptyReport(
Task task) throws IOException {
prepareTask(task, "objects/requireNonNullWithString");
build(this.gradleBuild.withProhibitObjectsRequireNonNull(task, false), task);
build(this.gradleBuild.withProhibitObjectsRequireNonNull(false), task);
}

@ParameterizedTest(name = "{0}")
Expand All @@ -195,7 +197,7 @@ void whenClassCallsObjectsRequireNonNullWithSupplierShouldFailAndWriteReport(Tas
void whenClassCallsObjectsRequireNonNullWithSupplierAndProhibitObjectsRequireNonNullIsFalseShouldSucceedAndWriteEmptyReport(
Task task) throws IOException {
prepareTask(task, "objects/requireNonNullWithSupplier");
build(this.gradleBuild.withProhibitObjectsRequireNonNull(task, false), task);
build(this.gradleBuild.withProhibitObjectsRequireNonNull(false), task);
}

@ParameterizedTest(name = "{0}")
Expand Down Expand Up @@ -295,6 +297,25 @@ void whenEnumSourceValueIsSameAsTypeOfMethodsFirstParameterShouldFailAndWriteRep
"should not have a value that is the same as the type of the method's first parameter");
}

@Test
void whenConditionalOnClassUsedOnBeanMethodsWithMainSourcesShouldFailAndWriteReport() throws IOException {
prepareTask(Task.CHECK_ARCHITECTURE_MAIN, "conditionalonclass", "annotations");
GradleBuild gradleBuild = this.gradleBuild.withDependencies(SPRING_CONTEXT)
.withConditionalOnClassAnnotation(TestConditionalOnClass.class.getName());
buildAndFail(gradleBuild, Task.CHECK_ARCHITECTURE_MAIN,
"because @ConditionalOnClass on @Bean methods is ineffective - it doesn't prevent"
+ " the method signature from being loaded. Such condition need to be placed"
+ " on a @Configuration class, allowing the condition to back off before the type is loaded");
}

@Test
void whenConditionalOnClassUsedOnBeanMethodsWithTestSourcesShouldSucceedAndWriteEmptyReport() throws IOException {
prepareTask(Task.CHECK_ARCHITECTURE_TEST, "conditionalonclass", "annotations");
GradleBuild gradleBuild = this.gradleBuild.withDependencies(SPRING_CONTEXT)
.withConditionalOnClassAnnotation(TestConditionalOnClass.class.getName());
build(gradleBuild, Task.CHECK_ARCHITECTURE_TEST);
}

private void prepareTask(Task task, String... sourceDirectories) throws IOException {
for (String sourceDirectory : sourceDirectories) {
FileSystemUtils.copyRecursively(
Expand All @@ -310,7 +331,7 @@ private void prepareTask(Task task, String... sourceDirectories) throws IOExcept
private void build(GradleBuild gradleBuild, Task task) throws IOException {
try {
BuildResult buildResult = gradleBuild.build(task.toString());
assertThat(buildResult.taskPaths(TaskOutcome.SUCCESS)).contains(":" + task);
assertThat(buildResult.taskPaths(TaskOutcome.SUCCESS)).as(buildResult.getOutput()).contains(":" + task);
assertThat(task.getFailureReport(gradleBuild.getProjectDir())).isEmpty();
}
catch (UnexpectedBuildFailure ex) {
Expand All @@ -326,7 +347,7 @@ private void build(GradleBuild gradleBuild, Task task) throws IOException {
private void buildAndFail(GradleBuild gradleBuild, Task task, String... messages) throws IOException {
try {
BuildResult buildResult = gradleBuild.buildAndFail(task.toString());
assertThat(buildResult.taskPaths(TaskOutcome.FAILED)).contains(":" + task);
assertThat(buildResult.taskPaths(TaskOutcome.FAILED)).as(buildResult.getOutput()).contains(":" + task);
assertThat(task.getFailureReport(gradleBuild.getProjectDir())).contains(messages);
}
catch (UnexpectedBuildSuccess ex) {
Expand Down Expand Up @@ -371,7 +392,7 @@ private static final class GradleBuild {

private final Set<String> dependencies = new LinkedHashSet<>();

private final Map<Task, Boolean> prohibitObjectsRequireNonNull = new LinkedHashMap<>();
private final Map<Task, TaskConfiguration> taskConfigurations = new LinkedHashMap<>();

private GradleBuild(Path projectDir) {
this.projectDir = projectDir;
Expand All @@ -381,12 +402,28 @@ Path getProjectDir() {
return this.projectDir;
}

GradleBuild withProhibitObjectsRequireNonNull(Task task, boolean prohibitObjectsRequireNonNull) {
this.prohibitObjectsRequireNonNull.put(task, prohibitObjectsRequireNonNull);
GradleBuild withProhibitObjectsRequireNonNull(Boolean prohibitObjectsRequireNonNull) {
for (Task task : Task.values()) {
configureTask(task, (configuration) -> configuration
.withProhibitObjectsRequireNonNull(prohibitObjectsRequireNonNull));
}
return this;
}

GradleBuild withConditionalOnClassAnnotation(String annotationName) {
for (Task task : Task.values()) {
configureTask(task, (configuration) -> configuration.withConditionalOnClassAnnotation(annotationName));
}
return this;
}

private void configureTask(Task task, UnaryOperator<TaskConfiguration> configurer) {
this.taskConfigurations.computeIfAbsent(task, (key) -> new TaskConfiguration(null, null));
this.taskConfigurations.compute(task, (key, value) -> configurer.apply(value));
}

GradleBuild withDependencies(String... dependencies) {
this.dependencies.clear();
this.dependencies.addAll(Arrays.asList(dependencies));
return this;
}
Expand Down Expand Up @@ -415,22 +452,40 @@ private GradleRunner prepareRunner(String... arguments) throws IOException {
if (!this.dependencies.isEmpty()) {
buildFile.append("dependencies {\n");
for (String dependency : this.dependencies) {
buildFile.append(" implementation '%s'\n".formatted(dependency));
buildFile.append("\n implementation ").append(StringUtils.quote(dependency));
}
buildFile.append("}\n");
}
this.prohibitObjectsRequireNonNull.forEach((task, prohibitObjectsRequireNonNull) -> buildFile.append(task)
.append(" {\n")
.append(" prohibitObjectsRequireNonNull = ")
.append(prohibitObjectsRequireNonNull)
.append("\n}\n\n"));
this.taskConfigurations.forEach((task, configuration) -> {
buildFile.append(task).append(" {");
if (configuration.conditionalOnClassAnnotation() != null) {
buildFile.append("\n conditionalOnClassAnnotation = ")
.append(StringUtils.quote(configuration.conditionalOnClassAnnotation()));
}
if (configuration.prohibitObjectsRequireNonNull() != null) {
buildFile.append("\n prohibitObjectsRequireNonNull = ")
.append(configuration.prohibitObjectsRequireNonNull());
}
buildFile.append("\n}\n");
});
Files.writeString(this.projectDir.resolve("build.gradle"), buildFile, StandardCharsets.UTF_8);
return GradleRunner.create()
.withProjectDir(this.projectDir.toFile())
.withArguments(arguments)
.withPluginClasspath();
}

private record TaskConfiguration(Boolean prohibitObjectsRequireNonNull, String conditionalOnClassAnnotation) {

private TaskConfiguration withConditionalOnClassAnnotation(String annotationName) {
return new TaskConfiguration(this.prohibitObjectsRequireNonNull, annotationName);
}

private TaskConfiguration withProhibitObjectsRequireNonNull(Boolean prohibitObjectsRequireNonNull) {
return new TaskConfiguration(prohibitObjectsRequireNonNull, this.conditionalOnClassAnnotation);
}
}

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* Copyright 2012-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.springframework.boot.build.architecture.annotations;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* {@code @ConditionalOnClass} analogue for architecture checks.
*
*/
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
public @interface TestConditionalOnClass {

Class<?>[] value() default {};

String[] name() default {};

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* Copyright 2012-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.springframework.boot.build.architecture.conditionalonclass;

import org.springframework.boot.build.architecture.annotations.TestConditionalOnClass;
import org.springframework.context.annotation.Bean;

class OnBeanMethod {

@Bean
@TestConditionalOnClass(String.class)
String helloWorld() {
return "Hello World";
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
* {@link EnableAutoConfiguration Auto-configuration} for {@link AuditEvent}s.
Expand All @@ -50,18 +51,28 @@ public AuditListener auditListener(AuditEventRepository auditEventRepository) {
return new AuditListener(auditEventRepository);
}

@Bean
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(name = "org.springframework.security.authentication.event.AbstractAuthenticationEvent")
@ConditionalOnMissingBean(AbstractAuthenticationAuditListener.class)
public AuthenticationAuditListener authenticationAuditListener() {
return new AuthenticationAuditListener();
static class AuthenticationAuditConfiguration {

@Bean
@ConditionalOnMissingBean(AbstractAuthenticationAuditListener.class)
AuthenticationAuditListener authenticationAuditListener() {
return new AuthenticationAuditListener();
}

}

@Bean
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(name = "org.springframework.security.access.event.AbstractAuthorizationEvent")
@ConditionalOnMissingBean(AbstractAuthorizationAuditListener.class)
public AuthorizationAuditListener authorizationAuditListener() {
return new AuthorizationAuditListener();
static class AuthorizationAuditConfiguration {

@Bean
@ConditionalOnMissingBean(AbstractAuthorizationAuditListener.class)
AuthorizationAuditListener authorizationAuditListener() {
return new AuthorizationAuditListener();
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,12 @@
* @since 3.0.0
*/
@AutoConfiguration(after = JacksonAutoConfiguration.class)
@ConditionalOnClass({ ObjectMapper.class, Jackson2ObjectMapperBuilder.class })
public class JacksonEndpointAutoConfiguration {

@Bean
@ConditionalOnProperty(name = "management.endpoints.jackson.isolated-object-mapper", matchIfMissing = true)
@ConditionalOnClass({ ObjectMapper.class, Jackson2ObjectMapperBuilder.class })
public EndpointObjectMapper endpointObjectMapper() {
EndpointObjectMapper endpointObjectMapper() {
ObjectMapper objectMapper = Jackson2ObjectMapperBuilder.json()
.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS,
SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@ public static class MvcRequestMatcherConfiguration {

@Bean
@ConditionalOnMissingBean
@ConditionalOnClass(DispatcherServlet.class)
public RequestMatcherProvider requestMatcherProvider(DispatcherServletPath servletPath) {
return new AntPathRequestMatcherProvider(servletPath::getRelativePath);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,22 +81,37 @@ ServletManagementWebServerFactoryCustomizer servletManagementWebServerFactoryCus
return new ServletManagementWebServerFactoryCustomizer(beanFactory);
}

@Bean
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(name = "io.undertow.Undertow")
UndertowAccessLogCustomizer undertowManagementAccessLogCustomizer() {
return new UndertowAccessLogCustomizer();
static class UndertowConfiguration {

@Bean
UndertowAccessLogCustomizer undertowManagementAccessLogCustomizer() {
return new UndertowAccessLogCustomizer();
}

}

@Bean
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(name = "org.apache.catalina.valves.AccessLogValve")
TomcatAccessLogCustomizer tomcatManagementAccessLogCustomizer() {
return new TomcatAccessLogCustomizer();
static class TomcatConfiguration {

@Bean
TomcatAccessLogCustomizer tomcatManagementAccessLogCustomizer() {
return new TomcatAccessLogCustomizer();
}

}

@Bean
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(name = "org.eclipse.jetty.server.Server")
JettyAccessLogCustomizer jettyManagementAccessLogCustomizer() {
return new JettyAccessLogCustomizer();
static class JettyConfiguration {

@Bean
JettyAccessLogCustomizer jettyManagementAccessLogCustomizer() {
return new JettyAccessLogCustomizer();
}

}

@Configuration(proxyBeanMethods = false)
Expand Down
Loading