性能工具之 Jmeter 通过 SpringBoot 工程启动

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: 【5月更文挑战第22天】性能工具之 Jmeter 通过 SpringBoot 工程启动

背景

Jmeter 平时性能测试工作一般都是通过命令行在 Linux 下执行,为了锻炼自己代码与逻辑能力,想 Jmeter 是否可以通过 springboot 工程启动,周末在家尝试写一写,一写原来需要处理很多事情,才可以启动起来,起来还是有很问题需要处理,下面是相应的代码,其实网上也有,但关键的是自己有意识收集知识,到用的时候能拿来改一改就用。

前置条件

需要在 linux 中配置 Jmeter,并且配置 Java 环境变量:

## 编辑
vi ~/.bash_profile

# jmeter:路径  根据自己事情情况修改
JMETER_HOME=/root/tools/apache-jmeter-5.1.1
PATH=$PATH:$HOME/bin:$JMETER_HOME/bin:
export PATH

## 执行生效
source ~/.bash_profile

页面设计

image.png

运行效果

点击上传脚本,弹出对话框,点击上传,后台日志显示上传成功。

image.png

点击启动,并且读取启动日志。
image.png

点击停止。

image.png

示意图说明:

通过访问 -> 调用 JAVA 代码-> 启动 shell命令-> 启动 Jmeter -> 获取启动日志

image.png

参考代码

前端代码

以下参考代码:

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <meta name="description" content="">
    <meta name="author" content="Mark Otto, Jacob Thornton, and Bootstrap contributors">
    <meta name="generator" content="Jekyll v3.8.6">
    <title>Jmeter启动</title>

    <link rel="canonical" href="https://v4ing.bootcss.com/docs/4.3/examples/checkout/">

    <!-- Bootstrap core CSS -->
    <!-- Bootstrap core CSS -->
    <link href="asserts/css/bootstrap.min.css" th:href="@{/webjars/bootstrap/4.3.1/css/bootstrap.css}" rel="stylesheet">
    <!-- Favicons -->
    <meta name="theme-color" content="#563d7c">


    <style>
        .bd-placeholder-img {
    
    
            font-size: 1.125rem;
            text-anchor: middle;
            -webkit-user-select: none;
            -moz-user-select: none;
            -ms-user-select: none;
            user-select: none;
        }

        @media (min-width: 768px) {
    
    
            .bd-placeholder-img-lg {
    
    
                font-size: 3.5rem;
            }
        }
    </style>
    <!-- Custom styles for this template -->
    <link href="https://v4.bootcss.com/docs/4.3/examples/checkout/form-validation.css" rel="stylesheet">
</head>
<body class="bg-light">
<div class="container">
    <div class="py-5 text-center">
        <img src="https://jmeter.apache.org/images/logo.svg" class="d-block mx-auto mb-4" alt="Apache JMeter">
        <p class="lead">The Apache JMeter™ application is open source software, a 100% pure Java application designed to
            load test functional behavior and measure performance. It was originally designed for testing Web
            Applications but has since expanded to other test functions.</p>
    </div>
    <div class="col-md-8 order-md-1">
        <h4 class="mb-3">上传脚本</h4>
        <form>
            <input id="jmeterId" type="file"/>
            <a class="btn btn-lg btn-primary btn-block" value="上传脚本" onclick="submitupload()">上传脚本</a>
        </form>
        <form>
            <input id="jmeterParam" type="file"/>
            <a class="btn btn-lg btn-primary btn-block" value="上传参数" onclick="submitParm()">上传参数文件</a>
        </form>
        <h4 class="mb-3">运行</h4>
        <!--        jmeter -n -t [jmx file] -l [results file] -e -o [Path to web report folder]-->
        <form class="needs-validation" novalidate>
            <div class="row">
                <div class="col-md-6 mb-3">
                    <label for="jmeterName">压测脚本</label>
                    <input type="text" class="form-control" id="jmeterName" name="jmeterName" placeholder="jmx file"
                           value="" required>
                    <div class="invalid-feedback">
                        jmx file
                    </div>
                </div>
                <div class="col-md-6 mb-3">
                    <label for="numberName">并发数</label>
                    <input type="text" class="form-control" id="numberName" name="num" placeholder="并发数" value=""
                           required>
                    <div class="invalid-feedback">
                        并发数
                    </div>
                </div>
                <div class="col-md-6 mb-3">
                    <label for="duration">执行时间</label>
                    <input type="text" class="form-control" id="duration" name="duration" placeholder="执行时间" value=""
                           required>
                    <div class="invalid-feedback">
                        并发数
                    </div>
                </div>
            </div>
            <hr class="mb-4">
            <a class="btn btn-success" onclick="JmeterRun()" type="submit">运行</a>
            <a class="btn btn-danger" onclick="Jmeterstop()" type="submit">停止</a>
            <a class="btn btn-info" onclick="JmeterInfo()" data-toggle="modal" data-target="#myModal">查看信息</a>
        </form>
    </div>
</div>

<!-- 日志模态框 -->
<div class="modal fade" id="myModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel">
    <div class="modal-dialog" role="document">
        <div class="modal-content">
            <div class="modal-header">
                <h4 class="modal-title">Jmeter运行日志</h4>
            </div>
            <div class="modal-body">
               <textarea rows="30" cols="20" id="JmeterMsg"></textarea>
            </div>
            <div class="modal-footer">
                <button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
<!--                <button type="button" class="btn btn-primary">Save changes</button>-->
            </div>
        </div>
    </div>
</div>


<footer class="my-5 pt-5 text-muted text-center text-small">
    <p class="mb-1">&copy; 2017-2019 Company Name</p>
    <ul class="list-inline">
        <li class="list-inline-item"><a href="#">Privacy</a></li>
        <li class="list-inline-item"><a href="#">Terms</a></li>
        <li class="list-inline-item"><a href="#">Support</a></li>
    </ul>
</footer>
</div>
<script type="text/javascript" th:src="@{/webjars/jquery/3.3.1/jquery.js}"></script>
<script th:src="@{/asserts/js/layer/layer.js}"></script>
<script>window.jQuery || document.write('<script src="/docs/4.3.1/assets/js/vendor/jquery.slim.min.js"><\/script>')</script>
<script src="/docs/4.3/dist/js/bootstrap.bundle.js"
        th:src="@{/webjars/bootstrap/4.3.1/js/bootstrap.bundle.js}"></script>
<script src="https://v4.bootcss.com/docs/4.3/examples/checkout/form-validation.js"></script>
</body>
<script>
    //上传脚本
    function submitupload() {
    
    
        var type = "file";              //后台接收时需要的参数名称,自定义即可
        var id = "jmeterId";            //即input的id,用来寻找值
        var formData = new FormData();
        var jmeterId = $("#jmeterId").val();
        if (jmeterId == "") {
    
    
            layer.msg("Jmeter文件不能为空,请输入", {
    
    time: 2000, icon: 5, shift: 6}, function () {
    
    
            });
            return;
        }
        formData.append(type, $("#" + id)[0].files[0]);
        $.ajax({
    
    
            type: "POST",
            url: '/jmeter/upload',
            data: formData,
            processData: false,
            contentType: false,
            success: function (data) {
    
    
                if (data.code == 100) {
    
    
                    layer.msg("用户信息保存成功", {
    
    time: 1000, icon: 6}, function () {
    
    
                        // console.log("相应结果:" + data.extend.file);
                        //通过返回结果进行赋值
                        $("#jmeterName").val(data.extend.file);
                        // window.location.href = "/jmeterIndex";
                    });
                } else {
    
    
                    layer.msg("信息保存失败,请重新操作" + data.err, {
    
    time: 2000, icon: 5, shift: 6}, function () {
    
    

                    });
                }
            }
        });
    }

    //上传参数
    function submitParm() {
    
    
        var type = "file";              //后台接收时需要的参数名称,自定义即可
        var id = "jmeterParam";            //即input的id,用来寻找值
        var formData = new FormData();
        var jmeterPara = $("#jmeterParam").val();
        if (jmeterPara == "") {
    
    
            layer.msg("Jmeter文件不能为空,请输入", {
    
    time: 2000, icon: 5, shift: 6}, function () {
    
    
            });
            return;
        }
        formData.append(type, $("#" + id)[0].files[0]);
        $.ajax({
    
    
            type: "POST",
            url: '/jmeter/Paramupload',
            data: formData,
            processData: false,
            contentType: false,
            success: function (data) {
    
    
                if (data.code == 100) {
    
    
                    layer.msg("参数文件保存成功", {
    
    time: 1000, icon: 6}, function () {
    
    
                    });
                } else {
    
    
                    layer.msg("信息保存失败,请重新操作" + data.err, {
    
    time: 2000, icon: 5, shift: 6}, function () {
    
    

                    });
                }
            }
        });
    }

    //运行
    function JmeterRun() {
    
    
        let JmeterName = $("#jmeterName").val();
        let number = $("#numberName").val();
        let duration = $("#duration").val();

        console.log(JmeterName);
        console.log(number);
        $.ajax({
    
    
            type: "POST",
            url: '/jmeter/JmeterRun',
            data: {
    
    
                "jmeterName": JmeterName,
                "numberName": number,
                "duration": duration
            },
            success: function (result) {
    
    
                if (result.code == 100) {
    
    
                    layer.msg("启动成功成功", {
    
    time: 1000, icon: 6}, function () {
    
    
                    });
                } else {
    
    
                    layer.msg("启动失败,请重新操作", {
    
    time: 2000, icon: 5, shift: 6}, function () {
    
    

                    });
                }
            }
        })
    }

    //停止
    function Jmeterstop() {
    
    
        $.ajax({
    
    
            type: "Get",
            url: '/jmeter/JmeterStop',
            processData: false,
            contentType: false,
            success: function (result) {
    
    
                if (result.code==100) {
    
    
                    layer.msg("停止成功", {
    
    time: 1000, icon: 6}, function () {
    
    
                    });
                } else {
    
    
                    layer.msg("停止失败,请重新操作", {
    
    time: 2000, icon: 5, shift: 6}, function () {
    
    

                    });
                }
            }
        })

    }

    //查看日志
    function JmeterInfo() {
    
    
        $.ajax({
    
    
            type: "Get",
            url: '/jmeter/Jmeterinfo',
            processData: false,
            contentType: false,
            success: function (result) {
    
    
                if (result.code == 100) {
    
    
                    layer.msg("启动成功成功", {
    
    time: 1000, icon: 6}, function () {
    
    
                        $("#JmeterMsg").val(data.extend.infopage);
                    });
                } else {
    
    
                    layer.msg("启动失败,请重新操作", {
    
    time: 2000, icon: 5, shift: 6}, function () {
    
    

                    });
                }
            }
        })
    }


</script>
</html>

服务端接口

参考代码如下:

package com.sevendays.controller;


import com.sevendays.pojo.Msg;
import com.sevendays.service.JmerterScriptService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.io.IOException;

/**
 * @author 7d
 * @Title: JmeterController
 * @Description: Jmeter启动页面
 * @date 2019/11/17 / 10:32
 */

@Controller
@RequestMapping("/jmeter")
public class JmeterController {
   
   
    private static final Logger logger = LoggerFactory.getLogger(JmeterController.class);

    @Autowired
    JmerterScriptService jmerterScriptService;

    @GetMapping("/jmeterIndex")
    public String jmeterIndex() {
   
   
        return "jmeter/jmterIndex";
    }


    /**
     * 上传脚本
     *
     * @param file
     * @return
     */
    @PostMapping("/upload")
    @ResponseBody
    public Msg upload(@RequestParam("file") MultipartFile file) {
   
   
        if (file.isEmpty()) {
   
   
            return Msg.fail().add("err", "上传失败");
        }
        String fileName = file.getOriginalFilename();
        logger.info("路径" + fileName);
        String filePath = "/home/7d/";
//        String filePath = "E:\\test\\7d\\data\\";
        if (!fileName.endsWith(".jmx")) {
   
   
            return Msg.fail().add("err", "脚本上传失败");
        }
        File dest = new File(filePath + fileName);

        String jmxName = fileName.substring(0, fileName.lastIndexOf("."));
        try {
   
   
            file.transferTo(dest);
            logger.info("上传成功:" + jmxName);
            return Msg.success().add("file", jmxName);
        } catch (IOException e) {
   
   
            logger.error(e.toString(), e);
        }
        return Msg.fail();
    }

    /**
     * 上传参数文件
     *
     * @param file
     * @return
     */
    @PostMapping("/Paramupload")
    @ResponseBody
    public Msg uploadParam(@RequestParam("file") MultipartFile file) {
   
   
        if (file.isEmpty()) {
   
   
            return Msg.fail().add("err", "上传失败");
        }
        String fileName = file.getOriginalFilename();
        logger.info("路径" + fileName);
        String filePath = "/home/7d";
//        String filePath = "E:\\test\\7d\\data\\";
        File dest = new File(filePath + fileName);
        String jmxName = fileName.substring(0, fileName.lastIndexOf("."));

        try {
   
   
            file.transferTo(dest);
            logger.info("上传成功:" + jmxName);
            return Msg.success().add("file", jmxName);
        } catch (IOException e) {
   
   
            logger.error(e.toString(), e);
        }
        return Msg.fail();
    }


    /**
     * 运行脚本
     *
     * @return
     */
    @PostMapping("/JmeterRun")
    @ResponseBody
    public Msg run(@RequestParam("jmeterName") String jmeterName, @RequestParam("numberName") String numberName, @RequestParam("duration") String duration) {
   
   
        logger.info(jmeterName);
        if (!jmeterName.isEmpty() && !numberName.isEmpty()) {
   
   
            jmerterScriptService.runCommand(jmeterName.trim(), numberName.trim(), duration);
            return Msg.success();
        } else {
   
   
            return Msg.fail();
        }
    }

    /**
     * 停止脚本
     *
     * @return
     */
    @GetMapping("/JmeterStop")
    @ResponseBody
    public Msg stop() {
   
   
        jmerterScriptService.stopCommand();
        return Msg.success();
    }


    /**
     * 查看日志
     *
     * @return
     */
    @GetMapping("/Jmeterinfo")
    @ResponseBody
    public Msg info() {
   
   
        String info = jmerterScriptService.selectInfo();
        return Msg.success().add("infopage", info);
    }
}

interface层代码

package com.sevendays.service;

/**
 * @author 7d
 * @Title: JmerterScriptService
 * @Description: Jmeterj脚本处理
 * @date 2019/11/17 / 18:06
 */

public interface JmerterScriptService {
   
   

    /**
     * 执行命令
     * @param cmd
     */
    void execCommand(String cmd);

    /**
     * 运行
     * @param script 脚本
     * @param num  数量
     * @param seconds 执行时间
      */
    void runCommand(String script, String num,String seconds);

    /**
     * 停止
     */
    void stopCommand();

    /**
     * 获取日志
     * @return
     */
    String selectInfo();

}

接口实现层

package com.sevendays.service.impl;

import com.sevendays.controller.JmeterController;
import com.sevendays.service.JmerterScriptService;
import com.sevendays.utils.LogSvrReadInput;
import com.sevendays.utils.execCmd;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

import java.io.BufferedReader;
import java.io.File;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.Date;

/**
 * @author 7d
 * @Title: JmerterScriptServiceImpl
 * @Description: 执行命令
 * @date 2019/11/17 / 18:49
 */

@Service
public class JmerterScriptServiceImpl implements JmerterScriptService {
   
   

    private static final Logger logger = LoggerFactory.getLogger(JmerterScriptServiceImpl.class);


    @Override
    public void execCommand(String cmd) {
   
   
        try {
   
   
            Runtime rt = Runtime.getRuntime();
            Process proc = rt.exec(cmd, null, null);
            InputStream stderr = proc.getInputStream();
            InputStreamReader isr = new InputStreamReader(stderr, "GBK");
            BufferedReader br = new BufferedReader(isr);
            String line = "";
            while ((line = br.readLine()) != null) {
   
   
                logger.info(line);
            }
        } catch (Exception e) {
   
   
            e.printStackTrace();
        }

    }

    @Override
    public void runCommand(String script, String num, String seconds) {
   
   

        //执行次数
        String numThread = "<stringProp name=\"ThreadGroup.num_threads\">#numThread</stringProp>";
        //执行时间
        String time = "<stringProp name=\"ThreadGroup.duration\">#timeDuration</stringProp>";
        String bak = "cp /home/7d/" + script + ".jmx /home/7d/" + script + "bak.jmx";
        String old = "/home/7d/" + script + ".jmx";
        execCmd.execCmd(bak);
        logger.info("路径:{}", old);
        //替换执行数量
        execCmd.replacTextContent(old, "#numThread", num);
        //替换执行时间
        execCmd.replacTextContent(old, "#timeDuration", seconds);
        String runcmd = "nohup jmeter -n -t /home/7d/#scriptName.jmx -l /home/7d/#scriptName.jtl -j /home/7d/jmeter.log > /home/7d/jmeterlog.log&".replaceAll("#scriptName", script);
        logger.info("运行命令{}", runcmd);
        execCmd.execCmd(runcmd);
    }

    @Override
    public void stopCommand() {
   
   
        String stoprunm = "/root/tools/apache-jmeter-5.1.1/bin/shutdown.sh";
        execCmd.execCmd(stoprunm);
    }

    @Override
    public String selectInfo() {
   
   
        String tail = "tail -f /home/7d/jmeterlog.log";
        File file = new File("/home/7d/jmeterlog.log");
        String s = LogSvrReadInput.realtimeShowLog(file);
        logger.info("输出日志:--》{}",s);
        return s;
    }
}

工具类

package com.sevendays.utils;

import com.sevendays.service.impl.JmerterScriptServiceImpl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.*;

/**
 * @author 7d
 * @Title: execCmd
 * @Description: 直接执行命令
 * @date 2019/11/17 / 19:17
 */

public class execCmd {
   
   

    private static final Logger logger = LoggerFactory.getLogger(execCmd.class);

    public execCmd() {
   
   
    }


    /**
     * 直接执行命令
     *
     * @param cmd
     */
    public static void execCmd(String cmd) {
   
   
        try {
   
   
            Runtime rt = Runtime.getRuntime();
            Process proc = rt.exec(cmd, null, null);
            InputStream stderr = proc.getInputStream();
            InputStreamReader isr = new InputStreamReader(stderr, "GBK");
            BufferedReader br = new BufferedReader(isr);
            String line = "";
            while ((line = br.readLine()) != null) {
   
   
                logger.info(line);
            }
        } catch (Exception e) {
   
   
            e.printStackTrace();
        }
    }


    /**
     * 替换文本文件中的 非法字符串
     *
     * @param path   路径
     * @param srcStr 原有的内容
     * @param newStr 要替换的内容
     */
    public static void replacTextContent(String path, String srcStr, String newStr) {
   
   
        File file = new File(path);
        if (!file.exists() && !file.isFile()) {
   
   
            logger.info("file{},不存在:", path);
            return;
        }
        try {
   
   
            FileReader in = new FileReader(file);
            BufferedReader bufIn = new BufferedReader(in);
            // 内存流, 作为临时流
            CharArrayWriter tempStream = new CharArrayWriter();
            // 替换
            String line = null;
            while ((line = bufIn.readLine()) != null) {
   
   
                // 替换每行中, 符合条件的字符串
                line = line.replaceAll(srcStr, newStr);
                // 将该行写入内存
                tempStream.write(line);
                // 添加换行符
                tempStream.append(System.getProperty("line.separator"));
            }
            // 关闭 输入流
            bufIn.close();
            // 将内存中的流 写入 文件
            FileWriter out = new FileWriter(file);
            tempStream.writeTo(out);
            out.close();
        } catch (IOException e) {
   
   
            e.printStackTrace();
        }
        logger.info("====path:" + path);

    }

}

jmeter脚本

脚本其实也没有什么东西,只有定义好规则,这样方便替换。

image.png

小结

上面 Demo 中还是一个问题没有解决就是在页面实时参看日志,目前还没实现,不过总体上实现自己想的功能。

源码地址:

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
目录
相关文章
|
2月前
|
测试技术 数据库 UED
Python 性能测试进阶之路:JMeter 与 Locust 的强强联合,解锁性能极限
【9月更文挑战第9天】在数字化时代,确保软件系统在高并发场景下的稳定性至关重要。Python 为此提供了丰富的性能测试工具,如 JMeter 和 Locust。JMeter 可模拟复杂请求场景,而 Locust 则能更灵活地模拟真实用户行为。结合两者优势,可全面评估系统性能并优化瓶颈。例如,在电商网站促销期间,通过 JMeter 模拟大量登录请求并用 Locust 模拟用户浏览和购物行为,可有效识别并解决性能问题,从而提升系统稳定性和用户体验。这种组合为性能测试开辟了新道路,助力应对复杂挑战。
108 2
|
11天前
|
缓存 IDE Java
SpringBoot入门(7)- 配置热部署devtools工具
SpringBoot入门(7)- 配置热部署devtools工具
23 2
 SpringBoot入门(7)- 配置热部署devtools工具
|
2月前
|
设计模式 Java 关系型数据库
【Java笔记+踩坑汇总】Java基础+JavaWeb+SSM+SpringBoot+SpringCloud+瑞吉外卖/谷粒商城/学成在线+设计模式+面试题汇总+性能调优/架构设计+源码解析
本文是“Java学习路线”专栏的导航文章,目标是为Java初学者和初中高级工程师提供一套完整的Java学习路线。
412 37
|
1月前
|
测试技术 持续交付 Apache
性能怪兽来袭!Python+JMeter+Locust,让你的应用性能飙升🦖
【10月更文挑战第10天】随着互联网应用规模的不断扩大,性能测试变得至关重要。本文将探讨如何利用Python结合Apache JMeter和Locust,构建高效且可定制的性能测试框架。通过介绍JMeter和Locust的使用方法及Python的集成技巧,帮助应用在高负载下保持稳定运行。
67 2
|
1月前
|
Java BI API
spring boot 整合 itextpdf 导出 PDF,写入大文本,写入HTML代码,分析当下导出PDF的几个工具
这篇文章介绍了如何在Spring Boot项目中整合iTextPDF库来导出PDF文件,包括写入大文本和HTML代码,并分析了几种常用的Java PDF导出工具。
418 0
spring boot 整合 itextpdf 导出 PDF,写入大文本,写入HTML代码,分析当下导出PDF的几个工具
|
2月前
|
缓存 Java 测试技术
谷粒商城笔记+踩坑(11)——性能压测和调优,JMeter压力测试+jvisualvm监控性能+资源动静分离+修改堆内存
使用JMeter对项目各个接口进行压力测试,并对前端进行动静分离优化,优化三级分类查询接口的性能
谷粒商城笔记+踩坑(11)——性能压测和调优,JMeter压力测试+jvisualvm监控性能+资源动静分离+修改堆内存
|
1月前
|
测试技术 持续交付 Apache
性能怪兽来袭!Python+JMeter+Locust,让你的应用性能飙升🦖
【10月更文挑战第2天】随着互联网应用规模的不断膨胀,性能测试变得至关重要。本文将介绍如何利用Python结合Apache JMeter和Locust构建高效且可定制的性能测试框架。Apache JMeter是一款广泛使用的开源负载测试工具,适合测试静态和动态资源;Locust则基于Python,通过编写简单的脚本模拟HTTP请求,更适合复杂的测试场景。
65 3
|
2月前
|
Java 应用服务中间件 Spring
IDEA 工具 启动 spring boot 的 main 方法报错。已解决
IDEA 工具 启动 spring boot 的 main 方法报错。已解决
|
1月前
|
JavaScript 安全 Java
如何使用 Spring Boot 和 Ant Design Pro Vue 实现动态路由和菜单功能,快速搭建前后端分离的应用框架
本文介绍了如何使用 Spring Boot 和 Ant Design Pro Vue 实现动态路由和菜单功能,快速搭建前后端分离的应用框架。首先,确保开发环境已安装必要的工具,然后创建并配置 Spring Boot 项目,包括添加依赖和配置 Spring Security。接着,创建后端 API 和前端项目,配置动态路由和菜单。最后,运行项目并分享实践心得,包括版本兼容性、安全性、性能调优等方面。
148 1