feat(extapi): 新增外部接口清单注解扫描与展示导出
新增 @ExternalApi/@ExternalCall 方法级注解,启动时扫描生成内存注册表。 提供 /sys/extApi/list 展示、exportXls(AutoPoi) 与 exportMarkdown 导出; 给 TaskApiController.countTaskforporter 打 @ExternalApi 示范并补菜单 SQL。
This commit is contained in:
+32
@@ -0,0 +1,32 @@
|
||||
package org.jeecg.common.aspect.annotation;
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* 对外提供的接口(供外部系统调用)
|
||||
* <p>标注在 Controller 方法上;接口地址与 HTTP 方法由框架从 @RequestMapping 自动推导。
|
||||
*/
|
||||
@Target(ElementType.METHOD)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
public @interface ExternalApi {
|
||||
|
||||
/** 接口名称/用途 */
|
||||
String name();
|
||||
|
||||
/** 调用方系统(谁在调我) */
|
||||
String caller() default "";
|
||||
|
||||
/** 认证机制 */
|
||||
ExternalApiAuthType auth() default ExternalApiAuthType.NONE;
|
||||
|
||||
/** 数据范围说明(返回/涉及什么数据) */
|
||||
String dataScope() default "";
|
||||
|
||||
/** 备注 */
|
||||
String remark() default "";
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
package org.jeecg.common.aspect.annotation;
|
||||
|
||||
/**
|
||||
* 外部接口认证机制
|
||||
*/
|
||||
public enum ExternalApiAuthType {
|
||||
/** 无需认证(裸调用) */
|
||||
NONE,
|
||||
/** 共享密钥签名(X-Sign / X-TIMESTAMP) */
|
||||
SIGN,
|
||||
/** JWT 登录态 */
|
||||
JWT
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
package org.jeecg.common.aspect.annotation;
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* 调用外部系统的接口(本系统调用其他系统)
|
||||
* <p>标注在发起出站调用的方法上(Service、客户端等)。
|
||||
*/
|
||||
@Target(ElementType.METHOD)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
public @interface ExternalCall {
|
||||
|
||||
/** 用途 */
|
||||
String name();
|
||||
|
||||
/** 被调用的外部系统 */
|
||||
String system();
|
||||
|
||||
/** 被调接口地址 */
|
||||
String url();
|
||||
|
||||
/** HTTP 方法 */
|
||||
String httpMethod() default "GET";
|
||||
|
||||
/** 对方要求的认证机制 */
|
||||
ExternalApiAuthType auth() default ExternalApiAuthType.NONE;
|
||||
|
||||
/** 交互的数据范围说明 */
|
||||
String dataScope() default "";
|
||||
|
||||
/** 备注 */
|
||||
String remark() default "";
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package org.jeecg.config.extapi;
|
||||
|
||||
import org.jeecg.config.extapi.model.ExternalApiInfo;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 外部接口清单内存注册表
|
||||
* <p>启动时由 {@link ExternalApiScanner} 填充,运行时只读。
|
||||
*/
|
||||
public class ExternalApiRegistry {
|
||||
|
||||
private static final List<ExternalApiInfo> REGISTRY = new ArrayList<>();
|
||||
|
||||
private ExternalApiRegistry() {
|
||||
}
|
||||
|
||||
public static synchronized void set(List<ExternalApiInfo> list) {
|
||||
REGISTRY.clear();
|
||||
if (list != null) {
|
||||
REGISTRY.addAll(list);
|
||||
}
|
||||
}
|
||||
|
||||
public static List<ExternalApiInfo> get() {
|
||||
return Collections.unmodifiableList(REGISTRY);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
package org.jeecg.config.extapi;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jeecg.common.aspect.annotation.ExternalApi;
|
||||
import org.jeecg.common.aspect.annotation.ExternalApiAuthType;
|
||||
import org.jeecg.common.aspect.annotation.ExternalCall;
|
||||
import org.jeecg.config.extapi.model.ExternalApiInfo;
|
||||
import org.jeecg.config.extapi.model.ExternalApiType;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PatchMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.method.HandlerMethod;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 外部接口清单扫描器
|
||||
* <p>启动时扫描两类注解生成内存注册表:
|
||||
* <ul>
|
||||
* <li>@ExternalApi:扫描 Controller 方法(接口地址由 @RequestMapping 推导)</li>
|
||||
* <li>@ExternalCall:扫描全部 Spring Bean 方法(出站调用,地址取自注解)</li>
|
||||
* </ul>
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class ExternalApiScanner implements InitializingBean {
|
||||
|
||||
private final RequestMappingHandlerMapping requestMappingHandlerMapping;
|
||||
private final ApplicationContext applicationContext;
|
||||
|
||||
public ExternalApiScanner(RequestMappingHandlerMapping requestMappingHandlerMapping,
|
||||
ApplicationContext applicationContext) {
|
||||
this.requestMappingHandlerMapping = requestMappingHandlerMapping;
|
||||
this.applicationContext = applicationContext;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() {
|
||||
long start = System.currentTimeMillis();
|
||||
List<ExternalApiInfo> list = new ArrayList<>();
|
||||
scanExposeApis(list);
|
||||
scanCallApis(list);
|
||||
ExternalApiRegistry.set(list);
|
||||
long cost = System.currentTimeMillis() - start;
|
||||
log.info("【外部接口清单-扫描器】初始化完成, 对外提供 {} 条, 对外调用 {} 条, 耗时 {} 毫秒",
|
||||
list.stream().filter(i -> ExternalApiType.EXPOSE.name().equals(i.getType())).count(),
|
||||
list.stream().filter(i -> ExternalApiType.CALL.name().equals(i.getType())).count(),
|
||||
cost);
|
||||
}
|
||||
|
||||
/**
|
||||
* 扫描对外提供接口(Controller 方法)
|
||||
*/
|
||||
private void scanExposeApis(List<ExternalApiInfo> list) {
|
||||
Set<Class<?>> controllers = requestMappingHandlerMapping.getHandlerMethods().values().stream()
|
||||
.map(HandlerMethod::getBeanType)
|
||||
.collect(java.util.stream.Collectors.toSet());
|
||||
for (Class<?> controller : controllers) {
|
||||
RequestMapping base = controller.getAnnotation(RequestMapping.class);
|
||||
String[] baseUrl = Objects.nonNull(base) ? base.value() : new String[]{};
|
||||
for (Method method : controller.getDeclaredMethods()) {
|
||||
ExternalApi ann = method.getAnnotation(ExternalApi.class);
|
||||
if (ann == null) {
|
||||
continue;
|
||||
}
|
||||
RequestMappingInfo info = resolveRequestMapping(method);
|
||||
if (info == null) {
|
||||
log.warn("【外部接口清单-扫描器】@ExternalApi 方法未找到 HTTP 映射, 跳过, method: {}.{}",
|
||||
controller.getSimpleName(), method.getName());
|
||||
continue;
|
||||
}
|
||||
for (String url : rebuildUrl(baseUrl, info.uris)) {
|
||||
ExternalApiInfo item = new ExternalApiInfo();
|
||||
item.setType(ExternalApiType.EXPOSE.name());
|
||||
item.setName(ann.name());
|
||||
item.setUrl(url);
|
||||
item.setHttpMethod(info.httpMethod);
|
||||
item.setCallerSystem(ann.caller());
|
||||
item.setTargetSystem("本系统");
|
||||
item.setDataFlow("外部系统→本系统(请求入站);本系统→外部系统(数据出站)");
|
||||
item.setDataScope(ann.dataScope());
|
||||
item.setAuth(authLabel(ann.auth()));
|
||||
item.setSource(controller.getName() + "#" + method.getName());
|
||||
item.setRemark(ann.remark());
|
||||
list.add(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 扫描对外调用接口(全部 Spring Bean 方法)
|
||||
*/
|
||||
private void scanCallApis(List<ExternalApiInfo> list) {
|
||||
Set<String> visited = new HashSet<>();
|
||||
String[] beanNames = applicationContext.getBeanDefinitionNames();
|
||||
for (String beanName : beanNames) {
|
||||
Class<?> type = applicationContext.getType(beanName);
|
||||
if (type == null || type.getName().startsWith("org.springframework")
|
||||
|| type.getName().startsWith("org.apache")
|
||||
|| type.getName().startsWith("java.")
|
||||
|| type.getName().startsWith("lombok")) {
|
||||
continue;
|
||||
}
|
||||
Class<?> target = type.getName().contains("$$") && type.getSuperclass() != null
|
||||
? type.getSuperclass() : type;
|
||||
for (Method method : target.getDeclaredMethods()) {
|
||||
ExternalCall ann = method.getAnnotation(ExternalCall.class);
|
||||
if (ann == null) {
|
||||
continue;
|
||||
}
|
||||
String key = target.getName() + "#" + method.getName();
|
||||
if (!visited.add(key)) {
|
||||
continue;
|
||||
}
|
||||
ExternalApiInfo item = new ExternalApiInfo();
|
||||
item.setType(ExternalApiType.CALL.name());
|
||||
item.setName(ann.name());
|
||||
item.setUrl(ann.url());
|
||||
item.setHttpMethod(ann.httpMethod());
|
||||
item.setCallerSystem("本系统");
|
||||
item.setTargetSystem(ann.system());
|
||||
item.setDataFlow("本系统→外部系统(数据出站)");
|
||||
item.setDataScope(ann.dataScope());
|
||||
item.setAuth(authLabel(ann.auth()));
|
||||
item.setSource(target.getName() + "#" + method.getName());
|
||||
item.setRemark(ann.remark());
|
||||
list.add(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从方法的 HTTP 映射注解解析 URL 与 HTTP 方法
|
||||
*/
|
||||
private RequestMappingInfo resolveRequestMapping(Method method) {
|
||||
RequestMapping rm = method.getAnnotation(RequestMapping.class);
|
||||
if (rm != null) {
|
||||
return new RequestMappingInfo(rm.value(), rm.method().length > 0 ? rm.method()[0].name() : "ALL");
|
||||
}
|
||||
GetMapping get = method.getAnnotation(GetMapping.class);
|
||||
if (get != null) {
|
||||
return new RequestMappingInfo(get.value(), "GET");
|
||||
}
|
||||
PostMapping post = method.getAnnotation(PostMapping.class);
|
||||
if (post != null) {
|
||||
return new RequestMappingInfo(post.value(), "POST");
|
||||
}
|
||||
PutMapping put = method.getAnnotation(PutMapping.class);
|
||||
if (put != null) {
|
||||
return new RequestMappingInfo(put.value(), "PUT");
|
||||
}
|
||||
DeleteMapping del = method.getAnnotation(DeleteMapping.class);
|
||||
if (del != null) {
|
||||
return new RequestMappingInfo(del.value(), "DELETE");
|
||||
}
|
||||
PatchMapping patch = method.getAnnotation(PatchMapping.class);
|
||||
if (patch != null) {
|
||||
return new RequestMappingInfo(patch.value(), "PATCH");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private List<String> rebuildUrl(String[] bases, String[] uris) {
|
||||
List<String> urls = new ArrayList<>();
|
||||
if (bases.length > 0) {
|
||||
for (String base : bases) {
|
||||
for (String uri : uris) {
|
||||
urls.add(prefix(base) + prefix(uri.replaceAll("\\{.*?}", "*")));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (String uri : uris) {
|
||||
urls.add(prefix(uri.replaceAll("\\{.*?}", "*")));
|
||||
}
|
||||
}
|
||||
return urls;
|
||||
}
|
||||
|
||||
private String prefix(String seg) {
|
||||
return seg.startsWith("/") ? seg : "/" + seg;
|
||||
}
|
||||
|
||||
private String authLabel(ExternalApiAuthType auth) {
|
||||
switch (auth) {
|
||||
case SIGN:
|
||||
return "签名";
|
||||
case JWT:
|
||||
return "JWT";
|
||||
default:
|
||||
return "无";
|
||||
}
|
||||
}
|
||||
|
||||
private static class RequestMappingInfo {
|
||||
private final String[] uris;
|
||||
private final String httpMethod;
|
||||
|
||||
private RequestMappingInfo(String[] uris, String httpMethod) {
|
||||
this.uris = uris;
|
||||
this.httpMethod = httpMethod;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package org.jeecg.config.extapi.model;
|
||||
|
||||
import lombok.Data;
|
||||
import org.jeecgframework.poi.excel.annotation.Excel;
|
||||
|
||||
/**
|
||||
* 外部接口清单条目
|
||||
*/
|
||||
@Data
|
||||
public class ExternalApiInfo {
|
||||
|
||||
/** 方向类型(对外提供 / 对外调用) */
|
||||
@Excel(name = "方向", width = 12)
|
||||
private String type;
|
||||
|
||||
/** 接口名称/用途 */
|
||||
@Excel(name = "接口名称", width = 28)
|
||||
private String name;
|
||||
|
||||
/** 接口地址 */
|
||||
@Excel(name = "接口地址", width = 50)
|
||||
private String url;
|
||||
|
||||
/** HTTP 方法 */
|
||||
@Excel(name = "HTTP方法", width = 10)
|
||||
private String httpMethod;
|
||||
|
||||
/** 调用方系统(谁在调) */
|
||||
@Excel(name = "调用方系统", width = 20)
|
||||
private String callerSystem;
|
||||
|
||||
/** 被调系统(调谁) */
|
||||
@Excel(name = "被调系统", width = 20)
|
||||
private String targetSystem;
|
||||
|
||||
/** 数据流向 */
|
||||
@Excel(name = "数据流向", width = 45)
|
||||
private String dataFlow;
|
||||
|
||||
/** 数据范围 */
|
||||
@Excel(name = "数据范围", width = 45)
|
||||
private String dataScope;
|
||||
|
||||
/** 认证机制 */
|
||||
@Excel(name = "认证机制", width = 12)
|
||||
private String auth;
|
||||
|
||||
/** 代码位置(类#方法) */
|
||||
@Excel(name = "代码位置", width = 70)
|
||||
private String source;
|
||||
|
||||
/** 备注 */
|
||||
@Excel(name = "备注", width = 30)
|
||||
private String remark;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package org.jeecg.config.extapi.model;
|
||||
|
||||
/**
|
||||
* 外部接口方向类型
|
||||
*/
|
||||
public enum ExternalApiType {
|
||||
/** 对外提供(外部系统调用本系统) */
|
||||
EXPOSE,
|
||||
/** 对外调用(本系统调用外部系统) */
|
||||
CALL
|
||||
}
|
||||
Reference in New Issue
Block a user