Skip to content

1.springboot树形结构 支持模糊查询,返回匹配节点和父节点,其他节点不返回

java
package com.me.meterdemo.ds;
import java.util.ArrayList;
import java.util.List;
 
public class TreeNode {
    private Long id;
    private String name;
    private Long parentId;
    private List<TreeNode> children = new ArrayList<>();
 
    // 构造方法
    public TreeNode(Long id, String name, Long parentId) {
        this.id = id;
        this.name = name;
        this.parentId = parentId;
    }
 
    // Getters and Setters
    public Long getId() { return id; }
    public String getName() { return name; }
    public Long getParentId() { return parentId; }
    public List<TreeNode> getChildren() { return children; }
 
    public void addChild(TreeNode child) {
        // 防止重复添加
        if (children.stream().noneMatch(c -> c.id.equals(child.id))) {
            children.add(child);
        }
    }
}
java
package com.me.meterdemo.ds;
 
import org.springframework.stereotype.Service;
 
import javax.annotation.PostConstruct;
import java.util.*;
import java.util.stream.Collectors;
 
 
 
@Service
public class TreeMemoryService {
    // 内存数据存储
    private final Map<Long, TreeNode> nodeMap = new HashMap<>();
 
    // 初始化测试数据
    @PostConstruct
    private void initData() {
        clearAndInitData();
    }
 
    private void clearAndInitData() {
        nodeMap.clear();
        addNode(1L, "集团总部", null);
        addNode(2L, "华东公司", 1L);
        addNode(3L, "技术部", 2L);
        addNode(4L, "开发组", 3L);
        addNode(5L, "测试组", 3L);
        addNode(6L, "华南公司", 1L);
    }
 
    private void addNode(Long id, String name, Long parentId) {
        TreeNode node = new TreeNode(id, name, parentId);
        nodeMap.put(id, node);
    }
 
    /**
     * 执行树结构搜索
     * @param keyword 查询关键词(可为空)
     * @return 符合条件节点及其父节点组成的树结构
     */
    public List<TreeNode> search(String keyword) {
        // 空关键词返回空结果
        if (keyword != null && keyword.trim().isEmpty()) {
            return Collections.emptyList();
        }
 
        // 获取所有匹配节点
        List<TreeNode> matches = nodeMap.values().stream()
                .filter(node -> matchesKeyword(node, keyword))
                .collect(Collectors.toList());
 
        // 收集需要展示的节点ID(匹配节点+父链)
        Set<Long> visibleIds = new LinkedHashSet<>();
        matches.forEach(node -> collectParentChain(node, visibleIds));
 
        // 构建精简树结构
        return buildPrunedTree(visibleIds);
    }
 
    // 判断节点是否匹配关键词
    private boolean matchesKeyword(TreeNode node, String keyword) {
        return keyword == null ||
                node.getName().contains(keyword);
    }
 
    // 收集节点及其所有父节点
    private void collectParentChain(TreeNode node, Set<Long> ids) {
        Long currentId = node.getId();
        while (currentId != null) {
            if (!ids.add(currentId)) break; // 防止循环
            TreeNode current = nodeMap.get(currentId);
            currentId = current.getParentId();
        }
    }
 
    // 构建修剪后的树结构
    private List<TreeNode> buildPrunedTree(Set<Long> visibleIds) {
        // 创建节点副本防止污染原始数据
        Map<Long, TreeNode> visibleNodes = createNodeCopies(visibleIds);
 
        // 构建层级关系
        List<TreeNode> roots = new ArrayList<>();
        visibleNodes.values().forEach(node -> {
            Long parentId = node.getParentId();
            if (parentId == null) {
                roots.add(node);
            } else {
                TreeNode parent = visibleNodes.get(parentId);
                if (parent != null) {
                    parent.addChild(node);
                }
            }
        });
 
        return roots;
    }
 
    // 创建节点副本(清空原有子节点)
    private Map<Long, TreeNode> createNodeCopies(Set<Long> visibleIds) {
        return visibleIds.stream()
                .map(id -> nodeMap.get(id))
                .filter(Objects::nonNull)
                .collect(Collectors.toMap(
                        TreeNode::getId,
                        orig -> new TreeNode(orig.getId(),
                                orig.getName(),
                                orig.getParentId())
                ));
    }
 
    // 重置测试数据(可选)
    public void resetData() {
        clearAndInitData();
    }
}
java
package com.me.meterdemo.ds;
 
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
 
import java.util.List;
 
@RestController
public class TreeDSController {
    private final TreeMemoryService treeMemoryService;
 
    public TreeDSController(TreeMemoryService treeService) {
        this.treeMemoryService = treeService;
    }
 
    @GetMapping("/search")
    public List<TreeNode> searchTree(@RequestParam(required = false) String keyword) {
        return treeMemoryService.search(keyword);
    }
}

2.springboot 返回树形结构 支持模糊匹配

java
package com.me.meterdemo.tree;

import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Data;
import java.util.List;

@Data
public class TreeNode {
    private Long id;
    private String name;
    private Long parentId;
    private List<TreeNode> children;


    @JsonIgnore
    private  boolean matched; // 标记是否匹配搜索条件
}
java
package com.me.meterdemo.tree;

import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

@Service
public class TreeService {
    
    // 模拟数据库数据
    private List<TreeNode> getAllNodes() {
        List<TreeNode> nodes = new ArrayList<>();
        // 添加一些测试数据
        TreeNode root = new TreeNode();
        root.setId(1L);
        root.setName("目录one");
        root.setParentId(0L);
        
        TreeNode child1 = new TreeNode();
        child1.setId(2L);
        child1.setName("测试函数1");
        child1.setParentId(1L);
        
        TreeNode child2 = new TreeNode();
        child2.setId(3L);
        child2.setName("测试时间函数");
        child2.setParentId(1L);
        
        TreeNode grandChild1 = new TreeNode();
        grandChild1.setId(4L);
        grandChild1.setName("测试date函数");
        grandChild1.setParentId(2L);
        
        nodes.add(root);
        nodes.add(child1);
        nodes.add(child2);
        nodes.add(grandChild1);
        
        return nodes;
    }

    public List<TreeNode> searchTree(String keyword) {
        List<TreeNode> allNodes = getAllNodes();
        Map<Long, TreeNode> nodeMap = allNodes.stream()
                .collect(Collectors.toMap(TreeNode::getId, node -> node));

        // 构建树形结构
        List<TreeNode> rootNodes = allNodes.stream()
                .filter(node -> node.getParentId() == 0)
                .collect(Collectors.toList());

        // 递归构建树
        rootNodes.forEach(root -> buildTree(root, nodeMap));

        // 如果没有关键字,返回完整的树形结构
        if (keyword == null || keyword.trim().isEmpty()) {
            return rootNodes;
        }

        // 搜索并过滤不匹配的节点
        List<TreeNode> filteredRoots = new ArrayList<>();
        for (TreeNode root : rootNodes) {
            TreeNode filteredRoot = filterTree(root, keyword);
            if (filteredRoot != null) {
                filteredRoots.add(filteredRoot);
            }
        }

        return filteredRoots;
    }

    private void buildTree(TreeNode node, Map<Long, TreeNode> nodeMap) {
        List<TreeNode> children = nodeMap.values().stream()
                .filter(n -> n.getParentId().equals(node.getId()))
                .collect(Collectors.toList());

        node.setChildren(children);
        children.forEach(child -> buildTree(child, nodeMap));
    }

    private TreeNode filterTree(TreeNode node, String keyword) {
        // 如果当前节点匹配,保留该节点
        boolean isMatched = node.getName().contains(keyword);
        node.setMatched(isMatched);

        // 处理子节点
        if (node.getChildren() != null && !node.getChildren().isEmpty()) {
            List<TreeNode> filteredChildren = new ArrayList<>();
            for (TreeNode child : node.getChildren()) {
                TreeNode filteredChild = filterTree(child, keyword);
                if (filteredChild != null) {
                    filteredChildren.add(filteredChild);
                }
            }

            // 如果当前节点匹配或者有匹配的子节点,保留该节点
            if (isMatched || !filteredChildren.isEmpty()) {
                node.setChildren(filteredChildren);
                return node;
            }
        } else {
            // 如果是叶子节点,只有匹配时才保留
            return isMatched ? node : null;
        }

        return null;
    }
}
java
package com.me.meterdemo.demos.web;

import com.me.meterdemo.ds.TreeNodeDTO;
import com.me.meterdemo.tree.TreeNode;
import com.me.meterdemo.tree.TreeService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;

@RestController
@RequestMapping("/api/tree")
public class TreeController {
    
    @Autowired
    private TreeService treeService;
    
        @GetMapping("/search")
    public List<TreeNode> searchTree(@RequestParam(required = false) String keyword) {
        return treeService.searchTree(keyword);
    }

}

3.springboot 返回树形结构 从任意节点开始生成,不一定有根节点

java
package com.me.meterdemo.treeList;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;

import java.util.ArrayList;
import java.util.List;
@Data
public class TreeNode {
    private Long id;
    private String name;
    private Long parentId;

    @JsonInclude(JsonInclude.Include.NON_EMPTY)
    private List<TreeNode> children = new ArrayList<>();

    // 构造方法、getter、setter
    public TreeNode(Long id, String name, Long parentId) {
        this.id = id;
        this.name = name;
        this.parentId = parentId;
    }

    // 省略getter和setter...
}
java
package com.me.meterdemo.treeList;

import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;


@Service
public class TreeListService {
    public List<TreeNode> buildTree() {
        List<TreeNode> allNodes = findAll();
        Map<Long, TreeNode> nodeMap = new HashMap<>();
        List<TreeNode> topNodes = new ArrayList<>();

        // 将所有节点存入Map,并初始化children
        for (TreeNode node : allNodes) {
            node.setChildren(new ArrayList<>());
            nodeMap.put(node.getId(), node);
        }

        // 构建树结构
        for (TreeNode node : allNodes) {
            Long parentId = node.getParentId();
            if (parentId != null) {
                TreeNode parent = nodeMap.get(parentId);
                if (parent != null) {
                    parent.getChildren().add(node);
                } else {
                    // 父节点不存在,当前节点作为顶级节点
                    topNodes.add(node);
                }
            } else {
                // parentId为null,直接作为顶级节点
                topNodes.add(node);
            }
        }

        return topNodes;
    }

    private List<TreeNode> findAll() {
        List<TreeNode> nodes = new ArrayList<>();
//        nodes.add(new TreeNode(1L, "集团总部", null));
//        nodes.add(new TreeNode(2L, "华东公司", 1L));
        nodes.add(new TreeNode(3L, "技术部", 2L));
        nodes.add(new TreeNode(4L, "开发组", 3L));
        nodes.add(new TreeNode(5L, "测试组", 3L));
        nodes.add(new TreeNode(6L, "华南公司", 1L));
        nodes.add(new TreeNode(7L, "中国烟草", null));




        return nodes;
    }

}
java
package com.me.meterdemo.treeList;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
public class TreeListController {

    @Autowired
    TreeListService treeListService;

    @GetMapping("/tree/1")
    public ResponseEntity<List<TreeNode>> getTree() {
        List<TreeNode> tree = treeListService.buildTree();
        return ResponseEntity.ok(tree);
    }
}

4 springboot naos seata 集成

4.1 maven 依赖 nacos 1.4.2 seata 1.4.2

客户端的每个数据库 度需要创建一个 undo_log 的表

xml
<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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.me</groupId>
    <artifactId>order-service</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>order-service</name>

    <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.3.12.RELEASE</spring-boot.version>
        <spring-cloud.version>Hoxton.SR12</spring-cloud.version>
        <spring-cloud-alibaba.version>2.2.6.RELEASE</spring-cloud-alibaba.version>
    </properties>

    <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>
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                <version>${spring-cloud-alibaba.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.46</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.bouncycastle</groupId>
            <artifactId>bcpkix-jdk18on</artifactId>
            <version>1.78.1</version>
        </dependency>
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.8.28</version>
        </dependency>

        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.10</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>io.seata</groupId>
                    <artifactId>seata-spring-boot-starter</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>io.seata</groupId>
            <artifactId>seata-spring-boot-starter</artifactId>
            <version>1.4.2</version>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>${spring-boot.version}</version> <!-- 添加这一行 -->
                <configuration>
                    <mainClass>com.me.OrderApp</mainClass>
                    <executable>true</executable>
                </configuration>
                <executions>
                    <execution>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

4.2 bootstrap .yml 配置

yml
server:
  port: 8080
spring:
  application:
    name: order-service
  cloud:

    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
        group: DEFAULT_GROUP

      config:
        server-addr: 127.0.0.1:8848
        file-extension: yml
        auto-refresh: true
        data-id: ${spring.application.name}
##  seata  配置
seata:
## 事务组配置  需要和 seata 的registry.conf 配置文件相同   
  tx-service-group:  my_test_tx_group
  service:
    vgroup-mapping.my_test_tx_group: default
  enabled: true
  application-id: ${spring.application.name}

  registry:
    type: nacos
    nacos:
      application: seata-server
      server-addr: 127.0.0.1:8848
      namespace: ''
      ## 注意这里有坑   源码中默认的是 SEATA_GROUP 如果不指定 就是这个默认值 会导致无法找到可用的服务
      group: 'DEFAULT_GROUP'
      username: "nacos"
      password: "nacos"
      # 事务模式  AT  XA 
  data-source-proxy-mode: AT

4.3 NACOS 配置文件修改 执行SQL脚本 application.properties

properties
spring.datasource.platform=mysql
### Count of DB:
db.num=1

### Connect URL of DB:
db.url.0=jdbc:mysql://127.0.0.1:3306/nacos23?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC
db.user.0=root
db.password.0=root

然后执行sql 脚本 创建 数据库 和表结构

4.4 seat 服务端配置 1.4.2

seata 的数据库需要执行脚本创建三个表 file.conf 修改为数据库存储

## transaction log store, only used in seata-server
store {
 ## store mode: file、db、redis
 mode = "db"
 ## rsa decryption public key
 publicKey = ""


 ## database store property
 db {
   ## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp)/HikariDataSource(hikari) etc.
   datasource = "druid"
   ## mysql/oracle/postgresql/h2/oceanbase etc.
   dbType = "mysql"
   driverClassName = "com.mysql.jdbc.Driver"
   ## if using mysql to store the data, recommend add rewriteBatchedStatements=true in jdbc connection param
   url = "jdbc:mysql://127.0.0.1:3306/seata?rewriteBatchedStatements=true"
   user = "root"
   password = "root"
   minConn = 5
   maxConn = 100
   globalTable = "global_table"
   branchTable = "branch_table"
   lockTable = "lock_table"
   queryLimit = 100
   maxWait = 5000
 }


}

registry.conf 修改为 nacos 注册中心 和配置中心

registry {
 # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
 type = "nacos"

 nacos {
   application = "seata-server"
   serverAddr = "127.0.0.1:8848"
   group = "DEFAULT_GROUP"
   namespace = ""
   cluster = "default"
   username = "nacos"
   password = "nacos"
   
 }

}

config {
 # file、nacos 、apollo、zk、consul、etcd3、springCloudConfig
 type = "nacos"

 nacos {
  serverAddr = "127.0.0.1:8848"
   namespace = ""
   group = "DEFAULT_GROUP"
   username = "nacos"
   password = "nacos"
   dataId = "seataServer.properties"
   
 }
 
}

4.5 nacos 创建配置

在nacos 中创建吗一个 dataId=seataServer.properties 的 配置文件 修改 数据库的配置 和事务组的配置

service.vgroupMapping.my_test_tx_group=default

properties
transport.type=TCP
transport.server=NIO
transport.heartbeat=true
transport.enableClientBatchSendRequest=false
transport.threadFactory.bossThreadPrefix=NettyBoss
transport.threadFactory.workerThreadPrefix=NettyServerNIOWorker
transport.threadFactory.serverExecutorThreadPrefix=NettyServerBizHandler
transport.threadFactory.shareBossWorker=false
transport.threadFactory.clientSelectorThreadPrefix=NettyClientSelector
transport.threadFactory.clientSelectorThreadSize=1
transport.threadFactory.clientWorkerThreadPrefix=NettyClientWorkerThread
transport.threadFactory.bossThreadSize=1
transport.threadFactory.workerThreadSize=default
transport.shutdown.wait=3
## 这里配置的事务组  值就是  seata cluster=default  在多机房切换用到
service.vgroupMapping.my_test_tx_group=default
service.default.grouplist=127.0.0.1:8091
service.enableDegrade=false
service.disableGlobalTransaction=false
client.rm.asyncCommitBufferLimit=10000
client.rm.lock.retryInterval=10
client.rm.lock.retryTimes=30
client.rm.lock.retryPolicyBranchRollbackOnConflict=true
client.rm.reportRetryCount=5
client.rm.tableMetaCheckEnable=false
client.rm.tableMetaCheckerInterval=60000
client.rm.sqlParserType=druid
client.rm.reportSuccessEnable=false
client.rm.sagaBranchRegisterEnable=false
client.tm.commitRetryCount=5
client.tm.rollbackRetryCount=5
client.tm.defaultGlobalTransactionTimeout=60000
client.tm.degradeCheck=false
client.tm.degradeCheckAllowTimes=10
client.tm.degradeCheckPeriod=2000
##  配置 存储为db 并配置响应的用户名和密码 等数据库信息
store.mode=db
store.publicKey=
store.file.dir=file_store/data
store.file.maxBranchSessionSize=16384
store.file.maxGlobalSessionSize=512
store.file.fileWriteBufferCacheSize=16384
store.file.flushDiskMode=async
store.file.sessionReloadReadSize=100
store.db.datasource=druid
store.db.dbType=mysql
store.db.driverClassName=com.mysql.jdbc.Driver
store.db.url=jdbc:mysql://127.0.0.1:3306/seata?useUnicode=true&rewriteBatchedStatements=true
store.db.user=root
store.db.password=root
store.db.minConn=5
store.db.maxConn=30
store.db.globalTable=global_table
store.db.branchTable=branch_table
store.db.queryLimit=100
store.db.lockTable=lock_table
store.db.maxWait=5000
store.redis.mode=single
store.redis.single.host=127.0.0.1
store.redis.single.port=6379
store.redis.sentinel.masterName=
store.redis.sentinel.sentinelHosts=
store.redis.maxConn=10
store.redis.minConn=1
store.redis.maxTotal=100
store.redis.database=0
store.redis.password=
store.redis.queryLimit=100
server.recovery.committingRetryPeriod=1000
server.recovery.asynCommittingRetryPeriod=1000
server.recovery.rollbackingRetryPeriod=1000
server.recovery.timeoutRetryPeriod=1000
server.maxCommitRetryTimeout=-1
server.maxRollbackRetryTimeout=-1
server.rollbackRetryTimeoutUnlockEnable=false
client.undo.dataValidation=true
client.undo.logSerialization=jackson
client.undo.onlyCareUpdateColumns=true
server.undo.logSaveDays=7
server.undo.logDeletePeriod=86400000
client.undo.logTable=undo_log
client.undo.compress.enable=true
client.undo.compress.type=zip
client.undo.compress.threshold=64k
log.exceptionRate=100
transport.serialization=seata
transport.compressor=none
metrics.enabled=false
metrics.registryType=compact
metrics.exporterList=prometheus
metrics.exporterPrometheusPort=9898
···
然后客户端便可以使用分布式事务进行处理

5. springboot aop 解决幂等 基于数据库表的修改时间字段, 时间戳机制

场景描述 一个表的数据 由于权限控制 导致 数据能够被很多人看到,这些人分布在全国各地,有同一时刻修改同一条数据的可能, 需要控制只能有一个修改成功

接口如下

java
package com.me.demo2.controller;

import com.me.demo2.domain.Emp;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.web.bind.annotation.*;

import java.util.Map;

@RestController
public class JDBCController {

    @Autowired
    JdbcTemplate jdbcTemplate;


    /**
     *  获取详情接口
     * @param id
     * @return
     */
    @GetMapping("/get/{id}")
    public Map<String,Object> get(@PathVariable("id") Integer id){
        Map<String,Object> res = jdbcTemplate.queryForMap("select * from tu_emp where id=?",new Object[]{id});
        return res;
    }
    /**
     *
     *   修改的前提是必须先调用 获取详情接口 查询信息
     *  修改接口
     * */
    @PostMapping("/update")
    public Map<String,Object> update(@RequestBody Emp emp){


        int update = jdbcTemplate.update("update tu_emp set name=? ,update_time=now() where id=?",emp.getName(),emp.getId() );
        Map<String,Object> res = jdbcTemplate.queryForMap("select * from tu_emp where id=?",emp.getId());

       return res;
    }
}

解决这个问题 需要对 update这个接口进行切面处理 保证只有有一个人修改成功

java
package com.me.demo2.aspects;

import com.me.demo2.domain.Emp;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Component;

import java.sql.ResultSet;
import java.sql.SQLException;

@Aspect
@Component
public class RepeatAspect {


    @Autowired
    JdbcTemplate jdbcTemplate;
	
    /**
      获取方法参数
      从数据库查询修改时间的这个字段  进行比较 如果不一致则为重复交易 则终止请求
    */
    @Around("execution(public * com.me.demo2.controller.JDBCController.update(..))")
    public Object repeatHandler(ProceedingJoinPoint joinPoint){
       Object[] args = joinPoint.getArgs();
        Emp dto = (Emp) args[0];
        Emp empDO = jdbcTemplate.queryForObject("select * from tu_emp where id=?", new RowMapper<Emp>() {
            @Override
            public Emp mapRow(ResultSet rs, int rowNum) throws SQLException {
                Emp emp1 = new Emp();
                emp1.setCreate_time(rs.getTimestamp("create_time"));
                emp1.setId(rs.getInt("id"));
                emp1.setName(rs.getString("name"));
                emp1.setUpdate_time(rs.getTimestamp("update_time"));
                return emp1;
            }
        }, dto.getId());
        if (!dto.getUpdate_time().equals(empDO.getUpdate_time())){
            throw new RuntimeException("重复交易 稍后重试");
        }else {
            try {
              return    joinPoint.proceed(args);
            }catch (Throwable e){
                e.printStackTrace();
            }
        }

        return  null;
    }
}

6. 树形列表

java
@Data
public class MenuDTO {
    private Long id;         // 菜单ID
    private Long parentId;   // 父菜单ID(顶级菜单parentId为0或null)
    private String name;     // 菜单名称
    private String path;     // 菜单路径
    private List<MenuDTO> children; // 子菜单列表
}
java
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class MenuTreeBuilder {

    /**
     * 将扁平的菜单列表组装成树形结构
     * @param menuList 扁平的菜单DTO列表
     * @return 树形菜单列表(根节点)
     */
    public static List<MenuDTO> buildTree(List<MenuDTO> menuList) {
        // 1. 构建ID到MenuDTO的映射Map
        Map<Long, MenuDTO> menuMap = new HashMap<>();
        for (MenuDTO menu : menuList) {
            menu.setChildren(new ArrayList<>()); // 初始化子菜单列表
            menuMap.put(menu.getId(), menu);
        }

        // 2. 组装树形结构
        List<MenuDTO> rootMenus = new ArrayList<>();
        for (MenuDTO menu : menuList) {
            Long parentId = menu.getParentId();
            if (parentId == null || parentId == 0) {
                // 顶级菜单,直接加入根列表
                rootMenus.add(menu);
            } else {
                // 非顶级菜单,找到父节点并添加子节点
                MenuDTO parentMenu = menuMap.get(parentId);
                if (parentMenu != null) {
                    parentMenu.getChildren().add(menu);
                }
            }
        }

        return rootMenus;
    }

    // 测试示例
    public static void main(String[] args) {
        // 模拟扁平菜单数据
        List<MenuDTO> menuList = new ArrayList<>();
        menuList.add(new MenuDTO(1L, 0L, "系统管理", "/system", null));
        menuList.add(new MenuDTO(2L, 1L, "用户管理", "/system/user", null));
        menuList.add(new MenuDTO(3L, 1L, "角色管理", "/system/role", null));
        menuList.add(new MenuDTO(4L, 0L, "内容管理", "/content", null));
        menuList.add(new MenuDTO(5L, 4L, "文章管理", "/content/article", null));

        // 构建树形结构
        List<MenuDTO> tree = buildTree(menuList);
        System.out.println(tree);
    }
}

7.格式校验工具

java
import java.math.BigDecimal;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;

/**
 * 字符串工具类,提供各种字符串验证方法
 */
public class StringUtils {
    
    /**
     * 判断一个字符串是否是数字
     * @param str 要判断的字符串
     * @return 如果是数字则返回true,否则返回false
     */
    public static boolean isNumeric(String str) {
        if (str == null || str.isEmpty()) {
            return false;
        }
        
        // 尝试将字符串转换为数字
        try {
            Double.parseDouble(str);
            return true;
        } catch (NumberFormatException e) {
            return false;
        }
    }
    
    /**
     * 判断一个字符串格式是否是 yyyy-MM-dd 的时间格式
     * @param str 要判断的字符串
     * @return 如果是yyyy-MM-dd格式则返回true,否则返回false
     */
    public static boolean isDateYYYYMMDD(String str) {
        if (str == null || str.isEmpty()) {
            return false;
        }
        
        // 使用正则表达式初步验证格式
        if (!str.matches("\\d{4}-\\d{2}-\\d{2}")) {
            return false;
        }
        
        // 使用SimpleDateFormat进一步验证日期有效性
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        sdf.setLenient(false); // 严格模式,不允许日期格式错误
        
        try {
            sdf.parse(str);
            return true;
        } catch (ParseException e) {
            return false;
        }
    }
    
    /**
     * 判断一个字符串是否为整数
     * @param str 要判断的字符串
     * @return 如果是整数则返回true,否则返回false
     */
    public static boolean isInteger(String str) {
        if (str == null || str.isEmpty()) {
            return false;
        }
        
        // 使用正则表达式判断整数
        // 允许正负号,后面跟一个或多个数字
        return str.matches("^-?\\d+$");
    }
    
    /**
     * 判断一个字符串是否为小数
     * @param str 要判断的字符串
     * @return 如果是小数则返回true,否则返回false
     */
    public static boolean isDecimal(String str) {
        if (str == null || str.isEmpty()) {
            return false;
        }
        
        // 使用正则表达式判断小数
        // 允许正负号,至少一个数字,一个小数点,后面至少一个数字
        // 或者一个小数点前后都有数字
        return str.matches("^-?\\d+\\.\\d+$");
    }
    
    /**
     * 判断输入的正则表达式是否合法
     * @param regex 要验证的正则表达式字符串
     * @return 如果正则表达式合法则返回true,否则返回false
     */
    public static boolean isValidRegex(String regex) {
        if (regex == null || regex.isEmpty()) {
            return false;
        }
        
        // 尝试编译正则表达式,如果成功则表示合法
        try {
            Pattern.compile(regex);
            return true;
        } catch (PatternSyntaxException e) {
            return false;
        }
    }
    
    /**
     * 判断一个字符串是否符合BigDecimal格式
     * @param str 要判断的字符串
     * @return 如果符合BigDecimal格式则返回true,否则返回false
     */
    public static boolean isBigDecimal(String str) {
        if (str == null || str.isEmpty()) {
            return false;
        }
        
        // 尝试将字符串转换为BigDecimal
        try {
            new BigDecimal(str);
            return true;
        } catch (NumberFormatException e) {
            return false;
        }
    }
}

8. 证书解析

java
 /**
     * 精准提取PEM证书中间纯Base64内容,彻底移除首尾标记行+所有冗余空白
     * @param pemContent 完整的PEM证书字符串(含首尾标记、任意空格/换行)
     * @return 仅保留中间的核心Base64内容,无任何多余字符
     */
    public static String extractPurePemContent(String pemContent) {
        // 1. 空值&空内容校验,兜底返回空串
        if (pemContent == null || pemContent.isEmpty()) {
            return "";
        }

        // 2. 核心替换:一次性移除 开头标记行 + 结尾标记行(兼容所有换行符、前后空格)
        String result = pemContent
                // 移除开头:匹配任意空格+BEGIN标记+任意换行/空白
                .replaceFirst("^\\s*-----BEGIN CERTIFICATE-----\\s*", "")
                // 移除结尾:匹配任意换行/空白+END标记+任意空格+字符串结束
                .replaceFirst("\\s*-----END CERTIFICATE-----\\s*$", "");

        // 3. 终极清理:可选2种模式,按需二选一!!!
        // ✅ 模式1【推荐你的需求】:合并所有内容为一行,无任何换行/空格(纯Base64单行)
//        return result.replaceAll("\\s+", "");

        // ✅ 模式2:保留原证书的换行格式,仅去除首尾冗余空白(如需分行的Base64则打开此行,注释上一行)
         return result.trim();
    }

一行命令生成自签证书(密钥 + 证书双文件,最便捷)

sh
## 一行命令生成自签证书(密钥 + 证书双文件,最便捷)
openssl req -x509 -nodes -days 3650 -newkey rsa:2048 -keyout nginx-self.key -out nginx-self.crt
##  
openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout nginx.key -out nginx.crt  -subj "/C=CN/ST=Beijing/L=Beijing/O=Company/CN=example.com"

# 从证书提取公钥
openssl x509 -in nginx.crt -pubkey -noout > public.pem
# 从私钥提取公钥
openssl rsa -in nginx.key -pubout -out public.pem