前言
目前在这家公司遇到一个比较有意思的需求,特此记录
需求是这样的,当前系统需要对外提供 OpenApi 接口以供第三方调用,类似提供给第三方直接调用的 SDK。最常见的实现可能就是定义专门对外提供服务的 Controller,指定不同的访问路径来访问我们的 OpenApi(类比普通 SpringBoot 接口)。但目前我们想要的效果是让所有的接口走一个统一的方法,再由这个方法分发到具体的实现上。那这个有意思的需求要如何实现呢?
实现思路
我们肯定需要一个统一的处理器,让所有的第三方都调用这个处理器。那处理器内部怎么知道不同的客户要调用哪些接口呢?首先想到的肯定是需要用户除了传递业务参数外,多传递一个 method 属性。通过这个 method 方法名属性去调用对应的方法
第一个难点在于我们如何通过一个字符串的方法名找到对应的方法呢?
- 我想到的是通过一个
Map<String, Method>
。key 对应方法名,value 对应 Method 方法、
第二个难点在于我们怎么知道有哪些方法需要存入 Map 中?前面说了 OpenApi 的方法都是在不同的类中的。难道我们要硬编码,写死具体的方法名吗?
- 肯定是不行的,这样就降低了复用性。我的思路是使用自定义注解,标记哪些类是供 OpenApi 使用。然后通过 Spring 的上下文对象解析自定义注解,找到所有被自定义注解注释的类
第二个难点在于我们如何初始化这个 Map 呢?应该怎么把这些信息封装进去呢?
- 这就需要了解 Spring 的 Bean 生命周期及相关知识
- 我们可以定义一个配置类,把我们的 Map 定义成一个 Bean,让 Spring 在启动的时候去加载并且初始化这个 Bean。这样 Spring 项目启动成功后,这个 Bean 里面也就有了数据
三大难点我们分析出来了,就一起来实现代码试试吧
代码实现
- 定义自定义注解,作用是所有被此注解标记的类都是对外提供 OpenApi 的
java
/**
* @Author: ZhangGongMing
* @CreateTime: 2025/4/25 17:05
* @Description: 标记初始化时需要扫描的服务
* @Version: 1.0
*/
@Target(ElementType.TYPE) // 作用在类上
@Retention(RetentionPolicy.RUNTIME) // 保留到运行期
public @interface ScannableService {
/**
* 方法名
*
*/
String value() default "";
}
- 定义 Config 配置文件,解析并向 Map 中存放 Method 数据
java
/**
* @Author: ZhangGongMing
* @CreateTime: 2025/4/25 17:24
* @Description: 初始化时扫描服务配置
* @Version: 1.0
*/
@Slf4j
@Configuration // 标记为配置文件
public class ScannableConfig {
@Bean // 标记成 bean,让 Spring 启动时加载并初始化
public Map<String, Method> methodMap(ApplicationContext context) {
HashMap<String, Method> methodMap = new HashMap<>();
this.init(context, methodMap); // 初始化逻辑
return methodMap;
}
/**
* 初始化上下文方法映射
*
* @param context 上下文对象
* @param methodMap 方法映射 Map
*/
private void init(ApplicationContext context, Map<String, Method> methodMap) {
log.info("========= 初始化上下文方法映射 =========");
// 1.获取所有带有 @ScannableService 注解的类
Map<String, Object> annotationBeanMap = context.getBeansWithAnnotation(ScannableService.class);
// 2.遍历每个类,获取内部方法并加入 map
annotationBeanMap.forEach((beanName, bean) -> {
Class<?> clazz = bean.getClass();
Method[] methods = clazz.getDeclaredMethods();
for (Method method : methods) {
methodMap.put(method.getName(), method);
}
});
log.info("========= 初始化上下文方法映射完成 =========");
}
}
这里有两个需要明确的知识点:
- @Bean 注解标记的方法必须要有返回值
- @PostConstruct 注解标记的方法会在依赖注入完成后执行,适合初始化操作,那此处为什么不直接在 init() 方法上使用次注解初始化呢?因为 @PostConstruct 注解必须用在没有形参的方法上,不然会报错无法执行
- 编写统一请求处理器
java
/**
* @Author: ZhangGongMing
* @CreateTime: 2025/4/25 14:15
* @Description: 统一请求处理器
* @Version: 1.0
*/
@Slf4j
@RestController
@RequestMapping("/api/v1/openapi")
@Api(tags = "统一请求类")
public class ConsistentController {
@Resource
private Map<String, Method> methodMap;
@Resource
private ApplicationContext context;
@PostMapping("/execute")
@ApiOperation(value = "统一请求处理方法")
public Result<?> execute(@RequestHeader(value = "method") String methodName,
@RequestBody Map<String, Object> paramMap) {
// 1.获取目标方法
Method method = methodMap.getOrDefault(methodName, null);
if (method == null) {
return Result.error(new BizException(ConsistentError.METHOD_NOT_FOUND), methodName);
}
// 2.解析方法参数
Object arg = this.resolveMethodParameters(method, paramMap);
Object targetBean = context.getBean(method.getDeclaringClass());
// 3.执行目标方法
Object result;
try {
result = method.invoke(targetBean, arg);
return Result.ok(result);
} catch (IllegalArgumentException e) {
return Result.error(ConsistentError.PARAM_TYPE_MISMATCH, e);
} catch (Exception e) {
return Result.error(ConsistentError.SYSTEM_ERROR, e);
}
}
/**
* 解析方法参数
*
* @param method 方法
* @param paramMap 形参列表
* @return 参数列表
*/
private Object resolveMethodParameters(Method method, Map<String, Object> paramMap) {
// 因为都是对象类型,保证形参只会有一个.直接写死 [0]
Class<?> parameterType = method.getParameterTypes()[0];
try {
ObjectMapper mapper = new ObjectMapper();
return mapper.convertValue(paramMap, parameterType);
} catch (IllegalArgumentException e) {
throw new BizException(ConsistentError.PARAM_TYPE_MISMATCH, e);
}
}
}
此处需要注意,Post 请求具体方法的参数我是使用 Map 来接受,然后 execute() 方法内部自己转成了对应的数据类型。而额外的 methodName 参数则是让第三方调用的时候填入请求头中,通过@RequestHeader 注解获取
这样,一个统一请求处理类就实现好了~
注意事项
这样统一请求路径的实现有什么好处呢?首先第三方用户不用去查看成百上千的 OpenApi 路径。只需要记住这一个统一请求方法的路径就好了,在一定程度上方便了第三方用户其次,有了统一请求处理器就能很方便的实现个性化需求。例如:我们可以方便的统计出第三方那个客户的调用频率、IP 地址等信息,方便我们进行数据分析等
还是那句话,不要为了用而用,不要炫技。这个方式不一定就比普通的通过 Controller 层的请求路径调用好。要根据需求及应用场景来进行选择!
本文由博客一文多发平台 OpenWrite 发布!