GH-1193 - Remove deprecated support for Spring AutoRestDocs for Javadoc extraction.

This commit is contained in:
Oliver Drotbohm
2025-05-16 15:58:25 +02:00
parent 74ffd65b5e
commit b594b69c27
2 changed files with 1 additions and 321 deletions

View File

@@ -40,7 +40,6 @@ import org.springframework.modulith.core.Source;
import org.springframework.modulith.core.SpringBean;
import org.springframework.modulith.docs.ConfigurationProperties.ModuleProperty;
import org.springframework.modulith.docs.Documenter.CanvasOptions;
import org.springframework.modulith.docs.util.BuildSystemUtils;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
@@ -57,8 +56,7 @@ class Asciidoctor {
private static final Pattern LINE_BREAKS = Pattern.compile("\\<\\s*br\\s*\\>");
private static final Logger LOG = LoggerFactory.getLogger(Asciidoctor.class);
private static final Optional<DocumentationSource> DOC_SOURCE = getSpringModulithDocsSource()
.or(() -> getSpringAutoRestDocsSource());
private static final Optional<DocumentationSource> DOC_SOURCE = getSpringModulithDocsSource();
private final ApplicationModules modules;
private final String javaDocBase;
@@ -424,19 +422,6 @@ class Asciidoctor {
return "include::" + componentsFilename + "[]" + System.lineSeparator();
}
@SuppressWarnings("deprecation")
private static Optional<DocumentationSource> getSpringAutoRestDocsSource() {
return BuildSystemUtils.getTargetResource("generated-javadoc-json")
.map(__ -> SpringAutoRestDocsDocumentationSource.INSTANCE)
.map(it -> {
LOG.debug("Using Javadoc generated by Spring Auto RESTDocs found in generated-javadoc-json.");
LOG.warn(
"Javadoc metadata generated by Spring Auto RESTDocs is deprecated! Switch to spring-modulith-apt instead!");
return it;
});
}
private static Optional<DocumentationSource> getSpringModulithDocsSource() {
return SpringModulithDocumentationSource.getInstance()

View File

@@ -1,305 +0,0 @@
/*
* Copyright 2020-2025 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.modulith.docs;
import static org.slf4j.LoggerFactory.*;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Predicate;
import org.slf4j.Logger;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.tngtech.archunit.core.domain.JavaClass;
import com.tngtech.archunit.core.domain.JavaMethod;
/**
* A {@link DocumentationSource} that uses metadata generated by Spring Auto REST Docs' Javadoc Doclet.
*
* @author Oliver Drotbohm
* @deprecated since 1.3, use {@link SpringModulithDocumentationSource} instead.
*/
@Deprecated
enum SpringAutoRestDocsDocumentationSource implements DocumentationSource {
INSTANCE;
static {
Assert.isTrue(
ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper",
SpringModulithDocumentationSource.class.getClassLoader()),
"Jackson is required on the classpath for Spring Auto RESTDocs generated Javadoc metadata!");
}
private final JavadocReader reader = JavadocReader.createWithSystemProperty();
/*
* (non-Javadoc)
* @see org.springframework.modulith.docs.JavadocSource#getDocumentation(com.tngtech.archunit.core.domain.JavaMethod)
*/
@Override
public Optional<String> getDocumentation(JavaMethod method) {
return Optional.of(reader.resolveMethodComment(method.getOwner().reflect(), method.getName()))
.filter(it -> !it.isEmpty());
}
/*
* (non-Javadoc)
* @see org.springframework.modulith.docs.DocumentationSource#getDocumentation(com.tngtech.archunit.core.domain.JavaClass)
*/
@Override
public Optional<String> getDocumentation(JavaClass type) {
return Optional.of(reader.resolveClassComment(type.reflect()))
.filter(Predicate.not(String::isEmpty));
}
static class ClassJavadoc {
private String comment;
private Map<String, MethodJavadoc> methods = new HashMap<>();
public String getClassComment() {
return comment;
}
public String getMethodComment(String methodName) {
MethodJavadoc methodJavadoc = methods.get(methodName);
if (methodJavadoc != null) {
return trimToEmpty(methodJavadoc.getComment());
} else {
return "";
}
}
private static String trimToEmpty(@Nullable String source) {
return source == null ? "" : source.trim();
}
static class MethodJavadoc {
private String comment;
private Map<String, String> parameters = new HashMap<>();
private Map<String, String> tags = new HashMap<>();
public String getComment() {
return comment;
}
public String getParameterComment(String parameterName) {
return parameters.get(parameterName);
}
public String getTag(String tagName) {
return tags.get(tagName);
}
}
}
static class JavadocReader {
private static final Logger log = getLogger(JavadocReader.class);
private static final String PATH_DELIMITER = ",";
private static final String JAVADOC_JSON_DIR_PROPERTY = "org.springframework.restdocs.javadocJsonDir";
private final Map<String, ClassJavadoc> classCache = new ConcurrentHashMap<>();
private final ObjectMapper mapper;
private final List<File> absoluteBaseDirs;
private JavadocReader(ObjectMapper mapper, List<File> absoluteBaseDirs) {
this.mapper = mapper;
this.absoluteBaseDirs = absoluteBaseDirs;
}
public static JavadocReader createWithSystemProperty() {
String jsonDir = System.getProperties().getProperty(JAVADOC_JSON_DIR_PROPERTY);
if (!StringUtils.hasText(jsonDir)) {
jsonDir = getDefaultJsonDirectory();
}
return new JavadocReader(objectMapper(), toAbsoluteDirs(jsonDir));
}
private static String getDefaultJsonDirectory() {
if (new File("pom.xml").exists()) {
return "target/generated-javadoc-json";
}
return "build/generated-javadoc-json";
}
/**
* Used for testing.
*/
static JavadocReader createWith(String javadocJsonDir) {
return new JavadocReader(objectMapper(), toAbsoluteDirs(javadocJsonDir));
}
public String resolveMethodComment(Class<?> javaBaseClass, final String javaMethodName) {
return resolveCommentFromClassHierarchy(javaBaseClass,
classJavadoc -> classJavadoc.getMethodComment(javaMethodName));
}
public String resolveClassComment(Class<?> javaBaseClass) {
return classJavadoc(javaBaseClass).getClassComment();
}
private ClassJavadoc classJavadoc(Class<?> clazz) {
String relativePath = classToRelativePath(clazz);
ClassJavadoc classJavadocFromCache = classCache.get(relativePath);
if (classJavadocFromCache != null) {
return classJavadocFromCache;
} else {
ClassJavadoc classJavadoc = readFiles(clazz, relativePath);
classCache.put(relativePath, classJavadoc);
return classJavadoc;
}
}
private String classToRelativePath(Class<?> clazz) {
String packageName = clazz.getPackage().getName();
String packageDir = packageName.replace(".", File.separator);
String className = clazz.getCanonicalName().replaceAll(packageName + "\\.?", "");
return new File(packageDir, className + ".json").getPath();
}
private ClassJavadoc readFiles(Class<?> clazz, String relativePath) {
if (absoluteBaseDirs.isEmpty()) {
// No absolute directory is configured and thus we try to find the file relative.
ClassJavadoc classJavadoc = readJson(new File(relativePath));
if (classJavadoc != null) {
return classJavadoc;
}
} else {
// Try to find the file in all configured directories.
for (File dir : absoluteBaseDirs) {
ClassJavadoc classJavadoc = readJson(new File(dir, relativePath));
if (classJavadoc != null) {
return classJavadoc;
}
}
}
// might be in some jar on the classpath
URL url = getClass().getClassLoader().getResource(relativePath);
if (url != null) {
return readJson(url);
}
log.debug("No Javadoc found for class {} in any of the found JSON files", clazz.getCanonicalName());
return new ClassJavadoc();
}
private ClassJavadoc readJson(File docSource) {
try {
return mapper
.readerFor(ClassJavadoc.class)
.readValue(docSource);
} catch (FileNotFoundException e) {
// Ignored as we might try more than one file and we warn if no Javadoc file
// is found at the end.
} catch (IOException e) {
log.error("Failed to read file {}", docSource.getName(), e);
}
return null;
}
private ClassJavadoc readJson(URL docSource) {
try {
return mapper
.readerFor(ClassJavadoc.class)
.readValue(docSource);
} catch (IOException e) {
log.error("Failed to read url {}", docSource, e);
}
return null;
}
private static ObjectMapper objectMapper() {
ObjectMapper mapper = new ObjectMapper();
mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
mapper.setVisibility(mapper.getSerializationConfig().getDefaultVisibilityChecker()
.withFieldVisibility(JsonAutoDetect.Visibility.ANY)
.withGetterVisibility(JsonAutoDetect.Visibility.NONE)
.withSetterVisibility(JsonAutoDetect.Visibility.NONE)
.withCreatorVisibility(JsonAutoDetect.Visibility.NONE));
return mapper;
}
private static List<File> toAbsoluteDirs(String javadocJsonDirs) {
List<File> absoluteDirs = new ArrayList<>();
if (StringUtils.hasText(javadocJsonDirs)) {
String[] dirs = javadocJsonDirs.split(PATH_DELIMITER);
for (String dir : dirs) {
if (StringUtils.hasText(dir)) {
absoluteDirs.add(new File(dir.trim()).getAbsoluteFile());
}
}
}
return absoluteDirs;
}
/**
* Walks up the class hierarchy and interfaces until a comment is found or top most class is reached.
* <p>
* Javadoc on super classes and Javadoc on interfaces of super classes has precedence over the Javadoc on direct
* interfaces of the class. This is only important in the rare case of competing Javadoc comments.
* <p>
* As we do not know the full method signature here, we can not check whether a method in the super class actually
* overwrites the given method. However, the Javadoc model ignores method signatures anyway and it should not cause
* issues for the usual use case.
*/
private String resolveCommentFromClassHierarchy(Class<?> javaBaseClass,
CommentExtractor commentExtractor) {
String comment = commentExtractor.comment(classJavadoc(javaBaseClass));
if (StringUtils.hasText(comment)) {
// Direct Javadoc on a method always wins.
return comment;
}
// Super class has precedence over interfaces, but this also means that interfaces
// of super classes have precedence over interfaces of the class itself.
if (javaBaseClass.getSuperclass() != null) {
String superClassComment = resolveCommentFromClassHierarchy(javaBaseClass.getSuperclass(),
commentExtractor);
if (StringUtils.hasText(superClassComment)) {
return superClassComment;
}
}
for (Class<?> i : javaBaseClass.getInterfaces()) {
String interfaceComment = resolveCommentFromClassHierarchy(i, commentExtractor);
if (StringUtils.hasText(interfaceComment)) {
return interfaceComment;
}
}
return "";
}
private interface CommentExtractor {
String comment(ClassJavadoc classJavadoc);
}
}
}