多模块下理解业务和查找问题不是很方便,个人认为一张含有url信息的思维导图还是蛮重要的,此处分享一个工具类,可以将各个模块的信息汇总生成一张思维导图,方便业务理解和方法定位。
设计思路:继承BeanPostProcessor,在每个类加载前解析类的注解信息生成对应的文本写入到markdown(.MD)文件中,通过免费的XMind Zen导入生成思维导图。
使用方式:将此类放置于共同模块下,使用时复制进去改改对应的生成信息然后依次运行各模块即可。使用完删除或者注释@Component注解
代码如下:
package com.lance.common.config;
import com.sun.deploy.util.StringUtils;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Controller;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.*;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
/**
* 将controller生成MD文档
*
* @author lance
*/
@Component
@SuppressWarnings("unchecked")
public class GenerateControllerMapping implements BeanPostProcessor
{
/**
* 项目名 MD文档内容最高级节点,即xMind最高级节点
*/
private static final String PROJECT_NAME = "Sample API";
/**
* 生成哪个包下的controller(此处主要是过滤框架的controller)
*/
private static final String SCAN_PACKAGE = "lance";
/**
* 文档生成路径(会创建文件但不会创建文件夹)
*/
private static final String MD_PATH = "C:\\Users\\lance\\Desktop\\Sample.md";
private static final String WRAP = "\n";
private static final String POUND = "#";
private static final String SPACE = " ";
private static final String DASH_AND_SPACE = "- ";
private static final String COLON_AND_SPACE = ": ";
/**
* 模块名 MD文档内容次高级节点,即xMind次高级节点
*/
@Value("${server.servlet.context-path}")
private String moduleName;
/**
* 是否生成模块名的层级,一个module中只生成一次此层级,即在生成的第一个controller文本信息中加入
*/
private boolean generateModuleLayer = true;
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException
{
// 扫描名字叫controller及在SCAN_PACKAGE下的类
if (beanName.toLowerCase().contains("controller") && bean.getClass().getPackage().toString().contains(SCAN_PACKAGE))
{
// 解析含controller相关注解的类
RestController restController = bean.getClass().getAnnotation(RestController.class);
Controller controller = bean.getClass().getAnnotation(Controller.class);
if (restController == null && controller == null)
{
return bean;
}
// 解析controller的RequestMapping注解,组装信息
RequestMapping requestMapping = bean.getClass().getAnnotation(RequestMapping.class);
String controllerMappingMsg = "";
if (requestMapping != null)
{
String controllerMappingUrl = appendStrListMsg(Arrays.asList(requestMapping.value()));
String controllerMappingMethod = appendStrListMsg(formatRequestMethods(requestMapping.method()));
controllerMappingUrl = controllerMappingUrl.isEmpty() ? "" : " URL: " + controllerMappingUrl;
controllerMappingMethod = controllerMappingMethod.isEmpty() ? "" : " TYPE: " + controllerMappingMethod;
if (!controllerMappingUrl.isEmpty() || !controllerMappingMethod.isEmpty())
{
controllerMappingMsg = "[" + controllerMappingUrl + controllerMappingMethod + " ]";
}
}
StringBuilder controllerInfoBuffer = new StringBuilder();
if (generateModuleLayer)
{
controllerInfoBuffer.append(appendDuplicatedCharacter(POUND, 2)).append(SPACE).append(moduleName).append(WRAP);
controllerInfoBuffer.append(WRAP);
generateModuleLayer = false;
}
controllerInfoBuffer.append(appendDuplicatedCharacter(POUND, 3)).append(SPACE).append(beanName).append(controllerMappingMsg).append(WRAP);
// 解析每个方法,生成方法的http请求方式及地址
Method[] methods = bean.getClass().getMethods();
List<MethodMap> methodMaps = analysisAndBuildMethods(methods);
methodMaps.forEach(methodMap -> controllerInfoBuffer.append(DASH_AND_SPACE).append(methodMap.getRequestTypesStr()).append(COLON_AND_SPACE).append(methodMap.getRequestUrlsStr()).append(WRAP));
appendMsgToFile(controllerInfoBuffer.toString());
controllerInfoBuffer.setLength(0);
System.out.println("####-Generated:" + beanName);
return bean;
}
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException
{
return bean;
}
/**
* 获取requestMapping等的相关信息
*/
private MappingResult getMappingUrls(Method method)
{
RequestMapping requestMapping = method.getAnnotation(RequestMapping.class);
if (requestMapping != null)
{
RequestMethod[] requestMethod = requestMapping.method();
return new MappingResult(formatRequestMethods(requestMethod), Arrays.asList(requestMapping.value()));
}
PutMapping putMapping = method.getAnnotation(PutMapping.class);
if (putMapping != null)
{
return new MappingResult(Arrays.asList(RequestMethod.PUT.toString()), Arrays.asList(putMapping.value()));
}
DeleteMapping deleteMapping = method.getAnnotation(DeleteMapping.class);
if (deleteMapping != null)
{
return new MappingResult(Arrays.asList(RequestMethod.DELETE.toString()), Arrays.asList(deleteMapping.value()));
}
PostMapping postMapping = method.getAnnotation(PostMapping.class);
if (postMapping != null)
{
return new MappingResult(Arrays.asList(RequestMethod.POST.toString()), Arrays.asList(postMapping.value()));
}
GetMapping getMapping = method.getAnnotation(GetMapping.class);
if (getMapping != null)
{
return new MappingResult(Arrays.asList(RequestMethod.GET.toString()), Arrays.asList(getMapping.value()));
}
PatchMapping patchMapping = method.getAnnotation(PatchMapping.class);
if (patchMapping != null)
{
return new MappingResult(Arrays.asList(RequestMethod.PATCH.toString()), Arrays.asList(patchMapping.value()));
}
return null;
}
/**
* 将RequestMethod枚举属性转为String
*/
private List<String> formatRequestMethods(RequestMethod[] requestMethod)
{
return Arrays.stream(requestMethod).map(Enum::name).collect(Collectors.toList());
}
/**
* 解析方法信息并生成信息串
*/
private List<MethodMap> analysisAndBuildMethods(Method[] methods)
{
List<MethodMap> methodMaps = new ArrayList<>(methods.length);
Arrays.stream(methods).forEach(method ->
{
MappingResult mappingResult = getMappingUrls(method);
if (mappingResult == null)
{
return;
}
List<String> httpRequestTypes = mappingResult.getRequestTypes();
List<String> requestMappingUrls = mappingResult.getRequestUrls();
String httpRequestTypeStr;
String requestMappingUrlStr;
if (CollectionUtils.isEmpty(httpRequestTypes))
{
httpRequestTypeStr = "ALL";
}
else
{
httpRequestTypeStr = appendStrListMsg(httpRequestTypes);
}
if (CollectionUtils.isEmpty(requestMappingUrls))
{
requestMappingUrlStr = "";
}
else
{
requestMappingUrlStr = appendStrListMsg(requestMappingUrls);
}
methodMaps.add(new MethodMap(httpRequestTypeStr, requestMappingUrlStr));
});
methodMaps.sort(Comparator.comparing(MethodMap::getRequestTypesStr).thenComparing(MethodMap::getRequestUrlsStr));
return methodMaps;
}
/**
* 将list转为 x,y,z
*/
private String appendStrListMsg(List<String> list)
{
return StringUtils.join(list, ",");
}
/**
* 针对#等需要多次拼接的字符串提供的一个简单方法,传入次数返回拼接对应次数的信息
*/
private String appendDuplicatedCharacter(String duplicatedCharacter, int round)
{
StringBuilder sb = new StringBuilder();
for (int i = 0; i < round; i++)
{
sb.append(duplicatedCharacter);
}
return sb.toString();
}
/**
* 将信息写入文本中
*/
private void appendMsgToFile(String appendMsg)
{
FileOutputStream fos = null;
String initHeader = "";
try
{
File file = new File(MD_PATH);
if (!file.exists())
{
file.createNewFile();
initHeader = POUND + SPACE + PROJECT_NAME + WRAP;
}
fos = new FileOutputStream(MD_PATH, true);
fos.write((initHeader + appendMsg).getBytes(StandardCharsets.UTF_8));
}
catch (IOException e)
{
e.printStackTrace();
}
finally
{
try
{
if (fos != null)
{
fos.close();
}
}
catch (IOException e)
{
e.printStackTrace();
}
}
}
@Data
@AllArgsConstructor
@NoArgsConstructor
class MappingResult
{
List requestTypes;
List requestUrls;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
class MethodMap
{
String requestTypesStr;
String requestUrlsStr;
}
}
运行完生成MD文件后,使用XMInd Zen导入即可: