基于springboot和freemarker动态生成接口

目背景与目标

当前正在开发数据共享模块的接口自动生成功能,核心目标是简化API开发流程。用户仅需在前端页面配置少量SQL语句,系统即可自动生成可直接调用的API接口,同时配套生成标准化的接口文档。

将用几篇文章记录一下整个探索过程。该系列文章仅展示核心原理,由于实际业务中涉及诸多功能耦合,这里仅呈现最小必要代码以保持清晰度。

方法一: RequestMappingHandlerMapping

简单来说,要实现动态生成接口功能,关键在于RequestMappingHandlerMapping。在SpringMVC框架中,该类存储了所有路径与对应方法的映射关系。要实现动态接口添加,只需在该类中注册新的路径与实现方法即可。

方法二 伪”动态接口

方法二是一种“伪”动态接口生成的方法,即采用一个统一接口接收用户的sql相关信息存储在数据库中,不管什么样的接口请求都会向这个接口请求。但是对用户来说有很多个不同的接口。在向RequestMappingHandlerMapping注册的只有一个接口。这种方法本篇文章暂不讨论。在实际工程落地的过程中采用了这种方案。

方法一的实现

我们到底需要实现什么?

  1. 实现一个注册接口,接收输查询sql和对应的路径URI和请求方式实现GET和POST。
  2. 向步骤1中创建的接口请求传参能够预期获取结果。
  3. 能够打印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就可以识别它了。

好了现在就可以正确运行。

创建并成功运行GET接口

在这里插入图片描述

创建并成功运行POST接口

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值