目背景与目标
当前正在开发数据共享模块的接口自动生成功能,核心目标是简化API开发流程。用户仅需在前端页面配置少量SQL语句,系统即可自动生成可直接调用的API接口,同时配套生成标准化的接口文档。
将用几篇文章记录一下整个探索过程。该系列文章仅展示核心原理,由于实际业务中涉及诸多功能耦合,这里仅呈现最小必要代码以保持清晰度。
文章目录
方法一: RequestMappingHandlerMapping
简单来说,要实现动态生成接口功能,关键在于RequestMappingHandlerMapping。在SpringMVC框架中,该类存储了所有路径与对应方法的映射关系。要实现动态接口添加,只需在该类中注册新的路径与实现方法即可。
方法二 伪”动态接口
方法二是一种“伪”动态接口生成的方法,即采用一个统一接口接收用户的sql相关信息存储在数据库中,不管什么样的接口请求都会向这个接口请求。但是对用户来说有很多个不同的接口。在向RequestMappingHandlerMapping注册的只有一个接口。这种方法本篇文章暂不讨论。在实际工程落地的过程中采用了这种方案。
方法一的实现
我们到底需要实现什么?
- 实现一个注册接口,接收输查询sql和对应的路径URI和请求方式实现GET和POST。
- 向步骤1中创建的接口请求传参能够预期获取结果。
- 能够打印RequestMappingHandlerMapping中注册的所有URI路径和对应的实现方法类。这样便于调试错误。
创建运行GET带参接口
创建运行POST带参接口
准备工作
pom 稍微注意一下就是引入了freemarker 用作动态生成sql
pom
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>demo</groupId>
<artifactId>cai</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>cai</name>
<description>cai</description>
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-boot.version>2.6.13</spring-boot.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.3.3</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.0</version>
</dependency>
<!-- lombok 代码简化 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.22</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.31</version>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot.version}</version>
<configuration>
<mainClass>demo.cai.CaiApplication</mainClass>
<skip>true</skip>
</configuration>
<executions>
<execution>
<id>repackage</id>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
注册接口服务
下面的就是向requestMappingHandlerMapping注册接口的核心类了。看着很简单,就是 调用 requestMappingHandlerMapping.registerMapping,向里面传参即可。但是这地方也要注意有坑点。
package demo.cai.dynamicApi;
import cn.hutool.core.collection.CollectionUtil;
import demo.cai.util.DynamicSqlGenerator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import org.springframework.web.util.pattern.PathPatternParser;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Predicate;
/**
* @Author xuliancheng
* @Date 2025/6/13 18:41
* @Version 1.0
*/
@Service
public class DynamicSqlApiService {
@Autowired
private RequestMappingHandlerMapping requestMappingHandlerMapping;
@Autowired
private JdbcTemplate jdbcTemplate;
public void registerApi(ApiDefinition apiDefinition) {
try {
// 创建动态控制器实例
Object controller = new DynamicSqlController(jdbcTemplate, apiDefinition);
// 获取方法
Method method = DynamicSqlController.class.getMethod("executeQuery", Map.class,Map.class);
// Method method2 = DynamicSqlController.class.getMethod("executeQuery2");
// 构建请求映射信息,使用 PathPatternParser 处理路径
RequestMappingInfo.Builder builder = RequestMappingInfo
.paths(apiDefinition.getPath())
.methods(RequestMethod.valueOf(apiDefinition.getMethod()));
// !!!!注意此处要加builderConfiguration 把下面的代码注释打开就是正确代码
// RequestMappingInfo.BuilderConfiguration builderConfiguration = new RequestMappingInfo.BuilderConfiguration();
//
// builderConfiguration.setPatternParser(new PathPatternParser());
// builder.options(builderConfiguration);
//
RequestMappingInfo mappingInfo = builder.build();
// 注册到Spring MVC
requestMappingHandlerMapping.registerMapping(mappingInfo, controller, method);
// requestMappingHandlerMapping.registerMapping(mappingInfo, controller, method2);
} catch (Exception e) {
throw new RuntimeException("注册API失败", e);
}
}
}
注册接口的controller
这是向requestMappingHandlerMapping 注册的controller和对应的方法。不管是GET还是POST都是用这个类进行解析。
package demo.cai.dynamicApi;
import cn.hutool.core.collection.CollectionUtil;
import demo.cai.util.DynamicSqlGenerator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @Author xuliancheng
* @Date 2025/6/17 19:32
* @Version 1.0
*/
//@RestController注释打开就是正确代码
//@RestController
public class DynamicSqlController {
private final JdbcTemplate jdbcTemplate;
private final ApiDefinition apiDefinition;
public DynamicSqlController(JdbcTemplate jdbcTemplate, @Autowired(required = false)ApiDefinition apiDefinition) {
this.jdbcTemplate = jdbcTemplate;
this.apiDefinition = apiDefinition;
}
public String executeQuery2()
{
System.out.println("我已经进入了executeQuery2");
return "hhhhhhhhh";
}
public List<Map<String, Object>> executeQuery(@RequestParam(required = false) Map<String, String> queryParams, @RequestBody(required = false) Map<String, Object> bodyParams) throws Exception {
//将所有查询参数放到一个Map中
HashMap<String, Object> allParams = new HashMap<>();
if (CollectionUtil.isNotEmpty(queryParams)) {
allParams.putAll(queryParams);
}
if (CollectionUtil.isNotEmpty(bodyParams)) {
allParams.putAll(bodyParams);
}
// 执行SQL查询
//动态拼接sql
String sql = DynamicSqlGenerator.generateSqlFromTemplateString(apiDefinition.getSql(), allParams);
System.out.println(sql);
return jdbcTemplate.queryForList(sql);
}
}
动态创建接口的接口
这个接口实现了两个功能,一个是调用register 创建一个接口
二是可以查看requestMappingHandlerMapping注册了哪些路径和对应的处理器。
package demo.cai.dynamicApi;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.mvc.condition.PathPatternsRequestCondition;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import java.util.Map;
/**
* @Author xuliancheng
* @Date 2025/6/13 18:41
* @Version 1.0
*/
@RestController
@RequestMapping("/api/dynamic")
public class DynamicSqlApiController {
@Autowired
private DynamicSqlApiService dynamicSqlApiService;
@Autowired
private RequestMappingHandlerMapping requestMappingHandlerMapping;
@PostMapping("/register")
public String registerDynamicApi(@RequestBody ApiDefinition apiDefinition) {
dynamicSqlApiService.registerApi(apiDefinition);
return "API 已成功注册: " + apiDefinition.getPath();
}
@GetMapping("/printPathInfo")
public void printPathInfo() {
// 获取所有的请求映射信息
Map<RequestMappingInfo, HandlerMethod> handlerMethods =
requestMappingHandlerMapping.getHandlerMethods();
// 遍历输出
for (Map.Entry<org.springframework.web.servlet.mvc.method.RequestMappingInfo, org.springframework.web.method.HandlerMethod> entry : handlerMethods.entrySet()) {
org.springframework.web.servlet.mvc.method.RequestMappingInfo info = entry.getKey();
org.springframework.web.method.HandlerMethod method = entry.getValue();
//路径
PathPatternsRequestCondition pathPatternsCondition = info.getPathPatternsCondition();
if (pathPatternsCondition!=null)
{
for (String directPath : pathPatternsCondition.getDirectPaths()) {
System.out.println("路径:"+directPath);
}
}
// 打印对应的方法名和类名
System.out.println("Class: " + method.getBeanType().getName());
System.out.println("Method:"+method.getMethod().getName());
System.out.println("----------------------------------------");
}
}
}
动态生成sql的freemarker工具类
package demo.cai.util;
import freemarker.template.Configuration;
import freemarker.template.Template;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.HashMap;
import java.util.Map;
/**
* @Author xuliancheng
* @Date 2025/6/17 18:37
* @Version 1.0
*/
public class DynamicSqlGenerator {
public static String generateSqlFromTemplateString(String templateString, Map<String, Object> params) throws Exception {
// 获取 FreeMarker 配置
Configuration cfg = new Configuration(Configuration.VERSION_2_3_31);
cfg.setDefaultEncoding("UTF-8");
// 设置不带逗号的数字格式
cfg.setNumberFormat("0.######"); // 不加千分位逗号,保留最多6位小数
// 使用字符串作为模板
Template template = new Template("sqlTemplate", new StringReader(templateString), cfg);
// 将 Map 中的参数传递给模板
StringWriter out = new StringWriter();
template.process(params, out);
// 返回生成的 SQL 语句
return out.toString();
}
public static void main(String[] args) throws Exception {
// // 定义 SQL 模板为字符串
// String sqlTemplateString = "SELECT * FROM user WHERE 1=1 "
// + "<#if name??> AND user_name = '${name}'</#if>"
// + "<#if email??> AND user_email = '${email}'</#if>"
// + "<#if age??> AND age = ${age}</#if>";
//
// // 准备动态参数
// Map<String, Object> params = new HashMap<>();
// params.put("name", "John");
// params.put("email", null); // email 不存在时 SQL 语句将跳过这个条件
// params.put("age", 25);
String sql2=" SELECT * FROM operation_ticket_data_digital_twin WHERE 1=1 <#if year??> AND year >= ${year}</#if>";
// 准备动态参数
Map<String, Object> params2 = new HashMap<>();
params2.put("year", 2021);
// 生成 SQL
String sql = generateSqlFromTemplateString(sql2, params2);
System.out.println(sql);
}
}
API定义的实体类
package demo.cai.dynamicApi;
import lombok.Data;
import java.util.List;
/**
* @Author xuliancheng
* @Date 2025/6/13 18:40
* @Version 1.0
*/
@Data
public class ApiDefinition {
private String path; // API 路径
private String sql; // SQL 查询语句
private String method; // HTTP 方法 (GET, POST 等)
}
好了,基础的准备代码已经做好了,这是最开始我写出的代码,下面将进行试错了。如果读者不关心调试过程,只需要将上面的代码中提示去掉注释的地方放开即可成功运行。
错误1:Expected lookupPath in request attribute “org.springframework.web.util.UrlPathHelper.PATH”.
这里的sql是:
SELECT "Index_code" ,"Index_text" FROM "ads_index_code" WHERE 1=1 <#if Index_code??> AND "Index_code" = '${Index_code}'</#if>
看到这样的sql 不要觉得奇怪,有点类似于mybatis的xml。这里用的是freemarker结合动态生成sql。关于freemarker的语法不在本文的讨论范畴。
很好,接口看似注册成功。那下面让我们调用一下。
报错了: Expected lookupPath in request attribute “org.springframework.web.util.UrlPathHelper.PATH”.
明明已经指定了url路径和对应的handler,为什么还会出错呢?
google了相关文档终于在一篇外文文章中找到了答案
这个问题是由 Spring 对路径模式匹配所做的更改引起的。现在 Spring 希望 RequestMappingInfo 使用 PathPatternsRequestCondition,而之前使用的是 PatternsRequestCondition。
解决这个问题有两种解决方法
1.在你的yml配置文件中设置属性:spring.mvc.pathmatch.matching-strategy=ANT_PATH_MATCHER
2. 使用下面的代码放入RequestMappingInfo.Builder.option中去。将上面代码中提示放开注释的地方就是正确代码。
RequestMappingInfo.BuilderConfiguration options = new RequestMappingInfo.BuilderConfiguration();
options.setPatternParser(new PathPatternParser());
好了让我们修复代码,再次请求接口进行调试。
错误2:很奇怪的404
{
"timestamp": "2025-06-18T11:51:40.831+00:00",
"status": 404,
"error": "Not Found",
"path": "/api/users2"
}
控制台没有任何报错,但是可以看到,我们的动态sql执行了,也进入了对应的方法。但是为什么会报错404呢?
我们调用printPathInfo接口查看到底接口注册成功没有,怎么会报404呢?
由上面的图可以知道,这个接口显然是已经注册成功了。
请求路径匹配成功(进入了方法),但 Spring MVC 没有识别该 Controller 是一个 @RestController 或没有正确处理其响应体。
最终导致 Spring 认为没有匹配的 HandlerMethod,或未触发响应写入。
所以将@RestController加上,springmvc对应的Processor就可以识别它了。
好了现在就可以正确运行。