Skip to content

Instantly share code, notes, and snippets.

@naosim
Last active February 20, 2021 23:48
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save naosim/68b555db03ecfcc1eb7f74867ea98fdd to your computer and use it in GitHub Desktop.
Save naosim/68b555db03ecfcc1eb7f74867ea98fdd to your computer and use it in GitHub Desktop.
Springフレームワークのエンドポイントを取得する 開発ドキュメント作成用ツール
import org.assertj.core.api.exception.RuntimeIOException;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
import org.springframework.core.type.filter.RegexPatternTypeFilter;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.util.*;
import java.util.concurrent.Callable;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* Springフレームワークのエンドポイントを取得する
* 開発ドキュメント作成用ツール
*
* usage:
* ```
* List<ApiEndPointAnalyzer.ApiEndPoint> endPointList = ApiEndPointAnalyzer.findAllEndPoint(new ApiEndPointAnalyzer.Config(
* new File("."),
* "your.root.package"
* ));
* ```
*/
public class ApiEndPointAnalyzer {
public static List<ApiEndPoint> findAllEndPoint(Config config) {
SourceCode.Repository sourceCodeRepository = new SourceCode.Repository(config.rootDir);
sourceCodeRepository.init();
return new Service(
sourceCodeRepository,
new Clazz.Repository(config.rootPackage)
).findAllEndPoint();
}
static class Service {
private final SourceCode.Repository sourceCodeRepository;
private final Clazz.Repository classRepository;
public Service(
SourceCode.Repository sourceCodeRepository,
Clazz.Repository classRepository
) {
this.sourceCodeRepository = sourceCodeRepository;
this.classRepository = classRepository;
}
public List<ApiEndPoint> findAllEndPoint() {
return classRepository.findAllEndPoint().stream().map(e -> new ApiEndPoint(
e.getEndPointPath(),
e.getApiPackageName(),
sourceCodeRepository.findCommentFirstLine(e.getApiPackageName())
)).collect(Collectors.toList());
}
}
public static class Config {
final File rootDir;
final String rootPackage;
public Config(File rootDir, String rootPackage) {
this.rootDir = rootDir;
this.rootPackage = rootPackage;
}
}
static class ApiEndPoint {
private final String endPointPath;
private final PackageName packageName;
private final Optional<CommentFirstLine> commentFirstLine;
public ApiEndPoint(String endPointPath, PackageName packageName, Optional<CommentFirstLine> commentFirstLine) {
this.endPointPath = endPointPath;
this.packageName = packageName;
this.commentFirstLine = commentFirstLine;
}
public String getEndPointPath() {
return endPointPath;
}
public PackageName getPackageName() {
return packageName;
}
public Optional<CommentFirstLine> getCommentFirstLine() {
return commentFirstLine;
}
}
static class CommentFirstLine {
private final String value;
public CommentFirstLine(String value) {
this.value = value;
}
public String getValue() {
return value;
}
}
static class PackageName {
private final String value;
public PackageName(String value) {
this.value = value;
}
public String getValue() {
return value;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
PackageName that = (PackageName) o;
return Objects.equals(value, that.value);
}
@Override
public int hashCode() {
return Objects.hash(value);
}
}
static class SourceCode {
static class ClassCodeFile {
private final PackageName packageName;
private final Optional<CommentFirstLine> commentFirstLine;
public ClassCodeFile(PackageName packageName, Optional<CommentFirstLine> commentFirstLine) {
this.packageName = packageName;
this.commentFirstLine = commentFirstLine;
}
}
static class ClassCodeFileFactory {
private final String text;
public ClassCodeFileFactory(String text) {
this.text = text;
}
public Optional<ClassCodeFile> create() throws IOException {
if(!isClass()) {
return Optional.empty();
}
return Optional.of(new ClassCodeFile(
getPackageName(),
getCommentFirstLine()
));
//String text = Files.readAllLines(file.toPath()).stream().collect(Collectors.joining("\n"));
}
public boolean isClass() {
if(!this.text.contains("class ")) {
return false;
}
if(this.text.contains("interface ")) {
if(this.text.indexOf("class ") > this.text.indexOf("interface ")) {
return false;
}
}
return true;
}
public String getClassName() {
String afterClassText = text.split("class ")[1];
return afterClassText.substring(0, Math.min(afterClassText.indexOf(" "), afterClassText.indexOf("{"))).trim();
}
public PackageName getPackageName() {
return new PackageName(text.split("package ")[1].split(";")[0].trim() + "." + getClassName());
}
public Optional<CommentFirstLine> getCommentFirstLine() {
String beforeClassText = this.text.split("class ")[0];
if(!beforeClassText.contains("/**")) {
return Optional.empty();
}
String firstLine = beforeClassText.substring(beforeClassText.indexOf("/**")).split("\n")[1];
if(firstLine.indexOf("*") == -1) {
throw new RuntimeException(firstLine);
}
return Optional.of(new CommentFirstLine(firstLine.substring(firstLine.indexOf("*") + 1).trim()));
}
}
static class Repository {
private final File rootDir;
private final Map<PackageName, ClassCodeFile> map = new HashMap<>();
public Repository(File rootDir) {
if(rootDir.isFile()) {
throw new RuntimeException("rootDirがファイルです。ディレクトリを指定してください。");
}
this.rootDir = rootDir;
}
public Optional<ClassCodeFile> find(PackageName packageName) {
return Optional.ofNullable(map.get(packageName));
}
public Optional<CommentFirstLine> findCommentFirstLine(PackageName packageName) {
return this.find(packageName).flatMap(v -> v.commentFirstLine);
}
public void init() {
// mapを作る
findAllJavaFile().stream().forEach(it -> {
try {
if(!it.getName().contains("Api")) {
return;
}
String text = Files.readAllLines(it.toPath()).stream().collect(Collectors.joining("\n"));
new ClassCodeFileFactory(text).create().ifPresent(v -> map.put(v.packageName, v));
} catch (IOException e) {
throw new RuntimeIOException(e.getMessage());
}
});
}
List<File> findAllJavaFile() {
return findAllFile(this.rootDir).stream().filter(it -> {
if(!it.getName().contains(("."))) {
return false;
}
return it.getName().substring(it.getName().lastIndexOf(".")).equals(".java");
}).collect(Collectors.toList());
}
List<File> findAllFile(File dir) {
if(dir.isFile()) {
throw new RuntimeException("rootDirがファイルです。ディレクトリを指定してください。");
}
List<File> result = new ArrayList<>();
Stream.of(dir.listFiles()).forEach(it -> {
if(it.isFile()) {
result.add(it);
} else if (it.isDirectory()) {
result.addAll(this.findAllFile(it));
}
});
return result;
}
}
}
static class Clazz {
static class Repository {
private final String rootPackage;
public Repository(String rootPackage) {
this.rootPackage = rootPackage;
}
public List<EndPointInClass> findAllEndPoint() {
// セットアップ
ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false);
provider.addIncludeFilter(new RegexPatternTypeFilter(Pattern.compile(".*")));
// APIクラスの取得
Set<ApiClass> apis = provider.findCandidateComponents(rootPackage).stream()
.map(ApiClass::create)
.filter(Optional::isPresent).map(Optional::get)
.collect(Collectors.toSet());
// エンドポイント情報の取得
List<EndPointInClass> result = new ArrayList<>();
apis.forEach(api -> api.getEndPoints().forEach(endPoint -> result.add(endPoint)));
return result;
}
}
private static class ApiClass {
private final Class value;
private ApiClass(Class value) {
this.value = value;
}
public String getName() {
return this.value.getName();
}
public List<EndPointInClass> getEndPoints() {
Method[] methods = this.value.getMethods();
if(methods == null || methods.length == 0) {
throw new RuntimeException("RequestMappingがないパタンは非対応:" + this.getName());
}
return Arrays.stream(methods)
.map(v -> Optional.ofNullable(v.getAnnotation(RequestMapping.class)).map(a -> new EndPointInClass(this.value, a)))
.filter(v -> v.isPresent()).map(v -> v.get())
.collect(Collectors.toList());
}
public static Optional<ApiClass> create(BeanDefinition bean) {
Class clazz = uncheckCall(() -> Class.forName(bean.getBeanClassName()));
// @RestControllerがあるか?
return Optional
.ofNullable(clazz.getAnnotation(RestController.class))
.map(v -> new ApiClass(clazz));
}
}
static class EndPointInClass {
private final Class clazz;
private final RequestMapping requestMapping;
public EndPointInClass(Class clazz, RequestMapping requestMapping) {
this.clazz = clazz;
this.requestMapping = requestMapping;
}
public PackageName getApiPackageName() {
return new PackageName(this.clazz.getName());
}
public String getEndPointPath() {
if(this.requestMapping.value().length >= 2) {
throw new RuntimeException("エンドポイントのパスが2つあるパタンは非対応:" + this.getApiPackageName());
}
return this.requestMapping.value()[0];
}
}
private static <T> T uncheckCall(Callable<T> callable) {
try {
return callable.call();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment