实现一个功能:springboot项目启动将controller地址拼接打印到txt文件

实现一个功能:springboot项目启动将controller地址拼接打印到txt文件

以下是实现「SpringBoot启动时扫描Controller接口地址并拼接打印到TXT文件」的完整方案,包含核心代码、配置说明和注意事项,适配你的学生管理项目场景:

txt 复制代码
=== SpringBoot Controller Interface URLs ===
=== Generate Time: Sun Dec 07 15:41:01 CST 2025
============================================

/user/queryByPage
POST -> /user/queryByPage
/user/deleteById
POST -> /user/deleteById
/user/queryById
POST -> /user/queryById
/user/update
POST -> /user/update
/user/insert
POST -> /user/insert
/hello/entry
GET -> /hello/entry
/hello/say
GET -> /hello/say
/hello/say3
GET -> /hello/say3
/hello/first
GET -> /hello/first
/user-temp/format
ALL -> /user-temp/format

一、核心思路

  1. 扫描Controller :通过Spring上下文获取所有标注@RestController/@Controller的Bean;
  2. 解析接口映射 :反射获取@RequestMapping/@GetMapping等注解的路径;
  3. 拼接地址:组合Controller类级路径 + 方法级路径,生成完整接口地址;
  4. 写入TXT文件:将拼接后的地址按规范写入指定TXT文件,支持自定义路径。

二、完整实现代码

1. 核心工具类(扫描+写入)
java 复制代码
package com.geekmice.springbootmpredis.util;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationContext;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.stereotype.Controller;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.context.WebApplicationContext;

import javax.annotation.PostConstruct;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.*;

/**
 * SpringBoot启动时扫描Controller接口并写入TXT文件
 */
@Slf4j
public class ControllerUrlScanner {

    // 注入Spring上下文(WebApplicationContext包含Servlet相关信息)
    private final WebApplicationContext applicationContext;

    // 自定义TXT文件路径(可通过配置文件配置,默认项目根目录)
    @Value("${controller.url.txt.path:./controller-urls.txt}")
    private String txtFilePath;

    public ControllerUrlScanner(WebApplicationContext applicationContext) {
        this.applicationContext = applicationContext;
    }

    /**
     * 项目启动后执行(PostConstruct保证在SpringBean初始化完成后执行)
     */
    @PostConstruct
    public void scanAndWriteControllerUrls() {
        // 1. 存储所有接口地址(去重)
        Set<String> fullUrlSet = new LinkedHashSet<>();

        // 2. 获取所有标注@Controller/@RestController的Bean
        String[] controllerBeanNames = applicationContext.getBeanNamesForAnnotation(Controller.class);
        String[] restControllerBeanNames = applicationContext.getBeanNamesForAnnotation(RestController.class);

        // 合并Bean名称(避免重复)
        Set<String> allControllerBeanNames = new HashSet<>();
        allControllerBeanNames.addAll(Arrays.asList(controllerBeanNames));
        allControllerBeanNames.addAll(Arrays.asList(restControllerBeanNames));

        // 3. 遍历所有Controller Bean,解析接口地址
        for (String beanName : allControllerBeanNames) {
            Object controllerBean = applicationContext.getBean(beanName);
            Class<?> controllerClass = controllerBean.getClass();
            // 获取Controller类级别的@RequestMapping注解(基础路径)
            RequestMapping classRequestMapping = AnnotationUtils.findAnnotation(controllerClass, RequestMapping.class);
            String classBasePath = "";
            if (classRequestMapping != null && classRequestMapping.value().length > 0) {
                classBasePath = classRequestMapping.value()[0];
                // 统一路径格式(开头加/,结尾去/)
                classBasePath = formatPath(classBasePath);
            }

            // 4. 遍历Controller中的所有方法,解析方法级路径
            Method[] methods = controllerClass.getDeclaredMethods();
            for (Method method : methods) {
                // 支持的请求注解:GetMapping/PostMapping/PutMapping/DeleteMapping/RequestMapping
                String methodPath = getMethodRequestMappingPath(method);
                if (!StringUtils.hasText(methodPath)) {
                    continue; // 无映射路径的方法跳过
                }
                methodPath = formatPath(methodPath);

                // 5. 拼接完整路径(类级路径 + 方法级路径)
                String fullUrl = classBasePath + methodPath;
                fullUrlSet.add(fullUrl);

                // 可选:获取请求方法(GET/POST等)
                String httpMethod = getHttpMethod(method);
                fullUrlSet.add(httpMethod + " -> " + fullUrl);
            }
        }

        // 6. 将接口地址写入TXT文件
        writeUrlsToTxt(fullUrlSet);
    }

    /**
     * 格式化路径(统一开头加/,结尾去/)
     */
    private String formatPath(String path) {
        if (!path.startsWith("/")) {
            path = "/" + path;
        }
        if (path.endsWith("/") && path.length() > 1) {
            path = path.substring(0, path.length() - 1);
        }
        return path;
    }

    /**
     * 获取方法级的请求映射路径
     */
    private String getMethodRequestMappingPath(Method method) {
        // 优先级:GetMapping > PostMapping > PutMapping > DeleteMapping > RequestMapping
        if (AnnotationUtils.findAnnotation(method, GetMapping.class) != null) {
            GetMapping annotation = AnnotationUtils.findAnnotation(method, GetMapping.class);
            return annotation.value().length > 0 ? annotation.value()[0] : "";
        }
        if (AnnotationUtils.findAnnotation(method, PostMapping.class) != null) {
            PostMapping annotation = AnnotationUtils.findAnnotation(method, PostMapping.class);
            return annotation.value().length > 0 ? annotation.value()[0] : "";
        }
        if (AnnotationUtils.findAnnotation(method, PutMapping.class) != null) {
            PutMapping annotation = AnnotationUtils.findAnnotation(method, PutMapping.class);
            return annotation.value().length > 0 ? annotation.value()[0] : "";
        }
        if (AnnotationUtils.findAnnotation(method, DeleteMapping.class) != null) {
            DeleteMapping annotation = AnnotationUtils.findAnnotation(method, DeleteMapping.class);
            return annotation.value().length > 0 ? annotation.value()[0] : "";
        }
        if (AnnotationUtils.findAnnotation(method, RequestMapping.class) != null) {
            RequestMapping annotation = AnnotationUtils.findAnnotation(method, RequestMapping.class);
            return annotation.value().length > 0 ? annotation.value()[0] : "";
        }
        return "";
    }

    /**
     * 获取HTTP请求方法(GET/POST等)
     */
    private String getHttpMethod(Method method) {
        if (AnnotationUtils.findAnnotation(method, GetMapping.class) != null) {
            return "GET";
        }
        if (AnnotationUtils.findAnnotation(method, PostMapping.class) != null) {
            return "POST";
        }
        if (AnnotationUtils.findAnnotation(method, PutMapping.class) != null) {
            return "PUT";
        }
        if (AnnotationUtils.findAnnotation(method, DeleteMapping.class) != null) {
            return "DELETE";
        }
        // RequestMapping默认返回ALL
        RequestMapping requestMapping = AnnotationUtils.findAnnotation(method, RequestMapping.class);
        if (requestMapping != null && requestMapping.method().length > 0) {
            return Arrays.toString(requestMapping.method()).replace("[", "").replace("]", "");
        }
        return "ALL";
    }

    /**
     * 将接口地址写入TXT文件
     */
    private void writeUrlsToTxt(Set<String> urlSet) {
        // 创建文件父目录(若不存在)
        File txtFile = new File(txtFilePath);
        File parentDir = txtFile.getParentFile();
        if (parentDir != null && !parentDir.exists()) {
            boolean mkdirs = parentDir.mkdirs();
            if (!mkdirs) {
                log.error("[controller-url-scan] Create parent directory failed, path={}", parentDir.getAbsolutePath());
                return;
            }
        }

        // 写入文件
        try (FileWriter writer = new FileWriter(txtFile, false)) { // false:覆盖写入,true:追加
            writer.write("=== SpringBoot Controller Interface URLs ===\n");
            writer.write("=== Generate Time: " + new Date() + "\n");
            writer.write("============================================\n\n");
            for (String url : urlSet) {
                writer.write(url + "\n");
            }
            log.info("[controller-url-scan] Write controller urls success, file path={}, total count={}",
                    txtFile.getAbsolutePath(), urlSet.size());
        } catch (IOException e) {
            log.error("[controller-url-scan] Write controller urls to txt failed", e);
        }
    }
}
2. 配置类(注册扫描Bean)
java 复制代码
package com.geekmice.springbootmpredis.config;

import com.geekmice.springbootmpredis.util.ControllerUrlScanner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.context.WebApplicationContext;

/**
 * 注册Controller地址扫描Bean
 */
@Configuration
public class ControllerScanConfig {

    /**
     * 注册ControllerUrlScanner(注入WebApplicationContext)
     */
    @Bean
    public ControllerUrlScanner controllerUrlScanner(WebApplicationContext webApplicationContext) {
        return new ControllerUrlScanner(webApplicationContext);
    }
}
3. 配置文件(可选:自定义TXT路径)

application.yml中配置TXT文件输出路径(默认项目根目录):

yaml 复制代码
# 自定义Controller接口地址TXT文件路径
controller:
  url:
    txt:
      path: ./logs/controller-urls.txt # 输出到logs目录下

三、关键说明

1. 核心特性
  • 自动扫描 :支持@Controller/@RestController,兼容@GetMapping/@PostMapping等所有请求注解;
  • 路径格式化 :统一路径格式(如student/student/student//student);
  • 去重处理 :使用LinkedHashSet保证地址唯一且有序;
  • 安全写入 :自动创建父目录,使用try-with-resources保证文件流关闭;
  • 日志记录:打印写入结果和异常信息,便于排查问题;
  • 请求方法标注 :输出GET -> /student/queryStudent/{stuNo}格式,明确接口请求方式。
2. 输出示例(controller-urls.txt)
复制代码
=== SpringBoot Controller Interface URLs ===
=== Generate Time: Sun Dec 07 15:30:00 CST 2025 ===
============================================

GET -> /student/queryStudent/{stuNo}
/student
ALL -> /student

四、注意事项

  1. 启动时机 :使用@PostConstruct保证在SpringBean初始化完成后执行,若需更早执行(如上下文刷新后),可实现ApplicationListener<ContextRefreshedEvent>

    java 复制代码
    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
        scanAndWriteControllerUrls();
    }
  2. 动态接口 :仅扫描编译期定义的静态接口,动态注册的接口(如RequestMappingHandlerMapping手动添加)无法扫描;

  3. 路径优先级 :方法级注解优先级高于类级注解,支持多层路径拼接(如类级/student + 方法级/query/student/query);

  4. 特殊路径 :支持路径变量(如{stuNo})、通配符(如/**),会原样输出;

  5. 编码问题 :默认使用系统编码,若需指定编码(如UTF-8),修改FileWriter为:

    java 复制代码
    try (OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream(txtFile), StandardCharsets.UTF_8)) {
        // 写入逻辑...
    }

五、扩展功能(可选)

  1. 按模块分类 :按Controller包名分类写入(如student模块、class模块);

  2. 追加模式 :修改FileWriter的第二个参数为true,实现追加写入(保留历史记录);

  3. 过滤指定Controller:添加白名单/黑名单,跳过不需要扫描的Controller;

  4. 输出更多信息 :如Controller类名、方法名、注解描述等:

    java 复制代码
    writer.write(String.format("%-50s %-20s %s\n", fullUrl, controllerClass.getSimpleName(), method.getName()));

六、验证方式

  1. 启动SpringBoot项目;
  2. 查看配置的TXT文件路径(如./controller-urls.txt);
  3. 检查文件内容是否包含所有Controller接口地址。

该方案轻量、无侵入,无需依赖额外组件,可直接集成到你的springbootmpredis项目中,便于快速查看所有接口地址,适合接口文档生成、测试、运维排查等场景。

相关推荐
老华带你飞1 小时前
旅游|基于Java旅游信息系统(源码+数据库+文档)
java·开发语言·数据库·vue.js·spring boot·旅游
小周在成长1 小时前
Java 线程安全问题
后端
bcbnb1 小时前
iOS应用完整上架App Store步骤与注意事项详解
后端
掘金考拉1 小时前
从原理到实战:JWT认证深度剖析与架构思考(一)——三部分结构的精妙设计
后端
小石头 100861 小时前
【JavaEE】进程和线程的区别
java·java-ee
疯狂的程序猴1 小时前
掌握iOS和Android设备应用运行状态监控与性能优化完整教程
后端
oioihoii1 小时前
C++对象生命周期与析构顺序深度解析
java·开发语言·c++
IMPYLH1 小时前
Lua 的 tonumber 函数
开发语言·笔记·后端·junit·游戏引擎·lua